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