Browse Source

Add Sendgrid Mass Mailing from v9

pull/185/head
Emanuel Cino 7 years ago
parent
commit
e0b6ac93e3
  1. 61
      mail_sendgrid_mass_mailing/README.rst
  2. 13
      mail_sendgrid_mass_mailing/__init__.py
  3. 47
      mail_sendgrid_mass_mailing/__openerp__.py
  4. 14
      mail_sendgrid_mass_mailing/models/__init__.py
  5. 39
      mail_sendgrid_mass_mailing/models/email_tracking.py
  6. 66
      mail_sendgrid_mass_mailing/models/mail_mail.py
  7. 135
      mail_sendgrid_mass_mailing/models/mass_mailing.py
  8. BIN
      mail_sendgrid_mass_mailing/static/description/icon.png
  9. 10
      mail_sendgrid_mass_mailing/static/description/icon.svg
  10. 12
      mail_sendgrid_mass_mailing/tests/__init__.py
  11. 123
      mail_sendgrid_mass_mailing/tests/test_mass_mailing.py
  12. 31
      mail_sendgrid_mass_mailing/views/mass_mailing_view.xml
  13. 13
      mail_sendgrid_mass_mailing/wizards/__init__.py
  14. 40
      mail_sendgrid_mass_mailing/wizards/mail_compose_message.py
  15. 55
      mail_sendgrid_mass_mailing/wizards/test_mailing.py

61
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 <https://github.com/OCA/social/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
`here <https://github.com/OCA/social/issues/new?body=module:%20mail_sendgrid_mass_mailing%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Credits
=======
Contributors
------------
* Emanuel Cino <ecino@compassion.ch>
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.

13
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

47
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 <ecino@compassion.ch>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': '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'],
},
}

14
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

39
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 <ecino@compassion.ch>
#
# 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

66
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 <ecino@compassion.ch>
#
# 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

135
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()

BIN
mail_sendgrid_mass_mailing/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 3.1 KiB

10
mail_sendgrid_mass_mailing/static/description/icon.svg

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<polygon fill="#9DE1F3" points="85.3335 85.333 0.0005 85.333 0.0005 170.666 0.0005 256 85.3335 256 170.6665 256 170.6665 170.666 170.6665 85.333"></polygon>
<polygon fill="#27B4E1" points="85.3335 0.0004 85.3335 85.3334 85.3335 170.6664 170.6665 170.6664 255.9995 170.6664 255.9995 0.0004"></polygon>
<polygon fill="#1A82E2" points="0 256 85.333 256 85.333 170.667 0 170.667"></polygon>
<polygon fill="#1A82E2" points="170.667 85.333 256 85.333 256 0 170.667 0"></polygon>
<polygon fill="#239FD7" points="85.334 170.667 170.667 170.667 170.667 85.334 85.334 85.334"></polygon>
</g>
</svg>

12
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

123
mail_sendgrid_mass_mailing/tests/test_mass_mailing.py

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
# © 2017 Emanuel Cino - <ecino@compassion.ch>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import mock
from openerp.tests.common import TransactionCase
mock_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'<h1>Test Sendgrid</h1><%body%>{footer}'
})
self.mail_template = self.env['mail.template'].create({
'name': 'Test Template',
'model_id': self.env.ref('base.model_res_partner').id,
'subject': 'Test e-mail',
'body_html': u'Dear ${object.name}, hello!',
'sendgrid_template_ids': [
(0, 0, {'lang': 'en_US', 'sendgrid_template_id':
self.sendgrid_template.id})]
})
self.recipient = self.env.ref('base.partner_demo')
self.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'<xxx.xxx.xxx-openerp-xxx-res.partner@test_db>'
}
self.metadata = {
'ip': '127.0.0.1',
'user_agent': False,
'os_family': False,
'ua_family': False,
}
self.request = FakeRequest(self.event)
@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')

31
mail_sendgrid_mass_mailing/views/mass_mailing_view.xml

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- Form view -->
<record id="view_email_template_sendgrid_form" model="ir.ui.view">
<field name="name">mass.mailing.sendgrid.form</field>
<field name="model">mail.mass_mailing</field>
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='mailing_model']/.." position="after">
<field name="email_template_id" domain="[('model', '=', mailing_model), ('sendgrid_template_ids', '!=', False)]"/>
<field name="lang" attrs="{'invisible': [('email_template_id', '=', False)]}"/>
</xpath>
<xpath expr="//notebook/page[1]" position="after">
<page string="Sendgrid Preview" attrs="{'invisible': [('email_template_id', '=', False)]}">
<field name="html_copy" invisible="1"/>
<field name="body_sendgrid" widget="html"/>
</page>
</xpath>
<xpath expr="//notebook/page/group[1]">
<group string="Sendgrid Unsubscribe">
<field name="enable_unsubscribe"/>
<field name="unsubscribe_text" attrs="{'invisible': [('enable_unsubscribe', '=', False)]}"/>
<field name="unsubscribe_tag" attrs="{'invisible': [('enable_unsubscribe', '=', False)]}"/>
</group>
</xpath>
</field>
</record>
</data>
</openerp>

13
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

40
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 <ecino@compassion.ch>
#
# 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

55
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 <ecino@compassion.ch>
#
# 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
Loading…
Cancel
Save