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) +