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