From 2d082a34dc61a832710d98a933858fd2c0059644 Mon Sep 17 00:00:00 2001 From: Malcolm Tredinnick Date: Wed, 27 Jun 2007 09:44:55 +0000 Subject: [PATCH] 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 --- django/core/mail.py | 74 ++++++++++++++++++++++++++++++++++++++++++--- docs/email.txt | 59 +++++++++++++++++++++++++++++++----- 2 files changed, 122 insertions(+), 11 deletions(-) diff --git a/django/core/mail.py b/django/core/mail.py index 8661d84287e..9c63569efa9 100644 --- a/django/core/mail.py +++ b/django/core/mail.py @@ -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 diff --git a/docs/email.txt b/docs/email.txt index 66948e52945..38683878846 100644 --- a/docs/email.txt +++ b/docs/email.txt @@ -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.