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