From c00876e9b837fff88c3040ce0d264ee64bb0e935 Mon Sep 17 00:00:00 2001 From: Emanuel Cino Date: Thu, 20 Apr 2017 13:19:59 +0200 Subject: [PATCH] Add Sendgrid modules --- .travis.yml | 1 + mail_sendgrid/README.rst | 120 ++++++++ mail_sendgrid/__init__.py | 14 + mail_sendgrid/__openerp__.py | 51 ++++ mail_sendgrid/controllers/__init__.py | 13 + mail_sendgrid/controllers/json_request.py | 53 ++++ .../controllers/sendgrid_event_webhook.py | 31 +++ mail_sendgrid/models/__init__.py | 18 ++ mail_sendgrid/models/email_lang_template.py | 29 ++ mail_sendgrid/models/email_template.py | 83 ++++++ mail_sendgrid/models/email_tracking.py | 174 ++++++++++++ mail_sendgrid/models/mail_mail.py | 259 ++++++++++++++++++ mail_sendgrid/models/mail_tracking_event.py | 14 + mail_sendgrid/models/sendgrid_template.py | 104 +++++++ mail_sendgrid/models/substitution.py | 28 ++ mail_sendgrid/security/ir.model.access.csv | 4 + mail_sendgrid/static/description/icon.png | Bin 0 -> 3167 bytes mail_sendgrid/static/description/icon.svg | 10 + mail_sendgrid/tests/__init__.py | 12 + mail_sendgrid/tests/test_mail_sendgrid.py | 178 ++++++++++++ mail_sendgrid/views/email_template_view.xml | 31 +++ .../views/mail_compose_message_view.xml | 21 ++ mail_sendgrid/views/sendgrid_email_view.xml | 77 ++++++ .../views/sendgrid_template_view.xml | 73 +++++ mail_sendgrid/wizards/__init__.py | 13 + .../wizards/email_template_preview.py | 30 ++ mail_sendgrid/wizards/mail_compose_message.py | 49 ++++ mail_sendgrid_mass_mailing/README.rst | 61 +++++ mail_sendgrid_mass_mailing/__init__.py | 13 + mail_sendgrid_mass_mailing/__openerp__.py | 47 ++++ mail_sendgrid_mass_mailing/models/__init__.py | 14 + .../models/email_tracking.py | 39 +++ .../models/mail_mail.py | 66 +++++ .../models/mass_mailing.py | 135 +++++++++ .../static/description/icon.png | Bin 0 -> 3167 bytes .../static/description/icon.svg | 10 + mail_sendgrid_mass_mailing/tests/__init__.py | 12 + .../tests/test_mass_mailing.py | 123 +++++++++ .../views/mass_mailing_view.xml | 31 +++ .../wizards/__init__.py | 13 + .../wizards/mail_compose_message.py | 40 +++ .../wizards/test_mailing.py | 55 ++++ 42 files changed, 2149 insertions(+) create mode 100644 mail_sendgrid/README.rst create mode 100644 mail_sendgrid/__init__.py create mode 100644 mail_sendgrid/__openerp__.py create mode 100644 mail_sendgrid/controllers/__init__.py create mode 100644 mail_sendgrid/controllers/json_request.py create mode 100644 mail_sendgrid/controllers/sendgrid_event_webhook.py create mode 100644 mail_sendgrid/models/__init__.py create mode 100644 mail_sendgrid/models/email_lang_template.py create mode 100644 mail_sendgrid/models/email_template.py create mode 100644 mail_sendgrid/models/email_tracking.py create mode 100644 mail_sendgrid/models/mail_mail.py create mode 100644 mail_sendgrid/models/mail_tracking_event.py create mode 100644 mail_sendgrid/models/sendgrid_template.py create mode 100644 mail_sendgrid/models/substitution.py create mode 100644 mail_sendgrid/security/ir.model.access.csv create mode 100644 mail_sendgrid/static/description/icon.png create mode 100644 mail_sendgrid/static/description/icon.svg create mode 100644 mail_sendgrid/tests/__init__.py create mode 100644 mail_sendgrid/tests/test_mail_sendgrid.py create mode 100644 mail_sendgrid/views/email_template_view.xml create mode 100644 mail_sendgrid/views/mail_compose_message_view.xml create mode 100644 mail_sendgrid/views/sendgrid_email_view.xml create mode 100644 mail_sendgrid/views/sendgrid_template_view.xml create mode 100644 mail_sendgrid/wizards/__init__.py create mode 100644 mail_sendgrid/wizards/email_template_preview.py create mode 100644 mail_sendgrid/wizards/mail_compose_message.py create mode 100644 mail_sendgrid_mass_mailing/README.rst create mode 100644 mail_sendgrid_mass_mailing/__init__.py create mode 100644 mail_sendgrid_mass_mailing/__openerp__.py create mode 100644 mail_sendgrid_mass_mailing/models/__init__.py create mode 100644 mail_sendgrid_mass_mailing/models/email_tracking.py create mode 100644 mail_sendgrid_mass_mailing/models/mail_mail.py create mode 100644 mail_sendgrid_mass_mailing/models/mass_mailing.py create mode 100644 mail_sendgrid_mass_mailing/static/description/icon.png create mode 100644 mail_sendgrid_mass_mailing/static/description/icon.svg create mode 100644 mail_sendgrid_mass_mailing/tests/__init__.py create mode 100644 mail_sendgrid_mass_mailing/tests/test_mass_mailing.py create mode 100644 mail_sendgrid_mass_mailing/views/mass_mailing_view.xml create mode 100644 mail_sendgrid_mass_mailing/wizards/__init__.py create mode 100644 mail_sendgrid_mass_mailing/wizards/mail_compose_message.py create mode 100644 mail_sendgrid_mass_mailing/wizards/test_mailing.py diff --git a/.travis.yml b/.travis.yml index 6ed82951b1..c8249309e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,7 @@ virtualenv: install: - git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} + - pip install sendgrid - travis_install_nightly script: diff --git a/mail_sendgrid/README.rst b/mail_sendgrid/README.rst new file mode 100644 index 0000000000..ba8270a1b7 --- /dev/null +++ b/mail_sendgrid/README.rst @@ -0,0 +1,120 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :alt: License: AGPL-3 + +================================== +SendGrid Mail Sending and Tracking +================================== + +This module integrates +`SendGrid `_ with Odoo. It can send transactional emails +through SendGrid, using templates defined on the +`SendGrid web interface `_. 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 `_. + ``{`` 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 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:///mail/tracking/sendgrid/`` + +Replace '' with your Odoo install domain name +and '' 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 +`_. 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 `_. + +Contributors +------------ + +* Emanuel Cino +* Roman Zoller + +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. diff --git a/mail_sendgrid/__init__.py b/mail_sendgrid/__init__.py new file mode 100644 index 0000000000..115c5e977b --- /dev/null +++ b/mail_sendgrid/__init__.py @@ -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 diff --git a/mail_sendgrid/__openerp__.py b/mail_sendgrid/__openerp__.py new file mode 100644 index 0000000000..f406ea474b --- /dev/null +++ b/mail_sendgrid/__openerp__.py @@ -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 . +# +############################################################################## + + +{ + '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'], + }, +} diff --git a/mail_sendgrid/controllers/__init__.py b/mail_sendgrid/controllers/__init__.py new file mode 100644 index 0000000000..f6dff205e0 --- /dev/null +++ b/mail_sendgrid/controllers/__init__.py @@ -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 json_request +from . import sendgrid_event_webhook diff --git a/mail_sendgrid/controllers/json_request.py b/mail_sendgrid/controllers/json_request.py new file mode 100644 index 0000000000..05a528bd9f --- /dev/null +++ b/mail_sendgrid/controllers/json_request.py @@ -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 +# +# 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))]) diff --git a/mail_sendgrid/controllers/sendgrid_event_webhook.py b/mail_sendgrid/controllers/sendgrid_event_webhook.py new file mode 100644 index 0000000000..06b9747969 --- /dev/null +++ b/mail_sendgrid/controllers/sendgrid_event_webhook.py @@ -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 +# +# 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/', + 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} diff --git a/mail_sendgrid/models/__init__.py b/mail_sendgrid/models/__init__.py new file mode 100644 index 0000000000..c2e2279cca --- /dev/null +++ b/mail_sendgrid/models/__init__.py @@ -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 diff --git a/mail_sendgrid/models/email_lang_template.py b/mail_sendgrid/models/email_lang_template.py new file mode 100644 index 0000000000..40d0480f8a --- /dev/null +++ b/mail_sendgrid/models/email_lang_template.py @@ -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] diff --git a/mail_sendgrid/models/email_template.py b/mail_sendgrid/models/email_template.py new file mode 100644 index 0000000000..a2799e4e69 --- /dev/null +++ b/mail_sendgrid/models/email_template.py @@ -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 diff --git a/mail_sendgrid/models/email_tracking.py b/mail_sendgrid/models/email_tracking.py new file mode 100644 index 0000000000..a5d6b4869b --- /dev/null +++ b/mail_sendgrid/models/email_tracking.py @@ -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 +# +# 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 diff --git a/mail_sendgrid/models/mail_mail.py b/mail_sendgrid/models/mail_mail.py new file mode 100644 index 0000000000..cde19d4926 --- /dev/null +++ b/mail_sendgrid/models/mail_mail.py @@ -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 +# +# 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('
', '\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, + } diff --git a/mail_sendgrid/models/mail_tracking_event.py b/mail_sendgrid/models/mail_tracking_event.py new file mode 100644 index 0000000000..1f534b974a --- /dev/null +++ b/mail_sendgrid/models/mail_tracking_event.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# © 2017 Emanuel Cino - +# 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') diff --git a/mail_sendgrid/models/sendgrid_template.py b/mail_sendgrid/models/sendgrid_template.py new file mode 100644 index 0000000000..7d4bb94858 --- /dev/null +++ b/mail_sendgrid/models/sendgrid_template.py @@ -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))) diff --git a/mail_sendgrid/models/substitution.py b/mail_sendgrid/models/substitution.py new file mode 100644 index 0000000000..0390325ba7 --- /dev/null +++ b/mail_sendgrid/models/substitution.py @@ -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() diff --git a/mail_sendgrid/security/ir.model.access.csv b/mail_sendgrid/security/ir.model.access.csv new file mode 100644 index 0000000000..6e7d2f02fc --- /dev/null +++ b/mail_sendgrid/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sendgrid_substitution,Full access on sendgrid_substitution,model_sendgrid_substitution,base.group_user,1,1,1,1 +access_sendgrid_template,Full access on sendgrid_template,model_sendgrid_template,base.group_user,1,1,1,1 +access_sendgrid_lang_template,Full access on sendgrid_lang_template,model_sendgrid_email_lang_template,base.group_user,1,1,1,1 diff --git a/mail_sendgrid/static/description/icon.png b/mail_sendgrid/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5567773a95ec6dfda1e1dfd2e54a4f67cd12181f GIT binary patch literal 3167 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRStyLirB|(Yh3I#>^X_+~x3MG{V zsS2qTnQ06R6}L+J0=XItc%1*2bM;Ty8oOK0-Q}QkMNRax8`Iw_^Dbw5iP69R|FM@9}PudwN>EckAoaq-DPRwi>ryrX+El zU6kW`?n9^%dt?2losU=@rs+s8?99z#VBliQ1Ue|f*T*V3KUXg?B|j-uuOhbq=u!p- z8~cia#N_PM5{0DH^vpb4rT4q{D=B2A*eZpa`WpBaIHzW0dQ=sq23ProBv)l8Tc#-4 z+i}@cSOE>lO)W`OsL0L9E4HezRRWu91!RMS^_3LBN=mYAl_Got6rA&mQWZ?}O!N$N zT`MxnjFjxS6l{u8(yW49+@MAPdA3R!B_#z``ugSN<$C4Ddih1^`i7R4mih)p`bI{& zKoz>hm3bwJ6}oxF${-^kX1JslCl_TFlw{`TDS*sOOv*1Uu~kw6$}2z(2L(Rd^t@td zAOM3yA0(r1sAr&$th^*M4To}&3_>|bvsFN5MQTojOJ;6rUNO)Kwn~OZ7GT2=vXNM1 zK_*#w<|d}6hG(XfWFWL5Bs@|x(=&iRff)#v@h{3u1%?60w?G*seNag1LxU0+Gy3qT z0fi6k!Kfu9h4E-`jRqGTqK16Ns32P*U$mdKI;Vst0I~{O@c;k- literal 0 HcmV?d00001 diff --git a/mail_sendgrid/static/description/icon.svg b/mail_sendgrid/static/description/icon.svg new file mode 100644 index 0000000000..8661fee817 --- /dev/null +++ b/mail_sendgrid/static/description/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/mail_sendgrid/tests/__init__.py b/mail_sendgrid/tests/__init__.py new file mode 100644 index 0000000000..be6907c444 --- /dev/null +++ b/mail_sendgrid/tests/__init__.py @@ -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 diff --git a/mail_sendgrid/tests/test_mail_sendgrid.py b/mail_sendgrid/tests/test_mail_sendgrid.py new file mode 100644 index 0000000000..698fbe223a --- /dev/null +++ b/mail_sendgrid/tests/test_mail_sendgrid.py @@ -0,0 +1,178 @@ +# -*- coding: utf-8 -*- +# © 2017 Emanuel Cino - +# 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'

Test Sendgrid

<%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'' + } + 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) diff --git a/mail_sendgrid/views/email_template_view.xml b/mail_sendgrid/views/email_template_view.xml new file mode 100644 index 0000000000..95ef9b1156 --- /dev/null +++ b/mail_sendgrid/views/email_template_view.xml @@ -0,0 +1,31 @@ + + + + + sendgrid.sendgrid.form + mail.template + + + + + + + + + + + + + + html + + + + + + + + + + + + + + + + + + + + + + + + + + + mail.mail.sendgrid.tree + mail.mail + + + + + + + + + + + + mail.mail.sendgrid.search + mail.mail + + + + + + + + + + + + sendgrid.substitution.tree + sendgrid.substitution + + + + + + + + diff --git a/mail_sendgrid/views/sendgrid_template_view.xml b/mail_sendgrid/views/sendgrid_template_view.xml new file mode 100644 index 0000000000..ef20f88e1b --- /dev/null +++ b/mail_sendgrid/views/sendgrid_template_view.xml @@ -0,0 +1,73 @@ + + + + + sendgrid.template.tree + sendgrid.template + + + + + + + + + + + sendgrid.template.form + sendgrid.template + +
+ + + + + + + + + + + + + + + +
+
+
+ + + + Template + sendgrid.template + form + form,tree + + + + + Update Sendgrid Templates + + +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' +} + + + + + + +
diff --git a/mail_sendgrid/wizards/__init__.py b/mail_sendgrid/wizards/__init__.py new file mode 100644 index 0000000000..8494af1621 --- /dev/null +++ b/mail_sendgrid/wizards/__init__.py @@ -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 diff --git a/mail_sendgrid/wizards/email_template_preview.py b/mail_sendgrid/wizards/email_template_preview.py new file mode 100644 index 0000000000..77e20fb4d6 --- /dev/null +++ b/mail_sendgrid/wizards/email_template_preview.py @@ -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 +# +# 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 diff --git a/mail_sendgrid/wizards/mail_compose_message.py b/mail_sendgrid/wizards/mail_compose_message.py new file mode 100644 index 0000000000..369081d895 --- /dev/null +++ b/mail_sendgrid/wizards/mail_compose_message.py @@ -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 +# +# 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 diff --git a/mail_sendgrid_mass_mailing/README.rst b/mail_sendgrid_mass_mailing/README.rst new file mode 100644 index 0000000000..19d462f6b9 --- /dev/null +++ b/mail_sendgrid_mass_mailing/README.rst @@ -0,0 +1,61 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :alt: License: AGPL-3 + +========================= +SendGrid for mass mailing +========================= + +Links mass mailing and mail statistics objects with Sendgrid. +Note that the mass mailing campaign will be sent with Sendgrid transactional +e-emails (not to mix up with Sendgrid marketing campaigns) + +Installation +============ +This addon will be automatically installed when 'mail_sendgrid' and +'mass_mailing' are both installed. + +Usage +===== + +From mass mailing, you can use Sendgrid templates. + +- If you select a Sendgrid template, the campaign will be sent through + Sendgrid. Otherwise it will use what you set in your system preference + (see module sendgrid). +- You can force usage of a language for the template. + +Known issues / Roadmap +====================== + +* Use Sendgrid marketing campaigns API + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 +`here `_. + +Credits +======= + +Contributors +------------ + +* Emanuel Cino + +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. diff --git a/mail_sendgrid_mass_mailing/__init__.py b/mail_sendgrid_mass_mailing/__init__.py new file mode 100644 index 0000000000..1c9429f196 --- /dev/null +++ b/mail_sendgrid_mass_mailing/__init__.py @@ -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 models +from . import wizards diff --git a/mail_sendgrid_mass_mailing/__openerp__.py b/mail_sendgrid_mass_mailing/__openerp__.py new file mode 100644 index 0000000000..acc4e3cd31 --- /dev/null +++ b/mail_sendgrid_mass_mailing/__openerp__.py @@ -0,0 +1,47 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# ______ Releasing children from poverty _ +# / ____/___ ____ ___ ____ ____ ___________(_)___ ____ +# / / / __ \/ __ `__ \/ __ \/ __ `/ ___/ ___/ / __ \/ __ \ +# / /___/ /_/ / / / / / / /_/ / /_/ (__ |__ ) / /_/ / / / / +# \____/\____/_/ /_/ /_/ .___/\__,_/____/____/_/\____/_/ /_/ +# /_/ +# in Jesus' name +# +# Copyright (C) 2016 Compassion CH (http://www.compassion.ch) +# @author: Emanuel Cino +# +# 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 . +# +############################################################################## + + +{ + 'name': 'Mass Mailing with SendGrid', + 'version': '9.0.1.0.0', + 'category': 'Social Network', + 'author': 'Compassion CH', + 'website': 'http://www.compassion.ch', + 'depends': ['mail_sendgrid', 'mail_tracking_mass_mailing'], + 'data': [ + 'views/mass_mailing_view.xml' + ], + 'demo': [], + 'installable': True, + 'auto_install': True, + 'external_dependencies': { + 'python': ['sendgrid'], + }, +} diff --git a/mail_sendgrid_mass_mailing/models/__init__.py b/mail_sendgrid_mass_mailing/models/__init__.py new file mode 100644 index 0000000000..a0d4e22ade --- /dev/null +++ b/mail_sendgrid_mass_mailing/models/__init__.py @@ -0,0 +1,14 @@ +# -*- 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 mass_mailing +from . import mail_mail +from . import email_tracking diff --git a/mail_sendgrid_mass_mailing/models/email_tracking.py b/mail_sendgrid_mass_mailing/models/email_tracking.py new file mode 100644 index 0000000000..21c236f92e --- /dev/null +++ b/mail_sendgrid_mass_mailing/models/email_tracking.py @@ -0,0 +1,39 @@ +# -*- 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, api + + +class MailTrackingEvent(models.Model): + """ Push events to campaign_statistics + """ + _inherit = 'mail.tracking.event' + + @api.model + def process_delivered(self, tracking_email, metadata): + res = super(MailTrackingEvent, self).process_delivered( + tracking_email, metadata) + mail_mail_stats = self.sudo().env['mail.mail.statistics'].search([ + ('mail_mail_id_int', '=', tracking_email.mail_id_int)]) + mail_mail_stats.write({ + 'sent': fields.Datetime.now() + }) + return res + + @api.model + def process_reject(self, tracking_email, metadata): + res = super(MailTrackingEvent, self).process_reject( + tracking_email, metadata) + mail_mail_stats = self.sudo().env['mail.mail.statistics'].search([ + ('mail_mail_id_int', '=', tracking_email.mail_id_int)]) + mail_mail_stats.write({ + 'exception': fields.Datetime.now() + }) + return res diff --git a/mail_sendgrid_mass_mailing/models/mail_mail.py b/mail_sendgrid_mass_mailing/models/mail_mail.py new file mode 100644 index 0000000000..c148df29ea --- /dev/null +++ b/mail_sendgrid_mass_mailing/models/mail_mail.py @@ -0,0 +1,66 @@ +# -*- 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 +import logging + +_logger = logging.getLogger(__name__) + +try: + from sendgrid.helpers.mail.mail import TrackingSettings, \ + SubscriptionTracking +except ImportError: + _logger.error("ImportError raised while loading module.") + _logger.debug("ImportError details:", exc_info=True) + + +class MailMail(models.Model): + _inherit = "mail.mail" + + def _prepare_sendgrid_tracking(self): + track_vals = super(MailMail, self)._prepare_sendgrid_tracking() + track_vals.update({ + 'mail_id_int': self.id, + 'mass_mailing_id': self.mailing_id.id, + 'mail_stats_id': self.statistics_ids[:1].id + if self.statistics_ids else False + }) + return track_vals + + def _track_sendgrid_emails(self): + """ Push tracking_email in mass_mail_statistic """ + tracking_emails = super(MailMail, self)._track_sendgrid_emails() + for tracking in tracking_emails.filtered('mail_stats_id'): + tracking.mail_stats_id.mail_tracking_id = tracking.id + return tracking_emails + + def _prepare_sendgrid_data(self): + """ + Add unsubscribe options in mass mailings + :return: Sendgrid Email + """ + s_mail = super(MailMail, self)._prepare_sendgrid_data() + tracking_settings = TrackingSettings() + if self.mailing_id.enable_unsubscribe: + sub_settings = SubscriptionTracking( + enable=True, + text=self.mailing_id.unsubscribe_text, + html=self.mailing_id.unsubscribe_text, + ) + if self.mailing_id.unsubscribe_tag: + sub_settings.substitution_tag = \ + self.mailing_id.unsubscribe_tag + tracking_settings.subscription_tracking = sub_settings + else: + tracking_settings.subscription_tracking = SubscriptionTracking( + enable=False) + + s_mail.tracking_settings = tracking_settings + return s_mail diff --git a/mail_sendgrid_mass_mailing/models/mass_mailing.py b/mail_sendgrid_mass_mailing/models/mass_mailing.py new file mode 100644 index 0000000000..e8df45b16a --- /dev/null +++ b/mail_sendgrid_mass_mailing/models/mass_mailing.py @@ -0,0 +1,135 @@ +# -*- 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 api, models, fields, _ +from openerp.exceptions import Warning as UserError +from openerp.tools.safe_eval import safe_eval + + +class MassMailing(models.Model): + """ Add a direct link to an e-mail template in order to retrieve all + Sendgrid configuration into the e-mails. Add ability to force a + template language. + """ + _inherit = 'mail.mass_mailing' + + email_template_id = fields.Many2one( + 'mail.template', 'Sengdrid Template', + ) + lang = fields.Many2one( + comodel_name="res.lang", string="Force language") + body_sendgrid = fields.Html(compute='_compute_sendgrid_view') + # Trick to save html when taken from the e-mail template + html_copy = fields.Html( + compute='_compute_sendgrid_view', inverse='_inverse_html_copy') + enable_unsubscribe = fields.Boolean() + unsubscribe_text = fields.Char( + default='If you would like to unsubscribe and stop receiving these ' + 'emails <% clickhere %>.') + unsubscribe_tag = fields.Char() + + @api.depends('body_html') + def _compute_sendgrid_view(self): + for wizard in self: + template = wizard.email_template_id.with_context( + lang=self.lang.code or self.env.context['lang']) + sendgrid_template = template.sendgrid_localized_template + if sendgrid_template and wizard.body_html: + res_id = self.env[wizard.mailing_model].search(safe_eval( + wizard.mailing_domain), limit=1).id + if res_id: + body = template.render_template( + wizard.body_html, template.model, [res_id], + post_process=True)[res_id] + wizard.body_sendgrid = \ + sendgrid_template.html_content.replace('<%body%>', + body) + else: + wizard.body_sendgrid = wizard.body_html + wizard.html_copy = wizard.body_html + + def _inverse_html_copy(self): + for wizard in self: + wizard.body_html = wizard.html_copy + + @api.onchange('email_template_id') + def onchange_email_template_id(self): + if self.email_template_id: + template = self.email_template_id.with_context( + lang=self.lang.code or self.env.context['lang']) + if template.email_from: + self.email_from = template.email_from + self.name = template.subject + self.body_html = template.body_html + + @api.onchange('lang') + def onchange_lang(self): + if self.lang and self.mailing_model == 'res.partner': + domain = safe_eval(self.mailing_domain) + lang_tuple = False + for tuple in domain: + if tuple[0] == 'lang': + lang_tuple = tuple + break + if lang_tuple: + domain.remove(lang_tuple) + domain.append(('lang', '=', self.lang.code)) + self.mailing_domain = str(domain) + self.onchange_email_template_id() + + @api.multi + def action_test_mailing(self): + wizard = self + if self.email_template_id: + wizard = self.with_context( + lang=self.lang.code or self.env.context['lang']) + return super(MassMailing, wizard).action_test_mailing() + + @api.multi + def send_mail(self): + self.ensure_one() + if self.email_template_id: + # use E-mail Template + res_ids = self.get_recipients(self) + if not res_ids: + raise UserError(_('Please select recipients.')) + template = self.email_template_id + composer_values = { + 'template_id': template.id, + 'composition_mode': 'mass_mail', + 'model': template.model, + 'author_id': self.env.user.partner_id.id, + 'res_id': res_ids[0], + 'attachment_ids': [(4, attachment.id) for attachment in + self.attachment_ids], + 'email_from': self.email_from, + 'body': self.body_html, + 'subject': self.name, + 'record_name': False, + 'mass_mailing_id': self.id, + 'mailing_list_ids': [(4, l.id) for l in + self.contact_list_ids], + 'no_auto_thread': self.reply_to_mode != 'thread', + } + if self.reply_to_mode == 'email': + composer_values['reply_to'] = self.reply_to + composer = self.env['mail.compose.message'].with_context( + lang=self.lang.code or self.env.context.get('lang', 'en_US'), + active_ids=res_ids) + emails = composer.mass_mailing_sendgrid(res_ids, composer_values) + self.write({ + 'state': 'done', + 'sent_date': fields.Datetime.now(), + }) + return emails + else: + # Traditional sending + return super(MassMailing, self).send_mail() diff --git a/mail_sendgrid_mass_mailing/static/description/icon.png b/mail_sendgrid_mass_mailing/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5567773a95ec6dfda1e1dfd2e54a4f67cd12181f GIT binary patch literal 3167 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRStyLirB|(Yh3I#>^X_+~x3MG{V zsS2qTnQ06R6}L+J0=XItc%1*2bM;Ty8oOK0-Q}QkMNRax8`Iw_^Dbw5iP69R|FM@9}PudwN>EckAoaq-DPRwi>ryrX+El zU6kW`?n9^%dt?2losU=@rs+s8?99z#VBliQ1Ue|f*T*V3KUXg?B|j-uuOhbq=u!p- z8~cia#N_PM5{0DH^vpb4rT4q{D=B2A*eZpa`WpBaIHzW0dQ=sq23ProBv)l8Tc#-4 z+i}@cSOE>lO)W`OsL0L9E4HezRRWu91!RMS^_3LBN=mYAl_Got6rA&mQWZ?}O!N$N zT`MxnjFjxS6l{u8(yW49+@MAPdA3R!B_#z``ugSN<$C4Ddih1^`i7R4mih)p`bI{& zKoz>hm3bwJ6}oxF${-^kX1JslCl_TFlw{`TDS*sOOv*1Uu~kw6$}2z(2L(Rd^t@td zAOM3yA0(r1sAr&$th^*M4To}&3_>|bvsFN5MQTojOJ;6rUNO)Kwn~OZ7GT2=vXNM1 zK_*#w<|d}6hG(XfWFWL5Bs@|x(=&iRff)#v@h{3u1%?60w?G*seNag1LxU0+Gy3qT z0fi6k!Kfu9h4E-`jRqGTqK16Ns32P*U$mdKI;Vst0I~{O@c;k- literal 0 HcmV?d00001 diff --git a/mail_sendgrid_mass_mailing/static/description/icon.svg b/mail_sendgrid_mass_mailing/static/description/icon.svg new file mode 100644 index 0000000000..8661fee817 --- /dev/null +++ b/mail_sendgrid_mass_mailing/static/description/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/mail_sendgrid_mass_mailing/tests/__init__.py b/mail_sendgrid_mass_mailing/tests/__init__.py new file mode 100644 index 0000000000..d1f8406a06 --- /dev/null +++ b/mail_sendgrid_mass_mailing/tests/__init__.py @@ -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_mass_mailing diff --git a/mail_sendgrid_mass_mailing/tests/test_mass_mailing.py b/mail_sendgrid_mass_mailing/tests/test_mass_mailing.py new file mode 100644 index 0000000000..e96b878c41 --- /dev/null +++ b/mail_sendgrid_mass_mailing/tests/test_mass_mailing.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# © 2017 Emanuel Cino - +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import mock +from openerp.tests.common import TransactionCase + +mock_sendgrid_api_client = ('openerp.addons.mail_sendgrid.models.mail_mail' + '.SendGridAPIClient') +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'

Test Sendgrid

<%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.mass_mailing = self.env['mail.mass_mailing'].create({ + 'email_from': 'admin@yourcompany.example.com', + 'name': 'Test Mass Mailing Sendgrid', + 'mailing_model': 'res.partner', + 'mailing_domain': "[('id', '=', %d)]" % self.recipient.id, + 'email_template_id': self.mail_template.id, + 'body_html': u'Dear ${object.name}, hello!', + 'reply_to_mode': 'thread', + }) + 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'' + } + self.metadata = { + 'ip': '127.0.0.1', + 'user_agent': False, + 'os_family': False, + 'ua_family': False, + } + self.request = FakeRequest(self.event) + + @mock.patch(mock_sendgrid_api_client) + @mock.patch(mock_config) + def test_send_campaign(self, m_config, mock_sendgrid): + """ + Test sending mass campaign with Sendgrid template + and statistics update + """ + self.env['ir.config_parameter'].set_param( + 'mail_sendgrid.send_method', 'sendgrid') + mock_sendgrid.return_value = FakeClient() + m_config.get.return_value = 'we4iorujeriu' + emails = self.mass_mailing.send_mail() + self.assertEqual(len(emails), 1) + self.assertEqual(emails.state, 'outgoing') + self.assertEqual(emails.sendgrid_template_id.id, + self.sendgrid_template.id) + + emails.send() + self.assertTrue(mock_sendgrid.called) + self.assertEqual(emails.state, 'sent') + mail_tracking = emails.tracking_email_ids + self.assertEqual(len(mail_tracking), 1) + self.assertFalse(mail_tracking.state) + stats = self.mass_mailing.statistics_ids + self.assertEqual(len(stats), 1) + self.assertFalse(stats.sent) + + # Test delivered + self.event.update({ + 'event': 'delivered', + 'odoo_id': emails.message_id + }) + self.env['mail.tracking.email'].event_process( + self.request, self.event, self.metadata) + self.assertTrue(stats.sent) + + # Test click e-mail + self.event.update({ + 'event': 'click', + }) + self.env['mail.tracking.email'].event_process( + self.request, self.event, self.metadata) + self.assertEqual(emails.click_count, 1) + events = stats.tracking_event_ids + self.assertEqual(len(events), 2) + self.assertEqual(events[0].event_type, 'delivered') + self.assertEqual(events[1].event_type, 'click') + self.assertEqual(stats.state, 'sent') diff --git a/mail_sendgrid_mass_mailing/views/mass_mailing_view.xml b/mail_sendgrid_mass_mailing/views/mass_mailing_view.xml new file mode 100644 index 0000000000..5c42f523e3 --- /dev/null +++ b/mail_sendgrid_mass_mailing/views/mass_mailing_view.xml @@ -0,0 +1,31 @@ + + + + + + mass.mailing.sendgrid.form + mail.mass_mailing + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mail_sendgrid_mass_mailing/wizards/__init__.py b/mail_sendgrid_mass_mailing/wizards/__init__.py new file mode 100644 index 0000000000..feb13ba4cf --- /dev/null +++ b/mail_sendgrid_mass_mailing/wizards/__init__.py @@ -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 test_mailing diff --git a/mail_sendgrid_mass_mailing/wizards/mail_compose_message.py b/mail_sendgrid_mass_mailing/wizards/mail_compose_message.py new file mode 100644 index 0000000000..46a9ada760 --- /dev/null +++ b/mail_sendgrid_mass_mailing/wizards/mail_compose_message.py @@ -0,0 +1,40 @@ +# -*- 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, api + + +class EmailComposeMessage(models.TransientModel): + _inherit = 'mail.compose.message' + + @api.model + def mass_mailing_sendgrid(self, res_ids, composer_values): + """ Helper to generate a new e-mail given a template and objects. + + :param res_ids: ids of the resource objects + :param composer_values: values for the composer wizard + :return: browse records of created e-mails (one per resource object) + """ + if not isinstance(res_ids, list): + res_ids = [res_ids] + wizard = self.create(composer_values) + all_mail_values = wizard.get_mail_values(res_ids) + email_obj = self.env['mail.mail'] + emails = email_obj + for res_id in res_ids: + mail_values = all_mail_values[res_id] + obj = self.env[wizard.model].browse(res_id) + if wizard.model == 'res.partner': + mail_values['recipient_ids'] = [(6, 0, obj.ids)] + else: + mail_values['email_to'] = obj.email + emails += email_obj.create(mail_values) + return emails diff --git a/mail_sendgrid_mass_mailing/wizards/test_mailing.py b/mail_sendgrid_mass_mailing/wizards/test_mailing.py new file mode 100644 index 0000000000..ae4e862f43 --- /dev/null +++ b/mail_sendgrid_mass_mailing/wizards/test_mailing.py @@ -0,0 +1,55 @@ +# -*- 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, api, tools + + +class TestMassMailing(models.TransientModel): + _inherit = 'mail.mass_mailing.test' + + @api.multi + def send_mail_test(self): + """ Send with Sendgrid if needed. + """ + self.ensure_one() + mailing = self.mass_mailing_id + template = mailing.email_template_id.with_context( + lang=mailing.lang.code or self.env.context['lang']) + if template: + # Send with SendGrid (and use E-mail Template) + sendgrid_template = template.sendgrid_localized_template + res_id = self.env.user.partner_id.id + body = template.render_template( + mailing.body_html, template.model, [res_id], + post_process=True)[res_id] + test_emails = tools.email_split(self.email_to) + emails = self.env['mail.mail'] + for test_mail in test_emails: + email_vals = { + 'email_from': mailing.email_from, + 'reply_to': mailing.reply_to, + 'email_to': test_mail, + 'subject': mailing.name, + 'body_html': body, + 'sendgrid_template_id': sendgrid_template.id, + 'substitution_ids': template.render_substitutions( + res_id)[res_id], + 'notification': True, + 'mailing_id': mailing.id, + 'attachment_ids': [(4, attachment.id) for attachment in + mailing.attachment_ids], + } + emails += emails.create(email_vals) + emails.send_sendgrid() + else: + super(TestMassMailing, self).send_mail_test() + + return True