diff -r 57b4279d8c4e -r 03e267d67478 app/django/core/mail.py --- /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)