From 32683de27c1b219af1aef5945e1976fdc66b746c Mon Sep 17 00:00:00 2001 From: Boulder Sprinters Date: Thu, 3 May 2007 19:26:47 +0000 Subject: boulder-oracle-sprint: Merged to [5147] git-svn-id: http://code.djangoproject.com/svn/django/branches/boulder-oracle-sprint@5148 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 1 + django/core/mail.py | 220 ++++++++++++++++++++++++++++++++--------- docs/documentation.txt | 8 +- docs/email.txt | 66 ++++++++++++- docs/sessions.txt | 2 +- docs/settings.txt | 9 ++ docs/syndication_feeds.txt | 6 +- docs/templates_python.txt | 2 +- 8 files changed, 259 insertions(+), 55 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 521caedbd0..61751db8cd 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -119,6 +119,7 @@ EMAIL_PORT = 25 # Optional SMTP authentication information for EMAIL_HOST. EMAIL_HOST_USER = '' EMAIL_HOST_PASSWORD = '' +EMAIL_USE_TLS = False # List of strings representing installed apps. INSTALLED_APPS = () diff --git a/django/core/mail.py b/django/core/mail.py index e7dcfb4132..8661d84287 100644 --- a/django/core/mail.py +++ b/django/core/mail.py @@ -1,14 +1,22 @@ -# 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 +from email import Charset +import os import smtplib import socket import time import random +# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from +# some spam filters. +Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8') + # 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): @@ -22,6 +30,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 +64,131 @@ 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, + use_tls=None, fail_silently=False): + self.host = host or settings.EMAIL_HOST + self.port = port or settings.EMAIL_PORT + self.username = username or settings.EMAIL_HOST_USER + self.password = password or settings.EMAIL_HOST_PASSWORD + self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS + 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.use_tls: + self.connection.ehlo() + self.connection.starttls() + self.connection.ehlo() + 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 socket.sslerror: + # This happens when calling quit() on a TLS connection + # sometimes. + self.connection.close() + 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.recipients(), + 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, bcc=None, connection=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.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() + if self.bcc: + msg['Bcc'] = ', '.join(self.bcc) + return msg + + def recipients(self): + """ + Returns a list of all recipients of the email (includes direct + addressees as well as Bcc entries). + """ + return self.to + self.bcc + + 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 +196,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 +213,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/documentation.txt b/docs/documentation.txt index e72dd47ba1..decb066fa1 100644 --- a/docs/documentation.txt +++ b/docs/documentation.txt @@ -94,12 +94,10 @@ Formatting The text documentation is written in ReST (ReStructured Text) format. That means it's easy to read but is also formatted in a way that makes it easy to -convert into other formats, such as HTML. If you're interested, the script that -converts the ReST text docs into djangoproject.com's HTML lives at -`djangoproject.com/django_website/apps/docs/parts/build_documentation.py`_ in -the Django Subversion repository. +convert into other formats, such as HTML. If you have the `reStructuredText`_ +library installed, you can use ``rst2html`` to generate your own HTML files. -.. _djangoproject.com/django_website/apps/docs/parts/build_documentation.py: http://code.djangoproject.com/browser/djangoproject.com/django_website/apps/docs/parts/build_documentation.py +.. _reStructuredText: http://docutils.sourceforge.net/rst.html Differences between versions ============================ diff --git a/docs/email.txt b/docs/email.txt index 8ebdaa8136..2793ee8ae3 100644 --- a/docs/email.txt +++ b/docs/email.txt @@ -22,7 +22,8 @@ In two lines:: Mail will be sent using the SMTP host and port specified in the `EMAIL_HOST`_ and `EMAIL_PORT`_ settings. The `EMAIL_HOST_USER`_ and `EMAIL_HOST_PASSWORD`_ -settings, if set, will be used to authenticate to the SMTP server. +settings, if set, will be used to authenticate to the SMTP server and the +`EMAIL_USE_TLS`_ settings will control whether a secure connection is used. .. note:: @@ -34,6 +35,7 @@ settings, if set, will be used to authenticate to the SMTP server. .. _EMAIL_PORT: ../settings/#email-port .. _EMAIL_HOST_USER: ../settings/#email-host-user .. _EMAIL_HOST_PASSWORD: ../settings/#email-host-password +.. _EMAIL_USE_TLS: ../settings/#email-use-tls send_mail() @@ -183,3 +185,65 @@ 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 +=========================================== + +**New in Django development version** + +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 is initialised as follows:: + + email = EmailMessage(subject, body, from_email, to, bcc, connection) + +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. + +The 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. + * ``recipients()`` returns a lists of all the recipients of the message, + whether they are recorded in the ``to`` or ``bcc`` attributes. This is + another method you need to possibly override when sub-classing, since the + SMTP server needs to be told the full list of recipients when the message + is sent. If you add another way to specify recipients in your class, they + need to be returned from this method as well. + +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) + diff --git a/docs/sessions.txt b/docs/sessions.txt index 55fbc2c3da..c7124ba703 100644 --- a/docs/sessions.txt +++ b/docs/sessions.txt @@ -107,7 +107,7 @@ posts a comment. It doesn't let a user post a comment more than once:: This simplistic view logs in a "member" of the site:: def login(request): - m = members.get_object(username__exact=request.POST['username']) + m = Member.objects.get(username=request.POST['username']) if m.password == request.POST['password']: request.session['member_id'] = m.id return HttpResponse("You're logged in.") diff --git a/docs/settings.txt b/docs/settings.txt index aae9a1da04..90d31bfeaa 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -428,6 +428,15 @@ Subject-line prefix for e-mail messages sent with ``django.core.mail.mail_admins or ``django.core.mail.mail_managers``. You'll probably want to include the trailing space. +EMAIL_USE_TLS +------------- + +**New in Django development version** + +Default: ``False`` + +Whether to use a TLS (secure) connection when talking to the SMTP server. + FIXTURE_DIRS ------------- diff --git a/docs/syndication_feeds.txt b/docs/syndication_feeds.txt index c3b02b5d3f..d9d4f53b88 100644 --- a/docs/syndication_feeds.txt +++ b/docs/syndication_feeds.txt @@ -646,15 +646,15 @@ This example illustrates all possible attributes and methods for a ``Feed`` clas def item_enclosure_mime_type(self, item): """ Takes an item, as returned by items(), and returns the item's - enclosure mime type. + enclosure MIME type. """ def item_enclosure_mime_type(self): """ - Returns the enclosure length, in bytes, for every item in the feed. + Returns the enclosure MIME type for every item in the feed. """ - item_enclosure_mime_type = "audio/mpeg" # Hard-coded enclosure mime-type. + item_enclosure_mime_type = "audio/mpeg" # Hard-coded enclosure MIME type. # ITEM PUBDATE -- It's optional to use one of these three. This is a # hook that specifies how to get the pubdate for a given item. diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 7cc9acede8..1eeede1fe8 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -345,7 +345,7 @@ If ``TEMPLATE_CONTEXT_PROCESSORS`` contains this processor, every ``request.user.get_and_delete_messages()`` for every request. That method collects the user's messages and deletes them from the database. - Note that messages are set with ``user.add_message()``. See the + Note that messages are set with ``user.message_set.create``. See the `message docs`_ for more. * ``perms`` -- An instance of -- cgit v1.2.1