Fixed #18967 -- Don't base64-encode message/rfc822 attachments.
Thanks Michael Farrell for the report and his work on the fix.
This commit is contained in:
parent
839940f27f
commit
01223840f3
|
@ -4,11 +4,13 @@ import mimetypes
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import time
|
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.generator import Generator
|
||||||
|
from email.message import Message
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.base import MIMEBase
|
from email.mime.base import MIMEBase
|
||||||
|
from email.mime.message import MIMEMessage
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
from email.utils import formatdate, getaddresses, formataddr, parseaddr
|
from email.utils import formatdate, getaddresses, formataddr, parseaddr
|
||||||
|
|
||||||
|
@ -118,6 +120,27 @@ def sanitize_address(addr, encoding):
|
||||||
return formataddr((nm, addr))
|
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):
|
class SafeMIMEText(MIMEText):
|
||||||
|
|
||||||
def __init__(self, text, subtype, charset):
|
def __init__(self, text, subtype, charset):
|
||||||
|
@ -137,7 +160,7 @@ class SafeMIMEText(MIMEText):
|
||||||
lines that begin with 'From '. See bug #13433 for details.
|
lines that begin with 'From '. See bug #13433 for details.
|
||||||
"""
|
"""
|
||||||
fp = six.StringIO()
|
fp = six.StringIO()
|
||||||
g = Generator(fp, mangle_from_ = False)
|
g = Generator(fp, mangle_from_=False)
|
||||||
g.flatten(self, unixfrom=unixfrom)
|
g.flatten(self, unixfrom=unixfrom)
|
||||||
return fp.getvalue()
|
return fp.getvalue()
|
||||||
|
|
||||||
|
@ -161,7 +184,7 @@ class SafeMIMEMultipart(MIMEMultipart):
|
||||||
lines that begin with 'From '. See bug #13433 for details.
|
lines that begin with 'From '. See bug #13433 for details.
|
||||||
"""
|
"""
|
||||||
fp = six.StringIO()
|
fp = six.StringIO()
|
||||||
g = Generator(fp, mangle_from_ = False)
|
g = Generator(fp, mangle_from_=False)
|
||||||
g.flatten(self, unixfrom=unixfrom)
|
g.flatten(self, unixfrom=unixfrom)
|
||||||
return fp.getvalue()
|
return fp.getvalue()
|
||||||
|
|
||||||
|
@ -292,11 +315,26 @@ class EmailMessage(object):
|
||||||
def _create_mime_attachment(self, content, mimetype):
|
def _create_mime_attachment(self, content, mimetype):
|
||||||
"""
|
"""
|
||||||
Converts the content, mimetype pair into a MIME attachment object.
|
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)
|
basetype, subtype = mimetype.split('/', 1)
|
||||||
if basetype == 'text':
|
if basetype == 'text':
|
||||||
encoding = self.encoding or settings.DEFAULT_CHARSET
|
encoding = self.encoding or settings.DEFAULT_CHARSET
|
||||||
attachment = SafeMIMEText(content, subtype, encoding)
|
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:
|
else:
|
||||||
# Encode non-text attachments with base64.
|
# Encode non-text attachments with base64.
|
||||||
attachment = MIMEBase(basetype, subtype)
|
attachment = MIMEBase(basetype, subtype)
|
||||||
|
|
|
@ -319,6 +319,18 @@ The class has the following methods:
|
||||||
|
|
||||||
message.attach('design.png', img_data, 'image/png')
|
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
|
* ``attach_file()`` creates a new attachment using a file from your
|
||||||
filesystem. Call it with the path of the file to attach and, optionally,
|
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
|
the MIME type to use for the attachment. If the MIME type is omitted, it
|
||||||
|
@ -326,8 +338,6 @@ The class has the following methods:
|
||||||
|
|
||||||
message.attach_file('/images/weather_map.png')
|
message.attach_file('/images/weather_map.png')
|
||||||
|
|
||||||
.. _DEFAULT_FROM_EMAIL: ../settings/#default-from-email
|
|
||||||
|
|
||||||
Sending alternative content types
|
Sending alternative content types
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -331,6 +331,39 @@ class MailTests(TestCase):
|
||||||
self.assertFalse(str('Content-Transfer-Encoding: quoted-printable') in s)
|
self.assertFalse(str('Content-Transfer-Encoding: quoted-printable') in s)
|
||||||
self.assertTrue(str('Content-Transfer-Encoding: 8bit') 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):
|
class BaseEmailBackendTests(object):
|
||||||
email_backend = None
|
email_backend = None
|
||||||
|
|
Loading…
Reference in New Issue