blob: 95dc4af7c35fe59f74c031c53e30f2735f2efeba [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
R David Murray6a31bc62012-07-22 21:47:53 -04001278 def test_mangle_from_in_preamble_and_epilog(self):
1279 s = StringIO()
1280 g = Generator(s, mangle_from_=True)
1281 msg = email.message_from_string(textwrap.dedent("""\
1282 From: foo@bar.com
1283 Mime-Version: 1.0
1284 Content-Type: multipart/mixed; boundary=XXX
1285
1286 From somewhere unknown
1287
1288 --XXX
1289 Content-Type: text/plain
1290
1291 foo
1292
1293 --XXX--
1294
1295 From somewhere unknowable
1296 """))
1297 g.flatten(msg)
1298 self.assertEqual(len([1 for x in s.getvalue().split('\n')
1299 if x.startswith('>From ')]), 2)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001300
Ezio Melottib3aedd42010-11-20 19:04:17 +00001301
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001302# Test the basic MIMEAudio class
1303class TestMIMEAudio(unittest.TestCase):
1304 def setUp(self):
1305 # Make sure we pick up the audiotest.au that lives in email/test/data.
1306 # In Python, there's an audiotest.au living in Lib/test but that isn't
1307 # included in some binary distros that don't include the test
1308 # package. The trailing empty string on the .join() is significant
1309 # since findfile() will do a dirname().
1310 datadir = os.path.join(os.path.dirname(landmark), 'data', '')
1311 with open(findfile('audiotest.au', datadir), 'rb') as fp:
1312 self._audiodata = fp.read()
1313 self._au = MIMEAudio(self._audiodata)
1314
1315 def test_guess_minor_type(self):
1316 self.assertEqual(self._au.get_content_type(), 'audio/basic')
1317
1318 def test_encoding(self):
1319 payload = self._au.get_payload()
R. David Murray7da8f062010-06-04 16:11:08 +00001320 self.assertEqual(base64.decodebytes(bytes(payload, 'ascii')),
1321 self._audiodata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001322
1323 def test_checkSetMinor(self):
1324 au = MIMEAudio(self._audiodata, 'fish')
1325 self.assertEqual(au.get_content_type(), 'audio/fish')
1326
1327 def test_add_header(self):
1328 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001329 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001330 self._au.add_header('Content-Disposition', 'attachment',
1331 filename='audiotest.au')
1332 eq(self._au['content-disposition'],
1333 'attachment; filename="audiotest.au"')
1334 eq(self._au.get_params(header='content-disposition'),
1335 [('attachment', ''), ('filename', 'audiotest.au')])
1336 eq(self._au.get_param('filename', header='content-disposition'),
1337 'audiotest.au')
1338 missing = []
1339 eq(self._au.get_param('attachment', header='content-disposition'), '')
1340 unless(self._au.get_param('foo', failobj=missing,
1341 header='content-disposition') is missing)
1342 # Try some missing stuff
1343 unless(self._au.get_param('foobar', missing) is missing)
1344 unless(self._au.get_param('attachment', missing,
1345 header='foobar') is missing)
1346
1347
Ezio Melottib3aedd42010-11-20 19:04:17 +00001348
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001349# Test the basic MIMEImage class
1350class TestMIMEImage(unittest.TestCase):
1351 def setUp(self):
1352 with openfile('PyBanner048.gif', 'rb') as fp:
1353 self._imgdata = fp.read()
1354 self._im = MIMEImage(self._imgdata)
1355
1356 def test_guess_minor_type(self):
1357 self.assertEqual(self._im.get_content_type(), 'image/gif')
1358
1359 def test_encoding(self):
1360 payload = self._im.get_payload()
R. David Murray7da8f062010-06-04 16:11:08 +00001361 self.assertEqual(base64.decodebytes(bytes(payload, 'ascii')),
1362 self._imgdata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001363
1364 def test_checkSetMinor(self):
1365 im = MIMEImage(self._imgdata, 'fish')
1366 self.assertEqual(im.get_content_type(), 'image/fish')
1367
1368 def test_add_header(self):
1369 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001370 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001371 self._im.add_header('Content-Disposition', 'attachment',
1372 filename='dingusfish.gif')
1373 eq(self._im['content-disposition'],
1374 'attachment; filename="dingusfish.gif"')
1375 eq(self._im.get_params(header='content-disposition'),
1376 [('attachment', ''), ('filename', 'dingusfish.gif')])
1377 eq(self._im.get_param('filename', header='content-disposition'),
1378 'dingusfish.gif')
1379 missing = []
1380 eq(self._im.get_param('attachment', header='content-disposition'), '')
1381 unless(self._im.get_param('foo', failobj=missing,
1382 header='content-disposition') is missing)
1383 # Try some missing stuff
1384 unless(self._im.get_param('foobar', missing) is missing)
1385 unless(self._im.get_param('attachment', missing,
1386 header='foobar') is missing)
1387
1388
Ezio Melottib3aedd42010-11-20 19:04:17 +00001389
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001390# Test the basic MIMEApplication class
1391class TestMIMEApplication(unittest.TestCase):
1392 def test_headers(self):
1393 eq = self.assertEqual
Barry Warsaw8b2af272007-08-31 03:04:26 +00001394 msg = MIMEApplication(b'\xfa\xfb\xfc\xfd\xfe\xff')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001395 eq(msg.get_content_type(), 'application/octet-stream')
1396 eq(msg['content-transfer-encoding'], 'base64')
1397
1398 def test_body(self):
1399 eq = self.assertEqual
R David Murray6d94bd42011-03-16 15:52:22 -04001400 bytesdata = b'\xfa\xfb\xfc\xfd\xfe\xff'
1401 msg = MIMEApplication(bytesdata)
1402 # whitespace in the cte encoded block is RFC-irrelevant.
1403 eq(msg.get_payload().strip(), '+vv8/f7/')
1404 eq(msg.get_payload(decode=True), bytesdata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001405
1406
Ezio Melottib3aedd42010-11-20 19:04:17 +00001407
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001408# Test the basic MIMEText class
1409class TestMIMEText(unittest.TestCase):
1410 def setUp(self):
1411 self._msg = MIMEText('hello there')
1412
1413 def test_types(self):
1414 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001415 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001416 eq(self._msg.get_content_type(), 'text/plain')
1417 eq(self._msg.get_param('charset'), 'us-ascii')
1418 missing = []
1419 unless(self._msg.get_param('foobar', missing) is missing)
1420 unless(self._msg.get_param('charset', missing, header='foobar')
1421 is missing)
1422
1423 def test_payload(self):
1424 self.assertEqual(self._msg.get_payload(), 'hello there')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001425 self.assertTrue(not self._msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001426
1427 def test_charset(self):
1428 eq = self.assertEqual
1429 msg = MIMEText('hello there', _charset='us-ascii')
1430 eq(msg.get_charset().input_charset, 'us-ascii')
1431 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1432
R. David Murray850fc852010-06-03 01:58:28 +00001433 def test_7bit_input(self):
1434 eq = self.assertEqual
1435 msg = MIMEText('hello there', _charset='us-ascii')
1436 eq(msg.get_charset().input_charset, 'us-ascii')
1437 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1438
1439 def test_7bit_input_no_charset(self):
1440 eq = self.assertEqual
1441 msg = MIMEText('hello there')
1442 eq(msg.get_charset(), 'us-ascii')
1443 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1444 self.assertTrue('hello there' in msg.as_string())
1445
1446 def test_utf8_input(self):
1447 teststr = '\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
1448 eq = self.assertEqual
1449 msg = MIMEText(teststr, _charset='utf-8')
1450 eq(msg.get_charset().output_charset, 'utf-8')
1451 eq(msg['content-type'], 'text/plain; charset="utf-8"')
1452 eq(msg.get_payload(decode=True), teststr.encode('utf-8'))
1453
1454 @unittest.skip("can't fix because of backward compat in email5, "
1455 "will fix in email6")
1456 def test_utf8_input_no_charset(self):
1457 teststr = '\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
1458 self.assertRaises(UnicodeEncodeError, MIMEText, teststr)
1459
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001460
Ezio Melottib3aedd42010-11-20 19:04:17 +00001461
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001462# Test complicated multipart/* messages
1463class TestMultipart(TestEmailBase):
1464 def setUp(self):
1465 with openfile('PyBanner048.gif', 'rb') as fp:
1466 data = fp.read()
1467 container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
1468 image = MIMEImage(data, name='dingusfish.gif')
1469 image.add_header('content-disposition', 'attachment',
1470 filename='dingusfish.gif')
1471 intro = MIMEText('''\
1472Hi there,
1473
1474This is the dingus fish.
1475''')
1476 container.attach(intro)
1477 container.attach(image)
1478 container['From'] = 'Barry <barry@digicool.com>'
1479 container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>'
1480 container['Subject'] = 'Here is your dingus fish'
1481
1482 now = 987809702.54848599
1483 timetuple = time.localtime(now)
1484 if timetuple[-1] == 0:
1485 tzsecs = time.timezone
1486 else:
1487 tzsecs = time.altzone
1488 if tzsecs > 0:
1489 sign = '-'
1490 else:
1491 sign = '+'
1492 tzoffset = ' %s%04d' % (sign, tzsecs / 36)
1493 container['Date'] = time.strftime(
1494 '%a, %d %b %Y %H:%M:%S',
1495 time.localtime(now)) + tzoffset
1496 self._msg = container
1497 self._im = image
1498 self._txt = intro
1499
1500 def test_hierarchy(self):
1501 # convenience
1502 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001503 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001504 raises = self.assertRaises
1505 # tests
1506 m = self._msg
1507 unless(m.is_multipart())
1508 eq(m.get_content_type(), 'multipart/mixed')
1509 eq(len(m.get_payload()), 2)
1510 raises(IndexError, m.get_payload, 2)
1511 m0 = m.get_payload(0)
1512 m1 = m.get_payload(1)
1513 unless(m0 is self._txt)
1514 unless(m1 is self._im)
1515 eq(m.get_payload(), [m0, m1])
1516 unless(not m0.is_multipart())
1517 unless(not m1.is_multipart())
1518
1519 def test_empty_multipart_idempotent(self):
1520 text = """\
1521Content-Type: multipart/mixed; boundary="BOUNDARY"
1522MIME-Version: 1.0
1523Subject: A subject
1524To: aperson@dom.ain
1525From: bperson@dom.ain
1526
1527
1528--BOUNDARY
1529
1530
1531--BOUNDARY--
1532"""
1533 msg = Parser().parsestr(text)
1534 self.ndiffAssertEqual(text, msg.as_string())
1535
1536 def test_no_parts_in_a_multipart_with_none_epilogue(self):
1537 outer = MIMEBase('multipart', 'mixed')
1538 outer['Subject'] = 'A subject'
1539 outer['To'] = 'aperson@dom.ain'
1540 outer['From'] = 'bperson@dom.ain'
1541 outer.set_boundary('BOUNDARY')
1542 self.ndiffAssertEqual(outer.as_string(), '''\
1543Content-Type: multipart/mixed; boundary="BOUNDARY"
1544MIME-Version: 1.0
1545Subject: A subject
1546To: aperson@dom.ain
1547From: bperson@dom.ain
1548
1549--BOUNDARY
1550
1551--BOUNDARY--''')
1552
1553 def test_no_parts_in_a_multipart_with_empty_epilogue(self):
1554 outer = MIMEBase('multipart', 'mixed')
1555 outer['Subject'] = 'A subject'
1556 outer['To'] = 'aperson@dom.ain'
1557 outer['From'] = 'bperson@dom.ain'
1558 outer.preamble = ''
1559 outer.epilogue = ''
1560 outer.set_boundary('BOUNDARY')
1561 self.ndiffAssertEqual(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
1569--BOUNDARY
1570
1571--BOUNDARY--
1572''')
1573
1574 def test_one_part_in_a_multipart(self):
1575 eq = self.ndiffAssertEqual
1576 outer = MIMEBase('multipart', 'mixed')
1577 outer['Subject'] = 'A subject'
1578 outer['To'] = 'aperson@dom.ain'
1579 outer['From'] = 'bperson@dom.ain'
1580 outer.set_boundary('BOUNDARY')
1581 msg = MIMEText('hello world')
1582 outer.attach(msg)
1583 eq(outer.as_string(), '''\
1584Content-Type: multipart/mixed; boundary="BOUNDARY"
1585MIME-Version: 1.0
1586Subject: A subject
1587To: aperson@dom.ain
1588From: bperson@dom.ain
1589
1590--BOUNDARY
1591Content-Type: text/plain; charset="us-ascii"
1592MIME-Version: 1.0
1593Content-Transfer-Encoding: 7bit
1594
1595hello world
1596--BOUNDARY--''')
1597
1598 def test_seq_parts_in_a_multipart_with_empty_preamble(self):
1599 eq = self.ndiffAssertEqual
1600 outer = MIMEBase('multipart', 'mixed')
1601 outer['Subject'] = 'A subject'
1602 outer['To'] = 'aperson@dom.ain'
1603 outer['From'] = 'bperson@dom.ain'
1604 outer.preamble = ''
1605 msg = MIMEText('hello world')
1606 outer.attach(msg)
1607 outer.set_boundary('BOUNDARY')
1608 eq(outer.as_string(), '''\
1609Content-Type: multipart/mixed; boundary="BOUNDARY"
1610MIME-Version: 1.0
1611Subject: A subject
1612To: aperson@dom.ain
1613From: bperson@dom.ain
1614
1615
1616--BOUNDARY
1617Content-Type: text/plain; charset="us-ascii"
1618MIME-Version: 1.0
1619Content-Transfer-Encoding: 7bit
1620
1621hello world
1622--BOUNDARY--''')
1623
1624
1625 def test_seq_parts_in_a_multipart_with_none_preamble(self):
1626 eq = self.ndiffAssertEqual
1627 outer = MIMEBase('multipart', 'mixed')
1628 outer['Subject'] = 'A subject'
1629 outer['To'] = 'aperson@dom.ain'
1630 outer['From'] = 'bperson@dom.ain'
1631 outer.preamble = None
1632 msg = MIMEText('hello world')
1633 outer.attach(msg)
1634 outer.set_boundary('BOUNDARY')
1635 eq(outer.as_string(), '''\
1636Content-Type: multipart/mixed; boundary="BOUNDARY"
1637MIME-Version: 1.0
1638Subject: A subject
1639To: aperson@dom.ain
1640From: bperson@dom.ain
1641
1642--BOUNDARY
1643Content-Type: text/plain; charset="us-ascii"
1644MIME-Version: 1.0
1645Content-Transfer-Encoding: 7bit
1646
1647hello world
1648--BOUNDARY--''')
1649
1650
1651 def test_seq_parts_in_a_multipart_with_none_epilogue(self):
1652 eq = self.ndiffAssertEqual
1653 outer = MIMEBase('multipart', 'mixed')
1654 outer['Subject'] = 'A subject'
1655 outer['To'] = 'aperson@dom.ain'
1656 outer['From'] = 'bperson@dom.ain'
1657 outer.epilogue = None
1658 msg = MIMEText('hello world')
1659 outer.attach(msg)
1660 outer.set_boundary('BOUNDARY')
1661 eq(outer.as_string(), '''\
1662Content-Type: multipart/mixed; boundary="BOUNDARY"
1663MIME-Version: 1.0
1664Subject: A subject
1665To: aperson@dom.ain
1666From: bperson@dom.ain
1667
1668--BOUNDARY
1669Content-Type: text/plain; charset="us-ascii"
1670MIME-Version: 1.0
1671Content-Transfer-Encoding: 7bit
1672
1673hello world
1674--BOUNDARY--''')
1675
1676
1677 def test_seq_parts_in_a_multipart_with_empty_epilogue(self):
1678 eq = self.ndiffAssertEqual
1679 outer = MIMEBase('multipart', 'mixed')
1680 outer['Subject'] = 'A subject'
1681 outer['To'] = 'aperson@dom.ain'
1682 outer['From'] = 'bperson@dom.ain'
1683 outer.epilogue = ''
1684 msg = MIMEText('hello world')
1685 outer.attach(msg)
1686 outer.set_boundary('BOUNDARY')
1687 eq(outer.as_string(), '''\
1688Content-Type: multipart/mixed; boundary="BOUNDARY"
1689MIME-Version: 1.0
1690Subject: A subject
1691To: aperson@dom.ain
1692From: bperson@dom.ain
1693
1694--BOUNDARY
1695Content-Type: text/plain; charset="us-ascii"
1696MIME-Version: 1.0
1697Content-Transfer-Encoding: 7bit
1698
1699hello world
1700--BOUNDARY--
1701''')
1702
1703
1704 def test_seq_parts_in_a_multipart_with_nl_epilogue(self):
1705 eq = self.ndiffAssertEqual
1706 outer = MIMEBase('multipart', 'mixed')
1707 outer['Subject'] = 'A subject'
1708 outer['To'] = 'aperson@dom.ain'
1709 outer['From'] = 'bperson@dom.ain'
1710 outer.epilogue = '\n'
1711 msg = MIMEText('hello world')
1712 outer.attach(msg)
1713 outer.set_boundary('BOUNDARY')
1714 eq(outer.as_string(), '''\
1715Content-Type: multipart/mixed; boundary="BOUNDARY"
1716MIME-Version: 1.0
1717Subject: A subject
1718To: aperson@dom.ain
1719From: bperson@dom.ain
1720
1721--BOUNDARY
1722Content-Type: text/plain; charset="us-ascii"
1723MIME-Version: 1.0
1724Content-Transfer-Encoding: 7bit
1725
1726hello world
1727--BOUNDARY--
1728
1729''')
1730
1731 def test_message_external_body(self):
1732 eq = self.assertEqual
1733 msg = self._msgobj('msg_36.txt')
1734 eq(len(msg.get_payload()), 2)
1735 msg1 = msg.get_payload(1)
1736 eq(msg1.get_content_type(), 'multipart/alternative')
1737 eq(len(msg1.get_payload()), 2)
1738 for subpart in msg1.get_payload():
1739 eq(subpart.get_content_type(), 'message/external-body')
1740 eq(len(subpart.get_payload()), 1)
1741 subsubpart = subpart.get_payload(0)
1742 eq(subsubpart.get_content_type(), 'text/plain')
1743
1744 def test_double_boundary(self):
1745 # msg_37.txt is a multipart that contains two dash-boundary's in a
1746 # row. Our interpretation of RFC 2046 calls for ignoring the second
1747 # and subsequent boundaries.
1748 msg = self._msgobj('msg_37.txt')
1749 self.assertEqual(len(msg.get_payload()), 3)
1750
1751 def test_nested_inner_contains_outer_boundary(self):
1752 eq = self.ndiffAssertEqual
1753 # msg_38.txt has an inner part that contains outer boundaries. My
1754 # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say
1755 # these are illegal and should be interpreted as unterminated inner
1756 # parts.
1757 msg = self._msgobj('msg_38.txt')
1758 sfp = StringIO()
1759 iterators._structure(msg, sfp)
1760 eq(sfp.getvalue(), """\
1761multipart/mixed
1762 multipart/mixed
1763 multipart/alternative
1764 text/plain
1765 text/plain
1766 text/plain
1767 text/plain
1768""")
1769
1770 def test_nested_with_same_boundary(self):
1771 eq = self.ndiffAssertEqual
1772 # msg 39.txt is similarly evil in that it's got inner parts that use
1773 # the same boundary as outer parts. Again, I believe the way this is
1774 # parsed is closest to the spirit of RFC 2046
1775 msg = self._msgobj('msg_39.txt')
1776 sfp = StringIO()
1777 iterators._structure(msg, sfp)
1778 eq(sfp.getvalue(), """\
1779multipart/mixed
1780 multipart/mixed
1781 multipart/alternative
1782 application/octet-stream
1783 application/octet-stream
1784 text/plain
1785""")
1786
1787 def test_boundary_in_non_multipart(self):
1788 msg = self._msgobj('msg_40.txt')
1789 self.assertEqual(msg.as_string(), '''\
1790MIME-Version: 1.0
1791Content-Type: text/html; boundary="--961284236552522269"
1792
1793----961284236552522269
1794Content-Type: text/html;
1795Content-Transfer-Encoding: 7Bit
1796
1797<html></html>
1798
1799----961284236552522269--
1800''')
1801
1802 def test_boundary_with_leading_space(self):
1803 eq = self.assertEqual
1804 msg = email.message_from_string('''\
1805MIME-Version: 1.0
1806Content-Type: multipart/mixed; boundary=" XXXX"
1807
1808-- XXXX
1809Content-Type: text/plain
1810
1811
1812-- XXXX
1813Content-Type: text/plain
1814
1815-- XXXX--
1816''')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001817 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001818 eq(msg.get_boundary(), ' XXXX')
1819 eq(len(msg.get_payload()), 2)
1820
1821 def test_boundary_without_trailing_newline(self):
1822 m = Parser().parsestr("""\
1823Content-Type: multipart/mixed; boundary="===============0012394164=="
1824MIME-Version: 1.0
1825
1826--===============0012394164==
1827Content-Type: image/file1.jpg
1828MIME-Version: 1.0
1829Content-Transfer-Encoding: base64
1830
1831YXNkZg==
1832--===============0012394164==--""")
Ezio Melottib3aedd42010-11-20 19:04:17 +00001833 self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001834
1835
Ezio Melottib3aedd42010-11-20 19:04:17 +00001836
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001837# Test some badly formatted messages
1838class TestNonConformant(TestEmailBase):
1839 def test_parse_missing_minor_type(self):
1840 eq = self.assertEqual
1841 msg = self._msgobj('msg_14.txt')
1842 eq(msg.get_content_type(), 'text/plain')
1843 eq(msg.get_content_maintype(), 'text')
1844 eq(msg.get_content_subtype(), 'plain')
1845
1846 def test_same_boundary_inner_outer(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001847 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001848 msg = self._msgobj('msg_15.txt')
1849 # XXX We can probably eventually do better
1850 inner = msg.get_payload(0)
1851 unless(hasattr(inner, 'defects'))
1852 self.assertEqual(len(inner.defects), 1)
1853 unless(isinstance(inner.defects[0],
1854 errors.StartBoundaryNotFoundDefect))
1855
1856 def test_multipart_no_boundary(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001857 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001858 msg = self._msgobj('msg_25.txt')
1859 unless(isinstance(msg.get_payload(), str))
1860 self.assertEqual(len(msg.defects), 2)
1861 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1862 unless(isinstance(msg.defects[1],
1863 errors.MultipartInvariantViolationDefect))
1864
1865 def test_invalid_content_type(self):
1866 eq = self.assertEqual
1867 neq = self.ndiffAssertEqual
1868 msg = Message()
1869 # RFC 2045, $5.2 says invalid yields text/plain
1870 msg['Content-Type'] = 'text'
1871 eq(msg.get_content_maintype(), 'text')
1872 eq(msg.get_content_subtype(), 'plain')
1873 eq(msg.get_content_type(), 'text/plain')
1874 # Clear the old value and try something /really/ invalid
1875 del msg['content-type']
1876 msg['Content-Type'] = 'foo'
1877 eq(msg.get_content_maintype(), 'text')
1878 eq(msg.get_content_subtype(), 'plain')
1879 eq(msg.get_content_type(), 'text/plain')
1880 # Still, make sure that the message is idempotently generated
1881 s = StringIO()
1882 g = Generator(s)
1883 g.flatten(msg)
1884 neq(s.getvalue(), 'Content-Type: foo\n\n')
1885
1886 def test_no_start_boundary(self):
1887 eq = self.ndiffAssertEqual
1888 msg = self._msgobj('msg_31.txt')
1889 eq(msg.get_payload(), """\
1890--BOUNDARY
1891Content-Type: text/plain
1892
1893message 1
1894
1895--BOUNDARY
1896Content-Type: text/plain
1897
1898message 2
1899
1900--BOUNDARY--
1901""")
1902
1903 def test_no_separating_blank_line(self):
1904 eq = self.ndiffAssertEqual
1905 msg = self._msgobj('msg_35.txt')
1906 eq(msg.as_string(), """\
1907From: aperson@dom.ain
1908To: bperson@dom.ain
1909Subject: here's something interesting
1910
1911counter to RFC 2822, there's no separating newline here
1912""")
1913
1914 def test_lying_multipart(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001915 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001916 msg = self._msgobj('msg_41.txt')
1917 unless(hasattr(msg, 'defects'))
1918 self.assertEqual(len(msg.defects), 2)
1919 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1920 unless(isinstance(msg.defects[1],
1921 errors.MultipartInvariantViolationDefect))
1922
1923 def test_missing_start_boundary(self):
1924 outer = self._msgobj('msg_42.txt')
1925 # The message structure is:
1926 #
1927 # multipart/mixed
1928 # text/plain
1929 # message/rfc822
1930 # multipart/mixed [*]
1931 #
1932 # [*] This message is missing its start boundary
1933 bad = outer.get_payload(1).get_payload(0)
1934 self.assertEqual(len(bad.defects), 1)
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001935 self.assertTrue(isinstance(bad.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001936 errors.StartBoundaryNotFoundDefect))
1937
1938 def test_first_line_is_continuation_header(self):
1939 eq = self.assertEqual
1940 m = ' Line 1\nLine 2\nLine 3'
1941 msg = email.message_from_string(m)
1942 eq(msg.keys(), [])
1943 eq(msg.get_payload(), 'Line 2\nLine 3')
1944 eq(len(msg.defects), 1)
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001945 self.assertTrue(isinstance(msg.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001946 errors.FirstHeaderLineIsContinuationDefect))
1947 eq(msg.defects[0].line, ' Line 1\n')
1948
1949
Ezio Melottib3aedd42010-11-20 19:04:17 +00001950
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001951# Test RFC 2047 header encoding and decoding
Guido van Rossum9604e662007-08-30 03:46:43 +00001952class TestRFC2047(TestEmailBase):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001953 def test_rfc2047_multiline(self):
1954 eq = self.assertEqual
1955 s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz
1956 foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?="""
1957 dh = decode_header(s)
1958 eq(dh, [
1959 (b'Re:', None),
1960 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland'),
1961 (b'baz foo bar', None),
1962 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland')])
1963 header = make_header(dh)
1964 eq(str(header),
1965 'Re: r\xe4ksm\xf6rg\xe5s baz foo bar r\xe4ksm\xf6rg\xe5s')
Barry Warsaw00b34222007-08-31 02:35:00 +00001966 self.ndiffAssertEqual(header.encode(maxlinelen=76), """\
Guido van Rossum9604e662007-08-30 03:46:43 +00001967Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar =?mac-iceland?q?r=8Aksm?=
1968 =?mac-iceland?q?=9Arg=8Cs?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001969
1970 def test_whitespace_eater_unicode(self):
1971 eq = self.assertEqual
1972 s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard@dom.ain>'
1973 dh = decode_header(s)
1974 eq(dh, [(b'Andr\xe9', 'iso-8859-1'),
1975 (b'Pirard <pirard@dom.ain>', None)])
1976 header = str(make_header(dh))
1977 eq(header, 'Andr\xe9 Pirard <pirard@dom.ain>')
1978
1979 def test_whitespace_eater_unicode_2(self):
1980 eq = self.assertEqual
1981 s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?='
1982 dh = decode_header(s)
1983 eq(dh, [(b'The', None), (b'quick brown fox', 'iso-8859-1'),
1984 (b'jumped over the', None), (b'lazy dog', 'iso-8859-1')])
1985 hu = str(make_header(dh))
1986 eq(hu, 'The quick brown fox jumped over the lazy dog')
1987
1988 def test_rfc2047_missing_whitespace(self):
1989 s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord'
1990 dh = decode_header(s)
1991 self.assertEqual(dh, [(s, None)])
1992
1993 def test_rfc2047_with_whitespace(self):
1994 s = 'Sm =?ISO-8859-1?B?9g==?= rg =?ISO-8859-1?B?5Q==?= sbord'
1995 dh = decode_header(s)
1996 self.assertEqual(dh, [(b'Sm', None), (b'\xf6', 'iso-8859-1'),
1997 (b'rg', None), (b'\xe5', 'iso-8859-1'),
1998 (b'sbord', None)])
1999
R. David Murrayc4e69cc2010-08-03 22:14:10 +00002000 def test_rfc2047_B_bad_padding(self):
2001 s = '=?iso-8859-1?B?%s?='
2002 data = [ # only test complete bytes
2003 ('dm==', b'v'), ('dm=', b'v'), ('dm', b'v'),
2004 ('dmk=', b'vi'), ('dmk', b'vi')
2005 ]
2006 for q, a in data:
2007 dh = decode_header(s % q)
2008 self.assertEqual(dh, [(a, 'iso-8859-1')])
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002009
R. David Murray31e984c2010-10-01 15:40:20 +00002010 def test_rfc2047_Q_invalid_digits(self):
2011 # issue 10004.
2012 s = '=?iso-8659-1?Q?andr=e9=zz?='
2013 self.assertEqual(decode_header(s),
2014 [(b'andr\xe9=zz', 'iso-8659-1')])
2015
Ezio Melottib3aedd42010-11-20 19:04:17 +00002016
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002017# Test the MIMEMessage class
2018class TestMIMEMessage(TestEmailBase):
2019 def setUp(self):
2020 with openfile('msg_11.txt') as fp:
2021 self._text = fp.read()
2022
2023 def test_type_error(self):
2024 self.assertRaises(TypeError, MIMEMessage, 'a plain string')
2025
2026 def test_valid_argument(self):
2027 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002028 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002029 subject = 'A sub-message'
2030 m = Message()
2031 m['Subject'] = subject
2032 r = MIMEMessage(m)
2033 eq(r.get_content_type(), 'message/rfc822')
2034 payload = r.get_payload()
2035 unless(isinstance(payload, list))
2036 eq(len(payload), 1)
2037 subpart = payload[0]
2038 unless(subpart is m)
2039 eq(subpart['subject'], subject)
2040
2041 def test_bad_multipart(self):
2042 eq = self.assertEqual
2043 msg1 = Message()
2044 msg1['Subject'] = 'subpart 1'
2045 msg2 = Message()
2046 msg2['Subject'] = 'subpart 2'
2047 r = MIMEMessage(msg1)
2048 self.assertRaises(errors.MultipartConversionError, r.attach, msg2)
2049
2050 def test_generate(self):
2051 # First craft the message to be encapsulated
2052 m = Message()
2053 m['Subject'] = 'An enclosed message'
2054 m.set_payload('Here is the body of the message.\n')
2055 r = MIMEMessage(m)
2056 r['Subject'] = 'The enclosing message'
2057 s = StringIO()
2058 g = Generator(s)
2059 g.flatten(r)
2060 self.assertEqual(s.getvalue(), """\
2061Content-Type: message/rfc822
2062MIME-Version: 1.0
2063Subject: The enclosing message
2064
2065Subject: An enclosed message
2066
2067Here is the body of the message.
2068""")
2069
2070 def test_parse_message_rfc822(self):
2071 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002072 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002073 msg = self._msgobj('msg_11.txt')
2074 eq(msg.get_content_type(), 'message/rfc822')
2075 payload = msg.get_payload()
2076 unless(isinstance(payload, list))
2077 eq(len(payload), 1)
2078 submsg = payload[0]
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002079 self.assertTrue(isinstance(submsg, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002080 eq(submsg['subject'], 'An enclosed message')
2081 eq(submsg.get_payload(), 'Here is the body of the message.\n')
2082
2083 def test_dsn(self):
2084 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002085 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002086 # msg 16 is a Delivery Status Notification, see RFC 1894
2087 msg = self._msgobj('msg_16.txt')
2088 eq(msg.get_content_type(), 'multipart/report')
2089 unless(msg.is_multipart())
2090 eq(len(msg.get_payload()), 3)
2091 # Subpart 1 is a text/plain, human readable section
2092 subpart = msg.get_payload(0)
2093 eq(subpart.get_content_type(), 'text/plain')
2094 eq(subpart.get_payload(), """\
2095This report relates to a message you sent with the following header fields:
2096
2097 Message-id: <002001c144a6$8752e060$56104586@oxy.edu>
2098 Date: Sun, 23 Sep 2001 20:10:55 -0700
2099 From: "Ian T. Henry" <henryi@oxy.edu>
2100 To: SoCal Raves <scr@socal-raves.org>
2101 Subject: [scr] yeah for Ians!!
2102
2103Your message cannot be delivered to the following recipients:
2104
2105 Recipient address: jangel1@cougar.noc.ucla.edu
2106 Reason: recipient reached disk quota
2107
2108""")
2109 # Subpart 2 contains the machine parsable DSN information. It
2110 # consists of two blocks of headers, represented by two nested Message
2111 # objects.
2112 subpart = msg.get_payload(1)
2113 eq(subpart.get_content_type(), 'message/delivery-status')
2114 eq(len(subpart.get_payload()), 2)
2115 # message/delivery-status should treat each block as a bunch of
2116 # headers, i.e. a bunch of Message objects.
2117 dsn1 = subpart.get_payload(0)
2118 unless(isinstance(dsn1, Message))
2119 eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu')
2120 eq(dsn1.get_param('dns', header='reporting-mta'), '')
2121 # Try a missing one <wink>
2122 eq(dsn1.get_param('nsd', header='reporting-mta'), None)
2123 dsn2 = subpart.get_payload(1)
2124 unless(isinstance(dsn2, Message))
2125 eq(dsn2['action'], 'failed')
2126 eq(dsn2.get_params(header='original-recipient'),
2127 [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')])
2128 eq(dsn2.get_param('rfc822', header='final-recipient'), '')
2129 # Subpart 3 is the original message
2130 subpart = msg.get_payload(2)
2131 eq(subpart.get_content_type(), 'message/rfc822')
2132 payload = subpart.get_payload()
2133 unless(isinstance(payload, list))
2134 eq(len(payload), 1)
2135 subsubpart = payload[0]
2136 unless(isinstance(subsubpart, Message))
2137 eq(subsubpart.get_content_type(), 'text/plain')
2138 eq(subsubpart['message-id'],
2139 '<002001c144a6$8752e060$56104586@oxy.edu>')
2140
2141 def test_epilogue(self):
2142 eq = self.ndiffAssertEqual
2143 with openfile('msg_21.txt') as fp:
2144 text = fp.read()
2145 msg = Message()
2146 msg['From'] = 'aperson@dom.ain'
2147 msg['To'] = 'bperson@dom.ain'
2148 msg['Subject'] = 'Test'
2149 msg.preamble = 'MIME message'
2150 msg.epilogue = 'End of MIME message\n'
2151 msg1 = MIMEText('One')
2152 msg2 = MIMEText('Two')
2153 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
2154 msg.attach(msg1)
2155 msg.attach(msg2)
2156 sfp = StringIO()
2157 g = Generator(sfp)
2158 g.flatten(msg)
2159 eq(sfp.getvalue(), text)
2160
2161 def test_no_nl_preamble(self):
2162 eq = self.ndiffAssertEqual
2163 msg = Message()
2164 msg['From'] = 'aperson@dom.ain'
2165 msg['To'] = 'bperson@dom.ain'
2166 msg['Subject'] = 'Test'
2167 msg.preamble = 'MIME message'
2168 msg.epilogue = ''
2169 msg1 = MIMEText('One')
2170 msg2 = MIMEText('Two')
2171 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
2172 msg.attach(msg1)
2173 msg.attach(msg2)
2174 eq(msg.as_string(), """\
2175From: aperson@dom.ain
2176To: bperson@dom.ain
2177Subject: Test
2178Content-Type: multipart/mixed; boundary="BOUNDARY"
2179
2180MIME message
2181--BOUNDARY
2182Content-Type: text/plain; charset="us-ascii"
2183MIME-Version: 1.0
2184Content-Transfer-Encoding: 7bit
2185
2186One
2187--BOUNDARY
2188Content-Type: text/plain; charset="us-ascii"
2189MIME-Version: 1.0
2190Content-Transfer-Encoding: 7bit
2191
2192Two
2193--BOUNDARY--
2194""")
2195
2196 def test_default_type(self):
2197 eq = self.assertEqual
2198 with openfile('msg_30.txt') as fp:
2199 msg = email.message_from_file(fp)
2200 container1 = msg.get_payload(0)
2201 eq(container1.get_default_type(), 'message/rfc822')
2202 eq(container1.get_content_type(), 'message/rfc822')
2203 container2 = msg.get_payload(1)
2204 eq(container2.get_default_type(), 'message/rfc822')
2205 eq(container2.get_content_type(), 'message/rfc822')
2206 container1a = container1.get_payload(0)
2207 eq(container1a.get_default_type(), 'text/plain')
2208 eq(container1a.get_content_type(), 'text/plain')
2209 container2a = container2.get_payload(0)
2210 eq(container2a.get_default_type(), 'text/plain')
2211 eq(container2a.get_content_type(), 'text/plain')
2212
2213 def test_default_type_with_explicit_container_type(self):
2214 eq = self.assertEqual
2215 with openfile('msg_28.txt') as fp:
2216 msg = email.message_from_file(fp)
2217 container1 = msg.get_payload(0)
2218 eq(container1.get_default_type(), 'message/rfc822')
2219 eq(container1.get_content_type(), 'message/rfc822')
2220 container2 = msg.get_payload(1)
2221 eq(container2.get_default_type(), 'message/rfc822')
2222 eq(container2.get_content_type(), 'message/rfc822')
2223 container1a = container1.get_payload(0)
2224 eq(container1a.get_default_type(), 'text/plain')
2225 eq(container1a.get_content_type(), 'text/plain')
2226 container2a = container2.get_payload(0)
2227 eq(container2a.get_default_type(), 'text/plain')
2228 eq(container2a.get_content_type(), 'text/plain')
2229
2230 def test_default_type_non_parsed(self):
2231 eq = self.assertEqual
2232 neq = self.ndiffAssertEqual
2233 # Set up container
2234 container = MIMEMultipart('digest', 'BOUNDARY')
2235 container.epilogue = ''
2236 # Set up subparts
2237 subpart1a = MIMEText('message 1\n')
2238 subpart2a = MIMEText('message 2\n')
2239 subpart1 = MIMEMessage(subpart1a)
2240 subpart2 = MIMEMessage(subpart2a)
2241 container.attach(subpart1)
2242 container.attach(subpart2)
2243 eq(subpart1.get_content_type(), 'message/rfc822')
2244 eq(subpart1.get_default_type(), 'message/rfc822')
2245 eq(subpart2.get_content_type(), 'message/rfc822')
2246 eq(subpart2.get_default_type(), 'message/rfc822')
2247 neq(container.as_string(0), '''\
2248Content-Type: multipart/digest; boundary="BOUNDARY"
2249MIME-Version: 1.0
2250
2251--BOUNDARY
2252Content-Type: message/rfc822
2253MIME-Version: 1.0
2254
2255Content-Type: text/plain; charset="us-ascii"
2256MIME-Version: 1.0
2257Content-Transfer-Encoding: 7bit
2258
2259message 1
2260
2261--BOUNDARY
2262Content-Type: message/rfc822
2263MIME-Version: 1.0
2264
2265Content-Type: text/plain; charset="us-ascii"
2266MIME-Version: 1.0
2267Content-Transfer-Encoding: 7bit
2268
2269message 2
2270
2271--BOUNDARY--
2272''')
2273 del subpart1['content-type']
2274 del subpart1['mime-version']
2275 del subpart2['content-type']
2276 del subpart2['mime-version']
2277 eq(subpart1.get_content_type(), 'message/rfc822')
2278 eq(subpart1.get_default_type(), 'message/rfc822')
2279 eq(subpart2.get_content_type(), 'message/rfc822')
2280 eq(subpart2.get_default_type(), 'message/rfc822')
2281 neq(container.as_string(0), '''\
2282Content-Type: multipart/digest; boundary="BOUNDARY"
2283MIME-Version: 1.0
2284
2285--BOUNDARY
2286
2287Content-Type: text/plain; charset="us-ascii"
2288MIME-Version: 1.0
2289Content-Transfer-Encoding: 7bit
2290
2291message 1
2292
2293--BOUNDARY
2294
2295Content-Type: text/plain; charset="us-ascii"
2296MIME-Version: 1.0
2297Content-Transfer-Encoding: 7bit
2298
2299message 2
2300
2301--BOUNDARY--
2302''')
2303
2304 def test_mime_attachments_in_constructor(self):
2305 eq = self.assertEqual
2306 text1 = MIMEText('')
2307 text2 = MIMEText('')
2308 msg = MIMEMultipart(_subparts=(text1, text2))
2309 eq(len(msg.get_payload()), 2)
2310 eq(msg.get_payload(0), text1)
2311 eq(msg.get_payload(1), text2)
2312
Christian Heimes587c2bf2008-01-19 16:21:02 +00002313 def test_default_multipart_constructor(self):
2314 msg = MIMEMultipart()
2315 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002316
Ezio Melottib3aedd42010-11-20 19:04:17 +00002317
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002318# A general test of parser->model->generator idempotency. IOW, read a message
2319# in, parse it into a message object tree, then without touching the tree,
2320# regenerate the plain text. The original text and the transformed text
2321# should be identical. Note: that we ignore the Unix-From since that may
2322# contain a changed date.
2323class TestIdempotent(TestEmailBase):
R. David Murray719a4492010-11-21 16:53:48 +00002324
2325 linesep = '\n'
2326
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002327 def _msgobj(self, filename):
2328 with openfile(filename) as fp:
2329 data = fp.read()
2330 msg = email.message_from_string(data)
2331 return msg, data
2332
R. David Murray719a4492010-11-21 16:53:48 +00002333 def _idempotent(self, msg, text, unixfrom=False):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002334 eq = self.ndiffAssertEqual
2335 s = StringIO()
2336 g = Generator(s, maxheaderlen=0)
R. David Murray719a4492010-11-21 16:53:48 +00002337 g.flatten(msg, unixfrom=unixfrom)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002338 eq(text, s.getvalue())
2339
2340 def test_parse_text_message(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002341 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002342 msg, text = self._msgobj('msg_01.txt')
2343 eq(msg.get_content_type(), 'text/plain')
2344 eq(msg.get_content_maintype(), 'text')
2345 eq(msg.get_content_subtype(), 'plain')
2346 eq(msg.get_params()[1], ('charset', 'us-ascii'))
2347 eq(msg.get_param('charset'), 'us-ascii')
2348 eq(msg.preamble, None)
2349 eq(msg.epilogue, None)
2350 self._idempotent(msg, text)
2351
2352 def test_parse_untyped_message(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002353 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002354 msg, text = self._msgobj('msg_03.txt')
2355 eq(msg.get_content_type(), 'text/plain')
2356 eq(msg.get_params(), None)
2357 eq(msg.get_param('charset'), None)
2358 self._idempotent(msg, text)
2359
2360 def test_simple_multipart(self):
2361 msg, text = self._msgobj('msg_04.txt')
2362 self._idempotent(msg, text)
2363
2364 def test_MIME_digest(self):
2365 msg, text = self._msgobj('msg_02.txt')
2366 self._idempotent(msg, text)
2367
2368 def test_long_header(self):
2369 msg, text = self._msgobj('msg_27.txt')
2370 self._idempotent(msg, text)
2371
2372 def test_MIME_digest_with_part_headers(self):
2373 msg, text = self._msgobj('msg_28.txt')
2374 self._idempotent(msg, text)
2375
2376 def test_mixed_with_image(self):
2377 msg, text = self._msgobj('msg_06.txt')
2378 self._idempotent(msg, text)
2379
2380 def test_multipart_report(self):
2381 msg, text = self._msgobj('msg_05.txt')
2382 self._idempotent(msg, text)
2383
2384 def test_dsn(self):
2385 msg, text = self._msgobj('msg_16.txt')
2386 self._idempotent(msg, text)
2387
2388 def test_preamble_epilogue(self):
2389 msg, text = self._msgobj('msg_21.txt')
2390 self._idempotent(msg, text)
2391
2392 def test_multipart_one_part(self):
2393 msg, text = self._msgobj('msg_23.txt')
2394 self._idempotent(msg, text)
2395
2396 def test_multipart_no_parts(self):
2397 msg, text = self._msgobj('msg_24.txt')
2398 self._idempotent(msg, text)
2399
2400 def test_no_start_boundary(self):
2401 msg, text = self._msgobj('msg_31.txt')
2402 self._idempotent(msg, text)
2403
2404 def test_rfc2231_charset(self):
2405 msg, text = self._msgobj('msg_32.txt')
2406 self._idempotent(msg, text)
2407
2408 def test_more_rfc2231_parameters(self):
2409 msg, text = self._msgobj('msg_33.txt')
2410 self._idempotent(msg, text)
2411
2412 def test_text_plain_in_a_multipart_digest(self):
2413 msg, text = self._msgobj('msg_34.txt')
2414 self._idempotent(msg, text)
2415
2416 def test_nested_multipart_mixeds(self):
2417 msg, text = self._msgobj('msg_12a.txt')
2418 self._idempotent(msg, text)
2419
2420 def test_message_external_body_idempotent(self):
2421 msg, text = self._msgobj('msg_36.txt')
2422 self._idempotent(msg, text)
2423
R. David Murray719a4492010-11-21 16:53:48 +00002424 def test_message_delivery_status(self):
2425 msg, text = self._msgobj('msg_43.txt')
2426 self._idempotent(msg, text, unixfrom=True)
2427
R. David Murray96fd54e2010-10-08 15:55:28 +00002428 def test_message_signed_idempotent(self):
2429 msg, text = self._msgobj('msg_45.txt')
2430 self._idempotent(msg, text)
2431
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002432 def test_content_type(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002433 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002434 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002435 # Get a message object and reset the seek pointer for other tests
2436 msg, text = self._msgobj('msg_05.txt')
2437 eq(msg.get_content_type(), 'multipart/report')
2438 # Test the Content-Type: parameters
2439 params = {}
2440 for pk, pv in msg.get_params():
2441 params[pk] = pv
2442 eq(params['report-type'], 'delivery-status')
2443 eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
R. David Murray719a4492010-11-21 16:53:48 +00002444 eq(msg.preamble, 'This is a MIME-encapsulated message.' + self.linesep)
2445 eq(msg.epilogue, self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002446 eq(len(msg.get_payload()), 3)
2447 # Make sure the subparts are what we expect
2448 msg1 = msg.get_payload(0)
2449 eq(msg1.get_content_type(), 'text/plain')
R. David Murray719a4492010-11-21 16:53:48 +00002450 eq(msg1.get_payload(), 'Yadda yadda yadda' + self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002451 msg2 = msg.get_payload(1)
2452 eq(msg2.get_content_type(), 'text/plain')
R. David Murray719a4492010-11-21 16:53:48 +00002453 eq(msg2.get_payload(), 'Yadda yadda yadda' + self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002454 msg3 = msg.get_payload(2)
2455 eq(msg3.get_content_type(), 'message/rfc822')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002456 self.assertTrue(isinstance(msg3, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002457 payload = msg3.get_payload()
2458 unless(isinstance(payload, list))
2459 eq(len(payload), 1)
2460 msg4 = payload[0]
2461 unless(isinstance(msg4, Message))
R. David Murray719a4492010-11-21 16:53:48 +00002462 eq(msg4.get_payload(), 'Yadda yadda yadda' + self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002463
2464 def test_parser(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002465 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002466 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002467 msg, text = self._msgobj('msg_06.txt')
2468 # Check some of the outer headers
2469 eq(msg.get_content_type(), 'message/rfc822')
2470 # Make sure the payload is a list of exactly one sub-Message, and that
2471 # that submessage has a type of text/plain
2472 payload = msg.get_payload()
2473 unless(isinstance(payload, list))
2474 eq(len(payload), 1)
2475 msg1 = payload[0]
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002476 self.assertTrue(isinstance(msg1, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002477 eq(msg1.get_content_type(), 'text/plain')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002478 self.assertTrue(isinstance(msg1.get_payload(), str))
R. David Murray719a4492010-11-21 16:53:48 +00002479 eq(msg1.get_payload(), self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002480
2481
Ezio Melottib3aedd42010-11-20 19:04:17 +00002482
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002483# Test various other bits of the package's functionality
2484class TestMiscellaneous(TestEmailBase):
2485 def test_message_from_string(self):
2486 with openfile('msg_01.txt') as fp:
2487 text = fp.read()
2488 msg = email.message_from_string(text)
2489 s = StringIO()
2490 # Don't wrap/continue long headers since we're trying to test
2491 # idempotency.
2492 g = Generator(s, maxheaderlen=0)
2493 g.flatten(msg)
2494 self.assertEqual(text, s.getvalue())
2495
2496 def test_message_from_file(self):
2497 with openfile('msg_01.txt') as fp:
2498 text = fp.read()
2499 fp.seek(0)
2500 msg = email.message_from_file(fp)
2501 s = StringIO()
2502 # Don't wrap/continue long headers since we're trying to test
2503 # idempotency.
2504 g = Generator(s, maxheaderlen=0)
2505 g.flatten(msg)
2506 self.assertEqual(text, s.getvalue())
2507
2508 def test_message_from_string_with_class(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002509 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002510 with openfile('msg_01.txt') as fp:
2511 text = fp.read()
2512
2513 # Create a subclass
2514 class MyMessage(Message):
2515 pass
2516
2517 msg = email.message_from_string(text, MyMessage)
2518 unless(isinstance(msg, MyMessage))
2519 # Try something more complicated
2520 with openfile('msg_02.txt') as fp:
2521 text = fp.read()
2522 msg = email.message_from_string(text, MyMessage)
2523 for subpart in msg.walk():
2524 unless(isinstance(subpart, MyMessage))
2525
2526 def test_message_from_file_with_class(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002527 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002528 # Create a subclass
2529 class MyMessage(Message):
2530 pass
2531
2532 with openfile('msg_01.txt') as fp:
2533 msg = email.message_from_file(fp, MyMessage)
2534 unless(isinstance(msg, MyMessage))
2535 # Try something more complicated
2536 with openfile('msg_02.txt') as fp:
2537 msg = email.message_from_file(fp, MyMessage)
2538 for subpart in msg.walk():
2539 unless(isinstance(subpart, MyMessage))
2540
2541 def test__all__(self):
2542 module = __import__('email')
R David Murray1b6c7242012-03-16 22:43:05 -04002543 self.assertEqual(sorted(module.__all__), [
2544 'base64mime', 'charset', 'encoders', 'errors', 'feedparser',
2545 'generator', 'header', 'iterators', 'message',
2546 'message_from_binary_file', 'message_from_bytes',
2547 'message_from_file', 'message_from_string', 'mime', 'parser',
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002548 'quoprimime', 'utils',
2549 ])
2550
2551 def test_formatdate(self):
2552 now = time.time()
2553 self.assertEqual(utils.parsedate(utils.formatdate(now))[:6],
2554 time.gmtime(now)[:6])
2555
2556 def test_formatdate_localtime(self):
2557 now = time.time()
2558 self.assertEqual(
2559 utils.parsedate(utils.formatdate(now, localtime=True))[:6],
2560 time.localtime(now)[:6])
2561
2562 def test_formatdate_usegmt(self):
2563 now = time.time()
2564 self.assertEqual(
2565 utils.formatdate(now, localtime=False),
2566 time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now)))
2567 self.assertEqual(
2568 utils.formatdate(now, localtime=False, usegmt=True),
2569 time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now)))
2570
2571 def test_parsedate_none(self):
2572 self.assertEqual(utils.parsedate(''), None)
2573
2574 def test_parsedate_compact(self):
2575 # The FWS after the comma is optional
2576 self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'),
2577 utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800'))
2578
2579 def test_parsedate_no_dayofweek(self):
2580 eq = self.assertEqual
2581 eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'),
2582 (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800))
2583
2584 def test_parsedate_compact_no_dayofweek(self):
2585 eq = self.assertEqual
2586 eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'),
2587 (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800))
2588
R. David Murray4a62e892010-12-23 20:35:46 +00002589 def test_parsedate_no_space_before_positive_offset(self):
2590 self.assertEqual(utils.parsedate_tz('Wed, 3 Apr 2002 14:58:26+0800'),
2591 (2002, 4, 3, 14, 58, 26, 0, 1, -1, 28800))
2592
2593 def test_parsedate_no_space_before_negative_offset(self):
2594 # Issue 1155362: we already handled '+' for this case.
2595 self.assertEqual(utils.parsedate_tz('Wed, 3 Apr 2002 14:58:26-0800'),
2596 (2002, 4, 3, 14, 58, 26, 0, 1, -1, -28800))
2597
2598
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002599 def test_parsedate_acceptable_to_time_functions(self):
2600 eq = self.assertEqual
2601 timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800')
2602 t = int(time.mktime(timetup))
2603 eq(time.localtime(t)[:6], timetup[:6])
2604 eq(int(time.strftime('%Y', timetup)), 2003)
2605 timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800')
2606 t = int(time.mktime(timetup[:9]))
2607 eq(time.localtime(t)[:6], timetup[:6])
2608 eq(int(time.strftime('%Y', timetup[:9])), 2003)
2609
Alexander Belopolskya07548e2012-06-21 20:34:09 -04002610 def test_mktime_tz(self):
2611 self.assertEqual(utils.mktime_tz((1970, 1, 1, 0, 0, 0,
2612 -1, -1, -1, 0)), 0)
2613 self.assertEqual(utils.mktime_tz((1970, 1, 1, 0, 0, 0,
2614 -1, -1, -1, 1234)), -1234)
2615
R. David Murray219d1c82010-08-25 00:45:55 +00002616 def test_parsedate_y2k(self):
2617 """Test for parsing a date with a two-digit year.
2618
2619 Parsing a date with a two-digit year should return the correct
2620 four-digit year. RFC822 allows two-digit years, but RFC2822 (which
2621 obsoletes RFC822) requires four-digit years.
2622
2623 """
2624 self.assertEqual(utils.parsedate_tz('25 Feb 03 13:47:26 -0800'),
2625 utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'))
2626 self.assertEqual(utils.parsedate_tz('25 Feb 71 13:47:26 -0800'),
2627 utils.parsedate_tz('25 Feb 1971 13:47:26 -0800'))
2628
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002629 def test_parseaddr_empty(self):
2630 self.assertEqual(utils.parseaddr('<>'), ('', ''))
2631 self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '')
2632
2633 def test_noquote_dump(self):
2634 self.assertEqual(
2635 utils.formataddr(('A Silly Person', 'person@dom.ain')),
2636 'A Silly Person <person@dom.ain>')
2637
2638 def test_escape_dump(self):
2639 self.assertEqual(
2640 utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')),
2641 r'"A \(Very\) Silly Person" <person@dom.ain>')
2642 a = r'A \(Special\) Person'
2643 b = 'person@dom.ain'
2644 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2645
2646 def test_escape_backslashes(self):
2647 self.assertEqual(
2648 utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')),
2649 r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>')
2650 a = r'Arthur \Backslash\ Foobar'
2651 b = 'person@dom.ain'
2652 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2653
2654 def test_name_with_dot(self):
2655 x = 'John X. Doe <jxd@example.com>'
2656 y = '"John X. Doe" <jxd@example.com>'
2657 a, b = ('John X. Doe', 'jxd@example.com')
2658 self.assertEqual(utils.parseaddr(x), (a, b))
2659 self.assertEqual(utils.parseaddr(y), (a, b))
2660 # formataddr() quotes the name if there's a dot in it
2661 self.assertEqual(utils.formataddr((a, b)), y)
2662
R. David Murray5397e862010-10-02 15:58:26 +00002663 def test_parseaddr_preserves_quoted_pairs_in_addresses(self):
2664 # issue 10005. Note that in the third test the second pair of
2665 # backslashes is not actually a quoted pair because it is not inside a
2666 # comment or quoted string: the address being parsed has a quoted
2667 # string containing a quoted backslash, followed by 'example' and two
2668 # backslashes, followed by another quoted string containing a space and
2669 # the word 'example'. parseaddr copies those two backslashes
2670 # literally. Per rfc5322 this is not technically correct since a \ may
2671 # not appear in an address outside of a quoted string. It is probably
2672 # a sensible Postel interpretation, though.
2673 eq = self.assertEqual
2674 eq(utils.parseaddr('""example" example"@example.com'),
2675 ('', '""example" example"@example.com'))
2676 eq(utils.parseaddr('"\\"example\\" example"@example.com'),
2677 ('', '"\\"example\\" example"@example.com'))
2678 eq(utils.parseaddr('"\\\\"example\\\\" example"@example.com'),
2679 ('', '"\\\\"example\\\\" example"@example.com'))
2680
R. David Murray63563cd2010-12-18 18:25:38 +00002681 def test_parseaddr_preserves_spaces_in_local_part(self):
2682 # issue 9286. A normal RFC5322 local part should not contain any
2683 # folding white space, but legacy local parts can (they are a sequence
2684 # of atoms, not dotatoms). On the other hand we strip whitespace from
2685 # before the @ and around dots, on the assumption that the whitespace
2686 # around the punctuation is a mistake in what would otherwise be
2687 # an RFC5322 local part. Leading whitespace is, usual, stripped as well.
2688 self.assertEqual(('', "merwok wok@xample.com"),
2689 utils.parseaddr("merwok wok@xample.com"))
2690 self.assertEqual(('', "merwok wok@xample.com"),
2691 utils.parseaddr("merwok wok@xample.com"))
2692 self.assertEqual(('', "merwok wok@xample.com"),
2693 utils.parseaddr(" merwok wok @xample.com"))
2694 self.assertEqual(('', 'merwok"wok" wok@xample.com'),
2695 utils.parseaddr('merwok"wok" wok@xample.com'))
2696 self.assertEqual(('', 'merwok.wok.wok@xample.com'),
2697 utils.parseaddr('merwok. wok . wok@xample.com'))
2698
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002699 def test_multiline_from_comment(self):
2700 x = """\
2701Foo
2702\tBar <foo@example.com>"""
2703 self.assertEqual(utils.parseaddr(x), ('Foo Bar', 'foo@example.com'))
2704
2705 def test_quote_dump(self):
2706 self.assertEqual(
2707 utils.formataddr(('A Silly; Person', 'person@dom.ain')),
2708 r'"A Silly; Person" <person@dom.ain>')
2709
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002710 def test_charset_richcomparisons(self):
2711 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002712 ne = self.assertNotEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002713 cset1 = Charset()
2714 cset2 = Charset()
2715 eq(cset1, 'us-ascii')
2716 eq(cset1, 'US-ASCII')
2717 eq(cset1, 'Us-AsCiI')
2718 eq('us-ascii', cset1)
2719 eq('US-ASCII', cset1)
2720 eq('Us-AsCiI', cset1)
2721 ne(cset1, 'usascii')
2722 ne(cset1, 'USASCII')
2723 ne(cset1, 'UsAsCiI')
2724 ne('usascii', cset1)
2725 ne('USASCII', cset1)
2726 ne('UsAsCiI', cset1)
2727 eq(cset1, cset2)
2728 eq(cset2, cset1)
2729
2730 def test_getaddresses(self):
2731 eq = self.assertEqual
2732 eq(utils.getaddresses(['aperson@dom.ain (Al Person)',
2733 'Bud Person <bperson@dom.ain>']),
2734 [('Al Person', 'aperson@dom.ain'),
2735 ('Bud Person', 'bperson@dom.ain')])
2736
2737 def test_getaddresses_nasty(self):
2738 eq = self.assertEqual
2739 eq(utils.getaddresses(['foo: ;']), [('', '')])
2740 eq(utils.getaddresses(
2741 ['[]*-- =~$']),
2742 [('', ''), ('', ''), ('', '*--')])
2743 eq(utils.getaddresses(
2744 ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
2745 [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
2746
2747 def test_getaddresses_embedded_comment(self):
2748 """Test proper handling of a nested comment"""
2749 eq = self.assertEqual
2750 addrs = utils.getaddresses(['User ((nested comment)) <foo@bar.com>'])
2751 eq(addrs[0][1], 'foo@bar.com')
2752
2753 def test_utils_quote_unquote(self):
2754 eq = self.assertEqual
2755 msg = Message()
2756 msg.add_header('content-disposition', 'attachment',
2757 filename='foo\\wacky"name')
2758 eq(msg.get_filename(), 'foo\\wacky"name')
2759
2760 def test_get_body_encoding_with_bogus_charset(self):
2761 charset = Charset('not a charset')
2762 self.assertEqual(charset.get_body_encoding(), 'base64')
2763
2764 def test_get_body_encoding_with_uppercase_charset(self):
2765 eq = self.assertEqual
2766 msg = Message()
2767 msg['Content-Type'] = 'text/plain; charset=UTF-8'
2768 eq(msg['content-type'], 'text/plain; charset=UTF-8')
2769 charsets = msg.get_charsets()
2770 eq(len(charsets), 1)
2771 eq(charsets[0], 'utf-8')
2772 charset = Charset(charsets[0])
2773 eq(charset.get_body_encoding(), 'base64')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002774 msg.set_payload(b'hello world', charset=charset)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002775 eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n')
2776 eq(msg.get_payload(decode=True), b'hello world')
2777 eq(msg['content-transfer-encoding'], 'base64')
2778 # Try another one
2779 msg = Message()
2780 msg['Content-Type'] = 'text/plain; charset="US-ASCII"'
2781 charsets = msg.get_charsets()
2782 eq(len(charsets), 1)
2783 eq(charsets[0], 'us-ascii')
2784 charset = Charset(charsets[0])
2785 eq(charset.get_body_encoding(), encoders.encode_7or8bit)
2786 msg.set_payload('hello world', charset=charset)
2787 eq(msg.get_payload(), 'hello world')
2788 eq(msg['content-transfer-encoding'], '7bit')
2789
2790 def test_charsets_case_insensitive(self):
2791 lc = Charset('us-ascii')
2792 uc = Charset('US-ASCII')
2793 self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding())
2794
2795 def test_partial_falls_inside_message_delivery_status(self):
2796 eq = self.ndiffAssertEqual
2797 # The Parser interface provides chunks of data to FeedParser in 8192
2798 # byte gulps. SF bug #1076485 found one of those chunks inside
2799 # message/delivery-status header block, which triggered an
2800 # unreadline() of NeedMoreData.
2801 msg = self._msgobj('msg_43.txt')
2802 sfp = StringIO()
2803 iterators._structure(msg, sfp)
2804 eq(sfp.getvalue(), """\
2805multipart/report
2806 text/plain
2807 message/delivery-status
2808 text/plain
2809 text/plain
2810 text/plain
2811 text/plain
2812 text/plain
2813 text/plain
2814 text/plain
2815 text/plain
2816 text/plain
2817 text/plain
2818 text/plain
2819 text/plain
2820 text/plain
2821 text/plain
2822 text/plain
2823 text/plain
2824 text/plain
2825 text/plain
2826 text/plain
2827 text/plain
2828 text/plain
2829 text/plain
2830 text/plain
2831 text/plain
2832 text/plain
2833 text/plain
2834 text/rfc822-headers
2835""")
2836
R. David Murraya0b44b52010-12-02 21:47:19 +00002837 def test_make_msgid_domain(self):
2838 self.assertEqual(
2839 email.utils.make_msgid(domain='testdomain-string')[-19:],
2840 '@testdomain-string>')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002841
Ezio Melottib3aedd42010-11-20 19:04:17 +00002842
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002843# Test the iterator/generators
2844class TestIterators(TestEmailBase):
2845 def test_body_line_iterator(self):
2846 eq = self.assertEqual
2847 neq = self.ndiffAssertEqual
2848 # First a simple non-multipart message
2849 msg = self._msgobj('msg_01.txt')
2850 it = iterators.body_line_iterator(msg)
2851 lines = list(it)
2852 eq(len(lines), 6)
2853 neq(EMPTYSTRING.join(lines), msg.get_payload())
2854 # Now a more complicated multipart
2855 msg = self._msgobj('msg_02.txt')
2856 it = iterators.body_line_iterator(msg)
2857 lines = list(it)
2858 eq(len(lines), 43)
2859 with openfile('msg_19.txt') as fp:
2860 neq(EMPTYSTRING.join(lines), fp.read())
2861
2862 def test_typed_subpart_iterator(self):
2863 eq = self.assertEqual
2864 msg = self._msgobj('msg_04.txt')
2865 it = iterators.typed_subpart_iterator(msg, 'text')
2866 lines = []
2867 subparts = 0
2868 for subpart in it:
2869 subparts += 1
2870 lines.append(subpart.get_payload())
2871 eq(subparts, 2)
2872 eq(EMPTYSTRING.join(lines), """\
2873a simple kind of mirror
2874to reflect upon our own
2875a simple kind of mirror
2876to reflect upon our own
2877""")
2878
2879 def test_typed_subpart_iterator_default_type(self):
2880 eq = self.assertEqual
2881 msg = self._msgobj('msg_03.txt')
2882 it = iterators.typed_subpart_iterator(msg, 'text', 'plain')
2883 lines = []
2884 subparts = 0
2885 for subpart in it:
2886 subparts += 1
2887 lines.append(subpart.get_payload())
2888 eq(subparts, 1)
2889 eq(EMPTYSTRING.join(lines), """\
2890
2891Hi,
2892
2893Do you like this message?
2894
2895-Me
2896""")
2897
R. David Murray45bf773f2010-07-17 01:19:57 +00002898 def test_pushCR_LF(self):
2899 '''FeedParser BufferedSubFile.push() assumed it received complete
2900 line endings. A CR ending one push() followed by a LF starting
2901 the next push() added an empty line.
2902 '''
2903 imt = [
2904 ("a\r \n", 2),
2905 ("b", 0),
2906 ("c\n", 1),
2907 ("", 0),
2908 ("d\r\n", 1),
2909 ("e\r", 0),
2910 ("\nf", 1),
2911 ("\r\n", 1),
2912 ]
2913 from email.feedparser import BufferedSubFile, NeedMoreData
2914 bsf = BufferedSubFile()
2915 om = []
2916 nt = 0
2917 for il, n in imt:
2918 bsf.push(il)
2919 nt += n
2920 n1 = 0
2921 while True:
2922 ol = bsf.readline()
2923 if ol == NeedMoreData:
2924 break
2925 om.append(ol)
2926 n1 += 1
2927 self.assertTrue(n == n1)
2928 self.assertTrue(len(om) == nt)
2929 self.assertTrue(''.join([il for il, n in imt]) == ''.join(om))
2930
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002931
Ezio Melottib3aedd42010-11-20 19:04:17 +00002932
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002933class TestParsers(TestEmailBase):
2934 def test_header_parser(self):
2935 eq = self.assertEqual
2936 # Parse only the headers of a complex multipart MIME document
2937 with openfile('msg_02.txt') as fp:
2938 msg = HeaderParser().parse(fp)
2939 eq(msg['from'], 'ppp-request@zzz.org')
2940 eq(msg['to'], 'ppp@zzz.org')
2941 eq(msg.get_content_type(), 'multipart/mixed')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002942 self.assertFalse(msg.is_multipart())
2943 self.assertTrue(isinstance(msg.get_payload(), str))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002944
2945 def test_whitespace_continuation(self):
2946 eq = self.assertEqual
2947 # This message contains a line after the Subject: header that has only
2948 # whitespace, but it is not empty!
2949 msg = email.message_from_string("""\
2950From: aperson@dom.ain
2951To: bperson@dom.ain
2952Subject: the next line has a space on it
2953\x20
2954Date: Mon, 8 Apr 2002 15:09:19 -0400
2955Message-ID: spam
2956
2957Here's the message body
2958""")
2959 eq(msg['subject'], 'the next line has a space on it\n ')
2960 eq(msg['message-id'], 'spam')
2961 eq(msg.get_payload(), "Here's the message body\n")
2962
2963 def test_whitespace_continuation_last_header(self):
2964 eq = self.assertEqual
2965 # Like the previous test, but the subject line is the last
2966 # header.
2967 msg = email.message_from_string("""\
2968From: aperson@dom.ain
2969To: bperson@dom.ain
2970Date: Mon, 8 Apr 2002 15:09:19 -0400
2971Message-ID: spam
2972Subject: the next line has a space on it
2973\x20
2974
2975Here's the message body
2976""")
2977 eq(msg['subject'], 'the next line has a space on it\n ')
2978 eq(msg['message-id'], 'spam')
2979 eq(msg.get_payload(), "Here's the message body\n")
2980
2981 def test_crlf_separation(self):
2982 eq = self.assertEqual
Guido van Rossum98297ee2007-11-06 21:34:58 +00002983 with openfile('msg_26.txt', newline='\n') as fp:
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002984 msg = Parser().parse(fp)
2985 eq(len(msg.get_payload()), 2)
2986 part1 = msg.get_payload(0)
2987 eq(part1.get_content_type(), 'text/plain')
2988 eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n')
2989 part2 = msg.get_payload(1)
2990 eq(part2.get_content_type(), 'application/riscos')
2991
R. David Murray8451c4b2010-10-23 22:19:56 +00002992 def test_crlf_flatten(self):
2993 # Using newline='\n' preserves the crlfs in this input file.
2994 with openfile('msg_26.txt', newline='\n') as fp:
2995 text = fp.read()
2996 msg = email.message_from_string(text)
2997 s = StringIO()
2998 g = Generator(s)
2999 g.flatten(msg, linesep='\r\n')
3000 self.assertEqual(s.getvalue(), text)
3001
3002 maxDiff = None
3003
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003004 def test_multipart_digest_with_extra_mime_headers(self):
3005 eq = self.assertEqual
3006 neq = self.ndiffAssertEqual
3007 with openfile('msg_28.txt') as fp:
3008 msg = email.message_from_file(fp)
3009 # Structure is:
3010 # multipart/digest
3011 # message/rfc822
3012 # text/plain
3013 # message/rfc822
3014 # text/plain
3015 eq(msg.is_multipart(), 1)
3016 eq(len(msg.get_payload()), 2)
3017 part1 = msg.get_payload(0)
3018 eq(part1.get_content_type(), 'message/rfc822')
3019 eq(part1.is_multipart(), 1)
3020 eq(len(part1.get_payload()), 1)
3021 part1a = part1.get_payload(0)
3022 eq(part1a.is_multipart(), 0)
3023 eq(part1a.get_content_type(), 'text/plain')
3024 neq(part1a.get_payload(), 'message 1\n')
3025 # next message/rfc822
3026 part2 = msg.get_payload(1)
3027 eq(part2.get_content_type(), 'message/rfc822')
3028 eq(part2.is_multipart(), 1)
3029 eq(len(part2.get_payload()), 1)
3030 part2a = part2.get_payload(0)
3031 eq(part2a.is_multipart(), 0)
3032 eq(part2a.get_content_type(), 'text/plain')
3033 neq(part2a.get_payload(), 'message 2\n')
3034
3035 def test_three_lines(self):
3036 # A bug report by Andrew McNamara
3037 lines = ['From: Andrew Person <aperson@dom.ain',
3038 'Subject: Test',
3039 'Date: Tue, 20 Aug 2002 16:43:45 +1000']
3040 msg = email.message_from_string(NL.join(lines))
3041 self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000')
3042
3043 def test_strip_line_feed_and_carriage_return_in_headers(self):
3044 eq = self.assertEqual
3045 # For [ 1002475 ] email message parser doesn't handle \r\n correctly
3046 value1 = 'text'
3047 value2 = 'more text'
3048 m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % (
3049 value1, value2)
3050 msg = email.message_from_string(m)
3051 eq(msg.get('Header'), value1)
3052 eq(msg.get('Next-Header'), value2)
3053
3054 def test_rfc2822_header_syntax(self):
3055 eq = self.assertEqual
3056 m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
3057 msg = email.message_from_string(m)
3058 eq(len(msg), 3)
3059 eq(sorted(field for field in msg), ['!"#QUX;~', '>From', 'From'])
3060 eq(msg.get_payload(), 'body')
3061
3062 def test_rfc2822_space_not_allowed_in_header(self):
3063 eq = self.assertEqual
3064 m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
3065 msg = email.message_from_string(m)
3066 eq(len(msg.keys()), 0)
3067
3068 def test_rfc2822_one_character_header(self):
3069 eq = self.assertEqual
3070 m = 'A: first header\nB: second header\nCC: third header\n\nbody'
3071 msg = email.message_from_string(m)
3072 headers = msg.keys()
3073 headers.sort()
3074 eq(headers, ['A', 'B', 'CC'])
3075 eq(msg.get_payload(), 'body')
3076
R. David Murray45e0e142010-06-16 02:19:40 +00003077 def test_CRLFLF_at_end_of_part(self):
3078 # issue 5610: feedparser should not eat two chars from body part ending
3079 # with "\r\n\n".
3080 m = (
3081 "From: foo@bar.com\n"
3082 "To: baz\n"
3083 "Mime-Version: 1.0\n"
3084 "Content-Type: multipart/mixed; boundary=BOUNDARY\n"
3085 "\n"
3086 "--BOUNDARY\n"
3087 "Content-Type: text/plain\n"
3088 "\n"
3089 "body ending with CRLF newline\r\n"
3090 "\n"
3091 "--BOUNDARY--\n"
3092 )
3093 msg = email.message_from_string(m)
3094 self.assertTrue(msg.get_payload(0).get_payload().endswith('\r\n'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003095
Ezio Melottib3aedd42010-11-20 19:04:17 +00003096
R. David Murray96fd54e2010-10-08 15:55:28 +00003097class Test8BitBytesHandling(unittest.TestCase):
3098 # In Python3 all input is string, but that doesn't work if the actual input
3099 # uses an 8bit transfer encoding. To hack around that, in email 5.1 we
3100 # decode byte streams using the surrogateescape error handler, and
3101 # reconvert to binary at appropriate places if we detect surrogates. This
3102 # doesn't allow us to transform headers with 8bit bytes (they get munged),
3103 # but it does allow us to parse and preserve them, and to decode body
3104 # parts that use an 8bit CTE.
3105
3106 bodytest_msg = textwrap.dedent("""\
3107 From: foo@bar.com
3108 To: baz
3109 Mime-Version: 1.0
3110 Content-Type: text/plain; charset={charset}
3111 Content-Transfer-Encoding: {cte}
3112
3113 {bodyline}
3114 """)
3115
3116 def test_known_8bit_CTE(self):
3117 m = self.bodytest_msg.format(charset='utf-8',
3118 cte='8bit',
3119 bodyline='pöstal').encode('utf-8')
3120 msg = email.message_from_bytes(m)
3121 self.assertEqual(msg.get_payload(), "pöstal\n")
3122 self.assertEqual(msg.get_payload(decode=True),
3123 "pöstal\n".encode('utf-8'))
3124
3125 def test_unknown_8bit_CTE(self):
3126 m = self.bodytest_msg.format(charset='notavalidcharset',
3127 cte='8bit',
3128 bodyline='pöstal').encode('utf-8')
3129 msg = email.message_from_bytes(m)
R. David Murray92532142011-01-07 23:25:30 +00003130 self.assertEqual(msg.get_payload(), "p\uFFFD\uFFFDstal\n")
R. David Murray96fd54e2010-10-08 15:55:28 +00003131 self.assertEqual(msg.get_payload(decode=True),
3132 "pöstal\n".encode('utf-8'))
3133
3134 def test_8bit_in_quopri_body(self):
3135 # This is non-RFC compliant data...without 'decode' the library code
3136 # decodes the body using the charset from the headers, and because the
3137 # source byte really is utf-8 this works. This is likely to fail
3138 # against real dirty data (ie: produce mojibake), but the data is
3139 # invalid anyway so it is as good a guess as any. But this means that
3140 # this test just confirms the current behavior; that behavior is not
3141 # necessarily the best possible behavior. With 'decode' it is
3142 # returning the raw bytes, so that test should be of correct behavior,
3143 # or at least produce the same result that email4 did.
3144 m = self.bodytest_msg.format(charset='utf-8',
3145 cte='quoted-printable',
3146 bodyline='p=C3=B6stál').encode('utf-8')
3147 msg = email.message_from_bytes(m)
3148 self.assertEqual(msg.get_payload(), 'p=C3=B6stál\n')
3149 self.assertEqual(msg.get_payload(decode=True),
3150 'pöstál\n'.encode('utf-8'))
3151
3152 def test_invalid_8bit_in_non_8bit_cte_uses_replace(self):
3153 # This is similar to the previous test, but proves that if the 8bit
3154 # byte is undecodeable in the specified charset, it gets replaced
3155 # by the unicode 'unknown' character. Again, this may or may not
3156 # be the ideal behavior. Note that if decode=False none of the
3157 # decoders will get involved, so this is the only test we need
3158 # for this behavior.
3159 m = self.bodytest_msg.format(charset='ascii',
3160 cte='quoted-printable',
3161 bodyline='p=C3=B6stál').encode('utf-8')
3162 msg = email.message_from_bytes(m)
R. David Murray92532142011-01-07 23:25:30 +00003163 self.assertEqual(msg.get_payload(), 'p=C3=B6st\uFFFD\uFFFDl\n')
R. David Murray96fd54e2010-10-08 15:55:28 +00003164 self.assertEqual(msg.get_payload(decode=True),
3165 'pöstál\n'.encode('utf-8'))
3166
3167 def test_8bit_in_base64_body(self):
3168 # Sticking an 8bit byte in a base64 block makes it undecodable by
3169 # normal means, so the block is returned undecoded, but as bytes.
3170 m = self.bodytest_msg.format(charset='utf-8',
3171 cte='base64',
3172 bodyline='cMO2c3RhbAá=').encode('utf-8')
3173 msg = email.message_from_bytes(m)
3174 self.assertEqual(msg.get_payload(decode=True),
3175 'cMO2c3RhbAá=\n'.encode('utf-8'))
3176
3177 def test_8bit_in_uuencode_body(self):
3178 # Sticking an 8bit byte in a uuencode block makes it undecodable by
3179 # normal means, so the block is returned undecoded, but as bytes.
3180 m = self.bodytest_msg.format(charset='utf-8',
3181 cte='uuencode',
3182 bodyline='<,.V<W1A; á ').encode('utf-8')
3183 msg = email.message_from_bytes(m)
3184 self.assertEqual(msg.get_payload(decode=True),
3185 '<,.V<W1A; á \n'.encode('utf-8'))
3186
3187
R. David Murray92532142011-01-07 23:25:30 +00003188 headertest_headers = (
3189 ('From: foo@bar.com', ('From', 'foo@bar.com')),
3190 ('To: báz', ('To', '=?unknown-8bit?q?b=C3=A1z?=')),
3191 ('Subject: Maintenant je vous présente mon collègue, le pouf célèbre\n'
3192 '\tJean de Baddie',
3193 ('Subject', '=?unknown-8bit?q?Maintenant_je_vous_pr=C3=A9sente_mon_'
3194 'coll=C3=A8gue=2C_le_pouf_c=C3=A9l=C3=A8bre?=\n'
3195 ' =?unknown-8bit?q?_Jean_de_Baddie?=')),
3196 ('From: göst', ('From', '=?unknown-8bit?b?Z8O2c3Q=?=')),
3197 )
3198 headertest_msg = ('\n'.join([src for (src, _) in headertest_headers]) +
3199 '\nYes, they are flying.\n').encode('utf-8')
R. David Murray96fd54e2010-10-08 15:55:28 +00003200
3201 def test_get_8bit_header(self):
3202 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003203 self.assertEqual(str(msg.get('to')), 'b\uFFFD\uFFFDz')
3204 self.assertEqual(str(msg['to']), 'b\uFFFD\uFFFDz')
R. David Murray96fd54e2010-10-08 15:55:28 +00003205
3206 def test_print_8bit_headers(self):
3207 msg = email.message_from_bytes(self.headertest_msg)
3208 self.assertEqual(str(msg),
R. David Murray92532142011-01-07 23:25:30 +00003209 textwrap.dedent("""\
3210 From: {}
3211 To: {}
3212 Subject: {}
3213 From: {}
3214
3215 Yes, they are flying.
3216 """).format(*[expected[1] for (_, expected) in
3217 self.headertest_headers]))
R. David Murray96fd54e2010-10-08 15:55:28 +00003218
3219 def test_values_with_8bit_headers(self):
3220 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003221 self.assertListEqual([str(x) for x in msg.values()],
R. David Murray96fd54e2010-10-08 15:55:28 +00003222 ['foo@bar.com',
R. David Murray92532142011-01-07 23:25:30 +00003223 'b\uFFFD\uFFFDz',
3224 'Maintenant je vous pr\uFFFD\uFFFDsente mon '
3225 'coll\uFFFD\uFFFDgue, le pouf '
3226 'c\uFFFD\uFFFDl\uFFFD\uFFFDbre\n'
R. David Murray96fd54e2010-10-08 15:55:28 +00003227 '\tJean de Baddie',
R. David Murray92532142011-01-07 23:25:30 +00003228 "g\uFFFD\uFFFDst"])
R. David Murray96fd54e2010-10-08 15:55:28 +00003229
3230 def test_items_with_8bit_headers(self):
3231 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003232 self.assertListEqual([(str(x), str(y)) for (x, y) in msg.items()],
R. David Murray96fd54e2010-10-08 15:55:28 +00003233 [('From', 'foo@bar.com'),
R. David Murray92532142011-01-07 23:25:30 +00003234 ('To', 'b\uFFFD\uFFFDz'),
3235 ('Subject', 'Maintenant je vous '
3236 'pr\uFFFD\uFFFDsente '
3237 'mon coll\uFFFD\uFFFDgue, le pouf '
3238 'c\uFFFD\uFFFDl\uFFFD\uFFFDbre\n'
3239 '\tJean de Baddie'),
3240 ('From', 'g\uFFFD\uFFFDst')])
R. David Murray96fd54e2010-10-08 15:55:28 +00003241
3242 def test_get_all_with_8bit_headers(self):
3243 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003244 self.assertListEqual([str(x) for x in msg.get_all('from')],
R. David Murray96fd54e2010-10-08 15:55:28 +00003245 ['foo@bar.com',
R. David Murray92532142011-01-07 23:25:30 +00003246 'g\uFFFD\uFFFDst'])
R. David Murray96fd54e2010-10-08 15:55:28 +00003247
R David Murraya2150232011-03-16 21:11:23 -04003248 def test_get_content_type_with_8bit(self):
3249 msg = email.message_from_bytes(textwrap.dedent("""\
3250 Content-Type: text/pl\xA7in; charset=utf-8
3251 """).encode('latin-1'))
3252 self.assertEqual(msg.get_content_type(), "text/pl\uFFFDin")
3253 self.assertEqual(msg.get_content_maintype(), "text")
3254 self.assertEqual(msg.get_content_subtype(), "pl\uFFFDin")
3255
3256 def test_get_params_with_8bit(self):
3257 msg = email.message_from_bytes(
3258 'X-Header: foo=\xa7ne; b\xa7r=two; baz=three\n'.encode('latin-1'))
3259 self.assertEqual(msg.get_params(header='x-header'),
3260 [('foo', '\uFFFDne'), ('b\uFFFDr', 'two'), ('baz', 'three')])
3261 self.assertEqual(msg.get_param('Foo', header='x-header'), '\uFFFdne')
3262 # XXX: someday you might be able to get 'b\xa7r', for now you can't.
3263 self.assertEqual(msg.get_param('b\xa7r', header='x-header'), None)
3264
3265 def test_get_rfc2231_params_with_8bit(self):
3266 msg = email.message_from_bytes(textwrap.dedent("""\
3267 Content-Type: text/plain; charset=us-ascii;
3268 title*=us-ascii'en'This%20is%20not%20f\xa7n"""
3269 ).encode('latin-1'))
3270 self.assertEqual(msg.get_param('title'),
3271 ('us-ascii', 'en', 'This is not f\uFFFDn'))
3272
3273 def test_set_rfc2231_params_with_8bit(self):
3274 msg = email.message_from_bytes(textwrap.dedent("""\
3275 Content-Type: text/plain; charset=us-ascii;
3276 title*=us-ascii'en'This%20is%20not%20f\xa7n"""
3277 ).encode('latin-1'))
3278 msg.set_param('title', 'test')
3279 self.assertEqual(msg.get_param('title'), 'test')
3280
3281 def test_del_rfc2231_params_with_8bit(self):
3282 msg = email.message_from_bytes(textwrap.dedent("""\
3283 Content-Type: text/plain; charset=us-ascii;
3284 title*=us-ascii'en'This%20is%20not%20f\xa7n"""
3285 ).encode('latin-1'))
3286 msg.del_param('title')
3287 self.assertEqual(msg.get_param('title'), None)
3288 self.assertEqual(msg.get_content_maintype(), 'text')
3289
3290 def test_get_payload_with_8bit_cte_header(self):
3291 msg = email.message_from_bytes(textwrap.dedent("""\
3292 Content-Transfer-Encoding: b\xa7se64
3293 Content-Type: text/plain; charset=latin-1
3294
3295 payload
3296 """).encode('latin-1'))
3297 self.assertEqual(msg.get_payload(), 'payload\n')
3298 self.assertEqual(msg.get_payload(decode=True), b'payload\n')
3299
R. David Murray96fd54e2010-10-08 15:55:28 +00003300 non_latin_bin_msg = textwrap.dedent("""\
3301 From: foo@bar.com
3302 To: báz
3303 Subject: Maintenant je vous présente mon collègue, le pouf célèbre
3304 \tJean de Baddie
3305 Mime-Version: 1.0
3306 Content-Type: text/plain; charset="utf-8"
3307 Content-Transfer-Encoding: 8bit
3308
3309 Да, они летят.
3310 """).encode('utf-8')
3311
3312 def test_bytes_generator(self):
3313 msg = email.message_from_bytes(self.non_latin_bin_msg)
3314 out = BytesIO()
3315 email.generator.BytesGenerator(out).flatten(msg)
3316 self.assertEqual(out.getvalue(), self.non_latin_bin_msg)
3317
R. David Murray7372a072011-01-26 21:21:32 +00003318 def test_bytes_generator_handles_None_body(self):
3319 #Issue 11019
3320 msg = email.message.Message()
3321 out = BytesIO()
3322 email.generator.BytesGenerator(out).flatten(msg)
3323 self.assertEqual(out.getvalue(), b"\n")
3324
R. David Murray92532142011-01-07 23:25:30 +00003325 non_latin_bin_msg_as7bit_wrapped = textwrap.dedent("""\
R. David Murray96fd54e2010-10-08 15:55:28 +00003326 From: foo@bar.com
R. David Murray92532142011-01-07 23:25:30 +00003327 To: =?unknown-8bit?q?b=C3=A1z?=
3328 Subject: =?unknown-8bit?q?Maintenant_je_vous_pr=C3=A9sente_mon_coll=C3=A8gue?=
3329 =?unknown-8bit?q?=2C_le_pouf_c=C3=A9l=C3=A8bre?=
3330 =?unknown-8bit?q?_Jean_de_Baddie?=
R. David Murray96fd54e2010-10-08 15:55:28 +00003331 Mime-Version: 1.0
3332 Content-Type: text/plain; charset="utf-8"
3333 Content-Transfer-Encoding: base64
3334
3335 0JTQsCwg0L7QvdC4INC70LXRgtGP0YIuCg==
3336 """)
3337
3338 def test_generator_handles_8bit(self):
3339 msg = email.message_from_bytes(self.non_latin_bin_msg)
3340 out = StringIO()
3341 email.generator.Generator(out).flatten(msg)
R. David Murray92532142011-01-07 23:25:30 +00003342 self.assertEqual(out.getvalue(), self.non_latin_bin_msg_as7bit_wrapped)
R. David Murray96fd54e2010-10-08 15:55:28 +00003343
3344 def test_bytes_generator_with_unix_from(self):
3345 # The unixfrom contains a current date, so we can't check it
3346 # literally. Just make sure the first word is 'From' and the
3347 # rest of the message matches the input.
3348 msg = email.message_from_bytes(self.non_latin_bin_msg)
3349 out = BytesIO()
3350 email.generator.BytesGenerator(out).flatten(msg, unixfrom=True)
3351 lines = out.getvalue().split(b'\n')
3352 self.assertEqual(lines[0].split()[0], b'From')
3353 self.assertEqual(b'\n'.join(lines[1:]), self.non_latin_bin_msg)
3354
R. David Murray92532142011-01-07 23:25:30 +00003355 non_latin_bin_msg_as7bit = non_latin_bin_msg_as7bit_wrapped.split('\n')
3356 non_latin_bin_msg_as7bit[2:4] = [
3357 'Subject: =?unknown-8bit?q?Maintenant_je_vous_pr=C3=A9sente_mon_'
3358 'coll=C3=A8gue=2C_le_pouf_c=C3=A9l=C3=A8bre?=']
3359 non_latin_bin_msg_as7bit = '\n'.join(non_latin_bin_msg_as7bit)
3360
R. David Murray96fd54e2010-10-08 15:55:28 +00003361 def test_message_from_binary_file(self):
3362 fn = 'test.msg'
3363 self.addCleanup(unlink, fn)
3364 with open(fn, 'wb') as testfile:
3365 testfile.write(self.non_latin_bin_msg)
Brett Cannon384917a2010-10-29 23:08:36 +00003366 with open(fn, 'rb') as testfile:
3367 m = email.parser.BytesParser().parse(testfile)
R. David Murray96fd54e2010-10-08 15:55:28 +00003368 self.assertEqual(str(m), self.non_latin_bin_msg_as7bit)
3369
3370 latin_bin_msg = textwrap.dedent("""\
3371 From: foo@bar.com
3372 To: Dinsdale
3373 Subject: Nudge nudge, wink, wink
3374 Mime-Version: 1.0
3375 Content-Type: text/plain; charset="latin-1"
3376 Content-Transfer-Encoding: 8bit
3377
3378 oh là là, know what I mean, know what I mean?
3379 """).encode('latin-1')
3380
3381 latin_bin_msg_as7bit = textwrap.dedent("""\
3382 From: foo@bar.com
3383 To: Dinsdale
3384 Subject: Nudge nudge, wink, wink
3385 Mime-Version: 1.0
3386 Content-Type: text/plain; charset="iso-8859-1"
3387 Content-Transfer-Encoding: quoted-printable
3388
3389 oh l=E0 l=E0, know what I mean, know what I mean?
3390 """)
3391
3392 def test_string_generator_reencodes_to_quopri_when_appropriate(self):
3393 m = email.message_from_bytes(self.latin_bin_msg)
3394 self.assertEqual(str(m), self.latin_bin_msg_as7bit)
3395
3396 def test_decoded_generator_emits_unicode_body(self):
3397 m = email.message_from_bytes(self.latin_bin_msg)
3398 out = StringIO()
3399 email.generator.DecodedGenerator(out).flatten(m)
3400 #DecodedHeader output contains an extra blank line compared
3401 #to the input message. RDM: not sure if this is a bug or not,
3402 #but it is not specific to the 8bit->7bit conversion.
3403 self.assertEqual(out.getvalue(),
3404 self.latin_bin_msg.decode('latin-1')+'\n')
3405
3406 def test_bytes_feedparser(self):
3407 bfp = email.feedparser.BytesFeedParser()
3408 for i in range(0, len(self.latin_bin_msg), 10):
3409 bfp.feed(self.latin_bin_msg[i:i+10])
3410 m = bfp.close()
3411 self.assertEqual(str(m), self.latin_bin_msg_as7bit)
3412
R. David Murray8451c4b2010-10-23 22:19:56 +00003413 def test_crlf_flatten(self):
3414 with openfile('msg_26.txt', 'rb') as fp:
3415 text = fp.read()
3416 msg = email.message_from_bytes(text)
3417 s = BytesIO()
3418 g = email.generator.BytesGenerator(s)
3419 g.flatten(msg, linesep='\r\n')
3420 self.assertEqual(s.getvalue(), text)
R David Murrayc5c14722011-04-06 08:13:02 -04003421
3422 def test_8bit_multipart(self):
3423 # Issue 11605
3424 source = textwrap.dedent("""\
3425 Date: Fri, 18 Mar 2011 17:15:43 +0100
3426 To: foo@example.com
3427 From: foodwatch-Newsletter <bar@example.com>
3428 Subject: Aktuelles zu Japan, Klonfleisch und Smiley-System
3429 Message-ID: <76a486bee62b0d200f33dc2ca08220ad@localhost.localdomain>
3430 MIME-Version: 1.0
3431 Content-Type: multipart/alternative;
3432 boundary="b1_76a486bee62b0d200f33dc2ca08220ad"
3433
3434 --b1_76a486bee62b0d200f33dc2ca08220ad
3435 Content-Type: text/plain; charset="utf-8"
3436 Content-Transfer-Encoding: 8bit
3437
3438 Guten Tag, ,
3439
3440 mit großer Betroffenheit verfolgen auch wir im foodwatch-Team die
3441 Nachrichten aus Japan.
3442
3443
3444 --b1_76a486bee62b0d200f33dc2ca08220ad
3445 Content-Type: text/html; charset="utf-8"
3446 Content-Transfer-Encoding: 8bit
3447
3448 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
3449 "http://www.w3.org/TR/html4/loose.dtd">
3450 <html lang="de">
3451 <head>
3452 <title>foodwatch - Newsletter</title>
3453 </head>
3454 <body>
3455 <p>mit gro&szlig;er Betroffenheit verfolgen auch wir im foodwatch-Team
3456 die Nachrichten aus Japan.</p>
3457 </body>
3458 </html>
3459 --b1_76a486bee62b0d200f33dc2ca08220ad--
3460
3461 """).encode('utf-8')
3462 msg = email.message_from_bytes(source)
3463 s = BytesIO()
3464 g = email.generator.BytesGenerator(s)
3465 g.flatten(msg)
3466 self.assertEqual(s.getvalue(), source)
3467
R David Murray9fd170e2012-03-14 14:05:03 -04003468 def test_bytes_generator_b_encoding_linesep(self):
3469 # Issue 14062: b encoding was tacking on an extra \n.
3470 m = Message()
3471 # This has enough non-ascii that it should always end up b encoded.
3472 m['Subject'] = Header('žluťoučký kůň')
3473 s = BytesIO()
3474 g = email.generator.BytesGenerator(s)
3475 g.flatten(m, linesep='\r\n')
3476 self.assertEqual(
3477 s.getvalue(),
3478 b'Subject: =?utf-8?b?xb5sdcWlb3XEjWvDvSBrxa/FiA==?=\r\n\r\n')
3479
3480 def test_generator_b_encoding_linesep(self):
3481 # Since this broke in ByteGenerator, test Generator for completeness.
3482 m = Message()
3483 # This has enough non-ascii that it should always end up b encoded.
3484 m['Subject'] = Header('žluťoučký kůň')
3485 s = StringIO()
3486 g = email.generator.Generator(s)
3487 g.flatten(m, linesep='\r\n')
3488 self.assertEqual(
3489 s.getvalue(),
3490 'Subject: =?utf-8?b?xb5sdcWlb3XEjWvDvSBrxa/FiA==?=\r\n\r\n')
3491
R. David Murray8451c4b2010-10-23 22:19:56 +00003492 maxDiff = None
3493
Ezio Melottib3aedd42010-11-20 19:04:17 +00003494
R. David Murray719a4492010-11-21 16:53:48 +00003495class BaseTestBytesGeneratorIdempotent:
R. David Murray96fd54e2010-10-08 15:55:28 +00003496
R. David Murraye5db2632010-11-20 15:10:13 +00003497 maxDiff = None
3498
R. David Murray96fd54e2010-10-08 15:55:28 +00003499 def _msgobj(self, filename):
3500 with openfile(filename, 'rb') as fp:
3501 data = fp.read()
R. David Murray719a4492010-11-21 16:53:48 +00003502 data = self.normalize_linesep_regex.sub(self.blinesep, data)
R. David Murray96fd54e2010-10-08 15:55:28 +00003503 msg = email.message_from_bytes(data)
3504 return msg, data
3505
R. David Murray719a4492010-11-21 16:53:48 +00003506 def _idempotent(self, msg, data, unixfrom=False):
R. David Murray96fd54e2010-10-08 15:55:28 +00003507 b = BytesIO()
3508 g = email.generator.BytesGenerator(b, maxheaderlen=0)
R. David Murray719a4492010-11-21 16:53:48 +00003509 g.flatten(msg, unixfrom=unixfrom, linesep=self.linesep)
R. David Murraye5db2632010-11-20 15:10:13 +00003510 self.assertByteStringsEqual(data, b.getvalue())
R. David Murray96fd54e2010-10-08 15:55:28 +00003511
R. David Murraye5db2632010-11-20 15:10:13 +00003512 def assertByteStringsEqual(self, str1, str2):
R. David Murray719a4492010-11-21 16:53:48 +00003513 # Not using self.blinesep here is intentional. This way the output
3514 # is more useful when the failure results in mixed line endings.
R. David Murray96fd54e2010-10-08 15:55:28 +00003515 self.assertListEqual(str1.split(b'\n'), str2.split(b'\n'))
3516
3517
R. David Murray719a4492010-11-21 16:53:48 +00003518class TestBytesGeneratorIdempotentNL(BaseTestBytesGeneratorIdempotent,
3519 TestIdempotent):
3520 linesep = '\n'
3521 blinesep = b'\n'
3522 normalize_linesep_regex = re.compile(br'\r\n')
3523
3524
3525class TestBytesGeneratorIdempotentCRLF(BaseTestBytesGeneratorIdempotent,
3526 TestIdempotent):
3527 linesep = '\r\n'
3528 blinesep = b'\r\n'
3529 normalize_linesep_regex = re.compile(br'(?<!\r)\n')
3530
Ezio Melottib3aedd42010-11-20 19:04:17 +00003531
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003532class TestBase64(unittest.TestCase):
3533 def test_len(self):
3534 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00003535 eq(base64mime.header_length('hello'),
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003536 len(base64mime.body_encode(b'hello', eol='')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003537 for size in range(15):
3538 if size == 0 : bsize = 0
3539 elif size <= 3 : bsize = 4
3540 elif size <= 6 : bsize = 8
3541 elif size <= 9 : bsize = 12
3542 elif size <= 12: bsize = 16
3543 else : bsize = 20
Guido van Rossum9604e662007-08-30 03:46:43 +00003544 eq(base64mime.header_length('x' * size), bsize)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003545
3546 def test_decode(self):
3547 eq = self.assertEqual
Barry Warsaw2cc1f6d2007-08-30 14:28:55 +00003548 eq(base64mime.decode(''), b'')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003549 eq(base64mime.decode('aGVsbG8='), b'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003550
3551 def test_encode(self):
3552 eq = self.assertEqual
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003553 eq(base64mime.body_encode(b''), b'')
3554 eq(base64mime.body_encode(b'hello'), 'aGVsbG8=\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003555 # Test the binary flag
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003556 eq(base64mime.body_encode(b'hello\n'), 'aGVsbG8K\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003557 # Test the maxlinelen arg
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003558 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003559eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
3560eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
3561eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
3562eHh4eCB4eHh4IA==
3563""")
3564 # Test the eol argument
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003565 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40, eol='\r\n'),
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003566 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003567eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
3568eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
3569eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
3570eHh4eCB4eHh4IA==\r
3571""")
3572
3573 def test_header_encode(self):
3574 eq = self.assertEqual
3575 he = base64mime.header_encode
3576 eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=')
Guido van Rossum9604e662007-08-30 03:46:43 +00003577 eq(he('hello\r\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=')
3578 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003579 # Test the charset option
3580 eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=')
3581 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003582
3583
Ezio Melottib3aedd42010-11-20 19:04:17 +00003584
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003585class TestQuopri(unittest.TestCase):
3586 def setUp(self):
3587 # Set of characters (as byte integers) that don't need to be encoded
3588 # in headers.
3589 self.hlit = list(chain(
3590 range(ord('a'), ord('z') + 1),
3591 range(ord('A'), ord('Z') + 1),
3592 range(ord('0'), ord('9') + 1),
Guido van Rossum9604e662007-08-30 03:46:43 +00003593 (c for c in b'!*+-/')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003594 # Set of characters (as byte integers) that do need to be encoded in
3595 # headers.
3596 self.hnon = [c for c in range(256) if c not in self.hlit]
3597 assert len(self.hlit) + len(self.hnon) == 256
3598 # Set of characters (as byte integers) that don't need to be encoded
3599 # in bodies.
3600 self.blit = list(range(ord(' '), ord('~') + 1))
3601 self.blit.append(ord('\t'))
3602 self.blit.remove(ord('='))
3603 # Set of characters (as byte integers) that do need to be encoded in
3604 # bodies.
3605 self.bnon = [c for c in range(256) if c not in self.blit]
3606 assert len(self.blit) + len(self.bnon) == 256
3607
Guido van Rossum9604e662007-08-30 03:46:43 +00003608 def test_quopri_header_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003609 for c in self.hlit:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003610 self.assertFalse(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003611 'Should not be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003612 for c in self.hnon:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003613 self.assertTrue(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003614 'Should be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003615
Guido van Rossum9604e662007-08-30 03:46:43 +00003616 def test_quopri_body_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003617 for c in self.blit:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003618 self.assertFalse(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003619 'Should not be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003620 for c in self.bnon:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003621 self.assertTrue(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003622 'Should be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003623
3624 def test_header_quopri_len(self):
3625 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00003626 eq(quoprimime.header_length(b'hello'), 5)
3627 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003628 eq(len(quoprimime.header_encode(b'hello', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00003629 quoprimime.header_length(b'hello') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003630 # =?xxx?q?...?= means 10 extra characters
3631 10)
Guido van Rossum9604e662007-08-30 03:46:43 +00003632 eq(quoprimime.header_length(b'h@e@l@l@o@'), 20)
3633 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003634 eq(len(quoprimime.header_encode(b'h@e@l@l@o@', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00003635 quoprimime.header_length(b'h@e@l@l@o@') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003636 # =?xxx?q?...?= means 10 extra characters
3637 10)
3638 for c in self.hlit:
Guido van Rossum9604e662007-08-30 03:46:43 +00003639 eq(quoprimime.header_length(bytes([c])), 1,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003640 'expected length 1 for %r' % chr(c))
3641 for c in self.hnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00003642 # Space is special; it's encoded to _
3643 if c == ord(' '):
3644 continue
3645 eq(quoprimime.header_length(bytes([c])), 3,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003646 'expected length 3 for %r' % chr(c))
Guido van Rossum9604e662007-08-30 03:46:43 +00003647 eq(quoprimime.header_length(b' '), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003648
3649 def test_body_quopri_len(self):
3650 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003651 for c in self.blit:
Guido van Rossum9604e662007-08-30 03:46:43 +00003652 eq(quoprimime.body_length(bytes([c])), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003653 for c in self.bnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00003654 eq(quoprimime.body_length(bytes([c])), 3)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003655
3656 def test_quote_unquote_idempotent(self):
3657 for x in range(256):
3658 c = chr(x)
3659 self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c)
3660
R David Murrayec1b5b82011-03-23 14:19:05 -04003661 def _test_header_encode(self, header, expected_encoded_header, charset=None):
3662 if charset is None:
3663 encoded_header = quoprimime.header_encode(header)
3664 else:
3665 encoded_header = quoprimime.header_encode(header, charset)
3666 self.assertEqual(encoded_header, expected_encoded_header)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003667
R David Murraycafd79d2011-03-23 15:25:55 -04003668 def test_header_encode_null(self):
3669 self._test_header_encode(b'', '')
3670
R David Murrayec1b5b82011-03-23 14:19:05 -04003671 def test_header_encode_one_word(self):
3672 self._test_header_encode(b'hello', '=?iso-8859-1?q?hello?=')
3673
3674 def test_header_encode_two_lines(self):
3675 self._test_header_encode(b'hello\nworld',
3676 '=?iso-8859-1?q?hello=0Aworld?=')
3677
3678 def test_header_encode_non_ascii(self):
3679 self._test_header_encode(b'hello\xc7there',
3680 '=?iso-8859-1?q?hello=C7there?=')
3681
3682 def test_header_encode_alt_charset(self):
3683 self._test_header_encode(b'hello', '=?iso-8859-2?q?hello?=',
3684 charset='iso-8859-2')
3685
3686 def _test_header_decode(self, encoded_header, expected_decoded_header):
3687 decoded_header = quoprimime.header_decode(encoded_header)
3688 self.assertEqual(decoded_header, expected_decoded_header)
3689
3690 def test_header_decode_null(self):
3691 self._test_header_decode('', '')
3692
3693 def test_header_decode_one_word(self):
3694 self._test_header_decode('hello', 'hello')
3695
3696 def test_header_decode_two_lines(self):
3697 self._test_header_decode('hello=0Aworld', 'hello\nworld')
3698
3699 def test_header_decode_non_ascii(self):
3700 self._test_header_decode('hello=C7there', 'hello\xc7there')
3701
3702 def _test_decode(self, encoded, expected_decoded, eol=None):
3703 if eol is None:
3704 decoded = quoprimime.decode(encoded)
3705 else:
3706 decoded = quoprimime.decode(encoded, eol=eol)
3707 self.assertEqual(decoded, expected_decoded)
3708
3709 def test_decode_null_word(self):
3710 self._test_decode('', '')
3711
3712 def test_decode_null_line_null_word(self):
3713 self._test_decode('\r\n', '\n')
3714
3715 def test_decode_one_word(self):
3716 self._test_decode('hello', 'hello')
3717
3718 def test_decode_one_word_eol(self):
3719 self._test_decode('hello', 'hello', eol='X')
3720
3721 def test_decode_one_line(self):
3722 self._test_decode('hello\r\n', 'hello\n')
3723
3724 def test_decode_one_line_lf(self):
3725 self._test_decode('hello\n', 'hello\n')
3726
R David Murraycafd79d2011-03-23 15:25:55 -04003727 def test_decode_one_line_cr(self):
3728 self._test_decode('hello\r', 'hello\n')
3729
3730 def test_decode_one_line_nl(self):
3731 self._test_decode('hello\n', 'helloX', eol='X')
3732
3733 def test_decode_one_line_crnl(self):
3734 self._test_decode('hello\r\n', 'helloX', eol='X')
3735
R David Murrayec1b5b82011-03-23 14:19:05 -04003736 def test_decode_one_line_one_word(self):
3737 self._test_decode('hello\r\nworld', 'hello\nworld')
3738
3739 def test_decode_one_line_one_word_eol(self):
3740 self._test_decode('hello\r\nworld', 'helloXworld', eol='X')
3741
3742 def test_decode_two_lines(self):
3743 self._test_decode('hello\r\nworld\r\n', 'hello\nworld\n')
3744
R David Murraycafd79d2011-03-23 15:25:55 -04003745 def test_decode_two_lines_eol(self):
3746 self._test_decode('hello\r\nworld\r\n', 'helloXworldX', eol='X')
3747
R David Murrayec1b5b82011-03-23 14:19:05 -04003748 def test_decode_one_long_line(self):
3749 self._test_decode('Spam' * 250, 'Spam' * 250)
3750
3751 def test_decode_one_space(self):
3752 self._test_decode(' ', '')
3753
3754 def test_decode_multiple_spaces(self):
3755 self._test_decode(' ' * 5, '')
3756
3757 def test_decode_one_line_trailing_spaces(self):
3758 self._test_decode('hello \r\n', 'hello\n')
3759
3760 def test_decode_two_lines_trailing_spaces(self):
3761 self._test_decode('hello \r\nworld \r\n', 'hello\nworld\n')
3762
3763 def test_decode_quoted_word(self):
3764 self._test_decode('=22quoted=20words=22', '"quoted words"')
3765
3766 def test_decode_uppercase_quoting(self):
3767 self._test_decode('ab=CD=EF', 'ab\xcd\xef')
3768
3769 def test_decode_lowercase_quoting(self):
3770 self._test_decode('ab=cd=ef', 'ab\xcd\xef')
3771
3772 def test_decode_soft_line_break(self):
3773 self._test_decode('soft line=\r\nbreak', 'soft linebreak')
3774
3775 def test_decode_false_quoting(self):
3776 self._test_decode('A=1,B=A ==> A+B==2', 'A=1,B=A ==> A+B==2')
3777
3778 def _test_encode(self, body, expected_encoded_body, maxlinelen=None, eol=None):
3779 kwargs = {}
3780 if maxlinelen is None:
3781 # Use body_encode's default.
3782 maxlinelen = 76
3783 else:
3784 kwargs['maxlinelen'] = maxlinelen
3785 if eol is None:
3786 # Use body_encode's default.
3787 eol = '\n'
3788 else:
3789 kwargs['eol'] = eol
3790 encoded_body = quoprimime.body_encode(body, **kwargs)
3791 self.assertEqual(encoded_body, expected_encoded_body)
3792 if eol == '\n' or eol == '\r\n':
3793 # We know how to split the result back into lines, so maxlinelen
3794 # can be checked.
3795 for line in encoded_body.splitlines():
3796 self.assertLessEqual(len(line), maxlinelen)
3797
3798 def test_encode_null(self):
3799 self._test_encode('', '')
3800
3801 def test_encode_null_lines(self):
3802 self._test_encode('\n\n', '\n\n')
3803
3804 def test_encode_one_line(self):
3805 self._test_encode('hello\n', 'hello\n')
3806
3807 def test_encode_one_line_crlf(self):
3808 self._test_encode('hello\r\n', 'hello\n')
3809
3810 def test_encode_one_line_eol(self):
3811 self._test_encode('hello\n', 'hello\r\n', eol='\r\n')
3812
3813 def test_encode_one_space(self):
3814 self._test_encode(' ', '=20')
3815
3816 def test_encode_one_line_one_space(self):
3817 self._test_encode(' \n', '=20\n')
3818
R David Murrayb938c8c2011-03-24 12:19:26 -04003819# XXX: body_encode() expect strings, but uses ord(char) from these strings
3820# to index into a 256-entry list. For code points above 255, this will fail.
3821# Should there be a check for 8-bit only ord() values in body, or at least
3822# a comment about the expected input?
3823
3824 def test_encode_two_lines_one_space(self):
3825 self._test_encode(' \n \n', '=20\n=20\n')
3826
R David Murrayec1b5b82011-03-23 14:19:05 -04003827 def test_encode_one_word_trailing_spaces(self):
3828 self._test_encode('hello ', 'hello =20')
3829
3830 def test_encode_one_line_trailing_spaces(self):
3831 self._test_encode('hello \n', 'hello =20\n')
3832
3833 def test_encode_one_word_trailing_tab(self):
3834 self._test_encode('hello \t', 'hello =09')
3835
3836 def test_encode_one_line_trailing_tab(self):
3837 self._test_encode('hello \t\n', 'hello =09\n')
3838
3839 def test_encode_trailing_space_before_maxlinelen(self):
3840 self._test_encode('abcd \n1234', 'abcd =\n\n1234', maxlinelen=6)
3841
R David Murrayb938c8c2011-03-24 12:19:26 -04003842 def test_encode_trailing_space_at_maxlinelen(self):
3843 self._test_encode('abcd \n1234', 'abcd=\n=20\n1234', maxlinelen=5)
3844
R David Murrayec1b5b82011-03-23 14:19:05 -04003845 def test_encode_trailing_space_beyond_maxlinelen(self):
R David Murrayb938c8c2011-03-24 12:19:26 -04003846 self._test_encode('abcd \n1234', 'abc=\nd=20\n1234', maxlinelen=4)
3847
3848 def test_encode_whitespace_lines(self):
3849 self._test_encode(' \n' * 5, '=20\n' * 5)
R David Murrayec1b5b82011-03-23 14:19:05 -04003850
3851 def test_encode_quoted_equals(self):
3852 self._test_encode('a = b', 'a =3D b')
3853
3854 def test_encode_one_long_string(self):
3855 self._test_encode('x' * 100, 'x' * 75 + '=\n' + 'x' * 25)
3856
3857 def test_encode_one_long_line(self):
3858 self._test_encode('x' * 100 + '\n', 'x' * 75 + '=\n' + 'x' * 25 + '\n')
3859
3860 def test_encode_one_very_long_line(self):
3861 self._test_encode('x' * 200 + '\n',
3862 2 * ('x' * 75 + '=\n') + 'x' * 50 + '\n')
3863
3864 def test_encode_one_long_line(self):
3865 self._test_encode('x' * 100 + '\n', 'x' * 75 + '=\n' + 'x' * 25 + '\n')
3866
3867 def test_encode_shortest_maxlinelen(self):
3868 self._test_encode('=' * 5, '=3D=\n' * 4 + '=3D', maxlinelen=4)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003869
R David Murrayb938c8c2011-03-24 12:19:26 -04003870 def test_encode_maxlinelen_too_small(self):
3871 self.assertRaises(ValueError, self._test_encode, '', '', maxlinelen=3)
3872
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003873 def test_encode(self):
3874 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00003875 eq(quoprimime.body_encode(''), '')
3876 eq(quoprimime.body_encode('hello'), 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003877 # Test the binary flag
Guido van Rossum9604e662007-08-30 03:46:43 +00003878 eq(quoprimime.body_encode('hello\r\nworld'), 'hello\nworld')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003879 # Test the maxlinelen arg
Guido van Rossum9604e662007-08-30 03:46:43 +00003880 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003881xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
3882 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
3883x xxxx xxxx xxxx xxxx=20""")
3884 # Test the eol argument
Guido van Rossum9604e662007-08-30 03:46:43 +00003885 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'),
3886 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003887xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
3888 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
3889x xxxx xxxx xxxx xxxx=20""")
Guido van Rossum9604e662007-08-30 03:46:43 +00003890 eq(quoprimime.body_encode("""\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003891one line
3892
3893two line"""), """\
3894one line
3895
3896two line""")
3897
3898
Ezio Melottib3aedd42010-11-20 19:04:17 +00003899
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003900# Test the Charset class
3901class TestCharset(unittest.TestCase):
3902 def tearDown(self):
3903 from email import charset as CharsetModule
3904 try:
3905 del CharsetModule.CHARSETS['fake']
3906 except KeyError:
3907 pass
3908
Guido van Rossum9604e662007-08-30 03:46:43 +00003909 def test_codec_encodeable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003910 eq = self.assertEqual
3911 # Make sure us-ascii = no Unicode conversion
3912 c = Charset('us-ascii')
Guido van Rossum9604e662007-08-30 03:46:43 +00003913 eq(c.header_encode('Hello World!'), 'Hello World!')
3914 # Test 8-bit idempotency with us-ascii
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003915 s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
Guido van Rossum9604e662007-08-30 03:46:43 +00003916 self.assertRaises(UnicodeError, c.header_encode, s)
3917 c = Charset('utf-8')
3918 eq(c.header_encode(s), '=?utf-8?b?wqTCosKkwqTCpMKmwqTCqMKkwqo=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003919
3920 def test_body_encode(self):
3921 eq = self.assertEqual
3922 # Try a charset with QP body encoding
3923 c = Charset('iso-8859-1')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003924 eq('hello w=F6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003925 # Try a charset with Base64 body encoding
3926 c = Charset('utf-8')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003927 eq('aGVsbG8gd29ybGQ=\n', c.body_encode(b'hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003928 # Try a charset with None body encoding
3929 c = Charset('us-ascii')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003930 eq('hello world', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003931 # Try the convert argument, where input codec != output codec
3932 c = Charset('euc-jp')
3933 # With apologies to Tokio Kikuchi ;)
Barry Warsawbef9d212007-08-31 10:55:37 +00003934 # XXX FIXME
3935## try:
3936## eq('\x1b$B5FCO;~IW\x1b(B',
3937## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
3938## eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
3939## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
3940## except LookupError:
3941## # We probably don't have the Japanese codecs installed
3942## pass
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003943 # Testing SF bug #625509, which we have to fake, since there are no
3944 # built-in encodings where the header encoding is QP but the body
3945 # encoding is not.
3946 from email import charset as CharsetModule
R David Murray56a9d7e2011-03-15 12:20:02 -04003947 CharsetModule.add_charset('fake', CharsetModule.QP, None, 'utf-8')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003948 c = Charset('fake')
R David Murray56a9d7e2011-03-15 12:20:02 -04003949 eq('hello world', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003950
3951 def test_unicode_charset_name(self):
3952 charset = Charset('us-ascii')
3953 self.assertEqual(str(charset), 'us-ascii')
3954 self.assertRaises(errors.CharsetError, Charset, 'asc\xffii')
3955
3956
Ezio Melottib3aedd42010-11-20 19:04:17 +00003957
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003958# Test multilingual MIME headers.
3959class TestHeader(TestEmailBase):
3960 def test_simple(self):
3961 eq = self.ndiffAssertEqual
3962 h = Header('Hello World!')
3963 eq(h.encode(), 'Hello World!')
3964 h.append(' Goodbye World!')
3965 eq(h.encode(), 'Hello World! Goodbye World!')
3966
3967 def test_simple_surprise(self):
3968 eq = self.ndiffAssertEqual
3969 h = Header('Hello World!')
3970 eq(h.encode(), 'Hello World!')
3971 h.append('Goodbye World!')
3972 eq(h.encode(), 'Hello World! Goodbye World!')
3973
3974 def test_header_needs_no_decoding(self):
3975 h = 'no decoding needed'
3976 self.assertEqual(decode_header(h), [(h, None)])
3977
3978 def test_long(self):
3979 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.",
3980 maxlinelen=76)
3981 for l in h.encode(splitchars=' ').split('\n '):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003982 self.assertTrue(len(l) <= 76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003983
3984 def test_multilingual(self):
3985 eq = self.ndiffAssertEqual
3986 g = Charset("iso-8859-1")
3987 cz = Charset("iso-8859-2")
3988 utf8 = Charset("utf-8")
3989 g_head = (b'Die Mieter treten hier ein werden mit einem '
3990 b'Foerderband komfortabel den Korridor entlang, '
3991 b'an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, '
3992 b'gegen die rotierenden Klingen bef\xf6rdert. ')
3993 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
3994 b'd\xf9vtipu.. ')
3995 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
3996 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
3997 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
3998 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
3999 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
4000 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
4001 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
4002 '\u3044\u307e\u3059\u3002')
4003 h = Header(g_head, g)
4004 h.append(cz_head, cz)
4005 h.append(utf8_head, utf8)
Guido van Rossum9604e662007-08-30 03:46:43 +00004006 enc = h.encode(maxlinelen=76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004007 eq(enc, """\
Guido van Rossum9604e662007-08-30 03:46:43 +00004008=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_kom?=
4009 =?iso-8859-1?q?fortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wand?=
4010 =?iso-8859-1?q?gem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6r?=
4011 =?iso-8859-1?q?dert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004012 =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
4013 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
4014 =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
4015 =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
Guido van Rossum9604e662007-08-30 03:46:43 +00004016 =?utf-8?b?IE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5k?=
4017 =?utf-8?b?IGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIA=?=
4018 =?utf-8?b?44Gj44Gm44GE44G+44GZ44CC?=""")
4019 decoded = decode_header(enc)
4020 eq(len(decoded), 3)
4021 eq(decoded[0], (g_head, 'iso-8859-1'))
4022 eq(decoded[1], (cz_head, 'iso-8859-2'))
4023 eq(decoded[2], (utf8_head.encode('utf-8'), 'utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004024 ustr = str(h)
Guido van Rossum9604e662007-08-30 03:46:43 +00004025 eq(ustr,
4026 (b'Die Mieter treten hier ein werden mit einem Foerderband '
4027 b'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
4028 b'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
4029 b'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
4030 b'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
4031 b'\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
4032 b'\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
4033 b'\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
4034 b'\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
4035 b'\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
4036 b'\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
4037 b'\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
4038 b'\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
4039 b'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
4040 b'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
4041 b'\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82'
4042 ).decode('utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004043 # Test make_header()
4044 newh = make_header(decode_header(enc))
Guido van Rossum9604e662007-08-30 03:46:43 +00004045 eq(newh, h)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004046
4047 def test_empty_header_encode(self):
4048 h = Header()
4049 self.assertEqual(h.encode(), '')
Barry Warsaw8b3d6592007-08-30 02:10:49 +00004050
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004051 def test_header_ctor_default_args(self):
4052 eq = self.ndiffAssertEqual
4053 h = Header()
4054 eq(h, '')
4055 h.append('foo', Charset('iso-8859-1'))
Guido van Rossum9604e662007-08-30 03:46:43 +00004056 eq(h, 'foo')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004057
4058 def test_explicit_maxlinelen(self):
4059 eq = self.ndiffAssertEqual
4060 hstr = ('A very long line that must get split to something other '
4061 'than at the 76th character boundary to test the non-default '
4062 'behavior')
4063 h = Header(hstr)
4064 eq(h.encode(), '''\
4065A very long line that must get split to something other than at the 76th
4066 character boundary to test the non-default behavior''')
4067 eq(str(h), hstr)
4068 h = Header(hstr, header_name='Subject')
4069 eq(h.encode(), '''\
4070A very long line that must get split to something other than at the
4071 76th character boundary to test the non-default behavior''')
4072 eq(str(h), hstr)
4073 h = Header(hstr, maxlinelen=1024, header_name='Subject')
4074 eq(h.encode(), hstr)
4075 eq(str(h), hstr)
4076
Guido van Rossum9604e662007-08-30 03:46:43 +00004077 def test_quopri_splittable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004078 eq = self.ndiffAssertEqual
4079 h = Header(charset='iso-8859-1', maxlinelen=20)
Guido van Rossum9604e662007-08-30 03:46:43 +00004080 x = 'xxxx ' * 20
4081 h.append(x)
4082 s = h.encode()
4083 eq(s, """\
4084=?iso-8859-1?q?xxx?=
4085 =?iso-8859-1?q?x_?=
4086 =?iso-8859-1?q?xx?=
4087 =?iso-8859-1?q?xx?=
4088 =?iso-8859-1?q?_x?=
4089 =?iso-8859-1?q?xx?=
4090 =?iso-8859-1?q?x_?=
4091 =?iso-8859-1?q?xx?=
4092 =?iso-8859-1?q?xx?=
4093 =?iso-8859-1?q?_x?=
4094 =?iso-8859-1?q?xx?=
4095 =?iso-8859-1?q?x_?=
4096 =?iso-8859-1?q?xx?=
4097 =?iso-8859-1?q?xx?=
4098 =?iso-8859-1?q?_x?=
4099 =?iso-8859-1?q?xx?=
4100 =?iso-8859-1?q?x_?=
4101 =?iso-8859-1?q?xx?=
4102 =?iso-8859-1?q?xx?=
4103 =?iso-8859-1?q?_x?=
4104 =?iso-8859-1?q?xx?=
4105 =?iso-8859-1?q?x_?=
4106 =?iso-8859-1?q?xx?=
4107 =?iso-8859-1?q?xx?=
4108 =?iso-8859-1?q?_x?=
4109 =?iso-8859-1?q?xx?=
4110 =?iso-8859-1?q?x_?=
4111 =?iso-8859-1?q?xx?=
4112 =?iso-8859-1?q?xx?=
4113 =?iso-8859-1?q?_x?=
4114 =?iso-8859-1?q?xx?=
4115 =?iso-8859-1?q?x_?=
4116 =?iso-8859-1?q?xx?=
4117 =?iso-8859-1?q?xx?=
4118 =?iso-8859-1?q?_x?=
4119 =?iso-8859-1?q?xx?=
4120 =?iso-8859-1?q?x_?=
4121 =?iso-8859-1?q?xx?=
4122 =?iso-8859-1?q?xx?=
4123 =?iso-8859-1?q?_x?=
4124 =?iso-8859-1?q?xx?=
4125 =?iso-8859-1?q?x_?=
4126 =?iso-8859-1?q?xx?=
4127 =?iso-8859-1?q?xx?=
4128 =?iso-8859-1?q?_x?=
4129 =?iso-8859-1?q?xx?=
4130 =?iso-8859-1?q?x_?=
4131 =?iso-8859-1?q?xx?=
4132 =?iso-8859-1?q?xx?=
4133 =?iso-8859-1?q?_?=""")
4134 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004135 h = Header(charset='iso-8859-1', maxlinelen=40)
4136 h.append('xxxx ' * 20)
Guido van Rossum9604e662007-08-30 03:46:43 +00004137 s = h.encode()
4138 eq(s, """\
4139=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xxx?=
4140 =?iso-8859-1?q?x_xxxx_xxxx_xxxx_xxxx_?=
4141 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
4142 =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
4143 =?iso-8859-1?q?_xxxx_xxxx_?=""")
4144 eq(x, str(make_header(decode_header(s))))
4145
4146 def test_base64_splittable(self):
4147 eq = self.ndiffAssertEqual
4148 h = Header(charset='koi8-r', maxlinelen=20)
4149 x = 'xxxx ' * 20
4150 h.append(x)
4151 s = h.encode()
4152 eq(s, """\
4153=?koi8-r?b?eHh4?=
4154 =?koi8-r?b?eCB4?=
4155 =?koi8-r?b?eHh4?=
4156 =?koi8-r?b?IHh4?=
4157 =?koi8-r?b?eHgg?=
4158 =?koi8-r?b?eHh4?=
4159 =?koi8-r?b?eCB4?=
4160 =?koi8-r?b?eHh4?=
4161 =?koi8-r?b?IHh4?=
4162 =?koi8-r?b?eHgg?=
4163 =?koi8-r?b?eHh4?=
4164 =?koi8-r?b?eCB4?=
4165 =?koi8-r?b?eHh4?=
4166 =?koi8-r?b?IHh4?=
4167 =?koi8-r?b?eHgg?=
4168 =?koi8-r?b?eHh4?=
4169 =?koi8-r?b?eCB4?=
4170 =?koi8-r?b?eHh4?=
4171 =?koi8-r?b?IHh4?=
4172 =?koi8-r?b?eHgg?=
4173 =?koi8-r?b?eHh4?=
4174 =?koi8-r?b?eCB4?=
4175 =?koi8-r?b?eHh4?=
4176 =?koi8-r?b?IHh4?=
4177 =?koi8-r?b?eHgg?=
4178 =?koi8-r?b?eHh4?=
4179 =?koi8-r?b?eCB4?=
4180 =?koi8-r?b?eHh4?=
4181 =?koi8-r?b?IHh4?=
4182 =?koi8-r?b?eHgg?=
4183 =?koi8-r?b?eHh4?=
4184 =?koi8-r?b?eCB4?=
4185 =?koi8-r?b?eHh4?=
4186 =?koi8-r?b?IA==?=""")
4187 eq(x, str(make_header(decode_header(s))))
4188 h = Header(charset='koi8-r', maxlinelen=40)
4189 h.append(x)
4190 s = h.encode()
4191 eq(s, """\
4192=?koi8-r?b?eHh4eCB4eHh4IHh4eHggeHh4?=
4193 =?koi8-r?b?eCB4eHh4IHh4eHggeHh4eCB4?=
4194 =?koi8-r?b?eHh4IHh4eHggeHh4eCB4eHh4?=
4195 =?koi8-r?b?IHh4eHggeHh4eCB4eHh4IHh4?=
4196 =?koi8-r?b?eHggeHh4eCB4eHh4IHh4eHgg?=
4197 =?koi8-r?b?eHh4eCB4eHh4IA==?=""")
4198 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004199
4200 def test_us_ascii_header(self):
4201 eq = self.assertEqual
4202 s = 'hello'
4203 x = decode_header(s)
4204 eq(x, [('hello', None)])
4205 h = make_header(x)
4206 eq(s, h.encode())
4207
4208 def test_string_charset(self):
4209 eq = self.assertEqual
4210 h = Header()
4211 h.append('hello', 'iso-8859-1')
Guido van Rossum9604e662007-08-30 03:46:43 +00004212 eq(h, 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004213
4214## def test_unicode_error(self):
4215## raises = self.assertRaises
4216## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')
4217## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')
4218## h = Header()
4219## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')
4220## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')
4221## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')
4222
4223 def test_utf8_shortest(self):
4224 eq = self.assertEqual
4225 h = Header('p\xf6stal', 'utf-8')
4226 eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
4227 h = Header('\u83ca\u5730\u6642\u592b', 'utf-8')
4228 eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
4229
4230 def test_bad_8bit_header(self):
4231 raises = self.assertRaises
4232 eq = self.assertEqual
4233 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
4234 raises(UnicodeError, Header, x)
4235 h = Header()
4236 raises(UnicodeError, h.append, x)
4237 e = x.decode('utf-8', 'replace')
4238 eq(str(Header(x, errors='replace')), e)
4239 h.append(x, errors='replace')
4240 eq(str(h), e)
4241
R David Murray041015c2011-03-25 15:10:55 -04004242 def test_escaped_8bit_header(self):
4243 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
R David Murray6bdb1762011-06-18 12:30:55 -04004244 e = x.decode('ascii', 'surrogateescape')
4245 h = Header(e, charset=email.charset.UNKNOWN8BIT)
R David Murray041015c2011-03-25 15:10:55 -04004246 self.assertEqual(str(h),
4247 'Ynwp4dUEbay Auction Semiar- No Charge \uFFFD Earn Big')
4248 self.assertEqual(email.header.decode_header(h), [(x, 'unknown-8bit')])
4249
R David Murraye5e366c2011-06-18 12:57:28 -04004250 def test_header_handles_binary_unknown8bit(self):
4251 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
4252 h = Header(x, charset=email.charset.UNKNOWN8BIT)
4253 self.assertEqual(str(h),
4254 'Ynwp4dUEbay Auction Semiar- No Charge \uFFFD Earn Big')
4255 self.assertEqual(email.header.decode_header(h), [(x, 'unknown-8bit')])
4256
4257 def test_make_header_handles_binary_unknown8bit(self):
4258 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
4259 h = Header(x, charset=email.charset.UNKNOWN8BIT)
4260 h2 = email.header.make_header(email.header.decode_header(h))
4261 self.assertEqual(str(h2),
4262 'Ynwp4dUEbay Auction Semiar- No Charge \uFFFD Earn Big')
4263 self.assertEqual(email.header.decode_header(h2), [(x, 'unknown-8bit')])
4264
R David Murray041015c2011-03-25 15:10:55 -04004265 def test_modify_returned_list_does_not_change_header(self):
4266 h = Header('test')
4267 chunks = email.header.decode_header(h)
4268 chunks.append(('ascii', 'test2'))
4269 self.assertEqual(str(h), 'test')
4270
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004271 def test_encoded_adjacent_nonencoded(self):
4272 eq = self.assertEqual
4273 h = Header()
4274 h.append('hello', 'iso-8859-1')
4275 h.append('world')
4276 s = h.encode()
4277 eq(s, '=?iso-8859-1?q?hello?= world')
4278 h = make_header(decode_header(s))
4279 eq(h.encode(), s)
4280
4281 def test_whitespace_eater(self):
4282 eq = self.assertEqual
4283 s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
4284 parts = decode_header(s)
4285 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)])
4286 hdr = make_header(parts)
4287 eq(hdr.encode(),
4288 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
4289
4290 def test_broken_base64_header(self):
4291 raises = self.assertRaises
R. David Murrayc4e69cc2010-08-03 22:14:10 +00004292 s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3I ?='
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004293 raises(errors.HeaderParseError, decode_header, s)
4294
R. David Murray477efb32011-01-05 01:39:32 +00004295 def test_shift_jis_charset(self):
4296 h = Header('文', charset='shift_jis')
4297 self.assertEqual(h.encode(), '=?iso-2022-jp?b?GyRCSjgbKEI=?=')
4298
R David Murrayde912762011-03-16 18:26:23 -04004299 def test_flatten_header_with_no_value(self):
4300 # Issue 11401 (regression from email 4.x) Note that the space after
4301 # the header doesn't reflect the input, but this is also the way
4302 # email 4.x behaved. At some point it would be nice to fix that.
4303 msg = email.message_from_string("EmptyHeader:")
4304 self.assertEqual(str(msg), "EmptyHeader: \n\n")
4305
R David Murray01581ee2011-04-18 10:04:34 -04004306 def test_encode_preserves_leading_ws_on_value(self):
4307 msg = Message()
4308 msg['SomeHeader'] = ' value with leading ws'
4309 self.assertEqual(str(msg), "SomeHeader: value with leading ws\n\n")
4310
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004311
Ezio Melottib3aedd42010-11-20 19:04:17 +00004312
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004313# Test RFC 2231 header parameters (en/de)coding
4314class TestRFC2231(TestEmailBase):
4315 def test_get_param(self):
4316 eq = self.assertEqual
4317 msg = self._msgobj('msg_29.txt')
4318 eq(msg.get_param('title'),
4319 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
4320 eq(msg.get_param('title', unquote=False),
4321 ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
4322
4323 def test_set_param(self):
4324 eq = self.ndiffAssertEqual
4325 msg = Message()
4326 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4327 charset='us-ascii')
4328 eq(msg.get_param('title'),
4329 ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
4330 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4331 charset='us-ascii', language='en')
4332 eq(msg.get_param('title'),
4333 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
4334 msg = self._msgobj('msg_01.txt')
4335 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4336 charset='us-ascii', language='en')
4337 eq(msg.as_string(maxheaderlen=78), """\
4338Return-Path: <bbb@zzz.org>
4339Delivered-To: bbb@zzz.org
4340Received: by mail.zzz.org (Postfix, from userid 889)
4341\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
4342MIME-Version: 1.0
4343Content-Transfer-Encoding: 7bit
4344Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
4345From: bbb@ddd.com (John X. Doe)
4346To: bbb@zzz.org
4347Subject: This is a test message
4348Date: Fri, 4 May 2001 14:05:44 -0400
4349Content-Type: text/plain; charset=us-ascii;
R. David Murraydfd7eb02010-12-24 22:36:49 +00004350 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 +00004351
4352
4353Hi,
4354
4355Do you like this message?
4356
4357-Me
4358""")
4359
R David Murraya2860e82011-04-16 09:20:30 -04004360 def test_set_param_requote(self):
4361 msg = Message()
4362 msg.set_param('title', 'foo')
4363 self.assertEqual(msg['content-type'], 'text/plain; title="foo"')
4364 msg.set_param('title', 'bar', requote=False)
4365 self.assertEqual(msg['content-type'], 'text/plain; title=bar')
4366 # tspecial is still quoted.
4367 msg.set_param('title', "(bar)bell", requote=False)
4368 self.assertEqual(msg['content-type'], 'text/plain; title="(bar)bell"')
4369
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004370 def test_del_param(self):
4371 eq = self.ndiffAssertEqual
4372 msg = self._msgobj('msg_01.txt')
4373 msg.set_param('foo', 'bar', charset='us-ascii', language='en')
4374 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4375 charset='us-ascii', language='en')
4376 msg.del_param('foo', header='Content-Type')
4377 eq(msg.as_string(maxheaderlen=78), """\
4378Return-Path: <bbb@zzz.org>
4379Delivered-To: bbb@zzz.org
4380Received: by mail.zzz.org (Postfix, from userid 889)
4381\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
4382MIME-Version: 1.0
4383Content-Transfer-Encoding: 7bit
4384Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
4385From: bbb@ddd.com (John X. Doe)
4386To: bbb@zzz.org
4387Subject: This is a test message
4388Date: Fri, 4 May 2001 14:05:44 -0400
4389Content-Type: text/plain; charset="us-ascii";
R. David Murraydfd7eb02010-12-24 22:36:49 +00004390 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 +00004391
4392
4393Hi,
4394
4395Do you like this message?
4396
4397-Me
4398""")
4399
4400 def test_rfc2231_get_content_charset(self):
4401 eq = self.assertEqual
4402 msg = self._msgobj('msg_32.txt')
4403 eq(msg.get_content_charset(), 'us-ascii')
4404
R. David Murraydfd7eb02010-12-24 22:36:49 +00004405 def test_rfc2231_parse_rfc_quoting(self):
4406 m = textwrap.dedent('''\
4407 Content-Disposition: inline;
4408 \tfilename*0*=''This%20is%20even%20more%20;
4409 \tfilename*1*=%2A%2A%2Afun%2A%2A%2A%20;
4410 \tfilename*2="is it not.pdf"
4411
4412 ''')
4413 msg = email.message_from_string(m)
4414 self.assertEqual(msg.get_filename(),
4415 'This is even more ***fun*** is it not.pdf')
4416 self.assertEqual(m, msg.as_string())
4417
4418 def test_rfc2231_parse_extra_quoting(self):
4419 m = textwrap.dedent('''\
4420 Content-Disposition: inline;
4421 \tfilename*0*="''This%20is%20even%20more%20";
4422 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4423 \tfilename*2="is it not.pdf"
4424
4425 ''')
4426 msg = email.message_from_string(m)
4427 self.assertEqual(msg.get_filename(),
4428 'This is even more ***fun*** is it not.pdf')
4429 self.assertEqual(m, msg.as_string())
4430
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004431 def test_rfc2231_no_language_or_charset(self):
4432 m = '''\
4433Content-Transfer-Encoding: 8bit
4434Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
4435Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
4436
4437'''
4438 msg = email.message_from_string(m)
4439 param = msg.get_param('NAME')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00004440 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004441 self.assertEqual(
4442 param,
4443 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
4444
4445 def test_rfc2231_no_language_or_charset_in_filename(self):
4446 m = '''\
4447Content-Disposition: inline;
4448\tfilename*0*="''This%20is%20even%20more%20";
4449\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4450\tfilename*2="is it not.pdf"
4451
4452'''
4453 msg = email.message_from_string(m)
4454 self.assertEqual(msg.get_filename(),
4455 'This is even more ***fun*** is it not.pdf')
4456
4457 def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
4458 m = '''\
4459Content-Disposition: inline;
4460\tfilename*0*="''This%20is%20even%20more%20";
4461\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4462\tfilename*2="is it not.pdf"
4463
4464'''
4465 msg = email.message_from_string(m)
4466 self.assertEqual(msg.get_filename(),
4467 'This is even more ***fun*** is it not.pdf')
4468
4469 def test_rfc2231_partly_encoded(self):
4470 m = '''\
4471Content-Disposition: inline;
4472\tfilename*0="''This%20is%20even%20more%20";
4473\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4474\tfilename*2="is it not.pdf"
4475
4476'''
4477 msg = email.message_from_string(m)
4478 self.assertEqual(
4479 msg.get_filename(),
4480 'This%20is%20even%20more%20***fun*** is it not.pdf')
4481
4482 def test_rfc2231_partly_nonencoded(self):
4483 m = '''\
4484Content-Disposition: inline;
4485\tfilename*0="This%20is%20even%20more%20";
4486\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
4487\tfilename*2="is it not.pdf"
4488
4489'''
4490 msg = email.message_from_string(m)
4491 self.assertEqual(
4492 msg.get_filename(),
4493 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
4494
4495 def test_rfc2231_no_language_or_charset_in_boundary(self):
4496 m = '''\
4497Content-Type: multipart/alternative;
4498\tboundary*0*="''This%20is%20even%20more%20";
4499\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
4500\tboundary*2="is it not.pdf"
4501
4502'''
4503 msg = email.message_from_string(m)
4504 self.assertEqual(msg.get_boundary(),
4505 'This is even more ***fun*** is it not.pdf')
4506
4507 def test_rfc2231_no_language_or_charset_in_charset(self):
4508 # This is a nonsensical charset value, but tests the code anyway
4509 m = '''\
4510Content-Type: text/plain;
4511\tcharset*0*="This%20is%20even%20more%20";
4512\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
4513\tcharset*2="is it not.pdf"
4514
4515'''
4516 msg = email.message_from_string(m)
4517 self.assertEqual(msg.get_content_charset(),
4518 'this is even more ***fun*** is it not.pdf')
4519
4520 def test_rfc2231_bad_encoding_in_filename(self):
4521 m = '''\
4522Content-Disposition: inline;
4523\tfilename*0*="bogus'xx'This%20is%20even%20more%20";
4524\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4525\tfilename*2="is it not.pdf"
4526
4527'''
4528 msg = email.message_from_string(m)
4529 self.assertEqual(msg.get_filename(),
4530 'This is even more ***fun*** is it not.pdf')
4531
4532 def test_rfc2231_bad_encoding_in_charset(self):
4533 m = """\
4534Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
4535
4536"""
4537 msg = email.message_from_string(m)
4538 # This should return None because non-ascii characters in the charset
4539 # are not allowed.
4540 self.assertEqual(msg.get_content_charset(), None)
4541
4542 def test_rfc2231_bad_character_in_charset(self):
4543 m = """\
4544Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D
4545
4546"""
4547 msg = email.message_from_string(m)
4548 # This should return None because non-ascii characters in the charset
4549 # are not allowed.
4550 self.assertEqual(msg.get_content_charset(), None)
4551
4552 def test_rfc2231_bad_character_in_filename(self):
4553 m = '''\
4554Content-Disposition: inline;
4555\tfilename*0*="ascii'xx'This%20is%20even%20more%20";
4556\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4557\tfilename*2*="is it not.pdf%E2"
4558
4559'''
4560 msg = email.message_from_string(m)
4561 self.assertEqual(msg.get_filename(),
4562 'This is even more ***fun*** is it not.pdf\ufffd')
4563
4564 def test_rfc2231_unknown_encoding(self):
4565 m = """\
4566Content-Transfer-Encoding: 8bit
4567Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
4568
4569"""
4570 msg = email.message_from_string(m)
4571 self.assertEqual(msg.get_filename(), 'myfile.txt')
4572
4573 def test_rfc2231_single_tick_in_filename_extended(self):
4574 eq = self.assertEqual
4575 m = """\
4576Content-Type: application/x-foo;
4577\tname*0*=\"Frank's\"; name*1*=\" Document\"
4578
4579"""
4580 msg = email.message_from_string(m)
4581 charset, language, s = msg.get_param('name')
4582 eq(charset, None)
4583 eq(language, None)
4584 eq(s, "Frank's Document")
4585
4586 def test_rfc2231_single_tick_in_filename(self):
4587 m = """\
4588Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
4589
4590"""
4591 msg = email.message_from_string(m)
4592 param = msg.get_param('name')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00004593 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004594 self.assertEqual(param, "Frank's Document")
4595
4596 def test_rfc2231_tick_attack_extended(self):
4597 eq = self.assertEqual
4598 m = """\
4599Content-Type: application/x-foo;
4600\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
4601
4602"""
4603 msg = email.message_from_string(m)
4604 charset, language, s = msg.get_param('name')
4605 eq(charset, 'us-ascii')
4606 eq(language, 'en-us')
4607 eq(s, "Frank's Document")
4608
4609 def test_rfc2231_tick_attack(self):
4610 m = """\
4611Content-Type: application/x-foo;
4612\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
4613
4614"""
4615 msg = email.message_from_string(m)
4616 param = msg.get_param('name')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00004617 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004618 self.assertEqual(param, "us-ascii'en-us'Frank's Document")
4619
4620 def test_rfc2231_no_extended_values(self):
4621 eq = self.assertEqual
4622 m = """\
4623Content-Type: application/x-foo; name=\"Frank's Document\"
4624
4625"""
4626 msg = email.message_from_string(m)
4627 eq(msg.get_param('name'), "Frank's Document")
4628
4629 def test_rfc2231_encoded_then_unencoded_segments(self):
4630 eq = self.assertEqual
4631 m = """\
4632Content-Type: application/x-foo;
4633\tname*0*=\"us-ascii'en-us'My\";
4634\tname*1=\" Document\";
4635\tname*2*=\" For You\"
4636
4637"""
4638 msg = email.message_from_string(m)
4639 charset, language, s = msg.get_param('name')
4640 eq(charset, 'us-ascii')
4641 eq(language, 'en-us')
4642 eq(s, 'My Document For You')
4643
4644 def test_rfc2231_unencoded_then_encoded_segments(self):
4645 eq = self.assertEqual
4646 m = """\
4647Content-Type: application/x-foo;
4648\tname*0=\"us-ascii'en-us'My\";
4649\tname*1*=\" Document\";
4650\tname*2*=\" For You\"
4651
4652"""
4653 msg = email.message_from_string(m)
4654 charset, language, s = msg.get_param('name')
4655 eq(charset, 'us-ascii')
4656 eq(language, 'en-us')
4657 eq(s, 'My Document For You')
4658
4659
Ezio Melottib3aedd42010-11-20 19:04:17 +00004660
R. David Murraya8f480f2010-01-16 18:30:03 +00004661# Tests to ensure that signed parts of an email are completely preserved, as
4662# required by RFC1847 section 2.1. Note that these are incomplete, because the
4663# email package does not currently always preserve the body. See issue 1670765.
4664class TestSigned(TestEmailBase):
4665
4666 def _msg_and_obj(self, filename):
4667 with openfile(findfile(filename)) as fp:
4668 original = fp.read()
4669 msg = email.message_from_string(original)
4670 return original, msg
4671
4672 def _signed_parts_eq(self, original, result):
4673 # Extract the first mime part of each message
4674 import re
4675 repart = re.compile(r'^--([^\n]+)\n(.*?)\n--\1$', re.S | re.M)
4676 inpart = repart.search(original).group(2)
4677 outpart = repart.search(result).group(2)
4678 self.assertEqual(outpart, inpart)
4679
4680 def test_long_headers_as_string(self):
4681 original, msg = self._msg_and_obj('msg_45.txt')
4682 result = msg.as_string()
4683 self._signed_parts_eq(original, result)
4684
4685 def test_long_headers_as_string_maxheaderlen(self):
4686 original, msg = self._msg_and_obj('msg_45.txt')
4687 result = msg.as_string(maxheaderlen=60)
4688 self._signed_parts_eq(original, result)
4689
4690 def test_long_headers_flatten(self):
4691 original, msg = self._msg_and_obj('msg_45.txt')
4692 fp = StringIO()
4693 Generator(fp).flatten(msg)
4694 result = fp.getvalue()
4695 self._signed_parts_eq(original, result)
4696
4697
Ezio Melottib3aedd42010-11-20 19:04:17 +00004698
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004699def _testclasses():
4700 mod = sys.modules[__name__]
4701 return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
4702
4703
4704def suite():
4705 suite = unittest.TestSuite()
4706 for testclass in _testclasses():
4707 suite.addTest(unittest.makeSuite(testclass))
4708 return suite
4709
4710
4711def test_main():
4712 for testclass in _testclasses():
4713 run_unittest(testclass)
4714
4715
Ezio Melottib3aedd42010-11-20 19:04:17 +00004716
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004717if __name__ == '__main__':
4718 unittest.main(defaultTest='suite')