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