Browse Source
Merge pull request #185 from CompassionCH/10.0-sendgrid
Merge pull request #185 from CompassionCH/10.0-sendgrid
[10.0][ADD] Sendgrid modulespull/259/head
Pedro M. Baeza
7 years ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 2105 additions and 0 deletions
-
1.travis.yml
-
120mail_sendgrid/README.rst
-
7mail_sendgrid/__init__.py
-
25mail_sendgrid/__manifest__.py
-
6mail_sendgrid/controllers/__init__.py
-
46mail_sendgrid/controllers/json_request.py
-
23mail_sendgrid/controllers/sendgrid_event_webhook.py
-
11mail_sendgrid/models/__init__.py
-
22mail_sendgrid/models/email_lang_template.py
-
76mail_sendgrid/models/email_template.py
-
171mail_sendgrid/models/email_tracking.py
-
242mail_sendgrid/models/mail_mail.py
-
14mail_sendgrid/models/mail_tracking_event.py
-
96mail_sendgrid/models/sendgrid_template.py
-
21mail_sendgrid/models/substitution.py
-
4mail_sendgrid/security/ir.model.access.csv
-
BINmail_sendgrid/static/description/icon.png
-
10mail_sendgrid/static/description/icon.svg
-
5mail_sendgrid/tests/__init__.py
-
270mail_sendgrid/tests/test_mail_sendgrid.py
-
31mail_sendgrid/views/email_template_view.xml
-
21mail_sendgrid/views/mail_compose_message_view.xml
-
77mail_sendgrid/views/sendgrid_email_view.xml
-
72mail_sendgrid/views/sendgrid_template_view.xml
-
6mail_sendgrid/wizards/__init__.py
-
23mail_sendgrid/wizards/email_template_preview.py
-
42mail_sendgrid/wizards/mail_compose_message.py
-
89mail_sendgrid_mass_mailing/README.rst
-
6mail_sendgrid_mass_mailing/__init__.py
-
21mail_sendgrid_mass_mailing/__manifest__.py
-
7mail_sendgrid_mass_mailing/models/__init__.py
-
32mail_sendgrid_mass_mailing/models/email_tracking.py
-
59mail_sendgrid_mass_mailing/models/mail_mail.py
-
140mail_sendgrid_mass_mailing/models/mass_mailing.py
-
BINmail_sendgrid_mass_mailing/static/description/icon.png
-
10mail_sendgrid_mass_mailing/static/description/icon.svg
-
5mail_sendgrid_mass_mailing/tests/__init__.py
-
173mail_sendgrid_mass_mailing/tests/test_mass_mailing.py
-
34mail_sendgrid_mass_mailing/views/mass_mailing_view.xml
-
6mail_sendgrid_mass_mailing/wizards/__init__.py
-
33mail_sendgrid_mass_mailing/wizards/mail_compose_message.py
-
48mail_sendgrid_mass_mailing/wizards/test_mailing.py
@ -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. |
@ -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 |
@ -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'], |
||||
|
}, |
||||
|
} |
@ -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 |
@ -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))]) |
@ -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} |
@ -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 |
@ -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] |
@ -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 |
@ -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 |
@ -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, |
||||
|
} |
@ -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') |
@ -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))) |
@ -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() |
@ -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 |
After Width: 128 | Height: 128 | Size: 3.1 KiB |
@ -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> |
@ -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 |
@ -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') |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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 |
@ -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 |
@ -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 |
@ -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. |
@ -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 |
@ -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'], |
||||
|
}, |
||||
|
} |
@ -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 |
@ -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 |
@ -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 |
@ -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', |
||||
|
} |
After Width: 128 | Height: 128 | Size: 3.1 KiB |
@ -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> |
@ -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 |
@ -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() |
@ -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> |
@ -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 |
@ -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 |
@ -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 |
Write
Preview
Loading…
Cancel
Save
Reference in new issue