PageRenderTime 61ms CodeModel.GetById 134ms app.highlight 2378ms RepoModel.GetById 111ms app.codeStats 2ms

/Lib/email/test/test_email_renamed.py

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