diff options
| author | Odoo's Mergebot <xmo+roboto@odoo.com> | 2019-11-14 15:14:12 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-11-14 15:14:12 +0100 |
| commit | 7352f0dea66df8d1acee75d7518051b91d0e01a3 (patch) | |
| tree | d2d755d514aa93acf06e515fe5e648605d1dda93 | |
| parent | 29d8fc091ab5b37f79156c736bfa027a816c1c7a (diff) | |
| parent | 7bf2dd43393be3758b3d8d6eeb94c877517c1bcf (diff) | |
[MERGE] base/mail: fix multiple issues in email address handling after #35929
[FIX] tools: correctly format emails when having an encoding issue
[IMP] tools: restore P2 formataddr default behavior
[FIX] ir.mail.server: squash redundant CRs (bpo-34424)
closes odoo/odoo#39516
Task: 2003936
Signed-off-by: Olivier Dony (odo) <odo@openerp.com>
| -rw-r--r-- | addons/mail/models/mail_thread.py | 2 | ||||
| -rw-r--r-- | odoo/addons/base/models/ir_mail_server.py | 10 | ||||
| -rw-r--r-- | odoo/addons/base/tests/test_mail.py | 61 | ||||
| -rw-r--r-- | odoo/tools/mail.py | 13 |
4 files changed, 75 insertions, 11 deletions
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 260fbfc9c24..499b0461b32 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1483,7 +1483,7 @@ class MailThread(models.AbstractModel): if partner and partner.id in [val[0] for val in result[self.ids[0]]]: # already existing partner ID -> skip return result if partner and partner.email: # complete profile: id, name <email> - result[self.ids[0]].append((partner.id, tools.formataddr((partner.name, partner.email)), reason)) + result[self.ids[0]].append((partner.id, partner.email_formatted, reason)) elif partner: # incomplete profile: id, name result[self.ids[0]].append((partner.id, '%s' % (partner.name), reason)) else: # unknown partner, we are probably managing an email address diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py index 65b6ee47c1e..8c5399236c7 100644 --- a/odoo/addons/base/models/ir_mail_server.py +++ b/odoo/addons/base/models/ir_mail_server.py @@ -9,6 +9,7 @@ import email.policy import logging import re import smtplib +import sys import threading import html2text @@ -414,7 +415,14 @@ class IrMailServer(models.Model): smtp = smtp or self.connect( smtp_server, smtp_port, smtp_user, smtp_password, smtp_encryption, smtp_debug, mail_server_id=mail_server_id) - smtp.sendmail(smtp_from, smtp_to_list, message.as_string()) + + message_str = message.as_string() + # header folding code is buggy and adds redundant carriage + # returns, it got fixed in 3.7.4 thanks to bpo-34424 + if sys.version_info < (3, 7, 4): + message_str = re.sub('\r+', '\r', message_str) + + smtp.sendmail(smtp_from, smtp_to_list, message_str) # do not quit() a pre-established smtp_session if not smtp_session: smtp.quit() diff --git a/odoo/addons/base/tests/test_mail.py b/odoo/addons/base/tests/test_mail.py index d47c9e6ae87..e1b80c52c28 100644 --- a/odoo/addons/base/tests/test_mail.py +++ b/odoo/addons/base/tests/test_mail.py @@ -3,10 +3,15 @@ from unittest.mock import patch +import email.policy +import email.message +import threading -from odoo.tests.common import BaseCase -from odoo.tests.common import SavepointCase -from odoo.tools import html_sanitize, append_content_to_html, plaintext2html, email_split, misc +from odoo.tests.common import BaseCase, SavepointCase, TransactionCase +from odoo.tools import ( + html_sanitize, append_content_to_html, plaintext2html, email_split, + misc, formataddr, +) from . import test_mail_examples @@ -328,6 +333,28 @@ class TestEmailTools(BaseCase): for text, expected in cases: self.assertEqual(email_split(text), expected, 'email_split is broken') + def test_email_formataddr(self): + email = 'joe@example.com' + cases = [ + # (name, address), charsets expected + (('', email), ['ascii', 'utf-8'], 'joe@example.com'), + (('joe', email), ['ascii', 'utf-8'], '"joe" <joe@example.com>'), + (('joe doe', email), ['ascii', 'utf-8'], '"joe doe" <joe@example.com>'), + (('joe"doe', email), ['ascii', 'utf-8'], '"joe\\"doe" <joe@example.com>'), + (('joé', email), ['ascii'], '=?utf-8?b?am/DqQ==?= <joe@example.com>'), + (('joé', email), ['utf-8'], '"joé" <joe@example.com>'), + (('', 'joé@example.com'), ['ascii', 'utf-8'], UnicodeEncodeError), # need SMTPUTF8 support + (('', 'joe@examplé.com'), ['ascii', 'utf-8'], UnicodeEncodeError), # need IDNA support + ] + + for pair, charsets, expected in cases: + for charset in charsets: + with self.subTest(pair=pair, charset=charset): + if isinstance(expected, str): + self.assertEqual(formataddr(pair, charset), expected) + else: + self.assertRaises(expected, formataddr, pair, charset) + class EmailConfigCase(SavepointCase): @patch.dict("odoo.tools.config.options", {"email_from": "settings@example.com"}) @@ -349,3 +376,31 @@ class EmailConfigCase(SavepointCase): "The body of an email", ) self.assertEqual(message["From"], "settings@example.com") + + +class TestEmailMessage(TransactionCase): + def test_as_string(self): + """Ensure all email sent are bpo-34424 free""" + + class FakeSMTP: + """SMTP stub""" + def __init__(this): + this.email_sent = False + + def sendmail(this, smtp_from, smtp_to_list, message_str): + this.email_sent = True + message_truth = ( + r'From: .+? <joe@example\.com>\r\n' + r'To: .+? <joe@example\.com>\r\n' + r'\r\n' + ) + self.assertRegex(message_str, message_truth) + + msg = email.message.EmailMessage(policy=email.policy.SMTP) + msg['From'] = '"Joé Doe" <joe@example.com>' + msg['To'] = '"Joé Doe" <joe@example.com>' + + smtp = FakeSMTP() + self.patch(threading.currentThread(), 'testing', False) + self.env['ir.mail_server'].send_email(msg, smtp_session=smtp) + self.assertTrue(smtp.email_sent) diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py index e1c999a041f..03ac8760ece 100644 --- a/odoo/tools/mail.py +++ b/odoo/tools/mail.py @@ -516,9 +516,10 @@ def email_escape_char(email_address): def decode_message_header(message, header, separator=' '): return separator.join(h for h in message.get_all(header, []) if h) -def formataddr(pair): - """Takes a 2-tuple of the form (realname, email_address) and returns - the string value suitable for an RFC 2822 From, To or Cc header. +def formataddr(pair, charset='utf-8'): + """Pretty format a 2-tuple of the form (realname, email_address). + + Set the charset to ascii to get a RFC-2822 compliant email. The email address is considered valid and is left unmodified. @@ -535,11 +536,11 @@ def formataddr(pair): address.encode('ascii') if name: try: - name.encode('ascii') + name.encode(charset) except UnicodeEncodeError: - # non-ascii name, transcode the name in a safe format + # charset mismatch, encode as utf-8/base64 # rfc2047 - MIME Message Header Extensions for Non-ASCII Text - return "=?utf-8?b?{name}?= {addr}".format( + return "=?utf-8?b?{name}?= <{addr}>".format( name=base64.b64encode(name.encode('utf-8')).decode('ascii'), addr=address) else: |
