|
0
|
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) |