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.assertIn('from', msg)
    235         self.assertIn('From', msg)
    236         self.assertIn('FROM', msg)
    237         self.assertIn('to', msg)
    238         self.assertIn('To', msg)
    239         self.assertIn('TO', 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         self._au.add_header('Content-Disposition', 'attachment',
    920                             filename='audiotest.au')
    921         eq(self._au['content-disposition'],
    922            'attachment; filename="audiotest.au"')
    923         eq(self._au.get_params(header='content-disposition'),
    924            [('attachment', ''), ('filename', 'audiotest.au')])
    925         eq(self._au.get_param('filename', header='content-disposition'),
    926            'audiotest.au')
    927         missing = []
    928         eq(self._au.get_param('attachment', header='content-disposition'), '')
    929         self.assertIs(self._au.get_param('foo', failobj=missing,
    930                                          header='content-disposition'),
    931                       missing)
    932         # Try some missing stuff
    933         self.assertIs(self._au.get_param('foobar', missing), missing)
    934         self.assertIs(self._au.get_param('attachment', missing,
    935                                          header='foobar'), 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         self._im.add_header('Content-Disposition', 'attachment',
    963                             filename='dingusfish.gif')
    964         eq(self._im['content-disposition'],
    965            'attachment; filename="dingusfish.gif"')
    966         eq(self._im.get_params(header='content-disposition'),
    967            [('attachment', ''), ('filename', 'dingusfish.gif')])
    968         eq(self._im.get_param('filename', header='content-disposition'),
    969            'dingusfish.gif')
    970         missing = []
    971         eq(self._im.get_param('attachment', header='content-disposition'), '')
    972         self.assertIs(self._im.get_param('foo', failobj=missing,
    973                                          header='content-disposition'),
    974                       missing)
    975         # Try some missing stuff
    976         self.assertIs(self._im.get_param('foobar', missing), missing)
    977         self.assertIs(self._im.get_param('attachment', missing,
    978                                          header='foobar'), 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         eq(self._msg.get_content_type(), 'text/plain')
   1039         eq(self._msg.get_param('charset'), 'us-ascii')
   1040         missing = []
   1041         self.assertIs(self._msg.get_param('foobar', missing), missing)
   1042         self.assertIs(self._msg.get_param('charset', missing, header='foobar'),
   1043                       missing)
   1044 
   1045     def test_payload(self):
   1046         self.assertEqual(self._msg.get_payload(), 'hello there')
   1047         self.assertFalse(self._msg.is_multipart())
   1048 
   1049     def test_charset(self):
   1050         eq = self.assertEqual
   1051         msg = MIMEText('hello there', _charset='us-ascii')
   1052         eq(msg.get_charset().input_charset, 'us-ascii')
   1053         eq(msg['content-type'], 'text/plain; charset="us-ascii"')
   1054 
   1055 
   1056 
   1057 # Test complicated multipart/* messages
   1058 class TestMultipart(TestEmailBase):
   1059     def setUp(self):
   1060         fp = openfile('PyBanner048.gif')
   1061         try:
   1062             data = fp.read()
   1063         finally:
   1064             fp.close()
   1065 
   1066         container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
   1067         image = MIMEImage(data, name='dingusfish.gif')
   1068         image.add_header('content-disposition', 'attachment',
   1069                          filename='dingusfish.gif')
   1070         intro = MIMEText('''\
   1071 Hi there,
   1072 
   1073 This is the dingus fish.
   1074 ''')
   1075         container.attach(intro)
   1076         container.attach(image)
   1077         container['From'] = 'Barry <barry (at] digicool.com>'
   1078         container['To'] = 'Dingus Lovers <cravindogs (at] cravindogs.com>'
   1079         container['Subject'] = 'Here is your dingus fish'
   1080 
   1081         now = 987809702.54848599
   1082         timetuple = time.localtime(now)
   1083         if timetuple[-1] == 0:
   1084             tzsecs = time.timezone
   1085         else:
   1086             tzsecs = time.altzone
   1087         if tzsecs > 0:
   1088             sign = '-'
   1089         else:
   1090             sign = '+'
   1091         tzoffset = ' %s%04d' % (sign, tzsecs // 36)
   1092         container['Date'] = time.strftime(
   1093             '%a, %d %b %Y %H:%M:%S',
   1094             time.localtime(now)) + tzoffset
   1095         self._msg = container
   1096         self._im = image
   1097         self._txt = intro
   1098 
   1099     def test_hierarchy(self):
   1100         # convenience
   1101         eq = self.assertEqual
   1102         raises = self.assertRaises
   1103         # tests
   1104         m = self._msg
   1105         self.assertTrue(m.is_multipart())
   1106         eq(m.get_content_type(), 'multipart/mixed')
   1107         eq(len(m.get_payload()), 2)
   1108         raises(IndexError, m.get_payload, 2)
   1109         m0 = m.get_payload(0)
   1110         m1 = m.get_payload(1)
   1111         self.assertIs(m0, self._txt)
   1112         self.assertIs(m1, self._im)
   1113         eq(m.get_payload(), [m0, m1])
   1114         self.assertFalse(m0.is_multipart())
   1115         self.assertFalse(m1.is_multipart())
   1116 
   1117     def test_empty_multipart_idempotent(self):
   1118         text = """\
   1119 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1120 MIME-Version: 1.0
   1121 Subject: A subject
   1122 To: aperson (at] dom.ain
   1123 From: bperson (at] dom.ain
   1124 
   1125 
   1126 --BOUNDARY
   1127 
   1128 
   1129 --BOUNDARY--
   1130 """
   1131         msg = Parser().parsestr(text)
   1132         self.ndiffAssertEqual(text, msg.as_string())
   1133 
   1134     def test_no_parts_in_a_multipart_with_none_epilogue(self):
   1135         outer = MIMEBase('multipart', 'mixed')
   1136         outer['Subject'] = 'A subject'
   1137         outer['To'] = 'aperson (at] dom.ain'
   1138         outer['From'] = 'bperson (at] dom.ain'
   1139         outer.set_boundary('BOUNDARY')
   1140         self.ndiffAssertEqual(outer.as_string(), '''\
   1141 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1142 MIME-Version: 1.0
   1143 Subject: A subject
   1144 To: aperson (at] dom.ain
   1145 From: bperson (at] dom.ain
   1146 
   1147 --BOUNDARY
   1148 
   1149 --BOUNDARY--
   1150 ''')
   1151 
   1152     def test_no_parts_in_a_multipart_with_empty_epilogue(self):
   1153         outer = MIMEBase('multipart', 'mixed')
   1154         outer['Subject'] = 'A subject'
   1155         outer['To'] = 'aperson (at] dom.ain'
   1156         outer['From'] = 'bperson (at] dom.ain'
   1157         outer.preamble = ''
   1158         outer.epilogue = ''
   1159         outer.set_boundary('BOUNDARY')
   1160         self.ndiffAssertEqual(outer.as_string(), '''\
   1161 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1162 MIME-Version: 1.0
   1163 Subject: A subject
   1164 To: aperson (at] dom.ain
   1165 From: bperson (at] dom.ain
   1166 
   1167 
   1168 --BOUNDARY
   1169 
   1170 --BOUNDARY--
   1171 ''')
   1172 
   1173     def test_one_part_in_a_multipart(self):
   1174         eq = self.ndiffAssertEqual
   1175         outer = MIMEBase('multipart', 'mixed')
   1176         outer['Subject'] = 'A subject'
   1177         outer['To'] = 'aperson (at] dom.ain'
   1178         outer['From'] = 'bperson (at] dom.ain'
   1179         outer.set_boundary('BOUNDARY')
   1180         msg = MIMEText('hello world')
   1181         outer.attach(msg)
   1182         eq(outer.as_string(), '''\
   1183 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1184 MIME-Version: 1.0
   1185 Subject: A subject
   1186 To: aperson (at] dom.ain
   1187 From: bperson (at] dom.ain
   1188 
   1189 --BOUNDARY
   1190 Content-Type: text/plain; charset="us-ascii"
   1191 MIME-Version: 1.0
   1192 Content-Transfer-Encoding: 7bit
   1193 
   1194 hello world
   1195 --BOUNDARY--
   1196 ''')
   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 
   1226     def test_seq_parts_in_a_multipart_with_none_preamble(self):
   1227         eq = self.ndiffAssertEqual
   1228         outer = MIMEBase('multipart', 'mixed')
   1229         outer['Subject'] = 'A subject'
   1230         outer['To'] = 'aperson (at] dom.ain'
   1231         outer['From'] = 'bperson (at] dom.ain'
   1232         outer.preamble = None
   1233         msg = MIMEText('hello world')
   1234         outer.attach(msg)
   1235         outer.set_boundary('BOUNDARY')
   1236         eq(outer.as_string(), '''\
   1237 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1238 MIME-Version: 1.0
   1239 Subject: A subject
   1240 To: aperson (at] dom.ain
   1241 From: bperson (at] dom.ain
   1242 
   1243 --BOUNDARY
   1244 Content-Type: text/plain; charset="us-ascii"
   1245 MIME-Version: 1.0
   1246 Content-Transfer-Encoding: 7bit
   1247 
   1248 hello world
   1249 --BOUNDARY--
   1250 ''')
   1251 
   1252 
   1253     def test_seq_parts_in_a_multipart_with_none_epilogue(self):
   1254         eq = self.ndiffAssertEqual
   1255         outer = MIMEBase('multipart', 'mixed')
   1256         outer['Subject'] = 'A subject'
   1257         outer['To'] = 'aperson (at] dom.ain'
   1258         outer['From'] = 'bperson (at] dom.ain'
   1259         outer.epilogue = None
   1260         msg = MIMEText('hello world')
   1261         outer.attach(msg)
   1262         outer.set_boundary('BOUNDARY')
   1263         eq(outer.as_string(), '''\
   1264 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1265 MIME-Version: 1.0
   1266 Subject: A subject
   1267 To: aperson (at] dom.ain
   1268 From: bperson (at] dom.ain
   1269 
   1270 --BOUNDARY
   1271 Content-Type: text/plain; charset="us-ascii"
   1272 MIME-Version: 1.0
   1273 Content-Transfer-Encoding: 7bit
   1274 
   1275 hello world
   1276 --BOUNDARY--
   1277 ''')
   1278 
   1279 
   1280     def test_seq_parts_in_a_multipart_with_empty_epilogue(self):
   1281         eq = self.ndiffAssertEqual
   1282         outer = MIMEBase('multipart', 'mixed')
   1283         outer['Subject'] = 'A subject'
   1284         outer['To'] = 'aperson (at] dom.ain'
   1285         outer['From'] = 'bperson (at] dom.ain'
   1286         outer.epilogue = ''
   1287         msg = MIMEText('hello world')
   1288         outer.attach(msg)
   1289         outer.set_boundary('BOUNDARY')
   1290         eq(outer.as_string(), '''\
   1291 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1292 MIME-Version: 1.0
   1293 Subject: A subject
   1294 To: aperson (at] dom.ain
   1295 From: bperson (at] dom.ain
   1296 
   1297 --BOUNDARY
   1298 Content-Type: text/plain; charset="us-ascii"
   1299 MIME-Version: 1.0
   1300 Content-Transfer-Encoding: 7bit
   1301 
   1302 hello world
   1303 --BOUNDARY--
   1304 ''')
   1305 
   1306 
   1307     def test_seq_parts_in_a_multipart_with_nl_epilogue(self):
   1308         eq = self.ndiffAssertEqual
   1309         outer = MIMEBase('multipart', 'mixed')
   1310         outer['Subject'] = 'A subject'
   1311         outer['To'] = 'aperson (at] dom.ain'
   1312         outer['From'] = 'bperson (at] dom.ain'
   1313         outer.epilogue = '\n'
   1314         msg = MIMEText('hello world')
   1315         outer.attach(msg)
   1316         outer.set_boundary('BOUNDARY')
   1317         eq(outer.as_string(), '''\
   1318 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1319 MIME-Version: 1.0
   1320 Subject: A subject
   1321 To: aperson (at] dom.ain
   1322 From: bperson (at] dom.ain
   1323 
   1324 --BOUNDARY
   1325 Content-Type: text/plain; charset="us-ascii"
   1326 MIME-Version: 1.0
   1327 Content-Transfer-Encoding: 7bit
   1328 
   1329 hello world
   1330 --BOUNDARY--
   1331 
   1332 ''')
   1333 
   1334     def test_message_external_body(self):
   1335         eq = self.assertEqual
   1336         msg = self._msgobj('msg_36.txt')
   1337         eq(len(msg.get_payload()), 2)
   1338         msg1 = msg.get_payload(1)
   1339         eq(msg1.get_content_type(), 'multipart/alternative')
   1340         eq(len(msg1.get_payload()), 2)
   1341         for subpart in msg1.get_payload():
   1342             eq(subpart.get_content_type(), 'message/external-body')
   1343             eq(len(subpart.get_payload()), 1)
   1344             subsubpart = subpart.get_payload(0)
   1345             eq(subsubpart.get_content_type(), 'text/plain')
   1346 
   1347     def test_double_boundary(self):
   1348         # msg_37.txt is a multipart that contains two dash-boundary's in a
   1349         # row.  Our interpretation of RFC 2046 calls for ignoring the second
   1350         # and subsequent boundaries.
   1351         msg = self._msgobj('msg_37.txt')
   1352         self.assertEqual(len(msg.get_payload()), 3)
   1353 
   1354     def test_nested_inner_contains_outer_boundary(self):
   1355         eq = self.ndiffAssertEqual
   1356         # msg_38.txt has an inner part that contains outer boundaries.  My
   1357         # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say
   1358         # these are illegal and should be interpreted as unterminated inner
   1359         # parts.
   1360         msg = self._msgobj('msg_38.txt')
   1361         sfp = StringIO()
   1362         iterators._structure(msg, sfp)
   1363         eq(sfp.getvalue(), """\
   1364 multipart/mixed
   1365     multipart/mixed
   1366         multipart/alternative
   1367             text/plain
   1368         text/plain
   1369     text/plain
   1370     text/plain
   1371 """)
   1372 
   1373     def test_nested_with_same_boundary(self):
   1374         eq = self.ndiffAssertEqual
   1375         # msg 39.txt is similarly evil in that it's got inner parts that use
   1376         # the same boundary as outer parts.  Again, I believe the way this is
   1377         # parsed is closest to the spirit of RFC 2046
   1378         msg = self._msgobj('msg_39.txt')
   1379         sfp = StringIO()
   1380         iterators._structure(msg, sfp)
   1381         eq(sfp.getvalue(), """\
   1382 multipart/mixed
   1383     multipart/mixed
   1384         multipart/alternative
   1385         application/octet-stream
   1386         application/octet-stream
   1387     text/plain
   1388 """)
   1389 
   1390     def test_boundary_in_non_multipart(self):
   1391         msg = self._msgobj('msg_40.txt')
   1392         self.assertEqual(msg.as_string(), '''\
   1393 MIME-Version: 1.0
   1394 Content-Type: text/html; boundary="--961284236552522269"
   1395 
   1396 ----961284236552522269
   1397 Content-Type: text/html;
   1398 Content-Transfer-Encoding: 7Bit
   1399 
   1400 <html></html>
   1401 
   1402 ----961284236552522269--
   1403 ''')
   1404 
   1405     def test_boundary_with_leading_space(self):
   1406         eq = self.assertEqual
   1407         msg = email.message_from_string('''\
   1408 MIME-Version: 1.0
   1409 Content-Type: multipart/mixed; boundary="    XXXX"
   1410 
   1411 --    XXXX
   1412 Content-Type: text/plain
   1413 
   1414 
   1415 --    XXXX
   1416 Content-Type: text/plain
   1417 
   1418 --    XXXX--
   1419 ''')
   1420         self.assertTrue(msg.is_multipart())
   1421         eq(msg.get_boundary(), '    XXXX')
   1422         eq(len(msg.get_payload()), 2)
   1423 
   1424     def test_boundary_without_trailing_newline(self):
   1425         m = Parser().parsestr("""\
   1426 Content-Type: multipart/mixed; boundary="===============0012394164=="
   1427 MIME-Version: 1.0
   1428 
   1429 --===============0012394164==
   1430 Content-Type: image/file1.jpg
   1431 MIME-Version: 1.0
   1432 Content-Transfer-Encoding: base64
   1433 
   1434 YXNkZg==
   1435 --===============0012394164==--""")
   1436         self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==')
   1437 
   1438 
   1439 
   1440 # Test some badly formatted messages
   1441 class TestNonConformant(TestEmailBase):
   1442     def test_parse_missing_minor_type(self):
   1443         eq = self.assertEqual
   1444         msg = self._msgobj('msg_14.txt')
   1445         eq(msg.get_content_type(), 'text/plain')
   1446         eq(msg.get_content_maintype(), 'text')
   1447         eq(msg.get_content_subtype(), 'plain')
   1448 
   1449     def test_same_boundary_inner_outer(self):
   1450         msg = self._msgobj('msg_15.txt')
   1451         # XXX We can probably eventually do better
   1452         inner = msg.get_payload(0)
   1453         self.assertTrue(hasattr(inner, 'defects'))
   1454         self.assertEqual(len(inner.defects), 1)
   1455         self.assertIsInstance(inner.defects[0],
   1456                               errors.StartBoundaryNotFoundDefect)
   1457 
   1458     def test_multipart_no_boundary(self):
   1459         msg = self._msgobj('msg_25.txt')
   1460         self.assertIsInstance(msg.get_payload(), str)
   1461         self.assertEqual(len(msg.defects), 2)
   1462         self.assertIsInstance(msg.defects[0],
   1463                               errors.NoBoundaryInMultipartDefect)
   1464         self.assertIsInstance(msg.defects[1],
   1465                               errors.MultipartInvariantViolationDefect)
   1466 
   1467     def test_invalid_content_type(self):
   1468         eq = self.assertEqual
   1469         neq = self.ndiffAssertEqual
   1470         msg = Message()
   1471         # RFC 2045, $5.2 says invalid yields text/plain
   1472         msg['Content-Type'] = 'text'
   1473         eq(msg.get_content_maintype(), 'text')
   1474         eq(msg.get_content_subtype(), 'plain')
   1475         eq(msg.get_content_type(), 'text/plain')
   1476         # Clear the old value and try something /really/ invalid
   1477         del msg['content-type']
   1478         msg['Content-Type'] = 'foo'
   1479         eq(msg.get_content_maintype(), 'text')
   1480         eq(msg.get_content_subtype(), 'plain')
   1481         eq(msg.get_content_type(), 'text/plain')
   1482         # Still, make sure that the message is idempotently generated
   1483         s = StringIO()
   1484         g = Generator(s)
   1485         g.flatten(msg)
   1486         neq(s.getvalue(), 'Content-Type: foo\n\n')
   1487 
   1488     def test_no_start_boundary(self):
   1489         eq = self.ndiffAssertEqual
   1490         msg = self._msgobj('msg_31.txt')
   1491         eq(msg.get_payload(), """\
   1492 --BOUNDARY
   1493 Content-Type: text/plain
   1494 
   1495 message 1
   1496 
   1497 --BOUNDARY
   1498 Content-Type: text/plain
   1499 
   1500 message 2
   1501 
   1502 --BOUNDARY--
   1503 """)
   1504 
   1505     def test_no_separating_blank_line(self):
   1506         eq = self.ndiffAssertEqual
   1507         msg = self._msgobj('msg_35.txt')
   1508         eq(msg.as_string(), """\
   1509 From: aperson (at] dom.ain
   1510 To: bperson (at] dom.ain
   1511 Subject: here's something interesting
   1512 
   1513 counter to RFC 2822, there's no separating newline here
   1514 """)
   1515 
   1516     def test_lying_multipart(self):
   1517         msg = self._msgobj('msg_41.txt')
   1518         self.assertTrue(hasattr(msg, 'defects'))
   1519         self.assertEqual(len(msg.defects), 2)
   1520         self.assertIsInstance(msg.defects[0],
   1521                               errors.NoBoundaryInMultipartDefect)
   1522         self.assertIsInstance(msg.defects[1],
   1523                               errors.MultipartInvariantViolationDefect)
   1524 
   1525     def test_missing_start_boundary(self):
   1526         outer = self._msgobj('msg_42.txt')
   1527         # The message structure is:
   1528         #
   1529         # multipart/mixed
   1530         #    text/plain
   1531         #    message/rfc822
   1532         #        multipart/mixed [*]
   1533         #
   1534         # [*] This message is missing its start boundary
   1535         bad = outer.get_payload(1).get_payload(0)
   1536         self.assertEqual(len(bad.defects), 1)
   1537         self.assertIsInstance(bad.defects[0],
   1538                               errors.StartBoundaryNotFoundDefect)
   1539 
   1540     def test_first_line_is_continuation_header(self):
   1541         eq = self.assertEqual
   1542         m = ' Line 1\nLine 2\nLine 3'
   1543         msg = email.message_from_string(m)
   1544         eq(msg.keys(), [])
   1545         eq(msg.get_payload(), 'Line 2\nLine 3')
   1546         eq(len(msg.defects), 1)
   1547         self.assertIsInstance(msg.defects[0],
   1548                               errors.FirstHeaderLineIsContinuationDefect)
   1549         eq(msg.defects[0].line, ' Line 1\n')
   1550 
   1551 
   1552 
   1553 # Test RFC 2047 header encoding and decoding
   1554 class TestRFC2047(unittest.TestCase):
   1555     def test_rfc2047_multiline(self):
   1556         eq = self.assertEqual
   1557         s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz
   1558  foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?="""
   1559         dh = decode_header(s)
   1560         eq(dh, [
   1561             ('Re:', None),
   1562             ('r\x8aksm\x9arg\x8cs', 'mac-iceland'),
   1563             ('baz foo bar', None),
   1564             ('r\x8aksm\x9arg\x8cs', 'mac-iceland')])
   1565         eq(str(make_header(dh)),
   1566            """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar
   1567  =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""")
   1568 
   1569     def test_whitespace_eater_unicode(self):
   1570         eq = self.assertEqual
   1571         s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard (at] dom.ain>'
   1572         dh = decode_header(s)
   1573         eq(dh, [('Andr\xe9', 'iso-8859-1'), ('Pirard <pirard (at] dom.ain>', None)])
   1574         hu = unicode(make_header(dh)).encode('latin-1')
   1575         eq(hu, 'Andr\xe9 Pirard <pirard (at] dom.ain>')
   1576 
   1577     def test_whitespace_eater_unicode_2(self):
   1578         eq = self.assertEqual
   1579         s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?='
   1580         dh = decode_header(s)
   1581         eq(dh, [('The', None), ('quick brown fox', 'iso-8859-1'),
   1582                 ('jumped over the', None), ('lazy dog', 'iso-8859-1')])
   1583         hu = make_header(dh).__unicode__()
   1584         eq(hu, u'The quick brown fox jumped over the lazy dog')
   1585 
   1586     def test_rfc2047_missing_whitespace(self):
   1587         s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord'
   1588         dh = decode_header(s)
   1589         self.assertEqual(dh, [(s, None)])
   1590 
   1591     def test_rfc2047_with_whitespace(self):
   1592         s = 'Sm =?ISO-8859-1?B?9g==?= rg =?ISO-8859-1?B?5Q==?= sbord'
   1593         dh = decode_header(s)
   1594         self.assertEqual(dh, [('Sm', None), ('\xf6', 'iso-8859-1'),
   1595                               ('rg', None), ('\xe5', 'iso-8859-1'),
   1596                               ('sbord', None)])
   1597 
   1598 
   1599 
   1600 # Test the MIMEMessage class
   1601 class TestMIMEMessage(TestEmailBase):
   1602     def setUp(self):
   1603         fp = openfile('msg_11.txt')
   1604         try:
   1605             self._text = fp.read()
   1606         finally:
   1607             fp.close()
   1608 
   1609     def test_type_error(self):
   1610         self.assertRaises(TypeError, MIMEMessage, 'a plain string')
   1611 
   1612     def test_valid_argument(self):
   1613         eq = self.assertEqual
   1614         subject = 'A sub-message'
   1615         m = Message()
   1616         m['Subject'] = subject
   1617         r = MIMEMessage(m)
   1618         eq(r.get_content_type(), 'message/rfc822')
   1619         payload = r.get_payload()
   1620         self.assertIsInstance(payload, list)
   1621         eq(len(payload), 1)
   1622         subpart = payload[0]
   1623         self.assertIs(subpart, m)
   1624         eq(subpart['subject'], subject)
   1625 
   1626     def test_bad_multipart(self):
   1627         eq = self.assertEqual
   1628         msg1 = Message()
   1629         msg1['Subject'] = 'subpart 1'
   1630         msg2 = Message()
   1631         msg2['Subject'] = 'subpart 2'
   1632         r = MIMEMessage(msg1)
   1633         self.assertRaises(errors.MultipartConversionError, r.attach, msg2)
   1634 
   1635     def test_generate(self):
   1636         # First craft the message to be encapsulated
   1637         m = Message()
   1638         m['Subject'] = 'An enclosed message'
   1639         m.set_payload('Here is the body of the message.\n')
   1640         r = MIMEMessage(m)
   1641         r['Subject'] = 'The enclosing message'
   1642         s = StringIO()
   1643         g = Generator(s)
   1644         g.flatten(r)
   1645         self.assertEqual(s.getvalue(), """\
   1646 Content-Type: message/rfc822
   1647 MIME-Version: 1.0
   1648 Subject: The enclosing message
   1649 
   1650 Subject: An enclosed message
   1651 
   1652 Here is the body of the message.
   1653 """)
   1654 
   1655     def test_parse_message_rfc822(self):
   1656         eq = self.assertEqual
   1657         msg = self._msgobj('msg_11.txt')
   1658         eq(msg.get_content_type(), 'message/rfc822')
   1659         payload = msg.get_payload()
   1660         self.assertIsInstance(payload, list)
   1661         eq(len(payload), 1)
   1662         submsg = payload[0]
   1663         self.assertIsInstance(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         # msg 16 is a Delivery Status Notification, see RFC 1894
   1670         msg = self._msgobj('msg_16.txt')
   1671         eq(msg.get_content_type(), 'multipart/report')
   1672         self.assertTrue(msg.is_multipart())
   1673         eq(len(msg.get_payload()), 3)
   1674         # Subpart 1 is a text/plain, human readable section
   1675         subpart = msg.get_payload(0)
   1676         eq(subpart.get_content_type(), 'text/plain')
   1677         eq(subpart.get_payload(), """\
   1678 This report relates to a message you sent with the following header fields:
   1679 
   1680   Message-id: <002001c144a6$8752e060$56104586 (at] oxy.edu>
   1681   Date: Sun, 23 Sep 2001 20:10:55 -0700
   1682   From: "Ian T. Henry" <henryi (at] oxy.edu>
   1683   To: SoCal Raves <scr (at] socal-raves.org>
   1684   Subject: [scr] yeah for Ians!!
   1685 
   1686 Your message cannot be delivered to the following recipients:
   1687 
   1688   Recipient address: jangel1 (at] cougar.noc.ucla.edu
   1689   Reason: recipient reached disk quota
   1690 
   1691 """)
   1692         # Subpart 2 contains the machine parsable DSN information.  It
   1693         # consists of two blocks of headers, represented by two nested Message
   1694         # objects.
   1695         subpart = msg.get_payload(1)
   1696         eq(subpart.get_content_type(), 'message/delivery-status')
   1697         eq(len(subpart.get_payload()), 2)
   1698         # message/delivery-status should treat each block as a bunch of
   1699         # headers, i.e. a bunch of Message objects.
   1700         dsn1 = subpart.get_payload(0)
   1701         self.assertIsInstance(dsn1, Message)
   1702         eq(dsn1['original-envelope-id'], '0GK500B4HD0888 (at] cougar.noc.ucla.edu')
   1703         eq(dsn1.get_param('dns', header='reporting-mta'), '')
   1704         # Try a missing one <wink>
   1705         eq(dsn1.get_param('nsd', header='reporting-mta'), None)
   1706         dsn2 = subpart.get_payload(1)
   1707         self.assertIsInstance(dsn2, Message)
   1708         eq(dsn2['action'], 'failed')
   1709         eq(dsn2.get_params(header='original-recipient'),
   1710            [('rfc822', ''), ('jangel1 (at] cougar.noc.ucla.edu', '')])
   1711         eq(dsn2.get_param('rfc822', header='final-recipient'), '')
   1712         # Subpart 3 is the original message
   1713         subpart = msg.get_payload(2)
   1714         eq(subpart.get_content_type(), 'message/rfc822')
   1715         payload = subpart.get_payload()
   1716         self.assertIsInstance(payload, list)
   1717         eq(len(payload), 1)
   1718         subsubpart = payload[0]
   1719         self.assertIsInstance(subsubpart, Message)
   1720         eq(subsubpart.get_content_type(), 'text/plain')
   1721         eq(subsubpart['message-id'],
   1722            '<002001c144a6$8752e060$56104586 (at] oxy.edu>')
   1723 
   1724     def test_epilogue(self):
   1725         eq = self.ndiffAssertEqual
   1726         fp = openfile('msg_21.txt')
   1727         try:
   1728             text = fp.read()
   1729         finally:
   1730             fp.close()
   1731         msg = Message()
   1732         msg['From'] = 'aperson (at] dom.ain'
   1733         msg['To'] = 'bperson (at] dom.ain'
   1734         msg['Subject'] = 'Test'
   1735         msg.preamble = 'MIME message'
   1736         msg.epilogue = 'End of MIME message\n'
   1737         msg1 = MIMEText('One')
   1738         msg2 = MIMEText('Two')
   1739         msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
   1740         msg.attach(msg1)
   1741         msg.attach(msg2)
   1742         sfp = StringIO()
   1743         g = Generator(sfp)
   1744         g.flatten(msg)
   1745         eq(sfp.getvalue(), text)
   1746 
   1747     def test_no_nl_preamble(self):
   1748         eq = self.ndiffAssertEqual
   1749         msg = Message()
   1750         msg['From'] = 'aperson (at] dom.ain'
   1751         msg['To'] = 'bperson (at] dom.ain'
   1752         msg['Subject'] = 'Test'
   1753         msg.preamble = 'MIME message'
   1754         msg.epilogue = ''
   1755         msg1 = MIMEText('One')
   1756         msg2 = MIMEText('Two')
   1757         msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
   1758         msg.attach(msg1)
   1759         msg.attach(msg2)
   1760         eq(msg.as_string(), """\
   1761 From: aperson (at] dom.ain
   1762 To: bperson (at] dom.ain
   1763 Subject: Test
   1764 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1765 
   1766 MIME message
   1767 --BOUNDARY
   1768 Content-Type: text/plain; charset="us-ascii"
   1769 MIME-Version: 1.0
   1770 Content-Transfer-Encoding: 7bit
   1771 
   1772 One
   1773 --BOUNDARY
   1774 Content-Type: text/plain; charset="us-ascii"
   1775 MIME-Version: 1.0
   1776 Content-Transfer-Encoding: 7bit
   1777 
   1778 Two
   1779 --BOUNDARY--
   1780 """)
   1781 
   1782     def test_default_type(self):
   1783         eq = self.assertEqual
   1784         fp = openfile('msg_30.txt')
   1785         try:
   1786             msg = email.message_from_file(fp)
   1787         finally:
   1788             fp.close()
   1789         container1 = msg.get_payload(0)
   1790         eq(container1.get_default_type(), 'message/rfc822')
   1791         eq(container1.get_content_type(), 'message/rfc822')
   1792         container2 = msg.get_payload(1)
   1793         eq(container2.get_default_type(), 'message/rfc822')
   1794         eq(container2.get_content_type(), 'message/rfc822')
   1795         container1a = container1.get_payload(0)
   1796         eq(container1a.get_default_type(), 'text/plain')
   1797         eq(container1a.get_content_type(), 'text/plain')
   1798         container2a = container2.get_payload(0)
   1799         eq(container2a.get_default_type(), 'text/plain')
   1800         eq(container2a.get_content_type(), 'text/plain')
   1801 
   1802     def test_default_type_with_explicit_container_type(self):
   1803         eq = self.assertEqual
   1804         fp = openfile('msg_28.txt')
   1805         try:
   1806             msg = email.message_from_file(fp)
   1807         finally:
   1808             fp.close()
   1809         container1 = msg.get_payload(0)
   1810         eq(container1.get_default_type(), 'message/rfc822')
   1811         eq(container1.get_content_type(), 'message/rfc822')
   1812         container2 = msg.get_payload(1)
   1813         eq(container2.get_default_type(), 'message/rfc822')
   1814         eq(container2.get_content_type(), 'message/rfc822')
   1815         container1a = container1.get_payload(0)
   1816         eq(container1a.get_default_type(), 'text/plain')
   1817         eq(container1a.get_content_type(), 'text/plain')
   1818         container2a = container2.get_payload(0)
   1819         eq(container2a.get_default_type(), 'text/plain')
   1820         eq(container2a.get_content_type(), 'text/plain')
   1821 
   1822     def test_default_type_non_parsed(self):
   1823         eq = self.assertEqual
   1824         neq = self.ndiffAssertEqual
   1825         # Set up container
   1826         container = MIMEMultipart('digest', 'BOUNDARY')
   1827         container.epilogue = ''
   1828         # Set up subparts
   1829         subpart1a = MIMEText('message 1\n')
   1830         subpart2a = MIMEText('message 2\n')
   1831         subpart1 = MIMEMessage(subpart1a)
   1832         subpart2 = MIMEMessage(subpart2a)
   1833         container.attach(subpart1)
   1834         container.attach(subpart2)
   1835         eq(subpart1.get_content_type(), 'message/rfc822')
   1836         eq(subpart1.get_default_type(), 'message/rfc822')
   1837         eq(subpart2.get_content_type(), 'message/rfc822')
   1838         eq(subpart2.get_default_type(), 'message/rfc822')
   1839         neq(container.as_string(0), '''\
   1840 Content-Type: multipart/digest; boundary="BOUNDARY"
   1841 MIME-Version: 1.0
   1842 
   1843 --BOUNDARY
   1844 Content-Type: message/rfc822
   1845 MIME-Version: 1.0
   1846 
   1847 Content-Type: text/plain; charset="us-ascii"
   1848 MIME-Version: 1.0
   1849 Content-Transfer-Encoding: 7bit
   1850 
   1851 message 1
   1852 
   1853 --BOUNDARY
   1854 Content-Type: message/rfc822
   1855 MIME-Version: 1.0
   1856 
   1857 Content-Type: text/plain; charset="us-ascii"
   1858 MIME-Version: 1.0
   1859 Content-Transfer-Encoding: 7bit
   1860 
   1861 message 2
   1862 
   1863 --BOUNDARY--
   1864 ''')
   1865         del subpart1['content-type']
   1866         del subpart1['mime-version']
   1867         del subpart2['content-type']
   1868         del subpart2['mime-version']
   1869         eq(subpart1.get_content_type(), 'message/rfc822')
   1870         eq(subpart1.get_default_type(), 'message/rfc822')
   1871         eq(subpart2.get_content_type(), 'message/rfc822')
   1872         eq(subpart2.get_default_type(), 'message/rfc822')
   1873         neq(container.as_string(0), '''\
   1874 Content-Type: multipart/digest; boundary="BOUNDARY"
   1875 MIME-Version: 1.0
   1876 
   1877 --BOUNDARY
   1878 
   1879 Content-Type: text/plain; charset="us-ascii"
   1880 MIME-Version: 1.0
   1881 Content-Transfer-Encoding: 7bit
   1882 
   1883 message 1
   1884 
   1885 --BOUNDARY
   1886 
   1887 Content-Type: text/plain; charset="us-ascii"
   1888 MIME-Version: 1.0
   1889 Content-Transfer-Encoding: 7bit
   1890 
   1891 message 2
   1892 
   1893 --BOUNDARY--
   1894 ''')
   1895 
   1896     def test_mime_attachments_in_constructor(self):
   1897         eq = self.assertEqual
   1898         text1 = MIMEText('')
   1899         text2 = MIMEText('')
   1900         msg = MIMEMultipart(_subparts=(text1, text2))
   1901         eq(len(msg.get_payload()), 2)
   1902         eq(msg.get_payload(0), text1)
   1903         eq(msg.get_payload(1), text2)
   1904 
   1905 
   1906 
   1907 # A general test of parser->model->generator idempotency.  IOW, read a message
   1908 # in, parse it into a message object tree, then without touching the tree,
   1909 # regenerate the plain text.  The original text and the transformed text
   1910 # should be identical.  Note: that we ignore the Unix-From since that may
   1911 # contain a changed date.
   1912 class TestIdempotent(TestEmailBase):
   1913     def _msgobj(self, filename):
   1914         fp = openfile(filename)
   1915         try:
   1916             data = fp.read()
   1917         finally:
   1918             fp.close()
   1919         msg = email.message_from_string(data)
   1920         return msg, data
   1921 
   1922     def _idempotent(self, msg, text):
   1923         eq = self.ndiffAssertEqual
   1924         s = StringIO()
   1925         g = Generator(s, maxheaderlen=0)
   1926         g.flatten(msg)
   1927         eq(text, s.getvalue())
   1928 
   1929     def test_parse_text_message(self):
   1930         eq = self.assertEqual
   1931         msg, text = self._msgobj('msg_01.txt')
   1932         eq(msg.get_content_type(), 'text/plain')
   1933         eq(msg.get_content_maintype(), 'text')
   1934         eq(msg.get_content_subtype(), 'plain')
   1935         eq(msg.get_params()[1], ('charset', 'us-ascii'))
   1936         eq(msg.get_param('charset'), 'us-ascii')
   1937         eq(msg.preamble, None)
   1938         eq(msg.epilogue, None)
   1939         self._idempotent(msg, text)
   1940 
   1941     def test_parse_untyped_message(self):
   1942         eq = self.assertEqual
   1943         msg, text = self._msgobj('msg_03.txt')
   1944         eq(msg.get_content_type(), 'text/plain')
   1945         eq(msg.get_params(), None)
   1946         eq(msg.get_param('charset'), None)
   1947         self._idempotent(msg, text)
   1948 
   1949     def test_simple_multipart(self):
   1950         msg, text = self._msgobj('msg_04.txt')
   1951         self._idempotent(msg, text)
   1952 
   1953     def test_MIME_digest(self):
   1954         msg, text = self._msgobj('msg_02.txt')
   1955         self._idempotent(msg, text)
   1956 
   1957     def test_long_header(self):
   1958         msg, text = self._msgobj('msg_27.txt')
   1959         self._idempotent(msg, text)
   1960 
   1961     def test_MIME_digest_with_part_headers(self):
   1962         msg, text = self._msgobj('msg_28.txt')
   1963         self._idempotent(msg, text)
   1964 
   1965     def test_mixed_with_image(self):
   1966         msg, text = self._msgobj('msg_06.txt')
   1967         self._idempotent(msg, text)
   1968 
   1969     def test_multipart_report(self):
   1970         msg, text = self._msgobj('msg_05.txt')
   1971         self._idempotent(msg, text)
   1972 
   1973     def test_dsn(self):
   1974         msg, text = self._msgobj('msg_16.txt')
   1975         self._idempotent(msg, text)
   1976 
   1977     def test_preamble_epilogue(self):
   1978         msg, text = self._msgobj('msg_21.txt')
   1979         self._idempotent(msg, text)
   1980 
   1981     def test_multipart_one_part(self):
   1982         msg, text = self._msgobj('msg_23.txt')
   1983         self._idempotent(msg, text)
   1984 
   1985     def test_multipart_no_parts(self):
   1986         msg, text = self._msgobj('msg_24.txt')
   1987         self._idempotent(msg, text)
   1988 
   1989     def test_no_start_boundary(self):
   1990         msg, text = self._msgobj('msg_31.txt')
   1991         self._idempotent(msg, text)
   1992 
   1993     def test_rfc2231_charset(self):
   1994         msg, text = self._msgobj('msg_32.txt')
   1995         self._idempotent(msg, text)
   1996 
   1997     def test_more_rfc2231_parameters(self):
   1998         msg, text = self._msgobj('msg_33.txt')
   1999         self._idempotent(msg, text)
   2000 
   2001     def test_text_plain_in_a_multipart_digest(self):
   2002         msg, text = self._msgobj('msg_34.txt')
   2003         self._idempotent(msg, text)
   2004 
   2005     def test_nested_multipart_mixeds(self):
   2006         msg, text = self._msgobj('msg_12a.txt')
   2007         self._idempotent(msg, text)
   2008 
   2009     def test_message_external_body_idempotent(self):
   2010         msg, text = self._msgobj('msg_36.txt')
   2011         self._idempotent(msg, text)
   2012 
   2013     def test_content_type(self):
   2014         eq = self.assertEqual
   2015         # Get a message object and reset the seek pointer for other tests
   2016         msg, text = self._msgobj('msg_05.txt')
   2017         eq(msg.get_content_type(), 'multipart/report')
   2018         # Test the Content-Type: parameters
   2019         params = {}
   2020         for pk, pv in msg.get_params():
   2021             params[pk] = pv
   2022         eq(params['report-type'], 'delivery-status')
   2023         eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
   2024         eq(msg.preamble, 'This is a MIME-encapsulated message.\n')
   2025         eq(msg.epilogue, '\n')
   2026         eq(len(msg.get_payload()), 3)
   2027         # Make sure the subparts are what we expect
   2028         msg1 = msg.get_payload(0)
   2029         eq(msg1.get_content_type(), 'text/plain')
   2030         eq(msg1.get_payload(), 'Yadda yadda yadda\n')
   2031         msg2 = msg.get_payload(1)
   2032         eq(msg2.get_content_type(), 'text/plain')
   2033         eq(msg2.get_payload(), 'Yadda yadda yadda\n')
   2034         msg3 = msg.get_payload(2)
   2035         eq(msg3.get_content_type(), 'message/rfc822')
   2036         self.assertIsInstance(msg3, Message)
   2037         payload = msg3.get_payload()
   2038         self.assertIsInstance(payload, list)
   2039         eq(len(payload), 1)
   2040         msg4 = payload[0]
   2041         self.assertIsInstance(msg4, Message)
   2042         eq(msg4.get_payload(), 'Yadda yadda yadda\n')
   2043 
   2044     def test_parser(self):
   2045         eq = self.assertEqual
   2046         msg, text = self._msgobj('msg_06.txt')
   2047         # Check some of the outer headers
   2048         eq(msg.get_content_type(), 'message/rfc822')
   2049         # Make sure the payload is a list of exactly one sub-Message, and that
   2050         # that submessage has a type of text/plain
   2051         payload = msg.get_payload()
   2052         self.assertIsInstance(payload, list)
   2053         eq(len(payload), 1)
   2054         msg1 = payload[0]
   2055         self.assertIsInstance(msg1, Message)
   2056         eq(msg1.get_content_type(), 'text/plain')
   2057         self.assertIsInstance(msg1.get_payload(), str)
   2058         eq(msg1.get_payload(), '\n')
   2059 
   2060 
   2061 
   2062 # Test various other bits of the package's functionality
   2063 class TestMiscellaneous(TestEmailBase):
   2064     def test_message_from_string(self):
   2065         fp = openfile('msg_01.txt')
   2066         try:
   2067             text = fp.read()
   2068         finally:
   2069             fp.close()
   2070         msg = email.message_from_string(text)
   2071         s = StringIO()
   2072         # Don't wrap/continue long headers since we're trying to test
   2073         # idempotency.
   2074         g = Generator(s, maxheaderlen=0)
   2075         g.flatten(msg)
   2076         self.assertEqual(text, s.getvalue())
   2077 
   2078     def test_message_from_file(self):
   2079         fp = openfile('msg_01.txt')
   2080         try:
   2081             text = fp.read()
   2082             fp.seek(0)
   2083             msg = email.message_from_file(fp)
   2084             s = StringIO()
   2085             # Don't wrap/continue long headers since we're trying to test
   2086             # idempotency.
   2087             g = Generator(s, maxheaderlen=0)
   2088             g.flatten(msg)
   2089             self.assertEqual(text, s.getvalue())
   2090         finally:
   2091             fp.close()
   2092 
   2093     def test_message_from_string_with_class(self):
   2094         fp = openfile('msg_01.txt')
   2095         try:
   2096             text = fp.read()
   2097         finally:
   2098             fp.close()
   2099         # Create a subclass
   2100         class MyMessage(Message):
   2101             pass
   2102 
   2103         msg = email.message_from_string(text, MyMessage)
   2104         self.assertIsInstance(msg, MyMessage)
   2105         # Try something more complicated
   2106         fp = openfile('msg_02.txt')
   2107         try:
   2108             text = fp.read()
   2109         finally:
   2110             fp.close()
   2111         msg = email.message_from_string(text, MyMessage)
   2112         for subpart in msg.walk():
   2113             self.assertIsInstance(subpart, MyMessage)
   2114 
   2115     def test_message_from_file_with_class(self):
   2116         # Create a subclass
   2117         class MyMessage(Message):
   2118             pass
   2119 
   2120         fp = openfile('msg_01.txt')
   2121         try:
   2122             msg = email.message_from_file(fp, MyMessage)
   2123         finally:
   2124             fp.close()
   2125         self.assertIsInstance(msg, MyMessage)
   2126         # Try something more complicated
   2127         fp = openfile('msg_02.txt')
   2128         try:
   2129             msg = email.message_from_file(fp, MyMessage)
   2130         finally:
   2131             fp.close()
   2132         for subpart in msg.walk():
   2133             self.assertIsInstance(subpart, MyMessage)
   2134 
   2135     def test__all__(self):
   2136         module = __import__('email')
   2137         # Can't use sorted() here due to Python 2.3 compatibility
   2138         all = module.__all__[:]
   2139         all.sort()
   2140         self.assertEqual(all, [
   2141             # Old names
   2142             'Charset', 'Encoders', 'Errors', 'Generator',
   2143             'Header', 'Iterators', 'MIMEAudio', 'MIMEBase',
   2144             'MIMEImage', 'MIMEMessage', 'MIMEMultipart',
   2145             'MIMENonMultipart', 'MIMEText', 'Message',
   2146             'Parser', 'Utils', 'base64MIME',
   2147             # new names
   2148             'base64mime', 'charset', 'encoders', 'errors', 'generator',
   2149             'header', 'iterators', 'message', 'message_from_file',
   2150             'message_from_string', 'mime', 'parser',
   2151             'quopriMIME', 'quoprimime', 'utils',
   2152             ])
   2153 
   2154     def test_formatdate(self):
   2155         now = time.time()
   2156         self.assertEqual(utils.parsedate(utils.formatdate(now))[:6],
   2157                          time.gmtime(now)[:6])
   2158 
   2159     def test_formatdate_localtime(self):
   2160         now = time.time()
   2161         self.assertEqual(
   2162             utils.parsedate(utils.formatdate(now, localtime=True))[:6],
   2163             time.localtime(now)[:6])
   2164 
   2165     def test_formatdate_usegmt(self):
   2166         now = time.time()
   2167         self.assertEqual(
   2168             utils.formatdate(now, localtime=False),
   2169             time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now)))
   2170         self.assertEqual(
   2171             utils.formatdate(now, localtime=False, usegmt=True),
   2172             time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now)))
   2173 
   2174     def test_parsedate_none(self):
   2175         self.assertEqual(utils.parsedate(''), None)
   2176 
   2177     def test_parsedate_compact(self):
   2178         # The FWS after the comma is optional
   2179         self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'),
   2180                          utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800'))
   2181 
   2182     def test_parsedate_no_dayofweek(self):
   2183         eq = self.assertEqual
   2184         eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'),
   2185            (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800))
   2186 
   2187     def test_parsedate_compact_no_dayofweek(self):
   2188         eq = self.assertEqual
   2189         eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'),
   2190            (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800))
   2191 
   2192     def test_parsedate_acceptable_to_time_functions(self):
   2193         eq = self.assertEqual
   2194         timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800')
   2195         t = int(time.mktime(timetup))
   2196         eq(time.localtime(t)[:6], timetup[:6])
   2197         eq(int(time.strftime('%Y', timetup)), 2003)
   2198         timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800')
   2199         t = int(time.mktime(timetup[:9]))
   2200         eq(time.localtime(t)[:6], timetup[:6])
   2201         eq(int(time.strftime('%Y', timetup[:9])), 2003)
   2202 
   2203     def test_parseaddr_empty(self):
   2204         self.assertEqual(utils.parseaddr('<>'), ('', ''))
   2205         self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '')
   2206 
   2207     def test_noquote_dump(self):
   2208         self.assertEqual(
   2209             utils.formataddr(('A Silly Person', 'person (at] dom.ain')),
   2210             'A Silly Person <person (at] dom.ain>')
   2211 
   2212     def test_escape_dump(self):
   2213         self.assertEqual(
   2214             utils.formataddr(('A (Very) Silly Person', 'person (at] dom.ain')),
   2215             r'"A \(Very\) Silly Person" <person (at] dom.ain>')
   2216         a = r'A \(Special\) Person'
   2217         b = 'person (at] dom.ain'
   2218         self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
   2219 
   2220     def test_escape_backslashes(self):
   2221         self.assertEqual(
   2222             utils.formataddr(('Arthur \Backslash\ Foobar', 'person (at] dom.ain')),
   2223             r'"Arthur \\Backslash\\ Foobar" <person (at] dom.ain>')
   2224         a = r'Arthur \Backslash\ Foobar'
   2225         b = 'person (at] dom.ain'
   2226         self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
   2227 
   2228     def test_name_with_dot(self):
   2229         x = 'John X. Doe <jxd (at] example.com>'
   2230         y = '"John X. Doe" <jxd (at] example.com>'
   2231         a, b = ('John X. Doe', 'jxd (at] example.com')
   2232         self.assertEqual(utils.parseaddr(x), (a, b))
   2233         self.assertEqual(utils.parseaddr(y), (a, b))
   2234         # formataddr() quotes the name if there's a dot in it
   2235         self.assertEqual(utils.formataddr((a, b)), y)
   2236 
   2237     def test_multiline_from_comment(self):
   2238         x = """\
   2239 Foo
   2240 \tBar <foo (at] example.com>"""
   2241         self.assertEqual(utils.parseaddr(x), ('Foo Bar', 'foo (at] example.com'))
   2242 
   2243     def test_quote_dump(self):
   2244         self.assertEqual(
   2245             utils.formataddr(('A Silly; Person', 'person (at] dom.ain')),
   2246             r'"A Silly; Person" <person (at] dom.ain>')
   2247 
   2248     def test_fix_eols(self):
   2249         eq = self.assertEqual
   2250         eq(utils.fix_eols('hello'), 'hello')
   2251         eq(utils.fix_eols('hello\n'), 'hello\r\n')
   2252         eq(utils.fix_eols('hello\r'), 'hello\r\n')
   2253         eq(utils.fix_eols('hello\r\n'), 'hello\r\n')
   2254         eq(utils.fix_eols('hello\n\r'), 'hello\r\n\r\n')
   2255 
   2256     def test_charset_richcomparisons(self):
   2257         eq = self.assertEqual
   2258         ne = self.assertNotEqual
   2259         cset1 = Charset()
   2260         cset2 = Charset()
   2261         eq(cset1, 'us-ascii')
   2262         eq(cset1, 'US-ASCII')
   2263         eq(cset1, 'Us-AsCiI')
   2264         eq('us-ascii', cset1)
   2265         eq('US-ASCII', cset1)
   2266         eq('Us-AsCiI', cset1)
   2267         ne(cset1, 'usascii')
   2268         ne(cset1, 'USASCII')
   2269         ne(cset1, 'UsAsCiI')
   2270         ne('usascii', cset1)
   2271         ne('USASCII', cset1)
   2272         ne('UsAsCiI', cset1)
   2273         eq(cset1, cset2)
   2274         eq(cset2, cset1)
   2275 
   2276     def test_getaddresses(self):
   2277         eq = self.assertEqual
   2278         eq(utils.getaddresses(['aperson (at] dom.ain (Al Person)',
   2279                                'Bud Person <bperson (at] dom.ain>']),
   2280            [('Al Person', 'aperson (at] dom.ain'),
   2281             ('Bud Person', 'bperson (at] dom.ain')])
   2282 
   2283     def test_getaddresses_nasty(self):
   2284         eq = self.assertEqual
   2285         eq(utils.getaddresses(['foo: ;']), [('', '')])
   2286         eq(utils.getaddresses(
   2287            ['[]*-- =~$']),
   2288            [('', ''), ('', ''), ('', '*--')])
   2289         eq(utils.getaddresses(
   2290            ['foo: ;', '"Jason R. Mastaler" <jason (at] dom.ain>']),
   2291            [('', ''), ('Jason R. Mastaler', 'jason (at] dom.ain')])
   2292 
   2293     def test_getaddresses_embedded_comment(self):
   2294         """Test proper handling of a nested comment"""
   2295         eq = self.assertEqual
   2296         addrs = utils.getaddresses(['User ((nested comment)) <foo (at] bar.com>'])
   2297         eq(addrs[0][1], 'foo (at] bar.com')
   2298 
   2299     def test_utils_quote_unquote(self):
   2300         eq = self.assertEqual
   2301         msg = Message()
   2302         msg.add_header('content-disposition', 'attachment',
   2303                        filename='foo\\wacky"name')
   2304         eq(msg.get_filename(), 'foo\\wacky"name')
   2305 
   2306     def test_get_body_encoding_with_bogus_charset(self):
   2307         charset = Charset('not a charset')
   2308         self.assertEqual(charset.get_body_encoding(), 'base64')
   2309 
   2310     def test_get_body_encoding_with_uppercase_charset(self):
   2311         eq = self.assertEqual
   2312         msg = Message()
   2313         msg['Content-Type'] = 'text/plain; charset=UTF-8'
   2314         eq(msg['content-type'], 'text/plain; charset=UTF-8')
   2315         charsets = msg.get_charsets()
   2316         eq(len(charsets), 1)
   2317         eq(charsets[0], 'utf-8')
   2318         charset = Charset(charsets[0])
   2319         eq(charset.get_body_encoding(), 'base64')
   2320         msg.set_payload('hello world', charset=charset)
   2321         eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n')
   2322         eq(msg.get_payload(decode=True), 'hello world')
   2323         eq(msg['content-transfer-encoding'], 'base64')
   2324         # Try another one
   2325         msg = Message()
   2326         msg['Content-Type'] = 'text/plain; charset="US-ASCII"'
   2327         charsets = msg.get_charsets()
   2328         eq(len(charsets), 1)
   2329         eq(charsets[0], 'us-ascii')
   2330         charset = Charset(charsets[0])
   2331         eq(charset.get_body_encoding(), encoders.encode_7or8bit)
   2332         msg.set_payload('hello world', charset=charset)
   2333         eq(msg.get_payload(), 'hello world')
   2334         eq(msg['content-transfer-encoding'], '7bit')
   2335 
   2336     def test_charsets_case_insensitive(self):
   2337         lc = Charset('us-ascii')
   2338         uc = Charset('US-ASCII')
   2339         self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding())
   2340 
   2341     def test_partial_falls_inside_message_delivery_status(self):
   2342         eq = self.ndiffAssertEqual
   2343         # The Parser interface provides chunks of data to FeedParser in 8192
   2344         # byte gulps.  SF bug #1076485 found one of those chunks inside
   2345         # message/delivery-status header block, which triggered an
   2346         # unreadline() of NeedMoreData.
   2347         msg = self._msgobj('msg_43.txt')
   2348         sfp = StringIO()
   2349         iterators._structure(msg, sfp)
   2350         eq(sfp.getvalue(), """\
   2351 multipart/report
   2352     text/plain
   2353     message/delivery-status
   2354         text/plain
   2355         text/plain
   2356         text/plain
   2357         text/plain
   2358         text/plain
   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/rfc822-headers
   2381 """)
   2382 
   2383 
   2384 
   2385 # Test the iterator/generators
   2386 class TestIterators(TestEmailBase):
   2387     def test_body_line_iterator(self):
   2388         eq = self.assertEqual
   2389         neq = self.ndiffAssertEqual
   2390         # First a simple non-multipart message
   2391         msg = self._msgobj('msg_01.txt')
   2392         it = iterators.body_line_iterator(msg)
   2393         lines = list(it)
   2394         eq(len(lines), 6)
   2395         neq(EMPTYSTRING.join(lines), msg.get_payload())
   2396         # Now a more complicated multipart
   2397         msg = self._msgobj('msg_02.txt')
   2398         it = iterators.body_line_iterator(msg)
   2399         lines = list(it)
   2400         eq(len(lines), 43)
   2401         fp = openfile('msg_19.txt')
   2402         try:
   2403             neq(EMPTYSTRING.join(lines), fp.read())
   2404         finally:
   2405             fp.close()
   2406 
   2407     def test_typed_subpart_iterator(self):
   2408         eq = self.assertEqual
   2409         msg = self._msgobj('msg_04.txt')
   2410         it = iterators.typed_subpart_iterator(msg, 'text')
   2411         lines = []
   2412         subparts = 0
   2413         for subpart in it:
   2414             subparts += 1
   2415             lines.append(subpart.get_payload())
   2416         eq(subparts, 2)
   2417         eq(EMPTYSTRING.join(lines), """\
   2418 a simple kind of mirror
   2419 to reflect upon our own
   2420 a simple kind of mirror
   2421 to reflect upon our own
   2422 """)
   2423 
   2424     def test_typed_subpart_iterator_default_type(self):
   2425         eq = self.assertEqual
   2426         msg = self._msgobj('msg_03.txt')
   2427         it = iterators.typed_subpart_iterator(msg, 'text', 'plain')
   2428         lines = []
   2429         subparts = 0
   2430         for subpart in it:
   2431             subparts += 1
   2432             lines.append(subpart.get_payload())
   2433         eq(subparts, 1)
   2434         eq(EMPTYSTRING.join(lines), """\
   2435 
   2436 Hi,
   2437 
   2438 Do you like this message?
   2439 
   2440 -Me
   2441 """)
   2442 
   2443 
   2444 
   2445 class TestParsers(TestEmailBase):
   2446     def test_header_parser(self):
   2447         eq = self.assertEqual
   2448         # Parse only the headers of a complex multipart MIME document
   2449         fp = openfile('msg_02.txt')
   2450         try:
   2451             msg = HeaderParser().parse(fp)
   2452         finally:
   2453             fp.close()
   2454         eq(msg['from'], 'ppp-request (at] zzz.org')
   2455         eq(msg['to'], 'ppp (at] zzz.org')
   2456         eq(msg.get_content_type(), 'multipart/mixed')
   2457         self.assertFalse(msg.is_multipart())
   2458         self.assertIsInstance(msg.get_payload(), str)
   2459 
   2460     def test_whitespace_continuation(self):
   2461         eq = self.assertEqual
   2462         # This message contains a line after the Subject: header that has only
   2463         # whitespace, but it is not empty!
   2464         msg = email.message_from_string("""\
   2465 From: aperson (at] dom.ain
   2466 To: bperson (at] dom.ain
   2467 Subject: the next line has a space on it
   2468 \x20
   2469 Date: Mon, 8 Apr 2002 15:09:19 -0400
   2470 Message-ID: spam
   2471 
   2472 Here's the message body
   2473 """)
   2474         eq(msg['subject'], 'the next line has a space on it\n ')
   2475         eq(msg['message-id'], 'spam')
   2476         eq(msg.get_payload(), "Here's the message body\n")
   2477 
   2478     def test_whitespace_continuation_last_header(self):
   2479         eq = self.assertEqual
   2480         # Like the previous test, but the subject line is the last
   2481         # header.
   2482         msg = email.message_from_string("""\
   2483 From: aperson (at] dom.ain
   2484 To: bperson (at] dom.ain
   2485 Date: Mon, 8 Apr 2002 15:09:19 -0400
   2486 Message-ID: spam
   2487 Subject: the next line has a space on it
   2488 \x20
   2489 
   2490 Here's the message body
   2491 """)
   2492         eq(msg['subject'], 'the next line has a space on it\n ')
   2493         eq(msg['message-id'], 'spam')
   2494         eq(msg.get_payload(), "Here's the message body\n")
   2495 
   2496     def test_crlf_separation(self):
   2497         eq = self.assertEqual
   2498         fp = openfile('msg_26.txt', mode='rb')
   2499         try:
   2500             msg = Parser().parse(fp)
   2501         finally:
   2502             fp.close()
   2503         eq(len(msg.get_payload()), 2)
   2504         part1 = msg.get_payload(0)
   2505         eq(part1.get_content_type(), 'text/plain')
   2506         eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n')
   2507         part2 = msg.get_payload(1)
   2508         eq(part2.get_content_type(), 'application/riscos')
   2509 
   2510     def test_multipart_digest_with_extra_mime_headers(self):
   2511         eq = self.assertEqual
   2512         neq = self.ndiffAssertEqual
   2513         fp = openfile('msg_28.txt')
   2514         try:
   2515             msg = email.message_from_file(fp)
   2516         finally:
   2517             fp.close()
   2518         # Structure is:
   2519         # multipart/digest
   2520         #   message/rfc822
   2521         #     text/plain
   2522         #   message/rfc822
   2523         #     text/plain
   2524         eq(msg.is_multipart(), 1)
   2525         eq(len(msg.get_payload()), 2)
   2526         part1 = msg.get_payload(0)
   2527         eq(part1.get_content_type(), 'message/rfc822')
   2528         eq(part1.is_multipart(), 1)
   2529         eq(len(part1.get_payload()), 1)
   2530         part1a = part1.get_payload(0)
   2531         eq(part1a.is_multipart(), 0)
   2532         eq(part1a.get_content_type(), 'text/plain')
   2533         neq(part1a.get_payload(), 'message 1\n')
   2534         # next message/rfc822
   2535         part2 = msg.get_payload(1)
   2536         eq(part2.get_content_type(), 'message/rfc822')
   2537         eq(part2.is_multipart(), 1)
   2538         eq(len(part2.get_payload()), 1)
   2539         part2a = part2.get_payload(0)
   2540         eq(part2a.is_multipart(), 0)
   2541         eq(part2a.get_content_type(), 'text/plain')
   2542         neq(part2a.get_payload(), 'message 2\n')
   2543 
   2544     def test_three_lines(self):
   2545         # A bug report by Andrew McNamara
   2546         lines = ['From: Andrew Person <aperson (at] dom.ain',
   2547                  'Subject: Test',
   2548                  'Date: Tue, 20 Aug 2002 16:43:45 +1000']
   2549         msg = email.message_from_string(NL.join(lines))
   2550         self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000')
   2551 
   2552     def test_strip_line_feed_and_carriage_return_in_headers(self):
   2553         eq = self.assertEqual
   2554         # For [ 1002475 ] email message parser doesn't handle \r\n correctly
   2555         value1 = 'text'
   2556         value2 = 'more text'
   2557         m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % (
   2558             value1, value2)
   2559         msg = email.message_from_string(m)
   2560         eq(msg.get('Header'), value1)
   2561         eq(msg.get('Next-Header'), value2)
   2562 
   2563     def test_rfc2822_header_syntax(self):
   2564         eq = self.assertEqual
   2565         m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
   2566         msg = email.message_from_string(m)
   2567         eq(len(msg.keys()), 3)
   2568         keys = msg.keys()
   2569         keys.sort()
   2570         eq(keys, ['!"#QUX;~', '>From', 'From'])
   2571         eq(msg.get_payload(), 'body')
   2572 
   2573     def test_rfc2822_space_not_allowed_in_header(self):
   2574         eq = self.assertEqual
   2575         m = '>From foo (at] example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
   2576         msg = email.message_from_string(m)
   2577         eq(len(msg.keys()), 0)
   2578 
   2579     def test_rfc2822_one_character_header(self):
   2580         eq = self.assertEqual
   2581         m = 'A: first header\nB: second header\nCC: third header\n\nbody'
   2582         msg = email.message_from_string(m)
   2583         headers = msg.keys()
   2584         headers.sort()
   2585         eq(headers, ['A', 'B', 'CC'])
   2586         eq(msg.get_payload(), 'body')
   2587 
   2588 
   2589 
   2590 class TestBase64(unittest.TestCase):
   2591     def test_len(self):
   2592         eq = self.assertEqual
   2593         eq(base64mime.base64_len('hello'),
   2594            len(base64mime.encode('hello', eol='')))
   2595         for size in range(15):
   2596             if   size == 0 : bsize = 0
   2597             elif size <= 3 : bsize = 4
   2598             elif size <= 6 : bsize = 8
   2599             elif size <= 9 : bsize = 12
   2600             elif size <= 12: bsize = 16
   2601             else           : bsize = 20
   2602             eq(base64mime.base64_len('x'*size), bsize)
   2603 
   2604     def test_decode(self):
   2605         eq = self.assertEqual
   2606         eq(base64mime.decode(''), '')
   2607         eq(base64mime.decode('aGVsbG8='), 'hello')
   2608         eq(base64mime.decode('aGVsbG8=', 'X'), 'hello')
   2609         eq(base64mime.decode('aGVsbG8NCndvcmxk\n', 'X'), 'helloXworld')
   2610 
   2611     def test_encode(self):
   2612         eq = self.assertEqual
   2613         eq(base64mime.encode(''), '')
   2614         eq(base64mime.encode('hello'), 'aGVsbG8=\n')
   2615         # Test the binary flag
   2616         eq(base64mime.encode('hello\n'), 'aGVsbG8K\n')
   2617         eq(base64mime.encode('hello\n', 0), 'aGVsbG8NCg==\n')
   2618         # Test the maxlinelen arg
   2619         eq(base64mime.encode('xxxx ' * 20, maxlinelen=40), """\
   2620 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
   2621 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
   2622 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
   2623 eHh4eCB4eHh4IA==
   2624 """)
   2625         # Test the eol argument
   2626         eq(base64mime.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
   2627 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
   2628 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
   2629 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
   2630 eHh4eCB4eHh4IA==\r
   2631 """)
   2632 
   2633     def test_header_encode(self):
   2634         eq = self.assertEqual
   2635         he = base64mime.header_encode
   2636         eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=')
   2637         eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=')
   2638         # Test the charset option
   2639         eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=')
   2640         # Test the keep_eols flag
   2641         eq(he('hello\nworld', keep_eols=True),
   2642            '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
   2643         # Test the maxlinelen argument
   2644         eq(he('xxxx ' * 20, maxlinelen=40), """\
   2645 =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?=
   2646  =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?=
   2647  =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?=
   2648  =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?=
   2649  =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?=
   2650  =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""")
   2651         # Test the eol argument
   2652         eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
   2653 =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?=\r
   2654  =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?=\r
   2655  =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?=\r
   2656  =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?=\r
   2657  =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?=\r
   2658  =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""")
   2659 
   2660 
   2661 
   2662 class TestQuopri(unittest.TestCase):
   2663     def setUp(self):
   2664         self.hlit = [chr(x) for x in range(ord('a'), ord('z')+1)] + \
   2665                     [chr(x) for x in range(ord('A'), ord('Z')+1)] + \
   2666                     [chr(x) for x in range(ord('0'), ord('9')+1)] + \
   2667                     ['!', '*', '+', '-', '/', ' ']
   2668         self.hnon = [chr(x) for x in range(256) if chr(x) not in self.hlit]
   2669         assert len(self.hlit) + len(self.hnon) == 256
   2670         self.blit = [chr(x) for x in range(ord(' '), ord('~')+1)] + ['\t']
   2671         self.blit.remove('=')
   2672         self.bnon = [chr(x) for x in range(256) if chr(x) not in self.blit]
   2673         assert len(self.blit) + len(self.bnon) == 256
   2674 
   2675     def test_header_quopri_check(self):
   2676         for c in self.hlit:
   2677             self.assertFalse(quoprimime.header_quopri_check(c))
   2678         for c in self.hnon:
   2679             self.assertTrue(quoprimime.header_quopri_check(c))
   2680 
   2681     def test_body_quopri_check(self):
   2682         for c in self.blit:
   2683             self.assertFalse(quoprimime.body_quopri_check(c))
   2684         for c in self.bnon:
   2685             self.assertTrue(quoprimime.body_quopri_check(c))
   2686 
   2687     def test_header_quopri_len(self):
   2688         eq = self.assertEqual
   2689         hql = quoprimime.header_quopri_len
   2690         enc = quoprimime.header_encode
   2691         for s in ('hello', 'h@e@l@l@o@'):
   2692             # Empty charset and no line-endings.  7 == RFC chrome
   2693             eq(hql(s), len(enc(s, charset='', eol=''))-7)
   2694         for c in self.hlit:
   2695             eq(hql(c), 1)
   2696         for c in self.hnon:
   2697             eq(hql(c), 3)
   2698 
   2699     def test_body_quopri_len(self):
   2700         eq = self.assertEqual
   2701         bql = quoprimime.body_quopri_len
   2702         for c in self.blit:
   2703             eq(bql(c), 1)
   2704         for c in self.bnon:
   2705             eq(bql(c), 3)
   2706 
   2707     def test_quote_unquote_idempotent(self):
   2708         for x in range(256):
   2709             c = chr(x)
   2710             self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c)
   2711 
   2712     def test_header_encode(self):
   2713         eq = self.assertEqual
   2714         he = quoprimime.header_encode
   2715         eq(he('hello'), '=?iso-8859-1?q?hello?=')
   2716         eq(he('hello\nworld'), '=?iso-8859-1?q?hello=0D=0Aworld?=')
   2717         # Test the charset option
   2718         eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=')
   2719         # Test the keep_eols flag
   2720         eq(he('hello\nworld', keep_eols=True), '=?iso-8859-1?q?hello=0Aworld?=')
   2721         # Test a non-ASCII character
   2722         eq(he('hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=')
   2723         # Test the maxlinelen argument
   2724         eq(he('xxxx ' * 20, maxlinelen=40), """\
   2725 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
   2726  =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
   2727  =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?=
   2728  =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?=
   2729  =?iso-8859-1?q?x_xxxx_xxxx_?=""")
   2730         # Test the eol argument
   2731         eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
   2732 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=\r
   2733  =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=\r
   2734  =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?=\r
   2735  =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?=\r
   2736  =?iso-8859-1?q?x_xxxx_xxxx_?=""")
   2737 
   2738     def test_decode(self):
   2739         eq = self.assertEqual
   2740         eq(quoprimime.decode(''), '')
   2741         eq(quoprimime.decode('hello'), 'hello')
   2742         eq(quoprimime.decode('hello', 'X'), 'hello')
   2743         eq(quoprimime.decode('hello\nworld', 'X'), 'helloXworld')
   2744 
   2745     def test_encode(self):
   2746         eq = self.assertEqual
   2747         eq(quoprimime.encode(''), '')
   2748         eq(quoprimime.encode('hello'), 'hello')
   2749         # Test the binary flag
   2750         eq(quoprimime.encode('hello\r\nworld'), 'hello\nworld')
   2751         eq(quoprimime.encode('hello\r\nworld', 0), 'hello\nworld')
   2752         # Test the maxlinelen arg
   2753         eq(quoprimime.encode('xxxx ' * 20, maxlinelen=40), """\
   2754 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
   2755  xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
   2756 x xxxx xxxx xxxx xxxx=20""")
   2757         # Test the eol argument
   2758         eq(quoprimime.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
   2759 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
   2760  xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
   2761 x xxxx xxxx xxxx xxxx=20""")
   2762         eq(quoprimime.encode("""\
   2763 one line
   2764 
   2765 two line"""), """\
   2766 one line
   2767 
   2768 two line""")
   2769 
   2770 
   2771 
   2772 # Test the Charset class
   2773 class TestCharset(unittest.TestCase):
   2774     def tearDown(self):
   2775         from email import charset as CharsetModule
   2776         try:
   2777             del CharsetModule.CHARSETS['fake']
   2778         except KeyError:
   2779             pass
   2780 
   2781     def test_idempotent(self):
   2782         eq = self.assertEqual
   2783         # Make sure us-ascii = no Unicode conversion
   2784         c = Charset('us-ascii')
   2785         s = 'Hello World!'
   2786         sp = c.to_splittable(s)
   2787         eq(s, c.from_splittable(sp))
   2788         # test 8-bit idempotency with us-ascii
   2789         s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
   2790         sp = c.to_splittable(s)
   2791         eq(s, c.from_splittable(sp))
   2792 
   2793     def test_body_encode(self):
   2794         eq = self.assertEqual
   2795         # Try a charset with QP body encoding
   2796         c = Charset('iso-8859-1')
   2797         eq('hello w=F6rld', c.body_encode('hello w\xf6rld'))
   2798         # Try a charset with Base64 body encoding
   2799         c = Charset('utf-8')
   2800         eq('aGVsbG8gd29ybGQ=\n', c.body_encode('hello world'))
   2801         # Try a charset with None body encoding
   2802         c = Charset('us-ascii')
   2803         eq('hello world', c.body_encode('hello world'))
   2804         # Try the convert argument, where input codec != output codec
   2805         c = Charset('euc-jp')
   2806         # With apologies to Tokio Kikuchi ;)
   2807         try:
   2808             eq('\x1b$B5FCO;~IW\x1b(B',
   2809                c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
   2810             eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
   2811                c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
   2812         except LookupError:
   2813             # We probably don't have the Japanese codecs installed
   2814             pass
   2815         # Testing SF bug #625509, which we have to fake, since there are no
   2816         # built-in encodings where the header encoding is QP but the body
   2817         # encoding is not.
   2818         from email import charset as CharsetModule
   2819         CharsetModule.add_charset('fake', CharsetModule.QP, None)
   2820         c = Charset('fake')
   2821         eq('hello w\xf6rld', c.body_encode('hello w\xf6rld'))
   2822 
   2823     def test_unicode_charset_name(self):
   2824         charset = Charset(u'us-ascii')
   2825         self.assertEqual(str(charset), 'us-ascii')
   2826         self.assertRaises(errors.CharsetError, Charset, 'asc\xffii')
   2827 
   2828 
   2829 
   2830 # Test multilingual MIME headers.
   2831 class TestHeader(TestEmailBase):
   2832     def test_simple(self):
   2833         eq = self.ndiffAssertEqual
   2834         h = Header('Hello World!')
   2835         eq(h.encode(), 'Hello World!')
   2836         h.append(' Goodbye World!')
   2837         eq(h.encode(), 'Hello World!  Goodbye World!')
   2838 
   2839     def test_simple_surprise(self):
   2840         eq = self.ndiffAssertEqual
   2841         h = Header('Hello World!')
   2842         eq(h.encode(), 'Hello World!')
   2843         h.append('Goodbye World!')
   2844         eq(h.encode(), 'Hello World! Goodbye World!')
   2845 
   2846     def test_header_needs_no_decoding(self):
   2847         h = 'no decoding needed'
   2848         self.assertEqual(decode_header(h), [(h, None)])
   2849 
   2850     def test_long(self):
   2851         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.",
   2852                    maxlinelen=76)
   2853         for l in h.encode(splitchars=' ').split('\n '):
   2854             self.assertLessEqual(len(l), 76)
   2855 
   2856     def test_multilingual(self):
   2857         eq = self.ndiffAssertEqual
   2858         g = Charset("iso-8859-1")
   2859         cz = Charset("iso-8859-2")
   2860         utf8 = Charset("utf-8")
   2861         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. "
   2862         cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. "
   2863         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")
   2864         h = Header(g_head, g)
   2865         h.append(cz_head, cz)
   2866         h.append(utf8_head, utf8)
   2867         enc = h.encode()
   2868         eq(enc, """\
   2869 =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_ko?=
   2870  =?iso-8859-1?q?mfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wan?=
   2871  =?iso-8859-1?q?dgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6?=
   2872  =?iso-8859-1?q?rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
   2873  =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
   2874  =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
   2875  =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
   2876  =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
   2877  =?utf-8?q?_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das_Oder_die_Fl?=
   2878  =?utf-8?b?aXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBo+OBpuOBhOOBvuOBmQ==?=
   2879  =?utf-8?b?44CC?=""")
   2880         eq(decode_header(enc),
   2881            [(g_head, "iso-8859-1"), (cz_head, "iso-8859-2"),
   2882             (utf8_head, "utf-8")])
   2883         ustr = unicode(h)
   2884         eq(ustr.encode('utf-8'),
   2885            'Die Mieter treten hier ein werden mit einem Foerderband '
   2886            'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
   2887            'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
   2888            'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
   2889            'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
   2890            '\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
   2891            '\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
   2892            '\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
   2893            '\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
   2894            '\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
   2895            '\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
   2896            '\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
   2897            '\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
   2898            'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
   2899            'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
   2900            '\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82')
   2901         # Test make_header()
   2902         newh = make_header(decode_header(enc))
   2903         eq(newh, enc)
   2904 
   2905     def test_header_ctor_default_args(self):
   2906         eq = self.ndiffAssertEqual
   2907         h = Header()
   2908         eq(h, '')
   2909         h.append('foo', Charset('iso-8859-1'))
   2910         eq(h, '=?iso-8859-1?q?foo?=')
   2911 
   2912     def test_explicit_maxlinelen(self):
   2913         eq = self.ndiffAssertEqual
   2914         hstr = 'A very long line that must get split to something other than at the 76th character boundary to test the non-default behavior'
   2915         h = Header(hstr)
   2916         eq(h.encode(), '''\
   2917 A very long line that must get split to something other than at the 76th
   2918  character boundary to test the non-default behavior''')
   2919         h = Header(hstr, header_name='Subject')
   2920         eq(h.encode(), '''\
   2921 A very long line that must get split to something other than at the
   2922  76th character boundary to test the non-default behavior''')
   2923         h = Header(hstr, maxlinelen=1024, header_name='Subject')
   2924         eq(h.encode(), hstr)
   2925 
   2926     def test_us_ascii_header(self):
   2927         eq = self.assertEqual
   2928         s = 'hello'
   2929         x = decode_header(s)
   2930         eq(x, [('hello', None)])
   2931         h = make_header(x)
   2932         eq(s, h.encode())
   2933 
   2934     def test_string_charset(self):
   2935         eq = self.assertEqual
   2936         h = Header()
   2937         h.append('hello', 'iso-8859-1')
   2938         eq(h, '=?iso-8859-1?q?hello?=')
   2939 
   2940 ##    def test_unicode_error(self):
   2941 ##        raises = self.assertRaises
   2942 ##        raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')
   2943 ##        raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')
   2944 ##        h = Header()
   2945 ##        raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')
   2946 ##        raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')
   2947 ##        raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')
   2948 
   2949     def test_utf8_shortest(self):
   2950         eq = self.assertEqual
   2951         h = Header(u'p\xf6stal', 'utf-8')
   2952         eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
   2953         h = Header(u'\u83ca\u5730\u6642\u592b', 'utf-8')
   2954         eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
   2955 
   2956     def test_bad_8bit_header(self):
   2957         raises = self.assertRaises
   2958         eq = self.assertEqual
   2959         x = 'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
   2960         raises(UnicodeError, Header, x)
   2961         h = Header()
   2962         raises(UnicodeError, h.append, x)
   2963         eq(str(Header(x, errors='replace')), x)
   2964         h.append(x, errors='replace')
   2965         eq(str(h), x)
   2966 
   2967     def test_encoded_adjacent_nonencoded(self):
   2968         eq = self.assertEqual
   2969         h = Header()
   2970         h.append('hello', 'iso-8859-1')
   2971         h.append('world')
   2972         s = h.encode()
   2973         eq(s, '=?iso-8859-1?q?hello?= world')
   2974         h = make_header(decode_header(s))
   2975         eq(h.encode(), s)
   2976 
   2977     def test_whitespace_eater(self):
   2978         eq = self.assertEqual
   2979         s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
   2980         parts = decode_header(s)
   2981         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)])
   2982         hdr = make_header(parts)
   2983         eq(hdr.encode(),
   2984            'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
   2985 
   2986     def test_broken_base64_header(self):
   2987         raises = self.assertRaises
   2988         s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3I ?='
   2989         raises(errors.HeaderParseError, decode_header, s)
   2990 
   2991 
   2992 
   2993 # Test RFC 2231 header parameters (en/de)coding
   2994 class TestRFC2231(TestEmailBase):
   2995     def test_get_param(self):
   2996         eq = self.assertEqual
   2997         msg = self._msgobj('msg_29.txt')
   2998         eq(msg.get_param('title'),
   2999            ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
   3000         eq(msg.get_param('title', unquote=False),
   3001            ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
   3002 
   3003     def test_set_param(self):
   3004         eq = self.assertEqual
   3005         msg = Message()
   3006         msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
   3007                       charset='us-ascii')
   3008         eq(msg.get_param('title'),
   3009            ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
   3010         msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
   3011                       charset='us-ascii', language='en')
   3012         eq(msg.get_param('title'),
   3013            ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
   3014         msg = self._msgobj('msg_01.txt')
   3015         msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
   3016                       charset='us-ascii', language='en')
   3017         self.ndiffAssertEqual(msg.as_string(), """\
   3018 Return-Path: <bbb (at] zzz.org>
   3019 Delivered-To: bbb (at] zzz.org
   3020 Received: by mail.zzz.org (Postfix, from userid 889)
   3021  id 27CEAD38CC; Fri,  4 May 2001 14:05:44 -0400 (EDT)
   3022 MIME-Version: 1.0
   3023 Content-Transfer-Encoding: 7bit
   3024 Message-ID: <15090.61304.110929.45684 (at] aaa.zzz.org>
   3025 From: bbb (at] ddd.com (John X. Doe)
   3026 To: bbb (at] zzz.org
   3027 Subject: This is a test message
   3028 Date: Fri, 4 May 2001 14:05:44 -0400
   3029 Content-Type: text/plain; charset=us-ascii;
   3030  title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
   3031 
   3032 
   3033 Hi,
   3034 
   3035 Do you like this message?
   3036 
   3037 -Me
   3038 """)
   3039 
   3040     def test_del_param(self):
   3041         eq = self.ndiffAssertEqual
   3042         msg = self._msgobj('msg_01.txt')
   3043         msg.set_param('foo', 'bar', charset='us-ascii', language='en')
   3044         msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
   3045             charset='us-ascii', language='en')
   3046         msg.del_param('foo', header='Content-Type')
   3047         eq(msg.as_string(), """\
   3048 Return-Path: <bbb (at] zzz.org>
   3049 Delivered-To: bbb (at] zzz.org
   3050 Received: by mail.zzz.org (Postfix, from userid 889)
   3051  id 27CEAD38CC; Fri,  4 May 2001 14:05:44 -0400 (EDT)
   3052 MIME-Version: 1.0
   3053 Content-Transfer-Encoding: 7bit
   3054 Message-ID: <15090.61304.110929.45684 (at] aaa.zzz.org>
   3055 From: bbb (at] ddd.com (John X. Doe)
   3056 To: bbb (at] zzz.org
   3057 Subject: This is a test message
   3058 Date: Fri, 4 May 2001 14:05:44 -0400
   3059 Content-Type: text/plain; charset="us-ascii";
   3060  title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
   3061 
   3062 
   3063 Hi,
   3064 
   3065 Do you like this message?
   3066 
   3067 -Me
   3068 """)
   3069 
   3070     def test_rfc2231_get_content_charset(self):
   3071         eq = self.assertEqual
   3072         msg = self._msgobj('msg_32.txt')
   3073         eq(msg.get_content_charset(), 'us-ascii')
   3074 
   3075     def test_rfc2231_no_language_or_charset(self):
   3076         m = '''\
   3077 Content-Transfer-Encoding: 8bit
   3078 Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
   3079 Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
   3080 
   3081 '''
   3082         msg = email.message_from_string(m)
   3083         param = msg.get_param('NAME')
   3084         self.assertFalse(isinstance(param, tuple))
   3085         self.assertEqual(
   3086             param,
   3087             'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
   3088 
   3089     def test_rfc2231_no_language_or_charset_in_filename(self):
   3090         m = '''\
   3091 Content-Disposition: inline;
   3092 \tfilename*0*="''This%20is%20even%20more%20";
   3093 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3094 \tfilename*2="is it not.pdf"
   3095 
   3096 '''
   3097         msg = email.message_from_string(m)
   3098         self.assertEqual(msg.get_filename(),
   3099                          'This is even more ***fun*** is it not.pdf')
   3100 
   3101     def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
   3102         m = '''\
   3103 Content-Disposition: inline;
   3104 \tfilename*0*="''This%20is%20even%20more%20";
   3105 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3106 \tfilename*2="is it not.pdf"
   3107 
   3108 '''
   3109         msg = email.message_from_string(m)
   3110         self.assertEqual(msg.get_filename(),
   3111                          'This is even more ***fun*** is it not.pdf')
   3112 
   3113     def test_rfc2231_partly_encoded(self):
   3114         m = '''\
   3115 Content-Disposition: inline;
   3116 \tfilename*0="''This%20is%20even%20more%20";
   3117 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3118 \tfilename*2="is it not.pdf"
   3119 
   3120 '''
   3121         msg = email.message_from_string(m)
   3122         self.assertEqual(
   3123             msg.get_filename(),
   3124             'This%20is%20even%20more%20***fun*** is it not.pdf')
   3125 
   3126     def test_rfc2231_partly_nonencoded(self):
   3127         m = '''\
   3128 Content-Disposition: inline;
   3129 \tfilename*0="This%20is%20even%20more%20";
   3130 \tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
   3131 \tfilename*2="is it not.pdf"
   3132 
   3133 '''
   3134         msg = email.message_from_string(m)
   3135         self.assertEqual(
   3136             msg.get_filename(),
   3137             'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
   3138 
   3139     def test_rfc2231_no_language_or_charset_in_boundary(self):
   3140         m = '''\
   3141 Content-Type: multipart/alternative;
   3142 \tboundary*0*="''This%20is%20even%20more%20";
   3143 \tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3144 \tboundary*2="is it not.pdf"
   3145 
   3146 '''
   3147         msg = email.message_from_string(m)
   3148         self.assertEqual(msg.get_boundary(),
   3149                          'This is even more ***fun*** is it not.pdf')
   3150 
   3151     def test_rfc2231_no_language_or_charset_in_charset(self):
   3152         # This is a nonsensical charset value, but tests the code anyway
   3153         m = '''\
   3154 Content-Type: text/plain;
   3155 \tcharset*0*="This%20is%20even%20more%20";
   3156 \tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3157 \tcharset*2="is it not.pdf"
   3158 
   3159 '''
   3160         msg = email.message_from_string(m)
   3161         self.assertEqual(msg.get_content_charset(),
   3162                          'this is even more ***fun*** is it not.pdf')
   3163 
   3164     def test_rfc2231_bad_encoding_in_filename(self):
   3165         m = '''\
   3166 Content-Disposition: inline;
   3167 \tfilename*0*="bogus'xx'This%20is%20even%20more%20";
   3168 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3169 \tfilename*2="is it not.pdf"
   3170 
   3171 '''
   3172         msg = email.message_from_string(m)
   3173         self.assertEqual(msg.get_filename(),
   3174                          'This is even more ***fun*** is it not.pdf')
   3175 
   3176     def test_rfc2231_bad_encoding_in_charset(self):
   3177         m = """\
   3178 Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
   3179 
   3180 """
   3181         msg = email.message_from_string(m)
   3182         # This should return None because non-ascii characters in the charset
   3183         # are not allowed.
   3184         self.assertEqual(msg.get_content_charset(), None)
   3185 
   3186     def test_rfc2231_bad_character_in_charset(self):
   3187         m = """\
   3188 Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D
   3189 
   3190 """
   3191         msg = email.message_from_string(m)
   3192         # This should return None because non-ascii characters in the charset
   3193         # are not allowed.
   3194         self.assertEqual(msg.get_content_charset(), None)
   3195 
   3196     def test_rfc2231_bad_character_in_filename(self):
   3197         m = '''\
   3198 Content-Disposition: inline;
   3199 \tfilename*0*="ascii'xx'This%20is%20even%20more%20";
   3200 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3201 \tfilename*2*="is it not.pdf%E2"
   3202 
   3203 '''
   3204         msg = email.message_from_string(m)
   3205         self.assertEqual(msg.get_filename(),
   3206                          u'This is even more ***fun*** is it not.pdf\ufffd')
   3207 
   3208     def test_rfc2231_unknown_encoding(self):
   3209         m = """\
   3210 Content-Transfer-Encoding: 8bit
   3211 Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
   3212 
   3213 """
   3214         msg = email.message_from_string(m)
   3215         self.assertEqual(msg.get_filename(), 'myfile.txt')
   3216 
   3217     def test_rfc2231_single_tick_in_filename_extended(self):
   3218         eq = self.assertEqual
   3219         m = """\
   3220 Content-Type: application/x-foo;
   3221 \tname*0*=\"Frank's\"; name*1*=\" Document\"
   3222 
   3223 """
   3224         msg = email.message_from_string(m)
   3225         charset, language, s = msg.get_param('name')
   3226         eq(charset, None)
   3227         eq(language, None)
   3228         eq(s, "Frank's Document")
   3229 
   3230     def test_rfc2231_single_tick_in_filename(self):
   3231         m = """\
   3232 Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
   3233 
   3234 """
   3235         msg = email.message_from_string(m)
   3236         param = msg.get_param('name')
   3237         self.assertFalse(isinstance(param, tuple))
   3238         self.assertEqual(param, "Frank's Document")
   3239 
   3240     def test_rfc2231_tick_attack_extended(self):
   3241         eq = self.assertEqual
   3242         m = """\
   3243 Content-Type: application/x-foo;
   3244 \tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
   3245 
   3246 """
   3247         msg = email.message_from_string(m)
   3248         charset, language, s = msg.get_param('name')
   3249         eq(charset, 'us-ascii')
   3250         eq(language, 'en-us')
   3251         eq(s, "Frank's Document")
   3252 
   3253     def test_rfc2231_tick_attack(self):
   3254         m = """\
   3255 Content-Type: application/x-foo;
   3256 \tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
   3257 
   3258 """
   3259         msg = email.message_from_string(m)
   3260         param = msg.get_param('name')
   3261         self.assertFalse(isinstance(param, tuple))
   3262         self.assertEqual(param, "us-ascii'en-us'Frank's Document")
   3263 
   3264     def test_rfc2231_no_extended_values(self):
   3265         eq = self.assertEqual
   3266         m = """\
   3267 Content-Type: application/x-foo; name=\"Frank's Document\"
   3268 
   3269 """
   3270         msg = email.message_from_string(m)
   3271         eq(msg.get_param('name'), "Frank's Document")
   3272 
   3273     def test_rfc2231_encoded_then_unencoded_segments(self):
   3274         eq = self.assertEqual
   3275         m = """\
   3276 Content-Type: application/x-foo;
   3277 \tname*0*=\"us-ascii'en-us'My\";
   3278 \tname*1=\" Document\";
   3279 \tname*2*=\" For You\"
   3280 
   3281 """
   3282         msg = email.message_from_string(m)
   3283         charset, language, s = msg.get_param('name')
   3284         eq(charset, 'us-ascii')
   3285         eq(language, 'en-us')
   3286         eq(s, 'My Document For You')
   3287 
   3288     def test_rfc2231_unencoded_then_encoded_segments(self):
   3289         eq = self.assertEqual
   3290         m = """\
   3291 Content-Type: application/x-foo;
   3292 \tname*0=\"us-ascii'en-us'My\";
   3293 \tname*1*=\" Document\";
   3294 \tname*2*=\" For You\"
   3295 
   3296 """
   3297         msg = email.message_from_string(m)
   3298         charset, language, s = msg.get_param('name')
   3299         eq(charset, 'us-ascii')
   3300         eq(language, 'en-us')
   3301         eq(s, 'My Document For You')
   3302 
   3303 
   3304 
   3305 def _testclasses():
   3306     mod = sys.modules[__name__]
   3307     return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
   3308 
   3309 
   3310 def suite():
   3311     suite = unittest.TestSuite()
   3312     for testclass in _testclasses():
   3313         suite.addTest(unittest.makeSuite(testclass))
   3314     return suite
   3315 
   3316 
   3317 def test_main():
   3318     for testclass in _testclasses():
   3319         run_unittest(testclass)
   3320 
   3321 
   3322 
   3323 if __name__ == '__main__':
   3324     unittest.main(defaultTest='suite')
   3325