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