From cc2f9dcdcd7246ad8a62599b52f2b03eb280d9b5 Mon Sep 17 00:00:00 2001 From: Antonio Espinosa Date: Fri, 15 Jul 2016 13:41:49 +0200 Subject: [PATCH] [ADD] mail_tracking_mailgun --- mail_tracking_mailgun/README.rst | 86 ++++++ mail_tracking_mailgun/__init__.py | 5 + mail_tracking_mailgun/__openerp__.py | 18 ++ mail_tracking_mailgun/models/__init__.py | 6 + .../models/ir_mail_server.py | 23 ++ .../models/mail_tracking_email.py | 198 ++++++++++++ .../static/description/icon.png | Bin 0 -> 6485 bytes .../static/description/icon.svg | 6 + mail_tracking_mailgun/tests/__init__.py | 5 + mail_tracking_mailgun/tests/test_mailgun.py | 281 ++++++++++++++++++ 10 files changed, 628 insertions(+) create mode 100644 mail_tracking_mailgun/README.rst create mode 100644 mail_tracking_mailgun/__init__.py create mode 100644 mail_tracking_mailgun/__openerp__.py create mode 100644 mail_tracking_mailgun/models/__init__.py create mode 100644 mail_tracking_mailgun/models/ir_mail_server.py create mode 100644 mail_tracking_mailgun/models/mail_tracking_email.py create mode 100644 mail_tracking_mailgun/static/description/icon.png create mode 100644 mail_tracking_mailgun/static/description/icon.svg create mode 100644 mail_tracking_mailgun/tests/__init__.py create mode 100644 mail_tracking_mailgun/tests/test_mailgun.py diff --git a/mail_tracking_mailgun/README.rst b/mail_tracking_mailgun/README.rst new file mode 100644 index 00000000..880d07e2 --- /dev/null +++ b/mail_tracking_mailgun/README.rst @@ -0,0 +1,86 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +========================= +Mail tracking for Mailgun +========================= + +This module integrates mail_tracking events with Mailgun webhooks. + +Mailgun (https://www.mailgun.com/) is a service that provides an e-mail +sending infrastructure through an SMTP server or via API. You can also +query that API for seeing statistics of your sent e-mails, or provide +hooks that processes the status changes in real time, which is the +function used here. + +Configuration +============= + +You must configure Mailgun webhooks in order to receive mail events: + +1. Got a Mailgun account and validate your sending domain. +2. Go to Webhook tab and configure the below URL for each event: + +.. code:: html + + https:///mail/tracking/all/ + +Replace '' with your Odoo install domain name +and '' with your database name. + +In order to validate Mailgun webhooks you have to save Mailgun api_key in +a system parameter named 'mailgun.apikey'. You can find Mailgun api_key in your +validated sending domain. + +Usage +===== + +In your mail tracking status screens (explained on module *mail_tracking*), you will +see a more accurate information, like the 'Received' or 'Bounced' status, which are +not usually detected by normal SMTP servers. + +.. 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/8.0 + +Known issues / Roadmap +====================== + +* There's no support for more than one Mailgun mail server. + +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 +------ + +* Mailgun logo: `SVG Icon `_. + +Contributors +------------ + +* Antonio Espinosa + +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 https://odoo-community.org. diff --git a/mail_tracking_mailgun/__init__.py b/mail_tracking_mailgun/__init__.py new file mode 100644 index 00000000..5935294f --- /dev/null +++ b/mail_tracking_mailgun/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/mail_tracking_mailgun/__openerp__.py b/mail_tracking_mailgun/__openerp__.py new file mode 100644 index 00000000..e0b0d480 --- /dev/null +++ b/mail_tracking_mailgun/__openerp__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Antonio Espinosa +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Mail tracking for Mailgun", + "summary": "Mail tracking and Mailgun webhooks integration", + "version": "8.0.1.0.0", + "category": "Social Network", + "website": "https://odoo-community.org/", + "author": "Tecnativa, " + "Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "mail_tracking", + ], +} diff --git a/mail_tracking_mailgun/models/__init__.py b/mail_tracking_mailgun/models/__init__.py new file mode 100644 index 00000000..64dc6c20 --- /dev/null +++ b/mail_tracking_mailgun/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Antonio Espinosa - +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import ir_mail_server +from . import mail_tracking_email diff --git a/mail_tracking_mailgun/models/ir_mail_server.py b/mail_tracking_mailgun/models/ir_mail_server.py new file mode 100644 index 00000000..2ebb4ce6 --- /dev/null +++ b/mail_tracking_mailgun/models/ir_mail_server.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Antonio Espinosa - +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import json +from openerp import models + + +class IrMailServer(models.Model): + _inherit = "ir.mail_server" + + def _tracking_headers_add(self, tracking_email_id, headers): + headers = super(IrMailServer, self)._tracking_headers_add( + tracking_email_id, headers) + headers = headers or {} + metadata = { + # NOTE: We can not use 'self.env.cr.dbname' because self is + # ir.mail_server object in old API (osv.osv) + 'odoo_db': self.pool.db_name, + 'tracking_email_id': tracking_email_id, + } + headers['X-Mailgun-Variables'] = json.dumps(metadata) + return headers diff --git a/mail_tracking_mailgun/models/mail_tracking_email.py b/mail_tracking_mailgun/models/mail_tracking_email.py new file mode 100644 index 00000000..ebd7d658 --- /dev/null +++ b/mail_tracking_mailgun/models/mail_tracking_email.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Antonio Espinosa - +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import hashlib +import hmac +from datetime import datetime +from openerp import models, api, fields + +import logging +_logger = logging.getLogger(__name__) + + +class MailTrackingEmail(models.Model): + _inherit = "mail.tracking.email" + + def _country_search(self, country_code): + country = False + if country_code: + country = self.env['res.country'].search([ + ('code', '=', country_code.upper()), + ]) + if country: + return country.id + return False + + @property + def _mailgun_mandatory_fields(self): + return ('event', 'timestamp', 'token', 'signature', + 'tracking_email_id', 'odoo_db') + + @property + def _mailgun_event_type_mapping(self): + return { + # Mailgun event type: tracking event type + 'delivered': 'delivered', + 'opened': 'open', + 'clicked': 'click', + 'unsubscribed': 'unsub', + 'complained': 'spam', + 'bounced': 'hard_bounce', + 'dropped': 'reject', + } + + @property + def _mailgun_supported_event_types(self): + return self._mailgun_event_type_mapping.keys() + + def _mailgun_event_type_verify(self, event): + event = event or {} + mailgun_event_type = event.get('event') + if mailgun_event_type not in self._mailgun_supported_event_types: + _logger.info("Mailgun: event type '%s' not supported", + mailgun_event_type) + return False + # OK, event type is valid + return True + + def _mailgun_signature(self, api_key, timestamp, token): + return hmac.new( + key=str(api_key), + msg='{}{}'.format(str(timestamp), str(token)), + digestmod=hashlib.sha256).hexdigest() + + def _mailgun_signature_verify(self, event): + event = event or {} + api_key = self.env['ir.config_parameter'].get_param('mailgun.apikey') + if not api_key: + _logger.info("No Mailgun api key configured. " + "Please add 'mailgun.apikey' to System parameters " + "to enable Mailgun authentication webhoook requests. " + "More info at: " + "https://documentation.mailgun.com/user_manual.html" + "#webhooks") + else: + timestamp = event.get('timestamp') + token = event.get('token') + signature = event.get('signature') + event_digest = self._mailgun_signature(api_key, timestamp, token) + if signature != event_digest: + _logger.error("Mailgun: Invalid signature '%s' != '%s'", + signature, event_digest) + return False + # OK, signature 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.info("Mailgun: Database '%s' is not the current database", + odoo_db) + return False + # OK, DB is current + return True + + def _mailgun_metadata(self, mailgun_event_type, event, metadata): + # Get Mailgun 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 + mapping = { + 'recipient': 'recipient', + 'ip': 'ip', + 'user_agent': 'user-agent', + 'os_family': 'client-os', + 'ua_family': 'client-name', + 'ua_type': 'client-type', + 'url': 'url', + } + for k, v in mapping.iteritems(): + if event.get(v, False): + metadata[k] = event[v] + # Special field mapping + metadata.update({ + 'mobile': event.get('device-type') in ('mobile', 'tablet'), + 'user_country_id': self._country_search( + event.get('country', False)), + }) + # Mapping for special events + if mailgun_event_type == 'bounced': + metadata.update({ + 'error_type': event.get('code', False), + 'error_description': event.get('error', False), + 'error_details': event.get('notification', False), + }) + elif mailgun_event_type == 'dropped': + metadata.update({ + 'error_type': event.get('reason', False), + 'error_description': event.get('code', False), + 'error_details': event.get('description', False), + }) + elif mailgun_event_type == 'complained': + metadata.update({ + 'error_type': 'spam', + 'error_description': + "Recipient '%s' mark this email as spam" % + event.get('recipient', False), + }) + return metadata + + def _mailgun_tracking_get(self, event): + tracking = False + tracking_email_id = event.get('tracking_email_id', False) + if tracking_email_id and tracking_email_id.isdigit(): + tracking = self.search([('id', '=', tracking_email_id)], limit=1) + return tracking + + def _event_is_from_mailgun(self, event): + event = event or {} + return all([k in event for k in self._mailgun_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) + if res == 'NONE' and self._event_is_from_mailgun(post): + if not self._mailgun_signature_verify(post): + res = 'ERROR: Signature' + elif not self._mailgun_event_type_verify(post): + res = 'ERROR: Event type not supported' + elif not self._db_verify(post): + res = 'ERROR: Invalid DB' + else: + res = 'OK' + if res == 'OK': + mailgun_event_type = post.get('event') + mapped_event_type = self._mailgun_event_type_mapping.get( + mailgun_event_type) or event_type + if not mapped_event_type: # pragma: no cover + res = 'ERROR: Bad event' + tracking = self._mailgun_tracking_get(post) + if not tracking: + res = 'ERROR: Tracking not found' + if res == 'OK': + # Complete metadata with mailgun event info + metadata = self._mailgun_metadata( + mailgun_event_type, post, metadata) + # Create event + tracking.event_create(mapped_event_type, metadata) + if res != 'NONE': + if event_type: + _logger.info( + "Mailgun: event '%s' process '%s'", event_type, res) + else: + _logger.info("Mailgun: event process '%s'", res) + return res diff --git a/mail_tracking_mailgun/static/description/icon.png b/mail_tracking_mailgun/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..22dc3f877c7c9046f4586b26c23404666151a034 GIT binary patch literal 6485 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 vgXkcm@}rKk$~)1CPIRIZo#;d-w#WYiZr3%g(rz + + + + + diff --git a/mail_tracking_mailgun/tests/__init__.py b/mail_tracking_mailgun/tests/__init__.py new file mode 100644 index 00000000..d7169b5d --- /dev/null +++ b/mail_tracking_mailgun/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Antonio Espinosa - +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_mailgun diff --git a/mail_tracking_mailgun/tests/test_mailgun.py b/mail_tracking_mailgun/tests/test_mailgun.py new file mode 100644 index 00000000..f4ecf16c --- /dev/null +++ b/mail_tracking_mailgun/tests/test_mailgun.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Antonio Espinosa - +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp.tests.common import TransactionCase + + +class TestMailgun(TransactionCase): + def mail_send(self): + mail = self.env['mail.mail'].create({ + 'subject': 'Test subject', + 'email_from': 'from@example.com', + 'email_to': self.recipient, + 'body_html': '

This is a test message

', + }) + mail.send() + # Search tracking created + tracking_email = self.env['mail.tracking.email'].search([ + ('mail_id', '=', mail.id), + ]) + return mail, tracking_email + + def setUp(self): + super(TestMailgun, self).setUp() + self.recipient = u'to@example.com' + self.mail, self.tracking_email = self.mail_send() + self.api_key = u'key-12345678901234567890123456789012' + self.token = u'f1349299097a51b9a7d886fcb5c2735b426ba200ada6e9e149' + self.timestamp = u'1471021089' + self.signature = ('4fb6d4dbbe10ce5d620265dcd7a3c0b8' + 'ca0dede1433103891bc1ae4086e9d5b2') + self.env['ir.config_parameter'].set_param( + 'mailgun.apikey', self.api_key) + self.event = { + 'Message-Id': u'', + 'X-Mailgun-Sid': u'WyIwNjgxZSIsICJ0b0BleGFtcGxlLmNvbSIsICI3MG' + 'I0MWYiXQ==', + 'token': self.token, + 'timestamp': self.timestamp, + 'signature': self.signature, + 'domain': u'example.com', + 'message-headers': u'[]', + 'recipient': self.recipient, + 'odoo_db': self.env.cr.dbname, + 'tracking_email_id': u'%s' % self.tracking_email.id + } + self.metadata = { + 'ip': '127.0.0.1', + 'user_agent': False, + 'os_family': False, + 'ua_family': False, + } + + def event_search(self, event_type): + event = self.env['mail.tracking.event'].search([ + ('tracking_email_id', '=', self.tracking_email.id), + ('event_type', '=', event_type), + ]) + self.assertTrue(event) + return event + + def test_no_api_key(self): + self.env['ir.config_parameter'].set_param('mailgun.apikey', '') + self.test_event_delivered() + + def test_bad_signature(self): + self.event.update({ + 'event': u'delivered', + 'signature': u'bad_signature', + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata) + self.assertEqual('ERROR: Signature', response) + + def test_bad_event_type(self): + self.event.update({ + 'event': u'bad_event', + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata) + self.assertEqual('ERROR: Event type not supported', response) + + def test_bad_db(self): + self.event.update({ + 'event': u'delivered', + 'odoo_db': u'bad_db', + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata) + self.assertEqual('ERROR: Invalid DB', response) + + def test_bad_ts(self): + timestamp = u'7a' # Now time will be used instead + signature = ('06cc05680f6e8110e59b41152b2d1c0f' + '1045d755ef2880ff922344325c89a6d4') + self.event.update({ + 'event': u'delivered', + 'timestamp': timestamp, + 'signature': signature, + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata) + self.assertEqual('OK', response) + + def test_tracking_not_found(self): + self.event.update({ + 'event': u'delivered', + 'tracking_email_id': u'bad_id', + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata) + self.assertEqual('ERROR: Tracking not found', response) + + # https://documentation.mailgun.com/user_manual.html#tracking-deliveries + def test_event_delivered(self): + self.event.update({ + 'event': u'delivered', + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata) + self.assertEqual('OK', response) + event = self.event_search('delivered') + self.assertEqual(event.timestamp, float(self.timestamp)) + self.assertEqual(event.recipient, self.recipient) + + # https://documentation.mailgun.com/user_manual.html#tracking-opens + def test_event_opened(self): + ip = u'127.0.0.1' + user_agent = u'Odoo Test/8.0 Gecko Firefox/11.0' + os_family = u'Linux' + ua_family = u'Firefox' + ua_type = u'browser' + self.event.update({ + 'event': u'opened', + 'city': u'Mountain View', + 'country': u'US', + 'region': u'CA', + 'client-name': ua_family, + 'client-os': os_family, + 'client-type': ua_type, + 'device-type': u'desktop', + 'ip': ip, + 'user-agent': user_agent, + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata) + self.assertEqual('OK', response) + event = self.event_search('open') + self.assertEqual(event.timestamp, float(self.timestamp)) + self.assertEqual(event.recipient, self.recipient) + self.assertEqual(event.ip, ip) + self.assertEqual(event.user_agent, user_agent) + self.assertEqual(event.os_family, os_family) + self.assertEqual(event.ua_family, ua_family) + self.assertEqual(event.ua_type, ua_type) + self.assertEqual(event.mobile, False) + self.assertEqual(event.user_country_id.code, 'US') + + # https://documentation.mailgun.com/user_manual.html#tracking-clicks + def test_event_clicked(self): + ip = u'127.0.0.1' + user_agent = u'Odoo Test/8.0 Gecko Firefox/11.0' + os_family = u'Linux' + ua_family = u'Firefox' + ua_type = u'browser' + url = u'https://odoo-community.org' + self.event.update({ + 'event': u'clicked', + 'city': u'Mountain View', + 'country': u'US', + 'region': u'CA', + 'client-name': ua_family, + 'client-os': os_family, + 'client-type': ua_type, + 'device-type': u'tablet', + 'ip': ip, + 'user-agent': user_agent, + 'url': url, + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata, event_type='click') + self.assertEqual('OK', response) + event = self.event_search('click') + self.assertEqual(event.timestamp, float(self.timestamp)) + self.assertEqual(event.recipient, self.recipient) + self.assertEqual(event.ip, ip) + self.assertEqual(event.user_agent, user_agent) + self.assertEqual(event.os_family, os_family) + self.assertEqual(event.ua_family, ua_family) + self.assertEqual(event.ua_type, ua_type) + self.assertEqual(event.mobile, True) + self.assertEqual(event.url, url) + + # https://documentation.mailgun.com/user_manual.html#tracking-unsubscribes + def test_event_unsubscribed(self): + ip = u'127.0.0.1' + user_agent = u'Odoo Test/8.0 Gecko Firefox/11.0' + os_family = u'Linux' + ua_family = u'Firefox' + ua_type = u'browser' + self.event.update({ + 'event': u'unsubscribed', + 'city': u'Mountain View', + 'country': u'US', + 'region': u'CA', + 'client-name': ua_family, + 'client-os': os_family, + 'client-type': ua_type, + 'device-type': u'mobile', + 'ip': ip, + 'user-agent': user_agent, + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata) + self.assertEqual('OK', response) + event = self.event_search('unsub') + self.assertEqual(event.timestamp, float(self.timestamp)) + self.assertEqual(event.recipient, self.recipient) + self.assertEqual(event.ip, ip) + self.assertEqual(event.user_agent, user_agent) + self.assertEqual(event.os_family, os_family) + self.assertEqual(event.ua_family, ua_family) + self.assertEqual(event.ua_type, ua_type) + self.assertEqual(event.mobile, True) + + # https://documentation.mailgun.com/ + # user_manual.html#tracking-spam-complaints + def test_event_complained(self): + self.event.update({ + 'event': u'complained', + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata) + self.assertEqual('OK', response) + event = self.event_search('spam') + self.assertEqual(event.timestamp, float(self.timestamp)) + self.assertEqual(event.recipient, self.recipient) + self.assertEqual(event.error_type, 'spam') + + # https://documentation.mailgun.com/user_manual.html#tracking-bounces + def test_event_bounced(self): + code = u'550' + error = (u"5.1.1 The email account does not exist.\n" + "5.1.1 double-checking the recipient's email address") + notification = u"Please, check recipient's email address" + self.event.update({ + 'event': u'bounced', + 'code': code, + 'error': error, + 'notification': notification, + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata) + self.assertEqual('OK', response) + event = self.event_search('hard_bounce') + self.assertEqual(event.timestamp, float(self.timestamp)) + self.assertEqual(event.recipient, self.recipient) + self.assertEqual(event.error_type, code) + self.assertEqual(event.error_description, error) + self.assertEqual(event.error_details, notification) + + # https://documentation.mailgun.com/user_manual.html#tracking-failures + def test_event_dropped(self): + reason = u'hardfail' + code = u'605' + description = u'Not delivering to previously bounced address' + self.event.update({ + 'event': u'dropped', + 'reason': reason, + 'code': code, + 'description': description, + }) + response = self.env['mail.tracking.email'].event_process( + None, self.event, self.metadata) + self.assertEqual('OK', response) + event = self.event_search('reject') + self.assertEqual(event.timestamp, float(self.timestamp)) + self.assertEqual(event.recipient, self.recipient) + self.assertEqual(event.error_type, reason) + self.assertEqual(event.error_description, code) + self.assertEqual(event.error_details, description)