[IMP] various: use 'email_formatted' instead of 'formataddr'

PURPOSE

Be defensive when dealing with email fields, notably when having multi-emails
or email field containing an already-formatted email.

SPECIFICATIONS

When possible, use 'email_formatted' field on partner, instead of calling
'format_addr'. That way management of corner case input (multi emails and
double encapsulation) is managed by the computed field itself.

When 'format_addr' has to be used, ensure email part is normalized to avoid
formatting issues.

Also use tools to extract emails instead of 'email_re' regex when possible.
That way all code extracting and managing emails go through the same stack.

Task-2612945 (Mail: Defensive email formatting)

X-original-commit: odoo/odoo@1c06014531
Part-of: odoo/odoo#134934
This commit is contained in:
Thibault Delavallée
2023-09-11 15:22:09 +00:00
parent 9339acaef7
commit daa4ee1881
7 changed files with 35 additions and 19 deletions
+21 -8
View File
@@ -17,7 +17,7 @@ from odoo.addons.phone_validation.tools import phone_validation
from odoo.exceptions import UserError, AccessError
from odoo.osv import expression
from odoo.tools.translate import _
from odoo.tools import date_utils, email_re, email_split, is_html_empty, groupby
from odoo.tools import date_utils, email_split, is_html_empty, groupby
from odoo.tools.misc import get_lang
from . import crm_stage
@@ -2046,14 +2046,27 @@ class Lead(models.Model):
return super(Lead, self)._message_post_after_hook(message, msg_vals)
def _message_partner_info_from_emails(self, emails, link_mail=False):
""" Try to propose a better recipient when having only an email by populating
it with the partner_name / contact_name field of the lead e.g. if lead
contact_name is "Raoul" and email is "raoul@raoul.fr", suggest
"Raoul" <raoul@raoul.fr> as recipient. """
result = super(Lead, self)._message_partner_info_from_emails(emails, link_mail=link_mail)
for partner_info in result:
if not partner_info.get('partner_id') and (self.partner_name or self.contact_name):
emails = email_re.findall(partner_info['full_name'] or '')
email = emails and emails[0] or ''
if email and self.email_from and email.lower() == self.email_from.lower():
partner_info['full_name'] = tools.formataddr((self.contact_name or self.partner_name, email))
break
for email, partner_info in zip(emails, result):
if partner_info.get('partner_id') or not email or not (self.partner_name or self.contact_name):
continue
# reformat email if no name information
name_emails = tools.email_split_tuples(email)
name_from_email = name_emails[0][0] if name_emails else False
if name_from_email:
continue # already containing name + email
name_from_email = self.partner_name or self.contact_name
emails_normalized = tools.email_normalize_all(email)
email_normalized = emails_normalized[0] if emails_normalized else False
if email.lower() == self.email_from.lower() or (email_normalized and self.email_normalized == email_normalized):
partner_info['full_name'] = tools.formataddr((
name_from_email,
','.join(emails_normalized) if emails_normalized else email))
break
return result
@api.model
@@ -65,9 +65,9 @@ class NewLeadNotification(TestCrmCommon):
'name': 'Format Name', 'user_id': self.user_sales_leads.id,
'team_id': self.sales_team_1.id,
}),
(False, 'new.customer.multi.1@test.example.com, new.customer.2@test.example.com', None, 'Customer Email',
(False, '"Multi Name" <new.customer.multi.1@test.example.com,new.customer.2@test.example.com>', None, 'Customer Email',
{}), # no email_normalized -> no information
(False, 'new.customer.simple@test.example.com', None, 'Customer Email',
(False, '"Std Name" <new.customer.simple@test.example.com>', None, 'Customer Email',
{'company_name': 'Std Name', 'email': 'new.customer.simple@test.example.com',
'name': 'Std Name', 'user_id': self.user_sales_leads.id,
'team_id': self.sales_team_1.id,
@@ -147,7 +147,7 @@ class NewLeadNotification(TestCrmCommon):
('Delivery Boy company', 'Test With Company', 'default_create_with_partner@example.com'),
('Delivery Boy company', '', 'default_create_with_partner_no_name@example.com'),
]:
formatted_email = formataddr((name, email))
formatted_email = formataddr((name, email)) if name else formataddr((partner_name, email))
with self.subTest(partner_name=partner_name):
lang = self.env['res.lang'].sudo().search([], limit=1)[0]
description = '<p>Top</p>'
+1 -1
View File
@@ -491,7 +491,7 @@ class Applicant(models.Model):
if applicant.partner_id:
applicant._message_add_suggested_recipient(recipients, partner=applicant.partner_id.sudo(), reason=_('Contact'))
elif applicant.email_from:
email_from = applicant.email_from
email_from = tools.email_normalize(applicant.email_from)
if applicant.partner_name:
email_from = tools.formataddr((applicant.partner_name, email_from))
applicant._message_add_suggested_recipient(recipients, email=email_from, reason=_('Contact Email'))
+4 -3
View File
@@ -665,7 +665,7 @@ class MailThread(models.AbstractModel):
'email_to': bounce_to,
'auto_delete': True,
}
bounce_from = self.env['ir.mail_server']._get_default_bounce_address()
bounce_from = tools.email_normalize(self.env['ir.mail_server']._get_default_bounce_address() or '')
if bounce_from:
bounce_mail_values['email_from'] = tools.formataddr(('MAILER-DAEMON', bounce_from))
elif self.env['ir.config_parameter'].sudo().get_param("mail.catchall.alias") not in message['To']:
@@ -1713,6 +1713,7 @@ class MailThread(models.AbstractModel):
partner_id, partner_name<partner_email> or partner_name, lang,
reason, create_values """
self.ensure_one()
partner_info = {}
if email and not partner:
# get partner info from email
partner_info = self._message_partner_info_from_emails([email])[0]
@@ -1727,11 +1728,11 @@ class MailThread(models.AbstractModel):
if partner and partner.email: # complete profile: id, name <email>
result[self.ids[0]].append((partner.id, partner.email_formatted, lang, reason, {}))
elif partner: # incomplete profile: id, name
result[self.ids[0]].append((partner.id, '%s' % (partner.name), lang, reason, {}))
result[self.ids[0]].append((partner.id, partner.name, lang, reason, {}))
else: # unknown partner, we are probably managing an email address
_, parsed_email = self.env['res.partner']._parse_partner_name(email)
partner_create_values = self._get_customer_information().get(parsed_email, {})
result[self.ids[0]].append((False, email, lang, reason, partner_create_values))
result[self.ids[0]].append((False, partner_info.get('full_name') or email, lang, reason, partner_create_values))
return result
def _message_get_suggested_recipients(self):
+4 -2
View File
@@ -15,8 +15,10 @@ class MailCCMixin(models.AbstractModel):
'''return a dict of sanitize_email:raw_email from a string of cc'''
if not cc_string:
return {}
return {tools.email_normalize(email): tools.formataddr((name, tools.email_normalize(email)))
for (name, email) in tools.email_split_tuples(cc_string)}
return {
tools.email_normalize(email): tools.formataddr((name, tools.email_normalize(email)))
for (name, email) in tools.email_split_tuples(cc_string)
}
@api.model
def message_new(self, msg_dict, custom_values=None):
-1
View File
@@ -3,7 +3,6 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.tools import formataddr
class SaleOrderCancel(models.TransientModel):
@@ -121,7 +121,8 @@ class WebsiteEventBoothController(WebsiteEventController):
def _prepare_booth_registration_partner_values(self, event, kwargs):
if request.env.user._is_public():
contact_name_email = tools.formataddr((kwargs['contact_name'], kwargs['contact_email']))
conctact_email_normalized = tools.email_normalize(kwargs['contact_email'])
contact_name_email = tools.formataddr((kwargs['contact_name'], conctact_email_normalized))
partner = request.env['res.partner'].sudo().find_or_create(contact_name_email)
if not partner.name and kwargs.get('contact_name'):
partner.name = kwargs['contact_name']