blob: 65b3ebd21186acdce98f1de7513fe2eca1c55bb9 [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
R. David Murray719a4492010-11-21 16:53:48 +00006import re
Guido van Rossum8b3febe2007-08-30 01:15:14 +00007import sys
8import time
9import base64
10import difflib
11import unittest
12import warnings
R. David Murray96fd54e2010-10-08 15:55:28 +000013import textwrap
Guido van Rossum8b3febe2007-08-30 01:15:14 +000014
R. David Murray96fd54e2010-10-08 15:55:28 +000015from io import StringIO, BytesIO
Guido van Rossum8b3febe2007-08-30 01:15:14 +000016from itertools import chain
17
18import email
19
20from email.charset import Charset
21from email.header import Header, decode_header, make_header
22from email.parser import Parser, HeaderParser
23from email.generator import Generator, DecodedGenerator
24from email.message import Message
25from email.mime.application import MIMEApplication
26from email.mime.audio import MIMEAudio
27from email.mime.text import MIMEText
28from email.mime.image import MIMEImage
29from email.mime.base import MIMEBase
30from email.mime.message import MIMEMessage
31from email.mime.multipart import MIMEMultipart
32from email import utils
33from email import errors
34from email import encoders
35from email import iterators
36from email import base64mime
37from email import quoprimime
38
R. David Murray96fd54e2010-10-08 15:55:28 +000039from test.support import findfile, run_unittest, unlink
Guido van Rossum8b3febe2007-08-30 01:15:14 +000040from email.test import __file__ as landmark
41
42
43NL = '\n'
44EMPTYSTRING = ''
45SPACE = ' '
46
47
Ezio Melottib3aedd42010-11-20 19:04:17 +000048
Guido van Rossum8b3febe2007-08-30 01:15:14 +000049def openfile(filename, *args, **kws):
50 path = os.path.join(os.path.dirname(landmark), 'data', filename)
51 return open(path, *args, **kws)
52
53
Ezio Melottib3aedd42010-11-20 19:04:17 +000054
Guido van Rossum8b3febe2007-08-30 01:15:14 +000055# Base test class
56class TestEmailBase(unittest.TestCase):
57 def ndiffAssertEqual(self, first, second):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +000058 """Like assertEqual except use ndiff for readable output."""
Guido van Rossum8b3febe2007-08-30 01:15:14 +000059 if first != second:
60 sfirst = str(first)
61 ssecond = str(second)
62 rfirst = [repr(line) for line in sfirst.splitlines()]
63 rsecond = [repr(line) for line in ssecond.splitlines()]
64 diff = difflib.ndiff(rfirst, rsecond)
65 raise self.failureException(NL + NL.join(diff))
66
67 def _msgobj(self, filename):
68 with openfile(findfile(filename)) as fp:
69 return email.message_from_file(fp)
70
71
Ezio Melottib3aedd42010-11-20 19:04:17 +000072
Guido van Rossum8b3febe2007-08-30 01:15:14 +000073# Test various aspects of the Message class's API
74class TestMessageAPI(TestEmailBase):
75 def test_get_all(self):
76 eq = self.assertEqual
77 msg = self._msgobj('msg_20.txt')
78 eq(msg.get_all('cc'), ['ccc@zzz.org', 'ddd@zzz.org', 'eee@zzz.org'])
79 eq(msg.get_all('xx', 'n/a'), 'n/a')
80
R. David Murraye5db2632010-11-20 15:10:13 +000081 def test_getset_charset(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +000082 eq = self.assertEqual
83 msg = Message()
84 eq(msg.get_charset(), None)
85 charset = Charset('iso-8859-1')
86 msg.set_charset(charset)
87 eq(msg['mime-version'], '1.0')
88 eq(msg.get_content_type(), 'text/plain')
89 eq(msg['content-type'], 'text/plain; charset="iso-8859-1"')
90 eq(msg.get_param('charset'), 'iso-8859-1')
91 eq(msg['content-transfer-encoding'], 'quoted-printable')
92 eq(msg.get_charset().input_charset, 'iso-8859-1')
93 # Remove the charset
94 msg.set_charset(None)
95 eq(msg.get_charset(), None)
96 eq(msg['content-type'], 'text/plain')
97 # Try adding a charset when there's already MIME headers present
98 msg = Message()
99 msg['MIME-Version'] = '2.0'
100 msg['Content-Type'] = 'text/x-weird'
101 msg['Content-Transfer-Encoding'] = 'quinted-puntable'
102 msg.set_charset(charset)
103 eq(msg['mime-version'], '2.0')
104 eq(msg['content-type'], 'text/x-weird; charset="iso-8859-1"')
105 eq(msg['content-transfer-encoding'], 'quinted-puntable')
106
107 def test_set_charset_from_string(self):
108 eq = self.assertEqual
109 msg = Message()
110 msg.set_charset('us-ascii')
111 eq(msg.get_charset().input_charset, 'us-ascii')
112 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
113
114 def test_set_payload_with_charset(self):
115 msg = Message()
116 charset = Charset('iso-8859-1')
117 msg.set_payload('This is a string payload', charset)
118 self.assertEqual(msg.get_charset().input_charset, 'iso-8859-1')
119
120 def test_get_charsets(self):
121 eq = self.assertEqual
122
123 msg = self._msgobj('msg_08.txt')
124 charsets = msg.get_charsets()
125 eq(charsets, [None, 'us-ascii', 'iso-8859-1', 'iso-8859-2', 'koi8-r'])
126
127 msg = self._msgobj('msg_09.txt')
128 charsets = msg.get_charsets('dingbat')
129 eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat',
130 'koi8-r'])
131
132 msg = self._msgobj('msg_12.txt')
133 charsets = msg.get_charsets()
134 eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2',
135 'iso-8859-3', 'us-ascii', 'koi8-r'])
136
137 def test_get_filename(self):
138 eq = self.assertEqual
139
140 msg = self._msgobj('msg_04.txt')
141 filenames = [p.get_filename() for p in msg.get_payload()]
142 eq(filenames, ['msg.txt', 'msg.txt'])
143
144 msg = self._msgobj('msg_07.txt')
145 subpart = msg.get_payload(1)
146 eq(subpart.get_filename(), 'dingusfish.gif')
147
148 def test_get_filename_with_name_parameter(self):
149 eq = self.assertEqual
150
151 msg = self._msgobj('msg_44.txt')
152 filenames = [p.get_filename() for p in msg.get_payload()]
153 eq(filenames, ['msg.txt', 'msg.txt'])
154
155 def test_get_boundary(self):
156 eq = self.assertEqual
157 msg = self._msgobj('msg_07.txt')
158 # No quotes!
159 eq(msg.get_boundary(), 'BOUNDARY')
160
161 def test_set_boundary(self):
162 eq = self.assertEqual
163 # This one has no existing boundary parameter, but the Content-Type:
164 # header appears fifth.
165 msg = self._msgobj('msg_01.txt')
166 msg.set_boundary('BOUNDARY')
167 header, value = msg.items()[4]
168 eq(header.lower(), 'content-type')
169 eq(value, 'text/plain; charset="us-ascii"; boundary="BOUNDARY"')
170 # This one has a Content-Type: header, with a boundary, stuck in the
171 # middle of its headers. Make sure the order is preserved; it should
172 # be fifth.
173 msg = self._msgobj('msg_04.txt')
174 msg.set_boundary('BOUNDARY')
175 header, value = msg.items()[4]
176 eq(header.lower(), 'content-type')
177 eq(value, 'multipart/mixed; boundary="BOUNDARY"')
178 # And this one has no Content-Type: header at all.
179 msg = self._msgobj('msg_03.txt')
180 self.assertRaises(errors.HeaderParseError,
181 msg.set_boundary, 'BOUNDARY')
182
R. David Murray73a559d2010-12-21 18:07:59 +0000183 def test_make_boundary(self):
184 msg = MIMEMultipart('form-data')
185 # Note that when the boundary gets created is an implementation
186 # detail and might change.
187 self.assertEqual(msg.items()[0][1], 'multipart/form-data')
188 # Trigger creation of boundary
189 msg.as_string()
190 self.assertEqual(msg.items()[0][1][:33],
191 'multipart/form-data; boundary="==')
192 # XXX: there ought to be tests of the uniqueness of the boundary, too.
193
R. David Murray57c45ac2010-02-21 04:39:40 +0000194 def test_message_rfc822_only(self):
195 # Issue 7970: message/rfc822 not in multipart parsed by
196 # HeaderParser caused an exception when flattened.
Brett Cannon384917a2010-10-29 23:08:36 +0000197 with openfile(findfile('msg_46.txt')) as fp:
198 msgdata = fp.read()
R. David Murray57c45ac2010-02-21 04:39:40 +0000199 parser = HeaderParser()
200 msg = parser.parsestr(msgdata)
201 out = StringIO()
202 gen = Generator(out, True, 0)
203 gen.flatten(msg, False)
204 self.assertEqual(out.getvalue(), msgdata)
205
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000206 def test_get_decoded_payload(self):
207 eq = self.assertEqual
208 msg = self._msgobj('msg_10.txt')
209 # The outer message is a multipart
210 eq(msg.get_payload(decode=True), None)
211 # Subpart 1 is 7bit encoded
212 eq(msg.get_payload(0).get_payload(decode=True),
213 b'This is a 7bit encoded message.\n')
214 # Subpart 2 is quopri
215 eq(msg.get_payload(1).get_payload(decode=True),
216 b'\xa1This is a Quoted Printable encoded message!\n')
217 # Subpart 3 is base64
218 eq(msg.get_payload(2).get_payload(decode=True),
219 b'This is a Base64 encoded message.')
R. David Murray57a4b982010-03-08 02:17:03 +0000220 # Subpart 4 is base64 with a trailing newline, which
221 # used to be stripped (issue 7143).
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000222 eq(msg.get_payload(3).get_payload(decode=True),
R. David Murray57a4b982010-03-08 02:17:03 +0000223 b'This is a Base64 encoded message.\n')
224 # Subpart 5 has no Content-Transfer-Encoding: header.
225 eq(msg.get_payload(4).get_payload(decode=True),
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000226 b'This has no Content-Transfer-Encoding: header.\n')
227
228 def test_get_decoded_uu_payload(self):
229 eq = self.assertEqual
230 msg = Message()
231 msg.set_payload('begin 666 -\n+:&5L;&\\@=V]R;&0 \n \nend\n')
232 for cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):
233 msg['content-transfer-encoding'] = cte
234 eq(msg.get_payload(decode=True), b'hello world')
235 # Now try some bogus data
236 msg.set_payload('foo')
237 eq(msg.get_payload(decode=True), b'foo')
238
R David Murraya2860e82011-04-16 09:20:30 -0400239 def test_get_payload_n_raises_on_non_multipart(self):
240 msg = Message()
241 self.assertRaises(TypeError, msg.get_payload, 1)
242
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000243 def test_decoded_generator(self):
244 eq = self.assertEqual
245 msg = self._msgobj('msg_07.txt')
246 with openfile('msg_17.txt') as fp:
247 text = fp.read()
248 s = StringIO()
249 g = DecodedGenerator(s)
250 g.flatten(msg)
251 eq(s.getvalue(), text)
252
253 def test__contains__(self):
254 msg = Message()
255 msg['From'] = 'Me'
256 msg['to'] = 'You'
257 # Check for case insensitivity
Benjamin Petersonc9c0f202009-06-30 23:06:06 +0000258 self.assertTrue('from' in msg)
259 self.assertTrue('From' in msg)
260 self.assertTrue('FROM' in msg)
261 self.assertTrue('to' in msg)
262 self.assertTrue('To' in msg)
263 self.assertTrue('TO' in msg)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000264
265 def test_as_string(self):
266 eq = self.ndiffAssertEqual
267 msg = self._msgobj('msg_01.txt')
268 with openfile('msg_01.txt') as fp:
269 text = fp.read()
270 eq(text, str(msg))
271 fullrepr = msg.as_string(unixfrom=True)
272 lines = fullrepr.split('\n')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +0000273 self.assertTrue(lines[0].startswith('From '))
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000274 eq(text, NL.join(lines[1:]))
275
276 def test_bad_param(self):
277 msg = email.message_from_string("Content-Type: blarg; baz; boo\n")
278 self.assertEqual(msg.get_param('baz'), '')
279
280 def test_missing_filename(self):
281 msg = email.message_from_string("From: foo\n")
282 self.assertEqual(msg.get_filename(), None)
283
284 def test_bogus_filename(self):
285 msg = email.message_from_string(
286 "Content-Disposition: blarg; filename\n")
287 self.assertEqual(msg.get_filename(), '')
288
289 def test_missing_boundary(self):
290 msg = email.message_from_string("From: foo\n")
291 self.assertEqual(msg.get_boundary(), None)
292
293 def test_get_params(self):
294 eq = self.assertEqual
295 msg = email.message_from_string(
296 'X-Header: foo=one; bar=two; baz=three\n')
297 eq(msg.get_params(header='x-header'),
298 [('foo', 'one'), ('bar', 'two'), ('baz', 'three')])
299 msg = email.message_from_string(
300 'X-Header: foo; bar=one; baz=two\n')
301 eq(msg.get_params(header='x-header'),
302 [('foo', ''), ('bar', 'one'), ('baz', 'two')])
303 eq(msg.get_params(), None)
304 msg = email.message_from_string(
305 'X-Header: foo; bar="one"; baz=two\n')
306 eq(msg.get_params(header='x-header'),
307 [('foo', ''), ('bar', 'one'), ('baz', 'two')])
308
309 def test_get_param_liberal(self):
310 msg = Message()
311 msg['Content-Type'] = 'Content-Type: Multipart/mixed; boundary = "CPIMSSMTPC06p5f3tG"'
312 self.assertEqual(msg.get_param('boundary'), 'CPIMSSMTPC06p5f3tG')
313
314 def test_get_param(self):
315 eq = self.assertEqual
316 msg = email.message_from_string(
317 "X-Header: foo=one; bar=two; baz=three\n")
318 eq(msg.get_param('bar', header='x-header'), 'two')
319 eq(msg.get_param('quuz', header='x-header'), None)
320 eq(msg.get_param('quuz'), None)
321 msg = email.message_from_string(
322 'X-Header: foo; bar="one"; baz=two\n')
323 eq(msg.get_param('foo', header='x-header'), '')
324 eq(msg.get_param('bar', header='x-header'), 'one')
325 eq(msg.get_param('baz', header='x-header'), 'two')
326 # XXX: We are not RFC-2045 compliant! We cannot parse:
327 # msg["Content-Type"] = 'text/plain; weird="hey; dolly? [you] @ <\\"home\\">?"'
328 # msg.get_param("weird")
329 # yet.
330
331 def test_get_param_funky_continuation_lines(self):
332 msg = self._msgobj('msg_22.txt')
333 self.assertEqual(msg.get_payload(1).get_param('name'), 'wibble.JPG')
334
335 def test_get_param_with_semis_in_quotes(self):
336 msg = email.message_from_string(
337 'Content-Type: image/pjpeg; name="Jim&amp;&amp;Jill"\n')
338 self.assertEqual(msg.get_param('name'), 'Jim&amp;&amp;Jill')
339 self.assertEqual(msg.get_param('name', unquote=False),
340 '"Jim&amp;&amp;Jill"')
341
R. David Murrayd48739f2010-04-14 18:59:18 +0000342 def test_get_param_with_quotes(self):
343 msg = email.message_from_string(
344 'Content-Type: foo; bar*0="baz\\"foobar"; bar*1="\\"baz"')
345 self.assertEqual(msg.get_param('bar'), 'baz"foobar"baz')
346 msg = email.message_from_string(
347 "Content-Type: foo; bar*0=\"baz\\\"foobar\"; bar*1=\"\\\"baz\"")
348 self.assertEqual(msg.get_param('bar'), 'baz"foobar"baz')
349
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000350 def test_field_containment(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +0000351 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000352 msg = email.message_from_string('Header: exists')
353 unless('header' in msg)
354 unless('Header' in msg)
355 unless('HEADER' in msg)
Benjamin Petersonc9c0f202009-06-30 23:06:06 +0000356 self.assertFalse('headerx' in msg)
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000357
358 def test_set_param(self):
359 eq = self.assertEqual
360 msg = Message()
361 msg.set_param('charset', 'iso-2022-jp')
362 eq(msg.get_param('charset'), 'iso-2022-jp')
363 msg.set_param('importance', 'high value')
364 eq(msg.get_param('importance'), 'high value')
365 eq(msg.get_param('importance', unquote=False), '"high value"')
366 eq(msg.get_params(), [('text/plain', ''),
367 ('charset', 'iso-2022-jp'),
368 ('importance', 'high value')])
369 eq(msg.get_params(unquote=False), [('text/plain', ''),
370 ('charset', '"iso-2022-jp"'),
371 ('importance', '"high value"')])
372 msg.set_param('charset', 'iso-9999-xx', header='X-Jimmy')
373 eq(msg.get_param('charset', header='X-Jimmy'), 'iso-9999-xx')
374
375 def test_del_param(self):
376 eq = self.assertEqual
377 msg = self._msgobj('msg_05.txt')
378 eq(msg.get_params(),
379 [('multipart/report', ''), ('report-type', 'delivery-status'),
380 ('boundary', 'D1690A7AC1.996856090/mail.example.com')])
381 old_val = msg.get_param("report-type")
382 msg.del_param("report-type")
383 eq(msg.get_params(),
384 [('multipart/report', ''),
385 ('boundary', 'D1690A7AC1.996856090/mail.example.com')])
386 msg.set_param("report-type", old_val)
387 eq(msg.get_params(),
388 [('multipart/report', ''),
389 ('boundary', 'D1690A7AC1.996856090/mail.example.com'),
390 ('report-type', old_val)])
391
392 def test_del_param_on_other_header(self):
393 msg = Message()
394 msg.add_header('Content-Disposition', 'attachment', filename='bud.gif')
395 msg.del_param('filename', 'content-disposition')
396 self.assertEqual(msg['content-disposition'], 'attachment')
397
R David Murraya2860e82011-04-16 09:20:30 -0400398 def test_del_param_on_nonexistent_header(self):
399 msg = Message()
400 msg.del_param('filename', 'content-disposition')
401
402 def test_del_nonexistent_param(self):
403 msg = Message()
404 msg.add_header('Content-Type', 'text/plain', charset='utf-8')
405 existing_header = msg['Content-Type']
406 msg.del_param('foobar', header='Content-Type')
407 self.assertEqual(msg['Content-Type'], 'text/plain; charset="utf-8"')
408
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000409 def test_set_type(self):
410 eq = self.assertEqual
411 msg = Message()
412 self.assertRaises(ValueError, msg.set_type, 'text')
413 msg.set_type('text/plain')
414 eq(msg['content-type'], 'text/plain')
415 msg.set_param('charset', 'us-ascii')
416 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
417 msg.set_type('text/html')
418 eq(msg['content-type'], 'text/html; charset="us-ascii"')
419
420 def test_set_type_on_other_header(self):
421 msg = Message()
422 msg['X-Content-Type'] = 'text/plain'
423 msg.set_type('application/octet-stream', 'X-Content-Type')
424 self.assertEqual(msg['x-content-type'], 'application/octet-stream')
425
426 def test_get_content_type_missing(self):
427 msg = Message()
428 self.assertEqual(msg.get_content_type(), 'text/plain')
429
430 def test_get_content_type_missing_with_default_type(self):
431 msg = Message()
432 msg.set_default_type('message/rfc822')
433 self.assertEqual(msg.get_content_type(), 'message/rfc822')
434
435 def test_get_content_type_from_message_implicit(self):
436 msg = self._msgobj('msg_30.txt')
437 self.assertEqual(msg.get_payload(0).get_content_type(),
438 'message/rfc822')
439
440 def test_get_content_type_from_message_explicit(self):
441 msg = self._msgobj('msg_28.txt')
442 self.assertEqual(msg.get_payload(0).get_content_type(),
443 'message/rfc822')
444
445 def test_get_content_type_from_message_text_plain_implicit(self):
446 msg = self._msgobj('msg_03.txt')
447 self.assertEqual(msg.get_content_type(), 'text/plain')
448
449 def test_get_content_type_from_message_text_plain_explicit(self):
450 msg = self._msgobj('msg_01.txt')
451 self.assertEqual(msg.get_content_type(), 'text/plain')
452
453 def test_get_content_maintype_missing(self):
454 msg = Message()
455 self.assertEqual(msg.get_content_maintype(), 'text')
456
457 def test_get_content_maintype_missing_with_default_type(self):
458 msg = Message()
459 msg.set_default_type('message/rfc822')
460 self.assertEqual(msg.get_content_maintype(), 'message')
461
462 def test_get_content_maintype_from_message_implicit(self):
463 msg = self._msgobj('msg_30.txt')
464 self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message')
465
466 def test_get_content_maintype_from_message_explicit(self):
467 msg = self._msgobj('msg_28.txt')
468 self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message')
469
470 def test_get_content_maintype_from_message_text_plain_implicit(self):
471 msg = self._msgobj('msg_03.txt')
472 self.assertEqual(msg.get_content_maintype(), 'text')
473
474 def test_get_content_maintype_from_message_text_plain_explicit(self):
475 msg = self._msgobj('msg_01.txt')
476 self.assertEqual(msg.get_content_maintype(), 'text')
477
478 def test_get_content_subtype_missing(self):
479 msg = Message()
480 self.assertEqual(msg.get_content_subtype(), 'plain')
481
482 def test_get_content_subtype_missing_with_default_type(self):
483 msg = Message()
484 msg.set_default_type('message/rfc822')
485 self.assertEqual(msg.get_content_subtype(), 'rfc822')
486
487 def test_get_content_subtype_from_message_implicit(self):
488 msg = self._msgobj('msg_30.txt')
489 self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822')
490
491 def test_get_content_subtype_from_message_explicit(self):
492 msg = self._msgobj('msg_28.txt')
493 self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822')
494
495 def test_get_content_subtype_from_message_text_plain_implicit(self):
496 msg = self._msgobj('msg_03.txt')
497 self.assertEqual(msg.get_content_subtype(), 'plain')
498
499 def test_get_content_subtype_from_message_text_plain_explicit(self):
500 msg = self._msgobj('msg_01.txt')
501 self.assertEqual(msg.get_content_subtype(), 'plain')
502
503 def test_get_content_maintype_error(self):
504 msg = Message()
505 msg['Content-Type'] = 'no-slash-in-this-string'
506 self.assertEqual(msg.get_content_maintype(), 'text')
507
508 def test_get_content_subtype_error(self):
509 msg = Message()
510 msg['Content-Type'] = 'no-slash-in-this-string'
511 self.assertEqual(msg.get_content_subtype(), 'plain')
512
513 def test_replace_header(self):
514 eq = self.assertEqual
515 msg = Message()
516 msg.add_header('First', 'One')
517 msg.add_header('Second', 'Two')
518 msg.add_header('Third', 'Three')
519 eq(msg.keys(), ['First', 'Second', 'Third'])
520 eq(msg.values(), ['One', 'Two', 'Three'])
521 msg.replace_header('Second', 'Twenty')
522 eq(msg.keys(), ['First', 'Second', 'Third'])
523 eq(msg.values(), ['One', 'Twenty', 'Three'])
524 msg.add_header('First', 'Eleven')
525 msg.replace_header('First', 'One Hundred')
526 eq(msg.keys(), ['First', 'Second', 'Third', 'First'])
527 eq(msg.values(), ['One Hundred', 'Twenty', 'Three', 'Eleven'])
528 self.assertRaises(KeyError, msg.replace_header, 'Fourth', 'Missing')
529
530 def test_broken_base64_payload(self):
531 x = 'AwDp0P7//y6LwKEAcPa/6Q=9'
532 msg = Message()
533 msg['content-type'] = 'audio/x-midi'
534 msg['content-transfer-encoding'] = 'base64'
535 msg.set_payload(x)
536 self.assertEqual(msg.get_payload(decode=True),
Guido van Rossum9604e662007-08-30 03:46:43 +0000537 bytes(x, 'raw-unicode-escape'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000538
R David Murraya2860e82011-04-16 09:20:30 -0400539 def test_broken_unicode_payload(self):
540 # This test improves coverage but is not a compliance test.
541 # The behavior in this situation is currently undefined by the API.
542 x = 'this is a br\xf6ken thing to do'
543 msg = Message()
544 msg['content-type'] = 'text/plain'
545 msg['content-transfer-encoding'] = '8bit'
546 msg.set_payload(x)
547 self.assertEqual(msg.get_payload(decode=True),
548 bytes(x, 'raw-unicode-escape'))
549
550 def test_questionable_bytes_payload(self):
551 # This test improves coverage but is not a compliance test,
552 # since it involves poking inside the black box.
553 x = 'this is a quéstionable thing to do'.encode('utf-8')
554 msg = Message()
555 msg['content-type'] = 'text/plain; charset="utf-8"'
556 msg['content-transfer-encoding'] = '8bit'
557 msg._payload = x
558 self.assertEqual(msg.get_payload(decode=True), x)
559
R. David Murray7ec754b2010-12-13 23:51:19 +0000560 # Issue 1078919
561 def test_ascii_add_header(self):
562 msg = Message()
563 msg.add_header('Content-Disposition', 'attachment',
564 filename='bud.gif')
565 self.assertEqual('attachment; filename="bud.gif"',
566 msg['Content-Disposition'])
567
568 def test_noascii_add_header(self):
569 msg = Message()
570 msg.add_header('Content-Disposition', 'attachment',
571 filename="Fußballer.ppt")
572 self.assertEqual(
R. David Murraydfd7eb02010-12-24 22:36:49 +0000573 'attachment; filename*=utf-8\'\'Fu%C3%9Fballer.ppt',
R. David Murray7ec754b2010-12-13 23:51:19 +0000574 msg['Content-Disposition'])
575
576 def test_nonascii_add_header_via_triple(self):
577 msg = Message()
578 msg.add_header('Content-Disposition', 'attachment',
579 filename=('iso-8859-1', '', 'Fußballer.ppt'))
580 self.assertEqual(
R. David Murraydfd7eb02010-12-24 22:36:49 +0000581 'attachment; filename*=iso-8859-1\'\'Fu%DFballer.ppt',
582 msg['Content-Disposition'])
583
584 def test_ascii_add_header_with_tspecial(self):
585 msg = Message()
586 msg.add_header('Content-Disposition', 'attachment',
587 filename="windows [filename].ppt")
588 self.assertEqual(
589 'attachment; filename="windows [filename].ppt"',
590 msg['Content-Disposition'])
591
592 def test_nonascii_add_header_with_tspecial(self):
593 msg = Message()
594 msg.add_header('Content-Disposition', 'attachment',
595 filename="Fußballer [filename].ppt")
596 self.assertEqual(
597 "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
R. David Murray7ec754b2010-12-13 23:51:19 +0000598 msg['Content-Disposition'])
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000599
R David Murraya2860e82011-04-16 09:20:30 -0400600 def test_add_header_with_name_only_param(self):
601 msg = Message()
602 msg.add_header('Content-Disposition', 'inline', foo_bar=None)
603 self.assertEqual("inline; foo-bar", msg['Content-Disposition'])
604
605 def test_add_header_with_no_value(self):
606 msg = Message()
607 msg.add_header('X-Status', None)
608 self.assertEqual('', msg['X-Status'])
609
R. David Murray5b2d9dd2011-01-09 02:35:24 +0000610 # Issue 5871: reject an attempt to embed a header inside a header value
611 # (header injection attack).
612 def test_embeded_header_via_Header_rejected(self):
613 msg = Message()
614 msg['Dummy'] = Header('dummy\nX-Injected-Header: test')
615 self.assertRaises(errors.HeaderParseError, msg.as_string)
616
617 def test_embeded_header_via_string_rejected(self):
618 msg = Message()
619 msg['Dummy'] = 'dummy\nX-Injected-Header: test'
620 self.assertRaises(errors.HeaderParseError, msg.as_string)
621
R David Murray7441a7a2012-03-14 02:59:51 -0400622 def test_unicode_header_defaults_to_utf8_encoding(self):
623 # Issue 14291
624 m = MIMEText('abc\n')
625 m['Subject'] = 'É test'
626 self.assertEqual(str(m),textwrap.dedent("""\
627 Content-Type: text/plain; charset="us-ascii"
628 MIME-Version: 1.0
629 Content-Transfer-Encoding: 7bit
630 Subject: =?utf-8?q?=C3=89_test?=
631
632 abc
633 """))
634
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000635# Test the email.encoders module
636class TestEncoders(unittest.TestCase):
R David Murray6d94bd42011-03-16 15:52:22 -0400637
638 def test_EncodersEncode_base64(self):
639 with openfile('PyBanner048.gif', 'rb') as fp:
640 bindata = fp.read()
641 mimed = email.mime.image.MIMEImage(bindata)
642 base64ed = mimed.get_payload()
643 # the transfer-encoded body lines should all be <=76 characters
644 lines = base64ed.split('\n')
645 self.assertLessEqual(max([ len(x) for x in lines ]), 76)
646
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000647 def test_encode_empty_payload(self):
648 eq = self.assertEqual
649 msg = Message()
650 msg.set_charset('us-ascii')
651 eq(msg['content-transfer-encoding'], '7bit')
652
653 def test_default_cte(self):
654 eq = self.assertEqual
Ezio Melottic303c122010-04-22 11:57:12 +0000655 # 7bit data and the default us-ascii _charset
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000656 msg = MIMEText('hello world')
657 eq(msg['content-transfer-encoding'], '7bit')
Ezio Melottic303c122010-04-22 11:57:12 +0000658 # Similar, but with 8bit data
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000659 msg = MIMEText('hello \xf8 world')
660 eq(msg['content-transfer-encoding'], '8bit')
661 # And now with a different charset
662 msg = MIMEText('hello \xf8 world', _charset='iso-8859-1')
663 eq(msg['content-transfer-encoding'], 'quoted-printable')
664
R. David Murraye85200d2010-05-06 01:41:14 +0000665 def test_encode7or8bit(self):
666 # Make sure a charset whose input character set is 8bit but
667 # whose output character set is 7bit gets a transfer-encoding
668 # of 7bit.
669 eq = self.assertEqual
R. David Murray850fc852010-06-03 01:58:28 +0000670 msg = MIMEText('文', _charset='euc-jp')
R. David Murraye85200d2010-05-06 01:41:14 +0000671 eq(msg['content-transfer-encoding'], '7bit')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000672
Ezio Melottib3aedd42010-11-20 19:04:17 +0000673
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000674# Test long header wrapping
675class TestLongHeaders(TestEmailBase):
R David Murray01581ee2011-04-18 10:04:34 -0400676
677 maxDiff = None
678
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000679 def test_split_long_continuation(self):
680 eq = self.ndiffAssertEqual
681 msg = email.message_from_string("""\
682Subject: bug demonstration
683\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
684\tmore text
685
686test
687""")
688 sfp = StringIO()
689 g = Generator(sfp)
690 g.flatten(msg)
691 eq(sfp.getvalue(), """\
692Subject: bug demonstration
693\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
694\tmore text
695
696test
697""")
698
699 def test_another_long_almost_unsplittable_header(self):
700 eq = self.ndiffAssertEqual
701 hstr = """\
702bug demonstration
703\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
704\tmore text"""
705 h = Header(hstr, continuation_ws='\t')
706 eq(h.encode(), """\
707bug demonstration
708\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
709\tmore text""")
710 h = Header(hstr.replace('\t', ' '))
711 eq(h.encode(), """\
712bug demonstration
713 12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
714 more text""")
715
716 def test_long_nonstring(self):
717 eq = self.ndiffAssertEqual
718 g = Charset("iso-8859-1")
719 cz = Charset("iso-8859-2")
720 utf8 = Charset("utf-8")
721 g_head = (b'Die Mieter treten hier ein werden mit einem Foerderband '
722 b'komfortabel den Korridor entlang, an s\xfcdl\xfcndischen '
723 b'Wandgem\xe4lden vorbei, gegen die rotierenden Klingen '
724 b'bef\xf6rdert. ')
725 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
726 b'd\xf9vtipu.. ')
727 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
728 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
729 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
730 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
731 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
732 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
733 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
734 '\u3044\u307e\u3059\u3002')
735 h = Header(g_head, g, header_name='Subject')
736 h.append(cz_head, cz)
737 h.append(utf8_head, utf8)
738 msg = Message()
739 msg['Subject'] = h
740 sfp = StringIO()
741 g = Generator(sfp)
742 g.flatten(msg)
743 eq(sfp.getvalue(), """\
Guido van Rossum9604e662007-08-30 03:46:43 +0000744Subject: =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderb?=
745 =?iso-8859-1?q?and_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen?=
746 =?iso-8859-1?q?_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef?=
747 =?iso-8859-1?q?=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hrouti?=
748 =?iso-8859-2?q?ly_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
749 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC5LiA?=
750 =?utf-8?b?6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn44Gf44KJ?=
751 =?utf-8?b?44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFzIE51bnN0dWNr?=
752 =?utf-8?b?IGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5kIGRhcyBPZGVyIGRp?=
753 =?utf-8?b?ZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIDjgaPjgabjgYTjgb7jgZk=?=
754 =?utf-8?b?44CC?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000755
756""")
Guido van Rossum9604e662007-08-30 03:46:43 +0000757 eq(h.encode(maxlinelen=76), """\
758=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerde?=
759 =?iso-8859-1?q?rband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndis?=
760 =?iso-8859-1?q?chen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klinge?=
761 =?iso-8859-1?q?n_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se?=
762 =?iso-8859-2?q?_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?=
763 =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb?=
764 =?utf-8?b?44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go?=
765 =?utf-8?b?44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBp?=
766 =?utf-8?b?c3QgZGFzIE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWlo?=
767 =?utf-8?b?ZXJodW5kIGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI0=?=
768 =?utf-8?b?44Go6KiA44Gj44Gm44GE44G+44GZ44CC?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000769
770 def test_long_header_encode(self):
771 eq = self.ndiffAssertEqual
772 h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
773 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
774 header_name='X-Foobar-Spoink-Defrobnit')
775 eq(h.encode(), '''\
776wasnipoop; giraffes="very-long-necked-animals";
777 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
778
779 def test_long_header_encode_with_tab_continuation_is_just_a_hint(self):
780 eq = self.ndiffAssertEqual
781 h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
782 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
783 header_name='X-Foobar-Spoink-Defrobnit',
784 continuation_ws='\t')
785 eq(h.encode(), '''\
786wasnipoop; giraffes="very-long-necked-animals";
787 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
788
789 def test_long_header_encode_with_tab_continuation(self):
790 eq = self.ndiffAssertEqual
791 h = Header('wasnipoop; giraffes="very-long-necked-animals";\t'
792 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
793 header_name='X-Foobar-Spoink-Defrobnit',
794 continuation_ws='\t')
795 eq(h.encode(), '''\
796wasnipoop; giraffes="very-long-necked-animals";
797\tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
798
R David Murray3a6152f2011-03-14 21:13:03 -0400799 def test_header_encode_with_different_output_charset(self):
800 h = Header('文', 'euc-jp')
801 self.assertEqual(h.encode(), "=?iso-2022-jp?b?GyRCSjgbKEI=?=")
802
803 def test_long_header_encode_with_different_output_charset(self):
804 h = Header(b'test-ja \xa4\xd8\xc5\xea\xb9\xc6\xa4\xb5\xa4\xec\xa4'
805 b'\xbf\xa5\xe1\xa1\xbc\xa5\xeb\xa4\xcf\xbb\xca\xb2\xf1\xbc\xd4'
806 b'\xa4\xce\xbe\xb5\xc7\xa7\xa4\xf2\xc2\xd4\xa4\xc3\xa4\xc6\xa4'
807 b'\xa4\xa4\xde\xa4\xb9'.decode('euc-jp'), 'euc-jp')
808 res = """\
809=?iso-2022-jp?b?dGVzdC1qYSAbJEIkWEVqOUYkNSRsJD8lYSE8JWskTztKMnE8VCROPjUbKEI=?=
810 =?iso-2022-jp?b?GyRCRyckckJUJEMkRiQkJF4kORsoQg==?="""
811 self.assertEqual(h.encode(), res)
812
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000813 def test_header_splitter(self):
814 eq = self.ndiffAssertEqual
815 msg = MIMEText('')
816 # It'd be great if we could use add_header() here, but that doesn't
817 # guarantee an order of the parameters.
818 msg['X-Foobar-Spoink-Defrobnit'] = (
819 'wasnipoop; giraffes="very-long-necked-animals"; '
820 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"')
821 sfp = StringIO()
822 g = Generator(sfp)
823 g.flatten(msg)
824 eq(sfp.getvalue(), '''\
825Content-Type: text/plain; charset="us-ascii"
826MIME-Version: 1.0
827Content-Transfer-Encoding: 7bit
828X-Foobar-Spoink-Defrobnit: wasnipoop; giraffes="very-long-necked-animals";
829 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"
830
831''')
832
833 def test_no_semis_header_splitter(self):
834 eq = self.ndiffAssertEqual
835 msg = Message()
836 msg['From'] = 'test@dom.ain'
837 msg['References'] = SPACE.join('<%d@dom.ain>' % i for i in range(10))
838 msg.set_payload('Test')
839 sfp = StringIO()
840 g = Generator(sfp)
841 g.flatten(msg)
842 eq(sfp.getvalue(), """\
843From: test@dom.ain
844References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain>
845 <5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain>
846
847Test""")
848
R David Murray7da4db12011-04-07 20:37:17 -0400849 def test_last_split_chunk_does_not_fit(self):
850 eq = self.ndiffAssertEqual
851 h = Header('Subject: the first part of this is short, but_the_second'
852 '_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line'
853 '_all_by_itself')
854 eq(h.encode(), """\
855Subject: the first part of this is short,
856 but_the_second_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself""")
857
858 def test_splittable_leading_char_followed_by_overlong_unsplitable(self):
859 eq = self.ndiffAssertEqual
860 h = Header(', but_the_second'
861 '_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line'
862 '_all_by_itself')
863 eq(h.encode(), """\
864,
865 but_the_second_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself""")
866
867 def test_multiple_splittable_leading_char_followed_by_overlong_unsplitable(self):
868 eq = self.ndiffAssertEqual
869 h = Header(', , but_the_second'
870 '_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line'
871 '_all_by_itself')
872 eq(h.encode(), """\
873, ,
874 but_the_second_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself""")
875
876 def test_trailing_splitable_on_overlong_unsplitable(self):
877 eq = self.ndiffAssertEqual
878 h = Header('this_part_does_not_fit_within_maxlinelen_and_thus_should_'
879 'be_on_a_line_all_by_itself;')
880 eq(h.encode(), "this_part_does_not_fit_within_maxlinelen_and_thus_should_"
881 "be_on_a_line_all_by_itself;")
882
883 def test_trailing_splitable_on_overlong_unsplitable_with_leading_splitable(self):
884 eq = self.ndiffAssertEqual
885 h = Header('; '
886 'this_part_does_not_fit_within_maxlinelen_and_thus_should_'
R David Murray01581ee2011-04-18 10:04:34 -0400887 'be_on_a_line_all_by_itself; ')
R David Murray7da4db12011-04-07 20:37:17 -0400888 eq(h.encode(), """\
889;
R David Murray01581ee2011-04-18 10:04:34 -0400890 this_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself; """)
R David Murray7da4db12011-04-07 20:37:17 -0400891
R David Murraye1292a22011-04-07 20:54:03 -0400892 def test_long_header_with_multiple_sequential_split_chars(self):
R David Murraye1292a22011-04-07 20:54:03 -0400893 eq = self.ndiffAssertEqual
894 h = Header('This is a long line that has two whitespaces in a row. '
895 'This used to cause truncation of the header when folded')
896 eq(h.encode(), """\
897This is a long line that has two whitespaces in a row. This used to cause
898 truncation of the header when folded""")
899
R David Murray01581ee2011-04-18 10:04:34 -0400900 def test_splitter_split_on_punctuation_only_if_fws(self):
901 eq = self.ndiffAssertEqual
902 h = Header('thisverylongheaderhas;semicolons;and,commas,but'
903 'they;arenotlegal;fold,points')
904 eq(h.encode(), "thisverylongheaderhas;semicolons;and,commas,butthey;"
905 "arenotlegal;fold,points")
906
907 def test_leading_splittable_in_the_middle_just_before_overlong_last_part(self):
908 eq = self.ndiffAssertEqual
909 h = Header('this is a test where we need to have more than one line '
910 'before; our final line that is just too big to fit;; '
911 'this_part_does_not_fit_within_maxlinelen_and_thus_should_'
912 'be_on_a_line_all_by_itself;')
913 eq(h.encode(), """\
914this is a test where we need to have more than one line before;
915 our final line that is just too big to fit;;
916 this_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself;""")
917
918 def test_overlong_last_part_followed_by_split_point(self):
919 eq = self.ndiffAssertEqual
920 h = Header('this_part_does_not_fit_within_maxlinelen_and_thus_should_'
921 'be_on_a_line_all_by_itself ')
922 eq(h.encode(), "this_part_does_not_fit_within_maxlinelen_and_thus_"
923 "should_be_on_a_line_all_by_itself ")
924
925 def test_multiline_with_overlong_parts_separated_by_two_split_points(self):
926 eq = self.ndiffAssertEqual
927 h = Header('this_is_a__test_where_we_need_to_have_more_than_one_line_'
928 'before_our_final_line_; ; '
929 'this_part_does_not_fit_within_maxlinelen_and_thus_should_'
930 'be_on_a_line_all_by_itself; ')
931 eq(h.encode(), """\
932this_is_a__test_where_we_need_to_have_more_than_one_line_before_our_final_line_;
933 ;
934 this_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself; """)
935
936 def test_multiline_with_overlong_last_part_followed_by_split_point(self):
937 eq = self.ndiffAssertEqual
938 h = Header('this is a test where we need to have more than one line '
939 'before our final line; ; '
940 'this_part_does_not_fit_within_maxlinelen_and_thus_should_'
941 'be_on_a_line_all_by_itself; ')
942 eq(h.encode(), """\
943this is a test where we need to have more than one line before our final line;
944 ;
945 this_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself; """)
946
947 def test_long_header_with_whitespace_runs(self):
948 eq = self.ndiffAssertEqual
949 msg = Message()
950 msg['From'] = 'test@dom.ain'
951 msg['References'] = SPACE.join(['<foo@dom.ain> '] * 10)
952 msg.set_payload('Test')
953 sfp = StringIO()
954 g = Generator(sfp)
955 g.flatten(msg)
956 eq(sfp.getvalue(), """\
957From: test@dom.ain
958References: <foo@dom.ain> <foo@dom.ain> <foo@dom.ain> <foo@dom.ain>
959 <foo@dom.ain> <foo@dom.ain> <foo@dom.ain> <foo@dom.ain>
960 <foo@dom.ain> <foo@dom.ain>\x20\x20
961
962Test""")
963
964 def test_long_run_with_semi_header_splitter(self):
965 eq = self.ndiffAssertEqual
966 msg = Message()
967 msg['From'] = 'test@dom.ain'
968 msg['References'] = SPACE.join(['<foo@dom.ain>'] * 10) + '; abc'
969 msg.set_payload('Test')
970 sfp = StringIO()
971 g = Generator(sfp)
972 g.flatten(msg)
973 eq(sfp.getvalue(), """\
974From: test@dom.ain
975References: <foo@dom.ain> <foo@dom.ain> <foo@dom.ain> <foo@dom.ain>
976 <foo@dom.ain> <foo@dom.ain> <foo@dom.ain> <foo@dom.ain> <foo@dom.ain>
977 <foo@dom.ain>; abc
978
979Test""")
980
981 def test_splitter_split_on_punctuation_only_if_fws(self):
982 eq = self.ndiffAssertEqual
983 msg = Message()
984 msg['From'] = 'test@dom.ain'
985 msg['References'] = ('thisverylongheaderhas;semicolons;and,commas,but'
986 'they;arenotlegal;fold,points')
987 msg.set_payload('Test')
988 sfp = StringIO()
989 g = Generator(sfp)
990 g.flatten(msg)
991 # XXX the space after the header should not be there.
992 eq(sfp.getvalue(), """\
993From: test@dom.ain
994References:\x20
995 thisverylongheaderhas;semicolons;and,commas,butthey;arenotlegal;fold,points
996
997Test""")
998
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000999 def test_no_split_long_header(self):
1000 eq = self.ndiffAssertEqual
1001 hstr = 'References: ' + 'x' * 80
Guido van Rossum9604e662007-08-30 03:46:43 +00001002 h = Header(hstr)
1003 # These come on two lines because Headers are really field value
1004 # classes and don't really know about their field names.
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001005 eq(h.encode(), """\
Guido van Rossum9604e662007-08-30 03:46:43 +00001006References:
1007 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""")
1008 h = Header('x' * 80)
1009 eq(h.encode(), 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001010
1011 def test_splitting_multiple_long_lines(self):
1012 eq = self.ndiffAssertEqual
1013 hstr = """\
1014from 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)
1015\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)
1016\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)
1017"""
1018 h = Header(hstr, continuation_ws='\t')
1019 eq(h.encode(), """\
1020from babylon.socal-raves.org (localhost [127.0.0.1]);
1021 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
1022 for <mailman-admin@babylon.socal-raves.org>;
1023 Sat, 2 Feb 2002 17:00:06 -0800 (PST)
1024\tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
1025 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
1026 for <mailman-admin@babylon.socal-raves.org>;
1027 Sat, 2 Feb 2002 17:00:06 -0800 (PST)
1028\tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
1029 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
1030 for <mailman-admin@babylon.socal-raves.org>;
1031 Sat, 2 Feb 2002 17:00:06 -0800 (PST)""")
1032
1033 def test_splitting_first_line_only_is_long(self):
1034 eq = self.ndiffAssertEqual
1035 hstr = """\
1036from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca)
1037\tby kronos.mems-exchange.org with esmtp (Exim 4.05)
1038\tid 17k4h5-00034i-00
1039\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400"""
1040 h = Header(hstr, maxlinelen=78, header_name='Received',
1041 continuation_ws='\t')
1042 eq(h.encode(), """\
1043from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93]
1044 helo=cthulhu.gerg.ca)
1045\tby kronos.mems-exchange.org with esmtp (Exim 4.05)
1046\tid 17k4h5-00034i-00
1047\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""")
1048
1049 def test_long_8bit_header(self):
1050 eq = self.ndiffAssertEqual
1051 msg = Message()
1052 h = Header('Britische Regierung gibt', 'iso-8859-1',
1053 header_name='Subject')
1054 h.append('gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte')
Guido van Rossum9604e662007-08-30 03:46:43 +00001055 eq(h.encode(maxlinelen=76), """\
1056=?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offs?=
1057 =?iso-8859-1?q?hore-Windkraftprojekte?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001058 msg['Subject'] = h
Guido van Rossum9604e662007-08-30 03:46:43 +00001059 eq(msg.as_string(maxheaderlen=76), """\
1060Subject: =?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offs?=
1061 =?iso-8859-1?q?hore-Windkraftprojekte?=
1062
1063""")
1064 eq(msg.as_string(maxheaderlen=0), """\
1065Subject: =?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offshore-Windkraftprojekte?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001066
1067""")
1068
1069 def test_long_8bit_header_no_charset(self):
1070 eq = self.ndiffAssertEqual
1071 msg = Message()
Barry Warsaw8c571042007-08-30 19:17:18 +00001072 header_string = ('Britische Regierung gibt gr\xfcnes Licht '
1073 'f\xfcr Offshore-Windkraftprojekte '
1074 '<a-very-long-address@example.com>')
1075 msg['Reply-To'] = header_string
R David Murray7441a7a2012-03-14 02:59:51 -04001076 eq(msg.as_string(maxheaderlen=78), """\
1077Reply-To: =?utf-8?q?Britische_Regierung_gibt_gr=C3=BCnes_Licht_f=C3=BCr_Offs?=
1078 =?utf-8?q?hore-Windkraftprojekte_=3Ca-very-long-address=40example=2Ecom=3E?=
1079
1080""")
Barry Warsaw8c571042007-08-30 19:17:18 +00001081 msg = Message()
R David Murray7441a7a2012-03-14 02:59:51 -04001082 msg['Reply-To'] = Header(header_string,
Barry Warsaw8c571042007-08-30 19:17:18 +00001083 header_name='Reply-To')
1084 eq(msg.as_string(maxheaderlen=78), """\
1085Reply-To: =?utf-8?q?Britische_Regierung_gibt_gr=C3=BCnes_Licht_f=C3=BCr_Offs?=
1086 =?utf-8?q?hore-Windkraftprojekte_=3Ca-very-long-address=40example=2Ecom=3E?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001087
1088""")
1089
1090 def test_long_to_header(self):
1091 eq = self.ndiffAssertEqual
1092 to = ('"Someone Test #A" <someone@eecs.umich.edu>,'
R David Murray01581ee2011-04-18 10:04:34 -04001093 '<someone@eecs.umich.edu>, '
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001094 '"Someone Test #B" <someone@umich.edu>, '
1095 '"Someone Test #C" <someone@eecs.umich.edu>, '
1096 '"Someone Test #D" <someone@eecs.umich.edu>')
1097 msg = Message()
1098 msg['To'] = to
1099 eq(msg.as_string(maxheaderlen=78), '''\
Guido van Rossum9604e662007-08-30 03:46:43 +00001100To: "Someone Test #A" <someone@eecs.umich.edu>,<someone@eecs.umich.edu>,
Barry Warsaw70d61ce2009-03-30 23:12:30 +00001101 "Someone Test #B" <someone@umich.edu>,
Guido van Rossum9604e662007-08-30 03:46:43 +00001102 "Someone Test #C" <someone@eecs.umich.edu>,
1103 "Someone Test #D" <someone@eecs.umich.edu>
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001104
1105''')
1106
1107 def test_long_line_after_append(self):
1108 eq = self.ndiffAssertEqual
1109 s = 'This is an example of string which has almost the limit of header length.'
1110 h = Header(s)
1111 h.append('Add another line.')
Guido van Rossum9604e662007-08-30 03:46:43 +00001112 eq(h.encode(maxlinelen=76), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001113This is an example of string which has almost the limit of header length.
1114 Add another line.""")
1115
1116 def test_shorter_line_with_append(self):
1117 eq = self.ndiffAssertEqual
1118 s = 'This is a shorter line.'
1119 h = Header(s)
1120 h.append('Add another sentence. (Surprise?)')
1121 eq(h.encode(),
1122 'This is a shorter line. Add another sentence. (Surprise?)')
1123
1124 def test_long_field_name(self):
1125 eq = self.ndiffAssertEqual
1126 fn = 'X-Very-Very-Very-Long-Header-Name'
Guido van Rossum9604e662007-08-30 03:46:43 +00001127 gs = ('Die Mieter treten hier ein werden mit einem Foerderband '
1128 'komfortabel den Korridor entlang, an s\xfcdl\xfcndischen '
1129 'Wandgem\xe4lden vorbei, gegen die rotierenden Klingen '
1130 'bef\xf6rdert. ')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001131 h = Header(gs, 'iso-8859-1', header_name=fn)
1132 # BAW: this seems broken because the first line is too long
Guido van Rossum9604e662007-08-30 03:46:43 +00001133 eq(h.encode(maxlinelen=76), """\
1134=?iso-8859-1?q?Die_Mieter_treten_hier_e?=
1135 =?iso-8859-1?q?in_werden_mit_einem_Foerderband_komfortabel_den_Korridor_e?=
1136 =?iso-8859-1?q?ntlang=2C_an_s=FCdl=FCndischen_Wandgem=E4lden_vorbei=2C_ge?=
1137 =?iso-8859-1?q?gen_die_rotierenden_Klingen_bef=F6rdert=2E_?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001138
1139 def test_long_received_header(self):
1140 h = ('from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) '
1141 'by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP; '
1142 'Wed, 05 Mar 2003 18:10:18 -0700')
1143 msg = Message()
1144 msg['Received-1'] = Header(h, continuation_ws='\t')
1145 msg['Received-2'] = h
Barry Warsawbef9d212007-08-31 10:55:37 +00001146 # This should be splitting on spaces not semicolons.
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001147 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
R David Murray01581ee2011-04-18 10:04:34 -04001148Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by
1149 hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
Barry Warsawbef9d212007-08-31 10:55:37 +00001150 Wed, 05 Mar 2003 18:10:18 -0700
R David Murray01581ee2011-04-18 10:04:34 -04001151Received-2: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by
1152 hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
Barry Warsawbef9d212007-08-31 10:55:37 +00001153 Wed, 05 Mar 2003 18:10:18 -0700
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001154
1155""")
1156
1157 def test_string_headerinst_eq(self):
1158 h = ('<15975.17901.207240.414604@sgigritzmann1.mathematik.'
1159 'tu-muenchen.de> (David Bremner\'s message of '
1160 '"Thu, 6 Mar 2003 13:58:21 +0100")')
1161 msg = Message()
1162 msg['Received-1'] = Header(h, header_name='Received-1',
1163 continuation_ws='\t')
1164 msg['Received-2'] = h
R David Murray01581ee2011-04-18 10:04:34 -04001165 # XXX The space after the ':' should not be there.
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001166 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
R David Murray01581ee2011-04-18 10:04:34 -04001167Received-1:\x20
1168 <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David
1169 Bremner's message of \"Thu, 6 Mar 2003 13:58:21 +0100\")
1170Received-2:\x20
1171 <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David
1172 Bremner's message of \"Thu, 6 Mar 2003 13:58:21 +0100\")
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001173
1174""")
1175
1176 def test_long_unbreakable_lines_with_continuation(self):
1177 eq = self.ndiffAssertEqual
1178 msg = Message()
1179 t = """\
1180iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
1181 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp"""
1182 msg['Face-1'] = t
1183 msg['Face-2'] = Header(t, header_name='Face-2')
R David Murray01581ee2011-04-18 10:04:34 -04001184 msg['Face-3'] = ' ' + t
Barry Warsawbef9d212007-08-31 10:55:37 +00001185 # XXX This splitting is all wrong. It the first value line should be
R David Murray01581ee2011-04-18 10:04:34 -04001186 # snug against the field name or the space after the header not there.
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001187 eq(msg.as_string(maxheaderlen=78), """\
Barry Warsawc5a6a302007-08-31 11:19:21 +00001188Face-1:\x20
Barry Warsaw70d61ce2009-03-30 23:12:30 +00001189 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001190 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
Barry Warsawc5a6a302007-08-31 11:19:21 +00001191Face-2:\x20
Barry Warsawbef9d212007-08-31 10:55:37 +00001192 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001193 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
R David Murray01581ee2011-04-18 10:04:34 -04001194Face-3:\x20
1195 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
1196 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001197
1198""")
1199
1200 def test_another_long_multiline_header(self):
1201 eq = self.ndiffAssertEqual
1202 m = ('Received: from siimage.com '
1203 '([172.25.1.3]) by zima.siliconimage.com with '
Guido van Rossum9604e662007-08-30 03:46:43 +00001204 'Microsoft SMTPSVC(5.0.2195.4905); '
1205 'Wed, 16 Oct 2002 07:41:11 -0700')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001206 msg = email.message_from_string(m)
1207 eq(msg.as_string(maxheaderlen=78), '''\
R David Murray01581ee2011-04-18 10:04:34 -04001208Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with
1209 Microsoft SMTPSVC(5.0.2195.4905); Wed, 16 Oct 2002 07:41:11 -0700
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001210
1211''')
1212
1213 def test_long_lines_with_different_header(self):
1214 eq = self.ndiffAssertEqual
1215 h = ('List-Unsubscribe: '
1216 '<http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,'
1217 ' <mailto:spamassassin-talk-request@lists.sourceforge.net'
1218 '?subject=unsubscribe>')
1219 msg = Message()
1220 msg['List'] = h
1221 msg['List'] = Header(h, header_name='List')
1222 eq(msg.as_string(maxheaderlen=78), """\
R David Murray01581ee2011-04-18 10:04:34 -04001223List: List-Unsubscribe:
1224 <http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
Barry Warsawbef9d212007-08-31 10:55:37 +00001225 <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
R David Murray01581ee2011-04-18 10:04:34 -04001226List: List-Unsubscribe:
1227 <http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
Barry Warsawbef9d212007-08-31 10:55:37 +00001228 <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001229
1230""")
1231
R. David Murray6f0022d2011-01-07 21:57:25 +00001232 def test_long_rfc2047_header_with_embedded_fws(self):
1233 h = Header(textwrap.dedent("""\
1234 We're going to pretend this header is in a non-ascii character set
1235 \tto see if line wrapping with encoded words and embedded
1236 folding white space works"""),
1237 charset='utf-8',
1238 header_name='Test')
1239 self.assertEqual(h.encode()+'\n', textwrap.dedent("""\
1240 =?utf-8?q?We=27re_going_to_pretend_this_header_is_in_a_non-ascii_chara?=
1241 =?utf-8?q?cter_set?=
1242 =?utf-8?q?_to_see_if_line_wrapping_with_encoded_words_and_embedded?=
1243 =?utf-8?q?_folding_white_space_works?=""")+'\n')
1244
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001245
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001246# Test mangling of "From " lines in the body of a message
1247class TestFromMangling(unittest.TestCase):
1248 def setUp(self):
1249 self.msg = Message()
1250 self.msg['From'] = 'aaa@bbb.org'
1251 self.msg.set_payload("""\
1252From the desk of A.A.A.:
1253Blah blah blah
1254""")
1255
1256 def test_mangled_from(self):
1257 s = StringIO()
1258 g = Generator(s, mangle_from_=True)
1259 g.flatten(self.msg)
1260 self.assertEqual(s.getvalue(), """\
1261From: aaa@bbb.org
1262
1263>From the desk of A.A.A.:
1264Blah blah blah
1265""")
1266
1267 def test_dont_mangle_from(self):
1268 s = StringIO()
1269 g = Generator(s, mangle_from_=False)
1270 g.flatten(self.msg)
1271 self.assertEqual(s.getvalue(), """\
1272From: aaa@bbb.org
1273
1274From the desk of A.A.A.:
1275Blah blah blah
1276""")
1277
1278
Ezio Melottib3aedd42010-11-20 19:04:17 +00001279
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001280# Test the basic MIMEAudio class
1281class TestMIMEAudio(unittest.TestCase):
1282 def setUp(self):
1283 # Make sure we pick up the audiotest.au that lives in email/test/data.
1284 # In Python, there's an audiotest.au living in Lib/test but that isn't
1285 # included in some binary distros that don't include the test
1286 # package. The trailing empty string on the .join() is significant
1287 # since findfile() will do a dirname().
1288 datadir = os.path.join(os.path.dirname(landmark), 'data', '')
1289 with open(findfile('audiotest.au', datadir), 'rb') as fp:
1290 self._audiodata = fp.read()
1291 self._au = MIMEAudio(self._audiodata)
1292
1293 def test_guess_minor_type(self):
1294 self.assertEqual(self._au.get_content_type(), 'audio/basic')
1295
1296 def test_encoding(self):
1297 payload = self._au.get_payload()
R. David Murray7da8f062010-06-04 16:11:08 +00001298 self.assertEqual(base64.decodebytes(bytes(payload, 'ascii')),
1299 self._audiodata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001300
1301 def test_checkSetMinor(self):
1302 au = MIMEAudio(self._audiodata, 'fish')
1303 self.assertEqual(au.get_content_type(), 'audio/fish')
1304
1305 def test_add_header(self):
1306 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001307 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001308 self._au.add_header('Content-Disposition', 'attachment',
1309 filename='audiotest.au')
1310 eq(self._au['content-disposition'],
1311 'attachment; filename="audiotest.au"')
1312 eq(self._au.get_params(header='content-disposition'),
1313 [('attachment', ''), ('filename', 'audiotest.au')])
1314 eq(self._au.get_param('filename', header='content-disposition'),
1315 'audiotest.au')
1316 missing = []
1317 eq(self._au.get_param('attachment', header='content-disposition'), '')
1318 unless(self._au.get_param('foo', failobj=missing,
1319 header='content-disposition') is missing)
1320 # Try some missing stuff
1321 unless(self._au.get_param('foobar', missing) is missing)
1322 unless(self._au.get_param('attachment', missing,
1323 header='foobar') is missing)
1324
1325
Ezio Melottib3aedd42010-11-20 19:04:17 +00001326
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001327# Test the basic MIMEImage class
1328class TestMIMEImage(unittest.TestCase):
1329 def setUp(self):
1330 with openfile('PyBanner048.gif', 'rb') as fp:
1331 self._imgdata = fp.read()
1332 self._im = MIMEImage(self._imgdata)
1333
1334 def test_guess_minor_type(self):
1335 self.assertEqual(self._im.get_content_type(), 'image/gif')
1336
1337 def test_encoding(self):
1338 payload = self._im.get_payload()
R. David Murray7da8f062010-06-04 16:11:08 +00001339 self.assertEqual(base64.decodebytes(bytes(payload, 'ascii')),
1340 self._imgdata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001341
1342 def test_checkSetMinor(self):
1343 im = MIMEImage(self._imgdata, 'fish')
1344 self.assertEqual(im.get_content_type(), 'image/fish')
1345
1346 def test_add_header(self):
1347 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001348 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001349 self._im.add_header('Content-Disposition', 'attachment',
1350 filename='dingusfish.gif')
1351 eq(self._im['content-disposition'],
1352 'attachment; filename="dingusfish.gif"')
1353 eq(self._im.get_params(header='content-disposition'),
1354 [('attachment', ''), ('filename', 'dingusfish.gif')])
1355 eq(self._im.get_param('filename', header='content-disposition'),
1356 'dingusfish.gif')
1357 missing = []
1358 eq(self._im.get_param('attachment', header='content-disposition'), '')
1359 unless(self._im.get_param('foo', failobj=missing,
1360 header='content-disposition') is missing)
1361 # Try some missing stuff
1362 unless(self._im.get_param('foobar', missing) is missing)
1363 unless(self._im.get_param('attachment', missing,
1364 header='foobar') is missing)
1365
1366
Ezio Melottib3aedd42010-11-20 19:04:17 +00001367
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001368# Test the basic MIMEApplication class
1369class TestMIMEApplication(unittest.TestCase):
1370 def test_headers(self):
1371 eq = self.assertEqual
Barry Warsaw8b2af272007-08-31 03:04:26 +00001372 msg = MIMEApplication(b'\xfa\xfb\xfc\xfd\xfe\xff')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001373 eq(msg.get_content_type(), 'application/octet-stream')
1374 eq(msg['content-transfer-encoding'], 'base64')
1375
1376 def test_body(self):
1377 eq = self.assertEqual
R David Murray6d94bd42011-03-16 15:52:22 -04001378 bytesdata = b'\xfa\xfb\xfc\xfd\xfe\xff'
1379 msg = MIMEApplication(bytesdata)
1380 # whitespace in the cte encoded block is RFC-irrelevant.
1381 eq(msg.get_payload().strip(), '+vv8/f7/')
1382 eq(msg.get_payload(decode=True), bytesdata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001383
1384
Ezio Melottib3aedd42010-11-20 19:04:17 +00001385
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001386# Test the basic MIMEText class
1387class TestMIMEText(unittest.TestCase):
1388 def setUp(self):
1389 self._msg = MIMEText('hello there')
1390
1391 def test_types(self):
1392 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001393 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001394 eq(self._msg.get_content_type(), 'text/plain')
1395 eq(self._msg.get_param('charset'), 'us-ascii')
1396 missing = []
1397 unless(self._msg.get_param('foobar', missing) is missing)
1398 unless(self._msg.get_param('charset', missing, header='foobar')
1399 is missing)
1400
1401 def test_payload(self):
1402 self.assertEqual(self._msg.get_payload(), 'hello there')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001403 self.assertTrue(not self._msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001404
1405 def test_charset(self):
1406 eq = self.assertEqual
1407 msg = MIMEText('hello there', _charset='us-ascii')
1408 eq(msg.get_charset().input_charset, 'us-ascii')
1409 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1410
R. David Murray850fc852010-06-03 01:58:28 +00001411 def test_7bit_input(self):
1412 eq = self.assertEqual
1413 msg = MIMEText('hello there', _charset='us-ascii')
1414 eq(msg.get_charset().input_charset, 'us-ascii')
1415 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1416
1417 def test_7bit_input_no_charset(self):
1418 eq = self.assertEqual
1419 msg = MIMEText('hello there')
1420 eq(msg.get_charset(), 'us-ascii')
1421 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1422 self.assertTrue('hello there' in msg.as_string())
1423
1424 def test_utf8_input(self):
1425 teststr = '\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
1426 eq = self.assertEqual
1427 msg = MIMEText(teststr, _charset='utf-8')
1428 eq(msg.get_charset().output_charset, 'utf-8')
1429 eq(msg['content-type'], 'text/plain; charset="utf-8"')
1430 eq(msg.get_payload(decode=True), teststr.encode('utf-8'))
1431
1432 @unittest.skip("can't fix because of backward compat in email5, "
1433 "will fix in email6")
1434 def test_utf8_input_no_charset(self):
1435 teststr = '\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
1436 self.assertRaises(UnicodeEncodeError, MIMEText, teststr)
1437
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001438
Ezio Melottib3aedd42010-11-20 19:04:17 +00001439
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001440# Test complicated multipart/* messages
1441class TestMultipart(TestEmailBase):
1442 def setUp(self):
1443 with openfile('PyBanner048.gif', 'rb') as fp:
1444 data = fp.read()
1445 container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
1446 image = MIMEImage(data, name='dingusfish.gif')
1447 image.add_header('content-disposition', 'attachment',
1448 filename='dingusfish.gif')
1449 intro = MIMEText('''\
1450Hi there,
1451
1452This is the dingus fish.
1453''')
1454 container.attach(intro)
1455 container.attach(image)
1456 container['From'] = 'Barry <barry@digicool.com>'
1457 container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>'
1458 container['Subject'] = 'Here is your dingus fish'
1459
1460 now = 987809702.54848599
1461 timetuple = time.localtime(now)
1462 if timetuple[-1] == 0:
1463 tzsecs = time.timezone
1464 else:
1465 tzsecs = time.altzone
1466 if tzsecs > 0:
1467 sign = '-'
1468 else:
1469 sign = '+'
1470 tzoffset = ' %s%04d' % (sign, tzsecs / 36)
1471 container['Date'] = time.strftime(
1472 '%a, %d %b %Y %H:%M:%S',
1473 time.localtime(now)) + tzoffset
1474 self._msg = container
1475 self._im = image
1476 self._txt = intro
1477
1478 def test_hierarchy(self):
1479 # convenience
1480 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001481 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001482 raises = self.assertRaises
1483 # tests
1484 m = self._msg
1485 unless(m.is_multipart())
1486 eq(m.get_content_type(), 'multipart/mixed')
1487 eq(len(m.get_payload()), 2)
1488 raises(IndexError, m.get_payload, 2)
1489 m0 = m.get_payload(0)
1490 m1 = m.get_payload(1)
1491 unless(m0 is self._txt)
1492 unless(m1 is self._im)
1493 eq(m.get_payload(), [m0, m1])
1494 unless(not m0.is_multipart())
1495 unless(not m1.is_multipart())
1496
1497 def test_empty_multipart_idempotent(self):
1498 text = """\
1499Content-Type: multipart/mixed; boundary="BOUNDARY"
1500MIME-Version: 1.0
1501Subject: A subject
1502To: aperson@dom.ain
1503From: bperson@dom.ain
1504
1505
1506--BOUNDARY
1507
1508
1509--BOUNDARY--
1510"""
1511 msg = Parser().parsestr(text)
1512 self.ndiffAssertEqual(text, msg.as_string())
1513
1514 def test_no_parts_in_a_multipart_with_none_epilogue(self):
1515 outer = MIMEBase('multipart', 'mixed')
1516 outer['Subject'] = 'A subject'
1517 outer['To'] = 'aperson@dom.ain'
1518 outer['From'] = 'bperson@dom.ain'
1519 outer.set_boundary('BOUNDARY')
1520 self.ndiffAssertEqual(outer.as_string(), '''\
1521Content-Type: multipart/mixed; boundary="BOUNDARY"
1522MIME-Version: 1.0
1523Subject: A subject
1524To: aperson@dom.ain
1525From: bperson@dom.ain
1526
1527--BOUNDARY
1528
1529--BOUNDARY--''')
1530
1531 def test_no_parts_in_a_multipart_with_empty_epilogue(self):
1532 outer = MIMEBase('multipart', 'mixed')
1533 outer['Subject'] = 'A subject'
1534 outer['To'] = 'aperson@dom.ain'
1535 outer['From'] = 'bperson@dom.ain'
1536 outer.preamble = ''
1537 outer.epilogue = ''
1538 outer.set_boundary('BOUNDARY')
1539 self.ndiffAssertEqual(outer.as_string(), '''\
1540Content-Type: multipart/mixed; boundary="BOUNDARY"
1541MIME-Version: 1.0
1542Subject: A subject
1543To: aperson@dom.ain
1544From: bperson@dom.ain
1545
1546
1547--BOUNDARY
1548
1549--BOUNDARY--
1550''')
1551
1552 def test_one_part_in_a_multipart(self):
1553 eq = self.ndiffAssertEqual
1554 outer = MIMEBase('multipart', 'mixed')
1555 outer['Subject'] = 'A subject'
1556 outer['To'] = 'aperson@dom.ain'
1557 outer['From'] = 'bperson@dom.ain'
1558 outer.set_boundary('BOUNDARY')
1559 msg = MIMEText('hello world')
1560 outer.attach(msg)
1561 eq(outer.as_string(), '''\
1562Content-Type: multipart/mixed; boundary="BOUNDARY"
1563MIME-Version: 1.0
1564Subject: A subject
1565To: aperson@dom.ain
1566From: bperson@dom.ain
1567
1568--BOUNDARY
1569Content-Type: text/plain; charset="us-ascii"
1570MIME-Version: 1.0
1571Content-Transfer-Encoding: 7bit
1572
1573hello world
1574--BOUNDARY--''')
1575
1576 def test_seq_parts_in_a_multipart_with_empty_preamble(self):
1577 eq = self.ndiffAssertEqual
1578 outer = MIMEBase('multipart', 'mixed')
1579 outer['Subject'] = 'A subject'
1580 outer['To'] = 'aperson@dom.ain'
1581 outer['From'] = 'bperson@dom.ain'
1582 outer.preamble = ''
1583 msg = MIMEText('hello world')
1584 outer.attach(msg)
1585 outer.set_boundary('BOUNDARY')
1586 eq(outer.as_string(), '''\
1587Content-Type: multipart/mixed; boundary="BOUNDARY"
1588MIME-Version: 1.0
1589Subject: A subject
1590To: aperson@dom.ain
1591From: bperson@dom.ain
1592
1593
1594--BOUNDARY
1595Content-Type: text/plain; charset="us-ascii"
1596MIME-Version: 1.0
1597Content-Transfer-Encoding: 7bit
1598
1599hello world
1600--BOUNDARY--''')
1601
1602
1603 def test_seq_parts_in_a_multipart_with_none_preamble(self):
1604 eq = self.ndiffAssertEqual
1605 outer = MIMEBase('multipart', 'mixed')
1606 outer['Subject'] = 'A subject'
1607 outer['To'] = 'aperson@dom.ain'
1608 outer['From'] = 'bperson@dom.ain'
1609 outer.preamble = None
1610 msg = MIMEText('hello world')
1611 outer.attach(msg)
1612 outer.set_boundary('BOUNDARY')
1613 eq(outer.as_string(), '''\
1614Content-Type: multipart/mixed; boundary="BOUNDARY"
1615MIME-Version: 1.0
1616Subject: A subject
1617To: aperson@dom.ain
1618From: bperson@dom.ain
1619
1620--BOUNDARY
1621Content-Type: text/plain; charset="us-ascii"
1622MIME-Version: 1.0
1623Content-Transfer-Encoding: 7bit
1624
1625hello world
1626--BOUNDARY--''')
1627
1628
1629 def test_seq_parts_in_a_multipart_with_none_epilogue(self):
1630 eq = self.ndiffAssertEqual
1631 outer = MIMEBase('multipart', 'mixed')
1632 outer['Subject'] = 'A subject'
1633 outer['To'] = 'aperson@dom.ain'
1634 outer['From'] = 'bperson@dom.ain'
1635 outer.epilogue = None
1636 msg = MIMEText('hello world')
1637 outer.attach(msg)
1638 outer.set_boundary('BOUNDARY')
1639 eq(outer.as_string(), '''\
1640Content-Type: multipart/mixed; boundary="BOUNDARY"
1641MIME-Version: 1.0
1642Subject: A subject
1643To: aperson@dom.ain
1644From: bperson@dom.ain
1645
1646--BOUNDARY
1647Content-Type: text/plain; charset="us-ascii"
1648MIME-Version: 1.0
1649Content-Transfer-Encoding: 7bit
1650
1651hello world
1652--BOUNDARY--''')
1653
1654
1655 def test_seq_parts_in_a_multipart_with_empty_epilogue(self):
1656 eq = self.ndiffAssertEqual
1657 outer = MIMEBase('multipart', 'mixed')
1658 outer['Subject'] = 'A subject'
1659 outer['To'] = 'aperson@dom.ain'
1660 outer['From'] = 'bperson@dom.ain'
1661 outer.epilogue = ''
1662 msg = MIMEText('hello world')
1663 outer.attach(msg)
1664 outer.set_boundary('BOUNDARY')
1665 eq(outer.as_string(), '''\
1666Content-Type: multipart/mixed; boundary="BOUNDARY"
1667MIME-Version: 1.0
1668Subject: A subject
1669To: aperson@dom.ain
1670From: bperson@dom.ain
1671
1672--BOUNDARY
1673Content-Type: text/plain; charset="us-ascii"
1674MIME-Version: 1.0
1675Content-Transfer-Encoding: 7bit
1676
1677hello world
1678--BOUNDARY--
1679''')
1680
1681
1682 def test_seq_parts_in_a_multipart_with_nl_epilogue(self):
1683 eq = self.ndiffAssertEqual
1684 outer = MIMEBase('multipart', 'mixed')
1685 outer['Subject'] = 'A subject'
1686 outer['To'] = 'aperson@dom.ain'
1687 outer['From'] = 'bperson@dom.ain'
1688 outer.epilogue = '\n'
1689 msg = MIMEText('hello world')
1690 outer.attach(msg)
1691 outer.set_boundary('BOUNDARY')
1692 eq(outer.as_string(), '''\
1693Content-Type: multipart/mixed; boundary="BOUNDARY"
1694MIME-Version: 1.0
1695Subject: A subject
1696To: aperson@dom.ain
1697From: bperson@dom.ain
1698
1699--BOUNDARY
1700Content-Type: text/plain; charset="us-ascii"
1701MIME-Version: 1.0
1702Content-Transfer-Encoding: 7bit
1703
1704hello world
1705--BOUNDARY--
1706
1707''')
1708
1709 def test_message_external_body(self):
1710 eq = self.assertEqual
1711 msg = self._msgobj('msg_36.txt')
1712 eq(len(msg.get_payload()), 2)
1713 msg1 = msg.get_payload(1)
1714 eq(msg1.get_content_type(), 'multipart/alternative')
1715 eq(len(msg1.get_payload()), 2)
1716 for subpart in msg1.get_payload():
1717 eq(subpart.get_content_type(), 'message/external-body')
1718 eq(len(subpart.get_payload()), 1)
1719 subsubpart = subpart.get_payload(0)
1720 eq(subsubpart.get_content_type(), 'text/plain')
1721
1722 def test_double_boundary(self):
1723 # msg_37.txt is a multipart that contains two dash-boundary's in a
1724 # row. Our interpretation of RFC 2046 calls for ignoring the second
1725 # and subsequent boundaries.
1726 msg = self._msgobj('msg_37.txt')
1727 self.assertEqual(len(msg.get_payload()), 3)
1728
1729 def test_nested_inner_contains_outer_boundary(self):
1730 eq = self.ndiffAssertEqual
1731 # msg_38.txt has an inner part that contains outer boundaries. My
1732 # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say
1733 # these are illegal and should be interpreted as unterminated inner
1734 # parts.
1735 msg = self._msgobj('msg_38.txt')
1736 sfp = StringIO()
1737 iterators._structure(msg, sfp)
1738 eq(sfp.getvalue(), """\
1739multipart/mixed
1740 multipart/mixed
1741 multipart/alternative
1742 text/plain
1743 text/plain
1744 text/plain
1745 text/plain
1746""")
1747
1748 def test_nested_with_same_boundary(self):
1749 eq = self.ndiffAssertEqual
1750 # msg 39.txt is similarly evil in that it's got inner parts that use
1751 # the same boundary as outer parts. Again, I believe the way this is
1752 # parsed is closest to the spirit of RFC 2046
1753 msg = self._msgobj('msg_39.txt')
1754 sfp = StringIO()
1755 iterators._structure(msg, sfp)
1756 eq(sfp.getvalue(), """\
1757multipart/mixed
1758 multipart/mixed
1759 multipart/alternative
1760 application/octet-stream
1761 application/octet-stream
1762 text/plain
1763""")
1764
1765 def test_boundary_in_non_multipart(self):
1766 msg = self._msgobj('msg_40.txt')
1767 self.assertEqual(msg.as_string(), '''\
1768MIME-Version: 1.0
1769Content-Type: text/html; boundary="--961284236552522269"
1770
1771----961284236552522269
1772Content-Type: text/html;
1773Content-Transfer-Encoding: 7Bit
1774
1775<html></html>
1776
1777----961284236552522269--
1778''')
1779
1780 def test_boundary_with_leading_space(self):
1781 eq = self.assertEqual
1782 msg = email.message_from_string('''\
1783MIME-Version: 1.0
1784Content-Type: multipart/mixed; boundary=" XXXX"
1785
1786-- XXXX
1787Content-Type: text/plain
1788
1789
1790-- XXXX
1791Content-Type: text/plain
1792
1793-- XXXX--
1794''')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001795 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001796 eq(msg.get_boundary(), ' XXXX')
1797 eq(len(msg.get_payload()), 2)
1798
1799 def test_boundary_without_trailing_newline(self):
1800 m = Parser().parsestr("""\
1801Content-Type: multipart/mixed; boundary="===============0012394164=="
1802MIME-Version: 1.0
1803
1804--===============0012394164==
1805Content-Type: image/file1.jpg
1806MIME-Version: 1.0
1807Content-Transfer-Encoding: base64
1808
1809YXNkZg==
1810--===============0012394164==--""")
Ezio Melottib3aedd42010-11-20 19:04:17 +00001811 self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001812
1813
Ezio Melottib3aedd42010-11-20 19:04:17 +00001814
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001815# Test some badly formatted messages
1816class TestNonConformant(TestEmailBase):
1817 def test_parse_missing_minor_type(self):
1818 eq = self.assertEqual
1819 msg = self._msgobj('msg_14.txt')
1820 eq(msg.get_content_type(), 'text/plain')
1821 eq(msg.get_content_maintype(), 'text')
1822 eq(msg.get_content_subtype(), 'plain')
1823
1824 def test_same_boundary_inner_outer(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001825 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001826 msg = self._msgobj('msg_15.txt')
1827 # XXX We can probably eventually do better
1828 inner = msg.get_payload(0)
1829 unless(hasattr(inner, 'defects'))
1830 self.assertEqual(len(inner.defects), 1)
1831 unless(isinstance(inner.defects[0],
1832 errors.StartBoundaryNotFoundDefect))
1833
1834 def test_multipart_no_boundary(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001835 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001836 msg = self._msgobj('msg_25.txt')
1837 unless(isinstance(msg.get_payload(), str))
1838 self.assertEqual(len(msg.defects), 2)
1839 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1840 unless(isinstance(msg.defects[1],
1841 errors.MultipartInvariantViolationDefect))
1842
1843 def test_invalid_content_type(self):
1844 eq = self.assertEqual
1845 neq = self.ndiffAssertEqual
1846 msg = Message()
1847 # RFC 2045, $5.2 says invalid yields text/plain
1848 msg['Content-Type'] = 'text'
1849 eq(msg.get_content_maintype(), 'text')
1850 eq(msg.get_content_subtype(), 'plain')
1851 eq(msg.get_content_type(), 'text/plain')
1852 # Clear the old value and try something /really/ invalid
1853 del msg['content-type']
1854 msg['Content-Type'] = 'foo'
1855 eq(msg.get_content_maintype(), 'text')
1856 eq(msg.get_content_subtype(), 'plain')
1857 eq(msg.get_content_type(), 'text/plain')
1858 # Still, make sure that the message is idempotently generated
1859 s = StringIO()
1860 g = Generator(s)
1861 g.flatten(msg)
1862 neq(s.getvalue(), 'Content-Type: foo\n\n')
1863
1864 def test_no_start_boundary(self):
1865 eq = self.ndiffAssertEqual
1866 msg = self._msgobj('msg_31.txt')
1867 eq(msg.get_payload(), """\
1868--BOUNDARY
1869Content-Type: text/plain
1870
1871message 1
1872
1873--BOUNDARY
1874Content-Type: text/plain
1875
1876message 2
1877
1878--BOUNDARY--
1879""")
1880
1881 def test_no_separating_blank_line(self):
1882 eq = self.ndiffAssertEqual
1883 msg = self._msgobj('msg_35.txt')
1884 eq(msg.as_string(), """\
1885From: aperson@dom.ain
1886To: bperson@dom.ain
1887Subject: here's something interesting
1888
1889counter to RFC 2822, there's no separating newline here
1890""")
1891
1892 def test_lying_multipart(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001893 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001894 msg = self._msgobj('msg_41.txt')
1895 unless(hasattr(msg, 'defects'))
1896 self.assertEqual(len(msg.defects), 2)
1897 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1898 unless(isinstance(msg.defects[1],
1899 errors.MultipartInvariantViolationDefect))
1900
1901 def test_missing_start_boundary(self):
1902 outer = self._msgobj('msg_42.txt')
1903 # The message structure is:
1904 #
1905 # multipart/mixed
1906 # text/plain
1907 # message/rfc822
1908 # multipart/mixed [*]
1909 #
1910 # [*] This message is missing its start boundary
1911 bad = outer.get_payload(1).get_payload(0)
1912 self.assertEqual(len(bad.defects), 1)
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001913 self.assertTrue(isinstance(bad.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001914 errors.StartBoundaryNotFoundDefect))
1915
1916 def test_first_line_is_continuation_header(self):
1917 eq = self.assertEqual
1918 m = ' Line 1\nLine 2\nLine 3'
1919 msg = email.message_from_string(m)
1920 eq(msg.keys(), [])
1921 eq(msg.get_payload(), 'Line 2\nLine 3')
1922 eq(len(msg.defects), 1)
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001923 self.assertTrue(isinstance(msg.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001924 errors.FirstHeaderLineIsContinuationDefect))
1925 eq(msg.defects[0].line, ' Line 1\n')
1926
1927
Ezio Melottib3aedd42010-11-20 19:04:17 +00001928
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001929# Test RFC 2047 header encoding and decoding
Guido van Rossum9604e662007-08-30 03:46:43 +00001930class TestRFC2047(TestEmailBase):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001931 def test_rfc2047_multiline(self):
1932 eq = self.assertEqual
1933 s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz
1934 foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?="""
1935 dh = decode_header(s)
1936 eq(dh, [
1937 (b'Re:', None),
1938 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland'),
1939 (b'baz foo bar', None),
1940 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland')])
1941 header = make_header(dh)
1942 eq(str(header),
1943 'Re: r\xe4ksm\xf6rg\xe5s baz foo bar r\xe4ksm\xf6rg\xe5s')
Barry Warsaw00b34222007-08-31 02:35:00 +00001944 self.ndiffAssertEqual(header.encode(maxlinelen=76), """\
Guido van Rossum9604e662007-08-30 03:46:43 +00001945Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar =?mac-iceland?q?r=8Aksm?=
1946 =?mac-iceland?q?=9Arg=8Cs?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001947
1948 def test_whitespace_eater_unicode(self):
1949 eq = self.assertEqual
1950 s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard@dom.ain>'
1951 dh = decode_header(s)
1952 eq(dh, [(b'Andr\xe9', 'iso-8859-1'),
1953 (b'Pirard <pirard@dom.ain>', None)])
1954 header = str(make_header(dh))
1955 eq(header, 'Andr\xe9 Pirard <pirard@dom.ain>')
1956
1957 def test_whitespace_eater_unicode_2(self):
1958 eq = self.assertEqual
1959 s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?='
1960 dh = decode_header(s)
1961 eq(dh, [(b'The', None), (b'quick brown fox', 'iso-8859-1'),
1962 (b'jumped over the', None), (b'lazy dog', 'iso-8859-1')])
1963 hu = str(make_header(dh))
1964 eq(hu, 'The quick brown fox jumped over the lazy dog')
1965
1966 def test_rfc2047_missing_whitespace(self):
1967 s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord'
1968 dh = decode_header(s)
1969 self.assertEqual(dh, [(s, None)])
1970
1971 def test_rfc2047_with_whitespace(self):
1972 s = 'Sm =?ISO-8859-1?B?9g==?= rg =?ISO-8859-1?B?5Q==?= sbord'
1973 dh = decode_header(s)
1974 self.assertEqual(dh, [(b'Sm', None), (b'\xf6', 'iso-8859-1'),
1975 (b'rg', None), (b'\xe5', 'iso-8859-1'),
1976 (b'sbord', None)])
1977
R. David Murrayc4e69cc2010-08-03 22:14:10 +00001978 def test_rfc2047_B_bad_padding(self):
1979 s = '=?iso-8859-1?B?%s?='
1980 data = [ # only test complete bytes
1981 ('dm==', b'v'), ('dm=', b'v'), ('dm', b'v'),
1982 ('dmk=', b'vi'), ('dmk', b'vi')
1983 ]
1984 for q, a in data:
1985 dh = decode_header(s % q)
1986 self.assertEqual(dh, [(a, 'iso-8859-1')])
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001987
R. David Murray31e984c2010-10-01 15:40:20 +00001988 def test_rfc2047_Q_invalid_digits(self):
1989 # issue 10004.
1990 s = '=?iso-8659-1?Q?andr=e9=zz?='
1991 self.assertEqual(decode_header(s),
1992 [(b'andr\xe9=zz', 'iso-8659-1')])
1993
Ezio Melottib3aedd42010-11-20 19:04:17 +00001994
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001995# Test the MIMEMessage class
1996class TestMIMEMessage(TestEmailBase):
1997 def setUp(self):
1998 with openfile('msg_11.txt') as fp:
1999 self._text = fp.read()
2000
2001 def test_type_error(self):
2002 self.assertRaises(TypeError, MIMEMessage, 'a plain string')
2003
2004 def test_valid_argument(self):
2005 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002006 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002007 subject = 'A sub-message'
2008 m = Message()
2009 m['Subject'] = subject
2010 r = MIMEMessage(m)
2011 eq(r.get_content_type(), 'message/rfc822')
2012 payload = r.get_payload()
2013 unless(isinstance(payload, list))
2014 eq(len(payload), 1)
2015 subpart = payload[0]
2016 unless(subpart is m)
2017 eq(subpart['subject'], subject)
2018
2019 def test_bad_multipart(self):
2020 eq = self.assertEqual
2021 msg1 = Message()
2022 msg1['Subject'] = 'subpart 1'
2023 msg2 = Message()
2024 msg2['Subject'] = 'subpart 2'
2025 r = MIMEMessage(msg1)
2026 self.assertRaises(errors.MultipartConversionError, r.attach, msg2)
2027
2028 def test_generate(self):
2029 # First craft the message to be encapsulated
2030 m = Message()
2031 m['Subject'] = 'An enclosed message'
2032 m.set_payload('Here is the body of the message.\n')
2033 r = MIMEMessage(m)
2034 r['Subject'] = 'The enclosing message'
2035 s = StringIO()
2036 g = Generator(s)
2037 g.flatten(r)
2038 self.assertEqual(s.getvalue(), """\
2039Content-Type: message/rfc822
2040MIME-Version: 1.0
2041Subject: The enclosing message
2042
2043Subject: An enclosed message
2044
2045Here is the body of the message.
2046""")
2047
2048 def test_parse_message_rfc822(self):
2049 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002050 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002051 msg = self._msgobj('msg_11.txt')
2052 eq(msg.get_content_type(), 'message/rfc822')
2053 payload = msg.get_payload()
2054 unless(isinstance(payload, list))
2055 eq(len(payload), 1)
2056 submsg = payload[0]
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002057 self.assertTrue(isinstance(submsg, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002058 eq(submsg['subject'], 'An enclosed message')
2059 eq(submsg.get_payload(), 'Here is the body of the message.\n')
2060
2061 def test_dsn(self):
2062 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002063 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002064 # msg 16 is a Delivery Status Notification, see RFC 1894
2065 msg = self._msgobj('msg_16.txt')
2066 eq(msg.get_content_type(), 'multipart/report')
2067 unless(msg.is_multipart())
2068 eq(len(msg.get_payload()), 3)
2069 # Subpart 1 is a text/plain, human readable section
2070 subpart = msg.get_payload(0)
2071 eq(subpart.get_content_type(), 'text/plain')
2072 eq(subpart.get_payload(), """\
2073This report relates to a message you sent with the following header fields:
2074
2075 Message-id: <002001c144a6$8752e060$56104586@oxy.edu>
2076 Date: Sun, 23 Sep 2001 20:10:55 -0700
2077 From: "Ian T. Henry" <henryi@oxy.edu>
2078 To: SoCal Raves <scr@socal-raves.org>
2079 Subject: [scr] yeah for Ians!!
2080
2081Your message cannot be delivered to the following recipients:
2082
2083 Recipient address: jangel1@cougar.noc.ucla.edu
2084 Reason: recipient reached disk quota
2085
2086""")
2087 # Subpart 2 contains the machine parsable DSN information. It
2088 # consists of two blocks of headers, represented by two nested Message
2089 # objects.
2090 subpart = msg.get_payload(1)
2091 eq(subpart.get_content_type(), 'message/delivery-status')
2092 eq(len(subpart.get_payload()), 2)
2093 # message/delivery-status should treat each block as a bunch of
2094 # headers, i.e. a bunch of Message objects.
2095 dsn1 = subpart.get_payload(0)
2096 unless(isinstance(dsn1, Message))
2097 eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu')
2098 eq(dsn1.get_param('dns', header='reporting-mta'), '')
2099 # Try a missing one <wink>
2100 eq(dsn1.get_param('nsd', header='reporting-mta'), None)
2101 dsn2 = subpart.get_payload(1)
2102 unless(isinstance(dsn2, Message))
2103 eq(dsn2['action'], 'failed')
2104 eq(dsn2.get_params(header='original-recipient'),
2105 [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')])
2106 eq(dsn2.get_param('rfc822', header='final-recipient'), '')
2107 # Subpart 3 is the original message
2108 subpart = msg.get_payload(2)
2109 eq(subpart.get_content_type(), 'message/rfc822')
2110 payload = subpart.get_payload()
2111 unless(isinstance(payload, list))
2112 eq(len(payload), 1)
2113 subsubpart = payload[0]
2114 unless(isinstance(subsubpart, Message))
2115 eq(subsubpart.get_content_type(), 'text/plain')
2116 eq(subsubpart['message-id'],
2117 '<002001c144a6$8752e060$56104586@oxy.edu>')
2118
2119 def test_epilogue(self):
2120 eq = self.ndiffAssertEqual
2121 with openfile('msg_21.txt') as fp:
2122 text = fp.read()
2123 msg = Message()
2124 msg['From'] = 'aperson@dom.ain'
2125 msg['To'] = 'bperson@dom.ain'
2126 msg['Subject'] = 'Test'
2127 msg.preamble = 'MIME message'
2128 msg.epilogue = 'End of MIME message\n'
2129 msg1 = MIMEText('One')
2130 msg2 = MIMEText('Two')
2131 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
2132 msg.attach(msg1)
2133 msg.attach(msg2)
2134 sfp = StringIO()
2135 g = Generator(sfp)
2136 g.flatten(msg)
2137 eq(sfp.getvalue(), text)
2138
2139 def test_no_nl_preamble(self):
2140 eq = self.ndiffAssertEqual
2141 msg = Message()
2142 msg['From'] = 'aperson@dom.ain'
2143 msg['To'] = 'bperson@dom.ain'
2144 msg['Subject'] = 'Test'
2145 msg.preamble = 'MIME message'
2146 msg.epilogue = ''
2147 msg1 = MIMEText('One')
2148 msg2 = MIMEText('Two')
2149 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
2150 msg.attach(msg1)
2151 msg.attach(msg2)
2152 eq(msg.as_string(), """\
2153From: aperson@dom.ain
2154To: bperson@dom.ain
2155Subject: Test
2156Content-Type: multipart/mixed; boundary="BOUNDARY"
2157
2158MIME message
2159--BOUNDARY
2160Content-Type: text/plain; charset="us-ascii"
2161MIME-Version: 1.0
2162Content-Transfer-Encoding: 7bit
2163
2164One
2165--BOUNDARY
2166Content-Type: text/plain; charset="us-ascii"
2167MIME-Version: 1.0
2168Content-Transfer-Encoding: 7bit
2169
2170Two
2171--BOUNDARY--
2172""")
2173
2174 def test_default_type(self):
2175 eq = self.assertEqual
2176 with openfile('msg_30.txt') as fp:
2177 msg = email.message_from_file(fp)
2178 container1 = msg.get_payload(0)
2179 eq(container1.get_default_type(), 'message/rfc822')
2180 eq(container1.get_content_type(), 'message/rfc822')
2181 container2 = msg.get_payload(1)
2182 eq(container2.get_default_type(), 'message/rfc822')
2183 eq(container2.get_content_type(), 'message/rfc822')
2184 container1a = container1.get_payload(0)
2185 eq(container1a.get_default_type(), 'text/plain')
2186 eq(container1a.get_content_type(), 'text/plain')
2187 container2a = container2.get_payload(0)
2188 eq(container2a.get_default_type(), 'text/plain')
2189 eq(container2a.get_content_type(), 'text/plain')
2190
2191 def test_default_type_with_explicit_container_type(self):
2192 eq = self.assertEqual
2193 with openfile('msg_28.txt') as fp:
2194 msg = email.message_from_file(fp)
2195 container1 = msg.get_payload(0)
2196 eq(container1.get_default_type(), 'message/rfc822')
2197 eq(container1.get_content_type(), 'message/rfc822')
2198 container2 = msg.get_payload(1)
2199 eq(container2.get_default_type(), 'message/rfc822')
2200 eq(container2.get_content_type(), 'message/rfc822')
2201 container1a = container1.get_payload(0)
2202 eq(container1a.get_default_type(), 'text/plain')
2203 eq(container1a.get_content_type(), 'text/plain')
2204 container2a = container2.get_payload(0)
2205 eq(container2a.get_default_type(), 'text/plain')
2206 eq(container2a.get_content_type(), 'text/plain')
2207
2208 def test_default_type_non_parsed(self):
2209 eq = self.assertEqual
2210 neq = self.ndiffAssertEqual
2211 # Set up container
2212 container = MIMEMultipart('digest', 'BOUNDARY')
2213 container.epilogue = ''
2214 # Set up subparts
2215 subpart1a = MIMEText('message 1\n')
2216 subpart2a = MIMEText('message 2\n')
2217 subpart1 = MIMEMessage(subpart1a)
2218 subpart2 = MIMEMessage(subpart2a)
2219 container.attach(subpart1)
2220 container.attach(subpart2)
2221 eq(subpart1.get_content_type(), 'message/rfc822')
2222 eq(subpart1.get_default_type(), 'message/rfc822')
2223 eq(subpart2.get_content_type(), 'message/rfc822')
2224 eq(subpart2.get_default_type(), 'message/rfc822')
2225 neq(container.as_string(0), '''\
2226Content-Type: multipart/digest; boundary="BOUNDARY"
2227MIME-Version: 1.0
2228
2229--BOUNDARY
2230Content-Type: message/rfc822
2231MIME-Version: 1.0
2232
2233Content-Type: text/plain; charset="us-ascii"
2234MIME-Version: 1.0
2235Content-Transfer-Encoding: 7bit
2236
2237message 1
2238
2239--BOUNDARY
2240Content-Type: message/rfc822
2241MIME-Version: 1.0
2242
2243Content-Type: text/plain; charset="us-ascii"
2244MIME-Version: 1.0
2245Content-Transfer-Encoding: 7bit
2246
2247message 2
2248
2249--BOUNDARY--
2250''')
2251 del subpart1['content-type']
2252 del subpart1['mime-version']
2253 del subpart2['content-type']
2254 del subpart2['mime-version']
2255 eq(subpart1.get_content_type(), 'message/rfc822')
2256 eq(subpart1.get_default_type(), 'message/rfc822')
2257 eq(subpart2.get_content_type(), 'message/rfc822')
2258 eq(subpart2.get_default_type(), 'message/rfc822')
2259 neq(container.as_string(0), '''\
2260Content-Type: multipart/digest; boundary="BOUNDARY"
2261MIME-Version: 1.0
2262
2263--BOUNDARY
2264
2265Content-Type: text/plain; charset="us-ascii"
2266MIME-Version: 1.0
2267Content-Transfer-Encoding: 7bit
2268
2269message 1
2270
2271--BOUNDARY
2272
2273Content-Type: text/plain; charset="us-ascii"
2274MIME-Version: 1.0
2275Content-Transfer-Encoding: 7bit
2276
2277message 2
2278
2279--BOUNDARY--
2280''')
2281
2282 def test_mime_attachments_in_constructor(self):
2283 eq = self.assertEqual
2284 text1 = MIMEText('')
2285 text2 = MIMEText('')
2286 msg = MIMEMultipart(_subparts=(text1, text2))
2287 eq(len(msg.get_payload()), 2)
2288 eq(msg.get_payload(0), text1)
2289 eq(msg.get_payload(1), text2)
2290
Christian Heimes587c2bf2008-01-19 16:21:02 +00002291 def test_default_multipart_constructor(self):
2292 msg = MIMEMultipart()
2293 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002294
Ezio Melottib3aedd42010-11-20 19:04:17 +00002295
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002296# A general test of parser->model->generator idempotency. IOW, read a message
2297# in, parse it into a message object tree, then without touching the tree,
2298# regenerate the plain text. The original text and the transformed text
2299# should be identical. Note: that we ignore the Unix-From since that may
2300# contain a changed date.
2301class TestIdempotent(TestEmailBase):
R. David Murray719a4492010-11-21 16:53:48 +00002302
2303 linesep = '\n'
2304
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002305 def _msgobj(self, filename):
2306 with openfile(filename) as fp:
2307 data = fp.read()
2308 msg = email.message_from_string(data)
2309 return msg, data
2310
R. David Murray719a4492010-11-21 16:53:48 +00002311 def _idempotent(self, msg, text, unixfrom=False):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002312 eq = self.ndiffAssertEqual
2313 s = StringIO()
2314 g = Generator(s, maxheaderlen=0)
R. David Murray719a4492010-11-21 16:53:48 +00002315 g.flatten(msg, unixfrom=unixfrom)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002316 eq(text, s.getvalue())
2317
2318 def test_parse_text_message(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002319 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002320 msg, text = self._msgobj('msg_01.txt')
2321 eq(msg.get_content_type(), 'text/plain')
2322 eq(msg.get_content_maintype(), 'text')
2323 eq(msg.get_content_subtype(), 'plain')
2324 eq(msg.get_params()[1], ('charset', 'us-ascii'))
2325 eq(msg.get_param('charset'), 'us-ascii')
2326 eq(msg.preamble, None)
2327 eq(msg.epilogue, None)
2328 self._idempotent(msg, text)
2329
2330 def test_parse_untyped_message(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002331 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002332 msg, text = self._msgobj('msg_03.txt')
2333 eq(msg.get_content_type(), 'text/plain')
2334 eq(msg.get_params(), None)
2335 eq(msg.get_param('charset'), None)
2336 self._idempotent(msg, text)
2337
2338 def test_simple_multipart(self):
2339 msg, text = self._msgobj('msg_04.txt')
2340 self._idempotent(msg, text)
2341
2342 def test_MIME_digest(self):
2343 msg, text = self._msgobj('msg_02.txt')
2344 self._idempotent(msg, text)
2345
2346 def test_long_header(self):
2347 msg, text = self._msgobj('msg_27.txt')
2348 self._idempotent(msg, text)
2349
2350 def test_MIME_digest_with_part_headers(self):
2351 msg, text = self._msgobj('msg_28.txt')
2352 self._idempotent(msg, text)
2353
2354 def test_mixed_with_image(self):
2355 msg, text = self._msgobj('msg_06.txt')
2356 self._idempotent(msg, text)
2357
2358 def test_multipart_report(self):
2359 msg, text = self._msgobj('msg_05.txt')
2360 self._idempotent(msg, text)
2361
2362 def test_dsn(self):
2363 msg, text = self._msgobj('msg_16.txt')
2364 self._idempotent(msg, text)
2365
2366 def test_preamble_epilogue(self):
2367 msg, text = self._msgobj('msg_21.txt')
2368 self._idempotent(msg, text)
2369
2370 def test_multipart_one_part(self):
2371 msg, text = self._msgobj('msg_23.txt')
2372 self._idempotent(msg, text)
2373
2374 def test_multipart_no_parts(self):
2375 msg, text = self._msgobj('msg_24.txt')
2376 self._idempotent(msg, text)
2377
2378 def test_no_start_boundary(self):
2379 msg, text = self._msgobj('msg_31.txt')
2380 self._idempotent(msg, text)
2381
2382 def test_rfc2231_charset(self):
2383 msg, text = self._msgobj('msg_32.txt')
2384 self._idempotent(msg, text)
2385
2386 def test_more_rfc2231_parameters(self):
2387 msg, text = self._msgobj('msg_33.txt')
2388 self._idempotent(msg, text)
2389
2390 def test_text_plain_in_a_multipart_digest(self):
2391 msg, text = self._msgobj('msg_34.txt')
2392 self._idempotent(msg, text)
2393
2394 def test_nested_multipart_mixeds(self):
2395 msg, text = self._msgobj('msg_12a.txt')
2396 self._idempotent(msg, text)
2397
2398 def test_message_external_body_idempotent(self):
2399 msg, text = self._msgobj('msg_36.txt')
2400 self._idempotent(msg, text)
2401
R. David Murray719a4492010-11-21 16:53:48 +00002402 def test_message_delivery_status(self):
2403 msg, text = self._msgobj('msg_43.txt')
2404 self._idempotent(msg, text, unixfrom=True)
2405
R. David Murray96fd54e2010-10-08 15:55:28 +00002406 def test_message_signed_idempotent(self):
2407 msg, text = self._msgobj('msg_45.txt')
2408 self._idempotent(msg, text)
2409
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002410 def test_content_type(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002411 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002412 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002413 # Get a message object and reset the seek pointer for other tests
2414 msg, text = self._msgobj('msg_05.txt')
2415 eq(msg.get_content_type(), 'multipart/report')
2416 # Test the Content-Type: parameters
2417 params = {}
2418 for pk, pv in msg.get_params():
2419 params[pk] = pv
2420 eq(params['report-type'], 'delivery-status')
2421 eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
R. David Murray719a4492010-11-21 16:53:48 +00002422 eq(msg.preamble, 'This is a MIME-encapsulated message.' + self.linesep)
2423 eq(msg.epilogue, self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002424 eq(len(msg.get_payload()), 3)
2425 # Make sure the subparts are what we expect
2426 msg1 = msg.get_payload(0)
2427 eq(msg1.get_content_type(), 'text/plain')
R. David Murray719a4492010-11-21 16:53:48 +00002428 eq(msg1.get_payload(), 'Yadda yadda yadda' + self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002429 msg2 = msg.get_payload(1)
2430 eq(msg2.get_content_type(), 'text/plain')
R. David Murray719a4492010-11-21 16:53:48 +00002431 eq(msg2.get_payload(), 'Yadda yadda yadda' + self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002432 msg3 = msg.get_payload(2)
2433 eq(msg3.get_content_type(), 'message/rfc822')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002434 self.assertTrue(isinstance(msg3, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002435 payload = msg3.get_payload()
2436 unless(isinstance(payload, list))
2437 eq(len(payload), 1)
2438 msg4 = payload[0]
2439 unless(isinstance(msg4, Message))
R. David Murray719a4492010-11-21 16:53:48 +00002440 eq(msg4.get_payload(), 'Yadda yadda yadda' + self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002441
2442 def test_parser(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002443 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002444 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002445 msg, text = self._msgobj('msg_06.txt')
2446 # Check some of the outer headers
2447 eq(msg.get_content_type(), 'message/rfc822')
2448 # Make sure the payload is a list of exactly one sub-Message, and that
2449 # that submessage has a type of text/plain
2450 payload = msg.get_payload()
2451 unless(isinstance(payload, list))
2452 eq(len(payload), 1)
2453 msg1 = payload[0]
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002454 self.assertTrue(isinstance(msg1, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002455 eq(msg1.get_content_type(), 'text/plain')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002456 self.assertTrue(isinstance(msg1.get_payload(), str))
R. David Murray719a4492010-11-21 16:53:48 +00002457 eq(msg1.get_payload(), self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002458
2459
Ezio Melottib3aedd42010-11-20 19:04:17 +00002460
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002461# Test various other bits of the package's functionality
2462class TestMiscellaneous(TestEmailBase):
2463 def test_message_from_string(self):
2464 with openfile('msg_01.txt') as fp:
2465 text = fp.read()
2466 msg = email.message_from_string(text)
2467 s = StringIO()
2468 # Don't wrap/continue long headers since we're trying to test
2469 # idempotency.
2470 g = Generator(s, maxheaderlen=0)
2471 g.flatten(msg)
2472 self.assertEqual(text, s.getvalue())
2473
2474 def test_message_from_file(self):
2475 with openfile('msg_01.txt') as fp:
2476 text = fp.read()
2477 fp.seek(0)
2478 msg = email.message_from_file(fp)
2479 s = StringIO()
2480 # Don't wrap/continue long headers since we're trying to test
2481 # idempotency.
2482 g = Generator(s, maxheaderlen=0)
2483 g.flatten(msg)
2484 self.assertEqual(text, s.getvalue())
2485
2486 def test_message_from_string_with_class(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002487 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002488 with openfile('msg_01.txt') as fp:
2489 text = fp.read()
2490
2491 # Create a subclass
2492 class MyMessage(Message):
2493 pass
2494
2495 msg = email.message_from_string(text, MyMessage)
2496 unless(isinstance(msg, MyMessage))
2497 # Try something more complicated
2498 with openfile('msg_02.txt') as fp:
2499 text = fp.read()
2500 msg = email.message_from_string(text, MyMessage)
2501 for subpart in msg.walk():
2502 unless(isinstance(subpart, MyMessage))
2503
2504 def test_message_from_file_with_class(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002505 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002506 # Create a subclass
2507 class MyMessage(Message):
2508 pass
2509
2510 with openfile('msg_01.txt') as fp:
2511 msg = email.message_from_file(fp, MyMessage)
2512 unless(isinstance(msg, MyMessage))
2513 # Try something more complicated
2514 with openfile('msg_02.txt') as fp:
2515 msg = email.message_from_file(fp, MyMessage)
2516 for subpart in msg.walk():
2517 unless(isinstance(subpart, MyMessage))
2518
2519 def test__all__(self):
2520 module = __import__('email')
R David Murray1b6c7242012-03-16 22:43:05 -04002521 self.assertEqual(sorted(module.__all__), [
2522 'base64mime', 'charset', 'encoders', 'errors', 'feedparser',
2523 'generator', 'header', 'iterators', 'message',
2524 'message_from_binary_file', 'message_from_bytes',
2525 'message_from_file', 'message_from_string', 'mime', 'parser',
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002526 'quoprimime', 'utils',
2527 ])
2528
2529 def test_formatdate(self):
2530 now = time.time()
2531 self.assertEqual(utils.parsedate(utils.formatdate(now))[:6],
2532 time.gmtime(now)[:6])
2533
2534 def test_formatdate_localtime(self):
2535 now = time.time()
2536 self.assertEqual(
2537 utils.parsedate(utils.formatdate(now, localtime=True))[:6],
2538 time.localtime(now)[:6])
2539
2540 def test_formatdate_usegmt(self):
2541 now = time.time()
2542 self.assertEqual(
2543 utils.formatdate(now, localtime=False),
2544 time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now)))
2545 self.assertEqual(
2546 utils.formatdate(now, localtime=False, usegmt=True),
2547 time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now)))
2548
2549 def test_parsedate_none(self):
2550 self.assertEqual(utils.parsedate(''), None)
2551
2552 def test_parsedate_compact(self):
2553 # The FWS after the comma is optional
2554 self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'),
2555 utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800'))
2556
2557 def test_parsedate_no_dayofweek(self):
2558 eq = self.assertEqual
2559 eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'),
2560 (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800))
2561
2562 def test_parsedate_compact_no_dayofweek(self):
2563 eq = self.assertEqual
2564 eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'),
2565 (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800))
2566
R. David Murray4a62e892010-12-23 20:35:46 +00002567 def test_parsedate_no_space_before_positive_offset(self):
2568 self.assertEqual(utils.parsedate_tz('Wed, 3 Apr 2002 14:58:26+0800'),
2569 (2002, 4, 3, 14, 58, 26, 0, 1, -1, 28800))
2570
2571 def test_parsedate_no_space_before_negative_offset(self):
2572 # Issue 1155362: we already handled '+' for this case.
2573 self.assertEqual(utils.parsedate_tz('Wed, 3 Apr 2002 14:58:26-0800'),
2574 (2002, 4, 3, 14, 58, 26, 0, 1, -1, -28800))
2575
2576
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002577 def test_parsedate_acceptable_to_time_functions(self):
2578 eq = self.assertEqual
2579 timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800')
2580 t = int(time.mktime(timetup))
2581 eq(time.localtime(t)[:6], timetup[:6])
2582 eq(int(time.strftime('%Y', timetup)), 2003)
2583 timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800')
2584 t = int(time.mktime(timetup[:9]))
2585 eq(time.localtime(t)[:6], timetup[:6])
2586 eq(int(time.strftime('%Y', timetup[:9])), 2003)
2587
Alexander Belopolskya07548e2012-06-21 20:34:09 -04002588 def test_mktime_tz(self):
2589 self.assertEqual(utils.mktime_tz((1970, 1, 1, 0, 0, 0,
2590 -1, -1, -1, 0)), 0)
2591 self.assertEqual(utils.mktime_tz((1970, 1, 1, 0, 0, 0,
2592 -1, -1, -1, 1234)), -1234)
2593
R. David Murray219d1c82010-08-25 00:45:55 +00002594 def test_parsedate_y2k(self):
2595 """Test for parsing a date with a two-digit year.
2596
2597 Parsing a date with a two-digit year should return the correct
2598 four-digit year. RFC822 allows two-digit years, but RFC2822 (which
2599 obsoletes RFC822) requires four-digit years.
2600
2601 """
2602 self.assertEqual(utils.parsedate_tz('25 Feb 03 13:47:26 -0800'),
2603 utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'))
2604 self.assertEqual(utils.parsedate_tz('25 Feb 71 13:47:26 -0800'),
2605 utils.parsedate_tz('25 Feb 1971 13:47:26 -0800'))
2606
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002607 def test_parseaddr_empty(self):
2608 self.assertEqual(utils.parseaddr('<>'), ('', ''))
2609 self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '')
2610
2611 def test_noquote_dump(self):
2612 self.assertEqual(
2613 utils.formataddr(('A Silly Person', 'person@dom.ain')),
2614 'A Silly Person <person@dom.ain>')
2615
2616 def test_escape_dump(self):
2617 self.assertEqual(
2618 utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')),
2619 r'"A \(Very\) Silly Person" <person@dom.ain>')
2620 a = r'A \(Special\) Person'
2621 b = 'person@dom.ain'
2622 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2623
2624 def test_escape_backslashes(self):
2625 self.assertEqual(
2626 utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')),
2627 r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>')
2628 a = r'Arthur \Backslash\ Foobar'
2629 b = 'person@dom.ain'
2630 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2631
2632 def test_name_with_dot(self):
2633 x = 'John X. Doe <jxd@example.com>'
2634 y = '"John X. Doe" <jxd@example.com>'
2635 a, b = ('John X. Doe', 'jxd@example.com')
2636 self.assertEqual(utils.parseaddr(x), (a, b))
2637 self.assertEqual(utils.parseaddr(y), (a, b))
2638 # formataddr() quotes the name if there's a dot in it
2639 self.assertEqual(utils.formataddr((a, b)), y)
2640
R. David Murray5397e862010-10-02 15:58:26 +00002641 def test_parseaddr_preserves_quoted_pairs_in_addresses(self):
2642 # issue 10005. Note that in the third test the second pair of
2643 # backslashes is not actually a quoted pair because it is not inside a
2644 # comment or quoted string: the address being parsed has a quoted
2645 # string containing a quoted backslash, followed by 'example' and two
2646 # backslashes, followed by another quoted string containing a space and
2647 # the word 'example'. parseaddr copies those two backslashes
2648 # literally. Per rfc5322 this is not technically correct since a \ may
2649 # not appear in an address outside of a quoted string. It is probably
2650 # a sensible Postel interpretation, though.
2651 eq = self.assertEqual
2652 eq(utils.parseaddr('""example" example"@example.com'),
2653 ('', '""example" example"@example.com'))
2654 eq(utils.parseaddr('"\\"example\\" example"@example.com'),
2655 ('', '"\\"example\\" example"@example.com'))
2656 eq(utils.parseaddr('"\\\\"example\\\\" example"@example.com'),
2657 ('', '"\\\\"example\\\\" example"@example.com'))
2658
R. David Murray63563cd2010-12-18 18:25:38 +00002659 def test_parseaddr_preserves_spaces_in_local_part(self):
2660 # issue 9286. A normal RFC5322 local part should not contain any
2661 # folding white space, but legacy local parts can (they are a sequence
2662 # of atoms, not dotatoms). On the other hand we strip whitespace from
2663 # before the @ and around dots, on the assumption that the whitespace
2664 # around the punctuation is a mistake in what would otherwise be
2665 # an RFC5322 local part. Leading whitespace is, usual, stripped as well.
2666 self.assertEqual(('', "merwok wok@xample.com"),
2667 utils.parseaddr("merwok wok@xample.com"))
2668 self.assertEqual(('', "merwok wok@xample.com"),
2669 utils.parseaddr("merwok wok@xample.com"))
2670 self.assertEqual(('', "merwok wok@xample.com"),
2671 utils.parseaddr(" merwok wok @xample.com"))
2672 self.assertEqual(('', 'merwok"wok" wok@xample.com'),
2673 utils.parseaddr('merwok"wok" wok@xample.com'))
2674 self.assertEqual(('', 'merwok.wok.wok@xample.com'),
2675 utils.parseaddr('merwok. wok . wok@xample.com'))
2676
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002677 def test_multiline_from_comment(self):
2678 x = """\
2679Foo
2680\tBar <foo@example.com>"""
2681 self.assertEqual(utils.parseaddr(x), ('Foo Bar', 'foo@example.com'))
2682
2683 def test_quote_dump(self):
2684 self.assertEqual(
2685 utils.formataddr(('A Silly; Person', 'person@dom.ain')),
2686 r'"A Silly; Person" <person@dom.ain>')
2687
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002688 def test_charset_richcomparisons(self):
2689 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002690 ne = self.assertNotEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002691 cset1 = Charset()
2692 cset2 = Charset()
2693 eq(cset1, 'us-ascii')
2694 eq(cset1, 'US-ASCII')
2695 eq(cset1, 'Us-AsCiI')
2696 eq('us-ascii', cset1)
2697 eq('US-ASCII', cset1)
2698 eq('Us-AsCiI', cset1)
2699 ne(cset1, 'usascii')
2700 ne(cset1, 'USASCII')
2701 ne(cset1, 'UsAsCiI')
2702 ne('usascii', cset1)
2703 ne('USASCII', cset1)
2704 ne('UsAsCiI', cset1)
2705 eq(cset1, cset2)
2706 eq(cset2, cset1)
2707
2708 def test_getaddresses(self):
2709 eq = self.assertEqual
2710 eq(utils.getaddresses(['aperson@dom.ain (Al Person)',
2711 'Bud Person <bperson@dom.ain>']),
2712 [('Al Person', 'aperson@dom.ain'),
2713 ('Bud Person', 'bperson@dom.ain')])
2714
2715 def test_getaddresses_nasty(self):
2716 eq = self.assertEqual
2717 eq(utils.getaddresses(['foo: ;']), [('', '')])
2718 eq(utils.getaddresses(
2719 ['[]*-- =~$']),
2720 [('', ''), ('', ''), ('', '*--')])
2721 eq(utils.getaddresses(
2722 ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
2723 [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
2724
2725 def test_getaddresses_embedded_comment(self):
2726 """Test proper handling of a nested comment"""
2727 eq = self.assertEqual
2728 addrs = utils.getaddresses(['User ((nested comment)) <foo@bar.com>'])
2729 eq(addrs[0][1], 'foo@bar.com')
2730
2731 def test_utils_quote_unquote(self):
2732 eq = self.assertEqual
2733 msg = Message()
2734 msg.add_header('content-disposition', 'attachment',
2735 filename='foo\\wacky"name')
2736 eq(msg.get_filename(), 'foo\\wacky"name')
2737
2738 def test_get_body_encoding_with_bogus_charset(self):
2739 charset = Charset('not a charset')
2740 self.assertEqual(charset.get_body_encoding(), 'base64')
2741
2742 def test_get_body_encoding_with_uppercase_charset(self):
2743 eq = self.assertEqual
2744 msg = Message()
2745 msg['Content-Type'] = 'text/plain; charset=UTF-8'
2746 eq(msg['content-type'], 'text/plain; charset=UTF-8')
2747 charsets = msg.get_charsets()
2748 eq(len(charsets), 1)
2749 eq(charsets[0], 'utf-8')
2750 charset = Charset(charsets[0])
2751 eq(charset.get_body_encoding(), 'base64')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002752 msg.set_payload(b'hello world', charset=charset)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002753 eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n')
2754 eq(msg.get_payload(decode=True), b'hello world')
2755 eq(msg['content-transfer-encoding'], 'base64')
2756 # Try another one
2757 msg = Message()
2758 msg['Content-Type'] = 'text/plain; charset="US-ASCII"'
2759 charsets = msg.get_charsets()
2760 eq(len(charsets), 1)
2761 eq(charsets[0], 'us-ascii')
2762 charset = Charset(charsets[0])
2763 eq(charset.get_body_encoding(), encoders.encode_7or8bit)
2764 msg.set_payload('hello world', charset=charset)
2765 eq(msg.get_payload(), 'hello world')
2766 eq(msg['content-transfer-encoding'], '7bit')
2767
2768 def test_charsets_case_insensitive(self):
2769 lc = Charset('us-ascii')
2770 uc = Charset('US-ASCII')
2771 self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding())
2772
2773 def test_partial_falls_inside_message_delivery_status(self):
2774 eq = self.ndiffAssertEqual
2775 # The Parser interface provides chunks of data to FeedParser in 8192
2776 # byte gulps. SF bug #1076485 found one of those chunks inside
2777 # message/delivery-status header block, which triggered an
2778 # unreadline() of NeedMoreData.
2779 msg = self._msgobj('msg_43.txt')
2780 sfp = StringIO()
2781 iterators._structure(msg, sfp)
2782 eq(sfp.getvalue(), """\
2783multipart/report
2784 text/plain
2785 message/delivery-status
2786 text/plain
2787 text/plain
2788 text/plain
2789 text/plain
2790 text/plain
2791 text/plain
2792 text/plain
2793 text/plain
2794 text/plain
2795 text/plain
2796 text/plain
2797 text/plain
2798 text/plain
2799 text/plain
2800 text/plain
2801 text/plain
2802 text/plain
2803 text/plain
2804 text/plain
2805 text/plain
2806 text/plain
2807 text/plain
2808 text/plain
2809 text/plain
2810 text/plain
2811 text/plain
2812 text/rfc822-headers
2813""")
2814
R. David Murraya0b44b52010-12-02 21:47:19 +00002815 def test_make_msgid_domain(self):
2816 self.assertEqual(
2817 email.utils.make_msgid(domain='testdomain-string')[-19:],
2818 '@testdomain-string>')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002819
Ezio Melottib3aedd42010-11-20 19:04:17 +00002820
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002821# Test the iterator/generators
2822class TestIterators(TestEmailBase):
2823 def test_body_line_iterator(self):
2824 eq = self.assertEqual
2825 neq = self.ndiffAssertEqual
2826 # First a simple non-multipart message
2827 msg = self._msgobj('msg_01.txt')
2828 it = iterators.body_line_iterator(msg)
2829 lines = list(it)
2830 eq(len(lines), 6)
2831 neq(EMPTYSTRING.join(lines), msg.get_payload())
2832 # Now a more complicated multipart
2833 msg = self._msgobj('msg_02.txt')
2834 it = iterators.body_line_iterator(msg)
2835 lines = list(it)
2836 eq(len(lines), 43)
2837 with openfile('msg_19.txt') as fp:
2838 neq(EMPTYSTRING.join(lines), fp.read())
2839
2840 def test_typed_subpart_iterator(self):
2841 eq = self.assertEqual
2842 msg = self._msgobj('msg_04.txt')
2843 it = iterators.typed_subpart_iterator(msg, 'text')
2844 lines = []
2845 subparts = 0
2846 for subpart in it:
2847 subparts += 1
2848 lines.append(subpart.get_payload())
2849 eq(subparts, 2)
2850 eq(EMPTYSTRING.join(lines), """\
2851a simple kind of mirror
2852to reflect upon our own
2853a simple kind of mirror
2854to reflect upon our own
2855""")
2856
2857 def test_typed_subpart_iterator_default_type(self):
2858 eq = self.assertEqual
2859 msg = self._msgobj('msg_03.txt')
2860 it = iterators.typed_subpart_iterator(msg, 'text', 'plain')
2861 lines = []
2862 subparts = 0
2863 for subpart in it:
2864 subparts += 1
2865 lines.append(subpart.get_payload())
2866 eq(subparts, 1)
2867 eq(EMPTYSTRING.join(lines), """\
2868
2869Hi,
2870
2871Do you like this message?
2872
2873-Me
2874""")
2875
R. David Murray45bf773f2010-07-17 01:19:57 +00002876 def test_pushCR_LF(self):
2877 '''FeedParser BufferedSubFile.push() assumed it received complete
2878 line endings. A CR ending one push() followed by a LF starting
2879 the next push() added an empty line.
2880 '''
2881 imt = [
2882 ("a\r \n", 2),
2883 ("b", 0),
2884 ("c\n", 1),
2885 ("", 0),
2886 ("d\r\n", 1),
2887 ("e\r", 0),
2888 ("\nf", 1),
2889 ("\r\n", 1),
2890 ]
2891 from email.feedparser import BufferedSubFile, NeedMoreData
2892 bsf = BufferedSubFile()
2893 om = []
2894 nt = 0
2895 for il, n in imt:
2896 bsf.push(il)
2897 nt += n
2898 n1 = 0
2899 while True:
2900 ol = bsf.readline()
2901 if ol == NeedMoreData:
2902 break
2903 om.append(ol)
2904 n1 += 1
2905 self.assertTrue(n == n1)
2906 self.assertTrue(len(om) == nt)
2907 self.assertTrue(''.join([il for il, n in imt]) == ''.join(om))
2908
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002909
Ezio Melottib3aedd42010-11-20 19:04:17 +00002910
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002911class TestParsers(TestEmailBase):
2912 def test_header_parser(self):
2913 eq = self.assertEqual
2914 # Parse only the headers of a complex multipart MIME document
2915 with openfile('msg_02.txt') as fp:
2916 msg = HeaderParser().parse(fp)
2917 eq(msg['from'], 'ppp-request@zzz.org')
2918 eq(msg['to'], 'ppp@zzz.org')
2919 eq(msg.get_content_type(), 'multipart/mixed')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002920 self.assertFalse(msg.is_multipart())
2921 self.assertTrue(isinstance(msg.get_payload(), str))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002922
2923 def test_whitespace_continuation(self):
2924 eq = self.assertEqual
2925 # This message contains a line after the Subject: header that has only
2926 # whitespace, but it is not empty!
2927 msg = email.message_from_string("""\
2928From: aperson@dom.ain
2929To: bperson@dom.ain
2930Subject: the next line has a space on it
2931\x20
2932Date: Mon, 8 Apr 2002 15:09:19 -0400
2933Message-ID: spam
2934
2935Here's the message body
2936""")
2937 eq(msg['subject'], 'the next line has a space on it\n ')
2938 eq(msg['message-id'], 'spam')
2939 eq(msg.get_payload(), "Here's the message body\n")
2940
2941 def test_whitespace_continuation_last_header(self):
2942 eq = self.assertEqual
2943 # Like the previous test, but the subject line is the last
2944 # header.
2945 msg = email.message_from_string("""\
2946From: aperson@dom.ain
2947To: bperson@dom.ain
2948Date: Mon, 8 Apr 2002 15:09:19 -0400
2949Message-ID: spam
2950Subject: the next line has a space on it
2951\x20
2952
2953Here's the message body
2954""")
2955 eq(msg['subject'], 'the next line has a space on it\n ')
2956 eq(msg['message-id'], 'spam')
2957 eq(msg.get_payload(), "Here's the message body\n")
2958
2959 def test_crlf_separation(self):
2960 eq = self.assertEqual
Guido van Rossum98297ee2007-11-06 21:34:58 +00002961 with openfile('msg_26.txt', newline='\n') as fp:
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002962 msg = Parser().parse(fp)
2963 eq(len(msg.get_payload()), 2)
2964 part1 = msg.get_payload(0)
2965 eq(part1.get_content_type(), 'text/plain')
2966 eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n')
2967 part2 = msg.get_payload(1)
2968 eq(part2.get_content_type(), 'application/riscos')
2969
R. David Murray8451c4b2010-10-23 22:19:56 +00002970 def test_crlf_flatten(self):
2971 # Using newline='\n' preserves the crlfs in this input file.
2972 with openfile('msg_26.txt', newline='\n') as fp:
2973 text = fp.read()
2974 msg = email.message_from_string(text)
2975 s = StringIO()
2976 g = Generator(s)
2977 g.flatten(msg, linesep='\r\n')
2978 self.assertEqual(s.getvalue(), text)
2979
2980 maxDiff = None
2981
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002982 def test_multipart_digest_with_extra_mime_headers(self):
2983 eq = self.assertEqual
2984 neq = self.ndiffAssertEqual
2985 with openfile('msg_28.txt') as fp:
2986 msg = email.message_from_file(fp)
2987 # Structure is:
2988 # multipart/digest
2989 # message/rfc822
2990 # text/plain
2991 # message/rfc822
2992 # text/plain
2993 eq(msg.is_multipart(), 1)
2994 eq(len(msg.get_payload()), 2)
2995 part1 = msg.get_payload(0)
2996 eq(part1.get_content_type(), 'message/rfc822')
2997 eq(part1.is_multipart(), 1)
2998 eq(len(part1.get_payload()), 1)
2999 part1a = part1.get_payload(0)
3000 eq(part1a.is_multipart(), 0)
3001 eq(part1a.get_content_type(), 'text/plain')
3002 neq(part1a.get_payload(), 'message 1\n')
3003 # next message/rfc822
3004 part2 = msg.get_payload(1)
3005 eq(part2.get_content_type(), 'message/rfc822')
3006 eq(part2.is_multipart(), 1)
3007 eq(len(part2.get_payload()), 1)
3008 part2a = part2.get_payload(0)
3009 eq(part2a.is_multipart(), 0)
3010 eq(part2a.get_content_type(), 'text/plain')
3011 neq(part2a.get_payload(), 'message 2\n')
3012
3013 def test_three_lines(self):
3014 # A bug report by Andrew McNamara
3015 lines = ['From: Andrew Person <aperson@dom.ain',
3016 'Subject: Test',
3017 'Date: Tue, 20 Aug 2002 16:43:45 +1000']
3018 msg = email.message_from_string(NL.join(lines))
3019 self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000')
3020
3021 def test_strip_line_feed_and_carriage_return_in_headers(self):
3022 eq = self.assertEqual
3023 # For [ 1002475 ] email message parser doesn't handle \r\n correctly
3024 value1 = 'text'
3025 value2 = 'more text'
3026 m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % (
3027 value1, value2)
3028 msg = email.message_from_string(m)
3029 eq(msg.get('Header'), value1)
3030 eq(msg.get('Next-Header'), value2)
3031
3032 def test_rfc2822_header_syntax(self):
3033 eq = self.assertEqual
3034 m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
3035 msg = email.message_from_string(m)
3036 eq(len(msg), 3)
3037 eq(sorted(field for field in msg), ['!"#QUX;~', '>From', 'From'])
3038 eq(msg.get_payload(), 'body')
3039
3040 def test_rfc2822_space_not_allowed_in_header(self):
3041 eq = self.assertEqual
3042 m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
3043 msg = email.message_from_string(m)
3044 eq(len(msg.keys()), 0)
3045
3046 def test_rfc2822_one_character_header(self):
3047 eq = self.assertEqual
3048 m = 'A: first header\nB: second header\nCC: third header\n\nbody'
3049 msg = email.message_from_string(m)
3050 headers = msg.keys()
3051 headers.sort()
3052 eq(headers, ['A', 'B', 'CC'])
3053 eq(msg.get_payload(), 'body')
3054
R. David Murray45e0e142010-06-16 02:19:40 +00003055 def test_CRLFLF_at_end_of_part(self):
3056 # issue 5610: feedparser should not eat two chars from body part ending
3057 # with "\r\n\n".
3058 m = (
3059 "From: foo@bar.com\n"
3060 "To: baz\n"
3061 "Mime-Version: 1.0\n"
3062 "Content-Type: multipart/mixed; boundary=BOUNDARY\n"
3063 "\n"
3064 "--BOUNDARY\n"
3065 "Content-Type: text/plain\n"
3066 "\n"
3067 "body ending with CRLF newline\r\n"
3068 "\n"
3069 "--BOUNDARY--\n"
3070 )
3071 msg = email.message_from_string(m)
3072 self.assertTrue(msg.get_payload(0).get_payload().endswith('\r\n'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003073
Ezio Melottib3aedd42010-11-20 19:04:17 +00003074
R. David Murray96fd54e2010-10-08 15:55:28 +00003075class Test8BitBytesHandling(unittest.TestCase):
3076 # In Python3 all input is string, but that doesn't work if the actual input
3077 # uses an 8bit transfer encoding. To hack around that, in email 5.1 we
3078 # decode byte streams using the surrogateescape error handler, and
3079 # reconvert to binary at appropriate places if we detect surrogates. This
3080 # doesn't allow us to transform headers with 8bit bytes (they get munged),
3081 # but it does allow us to parse and preserve them, and to decode body
3082 # parts that use an 8bit CTE.
3083
3084 bodytest_msg = textwrap.dedent("""\
3085 From: foo@bar.com
3086 To: baz
3087 Mime-Version: 1.0
3088 Content-Type: text/plain; charset={charset}
3089 Content-Transfer-Encoding: {cte}
3090
3091 {bodyline}
3092 """)
3093
3094 def test_known_8bit_CTE(self):
3095 m = self.bodytest_msg.format(charset='utf-8',
3096 cte='8bit',
3097 bodyline='pöstal').encode('utf-8')
3098 msg = email.message_from_bytes(m)
3099 self.assertEqual(msg.get_payload(), "pöstal\n")
3100 self.assertEqual(msg.get_payload(decode=True),
3101 "pöstal\n".encode('utf-8'))
3102
3103 def test_unknown_8bit_CTE(self):
3104 m = self.bodytest_msg.format(charset='notavalidcharset',
3105 cte='8bit',
3106 bodyline='pöstal').encode('utf-8')
3107 msg = email.message_from_bytes(m)
R. David Murray92532142011-01-07 23:25:30 +00003108 self.assertEqual(msg.get_payload(), "p\uFFFD\uFFFDstal\n")
R. David Murray96fd54e2010-10-08 15:55:28 +00003109 self.assertEqual(msg.get_payload(decode=True),
3110 "pöstal\n".encode('utf-8'))
3111
3112 def test_8bit_in_quopri_body(self):
3113 # This is non-RFC compliant data...without 'decode' the library code
3114 # decodes the body using the charset from the headers, and because the
3115 # source byte really is utf-8 this works. This is likely to fail
3116 # against real dirty data (ie: produce mojibake), but the data is
3117 # invalid anyway so it is as good a guess as any. But this means that
3118 # this test just confirms the current behavior; that behavior is not
3119 # necessarily the best possible behavior. With 'decode' it is
3120 # returning the raw bytes, so that test should be of correct behavior,
3121 # or at least produce the same result that email4 did.
3122 m = self.bodytest_msg.format(charset='utf-8',
3123 cte='quoted-printable',
3124 bodyline='p=C3=B6stál').encode('utf-8')
3125 msg = email.message_from_bytes(m)
3126 self.assertEqual(msg.get_payload(), 'p=C3=B6stál\n')
3127 self.assertEqual(msg.get_payload(decode=True),
3128 'pöstál\n'.encode('utf-8'))
3129
3130 def test_invalid_8bit_in_non_8bit_cte_uses_replace(self):
3131 # This is similar to the previous test, but proves that if the 8bit
3132 # byte is undecodeable in the specified charset, it gets replaced
3133 # by the unicode 'unknown' character. Again, this may or may not
3134 # be the ideal behavior. Note that if decode=False none of the
3135 # decoders will get involved, so this is the only test we need
3136 # for this behavior.
3137 m = self.bodytest_msg.format(charset='ascii',
3138 cte='quoted-printable',
3139 bodyline='p=C3=B6stál').encode('utf-8')
3140 msg = email.message_from_bytes(m)
R. David Murray92532142011-01-07 23:25:30 +00003141 self.assertEqual(msg.get_payload(), 'p=C3=B6st\uFFFD\uFFFDl\n')
R. David Murray96fd54e2010-10-08 15:55:28 +00003142 self.assertEqual(msg.get_payload(decode=True),
3143 'pöstál\n'.encode('utf-8'))
3144
3145 def test_8bit_in_base64_body(self):
3146 # Sticking an 8bit byte in a base64 block makes it undecodable by
3147 # normal means, so the block is returned undecoded, but as bytes.
3148 m = self.bodytest_msg.format(charset='utf-8',
3149 cte='base64',
3150 bodyline='cMO2c3RhbAá=').encode('utf-8')
3151 msg = email.message_from_bytes(m)
3152 self.assertEqual(msg.get_payload(decode=True),
3153 'cMO2c3RhbAá=\n'.encode('utf-8'))
3154
3155 def test_8bit_in_uuencode_body(self):
3156 # Sticking an 8bit byte in a uuencode block makes it undecodable by
3157 # normal means, so the block is returned undecoded, but as bytes.
3158 m = self.bodytest_msg.format(charset='utf-8',
3159 cte='uuencode',
3160 bodyline='<,.V<W1A; á ').encode('utf-8')
3161 msg = email.message_from_bytes(m)
3162 self.assertEqual(msg.get_payload(decode=True),
3163 '<,.V<W1A; á \n'.encode('utf-8'))
3164
3165
R. David Murray92532142011-01-07 23:25:30 +00003166 headertest_headers = (
3167 ('From: foo@bar.com', ('From', 'foo@bar.com')),
3168 ('To: báz', ('To', '=?unknown-8bit?q?b=C3=A1z?=')),
3169 ('Subject: Maintenant je vous présente mon collègue, le pouf célèbre\n'
3170 '\tJean de Baddie',
3171 ('Subject', '=?unknown-8bit?q?Maintenant_je_vous_pr=C3=A9sente_mon_'
3172 'coll=C3=A8gue=2C_le_pouf_c=C3=A9l=C3=A8bre?=\n'
3173 ' =?unknown-8bit?q?_Jean_de_Baddie?=')),
3174 ('From: göst', ('From', '=?unknown-8bit?b?Z8O2c3Q=?=')),
3175 )
3176 headertest_msg = ('\n'.join([src for (src, _) in headertest_headers]) +
3177 '\nYes, they are flying.\n').encode('utf-8')
R. David Murray96fd54e2010-10-08 15:55:28 +00003178
3179 def test_get_8bit_header(self):
3180 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003181 self.assertEqual(str(msg.get('to')), 'b\uFFFD\uFFFDz')
3182 self.assertEqual(str(msg['to']), 'b\uFFFD\uFFFDz')
R. David Murray96fd54e2010-10-08 15:55:28 +00003183
3184 def test_print_8bit_headers(self):
3185 msg = email.message_from_bytes(self.headertest_msg)
3186 self.assertEqual(str(msg),
R. David Murray92532142011-01-07 23:25:30 +00003187 textwrap.dedent("""\
3188 From: {}
3189 To: {}
3190 Subject: {}
3191 From: {}
3192
3193 Yes, they are flying.
3194 """).format(*[expected[1] for (_, expected) in
3195 self.headertest_headers]))
R. David Murray96fd54e2010-10-08 15:55:28 +00003196
3197 def test_values_with_8bit_headers(self):
3198 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003199 self.assertListEqual([str(x) for x in msg.values()],
R. David Murray96fd54e2010-10-08 15:55:28 +00003200 ['foo@bar.com',
R. David Murray92532142011-01-07 23:25:30 +00003201 'b\uFFFD\uFFFDz',
3202 'Maintenant je vous pr\uFFFD\uFFFDsente mon '
3203 'coll\uFFFD\uFFFDgue, le pouf '
3204 'c\uFFFD\uFFFDl\uFFFD\uFFFDbre\n'
R. David Murray96fd54e2010-10-08 15:55:28 +00003205 '\tJean de Baddie',
R. David Murray92532142011-01-07 23:25:30 +00003206 "g\uFFFD\uFFFDst"])
R. David Murray96fd54e2010-10-08 15:55:28 +00003207
3208 def test_items_with_8bit_headers(self):
3209 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003210 self.assertListEqual([(str(x), str(y)) for (x, y) in msg.items()],
R. David Murray96fd54e2010-10-08 15:55:28 +00003211 [('From', 'foo@bar.com'),
R. David Murray92532142011-01-07 23:25:30 +00003212 ('To', 'b\uFFFD\uFFFDz'),
3213 ('Subject', 'Maintenant je vous '
3214 'pr\uFFFD\uFFFDsente '
3215 'mon coll\uFFFD\uFFFDgue, le pouf '
3216 'c\uFFFD\uFFFDl\uFFFD\uFFFDbre\n'
3217 '\tJean de Baddie'),
3218 ('From', 'g\uFFFD\uFFFDst')])
R. David Murray96fd54e2010-10-08 15:55:28 +00003219
3220 def test_get_all_with_8bit_headers(self):
3221 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003222 self.assertListEqual([str(x) for x in msg.get_all('from')],
R. David Murray96fd54e2010-10-08 15:55:28 +00003223 ['foo@bar.com',
R. David Murray92532142011-01-07 23:25:30 +00003224 'g\uFFFD\uFFFDst'])
R. David Murray96fd54e2010-10-08 15:55:28 +00003225
R David Murraya2150232011-03-16 21:11:23 -04003226 def test_get_content_type_with_8bit(self):
3227 msg = email.message_from_bytes(textwrap.dedent("""\
3228 Content-Type: text/pl\xA7in; charset=utf-8
3229 """).encode('latin-1'))
3230 self.assertEqual(msg.get_content_type(), "text/pl\uFFFDin")
3231 self.assertEqual(msg.get_content_maintype(), "text")
3232 self.assertEqual(msg.get_content_subtype(), "pl\uFFFDin")
3233
3234 def test_get_params_with_8bit(self):
3235 msg = email.message_from_bytes(
3236 'X-Header: foo=\xa7ne; b\xa7r=two; baz=three\n'.encode('latin-1'))
3237 self.assertEqual(msg.get_params(header='x-header'),
3238 [('foo', '\uFFFDne'), ('b\uFFFDr', 'two'), ('baz', 'three')])
3239 self.assertEqual(msg.get_param('Foo', header='x-header'), '\uFFFdne')
3240 # XXX: someday you might be able to get 'b\xa7r', for now you can't.
3241 self.assertEqual(msg.get_param('b\xa7r', header='x-header'), None)
3242
3243 def test_get_rfc2231_params_with_8bit(self):
3244 msg = email.message_from_bytes(textwrap.dedent("""\
3245 Content-Type: text/plain; charset=us-ascii;
3246 title*=us-ascii'en'This%20is%20not%20f\xa7n"""
3247 ).encode('latin-1'))
3248 self.assertEqual(msg.get_param('title'),
3249 ('us-ascii', 'en', 'This is not f\uFFFDn'))
3250
3251 def test_set_rfc2231_params_with_8bit(self):
3252 msg = email.message_from_bytes(textwrap.dedent("""\
3253 Content-Type: text/plain; charset=us-ascii;
3254 title*=us-ascii'en'This%20is%20not%20f\xa7n"""
3255 ).encode('latin-1'))
3256 msg.set_param('title', 'test')
3257 self.assertEqual(msg.get_param('title'), 'test')
3258
3259 def test_del_rfc2231_params_with_8bit(self):
3260 msg = email.message_from_bytes(textwrap.dedent("""\
3261 Content-Type: text/plain; charset=us-ascii;
3262 title*=us-ascii'en'This%20is%20not%20f\xa7n"""
3263 ).encode('latin-1'))
3264 msg.del_param('title')
3265 self.assertEqual(msg.get_param('title'), None)
3266 self.assertEqual(msg.get_content_maintype(), 'text')
3267
3268 def test_get_payload_with_8bit_cte_header(self):
3269 msg = email.message_from_bytes(textwrap.dedent("""\
3270 Content-Transfer-Encoding: b\xa7se64
3271 Content-Type: text/plain; charset=latin-1
3272
3273 payload
3274 """).encode('latin-1'))
3275 self.assertEqual(msg.get_payload(), 'payload\n')
3276 self.assertEqual(msg.get_payload(decode=True), b'payload\n')
3277
R. David Murray96fd54e2010-10-08 15:55:28 +00003278 non_latin_bin_msg = textwrap.dedent("""\
3279 From: foo@bar.com
3280 To: báz
3281 Subject: Maintenant je vous présente mon collègue, le pouf célèbre
3282 \tJean de Baddie
3283 Mime-Version: 1.0
3284 Content-Type: text/plain; charset="utf-8"
3285 Content-Transfer-Encoding: 8bit
3286
3287 Да, они летят.
3288 """).encode('utf-8')
3289
3290 def test_bytes_generator(self):
3291 msg = email.message_from_bytes(self.non_latin_bin_msg)
3292 out = BytesIO()
3293 email.generator.BytesGenerator(out).flatten(msg)
3294 self.assertEqual(out.getvalue(), self.non_latin_bin_msg)
3295
R. David Murray7372a072011-01-26 21:21:32 +00003296 def test_bytes_generator_handles_None_body(self):
3297 #Issue 11019
3298 msg = email.message.Message()
3299 out = BytesIO()
3300 email.generator.BytesGenerator(out).flatten(msg)
3301 self.assertEqual(out.getvalue(), b"\n")
3302
R. David Murray92532142011-01-07 23:25:30 +00003303 non_latin_bin_msg_as7bit_wrapped = textwrap.dedent("""\
R. David Murray96fd54e2010-10-08 15:55:28 +00003304 From: foo@bar.com
R. David Murray92532142011-01-07 23:25:30 +00003305 To: =?unknown-8bit?q?b=C3=A1z?=
3306 Subject: =?unknown-8bit?q?Maintenant_je_vous_pr=C3=A9sente_mon_coll=C3=A8gue?=
3307 =?unknown-8bit?q?=2C_le_pouf_c=C3=A9l=C3=A8bre?=
3308 =?unknown-8bit?q?_Jean_de_Baddie?=
R. David Murray96fd54e2010-10-08 15:55:28 +00003309 Mime-Version: 1.0
3310 Content-Type: text/plain; charset="utf-8"
3311 Content-Transfer-Encoding: base64
3312
3313 0JTQsCwg0L7QvdC4INC70LXRgtGP0YIuCg==
3314 """)
3315
3316 def test_generator_handles_8bit(self):
3317 msg = email.message_from_bytes(self.non_latin_bin_msg)
3318 out = StringIO()
3319 email.generator.Generator(out).flatten(msg)
R. David Murray92532142011-01-07 23:25:30 +00003320 self.assertEqual(out.getvalue(), self.non_latin_bin_msg_as7bit_wrapped)
R. David Murray96fd54e2010-10-08 15:55:28 +00003321
3322 def test_bytes_generator_with_unix_from(self):
3323 # The unixfrom contains a current date, so we can't check it
3324 # literally. Just make sure the first word is 'From' and the
3325 # rest of the message matches the input.
3326 msg = email.message_from_bytes(self.non_latin_bin_msg)
3327 out = BytesIO()
3328 email.generator.BytesGenerator(out).flatten(msg, unixfrom=True)
3329 lines = out.getvalue().split(b'\n')
3330 self.assertEqual(lines[0].split()[0], b'From')
3331 self.assertEqual(b'\n'.join(lines[1:]), self.non_latin_bin_msg)
3332
R. David Murray92532142011-01-07 23:25:30 +00003333 non_latin_bin_msg_as7bit = non_latin_bin_msg_as7bit_wrapped.split('\n')
3334 non_latin_bin_msg_as7bit[2:4] = [
3335 'Subject: =?unknown-8bit?q?Maintenant_je_vous_pr=C3=A9sente_mon_'
3336 'coll=C3=A8gue=2C_le_pouf_c=C3=A9l=C3=A8bre?=']
3337 non_latin_bin_msg_as7bit = '\n'.join(non_latin_bin_msg_as7bit)
3338
R. David Murray96fd54e2010-10-08 15:55:28 +00003339 def test_message_from_binary_file(self):
3340 fn = 'test.msg'
3341 self.addCleanup(unlink, fn)
3342 with open(fn, 'wb') as testfile:
3343 testfile.write(self.non_latin_bin_msg)
Brett Cannon384917a2010-10-29 23:08:36 +00003344 with open(fn, 'rb') as testfile:
3345 m = email.parser.BytesParser().parse(testfile)
R. David Murray96fd54e2010-10-08 15:55:28 +00003346 self.assertEqual(str(m), self.non_latin_bin_msg_as7bit)
3347
3348 latin_bin_msg = textwrap.dedent("""\
3349 From: foo@bar.com
3350 To: Dinsdale
3351 Subject: Nudge nudge, wink, wink
3352 Mime-Version: 1.0
3353 Content-Type: text/plain; charset="latin-1"
3354 Content-Transfer-Encoding: 8bit
3355
3356 oh là là, know what I mean, know what I mean?
3357 """).encode('latin-1')
3358
3359 latin_bin_msg_as7bit = textwrap.dedent("""\
3360 From: foo@bar.com
3361 To: Dinsdale
3362 Subject: Nudge nudge, wink, wink
3363 Mime-Version: 1.0
3364 Content-Type: text/plain; charset="iso-8859-1"
3365 Content-Transfer-Encoding: quoted-printable
3366
3367 oh l=E0 l=E0, know what I mean, know what I mean?
3368 """)
3369
3370 def test_string_generator_reencodes_to_quopri_when_appropriate(self):
3371 m = email.message_from_bytes(self.latin_bin_msg)
3372 self.assertEqual(str(m), self.latin_bin_msg_as7bit)
3373
3374 def test_decoded_generator_emits_unicode_body(self):
3375 m = email.message_from_bytes(self.latin_bin_msg)
3376 out = StringIO()
3377 email.generator.DecodedGenerator(out).flatten(m)
3378 #DecodedHeader output contains an extra blank line compared
3379 #to the input message. RDM: not sure if this is a bug or not,
3380 #but it is not specific to the 8bit->7bit conversion.
3381 self.assertEqual(out.getvalue(),
3382 self.latin_bin_msg.decode('latin-1')+'\n')
3383
3384 def test_bytes_feedparser(self):
3385 bfp = email.feedparser.BytesFeedParser()
3386 for i in range(0, len(self.latin_bin_msg), 10):
3387 bfp.feed(self.latin_bin_msg[i:i+10])
3388 m = bfp.close()
3389 self.assertEqual(str(m), self.latin_bin_msg_as7bit)
3390
R. David Murray8451c4b2010-10-23 22:19:56 +00003391 def test_crlf_flatten(self):
3392 with openfile('msg_26.txt', 'rb') as fp:
3393 text = fp.read()
3394 msg = email.message_from_bytes(text)
3395 s = BytesIO()
3396 g = email.generator.BytesGenerator(s)
3397 g.flatten(msg, linesep='\r\n')
3398 self.assertEqual(s.getvalue(), text)
R David Murrayc5c14722011-04-06 08:13:02 -04003399
3400 def test_8bit_multipart(self):
3401 # Issue 11605
3402 source = textwrap.dedent("""\
3403 Date: Fri, 18 Mar 2011 17:15:43 +0100
3404 To: foo@example.com
3405 From: foodwatch-Newsletter <bar@example.com>
3406 Subject: Aktuelles zu Japan, Klonfleisch und Smiley-System
3407 Message-ID: <76a486bee62b0d200f33dc2ca08220ad@localhost.localdomain>
3408 MIME-Version: 1.0
3409 Content-Type: multipart/alternative;
3410 boundary="b1_76a486bee62b0d200f33dc2ca08220ad"
3411
3412 --b1_76a486bee62b0d200f33dc2ca08220ad
3413 Content-Type: text/plain; charset="utf-8"
3414 Content-Transfer-Encoding: 8bit
3415
3416 Guten Tag, ,
3417
3418 mit großer Betroffenheit verfolgen auch wir im foodwatch-Team die
3419 Nachrichten aus Japan.
3420
3421
3422 --b1_76a486bee62b0d200f33dc2ca08220ad
3423 Content-Type: text/html; charset="utf-8"
3424 Content-Transfer-Encoding: 8bit
3425
3426 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
3427 "http://www.w3.org/TR/html4/loose.dtd">
3428 <html lang="de">
3429 <head>
3430 <title>foodwatch - Newsletter</title>
3431 </head>
3432 <body>
3433 <p>mit gro&szlig;er Betroffenheit verfolgen auch wir im foodwatch-Team
3434 die Nachrichten aus Japan.</p>
3435 </body>
3436 </html>
3437 --b1_76a486bee62b0d200f33dc2ca08220ad--
3438
3439 """).encode('utf-8')
3440 msg = email.message_from_bytes(source)
3441 s = BytesIO()
3442 g = email.generator.BytesGenerator(s)
3443 g.flatten(msg)
3444 self.assertEqual(s.getvalue(), source)
3445
R David Murray9fd170e2012-03-14 14:05:03 -04003446 def test_bytes_generator_b_encoding_linesep(self):
3447 # Issue 14062: b encoding was tacking on an extra \n.
3448 m = Message()
3449 # This has enough non-ascii that it should always end up b encoded.
3450 m['Subject'] = Header('žluťoučký kůň')
3451 s = BytesIO()
3452 g = email.generator.BytesGenerator(s)
3453 g.flatten(m, linesep='\r\n')
3454 self.assertEqual(
3455 s.getvalue(),
3456 b'Subject: =?utf-8?b?xb5sdcWlb3XEjWvDvSBrxa/FiA==?=\r\n\r\n')
3457
3458 def test_generator_b_encoding_linesep(self):
3459 # Since this broke in ByteGenerator, test Generator for completeness.
3460 m = Message()
3461 # This has enough non-ascii that it should always end up b encoded.
3462 m['Subject'] = Header('žluťoučký kůň')
3463 s = StringIO()
3464 g = email.generator.Generator(s)
3465 g.flatten(m, linesep='\r\n')
3466 self.assertEqual(
3467 s.getvalue(),
3468 'Subject: =?utf-8?b?xb5sdcWlb3XEjWvDvSBrxa/FiA==?=\r\n\r\n')
3469
R. David Murray8451c4b2010-10-23 22:19:56 +00003470 maxDiff = None
3471
Ezio Melottib3aedd42010-11-20 19:04:17 +00003472
R. David Murray719a4492010-11-21 16:53:48 +00003473class BaseTestBytesGeneratorIdempotent:
R. David Murray96fd54e2010-10-08 15:55:28 +00003474
R. David Murraye5db2632010-11-20 15:10:13 +00003475 maxDiff = None
3476
R. David Murray96fd54e2010-10-08 15:55:28 +00003477 def _msgobj(self, filename):
3478 with openfile(filename, 'rb') as fp:
3479 data = fp.read()
R. David Murray719a4492010-11-21 16:53:48 +00003480 data = self.normalize_linesep_regex.sub(self.blinesep, data)
R. David Murray96fd54e2010-10-08 15:55:28 +00003481 msg = email.message_from_bytes(data)
3482 return msg, data
3483
R. David Murray719a4492010-11-21 16:53:48 +00003484 def _idempotent(self, msg, data, unixfrom=False):
R. David Murray96fd54e2010-10-08 15:55:28 +00003485 b = BytesIO()
3486 g = email.generator.BytesGenerator(b, maxheaderlen=0)
R. David Murray719a4492010-11-21 16:53:48 +00003487 g.flatten(msg, unixfrom=unixfrom, linesep=self.linesep)
R. David Murraye5db2632010-11-20 15:10:13 +00003488 self.assertByteStringsEqual(data, b.getvalue())
R. David Murray96fd54e2010-10-08 15:55:28 +00003489
R. David Murraye5db2632010-11-20 15:10:13 +00003490 def assertByteStringsEqual(self, str1, str2):
R. David Murray719a4492010-11-21 16:53:48 +00003491 # Not using self.blinesep here is intentional. This way the output
3492 # is more useful when the failure results in mixed line endings.
R. David Murray96fd54e2010-10-08 15:55:28 +00003493 self.assertListEqual(str1.split(b'\n'), str2.split(b'\n'))
3494
3495
R. David Murray719a4492010-11-21 16:53:48 +00003496class TestBytesGeneratorIdempotentNL(BaseTestBytesGeneratorIdempotent,
3497 TestIdempotent):
3498 linesep = '\n'
3499 blinesep = b'\n'
3500 normalize_linesep_regex = re.compile(br'\r\n')
3501
3502
3503class TestBytesGeneratorIdempotentCRLF(BaseTestBytesGeneratorIdempotent,
3504 TestIdempotent):
3505 linesep = '\r\n'
3506 blinesep = b'\r\n'
3507 normalize_linesep_regex = re.compile(br'(?<!\r)\n')
3508
Ezio Melottib3aedd42010-11-20 19:04:17 +00003509
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003510class TestBase64(unittest.TestCase):
3511 def test_len(self):
3512 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00003513 eq(base64mime.header_length('hello'),
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003514 len(base64mime.body_encode(b'hello', eol='')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003515 for size in range(15):
3516 if size == 0 : bsize = 0
3517 elif size <= 3 : bsize = 4
3518 elif size <= 6 : bsize = 8
3519 elif size <= 9 : bsize = 12
3520 elif size <= 12: bsize = 16
3521 else : bsize = 20
Guido van Rossum9604e662007-08-30 03:46:43 +00003522 eq(base64mime.header_length('x' * size), bsize)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003523
3524 def test_decode(self):
3525 eq = self.assertEqual
Barry Warsaw2cc1f6d2007-08-30 14:28:55 +00003526 eq(base64mime.decode(''), b'')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003527 eq(base64mime.decode('aGVsbG8='), b'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003528
3529 def test_encode(self):
3530 eq = self.assertEqual
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003531 eq(base64mime.body_encode(b''), b'')
3532 eq(base64mime.body_encode(b'hello'), 'aGVsbG8=\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003533 # Test the binary flag
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003534 eq(base64mime.body_encode(b'hello\n'), 'aGVsbG8K\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003535 # Test the maxlinelen arg
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003536 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003537eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
3538eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
3539eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
3540eHh4eCB4eHh4IA==
3541""")
3542 # Test the eol argument
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003543 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40, eol='\r\n'),
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003544 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003545eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
3546eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
3547eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
3548eHh4eCB4eHh4IA==\r
3549""")
3550
3551 def test_header_encode(self):
3552 eq = self.assertEqual
3553 he = base64mime.header_encode
3554 eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=')
Guido van Rossum9604e662007-08-30 03:46:43 +00003555 eq(he('hello\r\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=')
3556 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003557 # Test the charset option
3558 eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=')
3559 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003560
3561
Ezio Melottib3aedd42010-11-20 19:04:17 +00003562
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003563class TestQuopri(unittest.TestCase):
3564 def setUp(self):
3565 # Set of characters (as byte integers) that don't need to be encoded
3566 # in headers.
3567 self.hlit = list(chain(
3568 range(ord('a'), ord('z') + 1),
3569 range(ord('A'), ord('Z') + 1),
3570 range(ord('0'), ord('9') + 1),
Guido van Rossum9604e662007-08-30 03:46:43 +00003571 (c for c in b'!*+-/')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003572 # Set of characters (as byte integers) that do need to be encoded in
3573 # headers.
3574 self.hnon = [c for c in range(256) if c not in self.hlit]
3575 assert len(self.hlit) + len(self.hnon) == 256
3576 # Set of characters (as byte integers) that don't need to be encoded
3577 # in bodies.
3578 self.blit = list(range(ord(' '), ord('~') + 1))
3579 self.blit.append(ord('\t'))
3580 self.blit.remove(ord('='))
3581 # Set of characters (as byte integers) that do need to be encoded in
3582 # bodies.
3583 self.bnon = [c for c in range(256) if c not in self.blit]
3584 assert len(self.blit) + len(self.bnon) == 256
3585
Guido van Rossum9604e662007-08-30 03:46:43 +00003586 def test_quopri_header_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003587 for c in self.hlit:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003588 self.assertFalse(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003589 'Should not be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003590 for c in self.hnon:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003591 self.assertTrue(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003592 'Should be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003593
Guido van Rossum9604e662007-08-30 03:46:43 +00003594 def test_quopri_body_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003595 for c in self.blit:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003596 self.assertFalse(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003597 'Should not be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003598 for c in self.bnon:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003599 self.assertTrue(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003600 'Should be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003601
3602 def test_header_quopri_len(self):
3603 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00003604 eq(quoprimime.header_length(b'hello'), 5)
3605 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003606 eq(len(quoprimime.header_encode(b'hello', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00003607 quoprimime.header_length(b'hello') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003608 # =?xxx?q?...?= means 10 extra characters
3609 10)
Guido van Rossum9604e662007-08-30 03:46:43 +00003610 eq(quoprimime.header_length(b'h@e@l@l@o@'), 20)
3611 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003612 eq(len(quoprimime.header_encode(b'h@e@l@l@o@', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00003613 quoprimime.header_length(b'h@e@l@l@o@') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003614 # =?xxx?q?...?= means 10 extra characters
3615 10)
3616 for c in self.hlit:
Guido van Rossum9604e662007-08-30 03:46:43 +00003617 eq(quoprimime.header_length(bytes([c])), 1,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003618 'expected length 1 for %r' % chr(c))
3619 for c in self.hnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00003620 # Space is special; it's encoded to _
3621 if c == ord(' '):
3622 continue
3623 eq(quoprimime.header_length(bytes([c])), 3,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003624 'expected length 3 for %r' % chr(c))
Guido van Rossum9604e662007-08-30 03:46:43 +00003625 eq(quoprimime.header_length(b' '), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003626
3627 def test_body_quopri_len(self):
3628 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003629 for c in self.blit:
Guido van Rossum9604e662007-08-30 03:46:43 +00003630 eq(quoprimime.body_length(bytes([c])), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003631 for c in self.bnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00003632 eq(quoprimime.body_length(bytes([c])), 3)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003633
3634 def test_quote_unquote_idempotent(self):
3635 for x in range(256):
3636 c = chr(x)
3637 self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c)
3638
R David Murrayec1b5b82011-03-23 14:19:05 -04003639 def _test_header_encode(self, header, expected_encoded_header, charset=None):
3640 if charset is None:
3641 encoded_header = quoprimime.header_encode(header)
3642 else:
3643 encoded_header = quoprimime.header_encode(header, charset)
3644 self.assertEqual(encoded_header, expected_encoded_header)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003645
R David Murraycafd79d2011-03-23 15:25:55 -04003646 def test_header_encode_null(self):
3647 self._test_header_encode(b'', '')
3648
R David Murrayec1b5b82011-03-23 14:19:05 -04003649 def test_header_encode_one_word(self):
3650 self._test_header_encode(b'hello', '=?iso-8859-1?q?hello?=')
3651
3652 def test_header_encode_two_lines(self):
3653 self._test_header_encode(b'hello\nworld',
3654 '=?iso-8859-1?q?hello=0Aworld?=')
3655
3656 def test_header_encode_non_ascii(self):
3657 self._test_header_encode(b'hello\xc7there',
3658 '=?iso-8859-1?q?hello=C7there?=')
3659
3660 def test_header_encode_alt_charset(self):
3661 self._test_header_encode(b'hello', '=?iso-8859-2?q?hello?=',
3662 charset='iso-8859-2')
3663
3664 def _test_header_decode(self, encoded_header, expected_decoded_header):
3665 decoded_header = quoprimime.header_decode(encoded_header)
3666 self.assertEqual(decoded_header, expected_decoded_header)
3667
3668 def test_header_decode_null(self):
3669 self._test_header_decode('', '')
3670
3671 def test_header_decode_one_word(self):
3672 self._test_header_decode('hello', 'hello')
3673
3674 def test_header_decode_two_lines(self):
3675 self._test_header_decode('hello=0Aworld', 'hello\nworld')
3676
3677 def test_header_decode_non_ascii(self):
3678 self._test_header_decode('hello=C7there', 'hello\xc7there')
3679
3680 def _test_decode(self, encoded, expected_decoded, eol=None):
3681 if eol is None:
3682 decoded = quoprimime.decode(encoded)
3683 else:
3684 decoded = quoprimime.decode(encoded, eol=eol)
3685 self.assertEqual(decoded, expected_decoded)
3686
3687 def test_decode_null_word(self):
3688 self._test_decode('', '')
3689
3690 def test_decode_null_line_null_word(self):
3691 self._test_decode('\r\n', '\n')
3692
3693 def test_decode_one_word(self):
3694 self._test_decode('hello', 'hello')
3695
3696 def test_decode_one_word_eol(self):
3697 self._test_decode('hello', 'hello', eol='X')
3698
3699 def test_decode_one_line(self):
3700 self._test_decode('hello\r\n', 'hello\n')
3701
3702 def test_decode_one_line_lf(self):
3703 self._test_decode('hello\n', 'hello\n')
3704
R David Murraycafd79d2011-03-23 15:25:55 -04003705 def test_decode_one_line_cr(self):
3706 self._test_decode('hello\r', 'hello\n')
3707
3708 def test_decode_one_line_nl(self):
3709 self._test_decode('hello\n', 'helloX', eol='X')
3710
3711 def test_decode_one_line_crnl(self):
3712 self._test_decode('hello\r\n', 'helloX', eol='X')
3713
R David Murrayec1b5b82011-03-23 14:19:05 -04003714 def test_decode_one_line_one_word(self):
3715 self._test_decode('hello\r\nworld', 'hello\nworld')
3716
3717 def test_decode_one_line_one_word_eol(self):
3718 self._test_decode('hello\r\nworld', 'helloXworld', eol='X')
3719
3720 def test_decode_two_lines(self):
3721 self._test_decode('hello\r\nworld\r\n', 'hello\nworld\n')
3722
R David Murraycafd79d2011-03-23 15:25:55 -04003723 def test_decode_two_lines_eol(self):
3724 self._test_decode('hello\r\nworld\r\n', 'helloXworldX', eol='X')
3725
R David Murrayec1b5b82011-03-23 14:19:05 -04003726 def test_decode_one_long_line(self):
3727 self._test_decode('Spam' * 250, 'Spam' * 250)
3728
3729 def test_decode_one_space(self):
3730 self._test_decode(' ', '')
3731
3732 def test_decode_multiple_spaces(self):
3733 self._test_decode(' ' * 5, '')
3734
3735 def test_decode_one_line_trailing_spaces(self):
3736 self._test_decode('hello \r\n', 'hello\n')
3737
3738 def test_decode_two_lines_trailing_spaces(self):
3739 self._test_decode('hello \r\nworld \r\n', 'hello\nworld\n')
3740
3741 def test_decode_quoted_word(self):
3742 self._test_decode('=22quoted=20words=22', '"quoted words"')
3743
3744 def test_decode_uppercase_quoting(self):
3745 self._test_decode('ab=CD=EF', 'ab\xcd\xef')
3746
3747 def test_decode_lowercase_quoting(self):
3748 self._test_decode('ab=cd=ef', 'ab\xcd\xef')
3749
3750 def test_decode_soft_line_break(self):
3751 self._test_decode('soft line=\r\nbreak', 'soft linebreak')
3752
3753 def test_decode_false_quoting(self):
3754 self._test_decode('A=1,B=A ==> A+B==2', 'A=1,B=A ==> A+B==2')
3755
3756 def _test_encode(self, body, expected_encoded_body, maxlinelen=None, eol=None):
3757 kwargs = {}
3758 if maxlinelen is None:
3759 # Use body_encode's default.
3760 maxlinelen = 76
3761 else:
3762 kwargs['maxlinelen'] = maxlinelen
3763 if eol is None:
3764 # Use body_encode's default.
3765 eol = '\n'
3766 else:
3767 kwargs['eol'] = eol
3768 encoded_body = quoprimime.body_encode(body, **kwargs)
3769 self.assertEqual(encoded_body, expected_encoded_body)
3770 if eol == '\n' or eol == '\r\n':
3771 # We know how to split the result back into lines, so maxlinelen
3772 # can be checked.
3773 for line in encoded_body.splitlines():
3774 self.assertLessEqual(len(line), maxlinelen)
3775
3776 def test_encode_null(self):
3777 self._test_encode('', '')
3778
3779 def test_encode_null_lines(self):
3780 self._test_encode('\n\n', '\n\n')
3781
3782 def test_encode_one_line(self):
3783 self._test_encode('hello\n', 'hello\n')
3784
3785 def test_encode_one_line_crlf(self):
3786 self._test_encode('hello\r\n', 'hello\n')
3787
3788 def test_encode_one_line_eol(self):
3789 self._test_encode('hello\n', 'hello\r\n', eol='\r\n')
3790
3791 def test_encode_one_space(self):
3792 self._test_encode(' ', '=20')
3793
3794 def test_encode_one_line_one_space(self):
3795 self._test_encode(' \n', '=20\n')
3796
R David Murrayb938c8c2011-03-24 12:19:26 -04003797# XXX: body_encode() expect strings, but uses ord(char) from these strings
3798# to index into a 256-entry list. For code points above 255, this will fail.
3799# Should there be a check for 8-bit only ord() values in body, or at least
3800# a comment about the expected input?
3801
3802 def test_encode_two_lines_one_space(self):
3803 self._test_encode(' \n \n', '=20\n=20\n')
3804
R David Murrayec1b5b82011-03-23 14:19:05 -04003805 def test_encode_one_word_trailing_spaces(self):
3806 self._test_encode('hello ', 'hello =20')
3807
3808 def test_encode_one_line_trailing_spaces(self):
3809 self._test_encode('hello \n', 'hello =20\n')
3810
3811 def test_encode_one_word_trailing_tab(self):
3812 self._test_encode('hello \t', 'hello =09')
3813
3814 def test_encode_one_line_trailing_tab(self):
3815 self._test_encode('hello \t\n', 'hello =09\n')
3816
3817 def test_encode_trailing_space_before_maxlinelen(self):
3818 self._test_encode('abcd \n1234', 'abcd =\n\n1234', maxlinelen=6)
3819
R David Murrayb938c8c2011-03-24 12:19:26 -04003820 def test_encode_trailing_space_at_maxlinelen(self):
3821 self._test_encode('abcd \n1234', 'abcd=\n=20\n1234', maxlinelen=5)
3822
R David Murrayec1b5b82011-03-23 14:19:05 -04003823 def test_encode_trailing_space_beyond_maxlinelen(self):
R David Murrayb938c8c2011-03-24 12:19:26 -04003824 self._test_encode('abcd \n1234', 'abc=\nd=20\n1234', maxlinelen=4)
3825
3826 def test_encode_whitespace_lines(self):
3827 self._test_encode(' \n' * 5, '=20\n' * 5)
R David Murrayec1b5b82011-03-23 14:19:05 -04003828
3829 def test_encode_quoted_equals(self):
3830 self._test_encode('a = b', 'a =3D b')
3831
3832 def test_encode_one_long_string(self):
3833 self._test_encode('x' * 100, 'x' * 75 + '=\n' + 'x' * 25)
3834
3835 def test_encode_one_long_line(self):
3836 self._test_encode('x' * 100 + '\n', 'x' * 75 + '=\n' + 'x' * 25 + '\n')
3837
3838 def test_encode_one_very_long_line(self):
3839 self._test_encode('x' * 200 + '\n',
3840 2 * ('x' * 75 + '=\n') + 'x' * 50 + '\n')
3841
3842 def test_encode_one_long_line(self):
3843 self._test_encode('x' * 100 + '\n', 'x' * 75 + '=\n' + 'x' * 25 + '\n')
3844
3845 def test_encode_shortest_maxlinelen(self):
3846 self._test_encode('=' * 5, '=3D=\n' * 4 + '=3D', maxlinelen=4)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003847
R David Murrayb938c8c2011-03-24 12:19:26 -04003848 def test_encode_maxlinelen_too_small(self):
3849 self.assertRaises(ValueError, self._test_encode, '', '', maxlinelen=3)
3850
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003851 def test_encode(self):
3852 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00003853 eq(quoprimime.body_encode(''), '')
3854 eq(quoprimime.body_encode('hello'), 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003855 # Test the binary flag
Guido van Rossum9604e662007-08-30 03:46:43 +00003856 eq(quoprimime.body_encode('hello\r\nworld'), 'hello\nworld')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003857 # Test the maxlinelen arg
Guido van Rossum9604e662007-08-30 03:46:43 +00003858 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003859xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
3860 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
3861x xxxx xxxx xxxx xxxx=20""")
3862 # Test the eol argument
Guido van Rossum9604e662007-08-30 03:46:43 +00003863 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'),
3864 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003865xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
3866 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
3867x xxxx xxxx xxxx xxxx=20""")
Guido van Rossum9604e662007-08-30 03:46:43 +00003868 eq(quoprimime.body_encode("""\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003869one line
3870
3871two line"""), """\
3872one line
3873
3874two line""")
3875
3876
Ezio Melottib3aedd42010-11-20 19:04:17 +00003877
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003878# Test the Charset class
3879class TestCharset(unittest.TestCase):
3880 def tearDown(self):
3881 from email import charset as CharsetModule
3882 try:
3883 del CharsetModule.CHARSETS['fake']
3884 except KeyError:
3885 pass
3886
Guido van Rossum9604e662007-08-30 03:46:43 +00003887 def test_codec_encodeable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003888 eq = self.assertEqual
3889 # Make sure us-ascii = no Unicode conversion
3890 c = Charset('us-ascii')
Guido van Rossum9604e662007-08-30 03:46:43 +00003891 eq(c.header_encode('Hello World!'), 'Hello World!')
3892 # Test 8-bit idempotency with us-ascii
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003893 s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
Guido van Rossum9604e662007-08-30 03:46:43 +00003894 self.assertRaises(UnicodeError, c.header_encode, s)
3895 c = Charset('utf-8')
3896 eq(c.header_encode(s), '=?utf-8?b?wqTCosKkwqTCpMKmwqTCqMKkwqo=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003897
3898 def test_body_encode(self):
3899 eq = self.assertEqual
3900 # Try a charset with QP body encoding
3901 c = Charset('iso-8859-1')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003902 eq('hello w=F6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003903 # Try a charset with Base64 body encoding
3904 c = Charset('utf-8')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003905 eq('aGVsbG8gd29ybGQ=\n', c.body_encode(b'hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003906 # Try a charset with None body encoding
3907 c = Charset('us-ascii')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003908 eq('hello world', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003909 # Try the convert argument, where input codec != output codec
3910 c = Charset('euc-jp')
3911 # With apologies to Tokio Kikuchi ;)
Barry Warsawbef9d212007-08-31 10:55:37 +00003912 # XXX FIXME
3913## try:
3914## eq('\x1b$B5FCO;~IW\x1b(B',
3915## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
3916## eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
3917## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
3918## except LookupError:
3919## # We probably don't have the Japanese codecs installed
3920## pass
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003921 # Testing SF bug #625509, which we have to fake, since there are no
3922 # built-in encodings where the header encoding is QP but the body
3923 # encoding is not.
3924 from email import charset as CharsetModule
R David Murray56a9d7e2011-03-15 12:20:02 -04003925 CharsetModule.add_charset('fake', CharsetModule.QP, None, 'utf-8')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003926 c = Charset('fake')
R David Murray56a9d7e2011-03-15 12:20:02 -04003927 eq('hello world', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003928
3929 def test_unicode_charset_name(self):
3930 charset = Charset('us-ascii')
3931 self.assertEqual(str(charset), 'us-ascii')
3932 self.assertRaises(errors.CharsetError, Charset, 'asc\xffii')
3933
3934
Ezio Melottib3aedd42010-11-20 19:04:17 +00003935
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003936# Test multilingual MIME headers.
3937class TestHeader(TestEmailBase):
3938 def test_simple(self):
3939 eq = self.ndiffAssertEqual
3940 h = Header('Hello World!')
3941 eq(h.encode(), 'Hello World!')
3942 h.append(' Goodbye World!')
3943 eq(h.encode(), 'Hello World! Goodbye World!')
3944
3945 def test_simple_surprise(self):
3946 eq = self.ndiffAssertEqual
3947 h = Header('Hello World!')
3948 eq(h.encode(), 'Hello World!')
3949 h.append('Goodbye World!')
3950 eq(h.encode(), 'Hello World! Goodbye World!')
3951
3952 def test_header_needs_no_decoding(self):
3953 h = 'no decoding needed'
3954 self.assertEqual(decode_header(h), [(h, None)])
3955
3956 def test_long(self):
3957 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.",
3958 maxlinelen=76)
3959 for l in h.encode(splitchars=' ').split('\n '):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003960 self.assertTrue(len(l) <= 76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003961
3962 def test_multilingual(self):
3963 eq = self.ndiffAssertEqual
3964 g = Charset("iso-8859-1")
3965 cz = Charset("iso-8859-2")
3966 utf8 = Charset("utf-8")
3967 g_head = (b'Die Mieter treten hier ein werden mit einem '
3968 b'Foerderband komfortabel den Korridor entlang, '
3969 b'an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, '
3970 b'gegen die rotierenden Klingen bef\xf6rdert. ')
3971 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
3972 b'd\xf9vtipu.. ')
3973 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
3974 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
3975 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
3976 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
3977 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
3978 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
3979 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
3980 '\u3044\u307e\u3059\u3002')
3981 h = Header(g_head, g)
3982 h.append(cz_head, cz)
3983 h.append(utf8_head, utf8)
Guido van Rossum9604e662007-08-30 03:46:43 +00003984 enc = h.encode(maxlinelen=76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003985 eq(enc, """\
Guido van Rossum9604e662007-08-30 03:46:43 +00003986=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_kom?=
3987 =?iso-8859-1?q?fortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wand?=
3988 =?iso-8859-1?q?gem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6r?=
3989 =?iso-8859-1?q?dert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003990 =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
3991 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
3992 =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
3993 =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
Guido van Rossum9604e662007-08-30 03:46:43 +00003994 =?utf-8?b?IE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5k?=
3995 =?utf-8?b?IGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIA=?=
3996 =?utf-8?b?44Gj44Gm44GE44G+44GZ44CC?=""")
3997 decoded = decode_header(enc)
3998 eq(len(decoded), 3)
3999 eq(decoded[0], (g_head, 'iso-8859-1'))
4000 eq(decoded[1], (cz_head, 'iso-8859-2'))
4001 eq(decoded[2], (utf8_head.encode('utf-8'), 'utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004002 ustr = str(h)
Guido van Rossum9604e662007-08-30 03:46:43 +00004003 eq(ustr,
4004 (b'Die Mieter treten hier ein werden mit einem Foerderband '
4005 b'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
4006 b'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
4007 b'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
4008 b'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
4009 b'\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
4010 b'\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
4011 b'\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
4012 b'\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
4013 b'\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
4014 b'\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
4015 b'\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
4016 b'\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
4017 b'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
4018 b'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
4019 b'\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82'
4020 ).decode('utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004021 # Test make_header()
4022 newh = make_header(decode_header(enc))
Guido van Rossum9604e662007-08-30 03:46:43 +00004023 eq(newh, h)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004024
4025 def test_empty_header_encode(self):
4026 h = Header()
4027 self.assertEqual(h.encode(), '')
Barry Warsaw8b3d6592007-08-30 02:10:49 +00004028
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004029 def test_header_ctor_default_args(self):
4030 eq = self.ndiffAssertEqual
4031 h = Header()
4032 eq(h, '')
4033 h.append('foo', Charset('iso-8859-1'))
Guido van Rossum9604e662007-08-30 03:46:43 +00004034 eq(h, 'foo')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004035
4036 def test_explicit_maxlinelen(self):
4037 eq = self.ndiffAssertEqual
4038 hstr = ('A very long line that must get split to something other '
4039 'than at the 76th character boundary to test the non-default '
4040 'behavior')
4041 h = Header(hstr)
4042 eq(h.encode(), '''\
4043A very long line that must get split to something other than at the 76th
4044 character boundary to test the non-default behavior''')
4045 eq(str(h), hstr)
4046 h = Header(hstr, header_name='Subject')
4047 eq(h.encode(), '''\
4048A very long line that must get split to something other than at the
4049 76th character boundary to test the non-default behavior''')
4050 eq(str(h), hstr)
4051 h = Header(hstr, maxlinelen=1024, header_name='Subject')
4052 eq(h.encode(), hstr)
4053 eq(str(h), hstr)
4054
Guido van Rossum9604e662007-08-30 03:46:43 +00004055 def test_quopri_splittable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004056 eq = self.ndiffAssertEqual
4057 h = Header(charset='iso-8859-1', maxlinelen=20)
Guido van Rossum9604e662007-08-30 03:46:43 +00004058 x = 'xxxx ' * 20
4059 h.append(x)
4060 s = h.encode()
4061 eq(s, """\
4062=?iso-8859-1?q?xxx?=
4063 =?iso-8859-1?q?x_?=
4064 =?iso-8859-1?q?xx?=
4065 =?iso-8859-1?q?xx?=
4066 =?iso-8859-1?q?_x?=
4067 =?iso-8859-1?q?xx?=
4068 =?iso-8859-1?q?x_?=
4069 =?iso-8859-1?q?xx?=
4070 =?iso-8859-1?q?xx?=
4071 =?iso-8859-1?q?_x?=
4072 =?iso-8859-1?q?xx?=
4073 =?iso-8859-1?q?x_?=
4074 =?iso-8859-1?q?xx?=
4075 =?iso-8859-1?q?xx?=
4076 =?iso-8859-1?q?_x?=
4077 =?iso-8859-1?q?xx?=
4078 =?iso-8859-1?q?x_?=
4079 =?iso-8859-1?q?xx?=
4080 =?iso-8859-1?q?xx?=
4081 =?iso-8859-1?q?_x?=
4082 =?iso-8859-1?q?xx?=
4083 =?iso-8859-1?q?x_?=
4084 =?iso-8859-1?q?xx?=
4085 =?iso-8859-1?q?xx?=
4086 =?iso-8859-1?q?_x?=
4087 =?iso-8859-1?q?xx?=
4088 =?iso-8859-1?q?x_?=
4089 =?iso-8859-1?q?xx?=
4090 =?iso-8859-1?q?xx?=
4091 =?iso-8859-1?q?_x?=
4092 =?iso-8859-1?q?xx?=
4093 =?iso-8859-1?q?x_?=
4094 =?iso-8859-1?q?xx?=
4095 =?iso-8859-1?q?xx?=
4096 =?iso-8859-1?q?_x?=
4097 =?iso-8859-1?q?xx?=
4098 =?iso-8859-1?q?x_?=
4099 =?iso-8859-1?q?xx?=
4100 =?iso-8859-1?q?xx?=
4101 =?iso-8859-1?q?_x?=
4102 =?iso-8859-1?q?xx?=
4103 =?iso-8859-1?q?x_?=
4104 =?iso-8859-1?q?xx?=
4105 =?iso-8859-1?q?xx?=
4106 =?iso-8859-1?q?_x?=
4107 =?iso-8859-1?q?xx?=
4108 =?iso-8859-1?q?x_?=
4109 =?iso-8859-1?q?xx?=
4110 =?iso-8859-1?q?xx?=
4111 =?iso-8859-1?q?_?=""")
4112 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004113 h = Header(charset='iso-8859-1', maxlinelen=40)
4114 h.append('xxxx ' * 20)
Guido van Rossum9604e662007-08-30 03:46:43 +00004115 s = h.encode()
4116 eq(s, """\
4117=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xxx?=
4118 =?iso-8859-1?q?x_xxxx_xxxx_xxxx_xxxx_?=
4119 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
4120 =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
4121 =?iso-8859-1?q?_xxxx_xxxx_?=""")
4122 eq(x, str(make_header(decode_header(s))))
4123
4124 def test_base64_splittable(self):
4125 eq = self.ndiffAssertEqual
4126 h = Header(charset='koi8-r', maxlinelen=20)
4127 x = 'xxxx ' * 20
4128 h.append(x)
4129 s = h.encode()
4130 eq(s, """\
4131=?koi8-r?b?eHh4?=
4132 =?koi8-r?b?eCB4?=
4133 =?koi8-r?b?eHh4?=
4134 =?koi8-r?b?IHh4?=
4135 =?koi8-r?b?eHgg?=
4136 =?koi8-r?b?eHh4?=
4137 =?koi8-r?b?eCB4?=
4138 =?koi8-r?b?eHh4?=
4139 =?koi8-r?b?IHh4?=
4140 =?koi8-r?b?eHgg?=
4141 =?koi8-r?b?eHh4?=
4142 =?koi8-r?b?eCB4?=
4143 =?koi8-r?b?eHh4?=
4144 =?koi8-r?b?IHh4?=
4145 =?koi8-r?b?eHgg?=
4146 =?koi8-r?b?eHh4?=
4147 =?koi8-r?b?eCB4?=
4148 =?koi8-r?b?eHh4?=
4149 =?koi8-r?b?IHh4?=
4150 =?koi8-r?b?eHgg?=
4151 =?koi8-r?b?eHh4?=
4152 =?koi8-r?b?eCB4?=
4153 =?koi8-r?b?eHh4?=
4154 =?koi8-r?b?IHh4?=
4155 =?koi8-r?b?eHgg?=
4156 =?koi8-r?b?eHh4?=
4157 =?koi8-r?b?eCB4?=
4158 =?koi8-r?b?eHh4?=
4159 =?koi8-r?b?IHh4?=
4160 =?koi8-r?b?eHgg?=
4161 =?koi8-r?b?eHh4?=
4162 =?koi8-r?b?eCB4?=
4163 =?koi8-r?b?eHh4?=
4164 =?koi8-r?b?IA==?=""")
4165 eq(x, str(make_header(decode_header(s))))
4166 h = Header(charset='koi8-r', maxlinelen=40)
4167 h.append(x)
4168 s = h.encode()
4169 eq(s, """\
4170=?koi8-r?b?eHh4eCB4eHh4IHh4eHggeHh4?=
4171 =?koi8-r?b?eCB4eHh4IHh4eHggeHh4eCB4?=
4172 =?koi8-r?b?eHh4IHh4eHggeHh4eCB4eHh4?=
4173 =?koi8-r?b?IHh4eHggeHh4eCB4eHh4IHh4?=
4174 =?koi8-r?b?eHggeHh4eCB4eHh4IHh4eHgg?=
4175 =?koi8-r?b?eHh4eCB4eHh4IA==?=""")
4176 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004177
4178 def test_us_ascii_header(self):
4179 eq = self.assertEqual
4180 s = 'hello'
4181 x = decode_header(s)
4182 eq(x, [('hello', None)])
4183 h = make_header(x)
4184 eq(s, h.encode())
4185
4186 def test_string_charset(self):
4187 eq = self.assertEqual
4188 h = Header()
4189 h.append('hello', 'iso-8859-1')
Guido van Rossum9604e662007-08-30 03:46:43 +00004190 eq(h, 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004191
4192## def test_unicode_error(self):
4193## raises = self.assertRaises
4194## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')
4195## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')
4196## h = Header()
4197## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')
4198## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')
4199## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')
4200
4201 def test_utf8_shortest(self):
4202 eq = self.assertEqual
4203 h = Header('p\xf6stal', 'utf-8')
4204 eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
4205 h = Header('\u83ca\u5730\u6642\u592b', 'utf-8')
4206 eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
4207
4208 def test_bad_8bit_header(self):
4209 raises = self.assertRaises
4210 eq = self.assertEqual
4211 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
4212 raises(UnicodeError, Header, x)
4213 h = Header()
4214 raises(UnicodeError, h.append, x)
4215 e = x.decode('utf-8', 'replace')
4216 eq(str(Header(x, errors='replace')), e)
4217 h.append(x, errors='replace')
4218 eq(str(h), e)
4219
R David Murray041015c2011-03-25 15:10:55 -04004220 def test_escaped_8bit_header(self):
4221 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
R David Murray6bdb1762011-06-18 12:30:55 -04004222 e = x.decode('ascii', 'surrogateescape')
4223 h = Header(e, charset=email.charset.UNKNOWN8BIT)
R David Murray041015c2011-03-25 15:10:55 -04004224 self.assertEqual(str(h),
4225 'Ynwp4dUEbay Auction Semiar- No Charge \uFFFD Earn Big')
4226 self.assertEqual(email.header.decode_header(h), [(x, 'unknown-8bit')])
4227
R David Murraye5e366c2011-06-18 12:57:28 -04004228 def test_header_handles_binary_unknown8bit(self):
4229 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
4230 h = Header(x, charset=email.charset.UNKNOWN8BIT)
4231 self.assertEqual(str(h),
4232 'Ynwp4dUEbay Auction Semiar- No Charge \uFFFD Earn Big')
4233 self.assertEqual(email.header.decode_header(h), [(x, 'unknown-8bit')])
4234
4235 def test_make_header_handles_binary_unknown8bit(self):
4236 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
4237 h = Header(x, charset=email.charset.UNKNOWN8BIT)
4238 h2 = email.header.make_header(email.header.decode_header(h))
4239 self.assertEqual(str(h2),
4240 'Ynwp4dUEbay Auction Semiar- No Charge \uFFFD Earn Big')
4241 self.assertEqual(email.header.decode_header(h2), [(x, 'unknown-8bit')])
4242
R David Murray041015c2011-03-25 15:10:55 -04004243 def test_modify_returned_list_does_not_change_header(self):
4244 h = Header('test')
4245 chunks = email.header.decode_header(h)
4246 chunks.append(('ascii', 'test2'))
4247 self.assertEqual(str(h), 'test')
4248
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004249 def test_encoded_adjacent_nonencoded(self):
4250 eq = self.assertEqual
4251 h = Header()
4252 h.append('hello', 'iso-8859-1')
4253 h.append('world')
4254 s = h.encode()
4255 eq(s, '=?iso-8859-1?q?hello?= world')
4256 h = make_header(decode_header(s))
4257 eq(h.encode(), s)
4258
4259 def test_whitespace_eater(self):
4260 eq = self.assertEqual
4261 s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
4262 parts = decode_header(s)
4263 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)])
4264 hdr = make_header(parts)
4265 eq(hdr.encode(),
4266 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
4267
4268 def test_broken_base64_header(self):
4269 raises = self.assertRaises
R. David Murrayc4e69cc2010-08-03 22:14:10 +00004270 s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3I ?='
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004271 raises(errors.HeaderParseError, decode_header, s)
4272
R. David Murray477efb32011-01-05 01:39:32 +00004273 def test_shift_jis_charset(self):
4274 h = Header('文', charset='shift_jis')
4275 self.assertEqual(h.encode(), '=?iso-2022-jp?b?GyRCSjgbKEI=?=')
4276
R David Murrayde912762011-03-16 18:26:23 -04004277 def test_flatten_header_with_no_value(self):
4278 # Issue 11401 (regression from email 4.x) Note that the space after
4279 # the header doesn't reflect the input, but this is also the way
4280 # email 4.x behaved. At some point it would be nice to fix that.
4281 msg = email.message_from_string("EmptyHeader:")
4282 self.assertEqual(str(msg), "EmptyHeader: \n\n")
4283
R David Murray01581ee2011-04-18 10:04:34 -04004284 def test_encode_preserves_leading_ws_on_value(self):
4285 msg = Message()
4286 msg['SomeHeader'] = ' value with leading ws'
4287 self.assertEqual(str(msg), "SomeHeader: value with leading ws\n\n")
4288
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004289
Ezio Melottib3aedd42010-11-20 19:04:17 +00004290
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004291# Test RFC 2231 header parameters (en/de)coding
4292class TestRFC2231(TestEmailBase):
4293 def test_get_param(self):
4294 eq = self.assertEqual
4295 msg = self._msgobj('msg_29.txt')
4296 eq(msg.get_param('title'),
4297 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
4298 eq(msg.get_param('title', unquote=False),
4299 ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
4300
4301 def test_set_param(self):
4302 eq = self.ndiffAssertEqual
4303 msg = Message()
4304 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4305 charset='us-ascii')
4306 eq(msg.get_param('title'),
4307 ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
4308 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4309 charset='us-ascii', language='en')
4310 eq(msg.get_param('title'),
4311 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
4312 msg = self._msgobj('msg_01.txt')
4313 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4314 charset='us-ascii', language='en')
4315 eq(msg.as_string(maxheaderlen=78), """\
4316Return-Path: <bbb@zzz.org>
4317Delivered-To: bbb@zzz.org
4318Received: by mail.zzz.org (Postfix, from userid 889)
4319\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
4320MIME-Version: 1.0
4321Content-Transfer-Encoding: 7bit
4322Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
4323From: bbb@ddd.com (John X. Doe)
4324To: bbb@zzz.org
4325Subject: This is a test message
4326Date: Fri, 4 May 2001 14:05:44 -0400
4327Content-Type: text/plain; charset=us-ascii;
R. David Murraydfd7eb02010-12-24 22:36:49 +00004328 title*=us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004329
4330
4331Hi,
4332
4333Do you like this message?
4334
4335-Me
4336""")
4337
R David Murraya2860e82011-04-16 09:20:30 -04004338 def test_set_param_requote(self):
4339 msg = Message()
4340 msg.set_param('title', 'foo')
4341 self.assertEqual(msg['content-type'], 'text/plain; title="foo"')
4342 msg.set_param('title', 'bar', requote=False)
4343 self.assertEqual(msg['content-type'], 'text/plain; title=bar')
4344 # tspecial is still quoted.
4345 msg.set_param('title', "(bar)bell", requote=False)
4346 self.assertEqual(msg['content-type'], 'text/plain; title="(bar)bell"')
4347
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004348 def test_del_param(self):
4349 eq = self.ndiffAssertEqual
4350 msg = self._msgobj('msg_01.txt')
4351 msg.set_param('foo', 'bar', charset='us-ascii', language='en')
4352 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4353 charset='us-ascii', language='en')
4354 msg.del_param('foo', header='Content-Type')
4355 eq(msg.as_string(maxheaderlen=78), """\
4356Return-Path: <bbb@zzz.org>
4357Delivered-To: bbb@zzz.org
4358Received: by mail.zzz.org (Postfix, from userid 889)
4359\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
4360MIME-Version: 1.0
4361Content-Transfer-Encoding: 7bit
4362Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
4363From: bbb@ddd.com (John X. Doe)
4364To: bbb@zzz.org
4365Subject: This is a test message
4366Date: Fri, 4 May 2001 14:05:44 -0400
4367Content-Type: text/plain; charset="us-ascii";
R. David Murraydfd7eb02010-12-24 22:36:49 +00004368 title*=us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004369
4370
4371Hi,
4372
4373Do you like this message?
4374
4375-Me
4376""")
4377
4378 def test_rfc2231_get_content_charset(self):
4379 eq = self.assertEqual
4380 msg = self._msgobj('msg_32.txt')
4381 eq(msg.get_content_charset(), 'us-ascii')
4382
R. David Murraydfd7eb02010-12-24 22:36:49 +00004383 def test_rfc2231_parse_rfc_quoting(self):
4384 m = textwrap.dedent('''\
4385 Content-Disposition: inline;
4386 \tfilename*0*=''This%20is%20even%20more%20;
4387 \tfilename*1*=%2A%2A%2Afun%2A%2A%2A%20;
4388 \tfilename*2="is it not.pdf"
4389
4390 ''')
4391 msg = email.message_from_string(m)
4392 self.assertEqual(msg.get_filename(),
4393 'This is even more ***fun*** is it not.pdf')
4394 self.assertEqual(m, msg.as_string())
4395
4396 def test_rfc2231_parse_extra_quoting(self):
4397 m = textwrap.dedent('''\
4398 Content-Disposition: inline;
4399 \tfilename*0*="''This%20is%20even%20more%20";
4400 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4401 \tfilename*2="is it not.pdf"
4402
4403 ''')
4404 msg = email.message_from_string(m)
4405 self.assertEqual(msg.get_filename(),
4406 'This is even more ***fun*** is it not.pdf')
4407 self.assertEqual(m, msg.as_string())
4408
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004409 def test_rfc2231_no_language_or_charset(self):
4410 m = '''\
4411Content-Transfer-Encoding: 8bit
4412Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
4413Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
4414
4415'''
4416 msg = email.message_from_string(m)
4417 param = msg.get_param('NAME')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00004418 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004419 self.assertEqual(
4420 param,
4421 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
4422
4423 def test_rfc2231_no_language_or_charset_in_filename(self):
4424 m = '''\
4425Content-Disposition: inline;
4426\tfilename*0*="''This%20is%20even%20more%20";
4427\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4428\tfilename*2="is it not.pdf"
4429
4430'''
4431 msg = email.message_from_string(m)
4432 self.assertEqual(msg.get_filename(),
4433 'This is even more ***fun*** is it not.pdf')
4434
4435 def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
4436 m = '''\
4437Content-Disposition: inline;
4438\tfilename*0*="''This%20is%20even%20more%20";
4439\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4440\tfilename*2="is it not.pdf"
4441
4442'''
4443 msg = email.message_from_string(m)
4444 self.assertEqual(msg.get_filename(),
4445 'This is even more ***fun*** is it not.pdf')
4446
4447 def test_rfc2231_partly_encoded(self):
4448 m = '''\
4449Content-Disposition: inline;
4450\tfilename*0="''This%20is%20even%20more%20";
4451\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4452\tfilename*2="is it not.pdf"
4453
4454'''
4455 msg = email.message_from_string(m)
4456 self.assertEqual(
4457 msg.get_filename(),
4458 'This%20is%20even%20more%20***fun*** is it not.pdf')
4459
4460 def test_rfc2231_partly_nonencoded(self):
4461 m = '''\
4462Content-Disposition: inline;
4463\tfilename*0="This%20is%20even%20more%20";
4464\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
4465\tfilename*2="is it not.pdf"
4466
4467'''
4468 msg = email.message_from_string(m)
4469 self.assertEqual(
4470 msg.get_filename(),
4471 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
4472
4473 def test_rfc2231_no_language_or_charset_in_boundary(self):
4474 m = '''\
4475Content-Type: multipart/alternative;
4476\tboundary*0*="''This%20is%20even%20more%20";
4477\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
4478\tboundary*2="is it not.pdf"
4479
4480'''
4481 msg = email.message_from_string(m)
4482 self.assertEqual(msg.get_boundary(),
4483 'This is even more ***fun*** is it not.pdf')
4484
4485 def test_rfc2231_no_language_or_charset_in_charset(self):
4486 # This is a nonsensical charset value, but tests the code anyway
4487 m = '''\
4488Content-Type: text/plain;
4489\tcharset*0*="This%20is%20even%20more%20";
4490\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
4491\tcharset*2="is it not.pdf"
4492
4493'''
4494 msg = email.message_from_string(m)
4495 self.assertEqual(msg.get_content_charset(),
4496 'this is even more ***fun*** is it not.pdf')
4497
4498 def test_rfc2231_bad_encoding_in_filename(self):
4499 m = '''\
4500Content-Disposition: inline;
4501\tfilename*0*="bogus'xx'This%20is%20even%20more%20";
4502\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4503\tfilename*2="is it not.pdf"
4504
4505'''
4506 msg = email.message_from_string(m)
4507 self.assertEqual(msg.get_filename(),
4508 'This is even more ***fun*** is it not.pdf')
4509
4510 def test_rfc2231_bad_encoding_in_charset(self):
4511 m = """\
4512Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
4513
4514"""
4515 msg = email.message_from_string(m)
4516 # This should return None because non-ascii characters in the charset
4517 # are not allowed.
4518 self.assertEqual(msg.get_content_charset(), None)
4519
4520 def test_rfc2231_bad_character_in_charset(self):
4521 m = """\
4522Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D
4523
4524"""
4525 msg = email.message_from_string(m)
4526 # This should return None because non-ascii characters in the charset
4527 # are not allowed.
4528 self.assertEqual(msg.get_content_charset(), None)
4529
4530 def test_rfc2231_bad_character_in_filename(self):
4531 m = '''\
4532Content-Disposition: inline;
4533\tfilename*0*="ascii'xx'This%20is%20even%20more%20";
4534\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4535\tfilename*2*="is it not.pdf%E2"
4536
4537'''
4538 msg = email.message_from_string(m)
4539 self.assertEqual(msg.get_filename(),
4540 'This is even more ***fun*** is it not.pdf\ufffd')
4541
4542 def test_rfc2231_unknown_encoding(self):
4543 m = """\
4544Content-Transfer-Encoding: 8bit
4545Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
4546
4547"""
4548 msg = email.message_from_string(m)
4549 self.assertEqual(msg.get_filename(), 'myfile.txt')
4550
4551 def test_rfc2231_single_tick_in_filename_extended(self):
4552 eq = self.assertEqual
4553 m = """\
4554Content-Type: application/x-foo;
4555\tname*0*=\"Frank's\"; name*1*=\" Document\"
4556
4557"""
4558 msg = email.message_from_string(m)
4559 charset, language, s = msg.get_param('name')
4560 eq(charset, None)
4561 eq(language, None)
4562 eq(s, "Frank's Document")
4563
4564 def test_rfc2231_single_tick_in_filename(self):
4565 m = """\
4566Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
4567
4568"""
4569 msg = email.message_from_string(m)
4570 param = msg.get_param('name')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00004571 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004572 self.assertEqual(param, "Frank's Document")
4573
4574 def test_rfc2231_tick_attack_extended(self):
4575 eq = self.assertEqual
4576 m = """\
4577Content-Type: application/x-foo;
4578\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
4579
4580"""
4581 msg = email.message_from_string(m)
4582 charset, language, s = msg.get_param('name')
4583 eq(charset, 'us-ascii')
4584 eq(language, 'en-us')
4585 eq(s, "Frank's Document")
4586
4587 def test_rfc2231_tick_attack(self):
4588 m = """\
4589Content-Type: application/x-foo;
4590\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
4591
4592"""
4593 msg = email.message_from_string(m)
4594 param = msg.get_param('name')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00004595 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004596 self.assertEqual(param, "us-ascii'en-us'Frank's Document")
4597
4598 def test_rfc2231_no_extended_values(self):
4599 eq = self.assertEqual
4600 m = """\
4601Content-Type: application/x-foo; name=\"Frank's Document\"
4602
4603"""
4604 msg = email.message_from_string(m)
4605 eq(msg.get_param('name'), "Frank's Document")
4606
4607 def test_rfc2231_encoded_then_unencoded_segments(self):
4608 eq = self.assertEqual
4609 m = """\
4610Content-Type: application/x-foo;
4611\tname*0*=\"us-ascii'en-us'My\";
4612\tname*1=\" Document\";
4613\tname*2*=\" For You\"
4614
4615"""
4616 msg = email.message_from_string(m)
4617 charset, language, s = msg.get_param('name')
4618 eq(charset, 'us-ascii')
4619 eq(language, 'en-us')
4620 eq(s, 'My Document For You')
4621
4622 def test_rfc2231_unencoded_then_encoded_segments(self):
4623 eq = self.assertEqual
4624 m = """\
4625Content-Type: application/x-foo;
4626\tname*0=\"us-ascii'en-us'My\";
4627\tname*1*=\" Document\";
4628\tname*2*=\" For You\"
4629
4630"""
4631 msg = email.message_from_string(m)
4632 charset, language, s = msg.get_param('name')
4633 eq(charset, 'us-ascii')
4634 eq(language, 'en-us')
4635 eq(s, 'My Document For You')
4636
4637
Ezio Melottib3aedd42010-11-20 19:04:17 +00004638
R. David Murraya8f480f2010-01-16 18:30:03 +00004639# Tests to ensure that signed parts of an email are completely preserved, as
4640# required by RFC1847 section 2.1. Note that these are incomplete, because the
4641# email package does not currently always preserve the body. See issue 1670765.
4642class TestSigned(TestEmailBase):
4643
4644 def _msg_and_obj(self, filename):
4645 with openfile(findfile(filename)) as fp:
4646 original = fp.read()
4647 msg = email.message_from_string(original)
4648 return original, msg
4649
4650 def _signed_parts_eq(self, original, result):
4651 # Extract the first mime part of each message
4652 import re
4653 repart = re.compile(r'^--([^\n]+)\n(.*?)\n--\1$', re.S | re.M)
4654 inpart = repart.search(original).group(2)
4655 outpart = repart.search(result).group(2)
4656 self.assertEqual(outpart, inpart)
4657
4658 def test_long_headers_as_string(self):
4659 original, msg = self._msg_and_obj('msg_45.txt')
4660 result = msg.as_string()
4661 self._signed_parts_eq(original, result)
4662
4663 def test_long_headers_as_string_maxheaderlen(self):
4664 original, msg = self._msg_and_obj('msg_45.txt')
4665 result = msg.as_string(maxheaderlen=60)
4666 self._signed_parts_eq(original, result)
4667
4668 def test_long_headers_flatten(self):
4669 original, msg = self._msg_and_obj('msg_45.txt')
4670 fp = StringIO()
4671 Generator(fp).flatten(msg)
4672 result = fp.getvalue()
4673 self._signed_parts_eq(original, result)
4674
4675
Ezio Melottib3aedd42010-11-20 19:04:17 +00004676
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004677def _testclasses():
4678 mod = sys.modules[__name__]
4679 return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
4680
4681
4682def suite():
4683 suite = unittest.TestSuite()
4684 for testclass in _testclasses():
4685 suite.addTest(unittest.makeSuite(testclass))
4686 return suite
4687
4688
4689def test_main():
4690 for testclass in _testclasses():
4691 run_unittest(testclass)
4692
4693
Ezio Melottib3aedd42010-11-20 19:04:17 +00004694
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004695if __name__ == '__main__':
4696 unittest.main(defaultTest='suite')