mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	[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:
		| @@ -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 | ||||
|   | ||||
| @@ -130,21 +130,25 @@ 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.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) | ||||
|             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() | ||||
|  | ||||
|  | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 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. | ||||
|     """ | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user