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