Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #18967 -- Don't base64-encode message/rfc822 attachments.

Thanks Michael Farrell for the report and his work on the fix.
  • Loading branch information...
commit f9d1d5dc1377cb21b39452b0897e7a79a3d02844 1 parent 96346ed
Ramiro Morales authored August 20, 2013
44  django/core/mail/message.py
@@ -4,11 +4,13 @@
4 4
 import os
5 5
 import random
6 6
 import time
7  
-from email import charset as Charset, encoders as Encoders
  7
+from email import charset as Charset, encoders as Encoders, message_from_string
8 8
 from email.generator import Generator
  9
+from email.message import Message
9 10
 from email.mime.text import MIMEText
10 11
 from email.mime.multipart import MIMEMultipart
11 12
 from email.mime.base import MIMEBase
  13
+from email.mime.message import MIMEMessage
12 14
 from email.header import Header
13 15
 from email.utils import formatdate, getaddresses, formataddr, parseaddr
14 16
 
@@ -118,6 +120,27 @@ def sanitize_address(addr, encoding):
118 120
     return formataddr((nm, addr))
119 121
 
120 122
 
  123
+class SafeMIMEMessage(MIMEMessage):
  124
+
  125
+    def __setitem__(self, name, val):
  126
+        # message/rfc822 attachments must be ASCII
  127
+        name, val = forbid_multi_line_headers(name, val, 'ascii')
  128
+        MIMEMessage.__setitem__(self, name, val)
  129
+
  130
+    def as_string(self, unixfrom=False):
  131
+        """Return the entire formatted message as a string.
  132
+        Optional `unixfrom' when True, means include the Unix From_ envelope
  133
+        header.
  134
+
  135
+        This overrides the default as_string() implementation to not mangle
  136
+        lines that begin with 'From '. See bug #13433 for details.
  137
+        """
  138
+        fp = six.StringIO()
  139
+        g = Generator(fp, mangle_from_=False)
  140
+        g.flatten(self, unixfrom=unixfrom)
  141
+        return fp.getvalue()
  142
+
  143
+
121 144
 class SafeMIMEText(MIMEText):
122 145
 
123 146
     def __init__(self, text, subtype, charset):
@@ -137,7 +160,7 @@ def as_string(self, unixfrom=False):
137 160
         lines that begin with 'From '. See bug #13433 for details.
138 161
         """
139 162
         fp = six.StringIO()
140  
-        g = Generator(fp, mangle_from_ = False)
  163
+        g = Generator(fp, mangle_from_=False)
141 164
         g.flatten(self, unixfrom=unixfrom)
142 165
         return fp.getvalue()
143 166
 
@@ -161,7 +184,7 @@ def as_string(self, unixfrom=False):
161 184
         lines that begin with 'From '. See bug #13433 for details.
162 185
         """
163 186
         fp = six.StringIO()
164  
-        g = Generator(fp, mangle_from_ = False)
  187
+        g = Generator(fp, mangle_from_=False)
165 188
         g.flatten(self, unixfrom=unixfrom)
166 189
         return fp.getvalue()
167 190
 
@@ -292,11 +315,26 @@ def _create_attachments(self, msg):
292 315
     def _create_mime_attachment(self, content, mimetype):
293 316
         """
294 317
         Converts the content, mimetype pair into a MIME attachment object.
  318
+
  319
+        If the mimetype is message/rfc822, content may be an
  320
+        email.Message or EmailMessage object, as well as a str.
295 321
         """
296 322
         basetype, subtype = mimetype.split('/', 1)
297 323
         if basetype == 'text':
298 324
             encoding = self.encoding or settings.DEFAULT_CHARSET
299 325
             attachment = SafeMIMEText(content, subtype, encoding)
  326
+        elif basetype == 'message' and subtype == 'rfc822':
  327
+            # Bug #18967: per RFC2046 s5.2.1, message/rfc822 attachments
  328
+            # must not be base64 encoded.
  329
+            if isinstance(content, EmailMessage):
  330
+                # convert content into an email.Message first
  331
+                content = content.message()
  332
+            elif not isinstance(content, Message):
  333
+                # For compatibility with existing code, parse the message
  334
+                # into a email.Message object if it is not one already.
  335
+                content = message_from_string(content)
  336
+
  337
+            attachment = SafeMIMEMessage(content, subtype)
300 338
         else:
301 339
             # Encode non-text attachments with base64.
302 340
             attachment = MIMEBase(basetype, subtype)
14  docs/topics/email.txt
@@ -319,6 +319,18 @@ The class has the following methods:
319 319
 
320 320
        message.attach('design.png', img_data, 'image/png')
321 321
 
  322
+    .. versionchanged:: 1.7
  323
+
  324
+      If you specify a ``mimetype`` of ``message/rfc822``, it will also accept
  325
+      :class:`django.core.mail.EmailMessage` and :py:class:`email.message.Message`.
  326
+
  327
+      In addition, ``message/rfc822`` attachments will no longer be
  328
+      base64-encoded in violation of :rfc:`2046#section-5.2.1`, which can cause
  329
+      issues with displaying the attachments in `Evolution`__ and `Thunderbird`__.
  330
+
  331
+      __ https://bugzilla.gnome.org/show_bug.cgi?id=651197
  332
+      __ https://bugzilla.mozilla.org/show_bug.cgi?id=333880
  333
+
322 334
 * ``attach_file()`` creates a new attachment using a file from your
323 335
   filesystem. Call it with the path of the file to attach and, optionally,
324 336
   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:
326 338
 
327 339
     message.attach_file('/images/weather_map.png')
328 340
 
329  
-.. _DEFAULT_FROM_EMAIL: ../settings/#default-from-email
330  
-
331 341
 Sending alternative content types
332 342
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
333 343
 
33  tests/mail/tests.py
@@ -331,6 +331,39 @@ def test_dont_base64_encode(self):
331 331
         self.assertFalse(str('Content-Transfer-Encoding: quoted-printable') in s)
332 332
         self.assertTrue(str('Content-Transfer-Encoding: 8bit') in s)
333 333
 
  334
+    def test_dont_base64_encode_message_rfc822(self):
  335
+        # Ticket #18967
  336
+        # Shouldn't use base64 encoding for a child EmailMessage attachment.
  337
+        # Create a child message first
  338
+        child_msg = EmailMessage('Child Subject', 'Some body of child message', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
  339
+        child_s = child_msg.message().as_string()
  340
+
  341
+        # Now create a parent
  342
+        parent_msg = EmailMessage('Parent Subject', 'Some parent body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
  343
+
  344
+        # Attach to parent as a string
  345
+        parent_msg.attach(content=child_s, mimetype='message/rfc822')
  346
+        parent_s = parent_msg.message().as_string()
  347
+
  348
+        # Verify that the child message header is not base64 encoded
  349
+        self.assertTrue(str('Child Subject') in parent_s)
  350
+
  351
+        # Feature test: try attaching email.Message object directly to the mail.
  352
+        parent_msg = EmailMessage('Parent Subject', 'Some parent body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
  353
+        parent_msg.attach(content=child_msg.message(), mimetype='message/rfc822')
  354
+        parent_s = parent_msg.message().as_string()
  355
+
  356
+        # Verify that the child message header is not base64 encoded
  357
+        self.assertTrue(str('Child Subject') in parent_s)
  358
+
  359
+        # Feature test: try attaching Django's EmailMessage object directly to the mail.
  360
+        parent_msg = EmailMessage('Parent Subject', 'Some parent body', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
  361
+        parent_msg.attach(content=child_msg, mimetype='message/rfc822')
  362
+        parent_s = parent_msg.message().as_string()
  363
+
  364
+        # Verify that the child message header is not base64 encoded
  365
+        self.assertTrue(str('Child Subject') in parent_s)
  366
+
334 367
 
335 368
 class BaseEmailBackendTests(object):
336 369
     email_backend = None

0 notes on commit f9d1d5d

Please sign in to comment.
Something went wrong with that request. Please try again.