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