diff --git a/mail_mandrill/README.rst b/mail_mandrill/README.rst new file mode 100644 index 00000000..d9a26838 --- /dev/null +++ b/mail_mandrill/README.rst @@ -0,0 +1,110 @@ +.. 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 + +================================ +Mandrill mail events integration +================================ + +This module logs Mandrill email messages events. + + +Configuration +============= + +To configure this module, you need to: + +* Define an STMP server in Odoo at Settings > Technical > Email > Outgoing Mail Servers + using Mandrill credentials from Mandrill settings panel (Settings > SMTP & API Credentials) +* Define a webhook in Mandrill settings panel (Settings > Webhooks) with + several triggers (Message Is Sent, Message Is Delayed, ...) and 'Post to URL' + like https://your_odoodomain.com/mandrill/event +* Copy Webhook key and paste in your Odoo configuration file, in 'options' + section, using 'mandrill_webhook_key' variable. This is optional, but + recommended because it is used to validate Mandrill POST requests + + +Usage +===== + +When any email message is sent via Mandrill SMTP server, Odoo will add +some metadata to email (Odoo DB, Odoo Model and Odoo Model record ID) using an +special SMTP header (X-MC-Metadata). More info at `Mandrill doc: Using Custom Message Metadata `_ + +Then when an event occurs related with that message (sent, open, click, bounce, ...) +Mandrill will trigger webhook configured and Odoo will log the message and the event. + +In 'Setting > Technical > Email > Mandrill emails' you can see all messages sent +using Mandrill. When clicking in one of them you'll see message details and events +related with it + +In 'Setting > Technical > Email > Mandrill events' you can see all Mandrill events +received + +.. 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 + +For further information, please visit: + +* https://www.odoo.com/forum/help-1 + + +Known issues / Roadmap +====================== + +* Define actions associated with events like open/click or bounce + (via configuration or via other addon) +* Create another addon 'mass_mailing_mandrill' (inheriting from mass_mailing + and this addon) to process bounces like mass_mailing addon does + + +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 `_. + + +License +======= + +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 . + + +Credits +======= + +Contributors +------------ + +* Rafael Blasco +* 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 http://odoo-community.org. diff --git a/mail_mandrill/__init__.py b/mail_mandrill/__init__.py new file mode 100644 index 00000000..134fcd6c --- /dev/null +++ b/mail_mandrill/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +from . import models +from . import controllers diff --git a/mail_mandrill/__openerp__.py b/mail_mandrill/__openerp__.py new file mode 100644 index 00000000..f6617a73 --- /dev/null +++ b/mail_mandrill/__openerp__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +{ + 'name': "Mandrill mail events integration", + 'category': 'Social Network', + 'version': '8.0.1.0.0', + 'depends': [ + 'mail', + ], + 'external_dependencies': {}, + 'data': [ + 'security/ir.model.access.csv', + 'views/mail_mandrill_message_view.xml', + 'views/mail_mandrill_event_view.xml', + ], + 'author': 'Antiun IngenierĂ­a S.L., ' + 'Odoo Community Association (OCA)', + 'website': 'http://www.antiun.com', + 'license': 'AGPL-3', + 'demo': [], + 'test': [], + 'installable': True, +} diff --git a/mail_mandrill/controllers/__init__.py b/mail_mandrill/controllers/__init__.py new file mode 100644 index 00000000..b68a7c70 --- /dev/null +++ b/mail_mandrill/controllers/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +from . import main diff --git a/mail_mandrill/controllers/main.py b/mail_mandrill/controllers/main.py new file mode 100644 index 00000000..e380b93a --- /dev/null +++ b/mail_mandrill/controllers/main.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +import json +from hashlib import sha1 +import hmac +import logging +from psycopg2 import OperationalError + +import openerp +from openerp import api, http, SUPERUSER_ID, tools +from openerp.http import request + +_logger = logging.getLogger(__name__) + + +class MailController(http.Controller): + + def _mandrill_validation(self, **kw): + """ + Validate Mandrill POST reques using + https://mandrill.zendesk.com/hc/en-us/articles/ +205583257-Authenticating-webhook-requests + """ + headers = request.httprequest.headers + signature = headers.get('X-Mandrill-Signature', False) + key = tools.config.options.get('mandrill_webhook_key', False) + if not key: + _logger.info("No Mandrill validation key configured. " + "Please add 'mandrill_webhook_key' to [options] " + "section in odoo configuration file to enable " + "Mandrill authentication webhoook requests. " + "More info at: " + "https://mandrill.zendesk.com/hc/en-us/articles/" + "205583257-Authenticating-webhook-requests") + return True + if not signature: + return False + url = tools.config.options.get('mandrill_webhook_url', False) + if not url: + url = request.httprequest.url_root.rstrip('/') + '/mandrill/event' + data = url + kw_keys = kw.keys() + if kw_keys: + kw_keys.sort() + for kw_key in kw_keys: + data += kw_key + kw.get(kw_key) + hashed = hmac.new(key, data, sha1) + hash_text = hashed.digest().encode("base64").rstrip('\n') + if hash_text == signature: + return True + _logger.info("HASH[%s] != SIGNATURE[%s]" % (hash_text, signature)) + return False + + def _event_process(self, event): + message_id = event.get('_id') + event_type = event.get('event') + message = event.get('msg') + if not (message_id and event_type and message): + return False + + info = "%s event for Message ID '%s'" % (event_type, message_id) + metadata = message.get('metadata') + db = None + if metadata: + db = metadata.get('odoo_db', None) + + # Check database selected by mandrill event + if not db: + _logger.info('%s: No DB selected', info) + return False + try: + registry = openerp.registry(db) + except OperationalError: + _logger.info("%s: Selected BD '%s' not found", info, db) + return False + except: + _logger.info("%s: Selected BD '%s' connection error", info, db) + return False + + # Database has been selected, process event + with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + res = env['mail.mandrill.message'].process( + message_id, event_type, event) + if res: + _logger.info('%s: OK', info) + else: + _logger.info('%s: FAILED', info) + return res + + @http.route('/mandrill/event', type='http', auth='none') + def event(self, **kw): + """ + End-point to receive Mandrill event + Configuration in Mandrill app > Settings > Webhooks + (https://mandrillapp.com/settings/webhooks) + Add a webhook, selecting this type of events: + - Message Is Sent + - Message Is Bounced + - Message Is Opened + - Message Is Marked As Spam + - Message Is Rejected + - Message Is Delayed + - Message Is Soft-Bounced + - Message Is Clicked + - Message Recipient Unsubscribes + and setting this Post to URL: + https://your_odoodomain.com/mandrill/event + """ + if not self._mandrill_validation(**kw): + _logger.info('Validation error, ignoring this request') + return 'NO_AUTH' + events = [] + try: + events = json.loads(kw.get('mandrill_events', '[]')) + except: + pass + if not events: + return 'NO_EVENTS' + res = [] + for event in events: + res.append(self._event_process(event)) + msg = 'ALL_EVENTS_FAILED' + if all(res): + msg = 'OK' + elif any(res): + msg = 'SOME_EVENTS_FAILED' + return msg diff --git a/mail_mandrill/i18n/.gitkeep b/mail_mandrill/i18n/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/mail_mandrill/models/__init__.py b/mail_mandrill/models/__init__.py new file mode 100644 index 00000000..897ba6f8 --- /dev/null +++ b/mail_mandrill/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +from . import mail_mail +from . import mail_mandrill_message +from . import mail_mandrill_event diff --git a/mail_mandrill/models/mail_mail.py b/mail_mandrill/models/mail_mail.py new file mode 100644 index 00000000..f95e92a0 --- /dev/null +++ b/mail_mandrill/models/mail_mail.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +import json +import threading + +from openerp import models, api + + +class MailMail(models.Model): + _inherit = 'mail.mail' + + def _mandrill_headers_add(self): + for mail in self.sudo(): + headers = {} + if mail.headers: + try: + headers.update(eval(mail.headers)) + except Exception: + pass + + metadata = { + 'odoo_db': getattr(threading.currentThread(), 'dbname', None), + 'odoo_model': mail.model, + 'odoo_id': mail.res_id, + } + headers['X-MC-Metadata'] = json.dumps(metadata) + mail.headers = repr(headers) + return True + + @api.multi + def send(self, auto_commit=False, raise_exception=False): + self._mandrill_headers_add() + super(MailMail, self).send( + auto_commit=auto_commit, + raise_exception=raise_exception) + return True diff --git a/mail_mandrill/models/mail_mandrill_event.py b/mail_mandrill/models/mail_mandrill_event.py new file mode 100644 index 00000000..5065651b --- /dev/null +++ b/mail_mandrill/models/mail_mandrill_event.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +import datetime + +from openerp import models, fields, api + + +class MailMandrillEvent(models.Model): + _name = 'mail.mandrill.event' + _order = 'timestamp desc' + _rec_name = 'event_type' + + timestamp = fields.Integer(string='Mandrill UTC timestamp', readonly=True) + time = fields.Datetime(string='Mandrill time', readonly=True) + date = fields.Date(string='Mandrill date', readonly=True) + event_type = fields.Selection(string='Event type', selection=[ + ('send', 'Sent'), + ('deferral', 'Deferral'), + ('hard_bounce', 'Hard bounce'), + ('soft_bounce', 'Soft bounce'), + ('open', 'Opened'), + ('click', 'Clicked'), + ('spam', 'Spam'), + ('unsub', 'Unsubscribed'), + ('reject', 'Rejected'), + ], readonly=True) + url = fields.Char(string='Clicked URL', readonly=True) + ip = fields.Char(string='User IP', readonly=True) + user_agent = fields.Char(string='User agent', readonly=True) + mobile = fields.Boolean(string='Is mobile?', readonly=True) + os_family = fields.Char(string='Operating system family', readonly=True) + ua_family = fields.Char(string='User agent family', readonly=True) + ua_type = fields.Char(string='User agent type', readonly=True) + user_country_id = fields.Many2one(string='User country', readonly=True, + comodel_name='res.country') + message_id = fields.Many2one(string='Message', readonly=True, + comodel_name='mail.mandrill.message') + + def _country_search(self, country_code, state_name): + country = False + if country_code: + country = self.env['res.country'].search([ + ('code', 'ilike', country_code), + ]) + if not country and state_name: + state = self.env['res.country.state'].search([ + ('name', 'ilike', state_name), + ]) + if state: + country = state.country_id + + if country: + return country.id + return False + + def _process_bounce(self, message, event, event_type): + msg = event.get('msg') + bounce_type = msg.get('bounce_description', False) if msg else False + bounce_description = msg.get('diag', False) if msg else False + message.write({ + 'state': 'bounced', + 'bounce_type': bounce_type, + 'bounce_description': bounce_description, + }) + ts = event.get('ts', 0) + time = datetime.datetime.fromtimestamp(ts) + return { + 'message_id': message.id, + 'event_type': event_type, + 'timestamp': ts, + 'time': time.strftime('%Y-%m-%d %H:%M:%S') if ts else False, + 'date': time.strftime('%Y-%m-%d') if ts else False, + } + + def _process_status(self, message, event, event_type, state): + message.write({ + 'state': state, + }) + ts = event.get('ts', 0) + time = datetime.datetime.fromtimestamp(ts) + return { + 'message_id': message.id, + 'event_type': event_type, + 'timestamp': ts, + 'time': time.strftime('%Y-%m-%d %H:%M:%S') if ts else False, + 'date': time.strftime('%Y-%m-%d') if ts else False, + } + + def _process_action(self, message, event, event_type, state): + message.write({ + 'state': state, + }) + ts = event.get('ts', 0) + url = event.get('url', False) + ip = event.get('ip', False) + user_agent = event.get('user_agent', False) + os_family = False + ua_family = False + ua_type = False + mobile = False + country_code = False + state = False + location = event.get('location') + if location: + country_code = location.get('country_short', False) + state = location.get('region', False) + ua_parsed = event.get('user_agent_parsed') + if ua_parsed: + os_family = ua_parsed.get('os_family', False) + ua_family = ua_parsed.get('ua_family', False) + ua_type = ua_parsed.get('type', False) + mobile = ua_parsed.get('mobile', False) + country_id = self._country_search(country_code, state) + time = datetime.datetime.fromtimestamp(ts) + return { + 'message_id': message.id, + 'event_type': event_type, + 'timestamp': ts, + 'time': time.strftime('%Y-%m-%d %H:%M:%S') if ts else False, + 'date': time.strftime('%Y-%m-%d') if ts else False, + 'user_country_id': country_id, + 'ip': ip, + 'url': url, + 'mobile': mobile, + 'user_agent': user_agent, + 'os_family': os_family, + 'ua_family': ua_family, + 'ua_type': ua_type, + } + + @api.model + def process_send(self, message, event): + return self._process_status(message, event, 'send', 'sent') + + @api.model + def process_deferral(self, message, event): + return self._process_status(message, event, 'deferral', 'deferred') + + @api.model + def process_hard_bounce(self, message, event): + return self._process_bounce(message, event, 'hard_bounce') + + @api.model + def process_soft_bounce(self, message, event): + return self._process_bounce(message, event, 'soft_bounce') + + @api.model + def process_open(self, message, event): + return self._process_action(message, event, 'open', 'opened') + + @api.model + def process_click(self, message, event): + return self._process_action(message, event, 'click', 'opened') + + @api.model + def process_spam(self, message, event): + return self._process_status(message, event, 'spam', 'spam') + + @api.model + def process_unsub(self, message, event): + return self._process_status(message, event, 'unsub', 'unsub') + + @api.model + def process_reject(self, message, event): + return self._process_status(message, event, 'reject', 'rejected') diff --git a/mail_mandrill/models/mail_mandrill_message.py b/mail_mandrill/models/mail_mandrill_message.py new file mode 100644 index 00000000..377e494a --- /dev/null +++ b/mail_mandrill/models/mail_mandrill_message.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +import datetime +import json +import logging + +from openerp import models, fields, api + +_logger = logging.getLogger(__name__) + + +class MailMandrillMessage(models.Model): + _name = 'mail.mandrill.message' + _order = 'timestamp desc' + _rec_name = 'name' + + name = fields.Char(string='Subject', readonly=True) + mandrill_id = fields.Char(string='Mandrill message ID', required=True, + readonly=True) + timestamp = fields.Integer(string='Mandrill UTC timestamp', readonly=True) + time = fields.Datetime(string='Mandrill time', readonly=True) + date = fields.Date(string='Mandrill date', readonly=True) + recipient = fields.Char(string='Recipient email', readonly=True) + sender = fields.Char(string='Sender email', readonly=True) + state = fields.Selection([ + ('deferred', 'Deferred'), + ('sent', 'Sent'), + ('opened', 'Opened'), + ('rejected', 'Rejected'), + ('spam', 'Spam'), + ('unsub', 'Unsubscribed'), + ('bounced', 'Bounced'), + ('soft-bounced', 'Soft bounced'), + ], string='State', index=True, readonly=True, + help=" * The 'Sent' status indicates that message was succesfully " + "delivered to recipient Mail Exchange (MX) server.\n" + " * The 'Opened' status indicates that message was opened or " + "clicked by recipient.\n" + " * The 'Rejected' status indicates that recipient email " + "address is blacklisted by Mandrill. It is recomended to " + "delete this email address.\n" + " * The 'Spam' status indicates that Mandrill consider this " + "message as spam.\n" + " * The 'Unsubscribed' status indicates that recipient has " + "requested to be unsubscribed from this message.\n" + " * The 'Bounced' status indicates that message was bounced " + "by recipient Mail Exchange (MX) server.\n" + " * The 'Soft bounced' status indicates that message was soft " + "bounced by recipient Mail Exchange (MX) server.\n") + bounce_type = fields.Char(string='Bounce type', readonly=True) + bounce_description = fields.Char(string='Bounce description', + readonly=True) + tags = fields.Char(string='Tags', readonly=True) + metadata = fields.Text(string='Metadata', readonly=True) + event_ids = fields.One2many( + string='Mandrill events', + comodel_name='mail.mandrill.event', inverse_name='message_id') + + def _message_prepare(self, message_id, event_type, event): + msg = event.get('msg') + ts = msg.get('ts', 0) + time = datetime.datetime.fromtimestamp(ts) + tags = msg.get('tags', []) + metadata = msg.get('metadata', {}) + metatext = json.dumps(metadata, indent=4) if metadata else False + return { + 'mandrill_id': message_id, + 'timestamp': ts, + 'time': time.strftime('%Y-%m-%d %H:%M:%S') if ts else False, + 'date': time.strftime('%Y-%m-%d') if ts else False, + 'recipient': msg.get('email', False), + 'sender': msg.get('sender', False), + 'name': msg.get('subject', False), + 'tags': ', '.join(tags) if tags else False, + 'metadata': metatext, + } + + def _event_prepare(self, message, event_type, event): + m_event = self.env['mail.mandrill.event'] + method = getattr(m_event, 'process_' + event_type, None) + if method and hasattr(method, '__call__'): + return method(message, event) + else: + _logger.info('Unknown event type: %s' % event_type) + return False + + @api.model + def process(self, message_id, event_type, event): + if not (message_id and event_type and event): + return False + msg = event.get('msg') + message = self.search([('mandrill_id', '=', message_id)]) + if msg and not message: + data = self._message_prepare(message_id, event_type, event) + message = self.create(data) if data else False + if message: + m_event = self.env['mail.mandrill.event'] + data = self._event_prepare(message, event_type, event) + return m_event.create(data) if data else False + return False diff --git a/mail_mandrill/security/ir.model.access.csv b/mail_mandrill/security/ir.model.access.csv new file mode 100644 index 00000000..df008418 --- /dev/null +++ b/mail_mandrill/security/ir.model.access.csv @@ -0,0 +1,5 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_mail_mandrill_message_group_user","mail_mandrill_message group_user","model_mail_mandrill_message","base.group_user",1,0,0,0 +"access_mail_mandrill_event_group_user","mail_mandrill_event group_user","model_mail_mandrill_event","base.group_user",1,0,0,0 +"access_mail_mandrill_message_group_system","mail_mandrill_message group_system","model_mail_mandrill_message","base.group_system",1,1,1,1 +"access_mail_mandrill_event_group_system","mail_mandrill_event group_system","model_mail_mandrill_event","base.group_system",1,1,1,1 diff --git a/mail_mandrill/static/description/icon.png b/mail_mandrill/static/description/icon.png new file mode 100644 index 00000000..dbe8701e Binary files /dev/null and b/mail_mandrill/static/description/icon.png differ diff --git a/mail_mandrill/tests/__init__.py b/mail_mandrill/tests/__init__.py new file mode 100644 index 00000000..4eb197ab --- /dev/null +++ b/mail_mandrill/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +from . import test_mail_mandrill diff --git a/mail_mandrill/tests/test_mail_mandrill.py b/mail_mandrill/tests/test_mail_mandrill.py new file mode 100644 index 00000000..65f1782b --- /dev/null +++ b/mail_mandrill/tests/test_mail_mandrill.py @@ -0,0 +1,520 @@ +# -*- coding: utf-8 -*- +# License AGPL-3: Antiun Ingenieria S.L. - Antonio Espinosa +# See README.rst file on addon root folder for more details + +import json +from openerp.tests.common import TransactionCase +from openerp.tools.safe_eval import safe_eval + + +class TestMailMandrill(TransactionCase): + def setUp(self): + super(TestMailMandrill, self).setUp() + message_obj = self.env['mail.mandrill.message'] + self.partner_01 = self.env.ref('base.res_partner_1') + self.partner_02 = self.env.ref('base.res_partner_2') + self.model = 'res.partner' + self.res_id = self.partner_02.id + self.mandrill_message_id = '0123456789abcdef0123456789abcdef' + self.event_deferral = { + 'msg': { + 'sender': 'username01@example.com', + 'tags': [], + 'smtp_events': [ + { + 'destination_ip': '123.123.123.123', + 'diag': 'Event description', + 'source_ip': '145.145.145.145', + 'ts': 1455192896, + 'type': 'deferred', + 'size': 19513 + }, + ], + 'ts': 1455008558, + 'clicks': [], + 'resends': [], + 'state': 'deferred', + '_version': '1abcdefghijkABCDEFGHIJ', + 'template': None, + '_id': self.mandrill_message_id, + 'email': 'username02@example.com', + 'metadata': { + 'odoo_id': self.res_id, + 'odoo_db': 'test', + 'odoo_model': self.model, + }, + 'opens': [], + 'subject': 'My favorite subject' + }, + 'diag': '454 4.7.1 : Relay access denied', + '_id': self.mandrill_message_id, + 'event': 'deferral', + 'ts': 1455201028, + } + self.event_send = { + 'msg': { + '_id': self.mandrill_message_id, + 'subaccount': None, + 'tags': [], + 'smtp_events': [], + 'ts': 1455201157, + 'email': 'username02@example.com', + 'metadata': { + 'odoo_id': self.res_id, + 'odoo_db': 'test', + 'odoo_model': self.model, + }, + 'state': 'sent', + 'sender': 'username01@example.com', + 'template': None, + 'reject': None, + 'resends': [], + 'clicks': [], + 'opens': [], + 'subject': 'My favorite subject', + }, + '_id': self.mandrill_message_id, + 'event': 'send', + 'ts': 1455201159, + } + self.event_hard_bounce = { + 'msg': { + 'bounce_description': 'bad_mailbox', + 'sender': 'username01@example.com', + 'tags': [], + 'diag': 'smtp;550 5.4.1 [username02@example.com]: ' + 'Recipient address rejected: Access denied', + 'smtp_events': [], + 'ts': 1455194565, + 'template': None, + '_version': 'abcdefghi123456ABCDEFG', + 'metadata': { + 'odoo_id': self.res_id, + 'odoo_db': 'test', + 'odoo_model': self.model, + }, + 'resends': [], + 'state': 'bounced', + 'bgtools_code': 10, + '_id': self.mandrill_message_id, + 'email': 'username02@example.com', + 'subject': 'My favorite subject', + }, + '_id': self.mandrill_message_id, + 'event': 'hard_bounce', + 'ts': 1455195340 + } + self.event_soft_bounce = { + 'msg': { + 'bounce_description': 'general', + 'sender': 'username01@example.com', + 'tags': [], + 'diag': 'X-Notes; Error transferring to FQDN.EXAMPLE.COM\n ; ' + 'SMTP Protocol Returned a Permanent Error 550 5.7.1 ' + 'Unable to relay\n\n--==ABCDEFGHIJK12345678ABCDEFGH', + 'smtp_events': [], + 'ts': 1455194562, + 'template': None, + '_version': 'abcdefghi123456ABCDEFG', + 'metadata': { + 'odoo_id': self.res_id, + 'odoo_db': 'test', + 'odoo_model': self.model, + }, + 'resends': [], + 'state': 'soft-bounced', + 'bgtools_code': 40, + '_id': self.mandrill_message_id, + 'email': 'username02@example.com', + 'subject': 'My favorite subject', + }, + '_id': self.mandrill_message_id, + 'event': 'soft_bounce', + 'ts': 1455195622 + } + self.event_open = { + 'ip': '111.111.111.111', + 'ts': 1455189075, + 'location': { + 'country_short': 'PT', + 'city': 'Porto', + 'country': 'Portugal', + 'region': 'Porto', + 'longitude': -8.61098957062, + 'postal_code': '-', + 'latitude': 41.1496086121, + 'timezone': '+01:00', + }, + 'msg': { + 'sender': 'username01@example.com', + 'tags': [], + 'smtp_events': [ + { + 'destination_ip': '222.222.222.222', + 'diag': '250 2.0.0 ABCDEFGHIJK123456ABCDE mail ' + 'accepted for delivery', + 'source_ip': '111.1.1.1', + 'ts': 1455185877, + 'type': 'sent', + 'size': 30276, + }, + ], + 'ts': 1455185876, + 'clicks': [], + 'metadata': { + 'odoo_id': self.res_id, + 'odoo_db': 'test', + 'odoo_model': self.model, + }, + 'resends': [], + 'state': 'sent', + '_version': 'abcdefghi123456ABCDEFG', + 'template': None, + '_id': self.mandrill_message_id, + 'email': 'username02@example.com', + 'opens': [ + { + 'ip': '111.111.111.111', + 'ua': 'Windows/Windows 7/Outlook 2010/Outlook 2010', + 'ts': 1455186247, + 'location': + 'Porto, PT' + }, { + 'ip': '111.111.111.111', + 'ua': 'Windows/Windows 7/Outlook 2010/Outlook 2010', + 'ts': 1455189075, + 'location': 'Porto, PT' + }, + ], + 'subject': 'My favorite subject', + }, + '_id': self.mandrill_message_id, + 'user_agent_parsed': { + 'ua_name': 'Outlook 2010', + 'mobile': False, + 'ua_company_url': 'http://www.microsoft.com/', + 'os_icon': 'http://cdn.mandrill.com/img/email-client-icons/' + 'windows-7.png', + 'os_company': 'Microsoft Corporation.', + 'ua_version': None, + 'os_name': 'Windows 7', + 'ua_family': 'Outlook 2010', + 'os_url': 'http://en.wikipedia.org/wiki/Windows_7', + 'os_company_url': 'http://www.microsoft.com/', + 'ua_company': 'Microsoft Corporation.', + 'os_family': 'Windows', + 'type': 'Email Client', + 'ua_icon': 'http://cdn.mandrill.com/img/email-client-icons/' + 'outlook-2010.png', + 'ua_url': 'http://en.wikipedia.org/wiki/Microsoft_Outlook', + }, + 'event': 'open', + 'user_agent': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; ' + 'Trident/7.0; SLCC2; .NET CLR 2.0.50727; ' + '.NET CLR 3.5.30729; .NET CLR 3.0.30729; ' + 'Media Center PC 6.0; .NET4.0C; .NET4.0E; BRI/2; ' + 'Tablet PC 2.0; GWX:DOWNLOADED; ' + 'Microsoft Outlook 14.0.7166; ms-office; ' + 'MSOffice 14)', + } + self.event_click = { + 'url': 'http://www.example.com/index.php', + 'ip': '111.111.111.111', + 'ts': 1455186402, + 'user_agent': 'Mozilla/5.0 (Windows NT 6.1) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/48.0.2564.103 Safari/537.36', + 'msg': { + 'sender': 'username01@example.com', + 'tags': [], + 'smtp_events': [ + { + 'destination_ip': '222.222.222.222', + 'diag': '250 2.0.0 Ok: queued as 12345678', + 'source_ip': '111.1.1.1', + 'ts': 1455186065, + 'type': 'sent', + 'size': 30994, + }, + ], + 'ts': 1455186063, + 'clicks': [ + { + 'url': 'http://www.example.com/index.php', + 'ip': '111.111.111.111', + 'ua': 'Windows/Windows 7/Chrome/Chrome 48.0.2564.103', + 'ts': 1455186402, + 'location': 'Madrid, ES', + }, + ], + 'metadata': { + 'odoo_id': self.res_id, + 'odoo_db': 'test', + 'odoo_model': self.model, + }, + 'resends': [], + 'state': 'sent', + '_version': 'abcdefghi123456ABCDEFG', + 'template': None, + '_id': self.mandrill_message_id, + 'email': 'username02@example.com', + 'opens': [ + { + 'ip': '111.111.111.111', + 'ua': 'Windows/Windows 7/Chrome/Chrome 48.0.2564.103', + 'ts': 1455186402, + 'location': 'Madrid, ES', + }, + ], + 'subject': 'My favorite subject', + }, + '_id': self.mandrill_message_id, + 'user_agent_parsed': { + 'ua_name': 'Chrome 48.0.2564.103', + 'mobile': False, + 'ua_company_url': 'http://www.google.com/', + 'os_icon': 'http://cdn.mandrill.com/img/email-client-icons/' + 'windows-7.png', + 'os_company': 'Microsoft Corporation.', + 'ua_version': '48.0.2564.103', + 'os_name': 'Windows 7', + 'ua_family': 'Chrome', + 'os_url': 'http://en.wikipedia.org/wiki/Windows_7', + 'os_company_url': 'http://www.microsoft.com/', + 'ua_company': 'Google Inc.', + 'os_family': 'Windows', + 'type': 'Browser', + 'ua_icon': 'http://cdn.mandrill.com/img/email-client-icons/' + 'chrome.png', + 'ua_url': 'http://www.google.com/chrome', + }, + 'event': 'click', + 'location': { + 'country_short': 'ES', + 'city': 'Madrid', + 'country': 'Spain', + 'region': 'Madrid', + 'longitude': -3.70255994797, + 'postal_code': '-', + 'latitude': 40.4165000916, + 'timezone': '+02:00', + }, + } + self.event_spam = { + 'msg': { + 'sender': 'username01@example.com', + 'tags': [], + 'smtp_events': [], + 'ts': 1455186007, + 'clicks': [], + 'metadata': { + 'odoo_id': self.res_id, + 'odoo_db': 'test', + 'odoo_model': self.model, + }, + 'resends': [], + 'state': 'spam', + '_version': 'abcdefghi123456ABCDEFG', + 'template': None, + '_id': self.mandrill_message_id, + 'email': 'username02@example.com', + 'opens': [], + 'subject': 'My favorite subject', + }, + '_id': self.mandrill_message_id, + 'event': 'spam', + 'ts': 1455186366 + } + self.event_reject = { + 'msg': { + '_id': self.mandrill_message_id, + 'subaccount': None, + 'tags': [], + 'smtp_events': [], + 'ts': 1455194291, + 'email': 'username02@example.com', + 'metadata': { + 'odoo_id': self.res_id, + 'odoo_db': 'test', + 'odoo_model': self.model, + }, + 'state': 'rejected', + 'sender': 'username01@example.com', + 'template': None, + 'reject': None, + 'resends': [], + 'clicks': [], + 'opens': [], + 'subject': 'My favorite subject', + }, + '_id': self.mandrill_message_id, + 'event': 'reject', + 'ts': 1455194291, + } + self.event_unsub = { + 'msg': { + '_id': self.mandrill_message_id, + 'subaccount': None, + 'tags': [], + 'smtp_events': [], + 'ts': 1455194291, + 'email': 'username02@example.com', + 'metadata': { + 'odoo_id': self.res_id, + 'odoo_db': 'test', + 'odoo_model': self.model, + }, + 'state': 'unsub', + 'sender': 'username01@example.com', + 'template': None, + 'reject': None, + 'resends': [], + 'clicks': [], + 'opens': [], + 'subject': 'My favorite subject', + }, + '_id': self.mandrill_message_id, + 'event': 'unsub', + 'ts': 1455194291, + } + self.message = message_obj.create( + message_obj._message_prepare( + self.mandrill_message_id, 'deferral', self.event_deferral)) + + # Test Unit: mail_mail.py + def test_mandrill_headers_add(self): + mail_obj = self.env['mail.mail'] + message = self.env['mail.message'].create({ + 'author_id': self.partner_01.id, + 'subject': 'Test subject', + 'body': 'Test body', + 'partner_ids': [(4, self.partner_02.id)], + 'model': self.model, + 'res_id': self.res_id, + }) + mail = mail_obj.create({ + 'mail_message_id': message.id, + }) + mail._mandrill_headers_add() + headers = safe_eval(mail.headers) + self.assertIn('X-MC-Metadata', headers) + metadata = json.loads(headers.get('X-MC-Metadata', '[]')) + self.assertIn('odoo_db', metadata) + self.assertIn('odoo_model', metadata) + self.assertIn('odoo_id', metadata) + self.assertEqual(metadata['odoo_model'], self.model) + self.assertEqual(metadata['odoo_id'], self.res_id) + + # Test Unit: mail_mandrill_message.py + def test_message_prepare(self): + data = self.env['mail.mandrill.message']._message_prepare( + self.mandrill_message_id, 'deferral', self.event_deferral) + self.assertEqual(data['mandrill_id'], self.mandrill_message_id) + self.assertEqual(data['timestamp'], + self.event_deferral['msg']['ts']) + self.assertEqual(data['recipient'], + self.event_deferral['msg']['email']) + self.assertEqual(data['sender'], + self.event_deferral['msg']['sender']) + self.assertEqual(data['name'], + self.event_deferral['msg']['subject']) + + def test_event_prepare(self): + data = self.env['mail.mandrill.message']._event_prepare( + self.message, 'deferral', self.event_deferral) + self.assertEqual(self.message.state, 'deferred') + self.assertEqual(data['message_id'], self.message.id) + self.assertEqual(data['event_type'], 'deferral') + self.assertEqual(data['timestamp'], self.event_deferral['ts']) + + def test_process(self): + event = self.env['mail.mandrill.message'].process( + self.mandrill_message_id, 'deferral', self.event_deferral) + self.assertEqual(event.message_id.mandrill_id, + self.mandrill_message_id) + self.assertEqual(event.message_id.state, 'deferred') + self.assertEqual(event.event_type, 'deferral') + self.assertEqual(event.timestamp, self.event_deferral['ts']) + + # Test Unit: mail_mandrill_event.py + def test_process_send(self): + data = self.env['mail.mandrill.event'].process_send( + self.message, self.event_send) + self.assertEqual(self.message.state, 'sent') + self.assertEqual(data['message_id'], self.message.id) + self.assertEqual(data['event_type'], 'send') + self.assertEqual(data['timestamp'], self.event_send['ts']) + + def test_process_deferral(self): + data = self.env['mail.mandrill.event'].process_deferral( + self.message, self.event_deferral) + self.assertEqual(self.message.state, 'deferred') + self.assertEqual(data['message_id'], self.message.id) + self.assertEqual(data['event_type'], 'deferral') + self.assertEqual(data['timestamp'], self.event_deferral['ts']) + + def test_process_hard_bounce(self): + data = self.env['mail.mandrill.event'].process_hard_bounce( + self.message, self.event_hard_bounce) + self.assertEqual(self.message.state, 'bounced') + self.assertEqual(self.message.bounce_type, + self.event_hard_bounce['msg']['bounce_description']) + self.assertEqual(self.message.bounce_description, + self.event_hard_bounce['msg']['diag']) + self.assertEqual(data['message_id'], self.message.id) + self.assertEqual(data['event_type'], 'hard_bounce') + self.assertEqual(data['timestamp'], self.event_hard_bounce['ts']) + + def test_process_soft_bounce(self): + data = self.env['mail.mandrill.event'].process_soft_bounce( + self.message, self.event_soft_bounce) + self.assertEqual(self.message.state, 'bounced') + self.assertEqual(self.message.bounce_type, + self.event_soft_bounce['msg']['bounce_description']) + self.assertEqual(self.message.bounce_description, + self.event_soft_bounce['msg']['diag']) + self.assertEqual(data['message_id'], self.message.id) + self.assertEqual(data['event_type'], 'soft_bounce') + self.assertEqual(data['timestamp'], self.event_soft_bounce['ts']) + + def test_process_open(self): + data = self.env['mail.mandrill.event'].process_open( + self.message, self.event_open) + self.assertEqual(self.message.state, 'opened') + self.assertEqual(data['message_id'], self.message.id) + self.assertEqual(data['event_type'], 'open') + self.assertEqual(data['timestamp'], self.event_open['ts']) + self.assertEqual(data['ip'], self.event_open['ip']) + + def test_process_click(self): + data = self.env['mail.mandrill.event'].process_click( + self.message, self.event_open) + self.assertEqual(self.message.state, 'opened') + self.assertEqual(data['message_id'], self.message.id) + self.assertEqual(data['event_type'], 'click') + self.assertEqual(data['timestamp'], self.event_open['ts']) + self.assertEqual(data['ip'], self.event_open['ip']) + + def test_process_spam(self): + data = self.env['mail.mandrill.event'].process_spam( + self.message, self.event_spam) + self.assertEqual(self.message.state, 'spam') + self.assertEqual(data['message_id'], self.message.id) + self.assertEqual(data['event_type'], 'spam') + self.assertEqual(data['timestamp'], self.event_spam['ts']) + + def test_process_reject(self): + data = self.env['mail.mandrill.event'].process_reject( + self.message, self.event_reject) + self.assertEqual(self.message.state, 'rejected') + self.assertEqual(data['message_id'], self.message.id) + self.assertEqual(data['event_type'], 'reject') + self.assertEqual(data['timestamp'], self.event_reject['ts']) + + def test_process_unsub(self): + data = self.env['mail.mandrill.event'].process_unsub( + self.message, self.event_unsub) + self.assertEqual(self.message.state, 'unsub') + self.assertEqual(data['message_id'], self.message.id) + self.assertEqual(data['event_type'], 'unsub') + self.assertEqual(data['timestamp'], self.event_unsub['ts']) diff --git a/mail_mandrill/views/mail_mandrill_event_view.xml b/mail_mandrill/views/mail_mandrill_event_view.xml new file mode 100644 index 00000000..34756431 --- /dev/null +++ b/mail_mandrill/views/mail_mandrill_event_view.xml @@ -0,0 +1,109 @@ + + + + + + mail.mandrill.event.form + mail.mandrill.event + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + mail.mandrill.event.tree + mail.mandrill.event + + + + + + + + + + + + + + + + + mail.mandrill.event.search + mail.mandrill.event + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Mandrill events + mail.mandrill.event + form + tree,form + + + + + + +
+
\ No newline at end of file diff --git a/mail_mandrill/views/mail_mandrill_message_view.xml b/mail_mandrill/views/mail_mandrill_message_view.xml new file mode 100644 index 00000000..427762aa --- /dev/null +++ b/mail_mandrill/views/mail_mandrill_message_view.xml @@ -0,0 +1,114 @@ + + + + + + mail.mandrill.message.form + mail.mandrill.message + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + mail.mandrill.message.tree + mail.mandrill.message + + + + + + + + + + + + + + + mail.mandrill.message.search + mail.mandrill.message + + + + + + + + + + + + + + + + + + + + + + + + Mandrill emails + mail.mandrill.message + form + tree,form + + + + + + +
+
\ No newline at end of file