Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | # Copyright (C) 2001-2004 Python Software Foundation |
2 | # Contact: email-sig@python.org | |
3 | # email package unit tests | |
4 | ||
5 | import os | |
6 | import sys | |
7 | import time | |
8 | import base64 | |
9 | import difflib | |
10 | import unittest | |
11 | import warnings | |
12 | from cStringIO import StringIO | |
13 | ||
14 | import email | |
15 | ||
16 | from email.Charset import Charset | |
17 | from email.Header import Header, decode_header, make_header | |
18 | from email.Parser import Parser, HeaderParser | |
19 | from email.Generator import Generator, DecodedGenerator | |
20 | from email.Message import Message | |
21 | from email.MIMEAudio import MIMEAudio | |
22 | from email.MIMEText import MIMEText | |
23 | from email.MIMEImage import MIMEImage | |
24 | from email.MIMEBase import MIMEBase | |
25 | from email.MIMEMessage import MIMEMessage | |
26 | from email.MIMEMultipart import MIMEMultipart | |
27 | from email import Utils | |
28 | from email import Errors | |
29 | from email import Encoders | |
30 | from email import Iterators | |
31 | from email import base64MIME | |
32 | from email import quopriMIME | |
33 | ||
34 | from test.test_support import findfile, run_unittest | |
35 | from email.test import __file__ as landmark | |
36 | ||
37 | ||
38 | NL = '\n' | |
39 | EMPTYSTRING = '' | |
40 | SPACE = ' ' | |
41 | ||
42 | # We don't care about DeprecationWarnings | |
43 | warnings.filterwarnings('ignore', '', DeprecationWarning, __name__) | |
44 | ||
45 | ||
46 | \f | |
47 | def openfile(filename, mode='r'): | |
48 | path = os.path.join(os.path.dirname(landmark), 'data', filename) | |
49 | return open(path, mode) | |
50 | ||
51 | ||
52 | \f | |
53 | # Base test class | |
54 | class TestEmailBase(unittest.TestCase): | |
55 | def ndiffAssertEqual(self, first, second): | |
56 | """Like failUnlessEqual except use ndiff for readable output.""" | |
57 | if first <> second: | |
58 | sfirst = str(first) | |
59 | ssecond = str(second) | |
60 | diff = difflib.ndiff(sfirst.splitlines(), ssecond.splitlines()) | |
61 | fp = StringIO() | |
62 | print >> fp, NL, NL.join(diff) | |
63 | raise self.failureException, fp.getvalue() | |
64 | ||
65 | def _msgobj(self, filename): | |
66 | fp = openfile(findfile(filename)) | |
67 | try: | |
68 | msg = email.message_from_file(fp) | |
69 | finally: | |
70 | fp.close() | |
71 | return msg | |
72 | ||
73 | ||
74 | \f | |
75 | # Test various aspects of the Message class's API | |
76 | class TestMessageAPI(TestEmailBase): | |
77 | def test_get_all(self): | |
78 | eq = self.assertEqual | |
79 | msg = self._msgobj('msg_20.txt') | |
80 | eq(msg.get_all('cc'), ['ccc@zzz.org', 'ddd@zzz.org', 'eee@zzz.org']) | |
81 | eq(msg.get_all('xx', 'n/a'), 'n/a') | |
82 | ||
83 | def test_getset_charset(self): | |
84 | eq = self.assertEqual | |
85 | msg = Message() | |
86 | eq(msg.get_charset(), None) | |
87 | charset = Charset('iso-8859-1') | |
88 | msg.set_charset(charset) | |
89 | eq(msg['mime-version'], '1.0') | |
90 | eq(msg.get_type(), 'text/plain') | |
91 | eq(msg['content-type'], 'text/plain; charset="iso-8859-1"') | |
92 | eq(msg.get_param('charset'), 'iso-8859-1') | |
93 | eq(msg['content-transfer-encoding'], 'quoted-printable') | |
94 | eq(msg.get_charset().input_charset, 'iso-8859-1') | |
95 | # Remove the charset | |
96 | msg.set_charset(None) | |
97 | eq(msg.get_charset(), None) | |
98 | eq(msg['content-type'], 'text/plain') | |
99 | # Try adding a charset when there's already MIME headers present | |
100 | msg = Message() | |
101 | msg['MIME-Version'] = '2.0' | |
102 | msg['Content-Type'] = 'text/x-weird' | |
103 | msg['Content-Transfer-Encoding'] = 'quinted-puntable' | |
104 | msg.set_charset(charset) | |
105 | eq(msg['mime-version'], '2.0') | |
106 | eq(msg['content-type'], 'text/x-weird; charset="iso-8859-1"') | |
107 | eq(msg['content-transfer-encoding'], 'quinted-puntable') | |
108 | ||
109 | def test_set_charset_from_string(self): | |
110 | eq = self.assertEqual | |
111 | msg = Message() | |
112 | msg.set_charset('us-ascii') | |
113 | eq(msg.get_charset().input_charset, 'us-ascii') | |
114 | eq(msg['content-type'], 'text/plain; charset="us-ascii"') | |
115 | ||
116 | def test_set_payload_with_charset(self): | |
117 | msg = Message() | |
118 | charset = Charset('iso-8859-1') | |
119 | msg.set_payload('This is a string payload', charset) | |
120 | self.assertEqual(msg.get_charset().input_charset, 'iso-8859-1') | |
121 | ||
122 | def test_get_charsets(self): | |
123 | eq = self.assertEqual | |
124 | ||
125 | msg = self._msgobj('msg_08.txt') | |
126 | charsets = msg.get_charsets() | |
127 | eq(charsets, [None, 'us-ascii', 'iso-8859-1', 'iso-8859-2', 'koi8-r']) | |
128 | ||
129 | msg = self._msgobj('msg_09.txt') | |
130 | charsets = msg.get_charsets('dingbat') | |
131 | eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat', | |
132 | 'koi8-r']) | |
133 | ||
134 | msg = self._msgobj('msg_12.txt') | |
135 | charsets = msg.get_charsets() | |
136 | eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2', | |
137 | 'iso-8859-3', 'us-ascii', 'koi8-r']) | |
138 | ||
139 | def test_get_filename(self): | |
140 | eq = self.assertEqual | |
141 | ||
142 | msg = self._msgobj('msg_04.txt') | |
143 | filenames = [p.get_filename() for p in msg.get_payload()] | |
144 | eq(filenames, ['msg.txt', 'msg.txt']) | |
145 | ||
146 | msg = self._msgobj('msg_07.txt') | |
147 | subpart = msg.get_payload(1) | |
148 | eq(subpart.get_filename(), 'dingusfish.gif') | |
149 | ||
150 | def test_get_boundary(self): | |
151 | eq = self.assertEqual | |
152 | msg = self._msgobj('msg_07.txt') | |
153 | # No quotes! | |
154 | eq(msg.get_boundary(), 'BOUNDARY') | |
155 | ||
156 | def test_set_boundary(self): | |
157 | eq = self.assertEqual | |
158 | # This one has no existing boundary parameter, but the Content-Type: | |
159 | # header appears fifth. | |
160 | msg = self._msgobj('msg_01.txt') | |
161 | msg.set_boundary('BOUNDARY') | |
162 | header, value = msg.items()[4] | |
163 | eq(header.lower(), 'content-type') | |
164 | eq(value, 'text/plain; charset="us-ascii"; boundary="BOUNDARY"') | |
165 | # This one has a Content-Type: header, with a boundary, stuck in the | |
166 | # middle of its headers. Make sure the order is preserved; it should | |
167 | # be fifth. | |
168 | msg = self._msgobj('msg_04.txt') | |
169 | msg.set_boundary('BOUNDARY') | |
170 | header, value = msg.items()[4] | |
171 | eq(header.lower(), 'content-type') | |
172 | eq(value, 'multipart/mixed; boundary="BOUNDARY"') | |
173 | # And this one has no Content-Type: header at all. | |
174 | msg = self._msgobj('msg_03.txt') | |
175 | self.assertRaises(Errors.HeaderParseError, | |
176 | msg.set_boundary, 'BOUNDARY') | |
177 | ||
178 | def test_get_decoded_payload(self): | |
179 | eq = self.assertEqual | |
180 | msg = self._msgobj('msg_10.txt') | |
181 | # The outer message is a multipart | |
182 | eq(msg.get_payload(decode=True), None) | |
183 | # Subpart 1 is 7bit encoded | |
184 | eq(msg.get_payload(0).get_payload(decode=True), | |
185 | 'This is a 7bit encoded message.\n') | |
186 | # Subpart 2 is quopri | |
187 | eq(msg.get_payload(1).get_payload(decode=True), | |
188 | '\xa1This is a Quoted Printable encoded message!\n') | |
189 | # Subpart 3 is base64 | |
190 | eq(msg.get_payload(2).get_payload(decode=True), | |
191 | 'This is a Base64 encoded message.') | |
192 | # Subpart 4 has no Content-Transfer-Encoding: header. | |
193 | eq(msg.get_payload(3).get_payload(decode=True), | |
194 | 'This has no Content-Transfer-Encoding: header.\n') | |
195 | ||
196 | def test_get_decoded_uu_payload(self): | |
197 | eq = self.assertEqual | |
198 | msg = Message() | |
199 | msg.set_payload('begin 666 -\n+:&5L;&\\@=V]R;&0 \n \nend\n') | |
200 | for cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): | |
201 | msg['content-transfer-encoding'] = cte | |
202 | eq(msg.get_payload(decode=True), 'hello world') | |
203 | # Now try some bogus data | |
204 | msg.set_payload('foo') | |
205 | eq(msg.get_payload(decode=True), 'foo') | |
206 | ||
207 | def test_decoded_generator(self): | |
208 | eq = self.assertEqual | |
209 | msg = self._msgobj('msg_07.txt') | |
210 | fp = openfile('msg_17.txt') | |
211 | try: | |
212 | text = fp.read() | |
213 | finally: | |
214 | fp.close() | |
215 | s = StringIO() | |
216 | g = DecodedGenerator(s) | |
217 | g.flatten(msg) | |
218 | eq(s.getvalue(), text) | |
219 | ||
220 | def test__contains__(self): | |
221 | msg = Message() | |
222 | msg['From'] = 'Me' | |
223 | msg['to'] = 'You' | |
224 | # Check for case insensitivity | |
225 | self.failUnless('from' in msg) | |
226 | self.failUnless('From' in msg) | |
227 | self.failUnless('FROM' in msg) | |
228 | self.failUnless('to' in msg) | |
229 | self.failUnless('To' in msg) | |
230 | self.failUnless('TO' in msg) | |
231 | ||
232 | def test_as_string(self): | |
233 | eq = self.assertEqual | |
234 | msg = self._msgobj('msg_01.txt') | |
235 | fp = openfile('msg_01.txt') | |
236 | try: | |
237 | text = fp.read() | |
238 | finally: | |
239 | fp.close() | |
240 | eq(text, msg.as_string()) | |
241 | fullrepr = str(msg) | |
242 | lines = fullrepr.split('\n') | |
243 | self.failUnless(lines[0].startswith('From ')) | |
244 | eq(text, NL.join(lines[1:])) | |
245 | ||
246 | def test_bad_param(self): | |
247 | msg = email.message_from_string("Content-Type: blarg; baz; boo\n") | |
248 | self.assertEqual(msg.get_param('baz'), '') | |
249 | ||
250 | def test_missing_filename(self): | |
251 | msg = email.message_from_string("From: foo\n") | |
252 | self.assertEqual(msg.get_filename(), None) | |
253 | ||
254 | def test_bogus_filename(self): | |
255 | msg = email.message_from_string( | |
256 | "Content-Disposition: blarg; filename\n") | |
257 | self.assertEqual(msg.get_filename(), '') | |
258 | ||
259 | def test_missing_boundary(self): | |
260 | msg = email.message_from_string("From: foo\n") | |
261 | self.assertEqual(msg.get_boundary(), None) | |
262 | ||
263 | def test_get_params(self): | |
264 | eq = self.assertEqual | |
265 | msg = email.message_from_string( | |
266 | 'X-Header: foo=one; bar=two; baz=three\n') | |
267 | eq(msg.get_params(header='x-header'), | |
268 | [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]) | |
269 | msg = email.message_from_string( | |
270 | 'X-Header: foo; bar=one; baz=two\n') | |
271 | eq(msg.get_params(header='x-header'), | |
272 | [('foo', ''), ('bar', 'one'), ('baz', 'two')]) | |
273 | eq(msg.get_params(), None) | |
274 | msg = email.message_from_string( | |
275 | 'X-Header: foo; bar="one"; baz=two\n') | |
276 | eq(msg.get_params(header='x-header'), | |
277 | [('foo', ''), ('bar', 'one'), ('baz', 'two')]) | |
278 | ||
279 | def test_get_param_liberal(self): | |
280 | msg = Message() | |
281 | msg['Content-Type'] = 'Content-Type: Multipart/mixed; boundary = "CPIMSSMTPC06p5f3tG"' | |
282 | self.assertEqual(msg.get_param('boundary'), 'CPIMSSMTPC06p5f3tG') | |
283 | ||
284 | def test_get_param(self): | |
285 | eq = self.assertEqual | |
286 | msg = email.message_from_string( | |
287 | "X-Header: foo=one; bar=two; baz=three\n") | |
288 | eq(msg.get_param('bar', header='x-header'), 'two') | |
289 | eq(msg.get_param('quuz', header='x-header'), None) | |
290 | eq(msg.get_param('quuz'), None) | |
291 | msg = email.message_from_string( | |
292 | 'X-Header: foo; bar="one"; baz=two\n') | |
293 | eq(msg.get_param('foo', header='x-header'), '') | |
294 | eq(msg.get_param('bar', header='x-header'), 'one') | |
295 | eq(msg.get_param('baz', header='x-header'), 'two') | |
296 | # XXX: We are not RFC-2045 compliant! We cannot parse: | |
297 | # msg["Content-Type"] = 'text/plain; weird="hey; dolly? [you] @ <\\"home\\">?"' | |
298 | # msg.get_param("weird") | |
299 | # yet. | |
300 | ||
301 | def test_get_param_funky_continuation_lines(self): | |
302 | msg = self._msgobj('msg_22.txt') | |
303 | self.assertEqual(msg.get_payload(1).get_param('name'), 'wibble.JPG') | |
304 | ||
305 | def test_get_param_with_semis_in_quotes(self): | |
306 | msg = email.message_from_string( | |
307 | 'Content-Type: image/pjpeg; name="Jim&&Jill"\n') | |
308 | self.assertEqual(msg.get_param('name'), 'Jim&&Jill') | |
309 | self.assertEqual(msg.get_param('name', unquote=False), | |
310 | '"Jim&&Jill"') | |
311 | ||
312 | def test_has_key(self): | |
313 | msg = email.message_from_string('Header: exists') | |
314 | self.failUnless(msg.has_key('header')) | |
315 | self.failUnless(msg.has_key('Header')) | |
316 | self.failUnless(msg.has_key('HEADER')) | |
317 | self.failIf(msg.has_key('headeri')) | |
318 | ||
319 | def test_set_param(self): | |
320 | eq = self.assertEqual | |
321 | msg = Message() | |
322 | msg.set_param('charset', 'iso-2022-jp') | |
323 | eq(msg.get_param('charset'), 'iso-2022-jp') | |
324 | msg.set_param('importance', 'high value') | |
325 | eq(msg.get_param('importance'), 'high value') | |
326 | eq(msg.get_param('importance', unquote=False), '"high value"') | |
327 | eq(msg.get_params(), [('text/plain', ''), | |
328 | ('charset', 'iso-2022-jp'), | |
329 | ('importance', 'high value')]) | |
330 | eq(msg.get_params(unquote=False), [('text/plain', ''), | |
331 | ('charset', '"iso-2022-jp"'), | |
332 | ('importance', '"high value"')]) | |
333 | msg.set_param('charset', 'iso-9999-xx', header='X-Jimmy') | |
334 | eq(msg.get_param('charset', header='X-Jimmy'), 'iso-9999-xx') | |
335 | ||
336 | def test_del_param(self): | |
337 | eq = self.assertEqual | |
338 | msg = self._msgobj('msg_05.txt') | |
339 | eq(msg.get_params(), | |
340 | [('multipart/report', ''), ('report-type', 'delivery-status'), | |
341 | ('boundary', 'D1690A7AC1.996856090/mail.example.com')]) | |
342 | old_val = msg.get_param("report-type") | |
343 | msg.del_param("report-type") | |
344 | eq(msg.get_params(), | |
345 | [('multipart/report', ''), | |
346 | ('boundary', 'D1690A7AC1.996856090/mail.example.com')]) | |
347 | msg.set_param("report-type", old_val) | |
348 | eq(msg.get_params(), | |
349 | [('multipart/report', ''), | |
350 | ('boundary', 'D1690A7AC1.996856090/mail.example.com'), | |
351 | ('report-type', old_val)]) | |
352 | ||
353 | def test_del_param_on_other_header(self): | |
354 | msg = Message() | |
355 | msg.add_header('Content-Disposition', 'attachment', filename='bud.gif') | |
356 | msg.del_param('filename', 'content-disposition') | |
357 | self.assertEqual(msg['content-disposition'], 'attachment') | |
358 | ||
359 | def test_set_type(self): | |
360 | eq = self.assertEqual | |
361 | msg = Message() | |
362 | self.assertRaises(ValueError, msg.set_type, 'text') | |
363 | msg.set_type('text/plain') | |
364 | eq(msg['content-type'], 'text/plain') | |
365 | msg.set_param('charset', 'us-ascii') | |
366 | eq(msg['content-type'], 'text/plain; charset="us-ascii"') | |
367 | msg.set_type('text/html') | |
368 | eq(msg['content-type'], 'text/html; charset="us-ascii"') | |
369 | ||
370 | def test_set_type_on_other_header(self): | |
371 | msg = Message() | |
372 | msg['X-Content-Type'] = 'text/plain' | |
373 | msg.set_type('application/octet-stream', 'X-Content-Type') | |
374 | self.assertEqual(msg['x-content-type'], 'application/octet-stream') | |
375 | ||
376 | def test_get_content_type_missing(self): | |
377 | msg = Message() | |
378 | self.assertEqual(msg.get_content_type(), 'text/plain') | |
379 | ||
380 | def test_get_content_type_missing_with_default_type(self): | |
381 | msg = Message() | |
382 | msg.set_default_type('message/rfc822') | |
383 | self.assertEqual(msg.get_content_type(), 'message/rfc822') | |
384 | ||
385 | def test_get_content_type_from_message_implicit(self): | |
386 | msg = self._msgobj('msg_30.txt') | |
387 | self.assertEqual(msg.get_payload(0).get_content_type(), | |
388 | 'message/rfc822') | |
389 | ||
390 | def test_get_content_type_from_message_explicit(self): | |
391 | msg = self._msgobj('msg_28.txt') | |
392 | self.assertEqual(msg.get_payload(0).get_content_type(), | |
393 | 'message/rfc822') | |
394 | ||
395 | def test_get_content_type_from_message_text_plain_implicit(self): | |
396 | msg = self._msgobj('msg_03.txt') | |
397 | self.assertEqual(msg.get_content_type(), 'text/plain') | |
398 | ||
399 | def test_get_content_type_from_message_text_plain_explicit(self): | |
400 | msg = self._msgobj('msg_01.txt') | |
401 | self.assertEqual(msg.get_content_type(), 'text/plain') | |
402 | ||
403 | def test_get_content_maintype_missing(self): | |
404 | msg = Message() | |
405 | self.assertEqual(msg.get_content_maintype(), 'text') | |
406 | ||
407 | def test_get_content_maintype_missing_with_default_type(self): | |
408 | msg = Message() | |
409 | msg.set_default_type('message/rfc822') | |
410 | self.assertEqual(msg.get_content_maintype(), 'message') | |
411 | ||
412 | def test_get_content_maintype_from_message_implicit(self): | |
413 | msg = self._msgobj('msg_30.txt') | |
414 | self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message') | |
415 | ||
416 | def test_get_content_maintype_from_message_explicit(self): | |
417 | msg = self._msgobj('msg_28.txt') | |
418 | self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message') | |
419 | ||
420 | def test_get_content_maintype_from_message_text_plain_implicit(self): | |
421 | msg = self._msgobj('msg_03.txt') | |
422 | self.assertEqual(msg.get_content_maintype(), 'text') | |
423 | ||
424 | def test_get_content_maintype_from_message_text_plain_explicit(self): | |
425 | msg = self._msgobj('msg_01.txt') | |
426 | self.assertEqual(msg.get_content_maintype(), 'text') | |
427 | ||
428 | def test_get_content_subtype_missing(self): | |
429 | msg = Message() | |
430 | self.assertEqual(msg.get_content_subtype(), 'plain') | |
431 | ||
432 | def test_get_content_subtype_missing_with_default_type(self): | |
433 | msg = Message() | |
434 | msg.set_default_type('message/rfc822') | |
435 | self.assertEqual(msg.get_content_subtype(), 'rfc822') | |
436 | ||
437 | def test_get_content_subtype_from_message_implicit(self): | |
438 | msg = self._msgobj('msg_30.txt') | |
439 | self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822') | |
440 | ||
441 | def test_get_content_subtype_from_message_explicit(self): | |
442 | msg = self._msgobj('msg_28.txt') | |
443 | self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822') | |
444 | ||
445 | def test_get_content_subtype_from_message_text_plain_implicit(self): | |
446 | msg = self._msgobj('msg_03.txt') | |
447 | self.assertEqual(msg.get_content_subtype(), 'plain') | |
448 | ||
449 | def test_get_content_subtype_from_message_text_plain_explicit(self): | |
450 | msg = self._msgobj('msg_01.txt') | |
451 | self.assertEqual(msg.get_content_subtype(), 'plain') | |
452 | ||
453 | def test_get_content_maintype_error(self): | |
454 | msg = Message() | |
455 | msg['Content-Type'] = 'no-slash-in-this-string' | |
456 | self.assertEqual(msg.get_content_maintype(), 'text') | |
457 | ||
458 | def test_get_content_subtype_error(self): | |
459 | msg = Message() | |
460 | msg['Content-Type'] = 'no-slash-in-this-string' | |
461 | self.assertEqual(msg.get_content_subtype(), 'plain') | |
462 | ||
463 | def test_replace_header(self): | |
464 | eq = self.assertEqual | |
465 | msg = Message() | |
466 | msg.add_header('First', 'One') | |
467 | msg.add_header('Second', 'Two') | |
468 | msg.add_header('Third', 'Three') | |
469 | eq(msg.keys(), ['First', 'Second', 'Third']) | |
470 | eq(msg.values(), ['One', 'Two', 'Three']) | |
471 | msg.replace_header('Second', 'Twenty') | |
472 | eq(msg.keys(), ['First', 'Second', 'Third']) | |
473 | eq(msg.values(), ['One', 'Twenty', 'Three']) | |
474 | msg.add_header('First', 'Eleven') | |
475 | msg.replace_header('First', 'One Hundred') | |
476 | eq(msg.keys(), ['First', 'Second', 'Third', 'First']) | |
477 | eq(msg.values(), ['One Hundred', 'Twenty', 'Three', 'Eleven']) | |
478 | self.assertRaises(KeyError, msg.replace_header, 'Fourth', 'Missing') | |
479 | ||
480 | def test_broken_base64_payload(self): | |
481 | x = 'AwDp0P7//y6LwKEAcPa/6Q=9' | |
482 | msg = Message() | |
483 | msg['content-type'] = 'audio/x-midi' | |
484 | msg['content-transfer-encoding'] = 'base64' | |
485 | msg.set_payload(x) | |
486 | self.assertEqual(msg.get_payload(decode=True), x) | |
487 | ||
488 | ||
489 | \f | |
490 | # Test the email.Encoders module | |
491 | class TestEncoders(unittest.TestCase): | |
492 | def test_encode_empty_payload(self): | |
493 | eq = self.assertEqual | |
494 | msg = Message() | |
495 | msg.set_charset('us-ascii') | |
496 | eq(msg['content-transfer-encoding'], '7bit') | |
497 | ||
498 | def test_default_cte(self): | |
499 | eq = self.assertEqual | |
500 | msg = MIMEText('hello world') | |
501 | eq(msg['content-transfer-encoding'], '7bit') | |
502 | ||
503 | def test_default_cte(self): | |
504 | eq = self.assertEqual | |
505 | # With no explicit _charset its us-ascii, and all are 7-bit | |
506 | msg = MIMEText('hello world') | |
507 | eq(msg['content-transfer-encoding'], '7bit') | |
508 | # Similar, but with 8-bit data | |
509 | msg = MIMEText('hello \xf8 world') | |
510 | eq(msg['content-transfer-encoding'], '8bit') | |
511 | # And now with a different charset | |
512 | msg = MIMEText('hello \xf8 world', _charset='iso-8859-1') | |
513 | eq(msg['content-transfer-encoding'], 'quoted-printable') | |
514 | ||
515 | ||
516 | \f | |
517 | # Test long header wrapping | |
518 | class TestLongHeaders(TestEmailBase): | |
519 | def test_split_long_continuation(self): | |
520 | eq = self.ndiffAssertEqual | |
521 | msg = email.message_from_string("""\ | |
522 | Subject: bug demonstration | |
523 | \t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 | |
524 | \tmore text | |
525 | ||
526 | test | |
527 | """) | |
528 | sfp = StringIO() | |
529 | g = Generator(sfp) | |
530 | g.flatten(msg) | |
531 | eq(sfp.getvalue(), """\ | |
532 | Subject: bug demonstration | |
533 | \t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 | |
534 | \tmore text | |
535 | ||
536 | test | |
537 | """) | |
538 | ||
539 | def test_another_long_almost_unsplittable_header(self): | |
540 | eq = self.ndiffAssertEqual | |
541 | hstr = """\ | |
542 | bug demonstration | |
543 | \t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 | |
544 | \tmore text""" | |
545 | h = Header(hstr, continuation_ws='\t') | |
546 | eq(h.encode(), """\ | |
547 | bug demonstration | |
548 | \t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 | |
549 | \tmore text""") | |
550 | h = Header(hstr) | |
551 | eq(h.encode(), """\ | |
552 | bug demonstration | |
553 | 12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 | |
554 | more text""") | |
555 | ||
556 | def test_long_nonstring(self): | |
557 | eq = self.ndiffAssertEqual | |
558 | g = Charset("iso-8859-1") | |
559 | cz = Charset("iso-8859-2") | |
560 | utf8 = Charset("utf-8") | |
561 | g_head = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " | |
562 | cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. " | |
563 | utf8_head = u"\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066\u3044\u307e\u3059\u3002".encode("utf-8") | |
564 | h = Header(g_head, g, header_name='Subject') | |
565 | h.append(cz_head, cz) | |
566 | h.append(utf8_head, utf8) | |
567 | msg = Message() | |
568 | msg['Subject'] = h | |
569 | sfp = StringIO() | |
570 | g = Generator(sfp) | |
571 | g.flatten(msg) | |
572 | eq(sfp.getvalue(), """\ | |
573 | Subject: =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerd?= | |
574 | =?iso-8859-1?q?erband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndi?= | |
575 | =?iso-8859-1?q?schen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Kling?= | |
576 | =?iso-8859-1?q?en_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_met?= | |
577 | =?iso-8859-2?q?ropole_se_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= | |
578 | =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE?= | |
579 | =?utf-8?b?44G+44Gb44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB?= | |
580 | =?utf-8?b?44GC44Go44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CM?= | |
581 | =?utf-8?q?Wenn_ist_das_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das?= | |
582 | =?utf-8?b?IE9kZXIgZGllIEZsaXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBow==?= | |
583 | =?utf-8?b?44Gm44GE44G+44GZ44CC?= | |
584 | ||
585 | """) | |
586 | eq(h.encode(), """\ | |
587 | =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerd?= | |
588 | =?iso-8859-1?q?erband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndi?= | |
589 | =?iso-8859-1?q?schen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Kling?= | |
590 | =?iso-8859-1?q?en_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_met?= | |
591 | =?iso-8859-2?q?ropole_se_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= | |
592 | =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE?= | |
593 | =?utf-8?b?44G+44Gb44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB?= | |
594 | =?utf-8?b?44GC44Go44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CM?= | |
595 | =?utf-8?q?Wenn_ist_das_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das?= | |
596 | =?utf-8?b?IE9kZXIgZGllIEZsaXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBow==?= | |
597 | =?utf-8?b?44Gm44GE44G+44GZ44CC?=""") | |
598 | ||
599 | def test_long_header_encode(self): | |
600 | eq = self.ndiffAssertEqual | |
601 | h = Header('wasnipoop; giraffes="very-long-necked-animals"; ' | |
602 | 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"', | |
603 | header_name='X-Foobar-Spoink-Defrobnit') | |
604 | eq(h.encode(), '''\ | |
605 | wasnipoop; giraffes="very-long-necked-animals"; | |
606 | spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''') | |
607 | ||
608 | def test_long_header_encode_with_tab_continuation(self): | |
609 | eq = self.ndiffAssertEqual | |
610 | h = Header('wasnipoop; giraffes="very-long-necked-animals"; ' | |
611 | 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"', | |
612 | header_name='X-Foobar-Spoink-Defrobnit', | |
613 | continuation_ws='\t') | |
614 | eq(h.encode(), '''\ | |
615 | wasnipoop; giraffes="very-long-necked-animals"; | |
616 | \tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''') | |
617 | ||
618 | def test_header_splitter(self): | |
619 | eq = self.ndiffAssertEqual | |
620 | msg = MIMEText('') | |
621 | # It'd be great if we could use add_header() here, but that doesn't | |
622 | # guarantee an order of the parameters. | |
623 | msg['X-Foobar-Spoink-Defrobnit'] = ( | |
624 | 'wasnipoop; giraffes="very-long-necked-animals"; ' | |
625 | 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"') | |
626 | sfp = StringIO() | |
627 | g = Generator(sfp) | |
628 | g.flatten(msg) | |
629 | eq(sfp.getvalue(), '''\ | |
630 | Content-Type: text/plain; charset="us-ascii" | |
631 | MIME-Version: 1.0 | |
632 | Content-Transfer-Encoding: 7bit | |
633 | X-Foobar-Spoink-Defrobnit: wasnipoop; giraffes="very-long-necked-animals"; | |
634 | \tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey" | |
635 | ||
636 | ''') | |
637 | ||
638 | def test_no_semis_header_splitter(self): | |
639 | eq = self.ndiffAssertEqual | |
640 | msg = Message() | |
641 | msg['From'] = 'test@dom.ain' | |
642 | msg['References'] = SPACE.join(['<%d@dom.ain>' % i for i in range(10)]) | |
643 | msg.set_payload('Test') | |
644 | sfp = StringIO() | |
645 | g = Generator(sfp) | |
646 | g.flatten(msg) | |
647 | eq(sfp.getvalue(), """\ | |
648 | From: test@dom.ain | |
649 | References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain> | |
650 | \t<5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain> | |
651 | ||
652 | Test""") | |
653 | ||
654 | def test_no_split_long_header(self): | |
655 | eq = self.ndiffAssertEqual | |
656 | hstr = 'References: ' + 'x' * 80 | |
657 | h = Header(hstr, continuation_ws='\t') | |
658 | eq(h.encode(), """\ | |
659 | References: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""") | |
660 | ||
661 | def test_splitting_multiple_long_lines(self): | |
662 | eq = self.ndiffAssertEqual | |
663 | hstr = """\ | |
664 | from babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for <mailman-admin@babylon.socal-raves.org>; Sat, 2 Feb 2002 17:00:06 -0800 (PST) | |
665 | \tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for <mailman-admin@babylon.socal-raves.org>; Sat, 2 Feb 2002 17:00:06 -0800 (PST) | |
666 | \tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for <mailman-admin@babylon.socal-raves.org>; Sat, 2 Feb 2002 17:00:06 -0800 (PST) | |
667 | """ | |
668 | h = Header(hstr, continuation_ws='\t') | |
669 | eq(h.encode(), """\ | |
670 | from babylon.socal-raves.org (localhost [127.0.0.1]); | |
671 | \tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; | |
672 | \tfor <mailman-admin@babylon.socal-raves.org>; | |
673 | \tSat, 2 Feb 2002 17:00:06 -0800 (PST) | |
674 | \tfrom babylon.socal-raves.org (localhost [127.0.0.1]); | |
675 | \tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; | |
676 | \tfor <mailman-admin@babylon.socal-raves.org>; | |
677 | \tSat, 2 Feb 2002 17:00:06 -0800 (PST) | |
678 | \tfrom babylon.socal-raves.org (localhost [127.0.0.1]); | |
679 | \tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; | |
680 | \tfor <mailman-admin@babylon.socal-raves.org>; | |
681 | \tSat, 2 Feb 2002 17:00:06 -0800 (PST)""") | |
682 | ||
683 | def test_splitting_first_line_only_is_long(self): | |
684 | eq = self.ndiffAssertEqual | |
685 | hstr = """\ | |
686 | from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca) | |
687 | \tby kronos.mems-exchange.org with esmtp (Exim 4.05) | |
688 | \tid 17k4h5-00034i-00 | |
689 | \tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""" | |
690 | h = Header(hstr, maxlinelen=78, header_name='Received', | |
691 | continuation_ws='\t') | |
692 | eq(h.encode(), """\ | |
693 | from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] | |
694 | \thelo=cthulhu.gerg.ca) | |
695 | \tby kronos.mems-exchange.org with esmtp (Exim 4.05) | |
696 | \tid 17k4h5-00034i-00 | |
697 | \tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""") | |
698 | ||
699 | def test_long_8bit_header(self): | |
700 | eq = self.ndiffAssertEqual | |
701 | msg = Message() | |
702 | h = Header('Britische Regierung gibt', 'iso-8859-1', | |
703 | header_name='Subject') | |
704 | h.append('gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte') | |
705 | msg['Subject'] = h | |
706 | eq(msg.as_string(), """\ | |
707 | Subject: =?iso-8859-1?q?Britische_Regierung_gibt?= =?iso-8859-1?q?gr=FCnes?= | |
708 | =?iso-8859-1?q?_Licht_f=FCr_Offshore-Windkraftprojekte?= | |
709 | ||
710 | """) | |
711 | ||
712 | def test_long_8bit_header_no_charset(self): | |
713 | eq = self.ndiffAssertEqual | |
714 | msg = Message() | |
715 | msg['Reply-To'] = 'Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte <a-very-long-address@example.com>' | |
716 | eq(msg.as_string(), """\ | |
717 | Reply-To: Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte <a-very-long-address@example.com> | |
718 | ||
719 | """) | |
720 | ||
721 | def test_long_to_header(self): | |
722 | eq = self.ndiffAssertEqual | |
723 | to = '"Someone Test #A" <someone@eecs.umich.edu>,<someone@eecs.umich.edu>,"Someone Test #B" <someone@umich.edu>, "Someone Test #C" <someone@eecs.umich.edu>, "Someone Test #D" <someone@eecs.umich.edu>' | |
724 | msg = Message() | |
725 | msg['To'] = to | |
726 | eq(msg.as_string(0), '''\ | |
727 | To: "Someone Test #A" <someone@eecs.umich.edu>, <someone@eecs.umich.edu>, | |
728 | \t"Someone Test #B" <someone@umich.edu>, | |
729 | \t"Someone Test #C" <someone@eecs.umich.edu>, | |
730 | \t"Someone Test #D" <someone@eecs.umich.edu> | |
731 | ||
732 | ''') | |
733 | ||
734 | def test_long_line_after_append(self): | |
735 | eq = self.ndiffAssertEqual | |
736 | s = 'This is an example of string which has almost the limit of header length.' | |
737 | h = Header(s) | |
738 | h.append('Add another line.') | |
739 | eq(h.encode(), """\ | |
740 | This is an example of string which has almost the limit of header length. | |
741 | Add another line.""") | |
742 | ||
743 | def test_shorter_line_with_append(self): | |
744 | eq = self.ndiffAssertEqual | |
745 | s = 'This is a shorter line.' | |
746 | h = Header(s) | |
747 | h.append('Add another sentence. (Surprise?)') | |
748 | eq(h.encode(), | |
749 | 'This is a shorter line. Add another sentence. (Surprise?)') | |
750 | ||
751 | def test_long_field_name(self): | |
752 | eq = self.ndiffAssertEqual | |
753 | fn = 'X-Very-Very-Very-Long-Header-Name' | |
754 | gs = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " | |
755 | h = Header(gs, 'iso-8859-1', header_name=fn) | |
756 | # BAW: this seems broken because the first line is too long | |
757 | eq(h.encode(), """\ | |
758 | =?iso-8859-1?q?Die_Mieter_treten_hier_?= | |
759 | =?iso-8859-1?q?ein_werden_mit_einem_Foerderband_komfortabel_den_Korridor_?= | |
760 | =?iso-8859-1?q?entlang=2C_an_s=FCdl=FCndischen_Wandgem=E4lden_vorbei=2C_g?= | |
761 | =?iso-8859-1?q?egen_die_rotierenden_Klingen_bef=F6rdert=2E_?=""") | |
762 | ||
763 | def test_long_received_header(self): | |
764 | h = 'from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP; Wed, 05 Mar 2003 18:10:18 -0700' | |
765 | msg = Message() | |
766 | msg['Received-1'] = Header(h, continuation_ws='\t') | |
767 | msg['Received-2'] = h | |
768 | self.assertEqual(msg.as_string(), """\ | |
769 | Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by | |
770 | \throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP; | |
771 | \tWed, 05 Mar 2003 18:10:18 -0700 | |
772 | Received-2: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by | |
773 | \throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP; | |
774 | \tWed, 05 Mar 2003 18:10:18 -0700 | |
775 | ||
776 | """) | |
777 | ||
778 | def test_string_headerinst_eq(self): | |
779 | h = '<15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner\'s message of "Thu, 6 Mar 2003 13:58:21 +0100")' | |
780 | msg = Message() | |
781 | msg['Received-1'] = Header(h, header_name='Received-1', | |
782 | continuation_ws='\t') | |
783 | msg['Received-2'] = h | |
784 | self.assertEqual(msg.as_string(), """\ | |
785 | Received-1: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> | |
786 | \t(David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100") | |
787 | Received-2: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> | |
788 | \t(David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100") | |
789 | ||
790 | """) | |
791 | ||
792 | def test_long_unbreakable_lines_with_continuation(self): | |
793 | eq = self.ndiffAssertEqual | |
794 | msg = Message() | |
795 | t = """\ | |
796 | iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 | |
797 | locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp""" | |
798 | msg['Face-1'] = t | |
799 | msg['Face-2'] = Header(t, header_name='Face-2') | |
800 | eq(msg.as_string(), """\ | |
801 | Face-1: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 | |
802 | \tlocQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp | |
803 | Face-2: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 | |
804 | locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp | |
805 | ||
806 | """) | |
807 | ||
808 | def test_another_long_multiline_header(self): | |
809 | eq = self.ndiffAssertEqual | |
810 | m = '''\ | |
811 | Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with Microsoft SMTPSVC(5.0.2195.4905); | |
812 | \tWed, 16 Oct 2002 07:41:11 -0700''' | |
813 | msg = email.message_from_string(m) | |
814 | eq(msg.as_string(), '''\ | |
815 | Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with | |
816 | \tMicrosoft SMTPSVC(5.0.2195.4905); Wed, 16 Oct 2002 07:41:11 -0700 | |
817 | ||
818 | ''') | |
819 | ||
820 | def test_long_lines_with_different_header(self): | |
821 | eq = self.ndiffAssertEqual | |
822 | h = """\ | |
823 | List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/spamassassin-talk>, | |
824 | <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>""" | |
825 | msg = Message() | |
826 | msg['List'] = h | |
827 | msg['List'] = Header(h, header_name='List') | |
828 | eq(msg.as_string(), """\ | |
829 | List: List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/spamassassin-talk>, | |
830 | \t<mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe> | |
831 | List: List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/spamassassin-talk>, | |
832 | <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe> | |
833 | ||
834 | """) | |
835 | ||
836 | ||
837 | \f | |
838 | # Test mangling of "From " lines in the body of a message | |
839 | class TestFromMangling(unittest.TestCase): | |
840 | def setUp(self): | |
841 | self.msg = Message() | |
842 | self.msg['From'] = 'aaa@bbb.org' | |
843 | self.msg.set_payload("""\ | |
844 | From the desk of A.A.A.: | |
845 | Blah blah blah | |
846 | """) | |
847 | ||
848 | def test_mangled_from(self): | |
849 | s = StringIO() | |
850 | g = Generator(s, mangle_from_=True) | |
851 | g.flatten(self.msg) | |
852 | self.assertEqual(s.getvalue(), """\ | |
853 | From: aaa@bbb.org | |
854 | ||
855 | >From the desk of A.A.A.: | |
856 | Blah blah blah | |
857 | """) | |
858 | ||
859 | def test_dont_mangle_from(self): | |
860 | s = StringIO() | |
861 | g = Generator(s, mangle_from_=False) | |
862 | g.flatten(self.msg) | |
863 | self.assertEqual(s.getvalue(), """\ | |
864 | From: aaa@bbb.org | |
865 | ||
866 | From the desk of A.A.A.: | |
867 | Blah blah blah | |
868 | """) | |
869 | ||
870 | ||
871 | \f | |
872 | # Test the basic MIMEAudio class | |
873 | class TestMIMEAudio(unittest.TestCase): | |
874 | def setUp(self): | |
875 | # Make sure we pick up the audiotest.au that lives in email/test/data. | |
876 | # In Python, there's an audiotest.au living in Lib/test but that isn't | |
877 | # included in some binary distros that don't include the test | |
878 | # package. The trailing empty string on the .join() is significant | |
879 | # since findfile() will do a dirname(). | |
880 | datadir = os.path.join(os.path.dirname(landmark), 'data', '') | |
881 | fp = open(findfile('audiotest.au', datadir), 'rb') | |
882 | try: | |
883 | self._audiodata = fp.read() | |
884 | finally: | |
885 | fp.close() | |
886 | self._au = MIMEAudio(self._audiodata) | |
887 | ||
888 | def test_guess_minor_type(self): | |
889 | self.assertEqual(self._au.get_type(), 'audio/basic') | |
890 | ||
891 | def test_encoding(self): | |
892 | payload = self._au.get_payload() | |
893 | self.assertEqual(base64.decodestring(payload), self._audiodata) | |
894 | ||
895 | def test_checkSetMinor(self): | |
896 | au = MIMEAudio(self._audiodata, 'fish') | |
897 | self.assertEqual(au.get_type(), 'audio/fish') | |
898 | ||
899 | def test_add_header(self): | |
900 | eq = self.assertEqual | |
901 | unless = self.failUnless | |
902 | self._au.add_header('Content-Disposition', 'attachment', | |
903 | filename='audiotest.au') | |
904 | eq(self._au['content-disposition'], | |
905 | 'attachment; filename="audiotest.au"') | |
906 | eq(self._au.get_params(header='content-disposition'), | |
907 | [('attachment', ''), ('filename', 'audiotest.au')]) | |
908 | eq(self._au.get_param('filename', header='content-disposition'), | |
909 | 'audiotest.au') | |
910 | missing = [] | |
911 | eq(self._au.get_param('attachment', header='content-disposition'), '') | |
912 | unless(self._au.get_param('foo', failobj=missing, | |
913 | header='content-disposition') is missing) | |
914 | # Try some missing stuff | |
915 | unless(self._au.get_param('foobar', missing) is missing) | |
916 | unless(self._au.get_param('attachment', missing, | |
917 | header='foobar') is missing) | |
918 | ||
919 | ||
920 | \f | |
921 | # Test the basic MIMEImage class | |
922 | class TestMIMEImage(unittest.TestCase): | |
923 | def setUp(self): | |
924 | fp = openfile('PyBanner048.gif') | |
925 | try: | |
926 | self._imgdata = fp.read() | |
927 | finally: | |
928 | fp.close() | |
929 | self._im = MIMEImage(self._imgdata) | |
930 | ||
931 | def test_guess_minor_type(self): | |
932 | self.assertEqual(self._im.get_type(), 'image/gif') | |
933 | ||
934 | def test_encoding(self): | |
935 | payload = self._im.get_payload() | |
936 | self.assertEqual(base64.decodestring(payload), self._imgdata) | |
937 | ||
938 | def test_checkSetMinor(self): | |
939 | im = MIMEImage(self._imgdata, 'fish') | |
940 | self.assertEqual(im.get_type(), 'image/fish') | |
941 | ||
942 | def test_add_header(self): | |
943 | eq = self.assertEqual | |
944 | unless = self.failUnless | |
945 | self._im.add_header('Content-Disposition', 'attachment', | |
946 | filename='dingusfish.gif') | |
947 | eq(self._im['content-disposition'], | |
948 | 'attachment; filename="dingusfish.gif"') | |
949 | eq(self._im.get_params(header='content-disposition'), | |
950 | [('attachment', ''), ('filename', 'dingusfish.gif')]) | |
951 | eq(self._im.get_param('filename', header='content-disposition'), | |
952 | 'dingusfish.gif') | |
953 | missing = [] | |
954 | eq(self._im.get_param('attachment', header='content-disposition'), '') | |
955 | unless(self._im.get_param('foo', failobj=missing, | |
956 | header='content-disposition') is missing) | |
957 | # Try some missing stuff | |
958 | unless(self._im.get_param('foobar', missing) is missing) | |
959 | unless(self._im.get_param('attachment', missing, | |
960 | header='foobar') is missing) | |
961 | ||
962 | ||
963 | \f | |
964 | # Test the basic MIMEText class | |
965 | class TestMIMEText(unittest.TestCase): | |
966 | def setUp(self): | |
967 | self._msg = MIMEText('hello there') | |
968 | ||
969 | def test_types(self): | |
970 | eq = self.assertEqual | |
971 | unless = self.failUnless | |
972 | eq(self._msg.get_type(), 'text/plain') | |
973 | eq(self._msg.get_param('charset'), 'us-ascii') | |
974 | missing = [] | |
975 | unless(self._msg.get_param('foobar', missing) is missing) | |
976 | unless(self._msg.get_param('charset', missing, header='foobar') | |
977 | is missing) | |
978 | ||
979 | def test_payload(self): | |
980 | self.assertEqual(self._msg.get_payload(), 'hello there') | |
981 | self.failUnless(not self._msg.is_multipart()) | |
982 | ||
983 | def test_charset(self): | |
984 | eq = self.assertEqual | |
985 | msg = MIMEText('hello there', _charset='us-ascii') | |
986 | eq(msg.get_charset().input_charset, 'us-ascii') | |
987 | eq(msg['content-type'], 'text/plain; charset="us-ascii"') | |
988 | ||
989 | ||
990 | \f | |
991 | # Test complicated multipart/* messages | |
992 | class TestMultipart(TestEmailBase): | |
993 | def setUp(self): | |
994 | fp = openfile('PyBanner048.gif') | |
995 | try: | |
996 | data = fp.read() | |
997 | finally: | |
998 | fp.close() | |
999 | ||
1000 | container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY') | |
1001 | image = MIMEImage(data, name='dingusfish.gif') | |
1002 | image.add_header('content-disposition', 'attachment', | |
1003 | filename='dingusfish.gif') | |
1004 | intro = MIMEText('''\ | |
1005 | Hi there, | |
1006 | ||
1007 | This is the dingus fish. | |
1008 | ''') | |
1009 | container.attach(intro) | |
1010 | container.attach(image) | |
1011 | container['From'] = 'Barry <barry@digicool.com>' | |
1012 | container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>' | |
1013 | container['Subject'] = 'Here is your dingus fish' | |
1014 | ||
1015 | now = 987809702.54848599 | |
1016 | timetuple = time.localtime(now) | |
1017 | if timetuple[-1] == 0: | |
1018 | tzsecs = time.timezone | |
1019 | else: | |
1020 | tzsecs = time.altzone | |
1021 | if tzsecs > 0: | |
1022 | sign = '-' | |
1023 | else: | |
1024 | sign = '+' | |
1025 | tzoffset = ' %s%04d' % (sign, tzsecs / 36) | |
1026 | container['Date'] = time.strftime( | |
1027 | '%a, %d %b %Y %H:%M:%S', | |
1028 | time.localtime(now)) + tzoffset | |
1029 | self._msg = container | |
1030 | self._im = image | |
1031 | self._txt = intro | |
1032 | ||
1033 | def test_hierarchy(self): | |
1034 | # convenience | |
1035 | eq = self.assertEqual | |
1036 | unless = self.failUnless | |
1037 | raises = self.assertRaises | |
1038 | # tests | |
1039 | m = self._msg | |
1040 | unless(m.is_multipart()) | |
1041 | eq(m.get_type(), 'multipart/mixed') | |
1042 | eq(len(m.get_payload()), 2) | |
1043 | raises(IndexError, m.get_payload, 2) | |
1044 | m0 = m.get_payload(0) | |
1045 | m1 = m.get_payload(1) | |
1046 | unless(m0 is self._txt) | |
1047 | unless(m1 is self._im) | |
1048 | eq(m.get_payload(), [m0, m1]) | |
1049 | unless(not m0.is_multipart()) | |
1050 | unless(not m1.is_multipart()) | |
1051 | ||
1052 | def test_empty_multipart_idempotent(self): | |
1053 | text = """\ | |
1054 | Content-Type: multipart/mixed; boundary="BOUNDARY" | |
1055 | MIME-Version: 1.0 | |
1056 | Subject: A subject | |
1057 | To: aperson@dom.ain | |
1058 | From: bperson@dom.ain | |
1059 | ||
1060 | ||
1061 | --BOUNDARY | |
1062 | ||
1063 | ||
1064 | --BOUNDARY-- | |
1065 | """ | |
1066 | msg = Parser().parsestr(text) | |
1067 | self.ndiffAssertEqual(text, msg.as_string()) | |
1068 | ||
1069 | def test_no_parts_in_a_multipart_with_none_epilogue(self): | |
1070 | outer = MIMEBase('multipart', 'mixed') | |
1071 | outer['Subject'] = 'A subject' | |
1072 | outer['To'] = 'aperson@dom.ain' | |
1073 | outer['From'] = 'bperson@dom.ain' | |
1074 | outer.set_boundary('BOUNDARY') | |
1075 | self.ndiffAssertEqual(outer.as_string(), '''\ | |
1076 | Content-Type: multipart/mixed; boundary="BOUNDARY" | |
1077 | MIME-Version: 1.0 | |
1078 | Subject: A subject | |
1079 | To: aperson@dom.ain | |
1080 | From: bperson@dom.ain | |
1081 | ||
1082 | --BOUNDARY | |
1083 | ||
1084 | --BOUNDARY--''') | |
1085 | ||
1086 | def test_no_parts_in_a_multipart_with_empty_epilogue(self): | |
1087 | outer = MIMEBase('multipart', 'mixed') | |
1088 | outer['Subject'] = 'A subject' | |
1089 | outer['To'] = 'aperson@dom.ain' | |
1090 | outer['From'] = 'bperson@dom.ain' | |
1091 | outer.preamble = '' | |
1092 | outer.epilogue = '' | |
1093 | outer.set_boundary('BOUNDARY') | |
1094 | self.ndiffAssertEqual(outer.as_string(), '''\ | |
1095 | Content-Type: multipart/mixed; boundary="BOUNDARY" | |
1096 | MIME-Version: 1.0 | |
1097 | Subject: A subject | |
1098 | To: aperson@dom.ain | |
1099 | From: bperson@dom.ain | |
1100 | ||
1101 | ||
1102 | --BOUNDARY | |
1103 | ||
1104 | --BOUNDARY-- | |
1105 | ''') | |
1106 | ||
1107 | def test_one_part_in_a_multipart(self): | |
1108 | eq = self.ndiffAssertEqual | |
1109 | outer = MIMEBase('multipart', 'mixed') | |
1110 | outer['Subject'] = 'A subject' | |
1111 | outer['To'] = 'aperson@dom.ain' | |
1112 | outer['From'] = 'bperson@dom.ain' | |
1113 | outer.set_boundary('BOUNDARY') | |
1114 | msg = MIMEText('hello world') | |
1115 | outer.attach(msg) | |
1116 | eq(outer.as_string(), '''\ | |
1117 | Content-Type: multipart/mixed; boundary="BOUNDARY" | |
1118 | MIME-Version: 1.0 | |
1119 | Subject: A subject | |
1120 | To: aperson@dom.ain | |
1121 | From: bperson@dom.ain | |
1122 | ||
1123 | --BOUNDARY | |
1124 | Content-Type: text/plain; charset="us-ascii" | |
1125 | MIME-Version: 1.0 | |
1126 | Content-Transfer-Encoding: 7bit | |
1127 | ||
1128 | hello world | |
1129 | --BOUNDARY--''') | |
1130 | ||
1131 | def test_seq_parts_in_a_multipart_with_empty_preamble(self): | |
1132 | eq = self.ndiffAssertEqual | |
1133 | outer = MIMEBase('multipart', 'mixed') | |
1134 | outer['Subject'] = 'A subject' | |
1135 | outer['To'] = 'aperson@dom.ain' | |
1136 | outer['From'] = 'bperson@dom.ain' | |
1137 | outer.preamble = '' | |
1138 | msg = MIMEText('hello world') | |
1139 | outer.attach(msg) | |
1140 | outer.set_boundary('BOUNDARY') | |
1141 | eq(outer.as_string(), '''\ | |
1142 | Content-Type: multipart/mixed; boundary="BOUNDARY" | |
1143 | MIME-Version: 1.0 | |
1144 | Subject: A subject | |
1145 | To: aperson@dom.ain | |
1146 | From: bperson@dom.ain | |
1147 | ||
1148 | ||
1149 | --BOUNDARY | |
1150 | Content-Type: text/plain; charset="us-ascii" | |
1151 | MIME-Version: 1.0 | |
1152 | Content-Transfer-Encoding: 7bit | |
1153 | ||
1154 | hello world | |
1155 | --BOUNDARY--''') | |
1156 | ||
1157 | ||
1158 | def test_seq_parts_in_a_multipart_with_none_preamble(self): | |
1159 | eq = self.ndiffAssertEqual | |
1160 | outer = MIMEBase('multipart', 'mixed') | |
1161 | outer['Subject'] = 'A subject' | |
1162 | outer['To'] = 'aperson@dom.ain' | |
1163 | outer['From'] = 'bperson@dom.ain' | |
1164 | outer.preamble = None | |
1165 | msg = MIMEText('hello world') | |
1166 | outer.attach(msg) | |
1167 | outer.set_boundary('BOUNDARY') | |
1168 | eq(outer.as_string(), '''\ | |
1169 | Content-Type: multipart/mixed; boundary="BOUNDARY" | |
1170 | MIME-Version: 1.0 | |
1171 | Subject: A subject | |
1172 | To: aperson@dom.ain | |
1173 | From: bperson@dom.ain | |
1174 | ||
1175 | --BOUNDARY | |
1176 | Content-Type: text/plain; charset="us-ascii" | |
1177 | MIME-Version: 1.0 | |
1178 | Content-Transfer-Encoding: 7bit | |
1179 | ||
1180 | hello world | |
1181 | --BOUNDARY--''') | |
1182 | ||
1183 | ||
1184 | def test_seq_parts_in_a_multipart_with_none_epilogue(self): | |
1185 | eq = self.ndiffAssertEqual | |
1186 | outer = MIMEBase('multipart', 'mixed') | |
1187 | outer['Subject'] = 'A subject' | |
1188 | outer['To'] = 'aperson@dom.ain' | |
1189 | outer['From'] = 'bperson@dom.ain' | |
1190 | outer.epilogue = None | |
1191 | msg = MIMEText('hello world') | |
1192 | outer.attach(msg) | |
1193 | outer.set_boundary('BOUNDARY') | |
1194 | eq(outer.as_string(), '''\ | |
1195 | Content-Type: multipart/mixed; boundary="BOUNDARY" | |
1196 | MIME-Version: 1.0 | |
1197 | Subject: A subject | |
1198 | To: aperson@dom.ain | |
1199 | From: bperson@dom.ain | |
1200 | ||
1201 | --BOUNDARY | |
1202 | Content-Type: text/plain; charset="us-ascii" | |
1203 | MIME-Version: 1.0 | |
1204 | Content-Transfer-Encoding: 7bit | |
1205 | ||
1206 | hello world | |
1207 | --BOUNDARY--''') | |
1208 | ||
1209 | ||
1210 | def test_seq_parts_in_a_multipart_with_empty_epilogue(self): | |
1211 | eq = self.ndiffAssertEqual | |
1212 | outer = MIMEBase('multipart', 'mixed') | |
1213 | outer['Subject'] = 'A subject' | |
1214 | outer['To'] = 'aperson@dom.ain' | |
1215 | outer['From'] = 'bperson@dom.ain' | |
1216 | outer.epilogue = '' | |
1217 | msg = MIMEText('hello world') | |
1218 | outer.attach(msg) | |
1219 | outer.set_boundary('BOUNDARY') | |
1220 | eq(outer.as_string(), '''\ | |
1221 | Content-Type: multipart/mixed; boundary="BOUNDARY" | |
1222 | MIME-Version: 1.0 | |
1223 | Subject: A subject | |
1224 | To: aperson@dom.ain | |
1225 | From: bperson@dom.ain | |
1226 | ||
1227 | --BOUNDARY | |
1228 | Content-Type: text/plain; charset="us-ascii" | |
1229 | MIME-Version: 1.0 | |
1230 | Content-Transfer-Encoding: 7bit | |
1231 | ||
1232 | hello world | |
1233 | --BOUNDARY-- | |
1234 | ''') | |
1235 | ||
1236 | ||
1237 | def test_seq_parts_in_a_multipart_with_nl_epilogue(self): | |
1238 | eq = self.ndiffAssertEqual | |
1239 | outer = MIMEBase('multipart', 'mixed') | |
1240 | outer['Subject'] = 'A subject' | |
1241 | outer['To'] = 'aperson@dom.ain' | |
1242 | outer['From'] = 'bperson@dom.ain' | |
1243 | outer.epilogue = '\n' | |
1244 | msg = MIMEText('hello world') | |
1245 | outer.attach(msg) | |
1246 | outer.set_boundary('BOUNDARY') | |
1247 | eq(outer.as_string(), '''\ | |
1248 | Content-Type: multipart/mixed; boundary="BOUNDARY" | |
1249 | MIME-Version: 1.0 | |
1250 | Subject: A subject | |
1251 | To: aperson@dom.ain | |
1252 | From: bperson@dom.ain | |
1253 | ||
1254 | --BOUNDARY | |
1255 | Content-Type: text/plain; charset="us-ascii" | |
1256 | MIME-Version: 1.0 | |
1257 | Content-Transfer-Encoding: 7bit | |
1258 | ||
1259 | hello world | |
1260 | --BOUNDARY-- | |
1261 | ||
1262 | ''') | |
1263 | ||
1264 | def test_message_external_body(self): | |
1265 | eq = self.assertEqual | |
1266 | msg = self._msgobj('msg_36.txt') | |
1267 | eq(len(msg.get_payload()), 2) | |
1268 | msg1 = msg.get_payload(1) | |
1269 | eq(msg1.get_content_type(), 'multipart/alternative') | |
1270 | eq(len(msg1.get_payload()), 2) | |
1271 | for subpart in msg1.get_payload(): | |
1272 | eq(subpart.get_content_type(), 'message/external-body') | |
1273 | eq(len(subpart.get_payload()), 1) | |
1274 | subsubpart = subpart.get_payload(0) | |
1275 | eq(subsubpart.get_content_type(), 'text/plain') | |
1276 | ||
1277 | def test_double_boundary(self): | |
1278 | # msg_37.txt is a multipart that contains two dash-boundary's in a | |
1279 | # row. Our interpretation of RFC 2046 calls for ignoring the second | |
1280 | # and subsequent boundaries. | |
1281 | msg = self._msgobj('msg_37.txt') | |
1282 | self.assertEqual(len(msg.get_payload()), 3) | |
1283 | ||
1284 | def test_nested_inner_contains_outer_boundary(self): | |
1285 | eq = self.ndiffAssertEqual | |
1286 | # msg_38.txt has an inner part that contains outer boundaries. My | |
1287 | # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say | |
1288 | # these are illegal and should be interpreted as unterminated inner | |
1289 | # parts. | |
1290 | msg = self._msgobj('msg_38.txt') | |
1291 | sfp = StringIO() | |
1292 | Iterators._structure(msg, sfp) | |
1293 | eq(sfp.getvalue(), """\ | |
1294 | multipart/mixed | |
1295 | multipart/mixed | |
1296 | multipart/alternative | |
1297 | text/plain | |
1298 | text/plain | |
1299 | text/plain | |
1300 | text/plain | |
1301 | """) | |
1302 | ||
1303 | def test_nested_with_same_boundary(self): | |
1304 | eq = self.ndiffAssertEqual | |
1305 | # msg 39.txt is similarly evil in that it's got inner parts that use | |
1306 | # the same boundary as outer parts. Again, I believe the way this is | |
1307 | # parsed is closest to the spirit of RFC 2046 | |
1308 | msg = self._msgobj('msg_39.txt') | |
1309 | sfp = StringIO() | |
1310 | Iterators._structure(msg, sfp) | |
1311 | eq(sfp.getvalue(), """\ | |
1312 | multipart/mixed | |
1313 | multipart/mixed | |
1314 | multipart/alternative | |
1315 | application/octet-stream | |
1316 | application/octet-stream | |
1317 | text/plain | |
1318 | """) | |
1319 | ||
1320 | def test_boundary_in_non_multipart(self): | |
1321 | msg = self._msgobj('msg_40.txt') | |
1322 | self.assertEqual(msg.as_string(), '''\ | |
1323 | MIME-Version: 1.0 | |
1324 | Content-Type: text/html; boundary="--961284236552522269" | |
1325 | ||
1326 | ----961284236552522269 | |
1327 | Content-Type: text/html; | |
1328 | Content-Transfer-Encoding: 7Bit | |
1329 | ||
1330 | <html></html> | |
1331 | ||
1332 | ----961284236552522269-- | |
1333 | ''') | |
1334 | ||
1335 | def test_boundary_with_leading_space(self): | |
1336 | eq = self.assertEqual | |
1337 | msg = email.message_from_string('''\ | |
1338 | MIME-Version: 1.0 | |
1339 | Content-Type: multipart/mixed; boundary=" XXXX" | |
1340 | ||
1341 | -- XXXX | |
1342 | Content-Type: text/plain | |
1343 | ||
1344 | ||
1345 | -- XXXX | |
1346 | Content-Type: text/plain | |
1347 | ||
1348 | -- XXXX-- | |
1349 | ''') | |
1350 | self.failUnless(msg.is_multipart()) | |
1351 | eq(msg.get_boundary(), ' XXXX') | |
1352 | eq(len(msg.get_payload()), 2) | |
1353 | ||
1354 | def test_boundary_without_trailing_newline(self): | |
1355 | m = Parser().parsestr("""\ | |
1356 | Content-Type: multipart/mixed; boundary="===============0012394164==" | |
1357 | MIME-Version: 1.0 | |
1358 | ||
1359 | --===============0012394164== | |
1360 | Content-Type: image/file1.jpg | |
1361 | MIME-Version: 1.0 | |
1362 | Content-Transfer-Encoding: base64 | |
1363 | ||
1364 | YXNkZg== | |
1365 | --===============0012394164==--""") | |
1366 | self.assertEquals(m.get_payload(0).get_payload(), 'YXNkZg==') | |
1367 | ||
1368 | ||
1369 | \f | |
1370 | # Test some badly formatted messages | |
1371 | class TestNonConformant(TestEmailBase): | |
1372 | def test_parse_missing_minor_type(self): | |
1373 | eq = self.assertEqual | |
1374 | msg = self._msgobj('msg_14.txt') | |
1375 | eq(msg.get_type(), 'text') | |
1376 | eq(msg.get_content_maintype(), 'text') | |
1377 | eq(msg.get_content_subtype(), 'plain') | |
1378 | ||
1379 | def test_same_boundary_inner_outer(self): | |
1380 | unless = self.failUnless | |
1381 | msg = self._msgobj('msg_15.txt') | |
1382 | # XXX We can probably eventually do better | |
1383 | inner = msg.get_payload(0) | |
1384 | unless(hasattr(inner, 'defects')) | |
1385 | self.assertEqual(len(inner.defects), 1) | |
1386 | unless(isinstance(inner.defects[0], | |
1387 | Errors.StartBoundaryNotFoundDefect)) | |
1388 | ||
1389 | def test_multipart_no_boundary(self): | |
1390 | unless = self.failUnless | |
1391 | msg = self._msgobj('msg_25.txt') | |
1392 | unless(isinstance(msg.get_payload(), str)) | |
1393 | self.assertEqual(len(msg.defects), 2) | |
1394 | unless(isinstance(msg.defects[0], Errors.NoBoundaryInMultipartDefect)) | |
1395 | unless(isinstance(msg.defects[1], | |
1396 | Errors.MultipartInvariantViolationDefect)) | |
1397 | ||
1398 | def test_invalid_content_type(self): | |
1399 | eq = self.assertEqual | |
1400 | neq = self.ndiffAssertEqual | |
1401 | msg = Message() | |
1402 | # RFC 2045, $5.2 says invalid yields text/plain | |
1403 | msg['Content-Type'] = 'text' | |
1404 | eq(msg.get_content_maintype(), 'text') | |
1405 | eq(msg.get_content_subtype(), 'plain') | |
1406 | eq(msg.get_content_type(), 'text/plain') | |
1407 | # Clear the old value and try something /really/ invalid | |
1408 | del msg['content-type'] | |
1409 | msg['Content-Type'] = 'foo' | |
1410 | eq(msg.get_content_maintype(), 'text') | |
1411 | eq(msg.get_content_subtype(), 'plain') | |
1412 | eq(msg.get_content_type(), 'text/plain') | |
1413 | # Still, make sure that the message is idempotently generated | |
1414 | s = StringIO() | |
1415 | g = Generator(s) | |
1416 | g.flatten(msg) | |
1417 | neq(s.getvalue(), 'Content-Type: foo\n\n') | |
1418 | ||
1419 | def test_no_start_boundary(self): | |
1420 | eq = self.ndiffAssertEqual | |
1421 | msg = self._msgobj('msg_31.txt') | |
1422 | eq(msg.get_payload(), """\ | |
1423 | --BOUNDARY | |
1424 | Content-Type: text/plain | |
1425 | ||
1426 | message 1 | |
1427 | ||
1428 | --BOUNDARY | |
1429 | Content-Type: text/plain | |
1430 | ||
1431 | message 2 | |
1432 | ||
1433 | --BOUNDARY-- | |
1434 | """) | |
1435 | ||
1436 | def test_no_separating_blank_line(self): | |
1437 | eq = self.ndiffAssertEqual | |
1438 | msg = self._msgobj('msg_35.txt') | |
1439 | eq(msg.as_string(), """\ | |
1440 | From: aperson@dom.ain | |
1441 | To: bperson@dom.ain | |
1442 | Subject: here's something interesting | |
1443 | ||
1444 | counter to RFC 2822, there's no separating newline here | |
1445 | """) | |
1446 | ||
1447 | def test_lying_multipart(self): | |
1448 | unless = self.failUnless | |
1449 | msg = self._msgobj('msg_41.txt') | |
1450 | unless(hasattr(msg, 'defects')) | |
1451 | self.assertEqual(len(msg.defects), 2) | |
1452 | unless(isinstance(msg.defects[0], Errors.NoBoundaryInMultipartDefect)) | |
1453 | unless(isinstance(msg.defects[1], | |
1454 | Errors.MultipartInvariantViolationDefect)) | |
1455 | ||
1456 | def test_missing_start_boundary(self): | |
1457 | outer = self._msgobj('msg_42.txt') | |
1458 | # The message structure is: | |
1459 | # | |
1460 | # multipart/mixed | |
1461 | # text/plain | |
1462 | # message/rfc822 | |
1463 | # multipart/mixed [*] | |
1464 | # | |
1465 | # [*] This message is missing its start boundary | |
1466 | bad = outer.get_payload(1).get_payload(0) | |
1467 | self.assertEqual(len(bad.defects), 1) | |
1468 | self.failUnless(isinstance(bad.defects[0], | |
1469 | Errors.StartBoundaryNotFoundDefect)) | |
1470 | ||
1471 | ||
1472 | \f | |
1473 | # Test RFC 2047 header encoding and decoding | |
1474 | class TestRFC2047(unittest.TestCase): | |
1475 | def test_rfc2047_multiline(self): | |
1476 | eq = self.assertEqual | |
1477 | s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz | |
1478 | foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""" | |
1479 | dh = decode_header(s) | |
1480 | eq(dh, [ | |
1481 | ('Re:', None), | |
1482 | ('r\x8aksm\x9arg\x8cs', 'mac-iceland'), | |
1483 | ('baz foo bar', None), | |
1484 | ('r\x8aksm\x9arg\x8cs', 'mac-iceland')]) | |
1485 | eq(str(make_header(dh)), | |
1486 | """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar | |
1487 | =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""") | |
1488 | ||
1489 | def test_whitespace_eater_unicode(self): | |
1490 | eq = self.assertEqual | |
1491 | s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard@dom.ain>' | |
1492 | dh = decode_header(s) | |
1493 | eq(dh, [('Andr\xe9', 'iso-8859-1'), ('Pirard <pirard@dom.ain>', None)]) | |
1494 | hu = unicode(make_header(dh)).encode('latin-1') | |
1495 | eq(hu, 'Andr\xe9 Pirard <pirard@dom.ain>') | |
1496 | ||
1497 | def test_whitespace_eater_unicode_2(self): | |
1498 | eq = self.assertEqual | |
1499 | s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?=' | |
1500 | dh = decode_header(s) | |
1501 | eq(dh, [('The', None), ('quick brown fox', 'iso-8859-1'), | |
1502 | ('jumped over the', None), ('lazy dog', 'iso-8859-1')]) | |
1503 | hu = make_header(dh).__unicode__() | |
1504 | eq(hu, u'The quick brown fox jumped over the lazy dog') | |
1505 | ||
1506 | ||
1507 | \f | |
1508 | # Test the MIMEMessage class | |
1509 | class TestMIMEMessage(TestEmailBase): | |
1510 | def setUp(self): | |
1511 | fp = openfile('msg_11.txt') | |
1512 | try: | |
1513 | self._text = fp.read() | |
1514 | finally: | |
1515 | fp.close() | |
1516 | ||
1517 | def test_type_error(self): | |
1518 | self.assertRaises(TypeError, MIMEMessage, 'a plain string') | |
1519 | ||
1520 | def test_valid_argument(self): | |
1521 | eq = self.assertEqual | |
1522 | unless = self.failUnless | |
1523 | subject = 'A sub-message' | |
1524 | m = Message() | |
1525 | m['Subject'] = subject | |
1526 | r = MIMEMessage(m) | |
1527 | eq(r.get_type(), 'message/rfc822') | |
1528 | payload = r.get_payload() | |
1529 | unless(isinstance(payload, list)) | |
1530 | eq(len(payload), 1) | |
1531 | subpart = payload[0] | |
1532 | unless(subpart is m) | |
1533 | eq(subpart['subject'], subject) | |
1534 | ||
1535 | def test_bad_multipart(self): | |
1536 | eq = self.assertEqual | |
1537 | msg1 = Message() | |
1538 | msg1['Subject'] = 'subpart 1' | |
1539 | msg2 = Message() | |
1540 | msg2['Subject'] = 'subpart 2' | |
1541 | r = MIMEMessage(msg1) | |
1542 | self.assertRaises(Errors.MultipartConversionError, r.attach, msg2) | |
1543 | ||
1544 | def test_generate(self): | |
1545 | # First craft the message to be encapsulated | |
1546 | m = Message() | |
1547 | m['Subject'] = 'An enclosed message' | |
1548 | m.set_payload('Here is the body of the message.\n') | |
1549 | r = MIMEMessage(m) | |
1550 | r['Subject'] = 'The enclosing message' | |
1551 | s = StringIO() | |
1552 | g = Generator(s) | |
1553 | g.flatten(r) | |
1554 | self.assertEqual(s.getvalue(), """\ | |
1555 | Content-Type: message/rfc822 | |
1556 | MIME-Version: 1.0 | |
1557 | Subject: The enclosing message | |
1558 | ||
1559 | Subject: An enclosed message | |
1560 | ||
1561 | Here is the body of the message. | |
1562 | """) | |
1563 | ||
1564 | def test_parse_message_rfc822(self): | |
1565 | eq = self.assertEqual | |
1566 | unless = self.failUnless | |
1567 | msg = self._msgobj('msg_11.txt') | |
1568 | eq(msg.get_type(), 'message/rfc822') | |
1569 | payload = msg.get_payload() | |
1570 | unless(isinstance(payload, list)) | |
1571 | eq(len(payload), 1) | |
1572 | submsg = payload[0] | |
1573 | self.failUnless(isinstance(submsg, Message)) | |
1574 | eq(submsg['subject'], 'An enclosed message') | |
1575 | eq(submsg.get_payload(), 'Here is the body of the message.\n') | |
1576 | ||
1577 | def test_dsn(self): | |
1578 | eq = self.assertEqual | |
1579 | unless = self.failUnless | |
1580 | # msg 16 is a Delivery Status Notification, see RFC 1894 | |
1581 | msg = self._msgobj('msg_16.txt') | |
1582 | eq(msg.get_type(), 'multipart/report') | |
1583 | unless(msg.is_multipart()) | |
1584 | eq(len(msg.get_payload()), 3) | |
1585 | # Subpart 1 is a text/plain, human readable section | |
1586 | subpart = msg.get_payload(0) | |
1587 | eq(subpart.get_type(), 'text/plain') | |
1588 | eq(subpart.get_payload(), """\ | |
1589 | This report relates to a message you sent with the following header fields: | |
1590 | ||
1591 | Message-id: <002001c144a6$8752e060$56104586@oxy.edu> | |
1592 | Date: Sun, 23 Sep 2001 20:10:55 -0700 | |
1593 | From: "Ian T. Henry" <henryi@oxy.edu> | |
1594 | To: SoCal Raves <scr@socal-raves.org> | |
1595 | Subject: [scr] yeah for Ians!! | |
1596 | ||
1597 | Your message cannot be delivered to the following recipients: | |
1598 | ||
1599 | Recipient address: jangel1@cougar.noc.ucla.edu | |
1600 | Reason: recipient reached disk quota | |
1601 | ||
1602 | """) | |
1603 | # Subpart 2 contains the machine parsable DSN information. It | |
1604 | # consists of two blocks of headers, represented by two nested Message | |
1605 | # objects. | |
1606 | subpart = msg.get_payload(1) | |
1607 | eq(subpart.get_type(), 'message/delivery-status') | |
1608 | eq(len(subpart.get_payload()), 2) | |
1609 | # message/delivery-status should treat each block as a bunch of | |
1610 | # headers, i.e. a bunch of Message objects. | |
1611 | dsn1 = subpart.get_payload(0) | |
1612 | unless(isinstance(dsn1, Message)) | |
1613 | eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu') | |
1614 | eq(dsn1.get_param('dns', header='reporting-mta'), '') | |
1615 | # Try a missing one <wink> | |
1616 | eq(dsn1.get_param('nsd', header='reporting-mta'), None) | |
1617 | dsn2 = subpart.get_payload(1) | |
1618 | unless(isinstance(dsn2, Message)) | |
1619 | eq(dsn2['action'], 'failed') | |
1620 | eq(dsn2.get_params(header='original-recipient'), | |
1621 | [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')]) | |
1622 | eq(dsn2.get_param('rfc822', header='final-recipient'), '') | |
1623 | # Subpart 3 is the original message | |
1624 | subpart = msg.get_payload(2) | |
1625 | eq(subpart.get_type(), 'message/rfc822') | |
1626 | payload = subpart.get_payload() | |
1627 | unless(isinstance(payload, list)) | |
1628 | eq(len(payload), 1) | |
1629 | subsubpart = payload[0] | |
1630 | unless(isinstance(subsubpart, Message)) | |
1631 | eq(subsubpart.get_type(), 'text/plain') | |
1632 | eq(subsubpart['message-id'], | |
1633 | '<002001c144a6$8752e060$56104586@oxy.edu>') | |
1634 | ||
1635 | def test_epilogue(self): | |
1636 | eq = self.ndiffAssertEqual | |
1637 | fp = openfile('msg_21.txt') | |
1638 | try: | |
1639 | text = fp.read() | |
1640 | finally: | |
1641 | fp.close() | |
1642 | msg = Message() | |
1643 | msg['From'] = 'aperson@dom.ain' | |
1644 | msg['To'] = 'bperson@dom.ain' | |
1645 | msg['Subject'] = 'Test' | |
1646 | msg.preamble = 'MIME message' | |
1647 | msg.epilogue = 'End of MIME message\n' | |
1648 | msg1 = MIMEText('One') | |
1649 | msg2 = MIMEText('Two') | |
1650 | msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY') | |
1651 | msg.attach(msg1) | |
1652 | msg.attach(msg2) | |
1653 | sfp = StringIO() | |
1654 | g = Generator(sfp) | |
1655 | g.flatten(msg) | |
1656 | eq(sfp.getvalue(), text) | |
1657 | ||
1658 | def test_no_nl_preamble(self): | |
1659 | eq = self.ndiffAssertEqual | |
1660 | msg = Message() | |
1661 | msg['From'] = 'aperson@dom.ain' | |
1662 | msg['To'] = 'bperson@dom.ain' | |
1663 | msg['Subject'] = 'Test' | |
1664 | msg.preamble = 'MIME message' | |
1665 | msg.epilogue = '' | |
1666 | msg1 = MIMEText('One') | |
1667 | msg2 = MIMEText('Two') | |
1668 | msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY') | |
1669 | msg.attach(msg1) | |
1670 | msg.attach(msg2) | |
1671 | eq(msg.as_string(), """\ | |
1672 | From: aperson@dom.ain | |
1673 | To: bperson@dom.ain | |
1674 | Subject: Test | |
1675 | Content-Type: multipart/mixed; boundary="BOUNDARY" | |
1676 | ||
1677 | MIME message | |
1678 | --BOUNDARY | |
1679 | Content-Type: text/plain; charset="us-ascii" | |
1680 | MIME-Version: 1.0 | |
1681 | Content-Transfer-Encoding: 7bit | |
1682 | ||
1683 | One | |
1684 | --BOUNDARY | |
1685 | Content-Type: text/plain; charset="us-ascii" | |
1686 | MIME-Version: 1.0 | |
1687 | Content-Transfer-Encoding: 7bit | |
1688 | ||
1689 | Two | |
1690 | --BOUNDARY-- | |
1691 | """) | |
1692 | ||
1693 | def test_default_type(self): | |
1694 | eq = self.assertEqual | |
1695 | fp = openfile('msg_30.txt') | |
1696 | try: | |
1697 | msg = email.message_from_file(fp) | |
1698 | finally: | |
1699 | fp.close() | |
1700 | container1 = msg.get_payload(0) | |
1701 | eq(container1.get_default_type(), 'message/rfc822') | |
1702 | eq(container1.get_type(), None) | |
1703 | container2 = msg.get_payload(1) | |
1704 | eq(container2.get_default_type(), 'message/rfc822') | |
1705 | eq(container2.get_type(), None) | |
1706 | container1a = container1.get_payload(0) | |
1707 | eq(container1a.get_default_type(), 'text/plain') | |
1708 | eq(container1a.get_type(), 'text/plain') | |
1709 | container2a = container2.get_payload(0) | |
1710 | eq(container2a.get_default_type(), 'text/plain') | |
1711 | eq(container2a.get_type(), 'text/plain') | |
1712 | ||
1713 | def test_default_type_with_explicit_container_type(self): | |
1714 | eq = self.assertEqual | |
1715 | fp = openfile('msg_28.txt') | |
1716 | try: | |
1717 | msg = email.message_from_file(fp) | |
1718 | finally: | |
1719 | fp.close() | |
1720 | container1 = msg.get_payload(0) | |
1721 | eq(container1.get_default_type(), 'message/rfc822') | |
1722 | eq(container1.get_type(), 'message/rfc822') | |
1723 | container2 = msg.get_payload(1) | |
1724 | eq(container2.get_default_type(), 'message/rfc822') | |
1725 | eq(container2.get_type(), 'message/rfc822') | |
1726 | container1a = container1.get_payload(0) | |
1727 | eq(container1a.get_default_type(), 'text/plain') | |
1728 | eq(container1a.get_type(), 'text/plain') | |
1729 | container2a = container2.get_payload(0) | |
1730 | eq(container2a.get_default_type(), 'text/plain') | |
1731 | eq(container2a.get_type(), 'text/plain') | |
1732 | ||
1733 | def test_default_type_non_parsed(self): | |
1734 | eq = self.assertEqual | |
1735 | neq = self.ndiffAssertEqual | |
1736 | # Set up container | |
1737 | container = MIMEMultipart('digest', 'BOUNDARY') | |
1738 | container.epilogue = '' | |
1739 | # Set up subparts | |
1740 | subpart1a = MIMEText('message 1\n') | |
1741 | subpart2a = MIMEText('message 2\n') | |
1742 | subpart1 = MIMEMessage(subpart1a) | |
1743 | subpart2 = MIMEMessage(subpart2a) | |
1744 | container.attach(subpart1) | |
1745 | container.attach(subpart2) | |
1746 | eq(subpart1.get_type(), 'message/rfc822') | |
1747 | eq(subpart1.get_default_type(), 'message/rfc822') | |
1748 | eq(subpart2.get_type(), 'message/rfc822') | |
1749 | eq(subpart2.get_default_type(), 'message/rfc822') | |
1750 | neq(container.as_string(0), '''\ | |
1751 | Content-Type: multipart/digest; boundary="BOUNDARY" | |
1752 | MIME-Version: 1.0 | |
1753 | ||
1754 | --BOUNDARY | |
1755 | Content-Type: message/rfc822 | |
1756 | MIME-Version: 1.0 | |
1757 | ||
1758 | Content-Type: text/plain; charset="us-ascii" | |
1759 | MIME-Version: 1.0 | |
1760 | Content-Transfer-Encoding: 7bit | |
1761 | ||
1762 | message 1 | |
1763 | ||
1764 | --BOUNDARY | |
1765 | Content-Type: message/rfc822 | |
1766 | MIME-Version: 1.0 | |
1767 | ||
1768 | Content-Type: text/plain; charset="us-ascii" | |
1769 | MIME-Version: 1.0 | |
1770 | Content-Transfer-Encoding: 7bit | |
1771 | ||
1772 | message 2 | |
1773 | ||
1774 | --BOUNDARY-- | |
1775 | ''') | |
1776 | del subpart1['content-type'] | |
1777 | del subpart1['mime-version'] | |
1778 | del subpart2['content-type'] | |
1779 | del subpart2['mime-version'] | |
1780 | eq(subpart1.get_type(), None) | |
1781 | eq(subpart1.get_default_type(), 'message/rfc822') | |
1782 | eq(subpart2.get_type(), None) | |
1783 | eq(subpart2.get_default_type(), 'message/rfc822') | |
1784 | neq(container.as_string(0), '''\ | |
1785 | Content-Type: multipart/digest; boundary="BOUNDARY" | |
1786 | MIME-Version: 1.0 | |
1787 | ||
1788 | --BOUNDARY | |
1789 | ||
1790 | Content-Type: text/plain; charset="us-ascii" | |
1791 | MIME-Version: 1.0 | |
1792 | Content-Transfer-Encoding: 7bit | |
1793 | ||
1794 | message 1 | |
1795 | ||
1796 | --BOUNDARY | |
1797 | ||
1798 | Content-Type: text/plain; charset="us-ascii" | |
1799 | MIME-Version: 1.0 | |
1800 | Content-Transfer-Encoding: 7bit | |
1801 | ||
1802 | message 2 | |
1803 | ||
1804 | --BOUNDARY-- | |
1805 | ''') | |
1806 | ||
1807 | def test_mime_attachments_in_constructor(self): | |
1808 | eq = self.assertEqual | |
1809 | text1 = MIMEText('') | |
1810 | text2 = MIMEText('') | |
1811 | msg = MIMEMultipart(_subparts=(text1, text2)) | |
1812 | eq(len(msg.get_payload()), 2) | |
1813 | eq(msg.get_payload(0), text1) | |
1814 | eq(msg.get_payload(1), text2) | |
1815 | ||
1816 | ||
1817 | \f | |
1818 | # A general test of parser->model->generator idempotency. IOW, read a message | |
1819 | # in, parse it into a message object tree, then without touching the tree, | |
1820 | # regenerate the plain text. The original text and the transformed text | |
1821 | # should be identical. Note: that we ignore the Unix-From since that may | |
1822 | # contain a changed date. | |
1823 | class TestIdempotent(TestEmailBase): | |
1824 | def _msgobj(self, filename): | |
1825 | fp = openfile(filename) | |
1826 | try: | |
1827 | data = fp.read() | |
1828 | finally: | |
1829 | fp.close() | |
1830 | msg = email.message_from_string(data) | |
1831 | return msg, data | |
1832 | ||
1833 | def _idempotent(self, msg, text): | |
1834 | eq = self.ndiffAssertEqual | |
1835 | s = StringIO() | |
1836 | g = Generator(s, maxheaderlen=0) | |
1837 | g.flatten(msg) | |
1838 | eq(text, s.getvalue()) | |
1839 | ||
1840 | def test_parse_text_message(self): | |
1841 | eq = self.assertEquals | |
1842 | msg, text = self._msgobj('msg_01.txt') | |
1843 | eq(msg.get_type(), 'text/plain') | |
1844 | eq(msg.get_content_maintype(), 'text') | |
1845 | eq(msg.get_content_subtype(), 'plain') | |
1846 | eq(msg.get_params()[1], ('charset', 'us-ascii')) | |
1847 | eq(msg.get_param('charset'), 'us-ascii') | |
1848 | eq(msg.preamble, None) | |
1849 | eq(msg.epilogue, None) | |
1850 | self._idempotent(msg, text) | |
1851 | ||
1852 | def test_parse_untyped_message(self): | |
1853 | eq = self.assertEquals | |
1854 | msg, text = self._msgobj('msg_03.txt') | |
1855 | eq(msg.get_type(), None) | |
1856 | eq(msg.get_params(), None) | |
1857 | eq(msg.get_param('charset'), None) | |
1858 | self._idempotent(msg, text) | |
1859 | ||
1860 | def test_simple_multipart(self): | |
1861 | msg, text = self._msgobj('msg_04.txt') | |
1862 | self._idempotent(msg, text) | |
1863 | ||
1864 | def test_MIME_digest(self): | |
1865 | msg, text = self._msgobj('msg_02.txt') | |
1866 | self._idempotent(msg, text) | |
1867 | ||
1868 | def test_long_header(self): | |
1869 | msg, text = self._msgobj('msg_27.txt') | |
1870 | self._idempotent(msg, text) | |
1871 | ||
1872 | def test_MIME_digest_with_part_headers(self): | |
1873 | msg, text = self._msgobj('msg_28.txt') | |
1874 | self._idempotent(msg, text) | |
1875 | ||
1876 | def test_mixed_with_image(self): | |
1877 | msg, text = self._msgobj('msg_06.txt') | |
1878 | self._idempotent(msg, text) | |
1879 | ||
1880 | def test_multipart_report(self): | |
1881 | msg, text = self._msgobj('msg_05.txt') | |
1882 | self._idempotent(msg, text) | |
1883 | ||
1884 | def test_dsn(self): | |
1885 | msg, text = self._msgobj('msg_16.txt') | |
1886 | self._idempotent(msg, text) | |
1887 | ||
1888 | def test_preamble_epilogue(self): | |
1889 | msg, text = self._msgobj('msg_21.txt') | |
1890 | self._idempotent(msg, text) | |
1891 | ||
1892 | def test_multipart_one_part(self): | |
1893 | msg, text = self._msgobj('msg_23.txt') | |
1894 | self._idempotent(msg, text) | |
1895 | ||
1896 | def test_multipart_no_parts(self): | |
1897 | msg, text = self._msgobj('msg_24.txt') | |
1898 | self._idempotent(msg, text) | |
1899 | ||
1900 | def test_no_start_boundary(self): | |
1901 | msg, text = self._msgobj('msg_31.txt') | |
1902 | self._idempotent(msg, text) | |
1903 | ||
1904 | def test_rfc2231_charset(self): | |
1905 | msg, text = self._msgobj('msg_32.txt') | |
1906 | self._idempotent(msg, text) | |
1907 | ||
1908 | def test_more_rfc2231_parameters(self): | |
1909 | msg, text = self._msgobj('msg_33.txt') | |
1910 | self._idempotent(msg, text) | |
1911 | ||
1912 | def test_text_plain_in_a_multipart_digest(self): | |
1913 | msg, text = self._msgobj('msg_34.txt') | |
1914 | self._idempotent(msg, text) | |
1915 | ||
1916 | def test_nested_multipart_mixeds(self): | |
1917 | msg, text = self._msgobj('msg_12a.txt') | |
1918 | self._idempotent(msg, text) | |
1919 | ||
1920 | def test_message_external_body_idempotent(self): | |
1921 | msg, text = self._msgobj('msg_36.txt') | |
1922 | self._idempotent(msg, text) | |
1923 | ||
1924 | def test_content_type(self): | |
1925 | eq = self.assertEquals | |
1926 | unless = self.failUnless | |
1927 | # Get a message object and reset the seek pointer for other tests | |
1928 | msg, text = self._msgobj('msg_05.txt') | |
1929 | eq(msg.get_type(), 'multipart/report') | |
1930 | # Test the Content-Type: parameters | |
1931 | params = {} | |
1932 | for pk, pv in msg.get_params(): | |
1933 | params[pk] = pv | |
1934 | eq(params['report-type'], 'delivery-status') | |
1935 | eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com') | |
1936 | eq(msg.preamble, 'This is a MIME-encapsulated message.\n') | |
1937 | eq(msg.epilogue, '\n') | |
1938 | eq(len(msg.get_payload()), 3) | |
1939 | # Make sure the subparts are what we expect | |
1940 | msg1 = msg.get_payload(0) | |
1941 | eq(msg1.get_type(), 'text/plain') | |
1942 | eq(msg1.get_payload(), 'Yadda yadda yadda\n') | |
1943 | msg2 = msg.get_payload(1) | |
1944 | eq(msg2.get_type(), None) | |
1945 | eq(msg2.get_payload(), 'Yadda yadda yadda\n') | |
1946 | msg3 = msg.get_payload(2) | |
1947 | eq(msg3.get_type(), 'message/rfc822') | |
1948 | self.failUnless(isinstance(msg3, Message)) | |
1949 | payload = msg3.get_payload() | |
1950 | unless(isinstance(payload, list)) | |
1951 | eq(len(payload), 1) | |
1952 | msg4 = payload[0] | |
1953 | unless(isinstance(msg4, Message)) | |
1954 | eq(msg4.get_payload(), 'Yadda yadda yadda\n') | |
1955 | ||
1956 | def test_parser(self): | |
1957 | eq = self.assertEquals | |
1958 | unless = self.failUnless | |
1959 | msg, text = self._msgobj('msg_06.txt') | |
1960 | # Check some of the outer headers | |
1961 | eq(msg.get_type(), 'message/rfc822') | |
1962 | # Make sure the payload is a list of exactly one sub-Message, and that | |
1963 | # that submessage has a type of text/plain | |
1964 | payload = msg.get_payload() | |
1965 | unless(isinstance(payload, list)) | |
1966 | eq(len(payload), 1) | |
1967 | msg1 = payload[0] | |
1968 | self.failUnless(isinstance(msg1, Message)) | |
1969 | eq(msg1.get_type(), 'text/plain') | |
1970 | self.failUnless(isinstance(msg1.get_payload(), str)) | |
1971 | eq(msg1.get_payload(), '\n') | |
1972 | ||
1973 | ||
1974 | \f | |
1975 | # Test various other bits of the package's functionality | |
1976 | class TestMiscellaneous(TestEmailBase): | |
1977 | def test_message_from_string(self): | |
1978 | fp = openfile('msg_01.txt') | |
1979 | try: | |
1980 | text = fp.read() | |
1981 | finally: | |
1982 | fp.close() | |
1983 | msg = email.message_from_string(text) | |
1984 | s = StringIO() | |
1985 | # Don't wrap/continue long headers since we're trying to test | |
1986 | # idempotency. | |
1987 | g = Generator(s, maxheaderlen=0) | |
1988 | g.flatten(msg) | |
1989 | self.assertEqual(text, s.getvalue()) | |
1990 | ||
1991 | def test_message_from_file(self): | |
1992 | fp = openfile('msg_01.txt') | |
1993 | try: | |
1994 | text = fp.read() | |
1995 | fp.seek(0) | |
1996 | msg = email.message_from_file(fp) | |
1997 | s = StringIO() | |
1998 | # Don't wrap/continue long headers since we're trying to test | |
1999 | # idempotency. | |
2000 | g = Generator(s, maxheaderlen=0) | |
2001 | g.flatten(msg) | |
2002 | self.assertEqual(text, s.getvalue()) | |
2003 | finally: | |
2004 | fp.close() | |
2005 | ||
2006 | def test_message_from_string_with_class(self): | |
2007 | unless = self.failUnless | |
2008 | fp = openfile('msg_01.txt') | |
2009 | try: | |
2010 | text = fp.read() | |
2011 | finally: | |
2012 | fp.close() | |
2013 | # Create a subclass | |
2014 | class MyMessage(Message): | |
2015 | pass | |
2016 | ||
2017 | msg = email.message_from_string(text, MyMessage) | |
2018 | unless(isinstance(msg, MyMessage)) | |
2019 | # Try something more complicated | |
2020 | fp = openfile('msg_02.txt') | |
2021 | try: | |
2022 | text = fp.read() | |
2023 | finally: | |
2024 | fp.close() | |
2025 | msg = email.message_from_string(text, MyMessage) | |
2026 | for subpart in msg.walk(): | |
2027 | unless(isinstance(subpart, MyMessage)) | |
2028 | ||
2029 | def test_message_from_file_with_class(self): | |
2030 | unless = self.failUnless | |
2031 | # Create a subclass | |
2032 | class MyMessage(Message): | |
2033 | pass | |
2034 | ||
2035 | fp = openfile('msg_01.txt') | |
2036 | try: | |
2037 | msg = email.message_from_file(fp, MyMessage) | |
2038 | finally: | |
2039 | fp.close() | |
2040 | unless(isinstance(msg, MyMessage)) | |
2041 | # Try something more complicated | |
2042 | fp = openfile('msg_02.txt') | |
2043 | try: | |
2044 | msg = email.message_from_file(fp, MyMessage) | |
2045 | finally: | |
2046 | fp.close() | |
2047 | for subpart in msg.walk(): | |
2048 | unless(isinstance(subpart, MyMessage)) | |
2049 | ||
2050 | def test__all__(self): | |
2051 | module = __import__('email') | |
2052 | all = module.__all__ | |
2053 | all.sort() | |
2054 | self.assertEqual(all, ['Charset', 'Encoders', 'Errors', 'Generator', | |
2055 | 'Header', 'Iterators', 'MIMEAudio', 'MIMEBase', | |
2056 | 'MIMEImage', 'MIMEMessage', 'MIMEMultipart', | |
2057 | 'MIMENonMultipart', 'MIMEText', 'Message', | |
2058 | 'Parser', 'Utils', 'base64MIME', | |
2059 | 'message_from_file', 'message_from_string', | |
2060 | 'quopriMIME']) | |
2061 | ||
2062 | def test_formatdate(self): | |
2063 | now = time.time() | |
2064 | self.assertEqual(Utils.parsedate(Utils.formatdate(now))[:6], | |
2065 | time.gmtime(now)[:6]) | |
2066 | ||
2067 | def test_formatdate_localtime(self): | |
2068 | now = time.time() | |
2069 | self.assertEqual( | |
2070 | Utils.parsedate(Utils.formatdate(now, localtime=True))[:6], | |
2071 | time.localtime(now)[:6]) | |
2072 | ||
2073 | def test_formatdate_usegmt(self): | |
2074 | now = time.time() | |
2075 | self.assertEqual( | |
2076 | Utils.formatdate(now, localtime=False), | |
2077 | time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now))) | |
2078 | self.assertEqual( | |
2079 | Utils.formatdate(now, localtime=False, usegmt=True), | |
2080 | time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now))) | |
2081 | ||
2082 | def test_parsedate_none(self): | |
2083 | self.assertEqual(Utils.parsedate(''), None) | |
2084 | ||
2085 | def test_parsedate_compact(self): | |
2086 | # The FWS after the comma is optional | |
2087 | self.assertEqual(Utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'), | |
2088 | Utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800')) | |
2089 | ||
2090 | def test_parsedate_no_dayofweek(self): | |
2091 | eq = self.assertEqual | |
2092 | eq(Utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'), | |
2093 | (2003, 2, 25, 13, 47, 26, 0, 1, 0, -28800)) | |
2094 | ||
2095 | def test_parsedate_compact_no_dayofweek(self): | |
2096 | eq = self.assertEqual | |
2097 | eq(Utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'), | |
2098 | (2003, 2, 5, 13, 47, 26, 0, 1, 0, -28800)) | |
2099 | ||
2100 | def test_parseaddr_empty(self): | |
2101 | self.assertEqual(Utils.parseaddr('<>'), ('', '')) | |
2102 | self.assertEqual(Utils.formataddr(Utils.parseaddr('<>')), '') | |
2103 | ||
2104 | def test_noquote_dump(self): | |
2105 | self.assertEqual( | |
2106 | Utils.formataddr(('A Silly Person', 'person@dom.ain')), | |
2107 | 'A Silly Person <person@dom.ain>') | |
2108 | ||
2109 | def test_escape_dump(self): | |
2110 | self.assertEqual( | |
2111 | Utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')), | |
2112 | r'"A \(Very\) Silly Person" <person@dom.ain>') | |
2113 | a = r'A \(Special\) Person' | |
2114 | b = 'person@dom.ain' | |
2115 | self.assertEqual(Utils.parseaddr(Utils.formataddr((a, b))), (a, b)) | |
2116 | ||
2117 | def test_escape_backslashes(self): | |
2118 | self.assertEqual( | |
2119 | Utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')), | |
2120 | r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>') | |
2121 | a = r'Arthur \Backslash\ Foobar' | |
2122 | b = 'person@dom.ain' | |
2123 | self.assertEqual(Utils.parseaddr(Utils.formataddr((a, b))), (a, b)) | |
2124 | ||
2125 | def test_name_with_dot(self): | |
2126 | x = 'John X. Doe <jxd@example.com>' | |
2127 | y = '"John X. Doe" <jxd@example.com>' | |
2128 | a, b = ('John X. Doe', 'jxd@example.com') | |
2129 | self.assertEqual(Utils.parseaddr(x), (a, b)) | |
2130 | self.assertEqual(Utils.parseaddr(y), (a, b)) | |
2131 | # formataddr() quotes the name if there's a dot in it | |
2132 | self.assertEqual(Utils.formataddr((a, b)), y) | |
2133 | ||
2134 | def test_quote_dump(self): | |
2135 | self.assertEqual( | |
2136 | Utils.formataddr(('A Silly; Person', 'person@dom.ain')), | |
2137 | r'"A Silly; Person" <person@dom.ain>') | |
2138 | ||
2139 | def test_fix_eols(self): | |
2140 | eq = self.assertEqual | |
2141 | eq(Utils.fix_eols('hello'), 'hello') | |
2142 | eq(Utils.fix_eols('hello\n'), 'hello\r\n') | |
2143 | eq(Utils.fix_eols('hello\r'), 'hello\r\n') | |
2144 | eq(Utils.fix_eols('hello\r\n'), 'hello\r\n') | |
2145 | eq(Utils.fix_eols('hello\n\r'), 'hello\r\n\r\n') | |
2146 | ||
2147 | def test_charset_richcomparisons(self): | |
2148 | eq = self.assertEqual | |
2149 | ne = self.failIfEqual | |
2150 | cset1 = Charset() | |
2151 | cset2 = Charset() | |
2152 | eq(cset1, 'us-ascii') | |
2153 | eq(cset1, 'US-ASCII') | |
2154 | eq(cset1, 'Us-AsCiI') | |
2155 | eq('us-ascii', cset1) | |
2156 | eq('US-ASCII', cset1) | |
2157 | eq('Us-AsCiI', cset1) | |
2158 | ne(cset1, 'usascii') | |
2159 | ne(cset1, 'USASCII') | |
2160 | ne(cset1, 'UsAsCiI') | |
2161 | ne('usascii', cset1) | |
2162 | ne('USASCII', cset1) | |
2163 | ne('UsAsCiI', cset1) | |
2164 | eq(cset1, cset2) | |
2165 | eq(cset2, cset1) | |
2166 | ||
2167 | def test_getaddresses(self): | |
2168 | eq = self.assertEqual | |
2169 | eq(Utils.getaddresses(['aperson@dom.ain (Al Person)', | |
2170 | 'Bud Person <bperson@dom.ain>']), | |
2171 | [('Al Person', 'aperson@dom.ain'), | |
2172 | ('Bud Person', 'bperson@dom.ain')]) | |
2173 | ||
2174 | def test_getaddresses_nasty(self): | |
2175 | eq = self.assertEqual | |
2176 | eq(Utils.getaddresses(['foo: ;']), [('', '')]) | |
2177 | eq(Utils.getaddresses( | |
2178 | ['[]*-- =~$']), | |
2179 | [('', ''), ('', ''), ('', '*--')]) | |
2180 | eq(Utils.getaddresses( | |
2181 | ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']), | |
2182 | [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]) | |
2183 | ||
2184 | def test_utils_quote_unquote(self): | |
2185 | eq = self.assertEqual | |
2186 | msg = Message() | |
2187 | msg.add_header('content-disposition', 'attachment', | |
2188 | filename='foo\\wacky"name') | |
2189 | eq(msg.get_filename(), 'foo\\wacky"name') | |
2190 | ||
2191 | def test_get_body_encoding_with_bogus_charset(self): | |
2192 | charset = Charset('not a charset') | |
2193 | self.assertEqual(charset.get_body_encoding(), 'base64') | |
2194 | ||
2195 | def test_get_body_encoding_with_uppercase_charset(self): | |
2196 | eq = self.assertEqual | |
2197 | msg = Message() | |
2198 | msg['Content-Type'] = 'text/plain; charset=UTF-8' | |
2199 | eq(msg['content-type'], 'text/plain; charset=UTF-8') | |
2200 | charsets = msg.get_charsets() | |
2201 | eq(len(charsets), 1) | |
2202 | eq(charsets[0], 'utf-8') | |
2203 | charset = Charset(charsets[0]) | |
2204 | eq(charset.get_body_encoding(), 'base64') | |
2205 | msg.set_payload('hello world', charset=charset) | |
2206 | eq(msg.get_payload(), 'hello world') | |
2207 | eq(msg['content-transfer-encoding'], 'base64') | |
2208 | # Try another one | |
2209 | msg = Message() | |
2210 | msg['Content-Type'] = 'text/plain; charset="US-ASCII"' | |
2211 | charsets = msg.get_charsets() | |
2212 | eq(len(charsets), 1) | |
2213 | eq(charsets[0], 'us-ascii') | |
2214 | charset = Charset(charsets[0]) | |
2215 | eq(charset.get_body_encoding(), Encoders.encode_7or8bit) | |
2216 | msg.set_payload('hello world', charset=charset) | |
2217 | eq(msg.get_payload(), 'hello world') | |
2218 | eq(msg['content-transfer-encoding'], '7bit') | |
2219 | ||
2220 | def test_charsets_case_insensitive(self): | |
2221 | lc = Charset('us-ascii') | |
2222 | uc = Charset('US-ASCII') | |
2223 | self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding()) | |
2224 | ||
2225 | def test_partial_falls_inside_message_delivery_status(self): | |
2226 | eq = self.ndiffAssertEqual | |
2227 | # The Parser interface provides chunks of data to FeedParser in 8192 | |
2228 | # byte gulps. SF bug #1076485 found one of those chunks inside | |
2229 | # message/delivery-status header block, which triggered an | |
2230 | # unreadline() of NeedMoreData. | |
2231 | msg = self._msgobj('msg_43.txt') | |
2232 | sfp = StringIO() | |
2233 | Iterators._structure(msg, sfp) | |
2234 | eq(sfp.getvalue(), """\ | |
2235 | multipart/report | |
2236 | text/plain | |
2237 | message/delivery-status | |
2238 | text/plain | |
2239 | text/plain | |
2240 | text/plain | |
2241 | text/plain | |
2242 | text/plain | |
2243 | text/plain | |
2244 | text/plain | |
2245 | text/plain | |
2246 | text/plain | |
2247 | text/plain | |
2248 | text/plain | |
2249 | text/plain | |
2250 | text/plain | |
2251 | text/plain | |
2252 | text/plain | |
2253 | text/plain | |
2254 | text/plain | |
2255 | text/plain | |
2256 | text/plain | |
2257 | text/plain | |
2258 | text/plain | |
2259 | text/plain | |
2260 | text/plain | |
2261 | text/plain | |
2262 | text/plain | |
2263 | text/plain | |
2264 | text/rfc822-headers | |
2265 | """) | |
2266 | ||
2267 | ||
2268 | \f | |
2269 | # Test the iterator/generators | |
2270 | class TestIterators(TestEmailBase): | |
2271 | def test_body_line_iterator(self): | |
2272 | eq = self.assertEqual | |
2273 | neq = self.ndiffAssertEqual | |
2274 | # First a simple non-multipart message | |
2275 | msg = self._msgobj('msg_01.txt') | |
2276 | it = Iterators.body_line_iterator(msg) | |
2277 | lines = list(it) | |
2278 | eq(len(lines), 6) | |
2279 | neq(EMPTYSTRING.join(lines), msg.get_payload()) | |
2280 | # Now a more complicated multipart | |
2281 | msg = self._msgobj('msg_02.txt') | |
2282 | it = Iterators.body_line_iterator(msg) | |
2283 | lines = list(it) | |
2284 | eq(len(lines), 43) | |
2285 | fp = openfile('msg_19.txt') | |
2286 | try: | |
2287 | neq(EMPTYSTRING.join(lines), fp.read()) | |
2288 | finally: | |
2289 | fp.close() | |
2290 | ||
2291 | def test_typed_subpart_iterator(self): | |
2292 | eq = self.assertEqual | |
2293 | msg = self._msgobj('msg_04.txt') | |
2294 | it = Iterators.typed_subpart_iterator(msg, 'text') | |
2295 | lines = [] | |
2296 | subparts = 0 | |
2297 | for subpart in it: | |
2298 | subparts += 1 | |
2299 | lines.append(subpart.get_payload()) | |
2300 | eq(subparts, 2) | |
2301 | eq(EMPTYSTRING.join(lines), """\ | |
2302 | a simple kind of mirror | |
2303 | to reflect upon our own | |
2304 | a simple kind of mirror | |
2305 | to reflect upon our own | |
2306 | """) | |
2307 | ||
2308 | def test_typed_subpart_iterator_default_type(self): | |
2309 | eq = self.assertEqual | |
2310 | msg = self._msgobj('msg_03.txt') | |
2311 | it = Iterators.typed_subpart_iterator(msg, 'text', 'plain') | |
2312 | lines = [] | |
2313 | subparts = 0 | |
2314 | for subpart in it: | |
2315 | subparts += 1 | |
2316 | lines.append(subpart.get_payload()) | |
2317 | eq(subparts, 1) | |
2318 | eq(EMPTYSTRING.join(lines), """\ | |
2319 | ||
2320 | Hi, | |
2321 | ||
2322 | Do you like this message? | |
2323 | ||
2324 | -Me | |
2325 | """) | |
2326 | ||
2327 | ||
2328 | \f | |
2329 | class TestParsers(TestEmailBase): | |
2330 | def test_header_parser(self): | |
2331 | eq = self.assertEqual | |
2332 | # Parse only the headers of a complex multipart MIME document | |
2333 | fp = openfile('msg_02.txt') | |
2334 | try: | |
2335 | msg = HeaderParser().parse(fp) | |
2336 | finally: | |
2337 | fp.close() | |
2338 | eq(msg['from'], 'ppp-request@zzz.org') | |
2339 | eq(msg['to'], 'ppp@zzz.org') | |
2340 | eq(msg.get_type(), 'multipart/mixed') | |
2341 | self.failIf(msg.is_multipart()) | |
2342 | self.failUnless(isinstance(msg.get_payload(), str)) | |
2343 | ||
2344 | def test_whitespace_continuation(self): | |
2345 | eq = self.assertEqual | |
2346 | # This message contains a line after the Subject: header that has only | |
2347 | # whitespace, but it is not empty! | |
2348 | msg = email.message_from_string("""\ | |
2349 | From: aperson@dom.ain | |
2350 | To: bperson@dom.ain | |
2351 | Subject: the next line has a space on it | |
2352 | \x20 | |
2353 | Date: Mon, 8 Apr 2002 15:09:19 -0400 | |
2354 | Message-ID: spam | |
2355 | ||
2356 | Here's the message body | |
2357 | """) | |
2358 | eq(msg['subject'], 'the next line has a space on it\n ') | |
2359 | eq(msg['message-id'], 'spam') | |
2360 | eq(msg.get_payload(), "Here's the message body\n") | |
2361 | ||
2362 | def test_whitespace_continuation_last_header(self): | |
2363 | eq = self.assertEqual | |
2364 | # Like the previous test, but the subject line is the last | |
2365 | # header. | |
2366 | msg = email.message_from_string("""\ | |
2367 | From: aperson@dom.ain | |
2368 | To: bperson@dom.ain | |
2369 | Date: Mon, 8 Apr 2002 15:09:19 -0400 | |
2370 | Message-ID: spam | |
2371 | Subject: the next line has a space on it | |
2372 | \x20 | |
2373 | ||
2374 | Here's the message body | |
2375 | """) | |
2376 | eq(msg['subject'], 'the next line has a space on it\n ') | |
2377 | eq(msg['message-id'], 'spam') | |
2378 | eq(msg.get_payload(), "Here's the message body\n") | |
2379 | ||
2380 | def test_crlf_separation(self): | |
2381 | eq = self.assertEqual | |
2382 | fp = openfile('msg_26.txt', mode='rb') | |
2383 | try: | |
2384 | msg = Parser().parse(fp) | |
2385 | finally: | |
2386 | fp.close() | |
2387 | eq(len(msg.get_payload()), 2) | |
2388 | part1 = msg.get_payload(0) | |
2389 | eq(part1.get_type(), 'text/plain') | |
2390 | eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n') | |
2391 | part2 = msg.get_payload(1) | |
2392 | eq(part2.get_type(), 'application/riscos') | |
2393 | ||
2394 | def test_multipart_digest_with_extra_mime_headers(self): | |
2395 | eq = self.assertEqual | |
2396 | neq = self.ndiffAssertEqual | |
2397 | fp = openfile('msg_28.txt') | |
2398 | try: | |
2399 | msg = email.message_from_file(fp) | |
2400 | finally: | |
2401 | fp.close() | |
2402 | # Structure is: | |
2403 | # multipart/digest | |
2404 | # message/rfc822 | |
2405 | # text/plain | |
2406 | # message/rfc822 | |
2407 | # text/plain | |
2408 | eq(msg.is_multipart(), 1) | |
2409 | eq(len(msg.get_payload()), 2) | |
2410 | part1 = msg.get_payload(0) | |
2411 | eq(part1.get_type(), 'message/rfc822') | |
2412 | eq(part1.is_multipart(), 1) | |
2413 | eq(len(part1.get_payload()), 1) | |
2414 | part1a = part1.get_payload(0) | |
2415 | eq(part1a.is_multipart(), 0) | |
2416 | eq(part1a.get_type(), 'text/plain') | |
2417 | neq(part1a.get_payload(), 'message 1\n') | |
2418 | # next message/rfc822 | |
2419 | part2 = msg.get_payload(1) | |
2420 | eq(part2.get_type(), 'message/rfc822') | |
2421 | eq(part2.is_multipart(), 1) | |
2422 | eq(len(part2.get_payload()), 1) | |
2423 | part2a = part2.get_payload(0) | |
2424 | eq(part2a.is_multipart(), 0) | |
2425 | eq(part2a.get_type(), 'text/plain') | |
2426 | neq(part2a.get_payload(), 'message 2\n') | |
2427 | ||
2428 | def test_three_lines(self): | |
2429 | # A bug report by Andrew McNamara | |
2430 | lines = ['From: Andrew Person <aperson@dom.ain', | |
2431 | 'Subject: Test', | |
2432 | 'Date: Tue, 20 Aug 2002 16:43:45 +1000'] | |
2433 | msg = email.message_from_string(NL.join(lines)) | |
2434 | self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000') | |
2435 | ||
2436 | def test_strip_line_feed_and_carriage_return_in_headers(self): | |
2437 | eq = self.assertEqual | |
2438 | # For [ 1002475 ] email message parser doesn't handle \r\n correctly | |
2439 | value1 = 'text' | |
2440 | value2 = 'more text' | |
2441 | m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % ( | |
2442 | value1, value2) | |
2443 | msg = email.message_from_string(m) | |
2444 | eq(msg.get('Header'), value1) | |
2445 | eq(msg.get('Next-Header'), value2) | |
2446 | ||
2447 | def test_rfc2822_header_syntax(self): | |
2448 | eq = self.assertEqual | |
2449 | m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody' | |
2450 | msg = email.message_from_string(m) | |
2451 | eq(len(msg.keys()), 3) | |
2452 | keys = msg.keys() | |
2453 | keys.sort() | |
2454 | eq(keys, ['!"#QUX;~', '>From', 'From']) | |
2455 | eq(msg.get_payload(), 'body') | |
2456 | ||
2457 | def test_rfc2822_space_not_allowed_in_header(self): | |
2458 | eq = self.assertEqual | |
2459 | m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody' | |
2460 | msg = email.message_from_string(m) | |
2461 | eq(len(msg.keys()), 0) | |
2462 | ||
2463 | ||
2464 | \f | |
2465 | class TestBase64(unittest.TestCase): | |
2466 | def test_len(self): | |
2467 | eq = self.assertEqual | |
2468 | eq(base64MIME.base64_len('hello'), | |
2469 | len(base64MIME.encode('hello', eol=''))) | |
2470 | for size in range(15): | |
2471 | if size == 0 : bsize = 0 | |
2472 | elif size <= 3 : bsize = 4 | |
2473 | elif size <= 6 : bsize = 8 | |
2474 | elif size <= 9 : bsize = 12 | |
2475 | elif size <= 12: bsize = 16 | |
2476 | else : bsize = 20 | |
2477 | eq(base64MIME.base64_len('x'*size), bsize) | |
2478 | ||
2479 | def test_decode(self): | |
2480 | eq = self.assertEqual | |
2481 | eq(base64MIME.decode(''), '') | |
2482 | eq(base64MIME.decode('aGVsbG8='), 'hello') | |
2483 | eq(base64MIME.decode('aGVsbG8=', 'X'), 'hello') | |
2484 | eq(base64MIME.decode('aGVsbG8NCndvcmxk\n', 'X'), 'helloXworld') | |
2485 | ||
2486 | def test_encode(self): | |
2487 | eq = self.assertEqual | |
2488 | eq(base64MIME.encode(''), '') | |
2489 | eq(base64MIME.encode('hello'), 'aGVsbG8=\n') | |
2490 | # Test the binary flag | |
2491 | eq(base64MIME.encode('hello\n'), 'aGVsbG8K\n') | |
2492 | eq(base64MIME.encode('hello\n', 0), 'aGVsbG8NCg==\n') | |
2493 | # Test the maxlinelen arg | |
2494 | eq(base64MIME.encode('xxxx ' * 20, maxlinelen=40), """\ | |
2495 | eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg | |
2496 | eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg | |
2497 | eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg | |
2498 | eHh4eCB4eHh4IA== | |
2499 | """) | |
2500 | # Test the eol argument | |
2501 | eq(base64MIME.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ | |
2502 | eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r | |
2503 | eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r | |
2504 | eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r | |
2505 | eHh4eCB4eHh4IA==\r | |
2506 | """) | |
2507 | ||
2508 | def test_header_encode(self): | |
2509 | eq = self.assertEqual | |
2510 | he = base64MIME.header_encode | |
2511 | eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=') | |
2512 | eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=') | |
2513 | # Test the charset option | |
2514 | eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=') | |
2515 | # Test the keep_eols flag | |
2516 | eq(he('hello\nworld', keep_eols=True), | |
2517 | '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=') | |
2518 | # Test the maxlinelen argument | |
2519 | eq(he('xxxx ' * 20, maxlinelen=40), """\ | |
2520 | =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?= | |
2521 | =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?= | |
2522 | =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?= | |
2523 | =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?= | |
2524 | =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?= | |
2525 | =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""") | |
2526 | # Test the eol argument | |
2527 | eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ | |
2528 | =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?=\r | |
2529 | =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?=\r | |
2530 | =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?=\r | |
2531 | =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?=\r | |
2532 | =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?=\r | |
2533 | =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""") | |
2534 | ||
2535 | ||
2536 | \f | |
2537 | class TestQuopri(unittest.TestCase): | |
2538 | def setUp(self): | |
2539 | self.hlit = [chr(x) for x in range(ord('a'), ord('z')+1)] + \ | |
2540 | [chr(x) for x in range(ord('A'), ord('Z')+1)] + \ | |
2541 | [chr(x) for x in range(ord('0'), ord('9')+1)] + \ | |
2542 | ['!', '*', '+', '-', '/', ' '] | |
2543 | self.hnon = [chr(x) for x in range(256) if chr(x) not in self.hlit] | |
2544 | assert len(self.hlit) + len(self.hnon) == 256 | |
2545 | self.blit = [chr(x) for x in range(ord(' '), ord('~')+1)] + ['\t'] | |
2546 | self.blit.remove('=') | |
2547 | self.bnon = [chr(x) for x in range(256) if chr(x) not in self.blit] | |
2548 | assert len(self.blit) + len(self.bnon) == 256 | |
2549 | ||
2550 | def test_header_quopri_check(self): | |
2551 | for c in self.hlit: | |
2552 | self.failIf(quopriMIME.header_quopri_check(c)) | |
2553 | for c in self.hnon: | |
2554 | self.failUnless(quopriMIME.header_quopri_check(c)) | |
2555 | ||
2556 | def test_body_quopri_check(self): | |
2557 | for c in self.blit: | |
2558 | self.failIf(quopriMIME.body_quopri_check(c)) | |
2559 | for c in self.bnon: | |
2560 | self.failUnless(quopriMIME.body_quopri_check(c)) | |
2561 | ||
2562 | def test_header_quopri_len(self): | |
2563 | eq = self.assertEqual | |
2564 | hql = quopriMIME.header_quopri_len | |
2565 | enc = quopriMIME.header_encode | |
2566 | for s in ('hello', 'h@e@l@l@o@'): | |
2567 | # Empty charset and no line-endings. 7 == RFC chrome | |
2568 | eq(hql(s), len(enc(s, charset='', eol=''))-7) | |
2569 | for c in self.hlit: | |
2570 | eq(hql(c), 1) | |
2571 | for c in self.hnon: | |
2572 | eq(hql(c), 3) | |
2573 | ||
2574 | def test_body_quopri_len(self): | |
2575 | eq = self.assertEqual | |
2576 | bql = quopriMIME.body_quopri_len | |
2577 | for c in self.blit: | |
2578 | eq(bql(c), 1) | |
2579 | for c in self.bnon: | |
2580 | eq(bql(c), 3) | |
2581 | ||
2582 | def test_quote_unquote_idempotent(self): | |
2583 | for x in range(256): | |
2584 | c = chr(x) | |
2585 | self.assertEqual(quopriMIME.unquote(quopriMIME.quote(c)), c) | |
2586 | ||
2587 | def test_header_encode(self): | |
2588 | eq = self.assertEqual | |
2589 | he = quopriMIME.header_encode | |
2590 | eq(he('hello'), '=?iso-8859-1?q?hello?=') | |
2591 | eq(he('hello\nworld'), '=?iso-8859-1?q?hello=0D=0Aworld?=') | |
2592 | # Test the charset option | |
2593 | eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=') | |
2594 | # Test the keep_eols flag | |
2595 | eq(he('hello\nworld', keep_eols=True), '=?iso-8859-1?q?hello=0Aworld?=') | |
2596 | # Test a non-ASCII character | |
2597 | eq(he('hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=') | |
2598 | # Test the maxlinelen argument | |
2599 | eq(he('xxxx ' * 20, maxlinelen=40), """\ | |
2600 | =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?= | |
2601 | =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?= | |
2602 | =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?= | |
2603 | =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?= | |
2604 | =?iso-8859-1?q?x_xxxx_xxxx_?=""") | |
2605 | # Test the eol argument | |
2606 | eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ | |
2607 | =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=\r | |
2608 | =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=\r | |
2609 | =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?=\r | |
2610 | =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?=\r | |
2611 | =?iso-8859-1?q?x_xxxx_xxxx_?=""") | |
2612 | ||
2613 | def test_decode(self): | |
2614 | eq = self.assertEqual | |
2615 | eq(quopriMIME.decode(''), '') | |
2616 | eq(quopriMIME.decode('hello'), 'hello') | |
2617 | eq(quopriMIME.decode('hello', 'X'), 'hello') | |
2618 | eq(quopriMIME.decode('hello\nworld', 'X'), 'helloXworld') | |
2619 | ||
2620 | def test_encode(self): | |
2621 | eq = self.assertEqual | |
2622 | eq(quopriMIME.encode(''), '') | |
2623 | eq(quopriMIME.encode('hello'), 'hello') | |
2624 | # Test the binary flag | |
2625 | eq(quopriMIME.encode('hello\r\nworld'), 'hello\nworld') | |
2626 | eq(quopriMIME.encode('hello\r\nworld', 0), 'hello\nworld') | |
2627 | # Test the maxlinelen arg | |
2628 | eq(quopriMIME.encode('xxxx ' * 20, maxlinelen=40), """\ | |
2629 | xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx= | |
2630 | xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx= | |
2631 | x xxxx xxxx xxxx xxxx=20""") | |
2632 | # Test the eol argument | |
2633 | eq(quopriMIME.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ | |
2634 | xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r | |
2635 | xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r | |
2636 | x xxxx xxxx xxxx xxxx=20""") | |
2637 | eq(quopriMIME.encode("""\ | |
2638 | one line | |
2639 | ||
2640 | two line"""), """\ | |
2641 | one line | |
2642 | ||
2643 | two line""") | |
2644 | ||
2645 | ||
2646 | \f | |
2647 | # Test the Charset class | |
2648 | class TestCharset(unittest.TestCase): | |
2649 | def tearDown(self): | |
2650 | from email import Charset as CharsetModule | |
2651 | try: | |
2652 | del CharsetModule.CHARSETS['fake'] | |
2653 | except KeyError: | |
2654 | pass | |
2655 | ||
2656 | def test_idempotent(self): | |
2657 | eq = self.assertEqual | |
2658 | # Make sure us-ascii = no Unicode conversion | |
2659 | c = Charset('us-ascii') | |
2660 | s = 'Hello World!' | |
2661 | sp = c.to_splittable(s) | |
2662 | eq(s, c.from_splittable(sp)) | |
2663 | # test 8-bit idempotency with us-ascii | |
2664 | s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa' | |
2665 | sp = c.to_splittable(s) | |
2666 | eq(s, c.from_splittable(sp)) | |
2667 | ||
2668 | def test_body_encode(self): | |
2669 | eq = self.assertEqual | |
2670 | # Try a charset with QP body encoding | |
2671 | c = Charset('iso-8859-1') | |
2672 | eq('hello w=F6rld', c.body_encode('hello w\xf6rld')) | |
2673 | # Try a charset with Base64 body encoding | |
2674 | c = Charset('utf-8') | |
2675 | eq('aGVsbG8gd29ybGQ=\n', c.body_encode('hello world')) | |
2676 | # Try a charset with None body encoding | |
2677 | c = Charset('us-ascii') | |
2678 | eq('hello world', c.body_encode('hello world')) | |
2679 | # Try the convert argument, where input codec <> output codec | |
2680 | c = Charset('euc-jp') | |
2681 | # With apologies to Tokio Kikuchi ;) | |
2682 | try: | |
2683 | eq('\x1b$B5FCO;~IW\x1b(B', | |
2684 | c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7')) | |
2685 | eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', | |
2686 | c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False)) | |
2687 | except LookupError: | |
2688 | # We probably don't have the Japanese codecs installed | |
2689 | pass | |
2690 | # Testing SF bug #625509, which we have to fake, since there are no | |
2691 | # built-in encodings where the header encoding is QP but the body | |
2692 | # encoding is not. | |
2693 | from email import Charset as CharsetModule | |
2694 | CharsetModule.add_charset('fake', CharsetModule.QP, None) | |
2695 | c = Charset('fake') | |
2696 | eq('hello w\xf6rld', c.body_encode('hello w\xf6rld')) | |
2697 | ||
2698 | ||
2699 | \f | |
2700 | # Test multilingual MIME headers. | |
2701 | class TestHeader(TestEmailBase): | |
2702 | def test_simple(self): | |
2703 | eq = self.ndiffAssertEqual | |
2704 | h = Header('Hello World!') | |
2705 | eq(h.encode(), 'Hello World!') | |
2706 | h.append(' Goodbye World!') | |
2707 | eq(h.encode(), 'Hello World! Goodbye World!') | |
2708 | ||
2709 | def test_simple_surprise(self): | |
2710 | eq = self.ndiffAssertEqual | |
2711 | h = Header('Hello World!') | |
2712 | eq(h.encode(), 'Hello World!') | |
2713 | h.append('Goodbye World!') | |
2714 | eq(h.encode(), 'Hello World! Goodbye World!') | |
2715 | ||
2716 | def test_header_needs_no_decoding(self): | |
2717 | h = 'no decoding needed' | |
2718 | self.assertEqual(decode_header(h), [(h, None)]) | |
2719 | ||
2720 | def test_long(self): | |
2721 | h = Header("I am the very model of a modern Major-General; I've information vegetable, animal, and mineral; I know the kings of England, and I quote the fights historical from Marathon to Waterloo, in order categorical; I'm very well acquainted, too, with matters mathematical; I understand equations, both the simple and quadratical; about binomial theorem I'm teeming with a lot o' news, with many cheerful facts about the square of the hypotenuse.", | |
2722 | maxlinelen=76) | |
2723 | for l in h.encode(splitchars=' ').split('\n '): | |
2724 | self.failUnless(len(l) <= 76) | |
2725 | ||
2726 | def test_multilingual(self): | |
2727 | eq = self.ndiffAssertEqual | |
2728 | g = Charset("iso-8859-1") | |
2729 | cz = Charset("iso-8859-2") | |
2730 | utf8 = Charset("utf-8") | |
2731 | g_head = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " | |
2732 | cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. " | |
2733 | utf8_head = u"\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066\u3044\u307e\u3059\u3002".encode("utf-8") | |
2734 | h = Header(g_head, g) | |
2735 | h.append(cz_head, cz) | |
2736 | h.append(utf8_head, utf8) | |
2737 | enc = h.encode() | |
2738 | eq(enc, """\ | |
2739 | =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_ko?= | |
2740 | =?iso-8859-1?q?mfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wan?= | |
2741 | =?iso-8859-1?q?dgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6?= | |
2742 | =?iso-8859-1?q?rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?= | |
2743 | =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?= | |
2744 | =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?= | |
2745 | =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?= | |
2746 | =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?= | |
2747 | =?utf-8?q?_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das_Oder_die_Fl?= | |
2748 | =?utf-8?b?aXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBo+OBpuOBhOOBvuOBmQ==?= | |
2749 | =?utf-8?b?44CC?=""") | |
2750 | eq(decode_header(enc), | |
2751 | [(g_head, "iso-8859-1"), (cz_head, "iso-8859-2"), | |
2752 | (utf8_head, "utf-8")]) | |
2753 | ustr = unicode(h) | |
2754 | eq(ustr.encode('utf-8'), | |
2755 | 'Die Mieter treten hier ein werden mit einem Foerderband ' | |
2756 | 'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen ' | |
2757 | 'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen ' | |
2758 | 'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod ' | |
2759 | 'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81' | |
2760 | '\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3' | |
2761 | '\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3' | |
2762 | '\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83' | |
2763 | '\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e' | |
2764 | '\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3' | |
2765 | '\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82' | |
2766 | '\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b' | |
2767 | '\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git ' | |
2768 | 'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt ' | |
2769 | 'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81' | |
2770 | '\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82') | |
2771 | # Test make_header() | |
2772 | newh = make_header(decode_header(enc)) | |
2773 | eq(newh, enc) | |
2774 | ||
2775 | def test_header_ctor_default_args(self): | |
2776 | eq = self.ndiffAssertEqual | |
2777 | h = Header() | |
2778 | eq(h, '') | |
2779 | h.append('foo', Charset('iso-8859-1')) | |
2780 | eq(h, '=?iso-8859-1?q?foo?=') | |
2781 | ||
2782 | def test_explicit_maxlinelen(self): | |
2783 | eq = self.ndiffAssertEqual | |
2784 | hstr = 'A very long line that must get split to something other than at the 76th character boundary to test the non-default behavior' | |
2785 | h = Header(hstr) | |
2786 | eq(h.encode(), '''\ | |
2787 | A very long line that must get split to something other than at the 76th | |
2788 | character boundary to test the non-default behavior''') | |
2789 | h = Header(hstr, header_name='Subject') | |
2790 | eq(h.encode(), '''\ | |
2791 | A very long line that must get split to something other than at the | |
2792 | 76th character boundary to test the non-default behavior''') | |
2793 | h = Header(hstr, maxlinelen=1024, header_name='Subject') | |
2794 | eq(h.encode(), hstr) | |
2795 | ||
2796 | def test_us_ascii_header(self): | |
2797 | eq = self.assertEqual | |
2798 | s = 'hello' | |
2799 | x = decode_header(s) | |
2800 | eq(x, [('hello', None)]) | |
2801 | h = make_header(x) | |
2802 | eq(s, h.encode()) | |
2803 | ||
2804 | def test_string_charset(self): | |
2805 | eq = self.assertEqual | |
2806 | h = Header() | |
2807 | h.append('hello', 'iso-8859-1') | |
2808 | eq(h, '=?iso-8859-1?q?hello?=') | |
2809 | ||
2810 | ## def test_unicode_error(self): | |
2811 | ## raises = self.assertRaises | |
2812 | ## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii') | |
2813 | ## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii') | |
2814 | ## h = Header() | |
2815 | ## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii') | |
2816 | ## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii') | |
2817 | ## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1') | |
2818 | ||
2819 | def test_utf8_shortest(self): | |
2820 | eq = self.assertEqual | |
2821 | h = Header(u'p\xf6stal', 'utf-8') | |
2822 | eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=') | |
2823 | h = Header(u'\u83ca\u5730\u6642\u592b', 'utf-8') | |
2824 | eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=') | |
2825 | ||
2826 | def test_bad_8bit_header(self): | |
2827 | raises = self.assertRaises | |
2828 | eq = self.assertEqual | |
2829 | x = 'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big' | |
2830 | raises(UnicodeError, Header, x) | |
2831 | h = Header() | |
2832 | raises(UnicodeError, h.append, x) | |
2833 | eq(str(Header(x, errors='replace')), x) | |
2834 | h.append(x, errors='replace') | |
2835 | eq(str(h), x) | |
2836 | ||
2837 | def test_encoded_adjacent_nonencoded(self): | |
2838 | eq = self.assertEqual | |
2839 | h = Header() | |
2840 | h.append('hello', 'iso-8859-1') | |
2841 | h.append('world') | |
2842 | s = h.encode() | |
2843 | eq(s, '=?iso-8859-1?q?hello?= world') | |
2844 | h = make_header(decode_header(s)) | |
2845 | eq(h.encode(), s) | |
2846 | ||
2847 | def test_whitespace_eater(self): | |
2848 | eq = self.assertEqual | |
2849 | s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.' | |
2850 | parts = decode_header(s) | |
2851 | eq(parts, [('Subject:', None), ('\xf0\xd2\xcf\xd7\xc5\xd2\xcb\xc1 \xce\xc1 \xc6\xc9\xce\xc1\xcc\xd8\xce\xd9\xca', 'koi8-r'), ('zz.', None)]) | |
2852 | hdr = make_header(parts) | |
2853 | eq(hdr.encode(), | |
2854 | 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.') | |
2855 | ||
2856 | def test_broken_base64_header(self): | |
2857 | raises = self.assertRaises | |
2858 | s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3IQ?=' | |
2859 | raises(Errors.HeaderParseError, decode_header, s) | |
2860 | ||
2861 | ||
2862 | \f | |
2863 | # Test RFC 2231 header parameters (en/de)coding | |
2864 | class TestRFC2231(TestEmailBase): | |
2865 | def test_get_param(self): | |
2866 | eq = self.assertEqual | |
2867 | msg = self._msgobj('msg_29.txt') | |
2868 | eq(msg.get_param('title'), | |
2869 | ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!')) | |
2870 | eq(msg.get_param('title', unquote=False), | |
2871 | ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"')) | |
2872 | ||
2873 | def test_set_param(self): | |
2874 | eq = self.assertEqual | |
2875 | msg = Message() | |
2876 | msg.set_param('title', 'This is even more ***fun*** isn\'t it!', | |
2877 | charset='us-ascii') | |
2878 | eq(msg.get_param('title'), | |
2879 | ('us-ascii', '', 'This is even more ***fun*** isn\'t it!')) | |
2880 | msg.set_param('title', 'This is even more ***fun*** isn\'t it!', | |
2881 | charset='us-ascii', language='en') | |
2882 | eq(msg.get_param('title'), | |
2883 | ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!')) | |
2884 | msg = self._msgobj('msg_01.txt') | |
2885 | msg.set_param('title', 'This is even more ***fun*** isn\'t it!', | |
2886 | charset='us-ascii', language='en') | |
2887 | eq(msg.as_string(), """\ | |
2888 | Return-Path: <bbb@zzz.org> | |
2889 | Delivered-To: bbb@zzz.org | |
2890 | Received: by mail.zzz.org (Postfix, from userid 889) | |
2891 | \tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) | |
2892 | MIME-Version: 1.0 | |
2893 | Content-Transfer-Encoding: 7bit | |
2894 | Message-ID: <15090.61304.110929.45684@aaa.zzz.org> | |
2895 | From: bbb@ddd.com (John X. Doe) | |
2896 | To: bbb@zzz.org | |
2897 | Subject: This is a test message | |
2898 | Date: Fri, 4 May 2001 14:05:44 -0400 | |
2899 | Content-Type: text/plain; charset=us-ascii; | |
2900 | \ttitle*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21" | |
2901 | ||
2902 | ||
2903 | Hi, | |
2904 | ||
2905 | Do you like this message? | |
2906 | ||
2907 | -Me | |
2908 | """) | |
2909 | ||
2910 | def test_del_param(self): | |
2911 | eq = self.ndiffAssertEqual | |
2912 | msg = self._msgobj('msg_01.txt') | |
2913 | msg.set_param('foo', 'bar', charset='us-ascii', language='en') | |
2914 | msg.set_param('title', 'This is even more ***fun*** isn\'t it!', | |
2915 | charset='us-ascii', language='en') | |
2916 | msg.del_param('foo', header='Content-Type') | |
2917 | eq(msg.as_string(), """\ | |
2918 | Return-Path: <bbb@zzz.org> | |
2919 | Delivered-To: bbb@zzz.org | |
2920 | Received: by mail.zzz.org (Postfix, from userid 889) | |
2921 | \tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) | |
2922 | MIME-Version: 1.0 | |
2923 | Content-Transfer-Encoding: 7bit | |
2924 | Message-ID: <15090.61304.110929.45684@aaa.zzz.org> | |
2925 | From: bbb@ddd.com (John X. Doe) | |
2926 | To: bbb@zzz.org | |
2927 | Subject: This is a test message | |
2928 | Date: Fri, 4 May 2001 14:05:44 -0400 | |
2929 | Content-Type: text/plain; charset="us-ascii"; | |
2930 | \ttitle*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21" | |
2931 | ||
2932 | ||
2933 | Hi, | |
2934 | ||
2935 | Do you like this message? | |
2936 | ||
2937 | -Me | |
2938 | """) | |
2939 | ||
2940 | def test_rfc2231_get_content_charset(self): | |
2941 | eq = self.assertEqual | |
2942 | msg = self._msgobj('msg_32.txt') | |
2943 | eq(msg.get_content_charset(), 'us-ascii') | |
2944 | ||
2945 | def test_rfc2231_no_language_or_charset(self): | |
2946 | m = '''\ | |
2947 | Content-Transfer-Encoding: 8bit | |
2948 | Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm" | |
2949 | Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm | |
2950 | ||
2951 | ''' | |
2952 | msg = email.message_from_string(m) | |
2953 | self.assertEqual(msg.get_param('NAME'), | |
2954 | (None, None, 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')) | |
2955 | ||
2956 | def test_rfc2231_no_language_or_charset_in_filename(self): | |
2957 | m = '''\ | |
2958 | Content-Disposition: inline; | |
2959 | \tfilename*0="This%20is%20even%20more%20"; | |
2960 | \tfilename*1="%2A%2A%2Afun%2A%2A%2A%20"; | |
2961 | \tfilename*2="is it not.pdf" | |
2962 | ||
2963 | ''' | |
2964 | msg = email.message_from_string(m) | |
2965 | self.assertEqual(msg.get_filename(), | |
2966 | 'This is even more ***fun*** is it not.pdf') | |
2967 | ||
2968 | def test_rfc2231_no_language_or_charset_in_boundary(self): | |
2969 | m = '''\ | |
2970 | Content-Type: multipart/alternative; | |
2971 | \tboundary*0="This%20is%20even%20more%20"; | |
2972 | \tboundary*1="%2A%2A%2Afun%2A%2A%2A%20"; | |
2973 | \tboundary*2="is it not.pdf" | |
2974 | ||
2975 | ''' | |
2976 | msg = email.message_from_string(m) | |
2977 | self.assertEqual(msg.get_boundary(), | |
2978 | 'This is even more ***fun*** is it not.pdf') | |
2979 | ||
2980 | def test_rfc2231_no_language_or_charset_in_charset(self): | |
2981 | # This is a nonsensical charset value, but tests the code anyway | |
2982 | m = '''\ | |
2983 | Content-Type: text/plain; | |
2984 | \tcharset*0="This%20is%20even%20more%20"; | |
2985 | \tcharset*1="%2A%2A%2Afun%2A%2A%2A%20"; | |
2986 | \tcharset*2="is it not.pdf" | |
2987 | ||
2988 | ''' | |
2989 | msg = email.message_from_string(m) | |
2990 | self.assertEqual(msg.get_content_charset(), | |
2991 | 'this is even more ***fun*** is it not.pdf') | |
2992 | ||
2993 | def test_rfc2231_unknown_encoding(self): | |
2994 | m = """\ | |
2995 | Content-Transfer-Encoding: 8bit | |
2996 | Content-Disposition: inline; filename*0=X-UNKNOWN''myfile.txt | |
2997 | ||
2998 | """ | |
2999 | msg = email.message_from_string(m) | |
3000 | self.assertEqual(msg.get_filename(), 'myfile.txt') | |
3001 | ||
3002 | ||
3003 | \f | |
3004 | def _testclasses(): | |
3005 | mod = sys.modules[__name__] | |
3006 | return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')] | |
3007 | ||
3008 | ||
3009 | def suite(): | |
3010 | suite = unittest.TestSuite() | |
3011 | for testclass in _testclasses(): | |
3012 | suite.addTest(unittest.makeSuite(testclass)) | |
3013 | return suite | |
3014 | ||
3015 | ||
3016 | def test_main(): | |
3017 | for testclass in _testclasses(): | |
3018 | run_unittest(testclass) | |
3019 | ||
3020 | ||
3021 | \f | |
3022 | if __name__ == '__main__': | |
3023 | unittest.main(defaultTest='suite') |