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