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