summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOdoo's Mergebot <xmo+roboto@odoo.com>2019-11-14 15:14:12 +0100
committerGitHub <noreply@github.com>2019-11-14 15:14:12 +0100
commit7352f0dea66df8d1acee75d7518051b91d0e01a3 (patch)
treed2d755d514aa93acf06e515fe5e648605d1dda93
parent29d8fc091ab5b37f79156c736bfa027a816c1c7a (diff)
parent7bf2dd43393be3758b3d8d6eeb94c877517c1bcf (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.py2
-rw-r--r--odoo/addons/base/models/ir_mail_server.py10
-rw-r--r--odoo/addons/base/tests/test_mail.py61
-rw-r--r--odoo/tools/mail.py13
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: