2007-05-03 19:35:11 +08:00
|
|
|
"""
|
|
|
|
Tools for sending email.
|
|
|
|
"""
|
2005-07-13 09:25:57 +08:00
|
|
|
|
2006-04-11 11:19:57 +08:00
|
|
|
from django.conf import settings
|
2007-06-27 17:44:55 +08:00
|
|
|
from email import Charset, Encoders
|
2005-07-13 09:25:57 +08:00
|
|
|
from email.MIMEText import MIMEText
|
2007-06-27 17:44:55 +08:00
|
|
|
from email.MIMEMultipart import MIMEMultipart
|
|
|
|
from email.MIMEBase import MIMEBase
|
2006-05-14 01:18:42 +08:00
|
|
|
from email.Header import Header
|
2007-02-25 23:55:31 +08:00
|
|
|
from email.Utils import formatdate
|
2007-06-27 17:44:55 +08:00
|
|
|
import mimetypes
|
2007-05-03 19:35:11 +08:00
|
|
|
import os
|
2007-02-25 23:55:31 +08:00
|
|
|
import smtplib
|
2006-11-07 23:32:18 +08:00
|
|
|
import socket
|
|
|
|
import time
|
|
|
|
import random
|
2005-07-13 09:25:57 +08:00
|
|
|
|
2007-05-03 20:08:31 +08:00
|
|
|
# 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')
|
|
|
|
|
2007-06-27 17:44:55 +08:00
|
|
|
# Default MIME type to use on attachments (if it is not explicitly given
|
|
|
|
# and cannot be guessed).
|
|
|
|
DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream'
|
|
|
|
|
2007-02-17 14:01:17 +08:00
|
|
|
# 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()
|
2006-11-10 11:28:58 +08:00
|
|
|
|
2007-05-03 19:35:11 +08:00
|
|
|
# Copied from Python standard library and modified to used the cached hostname
|
|
|
|
# for performance.
|
|
|
|
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))
|
|
|
|
pid = os.getpid()
|
|
|
|
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
|
|
|
|
|
2005-12-30 06:12:54 +08:00
|
|
|
class BadHeaderError(ValueError):
|
|
|
|
pass
|
|
|
|
|
2007-06-27 17:44:55 +08:00
|
|
|
class SafeHeaderMixin(object):
|
2005-12-30 04:33:56 +08:00
|
|
|
def __setitem__(self, name, val):
|
|
|
|
"Forbids multi-line headers, to prevent header injection."
|
|
|
|
if '\n' in val or '\r' in val:
|
2005-12-30 06:12:54 +08:00
|
|
|
raise BadHeaderError, "Header values can't contain newlines (got %r for header %r)" % (val, name)
|
2006-05-14 01:18:42 +08:00
|
|
|
if name == "Subject":
|
|
|
|
val = Header(val, settings.DEFAULT_CHARSET)
|
2007-06-27 17:44:55 +08:00
|
|
|
# Note: using super() here is safe; any __setitem__ overrides must use
|
|
|
|
# the same argument signature.
|
|
|
|
super(SafeHeaderMixin, self).__setitem__(name, val)
|
|
|
|
|
|
|
|
class SafeMIMEText(MIMEText, SafeHeaderMixin):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class SafeMIMEMultipart(MIMEMultipart, SafeHeaderMixin):
|
|
|
|
pass
|
2005-12-30 04:33:56 +08:00
|
|
|
|
2007-05-03 19:35:11 +08:00
|
|
|
class SMTPConnection(object):
|
|
|
|
"""
|
|
|
|
A wrapper that manages the SMTP network connection.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, host=None, port=None, username=None, password=None,
|
2007-05-03 21:35:02 +08:00
|
|
|
use_tls=None, fail_silently=False):
|
|
|
|
self.host = host or settings.EMAIL_HOST
|
2007-05-03 21:53:42 +08:00
|
|
|
self.port = port or settings.EMAIL_PORT
|
2007-05-03 21:35:02 +08:00
|
|
|
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
|
2007-05-03 19:35:11 +08:00
|
|
|
self.fail_silently = fail_silently
|
|
|
|
self.connection = None
|
|
|
|
|
|
|
|
def open(self):
|
|
|
|
"""
|
|
|
|
Ensure we have a connection to the email server. Returns whether or not
|
|
|
|
a new connection was required.
|
|
|
|
"""
|
|
|
|
if self.connection:
|
|
|
|
# Nothing to do if the connection is already open.
|
|
|
|
return False
|
|
|
|
try:
|
|
|
|
self.connection = smtplib.SMTP(self.host, self.port)
|
2007-05-03 21:35:02 +08:00
|
|
|
if self.use_tls:
|
|
|
|
self.connection.ehlo()
|
|
|
|
self.connection.starttls()
|
|
|
|
self.connection.ehlo()
|
2007-05-03 19:35:11 +08:00
|
|
|
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):
|
|
|
|
"""Close the connection to the email server."""
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
self.connection.quit()
|
2007-05-03 21:35:02 +08:00
|
|
|
except socket.sslerror:
|
|
|
|
# This happens when calling quit() on a TLS connection
|
|
|
|
# sometimes.
|
|
|
|
self.connection.close()
|
2007-05-03 19:35:11 +08:00
|
|
|
except:
|
|
|
|
if self.fail_silently:
|
|
|
|
return
|
|
|
|
raise
|
|
|
|
finally:
|
|
|
|
self.connection = None
|
|
|
|
|
|
|
|
def send_messages(self, email_messages):
|
|
|
|
"""
|
|
|
|
Send one or more EmailMessage objects and return 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,
|
2007-05-03 22:38:45 +08:00
|
|
|
email_message.recipients(),
|
|
|
|
email_message.message().as_string())
|
2007-05-03 19:35:11 +08:00
|
|
|
except:
|
|
|
|
if not self.fail_silently:
|
|
|
|
raise
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
class EmailMessage(object):
|
|
|
|
"""
|
|
|
|
A container for email information.
|
|
|
|
"""
|
2007-06-27 20:18:05 +08:00
|
|
|
content_subtype = 'plain'
|
|
|
|
multipart_subtype = 'mixed'
|
|
|
|
|
2007-06-27 17:44:55 +08:00
|
|
|
def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
|
|
|
|
connection=None, attachments=None):
|
2007-05-03 19:35:11 +08:00
|
|
|
self.to = to or []
|
2007-05-03 22:38:45 +08:00
|
|
|
self.bcc = bcc or []
|
|
|
|
self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
|
2007-05-03 19:35:11 +08:00
|
|
|
self.subject = subject
|
|
|
|
self.body = body
|
2007-06-27 17:44:55 +08:00
|
|
|
self.attachments = attachments or []
|
2007-05-03 19:35:11 +08:00
|
|
|
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):
|
2007-06-27 20:18:05 +08:00
|
|
|
msg = SafeMIMEText(self.body, self.content_subtype, settings.DEFAULT_CHARSET)
|
2007-06-27 17:44:55 +08:00
|
|
|
if self.attachments:
|
|
|
|
body_msg = msg
|
2007-06-27 20:18:05 +08:00
|
|
|
msg = SafeMIMEMultipart(_subtype=self.multipart_subtype)
|
2007-06-27 17:44:55 +08:00
|
|
|
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))
|
2007-05-03 19:35:11 +08:00
|
|
|
msg['Subject'] = self.subject
|
|
|
|
msg['From'] = self.from_email
|
|
|
|
msg['To'] = ', '.join(self.to)
|
|
|
|
msg['Date'] = formatdate()
|
|
|
|
msg['Message-ID'] = make_msgid()
|
2007-05-03 22:38:45 +08:00
|
|
|
if self.bcc:
|
|
|
|
msg['Bcc'] = ', '.join(self.bcc)
|
2007-05-03 19:50:43 +08:00
|
|
|
return msg
|
2007-05-03 19:35:11 +08:00
|
|
|
|
2007-05-03 22:38:45 +08:00
|
|
|
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
|
|
|
|
|
2007-05-03 19:35:11 +08:00
|
|
|
def send(self, fail_silently=False):
|
|
|
|
"""Send the email message."""
|
|
|
|
return self.get_connection(fail_silently).send_messages([self])
|
|
|
|
|
2007-06-27 20:18:05 +08:00
|
|
|
def attach(self, filename=None, content=None, mimetype=None):
|
2007-06-27 17:44:55 +08:00
|
|
|
"""
|
2007-06-27 20:18:05 +08:00
|
|
|
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.
|
2007-06-27 17:44:55 +08:00
|
|
|
|
2007-06-27 20:18:05 +08:00
|
|
|
If the first parameter is a MIMEBase subclass it is inserted directly
|
|
|
|
into the resulting message attachments.
|
2007-06-27 17:44:55 +08:00
|
|
|
"""
|
|
|
|
if isinstance(filename, MIMEBase):
|
2007-06-27 20:18:05 +08:00
|
|
|
assert content == mimetype == None
|
2007-06-27 17:44:55 +08:00
|
|
|
self.attachements.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):
|
|
|
|
"""
|
|
|
|
Convert 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(content, subtype, settings.DEFAULT_CHARSET)
|
|
|
|
else:
|
|
|
|
# Encode non-text attachments with base64.
|
|
|
|
attachment = MIMEBase(basetype, subtype)
|
|
|
|
attachment.set_payload(content)
|
|
|
|
Encoders.encode_base64(attachment)
|
2007-06-27 20:18:05 +08:00
|
|
|
if filename:
|
|
|
|
attachment.add_header('Content-Disposition', 'attachment', filename=filename)
|
2007-06-27 17:44:55 +08:00
|
|
|
return attachment
|
|
|
|
|
2007-06-27 20:18:05 +08:00
|
|
|
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)
|
|
|
|
|
2007-02-26 00:29:09 +08:00
|
|
|
def send_mail(subject, message, from_email, recipient_list, fail_silently=False, auth_user=None, auth_password=None):
|
2005-07-13 09:25:57 +08:00
|
|
|
"""
|
|
|
|
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.
|
2007-02-26 00:29:09 +08:00
|
|
|
|
|
|
|
If auth_user is None, the EMAIL_HOST_USER setting is used.
|
|
|
|
If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
|
2007-05-03 19:35:11 +08:00
|
|
|
|
|
|
|
NOTE: This method is deprecated. It exists for backwards compatibility.
|
|
|
|
New code should use the EmailMessage class directly.
|
2005-07-13 09:25:57 +08:00
|
|
|
"""
|
2007-05-03 19:35:11 +08:00
|
|
|
connection = SMTPConnection(username=auth_user, password=auth_password,
|
|
|
|
fail_silently=fail_silently)
|
|
|
|
return EmailMessage(subject, message, from_email, recipient_list, connection=connection).send()
|
2005-07-13 09:25:57 +08:00
|
|
|
|
2007-02-26 00:29:09 +08:00
|
|
|
def send_mass_mail(datatuple, fail_silently=False, auth_user=None, auth_password=None):
|
2005-07-13 09:25:57 +08:00
|
|
|
"""
|
|
|
|
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.
|
2006-03-23 03:47:15 +08:00
|
|
|
If auth_user and auth_password are set, they're used to log in.
|
2007-02-26 00:29:09 +08:00
|
|
|
If auth_user is None, the EMAIL_HOST_USER setting is used.
|
|
|
|
If auth_password is None, the EMAIL_HOST_PASSWORD setting is used.
|
2007-05-03 19:35:11 +08:00
|
|
|
|
|
|
|
NOTE: This method is deprecated. It exists for backwards compatibility.
|
|
|
|
New code should use the EmailMessage class directly.
|
2005-07-13 09:25:57 +08:00
|
|
|
"""
|
2007-05-03 19:35:11 +08:00
|
|
|
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)
|
2005-07-13 09:25:57 +08:00
|
|
|
|
|
|
|
def mail_admins(subject, message, fail_silently=False):
|
2006-03-23 03:47:15 +08:00
|
|
|
"Sends a message to the admins, as defined by the ADMINS setting."
|
2007-05-03 19:35:11 +08:00
|
|
|
EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
|
|
|
|
settings.SERVER_EMAIL, [a[1] for a in
|
|
|
|
settings.ADMINS]).send(fail_silently=fail_silently)
|
2005-07-13 09:25:57 +08:00
|
|
|
|
|
|
|
def mail_managers(subject, message, fail_silently=False):
|
2006-03-23 03:47:15 +08:00
|
|
|
"Sends a message to the managers, as defined by the MANAGERS setting."
|
2007-05-03 19:35:11 +08:00
|
|
|
EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message,
|
|
|
|
settings.SERVER_EMAIL, [a[1] for a in
|
|
|
|
settings.MANAGERS]).send(fail_silently=fail_silently)
|
|
|
|
|