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