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