Browse files

Fixed #1541 -- Added ability to create multipart email messages. Tha…

…nks, Nick


git-svn-id: bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
malcolmt committed Jun 27, 2007
1 parent 551a361 commit 2d082a34dc61a832710d98a933858fd2c0059644
Showing with 122 additions and 11 deletions.
  1. +70 −4 django/core/
  2. +52 −7 docs/email.txt
@@ -3,10 +3,13 @@
from django.conf import settings
+from email import Charset, Encoders
from email.MIMEText import MIMEText
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEBase import MIMEBase
from email.Header import Header
from email.Utils import formatdate
-from email import Charset
+import mimetypes
import os
import smtplib
import socket
@@ -17,6 +20,10 @@
# some spam filters.
Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
+# Default MIME type to use on attachments (if it is not explicitly given
+# and cannot be guessed).
+DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream'
# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of
# seconds, which slows down the restart of the server.
class CachedDnsName(object):
@@ -55,14 +62,22 @@ def make_msgid(idstring=None):
class BadHeaderError(ValueError):
-class SafeMIMEText(MIMEText):
+class SafeHeaderMixin(object):
def __setitem__(self, name, val):
"Forbids multi-line headers, to prevent header injection."
if '\n' in val or '\r' in val:
raise BadHeaderError, "Header values can't contain newlines (got %r for header %r)" % (val, name)
if name == "Subject":
val = Header(val, settings.DEFAULT_CHARSET)
- MIMEText.__setitem__(self, name, val)
+ # Note: using super() here is safe; any __setitem__ overrides must use
+ # the same argument signature.
+ super(SafeHeaderMixin, self).__setitem__(name, val)
+class SafeMIMEText(MIMEText, SafeHeaderMixin):
+ pass
+class SafeMIMEMultipart(MIMEMultipart, SafeHeaderMixin):
+ pass
class SMTPConnection(object):
@@ -154,12 +169,14 @@ class EmailMessage(object):
A container for email information.
- def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, connection=None):
+ def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
+ connection=None, attachments=None): = to or []
self.bcc = bcc or []
self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
self.subject = subject
self.body = body
+ self.attachments = attachments or []
self.connection = connection
def get_connection(self, fail_silently=False):
@@ -169,6 +186,16 @@ def get_connection(self, fail_silently=False):
def message(self):
msg = SafeMIMEText(self.body, 'plain', settings.DEFAULT_CHARSET)
+ if self.attachments:
+ body_msg = msg
+ msg = SafeMIMEMultipart()
+ if self.body:
+ msg.attach(body_msg)
+ for attachment in self.attachments:
+ if isinstance(attachment, MIMEBase):
+ msg.attach(attachment)
+ else:
+ msg.attach(self._create_attachment(*attachment))
msg['Subject'] = self.subject
msg['From'] = self.from_email
msg['To'] = ', '.join(
@@ -189,6 +216,45 @@ def send(self, fail_silently=False):
"""Send the email message."""
return self.get_connection(fail_silently).send_messages([self])
+ def attach(self, filename, content=None, mimetype=None):
+ """
+ Attaches a file with the given filename and content.
+ Alternatively, the first parameter can be a MIMEBase subclass, which
+ is inserted directly into the resulting message attachments.
+ """
+ if isinstance(filename, MIMEBase):
+ self.attachements.append(filename)
+ else:
+ assert content is not None
+ self.attachments.append((filename, content, mimetype))
+ def attach_file(self, path, mimetype=None):
+ """Attaches a file from the filesystem."""
+ filename = os.path.basename(path)
+ content = open(path, 'rb').read()
+ self.attach(filename, content, mimetype)
+ def _create_attachment(self, filename, content, mimetype=None):
+ """
+ Convert the filename, content, mimetype triple into a MIME attachment
+ object.
+ """
+ if mimetype is None:
+ mimetype, _ = mimetypes.guess_type(filename)
+ if mimetype is None:
+ basetype, subtype = mimetype.split('/', 1)
+ if basetype == 'text':
+ attachment = SafeMIMEText(content, subtype, settings.DEFAULT_CHARSET)
+ else:
+ # Encode non-text attachments with base64.
+ attachment = MIMEBase(basetype, subtype)
+ attachment.set_payload(content)
+ Encoders.encode_base64(attachment)
+ attachment.add_header('Content-Disposition', 'attachment', filename=filename)
+ return attachment
def send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None):
Easy wrapper for sending a single message to a recipient list. All members
@@ -28,9 +28,9 @@ settings, if set, are used to authenticate to the SMTP server, and the
.. note::
The character set of e-mail sent with ``django.core.mail`` will be set to
- the value of your `DEFAULT_CHARSET setting`_.
+ the value of your `DEFAULT_CHARSET`_ setting.
-.. _DEFAULT_CHARSET setting: ../settings/#default-charset
+.. _DEFAULT_CHARSET: ../settings/#default-charset
.. _EMAIL_HOST: ../settings/#email-host
.. _EMAIL_PORT: ../settings/#email-port
.. _EMAIL_HOST_USER: ../settings/#email-host-user
@@ -198,21 +198,36 @@ e-mail, you can subclass these two classes to suit your needs.
.. note::
Not all features of the ``EmailMessage`` class are available through the
``send_mail()`` and related wrapper functions. If you wish to use advanced
- features, such as BCC'ed recipients or multi-part e-mail, you'll need to
- create ``EmailMessage`` instances directly.
+ features, such as BCC'ed recipients, file attachments, or multi-part
+ e-mail, you'll need to create ``EmailMessage`` instances directly.
+ This is a design feature. ``send_mail()`` and related functions were
+ originally the only interface Django provided. However, the list of
+ parameters they accepted was slowly growing over time. It made sense to
+ move to a more object-oriented design for e-mail messages and retain the
+ original functions only for backwards compatibility.
+ If you need to add new functionality to the e-mail infrastrcture,
+ sub-classing the ``EmailMessage`` class should make this a simple task.
In general, ``EmailMessage`` is responsible for creating the e-mail message
itself. ``SMTPConnection`` is responsible for the network connection side of
the operation. This means you can reuse the same connection (an
``SMTPConnection`` instance) for multiple messages.
+E-mail messages
The ``EmailMessage`` class is initialized as follows::
- email = EmailMessage(subject, body, from_email, to, bcc, connection)
+ email = EmailMessage(subject, body, from_email, to,
+ bcc, connection, attachments)
All of these parameters are optional. If ``from_email`` is omitted, the value
from ``settings.DEFAULT_FROM_EMAIL`` is used. Both the ``to`` and ``bcc``
-parameters are lists of addresses, as strings.
+parameters are lists of addresses, as strings. The ``attachments`` parameter is
+a list containing either ``(filename, content, mimetype)`` triples of
+``email.MIMEBase.MIMEBase`` instances.
For example::
@@ -227,7 +242,8 @@ The class has the following methods:
if none already exists.
* ``message()`` constructs a ``django.core.mail.SafeMIMEText`` object (a
- sub-class of Python's ``email.MIMEText.MIMEText`` class) holding the
+ sub-class of Python's ``email.MIMEText.MIMEText`` class) or a
+ ``django.core.mail.SafeMIMEMultipart`` object holding the
message to be sent. If you ever need to extend the `EmailMessage` class,
you'll probably want to override this method to put the content you wish
into the MIME object.
@@ -239,6 +255,35 @@ The class has the following methods:
is sent. If you add another way to specify recipients in your class, they
need to be returned from this method as well.
+ * ``attach()`` creates a new file attachment and adds it to the message.
+ There are two ways to call ``attach()``:
+ * You can pass it a single argument which is an
+ ``email.MIMBase.MIMEBase`` instance. This will be inserted directly
+ into the resulting message.
+ * Alternatively, you can pass ``attach()`` three arguments:
+ ``filename``, ``content`` and ``mimetype``. ``filename`` is the name
+ of the file attachment as it will appear in the email, ``content`` is
+ the data that will be contained inside the attachment and
+ ``mimetype`` is the optional MIME type for the attachment. If you
+ omit ``mimetype``, the MIME content type will be guessed from the
+ filename of the attachment.
+ For example::
+ message.attach('design.png', img_data, 'image/png')
+ * ``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')
+SMTP network connections
The ``SMTPConnection`` class is initialized with the host, port, username and
password for the SMTP server. If you don't specify one or more of those
options, they are read from your settings file.

0 comments on commit 2d082a3

Please sign in to comment.