Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #9367 - EmailMultiAlternatives does not properly handle attachm…

…ents.

Thanks to Loek Engels for the bulk of the patch.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@10983 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 9294121d3c4b169406f51d99a915060b175a9bfe 1 parent a5e7d97
Luke Plant authored June 12, 2009
87  django/core/mail.py
@@ -195,7 +195,7 @@ class EmailMessage(object):
195 195
     A container for email information.
196 196
     """
197 197
     content_subtype = 'plain'
198  
-    multipart_subtype = 'mixed'
  198
+    mixed_subtype = 'mixed'
199 199
     encoding = None     # None => use settings default
200 200
 
201 201
     def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
@@ -234,16 +234,7 @@ def message(self):
234 234
         encoding = self.encoding or settings.DEFAULT_CHARSET
235 235
         msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),
236 236
                            self.content_subtype, encoding)
237  
-        if self.attachments:
238  
-            body_msg = msg
239  
-            msg = SafeMIMEMultipart(_subtype=self.multipart_subtype)
240  
-            if self.body:
241  
-                msg.attach(body_msg)
242  
-            for attachment in self.attachments:
243  
-                if isinstance(attachment, MIMEBase):
244  
-                    msg.attach(attachment)
245  
-                else:
246  
-                    msg.attach(self._create_attachment(*attachment))
  237
+        msg = self._create_message(msg)
247 238
         msg['Subject'] = self.subject
248 239
         msg['From'] = self.extra_headers.pop('From', self.from_email)
249 240
         msg['To'] = ', '.join(self.to)
@@ -277,8 +268,7 @@ def send(self, fail_silently=False):
277 268
     def attach(self, filename=None, content=None, mimetype=None):
278 269
         """
279 270
         Attaches a file with the given filename and content. The filename can
280  
-        be omitted (useful for multipart/alternative messages) and the mimetype
281  
-        is guessed, if not provided.
  271
+        be omitted and the mimetype is guessed, if not provided.
282 272
 
283 273
         If the first parameter is a MIMEBase subclass it is inserted directly
284 274
         into the resulting message attachments.
@@ -296,15 +286,26 @@ def attach_file(self, path, mimetype=None):
296 286
         content = open(path, 'rb').read()
297 287
         self.attach(filename, content, mimetype)
298 288
 
299  
-    def _create_attachment(self, filename, content, mimetype=None):
  289
+    def _create_message(self, msg):
  290
+        return self._create_attachments(msg)
  291
+
  292
+    def _create_attachments(self, msg):
  293
+        if self.attachments:
  294
+            body_msg = msg
  295
+            msg = SafeMIMEMultipart(_subtype=self.mixed_subtype)
  296
+            if self.body:
  297
+                msg.attach(body_msg)
  298
+            for attachment in self.attachments:
  299
+                if isinstance(attachment, MIMEBase):
  300
+                    msg.attach(attachment)
  301
+                else:
  302
+                    msg.attach(self._create_attachment(*attachment))
  303
+        return msg
  304
+
  305
+    def _create_mime_attachment(self, content, mimetype):
300 306
         """
301  
-        Converts the filename, content, mimetype triple into a MIME attachment
302  
-        object.
  307
+        Converts the content, mimetype pair into a MIME attachment object.
303 308
         """
304  
-        if mimetype is None:
305  
-            mimetype, _ = mimetypes.guess_type(filename)
306  
-            if mimetype is None:
307  
-                mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
308 309
         basetype, subtype = mimetype.split('/', 1)
309 310
         if basetype == 'text':
310 311
             attachment = SafeMIMEText(smart_str(content,
@@ -314,6 +315,18 @@ def _create_attachment(self, filename, content, mimetype=None):
314 315
             attachment = MIMEBase(basetype, subtype)
315 316
             attachment.set_payload(content)
316 317
             Encoders.encode_base64(attachment)
  318
+        return attachment
  319
+
  320
+    def _create_attachment(self, filename, content, mimetype=None):
  321
+        """
  322
+        Converts the filename, content, mimetype triple into a MIME attachment
  323
+        object.
  324
+        """
  325
+        if mimetype is None:
  326
+            mimetype, _ = mimetypes.guess_type(filename)
  327
+            if mimetype is None:
  328
+                mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
  329
+        attachment = self._create_mime_attachment(content, mimetype)
317 330
         if filename:
318 331
             attachment.add_header('Content-Disposition', 'attachment',
319 332
                                   filename=filename)
@@ -325,11 +338,39 @@ class EmailMultiAlternatives(EmailMessage):
325 338
     messages. For example, including text and HTML versions of the text is
326 339
     made easier.
327 340
     """
328  
-    multipart_subtype = 'alternative'
  341
+    alternative_subtype = 'alternative'
329 342
 
330  
-    def attach_alternative(self, content, mimetype=None):
  343
+    def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
  344
+            connection=None, attachments=None, headers=None, alternatives=None):
  345
+        """
  346
+        Initialize a single email message (which can be sent to multiple
  347
+        recipients).
  348
+
  349
+        All strings used to create the message can be unicode strings (or UTF-8
  350
+        bytestrings). The SafeMIMEText class will handle any necessary encoding
  351
+        conversions.
  352
+        """
  353
+        super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers)
  354
+        self.alternatives=alternatives or []
  355
+
  356
+    def attach_alternative(self, content, mimetype):
331 357
         """Attach an alternative content representation."""
332  
-        self.attach(content=content, mimetype=mimetype)
  358
+        assert content is not None
  359
+        assert mimetype is not None
  360
+        self.alternatives.append((content, mimetype))
  361
+
  362
+    def _create_message(self, msg):
  363
+        return self._create_attachments(self._create_alternatives(msg))
  364
+
  365
+    def _create_alternatives(self, msg):
  366
+        if self.alternatives:
  367
+            body_msg = msg
  368
+            msg = SafeMIMEMultipart(_subtype=self.alternative_subtype)
  369
+            if self.body:
  370
+                msg.attach(body_msg)
  371
+            for alternative in self.alternatives:
  372
+                msg.attach(self._create_mime_attachment(*alternative))
  373
+        return msg
333 374
 
334 375
 def send_mail(subject, message, from_email, recipient_list,
335 376
               fail_silently=False, auth_user=None, auth_password=None):
46  tests/regressiontests/mail/tests.py
@@ -4,7 +4,7 @@
4 4
 
5 5
 >>> from django.conf import settings
6 6
 >>> from django.core import mail
7  
->>> from django.core.mail import EmailMessage, mail_admins, mail_managers
  7
+>>> from django.core.mail import EmailMessage, mail_admins, mail_managers, EmailMultiAlternatives
8 8
 >>> from django.utils.translation import ugettext_lazy
9 9
 
10 10
 # Test normal ascii character case:
@@ -95,4 +95,48 @@
95 95
 >>> message['From']
96 96
 'from@example.com'
97 97
 
  98
+# Handle attachments within an multipart/alternative mail correctly (#9367)
  99
+# (test is not as precise/clear as it could be w.r.t. email tree structure,
  100
+#  but it's good enough.)
  101
+
  102
+>>> headers = {"Date": "Fri, 09 Nov 2001 01:08:47 -0000", "Message-ID": "foo"}
  103
+>>> subject, from_email, to = 'hello', 'from@example.com', 'to@example.com'
  104
+>>> text_content = 'This is an important message.'
  105
+>>> html_content = '<p>This is an <strong>important</strong> message.</p>'
  106
+>>> msg = EmailMultiAlternatives(subject, text_content, from_email, [to], headers=headers)
  107
+>>> msg.attach_alternative(html_content, "text/html")
  108
+>>> msg.attach("an attachment.pdf", "%PDF-1.4.%...", mimetype="application/pdf")
  109
+>>> print msg.message().as_string()
  110
+Content-Type: multipart/mixed; boundary="..."
  111
+MIME-Version: 1.0
  112
+Subject: hello
  113
+From: from@example.com
  114
+To: to@example.com
  115
+Date: Fri, 09 Nov 2001 01:08:47 -0000
  116
+Message-ID: foo
  117
+...
  118
+Content-Type: multipart/alternative; boundary="..."
  119
+MIME-Version: 1.0
  120
+...
  121
+Content-Type: text/plain; charset="utf-8"
  122
+MIME-Version: 1.0
  123
+Content-Transfer-Encoding: quoted-printable
  124
+...
  125
+This is an important message.
  126
+...
  127
+Content-Type: text/html; charset="utf-8"
  128
+MIME-Version: 1.0
  129
+Content-Transfer-Encoding: quoted-printable
  130
+...
  131
+<p>This is an <strong>important</strong> message.</p>
  132
+...
  133
+...
  134
+Content-Type: application/pdf
  135
+MIME-Version: 1.0
  136
+Content-Transfer-Encoding: base64
  137
+Content-Disposition: attachment; filename="an attachment.pdf"
  138
+...
  139
+JVBERi0xLjQuJS4uLg==
  140
+...
  141
+
98 142
 """

0 notes on commit 9294121

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