PURPOSE
Improve modeling and performances of mass mailing by manually updating trace
status instead of using complex computed fields and lessening fields usage.
Remove some fields and keep only relevant metrics to simplify and clean trace
model.
SPECIFICATIONS
In this commit we clean the way trace state is managed. Currently it is a
computed field based on several datetime fields. However this generates a
lot of noise in the table as well as unnecessary computation
* there are several columns (one for each state) storing datetime at
which status was reached. Generally only 2 or 3 contain relevant
information;
* state could be set in code directly to avoid a computed field based on
many triggers;
* recomputing it each time a date changes is not necessarily necessary;
* state value can always be updated manually as this is main done through
some automated server update (mailgateway, link clicks, ...);
As trace states and its triggers should not be updated manually it is better
to synchronize it in code flow. When there is an exception or update done
through sending or gateway status is updated as well accordingly. Various
datetime fields are also updated at the same time. In order to align with
notification model mail and sms trace status are updated to a classic field.
Only last status update is now kept as there is no need to store the entire
history of status change.
We keep only a datetime for relevant metrics: open, reply and click. Other
datetime bring no real value. Knowing when a trace was in error or bounced
is not necessary. Indeed exception generally indicates a server issue (at
sending), cancel indicates a data issue (at sending) and bounce depends on
customer email server.
Status update is removed as using write_date is sufficient. Once created
traces are updated only when an external event occurs (opened, replied, ...).
It allows to simplify trace model.
We also rename ignored field into canceled to match naming use through mail
and sms.
QUERY COUNTERS
This change has some positive update on query counters when sending mailings
as traces have less unnecessary status update compared to priori this change.
LINKS
Task ID-2377974
Community PR odoo/odoo#61467
Enterprise PR odoo/enterprise#14633
Upgrade PR odoo/upgrade#1907
88 lines
4.7 KiB
Python
88 lines
4.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import datetime
|
|
|
|
from odoo import api, models, fields, tools
|
|
|
|
BLACKLIST_MAX_BOUNCED_LIMIT = 5
|
|
|
|
|
|
class MailThread(models.AbstractModel):
|
|
""" Update MailThread to add the support of bounce management in mass mailing traces. """
|
|
_inherit = 'mail.thread'
|
|
|
|
@api.model
|
|
def _message_route_process(self, message, message_dict, routes):
|
|
""" Override to update the parent mailing traces. The parent is found
|
|
by using the References header of the incoming message and looking for
|
|
matching message_id in mailing.trace. """
|
|
if routes:
|
|
# even if 'reply_to' in ref (cfr mail/mail_thread) that indicates a new thread redirection
|
|
# (aka bypass alias configuration in gateway) consider it as a reply for statistics purpose
|
|
thread_references = message_dict['references'] or message_dict['in_reply_to']
|
|
msg_references = tools.mail_header_msgid_re.findall(thread_references)
|
|
if msg_references:
|
|
self.env['mailing.trace'].set_opened(domain=[('message_id', 'in', msg_references)])
|
|
self.env['mailing.trace'].set_replied(domain=[('message_id', 'in', msg_references)])
|
|
return super(MailThread, self)._message_route_process(message, message_dict, routes)
|
|
|
|
def message_post_with_template(self, template_id, **kwargs):
|
|
# avoid having message send through `message_post*` methods being implicitly considered as
|
|
# mass-mailing
|
|
no_massmail = self.with_context(
|
|
default_mass_mailing_name=False,
|
|
default_mass_mailing_id=False,
|
|
)
|
|
return super(MailThread, no_massmail).message_post_with_template(template_id, **kwargs)
|
|
|
|
@api.model
|
|
def _routing_handle_bounce(self, email_message, message_dict):
|
|
""" In addition, an auto blacklist rule check if the email can be blacklisted
|
|
to avoid sending mails indefinitely to this email address.
|
|
This rule checks if the email bounced too much. If this is the case,
|
|
the email address is added to the blacklist in order to avoid continuing
|
|
to send mass_mail to that email address. If it bounced too much times
|
|
in the last month and the bounced are at least separated by one week,
|
|
to avoid blacklist someone because of a temporary mail server error,
|
|
then the email is considered as invalid and is blacklisted."""
|
|
super(MailThread, self)._routing_handle_bounce(email_message, message_dict)
|
|
|
|
bounced_email = message_dict['bounced_email']
|
|
bounced_msg_id = message_dict['bounced_msg_id']
|
|
bounced_partner = message_dict['bounced_partner']
|
|
|
|
if bounced_msg_id:
|
|
self.env['mailing.trace'].set_bounced(domain=[('message_id', 'in', bounced_msg_id)])
|
|
if bounced_email:
|
|
three_months_ago = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(weeks=13))
|
|
stats = self.env['mailing.trace'].search(['&', '&', ('trace_status', '=', 'bounce'), ('write_date', '>', three_months_ago), ('email', '=ilike', bounced_email)]).mapped('write_date')
|
|
if len(stats) >= BLACKLIST_MAX_BOUNCED_LIMIT and (not bounced_partner or any(p.message_bounce >= BLACKLIST_MAX_BOUNCED_LIMIT for p in bounced_partner)):
|
|
if max(stats) > min(stats) + datetime.timedelta(weeks=1):
|
|
blacklist_rec = self.env['mail.blacklist'].sudo()._add(bounced_email)
|
|
blacklist_rec._message_log(
|
|
body='This email has been automatically blacklisted because of too much bounced.')
|
|
|
|
@api.model
|
|
def message_new(self, msg_dict, custom_values=None):
|
|
""" Overrides mail_thread message_new that is called by the mailgateway
|
|
through message_process.
|
|
This override updates the document according to the email.
|
|
"""
|
|
defaults = {}
|
|
|
|
if issubclass(type(self), self.pool['utm.mixin']):
|
|
thread_references = msg_dict.get('references', '') or msg_dict.get('in_reply_to', '')
|
|
msg_references = tools.mail_header_msgid_re.findall(thread_references)
|
|
if msg_references:
|
|
traces = self.env['mailing.trace'].search([('message_id', 'in', msg_references)], limit=1)
|
|
if traces:
|
|
defaults['campaign_id'] = traces.campaign_id.id
|
|
defaults['source_id'] = traces.mass_mailing_id.source_id.id
|
|
defaults['medium_id'] = traces.mass_mailing_id.medium_id.id
|
|
|
|
if custom_values:
|
|
defaults.update(custom_values)
|
|
|
|
return super(MailThread, self).message_new(msg_dict, custom_values=defaults)
|