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