summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/core/mail.py201
-rw-r--r--docs/email.txt46
2 files changed, 203 insertions, 44 deletions
diff --git a/django/core/mail.py b/django/core/mail.py
index e7dcfb4132..25dfe4e9c9 100644
--- a/django/core/mail.py
+++ b/django/core/mail.py
@@ -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 @@ class CachedDnsName(object):
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):
pass
@@ -34,6 +59,117 @@ class SafeMIMEText(MIMEText):
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,
+ 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):
"""
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_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):
"""
@@ -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
- 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
+ connection = SMTPConnection(username=auth_user, password=auth_password,
+ fail_silently=fail_silently)
+ 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
+ settings.ADMINS]).send(fail_silently=fail_silently)
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
+ settings.MANAGERS]).send(fail_silently=fail_silently)
+
diff --git a/docs/email.txt b/docs/email.txt
index 8ebdaa8136..74127526dd 100644
--- a/docs/email.txt
+++ b/docs/email.txt
@@ -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.')
.. _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)
+