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