From 92fe83ecf8b4e64a42185bc056ba3c865df753cb Mon Sep 17 00:00:00 2001
From: David Beguin <dbe@odoo.com>
Date: Fri, 12 Oct 2018 15:33:15 +0000
Subject: [PATCH] [IMP] mail, mass_mailing : reset bounce on mail reception

PURPOSE

Add some improvements in mail gateway: remove private discussion, improve
bounce management, allow resetting bounce counters, improve automatic set or
reset of blacklists and ease mass mailing inheritance.

SPECIFICATIONS

The number of message bounce is incremented each time the email bounce
on a specific email address. To have a correct information, we should
reset to zero this counter if we receive a mail from this address.
Indeed, if we receive an email from an email address, this email is
active and the message bounce number (if > 0) is not relevant anymore.

Only models inheriting form blacklist are impacted by this improvement
in order to limit a bit side effect.

Also, add a check in autoblacklist rule. No need to check the stats
if message_bounce is < 5.

This commit introduces

  * ``_routing_reset_bounce`` routing method managing the bounce reset;
  * ``_message_reset_bounce`` model method that resets bounce counter
    in blacklist enabled models;

LINKS

Related to task ID : 1893155
Linked to PR #33340
---
 addons/mail/models/mail_blacklist.py      |  6 +++++
 addons/mail/models/mail_thread.py         | 28 +++++++++++++++++++++++
 addons/mass_mailing/models/mail_thread.py |  3 ++-
 3 files changed, 36 insertions(+), 1 deletion(-)

diff --git a/addons/mail/models/mail_blacklist.py b/addons/mail/models/mail_blacklist.py
index 16949dca8e8a..592928c3a5f9 100644
--- a/addons/mail/models/mail_blacklist.py
+++ b/addons/mail/models/mail_blacklist.py
@@ -156,3 +156,9 @@ class MailBlackListMixin(models.AbstractModel):
         super(MailBlackListMixin, self)._message_receive_bounce(email, partner)
         for record in self:
             record.message_bounce = record.message_bounce + 1
+
+    def _message_reset_bounce(self, email):
+        """ Override of mail.thread generic method. Purpose is to reset the
+        bounce counter of the record. """
+        super(MailBlackListMixin, self)._message_reset_bounce(email)
+        self.write({'message_bounce': 0})
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index 42916d46b5c6..bdfd821967cc 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -815,6 +815,22 @@ class MailThread(models.AbstractModel):
 
         return (model, thread_id, route[2], route[3], route[4])
 
+    @api.model
+    def _routing_reset_bounce(self, email_message, message_dict):
+        """Called by ``message_process`` when a new mail is received from an email address.
+        If the email is related to a partner, we consider that the number of message_bounce
+        is not relevant anymore as the email is valid - as we received an email from this
+        address. The model is here hardcoded because we cannot know with which model the
+        incomming mail match. We consider that if a mail arrives, we have to clear bounce for
+        each model having bounce count.
+
+        :param email_from: email address that sent the incoming email."""
+        valid_email = message_dict['email_from']
+        if valid_email:
+            bl_models = self.env['ir.model'].sudo().search(['&', ('is_mail_blacklist', '=', True), ('model', '!=', 'mail.thread.blacklist')])
+            for model in [bl_model for bl_model in bl_models if bl_model.model in self.env]:  # transient test mode
+                self.env[model.model].sudo().search([('message_bounce', '>', 0), ('email_normalized', '=', valid_email)])._message_reset_bounce(valid_email)
+
     @api.model
     def message_route(self, message, message_dict, model=None, thread_id=None, custom_values=None):
         """ Attempt to figure out the correct target model, thread_id,
@@ -883,6 +899,7 @@ class MailThread(models.AbstractModel):
         #       See http://datatracker.ietf.org/doc/rfc3462/?include_text=1
         #        As all MTA does not respect this RFC (googlemail is one of them),
         #       we also need to verify if the message come from "mailer-daemon"
+        #    If not a bounce: reset bounce information
         if bounce_alias and bounce_alias in email_to_localpart:
             bounce_re = re.compile("%s\+(\d+)-?([\w.]+)?-?(\d+)?" % re.escape(bounce_alias), re.UNICODE)
             bounce_match = bounce_re.search(email_to)
@@ -892,6 +909,7 @@ class MailThread(models.AbstractModel):
         if message.get_content_type() == 'multipart/report' or email_from_localpart == 'mailer-daemon':
             self._routing_handle_bounce(message, message_dict)
             return []
+        self._routing_reset_bounce(message, message_dict)
 
         # 1. Handle reply
         #    if destination = alias with different model -> consider it is a forward and not a reply
@@ -1149,6 +1167,16 @@ class MailThread(models.AbstractModel):
         """
         pass
 
+    def _message_reset_bounce(self, email):
+        """Called by ``message_process`` when an email is considered as not being
+        a bounce. The default behavior is to do nothing. This method is meant to
+        be overridden in various modules to add some specific behavior like
+        blacklist management.
+
+        :param string email: email for which to reset bounce information
+        """
+        pass
+
     def _message_parse_extract_payload_postprocess(self, message, payload_dict):
         """ Perform some cleaning / postprocess in the body and attachments
         extracted from the email. Note that this processing is specific to the
diff --git a/addons/mass_mailing/models/mail_thread.py b/addons/mass_mailing/models/mail_thread.py
index 09c6ba536261..93f657da3cf9 100644
--- a/addons/mass_mailing/models/mail_thread.py
+++ b/addons/mass_mailing/models/mail_thread.py
@@ -50,13 +50,14 @@ class MailThread(models.AbstractModel):
 
         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(mail_message_ids=[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(['&', ('bounced', '>', three_months_ago), ('email', '=ilike', bounced_email)]).mapped('bounced')
-            if len(stats) >= BLACKLIST_MAX_BOUNCED_LIMIT:
+            if len(stats) >= BLACKLIST_MAX_BOUNCED_LIMIT and (not bounced_partner or bounced_partner.message_bounce >= BLACKLIST_MAX_BOUNCED_LIMIT):
                 if max(stats) > min(stats) + datetime.timedelta(weeks=1):
                     blacklist_rec = self.env['mail.blacklist'].sudo()._add(bounced_email)
                     blacklist_rec._message_log(
-- 
GitLab