|
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 if '\n' in val or '\r' in val: |
|
75 raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name)) |
|
76 try: |
|
77 val = force_unicode(val).encode('ascii') |
|
78 except UnicodeEncodeError: |
|
79 if name.lower() in ('to', 'from', 'cc'): |
|
80 result = [] |
|
81 for item in val.split(', '): |
|
82 nm, addr = parseaddr(item) |
|
83 nm = str(Header(nm, settings.DEFAULT_CHARSET)) |
|
84 result.append(formataddr((nm, str(addr)))) |
|
85 val = ', '.join(result) |
|
86 else: |
|
87 val = Header(force_unicode(val), settings.DEFAULT_CHARSET) |
|
88 return name, val |
|
89 |
|
90 class SafeMIMEText(MIMEText): |
|
91 def __setitem__(self, name, val): |
|
92 name, val = forbid_multi_line_headers(name, val) |
|
93 MIMEText.__setitem__(self, name, val) |
|
94 |
|
95 class SafeMIMEMultipart(MIMEMultipart): |
|
96 def __setitem__(self, name, val): |
|
97 name, val = forbid_multi_line_headers(name, val) |
|
98 MIMEMultipart.__setitem__(self, name, val) |
|
99 |
|
100 class SMTPConnection(object): |
|
101 """ |
|
102 A wrapper that manages the SMTP network connection. |
|
103 """ |
|
104 |
|
105 def __init__(self, host=None, port=None, username=None, password=None, |
|
106 use_tls=None, fail_silently=False): |
|
107 self.host = host or settings.EMAIL_HOST |
|
108 self.port = port or settings.EMAIL_PORT |
|
109 self.username = username or settings.EMAIL_HOST_USER |
|
110 self.password = password or settings.EMAIL_HOST_PASSWORD |
|
111 self.use_tls = (use_tls is not None) and use_tls or settings.EMAIL_USE_TLS |
|
112 self.fail_silently = fail_silently |
|
113 self.connection = None |
|
114 |
|
115 def open(self): |
|
116 """ |
|
117 Ensures we have a connection to the email server. Returns whether or |
|
118 not a new connection was required (True or False). |
|
119 """ |
|
120 if self.connection: |
|
121 # Nothing to do if the connection is already open. |
|
122 return False |
|
123 try: |
|
124 # If local_hostname is not specified, socket.getfqdn() gets used. |
|
125 # For performance, we use the cached FQDN for local_hostname. |
|
126 self.connection = smtplib.SMTP(self.host, self.port, |
|
127 local_hostname=DNS_NAME.get_fqdn()) |
|
128 if self.use_tls: |
|
129 self.connection.ehlo() |
|
130 self.connection.starttls() |
|
131 self.connection.ehlo() |
|
132 if self.username and self.password: |
|
133 self.connection.login(self.username, self.password) |
|
134 return True |
|
135 except: |
|
136 if not self.fail_silently: |
|
137 raise |
|
138 |
|
139 def close(self): |
|
140 """Closes the connection to the email server.""" |
|
141 try: |
|
142 try: |
|
143 self.connection.quit() |
|
144 except socket.sslerror: |
|
145 # This happens when calling quit() on a TLS connection |
|
146 # sometimes. |
|
147 self.connection.close() |
|
148 except: |
|
149 if self.fail_silently: |
|
150 return |
|
151 raise |
|
152 finally: |
|
153 self.connection = None |
|
154 |
|
155 def send_messages(self, email_messages): |
|
156 """ |
|
157 Sends one or more EmailMessage objects and returns the number of email |
|
158 messages sent. |
|
159 """ |
|
160 if not email_messages: |
|
161 return |
|
162 new_conn_created = self.open() |
|
163 if not self.connection: |
|
164 # We failed silently on open(). Trying to send would be pointless. |
|
165 return |
|
166 num_sent = 0 |
|
167 for message in email_messages: |
|
168 sent = self._send(message) |
|
169 if sent: |
|
170 num_sent += 1 |
|
171 if new_conn_created: |
|
172 self.close() |
|
173 return num_sent |
|
174 |
|
175 def _send(self, email_message): |
|
176 """A helper method that does the actual sending.""" |
|
177 if not email_message.to: |
|
178 return False |
|
179 try: |
|
180 self.connection.sendmail(email_message.from_email, |
|
181 email_message.recipients(), |
|
182 email_message.message().as_string()) |
|
183 except: |
|
184 if not self.fail_silently: |
|
185 raise |
|
186 return False |
|
187 return True |
|
188 |
|
189 class EmailMessage(object): |
|
190 """ |
|
191 A container for email information. |
|
192 """ |
|
193 content_subtype = 'plain' |
|
194 multipart_subtype = 'mixed' |
|
195 encoding = None # None => use settings default |
|
196 |
|
197 def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, |
|
198 connection=None, attachments=None, headers=None): |
|
199 """ |
|
200 Initialize a single email message (which can be sent to multiple |
|
201 recipients). |
|
202 |
|
203 All strings used to create the message can be unicode strings (or UTF-8 |
|
204 bytestrings). The SafeMIMEText class will handle any necessary encoding |
|
205 conversions. |
|
206 """ |
|
207 if to: |
|
208 self.to = list(to) |
|
209 else: |
|
210 self.to = [] |
|
211 if bcc: |
|
212 self.bcc = list(bcc) |
|
213 else: |
|
214 self.bcc = [] |
|
215 self.from_email = from_email or settings.DEFAULT_FROM_EMAIL |
|
216 self.subject = subject |
|
217 self.body = body |
|
218 self.attachments = attachments or [] |
|
219 self.extra_headers = headers or {} |
|
220 self.connection = connection |
|
221 |
|
222 def get_connection(self, fail_silently=False): |
|
223 if not self.connection: |
|
224 self.connection = SMTPConnection(fail_silently=fail_silently) |
|
225 return self.connection |
|
226 |
|
227 def message(self): |
|
228 encoding = self.encoding or settings.DEFAULT_CHARSET |
|
229 msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET), |
|
230 self.content_subtype, encoding) |
|
231 if self.attachments: |
|
232 body_msg = msg |
|
233 msg = SafeMIMEMultipart(_subtype=self.multipart_subtype) |
|
234 if self.body: |
|
235 msg.attach(body_msg) |
|
236 for attachment in self.attachments: |
|
237 if isinstance(attachment, MIMEBase): |
|
238 msg.attach(attachment) |
|
239 else: |
|
240 msg.attach(self._create_attachment(*attachment)) |
|
241 msg['Subject'] = self.subject |
|
242 msg['From'] = self.from_email |
|
243 msg['To'] = ', '.join(self.to) |
|
244 msg['Date'] = formatdate() |
|
245 msg['Message-ID'] = make_msgid() |
|
246 for name, value in self.extra_headers.items(): |
|
247 msg[name] = value |
|
248 return msg |
|
249 |
|
250 def recipients(self): |
|
251 """ |
|
252 Returns a list of all recipients of the email (includes direct |
|
253 addressees as well as Bcc entries). |
|
254 """ |
|
255 return self.to + self.bcc |
|
256 |
|
257 def send(self, fail_silently=False): |
|
258 """Sends the email message.""" |
|
259 return self.get_connection(fail_silently).send_messages([self]) |
|
260 |
|
261 def attach(self, filename=None, content=None, mimetype=None): |
|
262 """ |
|
263 Attaches a file with the given filename and content. The filename can |
|
264 be omitted (useful for multipart/alternative messages) and the mimetype |
|
265 is guessed, if not provided. |
|
266 |
|
267 If the first parameter is a MIMEBase subclass it is inserted directly |
|
268 into the resulting message attachments. |
|
269 """ |
|
270 if isinstance(filename, MIMEBase): |
|
271 assert content == mimetype == None |
|
272 self.attachments.append(filename) |
|
273 else: |
|
274 assert content is not None |
|
275 self.attachments.append((filename, content, mimetype)) |
|
276 |
|
277 def attach_file(self, path, mimetype=None): |
|
278 """Attaches a file from the filesystem.""" |
|
279 filename = os.path.basename(path) |
|
280 content = open(path, 'rb').read() |
|
281 self.attach(filename, content, mimetype) |
|
282 |
|
283 def _create_attachment(self, filename, content, mimetype=None): |
|
284 """ |
|
285 Converts the filename, content, mimetype triple into a MIME attachment |
|
286 object. |
|
287 """ |
|
288 if mimetype is None: |
|
289 mimetype, _ = mimetypes.guess_type(filename) |
|
290 if mimetype is None: |
|
291 mimetype = DEFAULT_ATTACHMENT_MIME_TYPE |
|
292 basetype, subtype = mimetype.split('/', 1) |
|
293 if basetype == 'text': |
|
294 attachment = SafeMIMEText(smart_str(content, |
|
295 settings.DEFAULT_CHARSET), subtype, settings.DEFAULT_CHARSET) |
|
296 else: |
|
297 # Encode non-text attachments with base64. |
|
298 attachment = MIMEBase(basetype, subtype) |
|
299 attachment.set_payload(content) |
|
300 Encoders.encode_base64(attachment) |
|
301 if filename: |
|
302 attachment.add_header('Content-Disposition', 'attachment', |
|
303 filename=filename) |
|
304 return attachment |
|
305 |
|
306 class EmailMultiAlternatives(EmailMessage): |
|
307 """ |
|
308 A version of EmailMessage that makes it easy to send multipart/alternative |
|
309 messages. For example, including text and HTML versions of the text is |
|
310 made easier. |
|
311 """ |
|
312 multipart_subtype = 'alternative' |
|
313 |
|
314 def attach_alternative(self, content, mimetype=None): |
|
315 """Attach an alternative content representation.""" |
|
316 self.attach(content=content, mimetype=mimetype) |
|
317 |
|
318 def send_mail(subject, message, from_email, recipient_list, |
|
319 fail_silently=False, auth_user=None, auth_password=None): |
|
320 """ |
|
321 Easy wrapper for sending a single message to a recipient list. All members |
|
322 of the recipient list will see the other recipients in the 'To' field. |
|
323 |
|
324 If auth_user is None, the EMAIL_HOST_USER setting is used. |
|
325 If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. |
|
326 |
|
327 Note: The API for this method is frozen. New code wanting to extend the |
|
328 functionality should use the EmailMessage class directly. |
|
329 """ |
|
330 connection = SMTPConnection(username=auth_user, password=auth_password, |
|
331 fail_silently=fail_silently) |
|
332 return EmailMessage(subject, message, from_email, recipient_list, |
|
333 connection=connection).send() |
|
334 |
|
335 def send_mass_mail(datatuple, fail_silently=False, auth_user=None, |
|
336 auth_password=None): |
|
337 """ |
|
338 Given a datatuple of (subject, message, from_email, recipient_list), sends |
|
339 each message to each recipient list. Returns the number of e-mails sent. |
|
340 |
|
341 If from_email is None, the DEFAULT_FROM_EMAIL setting is used. |
|
342 If auth_user and auth_password are set, they're used to log in. |
|
343 If auth_user is None, the EMAIL_HOST_USER setting is used. |
|
344 If auth_password is None, the EMAIL_HOST_PASSWORD setting is used. |
|
345 |
|
346 Note: The API for this method is frozen. New code wanting to extend the |
|
347 functionality should use the EmailMessage class directly. |
|
348 """ |
|
349 connection = SMTPConnection(username=auth_user, password=auth_password, |
|
350 fail_silently=fail_silently) |
|
351 messages = [EmailMessage(subject, message, sender, recipient) |
|
352 for subject, message, sender, recipient in datatuple] |
|
353 return connection.send_messages(messages) |
|
354 |
|
355 def mail_admins(subject, message, fail_silently=False): |
|
356 """Sends a message to the admins, as defined by the ADMINS setting.""" |
|
357 EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, |
|
358 settings.SERVER_EMAIL, [a[1] for a in settings.ADMINS] |
|
359 ).send(fail_silently=fail_silently) |
|
360 |
|
361 def mail_managers(subject, message, fail_silently=False): |
|
362 """Sends a message to the managers, as defined by the MANAGERS setting.""" |
|
363 EmailMessage(settings.EMAIL_SUBJECT_PREFIX + subject, message, |
|
364 settings.SERVER_EMAIL, [a[1] for a in settings.MANAGERS] |
|
365 ).send(fail_silently=fail_silently) |