Fixed #3366 -- Part 1 of the email code refactoring and feature extension. 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: http://code.djangoproject.com/svn/django/trunk@5141 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2007-05-03 11:35:11 +00:00
parent 1f88c7fc67
commit 95d7cb27d0
2 changed files with 203 additions and 44 deletions

View File

@ -1,9 +1,12 @@
# Use this module for e-mailing. """
Tools for sending email.
"""
from django.conf import settings from django.conf import settings
from email.MIMEText import MIMEText from email.MIMEText import MIMEText
from email.Header import Header from email.Header import Header
from email.Utils import formatdate from email.Utils import formatdate
import os
import smtplib import smtplib
import socket import socket
import time import time
@ -22,6 +25,28 @@ class CachedDnsName(object):
DNS_NAME = CachedDnsName() 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:
<20020201195627.33539.96671@nightshade.la.mastaler.com>
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 = ''
else:
idstring = '.' + idstring
idhost = DNS_NAME
msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
return msgid
class BadHeaderError(ValueError): class BadHeaderError(ValueError):
pass pass
@ -34,6 +59,117 @@ class SafeMIMEText(MIMEText):
val = Header(val, settings.DEFAULT_CHARSET) val = Header(val, settings.DEFAULT_CHARSET)
MIMEText.__setitem__(self, name, val) 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,
fail_silently=False):
if host is None:
self.host = 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
try:
self.connection = smtplib.SMTP(self.host, self.port)
if self.username and self.password:
self.connection.login(self.username, self.password)
return True
except:
if not self.fail_silently:
raise
def close(self):
"""Close the connection to the email server."""
try:
try:
self.connection.quit()
except:
if self.fail_silently:
return
raise
finally:
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:
return
new_conn_created = self.open()
if not self.connection:
# We failed silently on open(). Trying to send would be pointless.
return
num_sent = 0
for message in email_messages:
sent = self._send(message)
if sent:
num_sent += 1
if new_conn_created:
self.close()
return num_sent
def _send(self, email_message):
"""A helper method that does the actual sending."""
if not email_message.to:
return False
try:
self.connection.sendmail(email_message.from_email,
email_message.to, email_message.message.as_string()())
except:
if not self.fail_silently:
raise
return False
return True
class EmailMessage(object):
"""
A container for email information.
"""
def __init__(self, subject='', body='', from_email=None, to=None, connection=None):
self.to = to or []
if from_email is None:
self.from_email = settings.DEFAULT_FROM_EMAIL
else:
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(self.to)
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): 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
@ -41,8 +177,13 @@ def send_mail(subject, message, from_email, recipient_list, fail_silently=False,
If auth_user is None, the EMAIL_HOST_USER setting is used. If auth_user is None, the EMAIL_HOST_USER setting is used.
If auth_password is None, the EMAIL_HOST_PASSWORD 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,
fail_silently=fail_silently)
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): 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 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_user is None, the EMAIL_HOST_USER setting is used.
If auth_password is None, the EMAIL_HOST_PASSWORD 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: connection = SMTPConnection(username=auth_user, password=auth_password,
auth_user = settings.EMAIL_HOST_USER fail_silently=fail_silently)
if auth_password is None: messages = [EmailMessage(subject, message, sender, recipient) for subject, message, sender, recipient in datatuple]
auth_password = settings.EMAIL_HOST_PASSWORD return connection.send_messages(messages)
try:
server = smtplib.SMTP(settings.EMAIL_HOST, settings.EMAIL_PORT)
if auth_user and auth_password:
server.login(auth_user, auth_password)
except:
if fail_silently:
return
raise
num_sent = 0
for subject, message, from_email, recipient_list in datatuple:
if not recipient_list:
continue
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()
try:
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)
try:
server.sendmail(from_email, recipient_list, msg.as_string())
num_sent += 1
except:
if not fail_silently:
raise
try:
server.quit()
except:
if fail_silently:
return
raise
return num_sent
def mail_admins(subject, message, fail_silently=False): def mail_admins(subject, message, fail_silently=False):
"Sends a message to the admins, as defined by the ADMINS setting." "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
settings.ADMINS]).send(fail_silently=fail_silently)
def mail_managers(subject, message, fail_silently=False): def mail_managers(subject, message, fail_silently=False):
"Sends a message to the managers, as defined by the MANAGERS setting." "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
settings.MANAGERS]).send(fail_silently=fail_silently)

View File

@ -183,3 +183,49 @@ from the request's POST data, sends that to admin@example.com and redirects to
return HttpResponse('Make sure all fields are entered and valid.') return HttpResponse('Make sure all fields are entered and valid.')
.. _Header injection: http://securephp.damonkohler.com/index.php/Email_Injection .. _Header injection: http://securephp.damonkohler.com/index.php/Email_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
exists.
* `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()
connection.send_messages(messages)