diff options
| -rw-r--r-- | addons/mail/models/mail_blacklist.py | 4 | ||||
| -rw-r--r-- | addons/mail/models/mail_channel.py | 4 | ||||
| -rw-r--r-- | addons/mail/models/mail_thread.py | 156 | ||||
| -rw-r--r-- | addons/mass_mailing/models/mail_thread.py | 47 | ||||
| -rw-r--r-- | addons/test_mail/data/test_mail_data.py | 2 | ||||
| -rw-r--r-- | addons/test_mail/tests/test_mail_gateway.py | 108 | ||||
| -rw-r--r-- | addons/test_mass_mailing/tests/test_mail_auto_blacklist.py | 64 |
7 files changed, 250 insertions, 135 deletions
diff --git a/addons/mail/models/mail_blacklist.py b/addons/mail/models/mail_blacklist.py index f2e8dddb65c..16949dca8e8 100644 --- a/addons/mail/models/mail_blacklist.py +++ b/addons/mail/models/mail_blacklist.py @@ -150,9 +150,9 @@ class MailBlackListMixin(models.AbstractModel): for record in self: record.is_blacklisted = record.email_normalized in blacklist - def _message_receive_bounce(self, email, partner, mail_id=None): + def _message_receive_bounce(self, email, partner): """ Override of mail.thread generic method. Purpose is to increment the bounce counter of the record. """ - super(MailBlackListMixin, self)._message_receive_bounce(email, partner, mail_id=mail_id) + super(MailBlackListMixin, self)._message_receive_bounce(email, partner) for record in self: record.message_bounce = record.message_bounce + 1 diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 974897c3c1c..0131981abe3 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -306,12 +306,12 @@ class Channel(models.Model): headers['X-Forge-To'] = list_to return headers - def _message_receive_bounce(self, email, partner, mail_id=None): + def _message_receive_bounce(self, email, partner): """ Override bounce management to unsubscribe bouncing addresses """ for p in partner: if p.message_bounce >= self.MAX_BOUNCE_LIMIT: self._action_unfollow(p) - return super(Channel, self)._message_receive_bounce(email, partner, mail_id=mail_id) + return super(Channel, self)._message_receive_bounce(email, partner) def _notify_email_recipient_values(self, recipient_ids): # Excluded Blacklisted diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 7a8788f19bc..42916d46b5c 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -673,6 +673,60 @@ class MailThread(models.AbstractModel): self.env['mail.mail'].create(bounce_mail_values).send() @api.model + def _routing_handle_bounce(self, email_message, message_dict): + """ Handle bounce of incoming email. Based on values of the bounce (email + and related partner, send message and its messageID) + + * find blacklist-enabled records with email_normalized = bounced email + and call ``_message_receive_bounce`` on each of them to propagate + bounce information through various records linked to same email; + * if not already done (i.e. if original record is not blacklist enabled + like a bounce on an applicant), find record linked to bounced message + and call ``_message_receive_bounce``; + + :param email_message: incoming email; + :type email_message: email.message; + :param message_dict: dictionary holding already-parsed values and in + which bounce-related values will be added; + :type message_dict: dictionary; + """ + bounced_record, bounced_record_done = False, False + bounced_email, bounced_partner = message_dict['bounced_email'], message_dict['bounced_partner'] + bounced_msg_id, bounced_message = message_dict['bounced_msg_id'], message_dict['bounced_message'] + + if bounced_email: + bounced_model, bounced_res_id = bounced_message.model, bounced_message.res_id + + if bounced_model and bounced_model in self.env and bounced_res_id: + bounced_record = self.env[bounced_model].sudo().browse(bounced_res_id).exists() + + 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 + rec_bounce_w_email = self.env[model.model].sudo().search([('email_normalized', '=', bounced_email)]) + rec_bounce_w_email._message_receive_bounce(bounced_email, bounced_partner) + bounced_record_done = bool(bounced_record and model.model == bounced_model and bounced_record in rec_bounce_w_email) + + # set record as bounced unless already done due to blacklist mixin + if bounced_record and not bounced_record_done and issubclass(type(bounced_record), self.pool['mail.thread']): + bounced_record._message_receive_bounce(bounced_email, bounced_partner) + + if bounced_partner and bounced_message: + self.env['mail.notification'].sudo().search([ + ('mail_message_id', '=', bounced_message.id), + ('res_partner_id', 'in', bounced_partner.ids)] + ).write({'notification_status': 'bounce'}) + + if bounced_record: + _logger.info('Routing mail from %s to %s with Message-Id %s: not routing bounce email from %s replying to %s (model %s ID %s)', + message_dict['email_from'], message_dict['to'], message_dict['message_id'], bounced_email, bounced_msg_id, bounced_model, bounced_res_id) + elif bounced_email: + _logger.info('Routing mail from %s to %s with Message-Id %s: not routing bounce email from %s replying to %s (no document found)', + message_dict['email_from'], message_dict['to'], message_dict['message_id'], bounced_email, bounced_msg_id) + else: + _logger.info('Routing mail from %s to %s with Message-Id %s: not routing bounce email.', + message_dict['email_from'], message_dict['to'], message_dict['message_id']) + + @api.model def _routing_check_route(self, message, message_dict, route, raise_exception=True): """ Verify route validity. Check and rules: 1 - if thread_id -> check that document effectively exists; otherwise @@ -822,57 +876,21 @@ class MailThread(models.AbstractModel): # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value. rcpt_tos_localparts = [e.split('@')[0].lower() for e in tools.email_split(message_dict['recipients'])] - # 0. Verify whether this is a bounced email and use it to collect bounce data and update notifications for customers + # 0. Handle bounce: verify whether this is a bounced email and use it to collect bounce data and update notifications for customers + # Bounce regex: typical form of bounce is bounce_alias+128-crm.lead-34@domain + # group(1) = the mail ID; group(2) = the model (if any); group(3) = the record ID + # Bounce message (not alias) + # 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 bounce_alias and bounce_alias in email_to_localpart: - # Bounce regex: typical form of bounce is bounce_alias+128-crm.lead-34@domain - # group(1) = the mail ID; group(2) = the model (if any); group(3) = the record ID bounce_re = re.compile("%s\+(\d+)-?([\w.]+)?-?(\d+)?" % re.escape(bounce_alias), re.UNICODE) bounce_match = bounce_re.search(email_to) - if bounce_match: - bounced_mail_id, bounced_model, bounced_thread_id = bounce_match.group(1), bounce_match.group(2), bounce_match.group(3) - - email_part = next((part for part in message.walk() if part.get_content_type() == 'message/rfc822'), None) - dsn_part = next((part for part in message.walk() if part.get_content_type() == 'message/delivery-status'), None) - - partners, partner_address = self.env['res.partner'], False - if dsn_part and len(dsn_part.get_payload()) > 1: - dsn = dsn_part.get_payload()[1] - final_recipient_data = tools.decode_message_header(dsn, 'Final-Recipient') - partner_address = final_recipient_data.split(';', 1)[1].strip() - if partner_address: - partners = partners.sudo().search([('email', '=', partner_address)]) - for partner in partners: - partner._message_receive_bounce(partner_address, partner, mail_id=bounced_mail_id) - - mail_message = self.env['mail.message'] - if email_part: - email = email_part.get_payload()[0] - bounced_message_id = tools.mail_header_msgid_re.findall(tools.decode_message_header(email, 'Message-Id')) - mail_message = self.env['mail.message'].sudo().search([('message_id', 'in', bounced_message_id)]) - - if partners and mail_message: - notifications = self.env['mail.notification'].sudo().search([ - ('mail_message_id', '=', mail_message.id), - ('res_partner_id', 'in', partners.ids)]) - notifications.write({ - 'notification_status': 'bounce' - }) - - if bounced_model in self.env and hasattr(self.env[bounced_model], '_message_receive_bounce') and bounced_thread_id: - self.env[bounced_model].browse(int(bounced_thread_id))._message_receive_bounce(partner_address, partners, mail_id=bounced_mail_id) - - _logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s: dest %s (partner %s)', - email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id, partner_address, partners) + self._routing_handle_bounce(message, message_dict) return [] - - # 0. First check if this is a bounce message or not. - # 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 message.get_content_type() == 'multipart/report' or email_from_localpart == 'mailer-daemon': - _logger.info('Routing mail with Message-Id %s: not routing bounce email from %s to %s', - message_id, email_from, email_to) + self._routing_handle_bounce(message, message_dict) return [] # 1. Handle reply @@ -1003,7 +1021,7 @@ class MailThread(models.AbstractModel): post_params = dict(subtype_id=subtype_id, partner_ids=partner_ids, **message_dict) # remove computational values not stored on mail.message and avoid warnings when creating it - for x in ('from', 'to', 'cc', 'recipients', 'references', 'in_reply_to'): + for x in ('from', 'to', 'cc', 'recipients', 'references', 'in_reply_to', 'bounced_email', 'bounced_message', 'bounced_msg_id', 'bounced_partner'): post_params.pop(x, None) new_msg = False if thread._name == 'mail.thread': # message with parent_id not linked to record @@ -1118,7 +1136,7 @@ class MailThread(models.AbstractModel): self.write(update_vals) return True - def _message_receive_bounce(self, email, partner, mail_id=None): + def _message_receive_bounce(self, email, partner): """Called by ``message_process`` when a bounce email (such as Undelivered Mail Returned to Sender) is received for an existing thread. The default behavior is to do nothing. This method is meant to be overridden in various @@ -1126,11 +1144,8 @@ class MailThread(models.AbstractModel): mailing statistics update. check is an integer ``message_bounce`` column exists. If it is the case, its content is incremented. - :param record partner: partner matching the bounced email address, if any; :param string email: email that caused the bounce; - :param mail_id: ID of the sent email that bounced. It may not exist anymore - but it could be useful if the information was kept. This is - used notably in mass mailing; + :param record partner: partner matching the bounced email address, if any; """ pass @@ -1247,6 +1262,44 @@ class MailThread(models.AbstractModel): return self._message_parse_extract_payload_postprocess(message, {'body': body, 'attachments': attachments}) + def _message_parse_extract_bounce(self, email_message, message_dict): + """ Parse email and extract bounce information to be used in future + processing. + + :param email_message: an email.message instance; + :param message_dict: dictionary holding already-parsed values and in + which bounce-related values will be added; + """ + if not isinstance(email_message, Message): + raise TypeError('message must be an email.message.Message at this point') + + email_part = next((part for part in email_message.walk() if part.get_content_type() == 'message/rfc822'), None) + dsn_part = next((part for part in email_message.walk() if part.get_content_type() == 'message/delivery-status'), None) + + bounced_email = False + bounced_partner = self.env['res.partner'].sudo() + if dsn_part and len(dsn_part.get_payload()) > 1: + dsn = dsn_part.get_payload()[1] + final_recipient_data = tools.decode_message_header(dsn, 'Final-Recipient') + bounced_email = tools.email_normalize(final_recipient_data.split(';', 1)[1].strip()) + if bounced_email: + bounced_partner = self.env['res.partner'].sudo().search([('email_normalized', '=', bounced_email)]) + + bounced_msg_id = False + bounced_message = self.env['mail.message'].sudo() + if email_part: + email = email_part.get_payload()[0] + bounced_msg_id = tools.mail_header_msgid_re.findall(tools.decode_message_header(email, 'Message-Id')) + if bounced_msg_id: + bounced_message = self.env['mail.message'].sudo().search([('message_id', 'in', bounced_msg_id)]) + + return { + 'bounced_email': bounced_email, + 'bounced_partner': bounced_partner, + 'bounced_msg_id': bounced_msg_id, + 'bounced_message': bounced_message, + } + @api.model def message_parse(self, message, save_original=False): """ Parses an email.message.Message representing an RFC-2822 email @@ -1354,6 +1407,7 @@ class MailThread(models.AbstractModel): msg_dict['internal'] = parent_ids.subtype_id and parent_ids.subtype_id.internal or False msg_dict.update(self._message_parse_extract_payload(message, save_original=save_original)) + msg_dict.update(self._message_parse_extract_bounce(message, msg_dict)) return msg_dict # ------------------------------------------------------ diff --git a/addons/mass_mailing/models/mail_thread.py b/addons/mass_mailing/models/mail_thread.py index 28b02921801..09c6ba53626 100644 --- a/addons/mass_mailing/models/mail_thread.py +++ b/addons/mass_mailing/models/mail_thread.py @@ -17,22 +17,6 @@ class MailThread(models.AbstractModel): _inherit = 'mail.thread' @api.model - def message_route(self, message, message_dict, model=None, thread_id=None, custom_values=None): - """ Override to udpate mass mailing traces based on bounce emails """ - bounce_alias = self.env['ir.config_parameter'].sudo().get_param("mail.bounce.alias") - email_to = decode_message_header(message, 'To') - email_to_localpart = (tools.email_split(email_to) or [''])[0].split('@', 1)[0].lower() - - 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) - if bounce_match: - bounced_mail_id = bounce_match.group(1) - self.env['mailing.trace'].set_bounced(mail_mail_ids=[bounced_mail_id]) - - return super(MailThread, self).message_route(message, message_dict, model, thread_id, custom_values) - - @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 @@ -52,8 +36,9 @@ class MailThread(models.AbstractModel): ) return super(MailThread, no_massmail).message_post_with_template(template_id, **kwargs) - def _message_receive_bounce(self, email, partner, mail_id=None): - """In addition, an auto blacklist rule check if the email can be blacklisted + @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 @@ -61,14 +46,18 @@ class MailThread(models.AbstractModel): 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)._message_receive_bounce(email, partner, mail_id=None) - - three_months_ago = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(weeks=13)) - traces = self.env['mailing.trace'] \ - .search(['&', ('bounced', '>', three_months_ago), ('email', '=ilike', email)]) \ - .mapped('bounced') - if len(traces) >= BLACKLIST_MAX_BOUNCED_LIMIT: - if max(traces) > min(traces) + datetime.timedelta(weeks=1): - blacklist_rec = self.env['mail.blacklist'].sudo()._add(email) - blacklist_rec._message_log( - body='This email has been automatically blacklisted because of too much bounced.') + super(MailThread, self)._routing_handle_bounce(email_message, message_dict) + + bounced_email = message_dict['bounced_email'] + bounced_msg_id = message_dict['bounced_msg_id'] + + 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 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.') diff --git a/addons/test_mail/data/test_mail_data.py b/addons/test_mail/data/test_mail_data.py index eb2b53dbf57..cfce6317440 100644 --- a/addons/test_mail/data/test_mail_data.py +++ b/addons/test_mail/data/test_mail_data.py @@ -744,7 +744,7 @@ DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=test.ironsky; s=mail; h+BQVyBKIvr/LDFPSN2hQmfXWwWupm1lgUhJ07T4= Content-Type: multipart/mixed; boundary="===============7355787381227985247==" MIME-Version: 1.0 -Message-Id: <288877393590487.1555335624.090619087219238-openerp-32-project.task@aaa> +Message-Id: {extra} references: <670034078674109.1555335454.587288856506348-openerp-32-project.task@aaa> Subject: Re: Test From: Mitchell Admin <admin@yourcompany.example.com> diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py index 45470ac6dff..098c7bc36eb 100644 --- a/addons/test_mail/tests/test_mail_gateway.py +++ b/addons/test_mail/tests/test_mail_gateway.py @@ -346,42 +346,116 @@ class TestMailgateway(BaseFunctionalTest, MockEmails): self.assertEqual(self._mails[0].get('email_from'), formataddr(('MAILER-DAEMON', 'bounce.test@test.com'))) @mute_logger('odoo.addons.mail.models.mail_thread') - def test_message_process_bounce(self): - """ Writing to bounce alias is considered as a bounce """ + def test_message_process_bounce_alias(self): + """ Writing to bounce alias is considered as a bounce even if not multipart/report bounce structure """ self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + bounced_mail_id = 4442 + bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com') + record = self.format_and_process(MAIL_TEMPLATE, self.partner_1.email_formatted, bounce_email_to, subject='Undelivered Mail Returned to Sender') + self.assertFalse(record) + # No information found in bounce email -> not possible to do anything except avoiding email + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_from_mailer_demon(self): + """ MAILER_DAEMON emails are considered as bounce """ + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + record = self.format_and_process(MAIL_TEMPLATE, 'MAILER-DAEMON@example.com', 'groups@test.com', subject='Undelivered Mail Returned to Sender') + self.assertFalse(record) + # No information found in bounce email -> not possible to do anything except avoiding email + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_multipart_alias(self): + """ Multipart/report bounce correctly make related partner bounce """ + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) bounced_mail_id = 4442 bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com') record = self.format_and_process(test_mail_data.MAIL_BOUNCE, self.partner_1.email_formatted, bounce_email_to, subject='Undelivered Mail Returned to Sender') self.assertFalse(record) - # self.assertEqual(self.partner_1.message_bounce, 1) # TDE FIXME: smart test crashing in current master + # Missing in reply to message_id -> cannot find original record + self.assertEqual(self.partner_1.message_bounce, 1) + self.assertEqual(self.test_record.message_bounce, 0) @mute_logger('odoo.addons.mail.models.mail_thread') - def test_message_process_bounce_whatever_from(self): - """ Writing to bounce alias is considered as a bounce """ + def test_message_process_bounce_multipart_alias_reply(self): + """ Multipart/report bounce correctly make related partner and record found in bounce email bounce """ self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 0) bounced_mail_id = 4442 bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com') - record = self.format_and_process(test_mail_data.MAIL_BOUNCE, 'Whatever <what@ever.com>', bounce_email_to, subject='Undelivered Mail Returned to Sender') + extra = self.fake_email.message_id + record = self.format_and_process(test_mail_data.MAIL_BOUNCE, self.partner_1.email_formatted, bounce_email_to, subject='Undelivered Mail Returned to Sender', extra=extra) self.assertFalse(record) - # self.assertEqual(self.partner_1.message_bounce, 1) # TDE FIXME: smart test crashing in current master + self.assertEqual(self.partner_1.message_bounce, 1) + self.assertEqual(self.test_record.message_bounce, 1) - @mute_logger('odoo.addons.mail.models.mail_thread') - def test_message_process_bounce_multipart_report(self): - """ Multipart/report emails should be considered as bounce """ + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_multipart_alias_whatever_from(self): + """ Multipart/report bounce correctly make related record found in bounce email bounce """ self.assertEqual(self.partner_1.message_bounce, 0) - record = self.format_and_process(test_mail_data.MAIL_BOUNCE, 'Whatever <what@ever.com>', 'groups@test.com', subject='Undelivered Mail Returned to Sender') + self.assertEqual(self.test_record.message_bounce, 0) + + bounced_mail_id = 4442 + bounce_email_to = '%s+%s-%s-%s@%s' % ('bounce.test', bounced_mail_id, self.test_record._name, self.test_record.id, 'test.com') + extra = self.fake_email.message_id + record = self.format_and_process(test_mail_data.MAIL_BOUNCE, 'Whatever <what@ever.com>', bounce_email_to, subject='Undelivered Mail Returned to Sender', extra=extra) self.assertFalse(record) - # self.assertEqual(self.partner_1.message_bounce, 1) # TDE FIXME: smart test crashing in current master + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 1) - @mute_logger('odoo.addons.mail.models.mail_thread') - def test_message_process_bounce_mailer_demon(self): - """ Multipart/report emails should be considered as bounce """ + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_multipart_whatever_to_and_from(self): + """ Multipart/report bounce correctly make related record found in bounce email bounce """ self.assertEqual(self.partner_1.message_bounce, 0) - record = self.format_and_process(MAIL_TEMPLATE, 'MAILER-DAEMON@example.com', 'groups@test.com', subject='Undelivered Mail Returned to Sender') + self.assertEqual(self.test_record.message_bounce, 0) + + extra = self.fake_email.message_id + record = self.format_and_process(test_mail_data.MAIL_BOUNCE, 'Whatever <what@ever.com>', 'groups@test.com', subject='Undelivered Mail Returned to Sender', extra=extra) self.assertFalse(record) - # self.assertEqual(self.partner_1.message_bounce, 1) # TDE FIXME: smart test crashing in current master + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.test_record.message_bounce, 1) + + @mute_logger('odoo.addons.mail.models.mail_thread') + def test_message_process_bounce_records_channel(self): + """ Test blacklist allow to multi-bounce and auto update of mail.channel """ + self.other_record = self.env['mail.test.gateway'].create({ + 'email_from': 'Another name <%s>' % self.partner_1.email + }) + self.yet_other_record = self.env['mail.test.gateway'].create({ + 'email_from': 'Yet Another name <%s>' % self.partner_1.email.upper() + }) + self.test_channel = self.env['mail.channel'].create({ + 'name': 'Test', + 'channel_last_seen_partner_ids': [(0, 0, {'partner_id': self.partner_1.id})], + }) + self.fake_email.write({ + 'model': 'mail.channel', + 'res_id': self.test_channel.id, + }) + self.assertIn(self.partner_1, self.test_channel.channel_partner_ids) + self.assertEqual(self.partner_1.message_bounce, 0) + self.assertEqual(self.other_record.message_bounce, 0) + self.assertEqual(self.yet_other_record.message_bounce, 0) + + extra = self.fake_email.message_id + for i in range(10): + record = self.format_and_process(test_mail_data.MAIL_BOUNCE, 'A third name <%s>' % self.partner_1.email, 'groups@test.com', subject='Undelivered Mail Returned to Sender', extra=extra) + self.assertFalse(record) + self.assertEqual(self.partner_1.message_bounce, 10) + self.assertEqual(self.test_record.message_bounce, 0) + self.assertEqual(self.other_record.message_bounce, 10) + self.assertEqual(self.yet_other_record.message_bounce, 10) + self.assertNotIn(self.partner_1, self.test_channel.channel_partner_ids) # -------------------------------------------------- # Thread formation diff --git a/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py b/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py index ff9bc5268c1..280bac1389b 100644 --- a/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py +++ b/addons/test_mass_mailing/tests/test_mail_auto_blacklist.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo.tests import common + import datetime +from odoo import tools +from odoo.tests import common + + class TestAutoBlacklist(common.TransactionCase): def test_mail_bounced_auto_blacklist(self): @@ -10,40 +14,29 @@ class TestAutoBlacklist(common.TransactionCase): mass_mailing = self.env['mailing.mailing'] mail_blacklist = self.env['mail.blacklist'] mail_statistics = self.env['mailing.trace'] - 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': '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': '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': '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': '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) + base_parsed_values = { + 'email_from': 'toto@yaourth.com', 'to': 'tata@yaourth.com', 'message_id': '<123.321@yaourth.com>', + 'bounced_partner': self.env['res.partner'].sudo(), 'bounced_message': self.env['mail.message'].sudo() + } + + # create bounced history of 4 statistics + for idx in range(4): + mail_statistics.create({ + 'model': 'mailing.contact', + 'res_id': self.mailing_contact_1.id, + 'bounced': datetime.datetime.now() - datetime.timedelta(weeks=idx+2), + 'email': self.mailing_contact_1.email, + 'message_id': '<123.00%s@iron.sky>' % idx, + }) + base_parsed_values.update({ + 'bounced_email': tools.email_normalize(self.mailing_contact_1.email), + 'bounced_msg_id': '<123.00%s@iron.sky>' % idx + }) + self.env['mail.thread']._routing_handle_bounce(False, base_parsed_values) # create mass mailing record self.mass_mailing = mass_mailing.create({ @@ -73,10 +66,15 @@ class TestAutoBlacklist(common.TransactionCase): 'model': 'mailing.contact', 'res_id': self.mailing_contact_1.id, 'bounced': datetime.datetime.now(), - 'email': self.mailing_contact_1.email + 'email': self.mailing_contact_1.email, + 'message_id': '<123.004@iron.sky>', + }) + base_parsed_values.update({ + 'bounced_email': tools.email_normalize(self.mailing_contact_1.email), + 'bounced_msg_id': '<123.004@iron.sky>' }) # call bounced - self.mailing_contact_1._message_receive_bounce(self.mailing_contact_1.email, self.mailing_contact_1) + self.env['mail.thread']._routing_handle_bounce(False, base_parsed_values) # check blacklist blacklist_record = mail_blacklist.search([('email', '=', self.mailing_contact_1.email)]) |
