Skip to content

Commit

Permalink
Fixed #18967 -- Don't base64-encode message/rfc822 attachments.
Browse files Browse the repository at this point in the history
Thanks Michael Farrell for the report and his work on the fix.
  • Loading branch information
ramiro authored and andrewgodwin committed Aug 21, 2013
1 parent 839940f commit 0122384
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 5 deletions.
44 changes: 41 additions & 3 deletions django/core/mail/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
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

Expand Down Expand Up @@ -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):
Expand All @@ -137,7 +160,7 @@ def as_string(self, unixfrom=False):
lines that begin with 'From '. See bug #13433 for details.
"""
fp = six.StringIO()
g = Generator(fp, mangle_from_ = False)
g = Generator(fp, mangle_from_=False)
g.flatten(self, unixfrom=unixfrom)
return fp.getvalue()

Expand All @@ -161,7 +184,7 @@ def as_string(self, unixfrom=False):
lines that begin with 'From '. See bug #13433 for details.
"""
fp = six.StringIO()
g = Generator(fp, mangle_from_ = False)
g = Generator(fp, mangle_from_=False)
g.flatten(self, unixfrom=unixfrom)
return fp.getvalue()

Expand Down Expand Up @@ -292,11 +315,26 @@ def _create_attachments(self, msg):
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)
Expand Down
14 changes: 12 additions & 2 deletions docs/topics/email.txt
Original file line number Diff line number Diff line change
Expand Up @@ -319,15 +319,25 @@ 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
will be guessed from the filename. The simplest use would be::

message.attach_file('/images/weather_map.png')

.. _DEFAULT_FROM_EMAIL: ../settings/#default-from-email

Sending alternative content types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
33 changes: 33 additions & 0 deletions tests/mail/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,39 @@ def test_dont_base64_encode(self):
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
Expand Down

0 comments on commit 0122384

Please sign in to comment.