[FIX] tools, base, mail: add a fallback when parsing wrongly-formatted emails
With input 'name email@domain.com' (missing chevrons allowing to clearly spot the email part) 'getaddresses' returns ('', 'name email@domain.com) i.e. the whole input is considered as being the email. To improve the heuristic we can add a fallback by recalling 'getadresses' on the input with spaces replaced by commas when it found only an email and no name. The new email will be split into sub pairs allowing to find the real email and various name parts, allowing to make a new name / email pair. Emails should not contain spaces thus this is coherent with email formation. This fallback actually comes from a specific code done in '_parse_partner_name' of Partner model. Supporting it directly at tools level make the behavior coherent for all models. Task-2612945 (Mail: Defensive email formatting) X-original-commit: odoo/odoo@18c71edf59 Part-of: odoo/odoo#134934
This commit is contained in:
@@ -372,8 +372,8 @@ class TestPartner(MailCommon):
|
||||
# multi email support
|
||||
(False, "Another Customer", "test.different.1@test.dupe.example.com"),
|
||||
(False, "Multi Email", "other.customer.2@test.dupe.example.com"),
|
||||
(False, "Invalid, Multi Format", "other.customer.😊@test.dupe.example.com"),
|
||||
(False, "Invalid, Multi Format", "other.customer.😊@test.dupe.example.com"),
|
||||
(False, "Multi Format", "other.customer.😊@test.dupe.example.com"),
|
||||
(False, "Multi Format", "other.customer.😊@test.dupe.example.com"),
|
||||
]
|
||||
with self.mockPartnerCalls():
|
||||
new_partners = self.env['res.partner'].with_context(lang='en_US')._find_or_create_from_emails(
|
||||
|
||||
@@ -243,7 +243,7 @@ class TestMassMailing(TestMassMailCommon):
|
||||
'partner': customer_weird,
|
||||
'trace_status': 'sent'}, # concatenates everything after domain
|
||||
{'email': 'Weird Format2 test.customer.weird.2@example.com',
|
||||
'email_to_recipients': [[f'"{customer_weird_2.name}" <weird format2 test.customer.weird.2@example.com>']],
|
||||
'email_to_recipients': [[f'"{customer_weird_2.name}" <test.customer.weird.2@example.com>']],
|
||||
'failure_type': False,
|
||||
'partner': customer_weird_2,
|
||||
'trace_status': 'sent'},
|
||||
@@ -273,8 +273,8 @@ class TestMassMailing(TestMassMailCommon):
|
||||
'failure_type': False,
|
||||
'trace_status': 'sent'},
|
||||
{'email': 'test.record.weird.2@example.com',
|
||||
'email_to_mail': 'weird format2 test.record.weird.2@example.com',
|
||||
'email_to_recipients': [['weird format2 test.record.weird.2@example.com']],
|
||||
'email_to_mail': 'test.record.weird.2@example.com',
|
||||
'email_to_recipients': [['test.record.weird.2@example.com']],
|
||||
'failure_type': False,
|
||||
'trace_status': 'sent'},
|
||||
],
|
||||
|
||||
@@ -517,7 +517,7 @@ class TestEmailTools(BaseCase):
|
||||
]
|
||||
expected_list = [
|
||||
'deboulonneur@example.com',
|
||||
'Déboulonneur deboulonneur@example.com',
|
||||
'deboulonneur@example.com',
|
||||
'deboulonneur@example.comdéboulonneur',
|
||||
False,
|
||||
'@example.com', # funny
|
||||
@@ -527,7 +527,7 @@ class TestEmailTools(BaseCase):
|
||||
]
|
||||
expected_fmt_utf8_list = [
|
||||
f'"{format_name}" <deboulonneur@example.com>',
|
||||
f'"{format_name}" <Déboulonneur deboulonneur@example.com>',
|
||||
f'"{format_name}" <deboulonneur@example.com>',
|
||||
f'"{format_name}" <deboulonneur@example.comdéboulonneur>',
|
||||
f'"{format_name}" <@>',
|
||||
f'"{format_name}" <@example.com>',
|
||||
@@ -537,7 +537,7 @@ class TestEmailTools(BaseCase):
|
||||
]
|
||||
expected_fmt_ascii_list = [
|
||||
f'{format_name_ascii} <deboulonneur@example.com>',
|
||||
f'{format_name_ascii} <Déboulonneur deboulonneur@example.com>',
|
||||
f'{format_name_ascii} <deboulonneur@example.com>',
|
||||
f'{format_name_ascii} <deboulonneur@example.xn--comdboulonneur-ekb>',
|
||||
f'{format_name_ascii} <@>',
|
||||
f'{format_name_ascii} <@example.com>',
|
||||
@@ -624,7 +624,7 @@ class TestEmailTools(BaseCase):
|
||||
['"Super Déboulonneur" <deboulonneur@example.com>'],
|
||||
# wrong formatting
|
||||
['"Déboulonneur" <deboulonneur@example.com>'],
|
||||
['Déboulonneur deboulonneur@example.com'], # returned as it, a bit strange but hey
|
||||
['"Déboulonneur" <deboulonneur@example.com>'], # extra part correctly considered as a name
|
||||
['deboulonneur@example.comDéboulonneur'], # concatenated, not sure why
|
||||
# multi
|
||||
['deboulonneur@example.com'],
|
||||
@@ -657,14 +657,14 @@ class TestEmailTools(BaseCase):
|
||||
[('Fredo The Great', 'alfred.astaire@test.example.com'), ('Evelyne The Goat', 'evelyne.gargouillis@test.example.com')],
|
||||
[('Fredo The Great', 'alfred.astaire@test.example.com'), ('', 'evelyne.gargouillis@test.example.com')],
|
||||
[('Fredo The Great', 'alfred.astaire@test.example.com'), ('', 'evelyne.gargouillis@test.example.com')],
|
||||
# text containing email -> probably not designed for that
|
||||
[('', 'Hello alfred.astaire@test.example.comhowareyou?')],
|
||||
[('', 'Hello alfred.astaire@test.example.com')],
|
||||
# text containing emails -> probably not designed for that
|
||||
# text containing email -> fallback on parsing to extract text from email
|
||||
[('Hello', 'alfred.astaire@test.example.comhowareyou?')],
|
||||
[('Hello', 'alfred.astaire@test.example.com')],
|
||||
[('Hello Fredo', 'alfred.astaire@test.example.com'), ('', 'evelyne.gargouillis@test.example.com')],
|
||||
[('Hello Fredo', 'alfred.astaire@test.example.com'), ('', 'and evelyne.gargouillis@test.example.com')],
|
||||
[('Hello Fredo', 'alfred.astaire@test.example.com'), ('and', 'evelyne.gargouillis@test.example.com')],
|
||||
# falsy -> probably not designed for that
|
||||
[], [('', "j'adore écrire des@gmail.comou"), ('', '@gmail.com')], [],
|
||||
[],
|
||||
[('j\'adore écrire', "des@gmail.comou"), ('', '@gmail.com')], [],
|
||||
]
|
||||
|
||||
for src, exp in zip(self.sources, expected):
|
||||
|
||||
+27
-12
@@ -526,14 +526,35 @@ def email_split_tuples(text):
|
||||
""" Return a list of (name, email) address tuples found in ``text`` . Note
|
||||
that text should be an email header or a stringified email list as it may
|
||||
give broader results than expected on actual text. """
|
||||
def _parse_based_on_spaces(pair):
|
||||
""" With input 'name email@domain.com' (missing quotes for a formatting)
|
||||
getaddresses returns ('', 'name email@domain.com). This when having no
|
||||
name and an email a fallback to enhance parsing is to redo a getaddresses
|
||||
by replacing spaces by commas. The new email will be split into sub pairs
|
||||
allowing to find the email and name parts, allowing to make a new name /
|
||||
email pair. Emails should not contain spaces thus this is coherent with
|
||||
email formation. """
|
||||
name, email = pair
|
||||
if not name and email and ' ' in email:
|
||||
inside_pairs = getaddresses([email.replace(' ', ',')])
|
||||
name_parts, found_email = [], False
|
||||
for pair in inside_pairs:
|
||||
if pair[1] and '@' not in pair[1]:
|
||||
name_parts.append(pair[1])
|
||||
if pair[1] and '@' in pair[1]:
|
||||
found_email = pair[1]
|
||||
name, email = (' '.join(name_parts), found_email) if found_email else (name, email)
|
||||
return (name, email)
|
||||
|
||||
if not text:
|
||||
return []
|
||||
return [(addr[0], addr[1]) for addr in getaddresses([text])
|
||||
# getaddresses() returns '' when email parsing fails, and
|
||||
# sometimes returns emails without at least '@'. The '@'
|
||||
# is strictly required in RFC2822's `addr-spec`.
|
||||
if addr[1]
|
||||
if '@' in addr[1]]
|
||||
return list(map(_parse_based_on_spaces, [
|
||||
(addr[0], addr[1]) for addr in getaddresses([text])
|
||||
# getaddresses() returns '' when email parsing fails, and
|
||||
# sometimes returns emails without at least '@'. The '@'
|
||||
# is strictly required in RFC2822's `addr-spec`.
|
||||
if addr[1] and '@' in addr[1]
|
||||
]))
|
||||
|
||||
def email_split(text):
|
||||
""" Return a list of the email addresses found in ``text`` """
|
||||
@@ -736,12 +757,6 @@ def parse_contact_from_email(text):
|
||||
split_results = email_split_tuples(text)
|
||||
name, email = split_results[0] if split_results else ('', '')
|
||||
|
||||
if email and not name:
|
||||
fallback_emails = email_split(text.replace(' ', ','))
|
||||
if fallback_emails:
|
||||
email = fallback_emails[0]
|
||||
name = text[:text.index(email)].replace('"', '').replace('<', '').strip()
|
||||
|
||||
if email:
|
||||
email_normalized = email_normalize(email, strict=False) or email
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user