app/django/core/mail.py
changeset 54 03e267d67478
child 323 ff1a9aa48cfd
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/app/django/core/mail.py	Fri Jul 18 18:22:23 2008 +0000
@@ -0,0 +1,365 @@
+"""
+Tools for sending email.
+"""
+
+import mimetypes
+import os
+import smtplib
+import socket
+import time
+import random
+from email import Charset, Encoders
+from email.MIMEText import MIMEText
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEBase import MIMEBase
+from email.Header import Header
+from email.Utils import formatdate, parseaddr, formataddr
+
+from django.conf import settings
+from django.utils.encoding import smart_str, force_unicode
+
+# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
+# some spam filters.
+Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
+
+# Default MIME type to use on attachments (if it is not explicitly given
+# and cannot be guessed).
+DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream'
+
+# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of
+# seconds, which slows down the restart of the server.
+class CachedDnsName(object):
+    def __str__(self):
+        return self.get_fqdn()
+
+    def get_fqdn(self):
+        if not hasattr(self, '_fqdn'):
+            self._fqdn = socket.getfqdn()
+        return self._fqdn
+
+DNS_NAME = CachedDnsName()
+
+# Copied from Python standard library, with the following modifications:
+# * Used cached hostname for performance.
+# * Added try/except to support lack of getpid() in Jython (#5496).
+def make_msgid(idstring=None):
+    """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
+
+    <20020201195627.33539.96671@nightshade.la.mastaler.com>
+
+    Optional idstring if given is a string used to strengthen the
+    uniqueness of the message id.
+    """
+    timeval = time.time()
+    utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
+    try:
+        pid = os.getpid()
+    except AttributeError:
+        # No getpid() in Jython, for example.
+        pid = 1
+    randint = random.randrange(100000)
+    if idstring is None:
+        idstring = ''
+    else:
+        idstring = '.' + idstring
+    idhost = DNS_NAME
+    msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
+    return msgid
+
+class BadHeaderError(ValueError):
+    pass
+
+def forbid_multi_line_headers(name, val):
+    """Forbids multi-line headers, to prevent header injection."""
+    if '\n' in val or '\r' in val:
+        raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name))
+    try:
+        val = force_unicode(val).encode('ascii')
+    except UnicodeEncodeError:
+        if name.lower() in ('to', 'from', 'cc'):
+            result = []
+            for item in val.split(', '):
+                nm, addr = parseaddr(item)
+                nm = str(Header(nm, settings.DEFAULT_CHARSET))
+                result.append(formataddr((nm, str(addr))))
+            val = ', '.join(result)
+        else:
+            val = Header(force_unicode(val), settings.DEFAULT_CHARSET)
+    return name, val
+
+class SafeMIMEText(MIMEText):
+    def __setitem__(self, name, val):
+        name, val = forbid_multi_line_headers(name, val)
+        MIMEText.__setitem__(self, name, val)
+
+class SafeMIMEMultipart(MIMEMultipart):
+    def __setitem__(self, name, val):
+        name, val = forbid_multi_line_headers(name, val)
+        MIMEMultipart.__setitem__(self, name, val)
+
+class SMTPConnection(object):
+    """
+    A wrapper that manages the SMTP network connection.
+    """
+
+    def __init__(self, host=None, port=None, username=None, password=None,
+                 use_tls=None, fail_silently=False):
+        self.host = host or settings.EMAIL_HOST
+        self.port = port or settings.EMAIL_PORT
+        self.username = username or settings.EMAIL_HOST_USER
+        self.password = password or settings.EMAIL_HOST_PASSWORD
+        self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS
+        self.fail_silently = fail_silently
+        self.connection = None
+
+    def open(self):
+        """
+        Ensures we have a connection to the email server. Returns whether or
+        not a new connection was required (True or False).
+        """
+        if self.connection:
+            # Nothing to do if the connection is already open.
+            return False
+        try:
+            # If local_hostname is not specified, socket.getfqdn() gets used.
+            # For performance, we use the cached FQDN for local_hostname.
+            self.connection = smtplib.SMTP(self.host, self.port,
+                                           local_hostname=DNS_NAME.get_fqdn())
+            if self.use_tls:
+                self.connection.ehlo()
+                self.connection.starttls()
+                self.connection.ehlo()
+            if self.username and self.password:
+                self.connection.login(self.username, self.password)
+            return True
+        except:
+            if not self.fail_silently:
+                raise
+
+    def close(self):
+        """Closes the connection to the email server."""
+        try:
+            try:
+                self.connection.quit()
+            except socket.sslerror:
+                # This happens when calling quit() on a TLS connection
+                # sometimes.
+                self.connection.close()
+            except:
+                if self.fail_silently:
+                    return
+                raise
+        finally:
+            self.connection = None
+
+    def send_messages(self, email_messages):
+        """
+        Sends one or more EmailMessage objects and returns the number of email
+        messages sent.
+        """
+        if not email_messages:
+            return
+        new_conn_created = self.open()
+        if not self.connection:
+            # We failed silently on open(). Trying to send would be pointless.
+            return
+        num_sent = 0
+        for message in email_messages:
+            sent = self._send(message)
+            if sent:
+                num_sent += 1
+        if new_conn_created:
+            self.close()
+        return num_sent
+
+    def _send(self, email_message):
+        """A helper method that does the actual sending."""
+        if not email_message.to:
+            return False
+        try:
+            self.connection.sendmail(email_message.from_email,
+                    email_message.recipients(),
+                    email_message.message().as_string())
+        except:
+            if not self.fail_silently:
+                raise
+            return False
+        return True
+
+class EmailMessage(object):
+    """
+    A container for email information.
+    """
+    content_subtype = 'plain'
+    multipart_subtype = 'mixed'
+    encoding = None     # None => use settings default
+
+    def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
+            connection=None, attachments=None, headers=None):
+        """
+        Initialize a single email message (which can be sent to multiple
+        recipients).
+
+        All strings used to create the message can be unicode strings (or UTF-8
+        bytestrings). The SafeMIMEText class will handle any necessary encoding
+        conversions.
+        """
+        if to:
+            self.to = list(to)
+        else:
+            self.to = []
+        if bcc:
+            self.bcc = list(bcc)
+        else:
+            self.bcc = []
+        self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
+        self.subject = subject
+        self.body = body
+        self.attachments = attachments or []
+        self.extra_headers = headers or {}
+        self.connection = connection
+
+    def get_connection(self, fail_silently=False):
+        if not self.connection:
+            self.connection = SMTPConnection(fail_silently=fail_silently)
+        return self.connection
+
+    def message(self):
+        encoding = self.encoding or settings.DEFAULT_CHARSET
+        msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),
+                           self.content_subtype, encoding)
+        if self.attachments:
+            body_msg = msg
+            msg = SafeMIMEMultipart(_subtype=self.multipart_subtype)
+            if self.body:
+                msg.attach(body_msg)
+            for attachment in self.attachments:
+                if isinstance(attachment, MIMEBase):
+                    msg.attach(attachment)
+                else:
+                    msg.attach(self._create_attachment(*attachment))
+        msg['Subject'] = self.subject
+        msg['From'] = self.from_email
+        msg['To'] = ', '.join(self.to)
+        msg['Date'] = formatdate()
+        msg['Message-ID'] = make_msgid()
+        for name, value in self.extra_headers.items():
+            msg[name] = value
+        return msg
+
+    def recipients(self):
+        """
+        Returns a list of all recipients of the email (includes direct
+        addressees as well as Bcc entries).
+        """
+        return self.to + self.bcc
+
+    def send(self, fail_silently=False):
+        """Sends the email message."""
+        return self.get_connection(fail_silently).send_messages([self])
+
+    def attach(self, filename=None, content=None, mimetype=None):
+        """
+        Attaches a file with the given filename and content. The filename can
+        be omitted (useful for multipart/alternative messages) and the mimetype
+        is guessed, if not provided.
+
+        If the first parameter is a MIMEBase subclass it is inserted directly
+        into the resulting message attachments.
+        """
+        if isinstance(filename, MIMEBase):
+            assert content == mimetype == None
+            self.attachments.append(filename)
+        else:
+            assert content is not None
+            self.attachments.append((filename, content, mimetype))
+
+    def attach_file(self, path, mimetype=None):
+        """Attaches a file from the filesystem."""
+        filename = os.path.basename(path)
+        content = open(path, 'rb').read()
+        self.attach(filename, content, mimetype)
+
+    def _create_attachment(self, filename, content, mimetype=None):
+        """
+        Converts the filename, content, mimetype triple into a MIME attachment
+        object.
+        """
+        if mimetype is None:
+            mimetype, _ = mimetypes.guess_type(filename)
+            if mimetype is None:
+                mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
+        basetype, subtype = mimetype.split('/', 1)
+        if basetype == 'text':
+            attachment = SafeMIMEText(smart_str(content,
+                settings.DEFAULT_CHARSET), subtype, settings.DEFAULT_CHARSET)
+        else:
+            # Encode non-text attachments with base64.
+            attachment = MIMEBase(basetype, subtype)
+            attachment.set_payload(content)
+            Encoders.encode_base64(attachment)
+        if filename:
+            attachment.add_header('Content-Disposition', 'attachment',
+                                  filename=filename)
+        return attachment
+
+class EmailMultiAlternatives(EmailMessage):
+    """
+    A version of EmailMessage that makes it easy to send multipart/alternative
+    messages. For example, including text and HTML versions of the text is
+    made easier.
+    """
+    multipart_subtype = 'alternative'
+
+    def attach_alternative(self, content, mimetype=None):
+        """Attach an alternative content representation."""
+        self.attach(content=content, mimetype=mimetype)
+
+def send_mail(subject, message, from_email, recipient_list,
+              fail_silently=False, auth_user=None, auth_password=None):
+    """
+    Easy wrapper for sending a single message to a recipient list. All members
+    of the recipient list will see the other recipients in the 'To' field.
+
+    If auth_user is None, the EMAIL_HOST_USER setting is used.
+    If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
+
+    Note: The API for this method is frozen. New code wanting to extend the
+    functionality should use the EmailMessage class directly.
+    """
+    connection = SMTPConnection(username=auth_user, password=auth_password,
+                                fail_silently=fail_silently)
+    return EmailMessage(subject, message, from_email, recipient_list,
+                        connection=connection).send()
+
+def send_mass_mail(datatuple, fail_silently=False, auth_user=None,
+                   auth_password=None):
+    """
+    Given a datatuple of (subject, message, from_email, recipient_list), sends
+    each message to each recipient list. Returns the number of e-mails sent.
+
+    If from_email is None, the DEFAULT_FROM_EMAIL setting is used.
+    If auth_user and auth_password are set, they're used to log in.
+    If auth_user is None, the EMAIL_HOST_USER setting is used.
+    If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
+
+    Note: The API for this method is frozen. New code wanting to extend the
+    functionality should use the EmailMessage class directly.
+    """
+    connection = SMTPConnection(username=auth_user, password=auth_password,
+                                fail_silently=fail_silently)
+    messages = [EmailMessage(subject, message, sender, recipient)
+                for subject, message, sender, recipient in datatuple]
+    return connection.send_messages(messages)
+
+def mail_admins(subject, message, fail_silently=False):
+    """Sends a message to the admins, as defined by the ADMINS setting."""
+    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
+                 settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS]
+                 ).send(fail_silently=fail_silently)
+
+def mail_managers(subject, message, fail_silently=False):
+    """Sends a message to the managers, as defined by the MANAGERS setting."""
+    EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
+                 settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS]
+                 ).send(fail_silently=fail_silently)