2
0
Эх сурвалжийг харах

Fixed #10355 -- Added an API for pluggable e-mail backends.

Thanks to Andi Albrecht for his work on this patch, and to everyone else that contributed during design and development.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@11709 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Russell Keith-Magee 15 жил өмнө
parent
commit
aba5389326

+ 1 - 0
AUTHORS

@@ -27,6 +27,7 @@ answer newbie questions, and generally made Django that much better:
 
     ajs <adi@sieker.info>
     alang@bright-green.com
+    Andi Albrecht <albrecht.andi@gmail.com>
     Marty Alchin <gulopine@gamemusic.org>
     Ahmad Alhashemi <trans@ahmadh.com>
     Daniel Alves Barbosa de Oliveira Vaz <danielvaz@gmail.com>

+ 6 - 0
django/conf/global_settings.py

@@ -131,6 +131,12 @@ DATABASE_HOST = ''             # Set to empty string for localhost. Not used wit
 DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.
 DATABASE_OPTIONS = {}          # Set to empty dictionary for default.
 
+# The email backend to use. For possible shortcuts see django.core.mail.
+# The default is to use the SMTP backend.
+# Third-party backends can be specified by providing a Python path
+# to a module that defines an EmailBackend class.
+EMAIL_BACKEND = 'django.core.mail.backends.smtp'
+
 # Host for sending e-mail.
 EMAIL_HOST = 'localhost'
 

+ 110 - 0
django/core/mail/__init__.py

@@ -0,0 +1,110 @@
+"""
+Tools for sending email.
+"""
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.importlib import import_module
+
+# Imported for backwards compatibility, and for the sake
+# of a cleaner namespace. These symbols used to be in
+# django/core/mail.py before the introduction of email
+# backends and the subsequent reorganization (See #10355)
+from django.core.mail.utils import CachedDnsName, DNS_NAME
+from django.core.mail.message import \
+    EmailMessage, EmailMultiAlternatives, \
+    SafeMIMEText, SafeMIMEMultipart, \
+    DEFAULT_ATTACHMENT_MIME_TYPE, make_msgid, \
+    BadHeaderError, forbid_multi_line_headers
+from django.core.mail.backends.smtp import EmailBackend as _SMTPConnection
+
+def get_connection(backend=None, fail_silently=False, **kwds):
+    """Load an e-mail backend and return an instance of it.
+
+    If backend is None (default) settings.EMAIL_BACKEND is used.
+
+    Both fail_silently and other keyword arguments are used in the
+    constructor of the backend.
+    """
+    path = backend or settings.EMAIL_BACKEND
+    try:
+        mod = import_module(path)
+    except ImportError, e:
+        raise ImproperlyConfigured(('Error importing email backend %s: "%s"'
+                                    % (path, e)))
+    try:
+        cls = getattr(mod, 'EmailBackend')
+    except AttributeError:
+        raise ImproperlyConfigured(('Module "%s" does not define a '
+                                    '"EmailBackend" class' % path))
+    return cls(fail_silently=fail_silently, **kwds)
+
+
+def send_mail(subject, message, from_email, recipient_list,
+              fail_silently=False, auth_user=None, auth_password=None,
+              connection=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: The API for this method is frozen. New code wanting to extend the
+    functionality should use the EmailMessage class directly.
+    """
+    connection = connection or get_connection(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, connection=None):
+    """
+    Given a datatuple of (subject, message, from_email, recipient_list), sends
+    each message to each recipient list. Returns the number of e-mails sent.
+
+    If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
+    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: The API for this method is frozen. New code wanting to extend the
+    functionality should use the EmailMessage class directly.
+    """
+    connection = connection or get_connection(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, connection=None):
+    """Sends a message to the admins, as defined by the ADMINS setting."""
+    if not settings.ADMINS:
+        return
+    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
+                 settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS],
+                 connection=connection).send(fail_silently=fail_silently)
+
+
+def mail_managers(subject, message, fail_silently=False, connection=None):
+    """Sends a message to the managers, as defined by the MANAGERS setting."""
+    if not settings.MANAGERS:
+        return
+    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
+                 settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS],
+                 connection=connection).send(fail_silently=fail_silently)
+
+
+class SMTPConnection(_SMTPConnection):
+    def __init__(self, *args, **kwds):
+        import warnings
+        warnings.warn(
+            'mail.SMTPConnection is deprecated; use mail.get_connection() instead.',
+            DeprecationWarning
+        )
+        super(SMTPConnection, self).__init__(*args, **kwds)

+ 1 - 0
django/core/mail/backends/__init__.py

@@ -0,0 +1 @@
+# Mail backends shipped with Django.

+ 39 - 0
django/core/mail/backends/base.py

@@ -0,0 +1,39 @@
+"""Base email backend class."""
+
+class BaseEmailBackend(object):
+    """
+    Base class for email backend implementations.
+
+    Subclasses must at least overwrite send_messages().
+    """
+    def __init__(self, fail_silently=False, **kwargs):
+        self.fail_silently = fail_silently
+
+    def open(self):
+        """Open a network connection.
+
+        This method can be overwritten by backend implementations to
+        open a network connection.
+
+        It's up to the backend implementation to track the status of
+        a network connection if it's needed by the backend.
+
+        This method can be called by applications to force a single
+        network connection to be used when sending mails. See the
+        send_messages() method of the SMTP backend for a reference
+        implementation.
+
+        The default implementation does nothing.
+        """
+        pass
+
+    def close(self):
+        """Close a network connection."""
+        pass
+
+    def send_messages(self, email_messages):
+        """
+        Sends one or more EmailMessage objects and returns the number of email
+        messages sent.
+        """
+        raise NotImplementedError

+ 34 - 0
django/core/mail/backends/console.py

@@ -0,0 +1,34 @@
+"""
+Email backend that writes messages to console instead of sending them.
+"""
+import sys
+import threading
+
+from django.core.mail.backends.base import BaseEmailBackend
+
+class EmailBackend(BaseEmailBackend):
+    def __init__(self, *args, **kwargs):
+        self.stream = kwargs.pop('stream', sys.stdout)
+        self._lock = threading.RLock()
+        super(EmailBackend, self).__init__(*args, **kwargs)
+
+    def send_messages(self, email_messages):
+        """Write all messages to the stream in a thread-safe way."""
+        if not email_messages:
+            return
+        self._lock.acquire()
+        try:
+            stream_created = self.open()
+            for message in email_messages:
+                self.stream.write('%s\n' % message.message().as_string())
+                self.stream.write('-'*79)
+                self.stream.write('\n')
+                self.stream.flush()  # flush after each message
+            if stream_created:
+                self.close()
+        except:
+            if not self.fail_silently:
+                raise
+        finally:
+            self._lock.release()
+        return len(email_messages)

+ 9 - 0
django/core/mail/backends/dummy.py

@@ -0,0 +1,9 @@
+"""
+Dummy email backend that does nothing.
+"""
+
+from django.core.mail.backends.base import BaseEmailBackend
+
+class EmailBackend(BaseEmailBackend):
+    def send_messages(self, email_messages):
+        return len(email_messages)

+ 59 - 0
django/core/mail/backends/filebased.py

@@ -0,0 +1,59 @@
+"""Email backend that writes messages to a file."""
+
+import datetime
+import os
+
+from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
+from django.core.mail.backends.console import EmailBackend as ConsoleEmailBackend
+
+class EmailBackend(ConsoleEmailBackend):
+    def __init__(self, *args, **kwargs):
+        self._fname = None
+        if 'file_path' in kwargs:
+            self.file_path = kwargs.pop('file_path')
+        else:
+            self.file_path = getattr(settings, 'EMAIL_FILE_PATH',None)
+        # Make sure self.file_path is a string.
+        if not isinstance(self.file_path, basestring):
+            raise ImproperlyConfigured('Path for saving emails is invalid: %r' % self.file_path)
+        self.file_path = os.path.abspath(self.file_path)
+        # Make sure that self.file_path is an directory if it exists.
+        if os.path.exists(self.file_path) and not os.path.isdir(self.file_path):
+            raise ImproperlyConfigured('Path for saving email messages exists, but is not a directory: %s' % self.file_path)
+        # Try to create it, if it not exists.
+        elif not os.path.exists(self.file_path):
+            try:
+                os.makedirs(self.file_path)
+            except OSError, err:
+                raise ImproperlyConfigured('Could not create directory for saving email messages: %s (%s)' % (self.file_path, err))
+        # Make sure that self.file_path is writable.
+        if not os.access(self.file_path, os.W_OK):
+            raise ImproperlyConfigured('Could not write to directory: %s' % self.file_path)
+        # Finally, call super().
+        # Since we're using the console-based backend as a base,
+        # force the stream to be None, so we don't default to stdout
+        kwargs['stream'] = None
+        super(EmailBackend, self).__init__(*args, **kwargs)
+
+    def _get_filename(self):
+        """Return a unique file name."""
+        if self._fname is None:
+            timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
+            fname = "%s-%s.log" % (timestamp, abs(id(self)))
+            self._fname = os.path.join(self.file_path, fname)
+        return self._fname
+
+    def open(self):
+        if self.stream is None:
+            self.stream = open(self._get_filename(), 'a')
+            return True
+        return False
+
+    def close(self):
+        try:
+            if self.stream is not None:
+                self.stream.close()
+        finally:
+            self.stream = None
+

+ 24 - 0
django/core/mail/backends/locmem.py

@@ -0,0 +1,24 @@
+"""
+Backend for test environment.
+"""
+
+from django.core import mail
+from django.core.mail.backends.base import BaseEmailBackend
+
+class EmailBackend(BaseEmailBackend):
+    """A email backend for use during test sessions.
+
+    The test connection stores email messages in a dummy outbox,
+    rather than sending them out on the wire.
+
+    The dummy outbox is accessible through the outbox instance attribute.
+    """
+    def __init__(self, *args, **kwargs):
+        super(EmailBackend, self).__init__(*args, **kwargs)
+        if not hasattr(mail, 'outbox'):
+            mail.outbox = []
+
+    def send_messages(self, messages):
+        """Redirect messages to the dummy outbox"""
+        mail.outbox.extend(messages)
+        return len(messages)

+ 103 - 0
django/core/mail/backends/smtp.py

@@ -0,0 +1,103 @@
+"""SMTP email backend class."""
+
+import smtplib
+import socket
+import threading
+
+from django.conf import settings
+from django.core.mail.backends.base import BaseEmailBackend
+from django.core.mail.utils import DNS_NAME
+
+class EmailBackend(BaseEmailBackend):
+    """
+    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, **kwargs):
+        super(EmailBackend, self).__init__(fail_silently=fail_silently)
+        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.connection = None
+        self._lock = threading.RLock()
+
+    def open(self):
+        """
+        Ensures we have a connection to the email server. Returns whether or
+        not a new connection was required (True or False).
+        """
+        if self.connection:
+            # Nothing to do if the connection is already open.
+            return False
+        try:
+            # If local_hostname is not specified, socket.getfqdn() gets used.
+            # For performance, we use the cached FQDN for local_hostname.
+            self.connection = smtplib.SMTP(self.host, self.port,
+                                           local_hostname=DNS_NAME.get_fqdn())
+            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):
+        """Closes 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):
+        """
+        Sends one or more EmailMessage objects and returns the number of email
+        messages sent.
+        """
+        if not email_messages:
+            return
+        self._lock.acquire()
+        try:
+            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()
+        finally:
+            self._lock.release()
+        return num_sent
+
+    def _send(self, email_message):
+        """A helper method that does the actual sending."""
+        if not email_message.recipients():
+            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

+ 13 - 165
django/core/mail.py → django/core/mail/message.py

@@ -1,13 +1,7 @@
-"""
-Tools for sending email.
-"""
-
 import mimetypes
 import os
-import smtplib
-import socket
-import time
 import random
+import time
 from email import Charset, Encoders
 from email.MIMEText import MIMEText
 from email.MIMEMultipart import MIMEMultipart
@@ -16,6 +10,7 @@ from email.Header import Header
 from email.Utils import formatdate, parseaddr, formataddr
 
 from django.conf import settings
+from django.core.mail.utils import DNS_NAME
 from django.utils.encoding import smart_str, force_unicode
 
 # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
@@ -26,18 +21,10 @@ Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
 # and cannot be guessed).
 DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream'
 
-# 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):
-    def __str__(self):
-        return self.get_fqdn()
 
-    def get_fqdn(self):
-        if not hasattr(self, '_fqdn'):
-            self._fqdn = socket.getfqdn()
-        return self._fqdn
+class BadHeaderError(ValueError):
+    pass
 
-DNS_NAME = CachedDnsName()
 
 # Copied from Python standard library, with the following modifications:
 # * Used cached hostname for performance.
@@ -66,8 +53,6 @@ def make_msgid(idstring=None):
     msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
     return msgid
 
-class BadHeaderError(ValueError):
-    pass
 
 def forbid_multi_line_headers(name, val):
     """Forbids multi-line headers, to prevent header injection."""
@@ -91,104 +76,18 @@ def forbid_multi_line_headers(name, val):
             val = Header(val)
     return name, val
 
+
 class SafeMIMEText(MIMEText):
     def __setitem__(self, name, val):
         name, val = forbid_multi_line_headers(name, val)
         MIMEText.__setitem__(self, name, val)
 
+
 class SafeMIMEMultipart(MIMEMultipart):
     def __setitem__(self, name, val):
         name, val = forbid_multi_line_headers(name, val)
         MIMEMultipart.__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):
-        """
-        Ensures we have a connection to the email server. Returns whether or
-        not a new connection was required (True or False).
-        """
-        if self.connection:
-            # Nothing to do if the connection is already open.
-            return False
-        try:
-            # If local_hostname is not specified, socket.getfqdn() gets used.
-            # For performance, we use the cached FQDN for local_hostname.
-            self.connection = smtplib.SMTP(self.host, self.port,
-                                           local_hostname=DNS_NAME.get_fqdn())
-            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):
-        """Closes 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):
-        """
-        Sends one or more EmailMessage objects and returns 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.recipients():
-            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):
     """
@@ -199,14 +98,14 @@ class EmailMessage(object):
     encoding = None     # None => use settings default
 
     def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
-            connection=None, attachments=None, headers=None):
+                 connection=None, attachments=None, headers=None):
         """
         Initialize a single email message (which can be sent to multiple
         recipients).
 
-        All strings used to create the message can be unicode strings (or UTF-8
-        bytestrings). The SafeMIMEText class will handle any necessary encoding
-        conversions.
+        All strings used to create the message can be unicode strings
+        (or UTF-8 bytestrings). The SafeMIMEText class will handle any
+        necessary encoding conversions.
         """
         if to:
             assert not isinstance(to, basestring), '"to" argument must be a list or tuple'
@@ -226,8 +125,9 @@ class EmailMessage(object):
         self.connection = connection
 
     def get_connection(self, fail_silently=False):
+        from django.core.mail import get_connection
         if not self.connection:
-            self.connection = SMTPConnection(fail_silently=fail_silently)
+            self.connection = get_connection(fail_silently=fail_silently)
         return self.connection
 
     def message(self):
@@ -332,6 +232,7 @@ class EmailMessage(object):
                                   filename=filename)
         return attachment
 
+
 class EmailMultiAlternatives(EmailMessage):
     """
     A version of EmailMessage that makes it easy to send multipart/alternative
@@ -371,56 +272,3 @@ class EmailMultiAlternatives(EmailMessage):
             for alternative in self.alternatives:
                 msg.attach(self._create_mime_attachment(*alternative))
         return msg
-
-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: The API for this method is frozen. New code wanting to extend the
-    functionality should use the EmailMessage class directly.
-    """
-    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):
-    """
-    Given a datatuple of (subject, message, from_email, recipient_list), sends
-    each message to each recipient list. Returns the number of e-mails sent.
-
-    If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
-    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: The API for this method is frozen. New code wanting to extend the
-    functionality should use the EmailMessage class directly.
-    """
-    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."""
-    if not settings.ADMINS:
-        return
-    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."""
-    if not settings.MANAGERS:
-        return
-    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
-                 settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS]
-                 ).send(fail_silently=fail_silently)

+ 19 - 0
django/core/mail/utils.py

@@ -0,0 +1,19 @@
+"""
+Email message and email sending related helper functions.
+"""
+
+import socket
+
+
+# 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):
+    def __str__(self):
+        return self.get_fqdn()
+
+    def get_fqdn(self):
+        if not hasattr(self, '_fqdn'):
+            self._fqdn = socket.getfqdn()
+        return self._fqdn
+
+DNS_NAME = CachedDnsName()

+ 9 - 21
django/test/utils.py

@@ -2,6 +2,7 @@ import sys, time, os
 from django.conf import settings
 from django.db import connection
 from django.core import mail
+from django.core.mail.backends import locmem
 from django.test import signals
 from django.template import Template
 from django.utils.translation import deactivate
@@ -28,37 +29,22 @@ def instrumented_test_render(self, context):
     signals.template_rendered.send(sender=self, template=self, context=context)
     return self.nodelist.render(context)
 
-class TestSMTPConnection(object):
-    """A substitute SMTP connection for use during test sessions.
-    The test connection stores email messages in a dummy outbox,
-    rather than sending them out on the wire.
-
-    """
-    def __init__(*args, **kwargs):
-        pass
-    def open(self):
-        "Mock the SMTPConnection open() interface"
-        pass
-    def close(self):
-        "Mock the SMTPConnection close() interface"
-        pass
-    def send_messages(self, messages):
-        "Redirect messages to the dummy outbox"
-        mail.outbox.extend(messages)
-        return len(messages)
 
 def setup_test_environment():
     """Perform any global pre-test setup. This involves:
 
         - Installing the instrumented test renderer
-        - Diverting the email sending functions to a test buffer
+        - Set the email backend to the locmem email backend.
         - Setting the active locale to match the LANGUAGE_CODE setting.
     """
     Template.original_render = Template.render
     Template.render = instrumented_test_render
 
     mail.original_SMTPConnection = mail.SMTPConnection
-    mail.SMTPConnection = TestSMTPConnection
+    mail.SMTPConnection = locmem.EmailBackend
+
+    settings.EMAIL_BACKEND = 'django.core.mail.backends.locmem'
+    mail.original_email_backend = settings.EMAIL_BACKEND
 
     mail.outbox = []
 
@@ -77,8 +63,10 @@ def teardown_test_environment():
     mail.SMTPConnection = mail.original_SMTPConnection
     del mail.original_SMTPConnection
 
-    del mail.outbox
+    settings.EMAIL_BACKEND = mail.original_email_backend
+    del mail.original_email_backend
 
+    del mail.outbox
 
 def get_runner(settings):
     test_path = settings.TEST_RUNNER.split('.')

+ 3 - 0
docs/internals/deprecation.txt

@@ -22,6 +22,9 @@ their deprecation, as per the :ref:`Django deprecation policy
         * The old imports for CSRF functionality (``django.contrib.csrf.*``),
           which moved to core in 1.2, will be removed.
 
+        * ``SMTPConnection``. The 1.2 release deprecated the ``SMTPConnection``
+          class in favor of a generic E-mail backend API.
+
     * 2.0
         * ``django.views.defaults.shortcut()``. This function has been moved
           to ``django.contrib.contenttypes.views.shortcut()`` as part of the

+ 23 - 0
docs/ref/settings.txt

@@ -424,6 +424,29 @@ are not allowed to visit any page, systemwide. Use this for bad robots/crawlers.
 This is only used if ``CommonMiddleware`` is installed (see
 :ref:`topics-http-middleware`).
 
+.. setting:: EMAIL_BACKEND
+
+EMAIL_BACKEND
+-------------
+
+.. versionadded:: 1.2
+
+Default: ``'smtp'``
+
+The backend to use for sending emails. For the list of available backends see
+:ref:`topics-email`.
+
+.. setting:: EMAIL_FILE_PATH
+
+EMAIL_FILE_PATH
+---------------
+
+.. versionadded:: 1.2
+
+Default: Not defined
+
+The directory used by the ``file`` email backend to store output files.
+
 .. setting:: EMAIL_HOST
 
 EMAIL_HOST

+ 318 - 91
docs/topics/email.txt

@@ -7,11 +7,13 @@ Sending e-mail
 .. module:: django.core.mail
    :synopsis: Helpers to easily send e-mail.
 
-Although Python makes sending e-mail relatively easy via the `smtplib library`_,
-Django provides a couple of light wrappers over it, to make sending e-mail
-extra quick.
+Although Python makes sending e-mail relatively easy via the `smtplib
+library`_, Django provides a couple of light wrappers over it. These wrappers
+are provided to make sending e-mail extra quick, to make it easy to test
+email sending during development, and to provide support for platforms that
+can't use SMTP.
 
-The code lives in a single module: ``django.core.mail``.
+The code lives in the ``django.core.mail`` module.
 
 .. _smtplib library: http://docs.python.org/library/smtplib.html
 
@@ -25,11 +27,11 @@ In two lines::
     send_mail('Subject here', 'Here is the message.', 'from@example.com',
         ['to@example.com'], fail_silently=False)
 
-Mail is sent using the SMTP host and port specified in the :setting:`EMAIL_HOST`
-and :setting:`EMAIL_PORT` settings. The :setting:`EMAIL_HOST_USER` and
-:setting:`EMAIL_HOST_PASSWORD` settings, if set, are used to authenticate to the
-SMTP server, and the :setting:`EMAIL_USE_TLS` setting controls whether a secure
-connection is used.
+Mail is sent using the SMTP host and port specified in the
+:setting:`EMAIL_HOST` and :setting:`EMAIL_PORT` settings. The
+:setting:`EMAIL_HOST_USER` and :setting:`EMAIL_HOST_PASSWORD` settings, if
+set, are used to authenticate to the SMTP server, and the
+:setting:`EMAIL_USE_TLS` setting controls whether a secure connection is used.
 
 .. note::
 
@@ -42,7 +44,7 @@ send_mail()
 The simplest way to send e-mail is using the function
 ``django.core.mail.send_mail()``. Here's its definition:
 
-    .. function:: send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None)
+    .. function:: send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None, connection=None)
 
 The ``subject``, ``message``, ``from_email`` and ``recipient_list`` parameters
 are required.
@@ -62,6 +64,10 @@ are required.
     * ``auth_password``: The optional password to use to authenticate to the
       SMTP server. If this isn't provided, Django will use the value of the
       ``EMAIL_HOST_PASSWORD`` setting.
+    * ``connection``: The optional email backend to use to send the mail.
+      If unspecified, an instance of the default backend will be used.
+      See the documentation on :ref:`E-mail backends <topic-email-backends>`
+      for more details.
 
 .. _smtplib docs: http://docs.python.org/library/smtplib.html
 
@@ -71,26 +77,29 @@ send_mass_mail()
 ``django.core.mail.send_mass_mail()`` is intended to handle mass e-mailing.
 Here's the definition:
 
-    .. function:: send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password=None)
+    .. function:: send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password=None, connection=None)
 
 ``datatuple`` is a tuple in which each element is in this format::
 
     (subject, message, from_email, recipient_list)
 
 ``fail_silently``, ``auth_user`` and ``auth_password`` have the same functions
-as in ``send_mail()``.
+as in :meth:`~django.core.mail.send_mail()`.
 
 Each separate element of ``datatuple`` results in a separate e-mail message.
-As in ``send_mail()``, recipients in the same ``recipient_list`` will all see
-the other addresses in the e-mail messages' "To:" field.
+As in :meth:`~django.core.mail.send_mail()`, recipients in the same
+``recipient_list`` will all see the other addresses in the e-mail messages'
+"To:" field.
 
 send_mass_mail() vs. send_mail()
 --------------------------------
 
-The main difference between ``send_mass_mail()`` and ``send_mail()`` is that
-``send_mail()`` opens a connection to the mail server each time it's executed,
-while ``send_mass_mail()`` uses a single connection for all of its messages.
-This makes ``send_mass_mail()`` slightly more efficient.
+The main difference between :meth:`~django.core.mail.send_mass_mail()` and
+:meth:`~django.core.mail.send_mail()` is that
+:meth:`~django.core.mail.send_mail()` opens a connection to the mail server
+each time it's executed, while :meth:`~django.core.mail.send_mass_mail()` uses
+a single connection for all of its messages. This makes
+:meth:`~django.core.mail.send_mass_mail()` slightly more efficient.
 
 mail_admins()
 =============
@@ -98,7 +107,7 @@ mail_admins()
 ``django.core.mail.mail_admins()`` is a shortcut for sending an e-mail to the
 site admins, as defined in the :setting:`ADMINS` setting. Here's the definition:
 
-    .. function:: mail_admins(subject, message, fail_silently=False)
+    .. function:: mail_admins(subject, message, fail_silently=False, connection=None)
 
 ``mail_admins()`` prefixes the subject with the value of the
 :setting:`EMAIL_SUBJECT_PREFIX` setting, which is ``"[Django] "`` by default.
@@ -115,7 +124,7 @@ mail_managers() function
 sends an e-mail to the site managers, as defined in the :setting:`MANAGERS`
 setting. Here's the definition:
 
-    .. function:: mail_managers(subject, message, fail_silently=False)
+    .. function:: mail_managers(subject, message, fail_silently=False, connection=None)
 
 Examples
 ========
@@ -145,7 +154,7 @@ scripts generate.
 The Django e-mail functions outlined above all protect against header injection
 by forbidding newlines in header values. If any ``subject``, ``from_email`` or
 ``recipient_list`` contains a newline (in either Unix, Windows or Mac style),
-the e-mail function (e.g. ``send_mail()``) will raise
+the e-mail function (e.g. :meth:`~django.core.mail.send_mail()`) will raise
 ``django.core.mail.BadHeaderError`` (a subclass of ``ValueError``) and, hence,
 will not send the e-mail. It's your responsibility to validate all data before
 passing it to the e-mail functions.
@@ -178,41 +187,47 @@ from the request's POST data, sends that to admin@example.com and redirects to
 
 .. _emailmessage-and-smtpconnection:
 
-The EmailMessage and SMTPConnection classes
-===========================================
+The EmailMessage class
+======================
 
 .. versionadded:: 1.0
 
-Django's ``send_mail()`` and ``send_mass_mail()`` functions are actually thin
-wrappers that make use of the ``EmailMessage`` and ``SMTPConnection`` classes
-in ``django.core.mail``.  If you ever need to customize the way Django sends
-e-mail, you can subclass these two classes to suit your needs.
+Django's :meth:`~django.core.mail.send_mail()` and
+:meth:`~django.core.mail.send_mass_mail()` functions are actually thin
+wrappers that make use of the :class:`~django.core.mail.EmailMessage` class.
+
+Not all features of the :class:`~django.core.mail.EmailMessage` class are
+available through the :meth:`~django.core.mail.send_mail()` and related
+wrapper functions. If you wish to use advanced features, such as BCC'ed
+recipients, file attachments, or multi-part e-mail, you'll need to create
+:class:`~django.core.mail.EmailMessage` instances directly.
 
 .. 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 BCC'ed recipients, file attachments, or multi-part
-    e-mail, you'll need to create ``EmailMessage`` instances directly.
-
-    This is a design feature. ``send_mail()`` and related functions were
-    originally the only interface Django provided. However, the list of
-    parameters they accepted was slowly growing over time. It made sense to
-    move to a more object-oriented design for e-mail messages and retain the
-    original functions only for backwards compatibility.
-
-In general, ``EmailMessage`` is responsible for creating the e-mail 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.
+    This is a design feature. :meth:`~django.core.mail.send_mail()` and
+    related functions were originally the only interface Django provided.
+    However, the list of parameters they accepted was slowly growing over
+    time. It made sense to move to a more object-oriented design for e-mail
+    messages and retain the original functions only for backwards
+    compatibility.
+
+:class:`~django.core.mail.EmailMessage` is responsible for creating the e-mail
+message itself. The :ref:`e-mail backend <topic-email-backends>` is then
+responsible for sending the e-mail.
+
+For convenience, :class:`~django.core.mail.EmailMessage` provides a simple
+``send()`` method for sending a single email. If you need to send multiple
+messages, the email backend API :ref:`provides an alternative
+<topics-sending-multiple-emails>`.
 
 EmailMessage Objects
 --------------------
 
 .. class:: EmailMessage
 
-The ``EmailMessage`` class is initialized with the following parameters (in
-the given order, if positional arguments are used). All parameters are
-optional and can be set at any time prior to calling the ``send()`` method.
+The :class:`~django.core.mail.EmailMessage` class is initialized with the
+following parameters (in the given order, if positional arguments are used).
+All parameters are optional and can be set at any time prior to calling the
+``send()`` method.
 
     * ``subject``: The subject line of the e-mail.
 
@@ -227,7 +242,7 @@ optional and can be set at any time prior to calling the ``send()`` method.
     * ``bcc``: A list or tuple of addresses used in the "Bcc" header when
       sending the e-mail.
 
-    * ``connection``: An ``SMTPConnection`` instance. Use this parameter if
+    * ``connection``: An e-mail backend instance. Use this parameter if
       you want to use the same connection for multiple messages. If omitted, a
       new connection is created when ``send()`` is called.
 
@@ -248,18 +263,18 @@ For example::
 
 The class has the following methods:
 
-    * ``send(fail_silently=False)`` sends the message, using either
-      the connection that is specified in the ``connection``
-      attribute, or creating a new connection if none already
-      exists. If the keyword argument ``fail_silently`` is ``True``,
-      exceptions raised while sending the message will be quashed.
+    * ``send(fail_silently=False)`` sends the message. If a connection was
+      specified when the email was constructed, that connection will be used.
+      Otherwise, an instance of the default backend will be instantiated and
+      used. If the keyword argument ``fail_silently`` is ``True``, exceptions
+      raised while sending the message will be quashed.
 
     * ``message()`` constructs a ``django.core.mail.SafeMIMEText`` object (a
       subclass of Python's ``email.MIMEText.MIMEText`` class) or a
-      ``django.core.mail.SafeMIMEMultipart`` object holding the
-      message to be sent. If you ever need to extend the ``EmailMessage`` class,
-      you'll probably want to override this method to put the content you want
-      into the MIME object.
+      ``django.core.mail.SafeMIMEMultipart`` object holding the message to be
+      sent. If you ever need to extend the
+      :class:`~django.core.mail.EmailMessage` class, you'll probably want to
+      override this method to put the content you want into the MIME object.
 
     * ``recipients()`` returns a list of all the recipients of the message,
       whether they're recorded in the ``to`` or ``bcc`` attributes. This is
@@ -299,13 +314,13 @@ The class has the following methods:
 Sending alternative content types
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-It can be useful to include multiple versions of the content in an e-mail;
-the classic example is to send both text and HTML versions of a message. With
+It can be useful to include multiple versions of the content in an e-mail; the
+classic example is to send both text and HTML versions of a message. With
 Django's e-mail library, you can do this using the ``EmailMultiAlternatives``
-class. This subclass of ``EmailMessage`` has an ``attach_alternative()`` method
-for including extra versions of the message body in the e-mail. All the other
-methods (including the class initialization) are inherited directly from
-``EmailMessage``.
+class. This subclass of :class:`~django.core.mail.EmailMessage` has an
+``attach_alternative()`` method for including extra versions of the message
+body in the e-mail. All the other methods (including the class initialization)
+are inherited directly from :class:`~django.core.mail.EmailMessage`.
 
 To send a text and HTML combination, you could write::
 
@@ -318,41 +333,231 @@ To send a text and HTML combination, you could write::
     msg.attach_alternative(html_content, "text/html")
     msg.send()
 
-By default, the MIME type of the ``body`` parameter in an ``EmailMessage`` is
-``"text/plain"``. It is good practice to leave this alone, because it
-guarantees that any recipient will be able to read the e-mail, regardless of
-their mail client. However, if you are confident that your recipients can
-handle an alternative content type, you can use the ``content_subtype``
-attribute on the ``EmailMessage`` class to change the main content type. The
-major type will always be ``"text"``, but you can change it to the subtype. For
-example::
+By default, the MIME type of the ``body`` parameter in an
+:class:`~django.core.mail.EmailMessage` is ``"text/plain"``. It is good
+practice to leave this alone, because it guarantees that any recipient will be
+able to read the e-mail, regardless of their mail client. However, if you are
+confident that your recipients can handle an alternative content type, you can
+use the ``content_subtype`` attribute on the
+:class:`~django.core.mail.EmailMessage` class to change the main content type.
+The major type will always be ``"text"``, but you can change it to the
+subtype. For example::
 
     msg = EmailMessage(subject, html_content, from_email, [to])
     msg.content_subtype = "html"  # Main content is now text/html
     msg.send()
 
-SMTPConnection Objects
-----------------------
+.. _topic-email-backends:
 
-.. class:: SMTPConnection
+E-Mail Backends
+===============
+
+.. versionadded:: 1.2
+
+The actual sending of an e-mail is handled by the e-mail backend.
+
+The e-mail backend class has the following methods:
+
+    * ``open()`` instantiates an long-lived email-sending connection.
+
+    * ``close()`` closes the current email-sending connection.
+
+    * ``send_messages(email_messages)`` sends a list of
+      :class:`~django.core.mail.EmailMessage` objects. If the connection is
+      not open, this call will implicitly open the connection, and close the
+      connection afterwards. If the connection is already open, it will be
+      left open after mail has been sent.
+
+Obtaining an instance of an e-mail backend
+------------------------------------------
+
+The :meth:`get_connection` function in ``django.core.mail`` returns an
+instance of the e-mail backend that you can use.
+
+.. currentmodule:: django.core.mail
+
+.. function:: get_connection(backend=None, fail_silently=False, *args, **kwargs)
+
+By default, a call to ``get_connection()`` will return an instance of the
+email backend specified in :setting:`EMAIL_BACKEND`. If you specify the
+``backend`` argument, an instance of that backend will be instantiated.
+
+The ``fail_silently`` argument controls how the backend should handle errors.
+If ``fail_silently`` is True, exceptions during the email sending process
+will be silently ignored.
+
+All other arguments are passed directly to the constructor of the
+e-mail backend.
+
+Django ships with several e-mail sending backends. With the exception of the
+SMTP backend (which is the default), these backends are only useful during
+testing and development. If you have special email sending requirements, you
+can :ref:`write your own email backend <topic-custom-email-backend>`.
+
+.. _topic-email-smtp-backend:
+
+SMTP backend
+~~~~~~~~~~~~
+
+This is the default backend. E-mail will be sent through a SMTP server.
+The server address and authentication credentials are set in the
+:setting:`EMAIL_HOST`, :setting:`EMAIL_POST`, :setting:`EMAIL_HOST_USER`,
+:setting:`EMAIL_HOST_PASSWORD` and :setting:`EMAIL_USE_TLS` settings in your
+settings file.
+
+The SMTP backend is the default configuration inherited by Django. If you
+want to specify it explicitly, put the following in your settings::
+
+    EMAIL_BACKEND = 'django.core.mail.backends.smtp'
+
+.. admonition:: SMTPConnection objects
+
+    Prior to version 1.2, Django provided a
+    :class:`~django.core.mail.SMTPConnection` class. This class provided a way
+    to directly control the use of SMTP to send email. This class has been
+    deprecated in favor of the generic email backend API.
+
+    For backwards compatibility :class:`~django.core.mail.SMTPConnection` is
+    still available in ``django.core.mail`` as an alias for the SMTP backend.
+    New code should use :meth:`~django.core.mail.get_connection` instead.
+
+Console backend
+~~~~~~~~~~~~~~~
+
+Instead of sending out real e-mails the console backend just writes the
+e-mails that would be send to the standard output. By default, the console
+backend writes to ``stdout``. You can use a different stream-like object by
+providing the ``stream`` keyword argument when constructing the connection.
+
+To specify this backend, put the following in your settings::
+
+    EMAIL_BACKEND = 'django.core.mail.backends.console'
+
+This backend is not intended for use in production -- it is provided as a
+convenience that can be used during development.
+
+File backend
+~~~~~~~~~~~~
+
+The file backend writes e-mails to a file. A new file is created for each new
+session that is opened on this backend. The directory to which the files are
+written is either taken from the :setting:`EMAIL_FILE_PATH` setting or from
+the ``file_path`` keyword when creating a connection with
+:meth:`~django.core.mail.get_connection`.
+
+To specify this backend, put the following in your settings::
+
+    EMAIL_BACKEND = 'django.core.mail.backends.filebased'
+    EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location
+
+This backend is not intended for use in production -- it is provided as a
+convenience that can be used during development.
+
+In-memory backend
+~~~~~~~~~~~~~~~~~
 
-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.
+The ``'locmem'`` backend stores messages in a special attribute of the
+``django.core.mail`` module. The ``outbox`` attribute is created when the
+first message is send. It's a list with an
+:class:`~django.core.mail.EmailMessage` instance for each message that would
+be send.
 
-If you're sending lots of messages at once, the ``send_messages()`` method of
-the ``SMTPConnection`` class is useful. It takes a list of ``EmailMessage``
-instances (or subclasses) 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 e-mail you wish to
-send out, you could send this with::
+To specify this backend, put the following in your settings::
 
-    connection = SMTPConnection()   # Use default settings for connection
+  EMAIL_BACKEND = 'django.core.mail.backends.locmem'
+
+This backend is not intended for use in production -- it is provided as a
+convenience that can be used during development and testing.
+
+Dummy backend
+~~~~~~~~~~~~~
+
+As the name suggests the dummy backend does nothing with your messages. To
+specify this backend, put the following in your settings::
+
+   EMAIL_BACKEND = 'django.core.mail.backends.dummy'
+
+This backend is not intended for use in production -- it is provided as a
+convenience that can be used during development.
+
+.. _topic-custom-email-backend:
+
+Defining a custom e-mail backend
+--------------------------------
+
+If you need to change how e-mails are send you can write your own e-mail
+backend. The ``EMAIL_BACKEND`` setting in your settings file is then the
+Python import path for your backend.
+
+Custom e-mail backends should subclass ``BaseEmailBackend`` that is located in
+the ``django.core.mail.backends.base`` module. A custom e-mail backend must
+implement the ``send_messages(email_messages)`` method. This method receives a
+list of :class:`~django.core.mail.EmailMessage` instances and returns the
+number of successfully delivered messages. If your backend has any concept of
+a persistent session or connection, you should also implement the ``open()``
+and ``close()`` methods. Refer to ``SMTPEmailBackend`` for a reference
+implementation.
+
+.. _topics-sending-multiple-emails:
+
+Sending multiple emails
+-----------------------
+
+Establishing and closing an SMTP connection (or any other network connection,
+for that matter) is an expensive process. If you have a lot of emails to send,
+it makes sense to reuse an SMTP connection, rather than creating and
+destroying a connection every time you want to send an email.
+
+There are two ways you tell an email backend to reuse a connection.
+
+Firstly, you can use the ``send_messages()`` method. ``send_messages()`` takes
+a list of :class:`~django.core.mail.EmailMessage` instances (or subclasses),
+and sends them all using a single connection.
+
+For example, if you have a function called ``get_notification_email()`` that
+returns a list of :class:`~django.core.mail.EmailMessage` objects representing
+some periodic e-mail you wish to send out, you could send these emails using
+a single call to send_messages::
+
+    from django.core import mail
+    connection = mail.get_connection()   # Use default email connection
     messages = get_notification_email()
     connection.send_messages(messages)
 
+In this example, the call to ``send_messages()`` opens a connection on the
+backend, sends the list of messages, and then closes the connection again.
+
+The second approach is to use the ``open()`` and ``close()`` methods on the
+email backend to manually control the connection. ``send_messages()`` will not
+manually open or close the connection if it is already open, so if you
+manually open the connection, you can control when it is closed. For example::
+
+    from django.core import mail
+    connection = mail.get_connection()
+
+    # Manually open the connection
+    connection.open()
+
+    # Construct an email message that uses the connection
+    email1 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
+                              ['to1@example.com'], connection=connection)
+    email1.send() # Send the email
+
+    # Construct two more messages
+    email2 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
+                              ['to2@example.com'])
+    email3 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
+                              ['to3@example.com'])
+
+    # Send the two emails in a single call -
+    connection.send_messages([email2, email3])
+    # The connection was already open so send_messages() doesn't close it.
+    # We need to manually close the connection.
+    connection.close()
+
+
 Testing e-mail sending
-----------------------
+======================
 
 The are times when you do not want Django to send e-mails at all. For example,
 while developing a website, you probably don't want to send out thousands of
@@ -360,19 +565,41 @@ e-mails -- but you may want to validate that e-mails will be sent to the right
 people under the right conditions, and that those e-mails will contain the
 correct content.
 
-The easiest way to test your project's use of e-mail is to use a "dumb" e-mail
-server that receives the e-mails locally and displays them to the terminal,
-but does not actually send anything. Python has a built-in way to accomplish
-this with a single command::
+The easiest way to test your project's use of e-mail is to use the ``console``
+email backend. This backend redirects all email to stdout, allowing you to
+inspect the content of mail.
+
+The ``file`` email backend can also be useful during development -- this backend
+dumps the contents of every SMTP connection to a file that can be inspected
+at your leisure.
+
+Another approach is to use a "dumb" SMTP server that receives the e-mails
+locally and displays them to the terminal, but does not actually send
+anything. Python has a built-in way to accomplish this with a single command::
 
     python -m smtpd -n -c DebuggingServer localhost:1025
 
 This command will start a simple SMTP server listening on port 1025 of
-localhost. This server simply prints to standard output all email headers and
-the email body. You then only need to set the :setting:`EMAIL_HOST` and
+localhost. This server simply prints to standard output all e-mail headers and
+the e-mail body. You then only need to set the :setting:`EMAIL_HOST` and
 :setting:`EMAIL_PORT` accordingly, and you are set.
 
-For more entailed testing and processing of e-mails locally, see the Python
-documentation on the `SMTP Server`_.
+For a more detailed discussion of testing and processing of e-mails locally,
+see the Python documentation on the `SMTP Server`_.
 
 .. _SMTP Server: http://docs.python.org/library/smtpd.html
+
+SMTPConnection
+==============
+
+.. class:: SMTPConnection
+
+.. deprecated:: 1.2
+
+The ``SMTPConnection`` class has been deprecated in favor of the generic email
+backend API.
+
+For backwards compatibility ``SMTPConnection`` is still available in
+``django.core.mail`` as an alias for the :ref:`SMTP backend
+<topic-email-smtp-backend>`. New code should use
+:meth:`~django.core.mail.get_connection` instead.

+ 4 - 8
docs/topics/testing.txt

@@ -1104,6 +1104,8 @@ applications:
     ``target_status_code`` will be the url and status code for the final
     point of the redirect chain.
 
+.. _topics-testing-email:
+
 E-mail services
 ---------------
 
@@ -1117,7 +1119,7 @@ test every aspect of sending e-mail -- from the number of messages sent to the
 contents of each message -- without actually sending the messages.
 
 The test runner accomplishes this by transparently replacing the normal
-:class:`~django.core.mail.SMTPConnection` class with a different version.
+email backend with a testing backend.
 (Don't worry -- this has no effect on any other e-mail senders outside of
 Django, such as your machine's mail server, if you're running one.)
 
@@ -1128,14 +1130,8 @@ Django, such as your machine's mail server, if you're running one.)
 During test running, each outgoing e-mail is saved in
 ``django.core.mail.outbox``. This is a simple list of all
 :class:`~django.core.mail.EmailMessage` instances that have been sent.
-It does not exist under normal execution conditions, i.e., when you're not
-running unit tests. The outbox is created during test setup, along with the
-dummy :class:`~django.core.mail.SMTPConnection`. When the test framework is
-torn down, the standard :class:`~django.core.mail.SMTPConnection` class is
-restored, and the test outbox is destroyed.
-
 The ``outbox`` attribute is a special attribute that is created *only* when
-the tests are run. It doesn't normally exist as part of the
+the ``locmem`` e-mail backend is used. It doesn't normally exist as part of the
 :mod:`django.core.mail` module and you can't import it directly. The code
 below shows how to access this attribute correctly.
 

+ 15 - 0
tests/regressiontests/mail/custombackend.py

@@ -0,0 +1,15 @@
+"""A custom backend for testing."""
+
+from django.core.mail.backends.base import BaseEmailBackend
+
+
+class EmailBackend(BaseEmailBackend):
+
+    def __init__(self, *args, **kwargs):
+        super(EmailBackend, self).__init__(*args, **kwargs)
+        self.test_outbox = []
+
+    def send_messages(self, email_messages):
+        # Messages are stored in a instance variable for testing.
+        self.test_outbox.extend(email_messages)
+        return len(email_messages)

+ 221 - 2
tests/regressiontests/mail/tests.py

@@ -1,10 +1,18 @@
 # coding: utf-8
+
 r"""
 # Tests for the django.core.mail.
 
+>>> import os
+>>> import shutil
+>>> import tempfile
+>>> from StringIO import StringIO
 >>> from django.conf import settings
 >>> from django.core import mail
 >>> from django.core.mail import EmailMessage, mail_admins, mail_managers, EmailMultiAlternatives
+>>> from django.core.mail import send_mail, send_mass_mail
+>>> from django.core.mail.backends.base import BaseEmailBackend
+>>> from django.core.mail.backends import console, dummy, locmem, filebased, smtp
 >>> from django.utils.translation import ugettext_lazy
 
 # Test normal ascii character case:
@@ -85,8 +93,6 @@ BadHeaderError: Header values can't contain newlines (got u'Subject\nInjection T
 >>> mail_managers('hi','there')
 >>> len(mail.outbox)
 1
->>> settings.ADMINS = old_admins
->>> settings.MANAGERS = old_managers
 
 # Make sure we can manually set the From header (#9214)
 
@@ -138,4 +144,217 @@ Content-Disposition: attachment; filename="an attachment.pdf"
 JVBERi0xLjQuJS4uLg==
 ...
 
+# Make sure that the console backend writes to stdout by default
+>>> connection = console.EmailBackend()
+>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> connection.send_messages([email])
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject
+From: from@example.com
+To: to@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+1
+
+# Test that the console backend can be pointed at an arbitrary stream
+>>> s = StringIO()
+>>> connection = mail.get_connection('django.core.mail.backends.console', stream=s)
+>>> send_mail('Subject', 'Content', 'from@example.com', ['to@example.com'], connection=connection)
+1
+>>> print s.getvalue()
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject
+From: from@example.com
+To: to@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+
+# Make sure that dummy backends returns correct number of sent messages
+>>> connection = dummy.EmailBackend()
+>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> connection.send_messages([email, email, email])
+3
+
+# Make sure that locmen backend populates the outbox
+>>> mail.outbox = []
+>>> connection = locmem.EmailBackend()
+>>> email1 = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> email2 = EmailMessage('Subject 2', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> connection.send_messages([email1, email2])
+2
+>>> len(mail.outbox)
+2
+>>> mail.outbox[0].subject
+'Subject'
+>>> mail.outbox[1].subject
+'Subject 2'
+
+# Make sure that multiple locmem connections share mail.outbox
+>>> mail.outbox = []
+>>> connection1 = locmem.EmailBackend()
+>>> connection2 = locmem.EmailBackend()
+>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> connection1.send_messages([email])
+1
+>>> connection2.send_messages([email])
+1
+>>> len(mail.outbox)
+2
+
+# Make sure that the file backend write to the right location
+>>> tmp_dir = tempfile.mkdtemp()
+>>> connection = filebased.EmailBackend(file_path=tmp_dir)
+>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> connection.send_messages([email])
+1
+>>> len(os.listdir(tmp_dir))
+1
+>>> print open(os.path.join(tmp_dir, os.listdir(tmp_dir)[0])).read()
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject
+From: from@example.com
+To: to@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+
+>>> connection2 = filebased.EmailBackend(file_path=tmp_dir)
+>>> connection2.send_messages([email])
+1
+>>> len(os.listdir(tmp_dir))
+2
+>>> connection.send_messages([email])
+1
+>>> len(os.listdir(tmp_dir))
+2
+>>> email.connection = filebased.EmailBackend(file_path=tmp_dir)
+>>> connection_created = connection.open()
+>>> num_sent = email.send()
+>>> len(os.listdir(tmp_dir))
+3
+>>> num_sent = email.send()
+>>> len(os.listdir(tmp_dir))
+3
+>>> connection.close()
+>>> shutil.rmtree(tmp_dir)
+
+# Make sure that get_connection() accepts arbitrary keyword that might be
+# used with custom backends.
+>>> c = mail.get_connection(fail_silently=True, foo='bar')
+>>> c.fail_silently
+True
+
+# Test custom backend defined in this suite.
+>>> conn = mail.get_connection('regressiontests.mail.custombackend')
+>>> hasattr(conn, 'test_outbox')
+True
+>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
+>>> conn.send_messages([email])
+1
+>>> len(conn.test_outbox)
+1
+
+# Test backend argument of mail.get_connection()
+>>> isinstance(mail.get_connection('django.core.mail.backends.smtp'), smtp.EmailBackend)
+True
+>>> isinstance(mail.get_connection('django.core.mail.backends.locmem'), locmem.EmailBackend)
+True
+>>> isinstance(mail.get_connection('django.core.mail.backends.dummy'), dummy.EmailBackend)
+True
+>>> isinstance(mail.get_connection('django.core.mail.backends.console'), console.EmailBackend)
+True
+>>> tmp_dir = tempfile.mkdtemp()
+>>> isinstance(mail.get_connection('django.core.mail.backends.filebased', file_path=tmp_dir), filebased.EmailBackend)
+True
+>>> shutil.rmtree(tmp_dir)
+>>> isinstance(mail.get_connection(), locmem.EmailBackend)
+True
+
+# Test connection argument of send_mail() et al
+>>> connection = mail.get_connection('django.core.mail.backends.console')
+>>> send_mail('Subject', 'Content', 'from@example.com', ['to@example.com'], connection=connection)
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject
+From: from@example.com
+To: to@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+1
+
+>>> send_mass_mail([
+...         ('Subject1', 'Content1', 'from1@example.com', ['to1@example.com']),
+...         ('Subject2', 'Content2', 'from2@example.com', ['to2@example.com'])
+...     ], connection=connection)
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject1
+From: from1@example.com
+To: to1@example.com
+Date: ...
+Message-ID: ...
+
+Content1
+-------------------------------------------------------------------------------
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: Subject2
+From: from2@example.com
+To: to2@example.com
+Date: ...
+Message-ID: ...
+
+Content2
+-------------------------------------------------------------------------------
+2
+
+>>> mail_admins('Subject', 'Content', connection=connection)
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: [Django] Subject
+From: root@localhost
+To: nobody@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+
+>>> mail_managers('Subject', 'Content', connection=connection)
+Content-Type: text/plain; charset="utf-8"
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Subject: [Django] Subject
+From: root@localhost
+To: nobody@example.com
+Date: ...
+Message-ID: ...
+
+Content
+-------------------------------------------------------------------------------
+
+>>> settings.ADMINS = old_admins
+>>> settings.MANAGERS = old_managers
+
 """