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