Browse files

Fixed #3366 -- Part 1 of the email code refactoring and feature exten…

…sion. This

part refactors email sending into a more object-oriented interface in order to
make adding new features possible without making the API unusable. Thanks to
Gary Wilson for doing the design thinking and initial coding on this.

Includes documentation addition, but it probably needs a rewrite/edit, since
I'm not very happy with it at the moment.

git-svn-id: bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
malcolmt committed May 3, 2007
1 parent 1f88c7f commit 95d7cb27d04bca14f98bbfc9b990488cb913df2c
Showing with 203 additions and 44 deletions.
  1. +157 −44 django/core/
  2. +46 −0 docs/email.txt
@@ -1,9 +1,12 @@
# Use this module for e-mailing.
Tools for sending email.
from django.conf import settings
from email.MIMEText import MIMEText
from email.Header import Header
from email.Utils import formatdate
import os
import smtplib
import socket
import time
@@ -22,6 +25,28 @@ def get_fqdn(self):
DNS_NAME = CachedDnsName()
# Copied from Python standard library and modified to used the cached hostname
# for performance.
def make_msgid(idstring=None):
"""Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
Optional idstring if given is a string used to strengthen the
uniqueness of the message id.
timeval = time.time()
utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
pid = os.getpid()
randint = random.randrange(100000)
if idstring is None:
idstring = ''
idstring = '.' + idstring
idhost = DNS_NAME
msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
return msgid
class BadHeaderError(ValueError):
@@ -34,15 +59,131 @@ def __setitem__(self, name, val):
val = Header(val, settings.DEFAULT_CHARSET)
MIMEText.__setitem__(self, name, val)
class SMTPConnection(object):
A wrapper that manages the SMTP network connection.
def __init__(self, host=None, port=None, username=None, password=None,
if host is None: = settings.EMAIL_HOST
if port is None:
self.port = settings.EMAIL_PORT
if username is None:
self.username = settings.EMAIL_HOST_USER
if password is None:
self.password = settings.EMAIL_HOST_PASSWORD
self.fail_silently = fail_silently
self.connection = None
def open(self):
Ensure we have a connection to the email server. Returns whether or not
a new connection was required.
if self.connection:
# Nothing to do if the connection is already open.
return False
self.connection = smtplib.SMTP(, self.port)
if self.username and self.password:
self.connection.login(self.username, self.password)
return True
if not self.fail_silently:
def close(self):
"""Close the connection to the email server."""
if self.fail_silently:
self.connection = None
def send_messages(self, email_messages):
Send one or more EmailMessage objects and return the number of email
messages sent.
if not email_messages:
new_conn_created =
if not self.connection:
# We failed silently on open(). Trying to send would be pointless.
num_sent = 0
for message in email_messages:
sent = self._send(message)
if sent:
num_sent += 1
if new_conn_created:
return num_sent
def _send(self, email_message):
"""A helper method that does the actual sending."""
if not
return False
self.connection.sendmail(email_message.from_email,, email_message.message.as_string()())
if not self.fail_silently:
return False
return True
class EmailMessage(object):
A container for email information.
def __init__(self, subject='', body='', from_email=None, to=None, connection=None): = to or []
if from_email is None:
self.from_email = settings.DEFAULT_FROM_EMAIL
self.from_email = from_email
self.subject = subject
self.body = body
self.connection = connection
def get_connection(self, fail_silently=False):
if not self.connection:
self.connection = SMTPConnection(fail_silently=fail_silently)
return self.connection
def message(self):
msg = SafeMIMEText(self.body, 'plain', settings.DEFAULT_CHARSET)
msg['Subject'] = self.subject
msg['From'] = self.from_email
msg['To'] = ', '.join(
msg['Date'] = formatdate()
msg['Message-ID'] = make_msgid()
def send(self, fail_silently=False):
"""Send the email message."""
return self.get_connection(fail_silently).send_messages([self])
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
of the recipient list will see the other recipients in the 'To' field.
If auth_user is None, the EMAIL_HOST_USER setting is used.
If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
NOTE: This method is deprecated. It exists for backwards compatibility.
New code should use the EmailMessage class directly.
return send_mass_mail([[subject, message, from_email, recipient_list]], fail_silently, auth_user, auth_password)
connection = SMTPConnection(username=auth_user, password=auth_password,
return EmailMessage(subject, message, from_email, recipient_list, connection=connection).send()
def send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password=None):
@@ -53,52 +194,24 @@ def send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password
If auth_user and auth_password are set, they're used to log in.
If auth_user is None, the EMAIL_HOST_USER setting is used.
If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
NOTE: This method is deprecated. It exists for backwards compatibility.
New code should use the EmailMessage class directly.
if auth_user is None:
auth_user = settings.EMAIL_HOST_USER
if auth_password is None:
auth_password = settings.EMAIL_HOST_PASSWORD
server = smtplib.SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT)
if auth_user and auth_password:
server.login(auth_user, auth_password)
if fail_silently:
num_sent = 0
for subject, message, from_email, recipient_list in datatuple:
if not recipient_list:
from_email = from_email or settings.DEFAULT_FROM_EMAIL
msg = SafeMIMEText(message, 'plain', settings.DEFAULT_CHARSET)
msg['Subject'] = subject
msg['From'] = from_email
msg['To'] = ', '.join(recipient_list)
msg['Date'] = formatdate()
random_bits = str(random.getrandbits(64))
except AttributeError: # Python 2.3 doesn't have random.getrandbits().
random_bits = ''.join([random.choice('1234567890') for i in range(19)])
msg['Message-ID'] = "<%d.%s@%s>" % (time.time(), random_bits, DNS_NAME)
server.sendmail(from_email, recipient_list, msg.as_string())
num_sent += 1
if not fail_silently:
if fail_silently:
return num_sent
connection = SMTPConnection(username=auth_user, password=auth_password,
messages = [EmailMessage(subject, message, sender, recipient) for subject, message, sender, recipient in datatuple]
return connection.send_messages(messages)
def mail_admins(subject, message, fail_silently=False):
"Sends a message to the admins, as defined by the ADMINS setting."
send_mail(settings.EMAIL_SUBJECT_PREFIX + subject, message, settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS], fail_silently)
EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
settings.SERVER_EMAIL, [a[1] for a in
def mail_managers(subject, message, fail_silently=False):
"Sends a message to the managers, as defined by the MANAGERS setting."
send_mail(settings.EMAIL_SUBJECT_PREFIX + subject, message, settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS], fail_silently)
EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
settings.SERVER_EMAIL, [a[1] for a in
@@ -183,3 +183,49 @@ from the request's POST data, sends that to and redirects to
return HttpResponse('Make sure all fields are entered and valid.')
.. _Header injection:
The EmailMessage and SMTPConnection classes
Django's `send_mail()` and `send_mass_mail()` functions are actually thin
wrappers that make use of the `EmailMessage` and `SMTPConnection` classes in
`django.mail`. If you ever need to customize the way Django sends email, 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 including BCC recipients or multi-part email, you will
need to create `EmailMessage` instances directly.
In general, `EmailMessage` is responsible for creating the email 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.
The `EmailMessage` class has the following methods that you can use:
* `send()` sends the message, using either the connection that is specified
in the `connection` attribute, or creating a new connection if none already
* `message()` constructs a `django.core.mail.SafeMIMEText` object (a
sub-class of Python's `email.MIMEText.MIMEText` class) holding the message
to be sent. If you ever need to extend the `EmailMessage` class, you will
probably want to override this method to put the content you wish into the
MIME object.
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.
If you are sending lots of messages at once, the `send_messages()` method of
the `SMTPConnection` class will be useful. It takes a list of `EmailMessage`
instances (or sub-classes) and sends them over a single connection. For
example, if you have a function called `get_notification_email()` that returns a
list of `EmailMessage` objects representing some periodic email you wish to
send out, you could send this with::
connection = SMTPConnection() # Use default settings for connection
messages = get_notification_email()

0 comments on commit 95d7cb2

Please sign in to comment.