Loading django/core/mail/message.py +41 −3 Original line number Diff line number Diff line Loading @@ -4,11 +4,13 @@ import mimetypes import os import random import time from email import charset as Charset, encoders as Encoders from email import charset as Charset, encoders as Encoders, message_from_string from email.generator import Generator from email.message import Message from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email.mime.message import MIMEMessage from email.header import Header from email.utils import formatdate, getaddresses, formataddr, parseaddr Loading Loading @@ -118,6 +120,27 @@ def sanitize_address(addr, encoding): return formataddr((nm, addr)) class SafeMIMEMessage(MIMEMessage): def __setitem__(self, name, val): # message/rfc822 attachments must be ASCII name, val = forbid_multi_line_headers(name, val, 'ascii') MIMEMessage.__setitem__(self, name, val) def as_string(self, unixfrom=False): """Return the entire formatted message as a string. Optional `unixfrom' when True, means include the Unix From_ envelope header. This overrides the default as_string() implementation to not mangle lines that begin with 'From '. See bug #13433 for details. """ fp = six.StringIO() g = Generator(fp, mangle_from_=False) g.flatten(self, unixfrom=unixfrom) return fp.getvalue() class SafeMIMEText(MIMEText): def __init__(self, text, subtype, charset): Loading Loading @@ -292,11 +315,26 @@ class EmailMessage(object): def _create_mime_attachment(self, content, mimetype): """ Converts the content, mimetype pair into a MIME attachment object. If the mimetype is message/rfc822, content may be an email.Message or EmailMessage object, as well as a str. """ basetype, subtype = mimetype.split('/', 1) if basetype == 'text': encoding = self.encoding or settings.DEFAULT_CHARSET attachment = SafeMIMEText(content, subtype, encoding) elif basetype == 'message' and subtype == 'rfc822': # Bug #18967: per RFC2046 s5.2.1, message/rfc822 attachments # must not be base64 encoded. if isinstance(content, EmailMessage): # convert content into an email.Message first content = content.message() elif not isinstance(content, Message): # For compatibility with existing code, parse the message # into a email.Message object if it is not one already. content = message_from_string(content) attachment = SafeMIMEMessage(content, subtype) else: # Encode non-text attachments with base64. attachment = MIMEBase(basetype, subtype) Loading docs/topics/email.txt +12 −2 Original line number Diff line number Diff line Loading @@ -319,6 +319,18 @@ The class has the following methods: message.attach('design.png', img_data, 'image/png') .. versionchanged:: 1.7 If you specify a ``mimetype`` of ``message/rfc822``, it will also accept :class:`django.core.mail.EmailMessage` and :py:class:`email.message.Message`. In addition, ``message/rfc822`` attachments will no longer be base64-encoded in violation of :rfc:`2046#section-5.2.1`, which can cause issues with displaying the attachments in `Evolution`__ and `Thunderbird`__. __ https://bugzilla.gnome.org/show_bug.cgi?id=651197 __ https://bugzilla.mozilla.org/show_bug.cgi?id=333880 * ``attach_file()`` creates a new attachment using a file from your filesystem. Call it with the path of the file to attach and, optionally, the MIME type to use for the attachment. If the MIME type is omitted, it Loading @@ -326,8 +338,6 @@ The class has the following methods: message.attach_file('/images/weather_map.png') .. _DEFAULT_FROM_EMAIL: ../settings/#default-from-email Sending alternative content types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Loading tests/mail/tests.py +33 −0 Original line number Diff line number Diff line Loading @@ -331,6 +331,39 @@ class MailTests(TestCase): self.assertFalse(str('Content-Transfer-Encoding: quoted-printable') in s) self.assertTrue(str('Content-Transfer-Encoding: 8bit') in s) def test_dont_base64_encode_message_rfc822(self): # Ticket #18967 # Shouldn't use base64 encoding for a child EmailMessage attachment. # Create a child message first child_msg = EmailMessage('Child Subject', 'Some body of child message', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) child_s = child_msg.message().as_string() # Now create a parent parent_msg = EmailMessage('Parent Subject', 'Some parent body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) # Attach to parent as a string parent_msg.attach(content=child_s, mimetype='message/rfc822') parent_s = parent_msg.message().as_string() # Verify that the child message header is not base64 encoded self.assertTrue(str('Child Subject') in parent_s) # Feature test: try attaching email.Message object directly to the mail. parent_msg = EmailMessage('Parent Subject', 'Some parent body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) parent_msg.attach(content=child_msg.message(), mimetype='message/rfc822') parent_s = parent_msg.message().as_string() # Verify that the child message header is not base64 encoded self.assertTrue(str('Child Subject') in parent_s) # Feature test: try attaching Django's EmailMessage object directly to the mail. parent_msg = EmailMessage('Parent Subject', 'Some parent body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) parent_msg.attach(content=child_msg, mimetype='message/rfc822') parent_s = parent_msg.message().as_string() # Verify that the child message header is not base64 encoded self.assertTrue(str('Child Subject') in parent_s) class BaseEmailBackendTests(object): email_backend = None Loading Loading
django/core/mail/message.py +41 −3 Original line number Diff line number Diff line Loading @@ -4,11 +4,13 @@ import mimetypes import os import random import time from email import charset as Charset, encoders as Encoders from email import charset as Charset, encoders as Encoders, message_from_string from email.generator import Generator from email.message import Message from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email.mime.message import MIMEMessage from email.header import Header from email.utils import formatdate, getaddresses, formataddr, parseaddr Loading Loading @@ -118,6 +120,27 @@ def sanitize_address(addr, encoding): return formataddr((nm, addr)) class SafeMIMEMessage(MIMEMessage): def __setitem__(self, name, val): # message/rfc822 attachments must be ASCII name, val = forbid_multi_line_headers(name, val, 'ascii') MIMEMessage.__setitem__(self, name, val) def as_string(self, unixfrom=False): """Return the entire formatted message as a string. Optional `unixfrom' when True, means include the Unix From_ envelope header. This overrides the default as_string() implementation to not mangle lines that begin with 'From '. See bug #13433 for details. """ fp = six.StringIO() g = Generator(fp, mangle_from_=False) g.flatten(self, unixfrom=unixfrom) return fp.getvalue() class SafeMIMEText(MIMEText): def __init__(self, text, subtype, charset): Loading Loading @@ -292,11 +315,26 @@ class EmailMessage(object): def _create_mime_attachment(self, content, mimetype): """ Converts the content, mimetype pair into a MIME attachment object. If the mimetype is message/rfc822, content may be an email.Message or EmailMessage object, as well as a str. """ basetype, subtype = mimetype.split('/', 1) if basetype == 'text': encoding = self.encoding or settings.DEFAULT_CHARSET attachment = SafeMIMEText(content, subtype, encoding) elif basetype == 'message' and subtype == 'rfc822': # Bug #18967: per RFC2046 s5.2.1, message/rfc822 attachments # must not be base64 encoded. if isinstance(content, EmailMessage): # convert content into an email.Message first content = content.message() elif not isinstance(content, Message): # For compatibility with existing code, parse the message # into a email.Message object if it is not one already. content = message_from_string(content) attachment = SafeMIMEMessage(content, subtype) else: # Encode non-text attachments with base64. attachment = MIMEBase(basetype, subtype) Loading
docs/topics/email.txt +12 −2 Original line number Diff line number Diff line Loading @@ -319,6 +319,18 @@ The class has the following methods: message.attach('design.png', img_data, 'image/png') .. versionchanged:: 1.7 If you specify a ``mimetype`` of ``message/rfc822``, it will also accept :class:`django.core.mail.EmailMessage` and :py:class:`email.message.Message`. In addition, ``message/rfc822`` attachments will no longer be base64-encoded in violation of :rfc:`2046#section-5.2.1`, which can cause issues with displaying the attachments in `Evolution`__ and `Thunderbird`__. __ https://bugzilla.gnome.org/show_bug.cgi?id=651197 __ https://bugzilla.mozilla.org/show_bug.cgi?id=333880 * ``attach_file()`` creates a new attachment using a file from your filesystem. Call it with the path of the file to attach and, optionally, the MIME type to use for the attachment. If the MIME type is omitted, it Loading @@ -326,8 +338,6 @@ The class has the following methods: message.attach_file('/images/weather_map.png') .. _DEFAULT_FROM_EMAIL: ../settings/#default-from-email Sending alternative content types ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Loading
tests/mail/tests.py +33 −0 Original line number Diff line number Diff line Loading @@ -331,6 +331,39 @@ class MailTests(TestCase): self.assertFalse(str('Content-Transfer-Encoding: quoted-printable') in s) self.assertTrue(str('Content-Transfer-Encoding: 8bit') in s) def test_dont_base64_encode_message_rfc822(self): # Ticket #18967 # Shouldn't use base64 encoding for a child EmailMessage attachment. # Create a child message first child_msg = EmailMessage('Child Subject', 'Some body of child message', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) child_s = child_msg.message().as_string() # Now create a parent parent_msg = EmailMessage('Parent Subject', 'Some parent body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) # Attach to parent as a string parent_msg.attach(content=child_s, mimetype='message/rfc822') parent_s = parent_msg.message().as_string() # Verify that the child message header is not base64 encoded self.assertTrue(str('Child Subject') in parent_s) # Feature test: try attaching email.Message object directly to the mail. parent_msg = EmailMessage('Parent Subject', 'Some parent body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) parent_msg.attach(content=child_msg.message(), mimetype='message/rfc822') parent_s = parent_msg.message().as_string() # Verify that the child message header is not base64 encoded self.assertTrue(str('Child Subject') in parent_s) # Feature test: try attaching Django's EmailMessage object directly to the mail. parent_msg = EmailMessage('Parent Subject', 'Some parent body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'}) parent_msg.attach(content=child_msg, mimetype='message/rfc822') parent_s = parent_msg.message().as_string() # Verify that the child message header is not base64 encoded self.assertTrue(str('Child Subject') in parent_s) class BaseEmailBackendTests(object): email_backend = None Loading