|
29
|
1 |
import mimetypes |
|
|
2 |
import os |
|
|
3 |
import random |
|
|
4 |
import time |
|
|
5 |
from email import Charset, Encoders |
|
|
6 |
from email.MIMEText import MIMEText |
|
|
7 |
from email.MIMEMultipart import MIMEMultipart |
|
|
8 |
from email.MIMEBase import MIMEBase |
|
|
9 |
from email.Header import Header |
|
|
10 |
from email.Utils import formatdate, getaddresses, formataddr |
|
|
11 |
|
|
|
12 |
from django.conf import settings |
|
|
13 |
from django.core.mail.utils import DNS_NAME |
|
|
14 |
from django.utils.encoding import smart_str, force_unicode |
|
|
15 |
|
|
|
16 |
# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from |
|
|
17 |
# some spam filters. |
|
|
18 |
Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8') |
|
|
19 |
|
|
|
20 |
# Default MIME type to use on attachments (if it is not explicitly given |
|
|
21 |
# and cannot be guessed). |
|
|
22 |
DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream' |
|
|
23 |
|
|
|
24 |
|
|
|
25 |
class BadHeaderError(ValueError): |
|
|
26 |
pass |
|
|
27 |
|
|
|
28 |
|
|
|
29 |
# Copied from Python standard library, with the following modifications: |
|
|
30 |
# * Used cached hostname for performance. |
|
|
31 |
# * Added try/except to support lack of getpid() in Jython (#5496). |
|
|
32 |
def make_msgid(idstring=None): |
|
|
33 |
"""Returns a string suitable for RFC 2822 compliant Message-ID, e.g: |
|
|
34 |
|
|
|
35 |
<20020201195627.33539.96671@nightshade.la.mastaler.com> |
|
|
36 |
|
|
|
37 |
Optional idstring if given is a string used to strengthen the |
|
|
38 |
uniqueness of the message id. |
|
|
39 |
""" |
|
|
40 |
timeval = time.time() |
|
|
41 |
utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval)) |
|
|
42 |
try: |
|
|
43 |
pid = os.getpid() |
|
|
44 |
except AttributeError: |
|
|
45 |
# No getpid() in Jython, for example. |
|
|
46 |
pid = 1 |
|
|
47 |
randint = random.randrange(100000) |
|
|
48 |
if idstring is None: |
|
|
49 |
idstring = '' |
|
|
50 |
else: |
|
|
51 |
idstring = '.' + idstring |
|
|
52 |
idhost = DNS_NAME |
|
|
53 |
msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost) |
|
|
54 |
return msgid |
|
|
55 |
|
|
|
56 |
|
|
|
57 |
def forbid_multi_line_headers(name, val, encoding): |
|
|
58 |
"""Forbids multi-line headers, to prevent header injection.""" |
|
|
59 |
encoding = encoding or settings.DEFAULT_CHARSET |
|
|
60 |
val = force_unicode(val) |
|
|
61 |
if '\n' in val or '\r' in val: |
|
|
62 |
raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name)) |
|
|
63 |
try: |
|
|
64 |
val = val.encode('ascii') |
|
|
65 |
except UnicodeEncodeError: |
|
|
66 |
if name.lower() in ('to', 'from', 'cc'): |
|
|
67 |
result = [] |
|
|
68 |
for nm, addr in getaddresses((val,)): |
|
|
69 |
nm = str(Header(nm.encode(encoding), encoding)) |
|
|
70 |
result.append(formataddr((nm, str(addr)))) |
|
|
71 |
val = ', '.join(result) |
|
|
72 |
else: |
|
|
73 |
val = Header(val.encode(encoding), encoding) |
|
|
74 |
else: |
|
|
75 |
if name.lower() == 'subject': |
|
|
76 |
val = Header(val) |
|
|
77 |
return name, val |
|
|
78 |
|
|
|
79 |
class SafeMIMEText(MIMEText): |
|
|
80 |
|
|
|
81 |
def __init__(self, text, subtype, charset): |
|
|
82 |
self.encoding = charset |
|
|
83 |
MIMEText.__init__(self, text, subtype, charset) |
|
|
84 |
|
|
|
85 |
def __setitem__(self, name, val): |
|
|
86 |
name, val = forbid_multi_line_headers(name, val, self.encoding) |
|
|
87 |
MIMEText.__setitem__(self, name, val) |
|
|
88 |
|
|
|
89 |
class SafeMIMEMultipart(MIMEMultipart): |
|
|
90 |
|
|
|
91 |
def __init__(self, _subtype='mixed', boundary=None, _subparts=None, encoding=None, **_params): |
|
|
92 |
self.encoding = encoding |
|
|
93 |
MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params) |
|
|
94 |
|
|
|
95 |
def __setitem__(self, name, val): |
|
|
96 |
name, val = forbid_multi_line_headers(name, val, self.encoding) |
|
|
97 |
MIMEMultipart.__setitem__(self, name, val) |
|
|
98 |
|
|
|
99 |
class EmailMessage(object): |
|
|
100 |
""" |
|
|
101 |
A container for email information. |
|
|
102 |
""" |
|
|
103 |
content_subtype = 'plain' |
|
|
104 |
mixed_subtype = 'mixed' |
|
|
105 |
encoding = None # None => use settings default |
|
|
106 |
|
|
|
107 |
def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, |
|
|
108 |
connection=None, attachments=None, headers=None): |
|
|
109 |
""" |
|
|
110 |
Initialize a single email message (which can be sent to multiple |
|
|
111 |
recipients). |
|
|
112 |
|
|
|
113 |
All strings used to create the message can be unicode strings |
|
|
114 |
(or UTF-8 bytestrings). The SafeMIMEText class will handle any |
|
|
115 |
necessary encoding conversions. |
|
|
116 |
""" |
|
|
117 |
if to: |
|
|
118 |
assert not isinstance(to, basestring), '"to" argument must be a list or tuple' |
|
|
119 |
self.to = list(to) |
|
|
120 |
else: |
|
|
121 |
self.to = [] |
|
|
122 |
if bcc: |
|
|
123 |
assert not isinstance(bcc, basestring), '"bcc" argument must be a list or tuple' |
|
|
124 |
self.bcc = list(bcc) |
|
|
125 |
else: |
|
|
126 |
self.bcc = [] |
|
|
127 |
self.from_email = from_email or settings.DEFAULT_FROM_EMAIL |
|
|
128 |
self.subject = subject |
|
|
129 |
self.body = body |
|
|
130 |
self.attachments = attachments or [] |
|
|
131 |
self.extra_headers = headers or {} |
|
|
132 |
self.connection = connection |
|
|
133 |
|
|
|
134 |
def get_connection(self, fail_silently=False): |
|
|
135 |
from django.core.mail import get_connection |
|
|
136 |
if not self.connection: |
|
|
137 |
self.connection = get_connection(fail_silently=fail_silently) |
|
|
138 |
return self.connection |
|
|
139 |
|
|
|
140 |
def message(self): |
|
|
141 |
encoding = self.encoding or settings.DEFAULT_CHARSET |
|
|
142 |
msg = SafeMIMEText(smart_str(self.body, encoding), |
|
|
143 |
self.content_subtype, encoding) |
|
|
144 |
msg = self._create_message(msg) |
|
|
145 |
msg['Subject'] = self.subject |
|
|
146 |
msg['From'] = self.extra_headers.get('From', self.from_email) |
|
|
147 |
msg['To'] = ', '.join(self.to) |
|
|
148 |
|
|
|
149 |
# Email header names are case-insensitive (RFC 2045), so we have to |
|
|
150 |
# accommodate that when doing comparisons. |
|
|
151 |
header_names = [key.lower() for key in self.extra_headers] |
|
|
152 |
if 'date' not in header_names: |
|
|
153 |
msg['Date'] = formatdate() |
|
|
154 |
if 'message-id' not in header_names: |
|
|
155 |
msg['Message-ID'] = make_msgid() |
|
|
156 |
for name, value in self.extra_headers.items(): |
|
|
157 |
if name.lower() == 'from': # From is already handled |
|
|
158 |
continue |
|
|
159 |
msg[name] = value |
|
|
160 |
return msg |
|
|
161 |
|
|
|
162 |
def recipients(self): |
|
|
163 |
""" |
|
|
164 |
Returns a list of all recipients of the email (includes direct |
|
|
165 |
addressees as well as Bcc entries). |
|
|
166 |
""" |
|
|
167 |
return self.to + self.bcc |
|
|
168 |
|
|
|
169 |
def send(self, fail_silently=False): |
|
|
170 |
"""Sends the email message.""" |
|
|
171 |
if not self.recipients(): |
|
|
172 |
# Don't bother creating the network connection if there's nobody to |
|
|
173 |
# send to. |
|
|
174 |
return 0 |
|
|
175 |
return self.get_connection(fail_silently).send_messages([self]) |
|
|
176 |
|
|
|
177 |
def attach(self, filename=None, content=None, mimetype=None): |
|
|
178 |
""" |
|
|
179 |
Attaches a file with the given filename and content. The filename can |
|
|
180 |
be omitted and the mimetype is guessed, if not provided. |
|
|
181 |
|
|
|
182 |
If the first parameter is a MIMEBase subclass it is inserted directly |
|
|
183 |
into the resulting message attachments. |
|
|
184 |
""" |
|
|
185 |
if isinstance(filename, MIMEBase): |
|
|
186 |
assert content == mimetype == None |
|
|
187 |
self.attachments.append(filename) |
|
|
188 |
else: |
|
|
189 |
assert content is not None |
|
|
190 |
self.attachments.append((filename, content, mimetype)) |
|
|
191 |
|
|
|
192 |
def attach_file(self, path, mimetype=None): |
|
|
193 |
"""Attaches a file from the filesystem.""" |
|
|
194 |
filename = os.path.basename(path) |
|
|
195 |
content = open(path, 'rb').read() |
|
|
196 |
self.attach(filename, content, mimetype) |
|
|
197 |
|
|
|
198 |
def _create_message(self, msg): |
|
|
199 |
return self._create_attachments(msg) |
|
|
200 |
|
|
|
201 |
def _create_attachments(self, msg): |
|
|
202 |
if self.attachments: |
|
|
203 |
encoding = self.encoding or settings.DEFAULT_CHARSET |
|
|
204 |
body_msg = msg |
|
|
205 |
msg = SafeMIMEMultipart(_subtype=self.mixed_subtype, encoding=encoding) |
|
|
206 |
if self.body: |
|
|
207 |
msg.attach(body_msg) |
|
|
208 |
for attachment in self.attachments: |
|
|
209 |
if isinstance(attachment, MIMEBase): |
|
|
210 |
msg.attach(attachment) |
|
|
211 |
else: |
|
|
212 |
msg.attach(self._create_attachment(*attachment)) |
|
|
213 |
return msg |
|
|
214 |
|
|
|
215 |
def _create_mime_attachment(self, content, mimetype): |
|
|
216 |
""" |
|
|
217 |
Converts the content, mimetype pair into a MIME attachment object. |
|
|
218 |
""" |
|
|
219 |
basetype, subtype = mimetype.split('/', 1) |
|
|
220 |
if basetype == 'text': |
|
|
221 |
encoding = self.encoding or settings.DEFAULT_CHARSET |
|
|
222 |
attachment = SafeMIMEText(smart_str(content, encoding), subtype, encoding) |
|
|
223 |
else: |
|
|
224 |
# Encode non-text attachments with base64. |
|
|
225 |
attachment = MIMEBase(basetype, subtype) |
|
|
226 |
attachment.set_payload(content) |
|
|
227 |
Encoders.encode_base64(attachment) |
|
|
228 |
return attachment |
|
|
229 |
|
|
|
230 |
def _create_attachment(self, filename, content, mimetype=None): |
|
|
231 |
""" |
|
|
232 |
Converts the filename, content, mimetype triple into a MIME attachment |
|
|
233 |
object. |
|
|
234 |
""" |
|
|
235 |
if mimetype is None: |
|
|
236 |
mimetype, _ = mimetypes.guess_type(filename) |
|
|
237 |
if mimetype is None: |
|
|
238 |
mimetype = DEFAULT_ATTACHMENT_MIME_TYPE |
|
|
239 |
attachment = self._create_mime_attachment(content, mimetype) |
|
|
240 |
if filename: |
|
|
241 |
attachment.add_header('Content-Disposition', 'attachment', |
|
|
242 |
filename=filename) |
|
|
243 |
return attachment |
|
|
244 |
|
|
|
245 |
|
|
|
246 |
class EmailMultiAlternatives(EmailMessage): |
|
|
247 |
""" |
|
|
248 |
A version of EmailMessage that makes it easy to send multipart/alternative |
|
|
249 |
messages. For example, including text and HTML versions of the text is |
|
|
250 |
made easier. |
|
|
251 |
""" |
|
|
252 |
alternative_subtype = 'alternative' |
|
|
253 |
|
|
|
254 |
def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, |
|
|
255 |
connection=None, attachments=None, headers=None, alternatives=None): |
|
|
256 |
""" |
|
|
257 |
Initialize a single email message (which can be sent to multiple |
|
|
258 |
recipients). |
|
|
259 |
|
|
|
260 |
All strings used to create the message can be unicode strings (or UTF-8 |
|
|
261 |
bytestrings). The SafeMIMEText class will handle any necessary encoding |
|
|
262 |
conversions. |
|
|
263 |
""" |
|
|
264 |
super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers) |
|
|
265 |
self.alternatives=alternatives or [] |
|
|
266 |
|
|
|
267 |
def attach_alternative(self, content, mimetype): |
|
|
268 |
"""Attach an alternative content representation.""" |
|
|
269 |
assert content is not None |
|
|
270 |
assert mimetype is not None |
|
|
271 |
self.alternatives.append((content, mimetype)) |
|
|
272 |
|
|
|
273 |
def _create_message(self, msg): |
|
|
274 |
return self._create_attachments(self._create_alternatives(msg)) |
|
|
275 |
|
|
|
276 |
def _create_alternatives(self, msg): |
|
|
277 |
encoding = self.encoding or settings.DEFAULT_CHARSET |
|
|
278 |
if self.alternatives: |
|
|
279 |
body_msg = msg |
|
|
280 |
msg = SafeMIMEMultipart(_subtype=self.alternative_subtype, encoding=encoding) |
|
|
281 |
if self.body: |
|
|
282 |
msg.attach(body_msg) |
|
|
283 |
for alternative in self.alternatives: |
|
|
284 |
msg.attach(self._create_mime_attachment(*alternative)) |
|
|
285 |
return msg |