Fixed #1541 -- Added ability to create multipart email messages. Thanks, Nick

Lane.


git-svn-id: http://code.djangoproject.com/svn/django/trunk@5547 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2007-06-27 09:44:55 +00:00
parent 551a36131e
commit 2d082a34dc
2 changed files with 122 additions and 11 deletions

View File

@ -3,10 +3,13 @@ Tools for sending email.
""" """
from django.conf import settings from django.conf import settings
from email import Charset, Encoders
from email.MIMEText import MIMEText from email.MIMEText import MIMEText
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.Header import Header from email.Header import Header
from email.Utils import formatdate from email.Utils import formatdate
from email import Charset import mimetypes
import os import os
import smtplib import smtplib
import socket import socket
@ -17,6 +20,10 @@ import random
# some spam filters. # some spam filters.
Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8') 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 # Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of
# seconds, which slows down the restart of the server. # seconds, which slows down the restart of the server.
class CachedDnsName(object): class CachedDnsName(object):
@ -55,14 +62,22 @@ def make_msgid(idstring=None):
class BadHeaderError(ValueError): class BadHeaderError(ValueError):
pass pass
class SafeMIMEText(MIMEText): class SafeHeaderMixin(object):
def __setitem__(self, name, val): def __setitem__(self, name, val):
"Forbids multi-line headers, to prevent header injection." "Forbids multi-line headers, to prevent header injection."
if '\n' in val or '\r' in val: if '\n' in val or '\r' in val:
raise BadHeaderError, "Header values can't contain newlines (got %r for header %r)" % (val, name) raise BadHeaderError, "Header values can't contain newlines (got %r for header %r)" % (val, name)
if name == "Subject": if name == "Subject":
val = Header(val, settings.DEFAULT_CHARSET) 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): class SMTPConnection(object):
""" """
@ -154,12 +169,14 @@ class EmailMessage(object):
""" """
A container for email information. 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):
self.to = to or [] self.to = to or []
self.bcc = bcc or [] self.bcc = bcc or []
self.from_email = from_email or settings.DEFAULT_FROM_EMAIL self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
self.subject = subject self.subject = subject
self.body = body self.body = body
self.attachments = attachments or []
self.connection = connection self.connection = connection
def get_connection(self, fail_silently=False): def get_connection(self, fail_silently=False):
@ -169,6 +186,16 @@ class EmailMessage(object):
def message(self): def message(self):
msg = SafeMIMEText(self.body, 'plain', settings.DEFAULT_CHARSET) 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['Subject'] = self.subject
msg['From'] = self.from_email msg['From'] = self.from_email
msg['To'] = ', '.join(self.to) msg['To'] = ', '.join(self.to)
@ -189,6 +216,45 @@ class EmailMessage(object):
"""Send the email message.""" """Send the email message."""
return self.get_connection(fail_silently).send_messages([self]) 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:
mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
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): 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 Easy wrapper for sending a single message to a recipient list. All members

View File

@ -28,9 +28,9 @@ settings, if set, are used to authenticate to the SMTP server, and the
.. note:: .. note::
The character set of e-mail sent with ``django.core.mail`` will be set to 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_HOST: ../settings/#email-host
.. _EMAIL_PORT: ../settings/#email-port .. _EMAIL_PORT: ../settings/#email-port
.. _EMAIL_HOST_USER: ../settings/#email-host-user .. _EMAIL_HOST_USER: ../settings/#email-host-user
@ -198,21 +198,36 @@ e-mail, you can subclass these two classes to suit your needs.
.. note:: .. note::
Not all features of the ``EmailMessage`` class are available through the Not all features of the ``EmailMessage`` class are available through the
``send_mail()`` and related wrapper functions. If you wish to use advanced ``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 features, such as BCC'ed recipients, file attachments, or multi-part
create ``EmailMessage`` instances directly. 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 In general, ``EmailMessage`` is responsible for creating the e-mail message
itself. ``SMTPConnection`` is responsible for the network connection side of itself. ``SMTPConnection`` is responsible for the network connection side of
the operation. This means you can reuse the same connection (an the operation. This means you can reuse the same connection (an
``SMTPConnection`` instance) for multiple messages. ``SMTPConnection`` instance) for multiple messages.
E-mail messages
----------------
The ``EmailMessage`` class is initialized as follows:: 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 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`` 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:: For example::
@ -227,7 +242,8 @@ The class has the following methods:
if none already exists. if none already exists.
* ``message()`` constructs a ``django.core.mail.SafeMIMEText`` object (a * ``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, 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 you'll probably want to override this method to put the content you wish
into the MIME object. 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 is sent. If you add another way to specify recipients in your class, they
need to be returned from this method as well. 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 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 password for the SMTP server. If you don't specify one or more of those
options, they are read from your settings file. options, they are read from your settings file.