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