diff --git a/addons/crm/tests/test_crm_lead_notification.py b/addons/crm/tests/test_crm_lead_notification.py index ca7df34cc4ff71c016bdc19b7f9c9414f906ae37..ec6c4e6039cc5730e20763cbf073d3a2d5472b03 100644 --- a/addons/crm/tests/test_crm_lead_notification.py +++ b/addons/crm/tests/test_crm_lead_notification.py @@ -39,7 +39,7 @@ class NewLeadNotification(TestCrmCommon): [(False, '"New Customer" <new.customer.format@test.example.com>', 'Customer Email'), (False, 'new.customer.multi.1@test.example.com, new.customer.2@test.example.com', 'Customer Email'), (False, 'new.customer.simple@test.example.com', 'Customer Email'), - (self.contact_1.id, '"Philip J Fry" <"Philip, J. Fry" <philip.j.fry@test.example.com>>', 'Customer'), + (self.contact_1.id, '"Philip J Fry" <philip.j.fry@test.example.com>', 'Customer'), ] ): with self.subTest(lead=lead, email_from=lead.email_from): diff --git a/addons/mail/tests/test_res_partner.py b/addons/mail/tests/test_res_partner.py index 56fb86f4db59f2a6a2a374f3759e399af9858e9a..588a969ca2b90d012162e846d2607a222b2da740 100644 --- a/addons/mail/tests/test_res_partner.py +++ b/addons/mail/tests/test_res_partner.py @@ -51,8 +51,8 @@ class TestPartner(MailCommon): self.assertEqual( partners.mapped('email_formatted'), ['"Classic Format" <classic.format@test.example.com>', - '"FindMe Format" <"FindMe Format" <find.me.format@test.example.com>>', - '"FindMe Multi" <find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>>'] + '"FindMe Format" <find.me.format@test.example.com>', + '"FindMe Multi" <find.me.multi.1@test.example.com,find.me.multi.2@test.example.com>'] ) self.assertEqual( partners.mapped('email_normalized'), diff --git a/addons/test_mail/tests/test_mail_composer.py b/addons/test_mail/tests/test_mail_composer.py index 107f6abad4bf27bb7d7cf711b0250a344f330a8e..99b6664fa6b70b03b8c38008bfaf503889c0468e 100644 --- a/addons/test_mail/tests/test_mail_composer.py +++ b/addons/test_mail/tests/test_mail_composer.py @@ -637,15 +637,15 @@ class TestComposerResultsComment(TestMailComposer): # ensure values used afterwards for testing self.assertEqual( self.partner_employee.email_formatted, - '"Ernest Employee" <email.from.1@test.example.com, email.from.2@test.example.com>', + '"Ernest Employee" <email.from.1@test.example.com,email.from.2@test.example.com>', 'Formatting: wrong formatting due to multi-email') self.assertEqual( self.partner_1.email_formatted, - '"Valid Lelitre" <"Valid Formatted" <valid.lelitre@agrolait.com>>', - 'Formatting: wrong double encapsulation') + '"Valid Lelitre" <valid.lelitre@agrolait.com>', + 'Formatting: avoid wrong double encapsulation') self.assertEqual( self.partner_2.email_formatted, - '"Valid Poilvache" <valid.other.1@agrolait.com, valid.other.cc@agrolait.com>', + '"Valid Poilvache" <valid.other.1@agrolait.com,valid.other.cc@agrolait.com>', 'Formatting: wrong formatting due to multi-email') # instantiate composer, post message @@ -680,8 +680,8 @@ class TestComposerResultsComment(TestMailComposer): ) self.assertEqual( sorted(new_partners.mapped('email_formatted')), - sorted(['"FindMe Format" <"FindMe Format" <find.me.format@test.example.com>>', - '"FindMe Multi" <find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>>', + sorted(['"FindMe Format" <find.me.format@test.example.com>', + '"FindMe Multi" <find.me.multi.1@test.example.com,find.me.multi.2@test.example.com>', '"find.me.multi.1@test.example.com" <find.me.multi.1@test.example.com>', '"find.me.multi.2@test.example.com" <find.me.multi.2@test.example.com>', '"test.cc.1@example.com" <test.cc.1@example.com>', @@ -722,12 +722,12 @@ class TestComposerResultsComment(TestMailComposer): email_values={ 'body_content': f'TemplateBody {self.test_record.name}', # currently holding multi-email 'from' - 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com, email.from.2@test.example.com')), + 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com,email.from.2@test.example.com')), 'subject': f'TemplateSubject {self.test_record.name}', }, fields_values={ # currently holding multi-email 'email_from' - 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com, email.from.2@test.example.com')), + 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com,email.from.2@test.example.com')), }, mail_message=self.test_record.message_ids[0], ) @@ -736,12 +736,12 @@ class TestComposerResultsComment(TestMailComposer): author=self.partner_employee, email_values={ 'body_content': f'TemplateBody {self.test_record.name}', - 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com, email.from.2@test.example.com')), + 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com,email.from.2@test.example.com')), 'subject': f'TemplateSubject {self.test_record.name}', }, fields_values={ # currently holding multi-email 'email_from' - 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com, email.from.2@test.example.com')), + 'email_from': formataddr((self.user_employee.name, 'email.from.1@test.example.com,email.from.2@test.example.com')), }, mail_message=self.test_record.message_ids[0], ) @@ -1080,15 +1080,15 @@ class TestComposerResultsMass(TestMailComposer): # ensure values used afterwards for testing self.assertEqual( self.partner_employee.email_formatted, - '"Ernest Employee" <email.from.1@test.example.com, email.from.2@test.example.com>', + '"Ernest Employee" <email.from.1@test.example.com,email.from.2@test.example.com>', 'Formatting: wrong formatting due to multi-email') self.assertEqual( self.partner_1.email_formatted, - '"Valid Lelitre" <"Valid Formatted" <valid.lelitre@agrolait.com>>', - 'Formatting: wrong double encapsulation') + '"Valid Lelitre" <valid.lelitre@agrolait.com>', + 'Formatting: avoid wrong double encapsulation') self.assertEqual( self.partner_2.email_formatted, - '"Valid Poilvache" <valid.other.1@agrolait.com, valid.other.cc@agrolait.com>', + '"Valid Poilvache" <valid.other.1@agrolait.com,valid.other.cc@agrolait.com>', 'Formatting: wrong formatting due to multi-email') # instantiate composer, send mailing @@ -1123,8 +1123,8 @@ class TestComposerResultsMass(TestMailComposer): ) self.assertEqual( sorted(new_partners.mapped('email_formatted')), - sorted(['"FindMe Format" <"FindMe Format" <find.me.format@test.example.com>>', - '"FindMe Multi" <find.me.multi.1@test.example.com, "FindMe Multi" <find.me.multi.2@test.example.com>>', + sorted(['"FindMe Format" <find.me.format@test.example.com>', + '"FindMe Multi" <find.me.multi.1@test.example.com,find.me.multi.2@test.example.com>', '"find.me.multi.1@test.example.com" <find.me.multi.1@test.example.com>', '"find.me.multi.2@test.example.com" <find.me.multi.2@test.example.com>', '"test.cc.1@example.com" <test.cc.1@example.com>', diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py index f468951a0a39be9556be38dffc4b5220b175d1aa..2d26e11df9af1ddc8d75d82ef4db03bf23034294 100644 --- a/odoo/addons/base/models/res_partner.py +++ b/odoo/addons/base/models/res_partner.py @@ -379,11 +379,36 @@ class Partner(models.Model): @api.depends('name', 'email') def _compute_email_formatted(self): + """ Compute formatted email for partner, using formataddr. Be defensive + in computation, notably + + * double format: if email already holds a formatted email like + 'Name' <email@domain.com> we should not use it as it to compute + email formatted like "Name <'Name' <email@domain.com>>"; + * multi emails: sometimes this field is used to hold several addresses + like email1@domain.com, email2@domain.com. We currently let this value + untouched, but remove any formatting from multi emails; + * invalid email: if something is wrong, keep it in email_formatted as + this eases management and understanding of failures at mail.mail, + mail.notification and mailing.trace level; + * void email: email_formatted is False, as we cannot do anything with + it; + """ + self.email_formatted = False for partner in self: - if partner.email: - partner.email_formatted = tools.formataddr((partner.name or u"False", partner.email or u"False")) - else: - partner.email_formatted = '' + emails_normalized = tools.email_normalize_all(partner.email) + if emails_normalized: + # note: multi-email input leads to invalid email like "Name" <email1, email2> + # but this is current behavior in Odoo 14+ and some servers allow it + partner.email_formatted = tools.formataddr(( + partner.name or u"False", + ','.join(emails_normalized) + )) + elif partner.email: + partner.email_formatted = tools.formataddr(( + partner.name or u"False", + partner.email + )) @api.depends('is_company') def _compute_company_type(self): diff --git a/odoo/addons/base/tests/test_res_partner.py b/odoo/addons/base/tests/test_res_partner.py index 2ec3458de8034a87fb15f940f18ed5ca94b89165..3160db404fae627b506e5975ee2884f78a5b7a17 100644 --- a/odoo/addons/base/tests/test_res_partner.py +++ b/odoo/addons/base/tests/test_res_partner.py @@ -61,25 +61,25 @@ class TestPartner(TransactionCase): # encapsulated email ( "Vlad the Impaler <vlad.the.impaler@example.com>", - '"Balázs" <Vlad the Impaler <vlad.the.impaler@example.com>>' + '"Balázs" <vlad.the.impaler@example.com>' ), ( '"Balázs" <balazs@adam.hu>', - '"Balázs" <"Balázs" <balazs@adam.hu>>' + '"Balázs" <balazs@adam.hu>' ), # multi email ( "vlad.the.impaler@example.com, vlad.the.dragon@example.com", - '"Balázs" <vlad.the.impaler@example.com, vlad.the.dragon@example.com>' + '"Balázs" <vlad.the.impaler@example.com,vlad.the.dragon@example.com>' ), ( "vlad.the.impaler.com, vlad.the.dragon@example.com", - '"Balázs" <vlad.the.impaler.com, vlad.the.dragon@example.com>' + '"Balázs" <vlad.the.dragon@example.com>' ), ( 'vlad.the.impaler.com, "Vlad the Dragon" <vlad.the.dragon@example.com>', - '"Balázs" <vlad.the.impaler.com, "Vlad the Dragon" <vlad.the.dragon@example.com>>' + '"Balázs" <vlad.the.dragon@example.com>' ), # falsy emails - (False, ''), - ('', ''), + (False, False), + ('', False), (' ', '"Balázs" <@ >'), ('notanemail', '"Balázs" <@notanemail>'), ]: diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py index 3562b1b726c327268e9914cee0dafd41957e236d..9e84681107816dde60199603e46bcdb6769b698b 100644 --- a/odoo/tools/mail.py +++ b/odoo/tools/mail.py @@ -552,6 +552,20 @@ def email_normalize(text): return False return emails[0].lower() +def email_normalize_all(text): + """ Tool method allowing to extract email addresses from a text input and returning + normalized version of all found emails. If no email is found, a void list + is returned. + + e.g. if email is 'tony@e.com, "Tony2" <tony2@e.com' returned result is ['tony@e.com, tony2@e.com'] + + :return list: list of normalized emails found in text + """ + if not text: + return [] + emails = email_split(text) + return list(filter(None, [email_normalize(email) for email in emails])) + def email_escape_char(email_address): """ Escape problematic characters in the given email address string""" return email_address.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')