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