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