Browse Source

[ADD] mail_mandrill addon

pull/18/head
Antonio Espinosa 9 years ago
parent
commit
98507842e8
  1. 110
      mail_mandrill/README.rst
  2. 6
      mail_mandrill/__init__.py
  3. 25
      mail_mandrill/__openerp__.py
  4. 5
      mail_mandrill/controllers/__init__.py
  5. 130
      mail_mandrill/controllers/main.py
  6. 0
      mail_mandrill/i18n/.gitkeep
  7. 7
      mail_mandrill/models/__init__.py
  8. 38
      mail_mandrill/models/mail_mail.py
  9. 167
      mail_mandrill/models/mail_mandrill_event.py
  10. 102
      mail_mandrill/models/mail_mandrill_message.py
  11. 5
      mail_mandrill/security/ir.model.access.csv
  12. BIN
      mail_mandrill/static/description/icon.png
  13. 5
      mail_mandrill/tests/__init__.py
  14. 520
      mail_mandrill/tests/test_mail_mandrill.py
  15. 109
      mail_mandrill/views/mail_mandrill_event_view.xml
  16. 114
      mail_mandrill/views/mail_mandrill_message_view.xml

110
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 <https://mandrill.zendesk.com/hc/en-us/articles/205582417-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 <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_mandrill%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
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 <http://www.gnu.org/licenses/agpl-3.0-standalone.html>.
Credits
=======
Contributors
------------
* Rafael Blasco <rafabn@antiun.com>
* Antonio Espinosa <antonioea@antiun.com>
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.

6
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

25
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,
}

5
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

130
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

0
mail_mandrill/i18n/.gitkeep

7
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

38
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

167
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')

102
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

5
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

BIN
mail_mandrill/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 5.1 KiB

5
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

520
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 <username02@example.com>: 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'])

109
mail_mandrill/views/mail_mandrill_event_view.xml

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_mail_mandrill_event_form">
<field name="name">mail.mandrill.event.form</field>
<field name="model">mail.mandrill.event</field>
<field name="arch" type="xml">
<form string="Mandrill email event">
<sheet>
<group>
<group>
<field name="message_id"/>
<field name="event_type"/>
</group>
<group>
<field name="timestamp"/>
<field name="time"/>
<field name="date"/>
</group>
</group>
<group attrs="{'invisible': [('event_type', 'not in', ('open', 'click'))]}">
<field name="url"/>
</group>
<group attrs="{'invisible': [('event_type', 'not in', ('open', 'click'))]}">
<group>
<field name="mobile"/>
<field name="ip"/>
<field name="user_country_id"/>
</group>
<group>
<field name="user_agent"/>
<field name="ua_family"/>
<field name="ua_type"/>
<field name="os_family"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mandrill_event_tree">
<field name="name">mail.mandrill.event.tree</field>
<field name="model">mail.mandrill.event</field>
<field name="arch" type="xml">
<tree string="Mandrill email events" colors="grey:event_type in ('deferral');black:event_type in ('send');red:event_type in ('hard_bounce', 'soft_bounce', 'spam', 'reject');blue:event_type in ('unsub', 'click', 'open')">
<field name="time"/>
<field name="message_id" string="Message subject"/>
<field name="event_type"/>
<field name="date" invisible="1"/>
<field name="ip"/>
<field name="url"/>
<field name="user_country_id" string="Country"/>
<field name="os_family" string="OS"/>
<field name="ua_family" string="User agent"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mandrill_event_search">
<field name="name">mail.mandrill.event.search</field>
<field name="model">mail.mandrill.event</field>
<field name="arch" type="xml">
<search string="Mandrill event Search">
<field name="message_id" string="Message subject"
filter_domain="[('message_id','ilike',self)]"/>
<field name="message_id" string="Subject"/>
<field name="time" string="Time"/>
<field name="date" string="Date"/>
<field name="ip" string="IP"/>
<field name="url" string="URL"/>
<filter name="send" string="Send" domain="[('event_type', '=', 'send')]"/>
<filter name="click" string="Click" domain="[('event_type', '=', 'click')]"/>
<filter name="open" string="Open" domain="[('event_type', '=', 'open')]"/>
<filter name="unsub" string="Unsubscribe" domain="[('event_type', '=', 'unsub')]"/>
<filter name="bounce" string="Bounce"
domain="[('event_type', 'in', ('hard_bounce', 'soft_bounce'))]"/>
<filter name="exception" string="Failed"
domain="[('event_type', 'in', ('reject', 'spam'))]"/>
<separator/>
<group expand="0" string="Group By">
<filter string="Type" domain="[]" context="{'group_by': 'event_type'}"/>
<filter string="Message subject" domain="[]" context="{'group_by': 'message_id'}"/>
<filter string="OS" domain="[('os_family', '!=', False)]" context="{'group_by': 'os_family'}"/>
<filter string="User agent" domain="[('ua_family', '!=', False)]" context="{'group_by': 'ua_family'}"/>
<filter string="User agent type" domain="[('ua_type', '!=', False)]" context="{'group_by': 'ua_type'}"/>
<filter string="Country" domain="[('user_country_id', '!=', False)]" context="{'group_by': 'user_country_id'}"/>
<filter string="Month" domain="[]" context="{'group_by': 'date'}"/>
</group>
</search>
</field>
</record>
<record id="action_view_mail_mandrill_event" model="ir.actions.act_window">
<field name="name">Mandrill events</field>
<field name="res_model">mail.mandrill.event</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_mail_mandrill_event_search"/>
</record>
<!-- Add menu entry in Settings/Email -->
<menuitem name="Mandrill events" id="menu_mail_mandrill_event"
parent="base.menu_email"
action="action_view_mail_mandrill_event"/>
</data>
</openerp>

114
mail_mandrill/views/mail_mandrill_message_view.xml

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record model="ir.ui.view" id="view_mail_mandrill_message_form">
<field name="name">mail.mandrill.message.form</field>
<field name="model">mail.mandrill.message</field>
<field name="arch" type="xml">
<form string="Mandrill email message">
<header>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="tags"/>
</group>
<group>
<group>
<field name="mandrill_id"/>
<field name="recipient"/>
<field name="sender"/>
</group>
<group>
<field name="timestamp"/>
<field name="time"/>
<field name="date"/>
</group>
</group>
<group attrs="{'invisible': [('bounce_type', '=', False)]}">
<field name="bounce_type"/>
<field name="bounce_description"/>
</group>
<label for="metadata"/>
<div>
<field name="metadata"/>
</div>
<label for="event_ids"/>
<div>
<field name="event_ids">
<tree string="Mandrill email events" colors="grey:event_type in ('deferral');black:event_type in ('send');red:event_type in ('hard_bounce', 'soft_bounce', 'spam', 'reject');blue:event_type in ('unsub', 'click', 'open')">
<field name="time"/>
<field name="event_type"/>
<field name="ip"/>
<field name="url"/>
<field name="user_country_id" string="Country"/>
<field name="os_family" string="OS"/>
<field name="ua_family" string="User agent"/>
</tree>
</field>
</div>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mandrill_message_tree">
<field name="name">mail.mandrill.message.tree</field>
<field name="model">mail.mandrill.message</field>
<field name="arch" type="xml">
<tree string="Mandrill emails" colors="grey:state in (False, 'deferred');black:state in ('sent');green:state in ('opened');red:state in ('rejected', 'spam', 'bounced', 'soft-bounced');blue:state in ('unsub')">
<field name="state" invisible="1"/>
<field name="time"/>
<field name="date" invisible="1"/>
<field name="name"/>
<field name="sender" string="Sender"/>
<field name="recipient" string="Recipient"/>
<field name="tags" string="Tags"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_mail_mandrill_message_search">
<field name="name">mail.mandrill.message.search</field>
<field name="model">mail.mandrill.message</field>
<field name="arch" type="xml">
<search string="Mandrill email Search">
<field name="sender" string="Email"
filter_domain="['|', ('sender','ilike',self), ('recipient','ilike',self)]"/>
<field name="name" string="Subject"/>
<field name="time" string="Time"/>
<field name="date" string="Date"/>
<filter name="deferred" string="Deferred" domain="[('state', '=', 'deferred')]"/>
<filter name="sent" string="Sent" domain="[('state', 'in', ('sent', 'opened'))]"/>
<filter name="unsub" string="Unsubscribed" domain="[('state', '=', 'unsub')]"/>
<filter name="exception" string="Failed"
domain="[('state', 'in', ('rejected', 'spam', 'bounced', 'soft-bounced'))]"/>
<separator/>
<group expand="0" string="Group By">
<filter string="State" domain="[]" context="{'group_by': 'state'}"/>
<filter string="Subject" domain="[]" context="{'group_by': 'name'}"/>
<filter string="Sender" domain="[]" context="{'group_by': 'sender'}"/>
<filter string="Month" domain="[]" context="{'group_by': 'date'}"/>
</group>
</search>
</field>
</record>
<record id="action_view_mail_mandrill_message" model="ir.actions.act_window">
<field name="name">Mandrill emails</field>
<field name="res_model">mail.mandrill.message</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_mail_mandrill_message_search"/>
</record>
<!-- Add menu entry in Settings/Email -->
<menuitem name="Mandrill emails" id="menu_mail_mandrill_message"
parent="base.menu_email"
action="action_view_mail_mandrill_message"/>
</data>
</openerp>
Loading…
Cancel
Save