blob: 0f28a9cdc9595cc066d0489c4cfbba08c824561c [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
556
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000557# Test the email.encoders module
558class TestEncoders(unittest.TestCase):
559 def test_encode_empty_payload(self):
560 eq = self.assertEqual
561 msg = Message()
562 msg.set_charset('us-ascii')
563 eq(msg['content-transfer-encoding'], '7bit')
564
565 def test_default_cte(self):
566 eq = self.assertEqual
Ezio Melottic30bb7d2010-04-22 11:58:06 +0000567 # 7bit data and the default us-ascii _charset
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000568 msg = MIMEText('hello world')
569 eq(msg['content-transfer-encoding'], '7bit')
Ezio Melottic30bb7d2010-04-22 11:58:06 +0000570 # Similar, but with 8bit data
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000571 msg = MIMEText('hello \xf8 world')
572 eq(msg['content-transfer-encoding'], '8bit')
573 # And now with a different charset
574 msg = MIMEText('hello \xf8 world', _charset='iso-8859-1')
575 eq(msg['content-transfer-encoding'], 'quoted-printable')
576
R. David Murrayf870d872010-05-06 01:53:03 +0000577 def test_encode7or8bit(self):
578 # Make sure a charset whose input character set is 8bit but
579 # whose output character set is 7bit gets a transfer-encoding
580 # of 7bit.
581 eq = self.assertEqual
R. David Murrayd2d08c62010-06-03 02:05:47 +0000582 msg = MIMEText('æ–‡', _charset='euc-jp')
R. David Murrayf870d872010-05-06 01:53:03 +0000583 eq(msg['content-transfer-encoding'], '7bit')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000584
Ezio Melotti19f2aeb2010-11-21 01:30:29 +0000585
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000586# Test long header wrapping
587class TestLongHeaders(TestEmailBase):
588 def test_split_long_continuation(self):
589 eq = self.ndiffAssertEqual
590 msg = email.message_from_string("""\
591Subject: bug demonstration
592\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
593\tmore text
594
595test
596""")
597 sfp = StringIO()
598 g = Generator(sfp)
599 g.flatten(msg)
600 eq(sfp.getvalue(), """\
601Subject: bug demonstration
602\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
603\tmore text
604
605test
606""")
607
608 def test_another_long_almost_unsplittable_header(self):
609 eq = self.ndiffAssertEqual
610 hstr = """\
611bug demonstration
612\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
613\tmore text"""
614 h = Header(hstr, continuation_ws='\t')
615 eq(h.encode(), """\
616bug demonstration
617\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
618\tmore text""")
619 h = Header(hstr.replace('\t', ' '))
620 eq(h.encode(), """\
621bug demonstration
622 12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
623 more text""")
624
625 def test_long_nonstring(self):
626 eq = self.ndiffAssertEqual
627 g = Charset("iso-8859-1")
628 cz = Charset("iso-8859-2")
629 utf8 = Charset("utf-8")
630 g_head = (b'Die Mieter treten hier ein werden mit einem Foerderband '
631 b'komfortabel den Korridor entlang, an s\xfcdl\xfcndischen '
632 b'Wandgem\xe4lden vorbei, gegen die rotierenden Klingen '
633 b'bef\xf6rdert. ')
634 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
635 b'd\xf9vtipu.. ')
636 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
637 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
638 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
639 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
640 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
641 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
642 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
643 '\u3044\u307e\u3059\u3002')
644 h = Header(g_head, g, header_name='Subject')
645 h.append(cz_head, cz)
646 h.append(utf8_head, utf8)
647 msg = Message()
648 msg['Subject'] = h
649 sfp = StringIO()
650 g = Generator(sfp)
651 g.flatten(msg)
652 eq(sfp.getvalue(), """\
Guido van Rossum9604e662007-08-30 03:46:43 +0000653Subject: =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderb?=
654 =?iso-8859-1?q?and_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen?=
655 =?iso-8859-1?q?_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef?=
656 =?iso-8859-1?q?=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hrouti?=
657 =?iso-8859-2?q?ly_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
658 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC5LiA?=
659 =?utf-8?b?6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn44Gf44KJ?=
660 =?utf-8?b?44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFzIE51bnN0dWNr?=
661 =?utf-8?b?IGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5kIGRhcyBPZGVyIGRp?=
662 =?utf-8?b?ZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIDjgaPjgabjgYTjgb7jgZk=?=
663 =?utf-8?b?44CC?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000664
665""")
Guido van Rossum9604e662007-08-30 03:46:43 +0000666 eq(h.encode(maxlinelen=76), """\
667=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerde?=
668 =?iso-8859-1?q?rband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndis?=
669 =?iso-8859-1?q?chen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klinge?=
670 =?iso-8859-1?q?n_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se?=
671 =?iso-8859-2?q?_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?=
672 =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb?=
673 =?utf-8?b?44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go?=
674 =?utf-8?b?44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBp?=
675 =?utf-8?b?c3QgZGFzIE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWlo?=
676 =?utf-8?b?ZXJodW5kIGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI0=?=
677 =?utf-8?b?44Go6KiA44Gj44Gm44GE44G+44GZ44CC?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000678
679 def test_long_header_encode(self):
680 eq = self.ndiffAssertEqual
681 h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
682 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
683 header_name='X-Foobar-Spoink-Defrobnit')
684 eq(h.encode(), '''\
685wasnipoop; giraffes="very-long-necked-animals";
686 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
687
688 def test_long_header_encode_with_tab_continuation_is_just_a_hint(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 continuation_ws='\t')
694 eq(h.encode(), '''\
695wasnipoop; giraffes="very-long-necked-animals";
696 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
697
698 def test_long_header_encode_with_tab_continuation(self):
699 eq = self.ndiffAssertEqual
700 h = Header('wasnipoop; giraffes="very-long-necked-animals";\t'
701 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
702 header_name='X-Foobar-Spoink-Defrobnit',
703 continuation_ws='\t')
704 eq(h.encode(), '''\
705wasnipoop; giraffes="very-long-necked-animals";
706\tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
707
708 def test_header_splitter(self):
709 eq = self.ndiffAssertEqual
710 msg = MIMEText('')
711 # It'd be great if we could use add_header() here, but that doesn't
712 # guarantee an order of the parameters.
713 msg['X-Foobar-Spoink-Defrobnit'] = (
714 'wasnipoop; giraffes="very-long-necked-animals"; '
715 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"')
716 sfp = StringIO()
717 g = Generator(sfp)
718 g.flatten(msg)
719 eq(sfp.getvalue(), '''\
720Content-Type: text/plain; charset="us-ascii"
721MIME-Version: 1.0
722Content-Transfer-Encoding: 7bit
723X-Foobar-Spoink-Defrobnit: wasnipoop; giraffes="very-long-necked-animals";
724 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"
725
726''')
727
728 def test_no_semis_header_splitter(self):
729 eq = self.ndiffAssertEqual
730 msg = Message()
731 msg['From'] = 'test@dom.ain'
732 msg['References'] = SPACE.join('<%d@dom.ain>' % i for i in range(10))
733 msg.set_payload('Test')
734 sfp = StringIO()
735 g = Generator(sfp)
736 g.flatten(msg)
737 eq(sfp.getvalue(), """\
738From: test@dom.ain
739References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain>
740 <5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain>
741
742Test""")
743
744 def test_no_split_long_header(self):
745 eq = self.ndiffAssertEqual
746 hstr = 'References: ' + 'x' * 80
Guido van Rossum9604e662007-08-30 03:46:43 +0000747 h = Header(hstr)
748 # These come on two lines because Headers are really field value
749 # classes and don't really know about their field names.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000750 eq(h.encode(), """\
Guido van Rossum9604e662007-08-30 03:46:43 +0000751References:
752 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""")
753 h = Header('x' * 80)
754 eq(h.encode(), 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000755
756 def test_splitting_multiple_long_lines(self):
757 eq = self.ndiffAssertEqual
758 hstr = """\
759from 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)
760\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)
761\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)
762"""
763 h = Header(hstr, continuation_ws='\t')
764 eq(h.encode(), """\
765from babylon.socal-raves.org (localhost [127.0.0.1]);
766 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
767 for <mailman-admin@babylon.socal-raves.org>;
768 Sat, 2 Feb 2002 17:00:06 -0800 (PST)
769\tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
770 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
771 for <mailman-admin@babylon.socal-raves.org>;
772 Sat, 2 Feb 2002 17:00:06 -0800 (PST)
773\tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
774 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
775 for <mailman-admin@babylon.socal-raves.org>;
776 Sat, 2 Feb 2002 17:00:06 -0800 (PST)""")
777
778 def test_splitting_first_line_only_is_long(self):
779 eq = self.ndiffAssertEqual
780 hstr = """\
781from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca)
782\tby kronos.mems-exchange.org with esmtp (Exim 4.05)
783\tid 17k4h5-00034i-00
784\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400"""
785 h = Header(hstr, maxlinelen=78, header_name='Received',
786 continuation_ws='\t')
787 eq(h.encode(), """\
788from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93]
789 helo=cthulhu.gerg.ca)
790\tby kronos.mems-exchange.org with esmtp (Exim 4.05)
791\tid 17k4h5-00034i-00
792\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""")
793
794 def test_long_8bit_header(self):
795 eq = self.ndiffAssertEqual
796 msg = Message()
797 h = Header('Britische Regierung gibt', 'iso-8859-1',
798 header_name='Subject')
799 h.append('gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte')
Guido van Rossum9604e662007-08-30 03:46:43 +0000800 eq(h.encode(maxlinelen=76), """\
801=?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offs?=
802 =?iso-8859-1?q?hore-Windkraftprojekte?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000803 msg['Subject'] = h
Guido van Rossum9604e662007-08-30 03:46:43 +0000804 eq(msg.as_string(maxheaderlen=76), """\
805Subject: =?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offs?=
806 =?iso-8859-1?q?hore-Windkraftprojekte?=
807
808""")
809 eq(msg.as_string(maxheaderlen=0), """\
810Subject: =?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offshore-Windkraftprojekte?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000811
812""")
813
814 def test_long_8bit_header_no_charset(self):
815 eq = self.ndiffAssertEqual
816 msg = Message()
Barry Warsaw8c571042007-08-30 19:17:18 +0000817 header_string = ('Britische Regierung gibt gr\xfcnes Licht '
818 'f\xfcr Offshore-Windkraftprojekte '
819 '<a-very-long-address@example.com>')
820 msg['Reply-To'] = header_string
821 self.assertRaises(UnicodeEncodeError, msg.as_string)
822 msg = Message()
823 msg['Reply-To'] = Header(header_string, 'utf-8',
824 header_name='Reply-To')
825 eq(msg.as_string(maxheaderlen=78), """\
826Reply-To: =?utf-8?q?Britische_Regierung_gibt_gr=C3=BCnes_Licht_f=C3=BCr_Offs?=
827 =?utf-8?q?hore-Windkraftprojekte_=3Ca-very-long-address=40example=2Ecom=3E?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000828
829""")
830
831 def test_long_to_header(self):
832 eq = self.ndiffAssertEqual
833 to = ('"Someone Test #A" <someone@eecs.umich.edu>,'
834 '<someone@eecs.umich.edu>,'
835 '"Someone Test #B" <someone@umich.edu>, '
836 '"Someone Test #C" <someone@eecs.umich.edu>, '
837 '"Someone Test #D" <someone@eecs.umich.edu>')
838 msg = Message()
839 msg['To'] = to
840 eq(msg.as_string(maxheaderlen=78), '''\
Guido van Rossum9604e662007-08-30 03:46:43 +0000841To: "Someone Test #A" <someone@eecs.umich.edu>,<someone@eecs.umich.edu>,
Barry Warsaw70d61ce2009-03-30 23:12:30 +0000842 "Someone Test #B" <someone@umich.edu>,
Guido van Rossum9604e662007-08-30 03:46:43 +0000843 "Someone Test #C" <someone@eecs.umich.edu>,
844 "Someone Test #D" <someone@eecs.umich.edu>
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000845
846''')
847
848 def test_long_line_after_append(self):
849 eq = self.ndiffAssertEqual
850 s = 'This is an example of string which has almost the limit of header length.'
851 h = Header(s)
852 h.append('Add another line.')
Guido van Rossum9604e662007-08-30 03:46:43 +0000853 eq(h.encode(maxlinelen=76), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000854This is an example of string which has almost the limit of header length.
855 Add another line.""")
856
857 def test_shorter_line_with_append(self):
858 eq = self.ndiffAssertEqual
859 s = 'This is a shorter line.'
860 h = Header(s)
861 h.append('Add another sentence. (Surprise?)')
862 eq(h.encode(),
863 'This is a shorter line. Add another sentence. (Surprise?)')
864
865 def test_long_field_name(self):
866 eq = self.ndiffAssertEqual
867 fn = 'X-Very-Very-Very-Long-Header-Name'
Guido van Rossum9604e662007-08-30 03:46:43 +0000868 gs = ('Die Mieter treten hier ein werden mit einem Foerderband '
869 'komfortabel den Korridor entlang, an s\xfcdl\xfcndischen '
870 'Wandgem\xe4lden vorbei, gegen die rotierenden Klingen '
871 'bef\xf6rdert. ')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000872 h = Header(gs, 'iso-8859-1', header_name=fn)
873 # BAW: this seems broken because the first line is too long
Guido van Rossum9604e662007-08-30 03:46:43 +0000874 eq(h.encode(maxlinelen=76), """\
875=?iso-8859-1?q?Die_Mieter_treten_hier_e?=
876 =?iso-8859-1?q?in_werden_mit_einem_Foerderband_komfortabel_den_Korridor_e?=
877 =?iso-8859-1?q?ntlang=2C_an_s=FCdl=FCndischen_Wandgem=E4lden_vorbei=2C_ge?=
878 =?iso-8859-1?q?gen_die_rotierenden_Klingen_bef=F6rdert=2E_?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000879
880 def test_long_received_header(self):
881 h = ('from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) '
882 'by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP; '
883 'Wed, 05 Mar 2003 18:10:18 -0700')
884 msg = Message()
885 msg['Received-1'] = Header(h, continuation_ws='\t')
886 msg['Received-2'] = h
Barry Warsawbef9d212007-08-31 10:55:37 +0000887 # This should be splitting on spaces not semicolons.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000888 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
Barry Warsawbef9d212007-08-31 10:55:37 +0000889Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
890 Wed, 05 Mar 2003 18:10:18 -0700
891Received-2: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
892 Wed, 05 Mar 2003 18:10:18 -0700
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000893
894""")
895
896 def test_string_headerinst_eq(self):
897 h = ('<15975.17901.207240.414604@sgigritzmann1.mathematik.'
898 'tu-muenchen.de> (David Bremner\'s message of '
899 '"Thu, 6 Mar 2003 13:58:21 +0100")')
900 msg = Message()
901 msg['Received-1'] = Header(h, header_name='Received-1',
902 continuation_ws='\t')
903 msg['Received-2'] = h
Barry Warsawbef9d212007-08-31 10:55:37 +0000904 # XXX This should be splitting on spaces not commas.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000905 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
Barry Warsawbef9d212007-08-31 10:55:37 +0000906Received-1: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner's message of \"Thu,
907 6 Mar 2003 13:58:21 +0100\")
908Received-2: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner's message of \"Thu,
909 6 Mar 2003 13:58:21 +0100\")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000910
911""")
912
913 def test_long_unbreakable_lines_with_continuation(self):
914 eq = self.ndiffAssertEqual
915 msg = Message()
916 t = """\
917iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
918 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp"""
919 msg['Face-1'] = t
920 msg['Face-2'] = Header(t, header_name='Face-2')
Barry Warsawbef9d212007-08-31 10:55:37 +0000921 # XXX This splitting is all wrong. It the first value line should be
922 # snug against the field name.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000923 eq(msg.as_string(maxheaderlen=78), """\
Barry Warsawc5a6a302007-08-31 11:19:21 +0000924Face-1:\x20
Barry Warsaw70d61ce2009-03-30 23:12:30 +0000925 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000926 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
Barry Warsawc5a6a302007-08-31 11:19:21 +0000927Face-2:\x20
Barry Warsawbef9d212007-08-31 10:55:37 +0000928 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000929 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
930
931""")
932
933 def test_another_long_multiline_header(self):
934 eq = self.ndiffAssertEqual
935 m = ('Received: from siimage.com '
936 '([172.25.1.3]) by zima.siliconimage.com with '
Guido van Rossum9604e662007-08-30 03:46:43 +0000937 'Microsoft SMTPSVC(5.0.2195.4905); '
938 'Wed, 16 Oct 2002 07:41:11 -0700')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000939 msg = email.message_from_string(m)
940 eq(msg.as_string(maxheaderlen=78), '''\
Barry Warsawbef9d212007-08-31 10:55:37 +0000941Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with Microsoft SMTPSVC(5.0.2195.4905);
942 Wed, 16 Oct 2002 07:41:11 -0700
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000943
944''')
945
946 def test_long_lines_with_different_header(self):
947 eq = self.ndiffAssertEqual
948 h = ('List-Unsubscribe: '
949 '<http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,'
950 ' <mailto:spamassassin-talk-request@lists.sourceforge.net'
951 '?subject=unsubscribe>')
952 msg = Message()
953 msg['List'] = h
954 msg['List'] = Header(h, header_name='List')
955 eq(msg.as_string(maxheaderlen=78), """\
956List: List-Unsubscribe: <http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
Barry Warsawbef9d212007-08-31 10:55:37 +0000957 <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000958List: List-Unsubscribe: <http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
Barry Warsawbef9d212007-08-31 10:55:37 +0000959 <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000960
961""")
962
R. David Murray43b2f452011-02-11 03:13:19 +0000963 def test_long_rfc2047_header_with_embedded_fws(self):
964 h = Header(textwrap.dedent("""\
965 We're going to pretend this header is in a non-ascii character set
966 \tto see if line wrapping with encoded words and embedded
967 folding white space works"""),
968 charset='utf-8',
969 header_name='Test')
970 self.assertEqual(h.encode()+'\n', textwrap.dedent("""\
971 =?utf-8?q?We=27re_going_to_pretend_this_header_is_in_a_non-ascii_chara?=
972 =?utf-8?q?cter_set?=
973 =?utf-8?q?_to_see_if_line_wrapping_with_encoded_words_and_embedded?=
974 =?utf-8?q?_folding_white_space_works?=""")+'\n')
975
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000976
Ezio Melotti19f2aeb2010-11-21 01:30:29 +0000977
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000978# Test mangling of "From " lines in the body of a message
979class TestFromMangling(unittest.TestCase):
980 def setUp(self):
981 self.msg = Message()
982 self.msg['From'] = 'aaa@bbb.org'
983 self.msg.set_payload("""\
984From the desk of A.A.A.:
985Blah blah blah
986""")
987
988 def test_mangled_from(self):
989 s = StringIO()
990 g = Generator(s, mangle_from_=True)
991 g.flatten(self.msg)
992 self.assertEqual(s.getvalue(), """\
993From: aaa@bbb.org
994
995>From the desk of A.A.A.:
996Blah blah blah
997""")
998
999 def test_dont_mangle_from(self):
1000 s = StringIO()
1001 g = Generator(s, mangle_from_=False)
1002 g.flatten(self.msg)
1003 self.assertEqual(s.getvalue(), """\
1004From: aaa@bbb.org
1005
1006From the desk of A.A.A.:
1007Blah blah blah
1008""")
1009
1010
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001011
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001012# Test the basic MIMEAudio class
1013class TestMIMEAudio(unittest.TestCase):
1014 def setUp(self):
1015 # Make sure we pick up the audiotest.au that lives in email/test/data.
1016 # In Python, there's an audiotest.au living in Lib/test but that isn't
1017 # included in some binary distros that don't include the test
1018 # package. The trailing empty string on the .join() is significant
1019 # since findfile() will do a dirname().
1020 datadir = os.path.join(os.path.dirname(landmark), 'data', '')
1021 with open(findfile('audiotest.au', datadir), 'rb') as fp:
1022 self._audiodata = fp.read()
1023 self._au = MIMEAudio(self._audiodata)
1024
1025 def test_guess_minor_type(self):
1026 self.assertEqual(self._au.get_content_type(), 'audio/basic')
1027
1028 def test_encoding(self):
1029 payload = self._au.get_payload()
R. David Murray99147c42010-06-04 16:15:34 +00001030 self.assertEqual(base64.decodebytes(bytes(payload, 'ascii')),
1031 self._audiodata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001032
1033 def test_checkSetMinor(self):
1034 au = MIMEAudio(self._audiodata, 'fish')
1035 self.assertEqual(au.get_content_type(), 'audio/fish')
1036
1037 def test_add_header(self):
1038 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001039 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001040 self._au.add_header('Content-Disposition', 'attachment',
1041 filename='audiotest.au')
1042 eq(self._au['content-disposition'],
1043 'attachment; filename="audiotest.au"')
1044 eq(self._au.get_params(header='content-disposition'),
1045 [('attachment', ''), ('filename', 'audiotest.au')])
1046 eq(self._au.get_param('filename', header='content-disposition'),
1047 'audiotest.au')
1048 missing = []
1049 eq(self._au.get_param('attachment', header='content-disposition'), '')
1050 unless(self._au.get_param('foo', failobj=missing,
1051 header='content-disposition') is missing)
1052 # Try some missing stuff
1053 unless(self._au.get_param('foobar', missing) is missing)
1054 unless(self._au.get_param('attachment', missing,
1055 header='foobar') is missing)
1056
1057
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001058
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001059# Test the basic MIMEImage class
1060class TestMIMEImage(unittest.TestCase):
1061 def setUp(self):
1062 with openfile('PyBanner048.gif', 'rb') as fp:
1063 self._imgdata = fp.read()
1064 self._im = MIMEImage(self._imgdata)
1065
1066 def test_guess_minor_type(self):
1067 self.assertEqual(self._im.get_content_type(), 'image/gif')
1068
1069 def test_encoding(self):
1070 payload = self._im.get_payload()
R. David Murray99147c42010-06-04 16:15:34 +00001071 self.assertEqual(base64.decodebytes(bytes(payload, 'ascii')),
1072 self._imgdata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001073
1074 def test_checkSetMinor(self):
1075 im = MIMEImage(self._imgdata, 'fish')
1076 self.assertEqual(im.get_content_type(), 'image/fish')
1077
1078 def test_add_header(self):
1079 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001080 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001081 self._im.add_header('Content-Disposition', 'attachment',
1082 filename='dingusfish.gif')
1083 eq(self._im['content-disposition'],
1084 'attachment; filename="dingusfish.gif"')
1085 eq(self._im.get_params(header='content-disposition'),
1086 [('attachment', ''), ('filename', 'dingusfish.gif')])
1087 eq(self._im.get_param('filename', header='content-disposition'),
1088 'dingusfish.gif')
1089 missing = []
1090 eq(self._im.get_param('attachment', header='content-disposition'), '')
1091 unless(self._im.get_param('foo', failobj=missing,
1092 header='content-disposition') is missing)
1093 # Try some missing stuff
1094 unless(self._im.get_param('foobar', missing) is missing)
1095 unless(self._im.get_param('attachment', missing,
1096 header='foobar') is missing)
1097
1098
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001099
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001100# Test the basic MIMEApplication class
1101class TestMIMEApplication(unittest.TestCase):
1102 def test_headers(self):
1103 eq = self.assertEqual
Barry Warsaw8b2af272007-08-31 03:04:26 +00001104 msg = MIMEApplication(b'\xfa\xfb\xfc\xfd\xfe\xff')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001105 eq(msg.get_content_type(), 'application/octet-stream')
1106 eq(msg['content-transfer-encoding'], 'base64')
1107
1108 def test_body(self):
1109 eq = self.assertEqual
Barry Warsaw8c571042007-08-30 19:17:18 +00001110 bytes = b'\xfa\xfb\xfc\xfd\xfe\xff'
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001111 msg = MIMEApplication(bytes)
R. David Murray99147c42010-06-04 16:15:34 +00001112 eq(msg.get_payload(), '+vv8/f7/')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001113 eq(msg.get_payload(decode=True), bytes)
1114
1115
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001116
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001117# Test the basic MIMEText class
1118class TestMIMEText(unittest.TestCase):
1119 def setUp(self):
1120 self._msg = MIMEText('hello there')
1121
1122 def test_types(self):
1123 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001124 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001125 eq(self._msg.get_content_type(), 'text/plain')
1126 eq(self._msg.get_param('charset'), 'us-ascii')
1127 missing = []
1128 unless(self._msg.get_param('foobar', missing) is missing)
1129 unless(self._msg.get_param('charset', missing, header='foobar')
1130 is missing)
1131
1132 def test_payload(self):
1133 self.assertEqual(self._msg.get_payload(), 'hello there')
Georg Brandlab91fde2009-08-13 08:51:18 +00001134 self.assertTrue(not self._msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001135
1136 def test_charset(self):
1137 eq = self.assertEqual
1138 msg = MIMEText('hello there', _charset='us-ascii')
1139 eq(msg.get_charset().input_charset, 'us-ascii')
1140 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1141
R. David Murrayd2d08c62010-06-03 02:05:47 +00001142 def test_7bit_input(self):
1143 eq = self.assertEqual
1144 msg = MIMEText('hello there', _charset='us-ascii')
1145 eq(msg.get_charset().input_charset, 'us-ascii')
1146 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1147
1148 def test_7bit_input_no_charset(self):
1149 eq = self.assertEqual
1150 msg = MIMEText('hello there')
1151 eq(msg.get_charset(), 'us-ascii')
1152 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1153 self.assertTrue('hello there' in msg.as_string())
1154
1155 def test_utf8_input(self):
1156 teststr = '\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
1157 eq = self.assertEqual
1158 msg = MIMEText(teststr, _charset='utf-8')
1159 eq(msg.get_charset().output_charset, 'utf-8')
1160 eq(msg['content-type'], 'text/plain; charset="utf-8"')
1161 eq(msg.get_payload(decode=True), teststr.encode('utf-8'))
1162
1163 @unittest.skip("can't fix because of backward compat in email5, "
1164 "will fix in email6")
1165 def test_utf8_input_no_charset(self):
1166 teststr = '\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
1167 self.assertRaises(UnicodeEncodeError, MIMEText, teststr)
1168
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001169
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001170
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001171# Test complicated multipart/* messages
1172class TestMultipart(TestEmailBase):
1173 def setUp(self):
1174 with openfile('PyBanner048.gif', 'rb') as fp:
1175 data = fp.read()
1176 container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
1177 image = MIMEImage(data, name='dingusfish.gif')
1178 image.add_header('content-disposition', 'attachment',
1179 filename='dingusfish.gif')
1180 intro = MIMEText('''\
1181Hi there,
1182
1183This is the dingus fish.
1184''')
1185 container.attach(intro)
1186 container.attach(image)
1187 container['From'] = 'Barry <barry@digicool.com>'
1188 container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>'
1189 container['Subject'] = 'Here is your dingus fish'
1190
1191 now = 987809702.54848599
1192 timetuple = time.localtime(now)
1193 if timetuple[-1] == 0:
1194 tzsecs = time.timezone
1195 else:
1196 tzsecs = time.altzone
1197 if tzsecs > 0:
1198 sign = '-'
1199 else:
1200 sign = '+'
1201 tzoffset = ' %s%04d' % (sign, tzsecs / 36)
1202 container['Date'] = time.strftime(
1203 '%a, %d %b %Y %H:%M:%S',
1204 time.localtime(now)) + tzoffset
1205 self._msg = container
1206 self._im = image
1207 self._txt = intro
1208
1209 def test_hierarchy(self):
1210 # convenience
1211 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001212 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001213 raises = self.assertRaises
1214 # tests
1215 m = self._msg
1216 unless(m.is_multipart())
1217 eq(m.get_content_type(), 'multipart/mixed')
1218 eq(len(m.get_payload()), 2)
1219 raises(IndexError, m.get_payload, 2)
1220 m0 = m.get_payload(0)
1221 m1 = m.get_payload(1)
1222 unless(m0 is self._txt)
1223 unless(m1 is self._im)
1224 eq(m.get_payload(), [m0, m1])
1225 unless(not m0.is_multipart())
1226 unless(not m1.is_multipart())
1227
1228 def test_empty_multipart_idempotent(self):
1229 text = """\
1230Content-Type: multipart/mixed; boundary="BOUNDARY"
1231MIME-Version: 1.0
1232Subject: A subject
1233To: aperson@dom.ain
1234From: bperson@dom.ain
1235
1236
1237--BOUNDARY
1238
1239
1240--BOUNDARY--
1241"""
1242 msg = Parser().parsestr(text)
1243 self.ndiffAssertEqual(text, msg.as_string())
1244
1245 def test_no_parts_in_a_multipart_with_none_epilogue(self):
1246 outer = MIMEBase('multipart', 'mixed')
1247 outer['Subject'] = 'A subject'
1248 outer['To'] = 'aperson@dom.ain'
1249 outer['From'] = 'bperson@dom.ain'
1250 outer.set_boundary('BOUNDARY')
1251 self.ndiffAssertEqual(outer.as_string(), '''\
1252Content-Type: multipart/mixed; boundary="BOUNDARY"
1253MIME-Version: 1.0
1254Subject: A subject
1255To: aperson@dom.ain
1256From: bperson@dom.ain
1257
1258--BOUNDARY
1259
1260--BOUNDARY--''')
1261
1262 def test_no_parts_in_a_multipart_with_empty_epilogue(self):
1263 outer = MIMEBase('multipart', 'mixed')
1264 outer['Subject'] = 'A subject'
1265 outer['To'] = 'aperson@dom.ain'
1266 outer['From'] = 'bperson@dom.ain'
1267 outer.preamble = ''
1268 outer.epilogue = ''
1269 outer.set_boundary('BOUNDARY')
1270 self.ndiffAssertEqual(outer.as_string(), '''\
1271Content-Type: multipart/mixed; boundary="BOUNDARY"
1272MIME-Version: 1.0
1273Subject: A subject
1274To: aperson@dom.ain
1275From: bperson@dom.ain
1276
1277
1278--BOUNDARY
1279
1280--BOUNDARY--
1281''')
1282
1283 def test_one_part_in_a_multipart(self):
1284 eq = self.ndiffAssertEqual
1285 outer = MIMEBase('multipart', 'mixed')
1286 outer['Subject'] = 'A subject'
1287 outer['To'] = 'aperson@dom.ain'
1288 outer['From'] = 'bperson@dom.ain'
1289 outer.set_boundary('BOUNDARY')
1290 msg = MIMEText('hello world')
1291 outer.attach(msg)
1292 eq(outer.as_string(), '''\
1293Content-Type: multipart/mixed; boundary="BOUNDARY"
1294MIME-Version: 1.0
1295Subject: A subject
1296To: aperson@dom.ain
1297From: bperson@dom.ain
1298
1299--BOUNDARY
1300Content-Type: text/plain; charset="us-ascii"
1301MIME-Version: 1.0
1302Content-Transfer-Encoding: 7bit
1303
1304hello world
1305--BOUNDARY--''')
1306
1307 def test_seq_parts_in_a_multipart_with_empty_preamble(self):
1308 eq = self.ndiffAssertEqual
1309 outer = MIMEBase('multipart', 'mixed')
1310 outer['Subject'] = 'A subject'
1311 outer['To'] = 'aperson@dom.ain'
1312 outer['From'] = 'bperson@dom.ain'
1313 outer.preamble = ''
1314 msg = MIMEText('hello world')
1315 outer.attach(msg)
1316 outer.set_boundary('BOUNDARY')
1317 eq(outer.as_string(), '''\
1318Content-Type: multipart/mixed; boundary="BOUNDARY"
1319MIME-Version: 1.0
1320Subject: A subject
1321To: aperson@dom.ain
1322From: bperson@dom.ain
1323
1324
1325--BOUNDARY
1326Content-Type: text/plain; charset="us-ascii"
1327MIME-Version: 1.0
1328Content-Transfer-Encoding: 7bit
1329
1330hello world
1331--BOUNDARY--''')
1332
1333
1334 def test_seq_parts_in_a_multipart_with_none_preamble(self):
1335 eq = self.ndiffAssertEqual
1336 outer = MIMEBase('multipart', 'mixed')
1337 outer['Subject'] = 'A subject'
1338 outer['To'] = 'aperson@dom.ain'
1339 outer['From'] = 'bperson@dom.ain'
1340 outer.preamble = None
1341 msg = MIMEText('hello world')
1342 outer.attach(msg)
1343 outer.set_boundary('BOUNDARY')
1344 eq(outer.as_string(), '''\
1345Content-Type: multipart/mixed; boundary="BOUNDARY"
1346MIME-Version: 1.0
1347Subject: A subject
1348To: aperson@dom.ain
1349From: bperson@dom.ain
1350
1351--BOUNDARY
1352Content-Type: text/plain; charset="us-ascii"
1353MIME-Version: 1.0
1354Content-Transfer-Encoding: 7bit
1355
1356hello world
1357--BOUNDARY--''')
1358
1359
1360 def test_seq_parts_in_a_multipart_with_none_epilogue(self):
1361 eq = self.ndiffAssertEqual
1362 outer = MIMEBase('multipart', 'mixed')
1363 outer['Subject'] = 'A subject'
1364 outer['To'] = 'aperson@dom.ain'
1365 outer['From'] = 'bperson@dom.ain'
1366 outer.epilogue = None
1367 msg = MIMEText('hello world')
1368 outer.attach(msg)
1369 outer.set_boundary('BOUNDARY')
1370 eq(outer.as_string(), '''\
1371Content-Type: multipart/mixed; boundary="BOUNDARY"
1372MIME-Version: 1.0
1373Subject: A subject
1374To: aperson@dom.ain
1375From: bperson@dom.ain
1376
1377--BOUNDARY
1378Content-Type: text/plain; charset="us-ascii"
1379MIME-Version: 1.0
1380Content-Transfer-Encoding: 7bit
1381
1382hello world
1383--BOUNDARY--''')
1384
1385
1386 def test_seq_parts_in_a_multipart_with_empty_epilogue(self):
1387 eq = self.ndiffAssertEqual
1388 outer = MIMEBase('multipart', 'mixed')
1389 outer['Subject'] = 'A subject'
1390 outer['To'] = 'aperson@dom.ain'
1391 outer['From'] = 'bperson@dom.ain'
1392 outer.epilogue = ''
1393 msg = MIMEText('hello world')
1394 outer.attach(msg)
1395 outer.set_boundary('BOUNDARY')
1396 eq(outer.as_string(), '''\
1397Content-Type: multipart/mixed; boundary="BOUNDARY"
1398MIME-Version: 1.0
1399Subject: A subject
1400To: aperson@dom.ain
1401From: bperson@dom.ain
1402
1403--BOUNDARY
1404Content-Type: text/plain; charset="us-ascii"
1405MIME-Version: 1.0
1406Content-Transfer-Encoding: 7bit
1407
1408hello world
1409--BOUNDARY--
1410''')
1411
1412
1413 def test_seq_parts_in_a_multipart_with_nl_epilogue(self):
1414 eq = self.ndiffAssertEqual
1415 outer = MIMEBase('multipart', 'mixed')
1416 outer['Subject'] = 'A subject'
1417 outer['To'] = 'aperson@dom.ain'
1418 outer['From'] = 'bperson@dom.ain'
1419 outer.epilogue = '\n'
1420 msg = MIMEText('hello world')
1421 outer.attach(msg)
1422 outer.set_boundary('BOUNDARY')
1423 eq(outer.as_string(), '''\
1424Content-Type: multipart/mixed; boundary="BOUNDARY"
1425MIME-Version: 1.0
1426Subject: A subject
1427To: aperson@dom.ain
1428From: bperson@dom.ain
1429
1430--BOUNDARY
1431Content-Type: text/plain; charset="us-ascii"
1432MIME-Version: 1.0
1433Content-Transfer-Encoding: 7bit
1434
1435hello world
1436--BOUNDARY--
1437
1438''')
1439
1440 def test_message_external_body(self):
1441 eq = self.assertEqual
1442 msg = self._msgobj('msg_36.txt')
1443 eq(len(msg.get_payload()), 2)
1444 msg1 = msg.get_payload(1)
1445 eq(msg1.get_content_type(), 'multipart/alternative')
1446 eq(len(msg1.get_payload()), 2)
1447 for subpart in msg1.get_payload():
1448 eq(subpart.get_content_type(), 'message/external-body')
1449 eq(len(subpart.get_payload()), 1)
1450 subsubpart = subpart.get_payload(0)
1451 eq(subsubpart.get_content_type(), 'text/plain')
1452
1453 def test_double_boundary(self):
1454 # msg_37.txt is a multipart that contains two dash-boundary's in a
1455 # row. Our interpretation of RFC 2046 calls for ignoring the second
1456 # and subsequent boundaries.
1457 msg = self._msgobj('msg_37.txt')
1458 self.assertEqual(len(msg.get_payload()), 3)
1459
1460 def test_nested_inner_contains_outer_boundary(self):
1461 eq = self.ndiffAssertEqual
1462 # msg_38.txt has an inner part that contains outer boundaries. My
1463 # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say
1464 # these are illegal and should be interpreted as unterminated inner
1465 # parts.
1466 msg = self._msgobj('msg_38.txt')
1467 sfp = StringIO()
1468 iterators._structure(msg, sfp)
1469 eq(sfp.getvalue(), """\
1470multipart/mixed
1471 multipart/mixed
1472 multipart/alternative
1473 text/plain
1474 text/plain
1475 text/plain
1476 text/plain
1477""")
1478
1479 def test_nested_with_same_boundary(self):
1480 eq = self.ndiffAssertEqual
1481 # msg 39.txt is similarly evil in that it's got inner parts that use
1482 # the same boundary as outer parts. Again, I believe the way this is
1483 # parsed is closest to the spirit of RFC 2046
1484 msg = self._msgobj('msg_39.txt')
1485 sfp = StringIO()
1486 iterators._structure(msg, sfp)
1487 eq(sfp.getvalue(), """\
1488multipart/mixed
1489 multipart/mixed
1490 multipart/alternative
1491 application/octet-stream
1492 application/octet-stream
1493 text/plain
1494""")
1495
1496 def test_boundary_in_non_multipart(self):
1497 msg = self._msgobj('msg_40.txt')
1498 self.assertEqual(msg.as_string(), '''\
1499MIME-Version: 1.0
1500Content-Type: text/html; boundary="--961284236552522269"
1501
1502----961284236552522269
1503Content-Type: text/html;
1504Content-Transfer-Encoding: 7Bit
1505
1506<html></html>
1507
1508----961284236552522269--
1509''')
1510
1511 def test_boundary_with_leading_space(self):
1512 eq = self.assertEqual
1513 msg = email.message_from_string('''\
1514MIME-Version: 1.0
1515Content-Type: multipart/mixed; boundary=" XXXX"
1516
1517-- XXXX
1518Content-Type: text/plain
1519
1520
1521-- XXXX
1522Content-Type: text/plain
1523
1524-- XXXX--
1525''')
Georg Brandlab91fde2009-08-13 08:51:18 +00001526 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001527 eq(msg.get_boundary(), ' XXXX')
1528 eq(len(msg.get_payload()), 2)
1529
1530 def test_boundary_without_trailing_newline(self):
1531 m = Parser().parsestr("""\
1532Content-Type: multipart/mixed; boundary="===============0012394164=="
1533MIME-Version: 1.0
1534
1535--===============0012394164==
1536Content-Type: image/file1.jpg
1537MIME-Version: 1.0
1538Content-Transfer-Encoding: base64
1539
1540YXNkZg==
1541--===============0012394164==--""")
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001542 self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001543
1544
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001545
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001546# Test some badly formatted messages
1547class TestNonConformant(TestEmailBase):
1548 def test_parse_missing_minor_type(self):
1549 eq = self.assertEqual
1550 msg = self._msgobj('msg_14.txt')
1551 eq(msg.get_content_type(), 'text/plain')
1552 eq(msg.get_content_maintype(), 'text')
1553 eq(msg.get_content_subtype(), 'plain')
1554
1555 def test_same_boundary_inner_outer(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00001556 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001557 msg = self._msgobj('msg_15.txt')
1558 # XXX We can probably eventually do better
1559 inner = msg.get_payload(0)
1560 unless(hasattr(inner, 'defects'))
1561 self.assertEqual(len(inner.defects), 1)
1562 unless(isinstance(inner.defects[0],
1563 errors.StartBoundaryNotFoundDefect))
1564
1565 def test_multipart_no_boundary(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_25.txt')
1568 unless(isinstance(msg.get_payload(), str))
1569 self.assertEqual(len(msg.defects), 2)
1570 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1571 unless(isinstance(msg.defects[1],
1572 errors.MultipartInvariantViolationDefect))
1573
1574 def test_invalid_content_type(self):
1575 eq = self.assertEqual
1576 neq = self.ndiffAssertEqual
1577 msg = Message()
1578 # RFC 2045, $5.2 says invalid yields text/plain
1579 msg['Content-Type'] = 'text'
1580 eq(msg.get_content_maintype(), 'text')
1581 eq(msg.get_content_subtype(), 'plain')
1582 eq(msg.get_content_type(), 'text/plain')
1583 # Clear the old value and try something /really/ invalid
1584 del msg['content-type']
1585 msg['Content-Type'] = 'foo'
1586 eq(msg.get_content_maintype(), 'text')
1587 eq(msg.get_content_subtype(), 'plain')
1588 eq(msg.get_content_type(), 'text/plain')
1589 # Still, make sure that the message is idempotently generated
1590 s = StringIO()
1591 g = Generator(s)
1592 g.flatten(msg)
1593 neq(s.getvalue(), 'Content-Type: foo\n\n')
1594
1595 def test_no_start_boundary(self):
1596 eq = self.ndiffAssertEqual
1597 msg = self._msgobj('msg_31.txt')
1598 eq(msg.get_payload(), """\
1599--BOUNDARY
1600Content-Type: text/plain
1601
1602message 1
1603
1604--BOUNDARY
1605Content-Type: text/plain
1606
1607message 2
1608
1609--BOUNDARY--
1610""")
1611
1612 def test_no_separating_blank_line(self):
1613 eq = self.ndiffAssertEqual
1614 msg = self._msgobj('msg_35.txt')
1615 eq(msg.as_string(), """\
1616From: aperson@dom.ain
1617To: bperson@dom.ain
1618Subject: here's something interesting
1619
1620counter to RFC 2822, there's no separating newline here
1621""")
1622
1623 def test_lying_multipart(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00001624 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001625 msg = self._msgobj('msg_41.txt')
1626 unless(hasattr(msg, 'defects'))
1627 self.assertEqual(len(msg.defects), 2)
1628 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1629 unless(isinstance(msg.defects[1],
1630 errors.MultipartInvariantViolationDefect))
1631
1632 def test_missing_start_boundary(self):
1633 outer = self._msgobj('msg_42.txt')
1634 # The message structure is:
1635 #
1636 # multipart/mixed
1637 # text/plain
1638 # message/rfc822
1639 # multipart/mixed [*]
1640 #
1641 # [*] This message is missing its start boundary
1642 bad = outer.get_payload(1).get_payload(0)
1643 self.assertEqual(len(bad.defects), 1)
Georg Brandlab91fde2009-08-13 08:51:18 +00001644 self.assertTrue(isinstance(bad.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001645 errors.StartBoundaryNotFoundDefect))
1646
1647 def test_first_line_is_continuation_header(self):
1648 eq = self.assertEqual
1649 m = ' Line 1\nLine 2\nLine 3'
1650 msg = email.message_from_string(m)
1651 eq(msg.keys(), [])
1652 eq(msg.get_payload(), 'Line 2\nLine 3')
1653 eq(len(msg.defects), 1)
Georg Brandlab91fde2009-08-13 08:51:18 +00001654 self.assertTrue(isinstance(msg.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001655 errors.FirstHeaderLineIsContinuationDefect))
1656 eq(msg.defects[0].line, ' Line 1\n')
1657
1658
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001659
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001660# Test RFC 2047 header encoding and decoding
Guido van Rossum9604e662007-08-30 03:46:43 +00001661class TestRFC2047(TestEmailBase):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001662 def test_rfc2047_multiline(self):
1663 eq = self.assertEqual
1664 s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz
1665 foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?="""
1666 dh = decode_header(s)
1667 eq(dh, [
1668 (b'Re:', None),
1669 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland'),
1670 (b'baz foo bar', None),
1671 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland')])
1672 header = make_header(dh)
1673 eq(str(header),
1674 'Re: r\xe4ksm\xf6rg\xe5s baz foo bar r\xe4ksm\xf6rg\xe5s')
Barry Warsaw00b34222007-08-31 02:35:00 +00001675 self.ndiffAssertEqual(header.encode(maxlinelen=76), """\
Guido van Rossum9604e662007-08-30 03:46:43 +00001676Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar =?mac-iceland?q?r=8Aksm?=
1677 =?mac-iceland?q?=9Arg=8Cs?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001678
1679 def test_whitespace_eater_unicode(self):
1680 eq = self.assertEqual
1681 s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard@dom.ain>'
1682 dh = decode_header(s)
1683 eq(dh, [(b'Andr\xe9', 'iso-8859-1'),
1684 (b'Pirard <pirard@dom.ain>', None)])
1685 header = str(make_header(dh))
1686 eq(header, 'Andr\xe9 Pirard <pirard@dom.ain>')
1687
1688 def test_whitespace_eater_unicode_2(self):
1689 eq = self.assertEqual
1690 s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?='
1691 dh = decode_header(s)
1692 eq(dh, [(b'The', None), (b'quick brown fox', 'iso-8859-1'),
1693 (b'jumped over the', None), (b'lazy dog', 'iso-8859-1')])
1694 hu = str(make_header(dh))
1695 eq(hu, 'The quick brown fox jumped over the lazy dog')
1696
1697 def test_rfc2047_missing_whitespace(self):
1698 s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord'
1699 dh = decode_header(s)
1700 self.assertEqual(dh, [(s, None)])
1701
1702 def test_rfc2047_with_whitespace(self):
1703 s = 'Sm =?ISO-8859-1?B?9g==?= rg =?ISO-8859-1?B?5Q==?= sbord'
1704 dh = decode_header(s)
1705 self.assertEqual(dh, [(b'Sm', None), (b'\xf6', 'iso-8859-1'),
1706 (b'rg', None), (b'\xe5', 'iso-8859-1'),
1707 (b'sbord', None)])
1708
R. David Murraye06528c2010-08-03 23:35:44 +00001709 def test_rfc2047_B_bad_padding(self):
1710 s = '=?iso-8859-1?B?%s?='
1711 data = [ # only test complete bytes
1712 ('dm==', b'v'), ('dm=', b'v'), ('dm', b'v'),
1713 ('dmk=', b'vi'), ('dmk', b'vi')
1714 ]
1715 for q, a in data:
1716 dh = decode_header(s % q)
1717 self.assertEqual(dh, [(a, 'iso-8859-1')])
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001718
R. David Murrayf9c957f2010-10-01 15:45:48 +00001719 def test_rfc2047_Q_invalid_digits(self):
1720 # issue 10004.
1721 s = '=?iso-8659-1?Q?andr=e9=zz?='
1722 self.assertEqual(decode_header(s),
1723 [(b'andr\xe9=zz', 'iso-8659-1')])
1724
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001725
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001726# Test the MIMEMessage class
1727class TestMIMEMessage(TestEmailBase):
1728 def setUp(self):
1729 with openfile('msg_11.txt') as fp:
1730 self._text = fp.read()
1731
1732 def test_type_error(self):
1733 self.assertRaises(TypeError, MIMEMessage, 'a plain string')
1734
1735 def test_valid_argument(self):
1736 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001737 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001738 subject = 'A sub-message'
1739 m = Message()
1740 m['Subject'] = subject
1741 r = MIMEMessage(m)
1742 eq(r.get_content_type(), 'message/rfc822')
1743 payload = r.get_payload()
1744 unless(isinstance(payload, list))
1745 eq(len(payload), 1)
1746 subpart = payload[0]
1747 unless(subpart is m)
1748 eq(subpart['subject'], subject)
1749
1750 def test_bad_multipart(self):
1751 eq = self.assertEqual
1752 msg1 = Message()
1753 msg1['Subject'] = 'subpart 1'
1754 msg2 = Message()
1755 msg2['Subject'] = 'subpart 2'
1756 r = MIMEMessage(msg1)
1757 self.assertRaises(errors.MultipartConversionError, r.attach, msg2)
1758
1759 def test_generate(self):
1760 # First craft the message to be encapsulated
1761 m = Message()
1762 m['Subject'] = 'An enclosed message'
1763 m.set_payload('Here is the body of the message.\n')
1764 r = MIMEMessage(m)
1765 r['Subject'] = 'The enclosing message'
1766 s = StringIO()
1767 g = Generator(s)
1768 g.flatten(r)
1769 self.assertEqual(s.getvalue(), """\
1770Content-Type: message/rfc822
1771MIME-Version: 1.0
1772Subject: The enclosing message
1773
1774Subject: An enclosed message
1775
1776Here is the body of the message.
1777""")
1778
1779 def test_parse_message_rfc822(self):
1780 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001781 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001782 msg = self._msgobj('msg_11.txt')
1783 eq(msg.get_content_type(), 'message/rfc822')
1784 payload = msg.get_payload()
1785 unless(isinstance(payload, list))
1786 eq(len(payload), 1)
1787 submsg = payload[0]
Georg Brandlab91fde2009-08-13 08:51:18 +00001788 self.assertTrue(isinstance(submsg, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001789 eq(submsg['subject'], 'An enclosed message')
1790 eq(submsg.get_payload(), 'Here is the body of the message.\n')
1791
1792 def test_dsn(self):
1793 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001794 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001795 # msg 16 is a Delivery Status Notification, see RFC 1894
1796 msg = self._msgobj('msg_16.txt')
1797 eq(msg.get_content_type(), 'multipart/report')
1798 unless(msg.is_multipart())
1799 eq(len(msg.get_payload()), 3)
1800 # Subpart 1 is a text/plain, human readable section
1801 subpart = msg.get_payload(0)
1802 eq(subpart.get_content_type(), 'text/plain')
1803 eq(subpart.get_payload(), """\
1804This report relates to a message you sent with the following header fields:
1805
1806 Message-id: <002001c144a6$8752e060$56104586@oxy.edu>
1807 Date: Sun, 23 Sep 2001 20:10:55 -0700
1808 From: "Ian T. Henry" <henryi@oxy.edu>
1809 To: SoCal Raves <scr@socal-raves.org>
1810 Subject: [scr] yeah for Ians!!
1811
1812Your message cannot be delivered to the following recipients:
1813
1814 Recipient address: jangel1@cougar.noc.ucla.edu
1815 Reason: recipient reached disk quota
1816
1817""")
1818 # Subpart 2 contains the machine parsable DSN information. It
1819 # consists of two blocks of headers, represented by two nested Message
1820 # objects.
1821 subpart = msg.get_payload(1)
1822 eq(subpart.get_content_type(), 'message/delivery-status')
1823 eq(len(subpart.get_payload()), 2)
1824 # message/delivery-status should treat each block as a bunch of
1825 # headers, i.e. a bunch of Message objects.
1826 dsn1 = subpart.get_payload(0)
1827 unless(isinstance(dsn1, Message))
1828 eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu')
1829 eq(dsn1.get_param('dns', header='reporting-mta'), '')
1830 # Try a missing one <wink>
1831 eq(dsn1.get_param('nsd', header='reporting-mta'), None)
1832 dsn2 = subpart.get_payload(1)
1833 unless(isinstance(dsn2, Message))
1834 eq(dsn2['action'], 'failed')
1835 eq(dsn2.get_params(header='original-recipient'),
1836 [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')])
1837 eq(dsn2.get_param('rfc822', header='final-recipient'), '')
1838 # Subpart 3 is the original message
1839 subpart = msg.get_payload(2)
1840 eq(subpart.get_content_type(), 'message/rfc822')
1841 payload = subpart.get_payload()
1842 unless(isinstance(payload, list))
1843 eq(len(payload), 1)
1844 subsubpart = payload[0]
1845 unless(isinstance(subsubpart, Message))
1846 eq(subsubpart.get_content_type(), 'text/plain')
1847 eq(subsubpart['message-id'],
1848 '<002001c144a6$8752e060$56104586@oxy.edu>')
1849
1850 def test_epilogue(self):
1851 eq = self.ndiffAssertEqual
1852 with openfile('msg_21.txt') as fp:
1853 text = fp.read()
1854 msg = Message()
1855 msg['From'] = 'aperson@dom.ain'
1856 msg['To'] = 'bperson@dom.ain'
1857 msg['Subject'] = 'Test'
1858 msg.preamble = 'MIME message'
1859 msg.epilogue = 'End of MIME message\n'
1860 msg1 = MIMEText('One')
1861 msg2 = MIMEText('Two')
1862 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1863 msg.attach(msg1)
1864 msg.attach(msg2)
1865 sfp = StringIO()
1866 g = Generator(sfp)
1867 g.flatten(msg)
1868 eq(sfp.getvalue(), text)
1869
1870 def test_no_nl_preamble(self):
1871 eq = self.ndiffAssertEqual
1872 msg = Message()
1873 msg['From'] = 'aperson@dom.ain'
1874 msg['To'] = 'bperson@dom.ain'
1875 msg['Subject'] = 'Test'
1876 msg.preamble = 'MIME message'
1877 msg.epilogue = ''
1878 msg1 = MIMEText('One')
1879 msg2 = MIMEText('Two')
1880 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1881 msg.attach(msg1)
1882 msg.attach(msg2)
1883 eq(msg.as_string(), """\
1884From: aperson@dom.ain
1885To: bperson@dom.ain
1886Subject: Test
1887Content-Type: multipart/mixed; boundary="BOUNDARY"
1888
1889MIME message
1890--BOUNDARY
1891Content-Type: text/plain; charset="us-ascii"
1892MIME-Version: 1.0
1893Content-Transfer-Encoding: 7bit
1894
1895One
1896--BOUNDARY
1897Content-Type: text/plain; charset="us-ascii"
1898MIME-Version: 1.0
1899Content-Transfer-Encoding: 7bit
1900
1901Two
1902--BOUNDARY--
1903""")
1904
1905 def test_default_type(self):
1906 eq = self.assertEqual
1907 with openfile('msg_30.txt') as fp:
1908 msg = email.message_from_file(fp)
1909 container1 = msg.get_payload(0)
1910 eq(container1.get_default_type(), 'message/rfc822')
1911 eq(container1.get_content_type(), 'message/rfc822')
1912 container2 = msg.get_payload(1)
1913 eq(container2.get_default_type(), 'message/rfc822')
1914 eq(container2.get_content_type(), 'message/rfc822')
1915 container1a = container1.get_payload(0)
1916 eq(container1a.get_default_type(), 'text/plain')
1917 eq(container1a.get_content_type(), 'text/plain')
1918 container2a = container2.get_payload(0)
1919 eq(container2a.get_default_type(), 'text/plain')
1920 eq(container2a.get_content_type(), 'text/plain')
1921
1922 def test_default_type_with_explicit_container_type(self):
1923 eq = self.assertEqual
1924 with openfile('msg_28.txt') as fp:
1925 msg = email.message_from_file(fp)
1926 container1 = msg.get_payload(0)
1927 eq(container1.get_default_type(), 'message/rfc822')
1928 eq(container1.get_content_type(), 'message/rfc822')
1929 container2 = msg.get_payload(1)
1930 eq(container2.get_default_type(), 'message/rfc822')
1931 eq(container2.get_content_type(), 'message/rfc822')
1932 container1a = container1.get_payload(0)
1933 eq(container1a.get_default_type(), 'text/plain')
1934 eq(container1a.get_content_type(), 'text/plain')
1935 container2a = container2.get_payload(0)
1936 eq(container2a.get_default_type(), 'text/plain')
1937 eq(container2a.get_content_type(), 'text/plain')
1938
1939 def test_default_type_non_parsed(self):
1940 eq = self.assertEqual
1941 neq = self.ndiffAssertEqual
1942 # Set up container
1943 container = MIMEMultipart('digest', 'BOUNDARY')
1944 container.epilogue = ''
1945 # Set up subparts
1946 subpart1a = MIMEText('message 1\n')
1947 subpart2a = MIMEText('message 2\n')
1948 subpart1 = MIMEMessage(subpart1a)
1949 subpart2 = MIMEMessage(subpart2a)
1950 container.attach(subpart1)
1951 container.attach(subpart2)
1952 eq(subpart1.get_content_type(), 'message/rfc822')
1953 eq(subpart1.get_default_type(), 'message/rfc822')
1954 eq(subpart2.get_content_type(), 'message/rfc822')
1955 eq(subpart2.get_default_type(), 'message/rfc822')
1956 neq(container.as_string(0), '''\
1957Content-Type: multipart/digest; boundary="BOUNDARY"
1958MIME-Version: 1.0
1959
1960--BOUNDARY
1961Content-Type: message/rfc822
1962MIME-Version: 1.0
1963
1964Content-Type: text/plain; charset="us-ascii"
1965MIME-Version: 1.0
1966Content-Transfer-Encoding: 7bit
1967
1968message 1
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 2
1979
1980--BOUNDARY--
1981''')
1982 del subpart1['content-type']
1983 del subpart1['mime-version']
1984 del subpart2['content-type']
1985 del subpart2['mime-version']
1986 eq(subpart1.get_content_type(), 'message/rfc822')
1987 eq(subpart1.get_default_type(), 'message/rfc822')
1988 eq(subpart2.get_content_type(), 'message/rfc822')
1989 eq(subpart2.get_default_type(), 'message/rfc822')
1990 neq(container.as_string(0), '''\
1991Content-Type: multipart/digest; boundary="BOUNDARY"
1992MIME-Version: 1.0
1993
1994--BOUNDARY
1995
1996Content-Type: text/plain; charset="us-ascii"
1997MIME-Version: 1.0
1998Content-Transfer-Encoding: 7bit
1999
2000message 1
2001
2002--BOUNDARY
2003
2004Content-Type: text/plain; charset="us-ascii"
2005MIME-Version: 1.0
2006Content-Transfer-Encoding: 7bit
2007
2008message 2
2009
2010--BOUNDARY--
2011''')
2012
2013 def test_mime_attachments_in_constructor(self):
2014 eq = self.assertEqual
2015 text1 = MIMEText('')
2016 text2 = MIMEText('')
2017 msg = MIMEMultipart(_subparts=(text1, text2))
2018 eq(len(msg.get_payload()), 2)
2019 eq(msg.get_payload(0), text1)
2020 eq(msg.get_payload(1), text2)
2021
Christian Heimes587c2bf2008-01-19 16:21:02 +00002022 def test_default_multipart_constructor(self):
2023 msg = MIMEMultipart()
2024 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002025
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002026
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002027# A general test of parser->model->generator idempotency. IOW, read a message
2028# in, parse it into a message object tree, then without touching the tree,
2029# regenerate the plain text. The original text and the transformed text
2030# should be identical. Note: that we ignore the Unix-From since that may
2031# contain a changed date.
2032class TestIdempotent(TestEmailBase):
2033 def _msgobj(self, filename):
2034 with openfile(filename) as fp:
2035 data = fp.read()
2036 msg = email.message_from_string(data)
2037 return msg, data
2038
2039 def _idempotent(self, msg, text):
2040 eq = self.ndiffAssertEqual
2041 s = StringIO()
2042 g = Generator(s, maxheaderlen=0)
2043 g.flatten(msg)
2044 eq(text, s.getvalue())
2045
2046 def test_parse_text_message(self):
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002047 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002048 msg, text = self._msgobj('msg_01.txt')
2049 eq(msg.get_content_type(), 'text/plain')
2050 eq(msg.get_content_maintype(), 'text')
2051 eq(msg.get_content_subtype(), 'plain')
2052 eq(msg.get_params()[1], ('charset', 'us-ascii'))
2053 eq(msg.get_param('charset'), 'us-ascii')
2054 eq(msg.preamble, None)
2055 eq(msg.epilogue, None)
2056 self._idempotent(msg, text)
2057
2058 def test_parse_untyped_message(self):
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002059 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002060 msg, text = self._msgobj('msg_03.txt')
2061 eq(msg.get_content_type(), 'text/plain')
2062 eq(msg.get_params(), None)
2063 eq(msg.get_param('charset'), None)
2064 self._idempotent(msg, text)
2065
2066 def test_simple_multipart(self):
2067 msg, text = self._msgobj('msg_04.txt')
2068 self._idempotent(msg, text)
2069
2070 def test_MIME_digest(self):
2071 msg, text = self._msgobj('msg_02.txt')
2072 self._idempotent(msg, text)
2073
2074 def test_long_header(self):
2075 msg, text = self._msgobj('msg_27.txt')
2076 self._idempotent(msg, text)
2077
2078 def test_MIME_digest_with_part_headers(self):
2079 msg, text = self._msgobj('msg_28.txt')
2080 self._idempotent(msg, text)
2081
2082 def test_mixed_with_image(self):
2083 msg, text = self._msgobj('msg_06.txt')
2084 self._idempotent(msg, text)
2085
2086 def test_multipart_report(self):
2087 msg, text = self._msgobj('msg_05.txt')
2088 self._idempotent(msg, text)
2089
2090 def test_dsn(self):
2091 msg, text = self._msgobj('msg_16.txt')
2092 self._idempotent(msg, text)
2093
2094 def test_preamble_epilogue(self):
2095 msg, text = self._msgobj('msg_21.txt')
2096 self._idempotent(msg, text)
2097
2098 def test_multipart_one_part(self):
2099 msg, text = self._msgobj('msg_23.txt')
2100 self._idempotent(msg, text)
2101
2102 def test_multipart_no_parts(self):
2103 msg, text = self._msgobj('msg_24.txt')
2104 self._idempotent(msg, text)
2105
2106 def test_no_start_boundary(self):
2107 msg, text = self._msgobj('msg_31.txt')
2108 self._idempotent(msg, text)
2109
2110 def test_rfc2231_charset(self):
2111 msg, text = self._msgobj('msg_32.txt')
2112 self._idempotent(msg, text)
2113
2114 def test_more_rfc2231_parameters(self):
2115 msg, text = self._msgobj('msg_33.txt')
2116 self._idempotent(msg, text)
2117
2118 def test_text_plain_in_a_multipart_digest(self):
2119 msg, text = self._msgobj('msg_34.txt')
2120 self._idempotent(msg, text)
2121
2122 def test_nested_multipart_mixeds(self):
2123 msg, text = self._msgobj('msg_12a.txt')
2124 self._idempotent(msg, text)
2125
2126 def test_message_external_body_idempotent(self):
2127 msg, text = self._msgobj('msg_36.txt')
2128 self._idempotent(msg, text)
2129
2130 def test_content_type(self):
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002131 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00002132 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002133 # Get a message object and reset the seek pointer for other tests
2134 msg, text = self._msgobj('msg_05.txt')
2135 eq(msg.get_content_type(), 'multipart/report')
2136 # Test the Content-Type: parameters
2137 params = {}
2138 for pk, pv in msg.get_params():
2139 params[pk] = pv
2140 eq(params['report-type'], 'delivery-status')
2141 eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
2142 eq(msg.preamble, 'This is a MIME-encapsulated message.\n')
2143 eq(msg.epilogue, '\n')
2144 eq(len(msg.get_payload()), 3)
2145 # Make sure the subparts are what we expect
2146 msg1 = msg.get_payload(0)
2147 eq(msg1.get_content_type(), 'text/plain')
2148 eq(msg1.get_payload(), 'Yadda yadda yadda\n')
2149 msg2 = msg.get_payload(1)
2150 eq(msg2.get_content_type(), 'text/plain')
2151 eq(msg2.get_payload(), 'Yadda yadda yadda\n')
2152 msg3 = msg.get_payload(2)
2153 eq(msg3.get_content_type(), 'message/rfc822')
Georg Brandlab91fde2009-08-13 08:51:18 +00002154 self.assertTrue(isinstance(msg3, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002155 payload = msg3.get_payload()
2156 unless(isinstance(payload, list))
2157 eq(len(payload), 1)
2158 msg4 = payload[0]
2159 unless(isinstance(msg4, Message))
2160 eq(msg4.get_payload(), 'Yadda yadda yadda\n')
2161
2162 def test_parser(self):
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002163 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00002164 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002165 msg, text = self._msgobj('msg_06.txt')
2166 # Check some of the outer headers
2167 eq(msg.get_content_type(), 'message/rfc822')
2168 # Make sure the payload is a list of exactly one sub-Message, and that
2169 # that submessage has a type of text/plain
2170 payload = msg.get_payload()
2171 unless(isinstance(payload, list))
2172 eq(len(payload), 1)
2173 msg1 = payload[0]
Georg Brandlab91fde2009-08-13 08:51:18 +00002174 self.assertTrue(isinstance(msg1, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002175 eq(msg1.get_content_type(), 'text/plain')
Georg Brandlab91fde2009-08-13 08:51:18 +00002176 self.assertTrue(isinstance(msg1.get_payload(), str))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002177 eq(msg1.get_payload(), '\n')
2178
2179
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002180
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002181# Test various other bits of the package's functionality
2182class TestMiscellaneous(TestEmailBase):
2183 def test_message_from_string(self):
2184 with openfile('msg_01.txt') as fp:
2185 text = fp.read()
2186 msg = email.message_from_string(text)
2187 s = StringIO()
2188 # Don't wrap/continue long headers since we're trying to test
2189 # idempotency.
2190 g = Generator(s, maxheaderlen=0)
2191 g.flatten(msg)
2192 self.assertEqual(text, s.getvalue())
2193
2194 def test_message_from_file(self):
2195 with openfile('msg_01.txt') as fp:
2196 text = fp.read()
2197 fp.seek(0)
2198 msg = email.message_from_file(fp)
2199 s = StringIO()
2200 # Don't wrap/continue long headers since we're trying to test
2201 # idempotency.
2202 g = Generator(s, maxheaderlen=0)
2203 g.flatten(msg)
2204 self.assertEqual(text, s.getvalue())
2205
2206 def test_message_from_string_with_class(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00002207 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002208 with openfile('msg_01.txt') as fp:
2209 text = fp.read()
2210
2211 # Create a subclass
2212 class MyMessage(Message):
2213 pass
2214
2215 msg = email.message_from_string(text, MyMessage)
2216 unless(isinstance(msg, MyMessage))
2217 # Try something more complicated
2218 with openfile('msg_02.txt') as fp:
2219 text = fp.read()
2220 msg = email.message_from_string(text, MyMessage)
2221 for subpart in msg.walk():
2222 unless(isinstance(subpart, MyMessage))
2223
2224 def test_message_from_file_with_class(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00002225 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002226 # Create a subclass
2227 class MyMessage(Message):
2228 pass
2229
2230 with openfile('msg_01.txt') as fp:
2231 msg = email.message_from_file(fp, MyMessage)
2232 unless(isinstance(msg, MyMessage))
2233 # Try something more complicated
2234 with openfile('msg_02.txt') as fp:
2235 msg = email.message_from_file(fp, MyMessage)
2236 for subpart in msg.walk():
2237 unless(isinstance(subpart, MyMessage))
2238
2239 def test__all__(self):
2240 module = __import__('email')
2241 # Can't use sorted() here due to Python 2.3 compatibility
2242 all = module.__all__[:]
2243 all.sort()
2244 self.assertEqual(all, [
2245 'base64mime', 'charset', 'encoders', 'errors', 'generator',
2246 'header', 'iterators', 'message', 'message_from_file',
2247 'message_from_string', 'mime', 'parser',
2248 'quoprimime', 'utils',
2249 ])
2250
2251 def test_formatdate(self):
2252 now = time.time()
2253 self.assertEqual(utils.parsedate(utils.formatdate(now))[:6],
2254 time.gmtime(now)[:6])
2255
2256 def test_formatdate_localtime(self):
2257 now = time.time()
2258 self.assertEqual(
2259 utils.parsedate(utils.formatdate(now, localtime=True))[:6],
2260 time.localtime(now)[:6])
2261
2262 def test_formatdate_usegmt(self):
2263 now = time.time()
2264 self.assertEqual(
2265 utils.formatdate(now, localtime=False),
2266 time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now)))
2267 self.assertEqual(
2268 utils.formatdate(now, localtime=False, usegmt=True),
2269 time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now)))
2270
2271 def test_parsedate_none(self):
2272 self.assertEqual(utils.parsedate(''), None)
2273
2274 def test_parsedate_compact(self):
2275 # The FWS after the comma is optional
2276 self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'),
2277 utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800'))
2278
2279 def test_parsedate_no_dayofweek(self):
2280 eq = self.assertEqual
2281 eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'),
2282 (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800))
2283
2284 def test_parsedate_compact_no_dayofweek(self):
2285 eq = self.assertEqual
2286 eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'),
2287 (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800))
2288
2289 def test_parsedate_acceptable_to_time_functions(self):
2290 eq = self.assertEqual
2291 timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800')
2292 t = int(time.mktime(timetup))
2293 eq(time.localtime(t)[:6], timetup[:6])
2294 eq(int(time.strftime('%Y', timetup)), 2003)
2295 timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800')
2296 t = int(time.mktime(timetup[:9]))
2297 eq(time.localtime(t)[:6], timetup[:6])
2298 eq(int(time.strftime('%Y', timetup[:9])), 2003)
2299
R. David Murray1061f182010-08-25 01:55:24 +00002300 def test_parsedate_y2k(self):
2301 """Test for parsing a date with a two-digit year.
2302
2303 Parsing a date with a two-digit year should return the correct
2304 four-digit year. RFC822 allows two-digit years, but RFC2822 (which
2305 obsoletes RFC822) requires four-digit years.
2306
2307 """
2308 self.assertEqual(utils.parsedate_tz('25 Feb 03 13:47:26 -0800'),
2309 utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'))
2310 self.assertEqual(utils.parsedate_tz('25 Feb 71 13:47:26 -0800'),
2311 utils.parsedate_tz('25 Feb 1971 13:47:26 -0800'))
2312
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002313 def test_parseaddr_empty(self):
2314 self.assertEqual(utils.parseaddr('<>'), ('', ''))
2315 self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '')
2316
2317 def test_noquote_dump(self):
2318 self.assertEqual(
2319 utils.formataddr(('A Silly Person', 'person@dom.ain')),
2320 'A Silly Person <person@dom.ain>')
2321
2322 def test_escape_dump(self):
2323 self.assertEqual(
2324 utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')),
2325 r'"A \(Very\) Silly Person" <person@dom.ain>')
2326 a = r'A \(Special\) Person'
2327 b = 'person@dom.ain'
2328 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2329
2330 def test_escape_backslashes(self):
2331 self.assertEqual(
2332 utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')),
2333 r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>')
2334 a = r'Arthur \Backslash\ Foobar'
2335 b = 'person@dom.ain'
2336 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2337
2338 def test_name_with_dot(self):
2339 x = 'John X. Doe <jxd@example.com>'
2340 y = '"John X. Doe" <jxd@example.com>'
2341 a, b = ('John X. Doe', 'jxd@example.com')
2342 self.assertEqual(utils.parseaddr(x), (a, b))
2343 self.assertEqual(utils.parseaddr(y), (a, b))
2344 # formataddr() quotes the name if there's a dot in it
2345 self.assertEqual(utils.formataddr((a, b)), y)
2346
R. David Murray7f8199a2010-10-02 16:04:44 +00002347 def test_parseaddr_preserves_quoted_pairs_in_addresses(self):
2348 # issue 10005. Note that in the third test the second pair of
2349 # backslashes is not actually a quoted pair because it is not inside a
2350 # comment or quoted string: the address being parsed has a quoted
2351 # string containing a quoted backslash, followed by 'example' and two
2352 # backslashes, followed by another quoted string containing a space and
2353 # the word 'example'. parseaddr copies those two backslashes
2354 # literally. Per rfc5322 this is not technically correct since a \ may
2355 # not appear in an address outside of a quoted string. It is probably
2356 # a sensible Postel interpretation, though.
2357 eq = self.assertEqual
2358 eq(utils.parseaddr('""example" example"@example.com'),
2359 ('', '""example" example"@example.com'))
2360 eq(utils.parseaddr('"\\"example\\" example"@example.com'),
2361 ('', '"\\"example\\" example"@example.com'))
2362 eq(utils.parseaddr('"\\\\"example\\\\" example"@example.com'),
2363 ('', '"\\\\"example\\\\" example"@example.com'))
2364
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002365 def test_multiline_from_comment(self):
2366 x = """\
2367Foo
2368\tBar <foo@example.com>"""
2369 self.assertEqual(utils.parseaddr(x), ('Foo Bar', 'foo@example.com'))
2370
2371 def test_quote_dump(self):
2372 self.assertEqual(
2373 utils.formataddr(('A Silly; Person', 'person@dom.ain')),
2374 r'"A Silly; Person" <person@dom.ain>')
2375
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002376 def test_charset_richcomparisons(self):
2377 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00002378 ne = self.assertNotEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002379 cset1 = Charset()
2380 cset2 = Charset()
2381 eq(cset1, 'us-ascii')
2382 eq(cset1, 'US-ASCII')
2383 eq(cset1, 'Us-AsCiI')
2384 eq('us-ascii', cset1)
2385 eq('US-ASCII', cset1)
2386 eq('Us-AsCiI', cset1)
2387 ne(cset1, 'usascii')
2388 ne(cset1, 'USASCII')
2389 ne(cset1, 'UsAsCiI')
2390 ne('usascii', cset1)
2391 ne('USASCII', cset1)
2392 ne('UsAsCiI', cset1)
2393 eq(cset1, cset2)
2394 eq(cset2, cset1)
2395
2396 def test_getaddresses(self):
2397 eq = self.assertEqual
2398 eq(utils.getaddresses(['aperson@dom.ain (Al Person)',
2399 'Bud Person <bperson@dom.ain>']),
2400 [('Al Person', 'aperson@dom.ain'),
2401 ('Bud Person', 'bperson@dom.ain')])
2402
2403 def test_getaddresses_nasty(self):
2404 eq = self.assertEqual
2405 eq(utils.getaddresses(['foo: ;']), [('', '')])
2406 eq(utils.getaddresses(
2407 ['[]*-- =~$']),
2408 [('', ''), ('', ''), ('', '*--')])
2409 eq(utils.getaddresses(
2410 ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
2411 [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
2412
2413 def test_getaddresses_embedded_comment(self):
2414 """Test proper handling of a nested comment"""
2415 eq = self.assertEqual
2416 addrs = utils.getaddresses(['User ((nested comment)) <foo@bar.com>'])
2417 eq(addrs[0][1], 'foo@bar.com')
2418
2419 def test_utils_quote_unquote(self):
2420 eq = self.assertEqual
2421 msg = Message()
2422 msg.add_header('content-disposition', 'attachment',
2423 filename='foo\\wacky"name')
2424 eq(msg.get_filename(), 'foo\\wacky"name')
2425
2426 def test_get_body_encoding_with_bogus_charset(self):
2427 charset = Charset('not a charset')
2428 self.assertEqual(charset.get_body_encoding(), 'base64')
2429
2430 def test_get_body_encoding_with_uppercase_charset(self):
2431 eq = self.assertEqual
2432 msg = Message()
2433 msg['Content-Type'] = 'text/plain; charset=UTF-8'
2434 eq(msg['content-type'], 'text/plain; charset=UTF-8')
2435 charsets = msg.get_charsets()
2436 eq(len(charsets), 1)
2437 eq(charsets[0], 'utf-8')
2438 charset = Charset(charsets[0])
2439 eq(charset.get_body_encoding(), 'base64')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002440 msg.set_payload(b'hello world', charset=charset)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002441 eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n')
2442 eq(msg.get_payload(decode=True), b'hello world')
2443 eq(msg['content-transfer-encoding'], 'base64')
2444 # Try another one
2445 msg = Message()
2446 msg['Content-Type'] = 'text/plain; charset="US-ASCII"'
2447 charsets = msg.get_charsets()
2448 eq(len(charsets), 1)
2449 eq(charsets[0], 'us-ascii')
2450 charset = Charset(charsets[0])
2451 eq(charset.get_body_encoding(), encoders.encode_7or8bit)
2452 msg.set_payload('hello world', charset=charset)
2453 eq(msg.get_payload(), 'hello world')
2454 eq(msg['content-transfer-encoding'], '7bit')
2455
2456 def test_charsets_case_insensitive(self):
2457 lc = Charset('us-ascii')
2458 uc = Charset('US-ASCII')
2459 self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding())
2460
2461 def test_partial_falls_inside_message_delivery_status(self):
2462 eq = self.ndiffAssertEqual
2463 # The Parser interface provides chunks of data to FeedParser in 8192
2464 # byte gulps. SF bug #1076485 found one of those chunks inside
2465 # message/delivery-status header block, which triggered an
2466 # unreadline() of NeedMoreData.
2467 msg = self._msgobj('msg_43.txt')
2468 sfp = StringIO()
2469 iterators._structure(msg, sfp)
2470 eq(sfp.getvalue(), """\
2471multipart/report
2472 text/plain
2473 message/delivery-status
2474 text/plain
2475 text/plain
2476 text/plain
2477 text/plain
2478 text/plain
2479 text/plain
2480 text/plain
2481 text/plain
2482 text/plain
2483 text/plain
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/rfc822-headers
2501""")
2502
2503
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002504
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002505# Test the iterator/generators
2506class TestIterators(TestEmailBase):
2507 def test_body_line_iterator(self):
2508 eq = self.assertEqual
2509 neq = self.ndiffAssertEqual
2510 # First a simple non-multipart message
2511 msg = self._msgobj('msg_01.txt')
2512 it = iterators.body_line_iterator(msg)
2513 lines = list(it)
2514 eq(len(lines), 6)
2515 neq(EMPTYSTRING.join(lines), msg.get_payload())
2516 # Now a more complicated multipart
2517 msg = self._msgobj('msg_02.txt')
2518 it = iterators.body_line_iterator(msg)
2519 lines = list(it)
2520 eq(len(lines), 43)
2521 with openfile('msg_19.txt') as fp:
2522 neq(EMPTYSTRING.join(lines), fp.read())
2523
2524 def test_typed_subpart_iterator(self):
2525 eq = self.assertEqual
2526 msg = self._msgobj('msg_04.txt')
2527 it = iterators.typed_subpart_iterator(msg, 'text')
2528 lines = []
2529 subparts = 0
2530 for subpart in it:
2531 subparts += 1
2532 lines.append(subpart.get_payload())
2533 eq(subparts, 2)
2534 eq(EMPTYSTRING.join(lines), """\
2535a simple kind of mirror
2536to reflect upon our own
2537a simple kind of mirror
2538to reflect upon our own
2539""")
2540
2541 def test_typed_subpart_iterator_default_type(self):
2542 eq = self.assertEqual
2543 msg = self._msgobj('msg_03.txt')
2544 it = iterators.typed_subpart_iterator(msg, 'text', 'plain')
2545 lines = []
2546 subparts = 0
2547 for subpart in it:
2548 subparts += 1
2549 lines.append(subpart.get_payload())
2550 eq(subparts, 1)
2551 eq(EMPTYSTRING.join(lines), """\
2552
2553Hi,
2554
2555Do you like this message?
2556
2557-Me
2558""")
2559
R. David Murray6d4a06c2010-07-17 01:28:04 +00002560 def test_pushCR_LF(self):
2561 '''FeedParser BufferedSubFile.push() assumed it received complete
2562 line endings. A CR ending one push() followed by a LF starting
2563 the next push() added an empty line.
2564 '''
2565 imt = [
2566 ("a\r \n", 2),
2567 ("b", 0),
2568 ("c\n", 1),
2569 ("", 0),
2570 ("d\r\n", 1),
2571 ("e\r", 0),
2572 ("\nf", 1),
2573 ("\r\n", 1),
2574 ]
2575 from email.feedparser import BufferedSubFile, NeedMoreData
2576 bsf = BufferedSubFile()
2577 om = []
2578 nt = 0
2579 for il, n in imt:
2580 bsf.push(il)
2581 nt += n
2582 n1 = 0
2583 while True:
2584 ol = bsf.readline()
2585 if ol == NeedMoreData:
2586 break
2587 om.append(ol)
2588 n1 += 1
2589 self.assertTrue(n == n1)
2590 self.assertTrue(len(om) == nt)
2591 self.assertTrue(''.join([il for il, n in imt]) == ''.join(om))
2592
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002593
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002594
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002595class TestParsers(TestEmailBase):
2596 def test_header_parser(self):
2597 eq = self.assertEqual
2598 # Parse only the headers of a complex multipart MIME document
2599 with openfile('msg_02.txt') as fp:
2600 msg = HeaderParser().parse(fp)
2601 eq(msg['from'], 'ppp-request@zzz.org')
2602 eq(msg['to'], 'ppp@zzz.org')
2603 eq(msg.get_content_type(), 'multipart/mixed')
Georg Brandlab91fde2009-08-13 08:51:18 +00002604 self.assertFalse(msg.is_multipart())
2605 self.assertTrue(isinstance(msg.get_payload(), str))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002606
2607 def test_whitespace_continuation(self):
2608 eq = self.assertEqual
2609 # This message contains a line after the Subject: header that has only
2610 # whitespace, but it is not empty!
2611 msg = email.message_from_string("""\
2612From: aperson@dom.ain
2613To: bperson@dom.ain
2614Subject: the next line has a space on it
2615\x20
2616Date: Mon, 8 Apr 2002 15:09:19 -0400
2617Message-ID: spam
2618
2619Here's the message body
2620""")
2621 eq(msg['subject'], 'the next line has a space on it\n ')
2622 eq(msg['message-id'], 'spam')
2623 eq(msg.get_payload(), "Here's the message body\n")
2624
2625 def test_whitespace_continuation_last_header(self):
2626 eq = self.assertEqual
2627 # Like the previous test, but the subject line is the last
2628 # header.
2629 msg = email.message_from_string("""\
2630From: aperson@dom.ain
2631To: bperson@dom.ain
2632Date: Mon, 8 Apr 2002 15:09:19 -0400
2633Message-ID: spam
2634Subject: the next line has a space on it
2635\x20
2636
2637Here's the message body
2638""")
2639 eq(msg['subject'], 'the next line has a space on it\n ')
2640 eq(msg['message-id'], 'spam')
2641 eq(msg.get_payload(), "Here's the message body\n")
2642
2643 def test_crlf_separation(self):
2644 eq = self.assertEqual
Guido van Rossum98297ee2007-11-06 21:34:58 +00002645 with openfile('msg_26.txt', newline='\n') as fp:
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002646 msg = Parser().parse(fp)
2647 eq(len(msg.get_payload()), 2)
2648 part1 = msg.get_payload(0)
2649 eq(part1.get_content_type(), 'text/plain')
2650 eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n')
2651 part2 = msg.get_payload(1)
2652 eq(part2.get_content_type(), 'application/riscos')
2653
2654 def test_multipart_digest_with_extra_mime_headers(self):
2655 eq = self.assertEqual
2656 neq = self.ndiffAssertEqual
2657 with openfile('msg_28.txt') as fp:
2658 msg = email.message_from_file(fp)
2659 # Structure is:
2660 # multipart/digest
2661 # message/rfc822
2662 # text/plain
2663 # message/rfc822
2664 # text/plain
2665 eq(msg.is_multipart(), 1)
2666 eq(len(msg.get_payload()), 2)
2667 part1 = msg.get_payload(0)
2668 eq(part1.get_content_type(), 'message/rfc822')
2669 eq(part1.is_multipart(), 1)
2670 eq(len(part1.get_payload()), 1)
2671 part1a = part1.get_payload(0)
2672 eq(part1a.is_multipart(), 0)
2673 eq(part1a.get_content_type(), 'text/plain')
2674 neq(part1a.get_payload(), 'message 1\n')
2675 # next message/rfc822
2676 part2 = msg.get_payload(1)
2677 eq(part2.get_content_type(), 'message/rfc822')
2678 eq(part2.is_multipart(), 1)
2679 eq(len(part2.get_payload()), 1)
2680 part2a = part2.get_payload(0)
2681 eq(part2a.is_multipart(), 0)
2682 eq(part2a.get_content_type(), 'text/plain')
2683 neq(part2a.get_payload(), 'message 2\n')
2684
2685 def test_three_lines(self):
2686 # A bug report by Andrew McNamara
2687 lines = ['From: Andrew Person <aperson@dom.ain',
2688 'Subject: Test',
2689 'Date: Tue, 20 Aug 2002 16:43:45 +1000']
2690 msg = email.message_from_string(NL.join(lines))
2691 self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000')
2692
2693 def test_strip_line_feed_and_carriage_return_in_headers(self):
2694 eq = self.assertEqual
2695 # For [ 1002475 ] email message parser doesn't handle \r\n correctly
2696 value1 = 'text'
2697 value2 = 'more text'
2698 m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % (
2699 value1, value2)
2700 msg = email.message_from_string(m)
2701 eq(msg.get('Header'), value1)
2702 eq(msg.get('Next-Header'), value2)
2703
2704 def test_rfc2822_header_syntax(self):
2705 eq = self.assertEqual
2706 m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2707 msg = email.message_from_string(m)
2708 eq(len(msg), 3)
2709 eq(sorted(field for field in msg), ['!"#QUX;~', '>From', 'From'])
2710 eq(msg.get_payload(), 'body')
2711
2712 def test_rfc2822_space_not_allowed_in_header(self):
2713 eq = self.assertEqual
2714 m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2715 msg = email.message_from_string(m)
2716 eq(len(msg.keys()), 0)
2717
2718 def test_rfc2822_one_character_header(self):
2719 eq = self.assertEqual
2720 m = 'A: first header\nB: second header\nCC: third header\n\nbody'
2721 msg = email.message_from_string(m)
2722 headers = msg.keys()
2723 headers.sort()
2724 eq(headers, ['A', 'B', 'CC'])
2725 eq(msg.get_payload(), 'body')
2726
R. David Murray71df9d92010-06-16 02:22:56 +00002727 def test_CRLFLF_at_end_of_part(self):
2728 # issue 5610: feedparser should not eat two chars from body part ending
2729 # with "\r\n\n".
2730 m = (
2731 "From: foo@bar.com\n"
2732 "To: baz\n"
2733 "Mime-Version: 1.0\n"
2734 "Content-Type: multipart/mixed; boundary=BOUNDARY\n"
2735 "\n"
2736 "--BOUNDARY\n"
2737 "Content-Type: text/plain\n"
2738 "\n"
2739 "body ending with CRLF newline\r\n"
2740 "\n"
2741 "--BOUNDARY--\n"
2742 )
2743 msg = email.message_from_string(m)
2744 self.assertTrue(msg.get_payload(0).get_payload().endswith('\r\n'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002745
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002746
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002747class TestBase64(unittest.TestCase):
2748 def test_len(self):
2749 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002750 eq(base64mime.header_length('hello'),
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002751 len(base64mime.body_encode(b'hello', eol='')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002752 for size in range(15):
2753 if size == 0 : bsize = 0
2754 elif size <= 3 : bsize = 4
2755 elif size <= 6 : bsize = 8
2756 elif size <= 9 : bsize = 12
2757 elif size <= 12: bsize = 16
2758 else : bsize = 20
Guido van Rossum9604e662007-08-30 03:46:43 +00002759 eq(base64mime.header_length('x' * size), bsize)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002760
2761 def test_decode(self):
2762 eq = self.assertEqual
Barry Warsaw2cc1f6d2007-08-30 14:28:55 +00002763 eq(base64mime.decode(''), b'')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002764 eq(base64mime.decode('aGVsbG8='), b'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002765
2766 def test_encode(self):
2767 eq = self.assertEqual
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002768 eq(base64mime.body_encode(b''), b'')
2769 eq(base64mime.body_encode(b'hello'), 'aGVsbG8=\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002770 # Test the binary flag
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002771 eq(base64mime.body_encode(b'hello\n'), 'aGVsbG8K\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002772 # Test the maxlinelen arg
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002773 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002774eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2775eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2776eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2777eHh4eCB4eHh4IA==
2778""")
2779 # Test the eol argument
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002780 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40, eol='\r\n'),
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002781 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002782eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2783eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2784eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2785eHh4eCB4eHh4IA==\r
2786""")
2787
2788 def test_header_encode(self):
2789 eq = self.assertEqual
2790 he = base64mime.header_encode
2791 eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=')
Guido van Rossum9604e662007-08-30 03:46:43 +00002792 eq(he('hello\r\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=')
2793 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002794 # Test the charset option
2795 eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=')
2796 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002797
2798
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002799
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002800class TestQuopri(unittest.TestCase):
2801 def setUp(self):
2802 # Set of characters (as byte integers) that don't need to be encoded
2803 # in headers.
2804 self.hlit = list(chain(
2805 range(ord('a'), ord('z') + 1),
2806 range(ord('A'), ord('Z') + 1),
2807 range(ord('0'), ord('9') + 1),
Guido van Rossum9604e662007-08-30 03:46:43 +00002808 (c for c in b'!*+-/')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002809 # Set of characters (as byte integers) that do need to be encoded in
2810 # headers.
2811 self.hnon = [c for c in range(256) if c not in self.hlit]
2812 assert len(self.hlit) + len(self.hnon) == 256
2813 # Set of characters (as byte integers) that don't need to be encoded
2814 # in bodies.
2815 self.blit = list(range(ord(' '), ord('~') + 1))
2816 self.blit.append(ord('\t'))
2817 self.blit.remove(ord('='))
2818 # Set of characters (as byte integers) that do need to be encoded in
2819 # bodies.
2820 self.bnon = [c for c in range(256) if c not in self.blit]
2821 assert len(self.blit) + len(self.bnon) == 256
2822
Guido van Rossum9604e662007-08-30 03:46:43 +00002823 def test_quopri_header_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002824 for c in self.hlit:
Georg Brandlab91fde2009-08-13 08:51:18 +00002825 self.assertFalse(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002826 'Should not be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002827 for c in self.hnon:
Georg Brandlab91fde2009-08-13 08:51:18 +00002828 self.assertTrue(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002829 'Should be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002830
Guido van Rossum9604e662007-08-30 03:46:43 +00002831 def test_quopri_body_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002832 for c in self.blit:
Georg Brandlab91fde2009-08-13 08:51:18 +00002833 self.assertFalse(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002834 'Should not be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002835 for c in self.bnon:
Georg Brandlab91fde2009-08-13 08:51:18 +00002836 self.assertTrue(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002837 'Should be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002838
2839 def test_header_quopri_len(self):
2840 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002841 eq(quoprimime.header_length(b'hello'), 5)
2842 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002843 eq(len(quoprimime.header_encode(b'hello', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00002844 quoprimime.header_length(b'hello') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002845 # =?xxx?q?...?= means 10 extra characters
2846 10)
Guido van Rossum9604e662007-08-30 03:46:43 +00002847 eq(quoprimime.header_length(b'h@e@l@l@o@'), 20)
2848 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002849 eq(len(quoprimime.header_encode(b'h@e@l@l@o@', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00002850 quoprimime.header_length(b'h@e@l@l@o@') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002851 # =?xxx?q?...?= means 10 extra characters
2852 10)
2853 for c in self.hlit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002854 eq(quoprimime.header_length(bytes([c])), 1,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002855 'expected length 1 for %r' % chr(c))
2856 for c in self.hnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002857 # Space is special; it's encoded to _
2858 if c == ord(' '):
2859 continue
2860 eq(quoprimime.header_length(bytes([c])), 3,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002861 'expected length 3 for %r' % chr(c))
Guido van Rossum9604e662007-08-30 03:46:43 +00002862 eq(quoprimime.header_length(b' '), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002863
2864 def test_body_quopri_len(self):
2865 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002866 for c in self.blit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002867 eq(quoprimime.body_length(bytes([c])), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002868 for c in self.bnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002869 eq(quoprimime.body_length(bytes([c])), 3)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002870
2871 def test_quote_unquote_idempotent(self):
2872 for x in range(256):
2873 c = chr(x)
2874 self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c)
2875
2876 def test_header_encode(self):
2877 eq = self.assertEqual
2878 he = quoprimime.header_encode
2879 eq(he(b'hello'), '=?iso-8859-1?q?hello?=')
2880 eq(he(b'hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=')
2881 eq(he(b'hello\nworld'), '=?iso-8859-1?q?hello=0Aworld?=')
2882 # Test a non-ASCII character
2883 eq(he(b'hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=')
2884
2885 def test_decode(self):
2886 eq = self.assertEqual
2887 eq(quoprimime.decode(''), '')
2888 eq(quoprimime.decode('hello'), 'hello')
2889 eq(quoprimime.decode('hello', 'X'), 'hello')
2890 eq(quoprimime.decode('hello\nworld', 'X'), 'helloXworld')
2891
2892 def test_encode(self):
2893 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002894 eq(quoprimime.body_encode(''), '')
2895 eq(quoprimime.body_encode('hello'), 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002896 # Test the binary flag
Guido van Rossum9604e662007-08-30 03:46:43 +00002897 eq(quoprimime.body_encode('hello\r\nworld'), 'hello\nworld')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002898 # Test the maxlinelen arg
Guido van Rossum9604e662007-08-30 03:46:43 +00002899 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002900xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
2901 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
2902x xxxx xxxx xxxx xxxx=20""")
2903 # Test the eol argument
Guido van Rossum9604e662007-08-30 03:46:43 +00002904 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'),
2905 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002906xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
2907 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
2908x xxxx xxxx xxxx xxxx=20""")
Guido van Rossum9604e662007-08-30 03:46:43 +00002909 eq(quoprimime.body_encode("""\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002910one line
2911
2912two line"""), """\
2913one line
2914
2915two line""")
2916
2917
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002918
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002919# Test the Charset class
2920class TestCharset(unittest.TestCase):
2921 def tearDown(self):
2922 from email import charset as CharsetModule
2923 try:
2924 del CharsetModule.CHARSETS['fake']
2925 except KeyError:
2926 pass
2927
Guido van Rossum9604e662007-08-30 03:46:43 +00002928 def test_codec_encodeable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002929 eq = self.assertEqual
2930 # Make sure us-ascii = no Unicode conversion
2931 c = Charset('us-ascii')
Guido van Rossum9604e662007-08-30 03:46:43 +00002932 eq(c.header_encode('Hello World!'), 'Hello World!')
2933 # Test 8-bit idempotency with us-ascii
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002934 s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
Guido van Rossum9604e662007-08-30 03:46:43 +00002935 self.assertRaises(UnicodeError, c.header_encode, s)
2936 c = Charset('utf-8')
2937 eq(c.header_encode(s), '=?utf-8?b?wqTCosKkwqTCpMKmwqTCqMKkwqo=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002938
2939 def test_body_encode(self):
2940 eq = self.assertEqual
2941 # Try a charset with QP body encoding
2942 c = Charset('iso-8859-1')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002943 eq('hello w=F6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002944 # Try a charset with Base64 body encoding
2945 c = Charset('utf-8')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002946 eq('aGVsbG8gd29ybGQ=\n', c.body_encode(b'hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002947 # Try a charset with None body encoding
2948 c = Charset('us-ascii')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002949 eq('hello world', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002950 # Try the convert argument, where input codec != output codec
2951 c = Charset('euc-jp')
2952 # With apologies to Tokio Kikuchi ;)
Barry Warsawbef9d212007-08-31 10:55:37 +00002953 # XXX FIXME
2954## try:
2955## eq('\x1b$B5FCO;~IW\x1b(B',
2956## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
2957## eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
2958## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
2959## except LookupError:
2960## # We probably don't have the Japanese codecs installed
2961## pass
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002962 # Testing SF bug #625509, which we have to fake, since there are no
2963 # built-in encodings where the header encoding is QP but the body
2964 # encoding is not.
2965 from email import charset as CharsetModule
2966 CharsetModule.add_charset('fake', CharsetModule.QP, None)
2967 c = Charset('fake')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002968 eq('hello w\xf6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002969
2970 def test_unicode_charset_name(self):
2971 charset = Charset('us-ascii')
2972 self.assertEqual(str(charset), 'us-ascii')
2973 self.assertRaises(errors.CharsetError, Charset, 'asc\xffii')
2974
2975
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002976
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002977# Test multilingual MIME headers.
2978class TestHeader(TestEmailBase):
2979 def test_simple(self):
2980 eq = self.ndiffAssertEqual
2981 h = Header('Hello World!')
2982 eq(h.encode(), 'Hello World!')
2983 h.append(' Goodbye World!')
2984 eq(h.encode(), 'Hello World! Goodbye World!')
2985
2986 def test_simple_surprise(self):
2987 eq = self.ndiffAssertEqual
2988 h = Header('Hello World!')
2989 eq(h.encode(), 'Hello World!')
2990 h.append('Goodbye World!')
2991 eq(h.encode(), 'Hello World! Goodbye World!')
2992
2993 def test_header_needs_no_decoding(self):
2994 h = 'no decoding needed'
2995 self.assertEqual(decode_header(h), [(h, None)])
2996
2997 def test_long(self):
2998 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.",
2999 maxlinelen=76)
3000 for l in h.encode(splitchars=' ').split('\n '):
Georg Brandlab91fde2009-08-13 08:51:18 +00003001 self.assertTrue(len(l) <= 76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003002
3003 def test_multilingual(self):
3004 eq = self.ndiffAssertEqual
3005 g = Charset("iso-8859-1")
3006 cz = Charset("iso-8859-2")
3007 utf8 = Charset("utf-8")
3008 g_head = (b'Die Mieter treten hier ein werden mit einem '
3009 b'Foerderband komfortabel den Korridor entlang, '
3010 b'an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, '
3011 b'gegen die rotierenden Klingen bef\xf6rdert. ')
3012 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
3013 b'd\xf9vtipu.. ')
3014 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
3015 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
3016 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
3017 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
3018 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
3019 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
3020 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
3021 '\u3044\u307e\u3059\u3002')
3022 h = Header(g_head, g)
3023 h.append(cz_head, cz)
3024 h.append(utf8_head, utf8)
Guido van Rossum9604e662007-08-30 03:46:43 +00003025 enc = h.encode(maxlinelen=76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003026 eq(enc, """\
Guido van Rossum9604e662007-08-30 03:46:43 +00003027=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_kom?=
3028 =?iso-8859-1?q?fortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wand?=
3029 =?iso-8859-1?q?gem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6r?=
3030 =?iso-8859-1?q?dert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003031 =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
3032 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
3033 =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
3034 =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
Guido van Rossum9604e662007-08-30 03:46:43 +00003035 =?utf-8?b?IE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5k?=
3036 =?utf-8?b?IGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIA=?=
3037 =?utf-8?b?44Gj44Gm44GE44G+44GZ44CC?=""")
3038 decoded = decode_header(enc)
3039 eq(len(decoded), 3)
3040 eq(decoded[0], (g_head, 'iso-8859-1'))
3041 eq(decoded[1], (cz_head, 'iso-8859-2'))
3042 eq(decoded[2], (utf8_head.encode('utf-8'), 'utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003043 ustr = str(h)
Guido van Rossum9604e662007-08-30 03:46:43 +00003044 eq(ustr,
3045 (b'Die Mieter treten hier ein werden mit einem Foerderband '
3046 b'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
3047 b'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
3048 b'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
3049 b'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
3050 b'\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
3051 b'\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
3052 b'\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
3053 b'\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
3054 b'\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
3055 b'\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
3056 b'\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
3057 b'\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
3058 b'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
3059 b'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
3060 b'\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82'
3061 ).decode('utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003062 # Test make_header()
3063 newh = make_header(decode_header(enc))
Guido van Rossum9604e662007-08-30 03:46:43 +00003064 eq(newh, h)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003065
3066 def test_empty_header_encode(self):
3067 h = Header()
3068 self.assertEqual(h.encode(), '')
Barry Warsaw8b3d6592007-08-30 02:10:49 +00003069
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003070 def test_header_ctor_default_args(self):
3071 eq = self.ndiffAssertEqual
3072 h = Header()
3073 eq(h, '')
3074 h.append('foo', Charset('iso-8859-1'))
Guido van Rossum9604e662007-08-30 03:46:43 +00003075 eq(h, 'foo')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003076
3077 def test_explicit_maxlinelen(self):
3078 eq = self.ndiffAssertEqual
3079 hstr = ('A very long line that must get split to something other '
3080 'than at the 76th character boundary to test the non-default '
3081 'behavior')
3082 h = Header(hstr)
3083 eq(h.encode(), '''\
3084A very long line that must get split to something other than at the 76th
3085 character boundary to test the non-default behavior''')
3086 eq(str(h), hstr)
3087 h = Header(hstr, header_name='Subject')
3088 eq(h.encode(), '''\
3089A very long line that must get split to something other than at the
3090 76th character boundary to test the non-default behavior''')
3091 eq(str(h), hstr)
3092 h = Header(hstr, maxlinelen=1024, header_name='Subject')
3093 eq(h.encode(), hstr)
3094 eq(str(h), hstr)
3095
Guido van Rossum9604e662007-08-30 03:46:43 +00003096 def test_quopri_splittable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003097 eq = self.ndiffAssertEqual
3098 h = Header(charset='iso-8859-1', maxlinelen=20)
Guido van Rossum9604e662007-08-30 03:46:43 +00003099 x = 'xxxx ' * 20
3100 h.append(x)
3101 s = h.encode()
3102 eq(s, """\
3103=?iso-8859-1?q?xxx?=
3104 =?iso-8859-1?q?x_?=
3105 =?iso-8859-1?q?xx?=
3106 =?iso-8859-1?q?xx?=
3107 =?iso-8859-1?q?_x?=
3108 =?iso-8859-1?q?xx?=
3109 =?iso-8859-1?q?x_?=
3110 =?iso-8859-1?q?xx?=
3111 =?iso-8859-1?q?xx?=
3112 =?iso-8859-1?q?_x?=
3113 =?iso-8859-1?q?xx?=
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?_?=""")
3153 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003154 h = Header(charset='iso-8859-1', maxlinelen=40)
3155 h.append('xxxx ' * 20)
Guido van Rossum9604e662007-08-30 03:46:43 +00003156 s = h.encode()
3157 eq(s, """\
3158=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xxx?=
3159 =?iso-8859-1?q?x_xxxx_xxxx_xxxx_xxxx_?=
3160 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
3161 =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
3162 =?iso-8859-1?q?_xxxx_xxxx_?=""")
3163 eq(x, str(make_header(decode_header(s))))
3164
3165 def test_base64_splittable(self):
3166 eq = self.ndiffAssertEqual
3167 h = Header(charset='koi8-r', maxlinelen=20)
3168 x = 'xxxx ' * 20
3169 h.append(x)
3170 s = h.encode()
3171 eq(s, """\
3172=?koi8-r?b?eHh4?=
3173 =?koi8-r?b?eCB4?=
3174 =?koi8-r?b?eHh4?=
3175 =?koi8-r?b?IHh4?=
3176 =?koi8-r?b?eHgg?=
3177 =?koi8-r?b?eHh4?=
3178 =?koi8-r?b?eCB4?=
3179 =?koi8-r?b?eHh4?=
3180 =?koi8-r?b?IHh4?=
3181 =?koi8-r?b?eHgg?=
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?IA==?=""")
3206 eq(x, str(make_header(decode_header(s))))
3207 h = Header(charset='koi8-r', maxlinelen=40)
3208 h.append(x)
3209 s = h.encode()
3210 eq(s, """\
3211=?koi8-r?b?eHh4eCB4eHh4IHh4eHggeHh4?=
3212 =?koi8-r?b?eCB4eHh4IHh4eHggeHh4eCB4?=
3213 =?koi8-r?b?eHh4IHh4eHggeHh4eCB4eHh4?=
3214 =?koi8-r?b?IHh4eHggeHh4eCB4eHh4IHh4?=
3215 =?koi8-r?b?eHggeHh4eCB4eHh4IHh4eHgg?=
3216 =?koi8-r?b?eHh4eCB4eHh4IA==?=""")
3217 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003218
3219 def test_us_ascii_header(self):
3220 eq = self.assertEqual
3221 s = 'hello'
3222 x = decode_header(s)
3223 eq(x, [('hello', None)])
3224 h = make_header(x)
3225 eq(s, h.encode())
3226
3227 def test_string_charset(self):
3228 eq = self.assertEqual
3229 h = Header()
3230 h.append('hello', 'iso-8859-1')
Guido van Rossum9604e662007-08-30 03:46:43 +00003231 eq(h, 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003232
3233## def test_unicode_error(self):
3234## raises = self.assertRaises
3235## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')
3236## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')
3237## h = Header()
3238## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')
3239## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')
3240## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')
3241
3242 def test_utf8_shortest(self):
3243 eq = self.assertEqual
3244 h = Header('p\xf6stal', 'utf-8')
3245 eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
3246 h = Header('\u83ca\u5730\u6642\u592b', 'utf-8')
3247 eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
3248
3249 def test_bad_8bit_header(self):
3250 raises = self.assertRaises
3251 eq = self.assertEqual
3252 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
3253 raises(UnicodeError, Header, x)
3254 h = Header()
3255 raises(UnicodeError, h.append, x)
3256 e = x.decode('utf-8', 'replace')
3257 eq(str(Header(x, errors='replace')), e)
3258 h.append(x, errors='replace')
3259 eq(str(h), e)
3260
3261 def test_encoded_adjacent_nonencoded(self):
3262 eq = self.assertEqual
3263 h = Header()
3264 h.append('hello', 'iso-8859-1')
3265 h.append('world')
3266 s = h.encode()
3267 eq(s, '=?iso-8859-1?q?hello?= world')
3268 h = make_header(decode_header(s))
3269 eq(h.encode(), s)
3270
3271 def test_whitespace_eater(self):
3272 eq = self.assertEqual
3273 s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
3274 parts = decode_header(s)
3275 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)])
3276 hdr = make_header(parts)
3277 eq(hdr.encode(),
3278 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
3279
3280 def test_broken_base64_header(self):
3281 raises = self.assertRaises
R. David Murraye06528c2010-08-03 23:35:44 +00003282 s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3I ?='
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003283 raises(errors.HeaderParseError, decode_header, s)
3284
R. David Murrayf9844c82011-01-05 01:47:38 +00003285 def test_shift_jis_charset(self):
3286 h = Header('æ–‡', charset='shift_jis')
3287 self.assertEqual(h.encode(), '=?iso-2022-jp?b?GyRCSjgbKEI=?=')
3288
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003289
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00003290
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003291# Test RFC 2231 header parameters (en/de)coding
3292class TestRFC2231(TestEmailBase):
3293 def test_get_param(self):
3294 eq = self.assertEqual
3295 msg = self._msgobj('msg_29.txt')
3296 eq(msg.get_param('title'),
3297 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3298 eq(msg.get_param('title', unquote=False),
3299 ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
3300
3301 def test_set_param(self):
3302 eq = self.ndiffAssertEqual
3303 msg = Message()
3304 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3305 charset='us-ascii')
3306 eq(msg.get_param('title'),
3307 ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
3308 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3309 charset='us-ascii', language='en')
3310 eq(msg.get_param('title'),
3311 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3312 msg = self._msgobj('msg_01.txt')
3313 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3314 charset='us-ascii', language='en')
3315 eq(msg.as_string(maxheaderlen=78), """\
3316Return-Path: <bbb@zzz.org>
3317Delivered-To: bbb@zzz.org
3318Received: by mail.zzz.org (Postfix, from userid 889)
3319\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3320MIME-Version: 1.0
3321Content-Transfer-Encoding: 7bit
3322Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3323From: bbb@ddd.com (John X. Doe)
3324To: bbb@zzz.org
3325Subject: This is a test message
3326Date: Fri, 4 May 2001 14:05:44 -0400
3327Content-Type: text/plain; charset=us-ascii;
3328 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3329
3330
3331Hi,
3332
3333Do you like this message?
3334
3335-Me
3336""")
3337
3338 def test_del_param(self):
3339 eq = self.ndiffAssertEqual
3340 msg = self._msgobj('msg_01.txt')
3341 msg.set_param('foo', 'bar', charset='us-ascii', language='en')
3342 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3343 charset='us-ascii', language='en')
3344 msg.del_param('foo', header='Content-Type')
3345 eq(msg.as_string(maxheaderlen=78), """\
3346Return-Path: <bbb@zzz.org>
3347Delivered-To: bbb@zzz.org
3348Received: by mail.zzz.org (Postfix, from userid 889)
3349\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3350MIME-Version: 1.0
3351Content-Transfer-Encoding: 7bit
3352Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3353From: bbb@ddd.com (John X. Doe)
3354To: bbb@zzz.org
3355Subject: This is a test message
3356Date: Fri, 4 May 2001 14:05:44 -0400
3357Content-Type: text/plain; charset="us-ascii";
3358 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3359
3360
3361Hi,
3362
3363Do you like this message?
3364
3365-Me
3366""")
3367
3368 def test_rfc2231_get_content_charset(self):
3369 eq = self.assertEqual
3370 msg = self._msgobj('msg_32.txt')
3371 eq(msg.get_content_charset(), 'us-ascii')
3372
3373 def test_rfc2231_no_language_or_charset(self):
3374 m = '''\
3375Content-Transfer-Encoding: 8bit
3376Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
3377Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
3378
3379'''
3380 msg = email.message_from_string(m)
3381 param = msg.get_param('NAME')
Georg Brandlab91fde2009-08-13 08:51:18 +00003382 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003383 self.assertEqual(
3384 param,
3385 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
3386
3387 def test_rfc2231_no_language_or_charset_in_filename(self):
3388 m = '''\
3389Content-Disposition: inline;
3390\tfilename*0*="''This%20is%20even%20more%20";
3391\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3392\tfilename*2="is it not.pdf"
3393
3394'''
3395 msg = email.message_from_string(m)
3396 self.assertEqual(msg.get_filename(),
3397 'This is even more ***fun*** is it not.pdf')
3398
3399 def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
3400 m = '''\
3401Content-Disposition: inline;
3402\tfilename*0*="''This%20is%20even%20more%20";
3403\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3404\tfilename*2="is it not.pdf"
3405
3406'''
3407 msg = email.message_from_string(m)
3408 self.assertEqual(msg.get_filename(),
3409 'This is even more ***fun*** is it not.pdf')
3410
3411 def test_rfc2231_partly_encoded(self):
3412 m = '''\
3413Content-Disposition: inline;
3414\tfilename*0="''This%20is%20even%20more%20";
3415\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3416\tfilename*2="is it not.pdf"
3417
3418'''
3419 msg = email.message_from_string(m)
3420 self.assertEqual(
3421 msg.get_filename(),
3422 'This%20is%20even%20more%20***fun*** is it not.pdf')
3423
3424 def test_rfc2231_partly_nonencoded(self):
3425 m = '''\
3426Content-Disposition: inline;
3427\tfilename*0="This%20is%20even%20more%20";
3428\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
3429\tfilename*2="is it not.pdf"
3430
3431'''
3432 msg = email.message_from_string(m)
3433 self.assertEqual(
3434 msg.get_filename(),
3435 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
3436
3437 def test_rfc2231_no_language_or_charset_in_boundary(self):
3438 m = '''\
3439Content-Type: multipart/alternative;
3440\tboundary*0*="''This%20is%20even%20more%20";
3441\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
3442\tboundary*2="is it not.pdf"
3443
3444'''
3445 msg = email.message_from_string(m)
3446 self.assertEqual(msg.get_boundary(),
3447 'This is even more ***fun*** is it not.pdf')
3448
3449 def test_rfc2231_no_language_or_charset_in_charset(self):
3450 # This is a nonsensical charset value, but tests the code anyway
3451 m = '''\
3452Content-Type: text/plain;
3453\tcharset*0*="This%20is%20even%20more%20";
3454\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
3455\tcharset*2="is it not.pdf"
3456
3457'''
3458 msg = email.message_from_string(m)
3459 self.assertEqual(msg.get_content_charset(),
3460 'this is even more ***fun*** is it not.pdf')
3461
3462 def test_rfc2231_bad_encoding_in_filename(self):
3463 m = '''\
3464Content-Disposition: inline;
3465\tfilename*0*="bogus'xx'This%20is%20even%20more%20";
3466\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3467\tfilename*2="is it not.pdf"
3468
3469'''
3470 msg = email.message_from_string(m)
3471 self.assertEqual(msg.get_filename(),
3472 'This is even more ***fun*** is it not.pdf')
3473
3474 def test_rfc2231_bad_encoding_in_charset(self):
3475 m = """\
3476Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
3477
3478"""
3479 msg = email.message_from_string(m)
3480 # This should return None because non-ascii characters in the charset
3481 # are not allowed.
3482 self.assertEqual(msg.get_content_charset(), None)
3483
3484 def test_rfc2231_bad_character_in_charset(self):
3485 m = """\
3486Content-Type: text/plain; charset*=ascii''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_filename(self):
3495 m = '''\
3496Content-Disposition: inline;
3497\tfilename*0*="ascii'xx'This%20is%20even%20more%20";
3498\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3499\tfilename*2*="is it not.pdf%E2"
3500
3501'''
3502 msg = email.message_from_string(m)
3503 self.assertEqual(msg.get_filename(),
3504 'This is even more ***fun*** is it not.pdf\ufffd')
3505
3506 def test_rfc2231_unknown_encoding(self):
3507 m = """\
3508Content-Transfer-Encoding: 8bit
3509Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
3510
3511"""
3512 msg = email.message_from_string(m)
3513 self.assertEqual(msg.get_filename(), 'myfile.txt')
3514
3515 def test_rfc2231_single_tick_in_filename_extended(self):
3516 eq = self.assertEqual
3517 m = """\
3518Content-Type: application/x-foo;
3519\tname*0*=\"Frank's\"; name*1*=\" Document\"
3520
3521"""
3522 msg = email.message_from_string(m)
3523 charset, language, s = msg.get_param('name')
3524 eq(charset, None)
3525 eq(language, None)
3526 eq(s, "Frank's Document")
3527
3528 def test_rfc2231_single_tick_in_filename(self):
3529 m = """\
3530Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
3531
3532"""
3533 msg = email.message_from_string(m)
3534 param = msg.get_param('name')
Georg Brandlab91fde2009-08-13 08:51:18 +00003535 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003536 self.assertEqual(param, "Frank's Document")
3537
3538 def test_rfc2231_tick_attack_extended(self):
3539 eq = self.assertEqual
3540 m = """\
3541Content-Type: application/x-foo;
3542\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
3543
3544"""
3545 msg = email.message_from_string(m)
3546 charset, language, s = msg.get_param('name')
3547 eq(charset, 'us-ascii')
3548 eq(language, 'en-us')
3549 eq(s, "Frank's Document")
3550
3551 def test_rfc2231_tick_attack(self):
3552 m = """\
3553Content-Type: application/x-foo;
3554\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
3555
3556"""
3557 msg = email.message_from_string(m)
3558 param = msg.get_param('name')
Georg Brandlab91fde2009-08-13 08:51:18 +00003559 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003560 self.assertEqual(param, "us-ascii'en-us'Frank's Document")
3561
3562 def test_rfc2231_no_extended_values(self):
3563 eq = self.assertEqual
3564 m = """\
3565Content-Type: application/x-foo; name=\"Frank's Document\"
3566
3567"""
3568 msg = email.message_from_string(m)
3569 eq(msg.get_param('name'), "Frank's Document")
3570
3571 def test_rfc2231_encoded_then_unencoded_segments(self):
3572 eq = self.assertEqual
3573 m = """\
3574Content-Type: application/x-foo;
3575\tname*0*=\"us-ascii'en-us'My\";
3576\tname*1=\" Document\";
3577\tname*2*=\" For You\"
3578
3579"""
3580 msg = email.message_from_string(m)
3581 charset, language, s = msg.get_param('name')
3582 eq(charset, 'us-ascii')
3583 eq(language, 'en-us')
3584 eq(s, 'My Document For You')
3585
3586 def test_rfc2231_unencoded_then_encoded_segments(self):
3587 eq = self.assertEqual
3588 m = """\
3589Content-Type: application/x-foo;
3590\tname*0=\"us-ascii'en-us'My\";
3591\tname*1*=\" Document\";
3592\tname*2*=\" For You\"
3593
3594"""
3595 msg = email.message_from_string(m)
3596 charset, language, s = msg.get_param('name')
3597 eq(charset, 'us-ascii')
3598 eq(language, 'en-us')
3599 eq(s, 'My Document For You')
3600
3601
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00003602
R. David Murrayfa606922010-01-16 18:41:00 +00003603# Tests to ensure that signed parts of an email are completely preserved, as
3604# required by RFC1847 section 2.1. Note that these are incomplete, because the
3605# email package does not currently always preserve the body. See issue 1670765.
3606class TestSigned(TestEmailBase):
3607
3608 def _msg_and_obj(self, filename):
3609 with openfile(findfile(filename)) as fp:
3610 original = fp.read()
3611 msg = email.message_from_string(original)
3612 return original, msg
3613
3614 def _signed_parts_eq(self, original, result):
3615 # Extract the first mime part of each message
3616 import re
3617 repart = re.compile(r'^--([^\n]+)\n(.*?)\n--\1$', re.S | re.M)
3618 inpart = repart.search(original).group(2)
3619 outpart = repart.search(result).group(2)
3620 self.assertEqual(outpart, inpart)
3621
3622 def test_long_headers_as_string(self):
3623 original, msg = self._msg_and_obj('msg_45.txt')
3624 result = msg.as_string()
3625 self._signed_parts_eq(original, result)
3626
3627 def test_long_headers_as_string_maxheaderlen(self):
3628 original, msg = self._msg_and_obj('msg_45.txt')
3629 result = msg.as_string(maxheaderlen=60)
3630 self._signed_parts_eq(original, result)
3631
3632 def test_long_headers_flatten(self):
3633 original, msg = self._msg_and_obj('msg_45.txt')
3634 fp = StringIO()
3635 Generator(fp).flatten(msg)
3636 result = fp.getvalue()
3637 self._signed_parts_eq(original, result)
3638
3639
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00003640
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003641def _testclasses():
3642 mod = sys.modules[__name__]
3643 return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
3644
3645
3646def suite():
3647 suite = unittest.TestSuite()
3648 for testclass in _testclasses():
3649 suite.addTest(unittest.makeSuite(testclass))
3650 return suite
3651
3652
3653def test_main():
3654 for testclass in _testclasses():
3655 run_unittest(testclass)
3656
3657
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00003658
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003659if __name__ == '__main__':
3660 unittest.main(defaultTest='suite')