1# Copyright (C) 2001-2007 Python Software Foundation
2# Contact: email-sig@python.org
3# email package unit tests
4
5import os
6import sys
7import time
8import base64
9import difflib
10import unittest
11import warnings
12from cStringIO import StringIO
13
14import email
15
16from email.charset import Charset
17from email.header import Header, decode_header, make_header
18from email.parser import Parser, HeaderParser
19from email.generator import Generator, DecodedGenerator
20from email.message import Message
21from email.mime.application import MIMEApplication
22from email.mime.audio import MIMEAudio
23from email.mime.text import MIMEText
24from email.mime.image import MIMEImage
25from email.mime.base import MIMEBase
26from email.mime.message import MIMEMessage
27from email.mime.multipart import MIMEMultipart
28from email import utils
29from email import errors
30from email import encoders
31from email import iterators
32from email import base64mime
33from email import quoprimime
34
35from test.test_support import findfile, run_unittest
36from email.test import __file__ as landmark
37
38
39NL = '\n'
40EMPTYSTRING = ''
41SPACE = ' '
42
43
44
45def openfile(filename, mode='r'):
46    path = os.path.join(os.path.dirname(landmark), 'data', filename)
47    return open(path, mode)
48
49
50
51# Base test class
52class TestEmailBase(unittest.TestCase):
53    def ndiffAssertEqual(self, first, second):
54        """Like assertEqual except use ndiff for readable output."""
55        if first != second:
56            sfirst = str(first)
57            ssecond = str(second)
58            diff = difflib.ndiff(sfirst.splitlines(), ssecond.splitlines())
59            fp = StringIO()
60            print >> fp, NL, NL.join(diff)
61            raise self.failureException, fp.getvalue()
62
63    def _msgobj(self, filename):
64        fp = openfile(findfile(filename))
65        try:
66            msg = email.message_from_file(fp)
67        finally:
68            fp.close()
69        return msg
70
71
72
73# Test various aspects of the Message class's API
74class TestMessageAPI(TestEmailBase):
75    def test_get_all(self):
76        eq = self.assertEqual
77        msg = self._msgobj('msg_20.txt')
78        eq(msg.get_all('cc'), ['ccc@zzz.org', 'ddd@zzz.org', 'eee@zzz.org'])
79        eq(msg.get_all('xx', 'n/a'), 'n/a')
80
81    def test_getset_charset(self):
82        eq = self.assertEqual
83        msg = Message()
84        eq(msg.get_charset(), None)
85        charset = Charset('iso-8859-1')
86        msg.set_charset(charset)
87        eq(msg['mime-version'], '1.0')
88        eq(msg.get_content_type(), 'text/plain')
89        eq(msg['content-type'], 'text/plain; charset="iso-8859-1"')
90        eq(msg.get_param('charset'), 'iso-8859-1')
91        eq(msg['content-transfer-encoding'], 'quoted-printable')
92        eq(msg.get_charset().input_charset, 'iso-8859-1')
93        # Remove the charset
94        msg.set_charset(None)
95        eq(msg.get_charset(), None)
96        eq(msg['content-type'], 'text/plain')
97        # Try adding a charset when there's already MIME headers present
98        msg = Message()
99        msg['MIME-Version'] = '2.0'
100        msg['Content-Type'] = 'text/x-weird'
101        msg['Content-Transfer-Encoding'] = 'quinted-puntable'
102        msg.set_charset(charset)
103        eq(msg['mime-version'], '2.0')
104        eq(msg['content-type'], 'text/x-weird; charset="iso-8859-1"')
105        eq(msg['content-transfer-encoding'], 'quinted-puntable')
106
107    def test_set_charset_from_string(self):
108        eq = self.assertEqual
109        msg = Message()
110        msg.set_charset('us-ascii')
111        eq(msg.get_charset().input_charset, 'us-ascii')
112        eq(msg['content-type'], 'text/plain; charset="us-ascii"')
113
114    def test_set_payload_with_charset(self):
115        msg = Message()
116        charset = Charset('iso-8859-1')
117        msg.set_payload('This is a string payload', charset)
118        self.assertEqual(msg.get_charset().input_charset, 'iso-8859-1')
119
120    def test_get_charsets(self):
121        eq = self.assertEqual
122
123        msg = self._msgobj('msg_08.txt')
124        charsets = msg.get_charsets()
125        eq(charsets, [None, 'us-ascii', 'iso-8859-1', 'iso-8859-2', 'koi8-r'])
126
127        msg = self._msgobj('msg_09.txt')
128        charsets = msg.get_charsets('dingbat')
129        eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat',
130                      'koi8-r'])
131
132        msg = self._msgobj('msg_12.txt')
133        charsets = msg.get_charsets()
134        eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2',
135                      'iso-8859-3', 'us-ascii', 'koi8-r'])
136
137    def test_get_filename(self):
138        eq = self.assertEqual
139
140        msg = self._msgobj('msg_04.txt')
141        filenames = [p.get_filename() for p in msg.get_payload()]
142        eq(filenames, ['msg.txt', 'msg.txt'])
143
144        msg = self._msgobj('msg_07.txt')
145        subpart = msg.get_payload(1)
146        eq(subpart.get_filename(), 'dingusfish.gif')
147
148    def test_get_filename_with_name_parameter(self):
149        eq = self.assertEqual
150
151        msg = self._msgobj('msg_44.txt')
152        filenames = [p.get_filename() for p in msg.get_payload()]
153        eq(filenames, ['msg.txt', 'msg.txt'])
154
155    def test_get_boundary(self):
156        eq = self.assertEqual
157        msg = self._msgobj('msg_07.txt')
158        # No quotes!
159        eq(msg.get_boundary(), 'BOUNDARY')
160
161    def test_set_boundary(self):
162        eq = self.assertEqual
163        # This one has no existing boundary parameter, but the Content-Type:
164        # header appears fifth.
165        msg = self._msgobj('msg_01.txt')
166        msg.set_boundary('BOUNDARY')
167        header, value = msg.items()[4]
168        eq(header.lower(), 'content-type')
169        eq(value, 'text/plain; charset="us-ascii"; boundary="BOUNDARY"')
170        # This one has a Content-Type: header, with a boundary, stuck in the
171        # middle of its headers.  Make sure the order is preserved; it should
172        # be fifth.
173        msg = self._msgobj('msg_04.txt')
174        msg.set_boundary('BOUNDARY')
175        header, value = msg.items()[4]
176        eq(header.lower(), 'content-type')
177        eq(value, 'multipart/mixed; boundary="BOUNDARY"')
178        # And this one has no Content-Type: header at all.
179        msg = self._msgobj('msg_03.txt')
180        self.assertRaises(errors.HeaderParseError,
181                          msg.set_boundary, 'BOUNDARY')
182
183    def test_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.assertTrue('from' in msg)
235        self.assertTrue('From' in msg)
236        self.assertTrue('FROM' in msg)
237        self.assertTrue('to' in msg)
238        self.assertTrue('To' in msg)
239        self.assertTrue('TO' in 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'] = 'test@dom.ain'
660        msg['References'] = SPACE.join(['<%d@dom.ain>' % 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: test@dom.ain
667References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain>
668 <5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain>
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 <mailman-admin@babylon.socal-raves.org>; 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 <mailman-admin@babylon.socal-raves.org>; 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 <mailman-admin@babylon.socal-raves.org>; 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 <mailman-admin@babylon.socal-raves.org>;
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 <mailman-admin@babylon.socal-raves.org>;
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 <mailman-admin@babylon.socal-raves.org>;
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 test@mems-exchange.org; 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 test@mems-exchange.org; 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 <a-very-long-address@example.com>'
734        eq(msg.as_string(), """\
735Reply-To: Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte <a-very-long-address@example.com>
736
737""")
738
739    def test_long_to_header(self):
740        eq = self.ndiffAssertEqual
741        to = '"Someone Test #A" <someone@eecs.umich.edu>,<someone@eecs.umich.edu>,"Someone Test #B" <someone@umich.edu>, "Someone Test #C" <someone@eecs.umich.edu>, "Someone Test #D" <someone@eecs.umich.edu>'
742        msg = Message()
743        msg['To'] = to
744        eq(msg.as_string(0), '''\
745To: "Someone Test #A" <someone@eecs.umich.edu>, <someone@eecs.umich.edu>,
746 "Someone Test #B" <someone@umich.edu>,
747 "Someone Test #C" <someone@eecs.umich.edu>,
748 "Someone Test #D" <someone@eecs.umich.edu>
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:spamassassin-talk-request@lists.sourceforge.net?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:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
849List: List-Unsubscribe: <https://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
850 <mailto:spamassassin-talk-request@lists.sourceforge.net?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'] = 'aaa@bbb.org'
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: aaa@bbb.org
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: aaa@bbb.org
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        unless = self.assertTrue
920        self._au.add_header('Content-Disposition', 'attachment',
921                            filename='audiotest.au')
922        eq(self._au['content-disposition'],
923           'attachment; filename="audiotest.au"')
924        eq(self._au.get_params(header='content-disposition'),
925           [('attachment', ''), ('filename', 'audiotest.au')])
926        eq(self._au.get_param('filename', header='content-disposition'),
927           'audiotest.au')
928        missing = []
929        eq(self._au.get_param('attachment', header='content-disposition'), '')
930        unless(self._au.get_param('foo', failobj=missing,
931                                  header='content-disposition') is missing)
932        # Try some missing stuff
933        unless(self._au.get_param('foobar', missing) is missing)
934        unless(self._au.get_param('attachment', missing,
935                                  header='foobar') is 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        unless = self.assertTrue
963        self._im.add_header('Content-Disposition', 'attachment',
964                            filename='dingusfish.gif')
965        eq(self._im['content-disposition'],
966           'attachment; filename="dingusfish.gif"')
967        eq(self._im.get_params(header='content-disposition'),
968           [('attachment', ''), ('filename', 'dingusfish.gif')])
969        eq(self._im.get_param('filename', header='content-disposition'),
970           'dingusfish.gif')
971        missing = []
972        eq(self._im.get_param('attachment', header='content-disposition'), '')
973        unless(self._im.get_param('foo', failobj=missing,
974                                  header='content-disposition') is missing)
975        # Try some missing stuff
976        unless(self._im.get_param('foobar', missing) is missing)
977        unless(self._im.get_param('attachment', missing,
978                                  header='foobar') is 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
998
999# Test the basic MIMEText class
1000class TestMIMEText(unittest.TestCase):
1001    def setUp(self):
1002        self._msg = MIMEText('hello there')
1003
1004    def test_types(self):
1005        eq = self.assertEqual
1006        unless = self.assertTrue
1007        eq(self._msg.get_content_type(), 'text/plain')
1008        eq(self._msg.get_param('charset'), 'us-ascii')
1009        missing = []
1010        unless(self._msg.get_param('foobar', missing) is missing)
1011        unless(self._msg.get_param('charset', missing, header='foobar')
1012               is missing)
1013
1014    def test_payload(self):
1015        self.assertEqual(self._msg.get_payload(), 'hello there')
1016        self.assertTrue(not self._msg.is_multipart())
1017
1018    def test_charset(self):
1019        eq = self.assertEqual
1020        msg = MIMEText('hello there', _charset='us-ascii')
1021        eq(msg.get_charset().input_charset, 'us-ascii')
1022        eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1023
1024
1025
1026# Test complicated multipart/* messages
1027class TestMultipart(TestEmailBase):
1028    def setUp(self):
1029        fp = openfile('PyBanner048.gif')
1030        try:
1031            data = fp.read()
1032        finally:
1033            fp.close()
1034
1035        container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
1036        image = MIMEImage(data, name='dingusfish.gif')
1037        image.add_header('content-disposition', 'attachment',
1038                         filename='dingusfish.gif')
1039        intro = MIMEText('''\
1040Hi there,
1041
1042This is the dingus fish.
1043''')
1044        container.attach(intro)
1045        container.attach(image)
1046        container['From'] = 'Barry <barry@digicool.com>'
1047        container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>'
1048        container['Subject'] = 'Here is your dingus fish'
1049
1050        now = 987809702.54848599
1051        timetuple = time.localtime(now)
1052        if timetuple[-1] == 0:
1053            tzsecs = time.timezone
1054        else:
1055            tzsecs = time.altzone
1056        if tzsecs > 0:
1057            sign = '-'
1058        else:
1059            sign = '+'
1060        tzoffset = ' %s%04d' % (sign, tzsecs // 36)
1061        container['Date'] = time.strftime(
1062            '%a, %d %b %Y %H:%M:%S',
1063            time.localtime(now)) + tzoffset
1064        self._msg = container
1065        self._im = image
1066        self._txt = intro
1067
1068    def test_hierarchy(self):
1069        # convenience
1070        eq = self.assertEqual
1071        unless = self.assertTrue
1072        raises = self.assertRaises
1073        # tests
1074        m = self._msg
1075        unless(m.is_multipart())
1076        eq(m.get_content_type(), 'multipart/mixed')
1077        eq(len(m.get_payload()), 2)
1078        raises(IndexError, m.get_payload, 2)
1079        m0 = m.get_payload(0)
1080        m1 = m.get_payload(1)
1081        unless(m0 is self._txt)
1082        unless(m1 is self._im)
1083        eq(m.get_payload(), [m0, m1])
1084        unless(not m0.is_multipart())
1085        unless(not m1.is_multipart())
1086
1087    def test_empty_multipart_idempotent(self):
1088        text = """\
1089Content-Type: multipart/mixed; boundary="BOUNDARY"
1090MIME-Version: 1.0
1091Subject: A subject
1092To: aperson@dom.ain
1093From: bperson@dom.ain
1094
1095
1096--BOUNDARY
1097
1098
1099--BOUNDARY--
1100"""
1101        msg = Parser().parsestr(text)
1102        self.ndiffAssertEqual(text, msg.as_string())
1103
1104    def test_no_parts_in_a_multipart_with_none_epilogue(self):
1105        outer = MIMEBase('multipart', 'mixed')
1106        outer['Subject'] = 'A subject'
1107        outer['To'] = 'aperson@dom.ain'
1108        outer['From'] = 'bperson@dom.ain'
1109        outer.set_boundary('BOUNDARY')
1110        self.ndiffAssertEqual(outer.as_string(), '''\
1111Content-Type: multipart/mixed; boundary="BOUNDARY"
1112MIME-Version: 1.0
1113Subject: A subject
1114To: aperson@dom.ain
1115From: bperson@dom.ain
1116
1117--BOUNDARY
1118
1119--BOUNDARY--''')
1120
1121    def test_no_parts_in_a_multipart_with_empty_epilogue(self):
1122        outer = MIMEBase('multipart', 'mixed')
1123        outer['Subject'] = 'A subject'
1124        outer['To'] = 'aperson@dom.ain'
1125        outer['From'] = 'bperson@dom.ain'
1126        outer.preamble = ''
1127        outer.epilogue = ''
1128        outer.set_boundary('BOUNDARY')
1129        self.ndiffAssertEqual(outer.as_string(), '''\
1130Content-Type: multipart/mixed; boundary="BOUNDARY"
1131MIME-Version: 1.0
1132Subject: A subject
1133To: aperson@dom.ain
1134From: bperson@dom.ain
1135
1136
1137--BOUNDARY
1138
1139--BOUNDARY--
1140''')
1141
1142    def test_one_part_in_a_multipart(self):
1143        eq = self.ndiffAssertEqual
1144        outer = MIMEBase('multipart', 'mixed')
1145        outer['Subject'] = 'A subject'
1146        outer['To'] = 'aperson@dom.ain'
1147        outer['From'] = 'bperson@dom.ain'
1148        outer.set_boundary('BOUNDARY')
1149        msg = MIMEText('hello world')
1150        outer.attach(msg)
1151        eq(outer.as_string(), '''\
1152Content-Type: multipart/mixed; boundary="BOUNDARY"
1153MIME-Version: 1.0
1154Subject: A subject
1155To: aperson@dom.ain
1156From: bperson@dom.ain
1157
1158--BOUNDARY
1159Content-Type: text/plain; charset="us-ascii"
1160MIME-Version: 1.0
1161Content-Transfer-Encoding: 7bit
1162
1163hello world
1164--BOUNDARY--''')
1165
1166    def test_seq_parts_in_a_multipart_with_empty_preamble(self):
1167        eq = self.ndiffAssertEqual
1168        outer = MIMEBase('multipart', 'mixed')
1169        outer['Subject'] = 'A subject'
1170        outer['To'] = 'aperson@dom.ain'
1171        outer['From'] = 'bperson@dom.ain'
1172        outer.preamble = ''
1173        msg = MIMEText('hello world')
1174        outer.attach(msg)
1175        outer.set_boundary('BOUNDARY')
1176        eq(outer.as_string(), '''\
1177Content-Type: multipart/mixed; boundary="BOUNDARY"
1178MIME-Version: 1.0
1179Subject: A subject
1180To: aperson@dom.ain
1181From: bperson@dom.ain
1182
1183
1184--BOUNDARY
1185Content-Type: text/plain; charset="us-ascii"
1186MIME-Version: 1.0
1187Content-Transfer-Encoding: 7bit
1188
1189hello world
1190--BOUNDARY--''')
1191
1192
1193    def test_seq_parts_in_a_multipart_with_none_preamble(self):
1194        eq = self.ndiffAssertEqual
1195        outer = MIMEBase('multipart', 'mixed')
1196        outer['Subject'] = 'A subject'
1197        outer['To'] = 'aperson@dom.ain'
1198        outer['From'] = 'bperson@dom.ain'
1199        outer.preamble = None
1200        msg = MIMEText('hello world')
1201        outer.attach(msg)
1202        outer.set_boundary('BOUNDARY')
1203        eq(outer.as_string(), '''\
1204Content-Type: multipart/mixed; boundary="BOUNDARY"
1205MIME-Version: 1.0
1206Subject: A subject
1207To: aperson@dom.ain
1208From: bperson@dom.ain
1209
1210--BOUNDARY
1211Content-Type: text/plain; charset="us-ascii"
1212MIME-Version: 1.0
1213Content-Transfer-Encoding: 7bit
1214
1215hello world
1216--BOUNDARY--''')
1217
1218
1219    def test_seq_parts_in_a_multipart_with_none_epilogue(self):
1220        eq = self.ndiffAssertEqual
1221        outer = MIMEBase('multipart', 'mixed')
1222        outer['Subject'] = 'A subject'
1223        outer['To'] = 'aperson@dom.ain'
1224        outer['From'] = 'bperson@dom.ain'
1225        outer.epilogue = None
1226        msg = MIMEText('hello world')
1227        outer.attach(msg)
1228        outer.set_boundary('BOUNDARY')
1229        eq(outer.as_string(), '''\
1230Content-Type: multipart/mixed; boundary="BOUNDARY"
1231MIME-Version: 1.0
1232Subject: A subject
1233To: aperson@dom.ain
1234From: bperson@dom.ain
1235
1236--BOUNDARY
1237Content-Type: text/plain; charset="us-ascii"
1238MIME-Version: 1.0
1239Content-Transfer-Encoding: 7bit
1240
1241hello world
1242--BOUNDARY--''')
1243
1244
1245    def test_seq_parts_in_a_multipart_with_empty_epilogue(self):
1246        eq = self.ndiffAssertEqual
1247        outer = MIMEBase('multipart', 'mixed')
1248        outer['Subject'] = 'A subject'
1249        outer['To'] = 'aperson@dom.ain'
1250        outer['From'] = 'bperson@dom.ain'
1251        outer.epilogue = ''
1252        msg = MIMEText('hello world')
1253        outer.attach(msg)
1254        outer.set_boundary('BOUNDARY')
1255        eq(outer.as_string(), '''\
1256Content-Type: multipart/mixed; boundary="BOUNDARY"
1257MIME-Version: 1.0
1258Subject: A subject
1259To: aperson@dom.ain
1260From: bperson@dom.ain
1261
1262--BOUNDARY
1263Content-Type: text/plain; charset="us-ascii"
1264MIME-Version: 1.0
1265Content-Transfer-Encoding: 7bit
1266
1267hello world
1268--BOUNDARY--
1269''')
1270
1271
1272    def test_seq_parts_in_a_multipart_with_nl_epilogue(self):
1273        eq = self.ndiffAssertEqual
1274        outer = MIMEBase('multipart', 'mixed')
1275        outer['Subject'] = 'A subject'
1276        outer['To'] = 'aperson@dom.ain'
1277        outer['From'] = 'bperson@dom.ain'
1278        outer.epilogue = '\n'
1279        msg = MIMEText('hello world')
1280        outer.attach(msg)
1281        outer.set_boundary('BOUNDARY')
1282        eq(outer.as_string(), '''\
1283Content-Type: multipart/mixed; boundary="BOUNDARY"
1284MIME-Version: 1.0
1285Subject: A subject
1286To: aperson@dom.ain
1287From: bperson@dom.ain
1288
1289--BOUNDARY
1290Content-Type: text/plain; charset="us-ascii"
1291MIME-Version: 1.0
1292Content-Transfer-Encoding: 7bit
1293
1294hello world
1295--BOUNDARY--
1296
1297''')
1298
1299    def test_message_external_body(self):
1300        eq = self.assertEqual
1301        msg = self._msgobj('msg_36.txt')
1302        eq(len(msg.get_payload()), 2)
1303        msg1 = msg.get_payload(1)
1304        eq(msg1.get_content_type(), 'multipart/alternative')
1305        eq(len(msg1.get_payload()), 2)
1306        for subpart in msg1.get_payload():
1307            eq(subpart.get_content_type(), 'message/external-body')
1308            eq(len(subpart.get_payload()), 1)
1309            subsubpart = subpart.get_payload(0)
1310            eq(subsubpart.get_content_type(), 'text/plain')
1311
1312    def test_double_boundary(self):
1313        # msg_37.txt is a multipart that contains two dash-boundary's in a
1314        # row.  Our interpretation of RFC 2046 calls for ignoring the second
1315        # and subsequent boundaries.
1316        msg = self._msgobj('msg_37.txt')
1317        self.assertEqual(len(msg.get_payload()), 3)
1318
1319    def test_nested_inner_contains_outer_boundary(self):
1320        eq = self.ndiffAssertEqual
1321        # msg_38.txt has an inner part that contains outer boundaries.  My
1322        # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say
1323        # these are illegal and should be interpreted as unterminated inner
1324        # parts.
1325        msg = self._msgobj('msg_38.txt')
1326        sfp = StringIO()
1327        iterators._structure(msg, sfp)
1328        eq(sfp.getvalue(), """\
1329multipart/mixed
1330    multipart/mixed
1331        multipart/alternative
1332            text/plain
1333        text/plain
1334    text/plain
1335    text/plain
1336""")
1337
1338    def test_nested_with_same_boundary(self):
1339        eq = self.ndiffAssertEqual
1340        # msg 39.txt is similarly evil in that it's got inner parts that use
1341        # the same boundary as outer parts.  Again, I believe the way this is
1342        # parsed is closest to the spirit of RFC 2046
1343        msg = self._msgobj('msg_39.txt')
1344        sfp = StringIO()
1345        iterators._structure(msg, sfp)
1346        eq(sfp.getvalue(), """\
1347multipart/mixed
1348    multipart/mixed
1349        multipart/alternative
1350        application/octet-stream
1351        application/octet-stream
1352    text/plain
1353""")
1354
1355    def test_boundary_in_non_multipart(self):
1356        msg = self._msgobj('msg_40.txt')
1357        self.assertEqual(msg.as_string(), '''\
1358MIME-Version: 1.0
1359Content-Type: text/html; boundary="--961284236552522269"
1360
1361----961284236552522269
1362Content-Type: text/html;
1363Content-Transfer-Encoding: 7Bit
1364
1365<html></html>
1366
1367----961284236552522269--
1368''')
1369
1370    def test_boundary_with_leading_space(self):
1371        eq = self.assertEqual
1372        msg = email.message_from_string('''\
1373MIME-Version: 1.0
1374Content-Type: multipart/mixed; boundary="    XXXX"
1375
1376--    XXXX
1377Content-Type: text/plain
1378
1379
1380--    XXXX
1381Content-Type: text/plain
1382
1383--    XXXX--
1384''')
1385        self.assertTrue(msg.is_multipart())
1386        eq(msg.get_boundary(), '    XXXX')
1387        eq(len(msg.get_payload()), 2)
1388
1389    def test_boundary_without_trailing_newline(self):
1390        m = Parser().parsestr("""\
1391Content-Type: multipart/mixed; boundary="===============0012394164=="
1392MIME-Version: 1.0
1393
1394--===============0012394164==
1395Content-Type: image/file1.jpg
1396MIME-Version: 1.0
1397Content-Transfer-Encoding: base64
1398
1399YXNkZg==
1400--===============0012394164==--""")
1401        self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==')
1402
1403
1404
1405# Test some badly formatted messages
1406class TestNonConformant(TestEmailBase):
1407    def test_parse_missing_minor_type(self):
1408        eq = self.assertEqual
1409        msg = self._msgobj('msg_14.txt')
1410        eq(msg.get_content_type(), 'text/plain')
1411        eq(msg.get_content_maintype(), 'text')
1412        eq(msg.get_content_subtype(), 'plain')
1413
1414    def test_same_boundary_inner_outer(self):
1415        unless = self.assertTrue
1416        msg = self._msgobj('msg_15.txt')
1417        # XXX We can probably eventually do better
1418        inner = msg.get_payload(0)
1419        unless(hasattr(inner, 'defects'))
1420        self.assertEqual(len(inner.defects), 1)
1421        unless(isinstance(inner.defects[0],
1422                          errors.StartBoundaryNotFoundDefect))
1423
1424    def test_multipart_no_boundary(self):
1425        unless = self.assertTrue
1426        msg = self._msgobj('msg_25.txt')
1427        unless(isinstance(msg.get_payload(), str))
1428        self.assertEqual(len(msg.defects), 2)
1429        unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1430        unless(isinstance(msg.defects[1],
1431                          errors.MultipartInvariantViolationDefect))
1432
1433    def test_invalid_content_type(self):
1434        eq = self.assertEqual
1435        neq = self.ndiffAssertEqual
1436        msg = Message()
1437        # RFC 2045, $5.2 says invalid yields text/plain
1438        msg['Content-Type'] = 'text'
1439        eq(msg.get_content_maintype(), 'text')
1440        eq(msg.get_content_subtype(), 'plain')
1441        eq(msg.get_content_type(), 'text/plain')
1442        # Clear the old value and try something /really/ invalid
1443        del msg['content-type']
1444        msg['Content-Type'] = 'foo'
1445        eq(msg.get_content_maintype(), 'text')
1446        eq(msg.get_content_subtype(), 'plain')
1447        eq(msg.get_content_type(), 'text/plain')
1448        # Still, make sure that the message is idempotently generated
1449        s = StringIO()
1450        g = Generator(s)
1451        g.flatten(msg)
1452        neq(s.getvalue(), 'Content-Type: foo\n\n')
1453
1454    def test_no_start_boundary(self):
1455        eq = self.ndiffAssertEqual
1456        msg = self._msgobj('msg_31.txt')
1457        eq(msg.get_payload(), """\
1458--BOUNDARY
1459Content-Type: text/plain
1460
1461message 1
1462
1463--BOUNDARY
1464Content-Type: text/plain
1465
1466message 2
1467
1468--BOUNDARY--
1469""")
1470
1471    def test_no_separating_blank_line(self):
1472        eq = self.ndiffAssertEqual
1473        msg = self._msgobj('msg_35.txt')
1474        eq(msg.as_string(), """\
1475From: aperson@dom.ain
1476To: bperson@dom.ain
1477Subject: here's something interesting
1478
1479counter to RFC 2822, there's no separating newline here
1480""")
1481
1482    def test_lying_multipart(self):
1483        unless = self.assertTrue
1484        msg = self._msgobj('msg_41.txt')
1485        unless(hasattr(msg, 'defects'))
1486        self.assertEqual(len(msg.defects), 2)
1487        unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1488        unless(isinstance(msg.defects[1],
1489                          errors.MultipartInvariantViolationDefect))
1490
1491    def test_missing_start_boundary(self):
1492        outer = self._msgobj('msg_42.txt')
1493        # The message structure is:
1494        #
1495        # multipart/mixed
1496        #    text/plain
1497        #    message/rfc822
1498        #        multipart/mixed [*]
1499        #
1500        # [*] This message is missing its start boundary
1501        bad = outer.get_payload(1).get_payload(0)
1502        self.assertEqual(len(bad.defects), 1)
1503        self.assertTrue(isinstance(bad.defects[0],
1504                                   errors.StartBoundaryNotFoundDefect))
1505
1506    def test_first_line_is_continuation_header(self):
1507        eq = self.assertEqual
1508        m = ' Line 1\nLine 2\nLine 3'
1509        msg = email.message_from_string(m)
1510        eq(msg.keys(), [])
1511        eq(msg.get_payload(), 'Line 2\nLine 3')
1512        eq(len(msg.defects), 1)
1513        self.assertTrue(isinstance(msg.defects[0],
1514                                   errors.FirstHeaderLineIsContinuationDefect))
1515        eq(msg.defects[0].line, ' Line 1\n')
1516
1517
1518
1519# Test RFC 2047 header encoding and decoding
1520class TestRFC2047(unittest.TestCase):
1521    def test_rfc2047_multiline(self):
1522        eq = self.assertEqual
1523        s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz
1524 foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?="""
1525        dh = decode_header(s)
1526        eq(dh, [
1527            ('Re:', None),
1528            ('r\x8aksm\x9arg\x8cs', 'mac-iceland'),
1529            ('baz foo bar', None),
1530            ('r\x8aksm\x9arg\x8cs', 'mac-iceland')])
1531        eq(str(make_header(dh)),
1532           """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar
1533 =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""")
1534
1535    def test_whitespace_eater_unicode(self):
1536        eq = self.assertEqual
1537        s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard@dom.ain>'
1538        dh = decode_header(s)
1539        eq(dh, [('Andr\xe9', 'iso-8859-1'), ('Pirard <pirard@dom.ain>', None)])
1540        hu = unicode(make_header(dh)).encode('latin-1')
1541        eq(hu, 'Andr\xe9 Pirard <pirard@dom.ain>')
1542
1543    def test_whitespace_eater_unicode_2(self):
1544        eq = self.assertEqual
1545        s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?='
1546        dh = decode_header(s)
1547        eq(dh, [('The', None), ('quick brown fox', 'iso-8859-1'),
1548                ('jumped over the', None), ('lazy dog', 'iso-8859-1')])
1549        hu = make_header(dh).__unicode__()
1550        eq(hu, u'The quick brown fox jumped over the lazy dog')
1551
1552    def test_rfc2047_missing_whitespace(self):
1553        s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord'
1554        dh = decode_header(s)
1555        self.assertEqual(dh, [(s, None)])
1556
1557    def test_rfc2047_with_whitespace(self):
1558        s = 'Sm =?ISO-8859-1?B?9g==?= rg =?ISO-8859-1?B?5Q==?= sbord'
1559        dh = decode_header(s)
1560        self.assertEqual(dh, [('Sm', None), ('\xf6', 'iso-8859-1'),
1561                              ('rg', None), ('\xe5', 'iso-8859-1'),
1562                              ('sbord', None)])
1563
1564
1565
1566# Test the MIMEMessage class
1567class TestMIMEMessage(TestEmailBase):
1568    def setUp(self):
1569        fp = openfile('msg_11.txt')
1570        try:
1571            self._text = fp.read()
1572        finally:
1573            fp.close()
1574
1575    def test_type_error(self):
1576        self.assertRaises(TypeError, MIMEMessage, 'a plain string')
1577
1578    def test_valid_argument(self):
1579        eq = self.assertEqual
1580        unless = self.assertTrue
1581        subject = 'A sub-message'
1582        m = Message()
1583        m['Subject'] = subject
1584        r = MIMEMessage(m)
1585        eq(r.get_content_type(), 'message/rfc822')
1586        payload = r.get_payload()
1587        unless(isinstance(payload, list))
1588        eq(len(payload), 1)
1589        subpart = payload[0]
1590        unless(subpart is m)
1591        eq(subpart['subject'], subject)
1592
1593    def test_bad_multipart(self):
1594        eq = self.assertEqual
1595        msg1 = Message()
1596        msg1['Subject'] = 'subpart 1'
1597        msg2 = Message()
1598        msg2['Subject'] = 'subpart 2'
1599        r = MIMEMessage(msg1)
1600        self.assertRaises(errors.MultipartConversionError, r.attach, msg2)
1601
1602    def test_generate(self):
1603        # First craft the message to be encapsulated
1604        m = Message()
1605        m['Subject'] = 'An enclosed message'
1606        m.set_payload('Here is the body of the message.\n')
1607        r = MIMEMessage(m)
1608        r['Subject'] = 'The enclosing message'
1609        s = StringIO()
1610        g = Generator(s)
1611        g.flatten(r)
1612        self.assertEqual(s.getvalue(), """\
1613Content-Type: message/rfc822
1614MIME-Version: 1.0
1615Subject: The enclosing message
1616
1617Subject: An enclosed message
1618
1619Here is the body of the message.
1620""")
1621
1622    def test_parse_message_rfc822(self):
1623        eq = self.assertEqual
1624        unless = self.assertTrue
1625        msg = self._msgobj('msg_11.txt')
1626        eq(msg.get_content_type(), 'message/rfc822')
1627        payload = msg.get_payload()
1628        unless(isinstance(payload, list))
1629        eq(len(payload), 1)
1630        submsg = payload[0]
1631        self.assertTrue(isinstance(submsg, Message))
1632        eq(submsg['subject'], 'An enclosed message')
1633        eq(submsg.get_payload(), 'Here is the body of the message.\n')
1634
1635    def test_dsn(self):
1636        eq = self.assertEqual
1637        unless = self.assertTrue
1638        # msg 16 is a Delivery Status Notification, see RFC 1894
1639        msg = self._msgobj('msg_16.txt')
1640        eq(msg.get_content_type(), 'multipart/report')
1641        unless(msg.is_multipart())
1642        eq(len(msg.get_payload()), 3)
1643        # Subpart 1 is a text/plain, human readable section
1644        subpart = msg.get_payload(0)
1645        eq(subpart.get_content_type(), 'text/plain')
1646        eq(subpart.get_payload(), """\
1647This report relates to a message you sent with the following header fields:
1648
1649  Message-id: <002001c144a6$8752e060$56104586@oxy.edu>
1650  Date: Sun, 23 Sep 2001 20:10:55 -0700
1651  From: "Ian T. Henry" <henryi@oxy.edu>
1652  To: SoCal Raves <scr@socal-raves.org>
1653  Subject: [scr] yeah for Ians!!
1654
1655Your message cannot be delivered to the following recipients:
1656
1657  Recipient address: jangel1@cougar.noc.ucla.edu
1658  Reason: recipient reached disk quota
1659
1660""")
1661        # Subpart 2 contains the machine parsable DSN information.  It
1662        # consists of two blocks of headers, represented by two nested Message
1663        # objects.
1664        subpart = msg.get_payload(1)
1665        eq(subpart.get_content_type(), 'message/delivery-status')
1666        eq(len(subpart.get_payload()), 2)
1667        # message/delivery-status should treat each block as a bunch of
1668        # headers, i.e. a bunch of Message objects.
1669        dsn1 = subpart.get_payload(0)
1670        unless(isinstance(dsn1, Message))
1671        eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu')
1672        eq(dsn1.get_param('dns', header='reporting-mta'), '')
1673        # Try a missing one <wink>
1674        eq(dsn1.get_param('nsd', header='reporting-mta'), None)
1675        dsn2 = subpart.get_payload(1)
1676        unless(isinstance(dsn2, Message))
1677        eq(dsn2['action'], 'failed')
1678        eq(dsn2.get_params(header='original-recipient'),
1679           [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')])
1680        eq(dsn2.get_param('rfc822', header='final-recipient'), '')
1681        # Subpart 3 is the original message
1682        subpart = msg.get_payload(2)
1683        eq(subpart.get_content_type(), 'message/rfc822')
1684        payload = subpart.get_payload()
1685        unless(isinstance(payload, list))
1686        eq(len(payload), 1)
1687        subsubpart = payload[0]
1688        unless(isinstance(subsubpart, Message))
1689        eq(subsubpart.get_content_type(), 'text/plain')
1690        eq(subsubpart['message-id'],
1691           '<002001c144a6$8752e060$56104586@oxy.edu>')
1692
1693    def test_epilogue(self):
1694        eq = self.ndiffAssertEqual
1695        fp = openfile('msg_21.txt')
1696        try:
1697            text = fp.read()
1698        finally:
1699            fp.close()
1700        msg = Message()
1701        msg['From'] = 'aperson@dom.ain'
1702        msg['To'] = 'bperson@dom.ain'
1703        msg['Subject'] = 'Test'
1704        msg.preamble = 'MIME message'
1705        msg.epilogue = 'End of MIME message\n'
1706        msg1 = MIMEText('One')
1707        msg2 = MIMEText('Two')
1708        msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1709        msg.attach(msg1)
1710        msg.attach(msg2)
1711        sfp = StringIO()
1712        g = Generator(sfp)
1713        g.flatten(msg)
1714        eq(sfp.getvalue(), text)
1715
1716    def test_no_nl_preamble(self):
1717        eq = self.ndiffAssertEqual
1718        msg = Message()
1719        msg['From'] = 'aperson@dom.ain'
1720        msg['To'] = 'bperson@dom.ain'
1721        msg['Subject'] = 'Test'
1722        msg.preamble = 'MIME message'
1723        msg.epilogue = ''
1724        msg1 = MIMEText('One')
1725        msg2 = MIMEText('Two')
1726        msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1727        msg.attach(msg1)
1728        msg.attach(msg2)
1729        eq(msg.as_string(), """\
1730From: aperson@dom.ain
1731To: bperson@dom.ain
1732Subject: Test
1733Content-Type: multipart/mixed; boundary="BOUNDARY"
1734
1735MIME message
1736--BOUNDARY
1737Content-Type: text/plain; charset="us-ascii"
1738MIME-Version: 1.0
1739Content-Transfer-Encoding: 7bit
1740
1741One
1742--BOUNDARY
1743Content-Type: text/plain; charset="us-ascii"
1744MIME-Version: 1.0
1745Content-Transfer-Encoding: 7bit
1746
1747Two
1748--BOUNDARY--
1749""")
1750
1751    def test_default_type(self):
1752        eq = self.assertEqual
1753        fp = openfile('msg_30.txt')
1754        try:
1755            msg = email.message_from_file(fp)
1756        finally:
1757            fp.close()
1758        container1 = msg.get_payload(0)
1759        eq(container1.get_default_type(), 'message/rfc822')
1760        eq(container1.get_content_type(), 'message/rfc822')
1761        container2 = msg.get_payload(1)
1762        eq(container2.get_default_type(), 'message/rfc822')
1763        eq(container2.get_content_type(), 'message/rfc822')
1764        container1a = container1.get_payload(0)
1765        eq(container1a.get_default_type(), 'text/plain')
1766        eq(container1a.get_content_type(), 'text/plain')
1767        container2a = container2.get_payload(0)
1768        eq(container2a.get_default_type(), 'text/plain')
1769        eq(container2a.get_content_type(), 'text/plain')
1770
1771    def test_default_type_with_explicit_container_type(self):
1772        eq = self.assertEqual
1773        fp = openfile('msg_28.txt')
1774        try:
1775            msg = email.message_from_file(fp)
1776        finally:
1777            fp.close()
1778        container1 = msg.get_payload(0)
1779        eq(container1.get_default_type(), 'message/rfc822')
1780        eq(container1.get_content_type(), 'message/rfc822')
1781        container2 = msg.get_payload(1)
1782        eq(container2.get_default_type(), 'message/rfc822')
1783        eq(container2.get_content_type(), 'message/rfc822')
1784        container1a = container1.get_payload(0)
1785        eq(container1a.get_default_type(), 'text/plain')
1786        eq(container1a.get_content_type(), 'text/plain')
1787        container2a = container2.get_payload(0)
1788        eq(container2a.get_default_type(), 'text/plain')
1789        eq(container2a.get_content_type(), 'text/plain')
1790
1791    def test_default_type_non_parsed(self):
1792        eq = self.assertEqual
1793        neq = self.ndiffAssertEqual
1794        # Set up container
1795        container = MIMEMultipart('digest', 'BOUNDARY')
1796        container.epilogue = ''
1797        # Set up subparts
1798        subpart1a = MIMEText('message 1\n')
1799        subpart2a = MIMEText('message 2\n')
1800        subpart1 = MIMEMessage(subpart1a)
1801        subpart2 = MIMEMessage(subpart2a)
1802        container.attach(subpart1)
1803        container.attach(subpart2)
1804        eq(subpart1.get_content_type(), 'message/rfc822')
1805        eq(subpart1.get_default_type(), 'message/rfc822')
1806        eq(subpart2.get_content_type(), 'message/rfc822')
1807        eq(subpart2.get_default_type(), 'message/rfc822')
1808        neq(container.as_string(0), '''\
1809Content-Type: multipart/digest; boundary="BOUNDARY"
1810MIME-Version: 1.0
1811
1812--BOUNDARY
1813Content-Type: message/rfc822
1814MIME-Version: 1.0
1815
1816Content-Type: text/plain; charset="us-ascii"
1817MIME-Version: 1.0
1818Content-Transfer-Encoding: 7bit
1819
1820message 1
1821
1822--BOUNDARY
1823Content-Type: message/rfc822
1824MIME-Version: 1.0
1825
1826Content-Type: text/plain; charset="us-ascii"
1827MIME-Version: 1.0
1828Content-Transfer-Encoding: 7bit
1829
1830message 2
1831
1832--BOUNDARY--
1833''')
1834        del subpart1['content-type']
1835        del subpart1['mime-version']
1836        del subpart2['content-type']
1837        del subpart2['mime-version']
1838        eq(subpart1.get_content_type(), 'message/rfc822')
1839        eq(subpart1.get_default_type(), 'message/rfc822')
1840        eq(subpart2.get_content_type(), 'message/rfc822')
1841        eq(subpart2.get_default_type(), 'message/rfc822')
1842        neq(container.as_string(0), '''\
1843Content-Type: multipart/digest; boundary="BOUNDARY"
1844MIME-Version: 1.0
1845
1846--BOUNDARY
1847
1848Content-Type: text/plain; charset="us-ascii"
1849MIME-Version: 1.0
1850Content-Transfer-Encoding: 7bit
1851
1852message 1
1853
1854--BOUNDARY
1855
1856Content-Type: text/plain; charset="us-ascii"
1857MIME-Version: 1.0
1858Content-Transfer-Encoding: 7bit
1859
1860message 2
1861
1862--BOUNDARY--
1863''')
1864
1865    def test_mime_attachments_in_constructor(self):
1866        eq = self.assertEqual
1867        text1 = MIMEText('')
1868        text2 = MIMEText('')
1869        msg = MIMEMultipart(_subparts=(text1, text2))
1870        eq(len(msg.get_payload()), 2)
1871        eq(msg.get_payload(0), text1)
1872        eq(msg.get_payload(1), text2)
1873
1874
1875
1876# A general test of parser->model->generator idempotency.  IOW, read a message
1877# in, parse it into a message object tree, then without touching the tree,
1878# regenerate the plain text.  The original text and the transformed text
1879# should be identical.  Note: that we ignore the Unix-From since that may
1880# contain a changed date.
1881class TestIdempotent(TestEmailBase):
1882    def _msgobj(self, filename):
1883        fp = openfile(filename)
1884        try:
1885            data = fp.read()
1886        finally:
1887            fp.close()
1888        msg = email.message_from_string(data)
1889        return msg, data
1890
1891    def _idempotent(self, msg, text):
1892        eq = self.ndiffAssertEqual
1893        s = StringIO()
1894        g = Generator(s, maxheaderlen=0)
1895        g.flatten(msg)
1896        eq(text, s.getvalue())
1897
1898    def test_parse_text_message(self):
1899        eq = self.assertEqual
1900        msg, text = self._msgobj('msg_01.txt')
1901        eq(msg.get_content_type(), 'text/plain')
1902        eq(msg.get_content_maintype(), 'text')
1903        eq(msg.get_content_subtype(), 'plain')
1904        eq(msg.get_params()[1], ('charset', 'us-ascii'))
1905        eq(msg.get_param('charset'), 'us-ascii')
1906        eq(msg.preamble, None)
1907        eq(msg.epilogue, None)
1908        self._idempotent(msg, text)
1909
1910    def test_parse_untyped_message(self):
1911        eq = self.assertEqual
1912        msg, text = self._msgobj('msg_03.txt')
1913        eq(msg.get_content_type(), 'text/plain')
1914        eq(msg.get_params(), None)
1915        eq(msg.get_param('charset'), None)
1916        self._idempotent(msg, text)
1917
1918    def test_simple_multipart(self):
1919        msg, text = self._msgobj('msg_04.txt')
1920        self._idempotent(msg, text)
1921
1922    def test_MIME_digest(self):
1923        msg, text = self._msgobj('msg_02.txt')
1924        self._idempotent(msg, text)
1925
1926    def test_long_header(self):
1927        msg, text = self._msgobj('msg_27.txt')
1928        self._idempotent(msg, text)
1929
1930    def test_MIME_digest_with_part_headers(self):
1931        msg, text = self._msgobj('msg_28.txt')
1932        self._idempotent(msg, text)
1933
1934    def test_mixed_with_image(self):
1935        msg, text = self._msgobj('msg_06.txt')
1936        self._idempotent(msg, text)
1937
1938    def test_multipart_report(self):
1939        msg, text = self._msgobj('msg_05.txt')
1940        self._idempotent(msg, text)
1941
1942    def test_dsn(self):
1943        msg, text = self._msgobj('msg_16.txt')
1944        self._idempotent(msg, text)
1945
1946    def test_preamble_epilogue(self):
1947        msg, text = self._msgobj('msg_21.txt')
1948        self._idempotent(msg, text)
1949
1950    def test_multipart_one_part(self):
1951        msg, text = self._msgobj('msg_23.txt')
1952        self._idempotent(msg, text)
1953
1954    def test_multipart_no_parts(self):
1955        msg, text = self._msgobj('msg_24.txt')
1956        self._idempotent(msg, text)
1957
1958    def test_no_start_boundary(self):
1959        msg, text = self._msgobj('msg_31.txt')
1960        self._idempotent(msg, text)
1961
1962    def test_rfc2231_charset(self):
1963        msg, text = self._msgobj('msg_32.txt')
1964        self._idempotent(msg, text)
1965
1966    def test_more_rfc2231_parameters(self):
1967        msg, text = self._msgobj('msg_33.txt')
1968        self._idempotent(msg, text)
1969
1970    def test_text_plain_in_a_multipart_digest(self):
1971        msg, text = self._msgobj('msg_34.txt')
1972        self._idempotent(msg, text)
1973
1974    def test_nested_multipart_mixeds(self):
1975        msg, text = self._msgobj('msg_12a.txt')
1976        self._idempotent(msg, text)
1977
1978    def test_message_external_body_idempotent(self):
1979        msg, text = self._msgobj('msg_36.txt')
1980        self._idempotent(msg, text)
1981
1982    def test_content_type(self):
1983        eq = self.assertEqual
1984        unless = self.assertTrue
1985        # Get a message object and reset the seek pointer for other tests
1986        msg, text = self._msgobj('msg_05.txt')
1987        eq(msg.get_content_type(), 'multipart/report')
1988        # Test the Content-Type: parameters
1989        params = {}
1990        for pk, pv in msg.get_params():
1991            params[pk] = pv
1992        eq(params['report-type'], 'delivery-status')
1993        eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
1994        eq(msg.preamble, 'This is a MIME-encapsulated message.\n')
1995        eq(msg.epilogue, '\n')
1996        eq(len(msg.get_payload()), 3)
1997        # Make sure the subparts are what we expect
1998        msg1 = msg.get_payload(0)
1999        eq(msg1.get_content_type(), 'text/plain')
2000        eq(msg1.get_payload(), 'Yadda yadda yadda\n')
2001        msg2 = msg.get_payload(1)
2002        eq(msg2.get_content_type(), 'text/plain')
2003        eq(msg2.get_payload(), 'Yadda yadda yadda\n')
2004        msg3 = msg.get_payload(2)
2005        eq(msg3.get_content_type(), 'message/rfc822')
2006        self.assertTrue(isinstance(msg3, Message))
2007        payload = msg3.get_payload()
2008        unless(isinstance(payload, list))
2009        eq(len(payload), 1)
2010        msg4 = payload[0]
2011        unless(isinstance(msg4, Message))
2012        eq(msg4.get_payload(), 'Yadda yadda yadda\n')
2013
2014    def test_parser(self):
2015        eq = self.assertEqual
2016        unless = self.assertTrue
2017        msg, text = self._msgobj('msg_06.txt')
2018        # Check some of the outer headers
2019        eq(msg.get_content_type(), 'message/rfc822')
2020        # Make sure the payload is a list of exactly one sub-Message, and that
2021        # that submessage has a type of text/plain
2022        payload = msg.get_payload()
2023        unless(isinstance(payload, list))
2024        eq(len(payload), 1)
2025        msg1 = payload[0]
2026        self.assertTrue(isinstance(msg1, Message))
2027        eq(msg1.get_content_type(), 'text/plain')
2028        self.assertTrue(isinstance(msg1.get_payload(), str))
2029        eq(msg1.get_payload(), '\n')
2030
2031
2032
2033# Test various other bits of the package's functionality
2034class TestMiscellaneous(TestEmailBase):
2035    def test_message_from_string(self):
2036        fp = openfile('msg_01.txt')
2037        try:
2038            text = fp.read()
2039        finally:
2040            fp.close()
2041        msg = email.message_from_string(text)
2042        s = StringIO()
2043        # Don't wrap/continue long headers since we're trying to test
2044        # idempotency.
2045        g = Generator(s, maxheaderlen=0)
2046        g.flatten(msg)
2047        self.assertEqual(text, s.getvalue())
2048
2049    def test_message_from_file(self):
2050        fp = openfile('msg_01.txt')
2051        try:
2052            text = fp.read()
2053            fp.seek(0)
2054            msg = email.message_from_file(fp)
2055            s = StringIO()
2056            # Don't wrap/continue long headers since we're trying to test
2057            # idempotency.
2058            g = Generator(s, maxheaderlen=0)
2059            g.flatten(msg)
2060            self.assertEqual(text, s.getvalue())
2061        finally:
2062            fp.close()
2063
2064    def test_message_from_string_with_class(self):
2065        unless = self.assertTrue
2066        fp = openfile('msg_01.txt')
2067        try:
2068            text = fp.read()
2069        finally:
2070            fp.close()
2071        # Create a subclass
2072        class MyMessage(Message):
2073            pass
2074
2075        msg = email.message_from_string(text, MyMessage)
2076        unless(isinstance(msg, MyMessage))
2077        # Try something more complicated
2078        fp = openfile('msg_02.txt')
2079        try:
2080            text = fp.read()
2081        finally:
2082            fp.close()
2083        msg = email.message_from_string(text, MyMessage)
2084        for subpart in msg.walk():
2085            unless(isinstance(subpart, MyMessage))
2086
2087    def test_message_from_file_with_class(self):
2088        unless = self.assertTrue
2089        # Create a subclass
2090        class MyMessage(Message):
2091            pass
2092
2093        fp = openfile('msg_01.txt')
2094        try:
2095            msg = email.message_from_file(fp, MyMessage)
2096        finally:
2097            fp.close()
2098        unless(isinstance(msg, MyMessage))
2099        # Try something more complicated
2100        fp = openfile('msg_02.txt')
2101        try:
2102            msg = email.message_from_file(fp, MyMessage)
2103        finally:
2104            fp.close()
2105        for subpart in msg.walk():
2106            unless(isinstance(subpart, MyMessage))
2107
2108    def test__all__(self):
2109        module = __import__('email')
2110        # Can't use sorted() here due to Python 2.3 compatibility
2111        all = module.__all__[:]
2112        all.sort()
2113        self.assertEqual(all, [
2114            # Old names
2115            'Charset', 'Encoders', 'Errors', 'Generator',
2116            'Header', 'Iterators', 'MIMEAudio', 'MIMEBase',
2117            'MIMEImage', 'MIMEMessage', 'MIMEMultipart',
2118            'MIMENonMultipart', 'MIMEText', 'Message',
2119            'Parser', 'Utils', 'base64MIME',
2120            # new names
2121            'base64mime', 'charset', 'encoders', 'errors', 'generator',
2122            'header', 'iterators', 'message', 'message_from_file',
2123            'message_from_string', 'mime', 'parser',
2124            'quopriMIME', 'quoprimime', 'utils',
2125            ])
2126
2127    def test_formatdate(self):
2128        now = time.time()
2129        self.assertEqual(utils.parsedate(utils.formatdate(now))[:6],
2130                         time.gmtime(now)[:6])
2131
2132    def test_formatdate_localtime(self):
2133        now = time.time()
2134        self.assertEqual(
2135            utils.parsedate(utils.formatdate(now, localtime=True))[:6],
2136            time.localtime(now)[:6])
2137
2138    def test_formatdate_usegmt(self):
2139        now = time.time()
2140        self.assertEqual(
2141            utils.formatdate(now, localtime=False),
2142            time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now)))
2143        self.assertEqual(
2144            utils.formatdate(now, localtime=False, usegmt=True),
2145            time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now)))
2146
2147    def test_parsedate_none(self):
2148        self.assertEqual(utils.parsedate(''), None)
2149
2150    def test_parsedate_compact(self):
2151        # The FWS after the comma is optional
2152        self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'),
2153                         utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800'))
2154
2155    def test_parsedate_no_dayofweek(self):
2156        eq = self.assertEqual
2157        eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'),
2158           (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800))
2159
2160    def test_parsedate_compact_no_dayofweek(self):
2161        eq = self.assertEqual
2162        eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'),
2163           (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800))
2164
2165    def test_parsedate_acceptable_to_time_functions(self):
2166        eq = self.assertEqual
2167        timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800')
2168        t = int(time.mktime(timetup))
2169        eq(time.localtime(t)[:6], timetup[:6])
2170        eq(int(time.strftime('%Y', timetup)), 2003)
2171        timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800')
2172        t = int(time.mktime(timetup[:9]))
2173        eq(time.localtime(t)[:6], timetup[:6])
2174        eq(int(time.strftime('%Y', timetup[:9])), 2003)
2175
2176    def test_parseaddr_empty(self):
2177        self.assertEqual(utils.parseaddr('<>'), ('', ''))
2178        self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '')
2179
2180    def test_noquote_dump(self):
2181        self.assertEqual(
2182            utils.formataddr(('A Silly Person', 'person@dom.ain')),
2183            'A Silly Person <person@dom.ain>')
2184
2185    def test_escape_dump(self):
2186        self.assertEqual(
2187            utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')),
2188            r'"A \(Very\) Silly Person" <person@dom.ain>')
2189        a = r'A \(Special\) Person'
2190        b = 'person@dom.ain'
2191        self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2192
2193    def test_escape_backslashes(self):
2194        self.assertEqual(
2195            utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')),
2196            r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>')
2197        a = r'Arthur \Backslash\ Foobar'
2198        b = 'person@dom.ain'
2199        self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2200
2201    def test_name_with_dot(self):
2202        x = 'John X. Doe <jxd@example.com>'
2203        y = '"John X. Doe" <jxd@example.com>'
2204        a, b = ('John X. Doe', 'jxd@example.com')
2205        self.assertEqual(utils.parseaddr(x), (a, b))
2206        self.assertEqual(utils.parseaddr(y), (a, b))
2207        # formataddr() quotes the name if there's a dot in it
2208        self.assertEqual(utils.formataddr((a, b)), y)
2209
2210    def test_multiline_from_comment(self):
2211        x = """\
2212Foo
2213\tBar <foo@example.com>"""
2214        self.assertEqual(utils.parseaddr(x), ('Foo Bar', 'foo@example.com'))
2215
2216    def test_quote_dump(self):
2217        self.assertEqual(
2218            utils.formataddr(('A Silly; Person', 'person@dom.ain')),
2219            r'"A Silly; Person" <person@dom.ain>')
2220
2221    def test_fix_eols(self):
2222        eq = self.assertEqual
2223        eq(utils.fix_eols('hello'), 'hello')
2224        eq(utils.fix_eols('hello\n'), 'hello\r\n')
2225        eq(utils.fix_eols('hello\r'), 'hello\r\n')
2226        eq(utils.fix_eols('hello\r\n'), 'hello\r\n')
2227        eq(utils.fix_eols('hello\n\r'), 'hello\r\n\r\n')
2228
2229    def test_charset_richcomparisons(self):
2230        eq = self.assertEqual
2231        ne = self.assertNotEqual
2232        cset1 = Charset()
2233        cset2 = Charset()
2234        eq(cset1, 'us-ascii')
2235        eq(cset1, 'US-ASCII')
2236        eq(cset1, 'Us-AsCiI')
2237        eq('us-ascii', cset1)
2238        eq('US-ASCII', cset1)
2239        eq('Us-AsCiI', cset1)
2240        ne(cset1, 'usascii')
2241        ne(cset1, 'USASCII')
2242        ne(cset1, 'UsAsCiI')
2243        ne('usascii', cset1)
2244        ne('USASCII', cset1)
2245        ne('UsAsCiI', cset1)
2246        eq(cset1, cset2)
2247        eq(cset2, cset1)
2248
2249    def test_getaddresses(self):
2250        eq = self.assertEqual
2251        eq(utils.getaddresses(['aperson@dom.ain (Al Person)',
2252                               'Bud Person <bperson@dom.ain>']),
2253           [('Al Person', 'aperson@dom.ain'),
2254            ('Bud Person', 'bperson@dom.ain')])
2255
2256    def test_getaddresses_nasty(self):
2257        eq = self.assertEqual
2258        eq(utils.getaddresses(['foo: ;']), [('', '')])
2259        eq(utils.getaddresses(
2260           ['[]*-- =~$']),
2261           [('', ''), ('', ''), ('', '*--')])
2262        eq(utils.getaddresses(
2263           ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
2264           [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
2265
2266    def test_getaddresses_embedded_comment(self):
2267        """Test proper handling of a nested comment"""
2268        eq = self.assertEqual
2269        addrs = utils.getaddresses(['User ((nested comment)) <foo@bar.com>'])
2270        eq(addrs[0][1], 'foo@bar.com')
2271
2272    def test_utils_quote_unquote(self):
2273        eq = self.assertEqual
2274        msg = Message()
2275        msg.add_header('content-disposition', 'attachment',
2276                       filename='foo\\wacky"name')
2277        eq(msg.get_filename(), 'foo\\wacky"name')
2278
2279    def test_get_body_encoding_with_bogus_charset(self):
2280        charset = Charset('not a charset')
2281        self.assertEqual(charset.get_body_encoding(), 'base64')
2282
2283    def test_get_body_encoding_with_uppercase_charset(self):
2284        eq = self.assertEqual
2285        msg = Message()
2286        msg['Content-Type'] = 'text/plain; charset=UTF-8'
2287        eq(msg['content-type'], 'text/plain; charset=UTF-8')
2288        charsets = msg.get_charsets()
2289        eq(len(charsets), 1)
2290        eq(charsets[0], 'utf-8')
2291        charset = Charset(charsets[0])
2292        eq(charset.get_body_encoding(), 'base64')
2293        msg.set_payload('hello world', charset=charset)
2294        eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n')
2295        eq(msg.get_payload(decode=True), 'hello world')
2296        eq(msg['content-transfer-encoding'], 'base64')
2297        # Try another one
2298        msg = Message()
2299        msg['Content-Type'] = 'text/plain; charset="US-ASCII"'
2300        charsets = msg.get_charsets()
2301        eq(len(charsets), 1)
2302        eq(charsets[0], 'us-ascii')
2303        charset = Charset(charsets[0])
2304        eq(charset.get_body_encoding(), encoders.encode_7or8bit)
2305        msg.set_payload('hello world', charset=charset)
2306        eq(msg.get_payload(), 'hello world')
2307        eq(msg['content-transfer-encoding'], '7bit')
2308
2309    def test_charsets_case_insensitive(self):
2310        lc = Charset('us-ascii')
2311        uc = Charset('US-ASCII')
2312        self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding())
2313
2314    def test_partial_falls_inside_message_delivery_status(self):
2315        eq = self.ndiffAssertEqual
2316        # The Parser interface provides chunks of data to FeedParser in 8192
2317        # byte gulps.  SF bug #1076485 found one of those chunks inside
2318        # message/delivery-status header block, which triggered an
2319        # unreadline() of NeedMoreData.
2320        msg = self._msgobj('msg_43.txt')
2321        sfp = StringIO()
2322        iterators._structure(msg, sfp)
2323        eq(sfp.getvalue(), """\
2324multipart/report
2325    text/plain
2326    message/delivery-status
2327        text/plain
2328        text/plain
2329        text/plain
2330        text/plain
2331        text/plain
2332        text/plain
2333        text/plain
2334        text/plain
2335        text/plain
2336        text/plain
2337        text/plain
2338        text/plain
2339        text/plain
2340        text/plain
2341        text/plain
2342        text/plain
2343        text/plain
2344        text/plain
2345        text/plain
2346        text/plain
2347        text/plain
2348        text/plain
2349        text/plain
2350        text/plain
2351        text/plain
2352        text/plain
2353    text/rfc822-headers
2354""")
2355
2356
2357
2358# Test the iterator/generators
2359class TestIterators(TestEmailBase):
2360    def test_body_line_iterator(self):
2361        eq = self.assertEqual
2362        neq = self.ndiffAssertEqual
2363        # First a simple non-multipart message
2364        msg = self._msgobj('msg_01.txt')
2365        it = iterators.body_line_iterator(msg)
2366        lines = list(it)
2367        eq(len(lines), 6)
2368        neq(EMPTYSTRING.join(lines), msg.get_payload())
2369        # Now a more complicated multipart
2370        msg = self._msgobj('msg_02.txt')
2371        it = iterators.body_line_iterator(msg)
2372        lines = list(it)
2373        eq(len(lines), 43)
2374        fp = openfile('msg_19.txt')
2375        try:
2376            neq(EMPTYSTRING.join(lines), fp.read())
2377        finally:
2378            fp.close()
2379
2380    def test_typed_subpart_iterator(self):
2381        eq = self.assertEqual
2382        msg = self._msgobj('msg_04.txt')
2383        it = iterators.typed_subpart_iterator(msg, 'text')
2384        lines = []
2385        subparts = 0
2386        for subpart in it:
2387            subparts += 1
2388            lines.append(subpart.get_payload())
2389        eq(subparts, 2)
2390        eq(EMPTYSTRING.join(lines), """\
2391a simple kind of mirror
2392to reflect upon our own
2393a simple kind of mirror
2394to reflect upon our own
2395""")
2396
2397    def test_typed_subpart_iterator_default_type(self):
2398        eq = self.assertEqual
2399        msg = self._msgobj('msg_03.txt')
2400        it = iterators.typed_subpart_iterator(msg, 'text', 'plain')
2401        lines = []
2402        subparts = 0
2403        for subpart in it:
2404            subparts += 1
2405            lines.append(subpart.get_payload())
2406        eq(subparts, 1)
2407        eq(EMPTYSTRING.join(lines), """\
2408
2409Hi,
2410
2411Do you like this message?
2412
2413-Me
2414""")
2415
2416
2417
2418class TestParsers(TestEmailBase):
2419    def test_header_parser(self):
2420        eq = self.assertEqual
2421        # Parse only the headers of a complex multipart MIME document
2422        fp = openfile('msg_02.txt')
2423        try:
2424            msg = HeaderParser().parse(fp)
2425        finally:
2426            fp.close()
2427        eq(msg['from'], 'ppp-request@zzz.org')
2428        eq(msg['to'], 'ppp@zzz.org')
2429        eq(msg.get_content_type(), 'multipart/mixed')
2430        self.assertFalse(msg.is_multipart())
2431        self.assertTrue(isinstance(msg.get_payload(), str))
2432
2433    def test_whitespace_continuation(self):
2434        eq = self.assertEqual
2435        # This message contains a line after the Subject: header that has only
2436        # whitespace, but it is not empty!
2437        msg = email.message_from_string("""\
2438From: aperson@dom.ain
2439To: bperson@dom.ain
2440Subject: the next line has a space on it
2441\x20
2442Date: Mon, 8 Apr 2002 15:09:19 -0400
2443Message-ID: spam
2444
2445Here's the message body
2446""")
2447        eq(msg['subject'], 'the next line has a space on it\n ')
2448        eq(msg['message-id'], 'spam')
2449        eq(msg.get_payload(), "Here's the message body\n")
2450
2451    def test_whitespace_continuation_last_header(self):
2452        eq = self.assertEqual
2453        # Like the previous test, but the subject line is the last
2454        # header.
2455        msg = email.message_from_string("""\
2456From: aperson@dom.ain
2457To: bperson@dom.ain
2458Date: Mon, 8 Apr 2002 15:09:19 -0400
2459Message-ID: spam
2460Subject: the next line has a space on it
2461\x20
2462
2463Here's the message body
2464""")
2465        eq(msg['subject'], 'the next line has a space on it\n ')
2466        eq(msg['message-id'], 'spam')
2467        eq(msg.get_payload(), "Here's the message body\n")
2468
2469    def test_crlf_separation(self):
2470        eq = self.assertEqual
2471        fp = openfile('msg_26.txt', mode='rb')
2472        try:
2473            msg = Parser().parse(fp)
2474        finally:
2475            fp.close()
2476        eq(len(msg.get_payload()), 2)
2477        part1 = msg.get_payload(0)
2478        eq(part1.get_content_type(), 'text/plain')
2479        eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n')
2480        part2 = msg.get_payload(1)
2481        eq(part2.get_content_type(), 'application/riscos')
2482
2483    def test_multipart_digest_with_extra_mime_headers(self):
2484        eq = self.assertEqual
2485        neq = self.ndiffAssertEqual
2486        fp = openfile('msg_28.txt')
2487        try:
2488            msg = email.message_from_file(fp)
2489        finally:
2490            fp.close()
2491        # Structure is:
2492        # multipart/digest
2493        #   message/rfc822
2494        #     text/plain
2495        #   message/rfc822
2496        #     text/plain
2497        eq(msg.is_multipart(), 1)
2498        eq(len(msg.get_payload()), 2)
2499        part1 = msg.get_payload(0)
2500        eq(part1.get_content_type(), 'message/rfc822')
2501        eq(part1.is_multipart(), 1)
2502        eq(len(part1.get_payload()), 1)
2503        part1a = part1.get_payload(0)
2504        eq(part1a.is_multipart(), 0)
2505        eq(part1a.get_content_type(), 'text/plain')
2506        neq(part1a.get_payload(), 'message 1\n')
2507        # next message/rfc822
2508        part2 = msg.get_payload(1)
2509        eq(part2.get_content_type(), 'message/rfc822')
2510        eq(part2.is_multipart(), 1)
2511        eq(len(part2.get_payload()), 1)
2512        part2a = part2.get_payload(0)
2513        eq(part2a.is_multipart(), 0)
2514        eq(part2a.get_content_type(), 'text/plain')
2515        neq(part2a.get_payload(), 'message 2\n')
2516
2517    def test_three_lines(self):
2518        # A bug report by Andrew McNamara
2519        lines = ['From: Andrew Person <aperson@dom.ain',
2520                 'Subject: Test',
2521                 'Date: Tue, 20 Aug 2002 16:43:45 +1000']
2522        msg = email.message_from_string(NL.join(lines))
2523        self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000')
2524
2525    def test_strip_line_feed_and_carriage_return_in_headers(self):
2526        eq = self.assertEqual
2527        # For [ 1002475 ] email message parser doesn't handle \r\n correctly
2528        value1 = 'text'
2529        value2 = 'more text'
2530        m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % (
2531            value1, value2)
2532        msg = email.message_from_string(m)
2533        eq(msg.get('Header'), value1)
2534        eq(msg.get('Next-Header'), value2)
2535
2536    def test_rfc2822_header_syntax(self):
2537        eq = self.assertEqual
2538        m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2539        msg = email.message_from_string(m)
2540        eq(len(msg.keys()), 3)
2541        keys = msg.keys()
2542        keys.sort()
2543        eq(keys, ['!"#QUX;~', '>From', 'From'])
2544        eq(msg.get_payload(), 'body')
2545
2546    def test_rfc2822_space_not_allowed_in_header(self):
2547        eq = self.assertEqual
2548        m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2549        msg = email.message_from_string(m)
2550        eq(len(msg.keys()), 0)
2551
2552    def test_rfc2822_one_character_header(self):
2553        eq = self.assertEqual
2554        m = 'A: first header\nB: second header\nCC: third header\n\nbody'
2555        msg = email.message_from_string(m)
2556        headers = msg.keys()
2557        headers.sort()
2558        eq(headers, ['A', 'B', 'CC'])
2559        eq(msg.get_payload(), 'body')
2560
2561
2562
2563class TestBase64(unittest.TestCase):
2564    def test_len(self):
2565        eq = self.assertEqual
2566        eq(base64mime.base64_len('hello'),
2567           len(base64mime.encode('hello', eol='')))
2568        for size in range(15):
2569            if   size == 0 : bsize = 0
2570            elif size <= 3 : bsize = 4
2571            elif size <= 6 : bsize = 8
2572            elif size <= 9 : bsize = 12
2573            elif size <= 12: bsize = 16
2574            else           : bsize = 20
2575            eq(base64mime.base64_len('x'*size), bsize)
2576
2577    def test_decode(self):
2578        eq = self.assertEqual
2579        eq(base64mime.decode(''), '')
2580        eq(base64mime.decode('aGVsbG8='), 'hello')
2581        eq(base64mime.decode('aGVsbG8=', 'X'), 'hello')
2582        eq(base64mime.decode('aGVsbG8NCndvcmxk\n', 'X'), 'helloXworld')
2583
2584    def test_encode(self):
2585        eq = self.assertEqual
2586        eq(base64mime.encode(''), '')
2587        eq(base64mime.encode('hello'), 'aGVsbG8=\n')
2588        # Test the binary flag
2589        eq(base64mime.encode('hello\n'), 'aGVsbG8K\n')
2590        eq(base64mime.encode('hello\n', 0), 'aGVsbG8NCg==\n')
2591        # Test the maxlinelen arg
2592        eq(base64mime.encode('xxxx ' * 20, maxlinelen=40), """\
2593eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2594eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2595eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2596eHh4eCB4eHh4IA==
2597""")
2598        # Test the eol argument
2599        eq(base64mime.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
2600eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2601eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2602eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2603eHh4eCB4eHh4IA==\r
2604""")
2605
2606    def test_header_encode(self):
2607        eq = self.assertEqual
2608        he = base64mime.header_encode
2609        eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=')
2610        eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=')
2611        # Test the charset option
2612        eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=')
2613        # Test the keep_eols flag
2614        eq(he('hello\nworld', keep_eols=True),
2615           '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
2616        # Test the maxlinelen argument
2617        eq(he('xxxx ' * 20, maxlinelen=40), """\
2618=?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?=
2619 =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?=
2620 =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?=
2621 =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?=
2622 =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?=
2623 =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""")
2624        # Test the eol argument
2625        eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
2626=?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?=\r
2627 =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?=\r
2628 =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?=\r
2629 =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?=\r
2630 =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?=\r
2631 =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""")
2632
2633
2634
2635class TestQuopri(unittest.TestCase):
2636    def setUp(self):
2637        self.hlit = [chr(x) for x in range(ord('a'), ord('z')+1)] + \
2638                    [chr(x) for x in range(ord('A'), ord('Z')+1)] + \
2639                    [chr(x) for x in range(ord('0'), ord('9')+1)] + \
2640                    ['!', '*', '+', '-', '/', ' ']
2641        self.hnon = [chr(x) for x in range(256) if chr(x) not in self.hlit]
2642        assert len(self.hlit) + len(self.hnon) == 256
2643        self.blit = [chr(x) for x in range(ord(' '), ord('~')+1)] + ['\t']
2644        self.blit.remove('=')
2645        self.bnon = [chr(x) for x in range(256) if chr(x) not in self.blit]
2646        assert len(self.blit) + len(self.bnon) == 256
2647
2648    def test_header_quopri_check(self):
2649        for c in self.hlit:
2650            self.assertFalse(quoprimime.header_quopri_check(c))
2651        for c in self.hnon:
2652            self.assertTrue(quoprimime.header_quopri_check(c))
2653
2654    def test_body_quopri_check(self):
2655        for c in self.blit:
2656            self.assertFalse(quoprimime.body_quopri_check(c))
2657        for c in self.bnon:
2658            self.assertTrue(quoprimime.body_quopri_check(c))
2659
2660    def test_header_quopri_len(self):
2661        eq = self.assertEqual
2662        hql = quoprimime.header_quopri_len
2663        enc = quoprimime.header_encode
2664        for s in ('hello', 'h@e@l@l@o@'):
2665            # Empty charset and no line-endings.  7 == RFC chrome
2666            eq(hql(s), len(enc(s, charset='', eol=''))-7)
2667        for c in self.hlit:
2668            eq(hql(c), 1)
2669        for c in self.hnon:
2670            eq(hql(c), 3)
2671
2672    def test_body_quopri_len(self):
2673        eq = self.assertEqual
2674        bql = quoprimime.body_quopri_len
2675        for c in self.blit:
2676            eq(bql(c), 1)
2677        for c in self.bnon:
2678            eq(bql(c), 3)
2679
2680    def test_quote_unquote_idempotent(self):
2681        for x in range(256):
2682            c = chr(x)
2683            self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c)
2684
2685    def test_header_encode(self):
2686        eq = self.assertEqual
2687        he = quoprimime.header_encode
2688        eq(he('hello'), '=?iso-8859-1?q?hello?=')
2689        eq(he('hello\nworld'), '=?iso-8859-1?q?hello=0D=0Aworld?=')
2690        # Test the charset option
2691        eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=')
2692        # Test the keep_eols flag
2693        eq(he('hello\nworld', keep_eols=True), '=?iso-8859-1?q?hello=0Aworld?=')
2694        # Test a non-ASCII character
2695        eq(he('hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=')
2696        # Test the maxlinelen argument
2697        eq(he('xxxx ' * 20, maxlinelen=40), """\
2698=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
2699 =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
2700 =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?=
2701 =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?=
2702 =?iso-8859-1?q?x_xxxx_xxxx_?=""")
2703        # Test the eol argument
2704        eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
2705=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=\r
2706 =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=\r
2707 =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?=\r
2708 =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?=\r
2709 =?iso-8859-1?q?x_xxxx_xxxx_?=""")
2710
2711    def test_decode(self):
2712        eq = self.assertEqual
2713        eq(quoprimime.decode(''), '')
2714        eq(quoprimime.decode('hello'), 'hello')
2715        eq(quoprimime.decode('hello', 'X'), 'hello')
2716        eq(quoprimime.decode('hello\nworld', 'X'), 'helloXworld')
2717
2718    def test_encode(self):
2719        eq = self.assertEqual
2720        eq(quoprimime.encode(''), '')
2721        eq(quoprimime.encode('hello'), 'hello')
2722        # Test the binary flag
2723        eq(quoprimime.encode('hello\r\nworld'), 'hello\nworld')
2724        eq(quoprimime.encode('hello\r\nworld', 0), 'hello\nworld')
2725        # Test the maxlinelen arg
2726        eq(quoprimime.encode('xxxx ' * 20, maxlinelen=40), """\
2727xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
2728 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
2729x xxxx xxxx xxxx xxxx=20""")
2730        # Test the eol argument
2731        eq(quoprimime.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
2732xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
2733 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
2734x xxxx xxxx xxxx xxxx=20""")
2735        eq(quoprimime.encode("""\
2736one line
2737
2738two line"""), """\
2739one line
2740
2741two line""")
2742
2743
2744
2745# Test the Charset class
2746class TestCharset(unittest.TestCase):
2747    def tearDown(self):
2748        from email import charset as CharsetModule
2749        try:
2750            del CharsetModule.CHARSETS['fake']
2751        except KeyError:
2752            pass
2753
2754    def test_idempotent(self):
2755        eq = self.assertEqual
2756        # Make sure us-ascii = no Unicode conversion
2757        c = Charset('us-ascii')
2758        s = 'Hello World!'
2759        sp = c.to_splittable(s)
2760        eq(s, c.from_splittable(sp))
2761        # test 8-bit idempotency with us-ascii
2762        s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
2763        sp = c.to_splittable(s)
2764        eq(s, c.from_splittable(sp))
2765
2766    def test_body_encode(self):
2767        eq = self.assertEqual
2768        # Try a charset with QP body encoding
2769        c = Charset('iso-8859-1')
2770        eq('hello w=F6rld', c.body_encode('hello w\xf6rld'))
2771        # Try a charset with Base64 body encoding
2772        c = Charset('utf-8')
2773        eq('aGVsbG8gd29ybGQ=\n', c.body_encode('hello world'))
2774        # Try a charset with None body encoding
2775        c = Charset('us-ascii')
2776        eq('hello world', c.body_encode('hello world'))
2777        # Try the convert argument, where input codec != output codec
2778        c = Charset('euc-jp')
2779        # With apologies to Tokio Kikuchi ;)
2780        try:
2781            eq('\x1b$B5FCO;~IW\x1b(B',
2782               c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
2783            eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
2784               c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
2785        except LookupError:
2786            # We probably don't have the Japanese codecs installed
2787            pass
2788        # Testing SF bug #625509, which we have to fake, since there are no
2789        # built-in encodings where the header encoding is QP but the body
2790        # encoding is not.
2791        from email import charset as CharsetModule
2792        CharsetModule.add_charset('fake', CharsetModule.QP, None)
2793        c = Charset('fake')
2794        eq('hello w\xf6rld', c.body_encode('hello w\xf6rld'))
2795
2796    def test_unicode_charset_name(self):
2797        charset = Charset(u'us-ascii')
2798        self.assertEqual(str(charset), 'us-ascii')
2799        self.assertRaises(errors.CharsetError, Charset, 'asc\xffii')
2800
2801
2802
2803# Test multilingual MIME headers.
2804class TestHeader(TestEmailBase):
2805    def test_simple(self):
2806        eq = self.ndiffAssertEqual
2807        h = Header('Hello World!')
2808        eq(h.encode(), 'Hello World!')
2809        h.append(' Goodbye World!')
2810        eq(h.encode(), 'Hello World!  Goodbye World!')
2811
2812    def test_simple_surprise(self):
2813        eq = self.ndiffAssertEqual
2814        h = Header('Hello World!')
2815        eq(h.encode(), 'Hello World!')
2816        h.append('Goodbye World!')
2817        eq(h.encode(), 'Hello World! Goodbye World!')
2818
2819    def test_header_needs_no_decoding(self):
2820        h = 'no decoding needed'
2821        self.assertEqual(decode_header(h), [(h, None)])
2822
2823    def test_long(self):
2824        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.",
2825                   maxlinelen=76)
2826        for l in h.encode(splitchars=' ').split('\n '):
2827            self.assertTrue(len(l) <= 76)
2828
2829    def test_multilingual(self):
2830        eq = self.ndiffAssertEqual
2831        g = Charset("iso-8859-1")
2832        cz = Charset("iso-8859-2")
2833        utf8 = Charset("utf-8")
2834        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. "
2835        cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. "
2836        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")
2837        h = Header(g_head, g)
2838        h.append(cz_head, cz)
2839        h.append(utf8_head, utf8)
2840        enc = h.encode()
2841        eq(enc, """\
2842=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_ko?=
2843 =?iso-8859-1?q?mfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wan?=
2844 =?iso-8859-1?q?dgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6?=
2845 =?iso-8859-1?q?rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
2846 =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
2847 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
2848 =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
2849 =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
2850 =?utf-8?q?_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das_Oder_die_Fl?=
2851 =?utf-8?b?aXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBo+OBpuOBhOOBvuOBmQ==?=
2852 =?utf-8?b?44CC?=""")
2853        eq(decode_header(enc),
2854           [(g_head, "iso-8859-1"), (cz_head, "iso-8859-2"),
2855            (utf8_head, "utf-8")])
2856        ustr = unicode(h)
2857        eq(ustr.encode('utf-8'),
2858           'Die Mieter treten hier ein werden mit einem Foerderband '
2859           'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
2860           'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
2861           'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
2862           'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
2863           '\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
2864           '\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
2865           '\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
2866           '\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
2867           '\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
2868           '\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
2869           '\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
2870           '\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
2871           'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
2872           'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
2873           '\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82')
2874        # Test make_header()
2875        newh = make_header(decode_header(enc))
2876        eq(newh, enc)
2877
2878    def test_header_ctor_default_args(self):
2879        eq = self.ndiffAssertEqual
2880        h = Header()
2881        eq(h, '')
2882        h.append('foo', Charset('iso-8859-1'))
2883        eq(h, '=?iso-8859-1?q?foo?=')
2884
2885    def test_explicit_maxlinelen(self):
2886        eq = self.ndiffAssertEqual
2887        hstr = 'A very long line that must get split to something other than at the 76th character boundary to test the non-default behavior'
2888        h = Header(hstr)
2889        eq(h.encode(), '''\
2890A very long line that must get split to something other than at the 76th
2891 character boundary to test the non-default behavior''')
2892        h = Header(hstr, header_name='Subject')
2893        eq(h.encode(), '''\
2894A very long line that must get split to something other than at the
2895 76th character boundary to test the non-default behavior''')
2896        h = Header(hstr, maxlinelen=1024, header_name='Subject')
2897        eq(h.encode(), hstr)
2898
2899    def test_us_ascii_header(self):
2900        eq = self.assertEqual
2901        s = 'hello'
2902        x = decode_header(s)
2903        eq(x, [('hello', None)])
2904        h = make_header(x)
2905        eq(s, h.encode())
2906
2907    def test_string_charset(self):
2908        eq = self.assertEqual
2909        h = Header()
2910        h.append('hello', 'iso-8859-1')
2911        eq(h, '=?iso-8859-1?q?hello?=')
2912
2913##    def test_unicode_error(self):
2914##        raises = self.assertRaises
2915##        raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')
2916##        raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')
2917##        h = Header()
2918##        raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')
2919##        raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')
2920##        raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')
2921
2922    def test_utf8_shortest(self):
2923        eq = self.assertEqual
2924        h = Header(u'p\xf6stal', 'utf-8')
2925        eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
2926        h = Header(u'\u83ca\u5730\u6642\u592b', 'utf-8')
2927        eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
2928
2929    def test_bad_8bit_header(self):
2930        raises = self.assertRaises
2931        eq = self.assertEqual
2932        x = 'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
2933        raises(UnicodeError, Header, x)
2934        h = Header()
2935        raises(UnicodeError, h.append, x)
2936        eq(str(Header(x, errors='replace')), x)
2937        h.append(x, errors='replace')
2938        eq(str(h), x)
2939
2940    def test_encoded_adjacent_nonencoded(self):
2941        eq = self.assertEqual
2942        h = Header()
2943        h.append('hello', 'iso-8859-1')
2944        h.append('world')
2945        s = h.encode()
2946        eq(s, '=?iso-8859-1?q?hello?= world')
2947        h = make_header(decode_header(s))
2948        eq(h.encode(), s)
2949
2950    def test_whitespace_eater(self):
2951        eq = self.assertEqual
2952        s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
2953        parts = decode_header(s)
2954        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)])
2955        hdr = make_header(parts)
2956        eq(hdr.encode(),
2957           'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
2958
2959    def test_broken_base64_header(self):
2960        raises = self.assertRaises
2961        s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3I ?='
2962        raises(errors.HeaderParseError, decode_header, s)
2963
2964
2965
2966# Test RFC 2231 header parameters (en/de)coding
2967class TestRFC2231(TestEmailBase):
2968    def test_get_param(self):
2969        eq = self.assertEqual
2970        msg = self._msgobj('msg_29.txt')
2971        eq(msg.get_param('title'),
2972           ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
2973        eq(msg.get_param('title', unquote=False),
2974           ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
2975
2976    def test_set_param(self):
2977        eq = self.assertEqual
2978        msg = Message()
2979        msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
2980                      charset='us-ascii')
2981        eq(msg.get_param('title'),
2982           ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
2983        msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
2984                      charset='us-ascii', language='en')
2985        eq(msg.get_param('title'),
2986           ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
2987        msg = self._msgobj('msg_01.txt')
2988        msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
2989                      charset='us-ascii', language='en')
2990        self.ndiffAssertEqual(msg.as_string(), """\
2991Return-Path: <bbb@zzz.org>
2992Delivered-To: bbb@zzz.org
2993Received: by mail.zzz.org (Postfix, from userid 889)
2994 id 27CEAD38CC; Fri,  4 May 2001 14:05:44 -0400 (EDT)
2995MIME-Version: 1.0
2996Content-Transfer-Encoding: 7bit
2997Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
2998From: bbb@ddd.com (John X. Doe)
2999To: bbb@zzz.org
3000Subject: This is a test message
3001Date: Fri, 4 May 2001 14:05:44 -0400
3002Content-Type: text/plain; charset=us-ascii;
3003 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3004
3005
3006Hi,
3007
3008Do you like this message?
3009
3010-Me
3011""")
3012
3013    def test_del_param(self):
3014        eq = self.ndiffAssertEqual
3015        msg = self._msgobj('msg_01.txt')
3016        msg.set_param('foo', 'bar', charset='us-ascii', language='en')
3017        msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3018            charset='us-ascii', language='en')
3019        msg.del_param('foo', header='Content-Type')
3020        eq(msg.as_string(), """\
3021Return-Path: <bbb@zzz.org>
3022Delivered-To: bbb@zzz.org
3023Received: by mail.zzz.org (Postfix, from userid 889)
3024 id 27CEAD38CC; Fri,  4 May 2001 14:05:44 -0400 (EDT)
3025MIME-Version: 1.0
3026Content-Transfer-Encoding: 7bit
3027Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3028From: bbb@ddd.com (John X. Doe)
3029To: bbb@zzz.org
3030Subject: This is a test message
3031Date: Fri, 4 May 2001 14:05:44 -0400
3032Content-Type: text/plain; charset="us-ascii";
3033 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3034
3035
3036Hi,
3037
3038Do you like this message?
3039
3040-Me
3041""")
3042
3043    def test_rfc2231_get_content_charset(self):
3044        eq = self.assertEqual
3045        msg = self._msgobj('msg_32.txt')
3046        eq(msg.get_content_charset(), 'us-ascii')
3047
3048    def test_rfc2231_no_language_or_charset(self):
3049        m = '''\
3050Content-Transfer-Encoding: 8bit
3051Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
3052Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
3053
3054'''
3055        msg = email.message_from_string(m)
3056        param = msg.get_param('NAME')
3057        self.assertFalse(isinstance(param, tuple))
3058        self.assertEqual(
3059            param,
3060            'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
3061
3062    def test_rfc2231_no_language_or_charset_in_filename(self):
3063        m = '''\
3064Content-Disposition: inline;
3065\tfilename*0*="''This%20is%20even%20more%20";
3066\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3067\tfilename*2="is it not.pdf"
3068
3069'''
3070        msg = email.message_from_string(m)
3071        self.assertEqual(msg.get_filename(),
3072                         'This is even more ***fun*** is it not.pdf')
3073
3074    def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
3075        m = '''\
3076Content-Disposition: inline;
3077\tfilename*0*="''This%20is%20even%20more%20";
3078\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3079\tfilename*2="is it not.pdf"
3080
3081'''
3082        msg = email.message_from_string(m)
3083        self.assertEqual(msg.get_filename(),
3084                         'This is even more ***fun*** is it not.pdf')
3085
3086    def test_rfc2231_partly_encoded(self):
3087        m = '''\
3088Content-Disposition: inline;
3089\tfilename*0="''This%20is%20even%20more%20";
3090\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3091\tfilename*2="is it not.pdf"
3092
3093'''
3094        msg = email.message_from_string(m)
3095        self.assertEqual(
3096            msg.get_filename(),
3097            'This%20is%20even%20more%20***fun*** is it not.pdf')
3098
3099    def test_rfc2231_partly_nonencoded(self):
3100        m = '''\
3101Content-Disposition: inline;
3102\tfilename*0="This%20is%20even%20more%20";
3103\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
3104\tfilename*2="is it not.pdf"
3105
3106'''
3107        msg = email.message_from_string(m)
3108        self.assertEqual(
3109            msg.get_filename(),
3110            'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
3111
3112    def test_rfc2231_no_language_or_charset_in_boundary(self):
3113        m = '''\
3114Content-Type: multipart/alternative;
3115\tboundary*0*="''This%20is%20even%20more%20";
3116\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
3117\tboundary*2="is it not.pdf"
3118
3119'''
3120        msg = email.message_from_string(m)
3121        self.assertEqual(msg.get_boundary(),
3122                         'This is even more ***fun*** is it not.pdf')
3123
3124    def test_rfc2231_no_language_or_charset_in_charset(self):
3125        # This is a nonsensical charset value, but tests the code anyway
3126        m = '''\
3127Content-Type: text/plain;
3128\tcharset*0*="This%20is%20even%20more%20";
3129\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
3130\tcharset*2="is it not.pdf"
3131
3132'''
3133        msg = email.message_from_string(m)
3134        self.assertEqual(msg.get_content_charset(),
3135                         'this is even more ***fun*** is it not.pdf')
3136
3137    def test_rfc2231_bad_encoding_in_filename(self):
3138        m = '''\
3139Content-Disposition: inline;
3140\tfilename*0*="bogus'xx'This%20is%20even%20more%20";
3141\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3142\tfilename*2="is it not.pdf"
3143
3144'''
3145        msg = email.message_from_string(m)
3146        self.assertEqual(msg.get_filename(),
3147                         'This is even more ***fun*** is it not.pdf')
3148
3149    def test_rfc2231_bad_encoding_in_charset(self):
3150        m = """\
3151Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
3152
3153"""
3154        msg = email.message_from_string(m)
3155        # This should return None because non-ascii characters in the charset
3156        # are not allowed.
3157        self.assertEqual(msg.get_content_charset(), None)
3158
3159    def test_rfc2231_bad_character_in_charset(self):
3160        m = """\
3161Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D
3162
3163"""
3164        msg = email.message_from_string(m)
3165        # This should return None because non-ascii characters in the charset
3166        # are not allowed.
3167        self.assertEqual(msg.get_content_charset(), None)
3168
3169    def test_rfc2231_bad_character_in_filename(self):
3170        m = '''\
3171Content-Disposition: inline;
3172\tfilename*0*="ascii'xx'This%20is%20even%20more%20";
3173\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3174\tfilename*2*="is it not.pdf%E2"
3175
3176'''
3177        msg = email.message_from_string(m)
3178        self.assertEqual(msg.get_filename(),
3179                         u'This is even more ***fun*** is it not.pdf\ufffd')
3180
3181    def test_rfc2231_unknown_encoding(self):
3182        m = """\
3183Content-Transfer-Encoding: 8bit
3184Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
3185
3186"""
3187        msg = email.message_from_string(m)
3188        self.assertEqual(msg.get_filename(), 'myfile.txt')
3189
3190    def test_rfc2231_single_tick_in_filename_extended(self):
3191        eq = self.assertEqual
3192        m = """\
3193Content-Type: application/x-foo;
3194\tname*0*=\"Frank's\"; name*1*=\" Document\"
3195
3196"""
3197        msg = email.message_from_string(m)
3198        charset, language, s = msg.get_param('name')
3199        eq(charset, None)
3200        eq(language, None)
3201        eq(s, "Frank's Document")
3202
3203    def test_rfc2231_single_tick_in_filename(self):
3204        m = """\
3205Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
3206
3207"""
3208        msg = email.message_from_string(m)
3209        param = msg.get_param('name')
3210        self.assertFalse(isinstance(param, tuple))
3211        self.assertEqual(param, "Frank's Document")
3212
3213    def test_rfc2231_tick_attack_extended(self):
3214        eq = self.assertEqual
3215        m = """\
3216Content-Type: application/x-foo;
3217\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
3218
3219"""
3220        msg = email.message_from_string(m)
3221        charset, language, s = msg.get_param('name')
3222        eq(charset, 'us-ascii')
3223        eq(language, 'en-us')
3224        eq(s, "Frank's Document")
3225
3226    def test_rfc2231_tick_attack(self):
3227        m = """\
3228Content-Type: application/x-foo;
3229\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
3230
3231"""
3232        msg = email.message_from_string(m)
3233        param = msg.get_param('name')
3234        self.assertFalse(isinstance(param, tuple))
3235        self.assertEqual(param, "us-ascii'en-us'Frank's Document")
3236
3237    def test_rfc2231_no_extended_values(self):
3238        eq = self.assertEqual
3239        m = """\
3240Content-Type: application/x-foo; name=\"Frank's Document\"
3241
3242"""
3243        msg = email.message_from_string(m)
3244        eq(msg.get_param('name'), "Frank's Document")
3245
3246    def test_rfc2231_encoded_then_unencoded_segments(self):
3247        eq = self.assertEqual
3248        m = """\
3249Content-Type: application/x-foo;
3250\tname*0*=\"us-ascii'en-us'My\";
3251\tname*1=\" Document\";
3252\tname*2*=\" For You\"
3253
3254"""
3255        msg = email.message_from_string(m)
3256        charset, language, s = msg.get_param('name')
3257        eq(charset, 'us-ascii')
3258        eq(language, 'en-us')
3259        eq(s, 'My Document For You')
3260
3261    def test_rfc2231_unencoded_then_encoded_segments(self):
3262        eq = self.assertEqual
3263        m = """\
3264Content-Type: application/x-foo;
3265\tname*0=\"us-ascii'en-us'My\";
3266\tname*1*=\" Document\";
3267\tname*2*=\" For You\"
3268
3269"""
3270        msg = email.message_from_string(m)
3271        charset, language, s = msg.get_param('name')
3272        eq(charset, 'us-ascii')
3273        eq(language, 'en-us')
3274        eq(s, 'My Document For You')
3275
3276
3277
3278def _testclasses():
3279    mod = sys.modules[__name__]
3280    return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
3281
3282
3283def suite():
3284    suite = unittest.TestSuite()
3285    for testclass in _testclasses():
3286        suite.addTest(unittest.makeSuite(testclass))
3287    return suite
3288
3289
3290def test_main():
3291    for testclass in _testclasses():
3292        run_unittest(testclass)
3293
3294
3295
3296if __name__ == '__main__':
3297    unittest.main(defaultTest='suite')
3298