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