blob: 2ce2e8c1a7cae4e18489b891a4b2f3d0120b34e8 [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
519 msg = MIMEText('hello world')
520 eq(msg['content-transfer-encoding'], '7bit')
521
522 def test_default_cte(self):
523 eq = self.assertEqual
524 # With no explicit _charset its us-ascii, and all are 7-bit
525 msg = MIMEText('hello world')
526 eq(msg['content-transfer-encoding'], '7bit')
527 # Similar, but with 8-bit data
528 msg = MIMEText('hello \xf8 world')
529 eq(msg['content-transfer-encoding'], '8bit')
530 # And now with a different charset
531 msg = MIMEText('hello \xf8 world', _charset='iso-8859-1')
532 eq(msg['content-transfer-encoding'], 'quoted-printable')
533
534
535
536# Test long header wrapping
537class TestLongHeaders(TestEmailBase):
538 def test_split_long_continuation(self):
539 eq = self.ndiffAssertEqual
540 msg = email.message_from_string("""\
541Subject: bug demonstration
542\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
543\tmore text
544
545test
546""")
547 sfp = StringIO()
548 g = Generator(sfp)
549 g.flatten(msg)
550 eq(sfp.getvalue(), """\
551Subject: bug demonstration
552\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
553\tmore text
554
555test
556""")
557
558 def test_another_long_almost_unsplittable_header(self):
559 eq = self.ndiffAssertEqual
560 hstr = """\
561bug demonstration
562\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
563\tmore text"""
564 h = Header(hstr, continuation_ws='\t')
565 eq(h.encode(), """\
566bug demonstration
567\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
568\tmore text""")
569 h = Header(hstr.replace('\t', ' '))
570 eq(h.encode(), """\
571bug demonstration
572 12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
573 more text""")
574
575 def test_long_nonstring(self):
576 eq = self.ndiffAssertEqual
577 g = Charset("iso-8859-1")
578 cz = Charset("iso-8859-2")
579 utf8 = Charset("utf-8")
580 g_head = (b'Die Mieter treten hier ein werden mit einem Foerderband '
581 b'komfortabel den Korridor entlang, an s\xfcdl\xfcndischen '
582 b'Wandgem\xe4lden vorbei, gegen die rotierenden Klingen '
583 b'bef\xf6rdert. ')
584 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
585 b'd\xf9vtipu.. ')
586 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
587 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
588 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
589 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
590 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
591 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
592 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
593 '\u3044\u307e\u3059\u3002')
594 h = Header(g_head, g, header_name='Subject')
595 h.append(cz_head, cz)
596 h.append(utf8_head, utf8)
597 msg = Message()
598 msg['Subject'] = h
599 sfp = StringIO()
600 g = Generator(sfp)
601 g.flatten(msg)
602 eq(sfp.getvalue(), """\
Guido van Rossum9604e662007-08-30 03:46:43 +0000603Subject: =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderb?=
604 =?iso-8859-1?q?and_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen?=
605 =?iso-8859-1?q?_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef?=
606 =?iso-8859-1?q?=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hrouti?=
607 =?iso-8859-2?q?ly_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
608 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC5LiA?=
609 =?utf-8?b?6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn44Gf44KJ?=
610 =?utf-8?b?44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFzIE51bnN0dWNr?=
611 =?utf-8?b?IGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5kIGRhcyBPZGVyIGRp?=
612 =?utf-8?b?ZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIDjgaPjgabjgYTjgb7jgZk=?=
613 =?utf-8?b?44CC?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000614
615""")
Guido van Rossum9604e662007-08-30 03:46:43 +0000616 eq(h.encode(maxlinelen=76), """\
617=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerde?=
618 =?iso-8859-1?q?rband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndis?=
619 =?iso-8859-1?q?chen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klinge?=
620 =?iso-8859-1?q?n_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se?=
621 =?iso-8859-2?q?_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?=
622 =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb?=
623 =?utf-8?b?44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go?=
624 =?utf-8?b?44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBp?=
625 =?utf-8?b?c3QgZGFzIE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWlo?=
626 =?utf-8?b?ZXJodW5kIGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI0=?=
627 =?utf-8?b?44Go6KiA44Gj44Gm44GE44G+44GZ44CC?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000628
629 def test_long_header_encode(self):
630 eq = self.ndiffAssertEqual
631 h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
632 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
633 header_name='X-Foobar-Spoink-Defrobnit')
634 eq(h.encode(), '''\
635wasnipoop; giraffes="very-long-necked-animals";
636 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
637
638 def test_long_header_encode_with_tab_continuation_is_just_a_hint(self):
639 eq = self.ndiffAssertEqual
640 h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
641 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
642 header_name='X-Foobar-Spoink-Defrobnit',
643 continuation_ws='\t')
644 eq(h.encode(), '''\
645wasnipoop; giraffes="very-long-necked-animals";
646 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
647
648 def test_long_header_encode_with_tab_continuation(self):
649 eq = self.ndiffAssertEqual
650 h = Header('wasnipoop; giraffes="very-long-necked-animals";\t'
651 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
652 header_name='X-Foobar-Spoink-Defrobnit',
653 continuation_ws='\t')
654 eq(h.encode(), '''\
655wasnipoop; giraffes="very-long-necked-animals";
656\tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
657
658 def test_header_splitter(self):
659 eq = self.ndiffAssertEqual
660 msg = MIMEText('')
661 # It'd be great if we could use add_header() here, but that doesn't
662 # guarantee an order of the parameters.
663 msg['X-Foobar-Spoink-Defrobnit'] = (
664 'wasnipoop; giraffes="very-long-necked-animals"; '
665 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"')
666 sfp = StringIO()
667 g = Generator(sfp)
668 g.flatten(msg)
669 eq(sfp.getvalue(), '''\
670Content-Type: text/plain; charset="us-ascii"
671MIME-Version: 1.0
672Content-Transfer-Encoding: 7bit
673X-Foobar-Spoink-Defrobnit: wasnipoop; giraffes="very-long-necked-animals";
674 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"
675
676''')
677
678 def test_no_semis_header_splitter(self):
679 eq = self.ndiffAssertEqual
680 msg = Message()
681 msg['From'] = 'test@dom.ain'
682 msg['References'] = SPACE.join('<%d@dom.ain>' % i for i in range(10))
683 msg.set_payload('Test')
684 sfp = StringIO()
685 g = Generator(sfp)
686 g.flatten(msg)
687 eq(sfp.getvalue(), """\
688From: test@dom.ain
689References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain>
690 <5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain>
691
692Test""")
693
694 def test_no_split_long_header(self):
695 eq = self.ndiffAssertEqual
696 hstr = 'References: ' + 'x' * 80
Guido van Rossum9604e662007-08-30 03:46:43 +0000697 h = Header(hstr)
698 # These come on two lines because Headers are really field value
699 # classes and don't really know about their field names.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000700 eq(h.encode(), """\
Guido van Rossum9604e662007-08-30 03:46:43 +0000701References:
702 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""")
703 h = Header('x' * 80)
704 eq(h.encode(), 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000705
706 def test_splitting_multiple_long_lines(self):
707 eq = self.ndiffAssertEqual
708 hstr = """\
709from 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)
710\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)
711\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)
712"""
713 h = Header(hstr, continuation_ws='\t')
714 eq(h.encode(), """\
715from babylon.socal-raves.org (localhost [127.0.0.1]);
716 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
717 for <mailman-admin@babylon.socal-raves.org>;
718 Sat, 2 Feb 2002 17:00:06 -0800 (PST)
719\tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
720 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
721 for <mailman-admin@babylon.socal-raves.org>;
722 Sat, 2 Feb 2002 17:00:06 -0800 (PST)
723\tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
724 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
725 for <mailman-admin@babylon.socal-raves.org>;
726 Sat, 2 Feb 2002 17:00:06 -0800 (PST)""")
727
728 def test_splitting_first_line_only_is_long(self):
729 eq = self.ndiffAssertEqual
730 hstr = """\
731from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca)
732\tby kronos.mems-exchange.org with esmtp (Exim 4.05)
733\tid 17k4h5-00034i-00
734\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400"""
735 h = Header(hstr, maxlinelen=78, header_name='Received',
736 continuation_ws='\t')
737 eq(h.encode(), """\
738from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93]
739 helo=cthulhu.gerg.ca)
740\tby kronos.mems-exchange.org with esmtp (Exim 4.05)
741\tid 17k4h5-00034i-00
742\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""")
743
744 def test_long_8bit_header(self):
745 eq = self.ndiffAssertEqual
746 msg = Message()
747 h = Header('Britische Regierung gibt', 'iso-8859-1',
748 header_name='Subject')
749 h.append('gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte')
Guido van Rossum9604e662007-08-30 03:46:43 +0000750 eq(h.encode(maxlinelen=76), """\
751=?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offs?=
752 =?iso-8859-1?q?hore-Windkraftprojekte?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000753 msg['Subject'] = h
Guido van Rossum9604e662007-08-30 03:46:43 +0000754 eq(msg.as_string(maxheaderlen=76), """\
755Subject: =?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offs?=
756 =?iso-8859-1?q?hore-Windkraftprojekte?=
757
758""")
759 eq(msg.as_string(maxheaderlen=0), """\
760Subject: =?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offshore-Windkraftprojekte?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000761
762""")
763
764 def test_long_8bit_header_no_charset(self):
765 eq = self.ndiffAssertEqual
766 msg = Message()
Barry Warsaw8c571042007-08-30 19:17:18 +0000767 header_string = ('Britische Regierung gibt gr\xfcnes Licht '
768 'f\xfcr Offshore-Windkraftprojekte '
769 '<a-very-long-address@example.com>')
770 msg['Reply-To'] = header_string
771 self.assertRaises(UnicodeEncodeError, msg.as_string)
772 msg = Message()
773 msg['Reply-To'] = Header(header_string, 'utf-8',
774 header_name='Reply-To')
775 eq(msg.as_string(maxheaderlen=78), """\
776Reply-To: =?utf-8?q?Britische_Regierung_gibt_gr=C3=BCnes_Licht_f=C3=BCr_Offs?=
777 =?utf-8?q?hore-Windkraftprojekte_=3Ca-very-long-address=40example=2Ecom=3E?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000778
779""")
780
781 def test_long_to_header(self):
782 eq = self.ndiffAssertEqual
783 to = ('"Someone Test #A" <someone@eecs.umich.edu>,'
784 '<someone@eecs.umich.edu>,'
785 '"Someone Test #B" <someone@umich.edu>, '
786 '"Someone Test #C" <someone@eecs.umich.edu>, '
787 '"Someone Test #D" <someone@eecs.umich.edu>')
788 msg = Message()
789 msg['To'] = to
790 eq(msg.as_string(maxheaderlen=78), '''\
Guido van Rossum9604e662007-08-30 03:46:43 +0000791To: "Someone Test #A" <someone@eecs.umich.edu>,<someone@eecs.umich.edu>,
Barry Warsaw70d61ce2009-03-30 23:12:30 +0000792 "Someone Test #B" <someone@umich.edu>,
Guido van Rossum9604e662007-08-30 03:46:43 +0000793 "Someone Test #C" <someone@eecs.umich.edu>,
794 "Someone Test #D" <someone@eecs.umich.edu>
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000795
796''')
797
798 def test_long_line_after_append(self):
799 eq = self.ndiffAssertEqual
800 s = 'This is an example of string which has almost the limit of header length.'
801 h = Header(s)
802 h.append('Add another line.')
Guido van Rossum9604e662007-08-30 03:46:43 +0000803 eq(h.encode(maxlinelen=76), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000804This is an example of string which has almost the limit of header length.
805 Add another line.""")
806
807 def test_shorter_line_with_append(self):
808 eq = self.ndiffAssertEqual
809 s = 'This is a shorter line.'
810 h = Header(s)
811 h.append('Add another sentence. (Surprise?)')
812 eq(h.encode(),
813 'This is a shorter line. Add another sentence. (Surprise?)')
814
815 def test_long_field_name(self):
816 eq = self.ndiffAssertEqual
817 fn = 'X-Very-Very-Very-Long-Header-Name'
Guido van Rossum9604e662007-08-30 03:46:43 +0000818 gs = ('Die Mieter treten hier ein werden mit einem Foerderband '
819 'komfortabel den Korridor entlang, an s\xfcdl\xfcndischen '
820 'Wandgem\xe4lden vorbei, gegen die rotierenden Klingen '
821 'bef\xf6rdert. ')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000822 h = Header(gs, 'iso-8859-1', header_name=fn)
823 # BAW: this seems broken because the first line is too long
Guido van Rossum9604e662007-08-30 03:46:43 +0000824 eq(h.encode(maxlinelen=76), """\
825=?iso-8859-1?q?Die_Mieter_treten_hier_e?=
826 =?iso-8859-1?q?in_werden_mit_einem_Foerderband_komfortabel_den_Korridor_e?=
827 =?iso-8859-1?q?ntlang=2C_an_s=FCdl=FCndischen_Wandgem=E4lden_vorbei=2C_ge?=
828 =?iso-8859-1?q?gen_die_rotierenden_Klingen_bef=F6rdert=2E_?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000829
830 def test_long_received_header(self):
831 h = ('from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) '
832 'by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP; '
833 'Wed, 05 Mar 2003 18:10:18 -0700')
834 msg = Message()
835 msg['Received-1'] = Header(h, continuation_ws='\t')
836 msg['Received-2'] = h
Barry Warsawbef9d212007-08-31 10:55:37 +0000837 # This should be splitting on spaces not semicolons.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000838 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
Barry Warsawbef9d212007-08-31 10:55:37 +0000839Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
840 Wed, 05 Mar 2003 18:10:18 -0700
841Received-2: 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
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000843
844""")
845
846 def test_string_headerinst_eq(self):
847 h = ('<15975.17901.207240.414604@sgigritzmann1.mathematik.'
848 'tu-muenchen.de> (David Bremner\'s message of '
849 '"Thu, 6 Mar 2003 13:58:21 +0100")')
850 msg = Message()
851 msg['Received-1'] = Header(h, header_name='Received-1',
852 continuation_ws='\t')
853 msg['Received-2'] = h
Barry Warsawbef9d212007-08-31 10:55:37 +0000854 # XXX This should be splitting on spaces not commas.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000855 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
Barry Warsawbef9d212007-08-31 10:55:37 +0000856Received-1: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner's message of \"Thu,
857 6 Mar 2003 13:58:21 +0100\")
858Received-2: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner's message of \"Thu,
859 6 Mar 2003 13:58:21 +0100\")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000860
861""")
862
863 def test_long_unbreakable_lines_with_continuation(self):
864 eq = self.ndiffAssertEqual
865 msg = Message()
866 t = """\
867iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
868 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp"""
869 msg['Face-1'] = t
870 msg['Face-2'] = Header(t, header_name='Face-2')
Barry Warsawbef9d212007-08-31 10:55:37 +0000871 # XXX This splitting is all wrong. It the first value line should be
872 # snug against the field name.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000873 eq(msg.as_string(maxheaderlen=78), """\
Barry Warsawc5a6a302007-08-31 11:19:21 +0000874Face-1:\x20
Barry Warsaw70d61ce2009-03-30 23:12:30 +0000875 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000876 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
Barry Warsawc5a6a302007-08-31 11:19:21 +0000877Face-2:\x20
Barry Warsawbef9d212007-08-31 10:55:37 +0000878 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000879 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
880
881""")
882
883 def test_another_long_multiline_header(self):
884 eq = self.ndiffAssertEqual
885 m = ('Received: from siimage.com '
886 '([172.25.1.3]) by zima.siliconimage.com with '
Guido van Rossum9604e662007-08-30 03:46:43 +0000887 'Microsoft SMTPSVC(5.0.2195.4905); '
888 'Wed, 16 Oct 2002 07:41:11 -0700')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000889 msg = email.message_from_string(m)
890 eq(msg.as_string(maxheaderlen=78), '''\
Barry Warsawbef9d212007-08-31 10:55:37 +0000891Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with Microsoft SMTPSVC(5.0.2195.4905);
892 Wed, 16 Oct 2002 07:41:11 -0700
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000893
894''')
895
896 def test_long_lines_with_different_header(self):
897 eq = self.ndiffAssertEqual
898 h = ('List-Unsubscribe: '
899 '<http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,'
900 ' <mailto:spamassassin-talk-request@lists.sourceforge.net'
901 '?subject=unsubscribe>')
902 msg = Message()
903 msg['List'] = h
904 msg['List'] = Header(h, header_name='List')
905 eq(msg.as_string(maxheaderlen=78), """\
906List: List-Unsubscribe: <http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
Barry Warsawbef9d212007-08-31 10:55:37 +0000907 <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000908List: 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 +0000910
911""")
912
913
914
915# Test mangling of "From " lines in the body of a message
916class TestFromMangling(unittest.TestCase):
917 def setUp(self):
918 self.msg = Message()
919 self.msg['From'] = 'aaa@bbb.org'
920 self.msg.set_payload("""\
921From the desk of A.A.A.:
922Blah blah blah
923""")
924
925 def test_mangled_from(self):
926 s = StringIO()
927 g = Generator(s, mangle_from_=True)
928 g.flatten(self.msg)
929 self.assertEqual(s.getvalue(), """\
930From: aaa@bbb.org
931
932>From the desk of A.A.A.:
933Blah blah blah
934""")
935
936 def test_dont_mangle_from(self):
937 s = StringIO()
938 g = Generator(s, mangle_from_=False)
939 g.flatten(self.msg)
940 self.assertEqual(s.getvalue(), """\
941From: aaa@bbb.org
942
943From the desk of A.A.A.:
944Blah blah blah
945""")
946
947
948
949# Test the basic MIMEAudio class
950class TestMIMEAudio(unittest.TestCase):
951 def setUp(self):
952 # Make sure we pick up the audiotest.au that lives in email/test/data.
953 # In Python, there's an audiotest.au living in Lib/test but that isn't
954 # included in some binary distros that don't include the test
955 # package. The trailing empty string on the .join() is significant
956 # since findfile() will do a dirname().
957 datadir = os.path.join(os.path.dirname(landmark), 'data', '')
958 with open(findfile('audiotest.au', datadir), 'rb') as fp:
959 self._audiodata = fp.read()
960 self._au = MIMEAudio(self._audiodata)
961
962 def test_guess_minor_type(self):
963 self.assertEqual(self._au.get_content_type(), 'audio/basic')
964
965 def test_encoding(self):
966 payload = self._au.get_payload()
Georg Brandl706824f2009-06-04 09:42:55 +0000967 self.assertEqual(base64.decodebytes(payload), self._audiodata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000968
969 def test_checkSetMinor(self):
970 au = MIMEAudio(self._audiodata, 'fish')
971 self.assertEqual(au.get_content_type(), 'audio/fish')
972
973 def test_add_header(self):
974 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +0000975 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000976 self._au.add_header('Content-Disposition', 'attachment',
977 filename='audiotest.au')
978 eq(self._au['content-disposition'],
979 'attachment; filename="audiotest.au"')
980 eq(self._au.get_params(header='content-disposition'),
981 [('attachment', ''), ('filename', 'audiotest.au')])
982 eq(self._au.get_param('filename', header='content-disposition'),
983 'audiotest.au')
984 missing = []
985 eq(self._au.get_param('attachment', header='content-disposition'), '')
986 unless(self._au.get_param('foo', failobj=missing,
987 header='content-disposition') is missing)
988 # Try some missing stuff
989 unless(self._au.get_param('foobar', missing) is missing)
990 unless(self._au.get_param('attachment', missing,
991 header='foobar') is missing)
992
993
994
995# Test the basic MIMEImage class
996class TestMIMEImage(unittest.TestCase):
997 def setUp(self):
998 with openfile('PyBanner048.gif', 'rb') as fp:
999 self._imgdata = fp.read()
1000 self._im = MIMEImage(self._imgdata)
1001
1002 def test_guess_minor_type(self):
1003 self.assertEqual(self._im.get_content_type(), 'image/gif')
1004
1005 def test_encoding(self):
1006 payload = self._im.get_payload()
Georg Brandl706824f2009-06-04 09:42:55 +00001007 self.assertEqual(base64.decodebytes(payload), self._imgdata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001008
1009 def test_checkSetMinor(self):
1010 im = MIMEImage(self._imgdata, 'fish')
1011 self.assertEqual(im.get_content_type(), 'image/fish')
1012
1013 def test_add_header(self):
1014 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001015 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001016 self._im.add_header('Content-Disposition', 'attachment',
1017 filename='dingusfish.gif')
1018 eq(self._im['content-disposition'],
1019 'attachment; filename="dingusfish.gif"')
1020 eq(self._im.get_params(header='content-disposition'),
1021 [('attachment', ''), ('filename', 'dingusfish.gif')])
1022 eq(self._im.get_param('filename', header='content-disposition'),
1023 'dingusfish.gif')
1024 missing = []
1025 eq(self._im.get_param('attachment', header='content-disposition'), '')
1026 unless(self._im.get_param('foo', failobj=missing,
1027 header='content-disposition') is missing)
1028 # Try some missing stuff
1029 unless(self._im.get_param('foobar', missing) is missing)
1030 unless(self._im.get_param('attachment', missing,
1031 header='foobar') is missing)
1032
1033
1034
1035# Test the basic MIMEApplication class
1036class TestMIMEApplication(unittest.TestCase):
1037 def test_headers(self):
1038 eq = self.assertEqual
Barry Warsaw8b2af272007-08-31 03:04:26 +00001039 msg = MIMEApplication(b'\xfa\xfb\xfc\xfd\xfe\xff')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001040 eq(msg.get_content_type(), 'application/octet-stream')
1041 eq(msg['content-transfer-encoding'], 'base64')
1042
1043 def test_body(self):
1044 eq = self.assertEqual
Barry Warsaw8c571042007-08-30 19:17:18 +00001045 bytes = b'\xfa\xfb\xfc\xfd\xfe\xff'
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001046 msg = MIMEApplication(bytes)
Barry Warsaw8c571042007-08-30 19:17:18 +00001047 eq(msg.get_payload(), b'+vv8/f7/')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001048 eq(msg.get_payload(decode=True), bytes)
1049
1050
1051
1052# Test the basic MIMEText class
1053class TestMIMEText(unittest.TestCase):
1054 def setUp(self):
1055 self._msg = MIMEText('hello there')
1056
1057 def test_types(self):
1058 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001059 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001060 eq(self._msg.get_content_type(), 'text/plain')
1061 eq(self._msg.get_param('charset'), 'us-ascii')
1062 missing = []
1063 unless(self._msg.get_param('foobar', missing) is missing)
1064 unless(self._msg.get_param('charset', missing, header='foobar')
1065 is missing)
1066
1067 def test_payload(self):
1068 self.assertEqual(self._msg.get_payload(), 'hello there')
Georg Brandlab91fde2009-08-13 08:51:18 +00001069 self.assertTrue(not self._msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001070
1071 def test_charset(self):
1072 eq = self.assertEqual
1073 msg = MIMEText('hello there', _charset='us-ascii')
1074 eq(msg.get_charset().input_charset, 'us-ascii')
1075 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1076
1077
1078
1079# Test complicated multipart/* messages
1080class TestMultipart(TestEmailBase):
1081 def setUp(self):
1082 with openfile('PyBanner048.gif', 'rb') as fp:
1083 data = fp.read()
1084 container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
1085 image = MIMEImage(data, name='dingusfish.gif')
1086 image.add_header('content-disposition', 'attachment',
1087 filename='dingusfish.gif')
1088 intro = MIMEText('''\
1089Hi there,
1090
1091This is the dingus fish.
1092''')
1093 container.attach(intro)
1094 container.attach(image)
1095 container['From'] = 'Barry <barry@digicool.com>'
1096 container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>'
1097 container['Subject'] = 'Here is your dingus fish'
1098
1099 now = 987809702.54848599
1100 timetuple = time.localtime(now)
1101 if timetuple[-1] == 0:
1102 tzsecs = time.timezone
1103 else:
1104 tzsecs = time.altzone
1105 if tzsecs > 0:
1106 sign = '-'
1107 else:
1108 sign = '+'
1109 tzoffset = ' %s%04d' % (sign, tzsecs / 36)
1110 container['Date'] = time.strftime(
1111 '%a, %d %b %Y %H:%M:%S',
1112 time.localtime(now)) + tzoffset
1113 self._msg = container
1114 self._im = image
1115 self._txt = intro
1116
1117 def test_hierarchy(self):
1118 # convenience
1119 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001120 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001121 raises = self.assertRaises
1122 # tests
1123 m = self._msg
1124 unless(m.is_multipart())
1125 eq(m.get_content_type(), 'multipart/mixed')
1126 eq(len(m.get_payload()), 2)
1127 raises(IndexError, m.get_payload, 2)
1128 m0 = m.get_payload(0)
1129 m1 = m.get_payload(1)
1130 unless(m0 is self._txt)
1131 unless(m1 is self._im)
1132 eq(m.get_payload(), [m0, m1])
1133 unless(not m0.is_multipart())
1134 unless(not m1.is_multipart())
1135
1136 def test_empty_multipart_idempotent(self):
1137 text = """\
1138Content-Type: multipart/mixed; boundary="BOUNDARY"
1139MIME-Version: 1.0
1140Subject: A subject
1141To: aperson@dom.ain
1142From: bperson@dom.ain
1143
1144
1145--BOUNDARY
1146
1147
1148--BOUNDARY--
1149"""
1150 msg = Parser().parsestr(text)
1151 self.ndiffAssertEqual(text, msg.as_string())
1152
1153 def test_no_parts_in_a_multipart_with_none_epilogue(self):
1154 outer = MIMEBase('multipart', 'mixed')
1155 outer['Subject'] = 'A subject'
1156 outer['To'] = 'aperson@dom.ain'
1157 outer['From'] = 'bperson@dom.ain'
1158 outer.set_boundary('BOUNDARY')
1159 self.ndiffAssertEqual(outer.as_string(), '''\
1160Content-Type: multipart/mixed; boundary="BOUNDARY"
1161MIME-Version: 1.0
1162Subject: A subject
1163To: aperson@dom.ain
1164From: bperson@dom.ain
1165
1166--BOUNDARY
1167
1168--BOUNDARY--''')
1169
1170 def test_no_parts_in_a_multipart_with_empty_epilogue(self):
1171 outer = MIMEBase('multipart', 'mixed')
1172 outer['Subject'] = 'A subject'
1173 outer['To'] = 'aperson@dom.ain'
1174 outer['From'] = 'bperson@dom.ain'
1175 outer.preamble = ''
1176 outer.epilogue = ''
1177 outer.set_boundary('BOUNDARY')
1178 self.ndiffAssertEqual(outer.as_string(), '''\
1179Content-Type: multipart/mixed; boundary="BOUNDARY"
1180MIME-Version: 1.0
1181Subject: A subject
1182To: aperson@dom.ain
1183From: bperson@dom.ain
1184
1185
1186--BOUNDARY
1187
1188--BOUNDARY--
1189''')
1190
1191 def test_one_part_in_a_multipart(self):
1192 eq = self.ndiffAssertEqual
1193 outer = MIMEBase('multipart', 'mixed')
1194 outer['Subject'] = 'A subject'
1195 outer['To'] = 'aperson@dom.ain'
1196 outer['From'] = 'bperson@dom.ain'
1197 outer.set_boundary('BOUNDARY')
1198 msg = MIMEText('hello world')
1199 outer.attach(msg)
1200 eq(outer.as_string(), '''\
1201Content-Type: multipart/mixed; boundary="BOUNDARY"
1202MIME-Version: 1.0
1203Subject: A subject
1204To: aperson@dom.ain
1205From: bperson@dom.ain
1206
1207--BOUNDARY
1208Content-Type: text/plain; charset="us-ascii"
1209MIME-Version: 1.0
1210Content-Transfer-Encoding: 7bit
1211
1212hello world
1213--BOUNDARY--''')
1214
1215 def test_seq_parts_in_a_multipart_with_empty_preamble(self):
1216 eq = self.ndiffAssertEqual
1217 outer = MIMEBase('multipart', 'mixed')
1218 outer['Subject'] = 'A subject'
1219 outer['To'] = 'aperson@dom.ain'
1220 outer['From'] = 'bperson@dom.ain'
1221 outer.preamble = ''
1222 msg = MIMEText('hello world')
1223 outer.attach(msg)
1224 outer.set_boundary('BOUNDARY')
1225 eq(outer.as_string(), '''\
1226Content-Type: multipart/mixed; boundary="BOUNDARY"
1227MIME-Version: 1.0
1228Subject: A subject
1229To: aperson@dom.ain
1230From: bperson@dom.ain
1231
1232
1233--BOUNDARY
1234Content-Type: text/plain; charset="us-ascii"
1235MIME-Version: 1.0
1236Content-Transfer-Encoding: 7bit
1237
1238hello world
1239--BOUNDARY--''')
1240
1241
1242 def test_seq_parts_in_a_multipart_with_none_preamble(self):
1243 eq = self.ndiffAssertEqual
1244 outer = MIMEBase('multipart', 'mixed')
1245 outer['Subject'] = 'A subject'
1246 outer['To'] = 'aperson@dom.ain'
1247 outer['From'] = 'bperson@dom.ain'
1248 outer.preamble = None
1249 msg = MIMEText('hello world')
1250 outer.attach(msg)
1251 outer.set_boundary('BOUNDARY')
1252 eq(outer.as_string(), '''\
1253Content-Type: multipart/mixed; boundary="BOUNDARY"
1254MIME-Version: 1.0
1255Subject: A subject
1256To: aperson@dom.ain
1257From: bperson@dom.ain
1258
1259--BOUNDARY
1260Content-Type: text/plain; charset="us-ascii"
1261MIME-Version: 1.0
1262Content-Transfer-Encoding: 7bit
1263
1264hello world
1265--BOUNDARY--''')
1266
1267
1268 def test_seq_parts_in_a_multipart_with_none_epilogue(self):
1269 eq = self.ndiffAssertEqual
1270 outer = MIMEBase('multipart', 'mixed')
1271 outer['Subject'] = 'A subject'
1272 outer['To'] = 'aperson@dom.ain'
1273 outer['From'] = 'bperson@dom.ain'
1274 outer.epilogue = None
1275 msg = MIMEText('hello world')
1276 outer.attach(msg)
1277 outer.set_boundary('BOUNDARY')
1278 eq(outer.as_string(), '''\
1279Content-Type: multipart/mixed; boundary="BOUNDARY"
1280MIME-Version: 1.0
1281Subject: A subject
1282To: aperson@dom.ain
1283From: bperson@dom.ain
1284
1285--BOUNDARY
1286Content-Type: text/plain; charset="us-ascii"
1287MIME-Version: 1.0
1288Content-Transfer-Encoding: 7bit
1289
1290hello world
1291--BOUNDARY--''')
1292
1293
1294 def test_seq_parts_in_a_multipart_with_empty_epilogue(self):
1295 eq = self.ndiffAssertEqual
1296 outer = MIMEBase('multipart', 'mixed')
1297 outer['Subject'] = 'A subject'
1298 outer['To'] = 'aperson@dom.ain'
1299 outer['From'] = 'bperson@dom.ain'
1300 outer.epilogue = ''
1301 msg = MIMEText('hello world')
1302 outer.attach(msg)
1303 outer.set_boundary('BOUNDARY')
1304 eq(outer.as_string(), '''\
1305Content-Type: multipart/mixed; boundary="BOUNDARY"
1306MIME-Version: 1.0
1307Subject: A subject
1308To: aperson@dom.ain
1309From: bperson@dom.ain
1310
1311--BOUNDARY
1312Content-Type: text/plain; charset="us-ascii"
1313MIME-Version: 1.0
1314Content-Transfer-Encoding: 7bit
1315
1316hello world
1317--BOUNDARY--
1318''')
1319
1320
1321 def test_seq_parts_in_a_multipart_with_nl_epilogue(self):
1322 eq = self.ndiffAssertEqual
1323 outer = MIMEBase('multipart', 'mixed')
1324 outer['Subject'] = 'A subject'
1325 outer['To'] = 'aperson@dom.ain'
1326 outer['From'] = 'bperson@dom.ain'
1327 outer.epilogue = '\n'
1328 msg = MIMEText('hello world')
1329 outer.attach(msg)
1330 outer.set_boundary('BOUNDARY')
1331 eq(outer.as_string(), '''\
1332Content-Type: multipart/mixed; boundary="BOUNDARY"
1333MIME-Version: 1.0
1334Subject: A subject
1335To: aperson@dom.ain
1336From: bperson@dom.ain
1337
1338--BOUNDARY
1339Content-Type: text/plain; charset="us-ascii"
1340MIME-Version: 1.0
1341Content-Transfer-Encoding: 7bit
1342
1343hello world
1344--BOUNDARY--
1345
1346''')
1347
1348 def test_message_external_body(self):
1349 eq = self.assertEqual
1350 msg = self._msgobj('msg_36.txt')
1351 eq(len(msg.get_payload()), 2)
1352 msg1 = msg.get_payload(1)
1353 eq(msg1.get_content_type(), 'multipart/alternative')
1354 eq(len(msg1.get_payload()), 2)
1355 for subpart in msg1.get_payload():
1356 eq(subpart.get_content_type(), 'message/external-body')
1357 eq(len(subpart.get_payload()), 1)
1358 subsubpart = subpart.get_payload(0)
1359 eq(subsubpart.get_content_type(), 'text/plain')
1360
1361 def test_double_boundary(self):
1362 # msg_37.txt is a multipart that contains two dash-boundary's in a
1363 # row. Our interpretation of RFC 2046 calls for ignoring the second
1364 # and subsequent boundaries.
1365 msg = self._msgobj('msg_37.txt')
1366 self.assertEqual(len(msg.get_payload()), 3)
1367
1368 def test_nested_inner_contains_outer_boundary(self):
1369 eq = self.ndiffAssertEqual
1370 # msg_38.txt has an inner part that contains outer boundaries. My
1371 # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say
1372 # these are illegal and should be interpreted as unterminated inner
1373 # parts.
1374 msg = self._msgobj('msg_38.txt')
1375 sfp = StringIO()
1376 iterators._structure(msg, sfp)
1377 eq(sfp.getvalue(), """\
1378multipart/mixed
1379 multipart/mixed
1380 multipart/alternative
1381 text/plain
1382 text/plain
1383 text/plain
1384 text/plain
1385""")
1386
1387 def test_nested_with_same_boundary(self):
1388 eq = self.ndiffAssertEqual
1389 # msg 39.txt is similarly evil in that it's got inner parts that use
1390 # the same boundary as outer parts. Again, I believe the way this is
1391 # parsed is closest to the spirit of RFC 2046
1392 msg = self._msgobj('msg_39.txt')
1393 sfp = StringIO()
1394 iterators._structure(msg, sfp)
1395 eq(sfp.getvalue(), """\
1396multipart/mixed
1397 multipart/mixed
1398 multipart/alternative
1399 application/octet-stream
1400 application/octet-stream
1401 text/plain
1402""")
1403
1404 def test_boundary_in_non_multipart(self):
1405 msg = self._msgobj('msg_40.txt')
1406 self.assertEqual(msg.as_string(), '''\
1407MIME-Version: 1.0
1408Content-Type: text/html; boundary="--961284236552522269"
1409
1410----961284236552522269
1411Content-Type: text/html;
1412Content-Transfer-Encoding: 7Bit
1413
1414<html></html>
1415
1416----961284236552522269--
1417''')
1418
1419 def test_boundary_with_leading_space(self):
1420 eq = self.assertEqual
1421 msg = email.message_from_string('''\
1422MIME-Version: 1.0
1423Content-Type: multipart/mixed; boundary=" XXXX"
1424
1425-- XXXX
1426Content-Type: text/plain
1427
1428
1429-- XXXX
1430Content-Type: text/plain
1431
1432-- XXXX--
1433''')
Georg Brandlab91fde2009-08-13 08:51:18 +00001434 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001435 eq(msg.get_boundary(), ' XXXX')
1436 eq(len(msg.get_payload()), 2)
1437
1438 def test_boundary_without_trailing_newline(self):
1439 m = Parser().parsestr("""\
1440Content-Type: multipart/mixed; boundary="===============0012394164=="
1441MIME-Version: 1.0
1442
1443--===============0012394164==
1444Content-Type: image/file1.jpg
1445MIME-Version: 1.0
1446Content-Transfer-Encoding: base64
1447
1448YXNkZg==
1449--===============0012394164==--""")
1450 self.assertEquals(m.get_payload(0).get_payload(), 'YXNkZg==')
1451
1452
1453
1454# Test some badly formatted messages
1455class TestNonConformant(TestEmailBase):
1456 def test_parse_missing_minor_type(self):
1457 eq = self.assertEqual
1458 msg = self._msgobj('msg_14.txt')
1459 eq(msg.get_content_type(), 'text/plain')
1460 eq(msg.get_content_maintype(), 'text')
1461 eq(msg.get_content_subtype(), 'plain')
1462
1463 def test_same_boundary_inner_outer(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00001464 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001465 msg = self._msgobj('msg_15.txt')
1466 # XXX We can probably eventually do better
1467 inner = msg.get_payload(0)
1468 unless(hasattr(inner, 'defects'))
1469 self.assertEqual(len(inner.defects), 1)
1470 unless(isinstance(inner.defects[0],
1471 errors.StartBoundaryNotFoundDefect))
1472
1473 def test_multipart_no_boundary(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00001474 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001475 msg = self._msgobj('msg_25.txt')
1476 unless(isinstance(msg.get_payload(), str))
1477 self.assertEqual(len(msg.defects), 2)
1478 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1479 unless(isinstance(msg.defects[1],
1480 errors.MultipartInvariantViolationDefect))
1481
1482 def test_invalid_content_type(self):
1483 eq = self.assertEqual
1484 neq = self.ndiffAssertEqual
1485 msg = Message()
1486 # RFC 2045, $5.2 says invalid yields text/plain
1487 msg['Content-Type'] = 'text'
1488 eq(msg.get_content_maintype(), 'text')
1489 eq(msg.get_content_subtype(), 'plain')
1490 eq(msg.get_content_type(), 'text/plain')
1491 # Clear the old value and try something /really/ invalid
1492 del msg['content-type']
1493 msg['Content-Type'] = 'foo'
1494 eq(msg.get_content_maintype(), 'text')
1495 eq(msg.get_content_subtype(), 'plain')
1496 eq(msg.get_content_type(), 'text/plain')
1497 # Still, make sure that the message is idempotently generated
1498 s = StringIO()
1499 g = Generator(s)
1500 g.flatten(msg)
1501 neq(s.getvalue(), 'Content-Type: foo\n\n')
1502
1503 def test_no_start_boundary(self):
1504 eq = self.ndiffAssertEqual
1505 msg = self._msgobj('msg_31.txt')
1506 eq(msg.get_payload(), """\
1507--BOUNDARY
1508Content-Type: text/plain
1509
1510message 1
1511
1512--BOUNDARY
1513Content-Type: text/plain
1514
1515message 2
1516
1517--BOUNDARY--
1518""")
1519
1520 def test_no_separating_blank_line(self):
1521 eq = self.ndiffAssertEqual
1522 msg = self._msgobj('msg_35.txt')
1523 eq(msg.as_string(), """\
1524From: aperson@dom.ain
1525To: bperson@dom.ain
1526Subject: here's something interesting
1527
1528counter to RFC 2822, there's no separating newline here
1529""")
1530
1531 def test_lying_multipart(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00001532 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001533 msg = self._msgobj('msg_41.txt')
1534 unless(hasattr(msg, 'defects'))
1535 self.assertEqual(len(msg.defects), 2)
1536 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1537 unless(isinstance(msg.defects[1],
1538 errors.MultipartInvariantViolationDefect))
1539
1540 def test_missing_start_boundary(self):
1541 outer = self._msgobj('msg_42.txt')
1542 # The message structure is:
1543 #
1544 # multipart/mixed
1545 # text/plain
1546 # message/rfc822
1547 # multipart/mixed [*]
1548 #
1549 # [*] This message is missing its start boundary
1550 bad = outer.get_payload(1).get_payload(0)
1551 self.assertEqual(len(bad.defects), 1)
Georg Brandlab91fde2009-08-13 08:51:18 +00001552 self.assertTrue(isinstance(bad.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001553 errors.StartBoundaryNotFoundDefect))
1554
1555 def test_first_line_is_continuation_header(self):
1556 eq = self.assertEqual
1557 m = ' Line 1\nLine 2\nLine 3'
1558 msg = email.message_from_string(m)
1559 eq(msg.keys(), [])
1560 eq(msg.get_payload(), 'Line 2\nLine 3')
1561 eq(len(msg.defects), 1)
Georg Brandlab91fde2009-08-13 08:51:18 +00001562 self.assertTrue(isinstance(msg.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001563 errors.FirstHeaderLineIsContinuationDefect))
1564 eq(msg.defects[0].line, ' Line 1\n')
1565
1566
1567
1568# Test RFC 2047 header encoding and decoding
Guido van Rossum9604e662007-08-30 03:46:43 +00001569class TestRFC2047(TestEmailBase):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001570 def test_rfc2047_multiline(self):
1571 eq = self.assertEqual
1572 s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz
1573 foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?="""
1574 dh = decode_header(s)
1575 eq(dh, [
1576 (b'Re:', None),
1577 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland'),
1578 (b'baz foo bar', None),
1579 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland')])
1580 header = make_header(dh)
1581 eq(str(header),
1582 'Re: r\xe4ksm\xf6rg\xe5s baz foo bar r\xe4ksm\xf6rg\xe5s')
Barry Warsaw00b34222007-08-31 02:35:00 +00001583 self.ndiffAssertEqual(header.encode(maxlinelen=76), """\
Guido van Rossum9604e662007-08-30 03:46:43 +00001584Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar =?mac-iceland?q?r=8Aksm?=
1585 =?mac-iceland?q?=9Arg=8Cs?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001586
1587 def test_whitespace_eater_unicode(self):
1588 eq = self.assertEqual
1589 s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard@dom.ain>'
1590 dh = decode_header(s)
1591 eq(dh, [(b'Andr\xe9', 'iso-8859-1'),
1592 (b'Pirard <pirard@dom.ain>', None)])
1593 header = str(make_header(dh))
1594 eq(header, 'Andr\xe9 Pirard <pirard@dom.ain>')
1595
1596 def test_whitespace_eater_unicode_2(self):
1597 eq = self.assertEqual
1598 s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?='
1599 dh = decode_header(s)
1600 eq(dh, [(b'The', None), (b'quick brown fox', 'iso-8859-1'),
1601 (b'jumped over the', None), (b'lazy dog', 'iso-8859-1')])
1602 hu = str(make_header(dh))
1603 eq(hu, 'The quick brown fox jumped over the lazy dog')
1604
1605 def test_rfc2047_missing_whitespace(self):
1606 s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord'
1607 dh = decode_header(s)
1608 self.assertEqual(dh, [(s, None)])
1609
1610 def test_rfc2047_with_whitespace(self):
1611 s = 'Sm =?ISO-8859-1?B?9g==?= rg =?ISO-8859-1?B?5Q==?= sbord'
1612 dh = decode_header(s)
1613 self.assertEqual(dh, [(b'Sm', None), (b'\xf6', 'iso-8859-1'),
1614 (b'rg', None), (b'\xe5', 'iso-8859-1'),
1615 (b'sbord', None)])
1616
1617
1618
1619# Test the MIMEMessage class
1620class TestMIMEMessage(TestEmailBase):
1621 def setUp(self):
1622 with openfile('msg_11.txt') as fp:
1623 self._text = fp.read()
1624
1625 def test_type_error(self):
1626 self.assertRaises(TypeError, MIMEMessage, 'a plain string')
1627
1628 def test_valid_argument(self):
1629 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001630 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001631 subject = 'A sub-message'
1632 m = Message()
1633 m['Subject'] = subject
1634 r = MIMEMessage(m)
1635 eq(r.get_content_type(), 'message/rfc822')
1636 payload = r.get_payload()
1637 unless(isinstance(payload, list))
1638 eq(len(payload), 1)
1639 subpart = payload[0]
1640 unless(subpart is m)
1641 eq(subpart['subject'], subject)
1642
1643 def test_bad_multipart(self):
1644 eq = self.assertEqual
1645 msg1 = Message()
1646 msg1['Subject'] = 'subpart 1'
1647 msg2 = Message()
1648 msg2['Subject'] = 'subpart 2'
1649 r = MIMEMessage(msg1)
1650 self.assertRaises(errors.MultipartConversionError, r.attach, msg2)
1651
1652 def test_generate(self):
1653 # First craft the message to be encapsulated
1654 m = Message()
1655 m['Subject'] = 'An enclosed message'
1656 m.set_payload('Here is the body of the message.\n')
1657 r = MIMEMessage(m)
1658 r['Subject'] = 'The enclosing message'
1659 s = StringIO()
1660 g = Generator(s)
1661 g.flatten(r)
1662 self.assertEqual(s.getvalue(), """\
1663Content-Type: message/rfc822
1664MIME-Version: 1.0
1665Subject: The enclosing message
1666
1667Subject: An enclosed message
1668
1669Here is the body of the message.
1670""")
1671
1672 def test_parse_message_rfc822(self):
1673 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001674 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001675 msg = self._msgobj('msg_11.txt')
1676 eq(msg.get_content_type(), 'message/rfc822')
1677 payload = msg.get_payload()
1678 unless(isinstance(payload, list))
1679 eq(len(payload), 1)
1680 submsg = payload[0]
Georg Brandlab91fde2009-08-13 08:51:18 +00001681 self.assertTrue(isinstance(submsg, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001682 eq(submsg['subject'], 'An enclosed message')
1683 eq(submsg.get_payload(), 'Here is the body of the message.\n')
1684
1685 def test_dsn(self):
1686 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00001687 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001688 # msg 16 is a Delivery Status Notification, see RFC 1894
1689 msg = self._msgobj('msg_16.txt')
1690 eq(msg.get_content_type(), 'multipart/report')
1691 unless(msg.is_multipart())
1692 eq(len(msg.get_payload()), 3)
1693 # Subpart 1 is a text/plain, human readable section
1694 subpart = msg.get_payload(0)
1695 eq(subpart.get_content_type(), 'text/plain')
1696 eq(subpart.get_payload(), """\
1697This report relates to a message you sent with the following header fields:
1698
1699 Message-id: <002001c144a6$8752e060$56104586@oxy.edu>
1700 Date: Sun, 23 Sep 2001 20:10:55 -0700
1701 From: "Ian T. Henry" <henryi@oxy.edu>
1702 To: SoCal Raves <scr@socal-raves.org>
1703 Subject: [scr] yeah for Ians!!
1704
1705Your message cannot be delivered to the following recipients:
1706
1707 Recipient address: jangel1@cougar.noc.ucla.edu
1708 Reason: recipient reached disk quota
1709
1710""")
1711 # Subpart 2 contains the machine parsable DSN information. It
1712 # consists of two blocks of headers, represented by two nested Message
1713 # objects.
1714 subpart = msg.get_payload(1)
1715 eq(subpart.get_content_type(), 'message/delivery-status')
1716 eq(len(subpart.get_payload()), 2)
1717 # message/delivery-status should treat each block as a bunch of
1718 # headers, i.e. a bunch of Message objects.
1719 dsn1 = subpart.get_payload(0)
1720 unless(isinstance(dsn1, Message))
1721 eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu')
1722 eq(dsn1.get_param('dns', header='reporting-mta'), '')
1723 # Try a missing one <wink>
1724 eq(dsn1.get_param('nsd', header='reporting-mta'), None)
1725 dsn2 = subpart.get_payload(1)
1726 unless(isinstance(dsn2, Message))
1727 eq(dsn2['action'], 'failed')
1728 eq(dsn2.get_params(header='original-recipient'),
1729 [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')])
1730 eq(dsn2.get_param('rfc822', header='final-recipient'), '')
1731 # Subpart 3 is the original message
1732 subpart = msg.get_payload(2)
1733 eq(subpart.get_content_type(), 'message/rfc822')
1734 payload = subpart.get_payload()
1735 unless(isinstance(payload, list))
1736 eq(len(payload), 1)
1737 subsubpart = payload[0]
1738 unless(isinstance(subsubpart, Message))
1739 eq(subsubpart.get_content_type(), 'text/plain')
1740 eq(subsubpart['message-id'],
1741 '<002001c144a6$8752e060$56104586@oxy.edu>')
1742
1743 def test_epilogue(self):
1744 eq = self.ndiffAssertEqual
1745 with openfile('msg_21.txt') as fp:
1746 text = fp.read()
1747 msg = Message()
1748 msg['From'] = 'aperson@dom.ain'
1749 msg['To'] = 'bperson@dom.ain'
1750 msg['Subject'] = 'Test'
1751 msg.preamble = 'MIME message'
1752 msg.epilogue = 'End of MIME message\n'
1753 msg1 = MIMEText('One')
1754 msg2 = MIMEText('Two')
1755 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1756 msg.attach(msg1)
1757 msg.attach(msg2)
1758 sfp = StringIO()
1759 g = Generator(sfp)
1760 g.flatten(msg)
1761 eq(sfp.getvalue(), text)
1762
1763 def test_no_nl_preamble(self):
1764 eq = self.ndiffAssertEqual
1765 msg = Message()
1766 msg['From'] = 'aperson@dom.ain'
1767 msg['To'] = 'bperson@dom.ain'
1768 msg['Subject'] = 'Test'
1769 msg.preamble = 'MIME message'
1770 msg.epilogue = ''
1771 msg1 = MIMEText('One')
1772 msg2 = MIMEText('Two')
1773 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1774 msg.attach(msg1)
1775 msg.attach(msg2)
1776 eq(msg.as_string(), """\
1777From: aperson@dom.ain
1778To: bperson@dom.ain
1779Subject: Test
1780Content-Type: multipart/mixed; boundary="BOUNDARY"
1781
1782MIME message
1783--BOUNDARY
1784Content-Type: text/plain; charset="us-ascii"
1785MIME-Version: 1.0
1786Content-Transfer-Encoding: 7bit
1787
1788One
1789--BOUNDARY
1790Content-Type: text/plain; charset="us-ascii"
1791MIME-Version: 1.0
1792Content-Transfer-Encoding: 7bit
1793
1794Two
1795--BOUNDARY--
1796""")
1797
1798 def test_default_type(self):
1799 eq = self.assertEqual
1800 with openfile('msg_30.txt') as fp:
1801 msg = email.message_from_file(fp)
1802 container1 = msg.get_payload(0)
1803 eq(container1.get_default_type(), 'message/rfc822')
1804 eq(container1.get_content_type(), 'message/rfc822')
1805 container2 = msg.get_payload(1)
1806 eq(container2.get_default_type(), 'message/rfc822')
1807 eq(container2.get_content_type(), 'message/rfc822')
1808 container1a = container1.get_payload(0)
1809 eq(container1a.get_default_type(), 'text/plain')
1810 eq(container1a.get_content_type(), 'text/plain')
1811 container2a = container2.get_payload(0)
1812 eq(container2a.get_default_type(), 'text/plain')
1813 eq(container2a.get_content_type(), 'text/plain')
1814
1815 def test_default_type_with_explicit_container_type(self):
1816 eq = self.assertEqual
1817 with openfile('msg_28.txt') as fp:
1818 msg = email.message_from_file(fp)
1819 container1 = msg.get_payload(0)
1820 eq(container1.get_default_type(), 'message/rfc822')
1821 eq(container1.get_content_type(), 'message/rfc822')
1822 container2 = msg.get_payload(1)
1823 eq(container2.get_default_type(), 'message/rfc822')
1824 eq(container2.get_content_type(), 'message/rfc822')
1825 container1a = container1.get_payload(0)
1826 eq(container1a.get_default_type(), 'text/plain')
1827 eq(container1a.get_content_type(), 'text/plain')
1828 container2a = container2.get_payload(0)
1829 eq(container2a.get_default_type(), 'text/plain')
1830 eq(container2a.get_content_type(), 'text/plain')
1831
1832 def test_default_type_non_parsed(self):
1833 eq = self.assertEqual
1834 neq = self.ndiffAssertEqual
1835 # Set up container
1836 container = MIMEMultipart('digest', 'BOUNDARY')
1837 container.epilogue = ''
1838 # Set up subparts
1839 subpart1a = MIMEText('message 1\n')
1840 subpart2a = MIMEText('message 2\n')
1841 subpart1 = MIMEMessage(subpart1a)
1842 subpart2 = MIMEMessage(subpart2a)
1843 container.attach(subpart1)
1844 container.attach(subpart2)
1845 eq(subpart1.get_content_type(), 'message/rfc822')
1846 eq(subpart1.get_default_type(), 'message/rfc822')
1847 eq(subpart2.get_content_type(), 'message/rfc822')
1848 eq(subpart2.get_default_type(), 'message/rfc822')
1849 neq(container.as_string(0), '''\
1850Content-Type: multipart/digest; boundary="BOUNDARY"
1851MIME-Version: 1.0
1852
1853--BOUNDARY
1854Content-Type: message/rfc822
1855MIME-Version: 1.0
1856
1857Content-Type: text/plain; charset="us-ascii"
1858MIME-Version: 1.0
1859Content-Transfer-Encoding: 7bit
1860
1861message 1
1862
1863--BOUNDARY
1864Content-Type: message/rfc822
1865MIME-Version: 1.0
1866
1867Content-Type: text/plain; charset="us-ascii"
1868MIME-Version: 1.0
1869Content-Transfer-Encoding: 7bit
1870
1871message 2
1872
1873--BOUNDARY--
1874''')
1875 del subpart1['content-type']
1876 del subpart1['mime-version']
1877 del subpart2['content-type']
1878 del subpart2['mime-version']
1879 eq(subpart1.get_content_type(), 'message/rfc822')
1880 eq(subpart1.get_default_type(), 'message/rfc822')
1881 eq(subpart2.get_content_type(), 'message/rfc822')
1882 eq(subpart2.get_default_type(), 'message/rfc822')
1883 neq(container.as_string(0), '''\
1884Content-Type: multipart/digest; boundary="BOUNDARY"
1885MIME-Version: 1.0
1886
1887--BOUNDARY
1888
1889Content-Type: text/plain; charset="us-ascii"
1890MIME-Version: 1.0
1891Content-Transfer-Encoding: 7bit
1892
1893message 1
1894
1895--BOUNDARY
1896
1897Content-Type: text/plain; charset="us-ascii"
1898MIME-Version: 1.0
1899Content-Transfer-Encoding: 7bit
1900
1901message 2
1902
1903--BOUNDARY--
1904''')
1905
1906 def test_mime_attachments_in_constructor(self):
1907 eq = self.assertEqual
1908 text1 = MIMEText('')
1909 text2 = MIMEText('')
1910 msg = MIMEMultipart(_subparts=(text1, text2))
1911 eq(len(msg.get_payload()), 2)
1912 eq(msg.get_payload(0), text1)
1913 eq(msg.get_payload(1), text2)
1914
Christian Heimes587c2bf2008-01-19 16:21:02 +00001915 def test_default_multipart_constructor(self):
1916 msg = MIMEMultipart()
1917 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001918
1919
1920# A general test of parser->model->generator idempotency. IOW, read a message
1921# in, parse it into a message object tree, then without touching the tree,
1922# regenerate the plain text. The original text and the transformed text
1923# should be identical. Note: that we ignore the Unix-From since that may
1924# contain a changed date.
1925class TestIdempotent(TestEmailBase):
1926 def _msgobj(self, filename):
1927 with openfile(filename) as fp:
1928 data = fp.read()
1929 msg = email.message_from_string(data)
1930 return msg, data
1931
1932 def _idempotent(self, msg, text):
1933 eq = self.ndiffAssertEqual
1934 s = StringIO()
1935 g = Generator(s, maxheaderlen=0)
1936 g.flatten(msg)
1937 eq(text, s.getvalue())
1938
1939 def test_parse_text_message(self):
1940 eq = self.assertEquals
1941 msg, text = self._msgobj('msg_01.txt')
1942 eq(msg.get_content_type(), 'text/plain')
1943 eq(msg.get_content_maintype(), 'text')
1944 eq(msg.get_content_subtype(), 'plain')
1945 eq(msg.get_params()[1], ('charset', 'us-ascii'))
1946 eq(msg.get_param('charset'), 'us-ascii')
1947 eq(msg.preamble, None)
1948 eq(msg.epilogue, None)
1949 self._idempotent(msg, text)
1950
1951 def test_parse_untyped_message(self):
1952 eq = self.assertEquals
1953 msg, text = self._msgobj('msg_03.txt')
1954 eq(msg.get_content_type(), 'text/plain')
1955 eq(msg.get_params(), None)
1956 eq(msg.get_param('charset'), None)
1957 self._idempotent(msg, text)
1958
1959 def test_simple_multipart(self):
1960 msg, text = self._msgobj('msg_04.txt')
1961 self._idempotent(msg, text)
1962
1963 def test_MIME_digest(self):
1964 msg, text = self._msgobj('msg_02.txt')
1965 self._idempotent(msg, text)
1966
1967 def test_long_header(self):
1968 msg, text = self._msgobj('msg_27.txt')
1969 self._idempotent(msg, text)
1970
1971 def test_MIME_digest_with_part_headers(self):
1972 msg, text = self._msgobj('msg_28.txt')
1973 self._idempotent(msg, text)
1974
1975 def test_mixed_with_image(self):
1976 msg, text = self._msgobj('msg_06.txt')
1977 self._idempotent(msg, text)
1978
1979 def test_multipart_report(self):
1980 msg, text = self._msgobj('msg_05.txt')
1981 self._idempotent(msg, text)
1982
1983 def test_dsn(self):
1984 msg, text = self._msgobj('msg_16.txt')
1985 self._idempotent(msg, text)
1986
1987 def test_preamble_epilogue(self):
1988 msg, text = self._msgobj('msg_21.txt')
1989 self._idempotent(msg, text)
1990
1991 def test_multipart_one_part(self):
1992 msg, text = self._msgobj('msg_23.txt')
1993 self._idempotent(msg, text)
1994
1995 def test_multipart_no_parts(self):
1996 msg, text = self._msgobj('msg_24.txt')
1997 self._idempotent(msg, text)
1998
1999 def test_no_start_boundary(self):
2000 msg, text = self._msgobj('msg_31.txt')
2001 self._idempotent(msg, text)
2002
2003 def test_rfc2231_charset(self):
2004 msg, text = self._msgobj('msg_32.txt')
2005 self._idempotent(msg, text)
2006
2007 def test_more_rfc2231_parameters(self):
2008 msg, text = self._msgobj('msg_33.txt')
2009 self._idempotent(msg, text)
2010
2011 def test_text_plain_in_a_multipart_digest(self):
2012 msg, text = self._msgobj('msg_34.txt')
2013 self._idempotent(msg, text)
2014
2015 def test_nested_multipart_mixeds(self):
2016 msg, text = self._msgobj('msg_12a.txt')
2017 self._idempotent(msg, text)
2018
2019 def test_message_external_body_idempotent(self):
2020 msg, text = self._msgobj('msg_36.txt')
2021 self._idempotent(msg, text)
2022
2023 def test_content_type(self):
2024 eq = self.assertEquals
Georg Brandlab91fde2009-08-13 08:51:18 +00002025 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002026 # Get a message object and reset the seek pointer for other tests
2027 msg, text = self._msgobj('msg_05.txt')
2028 eq(msg.get_content_type(), 'multipart/report')
2029 # Test the Content-Type: parameters
2030 params = {}
2031 for pk, pv in msg.get_params():
2032 params[pk] = pv
2033 eq(params['report-type'], 'delivery-status')
2034 eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
2035 eq(msg.preamble, 'This is a MIME-encapsulated message.\n')
2036 eq(msg.epilogue, '\n')
2037 eq(len(msg.get_payload()), 3)
2038 # Make sure the subparts are what we expect
2039 msg1 = msg.get_payload(0)
2040 eq(msg1.get_content_type(), 'text/plain')
2041 eq(msg1.get_payload(), 'Yadda yadda yadda\n')
2042 msg2 = msg.get_payload(1)
2043 eq(msg2.get_content_type(), 'text/plain')
2044 eq(msg2.get_payload(), 'Yadda yadda yadda\n')
2045 msg3 = msg.get_payload(2)
2046 eq(msg3.get_content_type(), 'message/rfc822')
Georg Brandlab91fde2009-08-13 08:51:18 +00002047 self.assertTrue(isinstance(msg3, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002048 payload = msg3.get_payload()
2049 unless(isinstance(payload, list))
2050 eq(len(payload), 1)
2051 msg4 = payload[0]
2052 unless(isinstance(msg4, Message))
2053 eq(msg4.get_payload(), 'Yadda yadda yadda\n')
2054
2055 def test_parser(self):
2056 eq = self.assertEquals
Georg Brandlab91fde2009-08-13 08:51:18 +00002057 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002058 msg, text = self._msgobj('msg_06.txt')
2059 # Check some of the outer headers
2060 eq(msg.get_content_type(), 'message/rfc822')
2061 # Make sure the payload is a list of exactly one sub-Message, and that
2062 # that submessage has a type of text/plain
2063 payload = msg.get_payload()
2064 unless(isinstance(payload, list))
2065 eq(len(payload), 1)
2066 msg1 = payload[0]
Georg Brandlab91fde2009-08-13 08:51:18 +00002067 self.assertTrue(isinstance(msg1, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002068 eq(msg1.get_content_type(), 'text/plain')
Georg Brandlab91fde2009-08-13 08:51:18 +00002069 self.assertTrue(isinstance(msg1.get_payload(), str))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002070 eq(msg1.get_payload(), '\n')
2071
2072
2073
2074# Test various other bits of the package's functionality
2075class TestMiscellaneous(TestEmailBase):
2076 def test_message_from_string(self):
2077 with openfile('msg_01.txt') as fp:
2078 text = fp.read()
2079 msg = email.message_from_string(text)
2080 s = StringIO()
2081 # Don't wrap/continue long headers since we're trying to test
2082 # idempotency.
2083 g = Generator(s, maxheaderlen=0)
2084 g.flatten(msg)
2085 self.assertEqual(text, s.getvalue())
2086
2087 def test_message_from_file(self):
2088 with openfile('msg_01.txt') as fp:
2089 text = fp.read()
2090 fp.seek(0)
2091 msg = email.message_from_file(fp)
2092 s = StringIO()
2093 # Don't wrap/continue long headers since we're trying to test
2094 # idempotency.
2095 g = Generator(s, maxheaderlen=0)
2096 g.flatten(msg)
2097 self.assertEqual(text, s.getvalue())
2098
2099 def test_message_from_string_with_class(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00002100 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002101 with openfile('msg_01.txt') as fp:
2102 text = fp.read()
2103
2104 # Create a subclass
2105 class MyMessage(Message):
2106 pass
2107
2108 msg = email.message_from_string(text, MyMessage)
2109 unless(isinstance(msg, MyMessage))
2110 # Try something more complicated
2111 with openfile('msg_02.txt') as fp:
2112 text = fp.read()
2113 msg = email.message_from_string(text, MyMessage)
2114 for subpart in msg.walk():
2115 unless(isinstance(subpart, MyMessage))
2116
2117 def test_message_from_file_with_class(self):
Georg Brandlab91fde2009-08-13 08:51:18 +00002118 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002119 # Create a subclass
2120 class MyMessage(Message):
2121 pass
2122
2123 with openfile('msg_01.txt') as fp:
2124 msg = email.message_from_file(fp, MyMessage)
2125 unless(isinstance(msg, MyMessage))
2126 # Try something more complicated
2127 with openfile('msg_02.txt') as fp:
2128 msg = email.message_from_file(fp, MyMessage)
2129 for subpart in msg.walk():
2130 unless(isinstance(subpart, MyMessage))
2131
2132 def test__all__(self):
2133 module = __import__('email')
2134 # Can't use sorted() here due to Python 2.3 compatibility
2135 all = module.__all__[:]
2136 all.sort()
2137 self.assertEqual(all, [
2138 'base64mime', 'charset', 'encoders', 'errors', 'generator',
2139 'header', 'iterators', 'message', 'message_from_file',
2140 'message_from_string', 'mime', 'parser',
2141 'quoprimime', 'utils',
2142 ])
2143
2144 def test_formatdate(self):
2145 now = time.time()
2146 self.assertEqual(utils.parsedate(utils.formatdate(now))[:6],
2147 time.gmtime(now)[:6])
2148
2149 def test_formatdate_localtime(self):
2150 now = time.time()
2151 self.assertEqual(
2152 utils.parsedate(utils.formatdate(now, localtime=True))[:6],
2153 time.localtime(now)[:6])
2154
2155 def test_formatdate_usegmt(self):
2156 now = time.time()
2157 self.assertEqual(
2158 utils.formatdate(now, localtime=False),
2159 time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now)))
2160 self.assertEqual(
2161 utils.formatdate(now, localtime=False, usegmt=True),
2162 time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now)))
2163
2164 def test_parsedate_none(self):
2165 self.assertEqual(utils.parsedate(''), None)
2166
2167 def test_parsedate_compact(self):
2168 # The FWS after the comma is optional
2169 self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'),
2170 utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800'))
2171
2172 def test_parsedate_no_dayofweek(self):
2173 eq = self.assertEqual
2174 eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'),
2175 (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800))
2176
2177 def test_parsedate_compact_no_dayofweek(self):
2178 eq = self.assertEqual
2179 eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'),
2180 (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800))
2181
2182 def test_parsedate_acceptable_to_time_functions(self):
2183 eq = self.assertEqual
2184 timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800')
2185 t = int(time.mktime(timetup))
2186 eq(time.localtime(t)[:6], timetup[:6])
2187 eq(int(time.strftime('%Y', timetup)), 2003)
2188 timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800')
2189 t = int(time.mktime(timetup[:9]))
2190 eq(time.localtime(t)[:6], timetup[:6])
2191 eq(int(time.strftime('%Y', timetup[:9])), 2003)
2192
2193 def test_parseaddr_empty(self):
2194 self.assertEqual(utils.parseaddr('<>'), ('', ''))
2195 self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '')
2196
2197 def test_noquote_dump(self):
2198 self.assertEqual(
2199 utils.formataddr(('A Silly Person', 'person@dom.ain')),
2200 'A Silly Person <person@dom.ain>')
2201
2202 def test_escape_dump(self):
2203 self.assertEqual(
2204 utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')),
2205 r'"A \(Very\) Silly Person" <person@dom.ain>')
2206 a = r'A \(Special\) Person'
2207 b = 'person@dom.ain'
2208 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2209
2210 def test_escape_backslashes(self):
2211 self.assertEqual(
2212 utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')),
2213 r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>')
2214 a = r'Arthur \Backslash\ Foobar'
2215 b = 'person@dom.ain'
2216 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2217
2218 def test_name_with_dot(self):
2219 x = 'John X. Doe <jxd@example.com>'
2220 y = '"John X. Doe" <jxd@example.com>'
2221 a, b = ('John X. Doe', 'jxd@example.com')
2222 self.assertEqual(utils.parseaddr(x), (a, b))
2223 self.assertEqual(utils.parseaddr(y), (a, b))
2224 # formataddr() quotes the name if there's a dot in it
2225 self.assertEqual(utils.formataddr((a, b)), y)
2226
2227 def test_multiline_from_comment(self):
2228 x = """\
2229Foo
2230\tBar <foo@example.com>"""
2231 self.assertEqual(utils.parseaddr(x), ('Foo Bar', 'foo@example.com'))
2232
2233 def test_quote_dump(self):
2234 self.assertEqual(
2235 utils.formataddr(('A Silly; Person', 'person@dom.ain')),
2236 r'"A Silly; Person" <person@dom.ain>')
2237
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002238 def test_charset_richcomparisons(self):
2239 eq = self.assertEqual
Georg Brandlab91fde2009-08-13 08:51:18 +00002240 ne = self.assertNotEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002241 cset1 = Charset()
2242 cset2 = Charset()
2243 eq(cset1, 'us-ascii')
2244 eq(cset1, 'US-ASCII')
2245 eq(cset1, 'Us-AsCiI')
2246 eq('us-ascii', cset1)
2247 eq('US-ASCII', cset1)
2248 eq('Us-AsCiI', cset1)
2249 ne(cset1, 'usascii')
2250 ne(cset1, 'USASCII')
2251 ne(cset1, 'UsAsCiI')
2252 ne('usascii', cset1)
2253 ne('USASCII', cset1)
2254 ne('UsAsCiI', cset1)
2255 eq(cset1, cset2)
2256 eq(cset2, cset1)
2257
2258 def test_getaddresses(self):
2259 eq = self.assertEqual
2260 eq(utils.getaddresses(['aperson@dom.ain (Al Person)',
2261 'Bud Person <bperson@dom.ain>']),
2262 [('Al Person', 'aperson@dom.ain'),
2263 ('Bud Person', 'bperson@dom.ain')])
2264
2265 def test_getaddresses_nasty(self):
2266 eq = self.assertEqual
2267 eq(utils.getaddresses(['foo: ;']), [('', '')])
2268 eq(utils.getaddresses(
2269 ['[]*-- =~$']),
2270 [('', ''), ('', ''), ('', '*--')])
2271 eq(utils.getaddresses(
2272 ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
2273 [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
2274
2275 def test_getaddresses_embedded_comment(self):
2276 """Test proper handling of a nested comment"""
2277 eq = self.assertEqual
2278 addrs = utils.getaddresses(['User ((nested comment)) <foo@bar.com>'])
2279 eq(addrs[0][1], 'foo@bar.com')
2280
2281 def test_utils_quote_unquote(self):
2282 eq = self.assertEqual
2283 msg = Message()
2284 msg.add_header('content-disposition', 'attachment',
2285 filename='foo\\wacky"name')
2286 eq(msg.get_filename(), 'foo\\wacky"name')
2287
2288 def test_get_body_encoding_with_bogus_charset(self):
2289 charset = Charset('not a charset')
2290 self.assertEqual(charset.get_body_encoding(), 'base64')
2291
2292 def test_get_body_encoding_with_uppercase_charset(self):
2293 eq = self.assertEqual
2294 msg = Message()
2295 msg['Content-Type'] = 'text/plain; charset=UTF-8'
2296 eq(msg['content-type'], 'text/plain; charset=UTF-8')
2297 charsets = msg.get_charsets()
2298 eq(len(charsets), 1)
2299 eq(charsets[0], 'utf-8')
2300 charset = Charset(charsets[0])
2301 eq(charset.get_body_encoding(), 'base64')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002302 msg.set_payload(b'hello world', charset=charset)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002303 eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n')
2304 eq(msg.get_payload(decode=True), b'hello world')
2305 eq(msg['content-transfer-encoding'], 'base64')
2306 # Try another one
2307 msg = Message()
2308 msg['Content-Type'] = 'text/plain; charset="US-ASCII"'
2309 charsets = msg.get_charsets()
2310 eq(len(charsets), 1)
2311 eq(charsets[0], 'us-ascii')
2312 charset = Charset(charsets[0])
2313 eq(charset.get_body_encoding(), encoders.encode_7or8bit)
2314 msg.set_payload('hello world', charset=charset)
2315 eq(msg.get_payload(), 'hello world')
2316 eq(msg['content-transfer-encoding'], '7bit')
2317
2318 def test_charsets_case_insensitive(self):
2319 lc = Charset('us-ascii')
2320 uc = Charset('US-ASCII')
2321 self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding())
2322
2323 def test_partial_falls_inside_message_delivery_status(self):
2324 eq = self.ndiffAssertEqual
2325 # The Parser interface provides chunks of data to FeedParser in 8192
2326 # byte gulps. SF bug #1076485 found one of those chunks inside
2327 # message/delivery-status header block, which triggered an
2328 # unreadline() of NeedMoreData.
2329 msg = self._msgobj('msg_43.txt')
2330 sfp = StringIO()
2331 iterators._structure(msg, sfp)
2332 eq(sfp.getvalue(), """\
2333multipart/report
2334 text/plain
2335 message/delivery-status
2336 text/plain
2337 text/plain
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/rfc822-headers
2363""")
2364
2365
2366
2367# Test the iterator/generators
2368class TestIterators(TestEmailBase):
2369 def test_body_line_iterator(self):
2370 eq = self.assertEqual
2371 neq = self.ndiffAssertEqual
2372 # First a simple non-multipart message
2373 msg = self._msgobj('msg_01.txt')
2374 it = iterators.body_line_iterator(msg)
2375 lines = list(it)
2376 eq(len(lines), 6)
2377 neq(EMPTYSTRING.join(lines), msg.get_payload())
2378 # Now a more complicated multipart
2379 msg = self._msgobj('msg_02.txt')
2380 it = iterators.body_line_iterator(msg)
2381 lines = list(it)
2382 eq(len(lines), 43)
2383 with openfile('msg_19.txt') as fp:
2384 neq(EMPTYSTRING.join(lines), fp.read())
2385
2386 def test_typed_subpart_iterator(self):
2387 eq = self.assertEqual
2388 msg = self._msgobj('msg_04.txt')
2389 it = iterators.typed_subpart_iterator(msg, 'text')
2390 lines = []
2391 subparts = 0
2392 for subpart in it:
2393 subparts += 1
2394 lines.append(subpart.get_payload())
2395 eq(subparts, 2)
2396 eq(EMPTYSTRING.join(lines), """\
2397a simple kind of mirror
2398to reflect upon our own
2399a simple kind of mirror
2400to reflect upon our own
2401""")
2402
2403 def test_typed_subpart_iterator_default_type(self):
2404 eq = self.assertEqual
2405 msg = self._msgobj('msg_03.txt')
2406 it = iterators.typed_subpart_iterator(msg, 'text', 'plain')
2407 lines = []
2408 subparts = 0
2409 for subpart in it:
2410 subparts += 1
2411 lines.append(subpart.get_payload())
2412 eq(subparts, 1)
2413 eq(EMPTYSTRING.join(lines), """\
2414
2415Hi,
2416
2417Do you like this message?
2418
2419-Me
2420""")
2421
2422
2423
2424class TestParsers(TestEmailBase):
2425 def test_header_parser(self):
2426 eq = self.assertEqual
2427 # Parse only the headers of a complex multipart MIME document
2428 with openfile('msg_02.txt') as fp:
2429 msg = HeaderParser().parse(fp)
2430 eq(msg['from'], 'ppp-request@zzz.org')
2431 eq(msg['to'], 'ppp@zzz.org')
2432 eq(msg.get_content_type(), 'multipart/mixed')
Georg Brandlab91fde2009-08-13 08:51:18 +00002433 self.assertFalse(msg.is_multipart())
2434 self.assertTrue(isinstance(msg.get_payload(), str))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002435
2436 def test_whitespace_continuation(self):
2437 eq = self.assertEqual
2438 # This message contains a line after the Subject: header that has only
2439 # whitespace, but it is not empty!
2440 msg = email.message_from_string("""\
2441From: aperson@dom.ain
2442To: bperson@dom.ain
2443Subject: the next line has a space on it
2444\x20
2445Date: Mon, 8 Apr 2002 15:09:19 -0400
2446Message-ID: spam
2447
2448Here's the message body
2449""")
2450 eq(msg['subject'], 'the next line has a space on it\n ')
2451 eq(msg['message-id'], 'spam')
2452 eq(msg.get_payload(), "Here's the message body\n")
2453
2454 def test_whitespace_continuation_last_header(self):
2455 eq = self.assertEqual
2456 # Like the previous test, but the subject line is the last
2457 # header.
2458 msg = email.message_from_string("""\
2459From: aperson@dom.ain
2460To: bperson@dom.ain
2461Date: Mon, 8 Apr 2002 15:09:19 -0400
2462Message-ID: spam
2463Subject: the next line has a space on it
2464\x20
2465
2466Here's the message body
2467""")
2468 eq(msg['subject'], 'the next line has a space on it\n ')
2469 eq(msg['message-id'], 'spam')
2470 eq(msg.get_payload(), "Here's the message body\n")
2471
2472 def test_crlf_separation(self):
2473 eq = self.assertEqual
Guido van Rossum98297ee2007-11-06 21:34:58 +00002474 with openfile('msg_26.txt', newline='\n') as fp:
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002475 msg = Parser().parse(fp)
2476 eq(len(msg.get_payload()), 2)
2477 part1 = msg.get_payload(0)
2478 eq(part1.get_content_type(), 'text/plain')
2479 eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n')
2480 part2 = msg.get_payload(1)
2481 eq(part2.get_content_type(), 'application/riscos')
2482
2483 def test_multipart_digest_with_extra_mime_headers(self):
2484 eq = self.assertEqual
2485 neq = self.ndiffAssertEqual
2486 with openfile('msg_28.txt') as fp:
2487 msg = email.message_from_file(fp)
2488 # Structure is:
2489 # multipart/digest
2490 # message/rfc822
2491 # text/plain
2492 # message/rfc822
2493 # text/plain
2494 eq(msg.is_multipart(), 1)
2495 eq(len(msg.get_payload()), 2)
2496 part1 = msg.get_payload(0)
2497 eq(part1.get_content_type(), 'message/rfc822')
2498 eq(part1.is_multipart(), 1)
2499 eq(len(part1.get_payload()), 1)
2500 part1a = part1.get_payload(0)
2501 eq(part1a.is_multipart(), 0)
2502 eq(part1a.get_content_type(), 'text/plain')
2503 neq(part1a.get_payload(), 'message 1\n')
2504 # next message/rfc822
2505 part2 = msg.get_payload(1)
2506 eq(part2.get_content_type(), 'message/rfc822')
2507 eq(part2.is_multipart(), 1)
2508 eq(len(part2.get_payload()), 1)
2509 part2a = part2.get_payload(0)
2510 eq(part2a.is_multipart(), 0)
2511 eq(part2a.get_content_type(), 'text/plain')
2512 neq(part2a.get_payload(), 'message 2\n')
2513
2514 def test_three_lines(self):
2515 # A bug report by Andrew McNamara
2516 lines = ['From: Andrew Person <aperson@dom.ain',
2517 'Subject: Test',
2518 'Date: Tue, 20 Aug 2002 16:43:45 +1000']
2519 msg = email.message_from_string(NL.join(lines))
2520 self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000')
2521
2522 def test_strip_line_feed_and_carriage_return_in_headers(self):
2523 eq = self.assertEqual
2524 # For [ 1002475 ] email message parser doesn't handle \r\n correctly
2525 value1 = 'text'
2526 value2 = 'more text'
2527 m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % (
2528 value1, value2)
2529 msg = email.message_from_string(m)
2530 eq(msg.get('Header'), value1)
2531 eq(msg.get('Next-Header'), value2)
2532
2533 def test_rfc2822_header_syntax(self):
2534 eq = self.assertEqual
2535 m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2536 msg = email.message_from_string(m)
2537 eq(len(msg), 3)
2538 eq(sorted(field for field in msg), ['!"#QUX;~', '>From', 'From'])
2539 eq(msg.get_payload(), 'body')
2540
2541 def test_rfc2822_space_not_allowed_in_header(self):
2542 eq = self.assertEqual
2543 m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2544 msg = email.message_from_string(m)
2545 eq(len(msg.keys()), 0)
2546
2547 def test_rfc2822_one_character_header(self):
2548 eq = self.assertEqual
2549 m = 'A: first header\nB: second header\nCC: third header\n\nbody'
2550 msg = email.message_from_string(m)
2551 headers = msg.keys()
2552 headers.sort()
2553 eq(headers, ['A', 'B', 'CC'])
2554 eq(msg.get_payload(), 'body')
2555
2556
2557
2558class TestBase64(unittest.TestCase):
2559 def test_len(self):
2560 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002561 eq(base64mime.header_length('hello'),
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002562 len(base64mime.body_encode(b'hello', eol='')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002563 for size in range(15):
2564 if size == 0 : bsize = 0
2565 elif size <= 3 : bsize = 4
2566 elif size <= 6 : bsize = 8
2567 elif size <= 9 : bsize = 12
2568 elif size <= 12: bsize = 16
2569 else : bsize = 20
Guido van Rossum9604e662007-08-30 03:46:43 +00002570 eq(base64mime.header_length('x' * size), bsize)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002571
2572 def test_decode(self):
2573 eq = self.assertEqual
Barry Warsaw2cc1f6d2007-08-30 14:28:55 +00002574 eq(base64mime.decode(''), b'')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002575 eq(base64mime.decode('aGVsbG8='), b'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002576
2577 def test_encode(self):
2578 eq = self.assertEqual
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002579 eq(base64mime.body_encode(b''), b'')
2580 eq(base64mime.body_encode(b'hello'), 'aGVsbG8=\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002581 # Test the binary flag
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002582 eq(base64mime.body_encode(b'hello\n'), 'aGVsbG8K\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002583 # Test the maxlinelen arg
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002584 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002585eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2586eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2587eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2588eHh4eCB4eHh4IA==
2589""")
2590 # Test the eol argument
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002591 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40, eol='\r\n'),
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002592 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002593eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2594eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2595eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2596eHh4eCB4eHh4IA==\r
2597""")
2598
2599 def test_header_encode(self):
2600 eq = self.assertEqual
2601 he = base64mime.header_encode
2602 eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=')
Guido van Rossum9604e662007-08-30 03:46:43 +00002603 eq(he('hello\r\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=')
2604 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002605 # Test the charset option
2606 eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=')
2607 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002608
2609
2610
2611class TestQuopri(unittest.TestCase):
2612 def setUp(self):
2613 # Set of characters (as byte integers) that don't need to be encoded
2614 # in headers.
2615 self.hlit = list(chain(
2616 range(ord('a'), ord('z') + 1),
2617 range(ord('A'), ord('Z') + 1),
2618 range(ord('0'), ord('9') + 1),
Guido van Rossum9604e662007-08-30 03:46:43 +00002619 (c for c in b'!*+-/')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002620 # Set of characters (as byte integers) that do need to be encoded in
2621 # headers.
2622 self.hnon = [c for c in range(256) if c not in self.hlit]
2623 assert len(self.hlit) + len(self.hnon) == 256
2624 # Set of characters (as byte integers) that don't need to be encoded
2625 # in bodies.
2626 self.blit = list(range(ord(' '), ord('~') + 1))
2627 self.blit.append(ord('\t'))
2628 self.blit.remove(ord('='))
2629 # Set of characters (as byte integers) that do need to be encoded in
2630 # bodies.
2631 self.bnon = [c for c in range(256) if c not in self.blit]
2632 assert len(self.blit) + len(self.bnon) == 256
2633
Guido van Rossum9604e662007-08-30 03:46:43 +00002634 def test_quopri_header_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002635 for c in self.hlit:
Georg Brandlab91fde2009-08-13 08:51:18 +00002636 self.assertFalse(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002637 'Should not be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002638 for c in self.hnon:
Georg Brandlab91fde2009-08-13 08:51:18 +00002639 self.assertTrue(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002640 'Should be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002641
Guido van Rossum9604e662007-08-30 03:46:43 +00002642 def test_quopri_body_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002643 for c in self.blit:
Georg Brandlab91fde2009-08-13 08:51:18 +00002644 self.assertFalse(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002645 'Should not be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002646 for c in self.bnon:
Georg Brandlab91fde2009-08-13 08:51:18 +00002647 self.assertTrue(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00002648 'Should be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002649
2650 def test_header_quopri_len(self):
2651 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002652 eq(quoprimime.header_length(b'hello'), 5)
2653 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002654 eq(len(quoprimime.header_encode(b'hello', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00002655 quoprimime.header_length(b'hello') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002656 # =?xxx?q?...?= means 10 extra characters
2657 10)
Guido van Rossum9604e662007-08-30 03:46:43 +00002658 eq(quoprimime.header_length(b'h@e@l@l@o@'), 20)
2659 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002660 eq(len(quoprimime.header_encode(b'h@e@l@l@o@', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00002661 quoprimime.header_length(b'h@e@l@l@o@') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002662 # =?xxx?q?...?= means 10 extra characters
2663 10)
2664 for c in self.hlit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002665 eq(quoprimime.header_length(bytes([c])), 1,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002666 'expected length 1 for %r' % chr(c))
2667 for c in self.hnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002668 # Space is special; it's encoded to _
2669 if c == ord(' '):
2670 continue
2671 eq(quoprimime.header_length(bytes([c])), 3,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002672 'expected length 3 for %r' % chr(c))
Guido van Rossum9604e662007-08-30 03:46:43 +00002673 eq(quoprimime.header_length(b' '), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002674
2675 def test_body_quopri_len(self):
2676 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002677 for c in self.blit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002678 eq(quoprimime.body_length(bytes([c])), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002679 for c in self.bnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002680 eq(quoprimime.body_length(bytes([c])), 3)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002681
2682 def test_quote_unquote_idempotent(self):
2683 for x in range(256):
2684 c = chr(x)
2685 self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c)
2686
2687 def test_header_encode(self):
2688 eq = self.assertEqual
2689 he = quoprimime.header_encode
2690 eq(he(b'hello'), '=?iso-8859-1?q?hello?=')
2691 eq(he(b'hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=')
2692 eq(he(b'hello\nworld'), '=?iso-8859-1?q?hello=0Aworld?=')
2693 # Test a non-ASCII character
2694 eq(he(b'hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=')
2695
2696 def test_decode(self):
2697 eq = self.assertEqual
2698 eq(quoprimime.decode(''), '')
2699 eq(quoprimime.decode('hello'), 'hello')
2700 eq(quoprimime.decode('hello', 'X'), 'hello')
2701 eq(quoprimime.decode('hello\nworld', 'X'), 'helloXworld')
2702
2703 def test_encode(self):
2704 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002705 eq(quoprimime.body_encode(''), '')
2706 eq(quoprimime.body_encode('hello'), 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002707 # Test the binary flag
Guido van Rossum9604e662007-08-30 03:46:43 +00002708 eq(quoprimime.body_encode('hello\r\nworld'), 'hello\nworld')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002709 # Test the maxlinelen arg
Guido van Rossum9604e662007-08-30 03:46:43 +00002710 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002711xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
2712 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
2713x xxxx xxxx xxxx xxxx=20""")
2714 # Test the eol argument
Guido van Rossum9604e662007-08-30 03:46:43 +00002715 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'),
2716 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002717xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
2718 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
2719x xxxx xxxx xxxx xxxx=20""")
Guido van Rossum9604e662007-08-30 03:46:43 +00002720 eq(quoprimime.body_encode("""\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002721one line
2722
2723two line"""), """\
2724one line
2725
2726two line""")
2727
2728
2729
2730# Test the Charset class
2731class TestCharset(unittest.TestCase):
2732 def tearDown(self):
2733 from email import charset as CharsetModule
2734 try:
2735 del CharsetModule.CHARSETS['fake']
2736 except KeyError:
2737 pass
2738
Guido van Rossum9604e662007-08-30 03:46:43 +00002739 def test_codec_encodeable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002740 eq = self.assertEqual
2741 # Make sure us-ascii = no Unicode conversion
2742 c = Charset('us-ascii')
Guido van Rossum9604e662007-08-30 03:46:43 +00002743 eq(c.header_encode('Hello World!'), 'Hello World!')
2744 # Test 8-bit idempotency with us-ascii
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002745 s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
Guido van Rossum9604e662007-08-30 03:46:43 +00002746 self.assertRaises(UnicodeError, c.header_encode, s)
2747 c = Charset('utf-8')
2748 eq(c.header_encode(s), '=?utf-8?b?wqTCosKkwqTCpMKmwqTCqMKkwqo=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002749
2750 def test_body_encode(self):
2751 eq = self.assertEqual
2752 # Try a charset with QP body encoding
2753 c = Charset('iso-8859-1')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002754 eq('hello w=F6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002755 # Try a charset with Base64 body encoding
2756 c = Charset('utf-8')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002757 eq('aGVsbG8gd29ybGQ=\n', c.body_encode(b'hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002758 # Try a charset with None body encoding
2759 c = Charset('us-ascii')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002760 eq('hello world', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002761 # Try the convert argument, where input codec != output codec
2762 c = Charset('euc-jp')
2763 # With apologies to Tokio Kikuchi ;)
Barry Warsawbef9d212007-08-31 10:55:37 +00002764 # XXX FIXME
2765## try:
2766## eq('\x1b$B5FCO;~IW\x1b(B',
2767## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
2768## eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
2769## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
2770## except LookupError:
2771## # We probably don't have the Japanese codecs installed
2772## pass
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002773 # Testing SF bug #625509, which we have to fake, since there are no
2774 # built-in encodings where the header encoding is QP but the body
2775 # encoding is not.
2776 from email import charset as CharsetModule
2777 CharsetModule.add_charset('fake', CharsetModule.QP, None)
2778 c = Charset('fake')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002779 eq('hello w\xf6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002780
2781 def test_unicode_charset_name(self):
2782 charset = Charset('us-ascii')
2783 self.assertEqual(str(charset), 'us-ascii')
2784 self.assertRaises(errors.CharsetError, Charset, 'asc\xffii')
2785
2786
2787
2788# Test multilingual MIME headers.
2789class TestHeader(TestEmailBase):
2790 def test_simple(self):
2791 eq = self.ndiffAssertEqual
2792 h = Header('Hello World!')
2793 eq(h.encode(), 'Hello World!')
2794 h.append(' Goodbye World!')
2795 eq(h.encode(), 'Hello World! Goodbye World!')
2796
2797 def test_simple_surprise(self):
2798 eq = self.ndiffAssertEqual
2799 h = Header('Hello World!')
2800 eq(h.encode(), 'Hello World!')
2801 h.append('Goodbye World!')
2802 eq(h.encode(), 'Hello World! Goodbye World!')
2803
2804 def test_header_needs_no_decoding(self):
2805 h = 'no decoding needed'
2806 self.assertEqual(decode_header(h), [(h, None)])
2807
2808 def test_long(self):
2809 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.",
2810 maxlinelen=76)
2811 for l in h.encode(splitchars=' ').split('\n '):
Georg Brandlab91fde2009-08-13 08:51:18 +00002812 self.assertTrue(len(l) <= 76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002813
2814 def test_multilingual(self):
2815 eq = self.ndiffAssertEqual
2816 g = Charset("iso-8859-1")
2817 cz = Charset("iso-8859-2")
2818 utf8 = Charset("utf-8")
2819 g_head = (b'Die Mieter treten hier ein werden mit einem '
2820 b'Foerderband komfortabel den Korridor entlang, '
2821 b'an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, '
2822 b'gegen die rotierenden Klingen bef\xf6rdert. ')
2823 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
2824 b'd\xf9vtipu.. ')
2825 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
2826 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
2827 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
2828 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
2829 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
2830 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
2831 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
2832 '\u3044\u307e\u3059\u3002')
2833 h = Header(g_head, g)
2834 h.append(cz_head, cz)
2835 h.append(utf8_head, utf8)
Guido van Rossum9604e662007-08-30 03:46:43 +00002836 enc = h.encode(maxlinelen=76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002837 eq(enc, """\
Guido van Rossum9604e662007-08-30 03:46:43 +00002838=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_kom?=
2839 =?iso-8859-1?q?fortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wand?=
2840 =?iso-8859-1?q?gem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6r?=
2841 =?iso-8859-1?q?dert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002842 =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
2843 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
2844 =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
2845 =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
Guido van Rossum9604e662007-08-30 03:46:43 +00002846 =?utf-8?b?IE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5k?=
2847 =?utf-8?b?IGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIA=?=
2848 =?utf-8?b?44Gj44Gm44GE44G+44GZ44CC?=""")
2849 decoded = decode_header(enc)
2850 eq(len(decoded), 3)
2851 eq(decoded[0], (g_head, 'iso-8859-1'))
2852 eq(decoded[1], (cz_head, 'iso-8859-2'))
2853 eq(decoded[2], (utf8_head.encode('utf-8'), 'utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002854 ustr = str(h)
Guido van Rossum9604e662007-08-30 03:46:43 +00002855 eq(ustr,
2856 (b'Die Mieter treten hier ein werden mit einem Foerderband '
2857 b'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
2858 b'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
2859 b'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
2860 b'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
2861 b'\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
2862 b'\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
2863 b'\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
2864 b'\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
2865 b'\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
2866 b'\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
2867 b'\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
2868 b'\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
2869 b'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
2870 b'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
2871 b'\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82'
2872 ).decode('utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002873 # Test make_header()
2874 newh = make_header(decode_header(enc))
Guido van Rossum9604e662007-08-30 03:46:43 +00002875 eq(newh, h)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002876
2877 def test_empty_header_encode(self):
2878 h = Header()
2879 self.assertEqual(h.encode(), '')
Barry Warsaw8b3d6592007-08-30 02:10:49 +00002880
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002881 def test_header_ctor_default_args(self):
2882 eq = self.ndiffAssertEqual
2883 h = Header()
2884 eq(h, '')
2885 h.append('foo', Charset('iso-8859-1'))
Guido van Rossum9604e662007-08-30 03:46:43 +00002886 eq(h, 'foo')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002887
2888 def test_explicit_maxlinelen(self):
2889 eq = self.ndiffAssertEqual
2890 hstr = ('A very long line that must get split to something other '
2891 'than at the 76th character boundary to test the non-default '
2892 'behavior')
2893 h = Header(hstr)
2894 eq(h.encode(), '''\
2895A very long line that must get split to something other than at the 76th
2896 character boundary to test the non-default behavior''')
2897 eq(str(h), hstr)
2898 h = Header(hstr, header_name='Subject')
2899 eq(h.encode(), '''\
2900A very long line that must get split to something other than at the
2901 76th character boundary to test the non-default behavior''')
2902 eq(str(h), hstr)
2903 h = Header(hstr, maxlinelen=1024, header_name='Subject')
2904 eq(h.encode(), hstr)
2905 eq(str(h), hstr)
2906
Guido van Rossum9604e662007-08-30 03:46:43 +00002907 def test_quopri_splittable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002908 eq = self.ndiffAssertEqual
2909 h = Header(charset='iso-8859-1', maxlinelen=20)
Guido van Rossum9604e662007-08-30 03:46:43 +00002910 x = 'xxxx ' * 20
2911 h.append(x)
2912 s = h.encode()
2913 eq(s, """\
2914=?iso-8859-1?q?xxx?=
2915 =?iso-8859-1?q?x_?=
2916 =?iso-8859-1?q?xx?=
2917 =?iso-8859-1?q?xx?=
2918 =?iso-8859-1?q?_x?=
2919 =?iso-8859-1?q?xx?=
2920 =?iso-8859-1?q?x_?=
2921 =?iso-8859-1?q?xx?=
2922 =?iso-8859-1?q?xx?=
2923 =?iso-8859-1?q?_x?=
2924 =?iso-8859-1?q?xx?=
2925 =?iso-8859-1?q?x_?=
2926 =?iso-8859-1?q?xx?=
2927 =?iso-8859-1?q?xx?=
2928 =?iso-8859-1?q?_x?=
2929 =?iso-8859-1?q?xx?=
2930 =?iso-8859-1?q?x_?=
2931 =?iso-8859-1?q?xx?=
2932 =?iso-8859-1?q?xx?=
2933 =?iso-8859-1?q?_x?=
2934 =?iso-8859-1?q?xx?=
2935 =?iso-8859-1?q?x_?=
2936 =?iso-8859-1?q?xx?=
2937 =?iso-8859-1?q?xx?=
2938 =?iso-8859-1?q?_x?=
2939 =?iso-8859-1?q?xx?=
2940 =?iso-8859-1?q?x_?=
2941 =?iso-8859-1?q?xx?=
2942 =?iso-8859-1?q?xx?=
2943 =?iso-8859-1?q?_x?=
2944 =?iso-8859-1?q?xx?=
2945 =?iso-8859-1?q?x_?=
2946 =?iso-8859-1?q?xx?=
2947 =?iso-8859-1?q?xx?=
2948 =?iso-8859-1?q?_x?=
2949 =?iso-8859-1?q?xx?=
2950 =?iso-8859-1?q?x_?=
2951 =?iso-8859-1?q?xx?=
2952 =?iso-8859-1?q?xx?=
2953 =?iso-8859-1?q?_x?=
2954 =?iso-8859-1?q?xx?=
2955 =?iso-8859-1?q?x_?=
2956 =?iso-8859-1?q?xx?=
2957 =?iso-8859-1?q?xx?=
2958 =?iso-8859-1?q?_x?=
2959 =?iso-8859-1?q?xx?=
2960 =?iso-8859-1?q?x_?=
2961 =?iso-8859-1?q?xx?=
2962 =?iso-8859-1?q?xx?=
2963 =?iso-8859-1?q?_?=""")
2964 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002965 h = Header(charset='iso-8859-1', maxlinelen=40)
2966 h.append('xxxx ' * 20)
Guido van Rossum9604e662007-08-30 03:46:43 +00002967 s = h.encode()
2968 eq(s, """\
2969=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xxx?=
2970 =?iso-8859-1?q?x_xxxx_xxxx_xxxx_xxxx_?=
2971 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
2972 =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
2973 =?iso-8859-1?q?_xxxx_xxxx_?=""")
2974 eq(x, str(make_header(decode_header(s))))
2975
2976 def test_base64_splittable(self):
2977 eq = self.ndiffAssertEqual
2978 h = Header(charset='koi8-r', maxlinelen=20)
2979 x = 'xxxx ' * 20
2980 h.append(x)
2981 s = h.encode()
2982 eq(s, """\
2983=?koi8-r?b?eHh4?=
2984 =?koi8-r?b?eCB4?=
2985 =?koi8-r?b?eHh4?=
2986 =?koi8-r?b?IHh4?=
2987 =?koi8-r?b?eHgg?=
2988 =?koi8-r?b?eHh4?=
2989 =?koi8-r?b?eCB4?=
2990 =?koi8-r?b?eHh4?=
2991 =?koi8-r?b?IHh4?=
2992 =?koi8-r?b?eHgg?=
2993 =?koi8-r?b?eHh4?=
2994 =?koi8-r?b?eCB4?=
2995 =?koi8-r?b?eHh4?=
2996 =?koi8-r?b?IHh4?=
2997 =?koi8-r?b?eHgg?=
2998 =?koi8-r?b?eHh4?=
2999 =?koi8-r?b?eCB4?=
3000 =?koi8-r?b?eHh4?=
3001 =?koi8-r?b?IHh4?=
3002 =?koi8-r?b?eHgg?=
3003 =?koi8-r?b?eHh4?=
3004 =?koi8-r?b?eCB4?=
3005 =?koi8-r?b?eHh4?=
3006 =?koi8-r?b?IHh4?=
3007 =?koi8-r?b?eHgg?=
3008 =?koi8-r?b?eHh4?=
3009 =?koi8-r?b?eCB4?=
3010 =?koi8-r?b?eHh4?=
3011 =?koi8-r?b?IHh4?=
3012 =?koi8-r?b?eHgg?=
3013 =?koi8-r?b?eHh4?=
3014 =?koi8-r?b?eCB4?=
3015 =?koi8-r?b?eHh4?=
3016 =?koi8-r?b?IA==?=""")
3017 eq(x, str(make_header(decode_header(s))))
3018 h = Header(charset='koi8-r', maxlinelen=40)
3019 h.append(x)
3020 s = h.encode()
3021 eq(s, """\
3022=?koi8-r?b?eHh4eCB4eHh4IHh4eHggeHh4?=
3023 =?koi8-r?b?eCB4eHh4IHh4eHggeHh4eCB4?=
3024 =?koi8-r?b?eHh4IHh4eHggeHh4eCB4eHh4?=
3025 =?koi8-r?b?IHh4eHggeHh4eCB4eHh4IHh4?=
3026 =?koi8-r?b?eHggeHh4eCB4eHh4IHh4eHgg?=
3027 =?koi8-r?b?eHh4eCB4eHh4IA==?=""")
3028 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003029
3030 def test_us_ascii_header(self):
3031 eq = self.assertEqual
3032 s = 'hello'
3033 x = decode_header(s)
3034 eq(x, [('hello', None)])
3035 h = make_header(x)
3036 eq(s, h.encode())
3037
3038 def test_string_charset(self):
3039 eq = self.assertEqual
3040 h = Header()
3041 h.append('hello', 'iso-8859-1')
Guido van Rossum9604e662007-08-30 03:46:43 +00003042 eq(h, 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003043
3044## def test_unicode_error(self):
3045## raises = self.assertRaises
3046## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')
3047## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')
3048## h = Header()
3049## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')
3050## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')
3051## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')
3052
3053 def test_utf8_shortest(self):
3054 eq = self.assertEqual
3055 h = Header('p\xf6stal', 'utf-8')
3056 eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
3057 h = Header('\u83ca\u5730\u6642\u592b', 'utf-8')
3058 eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
3059
3060 def test_bad_8bit_header(self):
3061 raises = self.assertRaises
3062 eq = self.assertEqual
3063 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
3064 raises(UnicodeError, Header, x)
3065 h = Header()
3066 raises(UnicodeError, h.append, x)
3067 e = x.decode('utf-8', 'replace')
3068 eq(str(Header(x, errors='replace')), e)
3069 h.append(x, errors='replace')
3070 eq(str(h), e)
3071
3072 def test_encoded_adjacent_nonencoded(self):
3073 eq = self.assertEqual
3074 h = Header()
3075 h.append('hello', 'iso-8859-1')
3076 h.append('world')
3077 s = h.encode()
3078 eq(s, '=?iso-8859-1?q?hello?= world')
3079 h = make_header(decode_header(s))
3080 eq(h.encode(), s)
3081
3082 def test_whitespace_eater(self):
3083 eq = self.assertEqual
3084 s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
3085 parts = decode_header(s)
3086 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)])
3087 hdr = make_header(parts)
3088 eq(hdr.encode(),
3089 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
3090
3091 def test_broken_base64_header(self):
3092 raises = self.assertRaises
3093 s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3IQ?='
3094 raises(errors.HeaderParseError, decode_header, s)
3095
3096
3097
3098# Test RFC 2231 header parameters (en/de)coding
3099class TestRFC2231(TestEmailBase):
3100 def test_get_param(self):
3101 eq = self.assertEqual
3102 msg = self._msgobj('msg_29.txt')
3103 eq(msg.get_param('title'),
3104 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3105 eq(msg.get_param('title', unquote=False),
3106 ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
3107
3108 def test_set_param(self):
3109 eq = self.ndiffAssertEqual
3110 msg = Message()
3111 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3112 charset='us-ascii')
3113 eq(msg.get_param('title'),
3114 ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
3115 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3116 charset='us-ascii', language='en')
3117 eq(msg.get_param('title'),
3118 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3119 msg = self._msgobj('msg_01.txt')
3120 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3121 charset='us-ascii', language='en')
3122 eq(msg.as_string(maxheaderlen=78), """\
3123Return-Path: <bbb@zzz.org>
3124Delivered-To: bbb@zzz.org
3125Received: by mail.zzz.org (Postfix, from userid 889)
3126\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3127MIME-Version: 1.0
3128Content-Transfer-Encoding: 7bit
3129Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3130From: bbb@ddd.com (John X. Doe)
3131To: bbb@zzz.org
3132Subject: This is a test message
3133Date: Fri, 4 May 2001 14:05:44 -0400
3134Content-Type: text/plain; charset=us-ascii;
3135 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3136
3137
3138Hi,
3139
3140Do you like this message?
3141
3142-Me
3143""")
3144
3145 def test_del_param(self):
3146 eq = self.ndiffAssertEqual
3147 msg = self._msgobj('msg_01.txt')
3148 msg.set_param('foo', 'bar', charset='us-ascii', language='en')
3149 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3150 charset='us-ascii', language='en')
3151 msg.del_param('foo', header='Content-Type')
3152 eq(msg.as_string(maxheaderlen=78), """\
3153Return-Path: <bbb@zzz.org>
3154Delivered-To: bbb@zzz.org
3155Received: by mail.zzz.org (Postfix, from userid 889)
3156\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3157MIME-Version: 1.0
3158Content-Transfer-Encoding: 7bit
3159Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3160From: bbb@ddd.com (John X. Doe)
3161To: bbb@zzz.org
3162Subject: This is a test message
3163Date: Fri, 4 May 2001 14:05:44 -0400
3164Content-Type: text/plain; charset="us-ascii";
3165 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3166
3167
3168Hi,
3169
3170Do you like this message?
3171
3172-Me
3173""")
3174
3175 def test_rfc2231_get_content_charset(self):
3176 eq = self.assertEqual
3177 msg = self._msgobj('msg_32.txt')
3178 eq(msg.get_content_charset(), 'us-ascii')
3179
3180 def test_rfc2231_no_language_or_charset(self):
3181 m = '''\
3182Content-Transfer-Encoding: 8bit
3183Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
3184Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
3185
3186'''
3187 msg = email.message_from_string(m)
3188 param = msg.get_param('NAME')
Georg Brandlab91fde2009-08-13 08:51:18 +00003189 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003190 self.assertEqual(
3191 param,
3192 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
3193
3194 def test_rfc2231_no_language_or_charset_in_filename(self):
3195 m = '''\
3196Content-Disposition: inline;
3197\tfilename*0*="''This%20is%20even%20more%20";
3198\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3199\tfilename*2="is it not.pdf"
3200
3201'''
3202 msg = email.message_from_string(m)
3203 self.assertEqual(msg.get_filename(),
3204 'This is even more ***fun*** is it not.pdf')
3205
3206 def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
3207 m = '''\
3208Content-Disposition: inline;
3209\tfilename*0*="''This%20is%20even%20more%20";
3210\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3211\tfilename*2="is it not.pdf"
3212
3213'''
3214 msg = email.message_from_string(m)
3215 self.assertEqual(msg.get_filename(),
3216 'This is even more ***fun*** is it not.pdf')
3217
3218 def test_rfc2231_partly_encoded(self):
3219 m = '''\
3220Content-Disposition: inline;
3221\tfilename*0="''This%20is%20even%20more%20";
3222\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3223\tfilename*2="is it not.pdf"
3224
3225'''
3226 msg = email.message_from_string(m)
3227 self.assertEqual(
3228 msg.get_filename(),
3229 'This%20is%20even%20more%20***fun*** is it not.pdf')
3230
3231 def test_rfc2231_partly_nonencoded(self):
3232 m = '''\
3233Content-Disposition: inline;
3234\tfilename*0="This%20is%20even%20more%20";
3235\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
3236\tfilename*2="is it not.pdf"
3237
3238'''
3239 msg = email.message_from_string(m)
3240 self.assertEqual(
3241 msg.get_filename(),
3242 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
3243
3244 def test_rfc2231_no_language_or_charset_in_boundary(self):
3245 m = '''\
3246Content-Type: multipart/alternative;
3247\tboundary*0*="''This%20is%20even%20more%20";
3248\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
3249\tboundary*2="is it not.pdf"
3250
3251'''
3252 msg = email.message_from_string(m)
3253 self.assertEqual(msg.get_boundary(),
3254 'This is even more ***fun*** is it not.pdf')
3255
3256 def test_rfc2231_no_language_or_charset_in_charset(self):
3257 # This is a nonsensical charset value, but tests the code anyway
3258 m = '''\
3259Content-Type: text/plain;
3260\tcharset*0*="This%20is%20even%20more%20";
3261\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
3262\tcharset*2="is it not.pdf"
3263
3264'''
3265 msg = email.message_from_string(m)
3266 self.assertEqual(msg.get_content_charset(),
3267 'this is even more ***fun*** is it not.pdf')
3268
3269 def test_rfc2231_bad_encoding_in_filename(self):
3270 m = '''\
3271Content-Disposition: inline;
3272\tfilename*0*="bogus'xx'This%20is%20even%20more%20";
3273\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3274\tfilename*2="is it not.pdf"
3275
3276'''
3277 msg = email.message_from_string(m)
3278 self.assertEqual(msg.get_filename(),
3279 'This is even more ***fun*** is it not.pdf')
3280
3281 def test_rfc2231_bad_encoding_in_charset(self):
3282 m = """\
3283Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
3284
3285"""
3286 msg = email.message_from_string(m)
3287 # This should return None because non-ascii characters in the charset
3288 # are not allowed.
3289 self.assertEqual(msg.get_content_charset(), None)
3290
3291 def test_rfc2231_bad_character_in_charset(self):
3292 m = """\
3293Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D
3294
3295"""
3296 msg = email.message_from_string(m)
3297 # This should return None because non-ascii characters in the charset
3298 # are not allowed.
3299 self.assertEqual(msg.get_content_charset(), None)
3300
3301 def test_rfc2231_bad_character_in_filename(self):
3302 m = '''\
3303Content-Disposition: inline;
3304\tfilename*0*="ascii'xx'This%20is%20even%20more%20";
3305\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3306\tfilename*2*="is it not.pdf%E2"
3307
3308'''
3309 msg = email.message_from_string(m)
3310 self.assertEqual(msg.get_filename(),
3311 'This is even more ***fun*** is it not.pdf\ufffd')
3312
3313 def test_rfc2231_unknown_encoding(self):
3314 m = """\
3315Content-Transfer-Encoding: 8bit
3316Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
3317
3318"""
3319 msg = email.message_from_string(m)
3320 self.assertEqual(msg.get_filename(), 'myfile.txt')
3321
3322 def test_rfc2231_single_tick_in_filename_extended(self):
3323 eq = self.assertEqual
3324 m = """\
3325Content-Type: application/x-foo;
3326\tname*0*=\"Frank's\"; name*1*=\" Document\"
3327
3328"""
3329 msg = email.message_from_string(m)
3330 charset, language, s = msg.get_param('name')
3331 eq(charset, None)
3332 eq(language, None)
3333 eq(s, "Frank's Document")
3334
3335 def test_rfc2231_single_tick_in_filename(self):
3336 m = """\
3337Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
3338
3339"""
3340 msg = email.message_from_string(m)
3341 param = msg.get_param('name')
Georg Brandlab91fde2009-08-13 08:51:18 +00003342 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003343 self.assertEqual(param, "Frank's Document")
3344
3345 def test_rfc2231_tick_attack_extended(self):
3346 eq = self.assertEqual
3347 m = """\
3348Content-Type: application/x-foo;
3349\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
3350
3351"""
3352 msg = email.message_from_string(m)
3353 charset, language, s = msg.get_param('name')
3354 eq(charset, 'us-ascii')
3355 eq(language, 'en-us')
3356 eq(s, "Frank's Document")
3357
3358 def test_rfc2231_tick_attack(self):
3359 m = """\
3360Content-Type: application/x-foo;
3361\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
3362
3363"""
3364 msg = email.message_from_string(m)
3365 param = msg.get_param('name')
Georg Brandlab91fde2009-08-13 08:51:18 +00003366 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003367 self.assertEqual(param, "us-ascii'en-us'Frank's Document")
3368
3369 def test_rfc2231_no_extended_values(self):
3370 eq = self.assertEqual
3371 m = """\
3372Content-Type: application/x-foo; name=\"Frank's Document\"
3373
3374"""
3375 msg = email.message_from_string(m)
3376 eq(msg.get_param('name'), "Frank's Document")
3377
3378 def test_rfc2231_encoded_then_unencoded_segments(self):
3379 eq = self.assertEqual
3380 m = """\
3381Content-Type: application/x-foo;
3382\tname*0*=\"us-ascii'en-us'My\";
3383\tname*1=\" Document\";
3384\tname*2*=\" For You\"
3385
3386"""
3387 msg = email.message_from_string(m)
3388 charset, language, s = msg.get_param('name')
3389 eq(charset, 'us-ascii')
3390 eq(language, 'en-us')
3391 eq(s, 'My Document For You')
3392
3393 def test_rfc2231_unencoded_then_encoded_segments(self):
3394 eq = self.assertEqual
3395 m = """\
3396Content-Type: application/x-foo;
3397\tname*0=\"us-ascii'en-us'My\";
3398\tname*1*=\" Document\";
3399\tname*2*=\" For You\"
3400
3401"""
3402 msg = email.message_from_string(m)
3403 charset, language, s = msg.get_param('name')
3404 eq(charset, 'us-ascii')
3405 eq(language, 'en-us')
3406 eq(s, 'My Document For You')
3407
3408
3409
R. David Murrayfa606922010-01-16 18:41:00 +00003410# Tests to ensure that signed parts of an email are completely preserved, as
3411# required by RFC1847 section 2.1. Note that these are incomplete, because the
3412# email package does not currently always preserve the body. See issue 1670765.
3413class TestSigned(TestEmailBase):
3414
3415 def _msg_and_obj(self, filename):
3416 with openfile(findfile(filename)) as fp:
3417 original = fp.read()
3418 msg = email.message_from_string(original)
3419 return original, msg
3420
3421 def _signed_parts_eq(self, original, result):
3422 # Extract the first mime part of each message
3423 import re
3424 repart = re.compile(r'^--([^\n]+)\n(.*?)\n--\1$', re.S | re.M)
3425 inpart = repart.search(original).group(2)
3426 outpart = repart.search(result).group(2)
3427 self.assertEqual(outpart, inpart)
3428
3429 def test_long_headers_as_string(self):
3430 original, msg = self._msg_and_obj('msg_45.txt')
3431 result = msg.as_string()
3432 self._signed_parts_eq(original, result)
3433
3434 def test_long_headers_as_string_maxheaderlen(self):
3435 original, msg = self._msg_and_obj('msg_45.txt')
3436 result = msg.as_string(maxheaderlen=60)
3437 self._signed_parts_eq(original, result)
3438
3439 def test_long_headers_flatten(self):
3440 original, msg = self._msg_and_obj('msg_45.txt')
3441 fp = StringIO()
3442 Generator(fp).flatten(msg)
3443 result = fp.getvalue()
3444 self._signed_parts_eq(original, result)
3445
3446
3447
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003448def _testclasses():
3449 mod = sys.modules[__name__]
3450 return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
3451
3452
3453def suite():
3454 suite = unittest.TestSuite()
3455 for testclass in _testclasses():
3456 suite.addTest(unittest.makeSuite(testclass))
3457 return suite
3458
3459
3460def test_main():
3461 for testclass in _testclasses():
3462 run_unittest(testclass)
3463
3464
3465
3466if __name__ == '__main__':
3467 unittest.main(defaultTest='suite')