app/django/core/mail.py
changeset 54 03e267d67478
child 323 ff1a9aa48cfd
equal deleted inserted replaced
53:57b4279d8c4e 54:03e267d67478
       
     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     if '\n' in val or '\r' in val:
       
    75         raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name))
       
    76     try:
       
    77         val = force_unicode(val).encode('ascii')
       
    78     except UnicodeEncodeError:
       
    79         if name.lower() in ('to', 'from', 'cc'):
       
    80             result = []
       
    81             for item in val.split(', '):
       
    82                 nm, addr = parseaddr(item)
       
    83                 nm = str(Header(nm, settings.DEFAULT_CHARSET))
       
    84                 result.append(formataddr((nm, str(addr))))
       
    85             val = ', '.join(result)
       
    86         else:
       
    87             val = Header(force_unicode(val), settings.DEFAULT_CHARSET)
       
    88     return name, val
       
    89 
       
    90 class SafeMIMEText(MIMEText):
       
    91     def __setitem__(self, name, val):
       
    92         name, val = forbid_multi_line_headers(name, val)
       
    93         MIMEText.__setitem__(self, name, val)
       
    94 
       
    95 class SafeMIMEMultipart(MIMEMultipart):
       
    96     def __setitem__(self, name, val):
       
    97         name, val = forbid_multi_line_headers(name, val)
       
    98         MIMEMultipart.__setitem__(self, name, val)
       
    99 
       
   100 class SMTPConnection(object):
       
   101     """
       
   102     A wrapper that manages the SMTP network connection.
       
   103     """
       
   104 
       
   105     def __init__(self, host=None, port=None, username=None, password=None,
       
   106                  use_tls=None, fail_silently=False):
       
   107         self.host = host or settings.EMAIL_HOST
       
   108         self.port = port or settings.EMAIL_PORT
       
   109         self.username = username or settings.EMAIL_HOST_USER
       
   110         self.password = password or settings.EMAIL_HOST_PASSWORD
       
   111         self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS
       
   112         self.fail_silently = fail_silently
       
   113         self.connection = None
       
   114 
       
   115     def open(self):
       
   116         """
       
   117         Ensures we have a connection to the email server. Returns whether or
       
   118         not a new connection was required (True or False).
       
   119         """
       
   120         if self.connection:
       
   121             # Nothing to do if the connection is already open.
       
   122             return False
       
   123         try:
       
   124             # If local_hostname is not specified, socket.getfqdn() gets used.
       
   125             # For performance, we use the cached FQDN for local_hostname.
       
   126             self.connection = smtplib.SMTP(self.host, self.port,
       
   127                                            local_hostname=DNS_NAME.get_fqdn())
       
   128             if self.use_tls:
       
   129                 self.connection.ehlo()
       
   130                 self.connection.starttls()
       
   131                 self.connection.ehlo()
       
   132             if self.username and self.password:
       
   133                 self.connection.login(self.username, self.password)
       
   134             return True
       
   135         except:
       
   136             if not self.fail_silently:
       
   137                 raise
       
   138 
       
   139     def close(self):
       
   140         """Closes the connection to the email server."""
       
   141         try:
       
   142             try:
       
   143                 self.connection.quit()
       
   144             except socket.sslerror:
       
   145                 # This happens when calling quit() on a TLS connection
       
   146                 # sometimes.
       
   147                 self.connection.close()
       
   148             except:
       
   149                 if self.fail_silently:
       
   150                     return
       
   151                 raise
       
   152         finally:
       
   153             self.connection = None
       
   154 
       
   155     def send_messages(self, email_messages):
       
   156         """
       
   157         Sends one or more EmailMessage objects and returns the number of email
       
   158         messages sent.
       
   159         """
       
   160         if not email_messages:
       
   161             return
       
   162         new_conn_created = self.open()
       
   163         if not self.connection:
       
   164             # We failed silently on open(). Trying to send would be pointless.
       
   165             return
       
   166         num_sent = 0
       
   167         for message in email_messages:
       
   168             sent = self._send(message)
       
   169             if sent:
       
   170                 num_sent += 1
       
   171         if new_conn_created:
       
   172             self.close()
       
   173         return num_sent
       
   174 
       
   175     def _send(self, email_message):
       
   176         """A helper method that does the actual sending."""
       
   177         if not email_message.to:
       
   178             return False
       
   179         try:
       
   180             self.connection.sendmail(email_message.from_email,
       
   181                     email_message.recipients(),
       
   182                     email_message.message().as_string())
       
   183         except:
       
   184             if not self.fail_silently:
       
   185                 raise
       
   186             return False
       
   187         return True
       
   188 
       
   189 class EmailMessage(object):
       
   190     """
       
   191     A container for email information.
       
   192     """
       
   193     content_subtype = 'plain'
       
   194     multipart_subtype = 'mixed'
       
   195     encoding = None     # None => use settings default
       
   196 
       
   197     def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
       
   198             connection=None, attachments=None, headers=None):
       
   199         """
       
   200         Initialize a single email message (which can be sent to multiple
       
   201         recipients).
       
   202 
       
   203         All strings used to create the message can be unicode strings (or UTF-8
       
   204         bytestrings). The SafeMIMEText class will handle any necessary encoding
       
   205         conversions.
       
   206         """
       
   207         if to:
       
   208             self.to = list(to)
       
   209         else:
       
   210             self.to = []
       
   211         if bcc:
       
   212             self.bcc = list(bcc)
       
   213         else:
       
   214             self.bcc = []
       
   215         self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
       
   216         self.subject = subject
       
   217         self.body = body
       
   218         self.attachments = attachments or []
       
   219         self.extra_headers = headers or {}
       
   220         self.connection = connection
       
   221 
       
   222     def get_connection(self, fail_silently=False):
       
   223         if not self.connection:
       
   224             self.connection = SMTPConnection(fail_silently=fail_silently)
       
   225         return self.connection
       
   226 
       
   227     def message(self):
       
   228         encoding = self.encoding or settings.DEFAULT_CHARSET
       
   229         msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),
       
   230                            self.content_subtype, encoding)
       
   231         if self.attachments:
       
   232             body_msg = msg
       
   233             msg = SafeMIMEMultipart(_subtype=self.multipart_subtype)
       
   234             if self.body:
       
   235                 msg.attach(body_msg)
       
   236             for attachment in self.attachments:
       
   237                 if isinstance(attachment, MIMEBase):
       
   238                     msg.attach(attachment)
       
   239                 else:
       
   240                     msg.attach(self._create_attachment(*attachment))
       
   241         msg['Subject'] = self.subject
       
   242         msg['From'] = self.from_email
       
   243         msg['To'] = ', '.join(self.to)
       
   244         msg['Date'] = formatdate()
       
   245         msg['Message-ID'] = make_msgid()
       
   246         for name, value in self.extra_headers.items():
       
   247             msg[name] = value
       
   248         return msg
       
   249 
       
   250     def recipients(self):
       
   251         """
       
   252         Returns a list of all recipients of the email (includes direct
       
   253         addressees as well as Bcc entries).
       
   254         """
       
   255         return self.to + self.bcc
       
   256 
       
   257     def send(self, fail_silently=False):
       
   258         """Sends the email message."""
       
   259         return self.get_connection(fail_silently).send_messages([self])
       
   260 
       
   261     def attach(self, filename=None, content=None, mimetype=None):
       
   262         """
       
   263         Attaches a file with the given filename and content. The filename can
       
   264         be omitted (useful for multipart/alternative messages) and the mimetype
       
   265         is guessed, if not provided.
       
   266 
       
   267         If the first parameter is a MIMEBase subclass it is inserted directly
       
   268         into the resulting message attachments.
       
   269         """
       
   270         if isinstance(filename, MIMEBase):
       
   271             assert content == mimetype == None
       
   272             self.attachments.append(filename)
       
   273         else:
       
   274             assert content is not None
       
   275             self.attachments.append((filename, content, mimetype))
       
   276 
       
   277     def attach_file(self, path, mimetype=None):
       
   278         """Attaches a file from the filesystem."""
       
   279         filename = os.path.basename(path)
       
   280         content = open(path, 'rb').read()
       
   281         self.attach(filename, content, mimetype)
       
   282 
       
   283     def _create_attachment(self, filename, content, mimetype=None):
       
   284         """
       
   285         Converts the filename, content, mimetype triple into a MIME attachment
       
   286         object.
       
   287         """
       
   288         if mimetype is None:
       
   289             mimetype, _ = mimetypes.guess_type(filename)
       
   290             if mimetype is None:
       
   291                 mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
       
   292         basetype, subtype = mimetype.split('/', 1)
       
   293         if basetype == 'text':
       
   294             attachment = SafeMIMEText(smart_str(content,
       
   295                 settings.DEFAULT_CHARSET), subtype, settings.DEFAULT_CHARSET)
       
   296         else:
       
   297             # Encode non-text attachments with base64.
       
   298             attachment = MIMEBase(basetype, subtype)
       
   299             attachment.set_payload(content)
       
   300             Encoders.encode_base64(attachment)
       
   301         if filename:
       
   302             attachment.add_header('Content-Disposition', 'attachment',
       
   303                                   filename=filename)
       
   304         return attachment
       
   305 
       
   306 class EmailMultiAlternatives(EmailMessage):
       
   307     """
       
   308     A version of EmailMessage that makes it easy to send multipart/alternative
       
   309     messages. For example, including text and HTML versions of the text is
       
   310     made easier.
       
   311     """
       
   312     multipart_subtype = 'alternative'
       
   313 
       
   314     def attach_alternative(self, content, mimetype=None):
       
   315         """Attach an alternative content representation."""
       
   316         self.attach(content=content, mimetype=mimetype)
       
   317 
       
   318 def send_mail(subject, message, from_email, recipient_list,
       
   319               fail_silently=False, auth_user=None, auth_password=None):
       
   320     """
       
   321     Easy wrapper for sending a single message to a recipient list. All members
       
   322     of the recipient list will see the other recipients in the 'To' field.
       
   323 
       
   324     If auth_user is None, the EMAIL_HOST_USER setting is used.
       
   325     If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
       
   326 
       
   327     Note: The API for this method is frozen. New code wanting to extend the
       
   328     functionality should use the EmailMessage class directly.
       
   329     """
       
   330     connection = SMTPConnection(username=auth_user, password=auth_password,
       
   331                                 fail_silently=fail_silently)
       
   332     return EmailMessage(subject, message, from_email, recipient_list,
       
   333                         connection=connection).send()
       
   334 
       
   335 def send_mass_mail(datatuple, fail_silently=False, auth_user=None,
       
   336                    auth_password=None):
       
   337     """
       
   338     Given a datatuple of (subject, message, from_email, recipient_list), sends
       
   339     each message to each recipient list. Returns the number of e-mails sent.
       
   340 
       
   341     If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
       
   342     If auth_user and auth_password are set, they're used to log in.
       
   343     If auth_user is None, the EMAIL_HOST_USER setting is used.
       
   344     If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
       
   345 
       
   346     Note: The API for this method is frozen. New code wanting to extend the
       
   347     functionality should use the EmailMessage class directly.
       
   348     """
       
   349     connection = SMTPConnection(username=auth_user, password=auth_password,
       
   350                                 fail_silently=fail_silently)
       
   351     messages = [EmailMessage(subject, message, sender, recipient)
       
   352                 for subject, message, sender, recipient in datatuple]
       
   353     return connection.send_messages(messages)
       
   354 
       
   355 def mail_admins(subject, message, fail_silently=False):
       
   356     """Sends a message to the admins, as defined by the ADMINS setting."""
       
   357     EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
       
   358                  settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS]
       
   359                  ).send(fail_silently=fail_silently)
       
   360 
       
   361 def mail_managers(subject, message, fail_silently=False):
       
   362     """Sends a message to the managers, as defined by the MANAGERS setting."""
       
   363     EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
       
   364                  settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS]
       
   365                  ).send(fail_silently=fail_silently)