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 00000000..5567773a Binary files /dev/null and b/mail_sendgrid_mass_mailing/static/description/icon.png differ 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