Mercurial > kallithea
comparison rhodecode/lib/rcmail/response.py @ 2031:82a88013a3fd
merge 1.3 into stable
author | Marcin Kuzminski <marcin@python-works.com> |
---|---|
date | Sun, 26 Feb 2012 17:25:09 +0200 |
parents | b3a3890b7160 |
children | d95ef6587bca |
comparison
equal
deleted
inserted
replaced
2005:ab0e122b38a7 | 2031:82a88013a3fd |
---|---|
1 # The code in this module is entirely lifted from the Lamson project | |
2 # (http://lamsonproject.org/). Its copyright is: | |
3 | |
4 # Copyright (c) 2008, Zed A. Shaw | |
5 # All rights reserved. | |
6 | |
7 # It is provided under this license: | |
8 | |
9 # Redistribution and use in source and binary forms, with or without | |
10 # modification, are permitted provided that the following conditions are met: | |
11 | |
12 # * Redistributions of source code must retain the above copyright notice, this | |
13 # list of conditions and the following disclaimer. | |
14 | |
15 # * Redistributions in binary form must reproduce the above copyright notice, | |
16 # this list of conditions and the following disclaimer in the documentation | |
17 # and/or other materials provided with the distribution. | |
18 | |
19 # * Neither the name of the Zed A. Shaw nor the names of its contributors may | |
20 # be used to endorse or promote products derived from this software without | |
21 # specific prior written permission. | |
22 | |
23 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
24 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
25 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | |
26 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE | |
27 # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, | |
28 # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
29 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
30 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
31 # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, | |
32 # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
33 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
34 # POSSIBILITY OF SUCH DAMAGE. | |
35 | |
36 import os | |
37 import mimetypes | |
38 import string | |
39 from email import encoders | |
40 from email.charset import Charset | |
41 from email.utils import parseaddr | |
42 from email.mime.base import MIMEBase | |
43 | |
44 ADDRESS_HEADERS_WHITELIST = ['From', 'To', 'Delivered-To', 'Cc'] | |
45 DEFAULT_ENCODING = "utf-8" | |
46 VALUE_IS_EMAIL_ADDRESS = lambda v: '@' in v | |
47 | |
48 | |
49 def normalize_header(header): | |
50 return string.capwords(header.lower(), '-') | |
51 | |
52 | |
53 class EncodingError(Exception): | |
54 """Thrown when there is an encoding error.""" | |
55 pass | |
56 | |
57 | |
58 class MailBase(object): | |
59 """MailBase is used as the basis of lamson.mail and contains the basics of | |
60 encoding an email. You actually can do all your email processing with this | |
61 class, but it's more raw. | |
62 """ | |
63 def __init__(self, items=()): | |
64 self.headers = dict(items) | |
65 self.parts = [] | |
66 self.body = None | |
67 self.content_encoding = {'Content-Type': (None, {}), | |
68 'Content-Disposition': (None, {}), | |
69 'Content-Transfer-Encoding': (None, {})} | |
70 | |
71 def __getitem__(self, key): | |
72 return self.headers.get(normalize_header(key), None) | |
73 | |
74 def __len__(self): | |
75 return len(self.headers) | |
76 | |
77 def __iter__(self): | |
78 return iter(self.headers) | |
79 | |
80 def __contains__(self, key): | |
81 return normalize_header(key) in self.headers | |
82 | |
83 def __setitem__(self, key, value): | |
84 self.headers[normalize_header(key)] = value | |
85 | |
86 def __delitem__(self, key): | |
87 del self.headers[normalize_header(key)] | |
88 | |
89 def __nonzero__(self): | |
90 return self.body != None or len(self.headers) > 0 or len(self.parts) > 0 | |
91 | |
92 def keys(self): | |
93 """Returns the sorted keys.""" | |
94 return sorted(self.headers.keys()) | |
95 | |
96 def attach_file(self, filename, data, ctype, disposition): | |
97 """ | |
98 A file attachment is a raw attachment with a disposition that | |
99 indicates the file name. | |
100 """ | |
101 assert filename, "You can't attach a file without a filename." | |
102 ctype = ctype.lower() | |
103 | |
104 part = MailBase() | |
105 part.body = data | |
106 part.content_encoding['Content-Type'] = (ctype, {'name': filename}) | |
107 part.content_encoding['Content-Disposition'] = (disposition, | |
108 {'filename': filename}) | |
109 self.parts.append(part) | |
110 | |
111 def attach_text(self, data, ctype): | |
112 """ | |
113 This attaches a simpler text encoded part, which doesn't have a | |
114 filename. | |
115 """ | |
116 ctype = ctype.lower() | |
117 | |
118 part = MailBase() | |
119 part.body = data | |
120 part.content_encoding['Content-Type'] = (ctype, {}) | |
121 self.parts.append(part) | |
122 | |
123 def walk(self): | |
124 for p in self.parts: | |
125 yield p | |
126 for x in p.walk(): | |
127 yield x | |
128 | |
129 | |
130 class MailResponse(object): | |
131 """ | |
132 You are given MailResponse objects from the lamson.view methods, and | |
133 whenever you want to generate an email to send to someone. It has the | |
134 same basic functionality as MailRequest, but it is designed to be written | |
135 to, rather than read from (although you can do both). | |
136 | |
137 You can easily set a Body or Html during creation or after by passing it | |
138 as __init__ parameters, or by setting those attributes. | |
139 | |
140 You can initially set the From, To, and Subject, but they are headers so | |
141 use the dict notation to change them: msg['From'] = 'joe@test.com'. | |
142 | |
143 The message is not fully crafted until right when you convert it with | |
144 MailResponse.to_message. This lets you change it and work with it, then | |
145 send it out when it's ready. | |
146 """ | |
147 def __init__(self, To=None, From=None, Subject=None, Body=None, Html=None, | |
148 separator="; "): | |
149 self.Body = Body | |
150 self.Html = Html | |
151 self.base = MailBase([('To', To), ('From', From), ('Subject', Subject)]) | |
152 self.multipart = self.Body and self.Html | |
153 self.attachments = [] | |
154 self.separator = separator | |
155 | |
156 def __contains__(self, key): | |
157 return self.base.__contains__(key) | |
158 | |
159 def __getitem__(self, key): | |
160 return self.base.__getitem__(key) | |
161 | |
162 def __setitem__(self, key, val): | |
163 return self.base.__setitem__(key, val) | |
164 | |
165 def __delitem__(self, name): | |
166 del self.base[name] | |
167 | |
168 def attach(self, filename=None, content_type=None, data=None, | |
169 disposition=None): | |
170 """ | |
171 | |
172 Simplifies attaching files from disk or data as files. To attach | |
173 simple text simple give data and a content_type. To attach a file, | |
174 give the data/content_type/filename/disposition combination. | |
175 | |
176 For convenience, if you don't give data and only a filename, then it | |
177 will read that file's contents when you call to_message() later. If | |
178 you give data and filename then it will assume you've filled data | |
179 with what the file's contents are and filename is just the name to | |
180 use. | |
181 """ | |
182 | |
183 assert filename or data, ("You must give a filename or some data to " | |
184 "attach.") | |
185 assert data or os.path.exists(filename), ("File doesn't exist, and no " | |
186 "data given.") | |
187 | |
188 self.multipart = True | |
189 | |
190 if filename and not content_type: | |
191 content_type, encoding = mimetypes.guess_type(filename) | |
192 | |
193 assert content_type, ("No content type given, and couldn't guess " | |
194 "from the filename: %r" % filename) | |
195 | |
196 self.attachments.append({'filename': filename, | |
197 'content_type': content_type, | |
198 'data': data, | |
199 'disposition': disposition,}) | |
200 | |
201 def attach_part(self, part): | |
202 """ | |
203 Attaches a raw MailBase part from a MailRequest (or anywhere) | |
204 so that you can copy it over. | |
205 """ | |
206 self.multipart = True | |
207 | |
208 self.attachments.append({'filename': None, | |
209 'content_type': None, | |
210 'data': None, | |
211 'disposition': None, | |
212 'part': part, | |
213 }) | |
214 | |
215 def attach_all_parts(self, mail_request): | |
216 """ | |
217 Used for copying the attachment parts of a mail.MailRequest | |
218 object for mailing lists that need to maintain attachments. | |
219 """ | |
220 for part in mail_request.all_parts(): | |
221 self.attach_part(part) | |
222 | |
223 self.base.content_encoding = mail_request.base.content_encoding.copy() | |
224 | |
225 def clear(self): | |
226 """ | |
227 Clears out the attachments so you can redo them. Use this to keep the | |
228 headers for a series of different messages with different attachments. | |
229 """ | |
230 del self.attachments[:] | |
231 del self.base.parts[:] | |
232 self.multipart = False | |
233 | |
234 def update(self, message): | |
235 """ | |
236 Used to easily set a bunch of heading from another dict | |
237 like object. | |
238 """ | |
239 for k in message.keys(): | |
240 self.base[k] = message[k] | |
241 | |
242 def __str__(self): | |
243 """ | |
244 Converts to a string. | |
245 """ | |
246 return self.to_message().as_string() | |
247 | |
248 def _encode_attachment(self, filename=None, content_type=None, data=None, | |
249 disposition=None, part=None): | |
250 """ | |
251 Used internally to take the attachments mentioned in self.attachments | |
252 and do the actual encoding in a lazy way when you call to_message. | |
253 """ | |
254 if part: | |
255 self.base.parts.append(part) | |
256 elif filename: | |
257 if not data: | |
258 data = open(filename).read() | |
259 | |
260 self.base.attach_file(filename, data, content_type, | |
261 disposition or 'attachment') | |
262 else: | |
263 self.base.attach_text(data, content_type) | |
264 | |
265 ctype = self.base.content_encoding['Content-Type'][0] | |
266 | |
267 if ctype and not ctype.startswith('multipart'): | |
268 self.base.content_encoding['Content-Type'] = ('multipart/mixed', {}) | |
269 | |
270 def to_message(self): | |
271 """ | |
272 Figures out all the required steps to finally craft the | |
273 message you need and return it. The resulting message | |
274 is also available as a self.base attribute. | |
275 | |
276 What is returned is a Python email API message you can | |
277 use with those APIs. The self.base attribute is the raw | |
278 lamson.encoding.MailBase. | |
279 """ | |
280 del self.base.parts[:] | |
281 | |
282 if self.Body and self.Html: | |
283 self.multipart = True | |
284 self.base.content_encoding['Content-Type'] = ( | |
285 'multipart/alternative', {}) | |
286 | |
287 if self.multipart: | |
288 self.base.body = None | |
289 if self.Body: | |
290 self.base.attach_text(self.Body, 'text/plain') | |
291 | |
292 if self.Html: | |
293 self.base.attach_text(self.Html, 'text/html') | |
294 | |
295 for args in self.attachments: | |
296 self._encode_attachment(**args) | |
297 | |
298 elif self.Body: | |
299 self.base.body = self.Body | |
300 self.base.content_encoding['Content-Type'] = ('text/plain', {}) | |
301 | |
302 elif self.Html: | |
303 self.base.body = self.Html | |
304 self.base.content_encoding['Content-Type'] = ('text/html', {}) | |
305 | |
306 return to_message(self.base, separator=self.separator) | |
307 | |
308 def all_parts(self): | |
309 """ | |
310 Returns all the encoded parts. Only useful for debugging | |
311 or inspecting after calling to_message(). | |
312 """ | |
313 return self.base.parts | |
314 | |
315 def keys(self): | |
316 return self.base.keys() | |
317 | |
318 | |
319 def to_message(mail, separator="; "): | |
320 """ | |
321 Given a MailBase message, this will construct a MIMEPart | |
322 that is canonicalized for use with the Python email API. | |
323 """ | |
324 ctype, params = mail.content_encoding['Content-Type'] | |
325 | |
326 if not ctype: | |
327 if mail.parts: | |
328 ctype = 'multipart/mixed' | |
329 else: | |
330 ctype = 'text/plain' | |
331 else: | |
332 if mail.parts: | |
333 assert ctype.startswith(("multipart", "message")), \ | |
334 "Content type should be multipart or message, not %r" % ctype | |
335 | |
336 # adjust the content type according to what it should be now | |
337 mail.content_encoding['Content-Type'] = (ctype, params) | |
338 | |
339 try: | |
340 out = MIMEPart(ctype, **params) | |
341 except TypeError, exc: # pragma: no cover | |
342 raise EncodingError("Content-Type malformed, not allowed: %r; " | |
343 "%r (Python ERROR: %s" % | |
344 (ctype, params, exc.message)) | |
345 | |
346 for k in mail.keys(): | |
347 if k in ADDRESS_HEADERS_WHITELIST: | |
348 out[k.encode('ascii')] = header_to_mime_encoding( | |
349 mail[k], | |
350 not_email=False, | |
351 separator=separator | |
352 ) | |
353 else: | |
354 out[k.encode('ascii')] = header_to_mime_encoding( | |
355 mail[k], | |
356 not_email=True | |
357 ) | |
358 | |
359 out.extract_payload(mail) | |
360 | |
361 # go through the children | |
362 for part in mail.parts: | |
363 out.attach(to_message(part)) | |
364 | |
365 return out | |
366 | |
367 class MIMEPart(MIMEBase): | |
368 """ | |
369 A reimplementation of nearly everything in email.mime to be more useful | |
370 for actually attaching things. Rather than one class for every type of | |
371 thing you'd encode, there's just this one, and it figures out how to | |
372 encode what you ask it. | |
373 """ | |
374 def __init__(self, type, **params): | |
375 self.maintype, self.subtype = type.split('/') | |
376 MIMEBase.__init__(self, self.maintype, self.subtype, **params) | |
377 | |
378 def add_text(self, content): | |
379 # this is text, so encode it in canonical form | |
380 try: | |
381 encoded = content.encode('ascii') | |
382 charset = 'ascii' | |
383 except UnicodeError: | |
384 encoded = content.encode('utf-8') | |
385 charset = 'utf-8' | |
386 | |
387 self.set_payload(encoded, charset=charset) | |
388 | |
389 def extract_payload(self, mail): | |
390 if mail.body == None: return # only None, '' is still ok | |
391 | |
392 ctype, ctype_params = mail.content_encoding['Content-Type'] | |
393 cdisp, cdisp_params = mail.content_encoding['Content-Disposition'] | |
394 | |
395 assert ctype, ("Extract payload requires that mail.content_encoding " | |
396 "have a valid Content-Type.") | |
397 | |
398 if ctype.startswith("text/"): | |
399 self.add_text(mail.body) | |
400 else: | |
401 if cdisp: | |
402 # replicate the content-disposition settings | |
403 self.add_header('Content-Disposition', cdisp, **cdisp_params) | |
404 | |
405 self.set_payload(mail.body) | |
406 encoders.encode_base64(self) | |
407 | |
408 def __repr__(self): | |
409 return "<MIMEPart '%s/%s': %r, %r, multipart=%r>" % ( | |
410 self.subtype, | |
411 self.maintype, | |
412 self['Content-Type'], | |
413 self['Content-Disposition'], | |
414 self.is_multipart()) | |
415 | |
416 | |
417 def header_to_mime_encoding(value, not_email=False, separator=", "): | |
418 if not value: return "" | |
419 | |
420 encoder = Charset(DEFAULT_ENCODING) | |
421 if type(value) == list: | |
422 return separator.join(properly_encode_header( | |
423 v, encoder, not_email) for v in value) | |
424 else: | |
425 return properly_encode_header(value, encoder, not_email) | |
426 | |
427 def properly_encode_header(value, encoder, not_email): | |
428 """ | |
429 The only thing special (weird) about this function is that it tries | |
430 to do a fast check to see if the header value has an email address in | |
431 it. Since random headers could have an email address, and email addresses | |
432 have weird special formatting rules, we have to check for it. | |
433 | |
434 Normally this works fine, but in Librelist, we need to "obfuscate" email | |
435 addresses by changing the '@' to '-AT-'. This is where | |
436 VALUE_IS_EMAIL_ADDRESS exists. It's a simple lambda returning True/False | |
437 to check if a header value has an email address. If you need to make this | |
438 check different, then change this. | |
439 """ | |
440 try: | |
441 return value.encode("ascii") | |
442 except UnicodeEncodeError: | |
443 if not_email is False and VALUE_IS_EMAIL_ADDRESS(value): | |
444 # this could have an email address, make sure we don't screw it up | |
445 name, address = parseaddr(value) | |
446 return '"%s" <%s>' % ( | |
447 encoder.header_encode(name.encode("utf-8")), address) | |
448 | |
449 return encoder.header_encode(value.encode("utf-8")) |