[FIX] ir.mail.server: squash redundant CRs (bpo-34424)

Problem description:
Using the chatter, send an email to someone having non-ascii characters
in their name: the body of the received email looks like it contains
a mix of the original body and headers.

Python encodes the name into either base64 or quoted-printable but
leaves some redundant carriage returns at the end of the header. Those
redundant carriage returns are not RFC conformant but are interpreted
as newlines by some email clients, thus are read like a headers/body
separator and the next headers are considered part of the body,
corrupting the email structure.

This problem is internal to Python and has been fixed in 3.8, cfr
bpo-34424 [1], and later backported to 3.7.4. As we also support 3.6
and earlier 3.7 and it is not possible to easily monkey-patch the
function, we fixed it by squashing duplicate carriage returns.
(There is no case where a series of bare CRs can occur in a normal
RFC5322 email message)

Task: 2003936

[1]: https://bugs.python.org/issue34424
This commit is contained in:
Julien Castiaux
2019-11-14 12:29:36 +00:00
committed by Olivier Dony
parent 7b65a63c97
commit 7bf2dd4339
2 changed files with 41 additions and 3 deletions
+9 -1
View File
@@ -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()
+32 -2
View File
@@ -3,9 +3,11 @@
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.tests.common import BaseCase, SavepointCase, TransactionCase
from odoo.tools import (
html_sanitize, append_content_to_html, plaintext2html, email_split,
misc, formataddr,
@@ -374,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)