diff --git a/addons/test_mail/tests/test_mail_mail.py b/addons/test_mail/tests/test_mail_mail.py
index 7e6b0f98ba2..97a6740211d 100644
--- a/addons/test_mail/tests/test_mail_mail.py
+++ b/addons/test_mail/tests/test_mail_mail.py
@@ -25,16 +25,6 @@ class TestMailMail(TestMailCommon):
self.assertSentEmail(mail.env.user.partner_id, ['test@example.com'])
self.assertEqual(len(self._mails), 1)
- @mute_logger('odoo.addons.mail.models.mail_mail')
- def test_mail_message_values_unicode(self):
- mail = self.env['mail.mail'].sudo().create({
- 'body_html': '
Test
',
- 'email_to': 'test.😊@example.com',
- 'partner_ids': [(4, self.user_employee.partner_id.id)]
- })
-
- self.assertRaises(MailDeliveryException, lambda: mail.send(raise_exception=True))
-
class TestMailMailRace(common.TransactionCase):
diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py
index 8d0bf2a21f1..af83fee5805 100644
--- a/odoo/addons/base/models/ir_mail_server.py
+++ b/odoo/addons/base/models/ir_mail_server.py
@@ -16,7 +16,7 @@ import html2text
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError
-from odoo.tools import ustr, pycompat
+from odoo.tools import ustr, pycompat, formataddr
_logger = logging.getLogger(__name__)
_test_logger = logging.getLogger('odoo.tests')
@@ -52,7 +52,7 @@ def extract_rfc2822_addresses(text):
if not text:
return []
candidates = address_pattern.findall(ustr(text))
- return [c for c in candidates if is_ascii(c)]
+ return [formataddr(('', c), charset='ascii') for c in candidates]
class IrMailServer(models.Model):
@@ -415,13 +415,21 @@ class IrMailServer(models.Model):
smtp_server, smtp_port, smtp_user, smtp_password,
smtp_encryption, smtp_debug, mail_server_id=mail_server_id)
- 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)
+ # header folding code is buggy and adds redundant carriage
+ # returns, it got fixed in 3.7.4 thanks to bpo-34424
+ message_str = message.as_string()
+ message_str = re.sub('\r+(?!\n)', '', message_str)
+
+ mail_options = []
+ if any((not is_ascii(addr) for addr in smtp_to_list + [smtp_from])):
+ # non ascii email found, require SMTPUTF8 extension,
+ # the relay may reject it
+ mail_options.append("SMTPUTF8")
+ smtp.sendmail(smtp_from, smtp_to_list, message_str, mail_options=mail_options)
+ else:
+ smtp.send_message(message, smtp_from, smtp_to_list)
- 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 c4343c0d377..3fdadaba373 100644
--- a/odoo/addons/base/tests/test_mail.py
+++ b/odoo/addons/base/tests/test_mail.py
@@ -351,6 +351,7 @@ class TestEmailTools(BaseCase):
def test_email_formataddr(self):
email = 'joe@example.com'
+ email_idna = 'joe@examplé.com'
cases = [
# (name, address), charsets expected
(('', email), ['ascii', 'utf-8'], 'joe@example.com'),
@@ -359,17 +360,17 @@ class TestEmailTools(BaseCase):
(('joe"doe', email), ['ascii', 'utf-8'], '"joe\\"doe" '),
(('joé', email), ['ascii'], '=?utf-8?b?am/DqQ==?= '),
(('joé', email), ['utf-8'], '"joé" '),
- (('', 'joé@example.com'), ['ascii', 'utf-8'], UnicodeEncodeError), # need SMTPUTF8 support
- (('', 'joe@examplé.com'), ['ascii', 'utf-8'], UnicodeEncodeError), # need IDNA support
+ (('', email_idna), ['ascii'], 'joe@xn--exampl-gva.com'),
+ (('', email_idna), ['utf-8'], 'joe@examplé.com'),
+ (('joé', email_idna), ['ascii'], '=?utf-8?b?am/DqQ==?= '),
+ (('joé', email_idna), ['utf-8'], '"joé" '),
+ (('', 'joé@example.com'), ['ascii', 'utf-8'], 'joé@example.com'),
]
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)
+ self.assertEqual(formataddr(pair, charset), expected)
class EmailConfigCase(SavepointCase):
@@ -403,7 +404,19 @@ class TestEmailMessage(TransactionCase):
def __init__(this):
this.email_sent = False
- def sendmail(this, smtp_from, smtp_to_list, message_str):
+ def sendmail(this, smtp_from, smtp_to_list, message_str,
+ mail_options=(), rcpt_options=()):
+ this.email_sent = True
+ message_truth = (
+ r'From: .+? \r\n'
+ r'To: .+? \r\n'
+ r'\r\n'
+ )
+ self.assertRegex(message_str, message_truth)
+
+ def send_message(this, message, smtp_from, smtp_to_list,
+ mail_options=(), rcpt_options=()):
+ message_str = message.as_string()
this.email_sent = True
message_truth = (
r'From: .+? \r\n'
diff --git a/odoo/tools/mail.py b/odoo/tools/mail.py
index c1a14e964fc..1a3e9d80b9f 100644
--- a/odoo/tools/mail.py
+++ b/odoo/tools/mail.py
@@ -14,6 +14,7 @@ import time
from email.utils import getaddresses
from lxml import etree
from werkzeug import urls
+import idna
import odoo
from odoo.loglevels import ustr
@@ -546,13 +547,15 @@ def decode_message_header(message, header, separator=' '):
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.
-
If the first element of pair is falsy then only the email address
is returned.
+ Set the charset to ascii to get a RFC-2822 compliant email. The
+ realname will be base64 encoded (if necessary) and the domain part
+ of the email will be punycode encoded (if necessary). The local part
+ is left unchanged thus require the SMTPUTF8 extension when there are
+ non-ascii characters.
+
>>> formataddr(('John Doe', 'johndoe@example.com'))
'"John Doe" '
@@ -560,21 +563,26 @@ def formataddr(pair, charset='utf-8'):
'johndoe@example.com'
"""
name, address = pair
- address.encode('ascii')
+ local, _, domain = address.rpartition('@')
+
+ try:
+ domain.encode(charset)
+ except UnicodeEncodeError:
+ # rfc5890 - Internationalized Domain Names for Applications (IDNA)
+ domain = idna.encode(domain).decode('ascii')
+
if name:
try:
name.encode(charset)
except UnicodeEncodeError:
# charset mismatch, encode as utf-8/base64
# rfc2047 - MIME Message Header Extensions for Non-ASCII Text
- return "=?utf-8?b?{name}?= <{addr}>".format(
- name=base64.b64encode(name.encode('utf-8')).decode('ascii'),
- addr=address)
+ name = base64.b64encode(name.encode('utf-8')).decode('ascii')
+ return f"=?utf-8?b?{name}?= <{local}@{domain}>"
else:
# ascii name, escape it if needed
# rfc2822 - Internet Message Format
# #section-3.4 - Address Specification
- return '"{name}" <{addr}>'.format(
- name=email_addr_escapes_re.sub(r'\\\g<0>', name),
- addr=address)
- return address
+ name = email_addr_escapes_re.sub(r'\\\g<0>', name)
+ return f'"{name}" <{local}@{domain}>'
+ return f"{local}@{domain}"
diff --git a/requirements.txt b/requirements.txt
index 3157d26b992..693b8347907 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,6 +10,7 @@ gevent==1.4.0 ; sys_platform == 'win32'
greenlet==0.4.10 ; python_version < '3.7'
greenlet==0.4.15 ; python_version >= '3.7'
html2text==2018.1.9
+idna==2.6
Jinja2==2.10.1
libsass==0.17.0
lxml==3.7.1 ; sys_platform != 'win32' and python_version < '3.7'
diff --git a/setup.cfg b/setup.cfg
index f3a0c3160dd..0523f3f81c1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,6 +20,7 @@ requires =
python3-gevent
python3-greenlet
python3-html2text
+ python3-idna
python3-jinja2
python3-lxml
python3-mako
diff --git a/setup.py b/setup.py
index cdcf6aa03b5..6142915cdde 100644
--- a/setup.py
+++ b/setup.py
@@ -29,6 +29,7 @@ setup(
'feedparser',
'gevent',
'html2text',
+ 'idna',
'Jinja2',
'lxml', # windows binary http://www.lfd.uci.edu/~gohlke/pythonlibs/
'libsass',