From 8d789449c7861b8cf8b10d244f26d9d354989aaf Mon Sep 17 00:00:00 2001 From: Florian Apolloner Date: Thu, 9 Oct 2014 14:18:31 +0200 Subject: [PATCH] Fixed #23063 -- Convert \n and \r to \r\n when using the SMTP backend as per RFC. --- django/core/mail/backends/smtp.py | 2 +- django/core/mail/message.py | 11 ++++++---- docs/releases/1.7.1.txt | 2 ++ tests/mail/tests.py | 36 ++++++++++++++++++++++++++++++- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/django/core/mail/backends/smtp.py b/django/core/mail/backends/smtp.py index 4c41eb8bd6..620168eab2 100644 --- a/django/core/mail/backends/smtp.py +++ b/django/core/mail/backends/smtp.py @@ -120,7 +120,7 @@ class EmailBackend(BaseEmailBackend): for addr in email_message.recipients()] message = email_message.message() try: - self.connection.sendmail(from_email, recipients, message.as_bytes()) + self.connection.sendmail(from_email, recipients, message.as_bytes(linesep='\r\n')) except smtplib.SMTPException: if not self.fail_silently: raise diff --git a/django/core/mail/message.py b/django/core/mail/message.py index da9891fb2f..63b00144f5 100644 --- a/django/core/mail/message.py +++ b/django/core/mail/message.py @@ -123,7 +123,7 @@ def sanitize_address(addr, encoding): class MIMEMixin(): - def as_string(self, unixfrom=False): + def as_string(self, unixfrom=False, linesep='\n'): """Return the entire formatted message as a string. Optional `unixfrom' when True, means include the Unix From_ envelope header. @@ -133,13 +133,16 @@ class MIMEMixin(): """ fp = six.StringIO() g = generator.Generator(fp, mangle_from_=False) - g.flatten(self, unixfrom=unixfrom) + if six.PY2: + g.flatten(self, unixfrom=unixfrom) + else: + g.flatten(self, unixfrom=unixfrom, linesep=linesep) return fp.getvalue() if six.PY2: as_bytes = as_string else: - def as_bytes(self, unixfrom=False): + def as_bytes(self, unixfrom=False, linesep='\n'): """Return the entire formatted message as bytes. Optional `unixfrom' when True, means include the Unix From_ envelope header. @@ -149,7 +152,7 @@ class MIMEMixin(): """ fp = six.BytesIO() g = generator.BytesGenerator(fp, mangle_from_=False) - g.flatten(self, unixfrom=unixfrom) + g.flatten(self, unixfrom=unixfrom, linesep=linesep) return fp.getvalue() diff --git a/docs/releases/1.7.1.txt b/docs/releases/1.7.1.txt index f8d3936719..b66e259679 100644 --- a/docs/releases/1.7.1.txt +++ b/docs/releases/1.7.1.txt @@ -115,3 +115,5 @@ Bugfixes (:ticket:`23609`). * Fixed generic relations in ``ModelAdmin.list_filter`` (:ticket:`23616`). + +* Restored RFC compliance for the SMTP backend on Python 3 (:ticket:`23063`). diff --git a/tests/mail/tests.py b/tests/mail/tests.py index df6daee5c8..ccfdc6c669 100644 --- a/tests/mail/tests.py +++ b/tests/mail/tests.py @@ -9,7 +9,7 @@ import smtpd import sys import tempfile import threading -from smtplib import SMTPException +from smtplib import SMTPException, SMTP from ssl import SSLError from django.core import mail @@ -1038,3 +1038,37 @@ class SMTPBackendTests(BaseEmailBackendTests, SimpleTestCase): def test_email_timeout_override_settings(self): backend = smtp.EmailBackend() self.assertEqual(backend.timeout, 10) + + def test_email_msg_uses_crlf(self): + """#23063 -- Test that RFC-compliant messages are sent over SMTP.""" + send = SMTP.send + try: + smtp_messages = [] + + def mock_send(self, s): + smtp_messages.append(s) + return send(self, s) + + SMTP.send = mock_send + + email = EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com']) + mail.get_connection().send_messages([email]) + + # Find the actual message + msg = None + for i, m in enumerate(smtp_messages): + if m[:4] == 'data': + msg = smtp_messages[i+1] + break + + self.assertTrue(msg) + + if PY3: + msg = msg.decode('utf-8') + # Ensure that the message only contains CRLF and not combinations of CRLF, LF, and CR. + msg = msg.replace('\r\n', '') + self.assertNotIn('\r', msg) + self.assertNotIn('\n', msg) + + finally: + SMTP.send = send