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