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