Ticket #10355: t10355-r11706.diff

File t10355-r11706.diff, 72.1 KB (added by russellm, 5 years ago)

RC1 of email backend patch

  • django/conf/global_settings.py

    diff -r 230a12f723d8 django/conf/global_settings.py
    a b  
    131131DATABASE_PORT = ''             # Set to empty string for default. Not used with sqlite3.
    132132DATABASE_OPTIONS = {}          # Set to empty dictionary for default.
    133133
     134# The email backend to use. For possible shortcuts see django.core.mail.
     135# The default is to use the 'smtp' backend for sending emails using a
     136# SMTP server.
     137# Third-party backends can be specified by providing a Python path
     138# to a module that defines an EmailBackend class.
     139EMAIL_BACKEND = 'smtp'
     140
    134141# Host for sending e-mail.
    135142EMAIL_HOST = 'localhost'
    136143
  • deleted file django/core/mail.py

    diff -r 230a12f723d8 django/core/mail.py
    + -  
    1 """
    2 Tools for sending email.
    3 """
    4 
    5 import mimetypes
    6 import os
    7 import smtplib
    8 import socket
    9 import time
    10 import random
    11 from email import Charset, Encoders
    12 from email.MIMEText import MIMEText
    13 from email.MIMEMultipart import MIMEMultipart
    14 from email.MIMEBase import MIMEBase
    15 from email.Header import Header
    16 from email.Utils import formatdate, parseaddr, formataddr
    17 
    18 from django.conf import settings
    19 from django.utils.encoding import smart_str, force_unicode
    20 
    21 # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
    22 # some spam filters.
    23 Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
    24 
    25 # Default MIME type to use on attachments (if it is not explicitly given
    26 # and cannot be guessed).
    27 DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream'
    28 
    29 # Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of
    30 # seconds, which slows down the restart of the server.
    31 class CachedDnsName(object):
    32     def __str__(self):
    33         return self.get_fqdn()
    34 
    35     def get_fqdn(self):
    36         if not hasattr(self, '_fqdn'):
    37             self._fqdn = socket.getfqdn()
    38         return self._fqdn
    39 
    40 DNS_NAME = CachedDnsName()
    41 
    42 # Copied from Python standard library, with the following modifications:
    43 # * Used cached hostname for performance.
    44 # * Added try/except to support lack of getpid() in Jython (#5496).
    45 def make_msgid(idstring=None):
    46     """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
    47 
    48     <20020201195627.33539.96671@nightshade.la.mastaler.com>
    49 
    50     Optional idstring if given is a string used to strengthen the
    51     uniqueness of the message id.
    52     """
    53     timeval = time.time()
    54     utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
    55     try:
    56         pid = os.getpid()
    57     except AttributeError:
    58         # No getpid() in Jython, for example.
    59         pid = 1
    60     randint = random.randrange(100000)
    61     if idstring is None:
    62         idstring = ''
    63     else:
    64         idstring = '.' + idstring
    65     idhost = DNS_NAME
    66     msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
    67     return msgid
    68 
    69 class BadHeaderError(ValueError):
    70     pass
    71 
    72 def forbid_multi_line_headers(name, val):
    73     """Forbids multi-line headers, to prevent header injection."""
    74     val = force_unicode(val)
    75     if '\n' in val or '\r' in val:
    76         raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name))
    77     try:
    78         val = val.encode('ascii')
    79     except UnicodeEncodeError:
    80         if name.lower() in ('to', 'from', 'cc'):
    81             result = []
    82             for item in val.split(', '):
    83                 nm, addr = parseaddr(item)
    84                 nm = str(Header(nm, settings.DEFAULT_CHARSET))
    85                 result.append(formataddr((nm, str(addr))))
    86             val = ', '.join(result)
    87         else:
    88             val = Header(val, settings.DEFAULT_CHARSET)
    89     else:
    90         if name.lower() == 'subject':
    91             val = Header(val)
    92     return name, val
    93 
    94 class SafeMIMEText(MIMEText):
    95     def __setitem__(self, name, val):
    96         name, val = forbid_multi_line_headers(name, val)
    97         MIMEText.__setitem__(self, name, val)
    98 
    99 class SafeMIMEMultipart(MIMEMultipart):
    100     def __setitem__(self, name, val):
    101         name, val = forbid_multi_line_headers(name, val)
    102         MIMEMultipart.__setitem__(self, name, val)
    103 
    104 class SMTPConnection(object):
    105     """
    106     A wrapper that manages the SMTP network connection.
    107     """
    108 
    109     def __init__(self, host=None, port=None, username=None, password=None,
    110                  use_tls=None, fail_silently=False):
    111         self.host = host or settings.EMAIL_HOST
    112         self.port = port or settings.EMAIL_PORT
    113         self.username = username or settings.EMAIL_HOST_USER
    114         self.password = password or settings.EMAIL_HOST_PASSWORD
    115         self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS
    116         self.fail_silently = fail_silently
    117         self.connection = None
    118 
    119     def open(self):
    120         """
    121         Ensures we have a connection to the email server. Returns whether or
    122         not a new connection was required (True or False).
    123         """
    124         if self.connection:
    125             # Nothing to do if the connection is already open.
    126             return False
    127         try:
    128             # If local_hostname is not specified, socket.getfqdn() gets used.
    129             # For performance, we use the cached FQDN for local_hostname.
    130             self.connection = smtplib.SMTP(self.host, self.port,
    131                                            local_hostname=DNS_NAME.get_fqdn())
    132             if self.use_tls:
    133                 self.connection.ehlo()
    134                 self.connection.starttls()
    135                 self.connection.ehlo()
    136             if self.username and self.password:
    137                 self.connection.login(self.username, self.password)
    138             return True
    139         except:
    140             if not self.fail_silently:
    141                 raise
    142 
    143     def close(self):
    144         """Closes the connection to the email server."""
    145         try:
    146             try:
    147                 self.connection.quit()
    148             except socket.sslerror:
    149                 # This happens when calling quit() on a TLS connection
    150                 # sometimes.
    151                 self.connection.close()
    152             except:
    153                 if self.fail_silently:
    154                     return
    155                 raise
    156         finally:
    157             self.connection = None
    158 
    159     def send_messages(self, email_messages):
    160         """
    161         Sends one or more EmailMessage objects and returns the number of email
    162         messages sent.
    163         """
    164         if not email_messages:
    165             return
    166         new_conn_created = self.open()
    167         if not self.connection:
    168             # We failed silently on open(). Trying to send would be pointless.
    169             return
    170         num_sent = 0
    171         for message in email_messages:
    172             sent = self._send(message)
    173             if sent:
    174                 num_sent += 1
    175         if new_conn_created:
    176             self.close()
    177         return num_sent
    178 
    179     def _send(self, email_message):
    180         """A helper method that does the actual sending."""
    181         if not email_message.recipients():
    182             return False
    183         try:
    184             self.connection.sendmail(email_message.from_email,
    185                     email_message.recipients(),
    186                     email_message.message().as_string())
    187         except:
    188             if not self.fail_silently:
    189                 raise
    190             return False
    191         return True
    192 
    193 class EmailMessage(object):
    194     """
    195     A container for email information.
    196     """
    197     content_subtype = 'plain'
    198     mixed_subtype = 'mixed'
    199     encoding = None     # None => use settings default
    200 
    201     def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
    202             connection=None, attachments=None, headers=None):
    203         """
    204         Initialize a single email message (which can be sent to multiple
    205         recipients).
    206 
    207         All strings used to create the message can be unicode strings (or UTF-8
    208         bytestrings). The SafeMIMEText class will handle any necessary encoding
    209         conversions.
    210         """
    211         if to:
    212             assert not isinstance(to, basestring), '"to" argument must be a list or tuple'
    213             self.to = list(to)
    214         else:
    215             self.to = []
    216         if bcc:
    217             assert not isinstance(bcc, basestring), '"bcc" argument must be a list or tuple'
    218             self.bcc = list(bcc)
    219         else:
    220             self.bcc = []
    221         self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
    222         self.subject = subject
    223         self.body = body
    224         self.attachments = attachments or []
    225         self.extra_headers = headers or {}
    226         self.connection = connection
    227 
    228     def get_connection(self, fail_silently=False):
    229         if not self.connection:
    230             self.connection = SMTPConnection(fail_silently=fail_silently)
    231         return self.connection
    232 
    233     def message(self):
    234         encoding = self.encoding or settings.DEFAULT_CHARSET
    235         msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),
    236                            self.content_subtype, encoding)
    237         msg = self._create_message(msg)
    238         msg['Subject'] = self.subject
    239         msg['From'] = self.extra_headers.pop('From', self.from_email)
    240         msg['To'] = ', '.join(self.to)
    241 
    242         # Email header names are case-insensitive (RFC 2045), so we have to
    243         # accommodate that when doing comparisons.
    244         header_names = [key.lower() for key in self.extra_headers]
    245         if 'date' not in header_names:
    246             msg['Date'] = formatdate()
    247         if 'message-id' not in header_names:
    248             msg['Message-ID'] = make_msgid()
    249         for name, value in self.extra_headers.items():
    250             msg[name] = value
    251         return msg
    252 
    253     def recipients(self):
    254         """
    255         Returns a list of all recipients of the email (includes direct
    256         addressees as well as Bcc entries).
    257         """
    258         return self.to + self.bcc
    259 
    260     def send(self, fail_silently=False):
    261         """Sends the email message."""
    262         if not self.recipients():
    263             # Don't bother creating the network connection if there's nobody to
    264             # send to.
    265             return 0
    266         return self.get_connection(fail_silently).send_messages([self])
    267 
    268     def attach(self, filename=None, content=None, mimetype=None):
    269         """
    270         Attaches a file with the given filename and content. The filename can
    271         be omitted and the mimetype is guessed, if not provided.
    272 
    273         If the first parameter is a MIMEBase subclass it is inserted directly
    274         into the resulting message attachments.
    275         """
    276         if isinstance(filename, MIMEBase):
    277             assert content == mimetype == None
    278             self.attachments.append(filename)
    279         else:
    280             assert content is not None
    281             self.attachments.append((filename, content, mimetype))
    282 
    283     def attach_file(self, path, mimetype=None):
    284         """Attaches a file from the filesystem."""
    285         filename = os.path.basename(path)
    286         content = open(path, 'rb').read()
    287         self.attach(filename, content, mimetype)
    288 
    289     def _create_message(self, msg):
    290         return self._create_attachments(msg)
    291 
    292     def _create_attachments(self, msg):
    293         if self.attachments:
    294             body_msg = msg
    295             msg = SafeMIMEMultipart(_subtype=self.mixed_subtype)
    296             if self.body:
    297                 msg.attach(body_msg)
    298             for attachment in self.attachments:
    299                 if isinstance(attachment, MIMEBase):
    300                     msg.attach(attachment)
    301                 else:
    302                     msg.attach(self._create_attachment(*attachment))
    303         return msg
    304 
    305     def _create_mime_attachment(self, content, mimetype):
    306         """
    307         Converts the content, mimetype pair into a MIME attachment object.
    308         """
    309         basetype, subtype = mimetype.split('/', 1)
    310         if basetype == 'text':
    311             attachment = SafeMIMEText(smart_str(content,
    312                 settings.DEFAULT_CHARSET), subtype, settings.DEFAULT_CHARSET)
    313         else:
    314             # Encode non-text attachments with base64.
    315             attachment = MIMEBase(basetype, subtype)
    316             attachment.set_payload(content)
    317             Encoders.encode_base64(attachment)
    318         return attachment
    319 
    320     def _create_attachment(self, filename, content, mimetype=None):
    321         """
    322         Converts the filename, content, mimetype triple into a MIME attachment
    323         object.
    324         """
    325         if mimetype is None:
    326             mimetype, _ = mimetypes.guess_type(filename)
    327             if mimetype is None:
    328                 mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
    329         attachment = self._create_mime_attachment(content, mimetype)
    330         if filename:
    331             attachment.add_header('Content-Disposition', 'attachment',
    332                                   filename=filename)
    333         return attachment
    334 
    335 class EmailMultiAlternatives(EmailMessage):
    336     """
    337     A version of EmailMessage that makes it easy to send multipart/alternative
    338     messages. For example, including text and HTML versions of the text is
    339     made easier.
    340     """
    341     alternative_subtype = 'alternative'
    342 
    343     def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
    344             connection=None, attachments=None, headers=None, alternatives=None):
    345         """
    346         Initialize a single email message (which can be sent to multiple
    347         recipients).
    348 
    349         All strings used to create the message can be unicode strings (or UTF-8
    350         bytestrings). The SafeMIMEText class will handle any necessary encoding
    351         conversions.
    352         """
    353         super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers)
    354         self.alternatives=alternatives or []
    355 
    356     def attach_alternative(self, content, mimetype):
    357         """Attach an alternative content representation."""
    358         assert content is not None
    359         assert mimetype is not None
    360         self.alternatives.append((content, mimetype))
    361 
    362     def _create_message(self, msg):
    363         return self._create_attachments(self._create_alternatives(msg))
    364 
    365     def _create_alternatives(self, msg):
    366         if self.alternatives:
    367             body_msg = msg
    368             msg = SafeMIMEMultipart(_subtype=self.alternative_subtype)
    369             if self.body:
    370                 msg.attach(body_msg)
    371             for alternative in self.alternatives:
    372                 msg.attach(self._create_mime_attachment(*alternative))
    373         return msg
    374 
    375 def send_mail(subject, message, from_email, recipient_list,
    376               fail_silently=False, auth_user=None, auth_password=None):
    377     """
    378     Easy wrapper for sending a single message to a recipient list. All members
    379     of the recipient list will see the other recipients in the 'To' field.
    380 
    381     If auth_user is None, the EMAIL_HOST_USER setting is used.
    382     If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
    383 
    384     Note: The API for this method is frozen. New code wanting to extend the
    385     functionality should use the EmailMessage class directly.
    386     """
    387     connection = SMTPConnection(username=auth_user, password=auth_password,
    388                                 fail_silently=fail_silently)
    389     return EmailMessage(subject, message, from_email, recipient_list,
    390                         connection=connection).send()
    391 
    392 def send_mass_mail(datatuple, fail_silently=False, auth_user=None,
    393                    auth_password=None):
    394     """
    395     Given a datatuple of (subject, message, from_email, recipient_list), sends
    396     each message to each recipient list. Returns the number of e-mails sent.
    397 
    398     If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
    399     If auth_user and auth_password are set, they're used to log in.
    400     If auth_user is None, the EMAIL_HOST_USER setting is used.
    401     If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
    402 
    403     Note: The API for this method is frozen. New code wanting to extend the
    404     functionality should use the EmailMessage class directly.
    405     """
    406     connection = SMTPConnection(username=auth_user, password=auth_password,
    407                                 fail_silently=fail_silently)
    408     messages = [EmailMessage(subject, message, sender, recipient)
    409                 for subject, message, sender, recipient in datatuple]
    410     return connection.send_messages(messages)
    411 
    412 def mail_admins(subject, message, fail_silently=False):
    413     """Sends a message to the admins, as defined by the ADMINS setting."""
    414     if not settings.ADMINS:
    415         return
    416     EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
    417                  settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS]
    418                  ).send(fail_silently=fail_silently)
    419 
    420 def mail_managers(subject, message, fail_silently=False):
    421     """Sends a message to the managers, as defined by the MANAGERS setting."""
    422     if not settings.MANAGERS:
    423         return
    424     EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
    425                  settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS]
    426                  ).send(fail_silently=fail_silently)
  • new file django/core/mail/__init__.py

    diff -r 230a12f723d8 django/core/mail/__init__.py
    - +  
     1"""
     2Tools for sending email.
     3"""
     4
     5from django.conf import settings
     6from django.core.exceptions import ImproperlyConfigured
     7from django.utils.importlib import import_module
     8
     9# Imported for backwards compatibility, and for the sake
     10# of a cleaner namespace. These symbols used to be in
     11# django/core/mail.py before the introduction of email
     12# backends and the subsequent reorganization (See #10355)
     13from django.core.mail.utils import CachedDnsName, DNS_NAME
     14from django.core.mail.message import \
     15    EmailMessage, EmailMultiAlternatives, \
     16    SafeMIMEText, SafeMIMEMultipart, \
     17    DEFAULT_ATTACHMENT_MIME_TYPE, make_msgid, \
     18    BadHeaderError, forbid_multi_line_headers
     19from django.core.mail.backends.smtp import EmailBackend as _SMTPConnection
     20
     21# Shortcuts for builtin email backends. The dictionary maps backend names
     22# to module names. If settings.EMAIL_BACKEND isn't in this dictionary
     23# it's treated as a Python import path.
     24BACKENDS = {
     25    'console': 'console',
     26    'dummy': 'dummy',
     27    'file': 'filebased',
     28    'locmem': 'locmem',
     29    'smtp': 'smtp',
     30}
     31
     32def get_connection(backend=None, fail_silently=False, **kwds):
     33    """Load an e-mail backend and return an instance of it.
     34
     35    If backend is None (default) settings.EMAIL_BACKEND is used.
     36
     37    Both fail_silently and other keyword arguments are used in the
     38    constructor of the backend.
     39    """
     40    path = backend or settings.EMAIL_BACKEND
     41    if path in BACKENDS:
     42        path = 'django.core.mail.backends.%s' % BACKENDS.get(path)
     43    try:
     44        mod = import_module(path)
     45    except ImportError, e:
     46        raise ImproperlyConfigured(('Error importing email backend %s: "%s"'
     47                                    % (path, e)))
     48    try:
     49        cls = getattr(mod, 'EmailBackend')
     50    except AttributeError:
     51        raise ImproperlyConfigured(('Module "%s" does not define a '
     52                                    '"EmailBackend" class' % path))
     53    return cls(fail_silently=fail_silently, **kwds)
     54
     55
     56def send_mail(subject, message, from_email, recipient_list,
     57              fail_silently=False, auth_user=None, auth_password=None):
     58    """
     59    Easy wrapper for sending a single message to a recipient list. All members
     60    of the recipient list will see the other recipients in the 'To' field.
     61
     62    If auth_user is None, the EMAIL_HOST_USER setting is used.
     63    If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
     64
     65    Note: The API for this method is frozen. New code wanting to extend the
     66    functionality should use the EmailMessage class directly.
     67    """
     68    connection = get_connection(username=auth_user, password=auth_password,
     69                                fail_silently=fail_silently)
     70    return EmailMessage(subject, message, from_email, recipient_list,
     71                        connection=connection).send()
     72
     73
     74def send_mass_mail(datatuple, fail_silently=False, auth_user=None,
     75                   auth_password=None):
     76    """
     77    Given a datatuple of (subject, message, from_email, recipient_list), sends
     78    each message to each recipient list. Returns the number of e-mails sent.
     79
     80    If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
     81    If auth_user and auth_password are set, they're used to log in.
     82    If auth_user is None, the EMAIL_HOST_USER setting is used.
     83    If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
     84
     85    Note: The API for this method is frozen. New code wanting to extend the
     86    functionality should use the EmailMessage class directly.
     87    """
     88    connection = get_connection(username=auth_user, password=auth_password,
     89                                fail_silently=fail_silently)
     90    messages = [EmailMessage(subject, message, sender, recipient)
     91                for subject, message, sender, recipient in datatuple]
     92    return connection.send_messages(messages)
     93
     94
     95def mail_admins(subject, message, fail_silently=False):
     96    """Sends a message to the admins, as defined by the ADMINS setting."""
     97    if not settings.ADMINS:
     98        return
     99    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
     100                 settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS]
     101                 ).send(fail_silently=fail_silently)
     102
     103
     104def mail_managers(subject, message, fail_silently=False):
     105    """Sends a message to the managers, as defined by the MANAGERS setting."""
     106    if not settings.MANAGERS:
     107        return
     108    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
     109                 settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS]
     110                 ).send(fail_silently=fail_silently)
     111
     112
     113class SMTPConnection(_SMTPConnection):
     114    def __init__(self, *args, **kwds):
     115        import warnings
     116        warnings.warn(
     117            'mail.SMTPConnection is deprectated; use mail.get_connection() instead.',
     118            DeprecationWarning
     119        )
     120        super(SMTPConnection, self).__init__(*args, **kwds)
  • new file django/core/mail/backends/__init__.py

    diff -r 230a12f723d8 django/core/mail/backends/__init__.py
    - +  
     1# Mail backends shipped with Django.
  • new file django/core/mail/backends/base.py

    diff -r 230a12f723d8 django/core/mail/backends/base.py
    - +  
     1"""Base email backend class."""
     2
     3
     4class BaseEmailBackend(object):
     5    """
     6    Base class for email backend implementations.
     7
     8    Subclasses must at least overwrite send_messages().
     9    """
     10
     11    def __init__(self, fail_silently=False, **kwargs):
     12        self.fail_silently = fail_silently
     13
     14    def open(self):
     15        """Open a network connection.
     16
     17        This method can be overwritten by backend implementations to
     18        open a network connection.
     19
     20        It's up to the backend implementation to track the status of
     21        a network connection if it's needed by the backend.
     22
     23        This method can be called by applications to force a single
     24        network connection to be used when sending mails. See the
     25        send_messages() method of the SMTP backend for a reference
     26        implementation.
     27
     28        The default implementation does nothing.
     29        """
     30        pass
     31
     32    def close(self):
     33        """Close a network connection."""
     34        pass
     35
     36    def send_messages(self, email_messages):
     37        """
     38        Sends one or more EmailMessage objects and returns the number of email
     39        messages sent.
     40        """
     41        raise NotImplementedError
  • new file django/core/mail/backends/console.py

    diff -r 230a12f723d8 django/core/mail/backends/console.py
    - +  
     1"""
     2Email backend that writes messages to console instead of sending them.
     3"""
     4
     5import sys
     6
     7from django.core.mail.backends.base import BaseEmailBackend
     8
     9
     10class EmailBackend(BaseEmailBackend):
     11
     12    def send_messages(self, email_messages):
     13        for message in email_messages:
     14            sys.stdout.write('%s\n' % message.message().as_string())
     15        return len(email_messages)
  • new file django/core/mail/backends/dummy.py

    diff -r 230a12f723d8 django/core/mail/backends/dummy.py
    - +  
     1"""
     2Dummy email backend that does nothing.
     3"""
     4
     5
     6from django.core.mail.backends.base import BaseEmailBackend
     7
     8
     9class EmailBackend(BaseEmailBackend):
     10
     11    def send_messages(self, email_messages):
     12        return len(email_messages)
  • new file django/core/mail/backends/filebased.py

    diff -r 230a12f723d8 django/core/mail/backends/filebased.py
    - +  
     1"""Email backend that writes messages to a file."""
     2
     3import datetime
     4import os
     5import threading
     6
     7from django.conf import settings
     8from django.core.exceptions import ImproperlyConfigured
     9from django.core.mail.backends.base import BaseEmailBackend
     10
     11
     12class EmailBackend(BaseEmailBackend):
     13
     14    def __init__(self, *args, **kwargs):
     15        self.stream = None
     16        self._lock = threading.RLock()
     17        self._counter = 0
     18        self._fname = None
     19        if 'file_path' in kwargs:
     20            self.file_path = kwargs.pop('file_path')
     21        else:
     22            self.file_path = getattr(settings, 'EMAIL_FILE_PATH',None)
     23        # Make sure self.file_path is a string.
     24        if not isinstance(self.file_path, basestring):
     25            raise ImproperlyConfigured('Path for saving emails is invalid: %r' % self.file_path)
     26        self.file_path = os.path.abspath(self.file_path)
     27        # Make sure that self.file_path is an directory if it exists.
     28        if os.path.exists(self.file_path) and not os.path.isdir(self.file_path):
     29            raise ImproperlyConfigured('Path for saving email messages exists, but is not a directory: %s' % self.file_path)
     30        # Try to create it, if it not exists.
     31        elif not os.path.exists(self.file_path):
     32            try:
     33                os.makedirs(self.file_path)
     34            except OSError, err:
     35                raise ImproperlyConfigured('Could not create directory for saving email messages: %s (%s)' % (self.file_path, err))
     36        # Make sure that self.file_path is writable.
     37        if not os.access(self.file_path, os.W_OK):
     38            raise ImproperlyConfigured('Could not write to directory: %s' % self.file_path)
     39        # Finally, call super().
     40        super(EmailBackend, self).__init__(*args, **kwargs)
     41
     42    def _get_filename(self):
     43        """Return a unique file name."""
     44        if self._fname is None:
     45            timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
     46            fname = "%s-%s.log" % (timestamp, abs(id(self)))
     47            self._fname = os.path.join(self.file_path, fname)
     48        return self._fname
     49
     50    def open(self):
     51        if self.stream is None:
     52            self.stream = open(self._get_filename(), 'a')
     53            return True
     54        return False
     55
     56    def close(self):
     57        try:
     58            if self.stream is not None:
     59                self.stream.close()
     60        finally:
     61            self.stream = None
     62
     63    def send_messages(self, email_messages):
     64        """Write all messages to the file in a thread-safe way."""
     65        if not email_messages:
     66            return
     67        self._lock.acquire()
     68        try:
     69            stream_created = self.open()
     70            for message in email_messages:
     71                self.stream.write('%s\n' % message.message().as_string())
     72                self.stream.write('-'*79)
     73                self.stream.write('\n')
     74                self.stream.flush()  # flush after each message
     75            if stream_created:
     76                self.close()
     77        except:
     78            if not self.fail_silently:
     79                raise
     80        finally:
     81            self._lock.release()
     82        return len(email_messages)
  • new file django/core/mail/backends/locmem.py

    diff -r 230a12f723d8 django/core/mail/backends/locmem.py
    - +  
     1"""
     2Backend for test environment.
     3"""
     4
     5from django.core import mail
     6from django.core.mail.backends.base import BaseEmailBackend
     7
     8
     9class EmailBackend(BaseEmailBackend):
     10    """A email backend for use during test sessions.
     11
     12    The test connection stores email messages in a dummy outbox,
     13    rather than sending them out on the wire.
     14
     15    The dummy outbox is accessible through the outbox instance attribute.
     16    """
     17
     18    def __init__(self, *args, **kwargs):
     19        super(EmailBackend, self).__init__(*args, **kwargs)
     20        if not hasattr(mail, 'outbox'):
     21            mail.outbox = []
     22
     23    def send_messages(self, messages):
     24        """Redirect messages to the dummy outbox"""
     25        mail.outbox.extend(messages)
     26        return len(messages)
  • new file django/core/mail/backends/smtp.py

    diff -r 230a12f723d8 django/core/mail/backends/smtp.py
    - +  
     1"""SMTP email backend class."""
     2
     3import smtplib
     4import socket
     5import threading
     6
     7from django.conf import settings
     8from django.core.mail.backends.base import BaseEmailBackend
     9from django.core.mail.utils import DNS_NAME
     10
     11
     12class EmailBackend(BaseEmailBackend):
     13    """
     14    A wrapper that manages the SMTP network connection.
     15    """
     16
     17    def __init__(self, host=None, port=None, username=None, password=None,
     18                 use_tls=None, fail_silently=False, **kwargs):
     19        super(EmailBackend, self).__init__(fail_silently=fail_silently)
     20        self.host = host or settings.EMAIL_HOST
     21        self.port = port or settings.EMAIL_PORT
     22        self.username = username or settings.EMAIL_HOST_USER
     23        self.password = password or settings.EMAIL_HOST_PASSWORD
     24        self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS
     25        self.connection = None
     26        self._lock = threading.RLock()
     27
     28    def open(self):
     29        """
     30        Ensures we have a connection to the email server. Returns whether or
     31        not a new connection was required (True or False).
     32        """
     33        if self.connection:
     34            # Nothing to do if the connection is already open.
     35            return False
     36        try:
     37            # If local_hostname is not specified, socket.getfqdn() gets used.
     38            # For performance, we use the cached FQDN for local_hostname.
     39            self.connection = smtplib.SMTP(self.host, self.port,
     40                                           local_hostname=DNS_NAME.get_fqdn())
     41            if self.use_tls:
     42                self.connection.ehlo()
     43                self.connection.starttls()
     44                self.connection.ehlo()
     45            if self.username and self.password:
     46                self.connection.login(self.username, self.password)
     47            return True
     48        except:
     49            if not self.fail_silently:
     50                raise
     51
     52    def close(self):
     53        """Closes the connection to the email server."""
     54        try:
     55            try:
     56                self.connection.quit()
     57            except socket.sslerror:
     58                # This happens when calling quit() on a TLS connection
     59                # sometimes.
     60                self.connection.close()
     61            except:
     62                if self.fail_silently:
     63                    return
     64                raise
     65        finally:
     66            self.connection = None
     67
     68    def send_messages(self, email_messages):
     69        """
     70        Sends one or more EmailMessage objects and returns the number of email
     71        messages sent.
     72        """
     73        if not email_messages:
     74            return
     75        self._lock.acquire()
     76        try:
     77            new_conn_created = self.open()
     78            if not self.connection:
     79                # We failed silently on open().
     80                # Trying to send would be pointless.
     81                return
     82            num_sent = 0
     83            for message in email_messages:
     84                sent = self._send(message)
     85                if sent:
     86                    num_sent += 1
     87            if new_conn_created:
     88                self.close()
     89        finally:
     90            self._lock.release()
     91        return num_sent
     92
     93    def _send(self, email_message):
     94        """A helper method that does the actual sending."""
     95        if not email_message.recipients():
     96            return False
     97        try:
     98            self.connection.sendmail(email_message.from_email,
     99                    email_message.recipients(),
     100                    email_message.message().as_string())
     101        except:
     102            if not self.fail_silently:
     103                raise
     104            return False
     105        return True
  • new file django/core/mail/message.py

    diff -r 230a12f723d8 django/core/mail/message.py
    - +  
     1import mimetypes
     2import os
     3import random
     4import time
     5from email import Charset, Encoders
     6from email.MIMEText import MIMEText
     7from email.MIMEMultipart import MIMEMultipart
     8from email.MIMEBase import MIMEBase
     9from email.Header import Header
     10from email.Utils import formatdate, parseaddr, formataddr
     11
     12from django.conf import settings
     13from django.core.mail.utils import DNS_NAME
     14from django.utils.encoding import smart_str, force_unicode
     15
     16# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
     17# some spam filters.
     18Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
     19
     20# Default MIME type to use on attachments (if it is not explicitly given
     21# and cannot be guessed).
     22DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream'
     23
     24
     25class BadHeaderError(ValueError):
     26    pass
     27
     28
     29# Copied from Python standard library, with the following modifications:
     30# * Used cached hostname for performance.
     31# * Added try/except to support lack of getpid() in Jython (#5496).
     32def make_msgid(idstring=None):
     33    """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
     34
     35    <20020201195627.33539.96671@nightshade.la.mastaler.com>
     36
     37    Optional idstring if given is a string used to strengthen the
     38    uniqueness of the message id.
     39    """
     40    timeval = time.time()
     41    utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
     42    try:
     43        pid = os.getpid()
     44    except AttributeError:
     45        # No getpid() in Jython, for example.
     46        pid = 1
     47    randint = random.randrange(100000)
     48    if idstring is None:
     49        idstring = ''
     50    else:
     51        idstring = '.' + idstring
     52    idhost = DNS_NAME
     53    msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
     54    return msgid
     55
     56
     57def forbid_multi_line_headers(name, val):
     58    """Forbids multi-line headers, to prevent header injection."""
     59    val = force_unicode(val)
     60    if '\n' in val or '\r' in val:
     61        raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name))
     62    try:
     63        val = val.encode('ascii')
     64    except UnicodeEncodeError:
     65        if name.lower() in ('to', 'from', 'cc'):
     66            result = []
     67            for item in val.split(', '):
     68                nm, addr = parseaddr(item)
     69                nm = str(Header(nm, settings.DEFAULT_CHARSET))
     70                result.append(formataddr((nm, str(addr))))
     71            val = ', '.join(result)
     72        else:
     73            val = Header(val, settings.DEFAULT_CHARSET)
     74    else:
     75        if name.lower() == 'subject':
     76            val = Header(val)
     77    return name, val
     78
     79
     80class SafeMIMEText(MIMEText):
     81    def __setitem__(self, name, val):
     82        name, val = forbid_multi_line_headers(name, val)
     83        MIMEText.__setitem__(self, name, val)
     84
     85
     86class SafeMIMEMultipart(MIMEMultipart):
     87    def __setitem__(self, name, val):
     88        name, val = forbid_multi_line_headers(name, val)
     89        MIMEMultipart.__setitem__(self, name, val)
     90
     91
     92class EmailMessage(object):
     93    """
     94    A container for email information.
     95    """
     96    content_subtype = 'plain'
     97    mixed_subtype = 'mixed'
     98    encoding = None     # None => use settings default
     99
     100    def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
     101                 connection=None, attachments=None, headers=None):
     102        """
     103        Initialize a single email message (which can be sent to multiple
     104        recipients).
     105
     106        All strings used to create the message can be unicode strings
     107        (or UTF-8 bytestrings). The SafeMIMEText class will handle any
     108        necessary encoding conversions.
     109        """
     110        if to:
     111            assert not isinstance(to, basestring), '"to" argument must be a list or tuple'
     112            self.to = list(to)
     113        else:
     114            self.to = []
     115        if bcc:
     116            assert not isinstance(bcc, basestring), '"bcc" argument must be a list or tuple'
     117            self.bcc = list(bcc)
     118        else:
     119            self.bcc = []
     120        self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
     121        self.subject = subject
     122        self.body = body
     123        self.attachments = attachments or []
     124        self.extra_headers = headers or {}
     125        self.connection = connection
     126
     127    def get_connection(self, fail_silently=False):
     128        from django.core.mail import get_connection
     129        if not self.connection:
     130            self.connection = get_connection(fail_silently=fail_silently)
     131        return self.connection
     132
     133    def message(self):
     134        encoding = self.encoding or settings.DEFAULT_CHARSET
     135        msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),
     136                           self.content_subtype, encoding)
     137        msg = self._create_message(msg)
     138        msg['Subject'] = self.subject
     139        msg['From'] = self.extra_headers.pop('From', self.from_email)
     140        msg['To'] = ', '.join(self.to)
     141
     142        # Email header names are case-insensitive (RFC 2045), so we have to
     143        # accommodate that when doing comparisons.
     144        header_names = [key.lower() for key in self.extra_headers]
     145        if 'date' not in header_names:
     146            msg['Date'] = formatdate()
     147        if 'message-id' not in header_names:
     148            msg['Message-ID'] = make_msgid()
     149        for name, value in self.extra_headers.items():
     150            msg[name] = value
     151        return msg
     152
     153    def recipients(self):
     154        """
     155        Returns a list of all recipients of the email (includes direct
     156        addressees as well as Bcc entries).
     157        """
     158        return self.to + self.bcc
     159
     160    def send(self, fail_silently=False):
     161        """Sends the email message."""
     162        if not self.recipients():
     163            # Don't bother creating the network connection if there's nobody to
     164            # send to.
     165            return 0
     166        return self.get_connection(fail_silently).send_messages([self])
     167
     168    def attach(self, filename=None, content=None, mimetype=None):
     169        """
     170        Attaches a file with the given filename and content. The filename can
     171        be omitted and the mimetype is guessed, if not provided.
     172
     173        If the first parameter is a MIMEBase subclass it is inserted directly
     174        into the resulting message attachments.
     175        """
     176        if isinstance(filename, MIMEBase):
     177            assert content == mimetype == None
     178            self.attachments.append(filename)
     179        else:
     180            assert content is not None
     181            self.attachments.append((filename, content, mimetype))
     182
     183    def attach_file(self, path, mimetype=None):
     184        """Attaches a file from the filesystem."""
     185        filename = os.path.basename(path)
     186        content = open(path, 'rb').read()
     187        self.attach(filename, content, mimetype)
     188
     189    def _create_message(self, msg):
     190        return self._create_attachments(msg)
     191
     192    def _create_attachments(self, msg):
     193        if self.attachments:
     194            body_msg = msg
     195            msg = SafeMIMEMultipart(_subtype=self.mixed_subtype)
     196            if self.body:
     197                msg.attach(body_msg)
     198            for attachment in self.attachments:
     199                if isinstance(attachment, MIMEBase):
     200                    msg.attach(attachment)
     201                else:
     202                    msg.attach(self._create_attachment(*attachment))
     203        return msg
     204
     205    def _create_mime_attachment(self, content, mimetype):
     206        """
     207        Converts the content, mimetype pair into a MIME attachment object.
     208        """
     209        basetype, subtype = mimetype.split('/', 1)
     210        if basetype == 'text':
     211            attachment = SafeMIMEText(smart_str(content,
     212                settings.DEFAULT_CHARSET), subtype, settings.DEFAULT_CHARSET)
     213        else:
     214            # Encode non-text attachments with base64.
     215            attachment = MIMEBase(basetype, subtype)
     216            attachment.set_payload(content)
     217            Encoders.encode_base64(attachment)
     218        return attachment
     219
     220    def _create_attachment(self, filename, content, mimetype=None):
     221        """
     222        Converts the filename, content, mimetype triple into a MIME attachment
     223        object.
     224        """
     225        if mimetype is None:
     226            mimetype, _ = mimetypes.guess_type(filename)
     227            if mimetype is None:
     228                mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
     229        attachment = self._create_mime_attachment(content, mimetype)
     230        if filename:
     231            attachment.add_header('Content-Disposition', 'attachment',
     232                                  filename=filename)
     233        return attachment
     234
     235
     236class EmailMultiAlternatives(EmailMessage):
     237    """
     238    A version of EmailMessage that makes it easy to send multipart/alternative
     239    messages. For example, including text and HTML versions of the text is
     240    made easier.
     241    """
     242    alternative_subtype = 'alternative'
     243
     244    def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
     245            connection=None, attachments=None, headers=None, alternatives=None):
     246        """
     247        Initialize a single email message (which can be sent to multiple
     248        recipients).
     249
     250        All strings used to create the message can be unicode strings (or UTF-8
     251        bytestrings). The SafeMIMEText class will handle any necessary encoding
     252        conversions.
     253        """
     254        super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers)
     255        self.alternatives=alternatives or []
     256
     257    def attach_alternative(self, content, mimetype):
     258        """Attach an alternative content representation."""
     259        assert content is not None
     260        assert mimetype is not None
     261        self.alternatives.append((content, mimetype))
     262
     263    def _create_message(self, msg):
     264        return self._create_attachments(self._create_alternatives(msg))
     265
     266    def _create_alternatives(self, msg):
     267        if self.alternatives:
     268            body_msg = msg
     269            msg = SafeMIMEMultipart(_subtype=self.alternative_subtype)
     270            if self.body:
     271                msg.attach(body_msg)
     272            for alternative in self.alternatives:
     273                msg.attach(self._create_mime_attachment(*alternative))
     274        return msg
  • new file django/core/mail/utils.py

    diff -r 230a12f723d8 django/core/mail/utils.py
    - +  
     1"""
     2Email message and email sending related helper functions.
     3"""
     4
     5import socket
     6
     7
     8# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of
     9# seconds, which slows down the restart of the server.
     10class CachedDnsName(object):
     11    def __str__(self):
     12        return self.get_fqdn()
     13
     14    def get_fqdn(self):
     15        if not hasattr(self, '_fqdn'):
     16            self._fqdn = socket.getfqdn()
     17        return self._fqdn
     18
     19DNS_NAME = CachedDnsName()
  • django/test/utils.py

    diff -r 230a12f723d8 django/test/utils.py
    a b  
    22from django.conf import settings
    33from django.db import connection
    44from django.core import mail
     5from django.core.mail.backends import locmem
    56from django.test import signals
    67from django.template import Template
    78from django.utils.translation import deactivate
     
    2829    signals.template_rendered.send(sender=self, template=self, context=context)
    2930    return self.nodelist.render(context)
    3031
    31 class TestSMTPConnection(object):
    32     """A substitute SMTP connection for use during test sessions.
    33     The test connection stores email messages in a dummy outbox,
    34     rather than sending them out on the wire.
    35 
    36     """
    37     def __init__(*args, **kwargs):
    38         pass
    39     def open(self):
    40         "Mock the SMTPConnection open() interface"
    41         pass
    42     def close(self):
    43         "Mock the SMTPConnection close() interface"
    44         pass
    45     def send_messages(self, messages):
    46         "Redirect messages to the dummy outbox"
    47         mail.outbox.extend(messages)
    48         return len(messages)
    4932
    5033def setup_test_environment():
    5134    """Perform any global pre-test setup. This involves:
    5235
    5336        - Installing the instrumented test renderer
    54         - Diverting the email sending functions to a test buffer
     37        - Set the email backend to the locmem email backend.
    5538        - Setting the active locale to match the LANGUAGE_CODE setting.
    5639    """
    5740    Template.original_render = Template.render
    5841    Template.render = instrumented_test_render
    5942
    6043    mail.original_SMTPConnection = mail.SMTPConnection
    61     mail.SMTPConnection = TestSMTPConnection
     44    mail.SMTPConnection = locmem.EmailBackend
     45
     46    settings.EMAIL_BACKEND = 'locmem'
     47    mail.original_email_backend = settings.EMAIL_BACKEND
    6248
    6349    mail.outbox = []
    6450
     
    7763    mail.SMTPConnection = mail.original_SMTPConnection
    7864    del mail.original_SMTPConnection
    7965
     66    settings.EMAIL_BACKEND = mail.original_email_backend
     67    del mail.original_email_backend
     68
    8069    del mail.outbox
    8170
    82 
    8371def get_runner(settings):
    8472    test_path = settings.TEST_RUNNER.split('.')
    8573    # Allow for Python 2.5 relative paths
  • docs/internals/deprecation.txt

    diff -r 230a12f723d8 docs/internals/deprecation.txt
    a b  
    2222        * The old imports for CSRF functionality (``django.contrib.csrf.*``),
    2323          which moved to core in 1.2, will be removed.
    2424
     25        * ``SMTPConnection``. The 1.2 release deprecated the ``SMTPConnection``
     26          class in favor of a generic E-mail backend API.
     27
    2528    * 2.0
    2629        * ``django.views.defaults.shortcut()``. This function has been moved
    2730          to ``django.contrib.contenttypes.views.shortcut()`` as part of the
  • docs/ref/settings.txt

    diff -r 230a12f723d8 docs/ref/settings.txt
    a b  
    424424This is only used if ``CommonMiddleware`` is installed (see
    425425:ref:`topics-http-middleware`).
    426426
     427.. setting:: EMAIL_BACKEND
     428
     429EMAIL_BACKEND
     430-------------
     431
     432.. versionadded:: 1.2
     433
     434Default: ``'smtp'``
     435
     436The backend to use for sending emails. For the list of available backends see
     437:ref:`topics-email`.
     438
     439.. setting:: EMAIL_FILE_PATH
     440
     441EMAIL_FILE_PATH
     442---------------
     443
     444.. versionadded:: 1.2
     445
     446Default: ``None``
     447
     448The directory used by the ``file`` email backend to store output files.
     449
    427450.. setting:: EMAIL_HOST
    428451
    429452EMAIL_HOST
  • docs/topics/email.txt

    diff -r 230a12f723d8 docs/topics/email.txt
    a b  
    178178
    179179.. _emailmessage-and-smtpconnection:
    180180
    181 The EmailMessage and SMTPConnection classes
    182 ===========================================
     181The EmailMessage class
     182======================
    183183
    184184.. versionadded:: 1.0
    185185
    186186Django's ``send_mail()`` and ``send_mass_mail()`` functions are actually thin
    187 wrappers that make use of the ``EmailMessage`` and ``SMTPConnection`` classes
    188 in ``django.core.mail``.  If you ever need to customize the way Django sends
    189 e-mail, you can subclass these two classes to suit your needs.
     187wrappers that make use of the :class:`~django.core.mail.EmailMessage`
     188class.
     189
     190Not all features of the :class:`~django.core.mail.EmailMessage` class are
     191available through the ``send_mail()`` and related wrapper functions. If you
     192wish to use advanced features, such as BCC'ed recipients, file attachments, or
     193multi-part e-mail, you'll need to create
     194:class:`~django.core.mail.EmailMessage` instances directly.
    190195
    191196.. note::
    192     Not all features of the ``EmailMessage`` class are available through the
    193     ``send_mail()`` and related wrapper functions. If you wish to use advanced
    194     features, such as BCC'ed recipients, file attachments, or multi-part
    195     e-mail, you'll need to create ``EmailMessage`` instances directly.
    196 
    197197    This is a design feature. ``send_mail()`` and related functions were
    198198    originally the only interface Django provided. However, the list of
    199199    parameters they accepted was slowly growing over time. It made sense to
    200200    move to a more object-oriented design for e-mail messages and retain the
    201201    original functions only for backwards compatibility.
    202202
    203 In general, ``EmailMessage`` is responsible for creating the e-mail message
    204 itself. ``SMTPConnection`` is responsible for the network connection side of
    205 the operation. This means you can reuse the same connection (an
    206 ``SMTPConnection`` instance) for multiple messages.
     203:class:`~django.core.mail.EmailMessage` is responsible for creating the e-mail
     204message itself. The :ref:`e-mail backend <topic-email-backends>` is then
     205responsible for sending the e-mail.
     206
     207For convenience, :class:`~django.core.mail.EmailMessage` provides a simple
     208``send()`` method for sending a single email. If you need to send multiple
     209messages, the email backend API :ref:`provides an alternative
     210<topics-sending-multiple-emails>`.
    207211
    208212EmailMessage Objects
    209213--------------------
    210214
    211 .. class:: EmailMessage
     215.. class:: django.core.mail.EmailMessage
    212216
    213 The ``EmailMessage`` class is initialized with the following parameters (in
    214 the given order, if positional arguments are used). All parameters are
    215 optional and can be set at any time prior to calling the ``send()`` method.
     217The :class:`~django.core.mail.EmailMessage` class is initialized with the
     218following parameters (in the given order, if positional arguments are used).
     219All parameters are optional and can be set at any time prior to calling the
     220``send()`` method.
    216221
    217222    * ``subject``: The subject line of the e-mail.
    218223
     
    227232    * ``bcc``: A list or tuple of addresses used in the "Bcc" header when
    228233      sending the e-mail.
    229234
    230     * ``connection``: An ``SMTPConnection`` instance. Use this parameter if
     235    * ``connection``: An e-mail backend instance. Use this parameter if
    231236      you want to use the same connection for multiple messages. If omitted, a
    232237      new connection is created when ``send()`` is called.
    233238
     
    248253
    249254The class has the following methods:
    250255
    251     * ``send(fail_silently=False)`` sends the message, using either
    252       the connection that is specified in the ``connection``
    253       attribute, or creating a new connection if none already
    254       exists. If the keyword argument ``fail_silently`` is ``True``,
    255       exceptions raised while sending the message will be quashed.
     256    * ``send(fail_silently=False)`` sends the message. If a connection was
     257      specified when the email was constructed, that connection will be used.
     258      Otherwise, an instance of the default backend will be instantiated and
     259      used. If the keyword argument ``fail_silently`` is ``True``, exceptions
     260      raised while sending the message will be quashed.
    256261
    257262    * ``message()`` constructs a ``django.core.mail.SafeMIMEText`` object (a
    258263      subclass of Python's ``email.MIMEText.MIMEText`` class) or a
    259       ``django.core.mail.SafeMIMEMultipart`` object holding the
    260       message to be sent. If you ever need to extend the ``EmailMessage`` class,
    261       you'll probably want to override this method to put the content you want
    262       into the MIME object.
     264      ``django.core.mail.SafeMIMEMultipart`` object holding the message to be
     265      sent. If you ever need to extend the
     266      :class:`~django.core.mail.EmailMessage` class, you'll probably want to
     267      override this method to put the content you want into the MIME object.
    263268
    264269    * ``recipients()`` returns a list of all the recipients of the message,
    265270      whether they're recorded in the ``to`` or ``bcc`` attributes. This is
     
    299304Sending alternative content types
    300305~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    301306
    302 It can be useful to include multiple versions of the content in an e-mail;
    303 the classic example is to send both text and HTML versions of a message. With
     307It can be useful to include multiple versions of the content in an e-mail; the
     308classic example is to send both text and HTML versions of a message. With
    304309Django's e-mail library, you can do this using the ``EmailMultiAlternatives``
    305 class. This subclass of ``EmailMessage`` has an ``attach_alternative()`` method
    306 for including extra versions of the message body in the e-mail. All the other
    307 methods (including the class initialization) are inherited directly from
    308 ``EmailMessage``.
     310class. This subclass of :class:`~django.core.mail.EmailMessage` has an
     311``attach_alternative()`` method for including extra versions of the message
     312body in the e-mail. All the other methods (including the class initialization)
     313are inherited directly from :class:`~django.core.mail.EmailMessage`.
    309314
    310315To send a text and HTML combination, you could write::
    311316
     
    318323    msg.attach_alternative(html_content, "text/html")
    319324    msg.send()
    320325
    321 By default, the MIME type of the ``body`` parameter in an ``EmailMessage`` is
    322 ``"text/plain"``. It is good practice to leave this alone, because it
    323 guarantees that any recipient will be able to read the e-mail, regardless of
    324 their mail client. However, if you are confident that your recipients can
    325 handle an alternative content type, you can use the ``content_subtype``
    326 attribute on the ``EmailMessage`` class to change the main content type. The
    327 major type will always be ``"text"``, but you can change it to the subtype. For
    328 example::
     326By default, the MIME type of the ``body`` parameter in an
     327:class:`~django.core.mail.EmailMessage` is ``"text/plain"``. It is good
     328practice to leave this alone, because it guarantees that any recipient will be
     329able to read the e-mail, regardless of their mail client. However, if you are
     330confident that your recipients can handle an alternative content type, you can
     331use the ``content_subtype`` attribute on the
     332:class:`~django.core.mail.EmailMessage` class to change the main content type.
     333The major type will always be ``"text"``, but you can change it to the
     334subtype. For example::
    329335
    330336    msg = EmailMessage(subject, html_content, from_email, [to])
    331337    msg.content_subtype = "html"  # Main content is now text/html
    332338    msg.send()
    333339
    334 SMTPConnection Objects
    335 ----------------------
     340.. _topic-email-backends:
    336341
    337 .. class:: SMTPConnection
     342E-Mail Backends
     343===============
    338344
    339 The ``SMTPConnection`` class is initialized with the host, port, username and
    340 password for the SMTP server. If you don't specify one or more of those
    341 options, they are read from your settings file.
     345.. versionadded:: 1.2
    342346
    343 If you're sending lots of messages at once, the ``send_messages()`` method of
    344 the ``SMTPConnection`` class is useful. It takes a list of ``EmailMessage``
    345 instances (or subclasses) and sends them over a single connection. For example,
    346 if you have a function called ``get_notification_email()`` that returns a
    347 list of ``EmailMessage`` objects representing some periodic e-mail you wish to
    348 send out, you could send this with::
     347The actual sending of an e-mail is handled by the e-mail backend.
    349348
    350     connection = SMTPConnection()   # Use default settings for connection
     349The e-mail backend class has the following methods:
     350
     351    * ``open()`` instantiates an long-lived email-sending connection.
     352
     353    * ``close()`` closes the current email-sending connection.
     354
     355    * ``send_messages(email_messages)`` sends a list of
     356      :class:`~django.core.mail.EmailMessage` objects. If the connection
     357      is not open, this call will implicitly open the connection, and
     358      close the connection afterwards.
     359
     360Obtaining an instance of an e-mail backend
     361------------------------------------------
     362
     363The :meth:`get_connection` function in ``django.core.mail`` returns an
     364instance of the e-mail backend that you can use.
     365
     366.. currentmodule:: django.core.mail
     367
     368.. function:: get_connection(backend=None, fail_silently=False, *args, **kwargs)
     369
     370By default, a call to ``get_connection()`` will return an instance of the
     371email backend specified in :setting:`EMAIL_BACKEND`. If you specify the
     372``backend`` argument, an instance of that backend will be instantiated.
     373
     374The ``fail_silently`` argument controls how the backend should handle errors.
     375If ``fail_silently`` is True, exceptions during the email sending process
     376will be silently ignored.
     377
     378All other arguments are passed directly to the constructor of the
     379e-mail backend.
     380
     381Django ships with several e-mail sending backends. With the exception of the
     382SMTP backend (which is the default), these backends are only useful during
     383testing and development. If you have special email sending requirements, you
     384can :ref:`write your own email backend <topic-custom-email-backend>`.
     385
     386SMTP backend
     387~~~~~~~~~~~~
     388
     389This is the default backend. E-mail will be sent through a SMTP server.
     390The server address and authentication credentials are set in the
     391:setting:`EMAIL_HOST`, :setting:`EMAIL_POST`, :setting:`EMAIL_HOST_USER`,
     392:setting:`EMAIL_HOST_PASSWORD` and :setting:`EMAIL_USE_TLS` settings in your
     393settings file.
     394
     395The SMTP backend is configured in the default settings::
     396
     397    EMAIL_BACKEND = 'smtp'
     398
     399.. admonition:: SMTPConnection objects
     400
     401    Prior to version 1.2, Django provided a ``mail.SMTPConnection`` class. This
     402    class provided a way to directly control the use of SMTP to send email.
     403    This class has been deprecated in favor of the generic email backend API.
     404
     405    For backwards compatibility ``SMTPConnection`` is still available in
     406    ``django.core.mail`` as an alias for the SMTP backend. New code should
     407    use :meth:`~django.core.mail.get_connection` instead.
     408
     409Console backend
     410~~~~~~~~~~~~~~~
     411
     412Instead of sending out real e-mails the console backend just writes the e-mails
     413that would be send to the standard output. To use this backend, use
     414in your settings::
     415
     416    EMAIL_BACKEND = 'console'
     417
     418This backend is not intended for use in production -- it is provided as a
     419convenience that can be used during development.
     420
     421File backend
     422~~~~~~~~~~~~
     423
     424The file backend writes e-mails to a file. A new file is created for each new
     425instance of this backend. The directory to which the files are written is
     426either taken from the :setting:`EMAIL_FILE_PATH` setting or from the
     427``file_path`` keyword when creating a connection with
     428:meth:`~django.core.mail.get_connection`. To use this backend, use in your
     429settings::
     430
     431    EMAIL_BACKEND = 'file'
     432    EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location
     433
     434This backend is not intended for use in production -- it is provided as a
     435convenience that can be used during development.
     436
     437In-memory backend
     438~~~~~~~~~~~~~~~~~
     439
     440The ``'locmem'`` backend stores messages in a special attribute of the
     441``django.core.mail`` module. The ``outbox`` attribute is created when the
     442first message is send. It's a list with an
     443:class:`~django.core.mail.EmailMessage` instance for each message that would
     444be send. To use this backend, use in your settings::
     445
     446  EMAIL_BACKEND = 'locmem'
     447
     448This backend is not intended for use in production -- it is provided as a
     449convenience that can be used during development and testing.
     450
     451Dummy backend
     452~~~~~~~~~~~~~
     453
     454As the name suggests the dummy backend does nothing with your messages.
     455To enable this backend, use in your settings file::
     456
     457   EMAIL_BACKEND = 'dummy'
     458
     459This backend is not intended for use in production -- it is provided as a
     460convenience that can be used during development.
     461
     462.. _topic-custom-email-backend:
     463
     464Defining a custom e-mail backend
     465--------------------------------
     466
     467If you need to change how e-mails are send you can write your own e-mail
     468backend. The ``EMAIL_BACKEND`` setting in your settings file is then the
     469Python import path for your backend.
     470
     471Custom e-mail backends should subclass ``BaseEmailBackend`` that is located in
     472the ``django.core.mail.backends.base`` module. A custom e-mail backend must
     473implement the ``send_messages(email_messages)`` method. This method receives a
     474list of :class:`~django.core.mail.EmailMessage` instances and returns the
     475number of successfully delivered messages. If your backend should be capable
     476of network connection handling, you should implement both the ``open()`` and
     477``close()`` methods too. Refer to ``SMTPEmailBackend`` for a reference
     478implementation.
     479
     480.. _topics-sending-multiple-emails:
     481
     482Sending multiple emails
     483-----------------------
     484
     485Establishing and closing an SMTP connection (or any other network connection,
     486for that matter) is an expensive process. If you have a lot of emails to send,
     487it makes sense to reuse an SMTP connection, rather than creating and
     488destroying a connection every time you want to send an email.
     489
     490There are two ways you tell an email backend to reuse a connection.
     491
     492Firstly, you can use the ``send_messages()`` method. ``send_messages()`` takes
     493a list of :class:`~django.core.mail.EmailMessage` instances (or subclasses),
     494and sends them all using a single connection.
     495
     496For example, if you have a function called ``get_notification_email()`` that
     497returns a list of :class:`~django.core.mail.EmailMessage` objects representing
     498some periodic e-mail you wish to send out, you could send these emails using
     499a single call to send_messages::
     500
     501    from django.core import mail
     502    connection = mail.get_connection()   # Use default email connection
    351503    messages = get_notification_email()
    352504    connection.send_messages(messages)
    353505
     506In this example, the call to ``send_messages()`` opens a connection on the
     507backend, sends the list of messages, and then closes the connection again.
     508
     509The second approach is to use the ``open()`` and ``close()`` methods on the
     510email backend to manually control the connection. ``send_messages()`` will not
     511manually open or close the connection if it is already open, so if you
     512manually open the connection, you can control when it is closed. For example::
     513
     514    from django.core import mail
     515    connection = mail.get_connection()
     516
     517    # Manually open the connection
     518    connection.open()
     519
     520    # Construct an email message that uses the connection
     521    email1 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
     522                              ['to1@example.com'], connection=connection)
     523    email1.send() # Send the email
     524
     525    # Construct two more messages
     526    email2 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
     527                              ['to2@example.com'])
     528    email3 = mail.EmailMessage('Hello', 'Body goes here', 'from@example.com',
     529                              ['to3@example.com'])
     530
     531    # Send the two emails in a single call -
     532    connection.send_messages([email2, email3])
     533    # The connection was already open so send_messages() doesn't close it.
     534    # We need to manually close the connection.
     535    connection.close()
     536
     537
    354538Testing e-mail sending
    355 ----------------------
     539======================
    356540
    357541The are times when you do not want Django to send e-mails at all. For example,
    358542while developing a website, you probably don't want to send out thousands of
     
    360544people under the right conditions, and that those e-mails will contain the
    361545correct content.
    362546
    363 The easiest way to test your project's use of e-mail is to use a "dumb" e-mail
    364 server that receives the e-mails locally and displays them to the terminal,
    365 but does not actually send anything. Python has a built-in way to accomplish
    366 this with a single command::
     547The easiest way to test your project's use of e-mail is to use the ``console``
     548email backend. This backend redirects all email to stdout, allowing you to
     549inspect the content of mail.
     550
     551The ``file`` email backend can also be useful during development -- this backend
     552dumps the contents of every SMTP connection to a file that can be inspected
     553at your leisure.
     554
     555Another approach is to use a "dumb" SMTP server that receives the e-mails
     556locally and displays them to the terminal, but does not actually send
     557anything. Python has a built-in way to accomplish this with a single command::
    367558
    368559    python -m smtpd -n -c DebuggingServer localhost:1025
    369560
    370561This command will start a simple SMTP server listening on port 1025 of
    371 localhost. This server simply prints to standard output all email headers and
    372 the email body. You then only need to set the :setting:`EMAIL_HOST` and
     562localhost. This server simply prints to standard output all e-mail headers and
     563the e-mail body. You then only need to set the :setting:`EMAIL_HOST` and
    373564:setting:`EMAIL_PORT` accordingly, and you are set.
    374565
    375 For more entailed testing and processing of e-mails locally, see the Python
    376 documentation on the `SMTP Server`_.
     566For a more detailed discussion of testing and processing of e-mails locally,
     567see the Python documentation on the `SMTP Server`_.
    377568
    378569.. _SMTP Server: http://docs.python.org/library/smtpd.html
  • docs/topics/testing.txt

    diff -r 230a12f723d8 docs/topics/testing.txt
    a b  
    11041104    ``target_status_code`` will be the url and status code for the final
    11051105    point of the redirect chain.
    11061106
     1107.. _topics-testing-email:
     1108
    11071109E-mail services
    11081110---------------
    11091111
     
    11171119contents of each message -- without actually sending the messages.
    11181120
    11191121The test runner accomplishes this by transparently replacing the normal
    1120 :class:`~django.core.mail.SMTPConnection` class with a different version.
     1122email backend with a testing backend.
    11211123(Don't worry -- this has no effect on any other e-mail senders outside of
    11221124Django, such as your machine's mail server, if you're running one.)
    11231125
     
    11281130During test running, each outgoing e-mail is saved in
    11291131``django.core.mail.outbox``. This is a simple list of all
    11301132:class:`~django.core.mail.EmailMessage` instances that have been sent.
    1131 It does not exist under normal execution conditions, i.e., when you're not
    1132 running unit tests. The outbox is created during test setup, along with the
    1133 dummy :class:`~django.core.mail.SMTPConnection`. When the test framework is
    1134 torn down, the standard :class:`~django.core.mail.SMTPConnection` class is
    1135 restored, and the test outbox is destroyed.
    1136 
    11371133The ``outbox`` attribute is a special attribute that is created *only* when
    1138 the tests are run. It doesn't normally exist as part of the
     1134the ``locmem`` e-mail backend is used. It doesn't normally exist as part of the
    11391135:mod:`django.core.mail` module and you can't import it directly. The code
    11401136below shows how to access this attribute correctly.
    11411137
  • new file tests/regressiontests/mail/custombackend.py

    diff -r 230a12f723d8 tests/regressiontests/mail/custombackend.py
    - +  
     1"""A custom backend for testing."""
     2
     3from django.core.mail.backends.base import BaseEmailBackend
     4
     5
     6class EmailBackend(BaseEmailBackend):
     7
     8    def __init__(self, *args, **kwargs):
     9        super(EmailBackend, self).__init__(*args, **kwargs)
     10        self.test_outbox = []
     11
     12    def send_messages(self, email_messages):
     13        # Messages are stored in a instance variable for testing.
     14        self.test_outbox.extend(email_messages)
     15        return len(email_messages)
  • tests/regressiontests/mail/tests.py

    diff -r 230a12f723d8 tests/regressiontests/mail/tests.py
    a b  
    11# coding: utf-8
     2
    23r"""
    34# Tests for the django.core.mail.
    45
     6>>> import os
     7>>> import shutil
     8>>> import tempfile
    59>>> from django.conf import settings
    610>>> from django.core import mail
    711>>> from django.core.mail import EmailMessage, mail_admins, mail_managers, EmailMultiAlternatives
     12>>> from django.core.mail.backends.base import BaseEmailBackend
     13>>> from django.core.mail.backends import console, dummy, locmem, filebased, smtp
    814>>> from django.utils.translation import ugettext_lazy
    915
    1016# Test normal ascii character case:
     
    138144JVBERi0xLjQuJS4uLg==
    139145...
    140146
     147# Make sure that the console backend writes to stdout
     148>>> connection = console.EmailBackend()
     149>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
     150>>> connection.send_messages([email])
     151Content-Type: text/plain; charset="utf-8"
     152MIME-Version: 1.0
     153Content-Transfer-Encoding: quoted-printable
     154Subject: Subject
     155From: from@example.com
     156To: to@example.com
     157Date: ...
     158Message-ID: ...
     159
     160Content
     161...
     1621
     163
     164# Make sure that dummy backends returns correct number of sent messages
     165>>> connection = dummy.EmailBackend()
     166>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
     167>>> connection.send_messages([email, email, email])
     1683
     169
     170# Make sure that locmen backend populates the outbox
     171>>> mail.outbox = []
     172>>> connection = locmem.EmailBackend()
     173>>> email1 = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
     174>>> email2 = EmailMessage('Subject 2', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
     175>>> connection.send_messages([email1, email2])
     1762
     177>>> len(mail.outbox)
     1782
     179>>> mail.outbox[0].subject
     180'Subject'
     181>>> mail.outbox[1].subject
     182'Subject 2'
     183
     184# Make sure that multiple locmem connections share mail.outbox
     185>>> mail.outbox = []
     186>>> connection1 = locmem.EmailBackend()
     187>>> connection2 = locmem.EmailBackend()
     188>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
     189>>> connection1.send_messages([email])
     1901
     191>>> connection2.send_messages([email])
     1921
     193>>> len(mail.outbox)
     1942
     195
     196# Make sure that the file backend write to the right location
     197>>> tmp_dir = tempfile.mkdtemp()
     198>>> connection = filebased.EmailBackend(file_path=tmp_dir)
     199>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
     200>>> connection.send_messages([email])
     2011
     202>>> len(os.listdir(tmp_dir))
     2031
     204>>> print open(os.path.join(tmp_dir, os.listdir(tmp_dir)[0])).read()
     205Content-Type: text/plain; charset="utf-8"
     206MIME-Version: 1.0
     207Content-Transfer-Encoding: quoted-printable
     208Subject: Subject
     209From: from@example.com
     210To: to@example.com
     211Date: ...
     212Message-ID: ...
     213
     214Content
     215...
     216>>> connection2 = filebased.EmailBackend(file_path=tmp_dir)
     217>>> connection2.send_messages([email])
     2181
     219>>> len(os.listdir(tmp_dir))
     2202
     221>>> connection.send_messages([email])
     2221
     223>>> len(os.listdir(tmp_dir))
     2242
     225>>> email.connection = filebased.EmailBackend(file_path=tmp_dir)
     226>>> connection_created = connection.open()
     227>>> num_sent = email.send()
     228>>> len(os.listdir(tmp_dir))
     2293
     230>>> num_sent = email.send()
     231>>> len(os.listdir(tmp_dir))
     2323
     233>>> connection.close()
     234>>> shutil.rmtree(tmp_dir)
     235
     236# Make sure that get_connection() accepts arbitrary keyword that might be
     237# used with custom backends.
     238>>> c = mail.get_connection(fail_silently=True, foo='bar')
     239>>> c.fail_silently
     240True
     241
     242# Test custom backend defined in this suite.
     243>>> conn = mail.get_connection('regressiontests.mail.custombackend')
     244>>> hasattr(conn, 'test_outbox')
     245True
     246>>> email = EmailMessage('Subject', 'Content', 'bounce@example.com', ['to@example.com'], headers={'From': 'from@example.com'})
     247>>> conn.send_messages([email])
     2481
     249>>> len(conn.test_outbox)
     2501
     251
     252# Test backend argument of mail.get_connection()
     253>>> isinstance(mail.get_connection('smtp'), smtp.EmailBackend)
     254True
     255>>> isinstance(mail.get_connection('locmem'), locmem.EmailBackend)
     256True
     257>>> isinstance(mail.get_connection('dummy'), dummy.EmailBackend)
     258True
     259>>> isinstance(mail.get_connection('console'), console.EmailBackend)
     260True
     261>>> tmp_dir = tempfile.mkdtemp()
     262>>> isinstance(mail.get_connection('file', file_path=tmp_dir), filebased.EmailBackend)
     263True
     264>>> shutil.rmtree(tmp_dir)
     265>>> isinstance(mail.get_connection(), locmem.EmailBackend)
     266True
     267
    141268"""
Back to Top