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 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 @@ import random
# 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):
pass
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):
self.to = 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 @@ class EmailMessage(object):
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(self.to)
@ -189,6 +216,45 @@ class EmailMessage(object):
"""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:
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):
"""
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::
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.