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