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)]} + + + +