diff --git a/addons/crm/tests/test_crm_lead_notification.py b/addons/crm/tests/test_crm_lead_notification.py index 344e2bef650..046271094c3 100644 --- a/addons/crm/tests/test_crm_lead_notification.py +++ b/addons/crm/tests/test_crm_lead_notification.py @@ -66,7 +66,10 @@ class NewLeadNotification(TestCrmCommon): 'team_id': self.sales_team_1.id, }), (False, '"Multi Name" ', None, 'Customer Email', - {}), # no email_normalized -> no information + {'company_name': 'Multi Name', 'email': 'new.customer.multi.1@test.example.com', + 'name': 'Multi Name', 'user_id': self.user_sales_leads.id, + 'team_id': self.sales_team_1.id, + }), (False, '"Std Name" ', None, 'Customer Email', {'company_name': 'Std Name', 'email': 'new.customer.simple@test.example.com', 'name': 'Std Name', 'user_id': self.user_sales_leads.id, diff --git a/addons/mail/models/mail_thread_blacklist.py b/addons/mail/models/mail_thread_blacklist.py index 4ce6752c805..3960c26c090 100644 --- a/addons/mail/models/mail_thread_blacklist.py +++ b/addons/mail/models/mail_thread_blacklist.py @@ -46,7 +46,7 @@ class MailBlackListMixin(models.AbstractModel): def _compute_email_normalized(self): self._assert_primary_email() for record in self: - record.email_normalized = tools.email_normalize(record[self._primary_email]) + record.email_normalized = tools.email_normalize(record[self._primary_email], strict=False) @api.model def _search_is_blacklisted(self, operator, value): diff --git a/addons/mail/tests/test_mail_tools.py b/addons/mail/tests/test_mail_tools.py index 6e62f045ff7..a3c4acabfe2 100644 --- a/addons/mail/tests/test_mail_tools.py +++ b/addons/mail/tests/test_mail_tools.py @@ -133,7 +133,7 @@ class TestMailTools(MailCommon): (f'{self._test_email}, {_test_email_2}', True), # multi-email, both matching, depends on comparison (f'{self._test_email}, {_test_email_2}', False) # multi-email, both matching, depends on comparison ] - expected = [self.env['res.partner'], self.env['res.partner'], + expected = [follower_partner, test_partner, self.env['res.partner'], self.env['res.partner'], self.env['res.partner'], self.env['res.partner'], self.env['res.partner'], self.env['res.partner']] diff --git a/addons/mail/tests/test_res_partner.py b/addons/mail/tests/test_res_partner.py index 6b4f2cb0138..2436308580e 100644 --- a/addons/mail/tests/test_res_partner.py +++ b/addons/mail/tests/test_res_partner.py @@ -182,9 +182,11 @@ class TestPartner(MailCommon): '"FindMe Format" ', '"FindMe Multi" '] ) + # when having multi emails, first found one is taken as normalized email self.assertEqual( partners.mapped('email_normalized'), - ['classic.format@test.example.com', 'find.me.format@test.example.com', False] + ['classic.format@test.example.com', 'find.me.format@test.example.com', + 'find.me.multi.1@test.example.com'] ) # classic find or create: use normalized email to compare records @@ -196,11 +198,18 @@ class TestPartner(MailCommon): with self.subTest(email=email): self.assertEqual(self.env['res.partner'].find_or_create(email), partners[1]) # multi-emails -> no normalized email -> fails each time, create new partner (FIXME) - for email in ('find.me.multi.1@test.example.com', 'find.me.multi.2@test.example.com'): - with self.subTest(email=email): - partner = self.env['res.partner'].find_or_create(email) - self.assertNotIn(partner, partners) - self.assertEqual(partner.email, email) + for email_input, match_partner in [ + ('find.me.multi.1@test.example.com', partners[2]), + ('find.me.multi.2@test.example.com', self.env['res.partner']), + ]: + with self.subTest(email_input=email_input): + partner = self.env['res.partner'].find_or_create(email_input) + # either matching existing, either new partner + if match_partner: + self.assertEqual(partner, match_partner) + else: + self.assertNotIn(partner, partners) + self.assertEqual(partner.email, email_input) partner.unlink() # do not mess with subsequent tests # now input is multi email -> '_parse_partner_name' used in 'find_or_create' diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index 592669cfa8e..98f92af1a15 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -1744,15 +1744,14 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): # FIXME: currently email finding based on formatted / multi emails does # not work new_partners = self.env['res.partner'].search([]).search([('id', 'not in', existing_partners.ids)]) - self.assertEqual(len(new_partners), 9, - 'Mail (FIXME): multiple partner creation due to formatted / multi emails: 2 extra partners') + self.assertEqual(len(new_partners), 8, + 'Mail (FIXME): multiple partner creation due to formatted / multi emails: 1 extra partners') self.assertIn(partner_format_tofind, new_partners) self.assertIn(partner_multi_tofind, new_partners) self.assertEqual( sorted(new_partners.mapped('email')), sorted(['"FindMe Format" ', 'find.me.multi.1@test.example.com, "FindMe Multi" ', - 'find.me.multi.1@test.example.com', 'find.me.multi.2@test.example.com', 'test.cc.1@example.com', 'test.cc.2@example.com', 'test.cc.2.2@example.com', 'test.to.1@example.com', 'test.to.2@example.com']), @@ -1762,7 +1761,6 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): sorted(new_partners.mapped('email_formatted')), sorted(['"FindMe Format" ', '"FindMe Multi" ', - '"find.me.multi.1@test.example.com" ', '"find.me.multi.2@test.example.com" ', '"test.cc.1@example.com" ', '"test.cc.2@example.com" ', @@ -1774,7 +1772,6 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): sorted(new_partners.mapped('name')), sorted(['FindMe Format', 'FindMe Multi', - 'find.me.multi.1@test.example.com', 'find.me.multi.2@test.example.com', 'test.cc.1@example.com', 'test.to.1@example.com', 'test.to.2@example.com', 'test.cc.2@example.com', 'test.cc.2.2@example.com']), @@ -1788,14 +1785,10 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): # FIXME: more partners created than real emails (see above) -> due to # transformation from email -> partner in template 'generate_recipients' # there are more partners than email to notify; - # NOTE: 'Findme Multi' is excluded as it has the same email as 'find.me.multi.1@test.example.com' - # (created by template) and comes second in a search based on email - mailed_new_partners = new_partners.filtered(lambda p: p.name != 'FindMe Multi') - self.assertEqual(len(mailed_new_partners), 8) self.assertEqual(len(self._new_mails), 2, 'Should have created 2 mail.mail') self.assertEqual( - len(self._mails), len(mailed_new_partners) + 3, - f'Should have sent {len(mailed_new_partners) + 3} emails, one / recipient ({len(mailed_new_partners)} mailed partners + partner_1 + partner_2 + partner_employee)') + len(self._mails), len(new_partners) + 3, + f'Should have sent {len(new_partners) + 3} emails, one / recipient ({len(new_partners)} mailed partners + partner_1 + partner_2 + partner_employee)') self.assertMailMail( self.partner_employee_2, 'sent', author=self.partner_employee, @@ -1812,12 +1805,14 @@ class TestComposerResultsComment(TestMailComposer, CronMixinCase): mail_message=self.test_record.message_ids[0], ) self.assertMailMail( - self.partner_1 + self.partner_2 + mailed_new_partners, 'sent', + self.partner_1 + self.partner_2 + new_partners, 'sent', author=self.partner_employee, email_to_recipients=[ [self.partner_1.email_formatted], [f'"{self.partner_2.name}" ', f'"{self.partner_2.name}" '], - ] + [[email] for email in mailed_new_partners.mapped('email_formatted')], + ] + [[new_partners[0]['email_formatted']], + ['"FindMe Multi" ', '"FindMe Multi" '] + ] + [[email] for email in new_partners[2:].mapped('email_formatted')], email_values={ 'body_content': f'TemplateBody {self.test_record.name}', # single email event if email field is multi-email @@ -2420,15 +2415,14 @@ class TestComposerResultsMass(TestMailComposer): # FIXME: currently email finding based on formatted / multi emails does # not work new_partners = self.env['res.partner'].search([]).search([('id', 'not in', existing_partners.ids)]) - self.assertEqual(len(new_partners), 9, - 'Mail (FIXME): did not find existing partners for formatted / multi emails: 2 extra partners') + self.assertEqual(len(new_partners), 8, + 'Mail (FIXME): did not find existing partners for formatted / multi emails: 1 extra partners') self.assertIn(partner_format_tofind, new_partners) self.assertIn(partner_multi_tofind, new_partners) self.assertEqual( sorted(new_partners.mapped('email')), sorted(['"FindMe Format" ', 'find.me.multi.1@test.example.com, "FindMe Multi" ', - 'find.me.multi.1@test.example.com', 'find.me.multi.2@test.example.com', 'test.cc.1@example.com', 'test.cc.2@example.com', 'test.cc.2.2@example.com', 'test.to.1@example.com', 'test.to.2@example.com']), @@ -2438,7 +2432,6 @@ class TestComposerResultsMass(TestMailComposer): sorted(new_partners.mapped('email_formatted')), sorted(['"FindMe Format" ', '"FindMe Multi" ', - '"find.me.multi.1@test.example.com" ', '"find.me.multi.2@test.example.com" ', '"test.cc.1@example.com" ', '"test.cc.2@example.com" ', @@ -2450,7 +2443,6 @@ class TestComposerResultsMass(TestMailComposer): sorted(new_partners.mapped('name')), sorted(['FindMe Format', 'FindMe Multi', - 'find.me.multi.1@test.example.com', 'find.me.multi.2@test.example.com', 'test.cc.1@example.com', 'test.to.1@example.com', 'test.to.2@example.com', 'test.cc.2@example.com', 'test.cc.2.2@example.com']), @@ -2465,23 +2457,21 @@ class TestComposerResultsMass(TestMailComposer): # FIXME: more partners created than real emails (see above) -> due to # transformation from email -> partner in template 'generate_recipients' # there are more partners than email to notify; - # NOTE: 'Findme Multi' is excluded as it has the same email as 'find.me.multi.1@test.example.com' - # (created by template) and comes second in a search based on email - mailed_new_partners = new_partners.filtered(lambda p: p.name != 'FindMe Multi') - self.assertEqual(len(mailed_new_partners), 8) self.assertEqual(len(self._new_mails), 2, 'Should have created 2 mail.mail') self.assertEqual( - len(self._mails), (len(mailed_new_partners) + 2) * 2, - f'Should have sent {(len(mailed_new_partners) + 2) * 2} emails, one / recipient ({len(mailed_new_partners)} mailed partners + partner_1 + partner_2) * 2 records') + len(self._mails), (len(new_partners) + 2) * 2, + f'Should have sent {(len(new_partners) + 2) * 2} emails, one / recipient ({len(new_partners)} mailed partners + partner_1 + partner_2) * 2 records') for record in self.test_records: self.assertMailMail( - self.partner_1 + self.partner_2 + mailed_new_partners, + self.partner_1 + self.partner_2 + new_partners, 'sent', author=self.partner_employee, email_to_recipients=[ [self.partner_1.email_formatted], [f'"{self.partner_2.name}" ', f'"{self.partner_2.name}" '], - ] + [[email] for email in mailed_new_partners.mapped('email_formatted')], + ] + [[new_partners[0]['email_formatted']], + ['"FindMe Multi" ', '"FindMe Multi" '] + ] + [[email] for email in new_partners[2:].mapped('email_formatted')], email_values={ 'body_content': f'TemplateBody {record.name}', # single email event if email field is multi-email diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py index 6c139c4ce49..4a38a8e9dcd 100644 --- a/addons/test_mail/tests/test_mail_gateway.py +++ b/addons/test_mail/tests/test_mail_gateway.py @@ -333,8 +333,8 @@ class TestMailgateway(MailCommon): record = self.format_and_process( MAIL_TEMPLATE, f'"Valid Lelitre" <{test_email}>', 'groups@test.com', subject='Test3') - self.assertFalse(record.message_ids[0].author_id, - 'message_process (FIXME): unrecognized email -> author_id due to multi email') + self.assertEqual(record.message_ids[0].author_id, self.partner_1, + 'message_process: found author based on first found email normalized, even with multi emails') self.assertEqual(record.message_ids[0].email_from, f'"Valid Lelitre" <{test_email}>') self.assertNotSentEmail() # No notification / bounce should be sent @@ -343,8 +343,8 @@ class TestMailgateway(MailCommon): record = self.format_and_process( MAIL_TEMPLATE, test_email, 'groups@test.com', subject='Test4') - self.assertFalse(record.message_ids[0].author_id, - 'message_process (FIXME): unrecognized email -> author_id due to multi email') + self.assertEqual(record.message_ids[0].author_id, self.partner_1, + 'message_process: found author based on first found email normalized, even with multi emails') self.assertEqual(record.message_ids[0].email_from, test_email) self.assertNotSentEmail() # No notification / bounce should be sent @@ -692,7 +692,7 @@ class TestMailgateway(MailCommon): for partner_email, passed in [ (formataddr((self.partner_1.name, self.partner_1.email_normalized)), True), - (f'{self.partner_1.email_normalized}, "Multi Email" ', False), + (f'{self.partner_1.email_normalized}, "Multi Email" ', True), (f'"Multi Email" , {self.partner_1.email_normalized}', False), ]: with self.subTest(partner_email=partner_email): diff --git a/addons/test_mail/tests/test_mail_thread_mixins.py b/addons/test_mail/tests/test_mail_thread_mixins.py index c28f4bd1706..a9221256561 100644 --- a/addons/test_mail/tests/test_mail_thread_mixins.py +++ b/addons/test_mail/tests/test_mail_thread_mixins.py @@ -30,9 +30,9 @@ class TestMailThread(MailCommon, TestRecipients): (' ', False)] multi_pairs = [ (f'{base_email}, other.email@test.example.com', - False), # multi not supported currently + base_email), # multi supports first found (f'{tools.formataddr(("Another Name", base_email))}, other.email@test.example.com', - False), # multi not supported currently + base_email), # multi supports first found ] for email_from, exp_email_normalized in valid_pairs + void_pairs + multi_pairs: with self.subTest(email_from=email_from, exp_email_normalized=exp_email_normalized): diff --git a/addons/test_mass_mailing/tests/test_mailing.py b/addons/test_mass_mailing/tests/test_mailing.py index 318a13bd56a..47f87ec1c39 100644 --- a/addons/test_mass_mailing/tests/test_mailing.py +++ b/addons/test_mass_mailing/tests/test_mailing.py @@ -213,6 +213,8 @@ class TestMassMailing(TestMassMailCommon): # as it uses email_normalized if possible if dst_model == 'mailing.test.customer': formatted_mailmail_email = '"Formatted Record" ' + multi_mail_mail_email = 'record.multi.1@example.com, "Record Multi 2" ' + multi_outgoing_emails = ['record.multi.1@example.com', '"Record Multi 2" '] unicode_email = '"Unicode Record" ' unicode_mailmail_email = '"Unicode Record" ' record_case_email = 'TEST.RECORD.CASE@EXAMPLE.COM' @@ -223,6 +225,8 @@ class TestMassMailing(TestMassMailCommon): else: formatted_mailmail_email = 'record.format@example.com' unicode_email = 'record.😊@example.com' + multi_mail_mail_email = 'record.multi.1@example.com' + multi_outgoing_emails = ['record.multi.1@example.com'] unicode_mailmail_email = 'record.😊@example.com' record_case_email = 'test.record.case@example.com' record_weird_email = 'test.record.weird@example.comweirdformat' @@ -264,8 +268,8 @@ class TestMassMailing(TestMassMailCommon): 'partner': customer_weird_2, 'trace_status': 'sent'}, {'email': 'record.multi.1@example.com', - 'email_to_mail': 'record.multi.1@example.com, "Record Multi 2" ', - 'email_to_recipients': [['record.multi.1@example.com', '"Record Multi 2" ']], + 'email_to_mail': multi_mail_mail_email, + 'email_to_recipients': [multi_outgoing_emails], 'failure_type': False, 'trace_status': 'sent'}, {'email': 'record.format@example.com', diff --git a/odoo/addons/base/tests/test_mail.py b/odoo/addons/base/tests/test_mail.py index 5a4d88d8044..da269ed2402 100644 --- a/odoo/addons/base/tests/test_mail.py +++ b/odoo/addons/base/tests/test_mail.py @@ -525,7 +525,7 @@ class TestEmailTools(BaseCase): ] for source, expected in zip(sources, expected_list): with self.subTest(source=source): - self.assertEqual(email_normalize(source), expected) + self.assertEqual(email_normalize(source, strict=True), expected) def test_email_re(self): """ Test 'email_re', finding emails in a given text """ diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py index d40fcaff12a..2123a4815ed 100644 --- a/odoo/tools/mail.py +++ b/odoo/tools/mail.py @@ -559,8 +559,11 @@ def email_normalize(text, strict=True): - Possible Input Email : 'Name ' - Normalized Output Email : 'name@domain.com' - :param bool strict: text should contain exactly one email (default behavior - and unique behavior before Odoo16); + :param boolean strict: if True, text should contain a single email + (default behavior in stable 14+). If more than one email is found no + normalized email is returned. If False the first found candidate is used + e.g. if email is 'tony@e.com, "Tony2" ', result is either + False (strict=True), either 'tony@e.com' (strict=False). :return: False if no email found (or if more than 1 email found when being in strict mode); normalized email otherwise;