blob: e06c4611a5351ea38582ed5e651956b259650a26 [file] [log] [blame]
Benjamin Petersonffeda292010-01-09 18:48:46 +00001# Copyright (C) 2001-2010 Python Software Foundation
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002# Contact: email-sig@python.org
3# email package unit tests
4
5import os
6import sys
7import time
8import base64
9import difflib
10import unittest
11import warnings
R. David Murray43b2f452011-02-11 03:13:19 +000012import textwrap
Guido van Rossum8b3febe2007-08-30 01:15:14 +000013
14from io import StringIO
15from itertools import chain
16
17import email
18
19from email.charset import Charset
20from email.header import Header, decode_header, make_header
21from email.parser import Parser, HeaderParser
22from email.generator import Generator, DecodedGenerator
23from email.message import Message
24from email.mime.application import MIMEApplication
25from email.mime.audio import MIMEAudio
26from email.mime.text import MIMEText
27from email.mime.image import MIMEImage
28from email.mime.base import MIMEBase
29from email.mime.message import MIMEMessage
30from email.mime.multipart import MIMEMultipart
31from email import utils
32from email import errors
33from email import encoders
34from email import iterators
35from email import base64mime
36from email import quoprimime
37
Benjamin Petersonee8712c2008-05-20 21:35:26 +000038from test.support import findfile, run_unittest
Guido van Rossum8b3febe2007-08-30 01:15:14 +000039from email.test import __file__ as landmark
40
41
42NL = '\n'
43EMPTYSTRING = ''
44SPACE = ' '
45
46
Ezio Melotti19f2aeb2010-11-21 01:30:29 +000047
Guido van Rossum8b3febe2007-08-30 01:15:14 +000048def openfile(filename, *args, **kws):
49 path = os.path.join(os.path.dirname(landmark), 'data', filename)
50 return open(path, *args, **kws)
51
52
Ezio Melotti19f2aeb2010-11-21 01:30:29 +000053
Guido van Rossum8b3febe2007-08-30 01:15:14 +000054# Base test class
55class TestEmailBase(unittest.TestCase):
56 def ndiffAssertEqual(self, first, second):
Georg Brandlab91fde2009-08-13 08:51:18 +000057 """Like assertEqual except use ndiff for readable output."""
Guido van Rossum8b3febe2007-08-30 01:15:14 +000058 if first != second:
59 sfirst = str(first)
60 ssecond = str(second)
61 rfirst = [repr(line) for line in sfirst.splitlines()]
62 rsecond = [repr(line) for line in ssecond.splitlines()]
63 diff = difflib.ndiff(rfirst, rsecond)
64 raise self.failureException(NL + NL.join(diff))
65
66 def _msgobj(self, filename):
67 with openfile(findfile(filename)) as fp:
68 return email.message_from_file(fp)
69
70
Ezio Melotti19f2aeb2010-11-21 01:30:29 +000071
Guido van Rossum8b3febe2007-08-30 01:15:14 +000072# Test various aspects of the Message class's API
73class TestMessageAPI(TestEmailBase):
74 def test_get_all(self):
75 eq = self.assertEqual
76 msg = self._msgobj('msg_20.txt')
77 eq(msg.get_all('cc'), ['ccc@zzz.org', 'ddd@zzz.org', 'eee@zzz.org'])
78 eq(msg.get_all('xx', 'n/a'), 'n/a')
79
80 def test_getset_charset(self):
81 eq = self.assertEqual
82 msg = Message()
83 eq(msg.get_charset(), None)
84 charset = Charset('iso-8859-1')
85 msg.set_charset(charset)
86 eq(msg['mime-version'], '1.0')
87 eq(msg.get_content_type(), 'text/plain')
88 eq(msg['content-type'], 'text/plain; charset="iso-8859-1"')
89 eq(msg.get_param('charset'), 'iso-8859-1')
90 eq(msg['content-transfer-encoding'], 'quoted-printable')
91 eq(msg.get_charset().input_charset, 'iso-8859-1')
92 # Remove the charset
93 msg.set_charset(None)
94 eq(msg.get_charset(), None)
95 eq(msg['content-type'], 'text/plain')
96 # Try adding a charset when there's already MIME headers present
97 msg = Message()
98 msg['MIME-Version'] = '2.0'
99 msg['Content-Type'] = 'text/x-weird'
100 msg['Content-Transfer-Encoding'] = 'quinted-puntable'
101 msg.set_charset(charset)
102 eq(msg['mime-version'], '2.0')
103 eq(msg['content-type'], 'text/x-weird; charset="iso-8859-1"')
104 eq(msg['content-transfer-encoding'], 'quinted-puntable')
105
106 def test_set_charset_from_string(self):
107 eq = self.assertEqual
108 msg = Message()
109 msg.set_charset('us-ascii')
110 eq(msg.get_charset().input_charset, 'us-ascii')
111 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
112
113 def test_set_payload_with_charset(self):
114 msg = Message()
115 charset = Charset('iso-8859-1')
116 msg.set_payload('This is a string payload', charset)
117 self.assertEqual(msg.get_charset().input_charset, 'iso-8859-1')
118
119 def test_get_charsets(self):
120 eq = self.assertEqual
121
122 msg = self._msgobj('msg_08.txt')
123 charsets = msg.get_charsets()
124 eq(charsets, [None, 'us-ascii', 'iso-8859-1', 'iso-8859-2', 'koi8-r'])
125
126 msg = self._msgobj('msg_09.txt')
127 charsets = msg.get_charsets('dingbat')
128 eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat',
129 'koi8-r'])
130
131 msg = self._msgobj('msg_12.txt')
132 charsets = msg.get_charsets()
133 eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2',
134 'iso-8859-3', 'us-ascii', 'koi8-r'])
135
136 def test_get_filename(self):
137 eq = self.assertEqual
138
139 msg = self._msgobj('msg_04.txt')
140 filenames = [p.get_filename() for p in msg.get_payload()]
141 eq(filenames, ['msg.txt', 'msg.txt'])
142
143 msg = self._msgobj('msg_07.txt')
144 subpart = msg.get_payload(1)
145 eq(subpart.get_filename(), 'dingusfish.gif')
146
147 def test_get_filename_with_name_parameter(self):
148 eq = self.assertEqual
149
150 msg = self._msgobj('msg_44.txt')
151 filenames = [p.get_filename() for p in msg.get_payload()]
152 eq(filenames, ['msg.txt', 'msg.txt'])
153
154 def test_get_boundary(self):
155 eq = self.assertEqual
156 msg = self._msgobj('msg_07.txt')
157 # No quotes!
158 eq(msg.get_boundary(), 'BOUNDARY')
159
160 def test_set_boundary(self):
161 eq = self.assertEqual
162 # This one has no existing boundary parameter, but the Content-Type:
163 # header appears fifth.
164 msg = self._msgobj('msg_01.txt')
165 msg.set_boundary('BOUNDARY')
166 header, value = msg.items()[4]
167 eq(header.lower(), 'content-type')
168 eq(value, 'text/plain; charset="us-ascii"; boundary="BOUNDARY"')
169 # This one has a Content-Type: header, with a boundary, stuck in the
170 # middle of its headers. Make sure the order is preserved; it should
171 # be fifth.
172 msg = self._msgobj('msg_04.txt')
173 msg.set_boundary('BOUNDARY')
174 header, value = msg.items()[4]
175 eq(header.lower(), 'content-type')
176 eq(value, 'multipart/mixed; boundary="BOUNDARY"')
177 # And this one has no Content-Type: header at all.
178 msg = self._msgobj('msg_03.txt')
179 self.assertRaises(errors.HeaderParseError,
180 msg.set_boundary, 'BOUNDARY')
181
R. David Murray27c19142010-12-21 18:11:01 +0000182 def test_make_boundary(self):
183 msg = MIMEMultipart('form-data')
184 # Note that when the boundary gets created is an implementation
185 # detail and might change.
186 self.assertEqual(msg.items()[0][1], 'multipart/form-data')
187 # Trigger creation of boundary
188 msg.as_string()
189 self.assertEqual(msg.items()[0][1][:33],
190 'multipart/form-data; boundary="==')
191 # XXX: there ought to be tests of the uniqueness of the boundary, too.
192
R. David Murrayd0a04ff2010-02-21 04:48:18 +0000193 def test_message_rfc822_only(self):
194 # Issue 7970: message/rfc822 not in multipart parsed by
195 # HeaderParser caused an exception when flattened.
196 fp = openfile(findfile('msg_46.txt'))
197 msgdata = fp.read()
198 parser = HeaderParser()
199 msg = parser.parsestr(msgdata)
200 out = StringIO()
201 gen = Generator(out, True, 0)
202 gen.flatten(msg, False)
203 self.assertEqual(out.getvalue(), msgdata)
204
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000205 def test_get_decoded_payload(self):
206 eq = self.assertEqual
207 msg = self._msgobj('msg_10.txt')
208 # The outer message is a multipart
209 eq(msg.get_payload(decode=True), None)
210 # Subpart 1 is 7bit encoded
211 eq(msg.get_payload(0).get_payload(decode=True),
212 b'This is a 7bit encoded message.\n')
213 # Subpart 2 is quopri
214 eq(msg.get_payload(1).get_payload(decode=True),
215 b'\xa1This is a Quoted Printable encoded message!\n')
216 # Subpart 3 is base64
217 eq(msg.get_payload(2).get_payload(decode=True),
218 b'This is a Base64 encoded message.')
219 # Subpart 4 has no Content-Transfer-Encoding: header.
220 eq(msg.get_payload(3).get_payload(decode=True),
221 b'This has no Content-Transfer-Encoding: header.\n')
222
223 def test_get_decoded_uu_payload(self):
224 eq = self.assertEqual
225 msg = Message()
226 msg.set_payload('begin 666 -\n+:&5L;&\\@=V]R;&0 \n \nend\n')
227 for cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):
228 msg['content-transfer-encoding'] = cte
229 eq(msg.get_payload(decode=True), b'hello world')
230 # Now try some bogus data
231 msg.set_payload('foo')
232 eq(msg.get_payload(decode=True), b'foo')
233
234 def test_decoded_generator(self):
235 eq = self.assertEqual
236 msg = self._msgobj('msg_07.txt')
237 with openfile('msg_17.txt') as fp:
238 text = fp.read()
239 s = StringIO()
240 g = DecodedGenerator(s)
241 g.flatten(msg)
242 eq(s.getvalue(), text)
243
244 def test__contains__(self):
245 msg = Message()
246 msg['From'] = 'Me'
247 msg['to'] = 'You'
248 # Check for case insensitivity
Georg Brandlab91fde2009-08-13 08:51:18 +0000249 self.assertTrue('from' in msg)
250 self.assertTrue('From' in msg)
251 self.assertTrue('FROM' in msg)
252 self.assertTrue('to' in msg)
253 self.assertTrue('To' in msg)
254 self.assertTrue('TO' in msg)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000255
256 def test_as_string(self):
257 eq = self.ndiffAssertEqual
258 msg = self._msgobj('msg_01.txt')
259 with openfile('msg_01.txt') as fp:
260 text = fp.read()
261 eq(text, str(msg))
262 fullrepr = msg.as_string(unixfrom=True)
263 lines = fullrepr.split('\n')
Georg Brandlab91fde2009-08-13 08:51:18 +0000264 self.assertTrue(lines[0].startswith('From '))
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000265 eq(text, NL.join(lines[1:]))
266
267 def test_bad_param(self):
268 msg = email.message_from_string("Content-Type: blarg; baz; boo\n")
269 self.assertEqual(msg.get_param('baz'), '')
270
271 def test_missing_filename(self):
272 msg = email.message_from_string("From: foo\n")
273 self.assertEqual(msg.get_filename(), None)
274
275 def test_bogus_filename(self):
276 msg = email.message_from_string(
277 "Content-Disposition: blarg; filename\n")
278 self.assertEqual(msg.get_filename(), '')
279
280 def test_missing_boundary(self):
281 msg = email.message_from_string("From: foo\n")
282 self.assertEqual(msg.get_boundary(), None)
283
284 def test_get_params(self):
285 eq = self.assertEqual
286 msg = email.message_from_string(
287 'X-Header: foo=one; bar=two; baz=three\n')
288 eq(msg.get_params(header='x-header'),
289 [('foo', 'one'), ('bar', 'two'), ('baz', 'three')])
290 msg = email.message_from_string(
291 'X-Header: foo; bar=one; baz=two\n')
292 eq(msg.get_params(header='x-header'),
293 [('foo', ''), ('bar', 'one'), ('baz', 'two')])
294 eq(msg.get_params(), None)
295 msg = email.message_from_string(
296 'X-Header: foo; bar="one"; baz=two\n')
297 eq(msg.get_params(header='x-header'),
298 [('foo', ''), ('bar', 'one'), ('baz', 'two')])
299
300 def test_get_param_liberal(self):
301 msg = Message()
302 msg['Content-Type'] = 'Content-Type: Multipart/mixed; boundary = "CPIMSSMTPC06p5f3tG"'
303 self.assertEqual(msg.get_param('boundary'), 'CPIMSSMTPC06p5f3tG')
304
305 def test_get_param(self):
306 eq = self.assertEqual
307 msg = email.message_from_string(
308 "X-Header: foo=one; bar=two; baz=three\n")
309 eq(msg.get_param('bar', header='x-header'), 'two')
310 eq(msg.get_param('quuz', header='x-header'), None)
311 eq(msg.get_param('quuz'), None)
312 msg = email.message_from_string(
313 'X-Header: foo; bar="one"; baz=two\n')
314 eq(msg.get_param('foo', header='x-header'), '')
315 eq(msg.get_param('bar', header='x-header'), 'one')
316 eq(msg.get_param('baz', header='x-header'), 'two')
317 # XXX: We are not RFC-2045 compliant! We cannot parse:
318 # msg["Content-Type"] = 'text/plain; weird="hey; dolly? [you] @ <\\"home\\">?"'
319 # msg.get_param("weird")
320 # yet.
321
322 def test_get_param_funky_continuation_lines(self):
323 msg = self._msgobj('msg_22.txt')
324 self.assertEqual(msg.get_payload(1).get_param('name'), 'wibble.JPG')
325
326 def test_get_param_with_semis_in_quotes(self):
327 msg = email.message_from_string(
328 'Content-Type: image/pjpeg; name="Jim&amp;&amp;Jill"\n')
329 self.assertEqual(msg.get_param('name'), 'Jim&amp;&amp;Jill')
330 self.assertEqual(msg.get_param('name', unquote=False),
331 '"Jim&amp;&amp;Jill"')
332
R. David Murray84ee3102010-04-14 19:05:38 +0000333 def test_get_param_with_quotes(self):
334 msg = email.message_from_string(
335 'Content-Type: foo; bar*0="baz\\"foobar"; bar*1="\\"baz"')
336 self.assertEqual(msg.get_param('bar'), 'baz"foobar"baz')
337 msg = email.message_from_string(
338 "Content-Type: foo; bar*0=\"baz\\\"foobar\"; bar*1=\"\\\"baz\"")
339 self.assertEqual(msg.get_param('bar'), 'baz"foobar"baz')
340
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000341 def test_field_containment(self):
Georg Brandlab91fde2009-08-13 08:51:18 +0000342 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000343 msg = email.message_from_string('Header: exists')
344 unless('header' in msg)
345 unless('Header' in msg)
346 unless('HEADER' in msg)
Georg Brandlab91fde2009-08-13 08:51:18 +0000347 self.assertFalse('headerx' in msg)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000348
349 def test_set_param(self):
350 eq = self.assertEqual
351 msg = Message()
352 msg.set_param('charset', 'iso-2022-jp')
353 eq(msg.get_param('charset'), 'iso-2022-jp')
354 msg.set_param('importance', 'high value')
355 eq(msg.get_param('importance'), 'high value')
356 eq(msg.get_param('importance', unquote=False), '"high value"')
357 eq(msg.get_params(), [('text/plain', ''),
358 ('charset', 'iso-2022-jp'),
359 ('importance', 'high value')])
360 eq(msg.get_params(unquote=False), [('text/plain', ''),
361 ('charset', '"iso-2022-jp"'),
362 ('importance', '"high value"')])
363 msg.set_param('charset', 'iso-9999-xx', header='X-Jimmy')
364 eq(msg.get_param('charset', header='X-Jimmy'), 'iso-9999-xx')
365
366 def test_del_param(self):
367 eq = self.assertEqual
368 msg = self._msgobj('msg_05.txt')
369 eq(msg.get_params(),
370 [('multipart/report', ''), ('report-type', 'delivery-status'),
371 ('boundary', 'D1690A7AC1.996856090/mail.example.com')])
372 old_val = msg.get_param("report-type")
373 msg.del_param("report-type")
374 eq(msg.get_params(),
375 [('multipart/report', ''),
376 ('boundary', 'D1690A7AC1.996856090/mail.example.com')])
377 msg.set_param("report-type", old_val)
378 eq(msg.get_params(),
379 [('multipart/report', ''),
380 ('boundary', 'D1690A7AC1.996856090/mail.example.com'),
381 ('report-type', old_val)])
382
383 def test_del_param_on_other_header(self):
384 msg = Message()
385 msg.add_header('Content-Disposition', 'attachment', filename='bud.gif')
386 msg.del_param('filename', 'content-disposition')
387 self.assertEqual(msg['content-disposition'], 'attachment')
388
389 def test_set_type(self):
390 eq = self.assertEqual
391 msg = Message()
392 self.assertRaises(ValueError, msg.set_type, 'text')
393 msg.set_type('text/plain')
394 eq(msg['content-type'], 'text/plain')
395 msg.set_param('charset', 'us-ascii')
396 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
397 msg.set_type('text/html')
398 eq(msg['content-type'], 'text/html; charset="us-ascii"')
399
400 def test_set_type_on_other_header(self):
401 msg = Message()
402 msg['X-Content-Type'] = 'text/plain'
403 msg.set_type('application/octet-stream', 'X-Content-Type')
404 self.assertEqual(msg['x-content-type'], 'application/octet-stream')
405
406 def test_get_content_type_missing(self):
407 msg = Message()
408 self.assertEqual(msg.get_content_type(), 'text/plain')
409
410 def test_get_content_type_missing_with_default_type(self):
411 msg = Message()
412 msg.set_default_type('message/rfc822')
413 self.assertEqual(msg.get_content_type(), 'message/rfc822')
414
415 def test_get_content_type_from_message_implicit(self):
416 msg = self._msgobj('msg_30.txt')
417 self.assertEqual(msg.get_payload(0).get_content_type(),
418 'message/rfc822')
419
420 def test_get_content_type_from_message_explicit(self):
421 msg = self._msgobj('msg_28.txt')
422 self.assertEqual(msg.get_payload(0).get_content_type(),
423 'message/rfc822')
424
425 def test_get_content_type_from_message_text_plain_implicit(self):
426 msg = self._msgobj('msg_03.txt')
427 self.assertEqual(msg.get_content_type(), 'text/plain')
428
429 def test_get_content_type_from_message_text_plain_explicit(self):
430 msg = self._msgobj('msg_01.txt')
431 self.assertEqual(msg.get_content_type(), 'text/plain')
432
433 def test_get_content_maintype_missing(self):
434 msg = Message()
435 self.assertEqual(msg.get_content_maintype(), 'text')
436
437 def test_get_content_maintype_missing_with_default_type(self):
438 msg = Message()
439 msg.set_default_type('message/rfc822')
440 self.assertEqual(msg.get_content_maintype(), 'message')
441
442 def test_get_content_maintype_from_message_implicit(self):
443 msg = self._msgobj('msg_30.txt')
444 self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message')
445
446 def test_get_content_maintype_from_message_explicit(self):
447 msg = self._msgobj('msg_28.txt')
448 self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message')
449
450 def test_get_content_maintype_from_message_text_plain_implicit(self):
451 msg = self._msgobj('msg_03.txt')
452 self.assertEqual(msg.get_content_maintype(), 'text')
453
454 def test_get_content_maintype_from_message_text_plain_explicit(self):
455 msg = self._msgobj('msg_01.txt')
456 self.assertEqual(msg.get_content_maintype(), 'text')
457
458 def test_get_content_subtype_missing(self):
459 msg = Message()
460 self.assertEqual(msg.get_content_subtype(), 'plain')
461
462 def test_get_content_subtype_missing_with_default_type(self):
463 msg = Message()
464 msg.set_default_type('message/rfc822')
465 self.assertEqual(msg.get_content_subtype(), 'rfc822')
466
467 def test_get_content_subtype_from_message_implicit(self):
468 msg = self._msgobj('msg_30.txt')
469 self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822')
470
471 def test_get_content_subtype_from_message_explicit(self):
472 msg = self._msgobj('msg_28.txt')
473 self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822')
474
475 def test_get_content_subtype_from_message_text_plain_implicit(self):
476 msg = self._msgobj('msg_03.txt')
477 self.assertEqual(msg.get_content_subtype(), 'plain')
478
479 def test_get_content_subtype_from_message_text_plain_explicit(self):
480 msg = self._msgobj('msg_01.txt')
481 self.assertEqual(msg.get_content_subtype(), 'plain')
482
483 def test_get_content_maintype_error(self):
484 msg = Message()
485 msg['Content-Type'] = 'no-slash-in-this-string'
486 self.assertEqual(msg.get_content_maintype(), 'text')
487
488 def test_get_content_subtype_error(self):
489 msg = Message()
490 msg['Content-Type'] = 'no-slash-in-this-string'
491 self.assertEqual(msg.get_content_subtype(), 'plain')
492
493 def test_replace_header(self):
494 eq = self.assertEqual
495 msg = Message()
496 msg.add_header('First', 'One')
497 msg.add_header('Second', 'Two')
498 msg.add_header('Third', 'Three')
499 eq(msg.keys(), ['First', 'Second', 'Third'])
500 eq(msg.values(), ['One', 'Two', 'Three'])
501 msg.replace_header('Second', 'Twenty')
502 eq(msg.keys(), ['First', 'Second', 'Third'])
503 eq(msg.values(), ['One', 'Twenty', 'Three'])
504 msg.add_header('First', 'Eleven')
505 msg.replace_header('First', 'One Hundred')
506 eq(msg.keys(), ['First', 'Second', 'Third', 'First'])
507 eq(msg.values(), ['One Hundred', 'Twenty', 'Three', 'Eleven'])
508 self.assertRaises(KeyError, msg.replace_header, 'Fourth', 'Missing')
509
510 def test_broken_base64_payload(self):
511 x = 'AwDp0P7//y6LwKEAcPa/6Q=9'
512 msg = Message()
513 msg['content-type'] = 'audio/x-midi'
514 msg['content-transfer-encoding'] = 'base64'
515 msg.set_payload(x)
516 self.assertEqual(msg.get_payload(decode=True),
Guido van Rossum9604e662007-08-30 03:46:43 +0000517 bytes(x, 'raw-unicode-escape'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000518
R. David Murrayccb9d052010-12-13 23:57:01 +0000519 # Issue 1078919
520 def test_ascii_add_header(self):
521 msg = Message()
522 msg.add_header('Content-Disposition', 'attachment',
523 filename='bud.gif')
524 self.assertEqual('attachment; filename="bud.gif"',
525 msg['Content-Disposition'])
526
527 def test_noascii_add_header(self):
528 msg = Message()
529 msg.add_header('Content-Disposition', 'attachment',
530 filename="Fußballer.ppt")
531 self.assertEqual(
532 'attachment; filename*="utf-8\'\'Fu%C3%9Fballer.ppt"',
533 msg['Content-Disposition'])
534
535 def test_nonascii_add_header_via_triple(self):
536 msg = Message()
537 msg.add_header('Content-Disposition', 'attachment',
538 filename=('iso-8859-1', '', 'Fußballer.ppt'))
539 self.assertEqual(
540 'attachment; filename*="iso-8859-1\'\'Fu%DFballer.ppt"',
541 msg['Content-Disposition'])
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000542
Ezio Melotti19f2aeb2010-11-21 01:30:29 +0000543
R. David Murray389af002011-01-09 02:48:04 +0000544 # Issue 5871: reject an attempt to embed a header inside a header value
545 # (header injection attack).
546 def test_embeded_header_via_Header_rejected(self):
547 msg = Message()
548 msg['Dummy'] = Header('dummy\nX-Injected-Header: test')
549 self.assertRaises(errors.HeaderParseError, msg.as_string)
550
551 def test_embeded_header_via_string_rejected(self):
552 msg = Message()
553 msg['Dummy'] = 'dummy\nX-Injected-Header: test'
554 self.assertRaises(errors.HeaderParseError, msg.as_string)
555
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000556# Test the email.encoders module
557class TestEncoders(unittest.TestCase):
R David Murray6d94bd42011-03-16 15:52:22 -0400558
559 def test_EncodersEncode_base64(self):
560 with openfile('PyBanner048.gif', 'rb') as fp:
561 bindata = fp.read()
562 mimed = email.mime.image.MIMEImage(bindata)
563 base64ed = mimed.get_payload()
564 # the transfer-encoded body lines should all be <=76 characters
565 lines = base64ed.split('\n')
566 self.assertLessEqual(max([ len(x) for x in lines ]), 76)
567
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000568 def test_encode_empty_payload(self):
569 eq = self.assertEqual
570 msg = Message()
571 msg.set_charset('us-ascii')
572 eq(msg['content-transfer-encoding'], '7bit')
573
574 def test_default_cte(self):
575 eq = self.assertEqual
Ezio Melottic30bb7d2010-04-22 11:58:06 +0000576 # 7bit data and the default us-ascii _charset
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000577 msg = MIMEText('hello world')
578 eq(msg['content-transfer-encoding'], '7bit')
Ezio Melottic30bb7d2010-04-22 11:58:06 +0000579 # Similar, but with 8bit data
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000580 msg = MIMEText('hello \xf8 world')
581 eq(msg['content-transfer-encoding'], '8bit')
582 # And now with a different charset
583 msg = MIMEText('hello \xf8 world', _charset='iso-8859-1')
584 eq(msg['content-transfer-encoding'], 'quoted-printable')
585
R. David Murrayf870d872010-05-06 01:53:03 +0000586 def test_encode7or8bit(self):
587 # Make sure a charset whose input character set is 8bit but
588 # whose output character set is 7bit gets a transfer-encoding
589 # of 7bit.
590 eq = self.assertEqual
R. David Murrayd2d08c62010-06-03 02:05:47 +0000591 msg = MIMEText('æ–‡', _charset='euc-jp')
R. David Murrayf870d872010-05-06 01:53:03 +0000592 eq(msg['content-transfer-encoding'], '7bit')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000593
Ezio Melotti19f2aeb2010-11-21 01:30:29 +0000594
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000595# Test long header wrapping
596class TestLongHeaders(TestEmailBase):
597 def test_split_long_continuation(self):
598 eq = self.ndiffAssertEqual
599 msg = email.message_from_string("""\
600Subject: bug demonstration
601\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
602\tmore text
603
604test
605""")
606 sfp = StringIO()
607 g = Generator(sfp)
608 g.flatten(msg)
609 eq(sfp.getvalue(), """\
610Subject: bug demonstration
611\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
612\tmore text
613
614test
615""")
616
617 def test_another_long_almost_unsplittable_header(self):
618 eq = self.ndiffAssertEqual
619 hstr = """\
620bug demonstration
621\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
622\tmore text"""
623 h = Header(hstr, continuation_ws='\t')
624 eq(h.encode(), """\
625bug demonstration
626\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
627\tmore text""")
628 h = Header(hstr.replace('\t', ' '))
629 eq(h.encode(), """\
630bug demonstration
631 12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
632 more text""")
633
634 def test_long_nonstring(self):
635 eq = self.ndiffAssertEqual
636 g = Charset("iso-8859-1")
637 cz = Charset("iso-8859-2")
638 utf8 = Charset("utf-8")
639 g_head = (b'Die Mieter treten hier ein werden mit einem Foerderband '
640 b'komfortabel den Korridor entlang, an s\xfcdl\xfcndischen '
641 b'Wandgem\xe4lden vorbei, gegen die rotierenden Klingen '
642 b'bef\xf6rdert. ')
643 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
644 b'd\xf9vtipu.. ')
645 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
646 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
647 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
648 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
649 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
650 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
651 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
652 '\u3044\u307e\u3059\u3002')
653 h = Header(g_head, g, header_name='Subject')
654 h.append(cz_head, cz)
655 h.append(utf8_head, utf8)
656 msg = Message()
657 msg['Subject'] = h
658 sfp = StringIO()
659 g = Generator(sfp)
660 g.flatten(msg)
661 eq(sfp.getvalue(), """\
Guido van Rossum9604e662007-08-30 03:46:43 +0000662Subject: =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderb?=
663 =?iso-8859-1?q?and_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen?=
664 =?iso-8859-1?q?_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef?=
665 =?iso-8859-1?q?=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hrouti?=
666 =?iso-8859-2?q?ly_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
667 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC5LiA?=
668 =?utf-8?b?6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn44Gf44KJ?=
669 =?utf-8?b?44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFzIE51bnN0dWNr?=
670 =?utf-8?b?IGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5kIGRhcyBPZGVyIGRp?=
671 =?utf-8?b?ZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIDjgaPjgabjgYTjgb7jgZk=?=
672 =?utf-8?b?44CC?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000673
674""")
Guido van Rossum9604e662007-08-30 03:46:43 +0000675 eq(h.encode(maxlinelen=76), """\
676=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerde?=
677 =?iso-8859-1?q?rband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndis?=
678 =?iso-8859-1?q?chen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klinge?=
679 =?iso-8859-1?q?n_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se?=
680 =?iso-8859-2?q?_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?=
681 =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb?=
682 =?utf-8?b?44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go?=
683 =?utf-8?b?44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBp?=
684 =?utf-8?b?c3QgZGFzIE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWlo?=
685 =?utf-8?b?ZXJodW5kIGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI0=?=
686 =?utf-8?b?44Go6KiA44Gj44Gm44GE44G+44GZ44CC?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000687
688 def test_long_header_encode(self):
689 eq = self.ndiffAssertEqual
690 h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
691 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
692 header_name='X-Foobar-Spoink-Defrobnit')
693 eq(h.encode(), '''\
694wasnipoop; giraffes="very-long-necked-animals";
695 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
696
697 def test_long_header_encode_with_tab_continuation_is_just_a_hint(self):
698 eq = self.ndiffAssertEqual
699 h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
700 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
701 header_name='X-Foobar-Spoink-Defrobnit',
702 continuation_ws='\t')
703 eq(h.encode(), '''\
704wasnipoop; giraffes="very-long-necked-animals";
705 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
706
707 def test_long_header_encode_with_tab_continuation(self):
708 eq = self.ndiffAssertEqual
709 h = Header('wasnipoop; giraffes="very-long-necked-animals";\t'
710 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
711 header_name='X-Foobar-Spoink-Defrobnit',
712 continuation_ws='\t')
713 eq(h.encode(), '''\
714wasnipoop; giraffes="very-long-necked-animals";
715\tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
716
717 def test_header_splitter(self):
718 eq = self.ndiffAssertEqual
719 msg = MIMEText('')
720 # It'd be great if we could use add_header() here, but that doesn't
721 # guarantee an order of the parameters.
722 msg['X-Foobar-Spoink-Defrobnit'] = (
723 'wasnipoop; giraffes="very-long-necked-animals"; '
724 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"')
725 sfp = StringIO()
726 g = Generator(sfp)
727 g.flatten(msg)
728 eq(sfp.getvalue(), '''\
729Content-Type: text/plain; charset="us-ascii"
730MIME-Version: 1.0
731Content-Transfer-Encoding: 7bit
732X-Foobar-Spoink-Defrobnit: wasnipoop; giraffes="very-long-necked-animals";
733 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"
734
735''')
736
737 def test_no_semis_header_splitter(self):
738 eq = self.ndiffAssertEqual
739 msg = Message()
740 msg['From'] = 'test@dom.ain'
741 msg['References'] = SPACE.join('<%d@dom.ain>' % i for i in range(10))
742 msg.set_payload('Test')
743 sfp = StringIO()
744 g = Generator(sfp)
745 g.flatten(msg)
746 eq(sfp.getvalue(), """\
747From: test@dom.ain
748References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain>
749 <5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain>
750
751Test""")
752
R David Murray7da4db12011-04-07 20:37:17 -0400753 def test_last_split_chunk_does_not_fit(self):
754 eq = self.ndiffAssertEqual
755 h = Header('Subject: the first part of this is short, but_the_second'
756 '_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line'
757 '_all_by_itself')
758 eq(h.encode(), """\
759Subject: the first part of this is short,
760 but_the_second_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself""")
761
762 def test_splittable_leading_char_followed_by_overlong_unsplitable(self):
763 eq = self.ndiffAssertEqual
764 h = Header(', but_the_second'
765 '_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line'
766 '_all_by_itself')
767 eq(h.encode(), """\
768,
769 but_the_second_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself""")
770
771 def test_multiple_splittable_leading_char_followed_by_overlong_unsplitable(self):
772 eq = self.ndiffAssertEqual
773 h = Header(', , but_the_second'
774 '_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line'
775 '_all_by_itself')
776 eq(h.encode(), """\
777, ,
778 but_the_second_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself""")
779
780 def test_trailing_splitable_on_overlong_unsplitable(self):
781 eq = self.ndiffAssertEqual
782 h = Header('this_part_does_not_fit_within_maxlinelen_and_thus_should_'
783 'be_on_a_line_all_by_itself;')
784 eq(h.encode(), "this_part_does_not_fit_within_maxlinelen_and_thus_should_"
785 "be_on_a_line_all_by_itself;")
786
787 def test_trailing_splitable_on_overlong_unsplitable_with_leading_splitable(self):
788 eq = self.ndiffAssertEqual
789 h = Header('; '
790 'this_part_does_not_fit_within_maxlinelen_and_thus_should_'
791 'be_on_a_line_all_by_itself;')
792 eq(h.encode(), """\
793;
794 this_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself;""")
795
R David Murraye1292a22011-04-07 20:54:03 -0400796 def test_long_header_with_multiple_sequential_split_chars(self):
797 # Issue 11492
798
799 eq = self.ndiffAssertEqual
800 h = Header('This is a long line that has two whitespaces in a row. '
801 'This used to cause truncation of the header when folded')
802 eq(h.encode(), """\
803This is a long line that has two whitespaces in a row. This used to cause
804 truncation of the header when folded""")
805
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000806 def test_no_split_long_header(self):
807 eq = self.ndiffAssertEqual
808 hstr = 'References: ' + 'x' * 80
Guido van Rossum9604e662007-08-30 03:46:43 +0000809 h = Header(hstr)
810 # These come on two lines because Headers are really field value
811 # classes and don't really know about their field names.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000812 eq(h.encode(), """\
Guido van Rossum9604e662007-08-30 03:46:43 +0000813References:
814 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""")
815 h = Header('x' * 80)
816 eq(h.encode(), 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000817
818 def test_splitting_multiple_long_lines(self):
819 eq = self.ndiffAssertEqual
820 hstr = """\
821from 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)
822\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)
823\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)
824"""
825 h = Header(hstr, continuation_ws='\t')
826 eq(h.encode(), """\
827from babylon.socal-raves.org (localhost [127.0.0.1]);
828 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
829 for <mailman-admin@babylon.socal-raves.org>;
830 Sat, 2 Feb 2002 17:00:06 -0800 (PST)
831\tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
832 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
833 for <mailman-admin@babylon.socal-raves.org>;
834 Sat, 2 Feb 2002 17:00:06 -0800 (PST)
835\tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
836 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
837 for <mailman-admin@babylon.socal-raves.org>;
838 Sat, 2 Feb 2002 17:00:06 -0800 (PST)""")
839
840 def test_splitting_first_line_only_is_long(self):
841 eq = self.ndiffAssertEqual
842 hstr = """\
843from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca)
844\tby kronos.mems-exchange.org with esmtp (Exim 4.05)
845\tid 17k4h5-00034i-00
846\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400"""
847 h = Header(hstr, maxlinelen=78, header_name='Received',
848 continuation_ws='\t')
849 eq(h.encode(), """\
850from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93]
851 helo=cthulhu.gerg.ca)
852\tby kronos.mems-exchange.org with esmtp (Exim 4.05)
853\tid 17k4h5-00034i-00
854\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""")
855
856 def test_long_8bit_header(self):
857 eq = self.ndiffAssertEqual
858 msg = Message()
859 h = Header('Britische Regierung gibt', 'iso-8859-1',
860 header_name='Subject')
861 h.append('gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte')
Guido van Rossum9604e662007-08-30 03:46:43 +0000862 eq(h.encode(maxlinelen=76), """\
863=?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offs?=
864 =?iso-8859-1?q?hore-Windkraftprojekte?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000865 msg['Subject'] = h
Guido van Rossum9604e662007-08-30 03:46:43 +0000866 eq(msg.as_string(maxheaderlen=76), """\
867Subject: =?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offs?=
868 =?iso-8859-1?q?hore-Windkraftprojekte?=
869
870""")
871 eq(msg.as_string(maxheaderlen=0), """\
872Subject: =?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offshore-Windkraftprojekte?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000873
874""")
875
876 def test_long_8bit_header_no_charset(self):
877 eq = self.ndiffAssertEqual
878 msg = Message()
Barry Warsaw8c571042007-08-30 19:17:18 +0000879 header_string = ('Britische Regierung gibt gr\xfcnes Licht '
880 'f\xfcr Offshore-Windkraftprojekte '
881 '<a-very-long-address@example.com>')
882 msg['Reply-To'] = header_string
883 self.assertRaises(UnicodeEncodeError, msg.as_string)
884 msg = Message()
885 msg['Reply-To'] = Header(header_string, 'utf-8',
886 header_name='Reply-To')
887 eq(msg.as_string(maxheaderlen=78), """\
888Reply-To: =?utf-8?q?Britische_Regierung_gibt_gr=C3=BCnes_Licht_f=C3=BCr_Offs?=
889 =?utf-8?q?hore-Windkraftprojekte_=3Ca-very-long-address=40example=2Ecom=3E?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000890
891""")
892
893 def test_long_to_header(self):
894 eq = self.ndiffAssertEqual
895 to = ('"Someone Test #A" <someone@eecs.umich.edu>,'
896 '<someone@eecs.umich.edu>,'
897 '"Someone Test #B" <someone@umich.edu>, '
898 '"Someone Test #C" <someone@eecs.umich.edu>, '
899 '"Someone Test #D" <someone@eecs.umich.edu>')
900 msg = Message()
901 msg['To'] = to
902 eq(msg.as_string(maxheaderlen=78), '''\
Guido van Rossum9604e662007-08-30 03:46:43 +0000903To: "Someone Test #A" <someone@eecs.umich.edu>,<someone@eecs.umich.edu>,
Barry Warsaw70d61ce2009-03-30 23:12:30 +0000904 "Someone Test #B" <someone@umich.edu>,
Guido van Rossum9604e662007-08-30 03:46:43 +0000905 "Someone Test #C" <someone@eecs.umich.edu>,
906 "Someone Test #D" <someone@eecs.umich.edu>
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000907
908''')
909
910 def test_long_line_after_append(self):
911 eq = self.ndiffAssertEqual
912 s = 'This is an example of string which has almost the limit of header length.'
913 h = Header(s)
914 h.append('Add another line.')
Guido van Rossum9604e662007-08-30 03:46:43 +0000915 eq(h.encode(maxlinelen=76), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000916This is an example of string which has almost the limit of header length.
917 Add another line.""")
918
919 def test_shorter_line_with_append(self):
920 eq = self.ndiffAssertEqual
921 s = 'This is a shorter line.'
922 h = Header(s)
923 h.append('Add another sentence. (Surprise?)')
924 eq(h.encode(),
925 'This is a shorter line. Add another sentence. (Surprise?)')
926
927 def test_long_field_name(self):
928 eq = self.ndiffAssertEqual
929 fn = 'X-Very-Very-Very-Long-Header-Name'
Guido van Rossum9604e662007-08-30 03:46:43 +0000930 gs = ('Die Mieter treten hier ein werden mit einem Foerderband '
931 'komfortabel den Korridor entlang, an s\xfcdl\xfcndischen '
932 'Wandgem\xe4lden vorbei, gegen die rotierenden Klingen '
933 'bef\xf6rdert. ')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000934 h = Header(gs, 'iso-8859-1', header_name=fn)
935 # BAW: this seems broken because the first line is too long
Guido van Rossum9604e662007-08-30 03:46:43 +0000936 eq(h.encode(maxlinelen=76), """\
937=?iso-8859-1?q?Die_Mieter_treten_hier_e?=
938 =?iso-8859-1?q?in_werden_mit_einem_Foerderband_komfortabel_den_Korridor_e?=
939 =?iso-8859-1?q?ntlang=2C_an_s=FCdl=FCndischen_Wandgem=E4lden_vorbei=2C_ge?=
940 =?iso-8859-1?q?gen_die_rotierenden_Klingen_bef=F6rdert=2E_?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000941
942 def test_long_received_header(self):
943 h = ('from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) '
944 'by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP; '
945 'Wed, 05 Mar 2003 18:10:18 -0700')
946 msg = Message()
947 msg['Received-1'] = Header(h, continuation_ws='\t')
948 msg['Received-2'] = h
Barry Warsawbef9d212007-08-31 10:55:37 +0000949 # This should be splitting on spaces not semicolons.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000950 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
Barry Warsawbef9d212007-08-31 10:55:37 +0000951Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
952 Wed, 05 Mar 2003 18:10:18 -0700
953Received-2: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
954 Wed, 05 Mar 2003 18:10:18 -0700
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000955
956""")
957
958 def test_string_headerinst_eq(self):
959 h = ('<15975.17901.207240.414604@sgigritzmann1.mathematik.'
960 'tu-muenchen.de> (David Bremner\'s message of '
961 '"Thu, 6 Mar 2003 13:58:21 +0100")')
962 msg = Message()
963 msg['Received-1'] = Header(h, header_name='Received-1',
964 continuation_ws='\t')
965 msg['Received-2'] = h
Barry Warsawbef9d212007-08-31 10:55:37 +0000966 # XXX This should be splitting on spaces not commas.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000967 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
Barry Warsawbef9d212007-08-31 10:55:37 +0000968Received-1: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner's message of \"Thu,
969 6 Mar 2003 13:58:21 +0100\")
970Received-2: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner's message of \"Thu,
971 6 Mar 2003 13:58:21 +0100\")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000972
973""")
974
975 def test_long_unbreakable_lines_with_continuation(self):
976 eq = self.ndiffAssertEqual
977 msg = Message()
978 t = """\
979iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
980 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp"""
981 msg['Face-1'] = t
982 msg['Face-2'] = Header(t, header_name='Face-2')
Barry Warsawbef9d212007-08-31 10:55:37 +0000983 # XXX This splitting is all wrong. It the first value line should be
984 # snug against the field name.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000985 eq(msg.as_string(maxheaderlen=78), """\
Barry Warsawc5a6a302007-08-31 11:19:21 +0000986Face-1:\x20
Barry Warsaw70d61ce2009-03-30 23:12:30 +0000987 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000988 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
Barry Warsawc5a6a302007-08-31 11:19:21 +0000989Face-2:\x20
Barry Warsawbef9d212007-08-31 10:55:37 +0000990 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000991 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
992
993""")
994
995 def test_another_long_multiline_header(self):
996 eq = self.ndiffAssertEqual
997 m = ('Received: from siimage.com '
998 '([172.25.1.3]) by zima.siliconimage.com with '
Guido van Rossum9604e662007-08-30 03:46:43 +0000999 'Microsoft SMTPSVC(5.0.2195.4905); '
1000 'Wed, 16 Oct 2002 07:41:11 -0700')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001001 msg = email.message_from_string(m)
1002 eq(msg.as_string(maxheaderlen=78), '''\
Barry Warsawbef9d212007-08-31 10:55:37 +00001003Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with Microsoft SMTPSVC(5.0.2195.4905);
1004 Wed, 16 Oct 2002 07:41:11 -0700
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001005
1006''')
1007
1008 def test_long_lines_with_different_header(self):
1009 eq = self.ndiffAssertEqual
1010 h = ('List-Unsubscribe: '
1011 '<http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,'
1012 ' <mailto:spamassassin-talk-request@lists.sourceforge.net'
1013 '?subject=unsubscribe>')
1014 msg = Message()
1015 msg['List'] = h
1016 msg['List'] = Header(h, header_name='List')
1017 eq(msg.as_string(maxheaderlen=78), """\
1018List: List-Unsubscribe: <http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
Barry Warsawbef9d212007-08-31 10:55:37 +00001019 <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001020List: List-Unsubscribe: <http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
Barry Warsawbef9d212007-08-31 10:55:37 +00001021 <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001022
1023""")
1024
R. David Murray43b2f452011-02-11 03:13:19 +00001025 def test_long_rfc2047_header_with_embedded_fws(self):
1026 h = Header(textwrap.dedent("""\
1027 We're going to pretend this header is in a non-ascii character set
1028 \tto see if line wrapping with encoded words and embedded
1029 folding white space works"""),
1030 charset='utf-8',
1031 header_name='Test')
1032 self.assertEqual(h.encode()+'\n', textwrap.dedent("""\
1033 =?utf-8?q?We=27re_going_to_pretend_this_header_is_in_a_non-ascii_chara?=
1034 =?utf-8?q?cter_set?=
1035 =?utf-8?q?_to_see_if_line_wrapping_with_encoded_words_and_embedded?=
1036 =?utf-8?q?_folding_white_space_works?=""")+'\n')
1037
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001038
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001039
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001040# Test mangling of "From " lines in the body of a message
1041class TestFromMangling(unittest.TestCase):
1042 def setUp(self):
1043 self.msg = Message()
1044 self.msg['From'] = 'aaa@bbb.org'
1045 self.msg.set_payload("""\
1046From the desk of A.A.A.:
1047Blah blah blah
1048""")
1049
1050 def test_mangled_from(self):
1051 s = StringIO()
1052 g = Generator(s, mangle_from_=True)
1053 g.flatten(self.msg)
1054 self.assertEqual(s.getvalue(), """\
1055From: aaa@bbb.org
1056
1057>From the desk of A.A.A.:
1058Blah blah blah
1059""")
1060
1061 def test_dont_mangle_from(self):
1062 s = StringIO()
1063 g = Generator(s, mangle_from_=False)
1064 g.flatten(self.msg)
1065 self.assertEqual(s.getvalue(), """\
1066From: aaa@bbb.org
1067
1068From the desk of A.A.A.:
1069Blah blah blah
1070""")
1071
1072
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001073
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001074# Test the basic MIMEAudio class
1075class TestMIMEAudio(unittest.TestCase):
1076 def setUp(self):
1077 # Make sure we pick up the audiotest.au that lives in email/test/data.
1078 # In Python, there's an audiotest.au living in Lib/test but that isn't
1079 # included in some binary distros that don't include the test
1080 # package. The trailing empty string on the .join() is significant
1081 # since findfile() will do a dirname().
1082 datadir = os.path.join(os.path.dirname(landmark), 'data', '')
1083 with open(findfile('audiotest.au', datadir), 'rb') as fp:
1084 self._audiodata = fp.read()
1085 self._au = MIMEAudio(self._audiodata)
1086
1087 def test_guess_minor_type(self):
1088 self.assertEqual(self._au.get_content_type(), 'audio/basic')
1089
1090 def test_encoding(self):
1091 payload = self._au.get_payload()
R. David Murray99147c42010-06-04 16:15:34 +00001092 self.assertEqual(base64.decodebytes(bytes(payload, 'ascii')),
1093 self._audiodata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001094
1095 def test_checkSetMinor(self):
1096 au = MIMEAudio(self._audiodata, 'fish')
1097 self.assertEqual(au.get_content_type(), 'audio/fish')
1098
1099 def test_add_header(self):
1100 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001101 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001102 self._au.add_header('Content-Disposition', 'attachment',
1103 filename='audiotest.au')
1104 eq(self._au['content-disposition'],
1105 'attachment; filename="audiotest.au"')
1106 eq(self._au.get_params(header='content-disposition'),
1107 [('attachment', ''), ('filename', 'audiotest.au')])
1108 eq(self._au.get_param('filename', header='content-disposition'),
1109 'audiotest.au')
1110 missing = []
1111 eq(self._au.get_param('attachment', header='content-disposition'), '')
1112 unless(self._au.get_param('foo', failobj=missing,
1113 header='content-disposition') is missing)
1114 # Try some missing stuff
1115 unless(self._au.get_param('foobar', missing) is missing)
1116 unless(self._au.get_param('attachment', missing,
1117 header='foobar') is missing)
1118
1119
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001120
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001121# Test the basic MIMEImage class
1122class TestMIMEImage(unittest.TestCase):
1123 def setUp(self):
1124 with openfile('PyBanner048.gif', 'rb') as fp:
1125 self._imgdata = fp.read()
1126 self._im = MIMEImage(self._imgdata)
1127
1128 def test_guess_minor_type(self):
1129 self.assertEqual(self._im.get_content_type(), 'image/gif')
1130
1131 def test_encoding(self):
1132 payload = self._im.get_payload()
R. David Murray99147c42010-06-04 16:15:34 +00001133 self.assertEqual(base64.decodebytes(bytes(payload, 'ascii')),
1134 self._imgdata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001135
1136 def test_checkSetMinor(self):
1137 im = MIMEImage(self._imgdata, 'fish')
1138 self.assertEqual(im.get_content_type(), 'image/fish')
1139
1140 def test_add_header(self):
1141 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001142 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001143 self._im.add_header('Content-Disposition', 'attachment',
1144 filename='dingusfish.gif')
1145 eq(self._im['content-disposition'],
1146 'attachment; filename="dingusfish.gif"')
1147 eq(self._im.get_params(header='content-disposition'),
1148 [('attachment', ''), ('filename', 'dingusfish.gif')])
1149 eq(self._im.get_param('filename', header='content-disposition'),
1150 'dingusfish.gif')
1151 missing = []
1152 eq(self._im.get_param('attachment', header='content-disposition'), '')
1153 unless(self._im.get_param('foo', failobj=missing,
1154 header='content-disposition') is missing)
1155 # Try some missing stuff
1156 unless(self._im.get_param('foobar', missing) is missing)
1157 unless(self._im.get_param('attachment', missing,
1158 header='foobar') is missing)
1159
1160
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001161
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001162# Test the basic MIMEApplication class
1163class TestMIMEApplication(unittest.TestCase):
1164 def test_headers(self):
1165 eq = self.assertEqual
Barry Warsaw8b2af272007-08-31 03:04:26 +00001166 msg = MIMEApplication(b'\xfa\xfb\xfc\xfd\xfe\xff')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001167 eq(msg.get_content_type(), 'application/octet-stream')
1168 eq(msg['content-transfer-encoding'], 'base64')
1169
1170 def test_body(self):
1171 eq = self.assertEqual
R David Murray6d94bd42011-03-16 15:52:22 -04001172 bytesdata = b'\xfa\xfb\xfc\xfd\xfe\xff'
1173 msg = MIMEApplication(bytesdata)
1174 # whitespace in the cte encoded block is RFC-irrelevant.
1175 eq(msg.get_payload().strip(), '+vv8/f7/')
1176 eq(msg.get_payload(decode=True), bytesdata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001177
1178
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001179
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001180# Test the basic MIMEText class
1181class TestMIMEText(unittest.TestCase):
1182 def setUp(self):
1183 self._msg = MIMEText('hello there')
1184
1185 def test_types(self):
1186 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001187 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001188 eq(self._msg.get_content_type(), 'text/plain')
1189 eq(self._msg.get_param('charset'), 'us-ascii')
1190 missing = []
1191 unless(self._msg.get_param('foobar', missing) is missing)
1192 unless(self._msg.get_param('charset', missing, header='foobar')
1193 is missing)
1194
1195 def test_payload(self):
1196 self.assertEqual(self._msg.get_payload(), 'hello there')
Georg Brandlab91fde2009-08-13 08:51:18 +00001197 self.assertTrue(not self._msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001198
1199 def test_charset(self):
1200 eq = self.assertEqual
1201 msg = MIMEText('hello there', _charset='us-ascii')
1202 eq(msg.get_charset().input_charset, 'us-ascii')
1203 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1204
R. David Murrayd2d08c62010-06-03 02:05:47 +00001205 def test_7bit_input(self):
1206 eq = self.assertEqual
1207 msg = MIMEText('hello there', _charset='us-ascii')
1208 eq(msg.get_charset().input_charset, 'us-ascii')
1209 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1210
1211 def test_7bit_input_no_charset(self):
1212 eq = self.assertEqual
1213 msg = MIMEText('hello there')
1214 eq(msg.get_charset(), 'us-ascii')
1215 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1216 self.assertTrue('hello there' in msg.as_string())
1217
1218 def test_utf8_input(self):
1219 teststr = '\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
1220 eq = self.assertEqual
1221 msg = MIMEText(teststr, _charset='utf-8')
1222 eq(msg.get_charset().output_charset, 'utf-8')
1223 eq(msg['content-type'], 'text/plain; charset="utf-8"')
1224 eq(msg.get_payload(decode=True), teststr.encode('utf-8'))
1225
1226 @unittest.skip("can't fix because of backward compat in email5, "
1227 "will fix in email6")
1228 def test_utf8_input_no_charset(self):
1229 teststr = '\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
1230 self.assertRaises(UnicodeEncodeError, MIMEText, teststr)
1231
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001232
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001233
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001234# Test complicated multipart/* messages
1235class TestMultipart(TestEmailBase):
1236 def setUp(self):
1237 with openfile('PyBanner048.gif', 'rb') as fp:
1238 data = fp.read()
1239 container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
1240 image = MIMEImage(data, name='dingusfish.gif')
1241 image.add_header('content-disposition', 'attachment',
1242 filename='dingusfish.gif')
1243 intro = MIMEText('''\
1244Hi there,
1245
1246This is the dingus fish.
1247''')
1248 container.attach(intro)
1249 container.attach(image)
1250 container['From'] = 'Barry <barry@digicool.com>'
1251 container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>'
1252 container['Subject'] = 'Here is your dingus fish'
1253
1254 now = 987809702.54848599
1255 timetuple = time.localtime(now)
1256 if timetuple[-1] == 0:
1257 tzsecs = time.timezone
1258 else:
1259 tzsecs = time.altzone
1260 if tzsecs > 0:
1261 sign = '-'
1262 else:
1263 sign = '+'
1264 tzoffset = ' %s%04d' % (sign, tzsecs / 36)
1265 container['Date'] = time.strftime(
1266 '%a, %d %b %Y %H:%M:%S',
1267 time.localtime(now)) + tzoffset
1268 self._msg = container
1269 self._im = image
1270 self._txt = intro
1271
1272 def test_hierarchy(self):
1273 # convenience
1274 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001275 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001276 raises = self.assertRaises
1277 # tests
1278 m = self._msg
1279 unless(m.is_multipart())
1280 eq(m.get_content_type(), 'multipart/mixed')
1281 eq(len(m.get_payload()), 2)
1282 raises(IndexError, m.get_payload, 2)
1283 m0 = m.get_payload(0)
1284 m1 = m.get_payload(1)
1285 unless(m0 is self._txt)
1286 unless(m1 is self._im)
1287 eq(m.get_payload(), [m0, m1])
1288 unless(not m0.is_multipart())
1289 unless(not m1.is_multipart())
1290
1291 def test_empty_multipart_idempotent(self):
1292 text = """\
1293Content-Type: multipart/mixed; boundary="BOUNDARY"
1294MIME-Version: 1.0
1295Subject: A subject
1296To: aperson@dom.ain
1297From: bperson@dom.ain
1298
1299
1300--BOUNDARY
1301
1302
1303--BOUNDARY--
1304"""
1305 msg = Parser().parsestr(text)
1306 self.ndiffAssertEqual(text, msg.as_string())
1307
1308 def test_no_parts_in_a_multipart_with_none_epilogue(self):
1309 outer = MIMEBase('multipart', 'mixed')
1310 outer['Subject'] = 'A subject'
1311 outer['To'] = 'aperson@dom.ain'
1312 outer['From'] = 'bperson@dom.ain'
1313 outer.set_boundary('BOUNDARY')
1314 self.ndiffAssertEqual(outer.as_string(), '''\
1315Content-Type: multipart/mixed; boundary="BOUNDARY"
1316MIME-Version: 1.0
1317Subject: A subject
1318To: aperson@dom.ain
1319From: bperson@dom.ain
1320
1321--BOUNDARY
1322
1323--BOUNDARY--''')
1324
1325 def test_no_parts_in_a_multipart_with_empty_epilogue(self):
1326 outer = MIMEBase('multipart', 'mixed')
1327 outer['Subject'] = 'A subject'
1328 outer['To'] = 'aperson@dom.ain'
1329 outer['From'] = 'bperson@dom.ain'
1330 outer.preamble = ''
1331 outer.epilogue = ''
1332 outer.set_boundary('BOUNDARY')
1333 self.ndiffAssertEqual(outer.as_string(), '''\
1334Content-Type: multipart/mixed; boundary="BOUNDARY"
1335MIME-Version: 1.0
1336Subject: A subject
1337To: aperson@dom.ain
1338From: bperson@dom.ain
1339
1340
1341--BOUNDARY
1342
1343--BOUNDARY--
1344''')
1345
1346 def test_one_part_in_a_multipart(self):
1347 eq = self.ndiffAssertEqual
1348 outer = MIMEBase('multipart', 'mixed')
1349 outer['Subject'] = 'A subject'
1350 outer['To'] = 'aperson@dom.ain'
1351 outer['From'] = 'bperson@dom.ain'
1352 outer.set_boundary('BOUNDARY')
1353 msg = MIMEText('hello world')
1354 outer.attach(msg)
1355 eq(outer.as_string(), '''\
1356Content-Type: multipart/mixed; boundary="BOUNDARY"
1357MIME-Version: 1.0
1358Subject: A subject
1359To: aperson@dom.ain
1360From: bperson@dom.ain
1361
1362--BOUNDARY
1363Content-Type: text/plain; charset="us-ascii"
1364MIME-Version: 1.0
1365Content-Transfer-Encoding: 7bit
1366
1367hello world
1368--BOUNDARY--''')
1369
1370 def test_seq_parts_in_a_multipart_with_empty_preamble(self):
1371 eq = self.ndiffAssertEqual
1372 outer = MIMEBase('multipart', 'mixed')
1373 outer['Subject'] = 'A subject'
1374 outer['To'] = 'aperson@dom.ain'
1375 outer['From'] = 'bperson@dom.ain'
1376 outer.preamble = ''
1377 msg = MIMEText('hello world')
1378 outer.attach(msg)
1379 outer.set_boundary('BOUNDARY')
1380 eq(outer.as_string(), '''\
1381Content-Type: multipart/mixed; boundary="BOUNDARY"
1382MIME-Version: 1.0
1383Subject: A subject
1384To: aperson@dom.ain
1385From: bperson@dom.ain
1386
1387
1388--BOUNDARY
1389Content-Type: text/plain; charset="us-ascii"
1390MIME-Version: 1.0
1391Content-Transfer-Encoding: 7bit
1392
1393hello world
1394--BOUNDARY--''')
1395
1396
1397 def test_seq_parts_in_a_multipart_with_none_preamble(self):
1398 eq = self.ndiffAssertEqual
1399 outer = MIMEBase('multipart', 'mixed')
1400 outer['Subject'] = 'A subject'
1401 outer['To'] = 'aperson@dom.ain'
1402 outer['From'] = 'bperson@dom.ain'
1403 outer.preamble = None
1404 msg = MIMEText('hello world')
1405 outer.attach(msg)
1406 outer.set_boundary('BOUNDARY')
1407 eq(outer.as_string(), '''\
1408Content-Type: multipart/mixed; boundary="BOUNDARY"
1409MIME-Version: 1.0
1410Subject: A subject
1411To: aperson@dom.ain
1412From: bperson@dom.ain
1413
1414--BOUNDARY
1415Content-Type: text/plain; charset="us-ascii"
1416MIME-Version: 1.0
1417Content-Transfer-Encoding: 7bit
1418
1419hello world
1420--BOUNDARY--''')
1421
1422
1423 def test_seq_parts_in_a_multipart_with_none_epilogue(self):
1424 eq = self.ndiffAssertEqual
1425 outer = MIMEBase('multipart', 'mixed')
1426 outer['Subject'] = 'A subject'
1427 outer['To'] = 'aperson@dom.ain'
1428 outer['From'] = 'bperson@dom.ain'
1429 outer.epilogue = None
1430 msg = MIMEText('hello world')
1431 outer.attach(msg)
1432 outer.set_boundary('BOUNDARY')
1433 eq(outer.as_string(), '''\
1434Content-Type: multipart/mixed; boundary="BOUNDARY"
1435MIME-Version: 1.0
1436Subject: A subject
1437To: aperson@dom.ain
1438From: bperson@dom.ain
1439
1440--BOUNDARY
1441Content-Type: text/plain; charset="us-ascii"
1442MIME-Version: 1.0
1443Content-Transfer-Encoding: 7bit
1444
1445hello world
1446--BOUNDARY--''')
1447
1448
1449 def test_seq_parts_in_a_multipart_with_empty_epilogue(self):
1450 eq = self.ndiffAssertEqual
1451 outer = MIMEBase('multipart', 'mixed')
1452 outer['Subject'] = 'A subject'
1453 outer['To'] = 'aperson@dom.ain'
1454 outer['From'] = 'bperson@dom.ain'
1455 outer.epilogue = ''
1456 msg = MIMEText('hello world')
1457 outer.attach(msg)
1458 outer.set_boundary('BOUNDARY')
1459 eq(outer.as_string(), '''\
1460Content-Type: multipart/mixed; boundary="BOUNDARY"
1461MIME-Version: 1.0
1462Subject: A subject
1463To: aperson@dom.ain
1464From: bperson@dom.ain
1465
1466--BOUNDARY
1467Content-Type: text/plain; charset="us-ascii"
1468MIME-Version: 1.0
1469Content-Transfer-Encoding: 7bit
1470
1471hello world
1472--BOUNDARY--
1473''')
1474
1475
1476 def test_seq_parts_in_a_multipart_with_nl_epilogue(self):
1477 eq = self.ndiffAssertEqual
1478 outer = MIMEBase('multipart', 'mixed')
1479 outer['Subject'] = 'A subject'
1480 outer['To'] = 'aperson@dom.ain'
1481 outer['From'] = 'bperson@dom.ain'
1482 outer.epilogue = '\n'
1483 msg = MIMEText('hello world')
1484 outer.attach(msg)
1485 outer.set_boundary('BOUNDARY')
1486 eq(outer.as_string(), '''\
1487Content-Type: multipart/mixed; boundary="BOUNDARY"
1488MIME-Version: 1.0
1489Subject: A subject
1490To: aperson@dom.ain
1491From: bperson@dom.ain
1492
1493--BOUNDARY
1494Content-Type: text/plain; charset="us-ascii"
1495MIME-Version: 1.0
1496Content-Transfer-Encoding: 7bit
1497
1498hello world
1499--BOUNDARY--
1500
1501''')
1502
1503 def test_message_external_body(self):
1504 eq = self.assertEqual
1505 msg = self._msgobj('msg_36.txt')
1506 eq(len(msg.get_payload()), 2)
1507 msg1 = msg.get_payload(1)
1508 eq(msg1.get_content_type(), 'multipart/alternative')
1509 eq(len(msg1.get_payload()), 2)
1510 for subpart in msg1.get_payload():
1511 eq(subpart.get_content_type(), 'message/external-body')
1512 eq(len(subpart.get_payload()), 1)
1513 subsubpart = subpart.get_payload(0)
1514 eq(subsubpart.get_content_type(), 'text/plain')
1515
1516 def test_double_boundary(self):
1517 # msg_37.txt is a multipart that contains two dash-boundary's in a
1518 # row. Our interpretation of RFC 2046 calls for ignoring the second
1519 # and subsequent boundaries.
1520 msg = self._msgobj('msg_37.txt')
1521 self.assertEqual(len(msg.get_payload()), 3)
1522
1523 def test_nested_inner_contains_outer_boundary(self):
1524 eq = self.ndiffAssertEqual
1525 # msg_38.txt has an inner part that contains outer boundaries. My
1526 # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say
1527 # these are illegal and should be interpreted as unterminated inner
1528 # parts.
1529 msg = self._msgobj('msg_38.txt')
1530 sfp = StringIO()
1531 iterators._structure(msg, sfp)
1532 eq(sfp.getvalue(), """\
1533multipart/mixed
1534 multipart/mixed
1535 multipart/alternative
1536 text/plain
1537 text/plain
1538 text/plain
1539 text/plain
1540""")
1541
1542 def test_nested_with_same_boundary(self):
1543 eq = self.ndiffAssertEqual
1544 # msg 39.txt is similarly evil in that it's got inner parts that use
1545 # the same boundary as outer parts. Again, I believe the way this is
1546 # parsed is closest to the spirit of RFC 2046
1547 msg = self._msgobj('msg_39.txt')
1548 sfp = StringIO()
1549 iterators._structure(msg, sfp)
1550 eq(sfp.getvalue(), """\
1551multipart/mixed
1552 multipart/mixed
1553 multipart/alternative
1554 application/octet-stream
1555 application/octet-stream
1556 text/plain
1557""")
1558
1559 def test_boundary_in_non_multipart(self):
1560 msg = self._msgobj('msg_40.txt')
1561 self.assertEqual(msg.as_string(), '''\
1562MIME-Version: 1.0
1563Content-Type: text/html; boundary="--961284236552522269"
1564
1565----961284236552522269
1566Content-Type: text/html;
1567Content-Transfer-Encoding: 7Bit
1568
1569<html></html>
1570
1571----961284236552522269--
1572''')
1573
1574 def test_boundary_with_leading_space(self):
1575 eq = self.assertEqual
1576 msg = email.message_from_string('''\
1577MIME-Version: 1.0
1578Content-Type: multipart/mixed; boundary=" XXXX"
1579
1580-- XXXX
1581Content-Type: text/plain
1582
1583
1584-- XXXX
1585Content-Type: text/plain
1586
1587-- XXXX--
1588''')
Georg Brandlab91fde2009-08-13 08:51:18 +00001589 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001590 eq(msg.get_boundary(), ' XXXX')
1591 eq(len(msg.get_payload()), 2)
1592
1593 def test_boundary_without_trailing_newline(self):
1594 m = Parser().parsestr("""\
1595Content-Type: multipart/mixed; boundary="===============0012394164=="
1596MIME-Version: 1.0
1597
1598--===============0012394164==
1599Content-Type: image/file1.jpg
1600MIME-Version: 1.0
1601Content-Transfer-Encoding: base64
1602
1603YXNkZg==
1604--===============0012394164==--""")
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001605 self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001606
1607
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001608
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001609# Test some badly formatted messages
1610class TestNonConformant(TestEmailBase):
1611 def test_parse_missing_minor_type(self):
1612 eq = self.assertEqual
1613 msg = self._msgobj('msg_14.txt')
1614 eq(msg.get_content_type(), 'text/plain')
1615 eq(msg.get_content_maintype(), 'text')
1616 eq(msg.get_content_subtype(), 'plain')
1617
1618 def test_same_boundary_inner_outer(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00001619 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001620 msg = self._msgobj('msg_15.txt')
1621 # XXX We can probably eventually do better
1622 inner = msg.get_payload(0)
1623 unless(hasattr(inner, 'defects'))
1624 self.assertEqual(len(inner.defects), 1)
1625 unless(isinstance(inner.defects[0],
1626 errors.StartBoundaryNotFoundDefect))
1627
1628 def test_multipart_no_boundary(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00001629 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001630 msg = self._msgobj('msg_25.txt')
1631 unless(isinstance(msg.get_payload(), str))
1632 self.assertEqual(len(msg.defects), 2)
1633 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1634 unless(isinstance(msg.defects[1],
1635 errors.MultipartInvariantViolationDefect))
1636
1637 def test_invalid_content_type(self):
1638 eq = self.assertEqual
1639 neq = self.ndiffAssertEqual
1640 msg = Message()
1641 # RFC 2045, $5.2 says invalid yields text/plain
1642 msg['Content-Type'] = 'text'
1643 eq(msg.get_content_maintype(), 'text')
1644 eq(msg.get_content_subtype(), 'plain')
1645 eq(msg.get_content_type(), 'text/plain')
1646 # Clear the old value and try something /really/ invalid
1647 del msg['content-type']
1648 msg['Content-Type'] = 'foo'
1649 eq(msg.get_content_maintype(), 'text')
1650 eq(msg.get_content_subtype(), 'plain')
1651 eq(msg.get_content_type(), 'text/plain')
1652 # Still, make sure that the message is idempotently generated
1653 s = StringIO()
1654 g = Generator(s)
1655 g.flatten(msg)
1656 neq(s.getvalue(), 'Content-Type: foo\n\n')
1657
1658 def test_no_start_boundary(self):
1659 eq = self.ndiffAssertEqual
1660 msg = self._msgobj('msg_31.txt')
1661 eq(msg.get_payload(), """\
1662--BOUNDARY
1663Content-Type: text/plain
1664
1665message 1
1666
1667--BOUNDARY
1668Content-Type: text/plain
1669
1670message 2
1671
1672--BOUNDARY--
1673""")
1674
1675 def test_no_separating_blank_line(self):
1676 eq = self.ndiffAssertEqual
1677 msg = self._msgobj('msg_35.txt')
1678 eq(msg.as_string(), """\
1679From: aperson@dom.ain
1680To: bperson@dom.ain
1681Subject: here's something interesting
1682
1683counter to RFC 2822, there's no separating newline here
1684""")
1685
1686 def test_lying_multipart(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00001687 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001688 msg = self._msgobj('msg_41.txt')
1689 unless(hasattr(msg, 'defects'))
1690 self.assertEqual(len(msg.defects), 2)
1691 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1692 unless(isinstance(msg.defects[1],
1693 errors.MultipartInvariantViolationDefect))
1694
1695 def test_missing_start_boundary(self):
1696 outer = self._msgobj('msg_42.txt')
1697 # The message structure is:
1698 #
1699 # multipart/mixed
1700 # text/plain
1701 # message/rfc822
1702 # multipart/mixed [*]
1703 #
1704 # [*] This message is missing its start boundary
1705 bad = outer.get_payload(1).get_payload(0)
1706 self.assertEqual(len(bad.defects), 1)
Georg Brandlab91fde2009-08-13 08:51:18 +00001707 self.assertTrue(isinstance(bad.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001708 errors.StartBoundaryNotFoundDefect))
1709
1710 def test_first_line_is_continuation_header(self):
1711 eq = self.assertEqual
1712 m = ' Line 1\nLine 2\nLine 3'
1713 msg = email.message_from_string(m)
1714 eq(msg.keys(), [])
1715 eq(msg.get_payload(), 'Line 2\nLine 3')
1716 eq(len(msg.defects), 1)
Georg Brandlab91fde2009-08-13 08:51:18 +00001717 self.assertTrue(isinstance(msg.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001718 errors.FirstHeaderLineIsContinuationDefect))
1719 eq(msg.defects[0].line, ' Line 1\n')
1720
1721
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001722
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001723# Test RFC 2047 header encoding and decoding
Guido van Rossum9604e662007-08-30 03:46:43 +00001724class TestRFC2047(TestEmailBase):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001725 def test_rfc2047_multiline(self):
1726 eq = self.assertEqual
1727 s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz
1728 foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?="""
1729 dh = decode_header(s)
1730 eq(dh, [
1731 (b'Re:', None),
1732 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland'),
1733 (b'baz foo bar', None),
1734 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland')])
1735 header = make_header(dh)
1736 eq(str(header),
1737 'Re: r\xe4ksm\xf6rg\xe5s baz foo bar r\xe4ksm\xf6rg\xe5s')
Barry Warsaw00b34222007-08-31 02:35:00 +00001738 self.ndiffAssertEqual(header.encode(maxlinelen=76), """\
Guido van Rossum9604e662007-08-30 03:46:43 +00001739Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar =?mac-iceland?q?r=8Aksm?=
1740 =?mac-iceland?q?=9Arg=8Cs?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001741
1742 def test_whitespace_eater_unicode(self):
1743 eq = self.assertEqual
1744 s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard@dom.ain>'
1745 dh = decode_header(s)
1746 eq(dh, [(b'Andr\xe9', 'iso-8859-1'),
1747 (b'Pirard <pirard@dom.ain>', None)])
1748 header = str(make_header(dh))
1749 eq(header, 'Andr\xe9 Pirard <pirard@dom.ain>')
1750
1751 def test_whitespace_eater_unicode_2(self):
1752 eq = self.assertEqual
1753 s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?='
1754 dh = decode_header(s)
1755 eq(dh, [(b'The', None), (b'quick brown fox', 'iso-8859-1'),
1756 (b'jumped over the', None), (b'lazy dog', 'iso-8859-1')])
1757 hu = str(make_header(dh))
1758 eq(hu, 'The quick brown fox jumped over the lazy dog')
1759
1760 def test_rfc2047_missing_whitespace(self):
1761 s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord'
1762 dh = decode_header(s)
1763 self.assertEqual(dh, [(s, None)])
1764
1765 def test_rfc2047_with_whitespace(self):
1766 s = 'Sm =?ISO-8859-1?B?9g==?= rg =?ISO-8859-1?B?5Q==?= sbord'
1767 dh = decode_header(s)
1768 self.assertEqual(dh, [(b'Sm', None), (b'\xf6', 'iso-8859-1'),
1769 (b'rg', None), (b'\xe5', 'iso-8859-1'),
1770 (b'sbord', None)])
1771
R. David Murraye06528c2010-08-03 23:35:44 +00001772 def test_rfc2047_B_bad_padding(self):
1773 s = '=?iso-8859-1?B?%s?='
1774 data = [ # only test complete bytes
1775 ('dm==', b'v'), ('dm=', b'v'), ('dm', b'v'),
1776 ('dmk=', b'vi'), ('dmk', b'vi')
1777 ]
1778 for q, a in data:
1779 dh = decode_header(s % q)
1780 self.assertEqual(dh, [(a, 'iso-8859-1')])
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001781
R. David Murrayf9c957f2010-10-01 15:45:48 +00001782 def test_rfc2047_Q_invalid_digits(self):
1783 # issue 10004.
1784 s = '=?iso-8659-1?Q?andr=e9=zz?='
1785 self.assertEqual(decode_header(s),
1786 [(b'andr\xe9=zz', 'iso-8659-1')])
1787
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00001788
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001789# Test the MIMEMessage class
1790class TestMIMEMessage(TestEmailBase):
1791 def setUp(self):
1792 with openfile('msg_11.txt') as fp:
1793 self._text = fp.read()
1794
1795 def test_type_error(self):
1796 self.assertRaises(TypeError, MIMEMessage, 'a plain string')
1797
1798 def test_valid_argument(self):
1799 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001800 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001801 subject = 'A sub-message'
1802 m = Message()
1803 m['Subject'] = subject
1804 r = MIMEMessage(m)
1805 eq(r.get_content_type(), 'message/rfc822')
1806 payload = r.get_payload()
1807 unless(isinstance(payload, list))
1808 eq(len(payload), 1)
1809 subpart = payload[0]
1810 unless(subpart is m)
1811 eq(subpart['subject'], subject)
1812
1813 def test_bad_multipart(self):
1814 eq = self.assertEqual
1815 msg1 = Message()
1816 msg1['Subject'] = 'subpart 1'
1817 msg2 = Message()
1818 msg2['Subject'] = 'subpart 2'
1819 r = MIMEMessage(msg1)
1820 self.assertRaises(errors.MultipartConversionError, r.attach, msg2)
1821
1822 def test_generate(self):
1823 # First craft the message to be encapsulated
1824 m = Message()
1825 m['Subject'] = 'An enclosed message'
1826 m.set_payload('Here is the body of the message.\n')
1827 r = MIMEMessage(m)
1828 r['Subject'] = 'The enclosing message'
1829 s = StringIO()
1830 g = Generator(s)
1831 g.flatten(r)
1832 self.assertEqual(s.getvalue(), """\
1833Content-Type: message/rfc822
1834MIME-Version: 1.0
1835Subject: The enclosing message
1836
1837Subject: An enclosed message
1838
1839Here is the body of the message.
1840""")
1841
1842 def test_parse_message_rfc822(self):
1843 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001844 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001845 msg = self._msgobj('msg_11.txt')
1846 eq(msg.get_content_type(), 'message/rfc822')
1847 payload = msg.get_payload()
1848 unless(isinstance(payload, list))
1849 eq(len(payload), 1)
1850 submsg = payload[0]
Georg Brandlab91fde2009-08-13 08:51:18 +00001851 self.assertTrue(isinstance(submsg, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001852 eq(submsg['subject'], 'An enclosed message')
1853 eq(submsg.get_payload(), 'Here is the body of the message.\n')
1854
1855 def test_dsn(self):
1856 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001857 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001858 # msg 16 is a Delivery Status Notification, see RFC 1894
1859 msg = self._msgobj('msg_16.txt')
1860 eq(msg.get_content_type(), 'multipart/report')
1861 unless(msg.is_multipart())
1862 eq(len(msg.get_payload()), 3)
1863 # Subpart 1 is a text/plain, human readable section
1864 subpart = msg.get_payload(0)
1865 eq(subpart.get_content_type(), 'text/plain')
1866 eq(subpart.get_payload(), """\
1867This report relates to a message you sent with the following header fields:
1868
1869 Message-id: <002001c144a6$8752e060$56104586@oxy.edu>
1870 Date: Sun, 23 Sep 2001 20:10:55 -0700
1871 From: "Ian T. Henry" <henryi@oxy.edu>
1872 To: SoCal Raves <scr@socal-raves.org>
1873 Subject: [scr] yeah for Ians!!
1874
1875Your message cannot be delivered to the following recipients:
1876
1877 Recipient address: jangel1@cougar.noc.ucla.edu
1878 Reason: recipient reached disk quota
1879
1880""")
1881 # Subpart 2 contains the machine parsable DSN information. It
1882 # consists of two blocks of headers, represented by two nested Message
1883 # objects.
1884 subpart = msg.get_payload(1)
1885 eq(subpart.get_content_type(), 'message/delivery-status')
1886 eq(len(subpart.get_payload()), 2)
1887 # message/delivery-status should treat each block as a bunch of
1888 # headers, i.e. a bunch of Message objects.
1889 dsn1 = subpart.get_payload(0)
1890 unless(isinstance(dsn1, Message))
1891 eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu')
1892 eq(dsn1.get_param('dns', header='reporting-mta'), '')
1893 # Try a missing one <wink>
1894 eq(dsn1.get_param('nsd', header='reporting-mta'), None)
1895 dsn2 = subpart.get_payload(1)
1896 unless(isinstance(dsn2, Message))
1897 eq(dsn2['action'], 'failed')
1898 eq(dsn2.get_params(header='original-recipient'),
1899 [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')])
1900 eq(dsn2.get_param('rfc822', header='final-recipient'), '')
1901 # Subpart 3 is the original message
1902 subpart = msg.get_payload(2)
1903 eq(subpart.get_content_type(), 'message/rfc822')
1904 payload = subpart.get_payload()
1905 unless(isinstance(payload, list))
1906 eq(len(payload), 1)
1907 subsubpart = payload[0]
1908 unless(isinstance(subsubpart, Message))
1909 eq(subsubpart.get_content_type(), 'text/plain')
1910 eq(subsubpart['message-id'],
1911 '<002001c144a6$8752e060$56104586@oxy.edu>')
1912
1913 def test_epilogue(self):
1914 eq = self.ndiffAssertEqual
1915 with openfile('msg_21.txt') as fp:
1916 text = fp.read()
1917 msg = Message()
1918 msg['From'] = 'aperson@dom.ain'
1919 msg['To'] = 'bperson@dom.ain'
1920 msg['Subject'] = 'Test'
1921 msg.preamble = 'MIME message'
1922 msg.epilogue = 'End of MIME message\n'
1923 msg1 = MIMEText('One')
1924 msg2 = MIMEText('Two')
1925 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1926 msg.attach(msg1)
1927 msg.attach(msg2)
1928 sfp = StringIO()
1929 g = Generator(sfp)
1930 g.flatten(msg)
1931 eq(sfp.getvalue(), text)
1932
1933 def test_no_nl_preamble(self):
1934 eq = self.ndiffAssertEqual
1935 msg = Message()
1936 msg['From'] = 'aperson@dom.ain'
1937 msg['To'] = 'bperson@dom.ain'
1938 msg['Subject'] = 'Test'
1939 msg.preamble = 'MIME message'
1940 msg.epilogue = ''
1941 msg1 = MIMEText('One')
1942 msg2 = MIMEText('Two')
1943 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1944 msg.attach(msg1)
1945 msg.attach(msg2)
1946 eq(msg.as_string(), """\
1947From: aperson@dom.ain
1948To: bperson@dom.ain
1949Subject: Test
1950Content-Type: multipart/mixed; boundary="BOUNDARY"
1951
1952MIME message
1953--BOUNDARY
1954Content-Type: text/plain; charset="us-ascii"
1955MIME-Version: 1.0
1956Content-Transfer-Encoding: 7bit
1957
1958One
1959--BOUNDARY
1960Content-Type: text/plain; charset="us-ascii"
1961MIME-Version: 1.0
1962Content-Transfer-Encoding: 7bit
1963
1964Two
1965--BOUNDARY--
1966""")
1967
1968 def test_default_type(self):
1969 eq = self.assertEqual
1970 with openfile('msg_30.txt') as fp:
1971 msg = email.message_from_file(fp)
1972 container1 = msg.get_payload(0)
1973 eq(container1.get_default_type(), 'message/rfc822')
1974 eq(container1.get_content_type(), 'message/rfc822')
1975 container2 = msg.get_payload(1)
1976 eq(container2.get_default_type(), 'message/rfc822')
1977 eq(container2.get_content_type(), 'message/rfc822')
1978 container1a = container1.get_payload(0)
1979 eq(container1a.get_default_type(), 'text/plain')
1980 eq(container1a.get_content_type(), 'text/plain')
1981 container2a = container2.get_payload(0)
1982 eq(container2a.get_default_type(), 'text/plain')
1983 eq(container2a.get_content_type(), 'text/plain')
1984
1985 def test_default_type_with_explicit_container_type(self):
1986 eq = self.assertEqual
1987 with openfile('msg_28.txt') as fp:
1988 msg = email.message_from_file(fp)
1989 container1 = msg.get_payload(0)
1990 eq(container1.get_default_type(), 'message/rfc822')
1991 eq(container1.get_content_type(), 'message/rfc822')
1992 container2 = msg.get_payload(1)
1993 eq(container2.get_default_type(), 'message/rfc822')
1994 eq(container2.get_content_type(), 'message/rfc822')
1995 container1a = container1.get_payload(0)
1996 eq(container1a.get_default_type(), 'text/plain')
1997 eq(container1a.get_content_type(), 'text/plain')
1998 container2a = container2.get_payload(0)
1999 eq(container2a.get_default_type(), 'text/plain')
2000 eq(container2a.get_content_type(), 'text/plain')
2001
2002 def test_default_type_non_parsed(self):
2003 eq = self.assertEqual
2004 neq = self.ndiffAssertEqual
2005 # Set up container
2006 container = MIMEMultipart('digest', 'BOUNDARY')
2007 container.epilogue = ''
2008 # Set up subparts
2009 subpart1a = MIMEText('message 1\n')
2010 subpart2a = MIMEText('message 2\n')
2011 subpart1 = MIMEMessage(subpart1a)
2012 subpart2 = MIMEMessage(subpart2a)
2013 container.attach(subpart1)
2014 container.attach(subpart2)
2015 eq(subpart1.get_content_type(), 'message/rfc822')
2016 eq(subpart1.get_default_type(), 'message/rfc822')
2017 eq(subpart2.get_content_type(), 'message/rfc822')
2018 eq(subpart2.get_default_type(), 'message/rfc822')
2019 neq(container.as_string(0), '''\
2020Content-Type: multipart/digest; boundary="BOUNDARY"
2021MIME-Version: 1.0
2022
2023--BOUNDARY
2024Content-Type: message/rfc822
2025MIME-Version: 1.0
2026
2027Content-Type: text/plain; charset="us-ascii"
2028MIME-Version: 1.0
2029Content-Transfer-Encoding: 7bit
2030
2031message 1
2032
2033--BOUNDARY
2034Content-Type: message/rfc822
2035MIME-Version: 1.0
2036
2037Content-Type: text/plain; charset="us-ascii"
2038MIME-Version: 1.0
2039Content-Transfer-Encoding: 7bit
2040
2041message 2
2042
2043--BOUNDARY--
2044''')
2045 del subpart1['content-type']
2046 del subpart1['mime-version']
2047 del subpart2['content-type']
2048 del subpart2['mime-version']
2049 eq(subpart1.get_content_type(), 'message/rfc822')
2050 eq(subpart1.get_default_type(), 'message/rfc822')
2051 eq(subpart2.get_content_type(), 'message/rfc822')
2052 eq(subpart2.get_default_type(), 'message/rfc822')
2053 neq(container.as_string(0), '''\
2054Content-Type: multipart/digest; boundary="BOUNDARY"
2055MIME-Version: 1.0
2056
2057--BOUNDARY
2058
2059Content-Type: text/plain; charset="us-ascii"
2060MIME-Version: 1.0
2061Content-Transfer-Encoding: 7bit
2062
2063message 1
2064
2065--BOUNDARY
2066
2067Content-Type: text/plain; charset="us-ascii"
2068MIME-Version: 1.0
2069Content-Transfer-Encoding: 7bit
2070
2071message 2
2072
2073--BOUNDARY--
2074''')
2075
2076 def test_mime_attachments_in_constructor(self):
2077 eq = self.assertEqual
2078 text1 = MIMEText('')
2079 text2 = MIMEText('')
2080 msg = MIMEMultipart(_subparts=(text1, text2))
2081 eq(len(msg.get_payload()), 2)
2082 eq(msg.get_payload(0), text1)
2083 eq(msg.get_payload(1), text2)
2084
Christian Heimes587c2bf2008-01-19 16:21:02 +00002085 def test_default_multipart_constructor(self):
2086 msg = MIMEMultipart()
2087 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002088
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002089
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002090# A general test of parser->model->generator idempotency. IOW, read a message
2091# in, parse it into a message object tree, then without touching the tree,
2092# regenerate the plain text. The original text and the transformed text
2093# should be identical. Note: that we ignore the Unix-From since that may
2094# contain a changed date.
2095class TestIdempotent(TestEmailBase):
2096 def _msgobj(self, filename):
2097 with openfile(filename) as fp:
2098 data = fp.read()
2099 msg = email.message_from_string(data)
2100 return msg, data
2101
2102 def _idempotent(self, msg, text):
2103 eq = self.ndiffAssertEqual
2104 s = StringIO()
2105 g = Generator(s, maxheaderlen=0)
2106 g.flatten(msg)
2107 eq(text, s.getvalue())
2108
2109 def test_parse_text_message(self):
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002110 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002111 msg, text = self._msgobj('msg_01.txt')
2112 eq(msg.get_content_type(), 'text/plain')
2113 eq(msg.get_content_maintype(), 'text')
2114 eq(msg.get_content_subtype(), 'plain')
2115 eq(msg.get_params()[1], ('charset', 'us-ascii'))
2116 eq(msg.get_param('charset'), 'us-ascii')
2117 eq(msg.preamble, None)
2118 eq(msg.epilogue, None)
2119 self._idempotent(msg, text)
2120
2121 def test_parse_untyped_message(self):
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002122 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002123 msg, text = self._msgobj('msg_03.txt')
2124 eq(msg.get_content_type(), 'text/plain')
2125 eq(msg.get_params(), None)
2126 eq(msg.get_param('charset'), None)
2127 self._idempotent(msg, text)
2128
2129 def test_simple_multipart(self):
2130 msg, text = self._msgobj('msg_04.txt')
2131 self._idempotent(msg, text)
2132
2133 def test_MIME_digest(self):
2134 msg, text = self._msgobj('msg_02.txt')
2135 self._idempotent(msg, text)
2136
2137 def test_long_header(self):
2138 msg, text = self._msgobj('msg_27.txt')
2139 self._idempotent(msg, text)
2140
2141 def test_MIME_digest_with_part_headers(self):
2142 msg, text = self._msgobj('msg_28.txt')
2143 self._idempotent(msg, text)
2144
2145 def test_mixed_with_image(self):
2146 msg, text = self._msgobj('msg_06.txt')
2147 self._idempotent(msg, text)
2148
2149 def test_multipart_report(self):
2150 msg, text = self._msgobj('msg_05.txt')
2151 self._idempotent(msg, text)
2152
2153 def test_dsn(self):
2154 msg, text = self._msgobj('msg_16.txt')
2155 self._idempotent(msg, text)
2156
2157 def test_preamble_epilogue(self):
2158 msg, text = self._msgobj('msg_21.txt')
2159 self._idempotent(msg, text)
2160
2161 def test_multipart_one_part(self):
2162 msg, text = self._msgobj('msg_23.txt')
2163 self._idempotent(msg, text)
2164
2165 def test_multipart_no_parts(self):
2166 msg, text = self._msgobj('msg_24.txt')
2167 self._idempotent(msg, text)
2168
2169 def test_no_start_boundary(self):
2170 msg, text = self._msgobj('msg_31.txt')
2171 self._idempotent(msg, text)
2172
2173 def test_rfc2231_charset(self):
2174 msg, text = self._msgobj('msg_32.txt')
2175 self._idempotent(msg, text)
2176
2177 def test_more_rfc2231_parameters(self):
2178 msg, text = self._msgobj('msg_33.txt')
2179 self._idempotent(msg, text)
2180
2181 def test_text_plain_in_a_multipart_digest(self):
2182 msg, text = self._msgobj('msg_34.txt')
2183 self._idempotent(msg, text)
2184
2185 def test_nested_multipart_mixeds(self):
2186 msg, text = self._msgobj('msg_12a.txt')
2187 self._idempotent(msg, text)
2188
2189 def test_message_external_body_idempotent(self):
2190 msg, text = self._msgobj('msg_36.txt')
2191 self._idempotent(msg, text)
2192
2193 def test_content_type(self):
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002194 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00002195 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002196 # Get a message object and reset the seek pointer for other tests
2197 msg, text = self._msgobj('msg_05.txt')
2198 eq(msg.get_content_type(), 'multipart/report')
2199 # Test the Content-Type: parameters
2200 params = {}
2201 for pk, pv in msg.get_params():
2202 params[pk] = pv
2203 eq(params['report-type'], 'delivery-status')
2204 eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
2205 eq(msg.preamble, 'This is a MIME-encapsulated message.\n')
2206 eq(msg.epilogue, '\n')
2207 eq(len(msg.get_payload()), 3)
2208 # Make sure the subparts are what we expect
2209 msg1 = msg.get_payload(0)
2210 eq(msg1.get_content_type(), 'text/plain')
2211 eq(msg1.get_payload(), 'Yadda yadda yadda\n')
2212 msg2 = msg.get_payload(1)
2213 eq(msg2.get_content_type(), 'text/plain')
2214 eq(msg2.get_payload(), 'Yadda yadda yadda\n')
2215 msg3 = msg.get_payload(2)
2216 eq(msg3.get_content_type(), 'message/rfc822')
Georg Brandlab91fde2009-08-13 08:51:18 +00002217 self.assertTrue(isinstance(msg3, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002218 payload = msg3.get_payload()
2219 unless(isinstance(payload, list))
2220 eq(len(payload), 1)
2221 msg4 = payload[0]
2222 unless(isinstance(msg4, Message))
2223 eq(msg4.get_payload(), 'Yadda yadda yadda\n')
2224
2225 def test_parser(self):
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002226 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00002227 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002228 msg, text = self._msgobj('msg_06.txt')
2229 # Check some of the outer headers
2230 eq(msg.get_content_type(), 'message/rfc822')
2231 # Make sure the payload is a list of exactly one sub-Message, and that
2232 # that submessage has a type of text/plain
2233 payload = msg.get_payload()
2234 unless(isinstance(payload, list))
2235 eq(len(payload), 1)
2236 msg1 = payload[0]
Georg Brandlab91fde2009-08-13 08:51:18 +00002237 self.assertTrue(isinstance(msg1, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002238 eq(msg1.get_content_type(), 'text/plain')
Georg Brandlab91fde2009-08-13 08:51:18 +00002239 self.assertTrue(isinstance(msg1.get_payload(), str))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002240 eq(msg1.get_payload(), '\n')
2241
2242
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002243
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002244# Test various other bits of the package's functionality
2245class TestMiscellaneous(TestEmailBase):
2246 def test_message_from_string(self):
2247 with openfile('msg_01.txt') as fp:
2248 text = fp.read()
2249 msg = email.message_from_string(text)
2250 s = StringIO()
2251 # Don't wrap/continue long headers since we're trying to test
2252 # idempotency.
2253 g = Generator(s, maxheaderlen=0)
2254 g.flatten(msg)
2255 self.assertEqual(text, s.getvalue())
2256
2257 def test_message_from_file(self):
2258 with openfile('msg_01.txt') as fp:
2259 text = fp.read()
2260 fp.seek(0)
2261 msg = email.message_from_file(fp)
2262 s = StringIO()
2263 # Don't wrap/continue long headers since we're trying to test
2264 # idempotency.
2265 g = Generator(s, maxheaderlen=0)
2266 g.flatten(msg)
2267 self.assertEqual(text, s.getvalue())
2268
2269 def test_message_from_string_with_class(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00002270 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002271 with openfile('msg_01.txt') as fp:
2272 text = fp.read()
2273
2274 # Create a subclass
2275 class MyMessage(Message):
2276 pass
2277
2278 msg = email.message_from_string(text, MyMessage)
2279 unless(isinstance(msg, MyMessage))
2280 # Try something more complicated
2281 with openfile('msg_02.txt') as fp:
2282 text = fp.read()
2283 msg = email.message_from_string(text, MyMessage)
2284 for subpart in msg.walk():
2285 unless(isinstance(subpart, MyMessage))
2286
2287 def test_message_from_file_with_class(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00002288 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002289 # Create a subclass
2290 class MyMessage(Message):
2291 pass
2292
2293 with openfile('msg_01.txt') as fp:
2294 msg = email.message_from_file(fp, MyMessage)
2295 unless(isinstance(msg, MyMessage))
2296 # Try something more complicated
2297 with openfile('msg_02.txt') as fp:
2298 msg = email.message_from_file(fp, MyMessage)
2299 for subpart in msg.walk():
2300 unless(isinstance(subpart, MyMessage))
2301
2302 def test__all__(self):
2303 module = __import__('email')
2304 # Can't use sorted() here due to Python 2.3 compatibility
2305 all = module.__all__[:]
2306 all.sort()
2307 self.assertEqual(all, [
2308 'base64mime', 'charset', 'encoders', 'errors', 'generator',
2309 'header', 'iterators', 'message', 'message_from_file',
2310 'message_from_string', 'mime', 'parser',
2311 'quoprimime', 'utils',
2312 ])
2313
2314 def test_formatdate(self):
2315 now = time.time()
2316 self.assertEqual(utils.parsedate(utils.formatdate(now))[:6],
2317 time.gmtime(now)[:6])
2318
2319 def test_formatdate_localtime(self):
2320 now = time.time()
2321 self.assertEqual(
2322 utils.parsedate(utils.formatdate(now, localtime=True))[:6],
2323 time.localtime(now)[:6])
2324
2325 def test_formatdate_usegmt(self):
2326 now = time.time()
2327 self.assertEqual(
2328 utils.formatdate(now, localtime=False),
2329 time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now)))
2330 self.assertEqual(
2331 utils.formatdate(now, localtime=False, usegmt=True),
2332 time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now)))
2333
2334 def test_parsedate_none(self):
2335 self.assertEqual(utils.parsedate(''), None)
2336
2337 def test_parsedate_compact(self):
2338 # The FWS after the comma is optional
2339 self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'),
2340 utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800'))
2341
2342 def test_parsedate_no_dayofweek(self):
2343 eq = self.assertEqual
2344 eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'),
2345 (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800))
2346
2347 def test_parsedate_compact_no_dayofweek(self):
2348 eq = self.assertEqual
2349 eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'),
2350 (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800))
2351
2352 def test_parsedate_acceptable_to_time_functions(self):
2353 eq = self.assertEqual
2354 timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800')
2355 t = int(time.mktime(timetup))
2356 eq(time.localtime(t)[:6], timetup[:6])
2357 eq(int(time.strftime('%Y', timetup)), 2003)
2358 timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800')
2359 t = int(time.mktime(timetup[:9]))
2360 eq(time.localtime(t)[:6], timetup[:6])
2361 eq(int(time.strftime('%Y', timetup[:9])), 2003)
2362
R. David Murray1061f182010-08-25 01:55:24 +00002363 def test_parsedate_y2k(self):
2364 """Test for parsing a date with a two-digit year.
2365
2366 Parsing a date with a two-digit year should return the correct
2367 four-digit year. RFC822 allows two-digit years, but RFC2822 (which
2368 obsoletes RFC822) requires four-digit years.
2369
2370 """
2371 self.assertEqual(utils.parsedate_tz('25 Feb 03 13:47:26 -0800'),
2372 utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'))
2373 self.assertEqual(utils.parsedate_tz('25 Feb 71 13:47:26 -0800'),
2374 utils.parsedate_tz('25 Feb 1971 13:47:26 -0800'))
2375
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002376 def test_parseaddr_empty(self):
2377 self.assertEqual(utils.parseaddr('<>'), ('', ''))
2378 self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '')
2379
2380 def test_noquote_dump(self):
2381 self.assertEqual(
2382 utils.formataddr(('A Silly Person', 'person@dom.ain')),
2383 'A Silly Person <person@dom.ain>')
2384
2385 def test_escape_dump(self):
2386 self.assertEqual(
2387 utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')),
2388 r'"A \(Very\) Silly Person" <person@dom.ain>')
2389 a = r'A \(Special\) Person'
2390 b = 'person@dom.ain'
2391 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2392
2393 def test_escape_backslashes(self):
2394 self.assertEqual(
2395 utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')),
2396 r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>')
2397 a = r'Arthur \Backslash\ Foobar'
2398 b = 'person@dom.ain'
2399 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2400
2401 def test_name_with_dot(self):
2402 x = 'John X. Doe <jxd@example.com>'
2403 y = '"John X. Doe" <jxd@example.com>'
2404 a, b = ('John X. Doe', 'jxd@example.com')
2405 self.assertEqual(utils.parseaddr(x), (a, b))
2406 self.assertEqual(utils.parseaddr(y), (a, b))
2407 # formataddr() quotes the name if there's a dot in it
2408 self.assertEqual(utils.formataddr((a, b)), y)
2409
R. David Murray7f8199a2010-10-02 16:04:44 +00002410 def test_parseaddr_preserves_quoted_pairs_in_addresses(self):
2411 # issue 10005. Note that in the third test the second pair of
2412 # backslashes is not actually a quoted pair because it is not inside a
2413 # comment or quoted string: the address being parsed has a quoted
2414 # string containing a quoted backslash, followed by 'example' and two
2415 # backslashes, followed by another quoted string containing a space and
2416 # the word 'example'. parseaddr copies those two backslashes
2417 # literally. Per rfc5322 this is not technically correct since a \ may
2418 # not appear in an address outside of a quoted string. It is probably
2419 # a sensible Postel interpretation, though.
2420 eq = self.assertEqual
2421 eq(utils.parseaddr('""example" example"@example.com'),
2422 ('', '""example" example"@example.com'))
2423 eq(utils.parseaddr('"\\"example\\" example"@example.com'),
2424 ('', '"\\"example\\" example"@example.com'))
2425 eq(utils.parseaddr('"\\\\"example\\\\" example"@example.com'),
2426 ('', '"\\\\"example\\\\" example"@example.com'))
2427
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002428 def test_multiline_from_comment(self):
2429 x = """\
2430Foo
2431\tBar <foo@example.com>"""
2432 self.assertEqual(utils.parseaddr(x), ('Foo Bar', 'foo@example.com'))
2433
2434 def test_quote_dump(self):
2435 self.assertEqual(
2436 utils.formataddr(('A Silly; Person', 'person@dom.ain')),
2437 r'"A Silly; Person" <person@dom.ain>')
2438
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002439 def test_charset_richcomparisons(self):
2440 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00002441 ne = self.assertNotEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002442 cset1 = Charset()
2443 cset2 = Charset()
2444 eq(cset1, 'us-ascii')
2445 eq(cset1, 'US-ASCII')
2446 eq(cset1, 'Us-AsCiI')
2447 eq('us-ascii', cset1)
2448 eq('US-ASCII', cset1)
2449 eq('Us-AsCiI', cset1)
2450 ne(cset1, 'usascii')
2451 ne(cset1, 'USASCII')
2452 ne(cset1, 'UsAsCiI')
2453 ne('usascii', cset1)
2454 ne('USASCII', cset1)
2455 ne('UsAsCiI', cset1)
2456 eq(cset1, cset2)
2457 eq(cset2, cset1)
2458
2459 def test_getaddresses(self):
2460 eq = self.assertEqual
2461 eq(utils.getaddresses(['aperson@dom.ain (Al Person)',
2462 'Bud Person <bperson@dom.ain>']),
2463 [('Al Person', 'aperson@dom.ain'),
2464 ('Bud Person', 'bperson@dom.ain')])
2465
2466 def test_getaddresses_nasty(self):
2467 eq = self.assertEqual
2468 eq(utils.getaddresses(['foo: ;']), [('', '')])
2469 eq(utils.getaddresses(
2470 ['[]*-- =~$']),
2471 [('', ''), ('', ''), ('', '*--')])
2472 eq(utils.getaddresses(
2473 ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
2474 [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
2475
2476 def test_getaddresses_embedded_comment(self):
2477 """Test proper handling of a nested comment"""
2478 eq = self.assertEqual
2479 addrs = utils.getaddresses(['User ((nested comment)) <foo@bar.com>'])
2480 eq(addrs[0][1], 'foo@bar.com')
2481
2482 def test_utils_quote_unquote(self):
2483 eq = self.assertEqual
2484 msg = Message()
2485 msg.add_header('content-disposition', 'attachment',
2486 filename='foo\\wacky"name')
2487 eq(msg.get_filename(), 'foo\\wacky"name')
2488
2489 def test_get_body_encoding_with_bogus_charset(self):
2490 charset = Charset('not a charset')
2491 self.assertEqual(charset.get_body_encoding(), 'base64')
2492
2493 def test_get_body_encoding_with_uppercase_charset(self):
2494 eq = self.assertEqual
2495 msg = Message()
2496 msg['Content-Type'] = 'text/plain; charset=UTF-8'
2497 eq(msg['content-type'], 'text/plain; charset=UTF-8')
2498 charsets = msg.get_charsets()
2499 eq(len(charsets), 1)
2500 eq(charsets[0], 'utf-8')
2501 charset = Charset(charsets[0])
2502 eq(charset.get_body_encoding(), 'base64')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002503 msg.set_payload(b'hello world', charset=charset)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002504 eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n')
2505 eq(msg.get_payload(decode=True), b'hello world')
2506 eq(msg['content-transfer-encoding'], 'base64')
2507 # Try another one
2508 msg = Message()
2509 msg['Content-Type'] = 'text/plain; charset="US-ASCII"'
2510 charsets = msg.get_charsets()
2511 eq(len(charsets), 1)
2512 eq(charsets[0], 'us-ascii')
2513 charset = Charset(charsets[0])
2514 eq(charset.get_body_encoding(), encoders.encode_7or8bit)
2515 msg.set_payload('hello world', charset=charset)
2516 eq(msg.get_payload(), 'hello world')
2517 eq(msg['content-transfer-encoding'], '7bit')
2518
2519 def test_charsets_case_insensitive(self):
2520 lc = Charset('us-ascii')
2521 uc = Charset('US-ASCII')
2522 self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding())
2523
2524 def test_partial_falls_inside_message_delivery_status(self):
2525 eq = self.ndiffAssertEqual
2526 # The Parser interface provides chunks of data to FeedParser in 8192
2527 # byte gulps. SF bug #1076485 found one of those chunks inside
2528 # message/delivery-status header block, which triggered an
2529 # unreadline() of NeedMoreData.
2530 msg = self._msgobj('msg_43.txt')
2531 sfp = StringIO()
2532 iterators._structure(msg, sfp)
2533 eq(sfp.getvalue(), """\
2534multipart/report
2535 text/plain
2536 message/delivery-status
2537 text/plain
2538 text/plain
2539 text/plain
2540 text/plain
2541 text/plain
2542 text/plain
2543 text/plain
2544 text/plain
2545 text/plain
2546 text/plain
2547 text/plain
2548 text/plain
2549 text/plain
2550 text/plain
2551 text/plain
2552 text/plain
2553 text/plain
2554 text/plain
2555 text/plain
2556 text/plain
2557 text/plain
2558 text/plain
2559 text/plain
2560 text/plain
2561 text/plain
2562 text/plain
2563 text/rfc822-headers
2564""")
2565
2566
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002567
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002568# Test the iterator/generators
2569class TestIterators(TestEmailBase):
2570 def test_body_line_iterator(self):
2571 eq = self.assertEqual
2572 neq = self.ndiffAssertEqual
2573 # First a simple non-multipart message
2574 msg = self._msgobj('msg_01.txt')
2575 it = iterators.body_line_iterator(msg)
2576 lines = list(it)
2577 eq(len(lines), 6)
2578 neq(EMPTYSTRING.join(lines), msg.get_payload())
2579 # Now a more complicated multipart
2580 msg = self._msgobj('msg_02.txt')
2581 it = iterators.body_line_iterator(msg)
2582 lines = list(it)
2583 eq(len(lines), 43)
2584 with openfile('msg_19.txt') as fp:
2585 neq(EMPTYSTRING.join(lines), fp.read())
2586
2587 def test_typed_subpart_iterator(self):
2588 eq = self.assertEqual
2589 msg = self._msgobj('msg_04.txt')
2590 it = iterators.typed_subpart_iterator(msg, 'text')
2591 lines = []
2592 subparts = 0
2593 for subpart in it:
2594 subparts += 1
2595 lines.append(subpart.get_payload())
2596 eq(subparts, 2)
2597 eq(EMPTYSTRING.join(lines), """\
2598a simple kind of mirror
2599to reflect upon our own
2600a simple kind of mirror
2601to reflect upon our own
2602""")
2603
2604 def test_typed_subpart_iterator_default_type(self):
2605 eq = self.assertEqual
2606 msg = self._msgobj('msg_03.txt')
2607 it = iterators.typed_subpart_iterator(msg, 'text', 'plain')
2608 lines = []
2609 subparts = 0
2610 for subpart in it:
2611 subparts += 1
2612 lines.append(subpart.get_payload())
2613 eq(subparts, 1)
2614 eq(EMPTYSTRING.join(lines), """\
2615
2616Hi,
2617
2618Do you like this message?
2619
2620-Me
2621""")
2622
R. David Murray6d4a06c2010-07-17 01:28:04 +00002623 def test_pushCR_LF(self):
2624 '''FeedParser BufferedSubFile.push() assumed it received complete
2625 line endings. A CR ending one push() followed by a LF starting
2626 the next push() added an empty line.
2627 '''
2628 imt = [
2629 ("a\r \n", 2),
2630 ("b", 0),
2631 ("c\n", 1),
2632 ("", 0),
2633 ("d\r\n", 1),
2634 ("e\r", 0),
2635 ("\nf", 1),
2636 ("\r\n", 1),
2637 ]
2638 from email.feedparser import BufferedSubFile, NeedMoreData
2639 bsf = BufferedSubFile()
2640 om = []
2641 nt = 0
2642 for il, n in imt:
2643 bsf.push(il)
2644 nt += n
2645 n1 = 0
2646 while True:
2647 ol = bsf.readline()
2648 if ol == NeedMoreData:
2649 break
2650 om.append(ol)
2651 n1 += 1
2652 self.assertTrue(n == n1)
2653 self.assertTrue(len(om) == nt)
2654 self.assertTrue(''.join([il for il, n in imt]) == ''.join(om))
2655
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002656
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002657
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002658class TestParsers(TestEmailBase):
2659 def test_header_parser(self):
2660 eq = self.assertEqual
2661 # Parse only the headers of a complex multipart MIME document
2662 with openfile('msg_02.txt') as fp:
2663 msg = HeaderParser().parse(fp)
2664 eq(msg['from'], 'ppp-request@zzz.org')
2665 eq(msg['to'], 'ppp@zzz.org')
2666 eq(msg.get_content_type(), 'multipart/mixed')
Georg Brandlab91fde2009-08-13 08:51:18 +00002667 self.assertFalse(msg.is_multipart())
2668 self.assertTrue(isinstance(msg.get_payload(), str))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002669
2670 def test_whitespace_continuation(self):
2671 eq = self.assertEqual
2672 # This message contains a line after the Subject: header that has only
2673 # whitespace, but it is not empty!
2674 msg = email.message_from_string("""\
2675From: aperson@dom.ain
2676To: bperson@dom.ain
2677Subject: the next line has a space on it
2678\x20
2679Date: Mon, 8 Apr 2002 15:09:19 -0400
2680Message-ID: spam
2681
2682Here's the message body
2683""")
2684 eq(msg['subject'], 'the next line has a space on it\n ')
2685 eq(msg['message-id'], 'spam')
2686 eq(msg.get_payload(), "Here's the message body\n")
2687
2688 def test_whitespace_continuation_last_header(self):
2689 eq = self.assertEqual
2690 # Like the previous test, but the subject line is the last
2691 # header.
2692 msg = email.message_from_string("""\
2693From: aperson@dom.ain
2694To: bperson@dom.ain
2695Date: Mon, 8 Apr 2002 15:09:19 -0400
2696Message-ID: spam
2697Subject: the next line has a space on it
2698\x20
2699
2700Here's the message body
2701""")
2702 eq(msg['subject'], 'the next line has a space on it\n ')
2703 eq(msg['message-id'], 'spam')
2704 eq(msg.get_payload(), "Here's the message body\n")
2705
2706 def test_crlf_separation(self):
2707 eq = self.assertEqual
Guido van Rossum98297ee2007-11-06 21:34:58 +00002708 with openfile('msg_26.txt', newline='\n') as fp:
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002709 msg = Parser().parse(fp)
2710 eq(len(msg.get_payload()), 2)
2711 part1 = msg.get_payload(0)
2712 eq(part1.get_content_type(), 'text/plain')
2713 eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n')
2714 part2 = msg.get_payload(1)
2715 eq(part2.get_content_type(), 'application/riscos')
2716
2717 def test_multipart_digest_with_extra_mime_headers(self):
2718 eq = self.assertEqual
2719 neq = self.ndiffAssertEqual
2720 with openfile('msg_28.txt') as fp:
2721 msg = email.message_from_file(fp)
2722 # Structure is:
2723 # multipart/digest
2724 # message/rfc822
2725 # text/plain
2726 # message/rfc822
2727 # text/plain
2728 eq(msg.is_multipart(), 1)
2729 eq(len(msg.get_payload()), 2)
2730 part1 = msg.get_payload(0)
2731 eq(part1.get_content_type(), 'message/rfc822')
2732 eq(part1.is_multipart(), 1)
2733 eq(len(part1.get_payload()), 1)
2734 part1a = part1.get_payload(0)
2735 eq(part1a.is_multipart(), 0)
2736 eq(part1a.get_content_type(), 'text/plain')
2737 neq(part1a.get_payload(), 'message 1\n')
2738 # next message/rfc822
2739 part2 = msg.get_payload(1)
2740 eq(part2.get_content_type(), 'message/rfc822')
2741 eq(part2.is_multipart(), 1)
2742 eq(len(part2.get_payload()), 1)
2743 part2a = part2.get_payload(0)
2744 eq(part2a.is_multipart(), 0)
2745 eq(part2a.get_content_type(), 'text/plain')
2746 neq(part2a.get_payload(), 'message 2\n')
2747
2748 def test_three_lines(self):
2749 # A bug report by Andrew McNamara
2750 lines = ['From: Andrew Person <aperson@dom.ain',
2751 'Subject: Test',
2752 'Date: Tue, 20 Aug 2002 16:43:45 +1000']
2753 msg = email.message_from_string(NL.join(lines))
2754 self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000')
2755
2756 def test_strip_line_feed_and_carriage_return_in_headers(self):
2757 eq = self.assertEqual
2758 # For [ 1002475 ] email message parser doesn't handle \r\n correctly
2759 value1 = 'text'
2760 value2 = 'more text'
2761 m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % (
2762 value1, value2)
2763 msg = email.message_from_string(m)
2764 eq(msg.get('Header'), value1)
2765 eq(msg.get('Next-Header'), value2)
2766
2767 def test_rfc2822_header_syntax(self):
2768 eq = self.assertEqual
2769 m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2770 msg = email.message_from_string(m)
2771 eq(len(msg), 3)
2772 eq(sorted(field for field in msg), ['!"#QUX;~', '>From', 'From'])
2773 eq(msg.get_payload(), 'body')
2774
2775 def test_rfc2822_space_not_allowed_in_header(self):
2776 eq = self.assertEqual
2777 m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2778 msg = email.message_from_string(m)
2779 eq(len(msg.keys()), 0)
2780
2781 def test_rfc2822_one_character_header(self):
2782 eq = self.assertEqual
2783 m = 'A: first header\nB: second header\nCC: third header\n\nbody'
2784 msg = email.message_from_string(m)
2785 headers = msg.keys()
2786 headers.sort()
2787 eq(headers, ['A', 'B', 'CC'])
2788 eq(msg.get_payload(), 'body')
2789
R. David Murray71df9d92010-06-16 02:22:56 +00002790 def test_CRLFLF_at_end_of_part(self):
2791 # issue 5610: feedparser should not eat two chars from body part ending
2792 # with "\r\n\n".
2793 m = (
2794 "From: foo@bar.com\n"
2795 "To: baz\n"
2796 "Mime-Version: 1.0\n"
2797 "Content-Type: multipart/mixed; boundary=BOUNDARY\n"
2798 "\n"
2799 "--BOUNDARY\n"
2800 "Content-Type: text/plain\n"
2801 "\n"
2802 "body ending with CRLF newline\r\n"
2803 "\n"
2804 "--BOUNDARY--\n"
2805 )
2806 msg = email.message_from_string(m)
2807 self.assertTrue(msg.get_payload(0).get_payload().endswith('\r\n'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002808
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002809
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002810class TestBase64(unittest.TestCase):
2811 def test_len(self):
2812 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002813 eq(base64mime.header_length('hello'),
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002814 len(base64mime.body_encode(b'hello', eol='')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002815 for size in range(15):
2816 if size == 0 : bsize = 0
2817 elif size <= 3 : bsize = 4
2818 elif size <= 6 : bsize = 8
2819 elif size <= 9 : bsize = 12
2820 elif size <= 12: bsize = 16
2821 else : bsize = 20
Guido van Rossum9604e662007-08-30 03:46:43 +00002822 eq(base64mime.header_length('x' * size), bsize)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002823
2824 def test_decode(self):
2825 eq = self.assertEqual
Barry Warsaw2cc1f6d2007-08-30 14:28:55 +00002826 eq(base64mime.decode(''), b'')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002827 eq(base64mime.decode('aGVsbG8='), b'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002828
2829 def test_encode(self):
2830 eq = self.assertEqual
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002831 eq(base64mime.body_encode(b''), b'')
2832 eq(base64mime.body_encode(b'hello'), 'aGVsbG8=\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002833 # Test the binary flag
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002834 eq(base64mime.body_encode(b'hello\n'), 'aGVsbG8K\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002835 # Test the maxlinelen arg
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002836 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002837eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2838eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2839eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2840eHh4eCB4eHh4IA==
2841""")
2842 # Test the eol argument
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002843 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40, eol='\r\n'),
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002844 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002845eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2846eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2847eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2848eHh4eCB4eHh4IA==\r
2849""")
2850
2851 def test_header_encode(self):
2852 eq = self.assertEqual
2853 he = base64mime.header_encode
2854 eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=')
Guido van Rossum9604e662007-08-30 03:46:43 +00002855 eq(he('hello\r\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=')
2856 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002857 # Test the charset option
2858 eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=')
2859 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002860
2861
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00002862
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002863class TestQuopri(unittest.TestCase):
2864 def setUp(self):
2865 # Set of characters (as byte integers) that don't need to be encoded
2866 # in headers.
2867 self.hlit = list(chain(
2868 range(ord('a'), ord('z') + 1),
2869 range(ord('A'), ord('Z') + 1),
2870 range(ord('0'), ord('9') + 1),
Guido van Rossum9604e662007-08-30 03:46:43 +00002871 (c for c in b'!*+-/')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002872 # Set of characters (as byte integers) that do need to be encoded in
2873 # headers.
2874 self.hnon = [c for c in range(256) if c not in self.hlit]
2875 assert len(self.hlit) + len(self.hnon) == 256
2876 # Set of characters (as byte integers) that don't need to be encoded
2877 # in bodies.
2878 self.blit = list(range(ord(' '), ord('~') + 1))
2879 self.blit.append(ord('\t'))
2880 self.blit.remove(ord('='))
2881 # Set of characters (as byte integers) that do need to be encoded in
2882 # bodies.
2883 self.bnon = [c for c in range(256) if c not in self.blit]
2884 assert len(self.blit) + len(self.bnon) == 256
2885
Guido van Rossum9604e662007-08-30 03:46:43 +00002886 def test_quopri_header_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002887 for c in self.hlit:
Georg Brandlab91fde2009-08-13 08:51:18 +00002888 self.assertFalse(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002889 'Should not be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002890 for c in self.hnon:
Georg Brandlab91fde2009-08-13 08:51:18 +00002891 self.assertTrue(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002892 'Should be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002893
Guido van Rossum9604e662007-08-30 03:46:43 +00002894 def test_quopri_body_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002895 for c in self.blit:
Georg Brandlab91fde2009-08-13 08:51:18 +00002896 self.assertFalse(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002897 'Should not be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002898 for c in self.bnon:
Georg Brandlab91fde2009-08-13 08:51:18 +00002899 self.assertTrue(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002900 'Should be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002901
2902 def test_header_quopri_len(self):
2903 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002904 eq(quoprimime.header_length(b'hello'), 5)
2905 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002906 eq(len(quoprimime.header_encode(b'hello', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00002907 quoprimime.header_length(b'hello') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002908 # =?xxx?q?...?= means 10 extra characters
2909 10)
Guido van Rossum9604e662007-08-30 03:46:43 +00002910 eq(quoprimime.header_length(b'h@e@l@l@o@'), 20)
2911 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002912 eq(len(quoprimime.header_encode(b'h@e@l@l@o@', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00002913 quoprimime.header_length(b'h@e@l@l@o@') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002914 # =?xxx?q?...?= means 10 extra characters
2915 10)
2916 for c in self.hlit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002917 eq(quoprimime.header_length(bytes([c])), 1,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002918 'expected length 1 for %r' % chr(c))
2919 for c in self.hnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002920 # Space is special; it's encoded to _
2921 if c == ord(' '):
2922 continue
2923 eq(quoprimime.header_length(bytes([c])), 3,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002924 'expected length 3 for %r' % chr(c))
Guido van Rossum9604e662007-08-30 03:46:43 +00002925 eq(quoprimime.header_length(b' '), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002926
2927 def test_body_quopri_len(self):
2928 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002929 for c in self.blit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002930 eq(quoprimime.body_length(bytes([c])), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002931 for c in self.bnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002932 eq(quoprimime.body_length(bytes([c])), 3)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002933
2934 def test_quote_unquote_idempotent(self):
2935 for x in range(256):
2936 c = chr(x)
2937 self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c)
2938
R David Murrayec1b5b82011-03-23 14:19:05 -04002939 def _test_header_encode(self, header, expected_encoded_header, charset=None):
2940 if charset is None:
2941 encoded_header = quoprimime.header_encode(header)
2942 else:
2943 encoded_header = quoprimime.header_encode(header, charset)
2944 self.assertEqual(encoded_header, expected_encoded_header)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002945
R David Murraycafd79d2011-03-23 15:25:55 -04002946 def test_header_encode_null(self):
2947 self._test_header_encode(b'', '')
2948
R David Murrayec1b5b82011-03-23 14:19:05 -04002949 def test_header_encode_one_word(self):
2950 self._test_header_encode(b'hello', '=?iso-8859-1?q?hello?=')
2951
2952 def test_header_encode_two_lines(self):
2953 self._test_header_encode(b'hello\nworld',
2954 '=?iso-8859-1?q?hello=0Aworld?=')
2955
2956 def test_header_encode_non_ascii(self):
2957 self._test_header_encode(b'hello\xc7there',
2958 '=?iso-8859-1?q?hello=C7there?=')
2959
2960 def test_header_encode_alt_charset(self):
2961 self._test_header_encode(b'hello', '=?iso-8859-2?q?hello?=',
2962 charset='iso-8859-2')
2963
2964 def _test_header_decode(self, encoded_header, expected_decoded_header):
2965 decoded_header = quoprimime.header_decode(encoded_header)
2966 self.assertEqual(decoded_header, expected_decoded_header)
2967
2968 def test_header_decode_null(self):
2969 self._test_header_decode('', '')
2970
2971 def test_header_decode_one_word(self):
2972 self._test_header_decode('hello', 'hello')
2973
2974 def test_header_decode_two_lines(self):
2975 self._test_header_decode('hello=0Aworld', 'hello\nworld')
2976
2977 def test_header_decode_non_ascii(self):
2978 self._test_header_decode('hello=C7there', 'hello\xc7there')
2979
2980 def _test_decode(self, encoded, expected_decoded, eol=None):
2981 if eol is None:
2982 decoded = quoprimime.decode(encoded)
2983 else:
2984 decoded = quoprimime.decode(encoded, eol=eol)
2985 self.assertEqual(decoded, expected_decoded)
2986
2987 def test_decode_null_word(self):
2988 self._test_decode('', '')
2989
2990 def test_decode_null_line_null_word(self):
2991 self._test_decode('\r\n', '\n')
2992
2993 def test_decode_one_word(self):
2994 self._test_decode('hello', 'hello')
2995
2996 def test_decode_one_word_eol(self):
2997 self._test_decode('hello', 'hello', eol='X')
2998
2999 def test_decode_one_line(self):
3000 self._test_decode('hello\r\n', 'hello\n')
3001
3002 def test_decode_one_line_lf(self):
3003 self._test_decode('hello\n', 'hello\n')
3004
R David Murraycafd79d2011-03-23 15:25:55 -04003005 def test_decode_one_line_cr(self):
3006 self._test_decode('hello\r', 'hello\n')
3007
3008 def test_decode_one_line_nl(self):
3009 self._test_decode('hello\n', 'helloX', eol='X')
3010
3011 def test_decode_one_line_crnl(self):
3012 self._test_decode('hello\r\n', 'helloX', eol='X')
3013
R David Murrayec1b5b82011-03-23 14:19:05 -04003014 def test_decode_one_line_one_word(self):
3015 self._test_decode('hello\r\nworld', 'hello\nworld')
3016
3017 def test_decode_one_line_one_word_eol(self):
3018 self._test_decode('hello\r\nworld', 'helloXworld', eol='X')
3019
3020 def test_decode_two_lines(self):
3021 self._test_decode('hello\r\nworld\r\n', 'hello\nworld\n')
3022
R David Murraycafd79d2011-03-23 15:25:55 -04003023 def test_decode_two_lines_eol(self):
3024 self._test_decode('hello\r\nworld\r\n', 'helloXworldX', eol='X')
3025
R David Murrayec1b5b82011-03-23 14:19:05 -04003026 def test_decode_one_long_line(self):
3027 self._test_decode('Spam' * 250, 'Spam' * 250)
3028
3029 def test_decode_one_space(self):
3030 self._test_decode(' ', '')
3031
3032 def test_decode_multiple_spaces(self):
3033 self._test_decode(' ' * 5, '')
3034
3035 def test_decode_one_line_trailing_spaces(self):
3036 self._test_decode('hello \r\n', 'hello\n')
3037
3038 def test_decode_two_lines_trailing_spaces(self):
3039 self._test_decode('hello \r\nworld \r\n', 'hello\nworld\n')
3040
3041 def test_decode_quoted_word(self):
3042 self._test_decode('=22quoted=20words=22', '"quoted words"')
3043
3044 def test_decode_uppercase_quoting(self):
3045 self._test_decode('ab=CD=EF', 'ab\xcd\xef')
3046
3047 def test_decode_lowercase_quoting(self):
3048 self._test_decode('ab=cd=ef', 'ab\xcd\xef')
3049
3050 def test_decode_soft_line_break(self):
3051 self._test_decode('soft line=\r\nbreak', 'soft linebreak')
3052
3053 def test_decode_false_quoting(self):
3054 self._test_decode('A=1,B=A ==> A+B==2', 'A=1,B=A ==> A+B==2')
3055
3056 def _test_encode(self, body, expected_encoded_body, maxlinelen=None, eol=None):
3057 kwargs = {}
3058 if maxlinelen is None:
3059 # Use body_encode's default.
3060 maxlinelen = 76
3061 else:
3062 kwargs['maxlinelen'] = maxlinelen
3063 if eol is None:
3064 # Use body_encode's default.
3065 eol = '\n'
3066 else:
3067 kwargs['eol'] = eol
3068 encoded_body = quoprimime.body_encode(body, **kwargs)
3069 self.assertEqual(encoded_body, expected_encoded_body)
3070 if eol == '\n' or eol == '\r\n':
3071 # We know how to split the result back into lines, so maxlinelen
3072 # can be checked.
3073 for line in encoded_body.splitlines():
3074 self.assertLessEqual(len(line), maxlinelen)
3075
3076 def test_encode_null(self):
3077 self._test_encode('', '')
3078
3079 def test_encode_null_lines(self):
3080 self._test_encode('\n\n', '\n\n')
3081
3082 def test_encode_one_line(self):
3083 self._test_encode('hello\n', 'hello\n')
3084
3085 def test_encode_one_line_crlf(self):
3086 self._test_encode('hello\r\n', 'hello\n')
3087
3088 def test_encode_one_line_eol(self):
3089 self._test_encode('hello\n', 'hello\r\n', eol='\r\n')
3090
3091 def test_encode_one_space(self):
3092 self._test_encode(' ', '=20')
3093
3094 def test_encode_one_line_one_space(self):
3095 self._test_encode(' \n', '=20\n')
3096
R David Murrayb938c8c2011-03-24 12:19:26 -04003097# XXX: body_encode() expect strings, but uses ord(char) from these strings
3098# to index into a 256-entry list. For code points above 255, this will fail.
3099# Should there be a check for 8-bit only ord() values in body, or at least
3100# a comment about the expected input?
3101
3102 def test_encode_two_lines_one_space(self):
3103 self._test_encode(' \n \n', '=20\n=20\n')
3104
R David Murrayec1b5b82011-03-23 14:19:05 -04003105 def test_encode_one_word_trailing_spaces(self):
3106 self._test_encode('hello ', 'hello =20')
3107
3108 def test_encode_one_line_trailing_spaces(self):
3109 self._test_encode('hello \n', 'hello =20\n')
3110
3111 def test_encode_one_word_trailing_tab(self):
3112 self._test_encode('hello \t', 'hello =09')
3113
3114 def test_encode_one_line_trailing_tab(self):
3115 self._test_encode('hello \t\n', 'hello =09\n')
3116
3117 def test_encode_trailing_space_before_maxlinelen(self):
3118 self._test_encode('abcd \n1234', 'abcd =\n\n1234', maxlinelen=6)
3119
R David Murrayb938c8c2011-03-24 12:19:26 -04003120 def test_encode_trailing_space_at_maxlinelen(self):
3121 self._test_encode('abcd \n1234', 'abcd=\n=20\n1234', maxlinelen=5)
3122
R David Murrayec1b5b82011-03-23 14:19:05 -04003123 def test_encode_trailing_space_beyond_maxlinelen(self):
R David Murrayb938c8c2011-03-24 12:19:26 -04003124 self._test_encode('abcd \n1234', 'abc=\nd=20\n1234', maxlinelen=4)
3125
3126 def test_encode_whitespace_lines(self):
3127 self._test_encode(' \n' * 5, '=20\n' * 5)
R David Murrayec1b5b82011-03-23 14:19:05 -04003128
3129 def test_encode_quoted_equals(self):
3130 self._test_encode('a = b', 'a =3D b')
3131
3132 def test_encode_one_long_string(self):
3133 self._test_encode('x' * 100, 'x' * 75 + '=\n' + 'x' * 25)
3134
3135 def test_encode_one_long_line(self):
3136 self._test_encode('x' * 100 + '\n', 'x' * 75 + '=\n' + 'x' * 25 + '\n')
3137
3138 def test_encode_one_very_long_line(self):
3139 self._test_encode('x' * 200 + '\n',
3140 2 * ('x' * 75 + '=\n') + 'x' * 50 + '\n')
3141
3142 def test_encode_one_long_line(self):
3143 self._test_encode('x' * 100 + '\n', 'x' * 75 + '=\n' + 'x' * 25 + '\n')
3144
3145 def test_encode_shortest_maxlinelen(self):
3146 self._test_encode('=' * 5, '=3D=\n' * 4 + '=3D', maxlinelen=4)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003147
R David Murrayb938c8c2011-03-24 12:19:26 -04003148 def test_encode_maxlinelen_too_small(self):
3149 self.assertRaises(ValueError, self._test_encode, '', '', maxlinelen=3)
3150
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003151 def test_encode(self):
3152 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00003153 eq(quoprimime.body_encode(''), '')
3154 eq(quoprimime.body_encode('hello'), 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003155 # Test the binary flag
Guido van Rossum9604e662007-08-30 03:46:43 +00003156 eq(quoprimime.body_encode('hello\r\nworld'), 'hello\nworld')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003157 # Test the maxlinelen arg
Guido van Rossum9604e662007-08-30 03:46:43 +00003158 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003159xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
3160 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
3161x xxxx xxxx xxxx xxxx=20""")
3162 # Test the eol argument
Guido van Rossum9604e662007-08-30 03:46:43 +00003163 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'),
3164 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003165xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
3166 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
3167x xxxx xxxx xxxx xxxx=20""")
Guido van Rossum9604e662007-08-30 03:46:43 +00003168 eq(quoprimime.body_encode("""\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003169one line
3170
3171two line"""), """\
3172one line
3173
3174two line""")
3175
3176
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00003177
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003178# Test the Charset class
3179class TestCharset(unittest.TestCase):
3180 def tearDown(self):
3181 from email import charset as CharsetModule
3182 try:
3183 del CharsetModule.CHARSETS['fake']
3184 except KeyError:
3185 pass
3186
Guido van Rossum9604e662007-08-30 03:46:43 +00003187 def test_codec_encodeable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003188 eq = self.assertEqual
3189 # Make sure us-ascii = no Unicode conversion
3190 c = Charset('us-ascii')
Guido van Rossum9604e662007-08-30 03:46:43 +00003191 eq(c.header_encode('Hello World!'), 'Hello World!')
3192 # Test 8-bit idempotency with us-ascii
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003193 s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
Guido van Rossum9604e662007-08-30 03:46:43 +00003194 self.assertRaises(UnicodeError, c.header_encode, s)
3195 c = Charset('utf-8')
3196 eq(c.header_encode(s), '=?utf-8?b?wqTCosKkwqTCpMKmwqTCqMKkwqo=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003197
3198 def test_body_encode(self):
3199 eq = self.assertEqual
3200 # Try a charset with QP body encoding
3201 c = Charset('iso-8859-1')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003202 eq('hello w=F6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003203 # Try a charset with Base64 body encoding
3204 c = Charset('utf-8')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003205 eq('aGVsbG8gd29ybGQ=\n', c.body_encode(b'hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003206 # Try a charset with None body encoding
3207 c = Charset('us-ascii')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003208 eq('hello world', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003209 # Try the convert argument, where input codec != output codec
3210 c = Charset('euc-jp')
3211 # With apologies to Tokio Kikuchi ;)
Barry Warsawbef9d212007-08-31 10:55:37 +00003212 # XXX FIXME
3213## try:
3214## eq('\x1b$B5FCO;~IW\x1b(B',
3215## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
3216## eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
3217## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
3218## except LookupError:
3219## # We probably don't have the Japanese codecs installed
3220## pass
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003221 # Testing SF bug #625509, which we have to fake, since there are no
3222 # built-in encodings where the header encoding is QP but the body
3223 # encoding is not.
3224 from email import charset as CharsetModule
3225 CharsetModule.add_charset('fake', CharsetModule.QP, None)
3226 c = Charset('fake')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003227 eq('hello w\xf6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003228
3229 def test_unicode_charset_name(self):
3230 charset = Charset('us-ascii')
3231 self.assertEqual(str(charset), 'us-ascii')
3232 self.assertRaises(errors.CharsetError, Charset, 'asc\xffii')
3233
3234
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00003235
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003236# Test multilingual MIME headers.
3237class TestHeader(TestEmailBase):
3238 def test_simple(self):
3239 eq = self.ndiffAssertEqual
3240 h = Header('Hello World!')
3241 eq(h.encode(), 'Hello World!')
3242 h.append(' Goodbye World!')
3243 eq(h.encode(), 'Hello World! Goodbye World!')
3244
3245 def test_simple_surprise(self):
3246 eq = self.ndiffAssertEqual
3247 h = Header('Hello World!')
3248 eq(h.encode(), 'Hello World!')
3249 h.append('Goodbye World!')
3250 eq(h.encode(), 'Hello World! Goodbye World!')
3251
3252 def test_header_needs_no_decoding(self):
3253 h = 'no decoding needed'
3254 self.assertEqual(decode_header(h), [(h, None)])
3255
3256 def test_long(self):
3257 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.",
3258 maxlinelen=76)
3259 for l in h.encode(splitchars=' ').split('\n '):
Georg Brandlab91fde2009-08-13 08:51:18 +00003260 self.assertTrue(len(l) <= 76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003261
3262 def test_multilingual(self):
3263 eq = self.ndiffAssertEqual
3264 g = Charset("iso-8859-1")
3265 cz = Charset("iso-8859-2")
3266 utf8 = Charset("utf-8")
3267 g_head = (b'Die Mieter treten hier ein werden mit einem '
3268 b'Foerderband komfortabel den Korridor entlang, '
3269 b'an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, '
3270 b'gegen die rotierenden Klingen bef\xf6rdert. ')
3271 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
3272 b'd\xf9vtipu.. ')
3273 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
3274 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
3275 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
3276 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
3277 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
3278 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
3279 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
3280 '\u3044\u307e\u3059\u3002')
3281 h = Header(g_head, g)
3282 h.append(cz_head, cz)
3283 h.append(utf8_head, utf8)
Guido van Rossum9604e662007-08-30 03:46:43 +00003284 enc = h.encode(maxlinelen=76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003285 eq(enc, """\
Guido van Rossum9604e662007-08-30 03:46:43 +00003286=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_kom?=
3287 =?iso-8859-1?q?fortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wand?=
3288 =?iso-8859-1?q?gem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6r?=
3289 =?iso-8859-1?q?dert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003290 =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
3291 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
3292 =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
3293 =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
Guido van Rossum9604e662007-08-30 03:46:43 +00003294 =?utf-8?b?IE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5k?=
3295 =?utf-8?b?IGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIA=?=
3296 =?utf-8?b?44Gj44Gm44GE44G+44GZ44CC?=""")
3297 decoded = decode_header(enc)
3298 eq(len(decoded), 3)
3299 eq(decoded[0], (g_head, 'iso-8859-1'))
3300 eq(decoded[1], (cz_head, 'iso-8859-2'))
3301 eq(decoded[2], (utf8_head.encode('utf-8'), 'utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003302 ustr = str(h)
Guido van Rossum9604e662007-08-30 03:46:43 +00003303 eq(ustr,
3304 (b'Die Mieter treten hier ein werden mit einem Foerderband '
3305 b'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
3306 b'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
3307 b'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
3308 b'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
3309 b'\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
3310 b'\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
3311 b'\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
3312 b'\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
3313 b'\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
3314 b'\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
3315 b'\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
3316 b'\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
3317 b'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
3318 b'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
3319 b'\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82'
3320 ).decode('utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003321 # Test make_header()
3322 newh = make_header(decode_header(enc))
Guido van Rossum9604e662007-08-30 03:46:43 +00003323 eq(newh, h)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003324
3325 def test_empty_header_encode(self):
3326 h = Header()
3327 self.assertEqual(h.encode(), '')
Barry Warsaw8b3d6592007-08-30 02:10:49 +00003328
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003329 def test_header_ctor_default_args(self):
3330 eq = self.ndiffAssertEqual
3331 h = Header()
3332 eq(h, '')
3333 h.append('foo', Charset('iso-8859-1'))
Guido van Rossum9604e662007-08-30 03:46:43 +00003334 eq(h, 'foo')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003335
3336 def test_explicit_maxlinelen(self):
3337 eq = self.ndiffAssertEqual
3338 hstr = ('A very long line that must get split to something other '
3339 'than at the 76th character boundary to test the non-default '
3340 'behavior')
3341 h = Header(hstr)
3342 eq(h.encode(), '''\
3343A very long line that must get split to something other than at the 76th
3344 character boundary to test the non-default behavior''')
3345 eq(str(h), hstr)
3346 h = Header(hstr, header_name='Subject')
3347 eq(h.encode(), '''\
3348A very long line that must get split to something other than at the
3349 76th character boundary to test the non-default behavior''')
3350 eq(str(h), hstr)
3351 h = Header(hstr, maxlinelen=1024, header_name='Subject')
3352 eq(h.encode(), hstr)
3353 eq(str(h), hstr)
3354
Guido van Rossum9604e662007-08-30 03:46:43 +00003355 def test_quopri_splittable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003356 eq = self.ndiffAssertEqual
3357 h = Header(charset='iso-8859-1', maxlinelen=20)
Guido van Rossum9604e662007-08-30 03:46:43 +00003358 x = 'xxxx ' * 20
3359 h.append(x)
3360 s = h.encode()
3361 eq(s, """\
3362=?iso-8859-1?q?xxx?=
3363 =?iso-8859-1?q?x_?=
3364 =?iso-8859-1?q?xx?=
3365 =?iso-8859-1?q?xx?=
3366 =?iso-8859-1?q?_x?=
3367 =?iso-8859-1?q?xx?=
3368 =?iso-8859-1?q?x_?=
3369 =?iso-8859-1?q?xx?=
3370 =?iso-8859-1?q?xx?=
3371 =?iso-8859-1?q?_x?=
3372 =?iso-8859-1?q?xx?=
3373 =?iso-8859-1?q?x_?=
3374 =?iso-8859-1?q?xx?=
3375 =?iso-8859-1?q?xx?=
3376 =?iso-8859-1?q?_x?=
3377 =?iso-8859-1?q?xx?=
3378 =?iso-8859-1?q?x_?=
3379 =?iso-8859-1?q?xx?=
3380 =?iso-8859-1?q?xx?=
3381 =?iso-8859-1?q?_x?=
3382 =?iso-8859-1?q?xx?=
3383 =?iso-8859-1?q?x_?=
3384 =?iso-8859-1?q?xx?=
3385 =?iso-8859-1?q?xx?=
3386 =?iso-8859-1?q?_x?=
3387 =?iso-8859-1?q?xx?=
3388 =?iso-8859-1?q?x_?=
3389 =?iso-8859-1?q?xx?=
3390 =?iso-8859-1?q?xx?=
3391 =?iso-8859-1?q?_x?=
3392 =?iso-8859-1?q?xx?=
3393 =?iso-8859-1?q?x_?=
3394 =?iso-8859-1?q?xx?=
3395 =?iso-8859-1?q?xx?=
3396 =?iso-8859-1?q?_x?=
3397 =?iso-8859-1?q?xx?=
3398 =?iso-8859-1?q?x_?=
3399 =?iso-8859-1?q?xx?=
3400 =?iso-8859-1?q?xx?=
3401 =?iso-8859-1?q?_x?=
3402 =?iso-8859-1?q?xx?=
3403 =?iso-8859-1?q?x_?=
3404 =?iso-8859-1?q?xx?=
3405 =?iso-8859-1?q?xx?=
3406 =?iso-8859-1?q?_x?=
3407 =?iso-8859-1?q?xx?=
3408 =?iso-8859-1?q?x_?=
3409 =?iso-8859-1?q?xx?=
3410 =?iso-8859-1?q?xx?=
3411 =?iso-8859-1?q?_?=""")
3412 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003413 h = Header(charset='iso-8859-1', maxlinelen=40)
3414 h.append('xxxx ' * 20)
Guido van Rossum9604e662007-08-30 03:46:43 +00003415 s = h.encode()
3416 eq(s, """\
3417=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xxx?=
3418 =?iso-8859-1?q?x_xxxx_xxxx_xxxx_xxxx_?=
3419 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
3420 =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
3421 =?iso-8859-1?q?_xxxx_xxxx_?=""")
3422 eq(x, str(make_header(decode_header(s))))
3423
3424 def test_base64_splittable(self):
3425 eq = self.ndiffAssertEqual
3426 h = Header(charset='koi8-r', maxlinelen=20)
3427 x = 'xxxx ' * 20
3428 h.append(x)
3429 s = h.encode()
3430 eq(s, """\
3431=?koi8-r?b?eHh4?=
3432 =?koi8-r?b?eCB4?=
3433 =?koi8-r?b?eHh4?=
3434 =?koi8-r?b?IHh4?=
3435 =?koi8-r?b?eHgg?=
3436 =?koi8-r?b?eHh4?=
3437 =?koi8-r?b?eCB4?=
3438 =?koi8-r?b?eHh4?=
3439 =?koi8-r?b?IHh4?=
3440 =?koi8-r?b?eHgg?=
3441 =?koi8-r?b?eHh4?=
3442 =?koi8-r?b?eCB4?=
3443 =?koi8-r?b?eHh4?=
3444 =?koi8-r?b?IHh4?=
3445 =?koi8-r?b?eHgg?=
3446 =?koi8-r?b?eHh4?=
3447 =?koi8-r?b?eCB4?=
3448 =?koi8-r?b?eHh4?=
3449 =?koi8-r?b?IHh4?=
3450 =?koi8-r?b?eHgg?=
3451 =?koi8-r?b?eHh4?=
3452 =?koi8-r?b?eCB4?=
3453 =?koi8-r?b?eHh4?=
3454 =?koi8-r?b?IHh4?=
3455 =?koi8-r?b?eHgg?=
3456 =?koi8-r?b?eHh4?=
3457 =?koi8-r?b?eCB4?=
3458 =?koi8-r?b?eHh4?=
3459 =?koi8-r?b?IHh4?=
3460 =?koi8-r?b?eHgg?=
3461 =?koi8-r?b?eHh4?=
3462 =?koi8-r?b?eCB4?=
3463 =?koi8-r?b?eHh4?=
3464 =?koi8-r?b?IA==?=""")
3465 eq(x, str(make_header(decode_header(s))))
3466 h = Header(charset='koi8-r', maxlinelen=40)
3467 h.append(x)
3468 s = h.encode()
3469 eq(s, """\
3470=?koi8-r?b?eHh4eCB4eHh4IHh4eHggeHh4?=
3471 =?koi8-r?b?eCB4eHh4IHh4eHggeHh4eCB4?=
3472 =?koi8-r?b?eHh4IHh4eHggeHh4eCB4eHh4?=
3473 =?koi8-r?b?IHh4eHggeHh4eCB4eHh4IHh4?=
3474 =?koi8-r?b?eHggeHh4eCB4eHh4IHh4eHgg?=
3475 =?koi8-r?b?eHh4eCB4eHh4IA==?=""")
3476 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003477
3478 def test_us_ascii_header(self):
3479 eq = self.assertEqual
3480 s = 'hello'
3481 x = decode_header(s)
3482 eq(x, [('hello', None)])
3483 h = make_header(x)
3484 eq(s, h.encode())
3485
3486 def test_string_charset(self):
3487 eq = self.assertEqual
3488 h = Header()
3489 h.append('hello', 'iso-8859-1')
Guido van Rossum9604e662007-08-30 03:46:43 +00003490 eq(h, 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003491
3492## def test_unicode_error(self):
3493## raises = self.assertRaises
3494## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')
3495## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')
3496## h = Header()
3497## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')
3498## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')
3499## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')
3500
3501 def test_utf8_shortest(self):
3502 eq = self.assertEqual
3503 h = Header('p\xf6stal', 'utf-8')
3504 eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
3505 h = Header('\u83ca\u5730\u6642\u592b', 'utf-8')
3506 eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
3507
3508 def test_bad_8bit_header(self):
3509 raises = self.assertRaises
3510 eq = self.assertEqual
3511 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
3512 raises(UnicodeError, Header, x)
3513 h = Header()
3514 raises(UnicodeError, h.append, x)
3515 e = x.decode('utf-8', 'replace')
3516 eq(str(Header(x, errors='replace')), e)
3517 h.append(x, errors='replace')
3518 eq(str(h), e)
3519
3520 def test_encoded_adjacent_nonencoded(self):
3521 eq = self.assertEqual
3522 h = Header()
3523 h.append('hello', 'iso-8859-1')
3524 h.append('world')
3525 s = h.encode()
3526 eq(s, '=?iso-8859-1?q?hello?= world')
3527 h = make_header(decode_header(s))
3528 eq(h.encode(), s)
3529
3530 def test_whitespace_eater(self):
3531 eq = self.assertEqual
3532 s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
3533 parts = decode_header(s)
3534 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)])
3535 hdr = make_header(parts)
3536 eq(hdr.encode(),
3537 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
3538
3539 def test_broken_base64_header(self):
3540 raises = self.assertRaises
R. David Murraye06528c2010-08-03 23:35:44 +00003541 s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3I ?='
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003542 raises(errors.HeaderParseError, decode_header, s)
3543
R. David Murrayf9844c82011-01-05 01:47:38 +00003544 def test_shift_jis_charset(self):
3545 h = Header('æ–‡', charset='shift_jis')
3546 self.assertEqual(h.encode(), '=?iso-2022-jp?b?GyRCSjgbKEI=?=')
3547
R David Murrayde912762011-03-16 18:26:23 -04003548 def test_flatten_header_with_no_value(self):
3549 # Issue 11401 (regression from email 4.x) Note that the space after
3550 # the header doesn't reflect the input, but this is also the way
3551 # email 4.x behaved. At some point it would be nice to fix that.
3552 msg = email.message_from_string("EmptyHeader:")
3553 self.assertEqual(str(msg), "EmptyHeader: \n\n")
3554
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003555
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00003556
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003557# Test RFC 2231 header parameters (en/de)coding
3558class TestRFC2231(TestEmailBase):
3559 def test_get_param(self):
3560 eq = self.assertEqual
3561 msg = self._msgobj('msg_29.txt')
3562 eq(msg.get_param('title'),
3563 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3564 eq(msg.get_param('title', unquote=False),
3565 ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
3566
3567 def test_set_param(self):
3568 eq = self.ndiffAssertEqual
3569 msg = Message()
3570 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3571 charset='us-ascii')
3572 eq(msg.get_param('title'),
3573 ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
3574 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3575 charset='us-ascii', language='en')
3576 eq(msg.get_param('title'),
3577 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3578 msg = self._msgobj('msg_01.txt')
3579 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3580 charset='us-ascii', language='en')
3581 eq(msg.as_string(maxheaderlen=78), """\
3582Return-Path: <bbb@zzz.org>
3583Delivered-To: bbb@zzz.org
3584Received: by mail.zzz.org (Postfix, from userid 889)
3585\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3586MIME-Version: 1.0
3587Content-Transfer-Encoding: 7bit
3588Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3589From: bbb@ddd.com (John X. Doe)
3590To: bbb@zzz.org
3591Subject: This is a test message
3592Date: Fri, 4 May 2001 14:05:44 -0400
3593Content-Type: text/plain; charset=us-ascii;
3594 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3595
3596
3597Hi,
3598
3599Do you like this message?
3600
3601-Me
3602""")
3603
3604 def test_del_param(self):
3605 eq = self.ndiffAssertEqual
3606 msg = self._msgobj('msg_01.txt')
3607 msg.set_param('foo', 'bar', charset='us-ascii', language='en')
3608 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3609 charset='us-ascii', language='en')
3610 msg.del_param('foo', header='Content-Type')
3611 eq(msg.as_string(maxheaderlen=78), """\
3612Return-Path: <bbb@zzz.org>
3613Delivered-To: bbb@zzz.org
3614Received: by mail.zzz.org (Postfix, from userid 889)
3615\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3616MIME-Version: 1.0
3617Content-Transfer-Encoding: 7bit
3618Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3619From: bbb@ddd.com (John X. Doe)
3620To: bbb@zzz.org
3621Subject: This is a test message
3622Date: Fri, 4 May 2001 14:05:44 -0400
3623Content-Type: text/plain; charset="us-ascii";
3624 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3625
3626
3627Hi,
3628
3629Do you like this message?
3630
3631-Me
3632""")
3633
3634 def test_rfc2231_get_content_charset(self):
3635 eq = self.assertEqual
3636 msg = self._msgobj('msg_32.txt')
3637 eq(msg.get_content_charset(), 'us-ascii')
3638
3639 def test_rfc2231_no_language_or_charset(self):
3640 m = '''\
3641Content-Transfer-Encoding: 8bit
3642Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
3643Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
3644
3645'''
3646 msg = email.message_from_string(m)
3647 param = msg.get_param('NAME')
Georg Brandlab91fde2009-08-13 08:51:18 +00003648 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003649 self.assertEqual(
3650 param,
3651 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
3652
3653 def test_rfc2231_no_language_or_charset_in_filename(self):
3654 m = '''\
3655Content-Disposition: inline;
3656\tfilename*0*="''This%20is%20even%20more%20";
3657\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3658\tfilename*2="is it not.pdf"
3659
3660'''
3661 msg = email.message_from_string(m)
3662 self.assertEqual(msg.get_filename(),
3663 'This is even more ***fun*** is it not.pdf')
3664
3665 def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
3666 m = '''\
3667Content-Disposition: inline;
3668\tfilename*0*="''This%20is%20even%20more%20";
3669\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3670\tfilename*2="is it not.pdf"
3671
3672'''
3673 msg = email.message_from_string(m)
3674 self.assertEqual(msg.get_filename(),
3675 'This is even more ***fun*** is it not.pdf')
3676
3677 def test_rfc2231_partly_encoded(self):
3678 m = '''\
3679Content-Disposition: inline;
3680\tfilename*0="''This%20is%20even%20more%20";
3681\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3682\tfilename*2="is it not.pdf"
3683
3684'''
3685 msg = email.message_from_string(m)
3686 self.assertEqual(
3687 msg.get_filename(),
3688 'This%20is%20even%20more%20***fun*** is it not.pdf')
3689
3690 def test_rfc2231_partly_nonencoded(self):
3691 m = '''\
3692Content-Disposition: inline;
3693\tfilename*0="This%20is%20even%20more%20";
3694\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
3695\tfilename*2="is it not.pdf"
3696
3697'''
3698 msg = email.message_from_string(m)
3699 self.assertEqual(
3700 msg.get_filename(),
3701 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
3702
3703 def test_rfc2231_no_language_or_charset_in_boundary(self):
3704 m = '''\
3705Content-Type: multipart/alternative;
3706\tboundary*0*="''This%20is%20even%20more%20";
3707\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
3708\tboundary*2="is it not.pdf"
3709
3710'''
3711 msg = email.message_from_string(m)
3712 self.assertEqual(msg.get_boundary(),
3713 'This is even more ***fun*** is it not.pdf')
3714
3715 def test_rfc2231_no_language_or_charset_in_charset(self):
3716 # This is a nonsensical charset value, but tests the code anyway
3717 m = '''\
3718Content-Type: text/plain;
3719\tcharset*0*="This%20is%20even%20more%20";
3720\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
3721\tcharset*2="is it not.pdf"
3722
3723'''
3724 msg = email.message_from_string(m)
3725 self.assertEqual(msg.get_content_charset(),
3726 'this is even more ***fun*** is it not.pdf')
3727
3728 def test_rfc2231_bad_encoding_in_filename(self):
3729 m = '''\
3730Content-Disposition: inline;
3731\tfilename*0*="bogus'xx'This%20is%20even%20more%20";
3732\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3733\tfilename*2="is it not.pdf"
3734
3735'''
3736 msg = email.message_from_string(m)
3737 self.assertEqual(msg.get_filename(),
3738 'This is even more ***fun*** is it not.pdf')
3739
3740 def test_rfc2231_bad_encoding_in_charset(self):
3741 m = """\
3742Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
3743
3744"""
3745 msg = email.message_from_string(m)
3746 # This should return None because non-ascii characters in the charset
3747 # are not allowed.
3748 self.assertEqual(msg.get_content_charset(), None)
3749
3750 def test_rfc2231_bad_character_in_charset(self):
3751 m = """\
3752Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D
3753
3754"""
3755 msg = email.message_from_string(m)
3756 # This should return None because non-ascii characters in the charset
3757 # are not allowed.
3758 self.assertEqual(msg.get_content_charset(), None)
3759
3760 def test_rfc2231_bad_character_in_filename(self):
3761 m = '''\
3762Content-Disposition: inline;
3763\tfilename*0*="ascii'xx'This%20is%20even%20more%20";
3764\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3765\tfilename*2*="is it not.pdf%E2"
3766
3767'''
3768 msg = email.message_from_string(m)
3769 self.assertEqual(msg.get_filename(),
3770 'This is even more ***fun*** is it not.pdf\ufffd')
3771
3772 def test_rfc2231_unknown_encoding(self):
3773 m = """\
3774Content-Transfer-Encoding: 8bit
3775Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
3776
3777"""
3778 msg = email.message_from_string(m)
3779 self.assertEqual(msg.get_filename(), 'myfile.txt')
3780
3781 def test_rfc2231_single_tick_in_filename_extended(self):
3782 eq = self.assertEqual
3783 m = """\
3784Content-Type: application/x-foo;
3785\tname*0*=\"Frank's\"; name*1*=\" Document\"
3786
3787"""
3788 msg = email.message_from_string(m)
3789 charset, language, s = msg.get_param('name')
3790 eq(charset, None)
3791 eq(language, None)
3792 eq(s, "Frank's Document")
3793
3794 def test_rfc2231_single_tick_in_filename(self):
3795 m = """\
3796Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
3797
3798"""
3799 msg = email.message_from_string(m)
3800 param = msg.get_param('name')
Georg Brandlab91fde2009-08-13 08:51:18 +00003801 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003802 self.assertEqual(param, "Frank's Document")
3803
3804 def test_rfc2231_tick_attack_extended(self):
3805 eq = self.assertEqual
3806 m = """\
3807Content-Type: application/x-foo;
3808\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
3809
3810"""
3811 msg = email.message_from_string(m)
3812 charset, language, s = msg.get_param('name')
3813 eq(charset, 'us-ascii')
3814 eq(language, 'en-us')
3815 eq(s, "Frank's Document")
3816
3817 def test_rfc2231_tick_attack(self):
3818 m = """\
3819Content-Type: application/x-foo;
3820\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
3821
3822"""
3823 msg = email.message_from_string(m)
3824 param = msg.get_param('name')
Georg Brandlab91fde2009-08-13 08:51:18 +00003825 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003826 self.assertEqual(param, "us-ascii'en-us'Frank's Document")
3827
3828 def test_rfc2231_no_extended_values(self):
3829 eq = self.assertEqual
3830 m = """\
3831Content-Type: application/x-foo; name=\"Frank's Document\"
3832
3833"""
3834 msg = email.message_from_string(m)
3835 eq(msg.get_param('name'), "Frank's Document")
3836
3837 def test_rfc2231_encoded_then_unencoded_segments(self):
3838 eq = self.assertEqual
3839 m = """\
3840Content-Type: application/x-foo;
3841\tname*0*=\"us-ascii'en-us'My\";
3842\tname*1=\" Document\";
3843\tname*2*=\" For You\"
3844
3845"""
3846 msg = email.message_from_string(m)
3847 charset, language, s = msg.get_param('name')
3848 eq(charset, 'us-ascii')
3849 eq(language, 'en-us')
3850 eq(s, 'My Document For You')
3851
3852 def test_rfc2231_unencoded_then_encoded_segments(self):
3853 eq = self.assertEqual
3854 m = """\
3855Content-Type: application/x-foo;
3856\tname*0=\"us-ascii'en-us'My\";
3857\tname*1*=\" Document\";
3858\tname*2*=\" For You\"
3859
3860"""
3861 msg = email.message_from_string(m)
3862 charset, language, s = msg.get_param('name')
3863 eq(charset, 'us-ascii')
3864 eq(language, 'en-us')
3865 eq(s, 'My Document For You')
3866
3867
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00003868
R. David Murrayfa606922010-01-16 18:41:00 +00003869# Tests to ensure that signed parts of an email are completely preserved, as
3870# required by RFC1847 section 2.1. Note that these are incomplete, because the
3871# email package does not currently always preserve the body. See issue 1670765.
3872class TestSigned(TestEmailBase):
3873
3874 def _msg_and_obj(self, filename):
3875 with openfile(findfile(filename)) as fp:
3876 original = fp.read()
3877 msg = email.message_from_string(original)
3878 return original, msg
3879
3880 def _signed_parts_eq(self, original, result):
3881 # Extract the first mime part of each message
3882 import re
3883 repart = re.compile(r'^--([^\n]+)\n(.*?)\n--\1$', re.S | re.M)
3884 inpart = repart.search(original).group(2)
3885 outpart = repart.search(result).group(2)
3886 self.assertEqual(outpart, inpart)
3887
3888 def test_long_headers_as_string(self):
3889 original, msg = self._msg_and_obj('msg_45.txt')
3890 result = msg.as_string()
3891 self._signed_parts_eq(original, result)
3892
3893 def test_long_headers_as_string_maxheaderlen(self):
3894 original, msg = self._msg_and_obj('msg_45.txt')
3895 result = msg.as_string(maxheaderlen=60)
3896 self._signed_parts_eq(original, result)
3897
3898 def test_long_headers_flatten(self):
3899 original, msg = self._msg_and_obj('msg_45.txt')
3900 fp = StringIO()
3901 Generator(fp).flatten(msg)
3902 result = fp.getvalue()
3903 self._signed_parts_eq(original, result)
3904
3905
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00003906
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003907def _testclasses():
3908 mod = sys.modules[__name__]
3909 return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
3910
3911
3912def suite():
3913 suite = unittest.TestSuite()
3914 for testclass in _testclasses():
3915 suite.addTest(unittest.makeSuite(testclass))
3916 return suite
3917
3918
3919def test_main():
3920 for testclass in _testclasses():
3921 run_unittest(testclass)
3922
3923
Ezio Melotti19f2aeb2010-11-21 01:30:29 +00003924
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003925if __name__ == '__main__':
3926 unittest.main(defaultTest='suite')