Browse Source

[IMP] Email stars and generic webhook

pull/87/head
Antonio Espinosa 9 years ago
committed by Pedro M. Baeza
parent
commit
367b487b25
  1. 1
      mail_tracking/__init__.py
  2. 6
      mail_tracking/__openerp__.py
  3. 55
      mail_tracking/controllers/main.py
  4. 24
      mail_tracking/hooks.py
  5. 1
      mail_tracking/models/__init__.py
  6. 4
      mail_tracking/models/ir_mail_server.py
  7. 107
      mail_tracking/models/mail_tracking_email.py
  8. 40
      mail_tracking/models/res_partner.py
  9. 71
      mail_tracking/tests/test_mail_tracking.py
  10. 11
      mail_tracking/views/mail_tracking_email_view.xml
  11. 5
      mail_tracking/views/mail_tracking_event_view.xml
  12. 33
      mail_tracking/views/res_partner_view.xml

1
mail_tracking/__init__.py

@ -5,3 +5,4 @@
from . import models from . import models
from . import controllers from . import controllers
from .hooks import post_init_hook

6
mail_tracking/__openerp__.py

@ -5,7 +5,7 @@
{ {
"name": "Email tracking", "name": "Email tracking",
"summary": "Email tracking system for all mails sent", "summary": "Email tracking system for all mails sent",
"version": "8.0.1.0.0",
"version": "8.0.2.0.0",
"category": "Social Network", "category": "Social Network",
"website": "http://www.tecnativa.com", "website": "http://www.tecnativa.com",
"author": "Tecnativa, " "author": "Tecnativa, "
@ -23,8 +23,10 @@
"views/assets.xml", "views/assets.xml",
"views/mail_tracking_email_view.xml", "views/mail_tracking_email_view.xml",
"views/mail_tracking_event_view.xml", "views/mail_tracking_event_view.xml",
"views/res_partner_view.xml",
], ],
"qweb": [ "qweb": [
"static/src/xml/mail_tracking.xml", "static/src/xml/mail_tracking.xml",
]
],
"post_init_hook": "post_init_hook",
} }

55
mail_tracking/controllers/main.py

@ -11,6 +11,19 @@ _logger = logging.getLogger(__name__)
BLANK = 'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' BLANK = 'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
def _env_get(db):
reg = False
try:
reg = registry(db)
except OperationalError:
_logger.warning("Selected BD '%s' not found", db)
except:
_logger.warning("Selected BD '%s' connection error", db)
if reg:
return api.Environment(reg.cursor(), SUPERUSER_ID, {})
return False
class MailTrackingController(http.Controller): class MailTrackingController(http.Controller):
def _request_metadata(self): def _request_metadata(self):
@ -22,29 +35,49 @@ class MailTrackingController(http.Controller):
'ua_family': request.user_agent.browser, 'ua_family': request.user_agent.browser,
} }
@http.route('/mail/tracking/all/<string:db>',
type='http', auth='none')
def mail_tracking_all(self, db, **kw):
env = _env_get(db)
if not env:
return 'NOT FOUND'
metadata = self._request_metadata()
response = env['mail.tracking.email'].event_process(
http.request, kw, metadata)
env.cr.commit()
env.cr.close()
return response
@http.route('/mail/tracking/event/<string:db>/<string:event_type>',
type='http', auth='none')
def mail_tracking_event(self, db, event_type, **kw):
env = _env_get(db)
if not env:
return 'NOT FOUND'
metadata = self._request_metadata()
response = env['mail.tracking.email'].event_process(
http.request, kw, metadata, event_type=event_type)
env.cr.commit()
env.cr.close()
return response
@http.route('/mail/tracking/open/<string:db>' @http.route('/mail/tracking/open/<string:db>'
'/<int:tracking_email_id>/blank.gif', '/<int:tracking_email_id>/blank.gif',
type='http', auth='none') type='http', auth='none')
def mail_tracking_open(self, db, tracking_email_id, **kw): def mail_tracking_open(self, db, tracking_email_id, **kw):
reg = False
try:
reg = registry(db)
except OperationalError:
_logger.warning("Selected BD '%s' not found", db)
except:
_logger.warning("Selected BD '%s' connection error", db)
if reg:
with reg.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
env = _env_get(db)
if env:
tracking_email = env['mail.tracking.email'].search([ tracking_email = env['mail.tracking.email'].search([
('id', '=', tracking_email_id), ('id', '=', tracking_email_id),
]) ])
if tracking_email: if tracking_email:
metadata = self._request_metadata() metadata = self._request_metadata()
tracking_email.event_process('open', metadata)
tracking_email.event_create('open', metadata)
else: else:
_logger.warning( _logger.warning(
"MailTracking email '%s' not found", tracking_email_id) "MailTracking email '%s' not found", tracking_email_id)
env.cr.commit()
env.cr.close()
# Always return GIF blank image # Always return GIF blank image
response = werkzeug.wrappers.Response() response = werkzeug.wrappers.Response()

24
mail_tracking/hooks.py

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
from openerp import api, SUPERUSER_ID
_logger = logging.getLogger(__name__)
def post_init_hook(cr, registry):
with api.Environment.manage():
env = api.Environment(cr, SUPERUSER_ID, {})
# Recalculate all partner tracking_email_ids
partners = env['res.partner'].search([
('email', '!=', False),
])
emails = partners.mapped('email')
_logger.info(
"Recalculating 'tracking_email_ids' in 'res.partner' "
"model for %d email addresses", len(emails))
for email in emails:
env['mail.tracking.email'].tracking_ids_recalculate(
'res.partner', 'email', 'tracking_email_ids', email)

1
mail_tracking/models/__init__.py

@ -8,3 +8,4 @@ from . import mail_mail
from . import mail_message from . import mail_message
from . import mail_tracking_email from . import mail_tracking_email
from . import mail_tracking_event from . import mail_tracking_event
from . import res_partner

4
mail_tracking/models/ir_mail_server.py

@ -24,7 +24,7 @@ class IrMailServer(models.Model):
if match: if match:
try: try:
tracking_email_id = int(match.group(1)) tracking_email_id = int(match.group(1))
except:
except: # pragma: no cover
pass pass
return tracking_email_id return tracking_email_id
@ -61,7 +61,7 @@ class IrMailServer(models.Model):
mail_server = mail_server_ids[0] if mail_server_ids else None mail_server = mail_server_ids[0] if mail_server_ids else None
if mail_server: if mail_server:
smtp_server_used = mail_server.smtp_host smtp_server_used = mail_server.smtp_host
else:
else: # pragma: no cover
smtp_server_used = smtp_server or tools.config.get('smtp_server') smtp_server_used = smtp_server or tools.config.get('smtp_server')
return smtp_server_used return smtp_server_used

107
mail_tracking/models/mail_tracking_email.py

@ -5,6 +5,7 @@
import logging import logging
import urlparse import urlparse
import time import time
import re
from datetime import datetime from datetime import datetime
from openerp import models, api, fields, tools from openerp import models, api, fields, tools
@ -16,10 +17,13 @@ _logger = logging.getLogger(__name__)
class MailTrackingEmail(models.Model): class MailTrackingEmail(models.Model):
_name = "mail.tracking.email" _name = "mail.tracking.email"
_order = 'time desc' _order = 'time desc'
_rec_name = 'name'
_rec_name = 'display_name'
_description = 'MailTracking email' _description = 'MailTracking email'
name = fields.Char(string="Subject", readonly=True, index=True) name = fields.Char(string="Subject", readonly=True, index=True)
display_name = fields.Char(
string="Display name", readonly=True, store=True,
compute="_compute_display_name")
timestamp = fields.Float( timestamp = fields.Float(
string='UTC timestamp', readonly=True, string='UTC timestamp', readonly=True,
digits=dp.get_precision('MailTracking Timestamp')) digits=dp.get_precision('MailTracking Timestamp'))
@ -33,6 +37,9 @@ class MailTrackingEmail(models.Model):
partner_id = fields.Many2one( partner_id = fields.Many2one(
string="Partner", comodel_name='res.partner', readonly=True) string="Partner", comodel_name='res.partner', readonly=True)
recipient = fields.Char(string='Recipient email', readonly=True) recipient = fields.Char(string='Recipient email', readonly=True)
recipient_address = fields.Char(
string='Recipient email address', readonly=True, store=True,
compute='_compute_recipient_address')
sender = fields.Char(string='Sender email', readonly=True) sender = fields.Char(string='Sender email', readonly=True)
state = fields.Selection([ state = fields.Selection([
('error', 'Error'), ('error', 'Error'),
@ -77,6 +84,84 @@ class MailTrackingEmail(models.Model):
string="Tracking events", comodel_name='mail.tracking.event', string="Tracking events", comodel_name='mail.tracking.event',
inverse_name='tracking_email_id', readonly=True) inverse_name='tracking_email_id', readonly=True)
@api.model
def tracking_ids_recalculate(self, model, email_field, tracking_field,
email, new_tracking=None):
objects = self.env[model].search([
(email_field, '=ilike', email),
])
for obj in objects:
trackings = obj[tracking_field]
if new_tracking:
trackings |= new_tracking
trackings = trackings._email_score_tracking_filter()
if set(obj[tracking_field].ids) != set(trackings.ids):
if trackings:
obj.write({
tracking_field: [(6, False, trackings.ids)]
})
else:
obj.write({
tracking_field: [(5, False, False)]
})
return True
@api.model
def _tracking_ids_to_write(self, email):
trackings = self.env['mail.tracking.email'].search([
('recipient_address', '=ilike', email)
])
trackings = trackings._email_score_tracking_filter()
if trackings:
return [(6, False, trackings.ids)]
else:
return [(5, False, False)]
@api.multi
def _email_score_tracking_filter(self):
"""Default email score filter for tracking emails"""
# Consider only last 10 tracking emails
return self.sorted(key=lambda r: r.time, reverse=True)[:10]
@api.multi
def email_score(self):
"""Default email score algorimth"""
score = 50.0
trackings = self._email_score_tracking_filter()
for tracking in trackings:
if tracking.state in ('error',):
score -= 50.0
elif tracking.state in ('rejected', 'spam', 'bounced'):
score -= 25.0
elif tracking.state in ('soft-bounced', 'unsub'):
score -= 10.0
elif tracking.state in ('delivered',):
score += 5.0
elif tracking.state in ('opened',):
score += 10.0
if score > 100.0:
score = 100.0
return score
@api.multi
@api.depends('recipient')
def _compute_recipient_address(self):
for email in self:
matches = re.search(r'<(.*@.*)>', email.recipient)
if matches:
email.recipient_address = matches.group(1)
else:
email.recipient_address = email.recipient
@api.multi
@api.depends('name', 'recipient')
def _compute_display_name(self):
for email in self:
parts = [email.name]
if email.recipient:
parts.append(email.recipient)
email.display_name = ' - '.join(parts)
@api.multi @api.multi
@api.depends('time') @api.depends('time')
def _compute_date(self): def _compute_date(self):
@ -84,6 +169,14 @@ class MailTrackingEmail(models.Model):
email.date = fields.Date.to_string( email.date = fields.Date.to_string(
fields.Date.from_string(email.time)) fields.Date.from_string(email.time))
@api.model
def create(self, vals):
tracking = super(MailTrackingEmail, self).create(vals)
self.tracking_ids_recalculate(
'res.partner', 'email', 'tracking_email_ids',
tracking.recipient_address, new_tracking=tracking)
return tracking
def _get_mail_tracking_img(self): def _get_mail_tracking_img(self):
base_url = self.env['ir.config_parameter'].get_param('web.base.url') base_url = self.env['ir.config_parameter'].get_param('web.base.url')
path_url = ( path_url = (
@ -159,15 +252,23 @@ class MailTrackingEmail(models.Model):
method = getattr(m_event, 'process_' + event_type, None) method = getattr(m_event, 'process_' + event_type, None)
if method and hasattr(method, '__call__'): if method and hasattr(method, '__call__'):
return method(self, metadata) return method(self, metadata)
else:
else: # pragma: no cover
_logger.info('Unknown event type: %s' % event_type) _logger.info('Unknown event type: %s' % event_type)
return False return False
@api.multi @api.multi
def event_process(self, event_type, metadata):
def event_create(self, event_type, metadata):
event_ids = self.env['mail.tracking.event'] event_ids = self.env['mail.tracking.event']
for tracking_email in self: for tracking_email in self:
vals = tracking_email._event_prepare(event_type, metadata) vals = tracking_email._event_prepare(event_type, metadata)
if vals: if vals:
event_ids += event_ids.sudo().create(vals) event_ids += event_ids.sudo().create(vals)
return event_ids return event_ids
@api.model
def event_process(self, request, post, metadata, event_type=None):
# Generic event process hook, inherit it and
# - return 'OK' if processed
# - return 'NONE' if this request is not for you
# - return 'ERROR' if any error
return 'NONE' # pragma: no cover

40
mail_tracking/models/res_partner.py

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models, api, fields
class ResPartner(models.Model):
_inherit = 'res.partner'
tracking_email_ids = fields.Many2many(
string="Tracking emails", comodel_name="mail.tracking.email",
readonly=True)
tracking_emails_count = fields.Integer(
string="Tracking emails count", store=True, readonly=True,
compute="_compute_tracking_emails_count")
email_score = fields.Float(
string="Email score",
compute="_compute_email_score", store=True, readonly=True)
@api.one
@api.depends('tracking_email_ids.state')
def _compute_email_score(self):
self.email_score = self.tracking_email_ids.email_score()
@api.one
@api.depends('tracking_email_ids')
def _compute_tracking_emails_count(self):
self.tracking_emails_count = self.env['mail.tracking.email'].\
search_count([
('recipient_address', '=ilike', self.email)
])
@api.multi
def write(self, vals):
email = vals.get('email')
if email is not None:
vals['tracking_email_ids'] = \
self.env['mail.tracking.email']._tracking_ids_to_write(email)
return super(ResPartner, self).write(vals)

71
mail_tracking/tests/test_mail_tracking.py

@ -2,7 +2,24 @@
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com> # © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import mock
import base64
from openerp.tests.common import TransactionCase from openerp.tests.common import TransactionCase
from openerp.addons.mail_tracking.controllers.main import \
MailTrackingController, BLANK
mock_request = 'openerp.http.request'
mock_send_email = ('openerp.addons.base.ir.ir_mail_server.'
'ir_mail_server.send_email')
class FakeUserAgent(object):
browser = 'Test browser'
platform = 'Test platform'
def __str__(self):
"""Return name"""
return 'Test suite'
# One test case per method # One test case per method
@ -20,6 +37,12 @@ class TestMailTracking(TransactionCase):
'email': 'recipient@example.com', 'email': 'recipient@example.com',
'notify_email': 'always', 'notify_email': 'always',
}) })
self.request = {
'httprequest': type('obj', (object,), {
'remote_addr': '123.123.123.123',
'user_agent': FakeUserAgent(),
}),
}
def test_message_post(self): def test_message_post(self):
# This message will generate a notification for recipient # This message will generate a notification for recipient
@ -62,5 +85,51 @@ class TestMailTracking(TransactionCase):
'os_family': 'linux', 'os_family': 'linux',
'ua_family': 'odoo', 'ua_family': 'odoo',
} }
tracking_email.event_process('open', metadata)
tracking_email.event_create('open', metadata)
self.assertEqual(tracking_email.state, 'opened') self.assertEqual(tracking_email.state, 'opened')
def mail_send(self):
mail = self.env['mail.mail'].create({
'subject': 'Test subject',
'email_from': 'from@domain.com',
'email_to': 'to@domain.com',
'body_html': '<p>This is a test message</p>',
})
mail.send()
# Search tracking created
tracking_email = self.env['mail.tracking.email'].search([
('mail_id', '=', mail.id),
])
return mail, tracking_email
def test_mail_send(self):
controller = MailTrackingController()
db = self.env.cr.dbname
image = base64.decodestring(BLANK)
mail, tracking = self.mail_send()
self.assertEqual(mail.email_to, tracking.recipient)
self.assertEqual(mail.email_from, tracking.sender)
with mock.patch(mock_request) as mock_func:
mock_func.return_value = type('obj', (object,), self.request)
res = controller.mail_tracking_open(db, tracking.id)
self.assertEqual(image, res.response[0])
def test_smtp_error(self):
with mock.patch(mock_send_email) as mock_func:
mock_func.side_effect = Warning('Test error')
mail, tracking = self.mail_send()
self.assertEqual('error', tracking.state)
self.assertEqual('Warning', tracking.error_type)
self.assertEqual('Test error', tracking.error_description)
def test_db(self):
db = self.env.cr.dbname
controller = MailTrackingController()
with mock.patch(mock_request) as mock_func:
mock_func.return_value = type('obj', (object,), self.request)
not_found = controller.mail_tracking_all('not_found_db')
self.assertEqual('NOT FOUND', not_found.response[0])
none = controller.mail_tracking_all(db)
self.assertEqual('NONE', none.response[0])
none = controller.mail_tracking_event(db, 'open')
self.assertEqual('NONE', none.response[0])

11
mail_tracking/views/mail_tracking_email_view.xml

@ -8,7 +8,7 @@
<field name="name">mail.tracking.email.form</field> <field name="name">mail.tracking.email.form</field>
<field name="model">mail.tracking.email</field> <field name="model">mail.tracking.email</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="MailTracking event">
<form string="MailTracking event" create="false" edit="false" delete="false">
<header> <header>
<field name="state" widget="statusbar"/> <field name="state" widget="statusbar"/>
</header> </header>
@ -63,13 +63,14 @@
<field name="name">mail.tracking.email.tree</field> <field name="name">mail.tracking.email.tree</field>
<field name="model">mail.tracking.email</field> <field name="model">mail.tracking.email</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="MailTracking emails" colors="grey:state in (False, 'deferred');black:state in ('sent', 'delivered');green:state in ('opened');red:state in ('rejected', 'spam', 'bounced', 'soft-bounced');blue:state in ('unsub')">
<field name="state" invisible="1"/>
<tree string="MailTracking emails" create="false" edit="false" delete="false"
colors="grey:state in (False, 'deferred');black:state in ('sent', 'delivered');green:state in ('opened');red:state in ('rejected', 'spam', 'bounced', 'soft-bounced');blue:state in ('unsub')">
<field name="time"/> <field name="time"/>
<field name="date" invisible="1"/> <field name="date" invisible="1"/>
<field name="name"/> <field name="name"/>
<field name="sender" string="Sender"/> <field name="sender" string="Sender"/>
<field name="recipient" string="Recipient"/> <field name="recipient" string="Recipient"/>
<field name="state"/>
</tree> </tree>
</field> </field>
</record> </record>
@ -79,8 +80,10 @@
<field name="model">mail.tracking.email</field> <field name="model">mail.tracking.email</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<search string="MailTracking email search"> <search string="MailTracking email search">
<field name="sender" string="Email"
<field name="display_name" string="Email"
filter_domain="['|', ('sender', 'ilike', self), ('recipient', 'ilike', self)]"/> filter_domain="['|', ('sender', 'ilike', self), ('recipient', 'ilike', self)]"/>
<field name="sender" string="Sender"/>
<field name="recipient" string="Sender"/>
<field name="name" string="Subject"/> <field name="name" string="Subject"/>
<field name="time" string="Time"/> <field name="time" string="Time"/>
<field name="date" string="Date"/> <field name="date" string="Date"/>

5
mail_tracking/views/mail_tracking_event_view.xml

@ -8,7 +8,7 @@
<field name="name">mail.tracking.event.form</field> <field name="name">mail.tracking.event.form</field>
<field name="model">mail.tracking.event</field> <field name="model">mail.tracking.event</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="MailTracking event">
<form string="MailTracking event" create="false" edit="false" delete="false">
<sheet> <sheet>
<group> <group>
<group> <group>
@ -50,7 +50,8 @@
<field name="name">mail.tracking.event.tree</field> <field name="name">mail.tracking.event.tree</field>
<field name="model">mail.tracking.event</field> <field name="model">mail.tracking.event</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<tree string="MailTracking events" colors="grey:event_type in ('deferral',);black:event_type in ('sent', 'delivered');red:event_type in ('hard_bounce', 'soft_bounce', 'spam', 'reject');blue:event_type in ('unsub', 'click', 'open')">
<tree string="MailTracking events" create="false" edit="false" delete="false"
colors="grey:event_type in ('deferral',);black:event_type in ('sent', 'delivered');red:event_type in ('hard_bounce', 'soft_bounce', 'spam', 'reject');blue:event_type in ('unsub', 'click', 'open')">
<field name="time"/> <field name="time"/>
<field name="tracking_email_id"/> <field name="tracking_email_id"/>
<field name="recipient"/> <field name="recipient"/>

33
mail_tracking/views/res_partner_view.xml

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
<openerp>
<data>
<record model="ir.ui.view" id="view_partner_form">
<field name="name">Partner Form with tracking emails</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<div class="oe_right oe_button_box" position="inside">
<button name="%(mail_tracking.action_view_mail_tracking_email)d"
context="{'search_default_recipient': email,
'default_recipient': email}"
type="action"
class="oe_stat_button oe_inline"
icon="fa-envelope-o"
attrs="{'invisible': [('email', '=', False)]}">
<field name="tracking_emails_count"
widget="statinfo"
string="Tracking emails"/>
</button>
</div>
<field name="email" position="after">
<field name="email_score" widget="progressbar"
attrs="{'invisible': [('email', '=', False)]}"/>
</field>
</field>
</record>
</data>
</openerp>
Loading…
Cancel
Save