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