Emanuel Cino
8 years ago
26 changed files with 1489 additions and 0 deletions
-
120mail_sendgrid/README.rst
-
14mail_sendgrid/__init__.py
-
51mail_sendgrid/__openerp__.py
-
13mail_sendgrid/controllers/__init__.py
-
53mail_sendgrid/controllers/json_request.py
-
31mail_sendgrid/controllers/sendgrid_event_webhook.py
-
18mail_sendgrid/models/__init__.py
-
29mail_sendgrid/models/email_lang_template.py
-
83mail_sendgrid/models/email_template.py
-
174mail_sendgrid/models/email_tracking.py
-
259mail_sendgrid/models/mail_mail.py
-
14mail_sendgrid/models/mail_tracking_event.py
-
104mail_sendgrid/models/sendgrid_template.py
-
28mail_sendgrid/models/substitution.py
-
4mail_sendgrid/security/ir.model.access.csv
-
BINmail_sendgrid/static/description/icon.png
-
10mail_sendgrid/static/description/icon.svg
-
12mail_sendgrid/tests/__init__.py
-
178mail_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
-
73mail_sendgrid/views/sendgrid_template_view.xml
-
13mail_sendgrid/wizards/__init__.py
-
30mail_sendgrid/wizards/email_template_preview.py
-
49mail_sendgrid/wizards/mail_compose_message.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. |
||||
|
Use any other value to disable traditional e-mail sending. 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/9.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,14 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2015 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Roman Zoller |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
from . import models |
||||
|
from . import wizards |
||||
|
from . import controllers |
@ -0,0 +1,51 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# ______ Releasing children from poverty _ |
||||
|
# / ____/___ ____ ___ ____ ____ ___________(_)___ ____ |
||||
|
# / / / __ \/ __ `__ \/ __ \/ __ `/ ___/ ___/ / __ \/ __ \ |
||||
|
# / /___/ /_/ / / / / / / /_/ / /_/ (__ |__ ) / /_/ / / / / |
||||
|
# \____/\____/_/ /_/ /_/ .___/\__,_/____/____/_/\____/_/ /_/ |
||||
|
# /_/ |
||||
|
# in Jesus' name |
||||
|
# |
||||
|
# Copyright (C) 2015-2017 Compassion CH (http://www.compassion.ch) |
||||
|
# @author: Emanuel Cino, Roman Zoller |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
|
||||
|
{ |
||||
|
'name': 'SendGrid', |
||||
|
'version': '9.0.1.0.0', |
||||
|
'category': 'Social Network', |
||||
|
'author': 'Compassion CH', |
||||
|
'website': 'http://www.compassion.ch', |
||||
|
'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,13 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2016 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Emanuel Cino <ecino@compassion.ch> |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
from . import json_request |
||||
|
from . import sendgrid_event_webhook |
@ -0,0 +1,53 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2016 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Emanuel Cino <ecino@compassion.ch> |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
import simplejson |
||||
|
|
||||
|
from openerp.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,31 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2016 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Emanuel Cino <ecino@compassion.ch> |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
import logging |
||||
|
|
||||
|
from openerp import http |
||||
|
from openerp.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: |
||||
|
return {'status': 400} |
@ -0,0 +1,18 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2015 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Roman Zoller |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
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,29 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2016 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Emanuel Cino |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
from openerp 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('_lang_get', 'Language', required=True) |
||||
|
sendgrid_template_id = fields.Many2one( |
||||
|
'sendgrid.template', 'Sendgrid Template', required=True) |
||||
|
|
||||
|
def _lang_get(self): |
||||
|
languages = self.env['res.lang'].search([]) |
||||
|
return [(language.code, language.name) for language in languages] |
@ -0,0 +1,83 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2015 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Roman Zoller |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
from openerp import models, fields, api |
||||
|
|
||||
|
|
||||
|
class EmailTemplate(models.Model): |
||||
|
_inherit = 'mail.template' |
||||
|
|
||||
|
########################################################################## |
||||
|
# FIELDS # |
||||
|
########################################################################## |
||||
|
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): |
||||
|
self.ensure_one() |
||||
|
new_substitutions = list() |
||||
|
for language_template in self.sendgrid_template_ids: |
||||
|
sendgrid_template = language_template.sendgrid_template_id |
||||
|
lang = language_template.lang |
||||
|
substitutions = self.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': self.id |
||||
|
} |
||||
|
new_substitutions.append((0, 0, substitution_vals)) |
||||
|
|
||||
|
return self.write({'substitution_ids': new_substitutions}) |
||||
|
|
||||
|
@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 = {res_id: list() for res_id in res_ids} |
||||
|
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,174 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2016-2017 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Emanuel Cino <ecino@compassion.ch> |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
import logging |
||||
|
from datetime import datetime |
||||
|
|
||||
|
from werkzeug.useragents import UserAgent |
||||
|
|
||||
|
from openerp 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) |
||||
|
|
||||
|
@api.depends('tracking_event_ids') |
||||
|
def _compute_clicks(self): |
||||
|
for mail in self: |
||||
|
mail.click_count = len(mail.tracking_event_ids.filtered( |
||||
|
lambda event: event.event_type == 'click')) |
||||
|
|
||||
|
@property |
||||
|
def _sendgrid_mandatory_fields(self): |
||||
|
return ('event', 'sg_event_id', '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 _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', False) |
||||
|
try: |
||||
|
ts = float(ts) |
||||
|
except: |
||||
|
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 == 'bounced': |
||||
|
metadata.update({ |
||||
|
'error_type': event.get('type', False), |
||||
|
'error_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._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 not tracking: |
||||
|
res = 'ERROR: Tracking not found' |
||||
|
if res == 'OK': |
||||
|
# Complete metadata with sendgrid event info |
||||
|
metadata = self._sendgrid_metadata( |
||||
|
sendgrid_event_type, event, metadata) |
||||
|
# Create event |
||||
|
tracking.event_create(mapped_event_type, metadata) |
||||
|
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,259 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2015-2016 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Roman Zoller, Emanuel Cino <ecino@compassion.ch> |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
from openerp import models, fields, api, exceptions, tools, _ |
||||
|
from openerp.tools.config import config |
||||
|
from openerp.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' |
||||
|
|
||||
|
########################################################################## |
||||
|
# FIELDS # |
||||
|
########################################################################## |
||||
|
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') |
||||
|
|
||||
|
########################################################################## |
||||
|
# FIELDS METHODS # |
||||
|
########################################################################## |
||||
|
@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 OdooMail(models.Model): |
||||
|
""" Email message sent through SendGrid """ |
||||
|
_inherit = 'mail.mail' |
||||
|
|
||||
|
########################################################################## |
||||
|
# FIELDS # |
||||
|
########################################################################## |
||||
|
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: |
||||
|
email.click_count = sum(email.tracking_email_ids.mapped( |
||||
|
'click_count')) |
||||
|
opened = len(email.tracking_email_ids.filtered( |
||||
|
lambda t: t.state == 'opened')) |
||||
|
email.opened = opened > 0 |
||||
|
|
||||
|
def _compute_events(self): |
||||
|
for email in self: |
||||
|
email.tracking_event_ids = email.tracking_email_ids.mapped( |
||||
|
'tracking_event_ids') |
||||
|
|
||||
|
########################################################################## |
||||
|
# PUBLIC METHODS # |
||||
|
########################################################################## |
||||
|
@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(OdooMail, traditional).send(auto_commit, raise_exception) |
||||
|
if sendgrid: |
||||
|
sendgrid.send_sendgrid() |
||||
|
unknown = self - traditional - sendgrid |
||||
|
if unknown: |
||||
|
_logger.warning( |
||||
|
"Traditional e-mails are disabled. Please remove system " |
||||
|
"parameter mail_sendgrid.send_method if you want to send " |
||||
|
"e-mails through your configured SMTP.") |
||||
|
unknown.write({'state': 'exception'}) |
||||
|
return True |
||||
|
|
||||
|
@api.multi |
||||
|
def send_sendgrid(self): |
||||
|
""" Use sendgrid transactional e-mails : e-mails are sent one by |
||||
|
one. """ |
||||
|
api_key = config.get('sendgrid_api_key') |
||||
|
if not api_key: |
||||
|
raise exceptions.Warning( |
||||
|
'ConfigError', |
||||
|
_('Missing sendgrid_api_key in conf file')) |
||||
|
|
||||
|
sg = SendGridAPIClient(apikey=api_key) |
||||
|
for email in self.filtered(lambda em: em.state == 'outgoing'): |
||||
|
# Commit at each e-mail processed to avoid any errors |
||||
|
# invalidating state. |
||||
|
with self.env.cr.savepoint(): |
||||
|
try: |
||||
|
response = sg.client.mail.send.post( |
||||
|
request_body=email._prepare_sendgrid_data().get()) |
||||
|
except Exception as e: |
||||
|
_logger.error(e.message) |
||||
|
continue |
||||
|
|
||||
|
status = response.status_code |
||||
|
msg = response.body |
||||
|
|
||||
|
if status == STATUS_OK: |
||||
|
_logger.info(str(msg)) |
||||
|
email._track_sendgrid_emails() |
||||
|
email.write({ |
||||
|
'sent_date': fields.Datetime.now(), |
||||
|
'state': 'sent' |
||||
|
}) |
||||
|
else: |
||||
|
_logger.error("Failed to send email: {}".format(str(msg))) |
||||
|
|
||||
|
########################################################################## |
||||
|
# PRIVATE METHODS # |
||||
|
########################################################################## |
||||
|
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)) |
||||
|
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 = list() |
||||
|
if not test_address: |
||||
|
if self.email_to and self.email_to not in addresses: |
||||
|
personalization.add_to(Email(self.email_to)) |
||||
|
addresses.append(self.email_to) |
||||
|
for recipient in self.recipient_ids: |
||||
|
if recipient.email not in addresses: |
||||
|
personalization.add_to(Email(recipient.email)) |
||||
|
addresses.append(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 -*- |
||||
|
# © 2017 Emanuel Cino - <ecino@compassion.com> |
||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from openerp 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,104 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2015 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Roman Zoller |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
from openerp import models, fields, api, exceptions, _ |
||||
|
from openerp.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(self): |
||||
|
api_key = config.get('sendgrid_api_key') |
||||
|
if not api_key: |
||||
|
raise exceptions.Warning( |
||||
|
'ConfigError', |
||||
|
_('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 20 characters and only contain |
||||
|
alphanumeric characters (underscore is allowed). |
||||
|
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'\w{0,20}' + suffix |
||||
|
return list(set(re.findall(pattern, self.html_content))) |
@ -0,0 +1,28 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2015 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Roman Zoller, Emanuel Cino |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
from openerp 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,12 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2017 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Emanuel Cino |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
from . import test_mail_sendgrid |
@ -0,0 +1,178 @@ |
|||||
|
# -*- 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 openerp.tests.common import TransactionCase |
||||
|
|
||||
|
mock_base_send = 'openerp.addons.mail.models.mail_mail.MailMail.send' |
||||
|
mock_sendgrid_api_client = ('openerp.addons.mail_sendgrid.models.mail_mail' |
||||
|
'.SendGridAPIClient') |
||||
|
mock_sendgrid_send = ('openerp.addons.mail_sendgrid.models.mail_mail.' |
||||
|
'OdooMail.send_sendgrid') |
||||
|
mock_config = ('openerp.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(TransactionCase): |
||||
|
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 |
||||
|
}) |
||||
|
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.render_message(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'].create(mail_vals) |
||||
|
|
||||
|
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.render_message(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') |
||||
|
mail = self.create_email() |
||||
|
mock_sendgrid.return_value = FakeClient() |
||||
|
m_config.get.return_value = "ushuwejhfkj" |
||||
|
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) |
@ -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']/../.." 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']/.." 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,73 @@ |
|||||
|
<?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="model_sendgrid_template"/> |
||||
|
<field name="code"> |
||||
|
self.update(cr, uid, context=context) |
||||
|
action = { |
||||
|
'name': 'Sendgrid templates', |
||||
|
'type': 'ir.actions.act_window', |
||||
|
'res_model': 'sendgrid.template', |
||||
|
'view_type': 'form', |
||||
|
'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,13 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2016 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Emanuel Cino |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
from . import mail_compose_message |
||||
|
from . import email_template_preview |
@ -0,0 +1,30 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2016 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Emanuel Cino <ecino@compassion.ch> |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
from openerp import models, api |
||||
|
|
||||
|
|
||||
|
class EmailTemplatePreview(models.TransientModel): |
||||
|
""" Put the preview inside sendgrid template """ |
||||
|
_inherit = 'email_template.preview' |
||||
|
|
||||
|
@api.multi |
||||
|
def on_change_res_id(self, res_id): |
||||
|
result = super(EmailTemplatePreview, self).on_change_res_id(res_id) |
||||
|
body_html = result['value']['body_html'] |
||||
|
template_id = self.env.context.get('template_id') |
||||
|
template = self.env['sendgrid'].browse(template_id) |
||||
|
sendgrid_template = template.sendgrid_localized_template |
||||
|
if sendgrid_template: |
||||
|
body_html = sendgrid_template.html_content.replace( |
||||
|
'<%body%>', body_html) |
||||
|
result['value']['body_html'] = body_html |
||||
|
return result |
@ -0,0 +1,49 @@ |
|||||
|
# -*- encoding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# Copyright (C) 2016 Compassion CH (http://www.compassion.ch) |
||||
|
# Releasing children from poverty in Jesus' name |
||||
|
# @author: Emanuel Cino <ecino@compassion.ch> |
||||
|
# |
||||
|
# The licence is in the file __openerp__.py |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
from openerp 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 |
Write
Preview
Loading…
Cancel
Save
Reference in new issue