[1.6.x] Introduced as_bytes for SafeMIMEText (and other SafeMIME-classes).

This is to provide a consistent interface (namely bytes) for the smtp
backend which after all sends bytes over the wire; encoding with as_string
yields different results since mails as unicode are not really specified.

as_string stays for backwardscompatibilty mostly and some debug outputs.
But keep in mind that the output doesn't match as_bytes!

Backport of 5dfd824d38 from master.
This commit is contained in:
Florian Apolloner 2013-12-28 18:35:17 +01:00
parent 2d554d29f2
commit 7c674dd1f1
3 changed files with 85 additions and 52 deletions

View File

@ -7,7 +7,6 @@ from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend from django.core.mail.backends.base import BaseEmailBackend
from django.core.mail.utils import DNS_NAME from django.core.mail.utils import DNS_NAME
from django.core.mail.message import sanitize_address from django.core.mail.message import sanitize_address
from django.utils.encoding import force_bytes
class EmailBackend(BaseEmailBackend): class EmailBackend(BaseEmailBackend):
@ -107,11 +106,9 @@ class EmailBackend(BaseEmailBackend):
recipients = [sanitize_address(addr, email_message.encoding) recipients = [sanitize_address(addr, email_message.encoding)
for addr in email_message.recipients()] for addr in email_message.recipients()]
message = email_message.message() message = email_message.message()
charset = message.get_charset().get_output_charset() if message.get_charset() else 'utf-8'
try: try:
self.connection.sendmail(from_email, recipients, self.connection.sendmail(from_email, recipients, message.as_bytes())
force_bytes(message.as_string(), charset)) except smtplib.SMTPException:
except:
if not self.fail_silently: if not self.fail_silently:
raise raise
return False return False

View File

@ -130,21 +130,25 @@ class MIMEMixin():
This overrides the default as_string() implementation to not mangle This overrides the default as_string() implementation to not mangle
lines that begin with 'From '. See bug #13433 for details. lines that begin with 'From '. See bug #13433 for details.
""" """
# Using a normal Generator on python 3 will yield a string, which will fp = six.StringIO()
# get base64 encoded in some cases to ensure that it's always convertable g = generator.Generator(fp, mangle_from_=False)
# to ascii. We don't want base64 encoded emails, so we use a BytesGenertor g.flatten(self, unixfrom=unixfrom)
# which will do the right thing and then decode according to our known return fp.getvalue()
# encoding. See #21093 and #3472 for details.
if six.PY3 and sys.version_info >= (3, 3, 3): if six.PY2:
as_bytes = as_string
else:
def as_bytes(self, unixfrom=False):
"""Return the entire formatted message as bytes.
Optional `unixfrom' when True, means include the Unix From_ envelope
header.
This overrides the default as_bytes() implementation to not mangle
lines that begin with 'From '. See bug #13433 for details.
"""
fp = six.BytesIO() fp = six.BytesIO()
g = generator.BytesGenerator(fp, mangle_from_=False) g = generator.BytesGenerator(fp, mangle_from_=False)
g.flatten(self, unixfrom=unixfrom) g.flatten(self, unixfrom=unixfrom)
encoding = self.get_charset().get_output_charset() if self.get_charset() else 'utf-8'
return fp.getvalue().decode(encoding)
else:
fp = six.StringIO()
g = generator.Generator(fp, mangle_from_=False)
g.flatten(self, unixfrom=unixfrom)
return fp.getvalue() return fp.getvalue()
@ -158,9 +162,8 @@ class SafeMIMEText(MIMEMixin, MIMEText):
# We do it manually and trigger re-encoding of the payload. # We do it manually and trigger re-encoding of the payload.
MIMEText.__init__(self, text, subtype, None) MIMEText.__init__(self, text, subtype, None)
del self['Content-Transfer-Encoding'] del self['Content-Transfer-Encoding']
# Work around a bug in python 3.3.3 [sic], see # Workaround for versions without http://bugs.python.org/issue19063
# http://bugs.python.org/issue19063 for details. if (3, 2) < sys.version_info < (3, 3, 4):
if sys.version_info[:3] == (3, 3, 3):
payload = text.encode(utf8_charset.output_charset) payload = text.encode(utf8_charset.output_charset)
self._payload = payload.decode('ascii', 'surrogateescape') self._payload = payload.decode('ascii', 'surrogateescape')
self.set_charset(utf8_charset) self.set_charset(utf8_charset)

View File

@ -15,14 +15,38 @@ from django.core.mail import (EmailMessage, mail_admins, mail_managers,
EmailMultiAlternatives, send_mail, send_mass_mail) EmailMultiAlternatives, send_mail, send_mass_mail)
from django.core.mail.backends import console, dummy, locmem, filebased, smtp from django.core.mail.backends import console, dummy, locmem, filebased, smtp
from django.core.mail.message import BadHeaderError from django.core.mail.message import BadHeaderError
from django.test import TestCase from django.test import SimpleTestCase
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils.encoding import force_str, force_text from django.utils.encoding import force_str, force_text, force_bytes
from django.utils.six import PY3, StringIO from django.utils.six import PY3, StringIO, binary_type
from django.utils.translation import ugettext_lazy from django.utils.translation import ugettext_lazy
if PY3:
from email.utils import parseaddr
from email import message_from_bytes
else:
from email.Utils import parseaddr
message_from_bytes = email.message_from_string
class MailTests(TestCase):
class HeadersCheckMixin(object):
def assertMessageHasHeaders(self, message, headers):
"""
Check that :param message: has all :param headers: headers.
:param message: can be an instance of an email.Message subclass or a
string with the contens of an email message.
:param headers: should be a set of (header-name, header-value) tuples.
"""
if isinstance(message, binary_type):
message = message_from_bytes(message)
msg_headers = set(message.items())
self.assertTrue(headers.issubset(msg_headers), msg='Message is missing '
'the following headers: %s' % (headers - msg_headers),)
class MailTests(HeadersCheckMixin, SimpleTestCase):
""" """
Non-backend specific tests. Non-backend specific tests.
""" """
@ -191,8 +215,18 @@ class MailTests(TestCase):
msg = EmailMultiAlternatives('Subject', text_content, 'from@example.com', ['to@example.com']) msg = EmailMultiAlternatives('Subject', text_content, 'from@example.com', ['to@example.com'])
msg.encoding = 'iso-8859-1' msg.encoding = 'iso-8859-1'
msg.attach_alternative(html_content, "text/html") msg.attach_alternative(html_content, "text/html")
self.assertEqual(msg.message().get_payload(0).as_string(), 'Content-Type: text/plain; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\n\nFirstname S=FCrname is a great guy.') payload0 = msg.message().get_payload(0)
self.assertEqual(msg.message().get_payload(1).as_string(), 'Content-Type: text/html; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\n\n<p>Firstname S=FCrname is a <strong>great</strong> guy.</p>') self.assertMessageHasHeaders(payload0, {
('MIME-Version', '1.0'),
('Content-Type', 'text/plain; charset="iso-8859-1"'),
('Content-Transfer-Encoding', 'quoted-printable')})
self.assertTrue(payload0.as_bytes().endswith(b'\n\nFirstname S=FCrname is a great guy.'))
payload1 = msg.message().get_payload(1)
self.assertMessageHasHeaders(payload1, {
('MIME-Version', '1.0'),
('Content-Type', 'text/html; charset="iso-8859-1"'),
('Content-Transfer-Encoding', 'quoted-printable')})
self.assertTrue(payload1.as_bytes().endswith(b'\n\n<p>Firstname S=FCrname is a <strong>great</strong> guy.</p>'))
def test_attachments(self): def test_attachments(self):
"""Regression test for #9367""" """Regression test for #9367"""
@ -203,8 +237,8 @@ class MailTests(TestCase):
msg = EmailMultiAlternatives(subject, text_content, from_email, [to], headers=headers) msg = EmailMultiAlternatives(subject, text_content, from_email, [to], headers=headers)
msg.attach_alternative(html_content, "text/html") msg.attach_alternative(html_content, "text/html")
msg.attach("an attachment.pdf", b"%PDF-1.4.%...", mimetype="application/pdf") msg.attach("an attachment.pdf", b"%PDF-1.4.%...", mimetype="application/pdf")
msg_str = msg.message().as_string() msg_bytes = msg.message().as_bytes()
message = email.message_from_string(msg_str) message = message_from_bytes(msg_bytes)
self.assertTrue(message.is_multipart()) self.assertTrue(message.is_multipart())
self.assertEqual(message.get_content_type(), 'multipart/mixed') self.assertEqual(message.get_content_type(), 'multipart/mixed')
self.assertEqual(message.get_default_type(), 'text/plain') self.assertEqual(message.get_default_type(), 'text/plain')
@ -220,8 +254,8 @@ class MailTests(TestCase):
msg = EmailMessage(subject, content, from_email, [to], headers=headers) msg = EmailMessage(subject, content, from_email, [to], headers=headers)
# Unicode in file name # Unicode in file name
msg.attach("une pièce jointe.pdf", b"%PDF-1.4.%...", mimetype="application/pdf") msg.attach("une pièce jointe.pdf", b"%PDF-1.4.%...", mimetype="application/pdf")
msg_str = msg.message().as_string() msg_bytes = msg.message().as_bytes()
message = email.message_from_string(msg_str) message = message_from_bytes(msg_bytes)
payload = message.get_payload() payload = message.get_payload()
self.assertEqual(payload[1].get_filename(), 'une pièce jointe.pdf') self.assertEqual(payload[1].get_filename(), 'une pièce jointe.pdf')
@ -303,31 +337,31 @@ class MailTests(TestCase):
# Regression for #13433 - Make sure that EmailMessage doesn't mangle # Regression for #13433 - Make sure that EmailMessage doesn't mangle
# 'From ' in message body. # 'From ' in message body.
email = EmailMessage('Subject', 'From the future', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) email = EmailMessage('Subject', 'From the future', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
self.assertFalse('>From the future' in email.message().as_string()) self.assertFalse(b'>From the future' in email.message().as_bytes())
def test_dont_base64_encode(self): def test_dont_base64_encode(self):
# Ticket #3472 # Ticket #3472
# Shouldn't use Base64 encoding at all # Shouldn't use Base64 encoding at all
msg = EmailMessage('Subject', 'UTF-8 encoded body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) msg = EmailMessage('Subject', 'UTF-8 encoded body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
self.assertFalse('Content-Transfer-Encoding: base64' in msg.message().as_string()) self.assertFalse(b'Content-Transfer-Encoding: base64' in msg.message().as_bytes())
# Ticket #11212 # Ticket #11212
# Shouldn't use quoted printable, should detect it can represent content with 7 bit data # Shouldn't use quoted printable, should detect it can represent content with 7 bit data
msg = EmailMessage('Subject', 'Body with only ASCII characters.', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) msg = EmailMessage('Subject', 'Body with only ASCII characters.', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
s = msg.message().as_string() s = msg.message().as_bytes()
self.assertFalse('Content-Transfer-Encoding: quoted-printable' in s) self.assertFalse(b'Content-Transfer-Encoding: quoted-printable' in s)
self.assertTrue('Content-Transfer-Encoding: 7bit' in s) self.assertTrue(b'Content-Transfer-Encoding: 7bit' in s)
# Shouldn't use quoted printable, should detect it can represent content with 8 bit data # Shouldn't use quoted printable, should detect it can represent content with 8 bit data
msg = EmailMessage('Subject', 'Body with latin characters: àáä.', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) msg = EmailMessage('Subject', 'Body with latin characters: àáä.', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
s = msg.message().as_string() s = msg.message().as_bytes()
self.assertFalse(str('Content-Transfer-Encoding: quoted-printable') in s) self.assertFalse(b'Content-Transfer-Encoding: quoted-printable' in s)
self.assertTrue(str('Content-Transfer-Encoding: 8bit') in s) self.assertTrue(b'Content-Transfer-Encoding: 8bit' in s)
msg = EmailMessage('Subject', 'Body with non latin characters: А Б В Г Д Е Ж Ѕ З И І К Л М Н О П.', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) msg = EmailMessage('Subject', 'Body with non latin characters: А Б В Г Д Е Ж Ѕ З И І К Л М Н О П.', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
s = msg.message().as_string() s = msg.message().as_bytes()
self.assertFalse(str('Content-Transfer-Encoding: quoted-printable') in s) self.assertFalse(b'Content-Transfer-Encoding: quoted-printable' in s)
self.assertTrue(str('Content-Transfer-Encoding: 8bit') in s) self.assertTrue(b'Content-Transfer-Encoding: 8bit' in s)
class BaseEmailBackendTests(object): class BaseEmailBackendTests(object):
@ -374,7 +408,7 @@ class BaseEmailBackendTests(object):
self.assertEqual(num_sent, 1) self.assertEqual(num_sent, 1)
message = self.get_the_message() message = self.get_the_message()
self.assertEqual(message["subject"], '=?utf-8?q?Ch=C3=A8re_maman?=') self.assertEqual(message["subject"], '=?utf-8?q?Ch=C3=A8re_maman?=')
self.assertEqual(force_text(message.get_payload()), 'Je t\'aime très fort') self.assertEqual(force_text(message.get_payload(decode=True)), 'Je t\'aime très fort')
def test_send_many(self): def test_send_many(self):
email1 = EmailMessage('Subject', 'Content1', 'from@example.com', ['to@example.com']) email1 = EmailMessage('Subject', 'Content1', 'from@example.com', ['to@example.com'])
@ -503,7 +537,7 @@ class BaseEmailBackendTests(object):
self.fail("close() unexpectedly raised an exception: %s" % e) self.fail("close() unexpectedly raised an exception: %s" % e)
class LocmemBackendTests(BaseEmailBackendTests, TestCase): class LocmemBackendTests(BaseEmailBackendTests, SimpleTestCase):
email_backend = 'django.core.mail.backends.locmem.EmailBackend' email_backend = 'django.core.mail.backends.locmem.EmailBackend'
def get_mailbox_content(self): def get_mailbox_content(self):
@ -533,7 +567,7 @@ class LocmemBackendTests(BaseEmailBackendTests, TestCase):
send_mail('Subject\nMultiline', 'Content', 'from@example.com', ['to@example.com']) send_mail('Subject\nMultiline', 'Content', 'from@example.com', ['to@example.com'])
class FileBackendTests(BaseEmailBackendTests, TestCase): class FileBackendTests(BaseEmailBackendTests, SimpleTestCase):
email_backend = 'django.core.mail.backends.filebased.EmailBackend' email_backend = 'django.core.mail.backends.filebased.EmailBackend'
def setUp(self): def setUp(self):
@ -590,7 +624,7 @@ class FileBackendTests(BaseEmailBackendTests, TestCase):
connection.close() connection.close()
class ConsoleBackendTests(BaseEmailBackendTests, TestCase): class ConsoleBackendTests(BaseEmailBackendTests, SimpleTestCase):
email_backend = 'django.core.mail.backends.console.EmailBackend' email_backend = 'django.core.mail.backends.console.EmailBackend'
def setUp(self): def setUp(self):
@ -608,8 +642,8 @@ class ConsoleBackendTests(BaseEmailBackendTests, TestCase):
self.stream = sys.stdout = StringIO() self.stream = sys.stdout = StringIO()
def get_mailbox_content(self): def get_mailbox_content(self):
messages = force_text(self.stream.getvalue()).split('\n' + ('-' * 79) + '\n') messages = self.stream.getvalue().split(force_str('\n' + ('-' * 79) + '\n'))
return [email.message_from_string(force_str(m)) for m in messages if m] return [message_from_bytes(force_bytes(m)) for m in messages if m]
def test_console_stream_kwarg(self): def test_console_stream_kwarg(self):
""" """
@ -636,11 +670,10 @@ class FakeSMTPServer(smtpd.SMTPServer, threading.Thread):
self.sink_lock = threading.Lock() self.sink_lock = threading.Lock()
def process_message(self, peer, mailfrom, rcpttos, data): def process_message(self, peer, mailfrom, rcpttos, data):
m = email.message_from_string(data)
if PY3: if PY3:
maddr = email.utils.parseaddr(m.get('from'))[1] data = data.encode('utf-8')
else: m = message_from_bytes(data)
maddr = email.Utils.parseaddr(m.get('from'))[1] maddr = parseaddr(m.get('from'))[1]
if mailfrom != maddr: if mailfrom != maddr:
return "553 '%s' != '%s'" % (mailfrom, maddr) return "553 '%s' != '%s'" % (mailfrom, maddr)
with self.sink_lock: with self.sink_lock:
@ -674,7 +707,7 @@ class FakeSMTPServer(smtpd.SMTPServer, threading.Thread):
self.join() self.join()
class SMTPBackendTests(BaseEmailBackendTests, TestCase): class SMTPBackendTests(BaseEmailBackendTests, SimpleTestCase):
email_backend = 'django.core.mail.backends.smtp.EmailBackend' email_backend = 'django.core.mail.backends.smtp.EmailBackend'
@classmethod @classmethod