From a1d6064dcc6d349c02556453e1aa052e60db41c0 Mon Sep 17 00:00:00 2001 From: David Beguin <dbe@odoo.com> Date: Wed, 25 Jul 2018 13:03:57 +0200 Subject: [PATCH] [IMP] mail : auto-blacklist rule when too much email bounced In order to avoid sending mail indefinitely to a wrong email address, Take the mail statistics for the recipient of the last 3 month : if more than 5 mails bounced (with interval of more than 1 week) the email is blacklisted. --- addons/mail/models/mail_thread.py | 17 ++++ addons/mass_mailing/models/mail_thread.py | 5 +- addons/mass_mailing/models/mass_mailing.py | 4 +- .../mass_mailing/models/mass_mailing_stats.py | 1 + .../tests/test_mail_auto_blacklist.py | 84 +++++++++++++++++++ .../wizard/mail_compose_message.py | 3 +- 6 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 addons/mass_mailing/tests/test_mail_auto_blacklist.py diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index f086a3220a78..cec45f30becf 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -31,6 +31,7 @@ from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) +BLACKLIST_MAX_BOUNCED_LIMIT = 5 class MailThread(models.AbstractModel): @@ -1452,6 +1453,14 @@ class MailThread(models.AbstractModel): Mail Returned to Sender) is received for an existing thread. The default behavior is to check is an integer ``message_bounce`` column exists. If it is the case, its content is incremented. + 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. :param mail_id: ID of the sent email that bounced. It may not exist anymore but it could be usefull if the information was kept. This is @@ -1461,6 +1470,14 @@ class MailThread(models.AbstractModel): if 'message_bounce' in self._fields: for record in self: record.message_bounce = record.message_bounce + 1 + three_months_ago = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(weeks=13)) + stats = self.env['mail.mail.statistics']\ + .search(['&', ('bounced', '>', three_months_ago), ('email','=ilike',email)])\ + .mapped('bounced') + if len(stats) >= BLACKLIST_MAX_BOUNCED_LIMIT: + if max(stats) > min(stats) + datetime.timedelta(weeks=1): + blacklist_rec = self.env['mail.blacklist'].sudo()._add(email) + blacklist_rec._message_log('This email has been automatically blacklisted because of too much bounced.') def _message_extract_payload_postprocess(self, message, body, attachments): """ Perform some cleaning / postprocess in the body and attachments diff --git a/addons/mass_mailing/models/mail_thread.py b/addons/mass_mailing/models/mail_thread.py index 8e4ee298c678..75f70f273fa2 100644 --- a/addons/mass_mailing/models/mail_thread.py +++ b/addons/mass_mailing/models/mail_thread.py @@ -3,8 +3,9 @@ import logging import re +import datetime -from odoo import api, models, tools +from odoo import api, models, tools, fields from odoo.tools import decode_smtp_header, decode_message_header _logger = logging.getLogger(__name__) @@ -49,4 +50,4 @@ class MailThread(models.AbstractModel): default_mass_mailing_name=False, default_mass_mailing_id=False, ) - return super(MailThread, no_massmail).message_post_with_template(template_id, **kwargs) + return super(MailThread, no_massmail).message_post_with_template(template_id, **kwargs) \ No newline at end of file diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 358fd79f2fb2..1d946765102d 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -713,10 +713,10 @@ class MassMailing(models.Model): # avoid loading a large number of records in memory # + use a basic heuristic for extracting emails query = """ - SELECT lower(substring(%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)')) + SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)')) FROM mail_mail_statistics s JOIN %(target)s t ON (s.res_id = t.id) - WHERE substring(%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL + WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL """ if self.mass_mailing_campaign_id.unique_ab_testing: query +=""" diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mass_mailing_stats.py index 190ed75805dd..062c29543ee5 100644 --- a/addons/mass_mailing/models/mass_mailing_stats.py +++ b/addons/mass_mailing/models/mass_mailing_stats.py @@ -57,6 +57,7 @@ class MailMailStats(models.Model): help='Last state update of the mail', store=True) recipient = fields.Char(compute="_compute_recipient") + email = fields.Char(string="Recipient email address") @api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception', 'ignored') def _compute_state(self): diff --git a/addons/mass_mailing/tests/test_mail_auto_blacklist.py b/addons/mass_mailing/tests/test_mail_auto_blacklist.py new file mode 100644 index 000000000000..86126487cd90 --- /dev/null +++ b/addons/mass_mailing/tests/test_mail_auto_blacklist.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.tests import common +import datetime + +class TestAutoBlacklist(common.TransactionCase): + + def test_mail_bounced_auto_blacklist(self): + mass_mailing_contacts = self.env['mail.mass_mailing.contact'] + mass_mailing = self.env['mail.mass_mailing'] + mail_blacklist = self.env['mail.blacklist'] + mail_statistics = self.env['mail.mail.statistics'] + mail_thread = self.env['mail.thread'] + + # create mailing contact record + self.mailing_contact_1 = mass_mailing_contacts.create({'name': 'test email 1', 'email': 'test1@email.com'}) + + # create bounced history + mail_statistics.create({ + 'model': 'mail.mass_mailing.contact', + 'res_id': self.mailing_contact_1.id, + 'bounced': datetime.datetime.now() - datetime.timedelta(weeks=2), + 'email': self.mailing_contact_1.email + }) + self.mailing_contact_1.message_receive_bounce(self.mailing_contact_1.email, self.mailing_contact_1) + mail_statistics.create({ + 'model': 'mail.mass_mailing.contact', + 'res_id': self.mailing_contact_1.id, + 'bounced': datetime.datetime.now() - datetime.timedelta(weeks=3), + 'email': self.mailing_contact_1.email + }) + self.mailing_contact_1.message_receive_bounce(self.mailing_contact_1.email, self.mailing_contact_1) + mail_statistics.create({ + 'model': 'mail.mass_mailing.contact', + 'res_id': self.mailing_contact_1.id, + 'bounced': datetime.datetime.now() - datetime.timedelta(weeks=4), + 'email': self.mailing_contact_1.email + }) + self.mailing_contact_1.message_receive_bounce(self.mailing_contact_1.email, self.mailing_contact_1) + mail_statistics.create({ + 'model': 'mail.mass_mailing.contact', + 'res_id': self.mailing_contact_1.id, + 'bounced': datetime.datetime.now() - datetime.timedelta(weeks=5), + 'email': self.mailing_contact_1.email + }) + self.mailing_contact_1.message_receive_bounce(self.mailing_contact_1.email, self.mailing_contact_1) + + + # create mass mailing record + self.mass_mailing = mass_mailing.create({ + 'name': 'test', + 'mailing_domain': [('id', 'in', + [self.mailing_contact_1.id])], + 'body_html': 'This is a bounced mail for auto blacklist demo'}) + self.mass_mailing.put_in_queue() + res_ids = self.mass_mailing.get_remaining_recipients() + composer_values = { + 'body': self.mass_mailing.convert_links()[self.mass_mailing.id], + 'subject': self.mass_mailing.name, + 'model': self.mass_mailing.mailing_model_real, + 'email_from': self.mass_mailing.email_from, + 'composition_mode': 'mass_mail', + 'mass_mailing_id': self.mass_mailing.id, + 'mailing_list_ids': [(4, l.id) for l in self.mass_mailing.contact_list_ids], + } + composer = self.env['mail.compose.message'].with_context( + active_ids=res_ids, + mass_mailing_seen_list=self.mass_mailing._get_seen_list() + ).create(composer_values) + composer.send_mail() + + mail_statistics.create({ + 'model': 'mail.mass_mailing.contact', + 'res_id': self.mailing_contact_1.id, + 'bounced': datetime.datetime.now(), + 'email': self.mailing_contact_1.email + }) + # call bounced + self.mailing_contact_1.message_receive_bounce(self.mailing_contact_1.email, self.mailing_contact_1) + + # check blacklist + blacklist_record = mail_blacklist.search([('email', '=', self.mailing_contact_1.email)]) + self.assertEqual(len(blacklist_record), 1, + 'The email %s must be blacklisted' % self.mailing_contact_1.email) diff --git a/addons/mass_mailing/wizard/mail_compose_message.py b/addons/mass_mailing/wizard/mail_compose_message.py index 1c326e700b0f..737485978d86 100644 --- a/addons/mass_mailing/wizard/mail_compose_message.py +++ b/addons/mass_mailing/wizard/mail_compose_message.py @@ -73,7 +73,8 @@ class MailComposeMessage(models.TransientModel): stat_vals = { 'model': self.model, 'res_id': res_id, - 'mass_mailing_id': mass_mailing.id + 'mass_mailing_id': mass_mailing.id, + 'email': mail_to, } if mail_values.get('body_html') and mass_mail_layout: mail_values['body_html'] = mass_mail_layout.render({'body': mail_values['body_html']}, engine='ir.qweb', minimal_qcontext=True) -- GitLab