From daa4ee1881760faf2baf1579e9713ac99ff0d2ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 11 Apr 2022 11:38:31 +0200 Subject: [PATCH] [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@1c060145315ce443c644f53d4cf3839b0b5a1bb6 Part-of: odoo/odoo#134934 --- addons/crm/models/crm_lead.py | 29 ++++++++++++++----- .../crm/tests/test_crm_lead_notification.py | 6 ++-- addons/hr_recruitment/models/hr_applicant.py | 2 +- addons/mail/models/mail_thread.py | 7 +++-- addons/mail/models/mail_thread_cc.py | 6 ++-- addons/sale/wizard/sale_order_cancel.py | 1 - .../controllers/event_booth.py | 3 +- 7 files changed, 35 insertions(+), 19 deletions(-) diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py index a3e4ebbe95f..3a823992a12 100644 --- a/addons/crm/models/crm_lead.py +++ b/addons/crm/models/crm_lead.py @@ -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" 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 diff --git a/addons/crm/tests/test_crm_lead_notification.py b/addons/crm/tests/test_crm_lead_notification.py index 420c0c391bf..344e2bef650 100644 --- a/addons/crm/tests/test_crm_lead_notification.py +++ b/addons/crm/tests/test_crm_lead_notification.py @@ -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" ', None, 'Customer Email', {}), # no email_normalized -> no information - (False, 'new.customer.simple@test.example.com', None, 'Customer Email', + (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, '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 = '

Top

' diff --git a/addons/hr_recruitment/models/hr_applicant.py b/addons/hr_recruitment/models/hr_applicant.py index 783576b6fb1..4be7554cffa 100644 --- a/addons/hr_recruitment/models/hr_applicant.py +++ b/addons/hr_recruitment/models/hr_applicant.py @@ -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')) diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 4aeb6f3b84e..871f965bfdf 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -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 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 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): diff --git a/addons/mail/models/mail_thread_cc.py b/addons/mail/models/mail_thread_cc.py index 0a5de43ca3d..fb1dcf6f877 100644 --- a/addons/mail/models/mail_thread_cc.py +++ b/addons/mail/models/mail_thread_cc.py @@ -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): diff --git a/addons/sale/wizard/sale_order_cancel.py b/addons/sale/wizard/sale_order_cancel.py index ecbe195dcd0..82bc4f61381 100644 --- a/addons/sale/wizard/sale_order_cancel.py +++ b/addons/sale/wizard/sale_order_cancel.py @@ -3,7 +3,6 @@ from odoo import _, api, fields, models from odoo.exceptions import UserError -from odoo.tools import formataddr class SaleOrderCancel(models.TransientModel): diff --git a/addons/website_event_booth/controllers/event_booth.py b/addons/website_event_booth/controllers/event_booth.py index 315ea0286a9..f78a2273ab0 100644 --- a/addons/website_event_booth/controllers/event_booth.py +++ b/addons/website_event_booth/controllers/event_booth.py @@ -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']