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