Home | History | Annotate | Download | only in test
      1 # Copyright (C) 2001-2010 Python Software Foundation

      2 # Contact: email-sig (at] python.org

      3 # email package unit tests

      4 
      5 import os
      6 import sys
      7 import time
      8 import base64
      9 import difflib
     10 import unittest
     11 import warnings
     12 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.MIMEAudio import MIMEAudio
     22 from email.MIMEText import MIMEText
     23 from email.MIMEImage import MIMEImage
     24 from email.MIMEBase import MIMEBase
     25 from email.MIMEMessage import MIMEMessage
     26 from email.MIMEMultipart import MIMEMultipart
     27 from email import Utils
     28 from email import Errors
     29 from email import Encoders
     30 from email import Iterators
     31 from email import base64MIME
     32 from email import quopriMIME
     33 
     34 from test.test_support import findfile, run_unittest
     35 from email.test import __file__ as landmark
     36 
     37 
     38 NL = '\n'
     39 EMPTYSTRING = ''
     40 SPACE = ' '
     41 
     42 
     43 
     44 def openfile(filename, mode='r'):
     45     path = os.path.join(os.path.dirname(landmark), 'data', filename)
     46     return open(path, mode)
     47 
     48 
     49 
     50 # Base test class

     51 class TestEmailBase(unittest.TestCase):
     52     def ndiffAssertEqual(self, first, second):
     53         """Like assertEqual except use ndiff for readable output."""
     54         if first != second:
     55             sfirst = str(first)
     56             ssecond = str(second)
     57             diff = difflib.ndiff(sfirst.splitlines(), ssecond.splitlines())
     58             fp = StringIO()
     59             print >> fp, NL, NL.join(diff)
     60             raise self.failureException, fp.getvalue()
     61 
     62     def _msgobj(self, filename):
     63         fp = openfile(findfile(filename))
     64         try:
     65             msg = email.message_from_file(fp)
     66         finally:
     67             fp.close()
     68         return msg
     69 
     70 
     71 
     72 # Test various aspects of the Message class's API

     73 class TestMessageAPI(TestEmailBase):
     74     def test_get_all(self):
     75         eq = self.assertEqual
     76         msg = self._msgobj('msg_20.txt')
     77         eq(msg.get_all('cc'), ['ccc (at] zzz.org', 'ddd (at] zzz.org', 'eee (at] zzz.org'])
     78         eq(msg.get_all('xx', 'n/a'), 'n/a')
     79 
     80     def test_getset_charset(self):
     81         eq = self.assertEqual
     82         msg = Message()
     83         eq(msg.get_charset(), None)
     84         charset = Charset('iso-8859-1')
     85         msg.set_charset(charset)
     86         eq(msg['mime-version'], '1.0')
     87         eq(msg.get_content_type(), 'text/plain')
     88         eq(msg['content-type'], 'text/plain; charset="iso-8859-1"')
     89         eq(msg.get_param('charset'), 'iso-8859-1')
     90         eq(msg['content-transfer-encoding'], 'quoted-printable')
     91         eq(msg.get_charset().input_charset, 'iso-8859-1')
     92         # Remove the charset

     93         msg.set_charset(None)
     94         eq(msg.get_charset(), None)
     95         eq(msg['content-type'], 'text/plain')
     96         # Try adding a charset when there's already MIME headers present

     97         msg = Message()
     98         msg['MIME-Version'] = '2.0'
     99         msg['Content-Type'] = 'text/x-weird'
    100         msg['Content-Transfer-Encoding'] = 'quinted-puntable'
    101         msg.set_charset(charset)
    102         eq(msg['mime-version'], '2.0')
    103         eq(msg['content-type'], 'text/x-weird; charset="iso-8859-1"')
    104         eq(msg['content-transfer-encoding'], 'quinted-puntable')
    105 
    106     def test_set_charset_from_string(self):
    107         eq = self.assertEqual
    108         msg = Message()
    109         msg.set_charset('us-ascii')
    110         eq(msg.get_charset().input_charset, 'us-ascii')
    111         eq(msg['content-type'], 'text/plain; charset="us-ascii"')
    112 
    113     def test_set_payload_with_charset(self):
    114         msg = Message()
    115         charset = Charset('iso-8859-1')
    116         msg.set_payload('This is a string payload', charset)
    117         self.assertEqual(msg.get_charset().input_charset, 'iso-8859-1')
    118 
    119     def test_get_charsets(self):
    120         eq = self.assertEqual
    121 
    122         msg = self._msgobj('msg_08.txt')
    123         charsets = msg.get_charsets()
    124         eq(charsets, [None, 'us-ascii', 'iso-8859-1', 'iso-8859-2', 'koi8-r'])
    125 
    126         msg = self._msgobj('msg_09.txt')
    127         charsets = msg.get_charsets('dingbat')
    128         eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat',
    129                       'koi8-r'])
    130 
    131         msg = self._msgobj('msg_12.txt')
    132         charsets = msg.get_charsets()
    133         eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2',
    134                       'iso-8859-3', 'us-ascii', 'koi8-r'])
    135 
    136     def test_get_filename(self):
    137         eq = self.assertEqual
    138 
    139         msg = self._msgobj('msg_04.txt')
    140         filenames = [p.get_filename() for p in msg.get_payload()]
    141         eq(filenames, ['msg.txt', 'msg.txt'])
    142 
    143         msg = self._msgobj('msg_07.txt')
    144         subpart = msg.get_payload(1)
    145         eq(subpart.get_filename(), 'dingusfish.gif')
    146 
    147     def test_get_filename_with_name_parameter(self):
    148         eq = self.assertEqual
    149 
    150         msg = self._msgobj('msg_44.txt')
    151         filenames = [p.get_filename() for p in msg.get_payload()]
    152         eq(filenames, ['msg.txt', 'msg.txt'])
    153 
    154     def test_get_boundary(self):
    155         eq = self.assertEqual
    156         msg = self._msgobj('msg_07.txt')
    157         # No quotes!

    158         eq(msg.get_boundary(), 'BOUNDARY')
    159 
    160     def test_set_boundary(self):
    161         eq = self.assertEqual
    162         # This one has no existing boundary parameter, but the Content-Type:

    163         # header appears fifth.

    164         msg = self._msgobj('msg_01.txt')
    165         msg.set_boundary('BOUNDARY')
    166         header, value = msg.items()[4]
    167         eq(header.lower(), 'content-type')
    168         eq(value, 'text/plain; charset="us-ascii"; boundary="BOUNDARY"')
    169         # This one has a Content-Type: header, with a boundary, stuck in the

    170         # middle of its headers.  Make sure the order is preserved; it should

    171         # be fifth.

    172         msg = self._msgobj('msg_04.txt')
    173         msg.set_boundary('BOUNDARY')
    174         header, value = msg.items()[4]
    175         eq(header.lower(), 'content-type')
    176         eq(value, 'multipart/mixed; boundary="BOUNDARY"')
    177         # And this one has no Content-Type: header at all.

    178         msg = self._msgobj('msg_03.txt')
    179         self.assertRaises(Errors.HeaderParseError,
    180                           msg.set_boundary, 'BOUNDARY')
    181 
    182     def test_make_boundary(self):
    183         msg = MIMEMultipart('form-data')
    184         # Note that when the boundary gets created is an implementation

    185         # detail and might change.

    186         self.assertEqual(msg.items()[0][1], 'multipart/form-data')
    187         # Trigger creation of boundary

    188         msg.as_string()
    189         self.assertEqual(msg.items()[0][1][:33],
    190                         'multipart/form-data; boundary="==')
    191         # XXX: there ought to be tests of the uniqueness of the boundary, too.

    192 
    193     def test_message_rfc822_only(self):
    194         # Issue 7970: message/rfc822 not in multipart parsed by

    195         # HeaderParser caused an exception when flattened.

    196         fp = openfile(findfile('msg_46.txt'))
    197         msgdata = fp.read()
    198         parser = email.Parser.HeaderParser()
    199         msg = parser.parsestr(msgdata)
    200         out = StringIO()
    201         gen = email.Generator.Generator(out, True, 0)
    202         gen.flatten(msg, False)
    203         self.assertEqual(out.getvalue(), msgdata)
    204 
    205     def test_get_decoded_payload(self):
    206         eq = self.assertEqual
    207         msg = self._msgobj('msg_10.txt')
    208         # The outer message is a multipart

    209         eq(msg.get_payload(decode=True), None)
    210         # Subpart 1 is 7bit encoded

    211         eq(msg.get_payload(0).get_payload(decode=True),
    212            'This is a 7bit encoded message.\n')
    213         # Subpart 2 is quopri

    214         eq(msg.get_payload(1).get_payload(decode=True),
    215            '\xa1This is a Quoted Printable encoded message!\n')
    216         # Subpart 3 is base64

    217         eq(msg.get_payload(2).get_payload(decode=True),
    218            'This is a Base64 encoded message.')
    219         # Subpart 4 is base64 with a trailing newline, which

    220         # used to be stripped (issue 7143).

    221         eq(msg.get_payload(3).get_payload(decode=True),
    222            'This is a Base64 encoded message.\n')
    223         # Subpart 5 has no Content-Transfer-Encoding: header.

    224         eq(msg.get_payload(4).get_payload(decode=True),
    225            'This has no Content-Transfer-Encoding: header.\n')
    226 
    227     def test_get_decoded_uu_payload(self):
    228         eq = self.assertEqual
    229         msg = Message()
    230         msg.set_payload('begin 666 -\n+:&5L;&\\@=V]R;&0 \n \nend\n')
    231         for cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):
    232             msg['content-transfer-encoding'] = cte
    233             eq(msg.get_payload(decode=True), 'hello world')
    234         # Now try some bogus data

    235         msg.set_payload('foo')
    236         eq(msg.get_payload(decode=True), 'foo')
    237 
    238     def test_decode_bogus_uu_payload_quietly(self):
    239         msg = Message()
    240         msg.set_payload('begin 664 foo.txt\n%<W1F=0000H \n \nend\n')
    241         msg['Content-Transfer-Encoding'] = 'x-uuencode'
    242         old_stderr = sys.stderr
    243         try:
    244             sys.stderr = sfp = StringIO()
    245             # We don't care about the payload

    246             msg.get_payload(decode=True)
    247         finally:
    248             sys.stderr = old_stderr
    249         self.assertEqual(sfp.getvalue(), '')
    250 
    251     def test_decoded_generator(self):
    252         eq = self.assertEqual
    253         msg = self._msgobj('msg_07.txt')
    254         fp = openfile('msg_17.txt')
    255         try:
    256             text = fp.read()
    257         finally:
    258             fp.close()
    259         s = StringIO()
    260         g = DecodedGenerator(s)
    261         g.flatten(msg)
    262         eq(s.getvalue(), text)
    263 
    264     def test__contains__(self):
    265         msg = Message()
    266         msg['From'] = 'Me'
    267         msg['to'] = 'You'
    268         # Check for case insensitivity

    269         self.assertTrue('from' in msg)
    270         self.assertTrue('From' in msg)
    271         self.assertTrue('FROM' in msg)
    272         self.assertTrue('to' in msg)
    273         self.assertTrue('To' in msg)
    274         self.assertTrue('TO' in msg)
    275 
    276     def test_as_string(self):
    277         eq = self.assertEqual
    278         msg = self._msgobj('msg_01.txt')
    279         fp = openfile('msg_01.txt')
    280         try:
    281             # BAW 30-Mar-2009 Evil be here.  So, the generator is broken with

    282             # respect to long line breaking.  It's also not idempotent when a

    283             # header from a parsed message is continued with tabs rather than

    284             # spaces.  Before we fixed bug 1974 it was reversedly broken,

    285             # i.e. headers that were continued with spaces got continued with

    286             # tabs.  For Python 2.x there's really no good fix and in Python

    287             # 3.x all this stuff is re-written to be right(er).  Chris Withers

    288             # convinced me that using space as the default continuation

    289             # character is less bad for more applications.

    290             text = fp.read().replace('\t', ' ')
    291         finally:
    292             fp.close()
    293         eq(text, msg.as_string())
    294         fullrepr = str(msg)
    295         lines = fullrepr.split('\n')
    296         self.assertTrue(lines[0].startswith('From '))
    297         eq(text, NL.join(lines[1:]))
    298 
    299     def test_bad_param(self):
    300         msg = email.message_from_string("Content-Type: blarg; baz; boo\n")
    301         self.assertEqual(msg.get_param('baz'), '')
    302 
    303     def test_missing_filename(self):
    304         msg = email.message_from_string("From: foo\n")
    305         self.assertEqual(msg.get_filename(), None)
    306 
    307     def test_bogus_filename(self):
    308         msg = email.message_from_string(
    309         "Content-Disposition: blarg; filename\n")
    310         self.assertEqual(msg.get_filename(), '')
    311 
    312     def test_missing_boundary(self):
    313         msg = email.message_from_string("From: foo\n")
    314         self.assertEqual(msg.get_boundary(), None)
    315 
    316     def test_get_params(self):
    317         eq = self.assertEqual
    318         msg = email.message_from_string(
    319             'X-Header: foo=one; bar=two; baz=three\n')
    320         eq(msg.get_params(header='x-header'),
    321            [('foo', 'one'), ('bar', 'two'), ('baz', 'three')])
    322         msg = email.message_from_string(
    323             'X-Header: foo; bar=one; baz=two\n')
    324         eq(msg.get_params(header='x-header'),
    325            [('foo', ''), ('bar', 'one'), ('baz', 'two')])
    326         eq(msg.get_params(), None)
    327         msg = email.message_from_string(
    328             'X-Header: foo; bar="one"; baz=two\n')
    329         eq(msg.get_params(header='x-header'),
    330            [('foo', ''), ('bar', 'one'), ('baz', 'two')])
    331 
    332     def test_get_param_liberal(self):
    333         msg = Message()
    334         msg['Content-Type'] = 'Content-Type: Multipart/mixed; boundary = "CPIMSSMTPC06p5f3tG"'
    335         self.assertEqual(msg.get_param('boundary'), 'CPIMSSMTPC06p5f3tG')
    336 
    337     def test_get_param(self):
    338         eq = self.assertEqual
    339         msg = email.message_from_string(
    340             "X-Header: foo=one; bar=two; baz=three\n")
    341         eq(msg.get_param('bar', header='x-header'), 'two')
    342         eq(msg.get_param('quuz', header='x-header'), None)
    343         eq(msg.get_param('quuz'), None)
    344         msg = email.message_from_string(
    345             'X-Header: foo; bar="one"; baz=two\n')
    346         eq(msg.get_param('foo', header='x-header'), '')
    347         eq(msg.get_param('bar', header='x-header'), 'one')
    348         eq(msg.get_param('baz', header='x-header'), 'two')
    349         # XXX: We are not RFC-2045 compliant!  We cannot parse:

    350         # msg["Content-Type"] = 'text/plain; weird="hey; dolly? [you] @ <\\"home\\">?"'

    351         # msg.get_param("weird")

    352         # yet.

    353 
    354     def test_get_param_funky_continuation_lines(self):
    355         msg = self._msgobj('msg_22.txt')
    356         self.assertEqual(msg.get_payload(1).get_param('name'), 'wibble.JPG')
    357 
    358     def test_get_param_with_semis_in_quotes(self):
    359         msg = email.message_from_string(
    360             'Content-Type: image/pjpeg; name="Jim&amp;&amp;Jill"\n')
    361         self.assertEqual(msg.get_param('name'), 'Jim&amp;&amp;Jill')
    362         self.assertEqual(msg.get_param('name', unquote=False),
    363                          '"Jim&amp;&amp;Jill"')
    364 
    365     def test_get_param_with_quotes(self):
    366         msg = email.message_from_string(
    367             'Content-Type: foo; bar*0="baz\\"foobar"; bar*1="\\"baz"')
    368         self.assertEqual(msg.get_param('bar'), 'baz"foobar"baz')
    369         msg = email.message_from_string(
    370             "Content-Type: foo; bar*0=\"baz\\\"foobar\"; bar*1=\"\\\"baz\"")
    371         self.assertEqual(msg.get_param('bar'), 'baz"foobar"baz')
    372 
    373     def test_has_key(self):
    374         msg = email.message_from_string('Header: exists')
    375         self.assertTrue(msg.has_key('header'))
    376         self.assertTrue(msg.has_key('Header'))
    377         self.assertTrue(msg.has_key('HEADER'))
    378         self.assertFalse(msg.has_key('headeri'))
    379 
    380     def test_set_param(self):
    381         eq = self.assertEqual
    382         msg = Message()
    383         msg.set_param('charset', 'iso-2022-jp')
    384         eq(msg.get_param('charset'), 'iso-2022-jp')
    385         msg.set_param('importance', 'high value')
    386         eq(msg.get_param('importance'), 'high value')
    387         eq(msg.get_param('importance', unquote=False), '"high value"')
    388         eq(msg.get_params(), [('text/plain', ''),
    389                               ('charset', 'iso-2022-jp'),
    390                               ('importance', 'high value')])
    391         eq(msg.get_params(unquote=False), [('text/plain', ''),
    392                                        ('charset', '"iso-2022-jp"'),
    393                                        ('importance', '"high value"')])
    394         msg.set_param('charset', 'iso-9999-xx', header='X-Jimmy')
    395         eq(msg.get_param('charset', header='X-Jimmy'), 'iso-9999-xx')
    396 
    397     def test_del_param(self):
    398         eq = self.assertEqual
    399         msg = self._msgobj('msg_05.txt')
    400         eq(msg.get_params(),
    401            [('multipart/report', ''), ('report-type', 'delivery-status'),
    402             ('boundary', 'D1690A7AC1.996856090/mail.example.com')])
    403         old_val = msg.get_param("report-type")
    404         msg.del_param("report-type")
    405         eq(msg.get_params(),
    406            [('multipart/report', ''),
    407             ('boundary', 'D1690A7AC1.996856090/mail.example.com')])
    408         msg.set_param("report-type", old_val)
    409         eq(msg.get_params(),
    410            [('multipart/report', ''),
    411             ('boundary', 'D1690A7AC1.996856090/mail.example.com'),
    412             ('report-type', old_val)])
    413 
    414     def test_del_param_on_other_header(self):
    415         msg = Message()
    416         msg.add_header('Content-Disposition', 'attachment', filename='bud.gif')
    417         msg.del_param('filename', 'content-disposition')
    418         self.assertEqual(msg['content-disposition'], 'attachment')
    419 
    420     def test_set_type(self):
    421         eq = self.assertEqual
    422         msg = Message()
    423         self.assertRaises(ValueError, msg.set_type, 'text')
    424         msg.set_type('text/plain')
    425         eq(msg['content-type'], 'text/plain')
    426         msg.set_param('charset', 'us-ascii')
    427         eq(msg['content-type'], 'text/plain; charset="us-ascii"')
    428         msg.set_type('text/html')
    429         eq(msg['content-type'], 'text/html; charset="us-ascii"')
    430 
    431     def test_set_type_on_other_header(self):
    432         msg = Message()
    433         msg['X-Content-Type'] = 'text/plain'
    434         msg.set_type('application/octet-stream', 'X-Content-Type')
    435         self.assertEqual(msg['x-content-type'], 'application/octet-stream')
    436 
    437     def test_get_content_type_missing(self):
    438         msg = Message()
    439         self.assertEqual(msg.get_content_type(), 'text/plain')
    440 
    441     def test_get_content_type_missing_with_default_type(self):
    442         msg = Message()
    443         msg.set_default_type('message/rfc822')
    444         self.assertEqual(msg.get_content_type(), 'message/rfc822')
    445 
    446     def test_get_content_type_from_message_implicit(self):
    447         msg = self._msgobj('msg_30.txt')
    448         self.assertEqual(msg.get_payload(0).get_content_type(),
    449                          'message/rfc822')
    450 
    451     def test_get_content_type_from_message_explicit(self):
    452         msg = self._msgobj('msg_28.txt')
    453         self.assertEqual(msg.get_payload(0).get_content_type(),
    454                          'message/rfc822')
    455 
    456     def test_get_content_type_from_message_text_plain_implicit(self):
    457         msg = self._msgobj('msg_03.txt')
    458         self.assertEqual(msg.get_content_type(), 'text/plain')
    459 
    460     def test_get_content_type_from_message_text_plain_explicit(self):
    461         msg = self._msgobj('msg_01.txt')
    462         self.assertEqual(msg.get_content_type(), 'text/plain')
    463 
    464     def test_get_content_maintype_missing(self):
    465         msg = Message()
    466         self.assertEqual(msg.get_content_maintype(), 'text')
    467 
    468     def test_get_content_maintype_missing_with_default_type(self):
    469         msg = Message()
    470         msg.set_default_type('message/rfc822')
    471         self.assertEqual(msg.get_content_maintype(), 'message')
    472 
    473     def test_get_content_maintype_from_message_implicit(self):
    474         msg = self._msgobj('msg_30.txt')
    475         self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message')
    476 
    477     def test_get_content_maintype_from_message_explicit(self):
    478         msg = self._msgobj('msg_28.txt')
    479         self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message')
    480 
    481     def test_get_content_maintype_from_message_text_plain_implicit(self):
    482         msg = self._msgobj('msg_03.txt')
    483         self.assertEqual(msg.get_content_maintype(), 'text')
    484 
    485     def test_get_content_maintype_from_message_text_plain_explicit(self):
    486         msg = self._msgobj('msg_01.txt')
    487         self.assertEqual(msg.get_content_maintype(), 'text')
    488 
    489     def test_get_content_subtype_missing(self):
    490         msg = Message()
    491         self.assertEqual(msg.get_content_subtype(), 'plain')
    492 
    493     def test_get_content_subtype_missing_with_default_type(self):
    494         msg = Message()
    495         msg.set_default_type('message/rfc822')
    496         self.assertEqual(msg.get_content_subtype(), 'rfc822')
    497 
    498     def test_get_content_subtype_from_message_implicit(self):
    499         msg = self._msgobj('msg_30.txt')
    500         self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822')
    501 
    502     def test_get_content_subtype_from_message_explicit(self):
    503         msg = self._msgobj('msg_28.txt')
    504         self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822')
    505 
    506     def test_get_content_subtype_from_message_text_plain_implicit(self):
    507         msg = self._msgobj('msg_03.txt')
    508         self.assertEqual(msg.get_content_subtype(), 'plain')
    509 
    510     def test_get_content_subtype_from_message_text_plain_explicit(self):
    511         msg = self._msgobj('msg_01.txt')
    512         self.assertEqual(msg.get_content_subtype(), 'plain')
    513 
    514     def test_get_content_maintype_error(self):
    515         msg = Message()
    516         msg['Content-Type'] = 'no-slash-in-this-string'
    517         self.assertEqual(msg.get_content_maintype(), 'text')
    518 
    519     def test_get_content_subtype_error(self):
    520         msg = Message()
    521         msg['Content-Type'] = 'no-slash-in-this-string'
    522         self.assertEqual(msg.get_content_subtype(), 'plain')
    523 
    524     def test_replace_header(self):
    525         eq = self.assertEqual
    526         msg = Message()
    527         msg.add_header('First', 'One')
    528         msg.add_header('Second', 'Two')
    529         msg.add_header('Third', 'Three')
    530         eq(msg.keys(), ['First', 'Second', 'Third'])
    531         eq(msg.values(), ['One', 'Two', 'Three'])
    532         msg.replace_header('Second', 'Twenty')
    533         eq(msg.keys(), ['First', 'Second', 'Third'])
    534         eq(msg.values(), ['One', 'Twenty', 'Three'])
    535         msg.add_header('First', 'Eleven')
    536         msg.replace_header('First', 'One Hundred')
    537         eq(msg.keys(), ['First', 'Second', 'Third', 'First'])
    538         eq(msg.values(), ['One Hundred', 'Twenty', 'Three', 'Eleven'])
    539         self.assertRaises(KeyError, msg.replace_header, 'Fourth', 'Missing')
    540 
    541     def test_broken_base64_payload(self):
    542         x = 'AwDp0P7//y6LwKEAcPa/6Q=9'
    543         msg = Message()
    544         msg['content-type'] = 'audio/x-midi'
    545         msg['content-transfer-encoding'] = 'base64'
    546         msg.set_payload(x)
    547         self.assertEqual(msg.get_payload(decode=True), x)
    548 
    549     def test_get_content_charset(self):
    550         msg = Message()
    551         msg.set_charset('us-ascii')
    552         self.assertEqual('us-ascii', msg.get_content_charset())
    553         msg.set_charset(u'us-ascii')
    554         self.assertEqual('us-ascii', msg.get_content_charset())
    555 
    556     # Issue 5871: reject an attempt to embed a header inside a header value

    557     # (header injection attack).

    558     def test_embeded_header_via_Header_rejected(self):
    559         msg = Message()
    560         msg['Dummy'] = Header('dummy\nX-Injected-Header: test')
    561         self.assertRaises(Errors.HeaderParseError, msg.as_string)
    562 
    563     def test_embeded_header_via_string_rejected(self):
    564         msg = Message()
    565         msg['Dummy'] = 'dummy\nX-Injected-Header: test'
    566         self.assertRaises(Errors.HeaderParseError, msg.as_string)
    567 
    568 
    569 # Test the email.Encoders module

    570 class TestEncoders(unittest.TestCase):
    571     def test_encode_empty_payload(self):
    572         eq = self.assertEqual
    573         msg = Message()
    574         msg.set_charset('us-ascii')
    575         eq(msg['content-transfer-encoding'], '7bit')
    576 
    577     def test_default_cte(self):
    578         eq = self.assertEqual
    579         # 7bit data and the default us-ascii _charset

    580         msg = MIMEText('hello world')
    581         eq(msg['content-transfer-encoding'], '7bit')
    582         # Similar, but with 8bit data

    583         msg = MIMEText('hello \xf8 world')
    584         eq(msg['content-transfer-encoding'], '8bit')
    585         # And now with a different charset

    586         msg = MIMEText('hello \xf8 world', _charset='iso-8859-1')
    587         eq(msg['content-transfer-encoding'], 'quoted-printable')
    588 
    589     def test_encode7or8bit(self):
    590         # Make sure a charset whose input character set is 8bit but

    591         # whose output character set is 7bit gets a transfer-encoding

    592         # of 7bit.

    593         eq = self.assertEqual
    594         msg = email.MIMEText.MIMEText('\xca\xb8', _charset='euc-jp')
    595         eq(msg['content-transfer-encoding'], '7bit')
    596 
    597 
    598 # Test long header wrapping

    599 class TestLongHeaders(TestEmailBase):
    600     def test_split_long_continuation(self):
    601         eq = self.ndiffAssertEqual
    602         msg = email.message_from_string("""\
    603 Subject: bug demonstration
    604 \t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
    605 \tmore text
    606 
    607 test
    608 """)
    609         sfp = StringIO()
    610         g = Generator(sfp)
    611         g.flatten(msg)
    612         eq(sfp.getvalue(), """\
    613 Subject: bug demonstration
    614  12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
    615  more text
    616 
    617 test
    618 """)
    619 
    620     def test_another_long_almost_unsplittable_header(self):
    621         eq = self.ndiffAssertEqual
    622         hstr = """\
    623 bug demonstration
    624 \t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
    625 \tmore text"""
    626         h = Header(hstr, continuation_ws='\t')
    627         eq(h.encode(), """\
    628 bug demonstration
    629 \t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
    630 \tmore text""")
    631         h = Header(hstr)
    632         eq(h.encode(), """\
    633 bug demonstration
    634  12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
    635  more text""")
    636 
    637     def test_long_nonstring(self):
    638         eq = self.ndiffAssertEqual
    639         g = Charset("iso-8859-1")
    640         cz = Charset("iso-8859-2")
    641         utf8 = Charset("utf-8")
    642         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. "
    643         cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. "
    644         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")
    645         h = Header(g_head, g, header_name='Subject')
    646         h.append(cz_head, cz)
    647         h.append(utf8_head, utf8)
    648         msg = Message()
    649         msg['Subject'] = h
    650         sfp = StringIO()
    651         g = Generator(sfp)
    652         g.flatten(msg)
    653         eq(sfp.getvalue(), """\
    654 Subject: =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerd?=
    655  =?iso-8859-1?q?erband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndi?=
    656  =?iso-8859-1?q?schen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Kling?=
    657  =?iso-8859-1?q?en_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_met?=
    658  =?iso-8859-2?q?ropole_se_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?=
    659  =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE?=
    660  =?utf-8?b?44G+44Gb44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB?=
    661  =?utf-8?b?44GC44Go44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CM?=
    662  =?utf-8?q?Wenn_ist_das_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das?=
    663  =?utf-8?b?IE9kZXIgZGllIEZsaXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBow==?=
    664  =?utf-8?b?44Gm44GE44G+44GZ44CC?=
    665 
    666 """)
    667         eq(h.encode(), """\
    668 =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerd?=
    669  =?iso-8859-1?q?erband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndi?=
    670  =?iso-8859-1?q?schen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Kling?=
    671  =?iso-8859-1?q?en_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_met?=
    672  =?iso-8859-2?q?ropole_se_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?=
    673  =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE?=
    674  =?utf-8?b?44G+44Gb44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB?=
    675  =?utf-8?b?44GC44Go44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CM?=
    676  =?utf-8?q?Wenn_ist_das_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das?=
    677  =?utf-8?b?IE9kZXIgZGllIEZsaXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBow==?=
    678  =?utf-8?b?44Gm44GE44G+44GZ44CC?=""")
    679 
    680     def test_long_header_encode(self):
    681         eq = self.ndiffAssertEqual
    682         h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
    683                    'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
    684                    header_name='X-Foobar-Spoink-Defrobnit')
    685         eq(h.encode(), '''\
    686 wasnipoop; giraffes="very-long-necked-animals";
    687  spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
    688 
    689     def test_long_header_encode_with_tab_continuation(self):
    690         eq = self.ndiffAssertEqual
    691         h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
    692                    'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
    693                    header_name='X-Foobar-Spoink-Defrobnit',
    694                    continuation_ws='\t')
    695         eq(h.encode(), '''\
    696 wasnipoop; giraffes="very-long-necked-animals";
    697 \tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
    698 
    699     def test_header_splitter(self):
    700         eq = self.ndiffAssertEqual
    701         msg = MIMEText('')
    702         # It'd be great if we could use add_header() here, but that doesn't

    703         # guarantee an order of the parameters.

    704         msg['X-Foobar-Spoink-Defrobnit'] = (
    705             'wasnipoop; giraffes="very-long-necked-animals"; '
    706             'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"')
    707         sfp = StringIO()
    708         g = Generator(sfp)
    709         g.flatten(msg)
    710         eq(sfp.getvalue(), '''\
    711 Content-Type: text/plain; charset="us-ascii"
    712 MIME-Version: 1.0
    713 Content-Transfer-Encoding: 7bit
    714 X-Foobar-Spoink-Defrobnit: wasnipoop; giraffes="very-long-necked-animals";
    715  spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"
    716 
    717 ''')
    718 
    719     def test_no_semis_header_splitter(self):
    720         eq = self.ndiffAssertEqual
    721         msg = Message()
    722         msg['From'] = 'test (at] dom.ain'
    723         msg['References'] = SPACE.join(['<%d (at] dom.ain>' % i for i in range(10)])
    724         msg.set_payload('Test')
    725         sfp = StringIO()
    726         g = Generator(sfp)
    727         g.flatten(msg)
    728         eq(sfp.getvalue(), """\
    729 From: test (at] dom.ain
    730 References: <0 (at] dom.ain> <1 (at] dom.ain> <2 (at] dom.ain> <3 (at] dom.ain> <4 (at] dom.ain>
    731  <5 (at] dom.ain> <6 (at] dom.ain> <7 (at] dom.ain> <8 (at] dom.ain> <9 (at] dom.ain>
    732 
    733 Test""")
    734 
    735     def test_no_split_long_header(self):
    736         eq = self.ndiffAssertEqual
    737         hstr = 'References: ' + 'x' * 80
    738         h = Header(hstr, continuation_ws='\t')
    739         eq(h.encode(), """\
    740 References: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""")
    741 
    742     def test_splitting_multiple_long_lines(self):
    743         eq = self.ndiffAssertEqual
    744         hstr = """\
    745 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)
    746 \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)
    747 \tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for <mailman-admin (at] babylon.socal-raves.org>; Sat, 2 Feb 2002 17:00:06 -0800 (PST)
    748 """
    749         h = Header(hstr, continuation_ws='\t')
    750         eq(h.encode(), """\
    751 from babylon.socal-raves.org (localhost [127.0.0.1]);
    752 \tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
    753 \tfor <mailman-admin (at] babylon.socal-raves.org>;
    754 \tSat, 2 Feb 2002 17:00:06 -0800 (PST)
    755 \tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
    756 \tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
    757 \tfor <mailman-admin (at] babylon.socal-raves.org>;
    758 \tSat, 2 Feb 2002 17:00:06 -0800 (PST)
    759 \tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
    760 \tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
    761 \tfor <mailman-admin (at] babylon.socal-raves.org>;
    762 \tSat, 2 Feb 2002 17:00:06 -0800 (PST)""")
    763 
    764     def test_splitting_first_line_only_is_long(self):
    765         eq = self.ndiffAssertEqual
    766         hstr = """\
    767 from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca)
    768 \tby kronos.mems-exchange.org with esmtp (Exim 4.05)
    769 \tid 17k4h5-00034i-00
    770 \tfor test (at] mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400"""
    771         h = Header(hstr, maxlinelen=78, header_name='Received',
    772                    continuation_ws='\t')
    773         eq(h.encode(), """\
    774 from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93]
    775 \thelo=cthulhu.gerg.ca)
    776 \tby kronos.mems-exchange.org with esmtp (Exim 4.05)
    777 \tid 17k4h5-00034i-00
    778 \tfor test (at] mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""")
    779 
    780     def test_long_8bit_header(self):
    781         eq = self.ndiffAssertEqual
    782         msg = Message()
    783         h = Header('Britische Regierung gibt', 'iso-8859-1',
    784                     header_name='Subject')
    785         h.append('gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte')
    786         msg['Subject'] = h
    787         eq(msg.as_string(), """\
    788 Subject: =?iso-8859-1?q?Britische_Regierung_gibt?= =?iso-8859-1?q?gr=FCnes?=
    789  =?iso-8859-1?q?_Licht_f=FCr_Offshore-Windkraftprojekte?=
    790 
    791 """)
    792 
    793     def test_long_8bit_header_no_charset(self):
    794         eq = self.ndiffAssertEqual
    795         msg = Message()
    796         msg['Reply-To'] = 'Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte <a-very-long-address (at] example.com>'
    797         eq(msg.as_string(), """\
    798 Reply-To: Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte <a-very-long-address (at] example.com>
    799 
    800 """)
    801 
    802     def test_long_to_header(self):
    803         eq = self.ndiffAssertEqual
    804         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>'
    805         msg = Message()
    806         msg['To'] = to
    807         eq(msg.as_string(0), '''\
    808 To: "Someone Test #A" <someone (at] eecs.umich.edu>, <someone (at] eecs.umich.edu>,
    809  "Someone Test #B" <someone (at] umich.edu>,
    810  "Someone Test #C" <someone (at] eecs.umich.edu>,
    811  "Someone Test #D" <someone (at] eecs.umich.edu>
    812 
    813 ''')
    814 
    815     def test_long_line_after_append(self):
    816         eq = self.ndiffAssertEqual
    817         s = 'This is an example of string which has almost the limit of header length.'
    818         h = Header(s)
    819         h.append('Add another line.')
    820         eq(h.encode(), """\
    821 This is an example of string which has almost the limit of header length.
    822  Add another line.""")
    823 
    824     def test_shorter_line_with_append(self):
    825         eq = self.ndiffAssertEqual
    826         s = 'This is a shorter line.'
    827         h = Header(s)
    828         h.append('Add another sentence. (Surprise?)')
    829         eq(h.encode(),
    830            'This is a shorter line. Add another sentence. (Surprise?)')
    831 
    832     def test_long_field_name(self):
    833         eq = self.ndiffAssertEqual
    834         fn = 'X-Very-Very-Very-Long-Header-Name'
    835         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. "
    836         h = Header(gs, 'iso-8859-1', header_name=fn)
    837         # BAW: this seems broken because the first line is too long

    838         eq(h.encode(), """\
    839 =?iso-8859-1?q?Die_Mieter_treten_hier_?=
    840  =?iso-8859-1?q?ein_werden_mit_einem_Foerderband_komfortabel_den_Korridor_?=
    841  =?iso-8859-1?q?entlang=2C_an_s=FCdl=FCndischen_Wandgem=E4lden_vorbei=2C_g?=
    842  =?iso-8859-1?q?egen_die_rotierenden_Klingen_bef=F6rdert=2E_?=""")
    843 
    844     def test_long_received_header(self):
    845         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'
    846         msg = Message()
    847         msg['Received-1'] = Header(h, continuation_ws='\t')
    848         msg['Received-2'] = h
    849         self.assertEqual(msg.as_string(), """\
    850 Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by
    851 \throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
    852 \tWed, 05 Mar 2003 18:10:18 -0700
    853 Received-2: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by
    854  hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
    855  Wed, 05 Mar 2003 18:10:18 -0700
    856 
    857 """)
    858 
    859     def test_string_headerinst_eq(self):
    860         h = '<15975.17901.207240.414604 (at] sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner\'s message of "Thu, 6 Mar 2003 13:58:21 +0100")'
    861         msg = Message()
    862         msg['Received'] = Header(h, header_name='Received',
    863                                  continuation_ws='\t')
    864         msg['Received'] = h
    865         self.ndiffAssertEqual(msg.as_string(), """\
    866 Received: <15975.17901.207240.414604 (at] sgigritzmann1.mathematik.tu-muenchen.de>
    867 \t(David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100")
    868 Received: <15975.17901.207240.414604 (at] sgigritzmann1.mathematik.tu-muenchen.de>
    869  (David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100")
    870 
    871 """)
    872 
    873     def test_long_unbreakable_lines_with_continuation(self):
    874         eq = self.ndiffAssertEqual
    875         msg = Message()
    876         t = """\
    877  iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
    878  locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp"""
    879         msg['Face-1'] = t
    880         msg['Face-2'] = Header(t, header_name='Face-2')
    881         eq(msg.as_string(), """\
    882 Face-1: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
    883  locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
    884 Face-2: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
    885  locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
    886 
    887 """)
    888 
    889     def test_another_long_multiline_header(self):
    890         eq = self.ndiffAssertEqual
    891         m = '''\
    892 Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with Microsoft SMTPSVC(5.0.2195.4905);
    893  Wed, 16 Oct 2002 07:41:11 -0700'''
    894         msg = email.message_from_string(m)
    895         eq(msg.as_string(), '''\
    896 Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with
    897  Microsoft SMTPSVC(5.0.2195.4905); Wed, 16 Oct 2002 07:41:11 -0700
    898 
    899 ''')
    900 
    901     def test_long_lines_with_different_header(self):
    902         eq = self.ndiffAssertEqual
    903         h = """\
    904 List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
    905         <mailto:spamassassin-talk-request (at] lists.sourceforge.net?subject=unsubscribe>"""
    906         msg = Message()
    907         msg['List'] = h
    908         msg['List'] = Header(h, header_name='List')
    909         eq(msg.as_string(), """\
    910 List: List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
    911  <mailto:spamassassin-talk-request (at] lists.sourceforge.net?subject=unsubscribe>
    912 List: List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
    913  <mailto:spamassassin-talk-request (at] lists.sourceforge.net?subject=unsubscribe>
    914 
    915 """)
    916 
    917 
    918 
    919 # Test mangling of "From " lines in the body of a message

    920 class TestFromMangling(unittest.TestCase):
    921     def setUp(self):
    922         self.msg = Message()
    923         self.msg['From'] = 'aaa (at] bbb.org'
    924         self.msg.set_payload("""\
    925 From the desk of A.A.A.:
    926 Blah blah blah
    927 """)
    928 
    929     def test_mangled_from(self):
    930         s = StringIO()
    931         g = Generator(s, mangle_from_=True)
    932         g.flatten(self.msg)
    933         self.assertEqual(s.getvalue(), """\
    934 From: aaa (at] bbb.org
    935 
    936 >From the desk of A.A.A.:
    937 Blah blah blah
    938 """)
    939 
    940     def test_dont_mangle_from(self):
    941         s = StringIO()
    942         g = Generator(s, mangle_from_=False)
    943         g.flatten(self.msg)
    944         self.assertEqual(s.getvalue(), """\
    945 From: aaa (at] bbb.org
    946 
    947 From the desk of A.A.A.:
    948 Blah blah blah
    949 """)
    950 
    951 
    952 
    953 # Test the basic MIMEAudio class

    954 class TestMIMEAudio(unittest.TestCase):
    955     def setUp(self):
    956         # Make sure we pick up the audiotest.au that lives in email/test/data.

    957         # In Python, there's an audiotest.au living in Lib/test but that isn't

    958         # included in some binary distros that don't include the test

    959         # package.  The trailing empty string on the .join() is significant

    960         # since findfile() will do a dirname().

    961         datadir = os.path.join(os.path.dirname(landmark), 'data', '')
    962         fp = open(findfile('audiotest.au', datadir), 'rb')
    963         try:
    964             self._audiodata = fp.read()
    965         finally:
    966             fp.close()
    967         self._au = MIMEAudio(self._audiodata)
    968 
    969     def test_guess_minor_type(self):
    970         self.assertEqual(self._au.get_content_type(), 'audio/basic')
    971 
    972     def test_encoding(self):
    973         payload = self._au.get_payload()
    974         self.assertEqual(base64.decodestring(payload), self._audiodata)
    975 
    976     def test_checkSetMinor(self):
    977         au = MIMEAudio(self._audiodata, 'fish')
    978         self.assertEqual(au.get_content_type(), 'audio/fish')
    979 
    980     def test_add_header(self):
    981         eq = self.assertEqual
    982         unless = self.assertTrue
    983         self._au.add_header('Content-Disposition', 'attachment',
    984                             filename='audiotest.au')
    985         eq(self._au['content-disposition'],
    986            'attachment; filename="audiotest.au"')
    987         eq(self._au.get_params(header='content-disposition'),
    988            [('attachment', ''), ('filename', 'audiotest.au')])
    989         eq(self._au.get_param('filename', header='content-disposition'),
    990            'audiotest.au')
    991         missing = []
    992         eq(self._au.get_param('attachment', header='content-disposition'), '')
    993         unless(self._au.get_param('foo', failobj=missing,
    994                                   header='content-disposition') is missing)
    995         # Try some missing stuff

    996         unless(self._au.get_param('foobar', missing) is missing)
    997         unless(self._au.get_param('attachment', missing,
    998                                   header='foobar') is missing)
    999 
   1000 
   1001 
   1002 # Test the basic MIMEImage class

   1003 class TestMIMEImage(unittest.TestCase):
   1004     def setUp(self):
   1005         fp = openfile('PyBanner048.gif')
   1006         try:
   1007             self._imgdata = fp.read()
   1008         finally:
   1009             fp.close()
   1010         self._im = MIMEImage(self._imgdata)
   1011 
   1012     def test_guess_minor_type(self):
   1013         self.assertEqual(self._im.get_content_type(), 'image/gif')
   1014 
   1015     def test_encoding(self):
   1016         payload = self._im.get_payload()
   1017         self.assertEqual(base64.decodestring(payload), self._imgdata)
   1018 
   1019     def test_checkSetMinor(self):
   1020         im = MIMEImage(self._imgdata, 'fish')
   1021         self.assertEqual(im.get_content_type(), 'image/fish')
   1022 
   1023     def test_add_header(self):
   1024         eq = self.assertEqual
   1025         unless = self.assertTrue
   1026         self._im.add_header('Content-Disposition', 'attachment',
   1027                             filename='dingusfish.gif')
   1028         eq(self._im['content-disposition'],
   1029            'attachment; filename="dingusfish.gif"')
   1030         eq(self._im.get_params(header='content-disposition'),
   1031            [('attachment', ''), ('filename', 'dingusfish.gif')])
   1032         eq(self._im.get_param('filename', header='content-disposition'),
   1033            'dingusfish.gif')
   1034         missing = []
   1035         eq(self._im.get_param('attachment', header='content-disposition'), '')
   1036         unless(self._im.get_param('foo', failobj=missing,
   1037                                   header='content-disposition') is missing)
   1038         # Try some missing stuff

   1039         unless(self._im.get_param('foobar', missing) is missing)
   1040         unless(self._im.get_param('attachment', missing,
   1041                                   header='foobar') is missing)
   1042 
   1043 
   1044 
   1045 # Test the basic MIMEText class

   1046 class TestMIMEText(unittest.TestCase):
   1047     def setUp(self):
   1048         self._msg = MIMEText('hello there')
   1049 
   1050     def test_types(self):
   1051         eq = self.assertEqual
   1052         unless = self.assertTrue
   1053         eq(self._msg.get_content_type(), 'text/plain')
   1054         eq(self._msg.get_param('charset'), 'us-ascii')
   1055         missing = []
   1056         unless(self._msg.get_param('foobar', missing) is missing)
   1057         unless(self._msg.get_param('charset', missing, header='foobar')
   1058                is missing)
   1059 
   1060     def test_payload(self):
   1061         self.assertEqual(self._msg.get_payload(), 'hello there')
   1062         self.assertTrue(not self._msg.is_multipart())
   1063 
   1064     def test_charset(self):
   1065         eq = self.assertEqual
   1066         msg = MIMEText('hello there', _charset='us-ascii')
   1067         eq(msg.get_charset().input_charset, 'us-ascii')
   1068         eq(msg['content-type'], 'text/plain; charset="us-ascii"')
   1069 
   1070     def test_7bit_unicode_input(self):
   1071         eq = self.assertEqual
   1072         msg = MIMEText(u'hello there', _charset='us-ascii')
   1073         eq(msg.get_charset().input_charset, 'us-ascii')
   1074         eq(msg['content-type'], 'text/plain; charset="us-ascii"')
   1075 
   1076     def test_7bit_unicode_input_no_charset(self):
   1077         eq = self.assertEqual
   1078         msg = MIMEText(u'hello there')
   1079         eq(msg.get_charset(), 'us-ascii')
   1080         eq(msg['content-type'], 'text/plain; charset="us-ascii"')
   1081         self.assertTrue('hello there' in msg.as_string())
   1082 
   1083     def test_8bit_unicode_input(self):
   1084         teststr = u'\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
   1085         eq = self.assertEqual
   1086         msg = MIMEText(teststr, _charset='utf-8')
   1087         eq(msg.get_charset().output_charset, 'utf-8')
   1088         eq(msg['content-type'], 'text/plain; charset="utf-8"')
   1089         eq(msg.get_payload(decode=True), teststr.encode('utf-8'))
   1090 
   1091     def test_8bit_unicode_input_no_charset(self):
   1092         teststr = u'\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
   1093         self.assertRaises(UnicodeEncodeError, MIMEText, teststr)
   1094 
   1095 
   1096 
   1097 # Test complicated multipart/* messages

   1098 class TestMultipart(TestEmailBase):
   1099     def setUp(self):
   1100         fp = openfile('PyBanner048.gif')
   1101         try:
   1102             data = fp.read()
   1103         finally:
   1104             fp.close()
   1105 
   1106         container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
   1107         image = MIMEImage(data, name='dingusfish.gif')
   1108         image.add_header('content-disposition', 'attachment',
   1109                          filename='dingusfish.gif')
   1110         intro = MIMEText('''\
   1111 Hi there,
   1112 
   1113 This is the dingus fish.
   1114 ''')
   1115         container.attach(intro)
   1116         container.attach(image)
   1117         container['From'] = 'Barry <barry (at] digicool.com>'
   1118         container['To'] = 'Dingus Lovers <cravindogs (at] cravindogs.com>'
   1119         container['Subject'] = 'Here is your dingus fish'
   1120 
   1121         now = 987809702.54848599
   1122         timetuple = time.localtime(now)
   1123         if timetuple[-1] == 0:
   1124             tzsecs = time.timezone
   1125         else:
   1126             tzsecs = time.altzone
   1127         if tzsecs > 0:
   1128             sign = '-'
   1129         else:
   1130             sign = '+'
   1131         tzoffset = ' %s%04d' % (sign, tzsecs // 36)
   1132         container['Date'] = time.strftime(
   1133             '%a, %d %b %Y %H:%M:%S',
   1134             time.localtime(now)) + tzoffset
   1135         self._msg = container
   1136         self._im = image
   1137         self._txt = intro
   1138 
   1139     def test_hierarchy(self):
   1140         # convenience

   1141         eq = self.assertEqual
   1142         unless = self.assertTrue
   1143         raises = self.assertRaises
   1144         # tests

   1145         m = self._msg
   1146         unless(m.is_multipart())
   1147         eq(m.get_content_type(), 'multipart/mixed')
   1148         eq(len(m.get_payload()), 2)
   1149         raises(IndexError, m.get_payload, 2)
   1150         m0 = m.get_payload(0)
   1151         m1 = m.get_payload(1)
   1152         unless(m0 is self._txt)
   1153         unless(m1 is self._im)
   1154         eq(m.get_payload(), [m0, m1])
   1155         unless(not m0.is_multipart())
   1156         unless(not m1.is_multipart())
   1157 
   1158     def test_empty_multipart_idempotent(self):
   1159         text = """\
   1160 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1161 MIME-Version: 1.0
   1162 Subject: A subject
   1163 To: aperson (at] dom.ain
   1164 From: bperson (at] dom.ain
   1165 
   1166 
   1167 --BOUNDARY
   1168 
   1169 
   1170 --BOUNDARY--
   1171 """
   1172         msg = Parser().parsestr(text)
   1173         self.ndiffAssertEqual(text, msg.as_string())
   1174 
   1175     def test_no_parts_in_a_multipart_with_none_epilogue(self):
   1176         outer = MIMEBase('multipart', 'mixed')
   1177         outer['Subject'] = 'A subject'
   1178         outer['To'] = 'aperson (at] dom.ain'
   1179         outer['From'] = 'bperson (at] dom.ain'
   1180         outer.set_boundary('BOUNDARY')
   1181         self.ndiffAssertEqual(outer.as_string(), '''\
   1182 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1183 MIME-Version: 1.0
   1184 Subject: A subject
   1185 To: aperson (at] dom.ain
   1186 From: bperson (at] dom.ain
   1187 
   1188 --BOUNDARY
   1189 
   1190 --BOUNDARY--''')
   1191 
   1192     def test_no_parts_in_a_multipart_with_empty_epilogue(self):
   1193         outer = MIMEBase('multipart', 'mixed')
   1194         outer['Subject'] = 'A subject'
   1195         outer['To'] = 'aperson (at] dom.ain'
   1196         outer['From'] = 'bperson (at] dom.ain'
   1197         outer.preamble = ''
   1198         outer.epilogue = ''
   1199         outer.set_boundary('BOUNDARY')
   1200         self.ndiffAssertEqual(outer.as_string(), '''\
   1201 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1202 MIME-Version: 1.0
   1203 Subject: A subject
   1204 To: aperson (at] dom.ain
   1205 From: bperson (at] dom.ain
   1206 
   1207 
   1208 --BOUNDARY
   1209 
   1210 --BOUNDARY--
   1211 ''')
   1212 
   1213     def test_one_part_in_a_multipart(self):
   1214         eq = self.ndiffAssertEqual
   1215         outer = MIMEBase('multipart', 'mixed')
   1216         outer['Subject'] = 'A subject'
   1217         outer['To'] = 'aperson (at] dom.ain'
   1218         outer['From'] = 'bperson (at] dom.ain'
   1219         outer.set_boundary('BOUNDARY')
   1220         msg = MIMEText('hello world')
   1221         outer.attach(msg)
   1222         eq(outer.as_string(), '''\
   1223 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1224 MIME-Version: 1.0
   1225 Subject: A subject
   1226 To: aperson (at] dom.ain
   1227 From: bperson (at] dom.ain
   1228 
   1229 --BOUNDARY
   1230 Content-Type: text/plain; charset="us-ascii"
   1231 MIME-Version: 1.0
   1232 Content-Transfer-Encoding: 7bit
   1233 
   1234 hello world
   1235 --BOUNDARY--''')
   1236 
   1237     def test_seq_parts_in_a_multipart_with_empty_preamble(self):
   1238         eq = self.ndiffAssertEqual
   1239         outer = MIMEBase('multipart', 'mixed')
   1240         outer['Subject'] = 'A subject'
   1241         outer['To'] = 'aperson (at] dom.ain'
   1242         outer['From'] = 'bperson (at] dom.ain'
   1243         outer.preamble = ''
   1244         msg = MIMEText('hello world')
   1245         outer.attach(msg)
   1246         outer.set_boundary('BOUNDARY')
   1247         eq(outer.as_string(), '''\
   1248 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1249 MIME-Version: 1.0
   1250 Subject: A subject
   1251 To: aperson (at] dom.ain
   1252 From: bperson (at] dom.ain
   1253 
   1254 
   1255 --BOUNDARY
   1256 Content-Type: text/plain; charset="us-ascii"
   1257 MIME-Version: 1.0
   1258 Content-Transfer-Encoding: 7bit
   1259 
   1260 hello world
   1261 --BOUNDARY--''')
   1262 
   1263 
   1264     def test_seq_parts_in_a_multipart_with_none_preamble(self):
   1265         eq = self.ndiffAssertEqual
   1266         outer = MIMEBase('multipart', 'mixed')
   1267         outer['Subject'] = 'A subject'
   1268         outer['To'] = 'aperson (at] dom.ain'
   1269         outer['From'] = 'bperson (at] dom.ain'
   1270         outer.preamble = None
   1271         msg = MIMEText('hello world')
   1272         outer.attach(msg)
   1273         outer.set_boundary('BOUNDARY')
   1274         eq(outer.as_string(), '''\
   1275 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1276 MIME-Version: 1.0
   1277 Subject: A subject
   1278 To: aperson (at] dom.ain
   1279 From: bperson (at] dom.ain
   1280 
   1281 --BOUNDARY
   1282 Content-Type: text/plain; charset="us-ascii"
   1283 MIME-Version: 1.0
   1284 Content-Transfer-Encoding: 7bit
   1285 
   1286 hello world
   1287 --BOUNDARY--''')
   1288 
   1289 
   1290     def test_seq_parts_in_a_multipart_with_none_epilogue(self):
   1291         eq = self.ndiffAssertEqual
   1292         outer = MIMEBase('multipart', 'mixed')
   1293         outer['Subject'] = 'A subject'
   1294         outer['To'] = 'aperson (at] dom.ain'
   1295         outer['From'] = 'bperson (at] dom.ain'
   1296         outer.epilogue = None
   1297         msg = MIMEText('hello world')
   1298         outer.attach(msg)
   1299         outer.set_boundary('BOUNDARY')
   1300         eq(outer.as_string(), '''\
   1301 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1302 MIME-Version: 1.0
   1303 Subject: A subject
   1304 To: aperson (at] dom.ain
   1305 From: bperson (at] dom.ain
   1306 
   1307 --BOUNDARY
   1308 Content-Type: text/plain; charset="us-ascii"
   1309 MIME-Version: 1.0
   1310 Content-Transfer-Encoding: 7bit
   1311 
   1312 hello world
   1313 --BOUNDARY--''')
   1314 
   1315 
   1316     def test_seq_parts_in_a_multipart_with_empty_epilogue(self):
   1317         eq = self.ndiffAssertEqual
   1318         outer = MIMEBase('multipart', 'mixed')
   1319         outer['Subject'] = 'A subject'
   1320         outer['To'] = 'aperson (at] dom.ain'
   1321         outer['From'] = 'bperson (at] dom.ain'
   1322         outer.epilogue = ''
   1323         msg = MIMEText('hello world')
   1324         outer.attach(msg)
   1325         outer.set_boundary('BOUNDARY')
   1326         eq(outer.as_string(), '''\
   1327 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1328 MIME-Version: 1.0
   1329 Subject: A subject
   1330 To: aperson (at] dom.ain
   1331 From: bperson (at] dom.ain
   1332 
   1333 --BOUNDARY
   1334 Content-Type: text/plain; charset="us-ascii"
   1335 MIME-Version: 1.0
   1336 Content-Transfer-Encoding: 7bit
   1337 
   1338 hello world
   1339 --BOUNDARY--
   1340 ''')
   1341 
   1342 
   1343     def test_seq_parts_in_a_multipart_with_nl_epilogue(self):
   1344         eq = self.ndiffAssertEqual
   1345         outer = MIMEBase('multipart', 'mixed')
   1346         outer['Subject'] = 'A subject'
   1347         outer['To'] = 'aperson (at] dom.ain'
   1348         outer['From'] = 'bperson (at] dom.ain'
   1349         outer.epilogue = '\n'
   1350         msg = MIMEText('hello world')
   1351         outer.attach(msg)
   1352         outer.set_boundary('BOUNDARY')
   1353         eq(outer.as_string(), '''\
   1354 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1355 MIME-Version: 1.0
   1356 Subject: A subject
   1357 To: aperson (at] dom.ain
   1358 From: bperson (at] dom.ain
   1359 
   1360 --BOUNDARY
   1361 Content-Type: text/plain; charset="us-ascii"
   1362 MIME-Version: 1.0
   1363 Content-Transfer-Encoding: 7bit
   1364 
   1365 hello world
   1366 --BOUNDARY--
   1367 
   1368 ''')
   1369 
   1370     def test_message_external_body(self):
   1371         eq = self.assertEqual
   1372         msg = self._msgobj('msg_36.txt')
   1373         eq(len(msg.get_payload()), 2)
   1374         msg1 = msg.get_payload(1)
   1375         eq(msg1.get_content_type(), 'multipart/alternative')
   1376         eq(len(msg1.get_payload()), 2)
   1377         for subpart in msg1.get_payload():
   1378             eq(subpart.get_content_type(), 'message/external-body')
   1379             eq(len(subpart.get_payload()), 1)
   1380             subsubpart = subpart.get_payload(0)
   1381             eq(subsubpart.get_content_type(), 'text/plain')
   1382 
   1383     def test_double_boundary(self):
   1384         # msg_37.txt is a multipart that contains two dash-boundary's in a

   1385         # row.  Our interpretation of RFC 2046 calls for ignoring the second

   1386         # and subsequent boundaries.

   1387         msg = self._msgobj('msg_37.txt')
   1388         self.assertEqual(len(msg.get_payload()), 3)
   1389 
   1390     def test_nested_inner_contains_outer_boundary(self):
   1391         eq = self.ndiffAssertEqual
   1392         # msg_38.txt has an inner part that contains outer boundaries.  My

   1393         # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say

   1394         # these are illegal and should be interpreted as unterminated inner

   1395         # parts.

   1396         msg = self._msgobj('msg_38.txt')
   1397         sfp = StringIO()
   1398         Iterators._structure(msg, sfp)
   1399         eq(sfp.getvalue(), """\
   1400 multipart/mixed
   1401     multipart/mixed
   1402         multipart/alternative
   1403             text/plain
   1404         text/plain
   1405     text/plain
   1406     text/plain
   1407 """)
   1408 
   1409     def test_nested_with_same_boundary(self):
   1410         eq = self.ndiffAssertEqual
   1411         # msg 39.txt is similarly evil in that it's got inner parts that use

   1412         # the same boundary as outer parts.  Again, I believe the way this is

   1413         # parsed is closest to the spirit of RFC 2046

   1414         msg = self._msgobj('msg_39.txt')
   1415         sfp = StringIO()
   1416         Iterators._structure(msg, sfp)
   1417         eq(sfp.getvalue(), """\
   1418 multipart/mixed
   1419     multipart/mixed
   1420         multipart/alternative
   1421         application/octet-stream
   1422         application/octet-stream
   1423     text/plain
   1424 """)
   1425 
   1426     def test_boundary_in_non_multipart(self):
   1427         msg = self._msgobj('msg_40.txt')
   1428         self.assertEqual(msg.as_string(), '''\
   1429 MIME-Version: 1.0
   1430 Content-Type: text/html; boundary="--961284236552522269"
   1431 
   1432 ----961284236552522269
   1433 Content-Type: text/html;
   1434 Content-Transfer-Encoding: 7Bit
   1435 
   1436 <html></html>
   1437 
   1438 ----961284236552522269--
   1439 ''')
   1440 
   1441     def test_boundary_with_leading_space(self):
   1442         eq = self.assertEqual
   1443         msg = email.message_from_string('''\
   1444 MIME-Version: 1.0
   1445 Content-Type: multipart/mixed; boundary="    XXXX"
   1446 
   1447 --    XXXX
   1448 Content-Type: text/plain
   1449 
   1450 
   1451 --    XXXX
   1452 Content-Type: text/plain
   1453 
   1454 --    XXXX--
   1455 ''')
   1456         self.assertTrue(msg.is_multipart())
   1457         eq(msg.get_boundary(), '    XXXX')
   1458         eq(len(msg.get_payload()), 2)
   1459 
   1460     def test_boundary_without_trailing_newline(self):
   1461         m = Parser().parsestr("""\
   1462 Content-Type: multipart/mixed; boundary="===============0012394164=="
   1463 MIME-Version: 1.0
   1464 
   1465 --===============0012394164==
   1466 Content-Type: image/file1.jpg
   1467 MIME-Version: 1.0
   1468 Content-Transfer-Encoding: base64
   1469 
   1470 YXNkZg==
   1471 --===============0012394164==--""")
   1472         self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==')
   1473 
   1474 
   1475 
   1476 # Test some badly formatted messages

   1477 class TestNonConformant(TestEmailBase):
   1478     def test_parse_missing_minor_type(self):
   1479         eq = self.assertEqual
   1480         msg = self._msgobj('msg_14.txt')
   1481         eq(msg.get_content_type(), 'text/plain')
   1482         eq(msg.get_content_maintype(), 'text')
   1483         eq(msg.get_content_subtype(), 'plain')
   1484 
   1485     def test_same_boundary_inner_outer(self):
   1486         unless = self.assertTrue
   1487         msg = self._msgobj('msg_15.txt')
   1488         # XXX We can probably eventually do better

   1489         inner = msg.get_payload(0)
   1490         unless(hasattr(inner, 'defects'))
   1491         self.assertEqual(len(inner.defects), 1)
   1492         unless(isinstance(inner.defects[0],
   1493                           Errors.StartBoundaryNotFoundDefect))
   1494 
   1495     def test_multipart_no_boundary(self):
   1496         unless = self.assertTrue
   1497         msg = self._msgobj('msg_25.txt')
   1498         unless(isinstance(msg.get_payload(), str))
   1499         self.assertEqual(len(msg.defects), 2)
   1500         unless(isinstance(msg.defects[0], Errors.NoBoundaryInMultipartDefect))
   1501         unless(isinstance(msg.defects[1],
   1502                           Errors.MultipartInvariantViolationDefect))
   1503 
   1504     def test_invalid_content_type(self):
   1505         eq = self.assertEqual
   1506         neq = self.ndiffAssertEqual
   1507         msg = Message()
   1508         # RFC 2045, $5.2 says invalid yields text/plain

   1509         msg['Content-Type'] = 'text'
   1510         eq(msg.get_content_maintype(), 'text')
   1511         eq(msg.get_content_subtype(), 'plain')
   1512         eq(msg.get_content_type(), 'text/plain')
   1513         # Clear the old value and try something /really/ invalid

   1514         del msg['content-type']
   1515         msg['Content-Type'] = 'foo'
   1516         eq(msg.get_content_maintype(), 'text')
   1517         eq(msg.get_content_subtype(), 'plain')
   1518         eq(msg.get_content_type(), 'text/plain')
   1519         # Still, make sure that the message is idempotently generated

   1520         s = StringIO()
   1521         g = Generator(s)
   1522         g.flatten(msg)
   1523         neq(s.getvalue(), 'Content-Type: foo\n\n')
   1524 
   1525     def test_no_start_boundary(self):
   1526         eq = self.ndiffAssertEqual
   1527         msg = self._msgobj('msg_31.txt')
   1528         eq(msg.get_payload(), """\
   1529 --BOUNDARY
   1530 Content-Type: text/plain
   1531 
   1532 message 1
   1533 
   1534 --BOUNDARY
   1535 Content-Type: text/plain
   1536 
   1537 message 2
   1538 
   1539 --BOUNDARY--
   1540 """)
   1541 
   1542     def test_no_separating_blank_line(self):
   1543         eq = self.ndiffAssertEqual
   1544         msg = self._msgobj('msg_35.txt')
   1545         eq(msg.as_string(), """\
   1546 From: aperson (at] dom.ain
   1547 To: bperson (at] dom.ain
   1548 Subject: here's something interesting
   1549 
   1550 counter to RFC 2822, there's no separating newline here
   1551 """)
   1552 
   1553     def test_lying_multipart(self):
   1554         unless = self.assertTrue
   1555         msg = self._msgobj('msg_41.txt')
   1556         unless(hasattr(msg, 'defects'))
   1557         self.assertEqual(len(msg.defects), 2)
   1558         unless(isinstance(msg.defects[0], Errors.NoBoundaryInMultipartDefect))
   1559         unless(isinstance(msg.defects[1],
   1560                           Errors.MultipartInvariantViolationDefect))
   1561 
   1562     def test_missing_start_boundary(self):
   1563         outer = self._msgobj('msg_42.txt')
   1564         # The message structure is:

   1565         #

   1566         # multipart/mixed

   1567         #    text/plain

   1568         #    message/rfc822

   1569         #        multipart/mixed [*]

   1570         #

   1571         # [*] This message is missing its start boundary

   1572         bad = outer.get_payload(1).get_payload(0)
   1573         self.assertEqual(len(bad.defects), 1)
   1574         self.assertTrue(isinstance(bad.defects[0],
   1575                                    Errors.StartBoundaryNotFoundDefect))
   1576 
   1577     def test_first_line_is_continuation_header(self):
   1578         eq = self.assertEqual
   1579         m = ' Line 1\nLine 2\nLine 3'
   1580         msg = email.message_from_string(m)
   1581         eq(msg.keys(), [])
   1582         eq(msg.get_payload(), 'Line 2\nLine 3')
   1583         eq(len(msg.defects), 1)
   1584         self.assertTrue(isinstance(msg.defects[0],
   1585                                    Errors.FirstHeaderLineIsContinuationDefect))
   1586         eq(msg.defects[0].line, ' Line 1\n')
   1587 
   1588 
   1589 
   1590 
   1591 # Test RFC 2047 header encoding and decoding

   1592 class TestRFC2047(unittest.TestCase):
   1593     def test_rfc2047_multiline(self):
   1594         eq = self.assertEqual
   1595         s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz
   1596  foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?="""
   1597         dh = decode_header(s)
   1598         eq(dh, [
   1599             ('Re:', None),
   1600             ('r\x8aksm\x9arg\x8cs', 'mac-iceland'),
   1601             ('baz foo bar', None),
   1602             ('r\x8aksm\x9arg\x8cs', 'mac-iceland')])
   1603         eq(str(make_header(dh)),
   1604            """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar
   1605  =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""")
   1606 
   1607     def test_whitespace_eater_unicode(self):
   1608         eq = self.assertEqual
   1609         s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard (at] dom.ain>'
   1610         dh = decode_header(s)
   1611         eq(dh, [('Andr\xe9', 'iso-8859-1'), ('Pirard <pirard (at] dom.ain>', None)])
   1612         hu = unicode(make_header(dh)).encode('latin-1')
   1613         eq(hu, 'Andr\xe9 Pirard <pirard (at] dom.ain>')
   1614 
   1615     def test_whitespace_eater_unicode_2(self):
   1616         eq = self.assertEqual
   1617         s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?='
   1618         dh = decode_header(s)
   1619         eq(dh, [('The', None), ('quick brown fox', 'iso-8859-1'),
   1620                 ('jumped over the', None), ('lazy dog', 'iso-8859-1')])
   1621         hu = make_header(dh).__unicode__()
   1622         eq(hu, u'The quick brown fox jumped over the lazy dog')
   1623 
   1624     def test_rfc2047_without_whitespace(self):
   1625         s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord'
   1626         dh = decode_header(s)
   1627         self.assertEqual(dh, [(s, None)])
   1628 
   1629     def test_rfc2047_with_whitespace(self):
   1630         s = 'Sm =?ISO-8859-1?B?9g==?= rg =?ISO-8859-1?B?5Q==?= sbord'
   1631         dh = decode_header(s)
   1632         self.assertEqual(dh, [('Sm', None), ('\xf6', 'iso-8859-1'),
   1633                               ('rg', None), ('\xe5', 'iso-8859-1'),
   1634                               ('sbord', None)])
   1635 
   1636     def test_rfc2047_B_bad_padding(self):
   1637         s = '=?iso-8859-1?B?%s?='
   1638         data = [                                # only test complete bytes

   1639             ('dm==', 'v'), ('dm=', 'v'), ('dm', 'v'),
   1640             ('dmk=', 'vi'), ('dmk', 'vi')
   1641           ]
   1642         for q, a in data:
   1643             dh = decode_header(s % q)
   1644             self.assertEqual(dh, [(a, 'iso-8859-1')])
   1645 
   1646     def test_rfc2047_Q_invalid_digits(self):
   1647         # issue 10004.

   1648         s = '=?iso-8659-1?Q?andr=e9=zz?='
   1649         self.assertEqual(decode_header(s),
   1650                         [(b'andr\xe9=zz', 'iso-8659-1')])
   1651 
   1652 
   1653 # Test the MIMEMessage class

   1654 class TestMIMEMessage(TestEmailBase):
   1655     def setUp(self):
   1656         fp = openfile('msg_11.txt')
   1657         try:
   1658             self._text = fp.read()
   1659         finally:
   1660             fp.close()
   1661 
   1662     def test_type_error(self):
   1663         self.assertRaises(TypeError, MIMEMessage, 'a plain string')
   1664 
   1665     def test_valid_argument(self):
   1666         eq = self.assertEqual
   1667         unless = self.assertTrue
   1668         subject = 'A sub-message'
   1669         m = Message()
   1670         m['Subject'] = subject
   1671         r = MIMEMessage(m)
   1672         eq(r.get_content_type(), 'message/rfc822')
   1673         payload = r.get_payload()
   1674         unless(isinstance(payload, list))
   1675         eq(len(payload), 1)
   1676         subpart = payload[0]
   1677         unless(subpart is m)
   1678         eq(subpart['subject'], subject)
   1679 
   1680     def test_bad_multipart(self):
   1681         eq = self.assertEqual
   1682         msg1 = Message()
   1683         msg1['Subject'] = 'subpart 1'
   1684         msg2 = Message()
   1685         msg2['Subject'] = 'subpart 2'
   1686         r = MIMEMessage(msg1)
   1687         self.assertRaises(Errors.MultipartConversionError, r.attach, msg2)
   1688 
   1689     def test_generate(self):
   1690         # First craft the message to be encapsulated

   1691         m = Message()
   1692         m['Subject'] = 'An enclosed message'
   1693         m.set_payload('Here is the body of the message.\n')
   1694         r = MIMEMessage(m)
   1695         r['Subject'] = 'The enclosing message'
   1696         s = StringIO()
   1697         g = Generator(s)
   1698         g.flatten(r)
   1699         self.assertEqual(s.getvalue(), """\
   1700 Content-Type: message/rfc822
   1701 MIME-Version: 1.0
   1702 Subject: The enclosing message
   1703 
   1704 Subject: An enclosed message
   1705 
   1706 Here is the body of the message.
   1707 """)
   1708 
   1709     def test_parse_message_rfc822(self):
   1710         eq = self.assertEqual
   1711         unless = self.assertTrue
   1712         msg = self._msgobj('msg_11.txt')
   1713         eq(msg.get_content_type(), 'message/rfc822')
   1714         payload = msg.get_payload()
   1715         unless(isinstance(payload, list))
   1716         eq(len(payload), 1)
   1717         submsg = payload[0]
   1718         self.assertTrue(isinstance(submsg, Message))
   1719         eq(submsg['subject'], 'An enclosed message')
   1720         eq(submsg.get_payload(), 'Here is the body of the message.\n')
   1721 
   1722     def test_dsn(self):
   1723         eq = self.assertEqual
   1724         unless = self.assertTrue
   1725         # msg 16 is a Delivery Status Notification, see RFC 1894

   1726         msg = self._msgobj('msg_16.txt')
   1727         eq(msg.get_content_type(), 'multipart/report')
   1728         unless(msg.is_multipart())
   1729         eq(len(msg.get_payload()), 3)
   1730         # Subpart 1 is a text/plain, human readable section

   1731         subpart = msg.get_payload(0)
   1732         eq(subpart.get_content_type(), 'text/plain')
   1733         eq(subpart.get_payload(), """\
   1734 This report relates to a message you sent with the following header fields:
   1735 
   1736   Message-id: <002001c144a6$8752e060$56104586 (at] oxy.edu>
   1737   Date: Sun, 23 Sep 2001 20:10:55 -0700
   1738   From: "Ian T. Henry" <henryi (at] oxy.edu>
   1739   To: SoCal Raves <scr (at] socal-raves.org>
   1740   Subject: [scr] yeah for Ians!!
   1741 
   1742 Your message cannot be delivered to the following recipients:
   1743 
   1744   Recipient address: jangel1 (at] cougar.noc.ucla.edu
   1745   Reason: recipient reached disk quota
   1746 
   1747 """)
   1748         # Subpart 2 contains the machine parsable DSN information.  It

   1749         # consists of two blocks of headers, represented by two nested Message

   1750         # objects.

   1751         subpart = msg.get_payload(1)
   1752         eq(subpart.get_content_type(), 'message/delivery-status')
   1753         eq(len(subpart.get_payload()), 2)
   1754         # message/delivery-status should treat each block as a bunch of

   1755         # headers, i.e. a bunch of Message objects.

   1756         dsn1 = subpart.get_payload(0)
   1757         unless(isinstance(dsn1, Message))
   1758         eq(dsn1['original-envelope-id'], '0GK500B4HD0888 (at] cougar.noc.ucla.edu')
   1759         eq(dsn1.get_param('dns', header='reporting-mta'), '')
   1760         # Try a missing one <wink>

   1761         eq(dsn1.get_param('nsd', header='reporting-mta'), None)
   1762         dsn2 = subpart.get_payload(1)
   1763         unless(isinstance(dsn2, Message))
   1764         eq(dsn2['action'], 'failed')
   1765         eq(dsn2.get_params(header='original-recipient'),
   1766            [('rfc822', ''), ('jangel1 (at] cougar.noc.ucla.edu', '')])
   1767         eq(dsn2.get_param('rfc822', header='final-recipient'), '')
   1768         # Subpart 3 is the original message

   1769         subpart = msg.get_payload(2)
   1770         eq(subpart.get_content_type(), 'message/rfc822')
   1771         payload = subpart.get_payload()
   1772         unless(isinstance(payload, list))
   1773         eq(len(payload), 1)
   1774         subsubpart = payload[0]
   1775         unless(isinstance(subsubpart, Message))
   1776         eq(subsubpart.get_content_type(), 'text/plain')
   1777         eq(subsubpart['message-id'],
   1778            '<002001c144a6$8752e060$56104586 (at] oxy.edu>')
   1779 
   1780     def test_epilogue(self):
   1781         eq = self.ndiffAssertEqual
   1782         fp = openfile('msg_21.txt')
   1783         try:
   1784             text = fp.read()
   1785         finally:
   1786             fp.close()
   1787         msg = Message()
   1788         msg['From'] = 'aperson (at] dom.ain'
   1789         msg['To'] = 'bperson (at] dom.ain'
   1790         msg['Subject'] = 'Test'
   1791         msg.preamble = 'MIME message'
   1792         msg.epilogue = 'End of MIME message\n'
   1793         msg1 = MIMEText('One')
   1794         msg2 = MIMEText('Two')
   1795         msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
   1796         msg.attach(msg1)
   1797         msg.attach(msg2)
   1798         sfp = StringIO()
   1799         g = Generator(sfp)
   1800         g.flatten(msg)
   1801         eq(sfp.getvalue(), text)
   1802 
   1803     def test_no_nl_preamble(self):
   1804         eq = self.ndiffAssertEqual
   1805         msg = Message()
   1806         msg['From'] = 'aperson (at] dom.ain'
   1807         msg['To'] = 'bperson (at] dom.ain'
   1808         msg['Subject'] = 'Test'
   1809         msg.preamble = 'MIME message'
   1810         msg.epilogue = ''
   1811         msg1 = MIMEText('One')
   1812         msg2 = MIMEText('Two')
   1813         msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
   1814         msg.attach(msg1)
   1815         msg.attach(msg2)
   1816         eq(msg.as_string(), """\
   1817 From: aperson (at] dom.ain
   1818 To: bperson (at] dom.ain
   1819 Subject: Test
   1820 Content-Type: multipart/mixed; boundary="BOUNDARY"
   1821 
   1822 MIME message
   1823 --BOUNDARY
   1824 Content-Type: text/plain; charset="us-ascii"
   1825 MIME-Version: 1.0
   1826 Content-Transfer-Encoding: 7bit
   1827 
   1828 One
   1829 --BOUNDARY
   1830 Content-Type: text/plain; charset="us-ascii"
   1831 MIME-Version: 1.0
   1832 Content-Transfer-Encoding: 7bit
   1833 
   1834 Two
   1835 --BOUNDARY--
   1836 """)
   1837 
   1838     def test_default_type(self):
   1839         eq = self.assertEqual
   1840         fp = openfile('msg_30.txt')
   1841         try:
   1842             msg = email.message_from_file(fp)
   1843         finally:
   1844             fp.close()
   1845         container1 = msg.get_payload(0)
   1846         eq(container1.get_default_type(), 'message/rfc822')
   1847         eq(container1.get_content_type(), 'message/rfc822')
   1848         container2 = msg.get_payload(1)
   1849         eq(container2.get_default_type(), 'message/rfc822')
   1850         eq(container2.get_content_type(), 'message/rfc822')
   1851         container1a = container1.get_payload(0)
   1852         eq(container1a.get_default_type(), 'text/plain')
   1853         eq(container1a.get_content_type(), 'text/plain')
   1854         container2a = container2.get_payload(0)
   1855         eq(container2a.get_default_type(), 'text/plain')
   1856         eq(container2a.get_content_type(), 'text/plain')
   1857 
   1858     def test_default_type_with_explicit_container_type(self):
   1859         eq = self.assertEqual
   1860         fp = openfile('msg_28.txt')
   1861         try:
   1862             msg = email.message_from_file(fp)
   1863         finally:
   1864             fp.close()
   1865         container1 = msg.get_payload(0)
   1866         eq(container1.get_default_type(), 'message/rfc822')
   1867         eq(container1.get_content_type(), 'message/rfc822')
   1868         container2 = msg.get_payload(1)
   1869         eq(container2.get_default_type(), 'message/rfc822')
   1870         eq(container2.get_content_type(), 'message/rfc822')
   1871         container1a = container1.get_payload(0)
   1872         eq(container1a.get_default_type(), 'text/plain')
   1873         eq(container1a.get_content_type(), 'text/plain')
   1874         container2a = container2.get_payload(0)
   1875         eq(container2a.get_default_type(), 'text/plain')
   1876         eq(container2a.get_content_type(), 'text/plain')
   1877 
   1878     def test_default_type_non_parsed(self):
   1879         eq = self.assertEqual
   1880         neq = self.ndiffAssertEqual
   1881         # Set up container

   1882         container = MIMEMultipart('digest', 'BOUNDARY')
   1883         container.epilogue = ''
   1884         # Set up subparts

   1885         subpart1a = MIMEText('message 1\n')
   1886         subpart2a = MIMEText('message 2\n')
   1887         subpart1 = MIMEMessage(subpart1a)
   1888         subpart2 = MIMEMessage(subpart2a)
   1889         container.attach(subpart1)
   1890         container.attach(subpart2)
   1891         eq(subpart1.get_content_type(), 'message/rfc822')
   1892         eq(subpart1.get_default_type(), 'message/rfc822')
   1893         eq(subpart2.get_content_type(), 'message/rfc822')
   1894         eq(subpart2.get_default_type(), 'message/rfc822')
   1895         neq(container.as_string(0), '''\
   1896 Content-Type: multipart/digest; boundary="BOUNDARY"
   1897 MIME-Version: 1.0
   1898 
   1899 --BOUNDARY
   1900 Content-Type: message/rfc822
   1901 MIME-Version: 1.0
   1902 
   1903 Content-Type: text/plain; charset="us-ascii"
   1904 MIME-Version: 1.0
   1905 Content-Transfer-Encoding: 7bit
   1906 
   1907 message 1
   1908 
   1909 --BOUNDARY
   1910 Content-Type: message/rfc822
   1911 MIME-Version: 1.0
   1912 
   1913 Content-Type: text/plain; charset="us-ascii"
   1914 MIME-Version: 1.0
   1915 Content-Transfer-Encoding: 7bit
   1916 
   1917 message 2
   1918 
   1919 --BOUNDARY--
   1920 ''')
   1921         del subpart1['content-type']
   1922         del subpart1['mime-version']
   1923         del subpart2['content-type']
   1924         del subpart2['mime-version']
   1925         eq(subpart1.get_content_type(), 'message/rfc822')
   1926         eq(subpart1.get_default_type(), 'message/rfc822')
   1927         eq(subpart2.get_content_type(), 'message/rfc822')
   1928         eq(subpart2.get_default_type(), 'message/rfc822')
   1929         neq(container.as_string(0), '''\
   1930 Content-Type: multipart/digest; boundary="BOUNDARY"
   1931 MIME-Version: 1.0
   1932 
   1933 --BOUNDARY
   1934 
   1935 Content-Type: text/plain; charset="us-ascii"
   1936 MIME-Version: 1.0
   1937 Content-Transfer-Encoding: 7bit
   1938 
   1939 message 1
   1940 
   1941 --BOUNDARY
   1942 
   1943 Content-Type: text/plain; charset="us-ascii"
   1944 MIME-Version: 1.0
   1945 Content-Transfer-Encoding: 7bit
   1946 
   1947 message 2
   1948 
   1949 --BOUNDARY--
   1950 ''')
   1951 
   1952     def test_mime_attachments_in_constructor(self):
   1953         eq = self.assertEqual
   1954         text1 = MIMEText('')
   1955         text2 = MIMEText('')
   1956         msg = MIMEMultipart(_subparts=(text1, text2))
   1957         eq(len(msg.get_payload()), 2)
   1958         eq(msg.get_payload(0), text1)
   1959         eq(msg.get_payload(1), text2)
   1960 
   1961     def test_default_multipart_constructor(self):
   1962         msg = MIMEMultipart()
   1963         self.assertTrue(msg.is_multipart())
   1964 
   1965 
   1966 # A general test of parser->model->generator idempotency.  IOW, read a message

   1967 # in, parse it into a message object tree, then without touching the tree,

   1968 # regenerate the plain text.  The original text and the transformed text

   1969 # should be identical.  Note: that we ignore the Unix-From since that may

   1970 # contain a changed date.

   1971 class TestIdempotent(TestEmailBase):
   1972     def _msgobj(self, filename):
   1973         fp = openfile(filename)
   1974         try:
   1975             data = fp.read()
   1976         finally:
   1977             fp.close()
   1978         msg = email.message_from_string(data)
   1979         return msg, data
   1980 
   1981     def _idempotent(self, msg, text):
   1982         eq = self.ndiffAssertEqual
   1983         s = StringIO()
   1984         g = Generator(s, maxheaderlen=0)
   1985         g.flatten(msg)
   1986         eq(text, s.getvalue())
   1987 
   1988     def test_parse_text_message(self):
   1989         eq = self.assertEqual
   1990         msg, text = self._msgobj('msg_01.txt')
   1991         eq(msg.get_content_type(), 'text/plain')
   1992         eq(msg.get_content_maintype(), 'text')
   1993         eq(msg.get_content_subtype(), 'plain')
   1994         eq(msg.get_params()[1], ('charset', 'us-ascii'))
   1995         eq(msg.get_param('charset'), 'us-ascii')
   1996         eq(msg.preamble, None)
   1997         eq(msg.epilogue, None)
   1998         self._idempotent(msg, text)
   1999 
   2000     def test_parse_untyped_message(self):
   2001         eq = self.assertEqual
   2002         msg, text = self._msgobj('msg_03.txt')
   2003         eq(msg.get_content_type(), 'text/plain')
   2004         eq(msg.get_params(), None)
   2005         eq(msg.get_param('charset'), None)
   2006         self._idempotent(msg, text)
   2007 
   2008     def test_simple_multipart(self):
   2009         msg, text = self._msgobj('msg_04.txt')
   2010         self._idempotent(msg, text)
   2011 
   2012     def test_MIME_digest(self):
   2013         msg, text = self._msgobj('msg_02.txt')
   2014         self._idempotent(msg, text)
   2015 
   2016     def test_long_header(self):
   2017         msg, text = self._msgobj('msg_27.txt')
   2018         self._idempotent(msg, text)
   2019 
   2020     def test_MIME_digest_with_part_headers(self):
   2021         msg, text = self._msgobj('msg_28.txt')
   2022         self._idempotent(msg, text)
   2023 
   2024     def test_mixed_with_image(self):
   2025         msg, text = self._msgobj('msg_06.txt')
   2026         self._idempotent(msg, text)
   2027 
   2028     def test_multipart_report(self):
   2029         msg, text = self._msgobj('msg_05.txt')
   2030         self._idempotent(msg, text)
   2031 
   2032     def test_dsn(self):
   2033         msg, text = self._msgobj('msg_16.txt')
   2034         self._idempotent(msg, text)
   2035 
   2036     def test_preamble_epilogue(self):
   2037         msg, text = self._msgobj('msg_21.txt')
   2038         self._idempotent(msg, text)
   2039 
   2040     def test_multipart_one_part(self):
   2041         msg, text = self._msgobj('msg_23.txt')
   2042         self._idempotent(msg, text)
   2043 
   2044     def test_multipart_no_parts(self):
   2045         msg, text = self._msgobj('msg_24.txt')
   2046         self._idempotent(msg, text)
   2047 
   2048     def test_no_start_boundary(self):
   2049         msg, text = self._msgobj('msg_31.txt')
   2050         self._idempotent(msg, text)
   2051 
   2052     def test_rfc2231_charset(self):
   2053         msg, text = self._msgobj('msg_32.txt')
   2054         self._idempotent(msg, text)
   2055 
   2056     def test_more_rfc2231_parameters(self):
   2057         msg, text = self._msgobj('msg_33.txt')
   2058         self._idempotent(msg, text)
   2059 
   2060     def test_text_plain_in_a_multipart_digest(self):
   2061         msg, text = self._msgobj('msg_34.txt')
   2062         self._idempotent(msg, text)
   2063 
   2064     def test_nested_multipart_mixeds(self):
   2065         msg, text = self._msgobj('msg_12a.txt')
   2066         self._idempotent(msg, text)
   2067 
   2068     def test_message_external_body_idempotent(self):
   2069         msg, text = self._msgobj('msg_36.txt')
   2070         self._idempotent(msg, text)
   2071 
   2072     def test_content_type(self):
   2073         eq = self.assertEqual
   2074         unless = self.assertTrue
   2075         # Get a message object and reset the seek pointer for other tests

   2076         msg, text = self._msgobj('msg_05.txt')
   2077         eq(msg.get_content_type(), 'multipart/report')
   2078         # Test the Content-Type: parameters

   2079         params = {}
   2080         for pk, pv in msg.get_params():
   2081             params[pk] = pv
   2082         eq(params['report-type'], 'delivery-status')
   2083         eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
   2084         eq(msg.preamble, 'This is a MIME-encapsulated message.\n')
   2085         eq(msg.epilogue, '\n')
   2086         eq(len(msg.get_payload()), 3)
   2087         # Make sure the subparts are what we expect

   2088         msg1 = msg.get_payload(0)
   2089         eq(msg1.get_content_type(), 'text/plain')
   2090         eq(msg1.get_payload(), 'Yadda yadda yadda\n')
   2091         msg2 = msg.get_payload(1)
   2092         eq(msg2.get_content_type(), 'text/plain')
   2093         eq(msg2.get_payload(), 'Yadda yadda yadda\n')
   2094         msg3 = msg.get_payload(2)
   2095         eq(msg3.get_content_type(), 'message/rfc822')
   2096         self.assertTrue(isinstance(msg3, Message))
   2097         payload = msg3.get_payload()
   2098         unless(isinstance(payload, list))
   2099         eq(len(payload), 1)
   2100         msg4 = payload[0]
   2101         unless(isinstance(msg4, Message))
   2102         eq(msg4.get_payload(), 'Yadda yadda yadda\n')
   2103 
   2104     def test_parser(self):
   2105         eq = self.assertEqual
   2106         unless = self.assertTrue
   2107         msg, text = self._msgobj('msg_06.txt')
   2108         # Check some of the outer headers

   2109         eq(msg.get_content_type(), 'message/rfc822')
   2110         # Make sure the payload is a list of exactly one sub-Message, and that

   2111         # that submessage has a type of text/plain

   2112         payload = msg.get_payload()
   2113         unless(isinstance(payload, list))
   2114         eq(len(payload), 1)
   2115         msg1 = payload[0]
   2116         self.assertTrue(isinstance(msg1, Message))
   2117         eq(msg1.get_content_type(), 'text/plain')
   2118         self.assertTrue(isinstance(msg1.get_payload(), str))
   2119         eq(msg1.get_payload(), '\n')
   2120 
   2121 
   2122 
   2123 # Test various other bits of the package's functionality

   2124 class TestMiscellaneous(TestEmailBase):
   2125     def test_message_from_string(self):
   2126         fp = openfile('msg_01.txt')
   2127         try:
   2128             text = fp.read()
   2129         finally:
   2130             fp.close()
   2131         msg = email.message_from_string(text)
   2132         s = StringIO()
   2133         # Don't wrap/continue long headers since we're trying to test

   2134         # idempotency.

   2135         g = Generator(s, maxheaderlen=0)
   2136         g.flatten(msg)
   2137         self.assertEqual(text, s.getvalue())
   2138 
   2139     def test_message_from_file(self):
   2140         fp = openfile('msg_01.txt')
   2141         try:
   2142             text = fp.read()
   2143             fp.seek(0)
   2144             msg = email.message_from_file(fp)
   2145             s = StringIO()
   2146             # Don't wrap/continue long headers since we're trying to test

   2147             # idempotency.

   2148             g = Generator(s, maxheaderlen=0)
   2149             g.flatten(msg)
   2150             self.assertEqual(text, s.getvalue())
   2151         finally:
   2152             fp.close()
   2153 
   2154     def test_message_from_string_with_class(self):
   2155         unless = self.assertTrue
   2156         fp = openfile('msg_01.txt')
   2157         try:
   2158             text = fp.read()
   2159         finally:
   2160             fp.close()
   2161         # Create a subclass

   2162         class MyMessage(Message):
   2163             pass
   2164 
   2165         msg = email.message_from_string(text, MyMessage)
   2166         unless(isinstance(msg, MyMessage))
   2167         # Try something more complicated

   2168         fp = openfile('msg_02.txt')
   2169         try:
   2170             text = fp.read()
   2171         finally:
   2172             fp.close()
   2173         msg = email.message_from_string(text, MyMessage)
   2174         for subpart in msg.walk():
   2175             unless(isinstance(subpart, MyMessage))
   2176 
   2177     def test_message_from_file_with_class(self):
   2178         unless = self.assertTrue
   2179         # Create a subclass

   2180         class MyMessage(Message):
   2181             pass
   2182 
   2183         fp = openfile('msg_01.txt')
   2184         try:
   2185             msg = email.message_from_file(fp, MyMessage)
   2186         finally:
   2187             fp.close()
   2188         unless(isinstance(msg, MyMessage))
   2189         # Try something more complicated

   2190         fp = openfile('msg_02.txt')
   2191         try:
   2192             msg = email.message_from_file(fp, MyMessage)
   2193         finally:
   2194             fp.close()
   2195         for subpart in msg.walk():
   2196             unless(isinstance(subpart, MyMessage))
   2197 
   2198     def test__all__(self):
   2199         module = __import__('email')
   2200         all = module.__all__
   2201         all.sort()
   2202         self.assertEqual(all, [
   2203             # Old names

   2204             'Charset', 'Encoders', 'Errors', 'Generator',
   2205             'Header', 'Iterators', 'MIMEAudio', 'MIMEBase',
   2206             'MIMEImage', 'MIMEMessage', 'MIMEMultipart',
   2207             'MIMENonMultipart', 'MIMEText', 'Message',
   2208             'Parser', 'Utils', 'base64MIME',
   2209             # new names

   2210             'base64mime', 'charset', 'encoders', 'errors', 'generator',
   2211             'header', 'iterators', 'message', 'message_from_file',
   2212             'message_from_string', 'mime', 'parser',
   2213             'quopriMIME', 'quoprimime', 'utils',
   2214             ])
   2215 
   2216     def test_formatdate(self):
   2217         now = time.time()
   2218         self.assertEqual(Utils.parsedate(Utils.formatdate(now))[:6],
   2219                          time.gmtime(now)[:6])
   2220 
   2221     def test_formatdate_localtime(self):
   2222         now = time.time()
   2223         self.assertEqual(
   2224             Utils.parsedate(Utils.formatdate(now, localtime=True))[:6],
   2225             time.localtime(now)[:6])
   2226 
   2227     def test_formatdate_usegmt(self):
   2228         now = time.time()
   2229         self.assertEqual(
   2230             Utils.formatdate(now, localtime=False),
   2231             time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now)))
   2232         self.assertEqual(
   2233             Utils.formatdate(now, localtime=False, usegmt=True),
   2234             time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now)))
   2235 
   2236     def test_parsedate_none(self):
   2237         self.assertEqual(Utils.parsedate(''), None)
   2238 
   2239     def test_parsedate_compact(self):
   2240         # The FWS after the comma is optional

   2241         self.assertEqual(Utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'),
   2242                          Utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800'))
   2243 
   2244     def test_parsedate_no_dayofweek(self):
   2245         eq = self.assertEqual
   2246         eq(Utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'),
   2247            (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800))
   2248 
   2249     def test_parsedate_compact_no_dayofweek(self):
   2250         eq = self.assertEqual
   2251         eq(Utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'),
   2252            (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800))
   2253 
   2254     def test_parsedate_acceptable_to_time_functions(self):
   2255         eq = self.assertEqual
   2256         timetup = Utils.parsedate('5 Feb 2003 13:47:26 -0800')
   2257         t = int(time.mktime(timetup))
   2258         eq(time.localtime(t)[:6], timetup[:6])
   2259         eq(int(time.strftime('%Y', timetup)), 2003)
   2260         timetup = Utils.parsedate_tz('5 Feb 2003 13:47:26 -0800')
   2261         t = int(time.mktime(timetup[:9]))
   2262         eq(time.localtime(t)[:6], timetup[:6])
   2263         eq(int(time.strftime('%Y', timetup[:9])), 2003)
   2264 
   2265     def test_parsedate_y2k(self):
   2266         """Test for parsing a date with a two-digit year.
   2267 
   2268         Parsing a date with a two-digit year should return the correct
   2269         four-digit year. RFC822 allows two-digit years, but RFC2822 (which
   2270         obsoletes RFC822) requires four-digit years.
   2271 
   2272         """
   2273         self.assertEqual(Utils.parsedate_tz('25 Feb 03 13:47:26 -0800'),
   2274                          Utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'))
   2275         self.assertEqual(Utils.parsedate_tz('25 Feb 71 13:47:26 -0800'),
   2276                          Utils.parsedate_tz('25 Feb 1971 13:47:26 -0800'))
   2277 
   2278     def test_parseaddr_empty(self):
   2279         self.assertEqual(Utils.parseaddr('<>'), ('', ''))
   2280         self.assertEqual(Utils.formataddr(Utils.parseaddr('<>')), '')
   2281 
   2282     def test_noquote_dump(self):
   2283         self.assertEqual(
   2284             Utils.formataddr(('A Silly Person', 'person (at] dom.ain')),
   2285             'A Silly Person <person (at] dom.ain>')
   2286 
   2287     def test_escape_dump(self):
   2288         self.assertEqual(
   2289             Utils.formataddr(('A (Very) Silly Person', 'person (at] dom.ain')),
   2290             r'"A \(Very\) Silly Person" <person (at] dom.ain>')
   2291         a = r'A \(Special\) Person'
   2292         b = 'person (at] dom.ain'
   2293         self.assertEqual(Utils.parseaddr(Utils.formataddr((a, b))), (a, b))
   2294 
   2295     def test_escape_backslashes(self):
   2296         self.assertEqual(
   2297             Utils.formataddr(('Arthur \Backslash\ Foobar', 'person (at] dom.ain')),
   2298             r'"Arthur \\Backslash\\ Foobar" <person (at] dom.ain>')
   2299         a = r'Arthur \Backslash\ Foobar'
   2300         b = 'person (at] dom.ain'
   2301         self.assertEqual(Utils.parseaddr(Utils.formataddr((a, b))), (a, b))
   2302 
   2303     def test_name_with_dot(self):
   2304         x = 'John X. Doe <jxd (at] example.com>'
   2305         y = '"John X. Doe" <jxd (at] example.com>'
   2306         a, b = ('John X. Doe', 'jxd (at] example.com')
   2307         self.assertEqual(Utils.parseaddr(x), (a, b))
   2308         self.assertEqual(Utils.parseaddr(y), (a, b))
   2309         # formataddr() quotes the name if there's a dot in it

   2310         self.assertEqual(Utils.formataddr((a, b)), y)
   2311 
   2312     def test_parseaddr_preserves_quoted_pairs_in_addresses(self):
   2313         # issue 10005.  Note that in the third test the second pair of

   2314         # backslashes is not actually a quoted pair because it is not inside a

   2315         # comment or quoted string: the address being parsed has a quoted

   2316         # string containing a quoted backslash, followed by 'example' and two

   2317         # backslashes, followed by another quoted string containing a space and

   2318         # the word 'example'.  parseaddr copies those two backslashes

   2319         # literally.  Per rfc5322 this is not technically correct since a \ may

   2320         # not appear in an address outside of a quoted string.  It is probably

   2321         # a sensible Postel interpretation, though.

   2322         eq = self.assertEqual
   2323         eq(Utils.parseaddr('""example" example"@example.com'),
   2324           ('', '""example" example"@example.com'))
   2325         eq(Utils.parseaddr('"\\"example\\" example"@example.com'),
   2326           ('', '"\\"example\\" example"@example.com'))
   2327         eq(Utils.parseaddr('"\\\\"example\\\\" example"@example.com'),
   2328           ('', '"\\\\"example\\\\" example"@example.com'))
   2329 
   2330     def test_multiline_from_comment(self):
   2331         x = """\
   2332 Foo
   2333 \tBar <foo (at] example.com>"""
   2334         self.assertEqual(Utils.parseaddr(x), ('Foo Bar', 'foo (at] example.com'))
   2335 
   2336     def test_quote_dump(self):
   2337         self.assertEqual(
   2338             Utils.formataddr(('A Silly; Person', 'person (at] dom.ain')),
   2339             r'"A Silly; Person" <person (at] dom.ain>')
   2340 
   2341     def test_fix_eols(self):
   2342         eq = self.assertEqual
   2343         eq(Utils.fix_eols('hello'), 'hello')
   2344         eq(Utils.fix_eols('hello\n'), 'hello\r\n')
   2345         eq(Utils.fix_eols('hello\r'), 'hello\r\n')
   2346         eq(Utils.fix_eols('hello\r\n'), 'hello\r\n')
   2347         eq(Utils.fix_eols('hello\n\r'), 'hello\r\n\r\n')
   2348 
   2349     def test_charset_richcomparisons(self):
   2350         eq = self.assertEqual
   2351         ne = self.assertNotEqual
   2352         cset1 = Charset()
   2353         cset2 = Charset()
   2354         eq(cset1, 'us-ascii')
   2355         eq(cset1, 'US-ASCII')
   2356         eq(cset1, 'Us-AsCiI')
   2357         eq('us-ascii', cset1)
   2358         eq('US-ASCII', cset1)
   2359         eq('Us-AsCiI', cset1)
   2360         ne(cset1, 'usascii')
   2361         ne(cset1, 'USASCII')
   2362         ne(cset1, 'UsAsCiI')
   2363         ne('usascii', cset1)
   2364         ne('USASCII', cset1)
   2365         ne('UsAsCiI', cset1)
   2366         eq(cset1, cset2)
   2367         eq(cset2, cset1)
   2368 
   2369     def test_getaddresses(self):
   2370         eq = self.assertEqual
   2371         eq(Utils.getaddresses(['aperson (at] dom.ain (Al Person)',
   2372                                'Bud Person <bperson (at] dom.ain>']),
   2373            [('Al Person', 'aperson (at] dom.ain'),
   2374             ('Bud Person', 'bperson (at] dom.ain')])
   2375 
   2376     def test_getaddresses_nasty(self):
   2377         eq = self.assertEqual
   2378         eq(Utils.getaddresses(['foo: ;']), [('', '')])
   2379         eq(Utils.getaddresses(
   2380            ['[]*-- =~$']),
   2381            [('', ''), ('', ''), ('', '*--')])
   2382         eq(Utils.getaddresses(
   2383            ['foo: ;', '"Jason R. Mastaler" <jason (at] dom.ain>']),
   2384            [('', ''), ('Jason R. Mastaler', 'jason (at] dom.ain')])
   2385 
   2386     def test_getaddresses_embedded_comment(self):
   2387         """Test proper handling of a nested comment"""
   2388         eq = self.assertEqual
   2389         addrs = Utils.getaddresses(['User ((nested comment)) <foo (at] bar.com>'])
   2390         eq(addrs[0][1], 'foo (at] bar.com')
   2391 
   2392     def test_utils_quote_unquote(self):
   2393         eq = self.assertEqual
   2394         msg = Message()
   2395         msg.add_header('content-disposition', 'attachment',
   2396                        filename='foo\\wacky"name')
   2397         eq(msg.get_filename(), 'foo\\wacky"name')
   2398 
   2399     def test_get_body_encoding_with_bogus_charset(self):
   2400         charset = Charset('not a charset')
   2401         self.assertEqual(charset.get_body_encoding(), 'base64')
   2402 
   2403     def test_get_body_encoding_with_uppercase_charset(self):
   2404         eq = self.assertEqual
   2405         msg = Message()
   2406         msg['Content-Type'] = 'text/plain; charset=UTF-8'
   2407         eq(msg['content-type'], 'text/plain; charset=UTF-8')
   2408         charsets = msg.get_charsets()
   2409         eq(len(charsets), 1)
   2410         eq(charsets[0], 'utf-8')
   2411         charset = Charset(charsets[0])
   2412         eq(charset.get_body_encoding(), 'base64')
   2413         msg.set_payload('hello world', charset=charset)
   2414         eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n')
   2415         eq(msg.get_payload(decode=True), 'hello world')
   2416         eq(msg['content-transfer-encoding'], 'base64')
   2417         # Try another one

   2418         msg = Message()
   2419         msg['Content-Type'] = 'text/plain; charset="US-ASCII"'
   2420         charsets = msg.get_charsets()
   2421         eq(len(charsets), 1)
   2422         eq(charsets[0], 'us-ascii')
   2423         charset = Charset(charsets[0])
   2424         eq(charset.get_body_encoding(), Encoders.encode_7or8bit)
   2425         msg.set_payload('hello world', charset=charset)
   2426         eq(msg.get_payload(), 'hello world')
   2427         eq(msg['content-transfer-encoding'], '7bit')
   2428 
   2429     def test_charsets_case_insensitive(self):
   2430         lc = Charset('us-ascii')
   2431         uc = Charset('US-ASCII')
   2432         self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding())
   2433 
   2434     def test_partial_falls_inside_message_delivery_status(self):
   2435         eq = self.ndiffAssertEqual
   2436         # The Parser interface provides chunks of data to FeedParser in 8192

   2437         # byte gulps.  SF bug #1076485 found one of those chunks inside

   2438         # message/delivery-status header block, which triggered an

   2439         # unreadline() of NeedMoreData.

   2440         msg = self._msgobj('msg_43.txt')
   2441         sfp = StringIO()
   2442         Iterators._structure(msg, sfp)
   2443         eq(sfp.getvalue(), """\
   2444 multipart/report
   2445     text/plain
   2446     message/delivery-status
   2447         text/plain
   2448         text/plain
   2449         text/plain
   2450         text/plain
   2451         text/plain
   2452         text/plain
   2453         text/plain
   2454         text/plain
   2455         text/plain
   2456         text/plain
   2457         text/plain
   2458         text/plain
   2459         text/plain
   2460         text/plain
   2461         text/plain
   2462         text/plain
   2463         text/plain
   2464         text/plain
   2465         text/plain
   2466         text/plain
   2467         text/plain
   2468         text/plain
   2469         text/plain
   2470         text/plain
   2471         text/plain
   2472         text/plain
   2473     text/rfc822-headers
   2474 """)
   2475 
   2476 
   2477 
   2478 # Test the iterator/generators

   2479 class TestIterators(TestEmailBase):
   2480     def test_body_line_iterator(self):
   2481         eq = self.assertEqual
   2482         neq = self.ndiffAssertEqual
   2483         # First a simple non-multipart message

   2484         msg = self._msgobj('msg_01.txt')
   2485         it = Iterators.body_line_iterator(msg)
   2486         lines = list(it)
   2487         eq(len(lines), 6)
   2488         neq(EMPTYSTRING.join(lines), msg.get_payload())
   2489         # Now a more complicated multipart

   2490         msg = self._msgobj('msg_02.txt')
   2491         it = Iterators.body_line_iterator(msg)
   2492         lines = list(it)
   2493         eq(len(lines), 43)
   2494         fp = openfile('msg_19.txt')
   2495         try:
   2496             neq(EMPTYSTRING.join(lines), fp.read())
   2497         finally:
   2498             fp.close()
   2499 
   2500     def test_typed_subpart_iterator(self):
   2501         eq = self.assertEqual
   2502         msg = self._msgobj('msg_04.txt')
   2503         it = Iterators.typed_subpart_iterator(msg, 'text')
   2504         lines = []
   2505         subparts = 0
   2506         for subpart in it:
   2507             subparts += 1
   2508             lines.append(subpart.get_payload())
   2509         eq(subparts, 2)
   2510         eq(EMPTYSTRING.join(lines), """\
   2511 a simple kind of mirror
   2512 to reflect upon our own
   2513 a simple kind of mirror
   2514 to reflect upon our own
   2515 """)
   2516 
   2517     def test_typed_subpart_iterator_default_type(self):
   2518         eq = self.assertEqual
   2519         msg = self._msgobj('msg_03.txt')
   2520         it = Iterators.typed_subpart_iterator(msg, 'text', 'plain')
   2521         lines = []
   2522         subparts = 0
   2523         for subpart in it:
   2524             subparts += 1
   2525             lines.append(subpart.get_payload())
   2526         eq(subparts, 1)
   2527         eq(EMPTYSTRING.join(lines), """\
   2528 
   2529 Hi,
   2530 
   2531 Do you like this message?
   2532 
   2533 -Me
   2534 """)
   2535 
   2536     def test_pushCR_LF(self):
   2537         '''FeedParser BufferedSubFile.push() assumed it received complete
   2538            line endings.  A CR ending one push() followed by a LF starting
   2539            the next push() added an empty line.
   2540         '''
   2541         imt = [
   2542             ("a\r \n",  2),
   2543             ("b",       0),
   2544             ("c\n",     1),
   2545             ("",        0),
   2546             ("d\r\n",   1),
   2547             ("e\r",     0),
   2548             ("\nf",     1),
   2549             ("\r\n",    1),
   2550           ]
   2551         from email.feedparser import BufferedSubFile, NeedMoreData
   2552         bsf = BufferedSubFile()
   2553         om = []
   2554         nt = 0
   2555         for il, n in imt:
   2556             bsf.push(il)
   2557             nt += n
   2558             n1 = 0
   2559             while True:
   2560                 ol = bsf.readline()
   2561                 if ol == NeedMoreData:
   2562                     break
   2563                 om.append(ol)
   2564                 n1 += 1
   2565             self.assertTrue(n == n1)
   2566         self.assertTrue(len(om) == nt)
   2567         self.assertTrue(''.join([il for il, n in imt]) == ''.join(om))
   2568 
   2569 
   2570 
   2571 class TestParsers(TestEmailBase):
   2572     def test_header_parser(self):
   2573         eq = self.assertEqual
   2574         # Parse only the headers of a complex multipart MIME document

   2575         fp = openfile('msg_02.txt')
   2576         try:
   2577             msg = HeaderParser().parse(fp)
   2578         finally:
   2579             fp.close()
   2580         eq(msg['from'], 'ppp-request (at] zzz.org')
   2581         eq(msg['to'], 'ppp (at] zzz.org')
   2582         eq(msg.get_content_type(), 'multipart/mixed')
   2583         self.assertFalse(msg.is_multipart())
   2584         self.assertTrue(isinstance(msg.get_payload(), str))
   2585 
   2586     def test_whitespace_continuation(self):
   2587         eq = self.assertEqual
   2588         # This message contains a line after the Subject: header that has only

   2589         # whitespace, but it is not empty!

   2590         msg = email.message_from_string("""\
   2591 From: aperson (at] dom.ain
   2592 To: bperson (at] dom.ain
   2593 Subject: the next line has a space on it
   2594 \x20
   2595 Date: Mon, 8 Apr 2002 15:09:19 -0400
   2596 Message-ID: spam
   2597 
   2598 Here's the message body
   2599 """)
   2600         eq(msg['subject'], 'the next line has a space on it\n ')
   2601         eq(msg['message-id'], 'spam')
   2602         eq(msg.get_payload(), "Here's the message body\n")
   2603 
   2604     def test_whitespace_continuation_last_header(self):
   2605         eq = self.assertEqual
   2606         # Like the previous test, but the subject line is the last

   2607         # header.

   2608         msg = email.message_from_string("""\
   2609 From: aperson (at] dom.ain
   2610 To: bperson (at] dom.ain
   2611 Date: Mon, 8 Apr 2002 15:09:19 -0400
   2612 Message-ID: spam
   2613 Subject: the next line has a space on it
   2614 \x20
   2615 
   2616 Here's the message body
   2617 """)
   2618         eq(msg['subject'], 'the next line has a space on it\n ')
   2619         eq(msg['message-id'], 'spam')
   2620         eq(msg.get_payload(), "Here's the message body\n")
   2621 
   2622     def test_crlf_separation(self):
   2623         eq = self.assertEqual
   2624         fp = openfile('msg_26.txt', mode='rb')
   2625         try:
   2626             msg = Parser().parse(fp)
   2627         finally:
   2628             fp.close()
   2629         eq(len(msg.get_payload()), 2)
   2630         part1 = msg.get_payload(0)
   2631         eq(part1.get_content_type(), 'text/plain')
   2632         eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n')
   2633         part2 = msg.get_payload(1)
   2634         eq(part2.get_content_type(), 'application/riscos')
   2635 
   2636     def test_multipart_digest_with_extra_mime_headers(self):
   2637         eq = self.assertEqual
   2638         neq = self.ndiffAssertEqual
   2639         fp = openfile('msg_28.txt')
   2640         try:
   2641             msg = email.message_from_file(fp)
   2642         finally:
   2643             fp.close()
   2644         # Structure is:

   2645         # multipart/digest

   2646         #   message/rfc822

   2647         #     text/plain

   2648         #   message/rfc822

   2649         #     text/plain

   2650         eq(msg.is_multipart(), 1)
   2651         eq(len(msg.get_payload()), 2)
   2652         part1 = msg.get_payload(0)
   2653         eq(part1.get_content_type(), 'message/rfc822')
   2654         eq(part1.is_multipart(), 1)
   2655         eq(len(part1.get_payload()), 1)
   2656         part1a = part1.get_payload(0)
   2657         eq(part1a.is_multipart(), 0)
   2658         eq(part1a.get_content_type(), 'text/plain')
   2659         neq(part1a.get_payload(), 'message 1\n')
   2660         # next message/rfc822

   2661         part2 = msg.get_payload(1)
   2662         eq(part2.get_content_type(), 'message/rfc822')
   2663         eq(part2.is_multipart(), 1)
   2664         eq(len(part2.get_payload()), 1)
   2665         part2a = part2.get_payload(0)
   2666         eq(part2a.is_multipart(), 0)
   2667         eq(part2a.get_content_type(), 'text/plain')
   2668         neq(part2a.get_payload(), 'message 2\n')
   2669 
   2670     def test_three_lines(self):
   2671         # A bug report by Andrew McNamara

   2672         lines = ['From: Andrew Person <aperson (at] dom.ain',
   2673                  'Subject: Test',
   2674                  'Date: Tue, 20 Aug 2002 16:43:45 +1000']
   2675         msg = email.message_from_string(NL.join(lines))
   2676         self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000')
   2677 
   2678     def test_strip_line_feed_and_carriage_return_in_headers(self):
   2679         eq = self.assertEqual
   2680         # For [ 1002475 ] email message parser doesn't handle \r\n correctly

   2681         value1 = 'text'
   2682         value2 = 'more text'
   2683         m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % (
   2684             value1, value2)
   2685         msg = email.message_from_string(m)
   2686         eq(msg.get('Header'), value1)
   2687         eq(msg.get('Next-Header'), value2)
   2688 
   2689     def test_rfc2822_header_syntax(self):
   2690         eq = self.assertEqual
   2691         m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
   2692         msg = email.message_from_string(m)
   2693         eq(len(msg.keys()), 3)
   2694         keys = msg.keys()
   2695         keys.sort()
   2696         eq(keys, ['!"#QUX;~', '>From', 'From'])
   2697         eq(msg.get_payload(), 'body')
   2698 
   2699     def test_rfc2822_space_not_allowed_in_header(self):
   2700         eq = self.assertEqual
   2701         m = '>From foo (at] example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
   2702         msg = email.message_from_string(m)
   2703         eq(len(msg.keys()), 0)
   2704 
   2705     def test_rfc2822_one_character_header(self):
   2706         eq = self.assertEqual
   2707         m = 'A: first header\nB: second header\nCC: third header\n\nbody'
   2708         msg = email.message_from_string(m)
   2709         headers = msg.keys()
   2710         headers.sort()
   2711         eq(headers, ['A', 'B', 'CC'])
   2712         eq(msg.get_payload(), 'body')
   2713 
   2714     def test_CRLFLF_at_end_of_part(self):
   2715         # issue 5610: feedparser should not eat two chars from body part ending

   2716         # with "\r\n\n".

   2717         m = (
   2718             "From: foo (at] bar.com\n"
   2719             "To: baz\n"
   2720             "Mime-Version: 1.0\n"
   2721             "Content-Type: multipart/mixed; boundary=BOUNDARY\n"
   2722             "\n"
   2723             "--BOUNDARY\n"
   2724             "Content-Type: text/plain\n"
   2725             "\n"
   2726             "body ending with CRLF newline\r\n"
   2727             "\n"
   2728             "--BOUNDARY--\n"
   2729           )
   2730         msg = email.message_from_string(m)
   2731         self.assertTrue(msg.get_payload(0).get_payload().endswith('\r\n'))
   2732 
   2733 
   2734 class TestBase64(unittest.TestCase):
   2735     def test_len(self):
   2736         eq = self.assertEqual
   2737         eq(base64MIME.base64_len('hello'),
   2738            len(base64MIME.encode('hello', eol='')))
   2739         for size in range(15):
   2740             if   size == 0 : bsize = 0
   2741             elif size <= 3 : bsize = 4
   2742             elif size <= 6 : bsize = 8
   2743             elif size <= 9 : bsize = 12
   2744             elif size <= 12: bsize = 16
   2745             else           : bsize = 20
   2746             eq(base64MIME.base64_len('x'*size), bsize)
   2747 
   2748     def test_decode(self):
   2749         eq = self.assertEqual
   2750         eq(base64MIME.decode(''), '')
   2751         eq(base64MIME.decode('aGVsbG8='), 'hello')
   2752         eq(base64MIME.decode('aGVsbG8=', 'X'), 'hello')
   2753         eq(base64MIME.decode('aGVsbG8NCndvcmxk\n', 'X'), 'helloXworld')
   2754 
   2755     def test_encode(self):
   2756         eq = self.assertEqual
   2757         eq(base64MIME.encode(''), '')
   2758         eq(base64MIME.encode('hello'), 'aGVsbG8=\n')
   2759         # Test the binary flag

   2760         eq(base64MIME.encode('hello\n'), 'aGVsbG8K\n')
   2761         eq(base64MIME.encode('hello\n', 0), 'aGVsbG8NCg==\n')
   2762         # Test the maxlinelen arg

   2763         eq(base64MIME.encode('xxxx ' * 20, maxlinelen=40), """\
   2764 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
   2765 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
   2766 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
   2767 eHh4eCB4eHh4IA==
   2768 """)
   2769         # Test the eol argument

   2770         eq(base64MIME.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
   2771 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
   2772 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
   2773 eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
   2774 eHh4eCB4eHh4IA==\r
   2775 """)
   2776 
   2777     def test_header_encode(self):
   2778         eq = self.assertEqual
   2779         he = base64MIME.header_encode
   2780         eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=')
   2781         eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=')
   2782         # Test the charset option

   2783         eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=')
   2784         # Test the keep_eols flag

   2785         eq(he('hello\nworld', keep_eols=True),
   2786            '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
   2787         # Test the maxlinelen argument

   2788         eq(he('xxxx ' * 20, maxlinelen=40), """\
   2789 =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?=
   2790  =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?=
   2791  =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?=
   2792  =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?=
   2793  =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?=
   2794  =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""")
   2795         # Test the eol argument

   2796         eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
   2797 =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?=\r
   2798  =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?=\r
   2799  =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?=\r
   2800  =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?=\r
   2801  =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?=\r
   2802  =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""")
   2803 
   2804 
   2805 
   2806 class TestQuopri(unittest.TestCase):
   2807     def setUp(self):
   2808         self.hlit = [chr(x) for x in range(ord('a'), ord('z')+1)] + \
   2809                     [chr(x) for x in range(ord('A'), ord('Z')+1)] + \
   2810                     [chr(x) for x in range(ord('0'), ord('9')+1)] + \
   2811                     ['!', '*', '+', '-', '/', ' ']
   2812         self.hnon = [chr(x) for x in range(256) if chr(x) not in self.hlit]
   2813         assert len(self.hlit) + len(self.hnon) == 256
   2814         self.blit = [chr(x) for x in range(ord(' '), ord('~')+1)] + ['\t']
   2815         self.blit.remove('=')
   2816         self.bnon = [chr(x) for x in range(256) if chr(x) not in self.blit]
   2817         assert len(self.blit) + len(self.bnon) == 256
   2818 
   2819     def test_header_quopri_check(self):
   2820         for c in self.hlit:
   2821             self.assertFalse(quopriMIME.header_quopri_check(c))
   2822         for c in self.hnon:
   2823             self.assertTrue(quopriMIME.header_quopri_check(c))
   2824 
   2825     def test_body_quopri_check(self):
   2826         for c in self.blit:
   2827             self.assertFalse(quopriMIME.body_quopri_check(c))
   2828         for c in self.bnon:
   2829             self.assertTrue(quopriMIME.body_quopri_check(c))
   2830 
   2831     def test_header_quopri_len(self):
   2832         eq = self.assertEqual
   2833         hql = quopriMIME.header_quopri_len
   2834         enc = quopriMIME.header_encode
   2835         for s in ('hello', 'h@e@l@l@o@'):
   2836             # Empty charset and no line-endings.  7 == RFC chrome

   2837             eq(hql(s), len(enc(s, charset='', eol=''))-7)
   2838         for c in self.hlit:
   2839             eq(hql(c), 1)
   2840         for c in self.hnon:
   2841             eq(hql(c), 3)
   2842 
   2843     def test_body_quopri_len(self):
   2844         eq = self.assertEqual
   2845         bql = quopriMIME.body_quopri_len
   2846         for c in self.blit:
   2847             eq(bql(c), 1)
   2848         for c in self.bnon:
   2849             eq(bql(c), 3)
   2850 
   2851     def test_quote_unquote_idempotent(self):
   2852         for x in range(256):
   2853             c = chr(x)
   2854             self.assertEqual(quopriMIME.unquote(quopriMIME.quote(c)), c)
   2855 
   2856     def test_header_encode(self):
   2857         eq = self.assertEqual
   2858         he = quopriMIME.header_encode
   2859         eq(he('hello'), '=?iso-8859-1?q?hello?=')
   2860         eq(he('hello\nworld'), '=?iso-8859-1?q?hello=0D=0Aworld?=')
   2861         # Test the charset option

   2862         eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=')
   2863         # Test the keep_eols flag

   2864         eq(he('hello\nworld', keep_eols=True), '=?iso-8859-1?q?hello=0Aworld?=')
   2865         # Test a non-ASCII character

   2866         eq(he('hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=')
   2867         # Test the maxlinelen argument

   2868         eq(he('xxxx ' * 20, maxlinelen=40), """\
   2869 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
   2870  =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
   2871  =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?=
   2872  =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?=
   2873  =?iso-8859-1?q?x_xxxx_xxxx_?=""")
   2874         # Test the eol argument

   2875         eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
   2876 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=\r
   2877  =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=\r
   2878  =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?=\r
   2879  =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?=\r
   2880  =?iso-8859-1?q?x_xxxx_xxxx_?=""")
   2881 
   2882     def test_decode(self):
   2883         eq = self.assertEqual
   2884         eq(quopriMIME.decode(''), '')
   2885         eq(quopriMIME.decode('hello'), 'hello')
   2886         eq(quopriMIME.decode('hello', 'X'), 'hello')
   2887         eq(quopriMIME.decode('hello\nworld', 'X'), 'helloXworld')
   2888 
   2889     def test_encode(self):
   2890         eq = self.assertEqual
   2891         eq(quopriMIME.encode(''), '')
   2892         eq(quopriMIME.encode('hello'), 'hello')
   2893         # Test the binary flag

   2894         eq(quopriMIME.encode('hello\r\nworld'), 'hello\nworld')
   2895         eq(quopriMIME.encode('hello\r\nworld', 0), 'hello\nworld')
   2896         # Test the maxlinelen arg

   2897         eq(quopriMIME.encode('xxxx ' * 20, maxlinelen=40), """\
   2898 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
   2899  xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
   2900 x xxxx xxxx xxxx xxxx=20""")
   2901         # Test the eol argument

   2902         eq(quopriMIME.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
   2903 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
   2904  xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
   2905 x xxxx xxxx xxxx xxxx=20""")
   2906         eq(quopriMIME.encode("""\
   2907 one line
   2908 
   2909 two line"""), """\
   2910 one line
   2911 
   2912 two line""")
   2913 
   2914 
   2915 
   2916 # Test the Charset class

   2917 class TestCharset(unittest.TestCase):
   2918     def tearDown(self):
   2919         from email import Charset as CharsetModule
   2920         try:
   2921             del CharsetModule.CHARSETS['fake']
   2922         except KeyError:
   2923             pass
   2924 
   2925     def test_idempotent(self):
   2926         eq = self.assertEqual
   2927         # Make sure us-ascii = no Unicode conversion

   2928         c = Charset('us-ascii')
   2929         s = 'Hello World!'
   2930         sp = c.to_splittable(s)
   2931         eq(s, c.from_splittable(sp))
   2932         # test 8-bit idempotency with us-ascii

   2933         s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
   2934         sp = c.to_splittable(s)
   2935         eq(s, c.from_splittable(sp))
   2936 
   2937     def test_body_encode(self):
   2938         eq = self.assertEqual
   2939         # Try a charset with QP body encoding

   2940         c = Charset('iso-8859-1')
   2941         eq('hello w=F6rld', c.body_encode('hello w\xf6rld'))
   2942         # Try a charset with Base64 body encoding

   2943         c = Charset('utf-8')
   2944         eq('aGVsbG8gd29ybGQ=\n', c.body_encode('hello world'))
   2945         # Try a charset with None body encoding

   2946         c = Charset('us-ascii')
   2947         eq('hello world', c.body_encode('hello world'))
   2948         # Try the convert argument, where input codec != output codec

   2949         c = Charset('euc-jp')
   2950         # With apologies to Tokio Kikuchi ;)

   2951         try:
   2952             eq('\x1b$B5FCO;~IW\x1b(B',
   2953                c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
   2954             eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
   2955                c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
   2956         except LookupError:
   2957             # We probably don't have the Japanese codecs installed

   2958             pass
   2959         # Testing SF bug #625509, which we have to fake, since there are no

   2960         # built-in encodings where the header encoding is QP but the body

   2961         # encoding is not.

   2962         from email import Charset as CharsetModule
   2963         CharsetModule.add_charset('fake', CharsetModule.QP, None)
   2964         c = Charset('fake')
   2965         eq('hello w\xf6rld', c.body_encode('hello w\xf6rld'))
   2966 
   2967     def test_unicode_charset_name(self):
   2968         charset = Charset(u'us-ascii')
   2969         self.assertEqual(str(charset), 'us-ascii')
   2970         self.assertRaises(Errors.CharsetError, Charset, 'asc\xffii')
   2971 
   2972     def test_codecs_aliases_accepted(self):
   2973         charset = Charset('utf8')
   2974         self.assertEqual(str(charset), 'utf-8')
   2975 
   2976 
   2977 # Test multilingual MIME headers.

   2978 class TestHeader(TestEmailBase):
   2979     def test_simple(self):
   2980         eq = self.ndiffAssertEqual
   2981         h = Header('Hello World!')
   2982         eq(h.encode(), 'Hello World!')
   2983         h.append(' Goodbye World!')
   2984         eq(h.encode(), 'Hello World!  Goodbye World!')
   2985 
   2986     def test_simple_surprise(self):
   2987         eq = self.ndiffAssertEqual
   2988         h = Header('Hello World!')
   2989         eq(h.encode(), 'Hello World!')
   2990         h.append('Goodbye World!')
   2991         eq(h.encode(), 'Hello World! Goodbye World!')
   2992 
   2993     def test_header_needs_no_decoding(self):
   2994         h = 'no decoding needed'
   2995         self.assertEqual(decode_header(h), [(h, None)])
   2996 
   2997     def test_long(self):
   2998         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.",
   2999                    maxlinelen=76)
   3000         for l in h.encode(splitchars=' ').split('\n '):
   3001             self.assertTrue(len(l) <= 76)
   3002 
   3003     def test_multilingual(self):
   3004         eq = self.ndiffAssertEqual
   3005         g = Charset("iso-8859-1")
   3006         cz = Charset("iso-8859-2")
   3007         utf8 = Charset("utf-8")
   3008         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. "
   3009         cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. "
   3010         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")
   3011         h = Header(g_head, g)
   3012         h.append(cz_head, cz)
   3013         h.append(utf8_head, utf8)
   3014         enc = h.encode()
   3015         eq(enc, """\
   3016 =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_ko?=
   3017  =?iso-8859-1?q?mfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wan?=
   3018  =?iso-8859-1?q?dgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6?=
   3019  =?iso-8859-1?q?rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
   3020  =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
   3021  =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
   3022  =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
   3023  =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
   3024  =?utf-8?q?_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das_Oder_die_Fl?=
   3025  =?utf-8?b?aXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBo+OBpuOBhOOBvuOBmQ==?=
   3026  =?utf-8?b?44CC?=""")
   3027         eq(decode_header(enc),
   3028            [(g_head, "iso-8859-1"), (cz_head, "iso-8859-2"),
   3029             (utf8_head, "utf-8")])
   3030         ustr = unicode(h)
   3031         eq(ustr.encode('utf-8'),
   3032            'Die Mieter treten hier ein werden mit einem Foerderband '
   3033            'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
   3034            'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
   3035            'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
   3036            'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
   3037            '\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
   3038            '\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
   3039            '\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
   3040            '\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
   3041            '\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
   3042            '\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
   3043            '\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
   3044            '\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
   3045            'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
   3046            'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
   3047            '\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82')
   3048         # Test make_header()

   3049         newh = make_header(decode_header(enc))
   3050         eq(newh, enc)
   3051 
   3052     def test_header_ctor_default_args(self):
   3053         eq = self.ndiffAssertEqual
   3054         h = Header()
   3055         eq(h, '')
   3056         h.append('foo', Charset('iso-8859-1'))
   3057         eq(h, '=?iso-8859-1?q?foo?=')
   3058 
   3059     def test_explicit_maxlinelen(self):
   3060         eq = self.ndiffAssertEqual
   3061         hstr = 'A very long line that must get split to something other than at the 76th character boundary to test the non-default behavior'
   3062         h = Header(hstr)
   3063         eq(h.encode(), '''\
   3064 A very long line that must get split to something other than at the 76th
   3065  character boundary to test the non-default behavior''')
   3066         h = Header(hstr, header_name='Subject')
   3067         eq(h.encode(), '''\
   3068 A very long line that must get split to something other than at the
   3069  76th character boundary to test the non-default behavior''')
   3070         h = Header(hstr, maxlinelen=1024, header_name='Subject')
   3071         eq(h.encode(), hstr)
   3072 
   3073     def test_us_ascii_header(self):
   3074         eq = self.assertEqual
   3075         s = 'hello'
   3076         x = decode_header(s)
   3077         eq(x, [('hello', None)])
   3078         h = make_header(x)
   3079         eq(s, h.encode())
   3080 
   3081     def test_string_charset(self):
   3082         eq = self.assertEqual
   3083         h = Header()
   3084         h.append('hello', 'iso-8859-1')
   3085         eq(h, '=?iso-8859-1?q?hello?=')
   3086 
   3087 ##    def test_unicode_error(self):

   3088 ##        raises = self.assertRaises

   3089 ##        raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')

   3090 ##        raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')

   3091 ##        h = Header()

   3092 ##        raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')

   3093 ##        raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')

   3094 ##        raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')

   3095 
   3096     def test_utf8_shortest(self):
   3097         eq = self.assertEqual
   3098         h = Header(u'p\xf6stal', 'utf-8')
   3099         eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
   3100         h = Header(u'\u83ca\u5730\u6642\u592b', 'utf-8')
   3101         eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
   3102 
   3103     def test_bad_8bit_header(self):
   3104         raises = self.assertRaises
   3105         eq = self.assertEqual
   3106         x = 'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
   3107         raises(UnicodeError, Header, x)
   3108         h = Header()
   3109         raises(UnicodeError, h.append, x)
   3110         eq(str(Header(x, errors='replace')), x)
   3111         h.append(x, errors='replace')
   3112         eq(str(h), x)
   3113 
   3114     def test_encoded_adjacent_nonencoded(self):
   3115         eq = self.assertEqual
   3116         h = Header()
   3117         h.append('hello', 'iso-8859-1')
   3118         h.append('world')
   3119         s = h.encode()
   3120         eq(s, '=?iso-8859-1?q?hello?= world')
   3121         h = make_header(decode_header(s))
   3122         eq(h.encode(), s)
   3123 
   3124     def test_whitespace_eater(self):
   3125         eq = self.assertEqual
   3126         s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
   3127         parts = decode_header(s)
   3128         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)])
   3129         hdr = make_header(parts)
   3130         eq(hdr.encode(),
   3131            'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
   3132 
   3133     def test_broken_base64_header(self):
   3134         raises = self.assertRaises
   3135         s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3I ?='
   3136         raises(Errors.HeaderParseError, decode_header, s)
   3137 
   3138     # Issue 1078919

   3139     def test_ascii_add_header(self):
   3140         msg = Message()
   3141         msg.add_header('Content-Disposition', 'attachment',
   3142                        filename='bud.gif')
   3143         self.assertEqual('attachment; filename="bud.gif"',
   3144             msg['Content-Disposition'])
   3145 
   3146     def test_nonascii_add_header_via_triple(self):
   3147         msg = Message()
   3148         msg.add_header('Content-Disposition', 'attachment',
   3149             filename=('iso-8859-1', '', 'Fu\xdfballer.ppt'))
   3150         self.assertEqual(
   3151             'attachment; filename*="iso-8859-1\'\'Fu%DFballer.ppt"',
   3152             msg['Content-Disposition'])
   3153 
   3154     def test_encode_unaliased_charset(self):
   3155         # Issue 1379416: when the charset has no output conversion,

   3156         # output was accidentally getting coerced to unicode.

   3157         res = Header('abc','iso-8859-2').encode()
   3158         self.assertEqual(res, '=?iso-8859-2?q?abc?=')
   3159         self.assertIsInstance(res, str)
   3160 
   3161 
   3162 # Test RFC 2231 header parameters (en/de)coding

   3163 class TestRFC2231(TestEmailBase):
   3164     def test_get_param(self):
   3165         eq = self.assertEqual
   3166         msg = self._msgobj('msg_29.txt')
   3167         eq(msg.get_param('title'),
   3168            ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
   3169         eq(msg.get_param('title', unquote=False),
   3170            ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
   3171 
   3172     def test_set_param(self):
   3173         eq = self.assertEqual
   3174         msg = Message()
   3175         msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
   3176                       charset='us-ascii')
   3177         eq(msg.get_param('title'),
   3178            ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
   3179         msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
   3180                       charset='us-ascii', language='en')
   3181         eq(msg.get_param('title'),
   3182            ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
   3183         msg = self._msgobj('msg_01.txt')
   3184         msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
   3185                       charset='us-ascii', language='en')
   3186         self.ndiffAssertEqual(msg.as_string(), """\
   3187 Return-Path: <bbb (at] zzz.org>
   3188 Delivered-To: bbb (at] zzz.org
   3189 Received: by mail.zzz.org (Postfix, from userid 889)
   3190  id 27CEAD38CC; Fri,  4 May 2001 14:05:44 -0400 (EDT)
   3191 MIME-Version: 1.0
   3192 Content-Transfer-Encoding: 7bit
   3193 Message-ID: <15090.61304.110929.45684 (at] aaa.zzz.org>
   3194 From: bbb (at] ddd.com (John X. Doe)
   3195 To: bbb (at] zzz.org
   3196 Subject: This is a test message
   3197 Date: Fri, 4 May 2001 14:05:44 -0400
   3198 Content-Type: text/plain; charset=us-ascii;
   3199  title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
   3200 
   3201 
   3202 Hi,
   3203 
   3204 Do you like this message?
   3205 
   3206 -Me
   3207 """)
   3208 
   3209     def test_del_param(self):
   3210         eq = self.ndiffAssertEqual
   3211         msg = self._msgobj('msg_01.txt')
   3212         msg.set_param('foo', 'bar', charset='us-ascii', language='en')
   3213         msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
   3214             charset='us-ascii', language='en')
   3215         msg.del_param('foo', header='Content-Type')
   3216         eq(msg.as_string(), """\
   3217 Return-Path: <bbb (at] zzz.org>
   3218 Delivered-To: bbb (at] zzz.org
   3219 Received: by mail.zzz.org (Postfix, from userid 889)
   3220  id 27CEAD38CC; Fri,  4 May 2001 14:05:44 -0400 (EDT)
   3221 MIME-Version: 1.0
   3222 Content-Transfer-Encoding: 7bit
   3223 Message-ID: <15090.61304.110929.45684 (at] aaa.zzz.org>
   3224 From: bbb (at] ddd.com (John X. Doe)
   3225 To: bbb (at] zzz.org
   3226 Subject: This is a test message
   3227 Date: Fri, 4 May 2001 14:05:44 -0400
   3228 Content-Type: text/plain; charset="us-ascii";
   3229  title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
   3230 
   3231 
   3232 Hi,
   3233 
   3234 Do you like this message?
   3235 
   3236 -Me
   3237 """)
   3238 
   3239     def test_rfc2231_get_content_charset(self):
   3240         eq = self.assertEqual
   3241         msg = self._msgobj('msg_32.txt')
   3242         eq(msg.get_content_charset(), 'us-ascii')
   3243 
   3244     def test_rfc2231_no_language_or_charset(self):
   3245         m = '''\
   3246 Content-Transfer-Encoding: 8bit
   3247 Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
   3248 Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
   3249 
   3250 '''
   3251         msg = email.message_from_string(m)
   3252         param = msg.get_param('NAME')
   3253         self.assertFalse(isinstance(param, tuple))
   3254         self.assertEqual(
   3255             param,
   3256             'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
   3257 
   3258     def test_rfc2231_no_language_or_charset_in_filename(self):
   3259         m = '''\
   3260 Content-Disposition: inline;
   3261 \tfilename*0*="''This%20is%20even%20more%20";
   3262 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3263 \tfilename*2="is it not.pdf"
   3264 
   3265 '''
   3266         msg = email.message_from_string(m)
   3267         self.assertEqual(msg.get_filename(),
   3268                          'This is even more ***fun*** is it not.pdf')
   3269 
   3270     def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
   3271         m = '''\
   3272 Content-Disposition: inline;
   3273 \tfilename*0*="''This%20is%20even%20more%20";
   3274 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3275 \tfilename*2="is it not.pdf"
   3276 
   3277 '''
   3278         msg = email.message_from_string(m)
   3279         self.assertEqual(msg.get_filename(),
   3280                          'This is even more ***fun*** is it not.pdf')
   3281 
   3282     def test_rfc2231_partly_encoded(self):
   3283         m = '''\
   3284 Content-Disposition: inline;
   3285 \tfilename*0="''This%20is%20even%20more%20";
   3286 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3287 \tfilename*2="is it not.pdf"
   3288 
   3289 '''
   3290         msg = email.message_from_string(m)
   3291         self.assertEqual(
   3292             msg.get_filename(),
   3293             'This%20is%20even%20more%20***fun*** is it not.pdf')
   3294 
   3295     def test_rfc2231_partly_nonencoded(self):
   3296         m = '''\
   3297 Content-Disposition: inline;
   3298 \tfilename*0="This%20is%20even%20more%20";
   3299 \tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
   3300 \tfilename*2="is it not.pdf"
   3301 
   3302 '''
   3303         msg = email.message_from_string(m)
   3304         self.assertEqual(
   3305             msg.get_filename(),
   3306             'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
   3307 
   3308     def test_rfc2231_no_language_or_charset_in_boundary(self):
   3309         m = '''\
   3310 Content-Type: multipart/alternative;
   3311 \tboundary*0*="''This%20is%20even%20more%20";
   3312 \tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3313 \tboundary*2="is it not.pdf"
   3314 
   3315 '''
   3316         msg = email.message_from_string(m)
   3317         self.assertEqual(msg.get_boundary(),
   3318                          'This is even more ***fun*** is it not.pdf')
   3319 
   3320     def test_rfc2231_no_language_or_charset_in_charset(self):
   3321         # This is a nonsensical charset value, but tests the code anyway

   3322         m = '''\
   3323 Content-Type: text/plain;
   3324 \tcharset*0*="This%20is%20even%20more%20";
   3325 \tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3326 \tcharset*2="is it not.pdf"
   3327 
   3328 '''
   3329         msg = email.message_from_string(m)
   3330         self.assertEqual(msg.get_content_charset(),
   3331                          'this is even more ***fun*** is it not.pdf')
   3332 
   3333     def test_rfc2231_bad_encoding_in_filename(self):
   3334         m = '''\
   3335 Content-Disposition: inline;
   3336 \tfilename*0*="bogus'xx'This%20is%20even%20more%20";
   3337 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3338 \tfilename*2="is it not.pdf"
   3339 
   3340 '''
   3341         msg = email.message_from_string(m)
   3342         self.assertEqual(msg.get_filename(),
   3343                          'This is even more ***fun*** is it not.pdf')
   3344 
   3345     def test_rfc2231_bad_encoding_in_charset(self):
   3346         m = """\
   3347 Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
   3348 
   3349 """
   3350         msg = email.message_from_string(m)
   3351         # This should return None because non-ascii characters in the charset

   3352         # are not allowed.

   3353         self.assertEqual(msg.get_content_charset(), None)
   3354 
   3355     def test_rfc2231_bad_character_in_charset(self):
   3356         m = """\
   3357 Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D
   3358 
   3359 """
   3360         msg = email.message_from_string(m)
   3361         # This should return None because non-ascii characters in the charset

   3362         # are not allowed.

   3363         self.assertEqual(msg.get_content_charset(), None)
   3364 
   3365     def test_rfc2231_bad_character_in_filename(self):
   3366         m = '''\
   3367 Content-Disposition: inline;
   3368 \tfilename*0*="ascii'xx'This%20is%20even%20more%20";
   3369 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
   3370 \tfilename*2*="is it not.pdf%E2"
   3371 
   3372 '''
   3373         msg = email.message_from_string(m)
   3374         self.assertEqual(msg.get_filename(),
   3375                          u'This is even more ***fun*** is it not.pdf\ufffd')
   3376 
   3377     def test_rfc2231_unknown_encoding(self):
   3378         m = """\
   3379 Content-Transfer-Encoding: 8bit
   3380 Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
   3381 
   3382 """
   3383         msg = email.message_from_string(m)
   3384         self.assertEqual(msg.get_filename(), 'myfile.txt')
   3385 
   3386     def test_rfc2231_single_tick_in_filename_extended(self):
   3387         eq = self.assertEqual
   3388         m = """\
   3389 Content-Type: application/x-foo;
   3390 \tname*0*=\"Frank's\"; name*1*=\" Document\"
   3391 
   3392 """
   3393         msg = email.message_from_string(m)
   3394         charset, language, s = msg.get_param('name')
   3395         eq(charset, None)
   3396         eq(language, None)
   3397         eq(s, "Frank's Document")
   3398 
   3399     def test_rfc2231_single_tick_in_filename(self):
   3400         m = """\
   3401 Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
   3402 
   3403 """
   3404         msg = email.message_from_string(m)
   3405         param = msg.get_param('name')
   3406         self.assertFalse(isinstance(param, tuple))
   3407         self.assertEqual(param, "Frank's Document")
   3408 
   3409     def test_rfc2231_tick_attack_extended(self):
   3410         eq = self.assertEqual
   3411         m = """\
   3412 Content-Type: application/x-foo;
   3413 \tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
   3414 
   3415 """
   3416         msg = email.message_from_string(m)
   3417         charset, language, s = msg.get_param('name')
   3418         eq(charset, 'us-ascii')
   3419         eq(language, 'en-us')
   3420         eq(s, "Frank's Document")
   3421 
   3422     def test_rfc2231_tick_attack(self):
   3423         m = """\
   3424 Content-Type: application/x-foo;
   3425 \tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
   3426 
   3427 """
   3428         msg = email.message_from_string(m)
   3429         param = msg.get_param('name')
   3430         self.assertFalse(isinstance(param, tuple))
   3431         self.assertEqual(param, "us-ascii'en-us'Frank's Document")
   3432 
   3433     def test_rfc2231_no_extended_values(self):
   3434         eq = self.assertEqual
   3435         m = """\
   3436 Content-Type: application/x-foo; name=\"Frank's Document\"
   3437 
   3438 """
   3439         msg = email.message_from_string(m)
   3440         eq(msg.get_param('name'), "Frank's Document")
   3441 
   3442     def test_rfc2231_encoded_then_unencoded_segments(self):
   3443         eq = self.assertEqual
   3444         m = """\
   3445 Content-Type: application/x-foo;
   3446 \tname*0*=\"us-ascii'en-us'My\";
   3447 \tname*1=\" Document\";
   3448 \tname*2*=\" For You\"
   3449 
   3450 """
   3451         msg = email.message_from_string(m)
   3452         charset, language, s = msg.get_param('name')
   3453         eq(charset, 'us-ascii')
   3454         eq(language, 'en-us')
   3455         eq(s, 'My Document For You')
   3456 
   3457     def test_rfc2231_unencoded_then_encoded_segments(self):
   3458         eq = self.assertEqual
   3459         m = """\
   3460 Content-Type: application/x-foo;
   3461 \tname*0=\"us-ascii'en-us'My\";
   3462 \tname*1*=\" Document\";
   3463 \tname*2*=\" For You\"
   3464 
   3465 """
   3466         msg = email.message_from_string(m)
   3467         charset, language, s = msg.get_param('name')
   3468         eq(charset, 'us-ascii')
   3469         eq(language, 'en-us')
   3470         eq(s, 'My Document For You')
   3471 
   3472 
   3473 
   3474 # Tests to ensure that signed parts of an email are completely preserved, as

   3475 # required by RFC1847 section 2.1.  Note that these are incomplete, because the

   3476 # email package does not currently always preserve the body.  See issue 1670765.

   3477 class TestSigned(TestEmailBase):
   3478 
   3479     def _msg_and_obj(self, filename):
   3480         fp = openfile(findfile(filename))
   3481         try:
   3482             original = fp.read()
   3483             msg = email.message_from_string(original)
   3484         finally:
   3485             fp.close()
   3486         return original, msg
   3487 
   3488     def _signed_parts_eq(self, original, result):
   3489         # Extract the first mime part of each message

   3490         import re
   3491         repart = re.compile(r'^--([^\n]+)\n(.*?)\n--\1$', re.S | re.M)
   3492         inpart = repart.search(original).group(2)
   3493         outpart = repart.search(result).group(2)
   3494         self.assertEqual(outpart, inpart)
   3495 
   3496     def test_long_headers_as_string(self):
   3497         original, msg = self._msg_and_obj('msg_45.txt')
   3498         result = msg.as_string()
   3499         self._signed_parts_eq(original, result)
   3500 
   3501     def test_long_headers_flatten(self):
   3502         original, msg = self._msg_and_obj('msg_45.txt')
   3503         fp = StringIO()
   3504         Generator(fp).flatten(msg)
   3505         result = fp.getvalue()
   3506         self._signed_parts_eq(original, result)
   3507 
   3508 
   3509 
   3510 def _testclasses():
   3511     mod = sys.modules[__name__]
   3512     return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
   3513 
   3514 
   3515 def suite():
   3516     suite = unittest.TestSuite()
   3517     for testclass in _testclasses():
   3518         suite.addTest(unittest.makeSuite(testclass))
   3519     return suite
   3520 
   3521 
   3522 def test_main():
   3523     for testclass in _testclasses():
   3524         run_unittest(testclass)
   3525 
   3526 
   3527 
   3528 if __name__ == '__main__':
   3529     unittest.main(defaultTest='suite')
   3530