Browse Source

Merge pull request #185 from CompassionCH/10.0-sendgrid

[10.0][ADD] Sendgrid modules
pull/259/head
Pedro M. Baeza 6 years ago
committed by GitHub
parent
commit
8b8ba50607
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .travis.yml
  2. 120
      mail_sendgrid/README.rst
  3. 7
      mail_sendgrid/__init__.py
  4. 25
      mail_sendgrid/__manifest__.py
  5. 6
      mail_sendgrid/controllers/__init__.py
  6. 46
      mail_sendgrid/controllers/json_request.py
  7. 23
      mail_sendgrid/controllers/sendgrid_event_webhook.py
  8. 11
      mail_sendgrid/models/__init__.py
  9. 22
      mail_sendgrid/models/email_lang_template.py
  10. 76
      mail_sendgrid/models/email_template.py
  11. 171
      mail_sendgrid/models/email_tracking.py
  12. 242
      mail_sendgrid/models/mail_mail.py
  13. 14
      mail_sendgrid/models/mail_tracking_event.py
  14. 96
      mail_sendgrid/models/sendgrid_template.py
  15. 21
      mail_sendgrid/models/substitution.py
  16. 4
      mail_sendgrid/security/ir.model.access.csv
  17. BIN
      mail_sendgrid/static/description/icon.png
  18. 10
      mail_sendgrid/static/description/icon.svg
  19. 5
      mail_sendgrid/tests/__init__.py
  20. 270
      mail_sendgrid/tests/test_mail_sendgrid.py
  21. 31
      mail_sendgrid/views/email_template_view.xml
  22. 21
      mail_sendgrid/views/mail_compose_message_view.xml
  23. 77
      mail_sendgrid/views/sendgrid_email_view.xml
  24. 72
      mail_sendgrid/views/sendgrid_template_view.xml
  25. 6
      mail_sendgrid/wizards/__init__.py
  26. 23
      mail_sendgrid/wizards/email_template_preview.py
  27. 42
      mail_sendgrid/wizards/mail_compose_message.py
  28. 89
      mail_sendgrid_mass_mailing/README.rst
  29. 6
      mail_sendgrid_mass_mailing/__init__.py
  30. 21
      mail_sendgrid_mass_mailing/__manifest__.py
  31. 7
      mail_sendgrid_mass_mailing/models/__init__.py
  32. 32
      mail_sendgrid_mass_mailing/models/email_tracking.py
  33. 59
      mail_sendgrid_mass_mailing/models/mail_mail.py
  34. 140
      mail_sendgrid_mass_mailing/models/mass_mailing.py
  35. BIN
      mail_sendgrid_mass_mailing/static/description/icon.png
  36. 10
      mail_sendgrid_mass_mailing/static/description/icon.svg
  37. 5
      mail_sendgrid_mass_mailing/tests/__init__.py
  38. 173
      mail_sendgrid_mass_mailing/tests/test_mass_mailing.py
  39. 34
      mail_sendgrid_mass_mailing/views/mass_mailing_view.xml
  40. 6
      mail_sendgrid_mass_mailing/wizards/__init__.py
  41. 33
      mail_sendgrid_mass_mailing/wizards/mail_compose_message.py
  42. 48
      mail_sendgrid_mass_mailing/wizards/test_mailing.py

1
.travis.yml

@ -28,6 +28,7 @@ virtualenv:
system_site_packages: true
install:
- pip install sendgrid
- git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools
- export PATH=${HOME}/maintainer-quality-tools/travis:${PATH}
- travis_install_nightly

120
mail_sendgrid/README.rst

@ -0,0 +1,120 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License: AGPL-3
==================================
SendGrid Mail Sending and Tracking
==================================
This module integrates
`SendGrid <https://sendgrid.com/>`_ with Odoo. It can send transactional emails
through SendGrid, using templates defined on the
`SendGrid web interface <https://sendgrid.com/templates>`_. It also supports
substitution of placeholder variables in these templates. The list of available
templates can be fetched automatically.
E-mails sent through SendGrid will be tracked using Sendgrid Webhook Events.
Installation
============
You need to install python-sendgrid v3 API in order to install the module.
If you're using a multi-database installation (with or without dbfilter option)
where /web/databse/selector returns a list of more than one database, then
you need to add ``mail_sendgrid`` addon to wide load addons list
(by default, only ``web`` addon), setting ``--load`` option.
For example, ``--load=web,mail_tracking,mail_sendgrid``
Configuration
=============
You can add the following system parameters to configure the usage of SendGrid:
* ``mail_sendgrid.substitution_prefix`` Any symbol or character used as a
prefix for `SendGrid Substitution Tags <https://sendgrid.com/docs/API_Reference/SMTP_API/substitution_tags.html>`_.
``{`` is used by default.
* ``mail_sendgrid.substitution_suffix`` Any symbol or character used as a
suffix for SendGrid Substitution Tags.
``}`` is used by default.
* ``mail_sendgrid.send_method`` Use value 'sendgrid' to override the traditional SMTP server used to send e-mails with sendgrid.
By default, SendGrid will co-exist with traditional system
(two buttons for sending either normally or with SendGrid).
In order to use this module, the following variables have to be defined in the
server command-line options (or in a configuration file):
- ``sendgrid_api_key`` A valid API key obtained from the
SendGrid web interface <https://app.sendgrid.com/settings/api_keys> with
full access for the ``Mail Send`` permission and read access for the
``Template Engine`` permission.
Optionally, the following configuration variables can be set as well:
- ``sendgrid_test_address`` Destination email address for testing purposes.
You can use ``odoo@sink.sendgrid.net``, which is an address that
will simply receive and discard all incoming email.
For tracking events to work, make sure you configure your Sendgrid Account with the correct Event Notification Url.
You can do it under 'Settings -> Mail Settings -> Event Notification '.
Set the URL to ``https://<your_domain>/mail/tracking/sendgrid/<your_database>``
Replace '<your_domain>' with your Odoo install domain name
and '<your_database>' with your database name.
Usage
=====
If you designed templates in Sendgrid that you wan't to use with Odoo:
* Go to 'Settings -> Email -> SendGrid Templates'
* Create a new Template
* Click the "Update" button : this will automatically import all your templates
In e-mail templates 'Settings -> Email -> Templates', you can attach a SendGrid template for any language.
You can substitute Sendgrid keywords with placeholders or static text like in the body of the e-mail.
The preview wizard now renders your e-mail with the SendGrid template applied.
From e-mails, use the "Send (SendGrid)" button to send the e-mail using Sendgrid.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/205/10.0
Known issues / Roadmap
======================
* Extend the features from SendGrid
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/social/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smashing it by providing a detailed and welcomed feedback.
Credits
=======
Images
------
* Sengrid logo: `SVG Icon <http://seeklogo.com/vector-logo/289294/sendgrid>`_.
Contributors
------------
* Emanuel Cino <ecino@compassion.ch>
* Roman Zoller <rzcomp@gmail.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit http://odoo-community.org.

7
mail_sendgrid/__init__.py

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2015-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models
from . import wizards
from . import controllers

25
mail_sendgrid/__manifest__.py

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Copyright 2015-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'SendGrid',
'version': '10.0.1.0.0',
'category': 'Social Network',
'author': 'Compassion CH, Odoo Community Association (OCA)',
'license': 'AGPL-3',
'website': 'https://github.com/OCA/social',
'depends': ['mail_tracking'],
'data': [
'security/ir.model.access.csv',
'views/sendgrid_email_view.xml',
'views/sendgrid_template_view.xml',
'views/mail_compose_message_view.xml',
'views/email_template_view.xml',
],
'demo': [],
'installable': True,
'auto_install': False,
'external_dependencies': {
'python': ['sendgrid'],
},
}

6
mail_sendgrid/controllers/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import json_request
from . import sendgrid_event_webhook

46
mail_sendgrid/controllers/json_request.py

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import simplejson
from odoo.http import JsonRequest, Root, Response
# Monkeypatch type of request rooter to use RESTJsonRequest
old_get_request = Root.get_request
def get_request(self, httprequest):
if (httprequest.mimetype == "application/json" and
httprequest.environ['PATH_INFO'].startswith('/mail')):
return RESTJsonRequest(httprequest)
return old_get_request(self, httprequest)
Root.get_request = get_request
class RESTJsonRequest(JsonRequest):
""" Special RestJson Handler to enable receiving lists in JSON
body
"""
def __init__(self, *args):
try:
super(RESTJsonRequest, self).__init__(*args)
except AttributeError:
# The JSON may contain a list
self.params = dict()
self.context = dict(self.session.context)
def _json_response(self, result=None, error=None):
response = {}
if error is not None:
response['error'] = error
if result is not None:
response['result'] = result
mime = 'application/json'
body = simplejson.dumps(response)
return Response(
body, headers=[('Content-Type', mime),
('Content-Length', len(body))])

23
mail_sendgrid/controllers/sendgrid_event_webhook.py

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from odoo import http
from odoo.addons.mail_tracking.controllers.main import \
MailTrackingController, _env_get
_logger = logging.getLogger(__name__)
class SendgridTrackingController(MailTrackingController):
"""Sendgrid is posting JSON so we must define a new route for tracking."""
@http.route('/mail/tracking/sendgrid/<string:db>',
type='json', auth='none', csrf=False)
def mail_tracking_sendgrid(self, db, **kw):
try:
_env_get(db, self._tracking_event, None, None, **kw)
return {'status': 200}
except Exception as e:
_logger.error(e.message, exc_info=True)
return {'status': 400}

11
mail_sendgrid/models/__init__.py

@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import mail_mail
from . import substitution
from . import sendgrid_template
from . import email_template
from . import email_lang_template
from . import email_tracking
from . import mail_tracking_event

22
mail_sendgrid/models/email_lang_template.py

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields
class LanguageTemplate(models.Model):
""" This class is the relation between and email_template object
and a sendgrid_template. It allows to specify a different
sendgrid_template for any selected language.
"""
_name = 'sendgrid.email.lang.template'
email_template_id = fields.Many2one('mail.template', 'E-mail Template')
lang = fields.Selection('_select_lang', 'Language', required=True)
sendgrid_template_id = fields.Many2one(
'sendgrid.template', 'Sendgrid Template', required=True)
def _select_lang(self):
languages = self.env['res.lang'].search([])
return [(language.code, language.name) for language in languages]

76
mail_sendgrid/models/email_template.py

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api
from collections import defaultdict
class EmailTemplate(models.Model):
_inherit = 'mail.template'
substitution_ids = fields.One2many(
'sendgrid.substitution', 'email_template_id', 'Substitutions')
sendgrid_template_ids = fields.One2many(
'sendgrid.email.lang.template', 'email_template_id',
'Sendgrid Templates')
sendgrid_localized_template = fields.Many2one(
'sendgrid.template', compute='_compute_localized_template')
def _compute_localized_template(self):
lang = self.env.context.get('lang', 'en_US')
for template in self:
lang_template = template.sendgrid_template_ids.filtered(
lambda t: t.lang == lang)
if lang_template and len(lang_template) == 1:
template.sendgrid_localized_template = \
lang_template.sendgrid_template_id
@api.multi
def update_substitutions(self):
for template in self:
new_substitutions = []
for language_template in template.sendgrid_template_ids:
sendgrid_template = language_template.sendgrid_template_id
lang = language_template.lang
substitutions = template.substitution_ids.filtered(
lambda s: s.lang == lang)
keywords = sendgrid_template.get_keywords()
# Add new keywords from the sendgrid template
for key in keywords:
if key not in substitutions.mapped('key'):
substitution_vals = {
'key': key,
'lang': lang,
'email_template_id': template.id
}
new_substitutions.append((0, 0, substitution_vals))
template.write({'substitution_ids': new_substitutions})
return True
@api.multi
def render_substitutions(self, res_ids):
"""
:param res_ids: resource ids for rendering the template
Returns values for substitutions in a mail.message creation
:return:
Values for mail creation (for each resource id given)
{res_id: list of substitutions values [0, 0 {substitution_vals}]}
"""
self.ensure_one()
if isinstance(res_ids, (int, long)):
res_ids = [res_ids]
substitutions = self.substitution_ids.filtered(
lambda s: s.lang == self.env.context.get('lang', 'en_US'))
substitution_vals = defaultdict(list)
for substitution in substitutions:
values = self.render_template(
substitution.value, self.model, res_ids)
for res_id in res_ids:
substitution_vals[res_id].append((0, 0, {
'key': substitution.key,
'value': values[res_id]
}))
return substitution_vals

171
mail_sendgrid/models/email_tracking.py

@ -0,0 +1,171 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from datetime import datetime
from werkzeug.useragents import UserAgent
from odoo import models, fields, api
_logger = logging.getLogger(__name__)
class MailTrackingEmail(models.Model):
""" Count the user clicks on links inside e-mails sent.
Add tracking methods to process Sendgrid Notifications
"""
_inherit = 'mail.tracking.email'
click_count = fields.Integer(
compute='_compute_clicks', store=True, readonly=True)
@api.depends('tracking_event_ids')
def _compute_clicks(self):
for mail in self:
mail.click_count = self.env['mail.tracking.event'].search_count([
('event_type', '=', 'click'),
('tracking_email_id', '=', mail.id)
])
@property
def _sendgrid_mandatory_fields(self):
return ('event', 'timestamp', 'odoo_id', 'odoo_db')
@property
def _sendgrid_event_type_mapping(self):
return {
# Sendgrid event type: tracking event type
'bounce': 'hard_bounce',
'click': 'click',
'deferred': 'deferral',
'delivered': 'delivered',
'dropped': 'reject',
'group_unsubscribe': 'unsub',
'open': 'open',
'processed': 'sent',
'spamreport': 'spam',
'unsubscribe': 'unsub',
}
def _sendgrid_event_type_verify(self, event):
event = event or {}
sendgrid_event_type = event.get('event')
if sendgrid_event_type not in self._sendgrid_event_type_mapping:
_logger.error("Sendgrid: event type '%s' not supported",
sendgrid_event_type)
return False
# OK, event type is valid
return True
def _sendgrid_db_verify(self, event):
event = event or {}
odoo_db = event.get('odoo_db')
current_db = self.env.cr.dbname
if odoo_db != current_db:
_logger.error("Sendgrid: Database '%s' is not the current "
"database",
odoo_db)
return False
# OK, DB is current
return True
def _sendgrid_metadata(self, sendgrid_event_type, event, metadata):
# Get sendgrid timestamp when found
ts = event.get('timestamp')
try:
ts = float(ts)
except ValueError:
ts = False
if ts:
dt = datetime.utcfromtimestamp(ts)
metadata.update({
'timestamp': ts,
'time': fields.Datetime.to_string(dt),
'date': fields.Date.to_string(dt),
})
# Common field mapping (sendgrid_field: odoo_field)
mapping = {
'email': 'recipient',
'ip': 'ip',
'url': 'url',
}
for k, v in mapping.iteritems():
if event.get(k, False):
metadata[v] = event[k]
# Special field mapping
if event.get('useragent'):
user_agent = UserAgent(event['useragent'])
metadata.update({
'user_agent': user_agent.string,
'os_family': user_agent.platform,
'ua_family': user_agent.browser,
'mobile': user_agent.platform in [
'android', 'iphone', 'ipad']
})
# Mapping for special events
if sendgrid_event_type == 'bounce':
metadata.update({
'error_type': event.get('type', False),
'bounce_type': event.get('type', False),
'error_description': event.get('reason', False),
'bounce_description': event.get('reason', False),
'error_details': event.get('status', False),
})
elif sendgrid_event_type == 'dropped':
metadata.update({
'error_type': event.get('reason', False),
})
return metadata
def _sendgrid_tracking_get(self, event):
tracking = False
message_id = event.get('odoo_id', False)
if message_id:
tracking = self.search([
('mail_id.message_id', '=', message_id),
('recipient', '=ilike', event.get('email'))], limit=1)
return tracking
def _event_is_from_sendgrid(self, event):
event = event or {}
return all([k in event for k in self._sendgrid_mandatory_fields])
@api.model
def event_process(self, request, post, metadata, event_type=None):
res = super(MailTrackingEmail, self).event_process(
request, post, metadata, event_type=event_type)
is_json = hasattr(request, 'jsonrequest') and isinstance(
request.jsonrequest, list)
if res == 'NONE' and is_json:
for event in request.jsonrequest:
if self._event_is_from_sendgrid(event):
if not self._sendgrid_event_type_verify(event):
res = 'ERROR: Event type not supported'
elif not self._sendgrid_db_verify(event):
res = 'ERROR: Invalid DB'
else:
res = 'OK'
if res == 'OK':
sendgrid_event_type = event.get('event')
mapped_event_type = self._sendgrid_event_type_mapping.get(
sendgrid_event_type) or event_type
if not mapped_event_type:
res = 'ERROR: Bad event'
tracking = self._sendgrid_tracking_get(event)
if tracking:
# Complete metadata with sendgrid event info
metadata = self._sendgrid_metadata(
sendgrid_event_type, event, metadata)
# Create event
tracking.event_create(mapped_event_type, metadata)
else:
res = 'ERROR: Tracking not found'
if res != 'NONE':
if event_type:
_logger.info(
"sendgrid: event '%s' process '%s'",
event_type, res)
else:
_logger.info("sendgrid: event process '%s'", res)
return res

242
mail_sendgrid/models/mail_mail.py

@ -0,0 +1,242 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api, tools
from odoo.tools.config import config
from odoo.tools.safe_eval import safe_eval
import base64
import logging
import re
import time
_logger = logging.getLogger(__name__)
try:
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Email, Attachment, CustomArg, Content, \
Personalization, Substitution, Mail, Header
except ImportError:
_logger.error("ImportError raised while loading module.")
_logger.debug("ImportError details:", exc_info=True)
STATUS_OK = 202
class MailMessage(models.Model):
""" Add SendGrid related fields so that they dispatch in all
subclasses of mail.message object
"""
_inherit = 'mail.message'
body_text = fields.Text(help='Text only version of the body')
sent_date = fields.Datetime(copy=False)
substitution_ids = fields.Many2many(
'sendgrid.substitution', string='Substitutions', copy=True)
sendgrid_template_id = fields.Many2one(
'sendgrid.template', 'Sendgrid Template')
send_method = fields.Char(compute='_compute_send_method')
@api.multi
def _compute_send_method(self):
""" Check whether to use traditional send method, sendgrid or disable.
"""
send_method = self.env['ir.config_parameter'].get_param(
'mail_sendgrid.send_method', 'traditional')
for email in self:
email.send_method = send_method
class MailMail(models.Model):
""" Email message sent through SendGrid """
_inherit = 'mail.mail'
tracking_email_ids = fields.One2many(
'mail.tracking.email', 'mail_id', string='Registered events',
readonly=True)
click_count = fields.Integer(
compute='_compute_tracking', store=True, readonly=True)
opened = fields.Boolean(
compute='_compute_tracking', store=True, readonly=True)
tracking_event_ids = fields.One2many(
'mail.tracking.event', compute='_compute_events')
@api.depends('tracking_email_ids', 'tracking_email_ids.click_count',
'tracking_email_ids.state')
def _compute_tracking(self):
for email in self:
click_count = sum(email.tracking_email_ids.mapped(
'click_count'))
opened = self.env['mail.tracking.email'].search_count([
('state', '=', 'opened'),
('mail_id', '=', email.id)
])
email.update({
'click_count': click_count,
'opened': opened > 0
})
def _compute_events(self):
for email in self:
email.tracking_event_ids = email.tracking_email_ids.mapped(
'tracking_event_ids')
@api.multi
def send(self, auto_commit=False, raise_exception=False):
""" Override send to select the method to send the e-mail. """
traditional = self.filtered(lambda e: e.send_method == 'traditional')
sendgrid = self.filtered(lambda e: e.send_method == 'sendgrid')
if traditional:
super(MailMail, traditional).send(auto_commit, raise_exception)
if sendgrid:
sendgrid.send_sendgrid()
return True
@api.multi
def send_sendgrid(self):
""" Use sendgrid transactional e-mails : e-mails are sent one by
one. """
outgoing = self.filtered(lambda em: em.state == 'outgoing')
api_key = config.get('sendgrid_api_key')
if outgoing and not api_key:
_logger.error(
'Missing sendgrid_api_key in conf file. Skipping Sendgrid '
'send.'
)
return
sg = SendGridAPIClient(apikey=api_key)
for email in outgoing:
try:
response = sg.client.mail.send.post(
request_body=email._prepare_sendgrid_data().get())
except Exception as e:
_logger.error(e.message or "mail not sent.")
continue
status = response.status_code
msg = response.body
if status == STATUS_OK:
_logger.info("e-mail sent. " + str(msg))
email._track_sendgrid_emails()
email.write({
'sent_date': fields.Datetime.now(),
'state': 'sent'
})
if not self.env.context.get('test_mode'):
# Commit at each e-mail processed to avoid any errors
# invalidating state.
self.env.cr.commit() # pylint: disable=invalid-commit
email._postprocess_sent_message(mail_sent=True)
else:
email._postprocess_sent_message(mail_sent=False)
_logger.error("Failed to send email: {}".format(str(msg)))
def _prepare_sendgrid_data(self):
"""
Prepare and creates the Sendgrid Email object
:return: sendgrid.helpers.mail.Email object
"""
self.ensure_one()
s_mail = Mail()
s_mail.from_email = Email(self.email_from)
if self.reply_to:
s_mail.reply_to = Email(self.reply_to)
# Add custom fields to match the tracking
s_mail.add_custom_arg(CustomArg('odoo_id', self.message_id))
s_mail.add_custom_arg(CustomArg('odoo_db', self.env.cr.dbname))
headers = {
'Message-Id': self.message_id
}
if self.headers:
try:
headers.update(safe_eval(self.headers))
except Exception:
pass
for h_name, h_val in headers.iteritems():
s_mail.add_header(Header(h_name, h_val))
html = self.body_html or ' '
p = re.compile(r'<.*?>') # Remove HTML markers
text_only = self.body_text or p.sub('', html.replace('<br/>', '\n'))
s_mail.add_content(Content("text/plain", text_only or ' '))
s_mail.add_content(Content("text/html", html))
test_address = config.get('sendgrid_test_address')
# We use only one personalization for transactional e-mail
personalization = Personalization()
subject = self.subject and self.subject.encode(
"utf_8") or "(No subject)"
personalization.subject = subject
addresses = set()
if not test_address:
if self.email_to:
addresses = set(self.email_to.split(','))
for address in addresses:
personalization.add_to(Email(address))
for recipient in self.recipient_ids:
if recipient.email not in addresses:
personalization.add_to(Email(recipient.email))
addresses.add(recipient.email)
if self.email_cc and self.email_cc not in addresses:
personalization.add_cc(Email(self.email_cc))
else:
_logger.info('Sending email to test address {}'.format(
test_address))
personalization.add_to(Email(test_address))
self.email_to = test_address
if self.sendgrid_template_id:
s_mail.template_id = self.sendgrid_template_id.remote_id
for substitution in self.substitution_ids:
personalization.add_substitution(Substitution(
substitution.key, substitution.value.encode('utf-8')))
s_mail.add_personalization(personalization)
for attachment in self.attachment_ids:
s_attachment = Attachment()
# Datas are not encoded properly for sendgrid
s_attachment.content = base64.b64encode(base64.b64decode(
attachment.datas))
s_attachment.filename = attachment.name
s_mail.add_attachment(s_attachment)
return s_mail
def _track_sendgrid_emails(self):
""" Create tracking e-mails after successfully sent with Sendgrid. """
self.ensure_one()
m_tracking = self.env['mail.tracking.email'].sudo()
track_vals = self._prepare_sendgrid_tracking()
for recipient in tools.email_split_and_format(self.email_to):
track_vals['recipient'] = recipient
m_tracking += m_tracking.create(track_vals)
for partner in self.recipient_ids:
track_vals.update({
'partner_id': partner.id,
'recipient': partner.email,
})
m_tracking += m_tracking.create(track_vals)
return m_tracking
def _prepare_sendgrid_tracking(self):
ts = time.time()
return {
'name': self.subject,
'timestamp': '%.6f' % ts,
'time': fields.Datetime.now(),
'mail_id': self.id,
'mail_message_id': self.mail_message_id.id,
'sender': self.email_from,
}

14
mail_sendgrid/models/mail_tracking_event.py

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Emanuel Cino - <ecino@compassion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from odoo import models, api
class MailTrackingEvent(models.Model):
_inherit = "mail.tracking.event"
@api.model
def process_sent(self, tracking_email, metadata):
return self._process_status(
tracking_email, metadata, 'sent', 'sent')

96
mail_sendgrid/models/sendgrid_template.py

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
# Copyright 2015-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api, exceptions, _
from odoo.tools.config import config
import json
import re
import logging
_logger = logging.getLogger(__name__)
try:
import sendgrid
except ImportError:
_logger.error("ImportError raised while loading module.")
_logger.debug("ImportError details:", exc_info=True)
class SendgridTemplate(models.Model):
""" Reference to a template available on the SendGrid user account. """
_name = 'sendgrid.template'
##########################################################################
# FIELDS #
##########################################################################
name = fields.Char()
remote_id = fields.Char(readonly=True)
html_content = fields.Html(readonly=True)
plain_content = fields.Text(readonly=True)
detected_keywords = fields.Char(compute='_compute_keywords')
def _compute_keywords(self):
for template in self:
if template.html_content:
keywords = template.get_keywords()
self.detected_keywords = ';'.join(keywords)
@api.model
def update_templates(self):
api_key = config.get('sendgrid_api_key')
if not api_key:
raise exceptions.UserError(
_('Missing sendgrid_api_key in conf file'))
sg = sendgrid.SendGridAPIClient(apikey=api_key)
template_client = sg.client.templates
msg = template_client.get().body
result = json.loads(msg)
for template in result.get("templates", list()):
id = template["id"]
msg = template_client._(id).get().body
template_versions = json.loads(msg)['versions']
for version in template_versions:
if version['active']:
template_vals = version
break
else:
continue
vals = {
"remote_id": id,
"name": template["name"],
"html_content": template_vals["html_content"],
"plain_content": template_vals["plain_content"],
}
record = self.search([('remote_id', '=', id)])
if record:
record.write(vals)
else:
self.create(vals)
return True
def get_keywords(self):
""" Search in the Sendgrid template for keywords included with the
following syntax: {keyword_name} and returns the list of keywords.
keyword_name shouldn't be longer than 50 characters and not contain
whitespaces.
You can replace the substitution prefix and suffix by adding values
in the system parameters
- mail_sendgrid.substitution_prefix
- mail_sendgrid.substitution_suffix
"""
self.ensure_one()
params = self.env['ir.config_parameter']
prefix = params.search([
('key', '=', 'mail_sendgrid.substitution_prefix')
]).value or '{'
suffix = params.search([
('key', '=', 'mail_sendgrid.substitution_suffix')
]) or '}'
pattern = prefix + r'\S{1,50}' + suffix
return list(set(re.findall(pattern, self.html_content)))

21
mail_sendgrid/models/substitution.py

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Copyright 2015-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields
class Substitution(models.Model):
""" Substitution values for a SendGrid email message """
_name = 'sendgrid.substitution'
##########################################################################
# FIELDS #
##########################################################################
key = fields.Char()
lang = fields.Char()
email_template_id = fields.Many2one(
'mail.template', ondelete='cascade')
email_id = fields.Many2one(
'mail.mail', ondelete='cascade')
value = fields.Char()

4
mail_sendgrid/security/ir.model.access.csv

@ -0,0 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sendgrid_substitution,Full access on sendgrid_substitution,model_sendgrid_substitution,base.group_user,1,1,1,1
access_sendgrid_template,Full access on sendgrid_template,model_sendgrid_template,base.group_user,1,1,1,1
access_sendgrid_lang_template,Full access on sendgrid_lang_template,model_sendgrid_email_lang_template,base.group_user,1,1,1,1

BIN
mail_sendgrid/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 3.1 KiB

10
mail_sendgrid/static/description/icon.svg

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<polygon fill="#9DE1F3" points="85.3335 85.333 0.0005 85.333 0.0005 170.666 0.0005 256 85.3335 256 170.6665 256 170.6665 170.666 170.6665 85.333"></polygon>
<polygon fill="#27B4E1" points="85.3335 0.0004 85.3335 85.3334 85.3335 170.6664 170.6665 170.6664 255.9995 170.6664 255.9995 0.0004"></polygon>
<polygon fill="#1A82E2" points="0 256 85.333 256 85.333 170.667 0 170.667"></polygon>
<polygon fill="#1A82E2" points="170.667 85.333 256 85.333 256 0 170.667 0"></polygon>
<polygon fill="#239FD7" points="85.334 170.667 170.667 170.667 170.667 85.334 85.334 85.334"></polygon>
</g>
</svg>

5
mail_sendgrid/tests/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2015-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_mail_sendgrid

270
mail_sendgrid/tests/test_mail_sendgrid.py

@ -0,0 +1,270 @@
# -*- coding: utf-8 -*-
# © 2017 Emanuel Cino - <ecino@compassion.ch>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import json
import mock
from odoo.tests.common import HttpCase
from ..controllers.json_request import RESTJsonRequest
mock_base_send = 'odoo.addons.mail.models.mail_mail.MailMail.send'
mock_sendgrid_api_client = ('odoo.addons.mail_sendgrid.models.mail_mail'
'.SendGridAPIClient')
mock_sendgrid_send = ('odoo.addons.mail_sendgrid.models.mail_mail.'
'MailMail.send_sendgrid')
mock_config = ('odoo.addons.mail_sendgrid.models.mail_mail.'
'config')
mock_config_template = ('odoo.addons.mail_sendgrid.models.sendgrid_template.'
'config')
mock_template_api_client = ('odoo.addons.mail_sendgrid.models.'
'sendgrid_template.sendgrid.SendGridAPIClient')
mock_json_request = 'odoo.http.Root.get_request'
def side_effect_json(http_request):
return RESTJsonRequest(http_request)
class FakeClient(object):
""" Mock Sendgrid APIClient """
status_code = 202
body = 'ok'
def __init__(self):
self.client = self
self.mail = self
self.send = self
def post(self, **kwargs):
return self
class FakeRequest(object):
""" Simulate a Sendgrid JSON request """
def __init__(self, data):
self.jsonrequest = [data]
class FakeTemplateClient(object):
""" Simulate the Sendgrid Template api"""
def __init__(self):
self.client = self
self.templates = self
self.body = json.dumps({
"templates": [{
"id": "fake_id",
"name": "Fake Template"
}],
"versions": [{
"active": True,
"html_content": "<h1>fake</h1>",
"plain_content": "fake",
}],
})
def get(self):
return self
def _(self, id):
return self
class TestMailSendgrid(HttpCase):
def setUp(self):
super(TestMailSendgrid, self).setUp()
self.sendgrid_template = self.env['sendgrid.template'].create({
'name': 'Test Template',
'remote_id': 'a74795d7-f926-4bad-8e7a-ae95fabd70fc',
'html_content': u'<h1>Test Sendgrid</h1><%body%>{footer}'
})
self.mail_template = self.env['mail.template'].create({
'name': 'Test Template',
'model_id': self.env.ref('base.model_res_partner').id,
'subject': 'Test e-mail',
'body_html': u'Dear ${object.name}, hello!',
'sendgrid_template_ids': [
(0, 0, {'lang': 'en_US', 'sendgrid_template_id':
self.sendgrid_template.id})]
})
self.recipient = self.env.ref('base.partner_demo')
self.mail_wizard = self.env['mail.compose.message'].create({
'template_id': self.mail_template.id,
'composition_mode': 'comment',
'model': 'res.partner',
'res_id': self.recipient.id
}).with_context(active_id=self.recipient.id)
self.mail_wizard.onchange_template_id_wrapper()
self.timestamp = u'1471021089'
self.event = {
'timestamp': self.timestamp,
'sg_event_id': u"f_JoKtrLQaOXUc4thXgROg",
'email': self.recipient.email,
'odoo_db': self.env.cr.dbname,
'odoo_id': u'<xxx.xxx.xxx-openerp-xxx-res.partner@test_db>'
}
self.metadata = {
'ip': '127.0.0.1',
'user_agent': False,
'os_family': False,
'ua_family': False,
}
self.request = FakeRequest(self.event)
def create_email(self, vals=None):
mail_vals = self.mail_wizard.get_mail_values(self.recipient.ids)[
self.recipient.id]
mail_vals['recipient_ids'] = [(6, 0, self.recipient.ids)]
if vals is not None:
mail_vals.update(vals)
return self.env['mail.mail'].with_context(test_mode=True).create(
mail_vals)
def test_preview(self):
"""
Test the preview email_template is getting the Sendgrid template
"""
preview_wizard = self.env['email_template.preview'].with_context(
template_id=self.mail_template.id,
default_res_id=self.recipient.id
).create({})
# For a strange reason, res_id is converted to string
preview_wizard.res_id = self.recipient.id
preview_wizard.on_change_res_id()
self.assertIn(u'<h1>Test Sendgrid</h1>', preview_wizard.body_html)
self.assertIn(self.recipient.name, preview_wizard.body_html)
def test_substitutions(self):
""" Test substitutions in templates. """
self.assertEqual(self.sendgrid_template.detected_keywords, "{footer}")
self.mail_template.update_substitutions()
substitutions = self.mail_template.substitution_ids
self.assertEqual(len(substitutions), 1)
self.assertEqual(substitutions.key, '{footer}')
def test_create_email(self):
""" Test that Sendgrid template is pushed in e-mail. """
self.mail_template.update_substitutions()
mail_values = self.mail_wizard.get_mail_values(self.recipient.ids)[
self.recipient.id]
# Test Sendgrid HTML preview
self.assertEqual(
self.mail_wizard.body_sendgrid,
self.sendgrid_template.html_content.replace(
'<%body%>', mail_values['body'])
)
mail = self.env['mail.mail'].create(mail_values)
self.assertEqual(mail.sendgrid_template_id.id,
self.sendgrid_template.id)
self.assertEqual(len(mail.substitution_ids), 1)
@mock.patch(mock_base_send)
@mock.patch(mock_sendgrid_send)
def test_send_email_default(self, mock_sendgrid, mock_email):
""" Tests that sending an e-mail by default doesn't use Sendgrid,
and that Sendgrid is used when system parameter is set.
"""
self.env['ir.config_parameter'].set_param(
'mail_sendgrid.send_method', False)
mock_sendgrid.return_value = True
mock_email.return_value = True
mail = self.create_email()
mail.send()
self.assertTrue(mock_email.called)
self.assertFalse(mock_sendgrid.called)
self.env['ir.config_parameter'].set_param(
'mail_sendgrid.send_method', 'sendgrid')
# Force again computation of send_method
self.env.invalidate_all()
mail.send()
self.assertEqual(mock_email.call_count, 1)
self.assertEqual(mock_sendgrid.call_count, 1)
@mock.patch(mock_sendgrid_api_client)
@mock.patch(mock_config)
def test_mail_tracking(self, m_config, mock_sendgrid):
""" Test various tracking events. """
self.env['ir.config_parameter'].set_param(
'mail_sendgrid.send_method', 'sendgrid')
mock_sendgrid.return_value = FakeClient()
m_config.get.return_value = "ushuwejhfkj"
# Send mail
mail = self.create_email()
mail.send()
self.assertEqual(mock_sendgrid.called, True)
self.assertEqual(mail.state, 'sent')
mail_tracking = mail.tracking_email_ids
self.assertEqual(len(mail_tracking), 1)
self.assertFalse(mail_tracking.state)
# Test mail processed
self.event.update({
'event': u'processed',
'odoo_id': mail.message_id
})
response = self.env['mail.tracking.email'].event_process(
self.request, self.event, self.metadata)
self.assertEqual(response, 'OK')
self.assertEqual(mail_tracking.state, 'sent')
# Test mail delivered
self.event['event'] = 'delivered'
self.env['mail.tracking.email'].event_process(
self.request, self.event, self.metadata)
self.assertEqual(mail_tracking.state, 'delivered')
self.assertEqual(mail_tracking.recipient, self.recipient.email)
self.assertFalse(mail.opened)
# Test mail opened
self.event['event'] = 'open'
self.env['mail.tracking.email'].event_process(
self.request, self.event, self.metadata)
self.assertEqual(mail_tracking.state, 'opened')
self.assertTrue(mail.opened)
# Test click e-mail
self.event['event'] = 'click'
self.env['mail.tracking.email'].event_process(
self.request, self.event, self.metadata)
self.assertEqual(mail_tracking.state, 'opened')
self.assertEqual(mail.click_count, 1)
# Test events are linked to e-mail
self.assertEquals(len(mail.tracking_event_ids), 4)
def test_controller(self):
""" Check the controller is working """
event_data = [self.event]
with mock.patch(mock_json_request,
side_effect=side_effect_json) as json_mock:
json_mock.return_value = True
result = self.url_open(
'/mail/tracking/sendgrid/' + self.session.db,
json.dumps(event_data)
)
self.assertTrue(json_mock.called)
self.assertTrue(result)
# Invalid request
self.url_open(
'/mail/tracking/sendgrid/' + self.session.db,
"[{'invalid': True}]"
)
@mock.patch(mock_template_api_client)
@mock.patch(mock_config_template)
def test_update_templates(self, m_config, m_sendgrid):
m_config.return_value = "ldkfjsOIWJRksfj"
m_sendgrid.return_value = FakeTemplateClient()
self.env['sendgrid.template'].update_templates()
template = self.env['sendgrid.template'].search([
('remote_id', '=', 'fake_id')
])
self.assertTrue(template)
def tearDown(self):
super(TestMailSendgrid, self).tearDown()
self.env['ir.config_parameter'].set_param(
'mail_sendgrid.send_method', 'traditional')

31
mail_sendgrid/views/email_template_view.xml

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form view -->
<record id="view_email_template_sendgrid_form" model="ir.ui.view">
<field name="name">sendgrid.sendgrid.form</field>
<field name="model">mail.template</field>
<field name="inherit_id" ref="mail.email_template_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='email_from']/ancestor::page" position="after">
<page string="SendGrid">
<group>
<field name="sendgrid_template_ids">
<tree editable="top">
<field name="lang"/>
<field name="sendgrid_template_id"/>
</tree>
</field>
<button name="update_substitutions" string="Get substitutions from templates" type="object" colspan="2"/>
<field name="substitution_ids">
<tree editable="top">
<field name="key"/>
<field name="lang"/>
<field name="value"/>
</tree>
</field>
</group>
</page>
</xpath>
</field>
</record>
</odoo>

21
mail_sendgrid/views/mail_compose_message_view.xml

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Add sendgrid preview in compose mail wizard -->
<record model="ir.ui.view" id="email_compose_message_sendgrid_form">
<field name="name">mail.compose.sendgrid.form</field>
<field name="model">mail.compose.message</field>
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
<field name="arch" type="xml">
<field name="body" position="replace">
<notebook>
<page string="Html">
<field name="body"/>
</page>
<page string="SendGrid Preview">
<field name="body_sendgrid"/>
</page>
</notebook>
</field>
</field>
</record>
</odoo>

77
mail_sendgrid/views/sendgrid_email_view.xml

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Extension of mail.mail form view -->
<record model="ir.ui.view" id="email_form_view">
<field name="name">mail.mail.sendgrid</field>
<field name="model">mail.mail</field>
<field name="inherit_id" ref="mail.view_mail_form"/>
<field name="arch" type="xml">
<button name="send" position="replace">
<field name="send_method" invisible="1"/>
<button name="send" string="Send Now" type="object" class="oe_highlight" attrs="{'invisible': ['|', ('send_method', 'not in', ['sendgrid','traditional']), ('state', '!=', 'outgoing')]}"/>
<button name="send_sendgrid" string="Send (SendGrid)" type="object" class="oe_highlight" attrs="{'invisible': ['|', ('send_method', '=', 'sendgrid'), ('state', '!=', 'outgoing')]}"/>
</button>
<field name="body_html" position="attributes">
<attribute name="widget">html</attribute>
</field>
<xpath expr="//field[@name='attachment_ids']/ancestor::page" position="after">
<page string="SendGrid">
<group>
<field name="sendgrid_template_id"/>
<field name="sent_date" readonly="1"/>
<field name="opened" readonly="1"/>
<field name="click_count" readonly="1"/>
<field name="body_text"/>
</group>
<field name="substitution_ids" widget="one2many_list"/>
<field name="tracking_event_ids">
<tree default_order="time desc">
<field name="tracking_email_id"/>
<field name="time"/>
<field name="event_type"/>
<field name="url"/>
</tree>
</field>
</page>
</xpath>
</field>
</record>
<!-- Extension of mail.mail tree view -->
<record model="ir.ui.view" id="sendgrid_email_tree_view">
<field name="name">mail.mail.sendgrid.tree</field>
<field name="model">mail.mail</field>
<field name="inherit_id" ref="mail.view_mail_tree"/>
<field name="arch" type="xml">
<field name="date" position="after">
<field name="opened"/>
<field name="click_count"/>
</field>
</field>
</record>
<!-- Extension of mail.mail search view -->
<record model="ir.ui.view" id="sendgrid_email_search_view">
<field name="name">mail.mail.sendgrid.search</field>
<field name="model">mail.mail</field>
<field name="inherit_id" ref="mail.view_mail_search"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='received']" position="after">
<filter name="opened" string="Opened" domain="[('opened','=',True)]"/>
<filter name="clicked" string="Clicked" domain="[('click_count','>',0)]"/>
</xpath>
</field>
</record>
<!-- Substitution line view -->
<record id="view_sendgrid_substitution_line_tree" model="ir.ui.view">
<field name="name">sendgrid.substitution.tree</field>
<field name="model">sendgrid.substitution</field>
<field name="arch" type="xml">
<tree string="Template substitutions" editable="bottom">
<field name="key"/>
<field name="value"/>
</tree>
</field>
</record>
</odoo>

72
mail_sendgrid/views/sendgrid_template_view.xml

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tree view -->
<record id="view_sendgrid_template_tree" model="ir.ui.view">
<field name="name">sendgrid.template.tree</field>
<field name="model">sendgrid.template</field>
<field name="arch" type="xml">
<tree string="Templates">
<field name="remote_id"/>
<field name="name"/>
</tree>
</field>
</record>
<!-- Form view -->
<record id="view_sendgrid_template_form" model="ir.ui.view">
<field name="name">sendgrid.template.form</field>
<field name="model">sendgrid.template</field>
<field name="arch" type="xml">
<form string="Template">
<sheet>
<group>
<field name="name"/>
<field name="remote_id"/>
<field name="detected_keywords"/>
</group>
<notebook>
<page string="Html">
<field name="html_content"/>
</page>
<page string="Plain text">
<field name="plain_content"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Action opening the tree view -->
<record id="open_view_sendgrid_template_tree" model="ir.actions.act_window">
<field name="name">Template</field>
<field name="res_model">sendgrid.template</field>
<field name="view_type">form</field>
<field name="view_mode">form,tree</field>
<field name="view_id" ref="view_sendgrid_template_tree"/>
</record>
<record model="ir.actions.server" id="update_sendgrid_templates">
<field name="name">Update Sendgrid Templates</field>
<field name="model_id" ref="mail_sendgrid.model_sendgrid_template"/>
<field name="code">
env['sendgrid.template'].update_templates()
action = {
'name': 'Sendgrid templates',
'type': 'ir.actions.act_window',
'res_model': 'sendgrid.template',
'view_mode': 'tree,form'
}
</field>
</record>
<!-- Add menu entry in Settings/Email -->
<menuitem name="SendGrid Templates" id="menu_sendgrid_template"
parent="base.menu_email"
sequence="8"
action="open_view_sendgrid_template_tree"/>
<menuitem name="Update SendGrid" id="menu_update_sendgrid_template"
parent="base.menu_email"
sequence="9"
action="update_sendgrid_templates"/>
</odoo>

6
mail_sendgrid/wizards/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2015-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import mail_compose_message
from . import email_template_preview

23
mail_sendgrid/wizards/email_template_preview.py

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Copyright 2015-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, api
class EmailTemplatePreview(models.TransientModel):
""" Put the preview inside sendgrid template """
_inherit = 'email_template.preview'
@api.onchange('res_id')
@api.multi
def on_change_res_id(self):
result = super(EmailTemplatePreview, self).on_change_res_id()
body_html = self.body_html
template_id = self.env.context.get('template_id')
template = self.env['mail.template'].browse(template_id)
sendgrid_template = template.sendgrid_localized_template
if sendgrid_template:
self.body_html = sendgrid_template.html_content.replace(
'<%body%>', body_html)
return result

42
mail_sendgrid/wizards/mail_compose_message.py

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# Copyright 2015-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api
class EmailComposeMessage(models.TransientModel):
""" Email message sent through SendGrid """
_inherit = 'mail.compose.message'
body_sendgrid = fields.Html(compute='_compute_sendgrid_view')
@api.depends('body')
def _compute_sendgrid_view(self):
for wizard in self:
template = wizard.template_id
sendgrid_template = template.sendgrid_localized_template
res_id = self.env.context.get('active_id')
render_body = self.render_template(
wizard.body, wizard.model, [res_id], post_process=True)[res_id]
if sendgrid_template and wizard.body:
wizard.body_sendgrid = sendgrid_template.html_content.replace(
'<%body%>', render_body)
else:
wizard.body_sendgrid = render_body
@api.multi
def get_mail_values(self, res_ids):
""" Attach sendgrid template to e-mail and render substitutions """
mail_values = super(EmailComposeMessage, self).get_mail_values(res_ids)
template = self.template_id
sendgrid_template_id = template.sendgrid_localized_template.id
if sendgrid_template_id:
substitutions = template.render_substitutions(res_ids)
for res_id, value in mail_values.iteritems():
value['sendgrid_template_id'] = sendgrid_template_id
value['substitution_ids'] = substitutions[res_id]
return mail_values

89
mail_sendgrid_mass_mailing/README.rst

@ -0,0 +1,89 @@
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: https://www.gnu.org/licenses/agpl
:alt: License: AGPL-3
=========================
SendGrid for mass mailing
=========================
Links mass mailing and mail statistics objects with Sendgrid.
Note that the mass mailing campaign will be sent with Sendgrid transactional
e-emails (not to mix up with Sendgrid marketing campaigns)
Installation
============
This addon will be automatically installed when 'mail_sendgrid' and
'mass_mailing' are both installed.
Configuration
=============
None
Usage
=====
From mass mailing, you can use Sendgrid templates.
#. If you select a Sendgrid template, the campaign will be sent through
Sendgrid. Otherwise it will use what you set in your system preference
(see module sendgrid).
#. You can force usage of a language for the template.
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/205/10.0
.. repo_id is available in https://github.com/OCA/maintainer-tools/blob/master/tools/repos_with_ids.txt
.. branch is "8.0" for example
Known issues / Roadmap
======================
* Use Sendgrid marketing campaigns API
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/social/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smash it by providing detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://odoo-community.org/logo.png>`_.
Contributors
------------
* Emanuel Cino <ecino@compassion.ch>
Do not contact contributors directly about support or help with technical issues.
Funders
-------
The development of this module has been financially supported by:
* Compassion Switzerland
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

6
mail_sendgrid_mass_mailing/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models
from . import wizards

21
mail_sendgrid_mass_mailing/__manifest__.py

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Mass Mailing with SendGrid',
'version': '10.0.1.0.0',
'category': 'Social Network',
'author': 'Compassion CH, Odoo Community Association (OCA)',
'license': 'AGPL-3',
'website': 'https://github.com/OCA/social',
'depends': ['mail_sendgrid', 'mail_tracking_mass_mailing'],
'data': [
'views/mass_mailing_view.xml'
],
'demo': [],
'installable': True,
'auto_install': True,
'external_dependencies': {
'python': ['sendgrid'],
},
}

7
mail_sendgrid_mass_mailing/models/__init__.py

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import mass_mailing
from . import mail_mail
from . import email_tracking

32
mail_sendgrid_mass_mailing/models/email_tracking.py

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api
class MailTrackingEvent(models.Model):
""" Push events to campaign_statistics
"""
_inherit = 'mail.tracking.event'
@api.model
def process_delivered(self, tracking_email, metadata):
res = super(MailTrackingEvent, self).process_delivered(
tracking_email, metadata)
mail_mail_stats = self.sudo().env['mail.mail.statistics'].search([
('mail_mail_id_int', '=', tracking_email.mail_id_int)])
mail_mail_stats.write({
'sent': fields.Datetime.now()
})
return res
@api.model
def process_reject(self, tracking_email, metadata):
res = super(MailTrackingEvent, self).process_reject(
tracking_email, metadata)
mail_mail_stats = self.sudo().env['mail.mail.statistics'].search([
('mail_mail_id_int', '=', tracking_email.mail_id_int)])
mail_mail_stats.write({
'exception': fields.Datetime.now()
})
return res

59
mail_sendgrid_mass_mailing/models/mail_mail.py

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
import logging
_logger = logging.getLogger(__name__)
try:
from sendgrid.helpers.mail.mail import TrackingSettings, \
SubscriptionTracking
except ImportError:
_logger.error("ImportError raised while loading module.")
_logger.debug("ImportError details:", exc_info=True)
class MailMail(models.Model):
_inherit = "mail.mail"
def _prepare_sendgrid_tracking(self):
track_vals = super(MailMail, self)._prepare_sendgrid_tracking()
track_vals.update({
'mail_id_int': self.id,
'mass_mailing_id': self.mailing_id.id,
'mail_stats_id': self.statistics_ids[:1].id
if self.statistics_ids else False
})
return track_vals
def _track_sendgrid_emails(self):
""" Push tracking_email in mass_mail_statistic """
tracking_emails = super(MailMail, self)._track_sendgrid_emails()
for tracking in tracking_emails.filtered('mail_stats_id'):
tracking.mail_stats_id.mail_tracking_id = tracking.id
return tracking_emails
def _prepare_sendgrid_data(self):
"""
Add unsubscribe options in mass mailings
:return: Sendgrid Email
"""
s_mail = super(MailMail, self)._prepare_sendgrid_data()
tracking_settings = TrackingSettings()
if self.mailing_id.enable_unsubscribe:
sub_settings = SubscriptionTracking(
enable=True,
text=self.mailing_id.unsubscribe_text,
html=self.mailing_id.unsubscribe_text,
)
if self.mailing_id.unsubscribe_tag:
sub_settings.substitution_tag = \
self.mailing_id.unsubscribe_tag
tracking_settings.subscription_tracking = sub_settings
else:
tracking_settings.subscription_tracking = SubscriptionTracking(
enable=False)
s_mail.tracking_settings = tracking_settings
return s_mail

140
mail_sendgrid_mass_mailing/models/mass_mailing.py

@ -0,0 +1,140 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, models, fields, _
from odoo.exceptions import Warning as UserError
from odoo.tools.safe_eval import safe_eval
class MassMailing(models.Model):
""" Add a direct link to an e-mail template in order to retrieve all
Sendgrid configuration into the e-mails. Add ability to force a
template language.
"""
_inherit = 'mail.mass_mailing'
email_template_id = fields.Many2one(
'mail.template', 'Sengdrid Template',
)
lang = fields.Many2one(
comodel_name="res.lang", string="Force language")
body_sendgrid = fields.Html(compute='_compute_sendgrid_view')
# Trick to save html when taken from the e-mail template
html_copy = fields.Html(
compute='_compute_sendgrid_view', inverse='_inverse_html_copy')
# Trick to display another widget when using Sendgrid
html_unframe = fields.Html(related='body_html')
enable_unsubscribe = fields.Boolean()
unsubscribe_text = fields.Char(
default='If you would like to unsubscribe and stop receiving these '
'emails <% clickhere %>.')
unsubscribe_tag = fields.Char()
@api.depends('body_html')
def _compute_sendgrid_view(self):
for wizard in self:
template = wizard.email_template_id.with_context(
lang=self.lang.code or self.env.context['lang'])
sendgrid_template = template.sendgrid_localized_template
if sendgrid_template and wizard.body_html:
res_id = self.env[wizard.mailing_model].search(safe_eval(
wizard.mailing_domain), limit=1).id
if res_id:
body = template.render_template(
wizard.body_html, template.model, [res_id],
post_process=True)[res_id]
wizard.body_sendgrid = \
sendgrid_template.html_content.replace('<%body%>',
body)
else:
wizard.body_sendgrid = wizard.body_html
wizard.html_copy = wizard.body_html
def _inverse_html_copy(self):
for wizard in self:
wizard.body_html = wizard.html_copy
@api.onchange('email_template_id')
def onchange_email_template_id(self):
if self.email_template_id:
template = self.email_template_id.with_context(
lang=self.lang.code or self.env.context['lang'])
if template.email_from:
self.email_from = template.email_from
self.name = template.subject
self.body_html = template.body_html
@api.onchange('lang')
def onchange_lang(self):
if self.lang and self.mailing_model == 'res.partner':
domain = safe_eval(self.mailing_domain)
lang_tuple = False
for tuple in domain:
if tuple[0] == 'lang':
lang_tuple = tuple
break
if lang_tuple:
domain.remove(lang_tuple)
domain.append(('lang', '=', self.lang.code))
self.mailing_domain = str(domain)
self.onchange_email_template_id()
@api.multi
def action_test_mailing(self):
wizard = self
if self.email_template_id:
wizard = self.with_context(
lang=self.lang.code or self.env.context['lang'])
return super(MassMailing, wizard).action_test_mailing()
@api.multi
def send_mail(self):
sendgrid = self.filtered('email_template_id')
emails = self.env['mail.mail']
for mailing in sendgrid:
# use E-mail Template
res_ids = mailing.get_recipients()
if not res_ids:
raise UserError(_('Please select recipients.'))
lang = mailing.lang.code or self.env.context.get('lang', 'en_US')
mailing = mailing.with_context(lang=lang)
composer_values = mailing._send_mail_get_composer_values()
if mailing.reply_to_mode == 'email':
composer_values['reply_to'] = mailing.reply_to
composer = self.env['mail.compose.message'].with_context(
lang=lang, active_ids=res_ids)
emails += composer.mass_mailing_sendgrid(res_ids, composer_values)
mailing.write({
'state': 'done',
'sent_date': fields.Datetime.now(),
})
# Traditional sending
super(MassMailing, self - sendgrid).send_mail()
return emails
def _send_mail_get_composer_values(self):
"""
Get the values used for the mail.compose.message wizard that will
generate the e-mails of a mass mailing campaign.
:return: dictionary of mail.compose.message values
"""
template = self.email_template_id
author = self.mass_mailing_campaign_id.user_id.partner_id or \
self.env.user.partner_id
return {
'template_id': template.id,
'composition_mode': 'mass_mail',
'model': template.model,
'author_id': author.id,
'attachment_ids': [(4, attachment.id) for attachment in
self.attachment_ids],
'email_from': self.email_from,
'body': self.body_html,
'subject': self.name,
'record_name': False,
'mass_mailing_id': self.id,
'mailing_list_ids': [(4, l.id) for l in
self.contact_list_ids],
'no_auto_thread': self.reply_to_mode != 'thread',
}

BIN
mail_sendgrid_mass_mailing/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 3.1 KiB

10
mail_sendgrid_mass_mailing/static/description/icon.svg

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<polygon fill="#9DE1F3" points="85.3335 85.333 0.0005 85.333 0.0005 170.666 0.0005 256 85.3335 256 170.6665 256 170.6665 170.666 170.6665 85.333"></polygon>
<polygon fill="#27B4E1" points="85.3335 0.0004 85.3335 85.3334 85.3335 170.6664 170.6665 170.6664 255.9995 170.6664 255.9995 0.0004"></polygon>
<polygon fill="#1A82E2" points="0 256 85.333 256 85.333 170.667 0 170.667"></polygon>
<polygon fill="#1A82E2" points="170.667 85.333 256 85.333 256 0 170.667 0"></polygon>
<polygon fill="#239FD7" points="85.334 170.667 170.667 170.667 170.667 85.334 85.334 85.334"></polygon>
</g>
</svg>

5
mail_sendgrid_mass_mailing/tests/__init__.py

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_mass_mailing

173
mail_sendgrid_mass_mailing/tests/test_mass_mailing.py

@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
# © 2017 Emanuel Cino - <ecino@compassion.ch>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import mock
from odoo.tests.common import SavepointCase
mock_sendgrid_api_client = ('odoo.addons.mail_sendgrid.models.mail_mail'
'.SendGridAPIClient')
mock_config = ('odoo.addons.mail_sendgrid.models.mail_mail.'
'config')
class FakeClient(object):
""" Mock Sendgrid APIClient """
status_code = 202
body = 'ok'
def __init__(self):
self.client = self
self.mail = self
self.send = self
def post(self, **kwargs):
return self
class FakeRequest(object):
""" Simulate a Sendgrid JSON request """
def __init__(self, data):
self.jsonrequest = [data]
class TestMailSendgrid(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestMailSendgrid, cls).setUpClass()
cls.sendgrid_template = cls.env['sendgrid.template'].create({
'name': 'Test Template',
'remote_id': 'a74795d7-f926-4bad-8e7a-ae95fabd70fc',
'html_content': u'<h1>Test Sendgrid</h1><%body%>{footer}'
})
cls.mail_template = cls.env['mail.template'].create({
'name': 'Test Template',
'model_id': cls.env.ref('base.model_res_partner').id,
'subject': 'Test e-mail',
'body_html': u'Dear ${object.name}, hello!',
'sendgrid_template_ids': [
(0, 0, {'lang': 'en_US', 'sendgrid_template_id':
cls.sendgrid_template.id})]
})
cls.recipient = cls.env.ref('base.partner_demo')
cls.mass_mailing = cls.env['mail.mass_mailing'].create({
'email_from': 'admin@yourcompany.example.com',
'name': 'Test Mass Mailing Sendgrid',
'mailing_model': 'res.partner',
'mailing_domain': "[('id', '=', %d)]" % cls.recipient.id,
'email_template_id': cls.mail_template.id,
'body_html': u'Dear ${object.name}, hello!',
'reply_to_mode': 'email',
'enable_unsubscribe': True,
'unsubscribe_tag': '[unsub]'
}).with_context(lang='en_US', test_mode=True)
cls.timestamp = u'1471021089'
cls.event = {
'timestamp': cls.timestamp,
'sg_event_id': u"f_JoKtrLQaOXUc4thXgROg",
'email': cls.recipient.email,
'odoo_db': cls.env.cr.dbname,
'odoo_id': u'<xxx.xxx.xxx-openerp-xxx-res.partner@test_db>'
}
cls.metadata = {
'ip': '127.0.0.1',
'user_agent': False,
'os_family': False,
'ua_family': False,
}
cls.request = FakeRequest(cls.event)
def test_sendgrid_preview(self):
"""
Test the preview field is getting the Sendgrid template
"""
self.mass_mailing.html_copy = self.mass_mailing.body_html
preview = self.mass_mailing.body_sendgrid
self.assertIn(u'<h1>Test Sendgrid</h1>', preview)
self.assertIn('hello!', preview)
def test_change_language(self):
"""
Test changing the language is changing the domain
"""
domain = self.mass_mailing.mailing_domain
self.mass_mailing.lang = self.env['res.lang'].search([], limit=1)
self.mass_mailing.onchange_lang()
self.assertTrue(len(self.mass_mailing.mailing_domain) > len(domain))
@mock.patch(mock_sendgrid_api_client)
@mock.patch(mock_config)
def test_send_campaign(self, m_config, mock_sendgrid):
"""
Test sending mass campaign with Sendgrid template
and statistics update
"""
self.env['ir.config_parameter'].set_param(
'mail_sendgrid.send_method', 'sendgrid')
mock_sendgrid.return_value = FakeClient()
m_config.get.return_value = 'we4iorujeriu'
# Test campaign
self.mass_mailing.action_test_mailing()
self.env['mail.mass_mailing.test'].create({
'mass_mailing_id': self.mass_mailing.id,
'email_to': 'test@sendgrid.com'
}).with_context(lang='en_US', test_mode=True).send_mail_test()
self.assertTrue(mock_sendgrid.called)
mock_sendgrid.reset_mock()
# Send campaign
emails = self.mass_mailing.send_mail()
# Dont delete emails sent
emails.write({'auto_delete': False})
self.assertEqual(len(emails), 1)
self.assertEqual(emails.state, 'outgoing')
self.assertEqual(emails.sendgrid_template_id.id,
self.sendgrid_template.id)
emails.send()
self.assertTrue(mock_sendgrid.called)
self.assertEqual(emails.state, 'sent')
mail_tracking = emails.tracking_email_ids
self.assertEqual(len(mail_tracking), 1)
self.assertFalse(mail_tracking.state)
stats = self.mass_mailing.statistics_ids
self.assertEqual(len(stats), 1)
self.assertTrue(stats.sent)
# Test delivered
self.event.update({
'event': 'delivered',
'odoo_id': emails.message_id
})
self.env['mail.tracking.email'].event_process(
self.request, self.event, self.metadata)
self.assertTrue(stats.sent)
# Test click e-mail
self.event.update({
'event': 'click',
})
self.env['mail.tracking.email'].event_process(
self.request, self.event, self.metadata)
self.assertEqual(emails.click_count, 1)
events = stats.tracking_event_ids
self.assertEqual(len(events), 2)
self.assertIn('delivered', events.mapped('event_type'))
self.assertIn('click', events.mapped('event_type'))
self.assertEqual(stats.state, 'sent')
# Test reject
self.event.update({
'event': 'dropped',
})
self.env['mail.tracking.email'].event_process(
self.request, self.event, self.metadata)
self.assertEqual(stats.state, 'exception')
@classmethod
def tearDownClass(cls):
cls.env['ir.config_parameter'].set_param(
'mail_sendgrid.send_method', 'traditional')
super(TestMailSendgrid, cls).tearDownClass()

34
mail_sendgrid_mass_mailing/views/mass_mailing_view.xml

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form view -->
<record id="view_email_template_sendgrid_form" model="ir.ui.view">
<field name="name">mass.mailing.sendgrid.form</field>
<field name="model">mail.mass_mailing</field>
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_form"/>
<field name="arch" type="xml">
<field name="body_html" position="attributes">
<attribute name="attrs">{'invisible': [('email_template_id', '!=', False)]}</attribute>
</field>
<field name="body_html" position="after">
<field name="html_unframe" widget="html" attrs="{'invisible': [('email_template_id', '=', False)]}"/>
</field>
<xpath expr="//field[@name='mailing_model']/.." position="after">
<field name="email_template_id" domain="[('model', '=', mailing_model), ('sendgrid_template_ids', '!=', False)]"/>
<field name="lang" attrs="{'invisible': [('email_template_id', '=', False)]}"/>
</xpath>
<xpath expr="//notebook/page[1]" position="after">
<page string="Sendgrid Preview" attrs="{'invisible': [('email_template_id', '=', False)]}">
<field name="html_copy" invisible="1"/>
<field name="body_sendgrid" widget="html"/>
</page>
</xpath>
<xpath expr="//notebook/page/group[1]">
<group string="Sendgrid Unsubscribe">
<field name="enable_unsubscribe"/>
<field name="unsubscribe_text" attrs="{'invisible': [('enable_unsubscribe', '=', False)]}"/>
<field name="unsubscribe_tag" attrs="{'invisible': [('enable_unsubscribe', '=', False)]}"/>
</group>
</xpath>
</field>
</record>
</odoo>

6
mail_sendgrid_mass_mailing/wizards/__init__.py

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import mail_compose_message
from . import test_mailing

33
mail_sendgrid_mass_mailing/wizards/mail_compose_message.py

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, api
class EmailComposeMessage(models.TransientModel):
_inherit = 'mail.compose.message'
@api.model
def mass_mailing_sendgrid(self, res_ids, composer_values):
""" Helper to generate a new e-mail given a template and objects.
:param res_ids: ids of the resource objects
:param composer_values: values for the composer wizard
:return: browse records of created e-mails (one per resource object)
"""
if not isinstance(res_ids, list):
res_ids = [res_ids]
wizard = self.create(composer_values)
all_mail_values = wizard.get_mail_values(res_ids)
email_obj = self.env['mail.mail']
emails = email_obj
for res_id in res_ids:
mail_values = all_mail_values[res_id]
obj = self.env[wizard.model].browse(res_id)
if wizard.model == 'res.partner':
mail_values['recipient_ids'] = [(6, 0, obj.ids)]
else:
mail_values['email_to'] = obj.email
emails += email_obj.create(mail_values)
return emails

48
mail_sendgrid_mass_mailing/wizards/test_mailing.py

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Copyright 2016-2017 Compassion CH (http://www.compassion.ch)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, api, tools
class TestMassMailing(models.TransientModel):
_inherit = 'mail.mass_mailing.test'
@api.multi
def send_mail_test(self):
""" Send with Sendgrid if needed.
"""
self.ensure_one()
mailing = self.mass_mailing_id
template = mailing.email_template_id.with_context(
lang=mailing.lang.code or self.env.context['lang'])
if template:
# Send with SendGrid (and use E-mail Template)
sendgrid_template = template.sendgrid_localized_template
res_id = self.env.user.partner_id.id
body = template.render_template(
mailing.body_html, template.model, [res_id],
post_process=True)[res_id]
test_emails = tools.email_split(self.email_to)
emails = self.env['mail.mail']
for test_mail in test_emails:
email_vals = {
'email_from': mailing.email_from,
'reply_to': mailing.reply_to,
'email_to': test_mail,
'subject': mailing.name,
'body_html': body,
'sendgrid_template_id': sendgrid_template.id,
'substitution_ids': template.render_substitutions(
res_id)[res_id],
'notification': True,
'mailing_id': mailing.id,
'attachment_ids': [(4, attachment.id) for attachment in
mailing.attachment_ids],
}
emails += emails.create(email_vals)
emails.send_sendgrid()
else:
super(TestMassMailing, self).send_mail_test()
return True
Loading…
Cancel
Save