mirror of https://github.com/OCA/social.git
[8.0][IMP] Performance issues in mail_tracking addons (#97)
[IMP] mail_tracking performance and bounce processpull/85/head
parent
f2b2cab2ce
commit
55250b99d4
|
@ -5,7 +5,7 @@
|
|||
{
|
||||
"name": "Email tracking",
|
||||
"summary": "Email tracking system for all mails sent",
|
||||
"version": "8.0.2.0.2",
|
||||
"version": "8.0.3.0.0",
|
||||
"category": "Social Network",
|
||||
"website": "http://www.tecnativa.com",
|
||||
"author": "Tecnativa, "
|
||||
|
|
|
@ -11,17 +11,30 @@ _logger = logging.getLogger(__name__)
|
|||
BLANK = 'R0lGODlhAQABAIAAANvf7wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
|
||||
|
||||
|
||||
def _env_get(db):
|
||||
def _env_get(db, callback, tracking_id, event_type, **kw):
|
||||
res = 'NOT FOUND'
|
||||
reg = False
|
||||
try:
|
||||
reg = registry(db)
|
||||
except OperationalError:
|
||||
_logger.warning("Selected BD '%s' not found", db)
|
||||
except: # pragma: no cover
|
||||
_logger.warning("Selected BD '%s' connection error", db)
|
||||
if reg:
|
||||
return api.Environment(reg.cursor(), SUPERUSER_ID, {})
|
||||
return False
|
||||
current = http.request.db and db == http.request.db
|
||||
env = current and http.request.env
|
||||
if not env:
|
||||
with api.Environment.manage():
|
||||
try:
|
||||
reg = registry(db)
|
||||
except OperationalError:
|
||||
_logger.warning("Selected BD '%s' not found", db)
|
||||
except: # pragma: no cover
|
||||
_logger.warning("Selected BD '%s' connection error", db)
|
||||
if reg:
|
||||
_logger.info("New environment for database '%s'", db)
|
||||
with reg.cursor() as new_cr:
|
||||
new_env = api.Environment(new_cr, SUPERUSER_ID, {})
|
||||
res = callback(new_env, tracking_id, event_type, **kw)
|
||||
new_env.cr.commit()
|
||||
else:
|
||||
# make sudo when reusing environment
|
||||
env = env(user=SUPERUSER_ID)
|
||||
res = callback(env, tracking_id, event_type, **kw)
|
||||
return res
|
||||
|
||||
|
||||
class MailTrackingController(http.Controller):
|
||||
|
@ -35,49 +48,37 @@ class MailTrackingController(http.Controller):
|
|||
'ua_family': request.user_agent.browser or False,
|
||||
}
|
||||
|
||||
def _tracking_open(self, env, tracking_id, event_type, **kw):
|
||||
tracking_email = env['mail.tracking.email'].search([
|
||||
('id', '=', tracking_id),
|
||||
])
|
||||
if tracking_email:
|
||||
metadata = self._request_metadata()
|
||||
tracking_email.event_create('open', metadata)
|
||||
else:
|
||||
_logger.warning(
|
||||
"MailTracking email '%s' not found", tracking_id)
|
||||
|
||||
def _tracking_event(self, env, tracking_id, event_type, **kw):
|
||||
metadata = self._request_metadata()
|
||||
return env['mail.tracking.email'].event_process(
|
||||
http.request, kw, metadata, event_type=event_type)
|
||||
|
||||
@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
|
||||
return _env_get(db, self._tracking_event, None, None, **kw)
|
||||
|
||||
@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
|
||||
return _env_get(db, self._tracking_event, None, event_type, **kw)
|
||||
|
||||
@http.route('/mail/tracking/open/<string:db>'
|
||||
'/<int:tracking_email_id>/blank.gif',
|
||||
type='http', auth='none')
|
||||
def mail_tracking_open(self, db, tracking_email_id, **kw):
|
||||
env = _env_get(db)
|
||||
if env:
|
||||
tracking_email = env['mail.tracking.email'].search([
|
||||
('id', '=', tracking_email_id),
|
||||
])
|
||||
if tracking_email:
|
||||
metadata = self._request_metadata()
|
||||
tracking_email.event_create('open', metadata)
|
||||
else:
|
||||
_logger.warning(
|
||||
"MailTracking email '%s' not found", tracking_email_id)
|
||||
env.cr.commit()
|
||||
env.cr.close()
|
||||
_env_get(db, self._tracking_open, tracking_email_id, None, **kw)
|
||||
|
||||
# Always return GIF blank image
|
||||
response = werkzeug.wrappers.Response()
|
||||
|
|
|
@ -23,6 +23,11 @@ class MailTrackingEmail(models.Model):
|
|||
_rec_name = 'display_name'
|
||||
_description = 'MailTracking email'
|
||||
|
||||
# This table is going to grow fast and to infinite, so we index:
|
||||
# - name: Search in tree view
|
||||
# - time: default order fields
|
||||
# - recipient_address: Used for email_store calculation (non-store)
|
||||
# - state: Search and group_by in tree view
|
||||
name = fields.Char(string="Subject", readonly=True, index=True)
|
||||
display_name = fields.Char(
|
||||
string="Display name", readonly=True, store=True,
|
||||
|
@ -30,7 +35,7 @@ class MailTrackingEmail(models.Model):
|
|||
timestamp = fields.Float(
|
||||
string='UTC timestamp', readonly=True,
|
||||
digits=dp.get_precision('MailTracking Timestamp'))
|
||||
time = fields.Datetime(string="Time", readonly=True)
|
||||
time = fields.Datetime(string="Time", readonly=True, index=True)
|
||||
date = fields.Date(
|
||||
string="Date", readonly=True, compute="_compute_date", store=True)
|
||||
mail_message_id = fields.Many2one(
|
||||
|
@ -42,7 +47,7 @@ class MailTrackingEmail(models.Model):
|
|||
recipient = fields.Char(string='Recipient email', readonly=True)
|
||||
recipient_address = fields.Char(
|
||||
string='Recipient email address', readonly=True, store=True,
|
||||
compute='_compute_recipient_address')
|
||||
compute='_compute_recipient_address', index=True)
|
||||
sender = fields.Char(string='Sender email', readonly=True)
|
||||
state = fields.Selection([
|
||||
('error', 'Error'),
|
||||
|
@ -88,69 +93,55 @@ class MailTrackingEmail(models.Model):
|
|||
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 objects
|
||||
def _email_score_tracking_filter(self, domain, order='time desc',
|
||||
limit=10):
|
||||
"""Default tracking search. Ready to be inherited."""
|
||||
return self.search(domain, limit=limit, order=order)
|
||||
|
||||
@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]
|
||||
def email_is_bounced(self, email):
|
||||
return len(self._email_score_tracking_filter([
|
||||
('recipient_address', '=ilike', email),
|
||||
('state', 'in', ('error', 'rejected', 'spam', 'bounced')),
|
||||
])) > 0
|
||||
|
||||
@api.model
|
||||
def email_score_from_email(self, email):
|
||||
trackings = self.env['mail.tracking.email'].search([
|
||||
return self._email_score_tracking_filter([
|
||||
('recipient_address', '=ilike', email)
|
||||
])
|
||||
return trackings.email_score()
|
||||
]).email_score()
|
||||
|
||||
@api.model
|
||||
def _email_score_weights(self):
|
||||
"""Default email score weights. Ready to be inherited"""
|
||||
return {
|
||||
'error': -50.0,
|
||||
'rejected': -25.0,
|
||||
'spam': -25.0,
|
||||
'bounced': -25.0,
|
||||
'soft-bounced': -10.0,
|
||||
'unsub': -10.0,
|
||||
'delivered': 1.0,
|
||||
'opened': 5.0,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def email_score(self):
|
||||
"""Default email score algorimth"""
|
||||
"""Default email score algorimth. Ready to be inherited
|
||||
|
||||
Must return a value beetwen 0.0 and 100.0
|
||||
- Bad reputation: Value between 0 and 50.0
|
||||
- Unknown reputation: Value 50.0
|
||||
- Good reputation: Value between 50.0 and 100.0
|
||||
"""
|
||||
weights = self._email_score_weights()
|
||||
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
|
||||
for tracking in self:
|
||||
score += weights.get(tracking.state, 0.0)
|
||||
if score > 100.0:
|
||||
score = 100.0
|
||||
elif score < 0.0:
|
||||
score = 0.0
|
||||
return score
|
||||
|
||||
@api.multi
|
||||
|
@ -167,7 +158,7 @@ class MailTrackingEmail(models.Model):
|
|||
@api.depends('name', 'recipient')
|
||||
def _compute_display_name(self):
|
||||
for email in self:
|
||||
parts = [email.name]
|
||||
parts = [email.name or '']
|
||||
if email.recipient:
|
||||
parts.append(email.recipient)
|
||||
email.display_name = ' - '.join(parts)
|
||||
|
@ -179,14 +170,6 @@ class MailTrackingEmail(models.Model):
|
|||
email.date = fields.Date.to_string(
|
||||
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):
|
||||
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
|
||||
path_url = (
|
||||
|
@ -202,6 +185,13 @@ class MailTrackingEmail(models.Model):
|
|||
'tracking_email_id': self.id,
|
||||
})
|
||||
|
||||
@api.multi
|
||||
def _partners_email_bounced_set(self, reason):
|
||||
for tracking_email in self:
|
||||
self.env['res.partner'].search([
|
||||
('email', '=ilike', tracking_email.recipient_address)
|
||||
]).email_bounced_set(tracking_email, reason)
|
||||
|
||||
@api.multi
|
||||
def smtp_error(self, mail_server, smtp_server, exception):
|
||||
self.sudo().write({
|
||||
|
@ -210,6 +200,7 @@ class MailTrackingEmail(models.Model):
|
|||
'error_description': tools.ustr(exception),
|
||||
'state': 'error',
|
||||
})
|
||||
self.sudo()._partners_email_bounced_set('error')
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
|
@ -228,7 +219,7 @@ class MailTrackingEmail(models.Model):
|
|||
partners = mail_message.notified_partner_ids | mail_message.partner_ids
|
||||
if (self.partner_id and self.partner_id not in partners):
|
||||
# If mail_message haven't tracking partner, then
|
||||
# add it in order to see his trackking status in chatter
|
||||
# add it in order to see his tracking status in chatter
|
||||
if mail_message.subtype_id:
|
||||
mail_message.sudo().write({
|
||||
'notified_partner_ids': [(4, self.partner_id.id)],
|
||||
|
@ -294,13 +285,10 @@ class MailTrackingEmail(models.Model):
|
|||
vals = tracking_email._event_prepare(event_type, metadata)
|
||||
if vals:
|
||||
event_ids += event_ids.sudo().create(vals)
|
||||
partners = self.tracking_ids_recalculate(
|
||||
'res.partner', 'email', 'tracking_email_ids',
|
||||
tracking_email.recipient_address)
|
||||
if partners:
|
||||
partners.email_score_calculate()
|
||||
else:
|
||||
_logger.debug("Concurrent event '%s' discarded", event_type)
|
||||
if event_type in {'hard_bounce', 'spam', 'reject'}:
|
||||
self.sudo()._partners_email_bounced_set(event_type)
|
||||
return event_ids
|
||||
|
||||
@api.model
|
||||
|
|
|
@ -23,7 +23,7 @@ class MailTrackingEvent(models.Model):
|
|||
date = fields.Date(
|
||||
string="Date", readonly=True, compute="_compute_date", store=True)
|
||||
tracking_email_id = fields.Many2one(
|
||||
string='Message', readonly=True,
|
||||
string='Message', readonly=True, required=True, ondelete='cascade',
|
||||
comodel_name='mail.tracking.email')
|
||||
event_type = fields.Selection(string='Event type', selection=[
|
||||
('sent', 'Sent'),
|
||||
|
|
|
@ -8,39 +8,42 @@ 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 and email_score are non-store fields in order
|
||||
# to improve performance
|
||||
# email_bounced is store=True and index=True field in order to filter
|
||||
# in tree view for processing bounces easier
|
||||
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", readonly=True, default=50.0)
|
||||
compute='_compute_tracking_emails_count', readonly=True)
|
||||
email_bounced = fields.Boolean(index=True)
|
||||
email_score = fields.Float(compute='_compute_email_score', readonly=True)
|
||||
|
||||
@api.multi
|
||||
def email_score_calculate(self):
|
||||
# This is not a compute method because is causing a inter-block
|
||||
# in mail_tracking_email PostgreSQL table
|
||||
# We suspect that tracking_email write to state field block that
|
||||
# table and then inside write ORM try to read from DB
|
||||
# tracking_email_ids because it's not in cache.
|
||||
# PostgreSQL blocks read because we have not committed yet the write
|
||||
for partner in self:
|
||||
partner.email_score = partner.tracking_email_ids.email_score()
|
||||
@api.depends('email')
|
||||
def _compute_email_score(self):
|
||||
for partner in self.filtered('email'):
|
||||
partner.email_score = self.env['mail.tracking.email'].\
|
||||
email_score_from_email(partner.email)
|
||||
|
||||
@api.one
|
||||
@api.depends('tracking_email_ids')
|
||||
@api.multi
|
||||
@api.depends('email')
|
||||
def _compute_tracking_emails_count(self):
|
||||
self.tracking_emails_count = self.env['mail.tracking.email'].\
|
||||
search_count([
|
||||
('recipient_address', '=ilike', self.email)
|
||||
])
|
||||
for partner in self:
|
||||
partner.tracking_emails_count = self.env['mail.tracking.email'].\
|
||||
search_count([
|
||||
('recipient_address', '=ilike', partner.email)
|
||||
])
|
||||
|
||||
@api.multi
|
||||
def email_bounced_set(self, tracking_email, reason):
|
||||
"""Inherit this method to make any other actions to partners"""
|
||||
partners = self.filtered(lambda r: not r.email_bounced)
|
||||
return partners.write({'email_bounced': True})
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
email = vals.get('email')
|
||||
if email is not None:
|
||||
m_track = self.env['mail.tracking.email']
|
||||
vals['tracking_email_ids'] = m_track._tracking_ids_to_write(email)
|
||||
vals['email_score'] = m_track.email_score_from_email(email)
|
||||
vals['email_bounced'] = (
|
||||
bool(email) and
|
||||
self.env['mail.tracking.email'].email_is_bounced(email))
|
||||
return super(ResPartner, self).write(vals)
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
import mock
|
||||
import base64
|
||||
import time
|
||||
from openerp import http
|
||||
from openerp.tests.common import TransactionCase
|
||||
from ..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')
|
||||
|
||||
|
@ -22,11 +22,9 @@ class FakeUserAgent(object):
|
|||
return 'Test suite'
|
||||
|
||||
|
||||
# One test case per method
|
||||
class TestMailTracking(TransactionCase):
|
||||
# Use case : Prepare some data for current test case
|
||||
def setUp(self):
|
||||
super(TestMailTracking, self).setUp()
|
||||
def setUp(self, *args, **kwargs):
|
||||
super(TestMailTracking, self).setUp(*args, **kwargs)
|
||||
self.sender = self.env['res.partner'].create({
|
||||
'name': 'Test sender',
|
||||
'email': 'sender@example.com',
|
||||
|
@ -37,12 +35,22 @@ class TestMailTracking(TransactionCase):
|
|||
'email': 'recipient@example.com',
|
||||
'notify_email': 'always',
|
||||
})
|
||||
self.request = {
|
||||
self.last_request = http.request
|
||||
http.request = type('obj', (object,), {
|
||||
'db': self.env.cr.dbname,
|
||||
'env': self.env,
|
||||
'endpoint': type('obj', (object,), {
|
||||
'routing': [],
|
||||
}),
|
||||
'httprequest': type('obj', (object,), {
|
||||
'remote_addr': '123.123.123.123',
|
||||
'user_agent': FakeUserAgent(),
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
def tearDown(self, *args, **kwargs):
|
||||
http.request = self.last_request
|
||||
return super(TestMailTracking, self).tearDown(*args, **kwargs)
|
||||
|
||||
def test_message_post(self):
|
||||
# This message will generate a notification for recipient
|
||||
|
@ -109,10 +117,15 @@ class TestMailTracking(TransactionCase):
|
|||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
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])
|
||||
res = controller.mail_tracking_open(db, tracking.id)
|
||||
self.assertEqual(image, res.response[0])
|
||||
# Two events: sent and open
|
||||
self.assertEqual(2, len(tracking.tracking_event_ids))
|
||||
# Fake event: tracking_email_id = False
|
||||
res = controller.mail_tracking_open(db, False)
|
||||
self.assertEqual(image, res.response[0])
|
||||
# Two events again because no tracking_email_id found for False
|
||||
self.assertEqual(2, len(tracking.tracking_event_ids))
|
||||
|
||||
def test_concurrent_open(self):
|
||||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
|
@ -192,31 +205,38 @@ class TestMailTracking(TransactionCase):
|
|||
self.assertEqual('error', tracking.state)
|
||||
self.assertEqual('Warning', tracking.error_type)
|
||||
self.assertEqual('Test error', tracking.error_description)
|
||||
self.assertTrue(self.recipient.email_bounced)
|
||||
|
||||
def test_partner_email_change(self):
|
||||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
tracking.event_create('open', {})
|
||||
orig_score = self.recipient.email_score
|
||||
orig_count = self.recipient.tracking_emails_count
|
||||
orig_email = self.recipient.email
|
||||
self.recipient.email = orig_email + '2'
|
||||
self.assertEqual(50.0, self.recipient.email_score)
|
||||
self.assertEqual(0, self.recipient.tracking_emails_count)
|
||||
self.recipient.email = orig_email
|
||||
self.assertEqual(orig_score, self.recipient.email_score)
|
||||
self.assertEqual(orig_count, self.recipient.tracking_emails_count)
|
||||
|
||||
def test_process_hard_bounce(self):
|
||||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
tracking.event_create('hard_bounce', {})
|
||||
self.assertEqual('bounced', tracking.state)
|
||||
self.assertTrue(self.recipient.email_score < 50.0)
|
||||
|
||||
def test_process_soft_bounce(self):
|
||||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
tracking.event_create('soft_bounce', {})
|
||||
self.assertEqual('soft-bounced', tracking.state)
|
||||
self.assertTrue(self.recipient.email_score < 50.0)
|
||||
|
||||
def test_process_delivered(self):
|
||||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
tracking.event_create('delivered', {})
|
||||
self.assertEqual('delivered', tracking.state)
|
||||
self.assertTrue(self.recipient.email_score > 50.0)
|
||||
|
||||
def test_process_deferral(self):
|
||||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
|
@ -227,35 +247,45 @@ class TestMailTracking(TransactionCase):
|
|||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
tracking.event_create('spam', {})
|
||||
self.assertEqual('spam', tracking.state)
|
||||
self.assertTrue(self.recipient.email_score < 50.0)
|
||||
|
||||
def test_process_unsub(self):
|
||||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
tracking.event_create('unsub', {})
|
||||
self.assertEqual('unsub', tracking.state)
|
||||
self.assertTrue(self.recipient.email_score < 50.0)
|
||||
|
||||
def test_process_reject(self):
|
||||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
tracking.event_create('reject', {})
|
||||
self.assertEqual('rejected', tracking.state)
|
||||
self.assertTrue(self.recipient.email_score < 50.0)
|
||||
|
||||
def test_process_open(self):
|
||||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
tracking.event_create('open', {})
|
||||
self.assertEqual('opened', tracking.state)
|
||||
self.assertTrue(self.recipient.email_score > 50.0)
|
||||
|
||||
def test_process_click(self):
|
||||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
tracking.event_create('click', {})
|
||||
self.assertEqual('opened', tracking.state)
|
||||
self.assertTrue(self.recipient.email_score > 50.0)
|
||||
|
||||
def test_process_several_bounce(self):
|
||||
for i in range(1, 10):
|
||||
mail, tracking = self.mail_send(self.recipient.email)
|
||||
tracking.event_create('hard_bounce', {})
|
||||
self.assertEqual('bounced', tracking.state)
|
||||
self.assertEqual(0.0, self.recipient.email_score)
|
||||
|
||||
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])
|
||||
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])
|
||||
|
|
|
@ -25,9 +25,24 @@
|
|||
<field name="email" position="after">
|
||||
<field name="email_score" widget="progressbar"
|
||||
attrs="{'invisible': [('email', '=', False)]}"/>
|
||||
<field name="email_bounced"
|
||||
attrs="{'invisible': [('email', '=', False)]}"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_res_partner_filter">
|
||||
<field name="name">Filter bounced partners</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_res_partner_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<filter name="type_company" position="after">
|
||||
<separator/>
|
||||
<filter string="Email bounced" name="email_bounced"
|
||||
domain="[('email', '!=' , False), ('email_bounced', '=', True)]"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -8,21 +8,27 @@ from openerp import models, api, fields
|
|||
class MailMassMailingContact(models.Model):
|
||||
_inherit = 'mail.mass_mailing.contact'
|
||||
|
||||
tracking_email_ids = fields.Many2many(
|
||||
string="Tracking emails", comodel_name="mail.tracking.email",
|
||||
readonly=True)
|
||||
email_bounced = fields.Boolean(string="Email bounced")
|
||||
email_score = fields.Float(
|
||||
string="Email score", readonly=True, default=50.0)
|
||||
string="Email score", readonly=True, store=False,
|
||||
compute='_compute_email_score')
|
||||
|
||||
@api.multi
|
||||
def email_score_calculate(self):
|
||||
for contact in self:
|
||||
contact.email_score = contact.tracking_email_ids.email_score()
|
||||
@api.depends('email')
|
||||
def _compute_email_score(self):
|
||||
for contact in self.filtered('email'):
|
||||
contact.email_score = self.env['mail.tracking.email'].\
|
||||
email_score_from_email(contact.email)
|
||||
|
||||
@api.multi
|
||||
def email_bounced_set(self, tracking_email, reason):
|
||||
return self.write({'email_bounced': True})
|
||||
|
||||
@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)
|
||||
vals['email_bounced'] = (
|
||||
bool(email) and
|
||||
self.env['mail.tracking.email'].email_is_bounced(email))
|
||||
return super(MailMassMailingContact, self).write(vals)
|
||||
|
|
|
@ -17,6 +17,7 @@ class MailTrackingEmail(models.Model):
|
|||
|
||||
@api.model
|
||||
def _statistics_link_prepare(self, tracking):
|
||||
"""Inherit this method to link other object to mail.mail.statistics"""
|
||||
return {
|
||||
'mail_tracking_id': tracking.id,
|
||||
}
|
||||
|
@ -28,25 +29,25 @@ class MailTrackingEmail(models.Model):
|
|||
if tracking.mail_stats_id:
|
||||
tracking.mail_stats_id.write(
|
||||
self._statistics_link_prepare(tracking))
|
||||
# Get partner from mail statistics
|
||||
# if mass_mailing_partner addon installed
|
||||
if ('partner_id' in tracking.mail_stats_id._fields and
|
||||
tracking.mail_stats_id.partner_id and
|
||||
not tracking.partner_id):
|
||||
tracking.partner_id = tracking.mail_stats_id.partner_id.id
|
||||
# Add this tracking to mass mailing contacts with this recipient
|
||||
self.tracking_ids_recalculate(
|
||||
'mail.mass_mailing.contact', 'email', 'tracking_email_ids',
|
||||
tracking.recipient_address, new_tracking=tracking)
|
||||
return tracking
|
||||
|
||||
@api.multi
|
||||
def _contacts_email_bounced_set(self, reason):
|
||||
for tracking_email in self:
|
||||
self.env['mail.mass_mailing.contact'].search([
|
||||
('email', '=ilike', tracking_email.recipient_address)
|
||||
]).email_bounced_set(tracking_email, reason)
|
||||
|
||||
@api.multi
|
||||
def smtp_error(self, mail_server, smtp_server, exception):
|
||||
res = super(MailTrackingEmail, self).smtp_error(
|
||||
mail_server, smtp_server, exception)
|
||||
self._contacts_email_bounced_set('error')
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def event_create(self, event_type, metadata):
|
||||
res = super(MailTrackingEmail, self).event_create(event_type, metadata)
|
||||
for tracking_email in self:
|
||||
contacts = self.tracking_ids_recalculate(
|
||||
'mail.mass_mailing.contact', 'email', 'tracking_email_ids',
|
||||
tracking_email.recipient_address)
|
||||
if contacts:
|
||||
contacts.email_score_calculate()
|
||||
if event_type in {'hard_bounce', 'spam', 'reject'}:
|
||||
self._contacts_email_bounced_set(event_type)
|
||||
return res
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
# © 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
|
||||
from openerp import api, fields, models
|
||||
|
||||
|
||||
class MailTrackingEvent(models.Model):
|
||||
_inherit = "mail.tracking.event"
|
||||
|
||||
mass_mailing_id = fields.Many2one(
|
||||
string="Mass mailing", comodel_name='mail.mass_mailing', readonly=True,
|
||||
related='tracking_email_id.mass_mailing_id', store=True)
|
||||
|
||||
@api.model
|
||||
def process_open(self, tracking_email, metadata):
|
||||
res = super(MailTrackingEvent, self).process_open(
|
||||
|
|
|
@ -2,14 +2,17 @@
|
|||
# © 2016 Antonio Espinosa - <antonio.espinosa@tecnativa.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
import mock
|
||||
from openerp.tests.common import TransactionCase
|
||||
from openerp.exceptions import Warning as UserError
|
||||
|
||||
mock_send_email = ('openerp.addons.base.ir.ir_mail_server.'
|
||||
'ir_mail_server.send_email')
|
||||
|
||||
|
||||
# One test case per method
|
||||
class TestMassMailing(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestMassMailing, self).setUp()
|
||||
def setUp(self, *args, **kwargs):
|
||||
super(TestMassMailing, self).setUp(*args, **kwargs)
|
||||
self.list = self.env['mail.mass_mailing.list'].create({
|
||||
'name': 'Test mail tracking',
|
||||
})
|
||||
|
@ -50,6 +53,21 @@ class TestMassMailing(TransactionCase):
|
|||
self.mailing.avoid_resend = False
|
||||
self.resend_mass_mailing(1, 3)
|
||||
|
||||
def test_smtp_error(self):
|
||||
with mock.patch(mock_send_email) as mock_func:
|
||||
mock_func.side_effect = Warning('Test error')
|
||||
self.mailing.send_mail()
|
||||
for stat in self.mailing.statistics_ids:
|
||||
if stat.mail_mail_id:
|
||||
stat.mail_mail_id.send()
|
||||
tracking = self.env['mail.tracking.email'].search([
|
||||
('mail_id_int', '=', stat.mail_mail_id_int),
|
||||
])
|
||||
self.assertEqual('error', tracking.state)
|
||||
self.assertEqual('Warning', tracking.error_type)
|
||||
self.assertEqual('Test error', tracking.error_description)
|
||||
self.assertTrue(self.contact_a.email_bounced)
|
||||
|
||||
def test_tracking_email_link(self):
|
||||
self.mailing.send_mail()
|
||||
for stat in self.mailing.statistics_ids:
|
||||
|
@ -90,24 +108,24 @@ class TestMassMailing(TransactionCase):
|
|||
self.assertTrue(stat.bounced)
|
||||
|
||||
def test_tracking_email_hard_bounce(self):
|
||||
self._tracking_email_bounce('hard_bounce', 'bounced')
|
||||
self._tracking_email_bounce('hard_bounce', 'bounced')
|
||||
|
||||
def test_tracking_email_soft_bounce(self):
|
||||
self._tracking_email_bounce('soft_bounce', 'soft-bounced')
|
||||
self._tracking_email_bounce('soft_bounce', 'soft-bounced')
|
||||
|
||||
def test_tracking_email_reject(self):
|
||||
self._tracking_email_bounce('reject', 'rejected')
|
||||
self._tracking_email_bounce('reject', 'rejected')
|
||||
|
||||
def test_tracking_email_spam(self):
|
||||
self._tracking_email_bounce('spam', 'spam')
|
||||
self._tracking_email_bounce('spam', 'spam')
|
||||
|
||||
def test_contact_tracking_emails(self):
|
||||
self.mailing.send_mail()
|
||||
for stat in self.mailing.statistics_ids:
|
||||
if stat.mail_mail_id:
|
||||
stat.mail_mail_id.send()
|
||||
self.assertEqual(len(self.contact_a.tracking_email_ids), 1)
|
||||
self._tracking_email_bounce('hard_bounce', 'bounced')
|
||||
self.assertTrue(self.contact_a.email_bounced)
|
||||
self.assertTrue(self.contact_a.email_score < 50.0)
|
||||
self.contact_a.email = 'other_contact_a@example.com'
|
||||
self.assertEqual(len(self.contact_a.tracking_email_ids), 0)
|
||||
self.assertFalse(self.contact_a.email_bounced)
|
||||
self.assertTrue(self.contact_a.email_score == 50.0)
|
||||
self.contact_a.email = 'contact_a@example.com'
|
||||
self.assertEqual(len(self.contact_a.tracking_email_ids), 1)
|
||||
self.assertTrue(self.contact_a.email_bounced)
|
||||
self.assertTrue(self.contact_a.email_score < 50.0)
|
||||
|
|
|
@ -10,10 +10,23 @@
|
|||
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_contact_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="opt_out" position="after">
|
||||
<field name="email_bounced"/>
|
||||
<field name="email_score" widget="progressbar"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_mail_mass_mailing_contact_search">
|
||||
<field name="name">Filter bounced contacts</field>
|
||||
<field name="model">mail.mass_mailing.contact</field>
|
||||
<field name="inherit_id" ref="mass_mailing.view_mail_mass_mailing_contact_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<filter name="not_opt_out" position="after">
|
||||
<filter string="Email bounced" name="email_bounced"
|
||||
domain="[('email_bounced', '=', True)]"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -15,5 +15,32 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_view_mail_tracking_email">
|
||||
<field name="name">Mail tracking emails</field>
|
||||
<field name="res_model">mail.tracking.email</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('mass_mailing_id', '!=', False)]</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_view_mail_tracking_event">
|
||||
<field name="name">Mail tracking events</field>
|
||||
<field name="res_model">mail.tracking.event</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('mass_mailing_id', '!=', False)]</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Mail tracking" id="mail_tracking_menu"
|
||||
parent="base.marketing_menu" sequence="50"/>
|
||||
|
||||
<menuitem name="Emails" id="mail_tracking_email_menu"
|
||||
parent="mail_tracking_menu" sequence="1"
|
||||
action="action_view_mail_tracking_email"/>
|
||||
|
||||
<menuitem name="Events" id="mail_tracking_event_menu"
|
||||
parent="mail_tracking_menu" sequence="2"
|
||||
action="action_view_mail_tracking_event"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
Loading…
Reference in New Issue