blob: 352b9b1d9a0ba6a7143b1b0bae0657555d4d907c [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
R David Murray638d40b2012-08-24 11:14:13 -040023from email.generator import Generator, DecodedGenerator, BytesGenerator
Guido van Rossum8b3febe2007-08-30 01:15:14 +000024from 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
R David Murray638d40b2012-08-24 11:14:13 -04001301 def test_mangled_from_with_bad_bytes(self):
1302 source = textwrap.dedent("""\
1303 Content-Type: text/plain; charset="utf-8"
1304 MIME-Version: 1.0
1305 Content-Transfer-Encoding: 8bit
1306 From: aaa@bbb.org
1307
1308 """).encode('utf-8')
1309 msg = email.message_from_bytes(source + b'From R\xc3\xb6lli\n')
1310 b = BytesIO()
1311 g = BytesGenerator(b, mangle_from_=True)
1312 g.flatten(msg)
1313 self.assertEqual(b.getvalue(), source + b'>From R\xc3\xb6lli\n')
1314
Ezio Melottib3aedd42010-11-20 19:04:17 +00001315
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001316# Test the basic MIMEAudio class
1317class TestMIMEAudio(unittest.TestCase):
1318 def setUp(self):
1319 # Make sure we pick up the audiotest.au that lives in email/test/data.
1320 # In Python, there's an audiotest.au living in Lib/test but that isn't
1321 # included in some binary distros that don't include the test
1322 # package. The trailing empty string on the .join() is significant
1323 # since findfile() will do a dirname().
1324 datadir = os.path.join(os.path.dirname(landmark), 'data', '')
1325 with open(findfile('audiotest.au', datadir), 'rb') as fp:
1326 self._audiodata = fp.read()
1327 self._au = MIMEAudio(self._audiodata)
1328
1329 def test_guess_minor_type(self):
1330 self.assertEqual(self._au.get_content_type(), 'audio/basic')
1331
1332 def test_encoding(self):
1333 payload = self._au.get_payload()
R. David Murray7da8f062010-06-04 16:11:08 +00001334 self.assertEqual(base64.decodebytes(bytes(payload, 'ascii')),
1335 self._audiodata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001336
1337 def test_checkSetMinor(self):
1338 au = MIMEAudio(self._audiodata, 'fish')
1339 self.assertEqual(au.get_content_type(), 'audio/fish')
1340
1341 def test_add_header(self):
1342 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001343 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001344 self._au.add_header('Content-Disposition', 'attachment',
1345 filename='audiotest.au')
1346 eq(self._au['content-disposition'],
1347 'attachment; filename="audiotest.au"')
1348 eq(self._au.get_params(header='content-disposition'),
1349 [('attachment', ''), ('filename', 'audiotest.au')])
1350 eq(self._au.get_param('filename', header='content-disposition'),
1351 'audiotest.au')
1352 missing = []
1353 eq(self._au.get_param('attachment', header='content-disposition'), '')
1354 unless(self._au.get_param('foo', failobj=missing,
1355 header='content-disposition') is missing)
1356 # Try some missing stuff
1357 unless(self._au.get_param('foobar', missing) is missing)
1358 unless(self._au.get_param('attachment', missing,
1359 header='foobar') is missing)
1360
1361
Ezio Melottib3aedd42010-11-20 19:04:17 +00001362
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001363# Test the basic MIMEImage class
1364class TestMIMEImage(unittest.TestCase):
1365 def setUp(self):
1366 with openfile('PyBanner048.gif', 'rb') as fp:
1367 self._imgdata = fp.read()
1368 self._im = MIMEImage(self._imgdata)
1369
1370 def test_guess_minor_type(self):
1371 self.assertEqual(self._im.get_content_type(), 'image/gif')
1372
1373 def test_encoding(self):
1374 payload = self._im.get_payload()
R. David Murray7da8f062010-06-04 16:11:08 +00001375 self.assertEqual(base64.decodebytes(bytes(payload, 'ascii')),
1376 self._imgdata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001377
1378 def test_checkSetMinor(self):
1379 im = MIMEImage(self._imgdata, 'fish')
1380 self.assertEqual(im.get_content_type(), 'image/fish')
1381
1382 def test_add_header(self):
1383 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001384 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001385 self._im.add_header('Content-Disposition', 'attachment',
1386 filename='dingusfish.gif')
1387 eq(self._im['content-disposition'],
1388 'attachment; filename="dingusfish.gif"')
1389 eq(self._im.get_params(header='content-disposition'),
1390 [('attachment', ''), ('filename', 'dingusfish.gif')])
1391 eq(self._im.get_param('filename', header='content-disposition'),
1392 'dingusfish.gif')
1393 missing = []
1394 eq(self._im.get_param('attachment', header='content-disposition'), '')
1395 unless(self._im.get_param('foo', failobj=missing,
1396 header='content-disposition') is missing)
1397 # Try some missing stuff
1398 unless(self._im.get_param('foobar', missing) is missing)
1399 unless(self._im.get_param('attachment', missing,
1400 header='foobar') is missing)
1401
1402
Ezio Melottib3aedd42010-11-20 19:04:17 +00001403
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001404# Test the basic MIMEApplication class
1405class TestMIMEApplication(unittest.TestCase):
1406 def test_headers(self):
1407 eq = self.assertEqual
Barry Warsaw8b2af272007-08-31 03:04:26 +00001408 msg = MIMEApplication(b'\xfa\xfb\xfc\xfd\xfe\xff')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001409 eq(msg.get_content_type(), 'application/octet-stream')
1410 eq(msg['content-transfer-encoding'], 'base64')
1411
1412 def test_body(self):
1413 eq = self.assertEqual
R David Murray6d94bd42011-03-16 15:52:22 -04001414 bytesdata = b'\xfa\xfb\xfc\xfd\xfe\xff'
1415 msg = MIMEApplication(bytesdata)
1416 # whitespace in the cte encoded block is RFC-irrelevant.
1417 eq(msg.get_payload().strip(), '+vv8/f7/')
1418 eq(msg.get_payload(decode=True), bytesdata)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001419
1420
Ezio Melottib3aedd42010-11-20 19:04:17 +00001421
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001422# Test the basic MIMEText class
1423class TestMIMEText(unittest.TestCase):
1424 def setUp(self):
1425 self._msg = MIMEText('hello there')
1426
1427 def test_types(self):
1428 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001429 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001430 eq(self._msg.get_content_type(), 'text/plain')
1431 eq(self._msg.get_param('charset'), 'us-ascii')
1432 missing = []
1433 unless(self._msg.get_param('foobar', missing) is missing)
1434 unless(self._msg.get_param('charset', missing, header='foobar')
1435 is missing)
1436
1437 def test_payload(self):
1438 self.assertEqual(self._msg.get_payload(), 'hello there')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001439 self.assertTrue(not self._msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001440
1441 def test_charset(self):
1442 eq = self.assertEqual
1443 msg = MIMEText('hello there', _charset='us-ascii')
1444 eq(msg.get_charset().input_charset, 'us-ascii')
1445 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1446
R. David Murray850fc852010-06-03 01:58:28 +00001447 def test_7bit_input(self):
1448 eq = self.assertEqual
1449 msg = MIMEText('hello there', _charset='us-ascii')
1450 eq(msg.get_charset().input_charset, 'us-ascii')
1451 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1452
1453 def test_7bit_input_no_charset(self):
1454 eq = self.assertEqual
1455 msg = MIMEText('hello there')
1456 eq(msg.get_charset(), 'us-ascii')
1457 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1458 self.assertTrue('hello there' in msg.as_string())
1459
1460 def test_utf8_input(self):
1461 teststr = '\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
1462 eq = self.assertEqual
1463 msg = MIMEText(teststr, _charset='utf-8')
1464 eq(msg.get_charset().output_charset, 'utf-8')
1465 eq(msg['content-type'], 'text/plain; charset="utf-8"')
1466 eq(msg.get_payload(decode=True), teststr.encode('utf-8'))
1467
1468 @unittest.skip("can't fix because of backward compat in email5, "
1469 "will fix in email6")
1470 def test_utf8_input_no_charset(self):
1471 teststr = '\u043a\u0438\u0440\u0438\u043b\u0438\u0446\u0430'
1472 self.assertRaises(UnicodeEncodeError, MIMEText, teststr)
1473
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001474
Ezio Melottib3aedd42010-11-20 19:04:17 +00001475
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001476# Test complicated multipart/* messages
1477class TestMultipart(TestEmailBase):
1478 def setUp(self):
1479 with openfile('PyBanner048.gif', 'rb') as fp:
1480 data = fp.read()
1481 container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
1482 image = MIMEImage(data, name='dingusfish.gif')
1483 image.add_header('content-disposition', 'attachment',
1484 filename='dingusfish.gif')
1485 intro = MIMEText('''\
1486Hi there,
1487
1488This is the dingus fish.
1489''')
1490 container.attach(intro)
1491 container.attach(image)
1492 container['From'] = 'Barry <barry@digicool.com>'
1493 container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>'
1494 container['Subject'] = 'Here is your dingus fish'
1495
1496 now = 987809702.54848599
1497 timetuple = time.localtime(now)
1498 if timetuple[-1] == 0:
1499 tzsecs = time.timezone
1500 else:
1501 tzsecs = time.altzone
1502 if tzsecs > 0:
1503 sign = '-'
1504 else:
1505 sign = '+'
1506 tzoffset = ' %s%04d' % (sign, tzsecs / 36)
1507 container['Date'] = time.strftime(
1508 '%a, %d %b %Y %H:%M:%S',
1509 time.localtime(now)) + tzoffset
1510 self._msg = container
1511 self._im = image
1512 self._txt = intro
1513
1514 def test_hierarchy(self):
1515 # convenience
1516 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001517 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001518 raises = self.assertRaises
1519 # tests
1520 m = self._msg
1521 unless(m.is_multipart())
1522 eq(m.get_content_type(), 'multipart/mixed')
1523 eq(len(m.get_payload()), 2)
1524 raises(IndexError, m.get_payload, 2)
1525 m0 = m.get_payload(0)
1526 m1 = m.get_payload(1)
1527 unless(m0 is self._txt)
1528 unless(m1 is self._im)
1529 eq(m.get_payload(), [m0, m1])
1530 unless(not m0.is_multipart())
1531 unless(not m1.is_multipart())
1532
1533 def test_empty_multipart_idempotent(self):
1534 text = """\
1535Content-Type: multipart/mixed; boundary="BOUNDARY"
1536MIME-Version: 1.0
1537Subject: A subject
1538To: aperson@dom.ain
1539From: bperson@dom.ain
1540
1541
1542--BOUNDARY
1543
1544
1545--BOUNDARY--
1546"""
1547 msg = Parser().parsestr(text)
1548 self.ndiffAssertEqual(text, msg.as_string())
1549
1550 def test_no_parts_in_a_multipart_with_none_epilogue(self):
1551 outer = MIMEBase('multipart', 'mixed')
1552 outer['Subject'] = 'A subject'
1553 outer['To'] = 'aperson@dom.ain'
1554 outer['From'] = 'bperson@dom.ain'
1555 outer.set_boundary('BOUNDARY')
1556 self.ndiffAssertEqual(outer.as_string(), '''\
1557Content-Type: multipart/mixed; boundary="BOUNDARY"
1558MIME-Version: 1.0
1559Subject: A subject
1560To: aperson@dom.ain
1561From: bperson@dom.ain
1562
1563--BOUNDARY
1564
1565--BOUNDARY--''')
1566
1567 def test_no_parts_in_a_multipart_with_empty_epilogue(self):
1568 outer = MIMEBase('multipart', 'mixed')
1569 outer['Subject'] = 'A subject'
1570 outer['To'] = 'aperson@dom.ain'
1571 outer['From'] = 'bperson@dom.ain'
1572 outer.preamble = ''
1573 outer.epilogue = ''
1574 outer.set_boundary('BOUNDARY')
1575 self.ndiffAssertEqual(outer.as_string(), '''\
1576Content-Type: multipart/mixed; boundary="BOUNDARY"
1577MIME-Version: 1.0
1578Subject: A subject
1579To: aperson@dom.ain
1580From: bperson@dom.ain
1581
1582
1583--BOUNDARY
1584
1585--BOUNDARY--
1586''')
1587
1588 def test_one_part_in_a_multipart(self):
1589 eq = self.ndiffAssertEqual
1590 outer = MIMEBase('multipart', 'mixed')
1591 outer['Subject'] = 'A subject'
1592 outer['To'] = 'aperson@dom.ain'
1593 outer['From'] = 'bperson@dom.ain'
1594 outer.set_boundary('BOUNDARY')
1595 msg = MIMEText('hello world')
1596 outer.attach(msg)
1597 eq(outer.as_string(), '''\
1598Content-Type: multipart/mixed; boundary="BOUNDARY"
1599MIME-Version: 1.0
1600Subject: A subject
1601To: aperson@dom.ain
1602From: bperson@dom.ain
1603
1604--BOUNDARY
1605Content-Type: text/plain; charset="us-ascii"
1606MIME-Version: 1.0
1607Content-Transfer-Encoding: 7bit
1608
1609hello world
1610--BOUNDARY--''')
1611
1612 def test_seq_parts_in_a_multipart_with_empty_preamble(self):
1613 eq = self.ndiffAssertEqual
1614 outer = MIMEBase('multipart', 'mixed')
1615 outer['Subject'] = 'A subject'
1616 outer['To'] = 'aperson@dom.ain'
1617 outer['From'] = 'bperson@dom.ain'
1618 outer.preamble = ''
1619 msg = MIMEText('hello world')
1620 outer.attach(msg)
1621 outer.set_boundary('BOUNDARY')
1622 eq(outer.as_string(), '''\
1623Content-Type: multipart/mixed; boundary="BOUNDARY"
1624MIME-Version: 1.0
1625Subject: A subject
1626To: aperson@dom.ain
1627From: bperson@dom.ain
1628
1629
1630--BOUNDARY
1631Content-Type: text/plain; charset="us-ascii"
1632MIME-Version: 1.0
1633Content-Transfer-Encoding: 7bit
1634
1635hello world
1636--BOUNDARY--''')
1637
1638
1639 def test_seq_parts_in_a_multipart_with_none_preamble(self):
1640 eq = self.ndiffAssertEqual
1641 outer = MIMEBase('multipart', 'mixed')
1642 outer['Subject'] = 'A subject'
1643 outer['To'] = 'aperson@dom.ain'
1644 outer['From'] = 'bperson@dom.ain'
1645 outer.preamble = None
1646 msg = MIMEText('hello world')
1647 outer.attach(msg)
1648 outer.set_boundary('BOUNDARY')
1649 eq(outer.as_string(), '''\
1650Content-Type: multipart/mixed; boundary="BOUNDARY"
1651MIME-Version: 1.0
1652Subject: A subject
1653To: aperson@dom.ain
1654From: bperson@dom.ain
1655
1656--BOUNDARY
1657Content-Type: text/plain; charset="us-ascii"
1658MIME-Version: 1.0
1659Content-Transfer-Encoding: 7bit
1660
1661hello world
1662--BOUNDARY--''')
1663
1664
1665 def test_seq_parts_in_a_multipart_with_none_epilogue(self):
1666 eq = self.ndiffAssertEqual
1667 outer = MIMEBase('multipart', 'mixed')
1668 outer['Subject'] = 'A subject'
1669 outer['To'] = 'aperson@dom.ain'
1670 outer['From'] = 'bperson@dom.ain'
1671 outer.epilogue = None
1672 msg = MIMEText('hello world')
1673 outer.attach(msg)
1674 outer.set_boundary('BOUNDARY')
1675 eq(outer.as_string(), '''\
1676Content-Type: multipart/mixed; boundary="BOUNDARY"
1677MIME-Version: 1.0
1678Subject: A subject
1679To: aperson@dom.ain
1680From: bperson@dom.ain
1681
1682--BOUNDARY
1683Content-Type: text/plain; charset="us-ascii"
1684MIME-Version: 1.0
1685Content-Transfer-Encoding: 7bit
1686
1687hello world
1688--BOUNDARY--''')
1689
1690
1691 def test_seq_parts_in_a_multipart_with_empty_epilogue(self):
1692 eq = self.ndiffAssertEqual
1693 outer = MIMEBase('multipart', 'mixed')
1694 outer['Subject'] = 'A subject'
1695 outer['To'] = 'aperson@dom.ain'
1696 outer['From'] = 'bperson@dom.ain'
1697 outer.epilogue = ''
1698 msg = MIMEText('hello world')
1699 outer.attach(msg)
1700 outer.set_boundary('BOUNDARY')
1701 eq(outer.as_string(), '''\
1702Content-Type: multipart/mixed; boundary="BOUNDARY"
1703MIME-Version: 1.0
1704Subject: A subject
1705To: aperson@dom.ain
1706From: bperson@dom.ain
1707
1708--BOUNDARY
1709Content-Type: text/plain; charset="us-ascii"
1710MIME-Version: 1.0
1711Content-Transfer-Encoding: 7bit
1712
1713hello world
1714--BOUNDARY--
1715''')
1716
1717
1718 def test_seq_parts_in_a_multipart_with_nl_epilogue(self):
1719 eq = self.ndiffAssertEqual
1720 outer = MIMEBase('multipart', 'mixed')
1721 outer['Subject'] = 'A subject'
1722 outer['To'] = 'aperson@dom.ain'
1723 outer['From'] = 'bperson@dom.ain'
1724 outer.epilogue = '\n'
1725 msg = MIMEText('hello world')
1726 outer.attach(msg)
1727 outer.set_boundary('BOUNDARY')
1728 eq(outer.as_string(), '''\
1729Content-Type: multipart/mixed; boundary="BOUNDARY"
1730MIME-Version: 1.0
1731Subject: A subject
1732To: aperson@dom.ain
1733From: bperson@dom.ain
1734
1735--BOUNDARY
1736Content-Type: text/plain; charset="us-ascii"
1737MIME-Version: 1.0
1738Content-Transfer-Encoding: 7bit
1739
1740hello world
1741--BOUNDARY--
1742
1743''')
1744
1745 def test_message_external_body(self):
1746 eq = self.assertEqual
1747 msg = self._msgobj('msg_36.txt')
1748 eq(len(msg.get_payload()), 2)
1749 msg1 = msg.get_payload(1)
1750 eq(msg1.get_content_type(), 'multipart/alternative')
1751 eq(len(msg1.get_payload()), 2)
1752 for subpart in msg1.get_payload():
1753 eq(subpart.get_content_type(), 'message/external-body')
1754 eq(len(subpart.get_payload()), 1)
1755 subsubpart = subpart.get_payload(0)
1756 eq(subsubpart.get_content_type(), 'text/plain')
1757
1758 def test_double_boundary(self):
1759 # msg_37.txt is a multipart that contains two dash-boundary's in a
1760 # row. Our interpretation of RFC 2046 calls for ignoring the second
1761 # and subsequent boundaries.
1762 msg = self._msgobj('msg_37.txt')
1763 self.assertEqual(len(msg.get_payload()), 3)
1764
1765 def test_nested_inner_contains_outer_boundary(self):
1766 eq = self.ndiffAssertEqual
1767 # msg_38.txt has an inner part that contains outer boundaries. My
1768 # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say
1769 # these are illegal and should be interpreted as unterminated inner
1770 # parts.
1771 msg = self._msgobj('msg_38.txt')
1772 sfp = StringIO()
1773 iterators._structure(msg, sfp)
1774 eq(sfp.getvalue(), """\
1775multipart/mixed
1776 multipart/mixed
1777 multipart/alternative
1778 text/plain
1779 text/plain
1780 text/plain
1781 text/plain
1782""")
1783
1784 def test_nested_with_same_boundary(self):
1785 eq = self.ndiffAssertEqual
1786 # msg 39.txt is similarly evil in that it's got inner parts that use
1787 # the same boundary as outer parts. Again, I believe the way this is
1788 # parsed is closest to the spirit of RFC 2046
1789 msg = self._msgobj('msg_39.txt')
1790 sfp = StringIO()
1791 iterators._structure(msg, sfp)
1792 eq(sfp.getvalue(), """\
1793multipart/mixed
1794 multipart/mixed
1795 multipart/alternative
1796 application/octet-stream
1797 application/octet-stream
1798 text/plain
1799""")
1800
1801 def test_boundary_in_non_multipart(self):
1802 msg = self._msgobj('msg_40.txt')
1803 self.assertEqual(msg.as_string(), '''\
1804MIME-Version: 1.0
1805Content-Type: text/html; boundary="--961284236552522269"
1806
1807----961284236552522269
1808Content-Type: text/html;
1809Content-Transfer-Encoding: 7Bit
1810
1811<html></html>
1812
1813----961284236552522269--
1814''')
1815
1816 def test_boundary_with_leading_space(self):
1817 eq = self.assertEqual
1818 msg = email.message_from_string('''\
1819MIME-Version: 1.0
1820Content-Type: multipart/mixed; boundary=" XXXX"
1821
1822-- XXXX
1823Content-Type: text/plain
1824
1825
1826-- XXXX
1827Content-Type: text/plain
1828
1829-- XXXX--
1830''')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001831 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001832 eq(msg.get_boundary(), ' XXXX')
1833 eq(len(msg.get_payload()), 2)
1834
1835 def test_boundary_without_trailing_newline(self):
1836 m = Parser().parsestr("""\
1837Content-Type: multipart/mixed; boundary="===============0012394164=="
1838MIME-Version: 1.0
1839
1840--===============0012394164==
1841Content-Type: image/file1.jpg
1842MIME-Version: 1.0
1843Content-Transfer-Encoding: base64
1844
1845YXNkZg==
1846--===============0012394164==--""")
Ezio Melottib3aedd42010-11-20 19:04:17 +00001847 self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001848
1849
Ezio Melottib3aedd42010-11-20 19:04:17 +00001850
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001851# Test some badly formatted messages
1852class TestNonConformant(TestEmailBase):
1853 def test_parse_missing_minor_type(self):
1854 eq = self.assertEqual
1855 msg = self._msgobj('msg_14.txt')
1856 eq(msg.get_content_type(), 'text/plain')
1857 eq(msg.get_content_maintype(), 'text')
1858 eq(msg.get_content_subtype(), 'plain')
1859
1860 def test_same_boundary_inner_outer(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001861 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001862 msg = self._msgobj('msg_15.txt')
1863 # XXX We can probably eventually do better
1864 inner = msg.get_payload(0)
1865 unless(hasattr(inner, 'defects'))
1866 self.assertEqual(len(inner.defects), 1)
1867 unless(isinstance(inner.defects[0],
1868 errors.StartBoundaryNotFoundDefect))
1869
1870 def test_multipart_no_boundary(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001871 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001872 msg = self._msgobj('msg_25.txt')
1873 unless(isinstance(msg.get_payload(), str))
1874 self.assertEqual(len(msg.defects), 2)
1875 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1876 unless(isinstance(msg.defects[1],
1877 errors.MultipartInvariantViolationDefect))
1878
1879 def test_invalid_content_type(self):
1880 eq = self.assertEqual
1881 neq = self.ndiffAssertEqual
1882 msg = Message()
1883 # RFC 2045, $5.2 says invalid yields text/plain
1884 msg['Content-Type'] = 'text'
1885 eq(msg.get_content_maintype(), 'text')
1886 eq(msg.get_content_subtype(), 'plain')
1887 eq(msg.get_content_type(), 'text/plain')
1888 # Clear the old value and try something /really/ invalid
1889 del msg['content-type']
1890 msg['Content-Type'] = 'foo'
1891 eq(msg.get_content_maintype(), 'text')
1892 eq(msg.get_content_subtype(), 'plain')
1893 eq(msg.get_content_type(), 'text/plain')
1894 # Still, make sure that the message is idempotently generated
1895 s = StringIO()
1896 g = Generator(s)
1897 g.flatten(msg)
1898 neq(s.getvalue(), 'Content-Type: foo\n\n')
1899
1900 def test_no_start_boundary(self):
1901 eq = self.ndiffAssertEqual
1902 msg = self._msgobj('msg_31.txt')
1903 eq(msg.get_payload(), """\
1904--BOUNDARY
1905Content-Type: text/plain
1906
1907message 1
1908
1909--BOUNDARY
1910Content-Type: text/plain
1911
1912message 2
1913
1914--BOUNDARY--
1915""")
1916
1917 def test_no_separating_blank_line(self):
1918 eq = self.ndiffAssertEqual
1919 msg = self._msgobj('msg_35.txt')
1920 eq(msg.as_string(), """\
1921From: aperson@dom.ain
1922To: bperson@dom.ain
1923Subject: here's something interesting
1924
1925counter to RFC 2822, there's no separating newline here
1926""")
1927
1928 def test_lying_multipart(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001929 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001930 msg = self._msgobj('msg_41.txt')
1931 unless(hasattr(msg, 'defects'))
1932 self.assertEqual(len(msg.defects), 2)
1933 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1934 unless(isinstance(msg.defects[1],
1935 errors.MultipartInvariantViolationDefect))
1936
1937 def test_missing_start_boundary(self):
1938 outer = self._msgobj('msg_42.txt')
1939 # The message structure is:
1940 #
1941 # multipart/mixed
1942 # text/plain
1943 # message/rfc822
1944 # multipart/mixed [*]
1945 #
1946 # [*] This message is missing its start boundary
1947 bad = outer.get_payload(1).get_payload(0)
1948 self.assertEqual(len(bad.defects), 1)
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001949 self.assertTrue(isinstance(bad.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001950 errors.StartBoundaryNotFoundDefect))
1951
1952 def test_first_line_is_continuation_header(self):
1953 eq = self.assertEqual
1954 m = ' Line 1\nLine 2\nLine 3'
1955 msg = email.message_from_string(m)
1956 eq(msg.keys(), [])
1957 eq(msg.get_payload(), 'Line 2\nLine 3')
1958 eq(len(msg.defects), 1)
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00001959 self.assertTrue(isinstance(msg.defects[0],
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001960 errors.FirstHeaderLineIsContinuationDefect))
1961 eq(msg.defects[0].line, ' Line 1\n')
1962
1963
Ezio Melottib3aedd42010-11-20 19:04:17 +00001964
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001965# Test RFC 2047 header encoding and decoding
Guido van Rossum9604e662007-08-30 03:46:43 +00001966class TestRFC2047(TestEmailBase):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001967 def test_rfc2047_multiline(self):
1968 eq = self.assertEqual
1969 s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz
1970 foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?="""
1971 dh = decode_header(s)
1972 eq(dh, [
1973 (b'Re:', None),
1974 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland'),
1975 (b'baz foo bar', None),
1976 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland')])
1977 header = make_header(dh)
1978 eq(str(header),
1979 'Re: r\xe4ksm\xf6rg\xe5s baz foo bar r\xe4ksm\xf6rg\xe5s')
Barry Warsaw00b34222007-08-31 02:35:00 +00001980 self.ndiffAssertEqual(header.encode(maxlinelen=76), """\
Guido van Rossum9604e662007-08-30 03:46:43 +00001981Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar =?mac-iceland?q?r=8Aksm?=
1982 =?mac-iceland?q?=9Arg=8Cs?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001983
1984 def test_whitespace_eater_unicode(self):
1985 eq = self.assertEqual
1986 s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard@dom.ain>'
1987 dh = decode_header(s)
1988 eq(dh, [(b'Andr\xe9', 'iso-8859-1'),
1989 (b'Pirard <pirard@dom.ain>', None)])
1990 header = str(make_header(dh))
1991 eq(header, 'Andr\xe9 Pirard <pirard@dom.ain>')
1992
1993 def test_whitespace_eater_unicode_2(self):
1994 eq = self.assertEqual
1995 s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?='
1996 dh = decode_header(s)
1997 eq(dh, [(b'The', None), (b'quick brown fox', 'iso-8859-1'),
1998 (b'jumped over the', None), (b'lazy dog', 'iso-8859-1')])
1999 hu = str(make_header(dh))
2000 eq(hu, 'The quick brown fox jumped over the lazy dog')
2001
2002 def test_rfc2047_missing_whitespace(self):
2003 s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord'
2004 dh = decode_header(s)
2005 self.assertEqual(dh, [(s, None)])
2006
2007 def test_rfc2047_with_whitespace(self):
2008 s = 'Sm =?ISO-8859-1?B?9g==?= rg =?ISO-8859-1?B?5Q==?= sbord'
2009 dh = decode_header(s)
2010 self.assertEqual(dh, [(b'Sm', None), (b'\xf6', 'iso-8859-1'),
2011 (b'rg', None), (b'\xe5', 'iso-8859-1'),
2012 (b'sbord', None)])
2013
R. David Murrayc4e69cc2010-08-03 22:14:10 +00002014 def test_rfc2047_B_bad_padding(self):
2015 s = '=?iso-8859-1?B?%s?='
2016 data = [ # only test complete bytes
2017 ('dm==', b'v'), ('dm=', b'v'), ('dm', b'v'),
2018 ('dmk=', b'vi'), ('dmk', b'vi')
2019 ]
2020 for q, a in data:
2021 dh = decode_header(s % q)
2022 self.assertEqual(dh, [(a, 'iso-8859-1')])
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002023
R. David Murray31e984c2010-10-01 15:40:20 +00002024 def test_rfc2047_Q_invalid_digits(self):
2025 # issue 10004.
2026 s = '=?iso-8659-1?Q?andr=e9=zz?='
2027 self.assertEqual(decode_header(s),
2028 [(b'andr\xe9=zz', 'iso-8659-1')])
2029
Ezio Melottib3aedd42010-11-20 19:04:17 +00002030
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002031# Test the MIMEMessage class
2032class TestMIMEMessage(TestEmailBase):
2033 def setUp(self):
2034 with openfile('msg_11.txt') as fp:
2035 self._text = fp.read()
2036
2037 def test_type_error(self):
2038 self.assertRaises(TypeError, MIMEMessage, 'a plain string')
2039
2040 def test_valid_argument(self):
2041 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002042 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002043 subject = 'A sub-message'
2044 m = Message()
2045 m['Subject'] = subject
2046 r = MIMEMessage(m)
2047 eq(r.get_content_type(), 'message/rfc822')
2048 payload = r.get_payload()
2049 unless(isinstance(payload, list))
2050 eq(len(payload), 1)
2051 subpart = payload[0]
2052 unless(subpart is m)
2053 eq(subpart['subject'], subject)
2054
2055 def test_bad_multipart(self):
2056 eq = self.assertEqual
2057 msg1 = Message()
2058 msg1['Subject'] = 'subpart 1'
2059 msg2 = Message()
2060 msg2['Subject'] = 'subpart 2'
2061 r = MIMEMessage(msg1)
2062 self.assertRaises(errors.MultipartConversionError, r.attach, msg2)
2063
2064 def test_generate(self):
2065 # First craft the message to be encapsulated
2066 m = Message()
2067 m['Subject'] = 'An enclosed message'
2068 m.set_payload('Here is the body of the message.\n')
2069 r = MIMEMessage(m)
2070 r['Subject'] = 'The enclosing message'
2071 s = StringIO()
2072 g = Generator(s)
2073 g.flatten(r)
2074 self.assertEqual(s.getvalue(), """\
2075Content-Type: message/rfc822
2076MIME-Version: 1.0
2077Subject: The enclosing message
2078
2079Subject: An enclosed message
2080
2081Here is the body of the message.
2082""")
2083
2084 def test_parse_message_rfc822(self):
2085 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002086 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002087 msg = self._msgobj('msg_11.txt')
2088 eq(msg.get_content_type(), 'message/rfc822')
2089 payload = msg.get_payload()
2090 unless(isinstance(payload, list))
2091 eq(len(payload), 1)
2092 submsg = payload[0]
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002093 self.assertTrue(isinstance(submsg, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002094 eq(submsg['subject'], 'An enclosed message')
2095 eq(submsg.get_payload(), 'Here is the body of the message.\n')
2096
2097 def test_dsn(self):
2098 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002099 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002100 # msg 16 is a Delivery Status Notification, see RFC 1894
2101 msg = self._msgobj('msg_16.txt')
2102 eq(msg.get_content_type(), 'multipart/report')
2103 unless(msg.is_multipart())
2104 eq(len(msg.get_payload()), 3)
2105 # Subpart 1 is a text/plain, human readable section
2106 subpart = msg.get_payload(0)
2107 eq(subpart.get_content_type(), 'text/plain')
2108 eq(subpart.get_payload(), """\
2109This report relates to a message you sent with the following header fields:
2110
2111 Message-id: <002001c144a6$8752e060$56104586@oxy.edu>
2112 Date: Sun, 23 Sep 2001 20:10:55 -0700
2113 From: "Ian T. Henry" <henryi@oxy.edu>
2114 To: SoCal Raves <scr@socal-raves.org>
2115 Subject: [scr] yeah for Ians!!
2116
2117Your message cannot be delivered to the following recipients:
2118
2119 Recipient address: jangel1@cougar.noc.ucla.edu
2120 Reason: recipient reached disk quota
2121
2122""")
2123 # Subpart 2 contains the machine parsable DSN information. It
2124 # consists of two blocks of headers, represented by two nested Message
2125 # objects.
2126 subpart = msg.get_payload(1)
2127 eq(subpart.get_content_type(), 'message/delivery-status')
2128 eq(len(subpart.get_payload()), 2)
2129 # message/delivery-status should treat each block as a bunch of
2130 # headers, i.e. a bunch of Message objects.
2131 dsn1 = subpart.get_payload(0)
2132 unless(isinstance(dsn1, Message))
2133 eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu')
2134 eq(dsn1.get_param('dns', header='reporting-mta'), '')
2135 # Try a missing one <wink>
2136 eq(dsn1.get_param('nsd', header='reporting-mta'), None)
2137 dsn2 = subpart.get_payload(1)
2138 unless(isinstance(dsn2, Message))
2139 eq(dsn2['action'], 'failed')
2140 eq(dsn2.get_params(header='original-recipient'),
2141 [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')])
2142 eq(dsn2.get_param('rfc822', header='final-recipient'), '')
2143 # Subpart 3 is the original message
2144 subpart = msg.get_payload(2)
2145 eq(subpart.get_content_type(), 'message/rfc822')
2146 payload = subpart.get_payload()
2147 unless(isinstance(payload, list))
2148 eq(len(payload), 1)
2149 subsubpart = payload[0]
2150 unless(isinstance(subsubpart, Message))
2151 eq(subsubpart.get_content_type(), 'text/plain')
2152 eq(subsubpart['message-id'],
2153 '<002001c144a6$8752e060$56104586@oxy.edu>')
2154
2155 def test_epilogue(self):
2156 eq = self.ndiffAssertEqual
2157 with openfile('msg_21.txt') as fp:
2158 text = fp.read()
2159 msg = Message()
2160 msg['From'] = 'aperson@dom.ain'
2161 msg['To'] = 'bperson@dom.ain'
2162 msg['Subject'] = 'Test'
2163 msg.preamble = 'MIME message'
2164 msg.epilogue = 'End of MIME message\n'
2165 msg1 = MIMEText('One')
2166 msg2 = MIMEText('Two')
2167 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
2168 msg.attach(msg1)
2169 msg.attach(msg2)
2170 sfp = StringIO()
2171 g = Generator(sfp)
2172 g.flatten(msg)
2173 eq(sfp.getvalue(), text)
2174
2175 def test_no_nl_preamble(self):
2176 eq = self.ndiffAssertEqual
2177 msg = Message()
2178 msg['From'] = 'aperson@dom.ain'
2179 msg['To'] = 'bperson@dom.ain'
2180 msg['Subject'] = 'Test'
2181 msg.preamble = 'MIME message'
2182 msg.epilogue = ''
2183 msg1 = MIMEText('One')
2184 msg2 = MIMEText('Two')
2185 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
2186 msg.attach(msg1)
2187 msg.attach(msg2)
2188 eq(msg.as_string(), """\
2189From: aperson@dom.ain
2190To: bperson@dom.ain
2191Subject: Test
2192Content-Type: multipart/mixed; boundary="BOUNDARY"
2193
2194MIME message
2195--BOUNDARY
2196Content-Type: text/plain; charset="us-ascii"
2197MIME-Version: 1.0
2198Content-Transfer-Encoding: 7bit
2199
2200One
2201--BOUNDARY
2202Content-Type: text/plain; charset="us-ascii"
2203MIME-Version: 1.0
2204Content-Transfer-Encoding: 7bit
2205
2206Two
2207--BOUNDARY--
2208""")
2209
2210 def test_default_type(self):
2211 eq = self.assertEqual
2212 with openfile('msg_30.txt') as fp:
2213 msg = email.message_from_file(fp)
2214 container1 = msg.get_payload(0)
2215 eq(container1.get_default_type(), 'message/rfc822')
2216 eq(container1.get_content_type(), 'message/rfc822')
2217 container2 = msg.get_payload(1)
2218 eq(container2.get_default_type(), 'message/rfc822')
2219 eq(container2.get_content_type(), 'message/rfc822')
2220 container1a = container1.get_payload(0)
2221 eq(container1a.get_default_type(), 'text/plain')
2222 eq(container1a.get_content_type(), 'text/plain')
2223 container2a = container2.get_payload(0)
2224 eq(container2a.get_default_type(), 'text/plain')
2225 eq(container2a.get_content_type(), 'text/plain')
2226
2227 def test_default_type_with_explicit_container_type(self):
2228 eq = self.assertEqual
2229 with openfile('msg_28.txt') as fp:
2230 msg = email.message_from_file(fp)
2231 container1 = msg.get_payload(0)
2232 eq(container1.get_default_type(), 'message/rfc822')
2233 eq(container1.get_content_type(), 'message/rfc822')
2234 container2 = msg.get_payload(1)
2235 eq(container2.get_default_type(), 'message/rfc822')
2236 eq(container2.get_content_type(), 'message/rfc822')
2237 container1a = container1.get_payload(0)
2238 eq(container1a.get_default_type(), 'text/plain')
2239 eq(container1a.get_content_type(), 'text/plain')
2240 container2a = container2.get_payload(0)
2241 eq(container2a.get_default_type(), 'text/plain')
2242 eq(container2a.get_content_type(), 'text/plain')
2243
2244 def test_default_type_non_parsed(self):
2245 eq = self.assertEqual
2246 neq = self.ndiffAssertEqual
2247 # Set up container
2248 container = MIMEMultipart('digest', 'BOUNDARY')
2249 container.epilogue = ''
2250 # Set up subparts
2251 subpart1a = MIMEText('message 1\n')
2252 subpart2a = MIMEText('message 2\n')
2253 subpart1 = MIMEMessage(subpart1a)
2254 subpart2 = MIMEMessage(subpart2a)
2255 container.attach(subpart1)
2256 container.attach(subpart2)
2257 eq(subpart1.get_content_type(), 'message/rfc822')
2258 eq(subpart1.get_default_type(), 'message/rfc822')
2259 eq(subpart2.get_content_type(), 'message/rfc822')
2260 eq(subpart2.get_default_type(), 'message/rfc822')
2261 neq(container.as_string(0), '''\
2262Content-Type: multipart/digest; boundary="BOUNDARY"
2263MIME-Version: 1.0
2264
2265--BOUNDARY
2266Content-Type: message/rfc822
2267MIME-Version: 1.0
2268
2269Content-Type: text/plain; charset="us-ascii"
2270MIME-Version: 1.0
2271Content-Transfer-Encoding: 7bit
2272
2273message 1
2274
2275--BOUNDARY
2276Content-Type: message/rfc822
2277MIME-Version: 1.0
2278
2279Content-Type: text/plain; charset="us-ascii"
2280MIME-Version: 1.0
2281Content-Transfer-Encoding: 7bit
2282
2283message 2
2284
2285--BOUNDARY--
2286''')
2287 del subpart1['content-type']
2288 del subpart1['mime-version']
2289 del subpart2['content-type']
2290 del subpart2['mime-version']
2291 eq(subpart1.get_content_type(), 'message/rfc822')
2292 eq(subpart1.get_default_type(), 'message/rfc822')
2293 eq(subpart2.get_content_type(), 'message/rfc822')
2294 eq(subpart2.get_default_type(), 'message/rfc822')
2295 neq(container.as_string(0), '''\
2296Content-Type: multipart/digest; boundary="BOUNDARY"
2297MIME-Version: 1.0
2298
2299--BOUNDARY
2300
2301Content-Type: text/plain; charset="us-ascii"
2302MIME-Version: 1.0
2303Content-Transfer-Encoding: 7bit
2304
2305message 1
2306
2307--BOUNDARY
2308
2309Content-Type: text/plain; charset="us-ascii"
2310MIME-Version: 1.0
2311Content-Transfer-Encoding: 7bit
2312
2313message 2
2314
2315--BOUNDARY--
2316''')
2317
2318 def test_mime_attachments_in_constructor(self):
2319 eq = self.assertEqual
2320 text1 = MIMEText('')
2321 text2 = MIMEText('')
2322 msg = MIMEMultipart(_subparts=(text1, text2))
2323 eq(len(msg.get_payload()), 2)
2324 eq(msg.get_payload(0), text1)
2325 eq(msg.get_payload(1), text2)
2326
Christian Heimes587c2bf2008-01-19 16:21:02 +00002327 def test_default_multipart_constructor(self):
2328 msg = MIMEMultipart()
2329 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002330
Ezio Melottib3aedd42010-11-20 19:04:17 +00002331
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002332# A general test of parser->model->generator idempotency. IOW, read a message
2333# in, parse it into a message object tree, then without touching the tree,
2334# regenerate the plain text. The original text and the transformed text
2335# should be identical. Note: that we ignore the Unix-From since that may
2336# contain a changed date.
2337class TestIdempotent(TestEmailBase):
R. David Murray719a4492010-11-21 16:53:48 +00002338
2339 linesep = '\n'
2340
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002341 def _msgobj(self, filename):
2342 with openfile(filename) as fp:
2343 data = fp.read()
2344 msg = email.message_from_string(data)
2345 return msg, data
2346
R. David Murray719a4492010-11-21 16:53:48 +00002347 def _idempotent(self, msg, text, unixfrom=False):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002348 eq = self.ndiffAssertEqual
2349 s = StringIO()
2350 g = Generator(s, maxheaderlen=0)
R. David Murray719a4492010-11-21 16:53:48 +00002351 g.flatten(msg, unixfrom=unixfrom)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002352 eq(text, s.getvalue())
2353
2354 def test_parse_text_message(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002355 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002356 msg, text = self._msgobj('msg_01.txt')
2357 eq(msg.get_content_type(), 'text/plain')
2358 eq(msg.get_content_maintype(), 'text')
2359 eq(msg.get_content_subtype(), 'plain')
2360 eq(msg.get_params()[1], ('charset', 'us-ascii'))
2361 eq(msg.get_param('charset'), 'us-ascii')
2362 eq(msg.preamble, None)
2363 eq(msg.epilogue, None)
2364 self._idempotent(msg, text)
2365
2366 def test_parse_untyped_message(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002367 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002368 msg, text = self._msgobj('msg_03.txt')
2369 eq(msg.get_content_type(), 'text/plain')
2370 eq(msg.get_params(), None)
2371 eq(msg.get_param('charset'), None)
2372 self._idempotent(msg, text)
2373
2374 def test_simple_multipart(self):
2375 msg, text = self._msgobj('msg_04.txt')
2376 self._idempotent(msg, text)
2377
2378 def test_MIME_digest(self):
2379 msg, text = self._msgobj('msg_02.txt')
2380 self._idempotent(msg, text)
2381
2382 def test_long_header(self):
2383 msg, text = self._msgobj('msg_27.txt')
2384 self._idempotent(msg, text)
2385
2386 def test_MIME_digest_with_part_headers(self):
2387 msg, text = self._msgobj('msg_28.txt')
2388 self._idempotent(msg, text)
2389
2390 def test_mixed_with_image(self):
2391 msg, text = self._msgobj('msg_06.txt')
2392 self._idempotent(msg, text)
2393
2394 def test_multipart_report(self):
2395 msg, text = self._msgobj('msg_05.txt')
2396 self._idempotent(msg, text)
2397
2398 def test_dsn(self):
2399 msg, text = self._msgobj('msg_16.txt')
2400 self._idempotent(msg, text)
2401
2402 def test_preamble_epilogue(self):
2403 msg, text = self._msgobj('msg_21.txt')
2404 self._idempotent(msg, text)
2405
2406 def test_multipart_one_part(self):
2407 msg, text = self._msgobj('msg_23.txt')
2408 self._idempotent(msg, text)
2409
2410 def test_multipart_no_parts(self):
2411 msg, text = self._msgobj('msg_24.txt')
2412 self._idempotent(msg, text)
2413
2414 def test_no_start_boundary(self):
2415 msg, text = self._msgobj('msg_31.txt')
2416 self._idempotent(msg, text)
2417
2418 def test_rfc2231_charset(self):
2419 msg, text = self._msgobj('msg_32.txt')
2420 self._idempotent(msg, text)
2421
2422 def test_more_rfc2231_parameters(self):
2423 msg, text = self._msgobj('msg_33.txt')
2424 self._idempotent(msg, text)
2425
2426 def test_text_plain_in_a_multipart_digest(self):
2427 msg, text = self._msgobj('msg_34.txt')
2428 self._idempotent(msg, text)
2429
2430 def test_nested_multipart_mixeds(self):
2431 msg, text = self._msgobj('msg_12a.txt')
2432 self._idempotent(msg, text)
2433
2434 def test_message_external_body_idempotent(self):
2435 msg, text = self._msgobj('msg_36.txt')
2436 self._idempotent(msg, text)
2437
R. David Murray719a4492010-11-21 16:53:48 +00002438 def test_message_delivery_status(self):
2439 msg, text = self._msgobj('msg_43.txt')
2440 self._idempotent(msg, text, unixfrom=True)
2441
R. David Murray96fd54e2010-10-08 15:55:28 +00002442 def test_message_signed_idempotent(self):
2443 msg, text = self._msgobj('msg_45.txt')
2444 self._idempotent(msg, text)
2445
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002446 def test_content_type(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002447 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002448 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002449 # Get a message object and reset the seek pointer for other tests
2450 msg, text = self._msgobj('msg_05.txt')
2451 eq(msg.get_content_type(), 'multipart/report')
2452 # Test the Content-Type: parameters
2453 params = {}
2454 for pk, pv in msg.get_params():
2455 params[pk] = pv
2456 eq(params['report-type'], 'delivery-status')
2457 eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
R. David Murray719a4492010-11-21 16:53:48 +00002458 eq(msg.preamble, 'This is a MIME-encapsulated message.' + self.linesep)
2459 eq(msg.epilogue, self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002460 eq(len(msg.get_payload()), 3)
2461 # Make sure the subparts are what we expect
2462 msg1 = msg.get_payload(0)
2463 eq(msg1.get_content_type(), 'text/plain')
R. David Murray719a4492010-11-21 16:53:48 +00002464 eq(msg1.get_payload(), 'Yadda yadda yadda' + self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002465 msg2 = msg.get_payload(1)
2466 eq(msg2.get_content_type(), 'text/plain')
R. David Murray719a4492010-11-21 16:53:48 +00002467 eq(msg2.get_payload(), 'Yadda yadda yadda' + self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002468 msg3 = msg.get_payload(2)
2469 eq(msg3.get_content_type(), 'message/rfc822')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002470 self.assertTrue(isinstance(msg3, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002471 payload = msg3.get_payload()
2472 unless(isinstance(payload, list))
2473 eq(len(payload), 1)
2474 msg4 = payload[0]
2475 unless(isinstance(msg4, Message))
R. David Murray719a4492010-11-21 16:53:48 +00002476 eq(msg4.get_payload(), 'Yadda yadda yadda' + self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002477
2478 def test_parser(self):
Ezio Melottib3aedd42010-11-20 19:04:17 +00002479 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002480 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002481 msg, text = self._msgobj('msg_06.txt')
2482 # Check some of the outer headers
2483 eq(msg.get_content_type(), 'message/rfc822')
2484 # Make sure the payload is a list of exactly one sub-Message, and that
2485 # that submessage has a type of text/plain
2486 payload = msg.get_payload()
2487 unless(isinstance(payload, list))
2488 eq(len(payload), 1)
2489 msg1 = payload[0]
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002490 self.assertTrue(isinstance(msg1, Message))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002491 eq(msg1.get_content_type(), 'text/plain')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002492 self.assertTrue(isinstance(msg1.get_payload(), str))
R. David Murray719a4492010-11-21 16:53:48 +00002493 eq(msg1.get_payload(), self.linesep)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002494
2495
Ezio Melottib3aedd42010-11-20 19:04:17 +00002496
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002497# Test various other bits of the package's functionality
2498class TestMiscellaneous(TestEmailBase):
2499 def test_message_from_string(self):
2500 with openfile('msg_01.txt') as fp:
2501 text = fp.read()
2502 msg = email.message_from_string(text)
2503 s = StringIO()
2504 # Don't wrap/continue long headers since we're trying to test
2505 # idempotency.
2506 g = Generator(s, maxheaderlen=0)
2507 g.flatten(msg)
2508 self.assertEqual(text, s.getvalue())
2509
2510 def test_message_from_file(self):
2511 with openfile('msg_01.txt') as fp:
2512 text = fp.read()
2513 fp.seek(0)
2514 msg = email.message_from_file(fp)
2515 s = StringIO()
2516 # Don't wrap/continue long headers since we're trying to test
2517 # idempotency.
2518 g = Generator(s, maxheaderlen=0)
2519 g.flatten(msg)
2520 self.assertEqual(text, s.getvalue())
2521
2522 def test_message_from_string_with_class(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002523 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002524 with openfile('msg_01.txt') as fp:
2525 text = fp.read()
2526
2527 # Create a subclass
2528 class MyMessage(Message):
2529 pass
2530
2531 msg = email.message_from_string(text, MyMessage)
2532 unless(isinstance(msg, MyMessage))
2533 # Try something more complicated
2534 with openfile('msg_02.txt') as fp:
2535 text = fp.read()
2536 msg = email.message_from_string(text, MyMessage)
2537 for subpart in msg.walk():
2538 unless(isinstance(subpart, MyMessage))
2539
2540 def test_message_from_file_with_class(self):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002541 unless = self.assertTrue
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002542 # Create a subclass
2543 class MyMessage(Message):
2544 pass
2545
2546 with openfile('msg_01.txt') as fp:
2547 msg = email.message_from_file(fp, MyMessage)
2548 unless(isinstance(msg, MyMessage))
2549 # Try something more complicated
2550 with openfile('msg_02.txt') as fp:
2551 msg = email.message_from_file(fp, MyMessage)
2552 for subpart in msg.walk():
2553 unless(isinstance(subpart, MyMessage))
2554
2555 def test__all__(self):
2556 module = __import__('email')
R David Murray1b6c7242012-03-16 22:43:05 -04002557 self.assertEqual(sorted(module.__all__), [
2558 'base64mime', 'charset', 'encoders', 'errors', 'feedparser',
2559 'generator', 'header', 'iterators', 'message',
2560 'message_from_binary_file', 'message_from_bytes',
2561 'message_from_file', 'message_from_string', 'mime', 'parser',
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002562 'quoprimime', 'utils',
2563 ])
2564
2565 def test_formatdate(self):
2566 now = time.time()
2567 self.assertEqual(utils.parsedate(utils.formatdate(now))[:6],
2568 time.gmtime(now)[:6])
2569
2570 def test_formatdate_localtime(self):
2571 now = time.time()
2572 self.assertEqual(
2573 utils.parsedate(utils.formatdate(now, localtime=True))[:6],
2574 time.localtime(now)[:6])
2575
2576 def test_formatdate_usegmt(self):
2577 now = time.time()
2578 self.assertEqual(
2579 utils.formatdate(now, localtime=False),
2580 time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now)))
2581 self.assertEqual(
2582 utils.formatdate(now, localtime=False, usegmt=True),
2583 time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now)))
2584
2585 def test_parsedate_none(self):
2586 self.assertEqual(utils.parsedate(''), None)
2587
2588 def test_parsedate_compact(self):
2589 # The FWS after the comma is optional
2590 self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'),
2591 utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800'))
2592
2593 def test_parsedate_no_dayofweek(self):
2594 eq = self.assertEqual
2595 eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'),
2596 (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800))
2597
2598 def test_parsedate_compact_no_dayofweek(self):
2599 eq = self.assertEqual
2600 eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'),
2601 (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800))
2602
R. David Murray4a62e892010-12-23 20:35:46 +00002603 def test_parsedate_no_space_before_positive_offset(self):
2604 self.assertEqual(utils.parsedate_tz('Wed, 3 Apr 2002 14:58:26+0800'),
2605 (2002, 4, 3, 14, 58, 26, 0, 1, -1, 28800))
2606
2607 def test_parsedate_no_space_before_negative_offset(self):
2608 # Issue 1155362: we already handled '+' for this case.
2609 self.assertEqual(utils.parsedate_tz('Wed, 3 Apr 2002 14:58:26-0800'),
2610 (2002, 4, 3, 14, 58, 26, 0, 1, -1, -28800))
2611
2612
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002613 def test_parsedate_acceptable_to_time_functions(self):
2614 eq = self.assertEqual
2615 timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800')
2616 t = int(time.mktime(timetup))
2617 eq(time.localtime(t)[:6], timetup[:6])
2618 eq(int(time.strftime('%Y', timetup)), 2003)
2619 timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800')
2620 t = int(time.mktime(timetup[:9]))
2621 eq(time.localtime(t)[:6], timetup[:6])
2622 eq(int(time.strftime('%Y', timetup[:9])), 2003)
2623
Alexander Belopolskya07548e2012-06-21 20:34:09 -04002624 def test_mktime_tz(self):
2625 self.assertEqual(utils.mktime_tz((1970, 1, 1, 0, 0, 0,
2626 -1, -1, -1, 0)), 0)
2627 self.assertEqual(utils.mktime_tz((1970, 1, 1, 0, 0, 0,
2628 -1, -1, -1, 1234)), -1234)
2629
R. David Murray219d1c82010-08-25 00:45:55 +00002630 def test_parsedate_y2k(self):
2631 """Test for parsing a date with a two-digit year.
2632
2633 Parsing a date with a two-digit year should return the correct
2634 four-digit year. RFC822 allows two-digit years, but RFC2822 (which
2635 obsoletes RFC822) requires four-digit years.
2636
2637 """
2638 self.assertEqual(utils.parsedate_tz('25 Feb 03 13:47:26 -0800'),
2639 utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'))
2640 self.assertEqual(utils.parsedate_tz('25 Feb 71 13:47:26 -0800'),
2641 utils.parsedate_tz('25 Feb 1971 13:47:26 -0800'))
2642
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002643 def test_parseaddr_empty(self):
2644 self.assertEqual(utils.parseaddr('<>'), ('', ''))
2645 self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '')
2646
2647 def test_noquote_dump(self):
2648 self.assertEqual(
2649 utils.formataddr(('A Silly Person', 'person@dom.ain')),
2650 'A Silly Person <person@dom.ain>')
2651
2652 def test_escape_dump(self):
2653 self.assertEqual(
2654 utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')),
2655 r'"A \(Very\) Silly Person" <person@dom.ain>')
2656 a = r'A \(Special\) Person'
2657 b = 'person@dom.ain'
2658 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2659
2660 def test_escape_backslashes(self):
2661 self.assertEqual(
2662 utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')),
2663 r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>')
2664 a = r'Arthur \Backslash\ Foobar'
2665 b = 'person@dom.ain'
2666 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2667
2668 def test_name_with_dot(self):
2669 x = 'John X. Doe <jxd@example.com>'
2670 y = '"John X. Doe" <jxd@example.com>'
2671 a, b = ('John X. Doe', 'jxd@example.com')
2672 self.assertEqual(utils.parseaddr(x), (a, b))
2673 self.assertEqual(utils.parseaddr(y), (a, b))
2674 # formataddr() quotes the name if there's a dot in it
2675 self.assertEqual(utils.formataddr((a, b)), y)
2676
R. David Murray5397e862010-10-02 15:58:26 +00002677 def test_parseaddr_preserves_quoted_pairs_in_addresses(self):
2678 # issue 10005. Note that in the third test the second pair of
2679 # backslashes is not actually a quoted pair because it is not inside a
2680 # comment or quoted string: the address being parsed has a quoted
2681 # string containing a quoted backslash, followed by 'example' and two
2682 # backslashes, followed by another quoted string containing a space and
2683 # the word 'example'. parseaddr copies those two backslashes
2684 # literally. Per rfc5322 this is not technically correct since a \ may
2685 # not appear in an address outside of a quoted string. It is probably
2686 # a sensible Postel interpretation, though.
2687 eq = self.assertEqual
2688 eq(utils.parseaddr('""example" example"@example.com'),
2689 ('', '""example" example"@example.com'))
2690 eq(utils.parseaddr('"\\"example\\" example"@example.com'),
2691 ('', '"\\"example\\" example"@example.com'))
2692 eq(utils.parseaddr('"\\\\"example\\\\" example"@example.com'),
2693 ('', '"\\\\"example\\\\" example"@example.com'))
2694
R. David Murray63563cd2010-12-18 18:25:38 +00002695 def test_parseaddr_preserves_spaces_in_local_part(self):
2696 # issue 9286. A normal RFC5322 local part should not contain any
2697 # folding white space, but legacy local parts can (they are a sequence
2698 # of atoms, not dotatoms). On the other hand we strip whitespace from
2699 # before the @ and around dots, on the assumption that the whitespace
2700 # around the punctuation is a mistake in what would otherwise be
2701 # an RFC5322 local part. Leading whitespace is, usual, stripped as well.
2702 self.assertEqual(('', "merwok wok@xample.com"),
2703 utils.parseaddr("merwok wok@xample.com"))
2704 self.assertEqual(('', "merwok wok@xample.com"),
2705 utils.parseaddr("merwok wok@xample.com"))
2706 self.assertEqual(('', "merwok wok@xample.com"),
2707 utils.parseaddr(" merwok wok @xample.com"))
2708 self.assertEqual(('', 'merwok"wok" wok@xample.com'),
2709 utils.parseaddr('merwok"wok" wok@xample.com'))
2710 self.assertEqual(('', 'merwok.wok.wok@xample.com'),
2711 utils.parseaddr('merwok. wok . wok@xample.com'))
2712
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002713 def test_multiline_from_comment(self):
2714 x = """\
2715Foo
2716\tBar <foo@example.com>"""
2717 self.assertEqual(utils.parseaddr(x), ('Foo Bar', 'foo@example.com'))
2718
2719 def test_quote_dump(self):
2720 self.assertEqual(
2721 utils.formataddr(('A Silly; Person', 'person@dom.ain')),
2722 r'"A Silly; Person" <person@dom.ain>')
2723
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002724 def test_charset_richcomparisons(self):
2725 eq = self.assertEqual
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002726 ne = self.assertNotEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002727 cset1 = Charset()
2728 cset2 = Charset()
2729 eq(cset1, 'us-ascii')
2730 eq(cset1, 'US-ASCII')
2731 eq(cset1, 'Us-AsCiI')
2732 eq('us-ascii', cset1)
2733 eq('US-ASCII', cset1)
2734 eq('Us-AsCiI', cset1)
2735 ne(cset1, 'usascii')
2736 ne(cset1, 'USASCII')
2737 ne(cset1, 'UsAsCiI')
2738 ne('usascii', cset1)
2739 ne('USASCII', cset1)
2740 ne('UsAsCiI', cset1)
2741 eq(cset1, cset2)
2742 eq(cset2, cset1)
2743
2744 def test_getaddresses(self):
2745 eq = self.assertEqual
2746 eq(utils.getaddresses(['aperson@dom.ain (Al Person)',
2747 'Bud Person <bperson@dom.ain>']),
2748 [('Al Person', 'aperson@dom.ain'),
2749 ('Bud Person', 'bperson@dom.ain')])
2750
2751 def test_getaddresses_nasty(self):
2752 eq = self.assertEqual
2753 eq(utils.getaddresses(['foo: ;']), [('', '')])
2754 eq(utils.getaddresses(
2755 ['[]*-- =~$']),
2756 [('', ''), ('', ''), ('', '*--')])
2757 eq(utils.getaddresses(
2758 ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
2759 [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
2760
2761 def test_getaddresses_embedded_comment(self):
2762 """Test proper handling of a nested comment"""
2763 eq = self.assertEqual
2764 addrs = utils.getaddresses(['User ((nested comment)) <foo@bar.com>'])
2765 eq(addrs[0][1], 'foo@bar.com')
2766
2767 def test_utils_quote_unquote(self):
2768 eq = self.assertEqual
2769 msg = Message()
2770 msg.add_header('content-disposition', 'attachment',
2771 filename='foo\\wacky"name')
2772 eq(msg.get_filename(), 'foo\\wacky"name')
2773
2774 def test_get_body_encoding_with_bogus_charset(self):
2775 charset = Charset('not a charset')
2776 self.assertEqual(charset.get_body_encoding(), 'base64')
2777
2778 def test_get_body_encoding_with_uppercase_charset(self):
2779 eq = self.assertEqual
2780 msg = Message()
2781 msg['Content-Type'] = 'text/plain; charset=UTF-8'
2782 eq(msg['content-type'], 'text/plain; charset=UTF-8')
2783 charsets = msg.get_charsets()
2784 eq(len(charsets), 1)
2785 eq(charsets[0], 'utf-8')
2786 charset = Charset(charsets[0])
2787 eq(charset.get_body_encoding(), 'base64')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002788 msg.set_payload(b'hello world', charset=charset)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002789 eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n')
2790 eq(msg.get_payload(decode=True), b'hello world')
2791 eq(msg['content-transfer-encoding'], 'base64')
2792 # Try another one
2793 msg = Message()
2794 msg['Content-Type'] = 'text/plain; charset="US-ASCII"'
2795 charsets = msg.get_charsets()
2796 eq(len(charsets), 1)
2797 eq(charsets[0], 'us-ascii')
2798 charset = Charset(charsets[0])
2799 eq(charset.get_body_encoding(), encoders.encode_7or8bit)
2800 msg.set_payload('hello world', charset=charset)
2801 eq(msg.get_payload(), 'hello world')
2802 eq(msg['content-transfer-encoding'], '7bit')
2803
2804 def test_charsets_case_insensitive(self):
2805 lc = Charset('us-ascii')
2806 uc = Charset('US-ASCII')
2807 self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding())
2808
2809 def test_partial_falls_inside_message_delivery_status(self):
2810 eq = self.ndiffAssertEqual
2811 # The Parser interface provides chunks of data to FeedParser in 8192
2812 # byte gulps. SF bug #1076485 found one of those chunks inside
2813 # message/delivery-status header block, which triggered an
2814 # unreadline() of NeedMoreData.
2815 msg = self._msgobj('msg_43.txt')
2816 sfp = StringIO()
2817 iterators._structure(msg, sfp)
2818 eq(sfp.getvalue(), """\
2819multipart/report
2820 text/plain
2821 message/delivery-status
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/plain
2835 text/plain
2836 text/plain
2837 text/plain
2838 text/plain
2839 text/plain
2840 text/plain
2841 text/plain
2842 text/plain
2843 text/plain
2844 text/plain
2845 text/plain
2846 text/plain
2847 text/plain
2848 text/rfc822-headers
2849""")
2850
R. David Murraya0b44b52010-12-02 21:47:19 +00002851 def test_make_msgid_domain(self):
2852 self.assertEqual(
2853 email.utils.make_msgid(domain='testdomain-string')[-19:],
2854 '@testdomain-string>')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002855
Ezio Melottib3aedd42010-11-20 19:04:17 +00002856
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002857# Test the iterator/generators
2858class TestIterators(TestEmailBase):
2859 def test_body_line_iterator(self):
2860 eq = self.assertEqual
2861 neq = self.ndiffAssertEqual
2862 # First a simple non-multipart message
2863 msg = self._msgobj('msg_01.txt')
2864 it = iterators.body_line_iterator(msg)
2865 lines = list(it)
2866 eq(len(lines), 6)
2867 neq(EMPTYSTRING.join(lines), msg.get_payload())
2868 # Now a more complicated multipart
2869 msg = self._msgobj('msg_02.txt')
2870 it = iterators.body_line_iterator(msg)
2871 lines = list(it)
2872 eq(len(lines), 43)
2873 with openfile('msg_19.txt') as fp:
2874 neq(EMPTYSTRING.join(lines), fp.read())
2875
2876 def test_typed_subpart_iterator(self):
2877 eq = self.assertEqual
2878 msg = self._msgobj('msg_04.txt')
2879 it = iterators.typed_subpart_iterator(msg, 'text')
2880 lines = []
2881 subparts = 0
2882 for subpart in it:
2883 subparts += 1
2884 lines.append(subpart.get_payload())
2885 eq(subparts, 2)
2886 eq(EMPTYSTRING.join(lines), """\
2887a simple kind of mirror
2888to reflect upon our own
2889a simple kind of mirror
2890to reflect upon our own
2891""")
2892
2893 def test_typed_subpart_iterator_default_type(self):
2894 eq = self.assertEqual
2895 msg = self._msgobj('msg_03.txt')
2896 it = iterators.typed_subpart_iterator(msg, 'text', 'plain')
2897 lines = []
2898 subparts = 0
2899 for subpart in it:
2900 subparts += 1
2901 lines.append(subpart.get_payload())
2902 eq(subparts, 1)
2903 eq(EMPTYSTRING.join(lines), """\
2904
2905Hi,
2906
2907Do you like this message?
2908
2909-Me
2910""")
2911
R. David Murray45bf773f2010-07-17 01:19:57 +00002912 def test_pushCR_LF(self):
2913 '''FeedParser BufferedSubFile.push() assumed it received complete
2914 line endings. A CR ending one push() followed by a LF starting
2915 the next push() added an empty line.
2916 '''
2917 imt = [
2918 ("a\r \n", 2),
2919 ("b", 0),
2920 ("c\n", 1),
2921 ("", 0),
2922 ("d\r\n", 1),
2923 ("e\r", 0),
2924 ("\nf", 1),
2925 ("\r\n", 1),
2926 ]
2927 from email.feedparser import BufferedSubFile, NeedMoreData
2928 bsf = BufferedSubFile()
2929 om = []
2930 nt = 0
2931 for il, n in imt:
2932 bsf.push(il)
2933 nt += n
2934 n1 = 0
2935 while True:
2936 ol = bsf.readline()
2937 if ol == NeedMoreData:
2938 break
2939 om.append(ol)
2940 n1 += 1
2941 self.assertTrue(n == n1)
2942 self.assertTrue(len(om) == nt)
2943 self.assertTrue(''.join([il for il, n in imt]) == ''.join(om))
2944
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002945
Ezio Melottib3aedd42010-11-20 19:04:17 +00002946
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002947class TestParsers(TestEmailBase):
2948 def test_header_parser(self):
2949 eq = self.assertEqual
2950 # Parse only the headers of a complex multipart MIME document
2951 with openfile('msg_02.txt') as fp:
2952 msg = HeaderParser().parse(fp)
2953 eq(msg['from'], 'ppp-request@zzz.org')
2954 eq(msg['to'], 'ppp@zzz.org')
2955 eq(msg.get_content_type(), 'multipart/mixed')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00002956 self.assertFalse(msg.is_multipart())
2957 self.assertTrue(isinstance(msg.get_payload(), str))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002958
2959 def test_whitespace_continuation(self):
2960 eq = self.assertEqual
2961 # This message contains a line after the Subject: header that has only
2962 # whitespace, but it is not empty!
2963 msg = email.message_from_string("""\
2964From: aperson@dom.ain
2965To: bperson@dom.ain
2966Subject: the next line has a space on it
2967\x20
2968Date: Mon, 8 Apr 2002 15:09:19 -0400
2969Message-ID: spam
2970
2971Here's the message body
2972""")
2973 eq(msg['subject'], 'the next line has a space on it\n ')
2974 eq(msg['message-id'], 'spam')
2975 eq(msg.get_payload(), "Here's the message body\n")
2976
2977 def test_whitespace_continuation_last_header(self):
2978 eq = self.assertEqual
2979 # Like the previous test, but the subject line is the last
2980 # header.
2981 msg = email.message_from_string("""\
2982From: aperson@dom.ain
2983To: bperson@dom.ain
2984Date: Mon, 8 Apr 2002 15:09:19 -0400
2985Message-ID: spam
2986Subject: the next line has a space on it
2987\x20
2988
2989Here's the message body
2990""")
2991 eq(msg['subject'], 'the next line has a space on it\n ')
2992 eq(msg['message-id'], 'spam')
2993 eq(msg.get_payload(), "Here's the message body\n")
2994
2995 def test_crlf_separation(self):
2996 eq = self.assertEqual
Guido van Rossum98297ee2007-11-06 21:34:58 +00002997 with openfile('msg_26.txt', newline='\n') as fp:
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002998 msg = Parser().parse(fp)
2999 eq(len(msg.get_payload()), 2)
3000 part1 = msg.get_payload(0)
3001 eq(part1.get_content_type(), 'text/plain')
3002 eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n')
3003 part2 = msg.get_payload(1)
3004 eq(part2.get_content_type(), 'application/riscos')
3005
R. David Murray8451c4b2010-10-23 22:19:56 +00003006 def test_crlf_flatten(self):
3007 # Using newline='\n' preserves the crlfs in this input file.
3008 with openfile('msg_26.txt', newline='\n') as fp:
3009 text = fp.read()
3010 msg = email.message_from_string(text)
3011 s = StringIO()
3012 g = Generator(s)
3013 g.flatten(msg, linesep='\r\n')
3014 self.assertEqual(s.getvalue(), text)
3015
3016 maxDiff = None
3017
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003018 def test_multipart_digest_with_extra_mime_headers(self):
3019 eq = self.assertEqual
3020 neq = self.ndiffAssertEqual
3021 with openfile('msg_28.txt') as fp:
3022 msg = email.message_from_file(fp)
3023 # Structure is:
3024 # multipart/digest
3025 # message/rfc822
3026 # text/plain
3027 # message/rfc822
3028 # text/plain
3029 eq(msg.is_multipart(), 1)
3030 eq(len(msg.get_payload()), 2)
3031 part1 = msg.get_payload(0)
3032 eq(part1.get_content_type(), 'message/rfc822')
3033 eq(part1.is_multipart(), 1)
3034 eq(len(part1.get_payload()), 1)
3035 part1a = part1.get_payload(0)
3036 eq(part1a.is_multipart(), 0)
3037 eq(part1a.get_content_type(), 'text/plain')
3038 neq(part1a.get_payload(), 'message 1\n')
3039 # next message/rfc822
3040 part2 = msg.get_payload(1)
3041 eq(part2.get_content_type(), 'message/rfc822')
3042 eq(part2.is_multipart(), 1)
3043 eq(len(part2.get_payload()), 1)
3044 part2a = part2.get_payload(0)
3045 eq(part2a.is_multipart(), 0)
3046 eq(part2a.get_content_type(), 'text/plain')
3047 neq(part2a.get_payload(), 'message 2\n')
3048
3049 def test_three_lines(self):
3050 # A bug report by Andrew McNamara
3051 lines = ['From: Andrew Person <aperson@dom.ain',
3052 'Subject: Test',
3053 'Date: Tue, 20 Aug 2002 16:43:45 +1000']
3054 msg = email.message_from_string(NL.join(lines))
3055 self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000')
3056
3057 def test_strip_line_feed_and_carriage_return_in_headers(self):
3058 eq = self.assertEqual
3059 # For [ 1002475 ] email message parser doesn't handle \r\n correctly
3060 value1 = 'text'
3061 value2 = 'more text'
3062 m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % (
3063 value1, value2)
3064 msg = email.message_from_string(m)
3065 eq(msg.get('Header'), value1)
3066 eq(msg.get('Next-Header'), value2)
3067
3068 def test_rfc2822_header_syntax(self):
3069 eq = self.assertEqual
3070 m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
3071 msg = email.message_from_string(m)
3072 eq(len(msg), 3)
3073 eq(sorted(field for field in msg), ['!"#QUX;~', '>From', 'From'])
3074 eq(msg.get_payload(), 'body')
3075
3076 def test_rfc2822_space_not_allowed_in_header(self):
3077 eq = self.assertEqual
3078 m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
3079 msg = email.message_from_string(m)
3080 eq(len(msg.keys()), 0)
3081
3082 def test_rfc2822_one_character_header(self):
3083 eq = self.assertEqual
3084 m = 'A: first header\nB: second header\nCC: third header\n\nbody'
3085 msg = email.message_from_string(m)
3086 headers = msg.keys()
3087 headers.sort()
3088 eq(headers, ['A', 'B', 'CC'])
3089 eq(msg.get_payload(), 'body')
3090
R. David Murray45e0e142010-06-16 02:19:40 +00003091 def test_CRLFLF_at_end_of_part(self):
3092 # issue 5610: feedparser should not eat two chars from body part ending
3093 # with "\r\n\n".
3094 m = (
3095 "From: foo@bar.com\n"
3096 "To: baz\n"
3097 "Mime-Version: 1.0\n"
3098 "Content-Type: multipart/mixed; boundary=BOUNDARY\n"
3099 "\n"
3100 "--BOUNDARY\n"
3101 "Content-Type: text/plain\n"
3102 "\n"
3103 "body ending with CRLF newline\r\n"
3104 "\n"
3105 "--BOUNDARY--\n"
3106 )
3107 msg = email.message_from_string(m)
3108 self.assertTrue(msg.get_payload(0).get_payload().endswith('\r\n'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003109
Ezio Melottib3aedd42010-11-20 19:04:17 +00003110
R. David Murray96fd54e2010-10-08 15:55:28 +00003111class Test8BitBytesHandling(unittest.TestCase):
3112 # In Python3 all input is string, but that doesn't work if the actual input
3113 # uses an 8bit transfer encoding. To hack around that, in email 5.1 we
3114 # decode byte streams using the surrogateescape error handler, and
3115 # reconvert to binary at appropriate places if we detect surrogates. This
3116 # doesn't allow us to transform headers with 8bit bytes (they get munged),
3117 # but it does allow us to parse and preserve them, and to decode body
3118 # parts that use an 8bit CTE.
3119
3120 bodytest_msg = textwrap.dedent("""\
3121 From: foo@bar.com
3122 To: baz
3123 Mime-Version: 1.0
3124 Content-Type: text/plain; charset={charset}
3125 Content-Transfer-Encoding: {cte}
3126
3127 {bodyline}
3128 """)
3129
3130 def test_known_8bit_CTE(self):
3131 m = self.bodytest_msg.format(charset='utf-8',
3132 cte='8bit',
3133 bodyline='pöstal').encode('utf-8')
3134 msg = email.message_from_bytes(m)
3135 self.assertEqual(msg.get_payload(), "pöstal\n")
3136 self.assertEqual(msg.get_payload(decode=True),
3137 "pöstal\n".encode('utf-8'))
3138
3139 def test_unknown_8bit_CTE(self):
3140 m = self.bodytest_msg.format(charset='notavalidcharset',
3141 cte='8bit',
3142 bodyline='pöstal').encode('utf-8')
3143 msg = email.message_from_bytes(m)
R. David Murray92532142011-01-07 23:25:30 +00003144 self.assertEqual(msg.get_payload(), "p\uFFFD\uFFFDstal\n")
R. David Murray96fd54e2010-10-08 15:55:28 +00003145 self.assertEqual(msg.get_payload(decode=True),
3146 "pöstal\n".encode('utf-8'))
3147
3148 def test_8bit_in_quopri_body(self):
3149 # This is non-RFC compliant data...without 'decode' the library code
3150 # decodes the body using the charset from the headers, and because the
3151 # source byte really is utf-8 this works. This is likely to fail
3152 # against real dirty data (ie: produce mojibake), but the data is
3153 # invalid anyway so it is as good a guess as any. But this means that
3154 # this test just confirms the current behavior; that behavior is not
3155 # necessarily the best possible behavior. With 'decode' it is
3156 # returning the raw bytes, so that test should be of correct behavior,
3157 # or at least produce the same result that email4 did.
3158 m = self.bodytest_msg.format(charset='utf-8',
3159 cte='quoted-printable',
3160 bodyline='p=C3=B6stál').encode('utf-8')
3161 msg = email.message_from_bytes(m)
3162 self.assertEqual(msg.get_payload(), 'p=C3=B6stál\n')
3163 self.assertEqual(msg.get_payload(decode=True),
3164 'pöstál\n'.encode('utf-8'))
3165
3166 def test_invalid_8bit_in_non_8bit_cte_uses_replace(self):
3167 # This is similar to the previous test, but proves that if the 8bit
3168 # byte is undecodeable in the specified charset, it gets replaced
3169 # by the unicode 'unknown' character. Again, this may or may not
3170 # be the ideal behavior. Note that if decode=False none of the
3171 # decoders will get involved, so this is the only test we need
3172 # for this behavior.
3173 m = self.bodytest_msg.format(charset='ascii',
3174 cte='quoted-printable',
3175 bodyline='p=C3=B6stál').encode('utf-8')
3176 msg = email.message_from_bytes(m)
R. David Murray92532142011-01-07 23:25:30 +00003177 self.assertEqual(msg.get_payload(), 'p=C3=B6st\uFFFD\uFFFDl\n')
R. David Murray96fd54e2010-10-08 15:55:28 +00003178 self.assertEqual(msg.get_payload(decode=True),
3179 'pöstál\n'.encode('utf-8'))
3180
3181 def test_8bit_in_base64_body(self):
3182 # Sticking an 8bit byte in a base64 block makes it undecodable by
3183 # normal means, so the block is returned undecoded, but as bytes.
3184 m = self.bodytest_msg.format(charset='utf-8',
3185 cte='base64',
3186 bodyline='cMO2c3RhbAá=').encode('utf-8')
3187 msg = email.message_from_bytes(m)
3188 self.assertEqual(msg.get_payload(decode=True),
3189 'cMO2c3RhbAá=\n'.encode('utf-8'))
3190
3191 def test_8bit_in_uuencode_body(self):
3192 # Sticking an 8bit byte in a uuencode block makes it undecodable by
3193 # normal means, so the block is returned undecoded, but as bytes.
3194 m = self.bodytest_msg.format(charset='utf-8',
3195 cte='uuencode',
3196 bodyline='<,.V<W1A; á ').encode('utf-8')
3197 msg = email.message_from_bytes(m)
3198 self.assertEqual(msg.get_payload(decode=True),
3199 '<,.V<W1A; á \n'.encode('utf-8'))
3200
3201
R. David Murray92532142011-01-07 23:25:30 +00003202 headertest_headers = (
3203 ('From: foo@bar.com', ('From', 'foo@bar.com')),
3204 ('To: báz', ('To', '=?unknown-8bit?q?b=C3=A1z?=')),
3205 ('Subject: Maintenant je vous présente mon collègue, le pouf célèbre\n'
3206 '\tJean de Baddie',
3207 ('Subject', '=?unknown-8bit?q?Maintenant_je_vous_pr=C3=A9sente_mon_'
3208 'coll=C3=A8gue=2C_le_pouf_c=C3=A9l=C3=A8bre?=\n'
3209 ' =?unknown-8bit?q?_Jean_de_Baddie?=')),
3210 ('From: göst', ('From', '=?unknown-8bit?b?Z8O2c3Q=?=')),
3211 )
3212 headertest_msg = ('\n'.join([src for (src, _) in headertest_headers]) +
3213 '\nYes, they are flying.\n').encode('utf-8')
R. David Murray96fd54e2010-10-08 15:55:28 +00003214
3215 def test_get_8bit_header(self):
3216 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003217 self.assertEqual(str(msg.get('to')), 'b\uFFFD\uFFFDz')
3218 self.assertEqual(str(msg['to']), 'b\uFFFD\uFFFDz')
R. David Murray96fd54e2010-10-08 15:55:28 +00003219
3220 def test_print_8bit_headers(self):
3221 msg = email.message_from_bytes(self.headertest_msg)
3222 self.assertEqual(str(msg),
R. David Murray92532142011-01-07 23:25:30 +00003223 textwrap.dedent("""\
3224 From: {}
3225 To: {}
3226 Subject: {}
3227 From: {}
3228
3229 Yes, they are flying.
3230 """).format(*[expected[1] for (_, expected) in
3231 self.headertest_headers]))
R. David Murray96fd54e2010-10-08 15:55:28 +00003232
3233 def test_values_with_8bit_headers(self):
3234 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003235 self.assertListEqual([str(x) for x in msg.values()],
R. David Murray96fd54e2010-10-08 15:55:28 +00003236 ['foo@bar.com',
R. David Murray92532142011-01-07 23:25:30 +00003237 'b\uFFFD\uFFFDz',
3238 'Maintenant je vous pr\uFFFD\uFFFDsente mon '
3239 'coll\uFFFD\uFFFDgue, le pouf '
3240 'c\uFFFD\uFFFDl\uFFFD\uFFFDbre\n'
R. David Murray96fd54e2010-10-08 15:55:28 +00003241 '\tJean de Baddie',
R. David Murray92532142011-01-07 23:25:30 +00003242 "g\uFFFD\uFFFDst"])
R. David Murray96fd54e2010-10-08 15:55:28 +00003243
3244 def test_items_with_8bit_headers(self):
3245 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003246 self.assertListEqual([(str(x), str(y)) for (x, y) in msg.items()],
R. David Murray96fd54e2010-10-08 15:55:28 +00003247 [('From', 'foo@bar.com'),
R. David Murray92532142011-01-07 23:25:30 +00003248 ('To', 'b\uFFFD\uFFFDz'),
3249 ('Subject', 'Maintenant je vous '
3250 'pr\uFFFD\uFFFDsente '
3251 'mon coll\uFFFD\uFFFDgue, le pouf '
3252 'c\uFFFD\uFFFDl\uFFFD\uFFFDbre\n'
3253 '\tJean de Baddie'),
3254 ('From', 'g\uFFFD\uFFFDst')])
R. David Murray96fd54e2010-10-08 15:55:28 +00003255
3256 def test_get_all_with_8bit_headers(self):
3257 msg = email.message_from_bytes(self.headertest_msg)
R. David Murray92532142011-01-07 23:25:30 +00003258 self.assertListEqual([str(x) for x in msg.get_all('from')],
R. David Murray96fd54e2010-10-08 15:55:28 +00003259 ['foo@bar.com',
R. David Murray92532142011-01-07 23:25:30 +00003260 'g\uFFFD\uFFFDst'])
R. David Murray96fd54e2010-10-08 15:55:28 +00003261
R David Murraya2150232011-03-16 21:11:23 -04003262 def test_get_content_type_with_8bit(self):
3263 msg = email.message_from_bytes(textwrap.dedent("""\
3264 Content-Type: text/pl\xA7in; charset=utf-8
3265 """).encode('latin-1'))
3266 self.assertEqual(msg.get_content_type(), "text/pl\uFFFDin")
3267 self.assertEqual(msg.get_content_maintype(), "text")
3268 self.assertEqual(msg.get_content_subtype(), "pl\uFFFDin")
3269
3270 def test_get_params_with_8bit(self):
3271 msg = email.message_from_bytes(
3272 'X-Header: foo=\xa7ne; b\xa7r=two; baz=three\n'.encode('latin-1'))
3273 self.assertEqual(msg.get_params(header='x-header'),
3274 [('foo', '\uFFFDne'), ('b\uFFFDr', 'two'), ('baz', 'three')])
3275 self.assertEqual(msg.get_param('Foo', header='x-header'), '\uFFFdne')
3276 # XXX: someday you might be able to get 'b\xa7r', for now you can't.
3277 self.assertEqual(msg.get_param('b\xa7r', header='x-header'), None)
3278
3279 def test_get_rfc2231_params_with_8bit(self):
3280 msg = email.message_from_bytes(textwrap.dedent("""\
3281 Content-Type: text/plain; charset=us-ascii;
3282 title*=us-ascii'en'This%20is%20not%20f\xa7n"""
3283 ).encode('latin-1'))
3284 self.assertEqual(msg.get_param('title'),
3285 ('us-ascii', 'en', 'This is not f\uFFFDn'))
3286
3287 def test_set_rfc2231_params_with_8bit(self):
3288 msg = email.message_from_bytes(textwrap.dedent("""\
3289 Content-Type: text/plain; charset=us-ascii;
3290 title*=us-ascii'en'This%20is%20not%20f\xa7n"""
3291 ).encode('latin-1'))
3292 msg.set_param('title', 'test')
3293 self.assertEqual(msg.get_param('title'), 'test')
3294
3295 def test_del_rfc2231_params_with_8bit(self):
3296 msg = email.message_from_bytes(textwrap.dedent("""\
3297 Content-Type: text/plain; charset=us-ascii;
3298 title*=us-ascii'en'This%20is%20not%20f\xa7n"""
3299 ).encode('latin-1'))
3300 msg.del_param('title')
3301 self.assertEqual(msg.get_param('title'), None)
3302 self.assertEqual(msg.get_content_maintype(), 'text')
3303
3304 def test_get_payload_with_8bit_cte_header(self):
3305 msg = email.message_from_bytes(textwrap.dedent("""\
3306 Content-Transfer-Encoding: b\xa7se64
3307 Content-Type: text/plain; charset=latin-1
3308
3309 payload
3310 """).encode('latin-1'))
3311 self.assertEqual(msg.get_payload(), 'payload\n')
3312 self.assertEqual(msg.get_payload(decode=True), b'payload\n')
3313
R. David Murray96fd54e2010-10-08 15:55:28 +00003314 non_latin_bin_msg = textwrap.dedent("""\
3315 From: foo@bar.com
3316 To: báz
3317 Subject: Maintenant je vous présente mon collègue, le pouf célèbre
3318 \tJean de Baddie
3319 Mime-Version: 1.0
3320 Content-Type: text/plain; charset="utf-8"
3321 Content-Transfer-Encoding: 8bit
3322
3323 Да, они летят.
3324 """).encode('utf-8')
3325
3326 def test_bytes_generator(self):
3327 msg = email.message_from_bytes(self.non_latin_bin_msg)
3328 out = BytesIO()
3329 email.generator.BytesGenerator(out).flatten(msg)
3330 self.assertEqual(out.getvalue(), self.non_latin_bin_msg)
3331
R. David Murray7372a072011-01-26 21:21:32 +00003332 def test_bytes_generator_handles_None_body(self):
3333 #Issue 11019
3334 msg = email.message.Message()
3335 out = BytesIO()
3336 email.generator.BytesGenerator(out).flatten(msg)
3337 self.assertEqual(out.getvalue(), b"\n")
3338
R. David Murray92532142011-01-07 23:25:30 +00003339 non_latin_bin_msg_as7bit_wrapped = textwrap.dedent("""\
R. David Murray96fd54e2010-10-08 15:55:28 +00003340 From: foo@bar.com
R. David Murray92532142011-01-07 23:25:30 +00003341 To: =?unknown-8bit?q?b=C3=A1z?=
3342 Subject: =?unknown-8bit?q?Maintenant_je_vous_pr=C3=A9sente_mon_coll=C3=A8gue?=
3343 =?unknown-8bit?q?=2C_le_pouf_c=C3=A9l=C3=A8bre?=
3344 =?unknown-8bit?q?_Jean_de_Baddie?=
R. David Murray96fd54e2010-10-08 15:55:28 +00003345 Mime-Version: 1.0
3346 Content-Type: text/plain; charset="utf-8"
3347 Content-Transfer-Encoding: base64
3348
3349 0JTQsCwg0L7QvdC4INC70LXRgtGP0YIuCg==
3350 """)
3351
3352 def test_generator_handles_8bit(self):
3353 msg = email.message_from_bytes(self.non_latin_bin_msg)
3354 out = StringIO()
3355 email.generator.Generator(out).flatten(msg)
R. David Murray92532142011-01-07 23:25:30 +00003356 self.assertEqual(out.getvalue(), self.non_latin_bin_msg_as7bit_wrapped)
R. David Murray96fd54e2010-10-08 15:55:28 +00003357
3358 def test_bytes_generator_with_unix_from(self):
3359 # The unixfrom contains a current date, so we can't check it
3360 # literally. Just make sure the first word is 'From' and the
3361 # rest of the message matches the input.
3362 msg = email.message_from_bytes(self.non_latin_bin_msg)
3363 out = BytesIO()
3364 email.generator.BytesGenerator(out).flatten(msg, unixfrom=True)
3365 lines = out.getvalue().split(b'\n')
3366 self.assertEqual(lines[0].split()[0], b'From')
3367 self.assertEqual(b'\n'.join(lines[1:]), self.non_latin_bin_msg)
3368
R. David Murray92532142011-01-07 23:25:30 +00003369 non_latin_bin_msg_as7bit = non_latin_bin_msg_as7bit_wrapped.split('\n')
3370 non_latin_bin_msg_as7bit[2:4] = [
3371 'Subject: =?unknown-8bit?q?Maintenant_je_vous_pr=C3=A9sente_mon_'
3372 'coll=C3=A8gue=2C_le_pouf_c=C3=A9l=C3=A8bre?=']
3373 non_latin_bin_msg_as7bit = '\n'.join(non_latin_bin_msg_as7bit)
3374
R. David Murray96fd54e2010-10-08 15:55:28 +00003375 def test_message_from_binary_file(self):
3376 fn = 'test.msg'
3377 self.addCleanup(unlink, fn)
3378 with open(fn, 'wb') as testfile:
3379 testfile.write(self.non_latin_bin_msg)
Brett Cannon384917a2010-10-29 23:08:36 +00003380 with open(fn, 'rb') as testfile:
3381 m = email.parser.BytesParser().parse(testfile)
R. David Murray96fd54e2010-10-08 15:55:28 +00003382 self.assertEqual(str(m), self.non_latin_bin_msg_as7bit)
3383
3384 latin_bin_msg = textwrap.dedent("""\
3385 From: foo@bar.com
3386 To: Dinsdale
3387 Subject: Nudge nudge, wink, wink
3388 Mime-Version: 1.0
3389 Content-Type: text/plain; charset="latin-1"
3390 Content-Transfer-Encoding: 8bit
3391
3392 oh là là, know what I mean, know what I mean?
3393 """).encode('latin-1')
3394
3395 latin_bin_msg_as7bit = textwrap.dedent("""\
3396 From: foo@bar.com
3397 To: Dinsdale
3398 Subject: Nudge nudge, wink, wink
3399 Mime-Version: 1.0
3400 Content-Type: text/plain; charset="iso-8859-1"
3401 Content-Transfer-Encoding: quoted-printable
3402
3403 oh l=E0 l=E0, know what I mean, know what I mean?
3404 """)
3405
3406 def test_string_generator_reencodes_to_quopri_when_appropriate(self):
3407 m = email.message_from_bytes(self.latin_bin_msg)
3408 self.assertEqual(str(m), self.latin_bin_msg_as7bit)
3409
3410 def test_decoded_generator_emits_unicode_body(self):
3411 m = email.message_from_bytes(self.latin_bin_msg)
3412 out = StringIO()
3413 email.generator.DecodedGenerator(out).flatten(m)
3414 #DecodedHeader output contains an extra blank line compared
3415 #to the input message. RDM: not sure if this is a bug or not,
3416 #but it is not specific to the 8bit->7bit conversion.
3417 self.assertEqual(out.getvalue(),
3418 self.latin_bin_msg.decode('latin-1')+'\n')
3419
3420 def test_bytes_feedparser(self):
3421 bfp = email.feedparser.BytesFeedParser()
3422 for i in range(0, len(self.latin_bin_msg), 10):
3423 bfp.feed(self.latin_bin_msg[i:i+10])
3424 m = bfp.close()
3425 self.assertEqual(str(m), self.latin_bin_msg_as7bit)
3426
R. David Murray8451c4b2010-10-23 22:19:56 +00003427 def test_crlf_flatten(self):
3428 with openfile('msg_26.txt', 'rb') as fp:
3429 text = fp.read()
3430 msg = email.message_from_bytes(text)
3431 s = BytesIO()
3432 g = email.generator.BytesGenerator(s)
3433 g.flatten(msg, linesep='\r\n')
3434 self.assertEqual(s.getvalue(), text)
R David Murrayc5c14722011-04-06 08:13:02 -04003435
3436 def test_8bit_multipart(self):
3437 # Issue 11605
3438 source = textwrap.dedent("""\
3439 Date: Fri, 18 Mar 2011 17:15:43 +0100
3440 To: foo@example.com
3441 From: foodwatch-Newsletter <bar@example.com>
3442 Subject: Aktuelles zu Japan, Klonfleisch und Smiley-System
3443 Message-ID: <76a486bee62b0d200f33dc2ca08220ad@localhost.localdomain>
3444 MIME-Version: 1.0
3445 Content-Type: multipart/alternative;
3446 boundary="b1_76a486bee62b0d200f33dc2ca08220ad"
3447
3448 --b1_76a486bee62b0d200f33dc2ca08220ad
3449 Content-Type: text/plain; charset="utf-8"
3450 Content-Transfer-Encoding: 8bit
3451
3452 Guten Tag, ,
3453
3454 mit großer Betroffenheit verfolgen auch wir im foodwatch-Team die
3455 Nachrichten aus Japan.
3456
3457
3458 --b1_76a486bee62b0d200f33dc2ca08220ad
3459 Content-Type: text/html; charset="utf-8"
3460 Content-Transfer-Encoding: 8bit
3461
3462 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
3463 "http://www.w3.org/TR/html4/loose.dtd">
3464 <html lang="de">
3465 <head>
3466 <title>foodwatch - Newsletter</title>
3467 </head>
3468 <body>
3469 <p>mit gro&szlig;er Betroffenheit verfolgen auch wir im foodwatch-Team
3470 die Nachrichten aus Japan.</p>
3471 </body>
3472 </html>
3473 --b1_76a486bee62b0d200f33dc2ca08220ad--
3474
3475 """).encode('utf-8')
3476 msg = email.message_from_bytes(source)
3477 s = BytesIO()
3478 g = email.generator.BytesGenerator(s)
3479 g.flatten(msg)
3480 self.assertEqual(s.getvalue(), source)
3481
R David Murray9fd170e2012-03-14 14:05:03 -04003482 def test_bytes_generator_b_encoding_linesep(self):
3483 # Issue 14062: b encoding was tacking on an extra \n.
3484 m = Message()
3485 # This has enough non-ascii that it should always end up b encoded.
3486 m['Subject'] = Header('žluťoučký kůň')
3487 s = BytesIO()
3488 g = email.generator.BytesGenerator(s)
3489 g.flatten(m, linesep='\r\n')
3490 self.assertEqual(
3491 s.getvalue(),
3492 b'Subject: =?utf-8?b?xb5sdcWlb3XEjWvDvSBrxa/FiA==?=\r\n\r\n')
3493
3494 def test_generator_b_encoding_linesep(self):
3495 # Since this broke in ByteGenerator, test Generator for completeness.
3496 m = Message()
3497 # This has enough non-ascii that it should always end up b encoded.
3498 m['Subject'] = Header('žluťoučký kůň')
3499 s = StringIO()
3500 g = email.generator.Generator(s)
3501 g.flatten(m, linesep='\r\n')
3502 self.assertEqual(
3503 s.getvalue(),
3504 'Subject: =?utf-8?b?xb5sdcWlb3XEjWvDvSBrxa/FiA==?=\r\n\r\n')
3505
R. David Murray8451c4b2010-10-23 22:19:56 +00003506 maxDiff = None
3507
Ezio Melottib3aedd42010-11-20 19:04:17 +00003508
R. David Murray719a4492010-11-21 16:53:48 +00003509class BaseTestBytesGeneratorIdempotent:
R. David Murray96fd54e2010-10-08 15:55:28 +00003510
R. David Murraye5db2632010-11-20 15:10:13 +00003511 maxDiff = None
3512
R. David Murray96fd54e2010-10-08 15:55:28 +00003513 def _msgobj(self, filename):
3514 with openfile(filename, 'rb') as fp:
3515 data = fp.read()
R. David Murray719a4492010-11-21 16:53:48 +00003516 data = self.normalize_linesep_regex.sub(self.blinesep, data)
R. David Murray96fd54e2010-10-08 15:55:28 +00003517 msg = email.message_from_bytes(data)
3518 return msg, data
3519
R. David Murray719a4492010-11-21 16:53:48 +00003520 def _idempotent(self, msg, data, unixfrom=False):
R. David Murray96fd54e2010-10-08 15:55:28 +00003521 b = BytesIO()
3522 g = email.generator.BytesGenerator(b, maxheaderlen=0)
R. David Murray719a4492010-11-21 16:53:48 +00003523 g.flatten(msg, unixfrom=unixfrom, linesep=self.linesep)
R. David Murraye5db2632010-11-20 15:10:13 +00003524 self.assertByteStringsEqual(data, b.getvalue())
R. David Murray96fd54e2010-10-08 15:55:28 +00003525
R. David Murraye5db2632010-11-20 15:10:13 +00003526 def assertByteStringsEqual(self, str1, str2):
R. David Murray719a4492010-11-21 16:53:48 +00003527 # Not using self.blinesep here is intentional. This way the output
3528 # is more useful when the failure results in mixed line endings.
R. David Murray96fd54e2010-10-08 15:55:28 +00003529 self.assertListEqual(str1.split(b'\n'), str2.split(b'\n'))
3530
3531
R. David Murray719a4492010-11-21 16:53:48 +00003532class TestBytesGeneratorIdempotentNL(BaseTestBytesGeneratorIdempotent,
3533 TestIdempotent):
3534 linesep = '\n'
3535 blinesep = b'\n'
3536 normalize_linesep_regex = re.compile(br'\r\n')
3537
3538
3539class TestBytesGeneratorIdempotentCRLF(BaseTestBytesGeneratorIdempotent,
3540 TestIdempotent):
3541 linesep = '\r\n'
3542 blinesep = b'\r\n'
3543 normalize_linesep_regex = re.compile(br'(?<!\r)\n')
3544
Ezio Melottib3aedd42010-11-20 19:04:17 +00003545
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003546class TestBase64(unittest.TestCase):
3547 def test_len(self):
3548 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00003549 eq(base64mime.header_length('hello'),
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003550 len(base64mime.body_encode(b'hello', eol='')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003551 for size in range(15):
3552 if size == 0 : bsize = 0
3553 elif size <= 3 : bsize = 4
3554 elif size <= 6 : bsize = 8
3555 elif size <= 9 : bsize = 12
3556 elif size <= 12: bsize = 16
3557 else : bsize = 20
Guido van Rossum9604e662007-08-30 03:46:43 +00003558 eq(base64mime.header_length('x' * size), bsize)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003559
3560 def test_decode(self):
3561 eq = self.assertEqual
Barry Warsaw2cc1f6d2007-08-30 14:28:55 +00003562 eq(base64mime.decode(''), b'')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003563 eq(base64mime.decode('aGVsbG8='), b'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003564
3565 def test_encode(self):
3566 eq = self.assertEqual
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003567 eq(base64mime.body_encode(b''), b'')
3568 eq(base64mime.body_encode(b'hello'), 'aGVsbG8=\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003569 # Test the binary flag
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003570 eq(base64mime.body_encode(b'hello\n'), 'aGVsbG8K\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003571 # Test the maxlinelen arg
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003572 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003573eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
3574eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
3575eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
3576eHh4eCB4eHh4IA==
3577""")
3578 # Test the eol argument
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003579 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40, eol='\r\n'),
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003580 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003581eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
3582eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
3583eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
3584eHh4eCB4eHh4IA==\r
3585""")
3586
3587 def test_header_encode(self):
3588 eq = self.assertEqual
3589 he = base64mime.header_encode
3590 eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=')
Guido van Rossum9604e662007-08-30 03:46:43 +00003591 eq(he('hello\r\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=')
3592 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003593 # Test the charset option
3594 eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=')
3595 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003596
3597
Ezio Melottib3aedd42010-11-20 19:04:17 +00003598
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003599class TestQuopri(unittest.TestCase):
3600 def setUp(self):
3601 # Set of characters (as byte integers) that don't need to be encoded
3602 # in headers.
3603 self.hlit = list(chain(
3604 range(ord('a'), ord('z') + 1),
3605 range(ord('A'), ord('Z') + 1),
3606 range(ord('0'), ord('9') + 1),
Guido van Rossum9604e662007-08-30 03:46:43 +00003607 (c for c in b'!*+-/')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003608 # Set of characters (as byte integers) that do need to be encoded in
3609 # headers.
3610 self.hnon = [c for c in range(256) if c not in self.hlit]
3611 assert len(self.hlit) + len(self.hnon) == 256
3612 # Set of characters (as byte integers) that don't need to be encoded
3613 # in bodies.
3614 self.blit = list(range(ord(' '), ord('~') + 1))
3615 self.blit.append(ord('\t'))
3616 self.blit.remove(ord('='))
3617 # Set of characters (as byte integers) that do need to be encoded in
3618 # bodies.
3619 self.bnon = [c for c in range(256) if c not in self.blit]
3620 assert len(self.blit) + len(self.bnon) == 256
3621
Guido van Rossum9604e662007-08-30 03:46:43 +00003622 def test_quopri_header_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003623 for c in self.hlit:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003624 self.assertFalse(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003625 'Should not be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003626 for c in self.hnon:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003627 self.assertTrue(quoprimime.header_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003628 'Should be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003629
Guido van Rossum9604e662007-08-30 03:46:43 +00003630 def test_quopri_body_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003631 for c in self.blit:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003632 self.assertFalse(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003633 'Should not be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003634 for c in self.bnon:
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003635 self.assertTrue(quoprimime.body_check(c),
Guido van Rossum9604e662007-08-30 03:46:43 +00003636 'Should be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003637
3638 def test_header_quopri_len(self):
3639 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00003640 eq(quoprimime.header_length(b'hello'), 5)
3641 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003642 eq(len(quoprimime.header_encode(b'hello', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00003643 quoprimime.header_length(b'hello') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003644 # =?xxx?q?...?= means 10 extra characters
3645 10)
Guido van Rossum9604e662007-08-30 03:46:43 +00003646 eq(quoprimime.header_length(b'h@e@l@l@o@'), 20)
3647 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003648 eq(len(quoprimime.header_encode(b'h@e@l@l@o@', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00003649 quoprimime.header_length(b'h@e@l@l@o@') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003650 # =?xxx?q?...?= means 10 extra characters
3651 10)
3652 for c in self.hlit:
Guido van Rossum9604e662007-08-30 03:46:43 +00003653 eq(quoprimime.header_length(bytes([c])), 1,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003654 'expected length 1 for %r' % chr(c))
3655 for c in self.hnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00003656 # Space is special; it's encoded to _
3657 if c == ord(' '):
3658 continue
3659 eq(quoprimime.header_length(bytes([c])), 3,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003660 'expected length 3 for %r' % chr(c))
Guido van Rossum9604e662007-08-30 03:46:43 +00003661 eq(quoprimime.header_length(b' '), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003662
3663 def test_body_quopri_len(self):
3664 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003665 for c in self.blit:
Guido van Rossum9604e662007-08-30 03:46:43 +00003666 eq(quoprimime.body_length(bytes([c])), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003667 for c in self.bnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00003668 eq(quoprimime.body_length(bytes([c])), 3)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003669
3670 def test_quote_unquote_idempotent(self):
3671 for x in range(256):
3672 c = chr(x)
3673 self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c)
3674
R David Murrayec1b5b82011-03-23 14:19:05 -04003675 def _test_header_encode(self, header, expected_encoded_header, charset=None):
3676 if charset is None:
3677 encoded_header = quoprimime.header_encode(header)
3678 else:
3679 encoded_header = quoprimime.header_encode(header, charset)
3680 self.assertEqual(encoded_header, expected_encoded_header)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003681
R David Murraycafd79d2011-03-23 15:25:55 -04003682 def test_header_encode_null(self):
3683 self._test_header_encode(b'', '')
3684
R David Murrayec1b5b82011-03-23 14:19:05 -04003685 def test_header_encode_one_word(self):
3686 self._test_header_encode(b'hello', '=?iso-8859-1?q?hello?=')
3687
3688 def test_header_encode_two_lines(self):
3689 self._test_header_encode(b'hello\nworld',
3690 '=?iso-8859-1?q?hello=0Aworld?=')
3691
3692 def test_header_encode_non_ascii(self):
3693 self._test_header_encode(b'hello\xc7there',
3694 '=?iso-8859-1?q?hello=C7there?=')
3695
3696 def test_header_encode_alt_charset(self):
3697 self._test_header_encode(b'hello', '=?iso-8859-2?q?hello?=',
3698 charset='iso-8859-2')
3699
3700 def _test_header_decode(self, encoded_header, expected_decoded_header):
3701 decoded_header = quoprimime.header_decode(encoded_header)
3702 self.assertEqual(decoded_header, expected_decoded_header)
3703
3704 def test_header_decode_null(self):
3705 self._test_header_decode('', '')
3706
3707 def test_header_decode_one_word(self):
3708 self._test_header_decode('hello', 'hello')
3709
3710 def test_header_decode_two_lines(self):
3711 self._test_header_decode('hello=0Aworld', 'hello\nworld')
3712
3713 def test_header_decode_non_ascii(self):
3714 self._test_header_decode('hello=C7there', 'hello\xc7there')
3715
3716 def _test_decode(self, encoded, expected_decoded, eol=None):
3717 if eol is None:
3718 decoded = quoprimime.decode(encoded)
3719 else:
3720 decoded = quoprimime.decode(encoded, eol=eol)
3721 self.assertEqual(decoded, expected_decoded)
3722
3723 def test_decode_null_word(self):
3724 self._test_decode('', '')
3725
3726 def test_decode_null_line_null_word(self):
3727 self._test_decode('\r\n', '\n')
3728
3729 def test_decode_one_word(self):
3730 self._test_decode('hello', 'hello')
3731
3732 def test_decode_one_word_eol(self):
3733 self._test_decode('hello', 'hello', eol='X')
3734
3735 def test_decode_one_line(self):
3736 self._test_decode('hello\r\n', 'hello\n')
3737
3738 def test_decode_one_line_lf(self):
3739 self._test_decode('hello\n', 'hello\n')
3740
R David Murraycafd79d2011-03-23 15:25:55 -04003741 def test_decode_one_line_cr(self):
3742 self._test_decode('hello\r', 'hello\n')
3743
3744 def test_decode_one_line_nl(self):
3745 self._test_decode('hello\n', 'helloX', eol='X')
3746
3747 def test_decode_one_line_crnl(self):
3748 self._test_decode('hello\r\n', 'helloX', eol='X')
3749
R David Murrayec1b5b82011-03-23 14:19:05 -04003750 def test_decode_one_line_one_word(self):
3751 self._test_decode('hello\r\nworld', 'hello\nworld')
3752
3753 def test_decode_one_line_one_word_eol(self):
3754 self._test_decode('hello\r\nworld', 'helloXworld', eol='X')
3755
3756 def test_decode_two_lines(self):
3757 self._test_decode('hello\r\nworld\r\n', 'hello\nworld\n')
3758
R David Murraycafd79d2011-03-23 15:25:55 -04003759 def test_decode_two_lines_eol(self):
3760 self._test_decode('hello\r\nworld\r\n', 'helloXworldX', eol='X')
3761
R David Murrayec1b5b82011-03-23 14:19:05 -04003762 def test_decode_one_long_line(self):
3763 self._test_decode('Spam' * 250, 'Spam' * 250)
3764
3765 def test_decode_one_space(self):
3766 self._test_decode(' ', '')
3767
3768 def test_decode_multiple_spaces(self):
3769 self._test_decode(' ' * 5, '')
3770
3771 def test_decode_one_line_trailing_spaces(self):
3772 self._test_decode('hello \r\n', 'hello\n')
3773
3774 def test_decode_two_lines_trailing_spaces(self):
3775 self._test_decode('hello \r\nworld \r\n', 'hello\nworld\n')
3776
3777 def test_decode_quoted_word(self):
3778 self._test_decode('=22quoted=20words=22', '"quoted words"')
3779
3780 def test_decode_uppercase_quoting(self):
3781 self._test_decode('ab=CD=EF', 'ab\xcd\xef')
3782
3783 def test_decode_lowercase_quoting(self):
3784 self._test_decode('ab=cd=ef', 'ab\xcd\xef')
3785
3786 def test_decode_soft_line_break(self):
3787 self._test_decode('soft line=\r\nbreak', 'soft linebreak')
3788
3789 def test_decode_false_quoting(self):
3790 self._test_decode('A=1,B=A ==> A+B==2', 'A=1,B=A ==> A+B==2')
3791
3792 def _test_encode(self, body, expected_encoded_body, maxlinelen=None, eol=None):
3793 kwargs = {}
3794 if maxlinelen is None:
3795 # Use body_encode's default.
3796 maxlinelen = 76
3797 else:
3798 kwargs['maxlinelen'] = maxlinelen
3799 if eol is None:
3800 # Use body_encode's default.
3801 eol = '\n'
3802 else:
3803 kwargs['eol'] = eol
3804 encoded_body = quoprimime.body_encode(body, **kwargs)
3805 self.assertEqual(encoded_body, expected_encoded_body)
3806 if eol == '\n' or eol == '\r\n':
3807 # We know how to split the result back into lines, so maxlinelen
3808 # can be checked.
3809 for line in encoded_body.splitlines():
3810 self.assertLessEqual(len(line), maxlinelen)
3811
3812 def test_encode_null(self):
3813 self._test_encode('', '')
3814
3815 def test_encode_null_lines(self):
3816 self._test_encode('\n\n', '\n\n')
3817
3818 def test_encode_one_line(self):
3819 self._test_encode('hello\n', 'hello\n')
3820
3821 def test_encode_one_line_crlf(self):
3822 self._test_encode('hello\r\n', 'hello\n')
3823
3824 def test_encode_one_line_eol(self):
3825 self._test_encode('hello\n', 'hello\r\n', eol='\r\n')
3826
3827 def test_encode_one_space(self):
3828 self._test_encode(' ', '=20')
3829
3830 def test_encode_one_line_one_space(self):
3831 self._test_encode(' \n', '=20\n')
3832
R David Murrayb938c8c2011-03-24 12:19:26 -04003833# XXX: body_encode() expect strings, but uses ord(char) from these strings
3834# to index into a 256-entry list. For code points above 255, this will fail.
3835# Should there be a check for 8-bit only ord() values in body, or at least
3836# a comment about the expected input?
3837
3838 def test_encode_two_lines_one_space(self):
3839 self._test_encode(' \n \n', '=20\n=20\n')
3840
R David Murrayec1b5b82011-03-23 14:19:05 -04003841 def test_encode_one_word_trailing_spaces(self):
3842 self._test_encode('hello ', 'hello =20')
3843
3844 def test_encode_one_line_trailing_spaces(self):
3845 self._test_encode('hello \n', 'hello =20\n')
3846
3847 def test_encode_one_word_trailing_tab(self):
3848 self._test_encode('hello \t', 'hello =09')
3849
3850 def test_encode_one_line_trailing_tab(self):
3851 self._test_encode('hello \t\n', 'hello =09\n')
3852
3853 def test_encode_trailing_space_before_maxlinelen(self):
3854 self._test_encode('abcd \n1234', 'abcd =\n\n1234', maxlinelen=6)
3855
R David Murrayb938c8c2011-03-24 12:19:26 -04003856 def test_encode_trailing_space_at_maxlinelen(self):
3857 self._test_encode('abcd \n1234', 'abcd=\n=20\n1234', maxlinelen=5)
3858
R David Murrayec1b5b82011-03-23 14:19:05 -04003859 def test_encode_trailing_space_beyond_maxlinelen(self):
R David Murrayb938c8c2011-03-24 12:19:26 -04003860 self._test_encode('abcd \n1234', 'abc=\nd=20\n1234', maxlinelen=4)
3861
3862 def test_encode_whitespace_lines(self):
3863 self._test_encode(' \n' * 5, '=20\n' * 5)
R David Murrayec1b5b82011-03-23 14:19:05 -04003864
3865 def test_encode_quoted_equals(self):
3866 self._test_encode('a = b', 'a =3D b')
3867
3868 def test_encode_one_long_string(self):
3869 self._test_encode('x' * 100, 'x' * 75 + '=\n' + 'x' * 25)
3870
3871 def test_encode_one_long_line(self):
3872 self._test_encode('x' * 100 + '\n', 'x' * 75 + '=\n' + 'x' * 25 + '\n')
3873
3874 def test_encode_one_very_long_line(self):
3875 self._test_encode('x' * 200 + '\n',
3876 2 * ('x' * 75 + '=\n') + 'x' * 50 + '\n')
3877
3878 def test_encode_one_long_line(self):
3879 self._test_encode('x' * 100 + '\n', 'x' * 75 + '=\n' + 'x' * 25 + '\n')
3880
3881 def test_encode_shortest_maxlinelen(self):
3882 self._test_encode('=' * 5, '=3D=\n' * 4 + '=3D', maxlinelen=4)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003883
R David Murrayb938c8c2011-03-24 12:19:26 -04003884 def test_encode_maxlinelen_too_small(self):
3885 self.assertRaises(ValueError, self._test_encode, '', '', maxlinelen=3)
3886
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003887 def test_encode(self):
3888 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00003889 eq(quoprimime.body_encode(''), '')
3890 eq(quoprimime.body_encode('hello'), 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003891 # Test the binary flag
Guido van Rossum9604e662007-08-30 03:46:43 +00003892 eq(quoprimime.body_encode('hello\r\nworld'), 'hello\nworld')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003893 # Test the maxlinelen arg
Guido van Rossum9604e662007-08-30 03:46:43 +00003894 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003895xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
3896 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
3897x xxxx xxxx xxxx xxxx=20""")
3898 # Test the eol argument
Guido van Rossum9604e662007-08-30 03:46:43 +00003899 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'),
3900 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003901xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
3902 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
3903x xxxx xxxx xxxx xxxx=20""")
Guido van Rossum9604e662007-08-30 03:46:43 +00003904 eq(quoprimime.body_encode("""\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003905one line
3906
3907two line"""), """\
3908one line
3909
3910two line""")
3911
3912
Ezio Melottib3aedd42010-11-20 19:04:17 +00003913
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003914# Test the Charset class
3915class TestCharset(unittest.TestCase):
3916 def tearDown(self):
3917 from email import charset as CharsetModule
3918 try:
3919 del CharsetModule.CHARSETS['fake']
3920 except KeyError:
3921 pass
3922
Guido van Rossum9604e662007-08-30 03:46:43 +00003923 def test_codec_encodeable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003924 eq = self.assertEqual
3925 # Make sure us-ascii = no Unicode conversion
3926 c = Charset('us-ascii')
Guido van Rossum9604e662007-08-30 03:46:43 +00003927 eq(c.header_encode('Hello World!'), 'Hello World!')
3928 # Test 8-bit idempotency with us-ascii
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003929 s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
Guido van Rossum9604e662007-08-30 03:46:43 +00003930 self.assertRaises(UnicodeError, c.header_encode, s)
3931 c = Charset('utf-8')
3932 eq(c.header_encode(s), '=?utf-8?b?wqTCosKkwqTCpMKmwqTCqMKkwqo=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003933
3934 def test_body_encode(self):
3935 eq = self.assertEqual
3936 # Try a charset with QP body encoding
3937 c = Charset('iso-8859-1')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003938 eq('hello w=F6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003939 # Try a charset with Base64 body encoding
3940 c = Charset('utf-8')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00003941 eq('aGVsbG8gd29ybGQ=\n', c.body_encode(b'hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003942 # Try a charset with None body encoding
3943 c = Charset('us-ascii')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00003944 eq('hello world', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003945 # Try the convert argument, where input codec != output codec
3946 c = Charset('euc-jp')
3947 # With apologies to Tokio Kikuchi ;)
Barry Warsawbef9d212007-08-31 10:55:37 +00003948 # XXX FIXME
3949## try:
3950## eq('\x1b$B5FCO;~IW\x1b(B',
3951## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
3952## eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
3953## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
3954## except LookupError:
3955## # We probably don't have the Japanese codecs installed
3956## pass
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003957 # Testing SF bug #625509, which we have to fake, since there are no
3958 # built-in encodings where the header encoding is QP but the body
3959 # encoding is not.
3960 from email import charset as CharsetModule
R David Murray56a9d7e2011-03-15 12:20:02 -04003961 CharsetModule.add_charset('fake', CharsetModule.QP, None, 'utf-8')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003962 c = Charset('fake')
R David Murray56a9d7e2011-03-15 12:20:02 -04003963 eq('hello world', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003964
3965 def test_unicode_charset_name(self):
3966 charset = Charset('us-ascii')
3967 self.assertEqual(str(charset), 'us-ascii')
3968 self.assertRaises(errors.CharsetError, Charset, 'asc\xffii')
3969
3970
Ezio Melottib3aedd42010-11-20 19:04:17 +00003971
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003972# Test multilingual MIME headers.
3973class TestHeader(TestEmailBase):
3974 def test_simple(self):
3975 eq = self.ndiffAssertEqual
3976 h = Header('Hello World!')
3977 eq(h.encode(), 'Hello World!')
3978 h.append(' Goodbye World!')
3979 eq(h.encode(), 'Hello World! Goodbye World!')
3980
3981 def test_simple_surprise(self):
3982 eq = self.ndiffAssertEqual
3983 h = Header('Hello World!')
3984 eq(h.encode(), 'Hello World!')
3985 h.append('Goodbye World!')
3986 eq(h.encode(), 'Hello World! Goodbye World!')
3987
3988 def test_header_needs_no_decoding(self):
3989 h = 'no decoding needed'
3990 self.assertEqual(decode_header(h), [(h, None)])
3991
3992 def test_long(self):
3993 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.",
3994 maxlinelen=76)
3995 for l in h.encode(splitchars=' ').split('\n '):
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00003996 self.assertTrue(len(l) <= 76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003997
3998 def test_multilingual(self):
3999 eq = self.ndiffAssertEqual
4000 g = Charset("iso-8859-1")
4001 cz = Charset("iso-8859-2")
4002 utf8 = Charset("utf-8")
4003 g_head = (b'Die Mieter treten hier ein werden mit einem '
4004 b'Foerderband komfortabel den Korridor entlang, '
4005 b'an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, '
4006 b'gegen die rotierenden Klingen bef\xf6rdert. ')
4007 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
4008 b'd\xf9vtipu.. ')
4009 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
4010 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
4011 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
4012 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
4013 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
4014 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
4015 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
4016 '\u3044\u307e\u3059\u3002')
4017 h = Header(g_head, g)
4018 h.append(cz_head, cz)
4019 h.append(utf8_head, utf8)
Guido van Rossum9604e662007-08-30 03:46:43 +00004020 enc = h.encode(maxlinelen=76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004021 eq(enc, """\
Guido van Rossum9604e662007-08-30 03:46:43 +00004022=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_kom?=
4023 =?iso-8859-1?q?fortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wand?=
4024 =?iso-8859-1?q?gem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6r?=
4025 =?iso-8859-1?q?dert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004026 =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
4027 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
4028 =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
4029 =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
Guido van Rossum9604e662007-08-30 03:46:43 +00004030 =?utf-8?b?IE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5k?=
4031 =?utf-8?b?IGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIA=?=
4032 =?utf-8?b?44Gj44Gm44GE44G+44GZ44CC?=""")
4033 decoded = decode_header(enc)
4034 eq(len(decoded), 3)
4035 eq(decoded[0], (g_head, 'iso-8859-1'))
4036 eq(decoded[1], (cz_head, 'iso-8859-2'))
4037 eq(decoded[2], (utf8_head.encode('utf-8'), 'utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004038 ustr = str(h)
Guido van Rossum9604e662007-08-30 03:46:43 +00004039 eq(ustr,
4040 (b'Die Mieter treten hier ein werden mit einem Foerderband '
4041 b'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
4042 b'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
4043 b'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
4044 b'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
4045 b'\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
4046 b'\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
4047 b'\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
4048 b'\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
4049 b'\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
4050 b'\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
4051 b'\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
4052 b'\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
4053 b'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
4054 b'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
4055 b'\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82'
4056 ).decode('utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004057 # Test make_header()
4058 newh = make_header(decode_header(enc))
Guido van Rossum9604e662007-08-30 03:46:43 +00004059 eq(newh, h)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004060
4061 def test_empty_header_encode(self):
4062 h = Header()
4063 self.assertEqual(h.encode(), '')
Barry Warsaw8b3d6592007-08-30 02:10:49 +00004064
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004065 def test_header_ctor_default_args(self):
4066 eq = self.ndiffAssertEqual
4067 h = Header()
4068 eq(h, '')
4069 h.append('foo', Charset('iso-8859-1'))
Guido van Rossum9604e662007-08-30 03:46:43 +00004070 eq(h, 'foo')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004071
4072 def test_explicit_maxlinelen(self):
4073 eq = self.ndiffAssertEqual
4074 hstr = ('A very long line that must get split to something other '
4075 'than at the 76th character boundary to test the non-default '
4076 'behavior')
4077 h = Header(hstr)
4078 eq(h.encode(), '''\
4079A very long line that must get split to something other than at the 76th
4080 character boundary to test the non-default behavior''')
4081 eq(str(h), hstr)
4082 h = Header(hstr, header_name='Subject')
4083 eq(h.encode(), '''\
4084A very long line that must get split to something other than at the
4085 76th character boundary to test the non-default behavior''')
4086 eq(str(h), hstr)
4087 h = Header(hstr, maxlinelen=1024, header_name='Subject')
4088 eq(h.encode(), hstr)
4089 eq(str(h), hstr)
4090
Guido van Rossum9604e662007-08-30 03:46:43 +00004091 def test_quopri_splittable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004092 eq = self.ndiffAssertEqual
4093 h = Header(charset='iso-8859-1', maxlinelen=20)
Guido van Rossum9604e662007-08-30 03:46:43 +00004094 x = 'xxxx ' * 20
4095 h.append(x)
4096 s = h.encode()
4097 eq(s, """\
4098=?iso-8859-1?q?xxx?=
4099 =?iso-8859-1?q?x_?=
4100 =?iso-8859-1?q?xx?=
4101 =?iso-8859-1?q?xx?=
4102 =?iso-8859-1?q?_x?=
4103 =?iso-8859-1?q?xx?=
4104 =?iso-8859-1?q?x_?=
4105 =?iso-8859-1?q?xx?=
4106 =?iso-8859-1?q?xx?=
4107 =?iso-8859-1?q?_x?=
4108 =?iso-8859-1?q?xx?=
4109 =?iso-8859-1?q?x_?=
4110 =?iso-8859-1?q?xx?=
4111 =?iso-8859-1?q?xx?=
4112 =?iso-8859-1?q?_x?=
4113 =?iso-8859-1?q?xx?=
4114 =?iso-8859-1?q?x_?=
4115 =?iso-8859-1?q?xx?=
4116 =?iso-8859-1?q?xx?=
4117 =?iso-8859-1?q?_x?=
4118 =?iso-8859-1?q?xx?=
4119 =?iso-8859-1?q?x_?=
4120 =?iso-8859-1?q?xx?=
4121 =?iso-8859-1?q?xx?=
4122 =?iso-8859-1?q?_x?=
4123 =?iso-8859-1?q?xx?=
4124 =?iso-8859-1?q?x_?=
4125 =?iso-8859-1?q?xx?=
4126 =?iso-8859-1?q?xx?=
4127 =?iso-8859-1?q?_x?=
4128 =?iso-8859-1?q?xx?=
4129 =?iso-8859-1?q?x_?=
4130 =?iso-8859-1?q?xx?=
4131 =?iso-8859-1?q?xx?=
4132 =?iso-8859-1?q?_x?=
4133 =?iso-8859-1?q?xx?=
4134 =?iso-8859-1?q?x_?=
4135 =?iso-8859-1?q?xx?=
4136 =?iso-8859-1?q?xx?=
4137 =?iso-8859-1?q?_x?=
4138 =?iso-8859-1?q?xx?=
4139 =?iso-8859-1?q?x_?=
4140 =?iso-8859-1?q?xx?=
4141 =?iso-8859-1?q?xx?=
4142 =?iso-8859-1?q?_x?=
4143 =?iso-8859-1?q?xx?=
4144 =?iso-8859-1?q?x_?=
4145 =?iso-8859-1?q?xx?=
4146 =?iso-8859-1?q?xx?=
4147 =?iso-8859-1?q?_?=""")
4148 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004149 h = Header(charset='iso-8859-1', maxlinelen=40)
4150 h.append('xxxx ' * 20)
Guido van Rossum9604e662007-08-30 03:46:43 +00004151 s = h.encode()
4152 eq(s, """\
4153=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xxx?=
4154 =?iso-8859-1?q?x_xxxx_xxxx_xxxx_xxxx_?=
4155 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
4156 =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
4157 =?iso-8859-1?q?_xxxx_xxxx_?=""")
4158 eq(x, str(make_header(decode_header(s))))
4159
4160 def test_base64_splittable(self):
4161 eq = self.ndiffAssertEqual
4162 h = Header(charset='koi8-r', maxlinelen=20)
4163 x = 'xxxx ' * 20
4164 h.append(x)
4165 s = h.encode()
4166 eq(s, """\
4167=?koi8-r?b?eHh4?=
4168 =?koi8-r?b?eCB4?=
4169 =?koi8-r?b?eHh4?=
4170 =?koi8-r?b?IHh4?=
4171 =?koi8-r?b?eHgg?=
4172 =?koi8-r?b?eHh4?=
4173 =?koi8-r?b?eCB4?=
4174 =?koi8-r?b?eHh4?=
4175 =?koi8-r?b?IHh4?=
4176 =?koi8-r?b?eHgg?=
4177 =?koi8-r?b?eHh4?=
4178 =?koi8-r?b?eCB4?=
4179 =?koi8-r?b?eHh4?=
4180 =?koi8-r?b?IHh4?=
4181 =?koi8-r?b?eHgg?=
4182 =?koi8-r?b?eHh4?=
4183 =?koi8-r?b?eCB4?=
4184 =?koi8-r?b?eHh4?=
4185 =?koi8-r?b?IHh4?=
4186 =?koi8-r?b?eHgg?=
4187 =?koi8-r?b?eHh4?=
4188 =?koi8-r?b?eCB4?=
4189 =?koi8-r?b?eHh4?=
4190 =?koi8-r?b?IHh4?=
4191 =?koi8-r?b?eHgg?=
4192 =?koi8-r?b?eHh4?=
4193 =?koi8-r?b?eCB4?=
4194 =?koi8-r?b?eHh4?=
4195 =?koi8-r?b?IHh4?=
4196 =?koi8-r?b?eHgg?=
4197 =?koi8-r?b?eHh4?=
4198 =?koi8-r?b?eCB4?=
4199 =?koi8-r?b?eHh4?=
4200 =?koi8-r?b?IA==?=""")
4201 eq(x, str(make_header(decode_header(s))))
4202 h = Header(charset='koi8-r', maxlinelen=40)
4203 h.append(x)
4204 s = h.encode()
4205 eq(s, """\
4206=?koi8-r?b?eHh4eCB4eHh4IHh4eHggeHh4?=
4207 =?koi8-r?b?eCB4eHh4IHh4eHggeHh4eCB4?=
4208 =?koi8-r?b?eHh4IHh4eHggeHh4eCB4eHh4?=
4209 =?koi8-r?b?IHh4eHggeHh4eCB4eHh4IHh4?=
4210 =?koi8-r?b?eHggeHh4eCB4eHh4IHh4eHgg?=
4211 =?koi8-r?b?eHh4eCB4eHh4IA==?=""")
4212 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004213
4214 def test_us_ascii_header(self):
4215 eq = self.assertEqual
4216 s = 'hello'
4217 x = decode_header(s)
4218 eq(x, [('hello', None)])
4219 h = make_header(x)
4220 eq(s, h.encode())
4221
4222 def test_string_charset(self):
4223 eq = self.assertEqual
4224 h = Header()
4225 h.append('hello', 'iso-8859-1')
Guido van Rossum9604e662007-08-30 03:46:43 +00004226 eq(h, 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004227
4228## def test_unicode_error(self):
4229## raises = self.assertRaises
4230## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')
4231## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')
4232## h = Header()
4233## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')
4234## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')
4235## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')
4236
4237 def test_utf8_shortest(self):
4238 eq = self.assertEqual
4239 h = Header('p\xf6stal', 'utf-8')
4240 eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
4241 h = Header('\u83ca\u5730\u6642\u592b', 'utf-8')
4242 eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
4243
4244 def test_bad_8bit_header(self):
4245 raises = self.assertRaises
4246 eq = self.assertEqual
4247 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
4248 raises(UnicodeError, Header, x)
4249 h = Header()
4250 raises(UnicodeError, h.append, x)
4251 e = x.decode('utf-8', 'replace')
4252 eq(str(Header(x, errors='replace')), e)
4253 h.append(x, errors='replace')
4254 eq(str(h), e)
4255
R David Murray041015c2011-03-25 15:10:55 -04004256 def test_escaped_8bit_header(self):
4257 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
R David Murray6bdb1762011-06-18 12:30:55 -04004258 e = x.decode('ascii', 'surrogateescape')
4259 h = Header(e, charset=email.charset.UNKNOWN8BIT)
R David Murray041015c2011-03-25 15:10:55 -04004260 self.assertEqual(str(h),
4261 'Ynwp4dUEbay Auction Semiar- No Charge \uFFFD Earn Big')
4262 self.assertEqual(email.header.decode_header(h), [(x, 'unknown-8bit')])
4263
R David Murraye5e366c2011-06-18 12:57:28 -04004264 def test_header_handles_binary_unknown8bit(self):
4265 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
4266 h = Header(x, charset=email.charset.UNKNOWN8BIT)
4267 self.assertEqual(str(h),
4268 'Ynwp4dUEbay Auction Semiar- No Charge \uFFFD Earn Big')
4269 self.assertEqual(email.header.decode_header(h), [(x, 'unknown-8bit')])
4270
4271 def test_make_header_handles_binary_unknown8bit(self):
4272 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
4273 h = Header(x, charset=email.charset.UNKNOWN8BIT)
4274 h2 = email.header.make_header(email.header.decode_header(h))
4275 self.assertEqual(str(h2),
4276 'Ynwp4dUEbay Auction Semiar- No Charge \uFFFD Earn Big')
4277 self.assertEqual(email.header.decode_header(h2), [(x, 'unknown-8bit')])
4278
R David Murray041015c2011-03-25 15:10:55 -04004279 def test_modify_returned_list_does_not_change_header(self):
4280 h = Header('test')
4281 chunks = email.header.decode_header(h)
4282 chunks.append(('ascii', 'test2'))
4283 self.assertEqual(str(h), 'test')
4284
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004285 def test_encoded_adjacent_nonencoded(self):
4286 eq = self.assertEqual
4287 h = Header()
4288 h.append('hello', 'iso-8859-1')
4289 h.append('world')
4290 s = h.encode()
4291 eq(s, '=?iso-8859-1?q?hello?= world')
4292 h = make_header(decode_header(s))
4293 eq(h.encode(), s)
4294
4295 def test_whitespace_eater(self):
4296 eq = self.assertEqual
4297 s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
4298 parts = decode_header(s)
4299 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)])
4300 hdr = make_header(parts)
4301 eq(hdr.encode(),
4302 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
4303
4304 def test_broken_base64_header(self):
4305 raises = self.assertRaises
R. David Murrayc4e69cc2010-08-03 22:14:10 +00004306 s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3I ?='
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004307 raises(errors.HeaderParseError, decode_header, s)
4308
R. David Murray477efb32011-01-05 01:39:32 +00004309 def test_shift_jis_charset(self):
4310 h = Header('文', charset='shift_jis')
4311 self.assertEqual(h.encode(), '=?iso-2022-jp?b?GyRCSjgbKEI=?=')
4312
R David Murrayde912762011-03-16 18:26:23 -04004313 def test_flatten_header_with_no_value(self):
4314 # Issue 11401 (regression from email 4.x) Note that the space after
4315 # the header doesn't reflect the input, but this is also the way
4316 # email 4.x behaved. At some point it would be nice to fix that.
4317 msg = email.message_from_string("EmptyHeader:")
4318 self.assertEqual(str(msg), "EmptyHeader: \n\n")
4319
R David Murray01581ee2011-04-18 10:04:34 -04004320 def test_encode_preserves_leading_ws_on_value(self):
4321 msg = Message()
4322 msg['SomeHeader'] = ' value with leading ws'
4323 self.assertEqual(str(msg), "SomeHeader: value with leading ws\n\n")
4324
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004325
Ezio Melottib3aedd42010-11-20 19:04:17 +00004326
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004327# Test RFC 2231 header parameters (en/de)coding
4328class TestRFC2231(TestEmailBase):
4329 def test_get_param(self):
4330 eq = self.assertEqual
4331 msg = self._msgobj('msg_29.txt')
4332 eq(msg.get_param('title'),
4333 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
4334 eq(msg.get_param('title', unquote=False),
4335 ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
4336
4337 def test_set_param(self):
4338 eq = self.ndiffAssertEqual
4339 msg = Message()
4340 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4341 charset='us-ascii')
4342 eq(msg.get_param('title'),
4343 ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
4344 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4345 charset='us-ascii', language='en')
4346 eq(msg.get_param('title'),
4347 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
4348 msg = self._msgobj('msg_01.txt')
4349 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4350 charset='us-ascii', language='en')
4351 eq(msg.as_string(maxheaderlen=78), """\
4352Return-Path: <bbb@zzz.org>
4353Delivered-To: bbb@zzz.org
4354Received: by mail.zzz.org (Postfix, from userid 889)
4355\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
4356MIME-Version: 1.0
4357Content-Transfer-Encoding: 7bit
4358Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
4359From: bbb@ddd.com (John X. Doe)
4360To: bbb@zzz.org
4361Subject: This is a test message
4362Date: Fri, 4 May 2001 14:05:44 -0400
4363Content-Type: text/plain; charset=us-ascii;
R. David Murraydfd7eb02010-12-24 22:36:49 +00004364 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 +00004365
4366
4367Hi,
4368
4369Do you like this message?
4370
4371-Me
4372""")
4373
R David Murraya2860e82011-04-16 09:20:30 -04004374 def test_set_param_requote(self):
4375 msg = Message()
4376 msg.set_param('title', 'foo')
4377 self.assertEqual(msg['content-type'], 'text/plain; title="foo"')
4378 msg.set_param('title', 'bar', requote=False)
4379 self.assertEqual(msg['content-type'], 'text/plain; title=bar')
4380 # tspecial is still quoted.
4381 msg.set_param('title', "(bar)bell", requote=False)
4382 self.assertEqual(msg['content-type'], 'text/plain; title="(bar)bell"')
4383
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004384 def test_del_param(self):
4385 eq = self.ndiffAssertEqual
4386 msg = self._msgobj('msg_01.txt')
4387 msg.set_param('foo', 'bar', charset='us-ascii', language='en')
4388 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
4389 charset='us-ascii', language='en')
4390 msg.del_param('foo', header='Content-Type')
4391 eq(msg.as_string(maxheaderlen=78), """\
4392Return-Path: <bbb@zzz.org>
4393Delivered-To: bbb@zzz.org
4394Received: by mail.zzz.org (Postfix, from userid 889)
4395\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
4396MIME-Version: 1.0
4397Content-Transfer-Encoding: 7bit
4398Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
4399From: bbb@ddd.com (John X. Doe)
4400To: bbb@zzz.org
4401Subject: This is a test message
4402Date: Fri, 4 May 2001 14:05:44 -0400
4403Content-Type: text/plain; charset="us-ascii";
R. David Murraydfd7eb02010-12-24 22:36:49 +00004404 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 +00004405
4406
4407Hi,
4408
4409Do you like this message?
4410
4411-Me
4412""")
4413
4414 def test_rfc2231_get_content_charset(self):
4415 eq = self.assertEqual
4416 msg = self._msgobj('msg_32.txt')
4417 eq(msg.get_content_charset(), 'us-ascii')
4418
R. David Murraydfd7eb02010-12-24 22:36:49 +00004419 def test_rfc2231_parse_rfc_quoting(self):
4420 m = textwrap.dedent('''\
4421 Content-Disposition: inline;
4422 \tfilename*0*=''This%20is%20even%20more%20;
4423 \tfilename*1*=%2A%2A%2Afun%2A%2A%2A%20;
4424 \tfilename*2="is it not.pdf"
4425
4426 ''')
4427 msg = email.message_from_string(m)
4428 self.assertEqual(msg.get_filename(),
4429 'This is even more ***fun*** is it not.pdf')
4430 self.assertEqual(m, msg.as_string())
4431
4432 def test_rfc2231_parse_extra_quoting(self):
4433 m = textwrap.dedent('''\
4434 Content-Disposition: inline;
4435 \tfilename*0*="''This%20is%20even%20more%20";
4436 \tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4437 \tfilename*2="is it not.pdf"
4438
4439 ''')
4440 msg = email.message_from_string(m)
4441 self.assertEqual(msg.get_filename(),
4442 'This is even more ***fun*** is it not.pdf')
4443 self.assertEqual(m, msg.as_string())
4444
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004445 def test_rfc2231_no_language_or_charset(self):
4446 m = '''\
4447Content-Transfer-Encoding: 8bit
4448Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
4449Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
4450
4451'''
4452 msg = email.message_from_string(m)
4453 param = msg.get_param('NAME')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00004454 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004455 self.assertEqual(
4456 param,
4457 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
4458
4459 def test_rfc2231_no_language_or_charset_in_filename(self):
4460 m = '''\
4461Content-Disposition: inline;
4462\tfilename*0*="''This%20is%20even%20more%20";
4463\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4464\tfilename*2="is it not.pdf"
4465
4466'''
4467 msg = email.message_from_string(m)
4468 self.assertEqual(msg.get_filename(),
4469 'This is even more ***fun*** is it not.pdf')
4470
4471 def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
4472 m = '''\
4473Content-Disposition: inline;
4474\tfilename*0*="''This%20is%20even%20more%20";
4475\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4476\tfilename*2="is it not.pdf"
4477
4478'''
4479 msg = email.message_from_string(m)
4480 self.assertEqual(msg.get_filename(),
4481 'This is even more ***fun*** is it not.pdf')
4482
4483 def test_rfc2231_partly_encoded(self):
4484 m = '''\
4485Content-Disposition: inline;
4486\tfilename*0="''This%20is%20even%20more%20";
4487\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4488\tfilename*2="is it not.pdf"
4489
4490'''
4491 msg = email.message_from_string(m)
4492 self.assertEqual(
4493 msg.get_filename(),
4494 'This%20is%20even%20more%20***fun*** is it not.pdf')
4495
4496 def test_rfc2231_partly_nonencoded(self):
4497 m = '''\
4498Content-Disposition: inline;
4499\tfilename*0="This%20is%20even%20more%20";
4500\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
4501\tfilename*2="is it not.pdf"
4502
4503'''
4504 msg = email.message_from_string(m)
4505 self.assertEqual(
4506 msg.get_filename(),
4507 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
4508
4509 def test_rfc2231_no_language_or_charset_in_boundary(self):
4510 m = '''\
4511Content-Type: multipart/alternative;
4512\tboundary*0*="''This%20is%20even%20more%20";
4513\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
4514\tboundary*2="is it not.pdf"
4515
4516'''
4517 msg = email.message_from_string(m)
4518 self.assertEqual(msg.get_boundary(),
4519 'This is even more ***fun*** is it not.pdf')
4520
4521 def test_rfc2231_no_language_or_charset_in_charset(self):
4522 # This is a nonsensical charset value, but tests the code anyway
4523 m = '''\
4524Content-Type: text/plain;
4525\tcharset*0*="This%20is%20even%20more%20";
4526\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
4527\tcharset*2="is it not.pdf"
4528
4529'''
4530 msg = email.message_from_string(m)
4531 self.assertEqual(msg.get_content_charset(),
4532 'this is even more ***fun*** is it not.pdf')
4533
4534 def test_rfc2231_bad_encoding_in_filename(self):
4535 m = '''\
4536Content-Disposition: inline;
4537\tfilename*0*="bogus'xx'This%20is%20even%20more%20";
4538\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4539\tfilename*2="is it not.pdf"
4540
4541'''
4542 msg = email.message_from_string(m)
4543 self.assertEqual(msg.get_filename(),
4544 'This is even more ***fun*** is it not.pdf')
4545
4546 def test_rfc2231_bad_encoding_in_charset(self):
4547 m = """\
4548Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
4549
4550"""
4551 msg = email.message_from_string(m)
4552 # This should return None because non-ascii characters in the charset
4553 # are not allowed.
4554 self.assertEqual(msg.get_content_charset(), None)
4555
4556 def test_rfc2231_bad_character_in_charset(self):
4557 m = """\
4558Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D
4559
4560"""
4561 msg = email.message_from_string(m)
4562 # This should return None because non-ascii characters in the charset
4563 # are not allowed.
4564 self.assertEqual(msg.get_content_charset(), None)
4565
4566 def test_rfc2231_bad_character_in_filename(self):
4567 m = '''\
4568Content-Disposition: inline;
4569\tfilename*0*="ascii'xx'This%20is%20even%20more%20";
4570\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
4571\tfilename*2*="is it not.pdf%E2"
4572
4573'''
4574 msg = email.message_from_string(m)
4575 self.assertEqual(msg.get_filename(),
4576 'This is even more ***fun*** is it not.pdf\ufffd')
4577
4578 def test_rfc2231_unknown_encoding(self):
4579 m = """\
4580Content-Transfer-Encoding: 8bit
4581Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
4582
4583"""
4584 msg = email.message_from_string(m)
4585 self.assertEqual(msg.get_filename(), 'myfile.txt')
4586
4587 def test_rfc2231_single_tick_in_filename_extended(self):
4588 eq = self.assertEqual
4589 m = """\
4590Content-Type: application/x-foo;
4591\tname*0*=\"Frank's\"; name*1*=\" Document\"
4592
4593"""
4594 msg = email.message_from_string(m)
4595 charset, language, s = msg.get_param('name')
4596 eq(charset, None)
4597 eq(language, None)
4598 eq(s, "Frank's Document")
4599
4600 def test_rfc2231_single_tick_in_filename(self):
4601 m = """\
4602Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
4603
4604"""
4605 msg = email.message_from_string(m)
4606 param = msg.get_param('name')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00004607 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004608 self.assertEqual(param, "Frank's Document")
4609
4610 def test_rfc2231_tick_attack_extended(self):
4611 eq = self.assertEqual
4612 m = """\
4613Content-Type: application/x-foo;
4614\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
4615
4616"""
4617 msg = email.message_from_string(m)
4618 charset, language, s = msg.get_param('name')
4619 eq(charset, 'us-ascii')
4620 eq(language, 'en-us')
4621 eq(s, "Frank's Document")
4622
4623 def test_rfc2231_tick_attack(self):
4624 m = """\
4625Content-Type: application/x-foo;
4626\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
4627
4628"""
4629 msg = email.message_from_string(m)
4630 param = msg.get_param('name')
Benjamin Petersonc9c0f202009-06-30 23:06:06 +00004631 self.assertFalse(isinstance(param, tuple))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004632 self.assertEqual(param, "us-ascii'en-us'Frank's Document")
4633
4634 def test_rfc2231_no_extended_values(self):
4635 eq = self.assertEqual
4636 m = """\
4637Content-Type: application/x-foo; name=\"Frank's Document\"
4638
4639"""
4640 msg = email.message_from_string(m)
4641 eq(msg.get_param('name'), "Frank's Document")
4642
4643 def test_rfc2231_encoded_then_unencoded_segments(self):
4644 eq = self.assertEqual
4645 m = """\
4646Content-Type: application/x-foo;
4647\tname*0*=\"us-ascii'en-us'My\";
4648\tname*1=\" Document\";
4649\tname*2*=\" For You\"
4650
4651"""
4652 msg = email.message_from_string(m)
4653 charset, language, s = msg.get_param('name')
4654 eq(charset, 'us-ascii')
4655 eq(language, 'en-us')
4656 eq(s, 'My Document For You')
4657
4658 def test_rfc2231_unencoded_then_encoded_segments(self):
4659 eq = self.assertEqual
4660 m = """\
4661Content-Type: application/x-foo;
4662\tname*0=\"us-ascii'en-us'My\";
4663\tname*1*=\" Document\";
4664\tname*2*=\" For You\"
4665
4666"""
4667 msg = email.message_from_string(m)
4668 charset, language, s = msg.get_param('name')
4669 eq(charset, 'us-ascii')
4670 eq(language, 'en-us')
4671 eq(s, 'My Document For You')
4672
4673
Ezio Melottib3aedd42010-11-20 19:04:17 +00004674
R. David Murraya8f480f2010-01-16 18:30:03 +00004675# Tests to ensure that signed parts of an email are completely preserved, as
4676# required by RFC1847 section 2.1. Note that these are incomplete, because the
4677# email package does not currently always preserve the body. See issue 1670765.
4678class TestSigned(TestEmailBase):
4679
4680 def _msg_and_obj(self, filename):
4681 with openfile(findfile(filename)) as fp:
4682 original = fp.read()
4683 msg = email.message_from_string(original)
4684 return original, msg
4685
4686 def _signed_parts_eq(self, original, result):
4687 # Extract the first mime part of each message
4688 import re
4689 repart = re.compile(r'^--([^\n]+)\n(.*?)\n--\1$', re.S | re.M)
4690 inpart = repart.search(original).group(2)
4691 outpart = repart.search(result).group(2)
4692 self.assertEqual(outpart, inpart)
4693
4694 def test_long_headers_as_string(self):
4695 original, msg = self._msg_and_obj('msg_45.txt')
4696 result = msg.as_string()
4697 self._signed_parts_eq(original, result)
4698
4699 def test_long_headers_as_string_maxheaderlen(self):
4700 original, msg = self._msg_and_obj('msg_45.txt')
4701 result = msg.as_string(maxheaderlen=60)
4702 self._signed_parts_eq(original, result)
4703
4704 def test_long_headers_flatten(self):
4705 original, msg = self._msg_and_obj('msg_45.txt')
4706 fp = StringIO()
4707 Generator(fp).flatten(msg)
4708 result = fp.getvalue()
4709 self._signed_parts_eq(original, result)
4710
4711
Ezio Melottib3aedd42010-11-20 19:04:17 +00004712
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004713def _testclasses():
4714 mod = sys.modules[__name__]
4715 return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
4716
4717
4718def suite():
4719 suite = unittest.TestSuite()
4720 for testclass in _testclasses():
4721 suite.addTest(unittest.makeSuite(testclass))
4722 return suite
4723
4724
4725def test_main():
4726 for testclass in _testclasses():
4727 run_unittest(testclass)
4728
4729
Ezio Melottib3aedd42010-11-20 19:04:17 +00004730
Guido van Rossum8b3febe2007-08-30 01:15:14 +00004731if __name__ == '__main__':
4732 unittest.main(defaultTest='suite')