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