web/lib/django/core/mail.py
changeset 29 cc9b7e14412b
parent 28 b758351d191f
child 30 239f9bcae806
equal deleted inserted replaced
28:b758351d191f 29:cc9b7e14412b
     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)