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