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