From 8e8219357bc754c532852cf9d7b767cc56c9c598 Mon Sep 17 00:00:00 2001 From: Emanuel Cino Date: Thu, 20 Apr 2017 13:19:59 +0200 Subject: [PATCH 1/4] Add Sendgrid module from v9 --- 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 ++++ 26 files changed, 1489 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 diff --git a/mail_sendgrid/README.rst b/mail_sendgrid/README.rst new file mode 100644 index 00000000..ba8270a1 --- /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 00000000..115c5e97 --- /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 00000000..f406ea47 --- /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 00000000..f6dff205 --- /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 00000000..05a528bd --- /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 00000000..06b97479 --- /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 00000000..c2e2279c --- /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 00000000..40d0480f --- /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 00000000..a2799e4e --- /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 00000000..a5d6b486 --- /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 00000000..cde19d49 --- /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 00000000..1f534b97 --- /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 00000000..7d4bb948 --- /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 00000000..0390325b --- /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 00000000..6e7d2f02 --- /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 00000000..8661fee8 --- /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 00000000..be6907c4 --- /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 00000000..698fbe22 --- /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 00000000..95ef9b11 --- /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 00000000..ef20f88e --- /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 00000000..8494af16 --- /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 00000000..77e20fb4 --- /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 00000000..369081d8 --- /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 From e0b6ac93e3e890b5116b92b8d1b79c325e11b203 Mon Sep 17 00:00:00 2001 From: Emanuel Cino Date: Thu, 20 Apr 2017 13:19:59 +0200 Subject: [PATCH 2/4] Add Sendgrid Mass Mailing from v9 --- 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 +++++++ 15 files changed, 659 insertions(+) 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/mail_sendgrid_mass_mailing/README.rst b/mail_sendgrid_mass_mailing/README.rst new file mode 100644 index 00000000..19d462f6 --- /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 00000000..1c9429f1 --- /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 00000000..acc4e3cd --- /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 00000000..a0d4e22a --- /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 00000000..21c236f9 --- /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 00000000..c148df29 --- /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 00000000..e8df45b1 --- /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 00000000..8661fee8 --- /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 00000000..d1f8406a --- /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 00000000..e96b878c --- /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 00000000..5c42f523 --- /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 00000000..feb13ba4 --- /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 00000000..46a9ada7 --- /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 00000000..ae4e862f --- /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 From a759994dc8771332e69316769fb673b2653e087e Mon Sep 17 00:00:00 2001 From: Emanuel Cino Date: Fri, 30 Jun 2017 10:50:58 +0200 Subject: [PATCH 3/4] [10.0] Migrate sendgrid modules CO-1192 The mail are now correctly sent to all recipients --- .travis.yml | 1 + mail_sendgrid/README.rst | 2 +- mail_sendgrid/__init__.py | 13 +---- mail_sendgrid/__manifest__.py | 25 ++++++++ mail_sendgrid/__openerp__.py | 51 ----------------- mail_sendgrid/controllers/__init__.py | 13 +---- mail_sendgrid/controllers/json_request.py | 15 ++--- .../controllers/sendgrid_event_webhook.py | 17 ++---- mail_sendgrid/models/__init__.py | 13 +---- mail_sendgrid/models/email_lang_template.py | 15 ++--- mail_sendgrid/models/email_template.py | 15 ++--- mail_sendgrid/models/email_tracking.py | 15 ++--- mail_sendgrid/models/mail_mail.py | 33 +++++------ mail_sendgrid/models/mail_tracking_event.py | 2 +- mail_sendgrid/models/sendgrid_template.py | 20 ++----- mail_sendgrid/models/substitution.py | 15 ++--- mail_sendgrid/tests/__init__.py | 13 +---- mail_sendgrid/tests/test_mail_sendgrid.py | 6 +- .../views/sendgrid_template_view.xml | 4 +- mail_sendgrid/wizards/__init__.py | 13 +---- .../wizards/email_template_preview.py | 15 ++--- mail_sendgrid/wizards/mail_compose_message.py | 17 ++---- mail_sendgrid_mass_mailing/__init__.py | 13 +---- mail_sendgrid_mass_mailing/__manifest__.py | 21 +++++++ mail_sendgrid_mass_mailing/__openerp__.py | 47 --------------- mail_sendgrid_mass_mailing/models/__init__.py | 13 +---- .../models/email_tracking.py | 15 ++--- .../models/mail_mail.py | 15 ++--- .../models/mass_mailing.py | 21 +++---- mail_sendgrid_mass_mailing/tests/__init__.py | 13 +---- .../tests/test_mass_mailing.py | 2 +- .../views/mass_mailing_view.xml | 57 +++++++++---------- .../wizards/__init__.py | 13 +---- .../wizards/mail_compose_message.py | 15 ++--- .../wizards/test_mailing.py | 15 ++--- 35 files changed, 184 insertions(+), 409 deletions(-) create mode 100644 mail_sendgrid/__manifest__.py delete mode 100644 mail_sendgrid/__openerp__.py create mode 100644 mail_sendgrid_mass_mailing/__manifest__.py delete mode 100644 mail_sendgrid_mass_mailing/__openerp__.py diff --git a/.travis.yml b/.travis.yml index c8f9fedb..f4ace9c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,6 +28,7 @@ virtualenv: system_site_packages: true install: + - pip install sendgrid - git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} - travis_install_nightly diff --git a/mail_sendgrid/README.rst b/mail_sendgrid/README.rst index ba8270a1..e3a9ed92 100644 --- a/mail_sendgrid/README.rst +++ b/mail_sendgrid/README.rst @@ -75,7 +75,7 @@ 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 + :target: https://runbot.odoo-community.org/runbot/205/10.0 Known issues / Roadmap ====================== diff --git a/mail_sendgrid/__init__.py b/mail_sendgrid/__init__.py index 115c5e97..673b18f9 100644 --- a/mail_sendgrid/__init__.py +++ b/mail_sendgrid/__init__.py @@ -1,13 +1,6 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2015-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import models from . import wizards diff --git a/mail_sendgrid/__manifest__.py b/mail_sendgrid/__manifest__.py new file mode 100644 index 00000000..a592afa9 --- /dev/null +++ b/mail_sendgrid/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright 2015-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + 'name': 'SendGrid', + 'version': '10.0.1.0.0', + 'category': 'Social Network', + 'author': 'Compassion CH, Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'website': '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/__openerp__.py b/mail_sendgrid/__openerp__.py deleted file mode 100644 index f406ea47..00000000 --- a/mail_sendgrid/__openerp__.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- 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 index f6dff205..5fcac7c2 100644 --- a/mail_sendgrid/controllers/__init__.py +++ b/mail_sendgrid/controllers/__init__.py @@ -1,13 +1,6 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import json_request from . import sendgrid_event_webhook diff --git a/mail_sendgrid/controllers/json_request.py b/mail_sendgrid/controllers/json_request.py index 05a528bd..189159ba 100644 --- a/mail_sendgrid/controllers/json_request.py +++ b/mail_sendgrid/controllers/json_request.py @@ -1,16 +1,9 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import simplejson -from openerp.http import JsonRequest, Root, Response +from odoo.http import JsonRequest, Root, Response # Monkeypatch type of request rooter to use RESTJsonRequest old_get_request = Root.get_request diff --git a/mail_sendgrid/controllers/sendgrid_event_webhook.py b/mail_sendgrid/controllers/sendgrid_event_webhook.py index 06b97479..87ee7fbf 100644 --- a/mail_sendgrid/controllers/sendgrid_event_webhook.py +++ b/mail_sendgrid/controllers/sendgrid_event_webhook.py @@ -1,17 +1,10 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from openerp import http -from openerp.addons.mail_tracking.controllers.main import \ +from odoo import http +from odoo.addons.mail_tracking.controllers.main import \ MailTrackingController, _env_get _logger = logging.getLogger(__name__) diff --git a/mail_sendgrid/models/__init__.py b/mail_sendgrid/models/__init__.py index c2e2279c..58da3aa4 100644 --- a/mail_sendgrid/models/__init__.py +++ b/mail_sendgrid/models/__init__.py @@ -1,13 +1,6 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import mail_mail from . import substitution diff --git a/mail_sendgrid/models/email_lang_template.py b/mail_sendgrid/models/email_lang_template.py index 40d0480f..a12d032e 100644 --- a/mail_sendgrid/models/email_lang_template.py +++ b/mail_sendgrid/models/email_lang_template.py @@ -1,15 +1,8 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, fields +from odoo import models, fields class LanguageTemplate(models.Model): diff --git a/mail_sendgrid/models/email_template.py b/mail_sendgrid/models/email_template.py index a2799e4e..4f290c88 100644 --- a/mail_sendgrid/models/email_template.py +++ b/mail_sendgrid/models/email_template.py @@ -1,15 +1,8 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, fields, api +from odoo import models, fields, api class EmailTemplate(models.Model): diff --git a/mail_sendgrid/models/email_tracking.py b/mail_sendgrid/models/email_tracking.py index a5d6b486..930f6ffd 100644 --- a/mail_sendgrid/models/email_tracking.py +++ b/mail_sendgrid/models/email_tracking.py @@ -1,19 +1,12 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging from datetime import datetime from werkzeug.useragents import UserAgent -from openerp import models, fields, api +from odoo import models, fields, api _logger = logging.getLogger(__name__) diff --git a/mail_sendgrid/models/mail_mail.py b/mail_sendgrid/models/mail_mail.py index cde19d49..52da8b68 100644 --- a/mail_sendgrid/models/mail_mail.py +++ b/mail_sendgrid/models/mail_mail.py @@ -1,16 +1,9 @@ -# -*- 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 +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models, fields, api, exceptions, tools, _ +from odoo.tools.config import config +from odoo.tools.safe_eval import safe_eval import base64 import logging @@ -122,8 +115,7 @@ class OdooMail(models.Model): one. """ api_key = config.get('sendgrid_api_key') if not api_key: - raise exceptions.Warning( - 'ConfigError', + raise exceptions.UserError( _('Missing sendgrid_api_key in conf file')) sg = SendGridAPIClient(apikey=api_key) @@ -195,15 +187,16 @@ class OdooMail(models.Model): subject = self.subject and self.subject.encode( "utf_8") or "(No subject)" personalization.subject = subject - addresses = list() + addresses = set() 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) + if self.email_to: + addresses = set(self.email_to.split(',')) + for address in addresses: + personalization.add_to(Email(address)) for recipient in self.recipient_ids: if recipient.email not in addresses: personalization.add_to(Email(recipient.email)) - addresses.append(recipient.email) + addresses.add(recipient.email) if self.email_cc and self.email_cc not in addresses: personalization.add_cc(Email(self.email_cc)) else: diff --git a/mail_sendgrid/models/mail_tracking_event.py b/mail_sendgrid/models/mail_tracking_event.py index 1f534b97..c327cc24 100644 --- a/mail_sendgrid/models/mail_tracking_event.py +++ b/mail_sendgrid/models/mail_tracking_event.py @@ -2,7 +2,7 @@ # © 2017 Emanuel Cino - # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp import models, api +from odoo import models, api class MailTrackingEvent(models.Model): diff --git a/mail_sendgrid/models/sendgrid_template.py b/mail_sendgrid/models/sendgrid_template.py index 7d4bb948..ea07f807 100644 --- a/mail_sendgrid/models/sendgrid_template.py +++ b/mail_sendgrid/models/sendgrid_template.py @@ -1,15 +1,8 @@ -# -*- 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 +# -*- coding: utf-8 -*- +# Copyright 2015-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models, fields, api, exceptions, _ +from odoo.tools.config import config import json import re @@ -49,8 +42,7 @@ class SendgridTemplate(models.Model): def update(self): api_key = config.get('sendgrid_api_key') if not api_key: - raise exceptions.Warning( - 'ConfigError', + raise exceptions.UserError( _('Missing sendgrid_api_key in conf file')) sg = sendgrid.SendGridAPIClient(apikey=api_key) diff --git a/mail_sendgrid/models/substitution.py b/mail_sendgrid/models/substitution.py index 0390325b..7f9c9bba 100644 --- a/mail_sendgrid/models/substitution.py +++ b/mail_sendgrid/models/substitution.py @@ -1,15 +1,8 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2015-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, fields +from odoo import models, fields class Substitution(models.Model): diff --git a/mail_sendgrid/tests/__init__.py b/mail_sendgrid/tests/__init__.py index be6907c4..7853a92b 100644 --- a/mail_sendgrid/tests/__init__.py +++ b/mail_sendgrid/tests/__init__.py @@ -1,12 +1,5 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2015-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import test_mail_sendgrid diff --git a/mail_sendgrid/tests/test_mail_sendgrid.py b/mail_sendgrid/tests/test_mail_sendgrid.py index 698fbe22..eea251d9 100644 --- a/mail_sendgrid/tests/test_mail_sendgrid.py +++ b/mail_sendgrid/tests/test_mail_sendgrid.py @@ -2,7 +2,7 @@ # © 2017 Emanuel Cino - # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import mock -from openerp.tests.common import TransactionCase +from odoo.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' @@ -75,7 +75,7 @@ class TestMailSendgrid(TransactionCase): self.request = FakeRequest(self.event) def create_email(self, vals=None): - mail_vals = self.mail_wizard.render_message(self.recipient.ids)[ + mail_vals = self.mail_wizard.get_mail_values(self.recipient.ids)[ self.recipient.id] mail_vals['recipient_ids'] = [(6, 0, self.recipient.ids)] if vals is not None: @@ -93,7 +93,7 @@ class TestMailSendgrid(TransactionCase): 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)[ + mail_values = self.mail_wizard.get_mail_values(self.recipient.ids)[ self.recipient.id] # Test Sendgrid HTML preview self.assertEqual( diff --git a/mail_sendgrid/views/sendgrid_template_view.xml b/mail_sendgrid/views/sendgrid_template_view.xml index ef20f88e..23000005 100644 --- a/mail_sendgrid/views/sendgrid_template_view.xml +++ b/mail_sendgrid/views/sendgrid_template_view.xml @@ -49,13 +49,11 @@ Update Sendgrid Templates - -self.update(cr, uid, context=context) + object.update() 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 index 8494af16..476a596f 100644 --- a/mail_sendgrid/wizards/__init__.py +++ b/mail_sendgrid/wizards/__init__.py @@ -1,13 +1,6 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2015-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import mail_compose_message from . import email_template_preview diff --git a/mail_sendgrid/wizards/email_template_preview.py b/mail_sendgrid/wizards/email_template_preview.py index 77e20fb4..95759be2 100644 --- a/mail_sendgrid/wizards/email_template_preview.py +++ b/mail_sendgrid/wizards/email_template_preview.py @@ -1,15 +1,8 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2015-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, api +from odoo import models, api class EmailTemplatePreview(models.TransientModel): diff --git a/mail_sendgrid/wizards/mail_compose_message.py b/mail_sendgrid/wizards/mail_compose_message.py index 369081d8..f2932c90 100644 --- a/mail_sendgrid/wizards/mail_compose_message.py +++ b/mail_sendgrid/wizards/mail_compose_message.py @@ -1,15 +1,8 @@ -# -*- 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 +# -*- coding: utf-8 -*- +# Copyright 2015-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api class EmailComposeMessage(models.TransientModel): diff --git a/mail_sendgrid_mass_mailing/__init__.py b/mail_sendgrid_mass_mailing/__init__.py index 1c9429f1..211f3a48 100644 --- a/mail_sendgrid_mass_mailing/__init__.py +++ b/mail_sendgrid_mass_mailing/__init__.py @@ -1,13 +1,6 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import models from . import wizards diff --git a/mail_sendgrid_mass_mailing/__manifest__.py b/mail_sendgrid_mass_mailing/__manifest__.py new file mode 100644 index 00000000..cbda3a80 --- /dev/null +++ b/mail_sendgrid_mass_mailing/__manifest__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + 'name': 'Mass Mailing with SendGrid', + 'version': '10.0.1.0.0', + 'category': 'Social Network', + 'author': 'Compassion CH, Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'website': '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/__openerp__.py b/mail_sendgrid_mass_mailing/__openerp__.py deleted file mode 100644 index acc4e3cd..00000000 --- a/mail_sendgrid_mass_mailing/__openerp__.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- 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 index a0d4e22a..1cfb554a 100644 --- a/mail_sendgrid_mass_mailing/models/__init__.py +++ b/mail_sendgrid_mass_mailing/models/__init__.py @@ -1,13 +1,6 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import mass_mailing from . import mail_mail diff --git a/mail_sendgrid_mass_mailing/models/email_tracking.py b/mail_sendgrid_mass_mailing/models/email_tracking.py index 21c236f9..d17cad35 100644 --- a/mail_sendgrid_mass_mailing/models/email_tracking.py +++ b/mail_sendgrid_mass_mailing/models/email_tracking.py @@ -1,14 +1,7 @@ -# -*- 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 +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models, fields, api class MailTrackingEvent(models.Model): diff --git a/mail_sendgrid_mass_mailing/models/mail_mail.py b/mail_sendgrid_mass_mailing/models/mail_mail.py index c148df29..d75a1547 100644 --- a/mail_sendgrid_mass_mailing/models/mail_mail.py +++ b/mail_sendgrid_mass_mailing/models/mail_mail.py @@ -1,14 +1,7 @@ -# -*- 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 +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import models import logging _logger = logging.getLogger(__name__) diff --git a/mail_sendgrid_mass_mailing/models/mass_mailing.py b/mail_sendgrid_mass_mailing/models/mass_mailing.py index e8df45b1..63f389ce 100644 --- a/mail_sendgrid_mass_mailing/models/mass_mailing.py +++ b/mail_sendgrid_mass_mailing/models/mass_mailing.py @@ -1,17 +1,10 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import api, models, fields, _ -from openerp.exceptions import Warning as UserError -from openerp.tools.safe_eval import safe_eval +from odoo import api, models, fields, _ +from odoo.exceptions import Warning as UserError +from odoo.tools.safe_eval import safe_eval class MassMailing(models.Model): @@ -98,7 +91,7 @@ class MassMailing(models.Model): self.ensure_one() if self.email_template_id: # use E-mail Template - res_ids = self.get_recipients(self) + res_ids = self.get_recipients() if not res_ids: raise UserError(_('Please select recipients.')) template = self.email_template_id diff --git a/mail_sendgrid_mass_mailing/tests/__init__.py b/mail_sendgrid_mass_mailing/tests/__init__.py index d1f8406a..b5c1ba78 100644 --- a/mail_sendgrid_mass_mailing/tests/__init__.py +++ b/mail_sendgrid_mass_mailing/tests/__init__.py @@ -1,12 +1,5 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import test_mass_mailing diff --git a/mail_sendgrid_mass_mailing/tests/test_mass_mailing.py b/mail_sendgrid_mass_mailing/tests/test_mass_mailing.py index e96b878c..2a217cc4 100644 --- a/mail_sendgrid_mass_mailing/tests/test_mass_mailing.py +++ b/mail_sendgrid_mass_mailing/tests/test_mass_mailing.py @@ -2,7 +2,7 @@ # © 2017 Emanuel Cino - # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import mock -from openerp.tests.common import TransactionCase +from odoo.tests.common import TransactionCase mock_sendgrid_api_client = ('openerp.addons.mail_sendgrid.models.mail_mail' '.SendGridAPIClient') diff --git a/mail_sendgrid_mass_mailing/views/mass_mailing_view.xml b/mail_sendgrid_mass_mailing/views/mass_mailing_view.xml index 5c42f523..e20a2d0f 100644 --- a/mail_sendgrid_mass_mailing/views/mass_mailing_view.xml +++ b/mail_sendgrid_mass_mailing/views/mass_mailing_view.xml @@ -1,31 +1,28 @@ - - - - - mass.mailing.sendgrid.form - mail.mass_mailing - - - - - - - - - - - - - - - - - - - - - - - - + + + + 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 index feb13ba4..133c9590 100644 --- a/mail_sendgrid_mass_mailing/wizards/__init__.py +++ b/mail_sendgrid_mass_mailing/wizards/__init__.py @@ -1,13 +1,6 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import mail_compose_message from . import test_mailing diff --git a/mail_sendgrid_mass_mailing/wizards/mail_compose_message.py b/mail_sendgrid_mass_mailing/wizards/mail_compose_message.py index 46a9ada7..47a80f64 100644 --- a/mail_sendgrid_mass_mailing/wizards/mail_compose_message.py +++ b/mail_sendgrid_mass_mailing/wizards/mail_compose_message.py @@ -1,15 +1,8 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, api +from odoo import models, api class EmailComposeMessage(models.TransientModel): diff --git a/mail_sendgrid_mass_mailing/wizards/test_mailing.py b/mail_sendgrid_mass_mailing/wizards/test_mailing.py index ae4e862f..ccb8f175 100644 --- a/mail_sendgrid_mass_mailing/wizards/test_mailing.py +++ b/mail_sendgrid_mass_mailing/wizards/test_mailing.py @@ -1,15 +1,8 @@ -# -*- 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 -# -############################################################################## +# -*- coding: utf-8 -*- +# Copyright 2016-2017 Compassion CH (http://www.compassion.ch) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, api, tools +from odoo import models, api, tools class TestMassMailing(models.TransientModel): From 824de6bde4c2ad6ad5c68c016398a3508faaa19d Mon Sep 17 00:00:00 2001 From: Emanuel Cino Date: Wed, 25 Oct 2017 15:27:40 +0200 Subject: [PATCH 4/4] Sendgrid code corrections for v10 + Add tests --- mail_sendgrid/README.rst | 2 +- mail_sendgrid/__manifest__.py | 2 +- .../controllers/sendgrid_event_webhook.py | 7 +- mail_sendgrid/models/email_lang_template.py | 4 +- mail_sendgrid/models/email_template.py | 44 +++---- mail_sendgrid/models/email_tracking.py | 38 +++--- mail_sendgrid/models/mail_mail.py | 104 ++++++++-------- mail_sendgrid/models/mail_tracking_event.py | 2 +- mail_sendgrid/models/sendgrid_template.py | 8 +- mail_sendgrid/tests/test_mail_sendgrid.py | 112 ++++++++++++++++-- mail_sendgrid/views/email_template_view.xml | 2 +- mail_sendgrid/views/sendgrid_email_view.xml | 2 +- .../views/sendgrid_template_view.xml | 5 +- .../wizards/email_template_preview.py | 12 +- mail_sendgrid_mass_mailing/README.rst | 50 ++++++-- mail_sendgrid_mass_mailing/__manifest__.py | 2 +- .../models/mass_mailing.py | 74 +++++++----- .../tests/test_mass_mailing.py | 102 ++++++++++++---- .../views/mass_mailing_view.xml | 6 + 19 files changed, 380 insertions(+), 198 deletions(-) diff --git a/mail_sendgrid/README.rst b/mail_sendgrid/README.rst index e3a9ed92..9de78760 100644 --- a/mail_sendgrid/README.rst +++ b/mail_sendgrid/README.rst @@ -35,7 +35,7 @@ You can add the following system parameters to configure the usage of SendGrid: 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 + 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 diff --git a/mail_sendgrid/__manifest__.py b/mail_sendgrid/__manifest__.py index a592afa9..f916e21b 100644 --- a/mail_sendgrid/__manifest__.py +++ b/mail_sendgrid/__manifest__.py @@ -7,7 +7,7 @@ 'category': 'Social Network', 'author': 'Compassion CH, Odoo Community Association (OCA)', 'license': 'AGPL-3', - 'website': 'http://www.compassion.ch', + 'website': 'https://github.com/OCA/social', 'depends': ['mail_tracking'], 'data': [ 'security/ir.model.access.csv', diff --git a/mail_sendgrid/controllers/sendgrid_event_webhook.py b/mail_sendgrid/controllers/sendgrid_event_webhook.py index 87ee7fbf..db1777d9 100644 --- a/mail_sendgrid/controllers/sendgrid_event_webhook.py +++ b/mail_sendgrid/controllers/sendgrid_event_webhook.py @@ -11,14 +11,13 @@ _logger = logging.getLogger(__name__) class SendgridTrackingController(MailTrackingController): - """ - Sendgrid is posting JSON so we must define a new route for tracking. - """ + """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: + except Exception as e: + _logger.error(e.message, exc_info=True) return {'status': 400} diff --git a/mail_sendgrid/models/email_lang_template.py b/mail_sendgrid/models/email_lang_template.py index a12d032e..a48cc625 100644 --- a/mail_sendgrid/models/email_lang_template.py +++ b/mail_sendgrid/models/email_lang_template.py @@ -13,10 +13,10 @@ class LanguageTemplate(models.Model): _name = 'sendgrid.email.lang.template' email_template_id = fields.Many2one('mail.template', 'E-mail Template') - lang = fields.Selection('_lang_get', 'Language', required=True) + lang = fields.Selection('_select_lang', 'Language', required=True) sendgrid_template_id = fields.Many2one( 'sendgrid.template', 'Sendgrid Template', required=True) - def _lang_get(self): + def _select_lang(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 index 4f290c88..5da331e0 100644 --- a/mail_sendgrid/models/email_template.py +++ b/mail_sendgrid/models/email_template.py @@ -3,14 +3,12 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import models, fields, api +from collections import defaultdict class EmailTemplate(models.Model): _inherit = 'mail.template' - ########################################################################## - # FIELDS # - ########################################################################## substitution_ids = fields.One2many( 'sendgrid.substitution', 'email_template_id', 'Substitutions') sendgrid_template_ids = fields.One2many( @@ -30,25 +28,27 @@ class EmailTemplate(models.Model): @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)) + for template in self: + new_substitutions = [] + for language_template in template.sendgrid_template_ids: + sendgrid_template = language_template.sendgrid_template_id + lang = language_template.lang + substitutions = template.substitution_ids.filtered( + lambda s: s.lang == lang) + keywords = sendgrid_template.get_keywords() + # Add new keywords from the sendgrid template + for key in keywords: + if key not in substitutions.mapped('key'): + substitution_vals = { + 'key': key, + 'lang': lang, + 'email_template_id': template.id + } + new_substitutions.append((0, 0, substitution_vals)) + + template.write({'substitution_ids': new_substitutions}) - return self.write({'substitution_ids': new_substitutions}) + return True @api.multi def render_substitutions(self, res_ids): @@ -64,7 +64,7 @@ class EmailTemplate(models.Model): 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} + substitution_vals = defaultdict(list) for substitution in substitutions: values = self.render_template( substitution.value, self.model, res_ids) diff --git a/mail_sendgrid/models/email_tracking.py b/mail_sendgrid/models/email_tracking.py index 930f6ffd..c7b97e51 100644 --- a/mail_sendgrid/models/email_tracking.py +++ b/mail_sendgrid/models/email_tracking.py @@ -17,18 +17,20 @@ class MailTrackingEmail(models.Model): """ _inherit = 'mail.tracking.email' - click_count = fields.Integer(compute='_compute_clicks', store=True) + click_count = fields.Integer( + compute='_compute_clicks', store=True, readonly=True) @api.depends('tracking_event_ids') def _compute_clicks(self): for mail in self: - mail.click_count = len(mail.tracking_event_ids.filtered( - lambda event: event.event_type == 'click')) + mail.click_count = self.env['mail.tracking.event'].search_count([ + ('event_type', '=', 'click'), + ('tracking_email_id', '=', mail.id) + ]) @property def _sendgrid_mandatory_fields(self): - return ('event', 'sg_event_id', 'timestamp', - 'odoo_id', 'odoo_db') + return ('event', 'timestamp', 'odoo_id', 'odoo_db') @property def _sendgrid_event_type_mapping(self): @@ -56,7 +58,7 @@ class MailTrackingEmail(models.Model): # OK, event type is valid return True - def _db_verify(self, event): + def _sendgrid_db_verify(self, event): event = event or {} odoo_db = event.get('odoo_db') current_db = self.env.cr.dbname @@ -70,10 +72,10 @@ class MailTrackingEmail(models.Model): def _sendgrid_metadata(self, sendgrid_event_type, event, metadata): # Get sendgrid timestamp when found - ts = event.get('timestamp', False) + ts = event.get('timestamp') try: ts = float(ts) - except: + except ValueError: ts = False if ts: dt = datetime.utcfromtimestamp(ts) @@ -102,10 +104,12 @@ class MailTrackingEmail(models.Model): 'android', 'iphone', 'ipad'] }) # Mapping for special events - if sendgrid_event_type == 'bounced': + if sendgrid_event_type == 'bounce': metadata.update({ 'error_type': event.get('type', False), + 'bounce_type': event.get('type', False), 'error_description': event.get('reason', False), + 'bounce_description': event.get('reason', False), 'error_details': event.get('status', False), }) elif sendgrid_event_type == 'dropped': @@ -138,7 +142,7 @@ class MailTrackingEmail(models.Model): 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): + elif not self._sendgrid_db_verify(event): res = 'ERROR: Invalid DB' else: res = 'OK' @@ -149,14 +153,14 @@ class MailTrackingEmail(models.Model): if not mapped_event_type: res = 'ERROR: Bad event' tracking = self._sendgrid_tracking_get(event) - if not tracking: + if tracking: + # Complete metadata with sendgrid event info + metadata = self._sendgrid_metadata( + sendgrid_event_type, event, metadata) + # Create event + tracking.event_create(mapped_event_type, metadata) + else: res = 'ERROR: Tracking not found' - if res == '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( diff --git a/mail_sendgrid/models/mail_mail.py b/mail_sendgrid/models/mail_mail.py index 52da8b68..9d1b8503 100644 --- a/mail_sendgrid/models/mail_mail.py +++ b/mail_sendgrid/models/mail_mail.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2016-2017 Compassion CH (http://www.compassion.ch) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import models, fields, api, exceptions, tools, _ +from odoo import models, fields, api, tools from odoo.tools.config import config from odoo.tools.safe_eval import safe_eval @@ -32,9 +32,6 @@ class MailMessage(models.Model): """ _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( @@ -43,9 +40,6 @@ class MailMessage(models.Model): '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. @@ -56,13 +50,10 @@ class MailMessage(models.Model): email.send_method = send_method -class OdooMail(models.Model): +class MailMail(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) @@ -77,75 +68,74 @@ class OdooMail(models.Model): 'tracking_email_ids.state') def _compute_tracking(self): for email in self: - email.click_count = sum(email.tracking_email_ids.mapped( + 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 + opened = self.env['mail.tracking.email'].search_count([ + ('state', '=', 'opened'), + ('mail_id', '=', email.id) + ]) + email.update({ + 'click_count': click_count, + 'opened': opened > 0 + }) def _compute_events(self): for email in self: email.tracking_event_ids = email.tracking_email_ids.mapped( 'tracking_event_ids') - ########################################################################## - # 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) + super(MailMail, 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. """ + outgoing = self.filtered(lambda em: em.state == 'outgoing') api_key = config.get('sendgrid_api_key') - if not api_key: - raise exceptions.UserError( - _('Missing sendgrid_api_key in conf file')) + if outgoing and not api_key: + _logger.error( + 'Missing sendgrid_api_key in conf file. Skipping Sendgrid ' + 'send.' + ) + return sg = SendGridAPIClient(apikey=api_key) - for email in 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 # - ########################################################################## + for email in outgoing: + try: + response = sg.client.mail.send.post( + request_body=email._prepare_sendgrid_data().get()) + except Exception as e: + _logger.error(e.message or "mail not sent.") + continue + + status = response.status_code + msg = response.body + + if status == STATUS_OK: + _logger.info("e-mail sent. " + str(msg)) + email._track_sendgrid_emails() + email.write({ + 'sent_date': fields.Datetime.now(), + 'state': 'sent' + }) + if not self.env.context.get('test_mode'): + # Commit at each e-mail processed to avoid any errors + # invalidating state. + self.env.cr.commit() # pylint: disable=invalid-commit + email._postprocess_sent_message(mail_sent=True) + else: + email._postprocess_sent_message(mail_sent=False) + _logger.error("Failed to send email: {}".format(str(msg))) + def _prepare_sendgrid_data(self): """ Prepare and creates the Sendgrid Email object @@ -177,7 +167,7 @@ class OdooMail(models.Model): 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/plain", text_only or ' ')) s_mail.add_content(Content("text/html", html)) test_address = config.get('sendgrid_test_address') diff --git a/mail_sendgrid/models/mail_tracking_event.py b/mail_sendgrid/models/mail_tracking_event.py index c327cc24..0a8ed15a 100644 --- a/mail_sendgrid/models/mail_tracking_event.py +++ b/mail_sendgrid/models/mail_tracking_event.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# © 2017 Emanuel Cino - +# Copyright 2017 Emanuel Cino - # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import models, api diff --git a/mail_sendgrid/models/sendgrid_template.py b/mail_sendgrid/models/sendgrid_template.py index ea07f807..d96bf641 100644 --- a/mail_sendgrid/models/sendgrid_template.py +++ b/mail_sendgrid/models/sendgrid_template.py @@ -39,7 +39,7 @@ class SendgridTemplate(models.Model): self.detected_keywords = ';'.join(keywords) @api.model - def update(self): + def update_templates(self): api_key = config.get('sendgrid_api_key') if not api_key: raise exceptions.UserError( @@ -77,8 +77,8 @@ class SendgridTemplate(models.Model): 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). + keyword_name shouldn't be longer than 50 characters and not contain + whitespaces. You can replace the substitution prefix and suffix by adding values in the system parameters - mail_sendgrid.substitution_prefix @@ -92,5 +92,5 @@ class SendgridTemplate(models.Model): suffix = params.search([ ('key', '=', 'mail_sendgrid.substitution_suffix') ]) or '}' - pattern = prefix + r'\w{0,20}' + suffix + pattern = prefix + r'\S{1,50}' + suffix return list(set(re.findall(pattern, self.html_content))) diff --git a/mail_sendgrid/tests/test_mail_sendgrid.py b/mail_sendgrid/tests/test_mail_sendgrid.py index eea251d9..be51d74b 100644 --- a/mail_sendgrid/tests/test_mail_sendgrid.py +++ b/mail_sendgrid/tests/test_mail_sendgrid.py @@ -1,17 +1,31 @@ # -*- coding: utf-8 -*- # © 2017 Emanuel Cino - # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import json + import mock -from odoo.tests.common import TransactionCase +from odoo.tests.common import HttpCase +from ..controllers.json_request import RESTJsonRequest -mock_base_send = 'openerp.addons.mail.models.mail_mail.MailMail.send' -mock_sendgrid_api_client = ('openerp.addons.mail_sendgrid.models.mail_mail' +mock_base_send = 'odoo.addons.mail.models.mail_mail.MailMail.send' +mock_sendgrid_api_client = ('odoo.addons.mail_sendgrid.models.mail_mail' '.SendGridAPIClient') -mock_sendgrid_send = ('openerp.addons.mail_sendgrid.models.mail_mail.' - 'OdooMail.send_sendgrid') -mock_config = ('openerp.addons.mail_sendgrid.models.mail_mail.' +mock_sendgrid_send = ('odoo.addons.mail_sendgrid.models.mail_mail.' + 'MailMail.send_sendgrid') +mock_config = ('odoo.addons.mail_sendgrid.models.mail_mail.' 'config') +mock_config_template = ('odoo.addons.mail_sendgrid.models.sendgrid_template.' + 'config') +mock_template_api_client = ('odoo.addons.mail_sendgrid.models.' + 'sendgrid_template.sendgrid.SendGridAPIClient') + +mock_json_request = 'odoo.http.Root.get_request' + + +def side_effect_json(http_request): + return RESTJsonRequest(http_request) + class FakeClient(object): """ Mock Sendgrid APIClient """ @@ -33,7 +47,31 @@ class FakeRequest(object): self.jsonrequest = [data] -class TestMailSendgrid(TransactionCase): +class FakeTemplateClient(object): + """ Simulate the Sendgrid Template api""" + def __init__(self): + self.client = self + self.templates = self + self.body = json.dumps({ + "templates": [{ + "id": "fake_id", + "name": "Fake Template" + }], + "versions": [{ + "active": True, + "html_content": "

fake

", + "plain_content": "fake", + }], + }) + + def get(self): + return self + + def _(self, id): + return self + + +class TestMailSendgrid(HttpCase): def setUp(self): super(TestMailSendgrid, self).setUp() self.sendgrid_template = self.env['sendgrid.template'].create({ @@ -56,7 +94,7 @@ class TestMailSendgrid(TransactionCase): 'composition_mode': 'comment', 'model': 'res.partner', 'res_id': self.recipient.id - }) + }).with_context(active_id=self.recipient.id) self.mail_wizard.onchange_template_id_wrapper() self.timestamp = u'1471021089' self.event = { @@ -80,7 +118,22 @@ class TestMailSendgrid(TransactionCase): 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) + return self.env['mail.mail'].with_context(test_mode=True).create( + mail_vals) + + def test_preview(self): + """ + Test the preview email_template is getting the Sendgrid template + """ + preview_wizard = self.env['email_template.preview'].with_context( + template_id=self.mail_template.id, + default_res_id=self.recipient.id + ).create({}) + # For a strange reason, res_id is converted to string + preview_wizard.res_id = self.recipient.id + preview_wizard.on_change_res_id() + self.assertIn(u'

Test Sendgrid

', preview_wizard.body_html) + self.assertIn(self.recipient.name, preview_wizard.body_html) def test_substitutions(self): """ Test substitutions in templates. """ @@ -135,9 +188,11 @@ class TestMailSendgrid(TransactionCase): """ 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" + + # Send mail + mail = self.create_email() mail.send() self.assertEqual(mock_sendgrid.called, True) self.assertEqual(mail.state, 'sent') @@ -176,3 +231,40 @@ class TestMailSendgrid(TransactionCase): self.request, self.event, self.metadata) self.assertEqual(mail_tracking.state, 'opened') self.assertEqual(mail.click_count, 1) + + # Test events are linked to e-mail + self.assertEquals(len(mail.tracking_event_ids), 4) + + def test_controller(self): + """ Check the controller is working """ + event_data = [self.event] + with mock.patch(mock_json_request, + side_effect=side_effect_json) as json_mock: + json_mock.return_value = True + result = self.url_open( + '/mail/tracking/sendgrid/' + self.session.db, + json.dumps(event_data) + ) + self.assertTrue(json_mock.called) + self.assertTrue(result) + # Invalid request + self.url_open( + '/mail/tracking/sendgrid/' + self.session.db, + "[{'invalid': True}]" + ) + + @mock.patch(mock_template_api_client) + @mock.patch(mock_config_template) + def test_update_templates(self, m_config, m_sendgrid): + m_config.return_value = "ldkfjsOIWJRksfj" + m_sendgrid.return_value = FakeTemplateClient() + self.env['sendgrid.template'].update_templates() + template = self.env['sendgrid.template'].search([ + ('remote_id', '=', 'fake_id') + ]) + self.assertTrue(template) + + def tearDown(self): + super(TestMailSendgrid, self).tearDown() + self.env['ir.config_parameter'].set_param( + 'mail_sendgrid.send_method', 'traditional') diff --git a/mail_sendgrid/views/email_template_view.xml b/mail_sendgrid/views/email_template_view.xml index 95ef9b11..15e2191c 100644 --- a/mail_sendgrid/views/email_template_view.xml +++ b/mail_sendgrid/views/email_template_view.xml @@ -6,7 +6,7 @@ mail.template - + diff --git a/mail_sendgrid/views/sendgrid_email_view.xml b/mail_sendgrid/views/sendgrid_email_view.xml index 5de839f5..5aeb6a05 100644 --- a/mail_sendgrid/views/sendgrid_email_view.xml +++ b/mail_sendgrid/views/sendgrid_email_view.xml @@ -14,7 +14,7 @@ html - + diff --git a/mail_sendgrid/views/sendgrid_template_view.xml b/mail_sendgrid/views/sendgrid_template_view.xml index 23000005..53f2f983 100644 --- a/mail_sendgrid/views/sendgrid_template_view.xml +++ b/mail_sendgrid/views/sendgrid_template_view.xml @@ -48,8 +48,9 @@ Update Sendgrid Templates - - object.update() + + +env['sendgrid.template'].update_templates() action = { 'name': 'Sendgrid templates', 'type': 'ir.actions.act_window', diff --git a/mail_sendgrid/wizards/email_template_preview.py b/mail_sendgrid/wizards/email_template_preview.py index 95759be2..2d1dfda1 100644 --- a/mail_sendgrid/wizards/email_template_preview.py +++ b/mail_sendgrid/wizards/email_template_preview.py @@ -9,15 +9,15 @@ class EmailTemplatePreview(models.TransientModel): """ Put the preview inside sendgrid template """ _inherit = 'email_template.preview' + @api.onchange('res_id') @api.multi - def on_change_res_id(self, res_id): - result = super(EmailTemplatePreview, self).on_change_res_id(res_id) - body_html = result['value']['body_html'] + def on_change_res_id(self): + result = super(EmailTemplatePreview, self).on_change_res_id() + body_html = self.body_html template_id = self.env.context.get('template_id') - template = self.env['sendgrid'].browse(template_id) + template = self.env['mail.template'].browse(template_id) sendgrid_template = template.sendgrid_localized_template if sendgrid_template: - body_html = sendgrid_template.html_content.replace( + self.body_html = sendgrid_template.html_content.replace( '<%body%>', body_html) - result['value']['body_html'] = body_html return result diff --git a/mail_sendgrid_mass_mailing/README.rst b/mail_sendgrid_mass_mailing/README.rst index 19d462f6..41652f72 100644 --- a/mail_sendgrid_mass_mailing/README.rst +++ b/mail_sendgrid_mass_mailing/README.rst @@ -1,5 +1,6 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :alt: License: AGPL-3 +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 ========================= SendGrid for mass mailing @@ -11,18 +12,31 @@ e-emails (not to mix up with Sendgrid marketing campaigns) Installation ============ + This addon will be automatically installed when 'mail_sendgrid' and 'mass_mailing' are both installed. +Configuration +============= +None + Usage ===== From mass mailing, you can use Sendgrid templates. -- If you select a Sendgrid template, the campaign will be sent through - Sendgrid. Otherwise it will use what you set in your system preference - (see module sendgrid). -- You can force usage of a language for the template. +#. If you select a Sendgrid template, the campaign will be sent through + Sendgrid. Otherwise it will use what you set in your system preference + (see module sendgrid). +#. You can force usage of a language for the template. + + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/205/10.0 + +.. repo_id is available in https://github.com/OCA/maintainer-tools/blob/master/tools/repos_with_ids.txt +.. branch is "8.0" for example Known issues / Roadmap ====================== @@ -32,19 +46,33 @@ Known issues / Roadmap 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 `_. +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 smash it by providing detailed and welcomed feedback. Credits ======= +Images +------ + +* Odoo Community Association: `Icon `_. + Contributors ------------ * Emanuel Cino +Do not contact contributors directly about support or help with technical issues. + +Funders +------- + +The development of this module has been financially supported by: + +* Compassion Switzerland + Maintainer ---------- @@ -58,4 +86,4 @@ 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. +To contribute to this module, please visit https://odoo-community.org. diff --git a/mail_sendgrid_mass_mailing/__manifest__.py b/mail_sendgrid_mass_mailing/__manifest__.py index cbda3a80..cef66bcd 100644 --- a/mail_sendgrid_mass_mailing/__manifest__.py +++ b/mail_sendgrid_mass_mailing/__manifest__.py @@ -7,7 +7,7 @@ 'category': 'Social Network', 'author': 'Compassion CH, Odoo Community Association (OCA)', 'license': 'AGPL-3', - 'website': 'http://www.compassion.ch', + 'website': 'https://github.com/OCA/social', 'depends': ['mail_sendgrid', 'mail_tracking_mass_mailing'], 'data': [ 'views/mass_mailing_view.xml' diff --git a/mail_sendgrid_mass_mailing/models/mass_mailing.py b/mail_sendgrid_mass_mailing/models/mass_mailing.py index 63f389ce..f4c681d1 100644 --- a/mail_sendgrid_mass_mailing/models/mass_mailing.py +++ b/mail_sendgrid_mass_mailing/models/mass_mailing.py @@ -23,6 +23,8 @@ class MassMailing(models.Model): # Trick to save html when taken from the e-mail template html_copy = fields.Html( compute='_compute_sendgrid_view', inverse='_inverse_html_copy') + # Trick to display another widget when using Sendgrid + html_unframe = fields.Html(related='body_html') enable_unsubscribe = fields.Boolean() unsubscribe_text = fields.Char( default='If you would like to unsubscribe and stop receiving these ' @@ -88,41 +90,51 @@ class MassMailing(models.Model): @api.multi def send_mail(self): - self.ensure_one() - if self.email_template_id: + sendgrid = self.filtered('email_template_id') + emails = self.env['mail.mail'] + for mailing in sendgrid: # use E-mail Template - res_ids = self.get_recipients() + res_ids = mailing.get_recipients() 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 + lang = mailing.lang.code or self.env.context.get('lang', 'en_US') + mailing = mailing.with_context(lang=lang) + composer_values = mailing._send_mail_get_composer_values() + if mailing.reply_to_mode == 'email': + composer_values['reply_to'] = mailing.reply_to composer = self.env['mail.compose.message'].with_context( - lang=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({ + lang=lang, active_ids=res_ids) + emails += composer.mass_mailing_sendgrid(res_ids, composer_values) + mailing.write({ 'state': 'done', 'sent_date': fields.Datetime.now(), }) - return emails - else: - # Traditional sending - return super(MassMailing, self).send_mail() + # Traditional sending + super(MassMailing, self - sendgrid).send_mail() + return emails + + def _send_mail_get_composer_values(self): + """ + Get the values used for the mail.compose.message wizard that will + generate the e-mails of a mass mailing campaign. + :return: dictionary of mail.compose.message values + """ + template = self.email_template_id + author = self.mass_mailing_campaign_id.user_id.partner_id or \ + self.env.user.partner_id + return { + 'template_id': template.id, + 'composition_mode': 'mass_mail', + 'model': template.model, + 'author_id': author.id, + 'attachment_ids': [(4, attachment.id) for attachment in + self.attachment_ids], + 'email_from': self.email_from, + 'body': self.body_html, + 'subject': self.name, + 'record_name': False, + 'mass_mailing_id': self.id, + 'mailing_list_ids': [(4, l.id) for l in + self.contact_list_ids], + 'no_auto_thread': self.reply_to_mode != 'thread', + } diff --git a/mail_sendgrid_mass_mailing/tests/test_mass_mailing.py b/mail_sendgrid_mass_mailing/tests/test_mass_mailing.py index 2a217cc4..fb77a9c6 100644 --- a/mail_sendgrid_mass_mailing/tests/test_mass_mailing.py +++ b/mail_sendgrid_mass_mailing/tests/test_mass_mailing.py @@ -2,11 +2,11 @@ # © 2017 Emanuel Cino - # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import mock -from odoo.tests.common import TransactionCase +from odoo.tests.common import SavepointCase -mock_sendgrid_api_client = ('openerp.addons.mail_sendgrid.models.mail_mail' +mock_sendgrid_api_client = ('odoo.addons.mail_sendgrid.models.mail_mail' '.SendGridAPIClient') -mock_config = ('openerp.addons.mail_sendgrid.models.mail_mail.' +mock_config = ('odoo.addons.mail_sendgrid.models.mail_mail.' 'config') @@ -30,48 +30,70 @@ class FakeRequest(object): self.jsonrequest = [data] -class TestMailSendgrid(TransactionCase): - def setUp(self): - super(TestMailSendgrid, self).setUp() - self.sendgrid_template = self.env['sendgrid.template'].create({ +class TestMailSendgrid(SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestMailSendgrid, cls).setUpClass() + cls.sendgrid_template = cls.env['sendgrid.template'].create({ 'name': 'Test Template', 'remote_id': 'a74795d7-f926-4bad-8e7a-ae95fabd70fc', 'html_content': u'

Test Sendgrid

<%body%>{footer}' }) - self.mail_template = self.env['mail.template'].create({ + cls.mail_template = cls.env['mail.template'].create({ 'name': 'Test Template', - 'model_id': self.env.ref('base.model_res_partner').id, + 'model_id': cls.env.ref('base.model_res_partner').id, 'subject': 'Test e-mail', 'body_html': u'Dear ${object.name}, hello!', 'sendgrid_template_ids': [ (0, 0, {'lang': 'en_US', 'sendgrid_template_id': - self.sendgrid_template.id})] + cls.sendgrid_template.id})] }) - self.recipient = self.env.ref('base.partner_demo') - self.mass_mailing = self.env['mail.mass_mailing'].create({ + cls.recipient = cls.env.ref('base.partner_demo') + cls.mass_mailing = cls.env['mail.mass_mailing'].create({ 'email_from': 'admin@yourcompany.example.com', 'name': 'Test Mass Mailing Sendgrid', 'mailing_model': 'res.partner', - 'mailing_domain': "[('id', '=', %d)]" % self.recipient.id, - 'email_template_id': self.mail_template.id, + 'mailing_domain': "[('id', '=', %d)]" % cls.recipient.id, + 'email_template_id': cls.mail_template.id, 'body_html': u'Dear ${object.name}, hello!', - 'reply_to_mode': 'thread', - }) - self.timestamp = u'1471021089' - self.event = { - 'timestamp': self.timestamp, + 'reply_to_mode': 'email', + 'enable_unsubscribe': True, + 'unsubscribe_tag': '[unsub]' + }).with_context(lang='en_US', test_mode=True) + cls.timestamp = u'1471021089' + cls.event = { + 'timestamp': cls.timestamp, 'sg_event_id': u"f_JoKtrLQaOXUc4thXgROg", - 'email': self.recipient.email, - 'odoo_db': self.env.cr.dbname, + 'email': cls.recipient.email, + 'odoo_db': cls.env.cr.dbname, 'odoo_id': u'' } - self.metadata = { + cls.metadata = { 'ip': '127.0.0.1', 'user_agent': False, 'os_family': False, 'ua_family': False, } - self.request = FakeRequest(self.event) + cls.request = FakeRequest(cls.event) + + def test_sendgrid_preview(self): + """ + Test the preview field is getting the Sendgrid template + """ + self.mass_mailing.html_copy = self.mass_mailing.body_html + preview = self.mass_mailing.body_sendgrid + self.assertIn(u'

Test Sendgrid

', preview) + self.assertIn('hello!', preview) + + def test_change_language(self): + """ + Test changing the language is changing the domain + """ + domain = self.mass_mailing.mailing_domain + self.mass_mailing.lang = self.env['res.lang'].search([], limit=1) + self.mass_mailing.onchange_lang() + self.assertTrue(len(self.mass_mailing.mailing_domain) > len(domain)) @mock.patch(mock_sendgrid_api_client) @mock.patch(mock_config) @@ -84,7 +106,20 @@ class TestMailSendgrid(TransactionCase): 'mail_sendgrid.send_method', 'sendgrid') mock_sendgrid.return_value = FakeClient() m_config.get.return_value = 'we4iorujeriu' + + # Test campaign + self.mass_mailing.action_test_mailing() + self.env['mail.mass_mailing.test'].create({ + 'mass_mailing_id': self.mass_mailing.id, + 'email_to': 'test@sendgrid.com' + }).with_context(lang='en_US', test_mode=True).send_mail_test() + self.assertTrue(mock_sendgrid.called) + mock_sendgrid.reset_mock() + + # Send campaign emails = self.mass_mailing.send_mail() + # Dont delete emails sent + emails.write({'auto_delete': False}) self.assertEqual(len(emails), 1) self.assertEqual(emails.state, 'outgoing') self.assertEqual(emails.sendgrid_template_id.id, @@ -98,7 +133,7 @@ class TestMailSendgrid(TransactionCase): self.assertFalse(mail_tracking.state) stats = self.mass_mailing.statistics_ids self.assertEqual(len(stats), 1) - self.assertFalse(stats.sent) + self.assertTrue(stats.sent) # Test delivered self.event.update({ @@ -113,11 +148,26 @@ class TestMailSendgrid(TransactionCase): 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.assertIn('delivered', events.mapped('event_type')) + self.assertIn('click', events.mapped('event_type')) self.assertEqual(stats.state, 'sent') + + # Test reject + self.event.update({ + 'event': 'dropped', + }) + self.env['mail.tracking.email'].event_process( + self.request, self.event, self.metadata) + self.assertEqual(stats.state, 'exception') + + @classmethod + def tearDownClass(cls): + cls.env['ir.config_parameter'].set_param( + 'mail_sendgrid.send_method', 'traditional') + super(TestMailSendgrid, cls).tearDownClass() diff --git a/mail_sendgrid_mass_mailing/views/mass_mailing_view.xml b/mail_sendgrid_mass_mailing/views/mass_mailing_view.xml index e20a2d0f..5a144187 100644 --- a/mail_sendgrid_mass_mailing/views/mass_mailing_view.xml +++ b/mail_sendgrid_mass_mailing/views/mass_mailing_view.xml @@ -6,6 +6,12 @@ mail.mass_mailing + + {'invisible': [('email_template_id', '!=', False)]} + + + +