Commit 7c674dd1 authored by Florian Apolloner's avatar Florian Apolloner
Browse files

[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 5dfd824d from master.
parent 2d554d29
Loading
Loading
Loading
Loading
+2 −5
Original line number Diff line number Diff line
@@ -7,7 +7,6 @@ from django.conf import settings
from django.core.mail.backends.base import BaseEmailBackend
from django.core.mail.utils import DNS_NAME
from django.core.mail.message import sanitize_address
from django.utils.encoding import force_bytes


class EmailBackend(BaseEmailBackend):
@@ -107,11 +106,9 @@ class EmailBackend(BaseEmailBackend):
        recipients = [sanitize_address(addr, email_message.encoding)
                      for addr in email_message.recipients()]
        message = email_message.message()
        charset = message.get_charset().get_output_charset() if message.get_charset() else 'utf-8'
        try:
            self.connection.sendmail(from_email, recipients,
                    force_bytes(message.as_string(), charset))
        except:
            self.connection.sendmail(from_email, recipients, message.as_bytes())
        except smtplib.SMTPException:
            if not self.fail_silently:
                raise
            return False
+18 −15
Original line number Diff line number Diff line
@@ -130,23 +130,27 @@ class MIMEMixin():
        This overrides the default as_string() implementation to not mangle
        lines that begin with 'From '. See bug #13433 for details.
        """
        # Using a normal Generator on python 3 will yield a string, which will
        # get base64 encoded in some cases to ensure that it's always convertable
        # to ascii. We don't want base64 encoded emails, so we use a BytesGenertor
        # which will do the right thing and then decode according to our known
        # encoding. See #21093 and #3472 for details.
        if six.PY3 and sys.version_info >= (3, 3, 3):
            fp = six.BytesIO()
            g = generator.BytesGenerator(fp, mangle_from_=False)
            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()

    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()
            g = generator.BytesGenerator(fp, mangle_from_=False)
            g.flatten(self, unixfrom=unixfrom)
            return fp.getvalue()


class SafeMIMEText(MIMEMixin, MIMEText):

@@ -158,9 +162,8 @@ class SafeMIMEText(MIMEMixin, MIMEText):
            # We do it manually and trigger re-encoding of the payload.
            MIMEText.__init__(self, text, subtype, None)
            del self['Content-Transfer-Encoding']
            # Work around a bug in python 3.3.3 [sic], see
            # http://bugs.python.org/issue19063 for details.
            if sys.version_info[:3] == (3, 3, 3):
            # Workaround for versions without http://bugs.python.org/issue19063
            if (3, 2) < sys.version_info < (3, 3, 4):
                payload = text.encode(utf8_charset.output_charset)
                self._payload = payload.decode('ascii', 'surrogateescape')
                self.set_charset(utf8_charset)
+65 −32
Original line number Diff line number Diff line
@@ -15,14 +15,38 @@ from django.core.mail import (EmailMessage, mail_admins, mail_managers,
        EmailMultiAlternatives, send_mail, send_mass_mail)
from django.core.mail.backends import console, dummy, locmem, filebased, smtp
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.utils.encoding import force_str, force_text
from django.utils.six import PY3, StringIO
from django.utils.encoding import force_str, force_text, force_bytes
from django.utils.six import PY3, StringIO, binary_type
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 HeadersCheckMixin(object):

    def assertMessageHasHeaders(self, message, headers):
        """
        Check that :param message: has all :param headers: headers.

class MailTests(TestCase):
        :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.
    """
@@ -191,8 +215,18 @@ class MailTests(TestCase):
        msg = EmailMultiAlternatives('Subject', text_content, 'from@example.com', ['to@example.com'])
        msg.encoding = 'iso-8859-1'
        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.')
        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>')
        payload0 = msg.message().get_payload(0)
        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):
        """Regression test for #9367"""
@@ -203,8 +237,8 @@ class MailTests(TestCase):
        msg = EmailMultiAlternatives(subject, text_content, from_email, [to], headers=headers)
        msg.attach_alternative(html_content, "text/html")
        msg.attach("an attachment.pdf", b"%PDF-1.4.%...", mimetype="application/pdf")
        msg_str = msg.message().as_string()
        message = email.message_from_string(msg_str)
        msg_bytes = msg.message().as_bytes()
        message = message_from_bytes(msg_bytes)
        self.assertTrue(message.is_multipart())
        self.assertEqual(message.get_content_type(), 'multipart/mixed')
        self.assertEqual(message.get_default_type(), 'text/plain')
@@ -220,8 +254,8 @@ class MailTests(TestCase):
        msg = EmailMessage(subject, content, from_email, [to], headers=headers)
        # Unicode in file name
        msg.attach("une pièce jointe.pdf", b"%PDF-1.4.%...", mimetype="application/pdf")
        msg_str = msg.message().as_string()
        message = email.message_from_string(msg_str)
        msg_bytes = msg.message().as_bytes()
        message = message_from_bytes(msg_bytes)
        payload = message.get_payload()
        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
        # 'From ' in message body.
        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):
        # Ticket #3472
        # 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'})
        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
        # 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'})
        s = msg.message().as_string()
        self.assertFalse('Content-Transfer-Encoding: quoted-printable' in s)
        self.assertTrue('Content-Transfer-Encoding: 7bit' in s)
        s = msg.message().as_bytes()
        self.assertFalse(b'Content-Transfer-Encoding: quoted-printable' 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
        msg = EmailMessage('Subject', 'Body with latin characters: àáä.', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
        s = msg.message().as_string()
        self.assertFalse(str('Content-Transfer-Encoding: quoted-printable') in s)
        self.assertTrue(str('Content-Transfer-Encoding: 8bit') in s)
        s = msg.message().as_bytes()
        self.assertFalse(b'Content-Transfer-Encoding: quoted-printable' 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'})
        s = msg.message().as_string()
        self.assertFalse(str('Content-Transfer-Encoding: quoted-printable') in s)
        self.assertTrue(str('Content-Transfer-Encoding: 8bit') in s)
        s = msg.message().as_bytes()
        self.assertFalse(b'Content-Transfer-Encoding: quoted-printable' in s)
        self.assertTrue(b'Content-Transfer-Encoding: 8bit' in s)


class BaseEmailBackendTests(object):
@@ -374,7 +408,7 @@ class BaseEmailBackendTests(object):
        self.assertEqual(num_sent, 1)
        message = self.get_the_message()
        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):
        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)


class LocmemBackendTests(BaseEmailBackendTests, TestCase):
class LocmemBackendTests(BaseEmailBackendTests, SimpleTestCase):
    email_backend = 'django.core.mail.backends.locmem.EmailBackend'

    def get_mailbox_content(self):
@@ -533,7 +567,7 @@ class LocmemBackendTests(BaseEmailBackendTests, TestCase):
            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'

    def setUp(self):
@@ -590,7 +624,7 @@ class FileBackendTests(BaseEmailBackendTests, TestCase):
        connection.close()


class ConsoleBackendTests(BaseEmailBackendTests, TestCase):
class ConsoleBackendTests(BaseEmailBackendTests, SimpleTestCase):
    email_backend = 'django.core.mail.backends.console.EmailBackend'

    def setUp(self):
@@ -608,8 +642,8 @@ class ConsoleBackendTests(BaseEmailBackendTests, TestCase):
        self.stream = sys.stdout = StringIO()

    def get_mailbox_content(self):
        messages = force_text(self.stream.getvalue()).split('\n' + ('-' * 79) + '\n')
        return [email.message_from_string(force_str(m)) for m in messages if m]
        messages = self.stream.getvalue().split(force_str('\n' + ('-' * 79) + '\n'))
        return [message_from_bytes(force_bytes(m)) for m in messages if m]

    def test_console_stream_kwarg(self):
        """
@@ -636,11 +670,10 @@ class FakeSMTPServer(smtpd.SMTPServer, threading.Thread):
        self.sink_lock = threading.Lock()

    def process_message(self, peer, mailfrom, rcpttos, data):
        m = email.message_from_string(data)
        if PY3:
            maddr = email.utils.parseaddr(m.get('from'))[1]
        else:
            maddr = email.Utils.parseaddr(m.get('from'))[1]
            data = data.encode('utf-8')
        m = message_from_bytes(data)
        maddr = parseaddr(m.get('from'))[1]
        if mailfrom != maddr:
            return "553 '%s' != '%s'" % (mailfrom, maddr)
        with self.sink_lock:
@@ -674,7 +707,7 @@ class FakeSMTPServer(smtpd.SMTPServer, threading.Thread):
            self.join()


class SMTPBackendTests(BaseEmailBackendTests, TestCase):
class SMTPBackendTests(BaseEmailBackendTests, SimpleTestCase):
    email_backend = 'django.core.mail.backends.smtp.EmailBackend'

    @classmethod