diff --git a/django/core/mail/message.py b/django/core/mail/message.py index 8f589ae33da8b..b332ffba04fa7 100644 --- a/django/core/mail/message.py +++ b/django/core/mail/message.py @@ -11,11 +11,10 @@ from email.mime.base import MIMEBase from email.header import Header from email.utils import formatdate, getaddresses, formataddr, parseaddr -from io import BytesIO from django.conf import settings from django.core.mail.utils import DNS_NAME -from django.utils.encoding import smart_bytes, force_text +from django.utils.encoding import force_text from django.utils import six @@ -83,34 +82,34 @@ def forbid_multi_line_headers(name, val, encoding): if '\n' in val or '\r' in val: raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name)) try: - val = val.encode('ascii') + val.encode('ascii') except UnicodeEncodeError: if name.lower() in ADDRESS_HEADERS: val = ', '.join(sanitize_address(addr, encoding) for addr in getaddresses((val,))) else: - val = str(Header(val, encoding)) + val = Header(val, encoding).encode() else: if name.lower() == 'subject': - val = Header(val) - return smart_bytes(name), val + val = Header(val).encode() + return str(name), val def sanitize_address(addr, encoding): if isinstance(addr, six.string_types): addr = parseaddr(force_text(addr)) nm, addr = addr - nm = str(Header(nm, encoding)) + nm = Header(nm, encoding).encode() try: - addr = addr.encode('ascii') + addr.encode('ascii') except UnicodeEncodeError: # IDN if '@' in addr: localpart, domain = addr.split('@', 1) localpart = str(Header(localpart, encoding)) - domain = domain.encode('idna') + domain = domain.encode('idna').decode('ascii') addr = '@'.join([localpart, domain]) else: - addr = str(Header(addr, encoding)) + addr = Header(addr, encoding).encode() return formataddr((nm, addr)) @@ -132,7 +131,7 @@ def as_string(self, unixfrom=False): This overrides the default as_string() implementation to not mangle lines that begin with 'From '. See bug #13433 for details. """ - fp = BytesIO() + fp = six.StringIO() g = Generator(fp, mangle_from_ = False) g.flatten(self, unixfrom=unixfrom) return fp.getvalue() @@ -156,7 +155,7 @@ def as_string(self, unixfrom=False): This overrides the default as_string() implementation to not mangle lines that begin with 'From '. See bug #13433 for details. """ - fp = BytesIO() + fp = six.StringIO() g = Generator(fp, mangle_from_ = False) g.flatten(self, unixfrom=unixfrom) return fp.getvalue() @@ -210,8 +209,7 @@ def get_connection(self, fail_silently=False): def message(self): encoding = self.encoding or settings.DEFAULT_CHARSET - msg = SafeMIMEText(smart_bytes(self.body, encoding), - self.content_subtype, encoding) + msg = SafeMIMEText(self.body, self.content_subtype, encoding) msg = self._create_message(msg) msg['Subject'] = self.subject msg['From'] = self.extra_headers.get('From', self.from_email) @@ -293,7 +291,7 @@ def _create_mime_attachment(self, content, mimetype): basetype, subtype = mimetype.split('/', 1) if basetype == 'text': encoding = self.encoding or settings.DEFAULT_CHARSET - attachment = SafeMIMEText(smart_bytes(content, encoding), subtype, encoding) + attachment = SafeMIMEText(content, subtype, encoding) else: # Encode non-text attachments with base64. attachment = MIMEBase(basetype, subtype) @@ -313,9 +311,11 @@ def _create_attachment(self, filename, content, mimetype=None): attachment = self._create_mime_attachment(content, mimetype) if filename: try: - filename = filename.encode('ascii') + filename.encode('ascii') except UnicodeEncodeError: - filename = ('utf-8', '', filename.encode('utf-8')) + if not six.PY3: + filename = filename.encode('utf-8') + filename = ('utf-8', '', filename) attachment.add_header('Content-Disposition', 'attachment', filename=filename) return attachment diff --git a/tests/regressiontests/mail/tests.py b/tests/regressiontests/mail/tests.py index 0d0af19427e0c..c948662bc34bb 100644 --- a/tests/regressiontests/mail/tests.py +++ b/tests/regressiontests/mail/tests.py @@ -17,7 +17,7 @@ from django.core.mail.message import BadHeaderError from django.test import TestCase from django.test.utils import override_settings -from django.utils.six import StringIO +from django.utils.six import PY3, StringIO from django.utils.translation import ugettext_lazy @@ -29,7 +29,7 @@ class MailTests(TestCase): def test_ascii(self): email = EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com']) message = email.message() - self.assertEqual(message['Subject'].encode(), 'Subject') + self.assertEqual(message['Subject'], 'Subject') self.assertEqual(message.get_payload(), 'Content') self.assertEqual(message['From'], 'from@example.com') self.assertEqual(message['To'], 'to@example.com') @@ -37,7 +37,7 @@ def test_ascii(self): def test_multiple_recipients(self): email = EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com', 'other@example.com']) message = email.message() - self.assertEqual(message['Subject'].encode(), 'Subject') + self.assertEqual(message['Subject'], 'Subject') self.assertEqual(message.get_payload(), 'Content') self.assertEqual(message['From'], 'from@example.com') self.assertEqual(message['To'], 'to@example.com, other@example.com') @@ -77,9 +77,10 @@ def test_space_continuation(self): """ Test for space continuation character in long (ascii) subject headers (#7747) """ - email = EmailMessage('Long subject lines that get wrapped should use a space continuation character to get expected behavior in Outlook and Thunderbird', 'Content', 'from@example.com', ['to@example.com']) + email = EmailMessage('Long subject lines that get wrapped should contain a space continuation character to get expected behavior in Outlook and Thunderbird', 'Content', 'from@example.com', ['to@example.com']) message = email.message() - self.assertEqual(message['Subject'], 'Long subject lines that get wrapped should use a space continuation\n character to get expected behavior in Outlook and Thunderbird') + # Note that in Python 3, maximum line length has increased from 76 to 78 + self.assertEqual(message['Subject'].encode(), b'Long subject lines that get wrapped should contain a space continuation\n character to get expected behavior in Outlook and Thunderbird') def test_message_header_overrides(self): """ @@ -88,7 +89,7 @@ def test_message_header_overrides(self): """ headers = {"date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"} email = EmailMessage('subject', 'content', 'from@example.com', ['to@example.com'], headers=headers) - self.assertEqual(email.message().as_string(), b'Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nSubject: subject\nFrom: from@example.com\nTo: to@example.com\ndate: Fri, 09 Nov 2001 01:08:47 -0000\nMessage-ID: foo\n\ncontent') + self.assertEqual(email.message().as_string(), 'Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nSubject: subject\nFrom: from@example.com\nTo: to@example.com\ndate: Fri, 09 Nov 2001 01:08:47 -0000\nMessage-ID: foo\n\ncontent') def test_from_header(self): """ @@ -160,7 +161,7 @@ def test_safe_mime_multipart(self): msg.attach_alternative(html_content, "text/html") msg.encoding = 'iso-8859-1' self.assertEqual(msg.message()['To'], '=?iso-8859-1?q?S=FCrname=2C_Firstname?= ') - self.assertEqual(msg.message()['Subject'].encode(), '=?iso-8859-1?q?Message_from_Firstname_S=FCrname?=') + self.assertEqual(msg.message()['Subject'], '=?iso-8859-1?q?Message_from_Firstname_S=FCrname?=') def test_encoding(self): """ @@ -170,7 +171,7 @@ def test_encoding(self): email = EmailMessage('Subject', 'Firstname Sürname is a great guy.', 'from@example.com', ['other@example.com']) email.encoding = 'iso-8859-1' message = email.message() - self.assertTrue(message.as_string().startswith(b'Content-Type: text/plain; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\nSubject: Subject\nFrom: from@example.com\nTo: other@example.com')) + self.assertTrue(message.as_string().startswith('Content-Type: text/plain; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\nSubject: Subject\nFrom: from@example.com\nTo: other@example.com')) self.assertEqual(message.get_payload(), 'Firstname S=FCrname is a great guy.') # Make sure MIME attachments also works correctly with other encodings than utf-8 @@ -179,8 +180,8 @@ def test_encoding(self): 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(), b'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(), b'Content-Type: text/html; charset="iso-8859-1"\nMIME-Version: 1.0\nContent-Transfer-Encoding: quoted-printable\n\n

Firstname S=FCrname is a great guy.

') + 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

Firstname S=FCrname is a great guy.

') def test_attachments(self): """Regression test for #9367""" @@ -291,31 +292,31 @@ def test_dont_mangle_from_in_body(self): # 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(b'>From the future' in email.message().as_string()) + self.assertFalse('>From the future' in email.message().as_string()) 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(b'Content-Transfer-Encoding: base64' in msg.message().as_string()) + self.assertFalse('Content-Transfer-Encoding: base64' in msg.message().as_string()) # 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(b'Content-Transfer-Encoding: quoted-printable' in s) - self.assertTrue(b'Content-Transfer-Encoding: 7bit' in s) + self.assertFalse('Content-Transfer-Encoding: quoted-printable' in s) + self.assertTrue('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(b'Content-Transfer-Encoding: quoted-printable' in s) - self.assertTrue(b'Content-Transfer-Encoding: 8bit' in s) + self.assertFalse(str('Content-Transfer-Encoding: quoted-printable') in s) + self.assertTrue(str('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(b'Content-Transfer-Encoding: quoted-printable' in s) - self.assertTrue(b'Content-Transfer-Encoding: 8bit' in s) + self.assertFalse(str('Content-Transfer-Encoding: quoted-printable') in s) + self.assertTrue(str('Content-Transfer-Encoding: 8bit') in s) class BaseEmailBackendTests(object): @@ -440,7 +441,7 @@ def test_message_cc_header(self): email = EmailMessage('Subject', 'Content', 'from@example.com', ['to@example.com'], cc=['cc@example.com']) mail.get_connection().send_messages([email]) message = self.get_the_message() - self.assertStartsWith(message.as_string(), b'Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nSubject: Subject\nFrom: from@example.com\nTo: to@example.com\nCc: cc@example.com\nDate: ') + self.assertStartsWith(message.as_string(), 'Content-Type: text/plain; charset="utf-8"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nSubject: Subject\nFrom: from@example.com\nTo: to@example.com\nCc: cc@example.com\nDate: ') def test_idn_send(self): """ @@ -519,9 +520,9 @@ def flush_mailbox(self): def get_mailbox_content(self): messages = [] for filename in os.listdir(self.tmp_dir): - with open(os.path.join(self.tmp_dir, filename), 'rb') as fp: - session = fp.read().split(b'\n' + (b'-' * 79) + b'\n') - messages.extend(email.message_from_string(m) for m in session if m) + with open(os.path.join(self.tmp_dir, filename), 'r') as fp: + session = fp.read().split('\n' + ('-' * 79) + '\n') + messages.extend(email.message_from_string(str(m)) for m in session if m) return messages def test_file_sessions(self): @@ -571,8 +572,8 @@ def flush_mailbox(self): self.stream = sys.stdout = StringIO() def get_mailbox_content(self): - messages = self.stream.getvalue().split(b'\n' + (b'-' * 79) + b'\n') - return [email.message_from_string(m) for m in messages if m] + messages = self.stream.getvalue().split('\n' + ('-' * 79) + '\n') + return [email.message_from_string(str(m)) for m in messages if m] def test_console_stream_kwarg(self): """ @@ -600,7 +601,10 @@ def __init__(self, *args, **kwargs): def process_message(self, peer, mailfrom, rcpttos, data): m = email.message_from_string(data) - maddr = email.Utils.parseaddr(m.get('from'))[1] + if PY3: + maddr = email.utils.parseaddr(m.get('from'))[1] + else: + maddr = email.Utils.parseaddr(m.get('from'))[1] if mailfrom != maddr: return "553 '%s' != '%s'" % (mailfrom, maddr) with self.sink_lock: