blob: 4b5065f942b6dea86c71bab001b27500f1c44782 [file] [log] [blame]
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001# Copyright (C) 2001-2007 Python Software Foundation
2# 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):
56 """Like failUnlessEqual except use ndiff for readable output."""
57 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
181 def test_get_decoded_payload(self):
182 eq = self.assertEqual
183 msg = self._msgobj('msg_10.txt')
184 # The outer message is a multipart
185 eq(msg.get_payload(decode=True), None)
186 # Subpart 1 is 7bit encoded
187 eq(msg.get_payload(0).get_payload(decode=True),
188 b'This is a 7bit encoded message.\n')
189 # Subpart 2 is quopri
190 eq(msg.get_payload(1).get_payload(decode=True),
191 b'\xa1This is a Quoted Printable encoded message!\n')
192 # Subpart 3 is base64
193 eq(msg.get_payload(2).get_payload(decode=True),
194 b'This is a Base64 encoded message.')
195 # Subpart 4 has no Content-Transfer-Encoding: header.
196 eq(msg.get_payload(3).get_payload(decode=True),
197 b'This has no Content-Transfer-Encoding: header.\n')
198
199 def test_get_decoded_uu_payload(self):
200 eq = self.assertEqual
201 msg = Message()
202 msg.set_payload('begin 666 -\n+:&5L;&\\@=V]R;&0 \n \nend\n')
203 for cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):
204 msg['content-transfer-encoding'] = cte
205 eq(msg.get_payload(decode=True), b'hello world')
206 # Now try some bogus data
207 msg.set_payload('foo')
208 eq(msg.get_payload(decode=True), b'foo')
209
210 def test_decoded_generator(self):
211 eq = self.assertEqual
212 msg = self._msgobj('msg_07.txt')
213 with openfile('msg_17.txt') as fp:
214 text = fp.read()
215 s = StringIO()
216 g = DecodedGenerator(s)
217 g.flatten(msg)
218 eq(s.getvalue(), text)
219
220 def test__contains__(self):
221 msg = Message()
222 msg['From'] = 'Me'
223 msg['to'] = 'You'
224 # Check for case insensitivity
225 self.failUnless('from' in msg)
226 self.failUnless('From' in msg)
227 self.failUnless('FROM' in msg)
228 self.failUnless('to' in msg)
229 self.failUnless('To' in msg)
230 self.failUnless('TO' in msg)
231
232 def test_as_string(self):
233 eq = self.ndiffAssertEqual
234 msg = self._msgobj('msg_01.txt')
235 with openfile('msg_01.txt') as fp:
236 text = fp.read()
237 eq(text, str(msg))
238 fullrepr = msg.as_string(unixfrom=True)
239 lines = fullrepr.split('\n')
240 self.failUnless(lines[0].startswith('From '))
241 eq(text, NL.join(lines[1:]))
242
243 def test_bad_param(self):
244 msg = email.message_from_string("Content-Type: blarg; baz; boo\n")
245 self.assertEqual(msg.get_param('baz'), '')
246
247 def test_missing_filename(self):
248 msg = email.message_from_string("From: foo\n")
249 self.assertEqual(msg.get_filename(), None)
250
251 def test_bogus_filename(self):
252 msg = email.message_from_string(
253 "Content-Disposition: blarg; filename\n")
254 self.assertEqual(msg.get_filename(), '')
255
256 def test_missing_boundary(self):
257 msg = email.message_from_string("From: foo\n")
258 self.assertEqual(msg.get_boundary(), None)
259
260 def test_get_params(self):
261 eq = self.assertEqual
262 msg = email.message_from_string(
263 'X-Header: foo=one; bar=two; baz=three\n')
264 eq(msg.get_params(header='x-header'),
265 [('foo', 'one'), ('bar', 'two'), ('baz', 'three')])
266 msg = email.message_from_string(
267 'X-Header: foo; bar=one; baz=two\n')
268 eq(msg.get_params(header='x-header'),
269 [('foo', ''), ('bar', 'one'), ('baz', 'two')])
270 eq(msg.get_params(), None)
271 msg = email.message_from_string(
272 'X-Header: foo; bar="one"; baz=two\n')
273 eq(msg.get_params(header='x-header'),
274 [('foo', ''), ('bar', 'one'), ('baz', 'two')])
275
276 def test_get_param_liberal(self):
277 msg = Message()
278 msg['Content-Type'] = 'Content-Type: Multipart/mixed; boundary = "CPIMSSMTPC06p5f3tG"'
279 self.assertEqual(msg.get_param('boundary'), 'CPIMSSMTPC06p5f3tG')
280
281 def test_get_param(self):
282 eq = self.assertEqual
283 msg = email.message_from_string(
284 "X-Header: foo=one; bar=two; baz=three\n")
285 eq(msg.get_param('bar', header='x-header'), 'two')
286 eq(msg.get_param('quuz', header='x-header'), None)
287 eq(msg.get_param('quuz'), None)
288 msg = email.message_from_string(
289 'X-Header: foo; bar="one"; baz=two\n')
290 eq(msg.get_param('foo', header='x-header'), '')
291 eq(msg.get_param('bar', header='x-header'), 'one')
292 eq(msg.get_param('baz', header='x-header'), 'two')
293 # XXX: We are not RFC-2045 compliant! We cannot parse:
294 # msg["Content-Type"] = 'text/plain; weird="hey; dolly? [you] @ <\\"home\\">?"'
295 # msg.get_param("weird")
296 # yet.
297
298 def test_get_param_funky_continuation_lines(self):
299 msg = self._msgobj('msg_22.txt')
300 self.assertEqual(msg.get_payload(1).get_param('name'), 'wibble.JPG')
301
302 def test_get_param_with_semis_in_quotes(self):
303 msg = email.message_from_string(
304 'Content-Type: image/pjpeg; name="Jim&amp;&amp;Jill"\n')
305 self.assertEqual(msg.get_param('name'), 'Jim&amp;&amp;Jill')
306 self.assertEqual(msg.get_param('name', unquote=False),
307 '"Jim&amp;&amp;Jill"')
308
309 def test_field_containment(self):
310 unless = self.failUnless
311 msg = email.message_from_string('Header: exists')
312 unless('header' in msg)
313 unless('Header' in msg)
314 unless('HEADER' in msg)
315 self.failIf('headerx' in msg)
316
317 def test_set_param(self):
318 eq = self.assertEqual
319 msg = Message()
320 msg.set_param('charset', 'iso-2022-jp')
321 eq(msg.get_param('charset'), 'iso-2022-jp')
322 msg.set_param('importance', 'high value')
323 eq(msg.get_param('importance'), 'high value')
324 eq(msg.get_param('importance', unquote=False), '"high value"')
325 eq(msg.get_params(), [('text/plain', ''),
326 ('charset', 'iso-2022-jp'),
327 ('importance', 'high value')])
328 eq(msg.get_params(unquote=False), [('text/plain', ''),
329 ('charset', '"iso-2022-jp"'),
330 ('importance', '"high value"')])
331 msg.set_param('charset', 'iso-9999-xx', header='X-Jimmy')
332 eq(msg.get_param('charset', header='X-Jimmy'), 'iso-9999-xx')
333
334 def test_del_param(self):
335 eq = self.assertEqual
336 msg = self._msgobj('msg_05.txt')
337 eq(msg.get_params(),
338 [('multipart/report', ''), ('report-type', 'delivery-status'),
339 ('boundary', 'D1690A7AC1.996856090/mail.example.com')])
340 old_val = msg.get_param("report-type")
341 msg.del_param("report-type")
342 eq(msg.get_params(),
343 [('multipart/report', ''),
344 ('boundary', 'D1690A7AC1.996856090/mail.example.com')])
345 msg.set_param("report-type", old_val)
346 eq(msg.get_params(),
347 [('multipart/report', ''),
348 ('boundary', 'D1690A7AC1.996856090/mail.example.com'),
349 ('report-type', old_val)])
350
351 def test_del_param_on_other_header(self):
352 msg = Message()
353 msg.add_header('Content-Disposition', 'attachment', filename='bud.gif')
354 msg.del_param('filename', 'content-disposition')
355 self.assertEqual(msg['content-disposition'], 'attachment')
356
357 def test_set_type(self):
358 eq = self.assertEqual
359 msg = Message()
360 self.assertRaises(ValueError, msg.set_type, 'text')
361 msg.set_type('text/plain')
362 eq(msg['content-type'], 'text/plain')
363 msg.set_param('charset', 'us-ascii')
364 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
365 msg.set_type('text/html')
366 eq(msg['content-type'], 'text/html; charset="us-ascii"')
367
368 def test_set_type_on_other_header(self):
369 msg = Message()
370 msg['X-Content-Type'] = 'text/plain'
371 msg.set_type('application/octet-stream', 'X-Content-Type')
372 self.assertEqual(msg['x-content-type'], 'application/octet-stream')
373
374 def test_get_content_type_missing(self):
375 msg = Message()
376 self.assertEqual(msg.get_content_type(), 'text/plain')
377
378 def test_get_content_type_missing_with_default_type(self):
379 msg = Message()
380 msg.set_default_type('message/rfc822')
381 self.assertEqual(msg.get_content_type(), 'message/rfc822')
382
383 def test_get_content_type_from_message_implicit(self):
384 msg = self._msgobj('msg_30.txt')
385 self.assertEqual(msg.get_payload(0).get_content_type(),
386 'message/rfc822')
387
388 def test_get_content_type_from_message_explicit(self):
389 msg = self._msgobj('msg_28.txt')
390 self.assertEqual(msg.get_payload(0).get_content_type(),
391 'message/rfc822')
392
393 def test_get_content_type_from_message_text_plain_implicit(self):
394 msg = self._msgobj('msg_03.txt')
395 self.assertEqual(msg.get_content_type(), 'text/plain')
396
397 def test_get_content_type_from_message_text_plain_explicit(self):
398 msg = self._msgobj('msg_01.txt')
399 self.assertEqual(msg.get_content_type(), 'text/plain')
400
401 def test_get_content_maintype_missing(self):
402 msg = Message()
403 self.assertEqual(msg.get_content_maintype(), 'text')
404
405 def test_get_content_maintype_missing_with_default_type(self):
406 msg = Message()
407 msg.set_default_type('message/rfc822')
408 self.assertEqual(msg.get_content_maintype(), 'message')
409
410 def test_get_content_maintype_from_message_implicit(self):
411 msg = self._msgobj('msg_30.txt')
412 self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message')
413
414 def test_get_content_maintype_from_message_explicit(self):
415 msg = self._msgobj('msg_28.txt')
416 self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message')
417
418 def test_get_content_maintype_from_message_text_plain_implicit(self):
419 msg = self._msgobj('msg_03.txt')
420 self.assertEqual(msg.get_content_maintype(), 'text')
421
422 def test_get_content_maintype_from_message_text_plain_explicit(self):
423 msg = self._msgobj('msg_01.txt')
424 self.assertEqual(msg.get_content_maintype(), 'text')
425
426 def test_get_content_subtype_missing(self):
427 msg = Message()
428 self.assertEqual(msg.get_content_subtype(), 'plain')
429
430 def test_get_content_subtype_missing_with_default_type(self):
431 msg = Message()
432 msg.set_default_type('message/rfc822')
433 self.assertEqual(msg.get_content_subtype(), 'rfc822')
434
435 def test_get_content_subtype_from_message_implicit(self):
436 msg = self._msgobj('msg_30.txt')
437 self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822')
438
439 def test_get_content_subtype_from_message_explicit(self):
440 msg = self._msgobj('msg_28.txt')
441 self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822')
442
443 def test_get_content_subtype_from_message_text_plain_implicit(self):
444 msg = self._msgobj('msg_03.txt')
445 self.assertEqual(msg.get_content_subtype(), 'plain')
446
447 def test_get_content_subtype_from_message_text_plain_explicit(self):
448 msg = self._msgobj('msg_01.txt')
449 self.assertEqual(msg.get_content_subtype(), 'plain')
450
451 def test_get_content_maintype_error(self):
452 msg = Message()
453 msg['Content-Type'] = 'no-slash-in-this-string'
454 self.assertEqual(msg.get_content_maintype(), 'text')
455
456 def test_get_content_subtype_error(self):
457 msg = Message()
458 msg['Content-Type'] = 'no-slash-in-this-string'
459 self.assertEqual(msg.get_content_subtype(), 'plain')
460
461 def test_replace_header(self):
462 eq = self.assertEqual
463 msg = Message()
464 msg.add_header('First', 'One')
465 msg.add_header('Second', 'Two')
466 msg.add_header('Third', 'Three')
467 eq(msg.keys(), ['First', 'Second', 'Third'])
468 eq(msg.values(), ['One', 'Two', 'Three'])
469 msg.replace_header('Second', 'Twenty')
470 eq(msg.keys(), ['First', 'Second', 'Third'])
471 eq(msg.values(), ['One', 'Twenty', 'Three'])
472 msg.add_header('First', 'Eleven')
473 msg.replace_header('First', 'One Hundred')
474 eq(msg.keys(), ['First', 'Second', 'Third', 'First'])
475 eq(msg.values(), ['One Hundred', 'Twenty', 'Three', 'Eleven'])
476 self.assertRaises(KeyError, msg.replace_header, 'Fourth', 'Missing')
477
478 def test_broken_base64_payload(self):
479 x = 'AwDp0P7//y6LwKEAcPa/6Q=9'
480 msg = Message()
481 msg['content-type'] = 'audio/x-midi'
482 msg['content-transfer-encoding'] = 'base64'
483 msg.set_payload(x)
484 self.assertEqual(msg.get_payload(decode=True),
Guido van Rossum9604e662007-08-30 03:46:43 +0000485 bytes(x, 'raw-unicode-escape'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000486
487
488
489# Test the email.encoders module
490class TestEncoders(unittest.TestCase):
491 def test_encode_empty_payload(self):
492 eq = self.assertEqual
493 msg = Message()
494 msg.set_charset('us-ascii')
495 eq(msg['content-transfer-encoding'], '7bit')
496
497 def test_default_cte(self):
498 eq = self.assertEqual
499 msg = MIMEText('hello world')
500 eq(msg['content-transfer-encoding'], '7bit')
501
502 def test_default_cte(self):
503 eq = self.assertEqual
504 # With no explicit _charset its us-ascii, and all are 7-bit
505 msg = MIMEText('hello world')
506 eq(msg['content-transfer-encoding'], '7bit')
507 # Similar, but with 8-bit data
508 msg = MIMEText('hello \xf8 world')
509 eq(msg['content-transfer-encoding'], '8bit')
510 # And now with a different charset
511 msg = MIMEText('hello \xf8 world', _charset='iso-8859-1')
512 eq(msg['content-transfer-encoding'], 'quoted-printable')
513
514
515
516# Test long header wrapping
517class TestLongHeaders(TestEmailBase):
518 def test_split_long_continuation(self):
519 eq = self.ndiffAssertEqual
520 msg = email.message_from_string("""\
521Subject: bug demonstration
522\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
523\tmore text
524
525test
526""")
527 sfp = StringIO()
528 g = Generator(sfp)
529 g.flatten(msg)
530 eq(sfp.getvalue(), """\
531Subject: bug demonstration
532\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
533\tmore text
534
535test
536""")
537
538 def test_another_long_almost_unsplittable_header(self):
539 eq = self.ndiffAssertEqual
540 hstr = """\
541bug demonstration
542\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
543\tmore text"""
544 h = Header(hstr, continuation_ws='\t')
545 eq(h.encode(), """\
546bug demonstration
547\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
548\tmore text""")
549 h = Header(hstr.replace('\t', ' '))
550 eq(h.encode(), """\
551bug demonstration
552 12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
553 more text""")
554
555 def test_long_nonstring(self):
556 eq = self.ndiffAssertEqual
557 g = Charset("iso-8859-1")
558 cz = Charset("iso-8859-2")
559 utf8 = Charset("utf-8")
560 g_head = (b'Die Mieter treten hier ein werden mit einem Foerderband '
561 b'komfortabel den Korridor entlang, an s\xfcdl\xfcndischen '
562 b'Wandgem\xe4lden vorbei, gegen die rotierenden Klingen '
563 b'bef\xf6rdert. ')
564 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
565 b'd\xf9vtipu.. ')
566 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
567 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
568 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
569 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
570 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
571 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
572 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
573 '\u3044\u307e\u3059\u3002')
574 h = Header(g_head, g, header_name='Subject')
575 h.append(cz_head, cz)
576 h.append(utf8_head, utf8)
577 msg = Message()
578 msg['Subject'] = h
579 sfp = StringIO()
580 g = Generator(sfp)
581 g.flatten(msg)
582 eq(sfp.getvalue(), """\
Guido van Rossum9604e662007-08-30 03:46:43 +0000583Subject: =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderb?=
584 =?iso-8859-1?q?and_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen?=
585 =?iso-8859-1?q?_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef?=
586 =?iso-8859-1?q?=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hrouti?=
587 =?iso-8859-2?q?ly_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
588 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC5LiA?=
589 =?utf-8?b?6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn44Gf44KJ?=
590 =?utf-8?b?44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFzIE51bnN0dWNr?=
591 =?utf-8?b?IGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5kIGRhcyBPZGVyIGRp?=
592 =?utf-8?b?ZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIDjgaPjgabjgYTjgb7jgZk=?=
593 =?utf-8?b?44CC?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000594
595""")
Guido van Rossum9604e662007-08-30 03:46:43 +0000596 eq(h.encode(maxlinelen=76), """\
597=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerde?=
598 =?iso-8859-1?q?rband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndis?=
599 =?iso-8859-1?q?chen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klinge?=
600 =?iso-8859-1?q?n_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se?=
601 =?iso-8859-2?q?_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?=
602 =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb?=
603 =?utf-8?b?44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go?=
604 =?utf-8?b?44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBp?=
605 =?utf-8?b?c3QgZGFzIE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWlo?=
606 =?utf-8?b?ZXJodW5kIGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI0=?=
607 =?utf-8?b?44Go6KiA44Gj44Gm44GE44G+44GZ44CC?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000608
609 def test_long_header_encode(self):
610 eq = self.ndiffAssertEqual
611 h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
612 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
613 header_name='X-Foobar-Spoink-Defrobnit')
614 eq(h.encode(), '''\
615wasnipoop; giraffes="very-long-necked-animals";
616 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
617
618 def test_long_header_encode_with_tab_continuation_is_just_a_hint(self):
619 eq = self.ndiffAssertEqual
620 h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
621 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
622 header_name='X-Foobar-Spoink-Defrobnit',
623 continuation_ws='\t')
624 eq(h.encode(), '''\
625wasnipoop; giraffes="very-long-necked-animals";
626 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
627
628 def test_long_header_encode_with_tab_continuation(self):
629 eq = self.ndiffAssertEqual
630 h = Header('wasnipoop; giraffes="very-long-necked-animals";\t'
631 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
632 header_name='X-Foobar-Spoink-Defrobnit',
633 continuation_ws='\t')
634 eq(h.encode(), '''\
635wasnipoop; giraffes="very-long-necked-animals";
636\tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
637
638 def test_header_splitter(self):
639 eq = self.ndiffAssertEqual
640 msg = MIMEText('')
641 # It'd be great if we could use add_header() here, but that doesn't
642 # guarantee an order of the parameters.
643 msg['X-Foobar-Spoink-Defrobnit'] = (
644 'wasnipoop; giraffes="very-long-necked-animals"; '
645 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"')
646 sfp = StringIO()
647 g = Generator(sfp)
648 g.flatten(msg)
649 eq(sfp.getvalue(), '''\
650Content-Type: text/plain; charset="us-ascii"
651MIME-Version: 1.0
652Content-Transfer-Encoding: 7bit
653X-Foobar-Spoink-Defrobnit: wasnipoop; giraffes="very-long-necked-animals";
654 spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"
655
656''')
657
658 def test_no_semis_header_splitter(self):
659 eq = self.ndiffAssertEqual
660 msg = Message()
661 msg['From'] = 'test@dom.ain'
662 msg['References'] = SPACE.join('<%d@dom.ain>' % i for i in range(10))
663 msg.set_payload('Test')
664 sfp = StringIO()
665 g = Generator(sfp)
666 g.flatten(msg)
667 eq(sfp.getvalue(), """\
668From: test@dom.ain
669References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain>
670 <5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain>
671
672Test""")
673
674 def test_no_split_long_header(self):
675 eq = self.ndiffAssertEqual
676 hstr = 'References: ' + 'x' * 80
Guido van Rossum9604e662007-08-30 03:46:43 +0000677 h = Header(hstr)
678 # These come on two lines because Headers are really field value
679 # classes and don't really know about their field names.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000680 eq(h.encode(), """\
Guido van Rossum9604e662007-08-30 03:46:43 +0000681References:
682 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""")
683 h = Header('x' * 80)
684 eq(h.encode(), 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000685
686 def test_splitting_multiple_long_lines(self):
687 eq = self.ndiffAssertEqual
688 hstr = """\
689from 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)
690\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)
691\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)
692"""
693 h = Header(hstr, continuation_ws='\t')
694 eq(h.encode(), """\
695from babylon.socal-raves.org (localhost [127.0.0.1]);
696 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
697 for <mailman-admin@babylon.socal-raves.org>;
698 Sat, 2 Feb 2002 17:00:06 -0800 (PST)
699\tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
700 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
701 for <mailman-admin@babylon.socal-raves.org>;
702 Sat, 2 Feb 2002 17:00:06 -0800 (PST)
703\tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
704 by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
705 for <mailman-admin@babylon.socal-raves.org>;
706 Sat, 2 Feb 2002 17:00:06 -0800 (PST)""")
707
708 def test_splitting_first_line_only_is_long(self):
709 eq = self.ndiffAssertEqual
710 hstr = """\
711from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca)
712\tby kronos.mems-exchange.org with esmtp (Exim 4.05)
713\tid 17k4h5-00034i-00
714\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400"""
715 h = Header(hstr, maxlinelen=78, header_name='Received',
716 continuation_ws='\t')
717 eq(h.encode(), """\
718from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93]
719 helo=cthulhu.gerg.ca)
720\tby kronos.mems-exchange.org with esmtp (Exim 4.05)
721\tid 17k4h5-00034i-00
722\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""")
723
724 def test_long_8bit_header(self):
725 eq = self.ndiffAssertEqual
726 msg = Message()
727 h = Header('Britische Regierung gibt', 'iso-8859-1',
728 header_name='Subject')
729 h.append('gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte')
Guido van Rossum9604e662007-08-30 03:46:43 +0000730 eq(h.encode(maxlinelen=76), """\
731=?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offs?=
732 =?iso-8859-1?q?hore-Windkraftprojekte?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000733 msg['Subject'] = h
Guido van Rossum9604e662007-08-30 03:46:43 +0000734 eq(msg.as_string(maxheaderlen=76), """\
735Subject: =?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offs?=
736 =?iso-8859-1?q?hore-Windkraftprojekte?=
737
738""")
739 eq(msg.as_string(maxheaderlen=0), """\
740Subject: =?iso-8859-1?q?Britische_Regierung_gibt_gr=FCnes_Licht_f=FCr_Offshore-Windkraftprojekte?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000741
742""")
743
744 def test_long_8bit_header_no_charset(self):
745 eq = self.ndiffAssertEqual
746 msg = Message()
Barry Warsaw8c571042007-08-30 19:17:18 +0000747 header_string = ('Britische Regierung gibt gr\xfcnes Licht '
748 'f\xfcr Offshore-Windkraftprojekte '
749 '<a-very-long-address@example.com>')
750 msg['Reply-To'] = header_string
751 self.assertRaises(UnicodeEncodeError, msg.as_string)
752 msg = Message()
753 msg['Reply-To'] = Header(header_string, 'utf-8',
754 header_name='Reply-To')
755 eq(msg.as_string(maxheaderlen=78), """\
756Reply-To: =?utf-8?q?Britische_Regierung_gibt_gr=C3=BCnes_Licht_f=C3=BCr_Offs?=
757 =?utf-8?q?hore-Windkraftprojekte_=3Ca-very-long-address=40example=2Ecom=3E?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000758
759""")
760
761 def test_long_to_header(self):
762 eq = self.ndiffAssertEqual
763 to = ('"Someone Test #A" <someone@eecs.umich.edu>,'
764 '<someone@eecs.umich.edu>,'
765 '"Someone Test #B" <someone@umich.edu>, '
766 '"Someone Test #C" <someone@eecs.umich.edu>, '
767 '"Someone Test #D" <someone@eecs.umich.edu>')
768 msg = Message()
769 msg['To'] = to
770 eq(msg.as_string(maxheaderlen=78), '''\
Guido van Rossum9604e662007-08-30 03:46:43 +0000771To: "Someone Test #A" <someone@eecs.umich.edu>,<someone@eecs.umich.edu>,
Barry Warsaw70d61ce2009-03-30 23:12:30 +0000772 "Someone Test #B" <someone@umich.edu>,
Guido van Rossum9604e662007-08-30 03:46:43 +0000773 "Someone Test #C" <someone@eecs.umich.edu>,
774 "Someone Test #D" <someone@eecs.umich.edu>
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000775
776''')
777
778 def test_long_line_after_append(self):
779 eq = self.ndiffAssertEqual
780 s = 'This is an example of string which has almost the limit of header length.'
781 h = Header(s)
782 h.append('Add another line.')
Guido van Rossum9604e662007-08-30 03:46:43 +0000783 eq(h.encode(maxlinelen=76), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000784This is an example of string which has almost the limit of header length.
785 Add another line.""")
786
787 def test_shorter_line_with_append(self):
788 eq = self.ndiffAssertEqual
789 s = 'This is a shorter line.'
790 h = Header(s)
791 h.append('Add another sentence. (Surprise?)')
792 eq(h.encode(),
793 'This is a shorter line. Add another sentence. (Surprise?)')
794
795 def test_long_field_name(self):
796 eq = self.ndiffAssertEqual
797 fn = 'X-Very-Very-Very-Long-Header-Name'
Guido van Rossum9604e662007-08-30 03:46:43 +0000798 gs = ('Die Mieter treten hier ein werden mit einem Foerderband '
799 'komfortabel den Korridor entlang, an s\xfcdl\xfcndischen '
800 'Wandgem\xe4lden vorbei, gegen die rotierenden Klingen '
801 'bef\xf6rdert. ')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000802 h = Header(gs, 'iso-8859-1', header_name=fn)
803 # BAW: this seems broken because the first line is too long
Guido van Rossum9604e662007-08-30 03:46:43 +0000804 eq(h.encode(maxlinelen=76), """\
805=?iso-8859-1?q?Die_Mieter_treten_hier_e?=
806 =?iso-8859-1?q?in_werden_mit_einem_Foerderband_komfortabel_den_Korridor_e?=
807 =?iso-8859-1?q?ntlang=2C_an_s=FCdl=FCndischen_Wandgem=E4lden_vorbei=2C_ge?=
808 =?iso-8859-1?q?gen_die_rotierenden_Klingen_bef=F6rdert=2E_?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000809
810 def test_long_received_header(self):
811 h = ('from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) '
812 'by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP; '
813 'Wed, 05 Mar 2003 18:10:18 -0700')
814 msg = Message()
815 msg['Received-1'] = Header(h, continuation_ws='\t')
816 msg['Received-2'] = h
Barry Warsawbef9d212007-08-31 10:55:37 +0000817 # This should be splitting on spaces not semicolons.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000818 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
Barry Warsawbef9d212007-08-31 10:55:37 +0000819Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
820 Wed, 05 Mar 2003 18:10:18 -0700
821Received-2: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
822 Wed, 05 Mar 2003 18:10:18 -0700
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000823
824""")
825
826 def test_string_headerinst_eq(self):
827 h = ('<15975.17901.207240.414604@sgigritzmann1.mathematik.'
828 'tu-muenchen.de> (David Bremner\'s message of '
829 '"Thu, 6 Mar 2003 13:58:21 +0100")')
830 msg = Message()
831 msg['Received-1'] = Header(h, header_name='Received-1',
832 continuation_ws='\t')
833 msg['Received-2'] = h
Barry Warsawbef9d212007-08-31 10:55:37 +0000834 # XXX This should be splitting on spaces not commas.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000835 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
Barry Warsawbef9d212007-08-31 10:55:37 +0000836Received-1: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner's message of \"Thu,
837 6 Mar 2003 13:58:21 +0100\")
838Received-2: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner's message of \"Thu,
839 6 Mar 2003 13:58:21 +0100\")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000840
841""")
842
843 def test_long_unbreakable_lines_with_continuation(self):
844 eq = self.ndiffAssertEqual
845 msg = Message()
846 t = """\
847iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
848 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp"""
849 msg['Face-1'] = t
850 msg['Face-2'] = Header(t, header_name='Face-2')
Barry Warsawbef9d212007-08-31 10:55:37 +0000851 # XXX This splitting is all wrong. It the first value line should be
852 # snug against the field name.
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000853 eq(msg.as_string(maxheaderlen=78), """\
Barry Warsawc5a6a302007-08-31 11:19:21 +0000854Face-1:\x20
Barry Warsaw70d61ce2009-03-30 23:12:30 +0000855 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000856 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
Barry Warsawc5a6a302007-08-31 11:19:21 +0000857Face-2:\x20
Barry Warsawbef9d212007-08-31 10:55:37 +0000858 iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000859 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
860
861""")
862
863 def test_another_long_multiline_header(self):
864 eq = self.ndiffAssertEqual
865 m = ('Received: from siimage.com '
866 '([172.25.1.3]) by zima.siliconimage.com with '
Guido van Rossum9604e662007-08-30 03:46:43 +0000867 'Microsoft SMTPSVC(5.0.2195.4905); '
868 'Wed, 16 Oct 2002 07:41:11 -0700')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000869 msg = email.message_from_string(m)
870 eq(msg.as_string(maxheaderlen=78), '''\
Barry Warsawbef9d212007-08-31 10:55:37 +0000871Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with Microsoft SMTPSVC(5.0.2195.4905);
872 Wed, 16 Oct 2002 07:41:11 -0700
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000873
874''')
875
876 def test_long_lines_with_different_header(self):
877 eq = self.ndiffAssertEqual
878 h = ('List-Unsubscribe: '
879 '<http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,'
880 ' <mailto:spamassassin-talk-request@lists.sourceforge.net'
881 '?subject=unsubscribe>')
882 msg = Message()
883 msg['List'] = h
884 msg['List'] = Header(h, header_name='List')
885 eq(msg.as_string(maxheaderlen=78), """\
886List: List-Unsubscribe: <http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
Barry Warsawbef9d212007-08-31 10:55:37 +0000887 <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000888List: List-Unsubscribe: <http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
Barry Warsawbef9d212007-08-31 10:55:37 +0000889 <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000890
891""")
892
893
894
895# Test mangling of "From " lines in the body of a message
896class TestFromMangling(unittest.TestCase):
897 def setUp(self):
898 self.msg = Message()
899 self.msg['From'] = 'aaa@bbb.org'
900 self.msg.set_payload("""\
901From the desk of A.A.A.:
902Blah blah blah
903""")
904
905 def test_mangled_from(self):
906 s = StringIO()
907 g = Generator(s, mangle_from_=True)
908 g.flatten(self.msg)
909 self.assertEqual(s.getvalue(), """\
910From: aaa@bbb.org
911
912>From the desk of A.A.A.:
913Blah blah blah
914""")
915
916 def test_dont_mangle_from(self):
917 s = StringIO()
918 g = Generator(s, mangle_from_=False)
919 g.flatten(self.msg)
920 self.assertEqual(s.getvalue(), """\
921From: aaa@bbb.org
922
923From the desk of A.A.A.:
924Blah blah blah
925""")
926
927
928
929# Test the basic MIMEAudio class
930class TestMIMEAudio(unittest.TestCase):
931 def setUp(self):
932 # Make sure we pick up the audiotest.au that lives in email/test/data.
933 # In Python, there's an audiotest.au living in Lib/test but that isn't
934 # included in some binary distros that don't include the test
935 # package. The trailing empty string on the .join() is significant
936 # since findfile() will do a dirname().
937 datadir = os.path.join(os.path.dirname(landmark), 'data', '')
938 with open(findfile('audiotest.au', datadir), 'rb') as fp:
939 self._audiodata = fp.read()
940 self._au = MIMEAudio(self._audiodata)
941
942 def test_guess_minor_type(self):
943 self.assertEqual(self._au.get_content_type(), 'audio/basic')
944
945 def test_encoding(self):
946 payload = self._au.get_payload()
947 self.assertEqual(base64.decodestring(payload), self._audiodata)
948
949 def test_checkSetMinor(self):
950 au = MIMEAudio(self._audiodata, 'fish')
951 self.assertEqual(au.get_content_type(), 'audio/fish')
952
953 def test_add_header(self):
954 eq = self.assertEqual
955 unless = self.failUnless
956 self._au.add_header('Content-Disposition', 'attachment',
957 filename='audiotest.au')
958 eq(self._au['content-disposition'],
959 'attachment; filename="audiotest.au"')
960 eq(self._au.get_params(header='content-disposition'),
961 [('attachment', ''), ('filename', 'audiotest.au')])
962 eq(self._au.get_param('filename', header='content-disposition'),
963 'audiotest.au')
964 missing = []
965 eq(self._au.get_param('attachment', header='content-disposition'), '')
966 unless(self._au.get_param('foo', failobj=missing,
967 header='content-disposition') is missing)
968 # Try some missing stuff
969 unless(self._au.get_param('foobar', missing) is missing)
970 unless(self._au.get_param('attachment', missing,
971 header='foobar') is missing)
972
973
974
975# Test the basic MIMEImage class
976class TestMIMEImage(unittest.TestCase):
977 def setUp(self):
978 with openfile('PyBanner048.gif', 'rb') as fp:
979 self._imgdata = fp.read()
980 self._im = MIMEImage(self._imgdata)
981
982 def test_guess_minor_type(self):
983 self.assertEqual(self._im.get_content_type(), 'image/gif')
984
985 def test_encoding(self):
986 payload = self._im.get_payload()
987 self.assertEqual(base64.decodestring(payload), self._imgdata)
988
989 def test_checkSetMinor(self):
990 im = MIMEImage(self._imgdata, 'fish')
991 self.assertEqual(im.get_content_type(), 'image/fish')
992
993 def test_add_header(self):
994 eq = self.assertEqual
995 unless = self.failUnless
996 self._im.add_header('Content-Disposition', 'attachment',
997 filename='dingusfish.gif')
998 eq(self._im['content-disposition'],
999 'attachment; filename="dingusfish.gif"')
1000 eq(self._im.get_params(header='content-disposition'),
1001 [('attachment', ''), ('filename', 'dingusfish.gif')])
1002 eq(self._im.get_param('filename', header='content-disposition'),
1003 'dingusfish.gif')
1004 missing = []
1005 eq(self._im.get_param('attachment', header='content-disposition'), '')
1006 unless(self._im.get_param('foo', failobj=missing,
1007 header='content-disposition') is missing)
1008 # Try some missing stuff
1009 unless(self._im.get_param('foobar', missing) is missing)
1010 unless(self._im.get_param('attachment', missing,
1011 header='foobar') is missing)
1012
1013
1014
1015# Test the basic MIMEApplication class
1016class TestMIMEApplication(unittest.TestCase):
1017 def test_headers(self):
1018 eq = self.assertEqual
Barry Warsaw8b2af272007-08-31 03:04:26 +00001019 msg = MIMEApplication(b'\xfa\xfb\xfc\xfd\xfe\xff')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001020 eq(msg.get_content_type(), 'application/octet-stream')
1021 eq(msg['content-transfer-encoding'], 'base64')
1022
1023 def test_body(self):
1024 eq = self.assertEqual
Barry Warsaw8c571042007-08-30 19:17:18 +00001025 bytes = b'\xfa\xfb\xfc\xfd\xfe\xff'
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001026 msg = MIMEApplication(bytes)
Barry Warsaw8c571042007-08-30 19:17:18 +00001027 eq(msg.get_payload(), b'+vv8/f7/')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001028 eq(msg.get_payload(decode=True), bytes)
1029
1030
1031
1032# Test the basic MIMEText class
1033class TestMIMEText(unittest.TestCase):
1034 def setUp(self):
1035 self._msg = MIMEText('hello there')
1036
1037 def test_types(self):
1038 eq = self.assertEqual
1039 unless = self.failUnless
1040 eq(self._msg.get_content_type(), 'text/plain')
1041 eq(self._msg.get_param('charset'), 'us-ascii')
1042 missing = []
1043 unless(self._msg.get_param('foobar', missing) is missing)
1044 unless(self._msg.get_param('charset', missing, header='foobar')
1045 is missing)
1046
1047 def test_payload(self):
1048 self.assertEqual(self._msg.get_payload(), 'hello there')
1049 self.failUnless(not self._msg.is_multipart())
1050
1051 def test_charset(self):
1052 eq = self.assertEqual
1053 msg = MIMEText('hello there', _charset='us-ascii')
1054 eq(msg.get_charset().input_charset, 'us-ascii')
1055 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1056
1057
1058
1059# Test complicated multipart/* messages
1060class TestMultipart(TestEmailBase):
1061 def setUp(self):
1062 with openfile('PyBanner048.gif', 'rb') as fp:
1063 data = fp.read()
1064 container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
1065 image = MIMEImage(data, name='dingusfish.gif')
1066 image.add_header('content-disposition', 'attachment',
1067 filename='dingusfish.gif')
1068 intro = MIMEText('''\
1069Hi there,
1070
1071This is the dingus fish.
1072''')
1073 container.attach(intro)
1074 container.attach(image)
1075 container['From'] = 'Barry <barry@digicool.com>'
1076 container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>'
1077 container['Subject'] = 'Here is your dingus fish'
1078
1079 now = 987809702.54848599
1080 timetuple = time.localtime(now)
1081 if timetuple[-1] == 0:
1082 tzsecs = time.timezone
1083 else:
1084 tzsecs = time.altzone
1085 if tzsecs > 0:
1086 sign = '-'
1087 else:
1088 sign = '+'
1089 tzoffset = ' %s%04d' % (sign, tzsecs / 36)
1090 container['Date'] = time.strftime(
1091 '%a, %d %b %Y %H:%M:%S',
1092 time.localtime(now)) + tzoffset
1093 self._msg = container
1094 self._im = image
1095 self._txt = intro
1096
1097 def test_hierarchy(self):
1098 # convenience
1099 eq = self.assertEqual
1100 unless = self.failUnless
1101 raises = self.assertRaises
1102 # tests
1103 m = self._msg
1104 unless(m.is_multipart())
1105 eq(m.get_content_type(), 'multipart/mixed')
1106 eq(len(m.get_payload()), 2)
1107 raises(IndexError, m.get_payload, 2)
1108 m0 = m.get_payload(0)
1109 m1 = m.get_payload(1)
1110 unless(m0 is self._txt)
1111 unless(m1 is self._im)
1112 eq(m.get_payload(), [m0, m1])
1113 unless(not m0.is_multipart())
1114 unless(not m1.is_multipart())
1115
1116 def test_empty_multipart_idempotent(self):
1117 text = """\
1118Content-Type: multipart/mixed; boundary="BOUNDARY"
1119MIME-Version: 1.0
1120Subject: A subject
1121To: aperson@dom.ain
1122From: bperson@dom.ain
1123
1124
1125--BOUNDARY
1126
1127
1128--BOUNDARY--
1129"""
1130 msg = Parser().parsestr(text)
1131 self.ndiffAssertEqual(text, msg.as_string())
1132
1133 def test_no_parts_in_a_multipart_with_none_epilogue(self):
1134 outer = MIMEBase('multipart', 'mixed')
1135 outer['Subject'] = 'A subject'
1136 outer['To'] = 'aperson@dom.ain'
1137 outer['From'] = 'bperson@dom.ain'
1138 outer.set_boundary('BOUNDARY')
1139 self.ndiffAssertEqual(outer.as_string(), '''\
1140Content-Type: multipart/mixed; boundary="BOUNDARY"
1141MIME-Version: 1.0
1142Subject: A subject
1143To: aperson@dom.ain
1144From: bperson@dom.ain
1145
1146--BOUNDARY
1147
1148--BOUNDARY--''')
1149
1150 def test_no_parts_in_a_multipart_with_empty_epilogue(self):
1151 outer = MIMEBase('multipart', 'mixed')
1152 outer['Subject'] = 'A subject'
1153 outer['To'] = 'aperson@dom.ain'
1154 outer['From'] = 'bperson@dom.ain'
1155 outer.preamble = ''
1156 outer.epilogue = ''
1157 outer.set_boundary('BOUNDARY')
1158 self.ndiffAssertEqual(outer.as_string(), '''\
1159Content-Type: multipart/mixed; boundary="BOUNDARY"
1160MIME-Version: 1.0
1161Subject: A subject
1162To: aperson@dom.ain
1163From: bperson@dom.ain
1164
1165
1166--BOUNDARY
1167
1168--BOUNDARY--
1169''')
1170
1171 def test_one_part_in_a_multipart(self):
1172 eq = self.ndiffAssertEqual
1173 outer = MIMEBase('multipart', 'mixed')
1174 outer['Subject'] = 'A subject'
1175 outer['To'] = 'aperson@dom.ain'
1176 outer['From'] = 'bperson@dom.ain'
1177 outer.set_boundary('BOUNDARY')
1178 msg = MIMEText('hello world')
1179 outer.attach(msg)
1180 eq(outer.as_string(), '''\
1181Content-Type: multipart/mixed; boundary="BOUNDARY"
1182MIME-Version: 1.0
1183Subject: A subject
1184To: aperson@dom.ain
1185From: bperson@dom.ain
1186
1187--BOUNDARY
1188Content-Type: text/plain; charset="us-ascii"
1189MIME-Version: 1.0
1190Content-Transfer-Encoding: 7bit
1191
1192hello world
1193--BOUNDARY--''')
1194
1195 def test_seq_parts_in_a_multipart_with_empty_preamble(self):
1196 eq = self.ndiffAssertEqual
1197 outer = MIMEBase('multipart', 'mixed')
1198 outer['Subject'] = 'A subject'
1199 outer['To'] = 'aperson@dom.ain'
1200 outer['From'] = 'bperson@dom.ain'
1201 outer.preamble = ''
1202 msg = MIMEText('hello world')
1203 outer.attach(msg)
1204 outer.set_boundary('BOUNDARY')
1205 eq(outer.as_string(), '''\
1206Content-Type: multipart/mixed; boundary="BOUNDARY"
1207MIME-Version: 1.0
1208Subject: A subject
1209To: aperson@dom.ain
1210From: bperson@dom.ain
1211
1212
1213--BOUNDARY
1214Content-Type: text/plain; charset="us-ascii"
1215MIME-Version: 1.0
1216Content-Transfer-Encoding: 7bit
1217
1218hello world
1219--BOUNDARY--''')
1220
1221
1222 def test_seq_parts_in_a_multipart_with_none_preamble(self):
1223 eq = self.ndiffAssertEqual
1224 outer = MIMEBase('multipart', 'mixed')
1225 outer['Subject'] = 'A subject'
1226 outer['To'] = 'aperson@dom.ain'
1227 outer['From'] = 'bperson@dom.ain'
1228 outer.preamble = None
1229 msg = MIMEText('hello world')
1230 outer.attach(msg)
1231 outer.set_boundary('BOUNDARY')
1232 eq(outer.as_string(), '''\
1233Content-Type: multipart/mixed; boundary="BOUNDARY"
1234MIME-Version: 1.0
1235Subject: A subject
1236To: aperson@dom.ain
1237From: bperson@dom.ain
1238
1239--BOUNDARY
1240Content-Type: text/plain; charset="us-ascii"
1241MIME-Version: 1.0
1242Content-Transfer-Encoding: 7bit
1243
1244hello world
1245--BOUNDARY--''')
1246
1247
1248 def test_seq_parts_in_a_multipart_with_none_epilogue(self):
1249 eq = self.ndiffAssertEqual
1250 outer = MIMEBase('multipart', 'mixed')
1251 outer['Subject'] = 'A subject'
1252 outer['To'] = 'aperson@dom.ain'
1253 outer['From'] = 'bperson@dom.ain'
1254 outer.epilogue = None
1255 msg = MIMEText('hello world')
1256 outer.attach(msg)
1257 outer.set_boundary('BOUNDARY')
1258 eq(outer.as_string(), '''\
1259Content-Type: multipart/mixed; boundary="BOUNDARY"
1260MIME-Version: 1.0
1261Subject: A subject
1262To: aperson@dom.ain
1263From: bperson@dom.ain
1264
1265--BOUNDARY
1266Content-Type: text/plain; charset="us-ascii"
1267MIME-Version: 1.0
1268Content-Transfer-Encoding: 7bit
1269
1270hello world
1271--BOUNDARY--''')
1272
1273
1274 def test_seq_parts_in_a_multipart_with_empty_epilogue(self):
1275 eq = self.ndiffAssertEqual
1276 outer = MIMEBase('multipart', 'mixed')
1277 outer['Subject'] = 'A subject'
1278 outer['To'] = 'aperson@dom.ain'
1279 outer['From'] = 'bperson@dom.ain'
1280 outer.epilogue = ''
1281 msg = MIMEText('hello world')
1282 outer.attach(msg)
1283 outer.set_boundary('BOUNDARY')
1284 eq(outer.as_string(), '''\
1285Content-Type: multipart/mixed; boundary="BOUNDARY"
1286MIME-Version: 1.0
1287Subject: A subject
1288To: aperson@dom.ain
1289From: bperson@dom.ain
1290
1291--BOUNDARY
1292Content-Type: text/plain; charset="us-ascii"
1293MIME-Version: 1.0
1294Content-Transfer-Encoding: 7bit
1295
1296hello world
1297--BOUNDARY--
1298''')
1299
1300
1301 def test_seq_parts_in_a_multipart_with_nl_epilogue(self):
1302 eq = self.ndiffAssertEqual
1303 outer = MIMEBase('multipart', 'mixed')
1304 outer['Subject'] = 'A subject'
1305 outer['To'] = 'aperson@dom.ain'
1306 outer['From'] = 'bperson@dom.ain'
1307 outer.epilogue = '\n'
1308 msg = MIMEText('hello world')
1309 outer.attach(msg)
1310 outer.set_boundary('BOUNDARY')
1311 eq(outer.as_string(), '''\
1312Content-Type: multipart/mixed; boundary="BOUNDARY"
1313MIME-Version: 1.0
1314Subject: A subject
1315To: aperson@dom.ain
1316From: bperson@dom.ain
1317
1318--BOUNDARY
1319Content-Type: text/plain; charset="us-ascii"
1320MIME-Version: 1.0
1321Content-Transfer-Encoding: 7bit
1322
1323hello world
1324--BOUNDARY--
1325
1326''')
1327
1328 def test_message_external_body(self):
1329 eq = self.assertEqual
1330 msg = self._msgobj('msg_36.txt')
1331 eq(len(msg.get_payload()), 2)
1332 msg1 = msg.get_payload(1)
1333 eq(msg1.get_content_type(), 'multipart/alternative')
1334 eq(len(msg1.get_payload()), 2)
1335 for subpart in msg1.get_payload():
1336 eq(subpart.get_content_type(), 'message/external-body')
1337 eq(len(subpart.get_payload()), 1)
1338 subsubpart = subpart.get_payload(0)
1339 eq(subsubpart.get_content_type(), 'text/plain')
1340
1341 def test_double_boundary(self):
1342 # msg_37.txt is a multipart that contains two dash-boundary's in a
1343 # row. Our interpretation of RFC 2046 calls for ignoring the second
1344 # and subsequent boundaries.
1345 msg = self._msgobj('msg_37.txt')
1346 self.assertEqual(len(msg.get_payload()), 3)
1347
1348 def test_nested_inner_contains_outer_boundary(self):
1349 eq = self.ndiffAssertEqual
1350 # msg_38.txt has an inner part that contains outer boundaries. My
1351 # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say
1352 # these are illegal and should be interpreted as unterminated inner
1353 # parts.
1354 msg = self._msgobj('msg_38.txt')
1355 sfp = StringIO()
1356 iterators._structure(msg, sfp)
1357 eq(sfp.getvalue(), """\
1358multipart/mixed
1359 multipart/mixed
1360 multipart/alternative
1361 text/plain
1362 text/plain
1363 text/plain
1364 text/plain
1365""")
1366
1367 def test_nested_with_same_boundary(self):
1368 eq = self.ndiffAssertEqual
1369 # msg 39.txt is similarly evil in that it's got inner parts that use
1370 # the same boundary as outer parts. Again, I believe the way this is
1371 # parsed is closest to the spirit of RFC 2046
1372 msg = self._msgobj('msg_39.txt')
1373 sfp = StringIO()
1374 iterators._structure(msg, sfp)
1375 eq(sfp.getvalue(), """\
1376multipart/mixed
1377 multipart/mixed
1378 multipart/alternative
1379 application/octet-stream
1380 application/octet-stream
1381 text/plain
1382""")
1383
1384 def test_boundary_in_non_multipart(self):
1385 msg = self._msgobj('msg_40.txt')
1386 self.assertEqual(msg.as_string(), '''\
1387MIME-Version: 1.0
1388Content-Type: text/html; boundary="--961284236552522269"
1389
1390----961284236552522269
1391Content-Type: text/html;
1392Content-Transfer-Encoding: 7Bit
1393
1394<html></html>
1395
1396----961284236552522269--
1397''')
1398
1399 def test_boundary_with_leading_space(self):
1400 eq = self.assertEqual
1401 msg = email.message_from_string('''\
1402MIME-Version: 1.0
1403Content-Type: multipart/mixed; boundary=" XXXX"
1404
1405-- XXXX
1406Content-Type: text/plain
1407
1408
1409-- XXXX
1410Content-Type: text/plain
1411
1412-- XXXX--
1413''')
1414 self.failUnless(msg.is_multipart())
1415 eq(msg.get_boundary(), ' XXXX')
1416 eq(len(msg.get_payload()), 2)
1417
1418 def test_boundary_without_trailing_newline(self):
1419 m = Parser().parsestr("""\
1420Content-Type: multipart/mixed; boundary="===============0012394164=="
1421MIME-Version: 1.0
1422
1423--===============0012394164==
1424Content-Type: image/file1.jpg
1425MIME-Version: 1.0
1426Content-Transfer-Encoding: base64
1427
1428YXNkZg==
1429--===============0012394164==--""")
1430 self.assertEquals(m.get_payload(0).get_payload(), 'YXNkZg==')
1431
1432
1433
1434# Test some badly formatted messages
1435class TestNonConformant(TestEmailBase):
1436 def test_parse_missing_minor_type(self):
1437 eq = self.assertEqual
1438 msg = self._msgobj('msg_14.txt')
1439 eq(msg.get_content_type(), 'text/plain')
1440 eq(msg.get_content_maintype(), 'text')
1441 eq(msg.get_content_subtype(), 'plain')
1442
1443 def test_same_boundary_inner_outer(self):
1444 unless = self.failUnless
1445 msg = self._msgobj('msg_15.txt')
1446 # XXX We can probably eventually do better
1447 inner = msg.get_payload(0)
1448 unless(hasattr(inner, 'defects'))
1449 self.assertEqual(len(inner.defects), 1)
1450 unless(isinstance(inner.defects[0],
1451 errors.StartBoundaryNotFoundDefect))
1452
1453 def test_multipart_no_boundary(self):
1454 unless = self.failUnless
1455 msg = self._msgobj('msg_25.txt')
1456 unless(isinstance(msg.get_payload(), str))
1457 self.assertEqual(len(msg.defects), 2)
1458 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1459 unless(isinstance(msg.defects[1],
1460 errors.MultipartInvariantViolationDefect))
1461
1462 def test_invalid_content_type(self):
1463 eq = self.assertEqual
1464 neq = self.ndiffAssertEqual
1465 msg = Message()
1466 # RFC 2045, $5.2 says invalid yields text/plain
1467 msg['Content-Type'] = 'text'
1468 eq(msg.get_content_maintype(), 'text')
1469 eq(msg.get_content_subtype(), 'plain')
1470 eq(msg.get_content_type(), 'text/plain')
1471 # Clear the old value and try something /really/ invalid
1472 del msg['content-type']
1473 msg['Content-Type'] = 'foo'
1474 eq(msg.get_content_maintype(), 'text')
1475 eq(msg.get_content_subtype(), 'plain')
1476 eq(msg.get_content_type(), 'text/plain')
1477 # Still, make sure that the message is idempotently generated
1478 s = StringIO()
1479 g = Generator(s)
1480 g.flatten(msg)
1481 neq(s.getvalue(), 'Content-Type: foo\n\n')
1482
1483 def test_no_start_boundary(self):
1484 eq = self.ndiffAssertEqual
1485 msg = self._msgobj('msg_31.txt')
1486 eq(msg.get_payload(), """\
1487--BOUNDARY
1488Content-Type: text/plain
1489
1490message 1
1491
1492--BOUNDARY
1493Content-Type: text/plain
1494
1495message 2
1496
1497--BOUNDARY--
1498""")
1499
1500 def test_no_separating_blank_line(self):
1501 eq = self.ndiffAssertEqual
1502 msg = self._msgobj('msg_35.txt')
1503 eq(msg.as_string(), """\
1504From: aperson@dom.ain
1505To: bperson@dom.ain
1506Subject: here's something interesting
1507
1508counter to RFC 2822, there's no separating newline here
1509""")
1510
1511 def test_lying_multipart(self):
1512 unless = self.failUnless
1513 msg = self._msgobj('msg_41.txt')
1514 unless(hasattr(msg, 'defects'))
1515 self.assertEqual(len(msg.defects), 2)
1516 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1517 unless(isinstance(msg.defects[1],
1518 errors.MultipartInvariantViolationDefect))
1519
1520 def test_missing_start_boundary(self):
1521 outer = self._msgobj('msg_42.txt')
1522 # The message structure is:
1523 #
1524 # multipart/mixed
1525 # text/plain
1526 # message/rfc822
1527 # multipart/mixed [*]
1528 #
1529 # [*] This message is missing its start boundary
1530 bad = outer.get_payload(1).get_payload(0)
1531 self.assertEqual(len(bad.defects), 1)
1532 self.failUnless(isinstance(bad.defects[0],
1533 errors.StartBoundaryNotFoundDefect))
1534
1535 def test_first_line_is_continuation_header(self):
1536 eq = self.assertEqual
1537 m = ' Line 1\nLine 2\nLine 3'
1538 msg = email.message_from_string(m)
1539 eq(msg.keys(), [])
1540 eq(msg.get_payload(), 'Line 2\nLine 3')
1541 eq(len(msg.defects), 1)
1542 self.failUnless(isinstance(msg.defects[0],
1543 errors.FirstHeaderLineIsContinuationDefect))
1544 eq(msg.defects[0].line, ' Line 1\n')
1545
1546
1547
1548# Test RFC 2047 header encoding and decoding
Guido van Rossum9604e662007-08-30 03:46:43 +00001549class TestRFC2047(TestEmailBase):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001550 def test_rfc2047_multiline(self):
1551 eq = self.assertEqual
1552 s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz
1553 foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?="""
1554 dh = decode_header(s)
1555 eq(dh, [
1556 (b'Re:', None),
1557 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland'),
1558 (b'baz foo bar', None),
1559 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland')])
1560 header = make_header(dh)
1561 eq(str(header),
1562 'Re: r\xe4ksm\xf6rg\xe5s baz foo bar r\xe4ksm\xf6rg\xe5s')
Barry Warsaw00b34222007-08-31 02:35:00 +00001563 self.ndiffAssertEqual(header.encode(maxlinelen=76), """\
Guido van Rossum9604e662007-08-30 03:46:43 +00001564Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar =?mac-iceland?q?r=8Aksm?=
1565 =?mac-iceland?q?=9Arg=8Cs?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001566
1567 def test_whitespace_eater_unicode(self):
1568 eq = self.assertEqual
1569 s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard@dom.ain>'
1570 dh = decode_header(s)
1571 eq(dh, [(b'Andr\xe9', 'iso-8859-1'),
1572 (b'Pirard <pirard@dom.ain>', None)])
1573 header = str(make_header(dh))
1574 eq(header, 'Andr\xe9 Pirard <pirard@dom.ain>')
1575
1576 def test_whitespace_eater_unicode_2(self):
1577 eq = self.assertEqual
1578 s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?='
1579 dh = decode_header(s)
1580 eq(dh, [(b'The', None), (b'quick brown fox', 'iso-8859-1'),
1581 (b'jumped over the', None), (b'lazy dog', 'iso-8859-1')])
1582 hu = str(make_header(dh))
1583 eq(hu, 'The quick brown fox jumped over the lazy dog')
1584
1585 def test_rfc2047_missing_whitespace(self):
1586 s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord'
1587 dh = decode_header(s)
1588 self.assertEqual(dh, [(s, None)])
1589
1590 def test_rfc2047_with_whitespace(self):
1591 s = 'Sm =?ISO-8859-1?B?9g==?= rg =?ISO-8859-1?B?5Q==?= sbord'
1592 dh = decode_header(s)
1593 self.assertEqual(dh, [(b'Sm', None), (b'\xf6', 'iso-8859-1'),
1594 (b'rg', None), (b'\xe5', 'iso-8859-1'),
1595 (b'sbord', None)])
1596
1597
1598
1599# Test the MIMEMessage class
1600class TestMIMEMessage(TestEmailBase):
1601 def setUp(self):
1602 with openfile('msg_11.txt') as fp:
1603 self._text = fp.read()
1604
1605 def test_type_error(self):
1606 self.assertRaises(TypeError, MIMEMessage, 'a plain string')
1607
1608 def test_valid_argument(self):
1609 eq = self.assertEqual
1610 unless = self.failUnless
1611 subject = 'A sub-message'
1612 m = Message()
1613 m['Subject'] = subject
1614 r = MIMEMessage(m)
1615 eq(r.get_content_type(), 'message/rfc822')
1616 payload = r.get_payload()
1617 unless(isinstance(payload, list))
1618 eq(len(payload), 1)
1619 subpart = payload[0]
1620 unless(subpart is m)
1621 eq(subpart['subject'], subject)
1622
1623 def test_bad_multipart(self):
1624 eq = self.assertEqual
1625 msg1 = Message()
1626 msg1['Subject'] = 'subpart 1'
1627 msg2 = Message()
1628 msg2['Subject'] = 'subpart 2'
1629 r = MIMEMessage(msg1)
1630 self.assertRaises(errors.MultipartConversionError, r.attach, msg2)
1631
1632 def test_generate(self):
1633 # First craft the message to be encapsulated
1634 m = Message()
1635 m['Subject'] = 'An enclosed message'
1636 m.set_payload('Here is the body of the message.\n')
1637 r = MIMEMessage(m)
1638 r['Subject'] = 'The enclosing message'
1639 s = StringIO()
1640 g = Generator(s)
1641 g.flatten(r)
1642 self.assertEqual(s.getvalue(), """\
1643Content-Type: message/rfc822
1644MIME-Version: 1.0
1645Subject: The enclosing message
1646
1647Subject: An enclosed message
1648
1649Here is the body of the message.
1650""")
1651
1652 def test_parse_message_rfc822(self):
1653 eq = self.assertEqual
1654 unless = self.failUnless
1655 msg = self._msgobj('msg_11.txt')
1656 eq(msg.get_content_type(), 'message/rfc822')
1657 payload = msg.get_payload()
1658 unless(isinstance(payload, list))
1659 eq(len(payload), 1)
1660 submsg = payload[0]
1661 self.failUnless(isinstance(submsg, Message))
1662 eq(submsg['subject'], 'An enclosed message')
1663 eq(submsg.get_payload(), 'Here is the body of the message.\n')
1664
1665 def test_dsn(self):
1666 eq = self.assertEqual
1667 unless = self.failUnless
1668 # msg 16 is a Delivery Status Notification, see RFC 1894
1669 msg = self._msgobj('msg_16.txt')
1670 eq(msg.get_content_type(), 'multipart/report')
1671 unless(msg.is_multipart())
1672 eq(len(msg.get_payload()), 3)
1673 # Subpart 1 is a text/plain, human readable section
1674 subpart = msg.get_payload(0)
1675 eq(subpart.get_content_type(), 'text/plain')
1676 eq(subpart.get_payload(), """\
1677This report relates to a message you sent with the following header fields:
1678
1679 Message-id: <002001c144a6$8752e060$56104586@oxy.edu>
1680 Date: Sun, 23 Sep 2001 20:10:55 -0700
1681 From: "Ian T. Henry" <henryi@oxy.edu>
1682 To: SoCal Raves <scr@socal-raves.org>
1683 Subject: [scr] yeah for Ians!!
1684
1685Your message cannot be delivered to the following recipients:
1686
1687 Recipient address: jangel1@cougar.noc.ucla.edu
1688 Reason: recipient reached disk quota
1689
1690""")
1691 # Subpart 2 contains the machine parsable DSN information. It
1692 # consists of two blocks of headers, represented by two nested Message
1693 # objects.
1694 subpart = msg.get_payload(1)
1695 eq(subpart.get_content_type(), 'message/delivery-status')
1696 eq(len(subpart.get_payload()), 2)
1697 # message/delivery-status should treat each block as a bunch of
1698 # headers, i.e. a bunch of Message objects.
1699 dsn1 = subpart.get_payload(0)
1700 unless(isinstance(dsn1, Message))
1701 eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu')
1702 eq(dsn1.get_param('dns', header='reporting-mta'), '')
1703 # Try a missing one <wink>
1704 eq(dsn1.get_param('nsd', header='reporting-mta'), None)
1705 dsn2 = subpart.get_payload(1)
1706 unless(isinstance(dsn2, Message))
1707 eq(dsn2['action'], 'failed')
1708 eq(dsn2.get_params(header='original-recipient'),
1709 [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')])
1710 eq(dsn2.get_param('rfc822', header='final-recipient'), '')
1711 # Subpart 3 is the original message
1712 subpart = msg.get_payload(2)
1713 eq(subpart.get_content_type(), 'message/rfc822')
1714 payload = subpart.get_payload()
1715 unless(isinstance(payload, list))
1716 eq(len(payload), 1)
1717 subsubpart = payload[0]
1718 unless(isinstance(subsubpart, Message))
1719 eq(subsubpart.get_content_type(), 'text/plain')
1720 eq(subsubpart['message-id'],
1721 '<002001c144a6$8752e060$56104586@oxy.edu>')
1722
1723 def test_epilogue(self):
1724 eq = self.ndiffAssertEqual
1725 with openfile('msg_21.txt') as fp:
1726 text = fp.read()
1727 msg = Message()
1728 msg['From'] = 'aperson@dom.ain'
1729 msg['To'] = 'bperson@dom.ain'
1730 msg['Subject'] = 'Test'
1731 msg.preamble = 'MIME message'
1732 msg.epilogue = 'End of MIME message\n'
1733 msg1 = MIMEText('One')
1734 msg2 = MIMEText('Two')
1735 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1736 msg.attach(msg1)
1737 msg.attach(msg2)
1738 sfp = StringIO()
1739 g = Generator(sfp)
1740 g.flatten(msg)
1741 eq(sfp.getvalue(), text)
1742
1743 def test_no_nl_preamble(self):
1744 eq = self.ndiffAssertEqual
1745 msg = Message()
1746 msg['From'] = 'aperson@dom.ain'
1747 msg['To'] = 'bperson@dom.ain'
1748 msg['Subject'] = 'Test'
1749 msg.preamble = 'MIME message'
1750 msg.epilogue = ''
1751 msg1 = MIMEText('One')
1752 msg2 = MIMEText('Two')
1753 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1754 msg.attach(msg1)
1755 msg.attach(msg2)
1756 eq(msg.as_string(), """\
1757From: aperson@dom.ain
1758To: bperson@dom.ain
1759Subject: Test
1760Content-Type: multipart/mixed; boundary="BOUNDARY"
1761
1762MIME message
1763--BOUNDARY
1764Content-Type: text/plain; charset="us-ascii"
1765MIME-Version: 1.0
1766Content-Transfer-Encoding: 7bit
1767
1768One
1769--BOUNDARY
1770Content-Type: text/plain; charset="us-ascii"
1771MIME-Version: 1.0
1772Content-Transfer-Encoding: 7bit
1773
1774Two
1775--BOUNDARY--
1776""")
1777
1778 def test_default_type(self):
1779 eq = self.assertEqual
1780 with openfile('msg_30.txt') as fp:
1781 msg = email.message_from_file(fp)
1782 container1 = msg.get_payload(0)
1783 eq(container1.get_default_type(), 'message/rfc822')
1784 eq(container1.get_content_type(), 'message/rfc822')
1785 container2 = msg.get_payload(1)
1786 eq(container2.get_default_type(), 'message/rfc822')
1787 eq(container2.get_content_type(), 'message/rfc822')
1788 container1a = container1.get_payload(0)
1789 eq(container1a.get_default_type(), 'text/plain')
1790 eq(container1a.get_content_type(), 'text/plain')
1791 container2a = container2.get_payload(0)
1792 eq(container2a.get_default_type(), 'text/plain')
1793 eq(container2a.get_content_type(), 'text/plain')
1794
1795 def test_default_type_with_explicit_container_type(self):
1796 eq = self.assertEqual
1797 with openfile('msg_28.txt') as fp:
1798 msg = email.message_from_file(fp)
1799 container1 = msg.get_payload(0)
1800 eq(container1.get_default_type(), 'message/rfc822')
1801 eq(container1.get_content_type(), 'message/rfc822')
1802 container2 = msg.get_payload(1)
1803 eq(container2.get_default_type(), 'message/rfc822')
1804 eq(container2.get_content_type(), 'message/rfc822')
1805 container1a = container1.get_payload(0)
1806 eq(container1a.get_default_type(), 'text/plain')
1807 eq(container1a.get_content_type(), 'text/plain')
1808 container2a = container2.get_payload(0)
1809 eq(container2a.get_default_type(), 'text/plain')
1810 eq(container2a.get_content_type(), 'text/plain')
1811
1812 def test_default_type_non_parsed(self):
1813 eq = self.assertEqual
1814 neq = self.ndiffAssertEqual
1815 # Set up container
1816 container = MIMEMultipart('digest', 'BOUNDARY')
1817 container.epilogue = ''
1818 # Set up subparts
1819 subpart1a = MIMEText('message 1\n')
1820 subpart2a = MIMEText('message 2\n')
1821 subpart1 = MIMEMessage(subpart1a)
1822 subpart2 = MIMEMessage(subpart2a)
1823 container.attach(subpart1)
1824 container.attach(subpart2)
1825 eq(subpart1.get_content_type(), 'message/rfc822')
1826 eq(subpart1.get_default_type(), 'message/rfc822')
1827 eq(subpart2.get_content_type(), 'message/rfc822')
1828 eq(subpart2.get_default_type(), 'message/rfc822')
1829 neq(container.as_string(0), '''\
1830Content-Type: multipart/digest; boundary="BOUNDARY"
1831MIME-Version: 1.0
1832
1833--BOUNDARY
1834Content-Type: message/rfc822
1835MIME-Version: 1.0
1836
1837Content-Type: text/plain; charset="us-ascii"
1838MIME-Version: 1.0
1839Content-Transfer-Encoding: 7bit
1840
1841message 1
1842
1843--BOUNDARY
1844Content-Type: message/rfc822
1845MIME-Version: 1.0
1846
1847Content-Type: text/plain; charset="us-ascii"
1848MIME-Version: 1.0
1849Content-Transfer-Encoding: 7bit
1850
1851message 2
1852
1853--BOUNDARY--
1854''')
1855 del subpart1['content-type']
1856 del subpart1['mime-version']
1857 del subpart2['content-type']
1858 del subpart2['mime-version']
1859 eq(subpart1.get_content_type(), 'message/rfc822')
1860 eq(subpart1.get_default_type(), 'message/rfc822')
1861 eq(subpart2.get_content_type(), 'message/rfc822')
1862 eq(subpart2.get_default_type(), 'message/rfc822')
1863 neq(container.as_string(0), '''\
1864Content-Type: multipart/digest; boundary="BOUNDARY"
1865MIME-Version: 1.0
1866
1867--BOUNDARY
1868
1869Content-Type: text/plain; charset="us-ascii"
1870MIME-Version: 1.0
1871Content-Transfer-Encoding: 7bit
1872
1873message 1
1874
1875--BOUNDARY
1876
1877Content-Type: text/plain; charset="us-ascii"
1878MIME-Version: 1.0
1879Content-Transfer-Encoding: 7bit
1880
1881message 2
1882
1883--BOUNDARY--
1884''')
1885
1886 def test_mime_attachments_in_constructor(self):
1887 eq = self.assertEqual
1888 text1 = MIMEText('')
1889 text2 = MIMEText('')
1890 msg = MIMEMultipart(_subparts=(text1, text2))
1891 eq(len(msg.get_payload()), 2)
1892 eq(msg.get_payload(0), text1)
1893 eq(msg.get_payload(1), text2)
1894
Christian Heimes587c2bf2008-01-19 16:21:02 +00001895 def test_default_multipart_constructor(self):
1896 msg = MIMEMultipart()
1897 self.assertTrue(msg.is_multipart())
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001898
1899
1900# A general test of parser->model->generator idempotency. IOW, read a message
1901# in, parse it into a message object tree, then without touching the tree,
1902# regenerate the plain text. The original text and the transformed text
1903# should be identical. Note: that we ignore the Unix-From since that may
1904# contain a changed date.
1905class TestIdempotent(TestEmailBase):
1906 def _msgobj(self, filename):
1907 with openfile(filename) as fp:
1908 data = fp.read()
1909 msg = email.message_from_string(data)
1910 return msg, data
1911
1912 def _idempotent(self, msg, text):
1913 eq = self.ndiffAssertEqual
1914 s = StringIO()
1915 g = Generator(s, maxheaderlen=0)
1916 g.flatten(msg)
1917 eq(text, s.getvalue())
1918
1919 def test_parse_text_message(self):
1920 eq = self.assertEquals
1921 msg, text = self._msgobj('msg_01.txt')
1922 eq(msg.get_content_type(), 'text/plain')
1923 eq(msg.get_content_maintype(), 'text')
1924 eq(msg.get_content_subtype(), 'plain')
1925 eq(msg.get_params()[1], ('charset', 'us-ascii'))
1926 eq(msg.get_param('charset'), 'us-ascii')
1927 eq(msg.preamble, None)
1928 eq(msg.epilogue, None)
1929 self._idempotent(msg, text)
1930
1931 def test_parse_untyped_message(self):
1932 eq = self.assertEquals
1933 msg, text = self._msgobj('msg_03.txt')
1934 eq(msg.get_content_type(), 'text/plain')
1935 eq(msg.get_params(), None)
1936 eq(msg.get_param('charset'), None)
1937 self._idempotent(msg, text)
1938
1939 def test_simple_multipart(self):
1940 msg, text = self._msgobj('msg_04.txt')
1941 self._idempotent(msg, text)
1942
1943 def test_MIME_digest(self):
1944 msg, text = self._msgobj('msg_02.txt')
1945 self._idempotent(msg, text)
1946
1947 def test_long_header(self):
1948 msg, text = self._msgobj('msg_27.txt')
1949 self._idempotent(msg, text)
1950
1951 def test_MIME_digest_with_part_headers(self):
1952 msg, text = self._msgobj('msg_28.txt')
1953 self._idempotent(msg, text)
1954
1955 def test_mixed_with_image(self):
1956 msg, text = self._msgobj('msg_06.txt')
1957 self._idempotent(msg, text)
1958
1959 def test_multipart_report(self):
1960 msg, text = self._msgobj('msg_05.txt')
1961 self._idempotent(msg, text)
1962
1963 def test_dsn(self):
1964 msg, text = self._msgobj('msg_16.txt')
1965 self._idempotent(msg, text)
1966
1967 def test_preamble_epilogue(self):
1968 msg, text = self._msgobj('msg_21.txt')
1969 self._idempotent(msg, text)
1970
1971 def test_multipart_one_part(self):
1972 msg, text = self._msgobj('msg_23.txt')
1973 self._idempotent(msg, text)
1974
1975 def test_multipart_no_parts(self):
1976 msg, text = self._msgobj('msg_24.txt')
1977 self._idempotent(msg, text)
1978
1979 def test_no_start_boundary(self):
1980 msg, text = self._msgobj('msg_31.txt')
1981 self._idempotent(msg, text)
1982
1983 def test_rfc2231_charset(self):
1984 msg, text = self._msgobj('msg_32.txt')
1985 self._idempotent(msg, text)
1986
1987 def test_more_rfc2231_parameters(self):
1988 msg, text = self._msgobj('msg_33.txt')
1989 self._idempotent(msg, text)
1990
1991 def test_text_plain_in_a_multipart_digest(self):
1992 msg, text = self._msgobj('msg_34.txt')
1993 self._idempotent(msg, text)
1994
1995 def test_nested_multipart_mixeds(self):
1996 msg, text = self._msgobj('msg_12a.txt')
1997 self._idempotent(msg, text)
1998
1999 def test_message_external_body_idempotent(self):
2000 msg, text = self._msgobj('msg_36.txt')
2001 self._idempotent(msg, text)
2002
2003 def test_content_type(self):
2004 eq = self.assertEquals
2005 unless = self.failUnless
2006 # Get a message object and reset the seek pointer for other tests
2007 msg, text = self._msgobj('msg_05.txt')
2008 eq(msg.get_content_type(), 'multipart/report')
2009 # Test the Content-Type: parameters
2010 params = {}
2011 for pk, pv in msg.get_params():
2012 params[pk] = pv
2013 eq(params['report-type'], 'delivery-status')
2014 eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
2015 eq(msg.preamble, 'This is a MIME-encapsulated message.\n')
2016 eq(msg.epilogue, '\n')
2017 eq(len(msg.get_payload()), 3)
2018 # Make sure the subparts are what we expect
2019 msg1 = msg.get_payload(0)
2020 eq(msg1.get_content_type(), 'text/plain')
2021 eq(msg1.get_payload(), 'Yadda yadda yadda\n')
2022 msg2 = msg.get_payload(1)
2023 eq(msg2.get_content_type(), 'text/plain')
2024 eq(msg2.get_payload(), 'Yadda yadda yadda\n')
2025 msg3 = msg.get_payload(2)
2026 eq(msg3.get_content_type(), 'message/rfc822')
2027 self.failUnless(isinstance(msg3, Message))
2028 payload = msg3.get_payload()
2029 unless(isinstance(payload, list))
2030 eq(len(payload), 1)
2031 msg4 = payload[0]
2032 unless(isinstance(msg4, Message))
2033 eq(msg4.get_payload(), 'Yadda yadda yadda\n')
2034
2035 def test_parser(self):
2036 eq = self.assertEquals
2037 unless = self.failUnless
2038 msg, text = self._msgobj('msg_06.txt')
2039 # Check some of the outer headers
2040 eq(msg.get_content_type(), 'message/rfc822')
2041 # Make sure the payload is a list of exactly one sub-Message, and that
2042 # that submessage has a type of text/plain
2043 payload = msg.get_payload()
2044 unless(isinstance(payload, list))
2045 eq(len(payload), 1)
2046 msg1 = payload[0]
2047 self.failUnless(isinstance(msg1, Message))
2048 eq(msg1.get_content_type(), 'text/plain')
2049 self.failUnless(isinstance(msg1.get_payload(), str))
2050 eq(msg1.get_payload(), '\n')
2051
2052
2053
2054# Test various other bits of the package's functionality
2055class TestMiscellaneous(TestEmailBase):
2056 def test_message_from_string(self):
2057 with openfile('msg_01.txt') as fp:
2058 text = fp.read()
2059 msg = email.message_from_string(text)
2060 s = StringIO()
2061 # Don't wrap/continue long headers since we're trying to test
2062 # idempotency.
2063 g = Generator(s, maxheaderlen=0)
2064 g.flatten(msg)
2065 self.assertEqual(text, s.getvalue())
2066
2067 def test_message_from_file(self):
2068 with openfile('msg_01.txt') as fp:
2069 text = fp.read()
2070 fp.seek(0)
2071 msg = email.message_from_file(fp)
2072 s = StringIO()
2073 # Don't wrap/continue long headers since we're trying to test
2074 # idempotency.
2075 g = Generator(s, maxheaderlen=0)
2076 g.flatten(msg)
2077 self.assertEqual(text, s.getvalue())
2078
2079 def test_message_from_string_with_class(self):
2080 unless = self.failUnless
2081 with openfile('msg_01.txt') as fp:
2082 text = fp.read()
2083
2084 # Create a subclass
2085 class MyMessage(Message):
2086 pass
2087
2088 msg = email.message_from_string(text, MyMessage)
2089 unless(isinstance(msg, MyMessage))
2090 # Try something more complicated
2091 with openfile('msg_02.txt') as fp:
2092 text = fp.read()
2093 msg = email.message_from_string(text, MyMessage)
2094 for subpart in msg.walk():
2095 unless(isinstance(subpart, MyMessage))
2096
2097 def test_message_from_file_with_class(self):
2098 unless = self.failUnless
2099 # Create a subclass
2100 class MyMessage(Message):
2101 pass
2102
2103 with openfile('msg_01.txt') as fp:
2104 msg = email.message_from_file(fp, MyMessage)
2105 unless(isinstance(msg, MyMessage))
2106 # Try something more complicated
2107 with openfile('msg_02.txt') as fp:
2108 msg = email.message_from_file(fp, MyMessage)
2109 for subpart in msg.walk():
2110 unless(isinstance(subpart, MyMessage))
2111
2112 def test__all__(self):
2113 module = __import__('email')
2114 # Can't use sorted() here due to Python 2.3 compatibility
2115 all = module.__all__[:]
2116 all.sort()
2117 self.assertEqual(all, [
2118 'base64mime', 'charset', 'encoders', 'errors', 'generator',
2119 'header', 'iterators', 'message', 'message_from_file',
2120 'message_from_string', 'mime', 'parser',
2121 'quoprimime', 'utils',
2122 ])
2123
2124 def test_formatdate(self):
2125 now = time.time()
2126 self.assertEqual(utils.parsedate(utils.formatdate(now))[:6],
2127 time.gmtime(now)[:6])
2128
2129 def test_formatdate_localtime(self):
2130 now = time.time()
2131 self.assertEqual(
2132 utils.parsedate(utils.formatdate(now, localtime=True))[:6],
2133 time.localtime(now)[:6])
2134
2135 def test_formatdate_usegmt(self):
2136 now = time.time()
2137 self.assertEqual(
2138 utils.formatdate(now, localtime=False),
2139 time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now)))
2140 self.assertEqual(
2141 utils.formatdate(now, localtime=False, usegmt=True),
2142 time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now)))
2143
2144 def test_parsedate_none(self):
2145 self.assertEqual(utils.parsedate(''), None)
2146
2147 def test_parsedate_compact(self):
2148 # The FWS after the comma is optional
2149 self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'),
2150 utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800'))
2151
2152 def test_parsedate_no_dayofweek(self):
2153 eq = self.assertEqual
2154 eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'),
2155 (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800))
2156
2157 def test_parsedate_compact_no_dayofweek(self):
2158 eq = self.assertEqual
2159 eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'),
2160 (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800))
2161
2162 def test_parsedate_acceptable_to_time_functions(self):
2163 eq = self.assertEqual
2164 timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800')
2165 t = int(time.mktime(timetup))
2166 eq(time.localtime(t)[:6], timetup[:6])
2167 eq(int(time.strftime('%Y', timetup)), 2003)
2168 timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800')
2169 t = int(time.mktime(timetup[:9]))
2170 eq(time.localtime(t)[:6], timetup[:6])
2171 eq(int(time.strftime('%Y', timetup[:9])), 2003)
2172
2173 def test_parseaddr_empty(self):
2174 self.assertEqual(utils.parseaddr('<>'), ('', ''))
2175 self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '')
2176
2177 def test_noquote_dump(self):
2178 self.assertEqual(
2179 utils.formataddr(('A Silly Person', 'person@dom.ain')),
2180 'A Silly Person <person@dom.ain>')
2181
2182 def test_escape_dump(self):
2183 self.assertEqual(
2184 utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')),
2185 r'"A \(Very\) Silly Person" <person@dom.ain>')
2186 a = r'A \(Special\) Person'
2187 b = 'person@dom.ain'
2188 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2189
2190 def test_escape_backslashes(self):
2191 self.assertEqual(
2192 utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')),
2193 r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>')
2194 a = r'Arthur \Backslash\ Foobar'
2195 b = 'person@dom.ain'
2196 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2197
2198 def test_name_with_dot(self):
2199 x = 'John X. Doe <jxd@example.com>'
2200 y = '"John X. Doe" <jxd@example.com>'
2201 a, b = ('John X. Doe', 'jxd@example.com')
2202 self.assertEqual(utils.parseaddr(x), (a, b))
2203 self.assertEqual(utils.parseaddr(y), (a, b))
2204 # formataddr() quotes the name if there's a dot in it
2205 self.assertEqual(utils.formataddr((a, b)), y)
2206
2207 def test_multiline_from_comment(self):
2208 x = """\
2209Foo
2210\tBar <foo@example.com>"""
2211 self.assertEqual(utils.parseaddr(x), ('Foo Bar', 'foo@example.com'))
2212
2213 def test_quote_dump(self):
2214 self.assertEqual(
2215 utils.formataddr(('A Silly; Person', 'person@dom.ain')),
2216 r'"A Silly; Person" <person@dom.ain>')
2217
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002218 def test_charset_richcomparisons(self):
2219 eq = self.assertEqual
2220 ne = self.failIfEqual
2221 cset1 = Charset()
2222 cset2 = Charset()
2223 eq(cset1, 'us-ascii')
2224 eq(cset1, 'US-ASCII')
2225 eq(cset1, 'Us-AsCiI')
2226 eq('us-ascii', cset1)
2227 eq('US-ASCII', cset1)
2228 eq('Us-AsCiI', cset1)
2229 ne(cset1, 'usascii')
2230 ne(cset1, 'USASCII')
2231 ne(cset1, 'UsAsCiI')
2232 ne('usascii', cset1)
2233 ne('USASCII', cset1)
2234 ne('UsAsCiI', cset1)
2235 eq(cset1, cset2)
2236 eq(cset2, cset1)
2237
2238 def test_getaddresses(self):
2239 eq = self.assertEqual
2240 eq(utils.getaddresses(['aperson@dom.ain (Al Person)',
2241 'Bud Person <bperson@dom.ain>']),
2242 [('Al Person', 'aperson@dom.ain'),
2243 ('Bud Person', 'bperson@dom.ain')])
2244
2245 def test_getaddresses_nasty(self):
2246 eq = self.assertEqual
2247 eq(utils.getaddresses(['foo: ;']), [('', '')])
2248 eq(utils.getaddresses(
2249 ['[]*-- =~$']),
2250 [('', ''), ('', ''), ('', '*--')])
2251 eq(utils.getaddresses(
2252 ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
2253 [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
2254
2255 def test_getaddresses_embedded_comment(self):
2256 """Test proper handling of a nested comment"""
2257 eq = self.assertEqual
2258 addrs = utils.getaddresses(['User ((nested comment)) <foo@bar.com>'])
2259 eq(addrs[0][1], 'foo@bar.com')
2260
2261 def test_utils_quote_unquote(self):
2262 eq = self.assertEqual
2263 msg = Message()
2264 msg.add_header('content-disposition', 'attachment',
2265 filename='foo\\wacky"name')
2266 eq(msg.get_filename(), 'foo\\wacky"name')
2267
2268 def test_get_body_encoding_with_bogus_charset(self):
2269 charset = Charset('not a charset')
2270 self.assertEqual(charset.get_body_encoding(), 'base64')
2271
2272 def test_get_body_encoding_with_uppercase_charset(self):
2273 eq = self.assertEqual
2274 msg = Message()
2275 msg['Content-Type'] = 'text/plain; charset=UTF-8'
2276 eq(msg['content-type'], 'text/plain; charset=UTF-8')
2277 charsets = msg.get_charsets()
2278 eq(len(charsets), 1)
2279 eq(charsets[0], 'utf-8')
2280 charset = Charset(charsets[0])
2281 eq(charset.get_body_encoding(), 'base64')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002282 msg.set_payload(b'hello world', charset=charset)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002283 eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n')
2284 eq(msg.get_payload(decode=True), b'hello world')
2285 eq(msg['content-transfer-encoding'], 'base64')
2286 # Try another one
2287 msg = Message()
2288 msg['Content-Type'] = 'text/plain; charset="US-ASCII"'
2289 charsets = msg.get_charsets()
2290 eq(len(charsets), 1)
2291 eq(charsets[0], 'us-ascii')
2292 charset = Charset(charsets[0])
2293 eq(charset.get_body_encoding(), encoders.encode_7or8bit)
2294 msg.set_payload('hello world', charset=charset)
2295 eq(msg.get_payload(), 'hello world')
2296 eq(msg['content-transfer-encoding'], '7bit')
2297
2298 def test_charsets_case_insensitive(self):
2299 lc = Charset('us-ascii')
2300 uc = Charset('US-ASCII')
2301 self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding())
2302
2303 def test_partial_falls_inside_message_delivery_status(self):
2304 eq = self.ndiffAssertEqual
2305 # The Parser interface provides chunks of data to FeedParser in 8192
2306 # byte gulps. SF bug #1076485 found one of those chunks inside
2307 # message/delivery-status header block, which triggered an
2308 # unreadline() of NeedMoreData.
2309 msg = self._msgobj('msg_43.txt')
2310 sfp = StringIO()
2311 iterators._structure(msg, sfp)
2312 eq(sfp.getvalue(), """\
2313multipart/report
2314 text/plain
2315 message/delivery-status
2316 text/plain
2317 text/plain
2318 text/plain
2319 text/plain
2320 text/plain
2321 text/plain
2322 text/plain
2323 text/plain
2324 text/plain
2325 text/plain
2326 text/plain
2327 text/plain
2328 text/plain
2329 text/plain
2330 text/plain
2331 text/plain
2332 text/plain
2333 text/plain
2334 text/plain
2335 text/plain
2336 text/plain
2337 text/plain
2338 text/plain
2339 text/plain
2340 text/plain
2341 text/plain
2342 text/rfc822-headers
2343""")
2344
2345
2346
2347# Test the iterator/generators
2348class TestIterators(TestEmailBase):
2349 def test_body_line_iterator(self):
2350 eq = self.assertEqual
2351 neq = self.ndiffAssertEqual
2352 # First a simple non-multipart message
2353 msg = self._msgobj('msg_01.txt')
2354 it = iterators.body_line_iterator(msg)
2355 lines = list(it)
2356 eq(len(lines), 6)
2357 neq(EMPTYSTRING.join(lines), msg.get_payload())
2358 # Now a more complicated multipart
2359 msg = self._msgobj('msg_02.txt')
2360 it = iterators.body_line_iterator(msg)
2361 lines = list(it)
2362 eq(len(lines), 43)
2363 with openfile('msg_19.txt') as fp:
2364 neq(EMPTYSTRING.join(lines), fp.read())
2365
2366 def test_typed_subpart_iterator(self):
2367 eq = self.assertEqual
2368 msg = self._msgobj('msg_04.txt')
2369 it = iterators.typed_subpart_iterator(msg, 'text')
2370 lines = []
2371 subparts = 0
2372 for subpart in it:
2373 subparts += 1
2374 lines.append(subpart.get_payload())
2375 eq(subparts, 2)
2376 eq(EMPTYSTRING.join(lines), """\
2377a simple kind of mirror
2378to reflect upon our own
2379a simple kind of mirror
2380to reflect upon our own
2381""")
2382
2383 def test_typed_subpart_iterator_default_type(self):
2384 eq = self.assertEqual
2385 msg = self._msgobj('msg_03.txt')
2386 it = iterators.typed_subpart_iterator(msg, 'text', 'plain')
2387 lines = []
2388 subparts = 0
2389 for subpart in it:
2390 subparts += 1
2391 lines.append(subpart.get_payload())
2392 eq(subparts, 1)
2393 eq(EMPTYSTRING.join(lines), """\
2394
2395Hi,
2396
2397Do you like this message?
2398
2399-Me
2400""")
2401
2402
2403
2404class TestParsers(TestEmailBase):
2405 def test_header_parser(self):
2406 eq = self.assertEqual
2407 # Parse only the headers of a complex multipart MIME document
2408 with openfile('msg_02.txt') as fp:
2409 msg = HeaderParser().parse(fp)
2410 eq(msg['from'], 'ppp-request@zzz.org')
2411 eq(msg['to'], 'ppp@zzz.org')
2412 eq(msg.get_content_type(), 'multipart/mixed')
2413 self.failIf(msg.is_multipart())
2414 self.failUnless(isinstance(msg.get_payload(), str))
2415
2416 def test_whitespace_continuation(self):
2417 eq = self.assertEqual
2418 # This message contains a line after the Subject: header that has only
2419 # whitespace, but it is not empty!
2420 msg = email.message_from_string("""\
2421From: aperson@dom.ain
2422To: bperson@dom.ain
2423Subject: the next line has a space on it
2424\x20
2425Date: Mon, 8 Apr 2002 15:09:19 -0400
2426Message-ID: spam
2427
2428Here's the message body
2429""")
2430 eq(msg['subject'], 'the next line has a space on it\n ')
2431 eq(msg['message-id'], 'spam')
2432 eq(msg.get_payload(), "Here's the message body\n")
2433
2434 def test_whitespace_continuation_last_header(self):
2435 eq = self.assertEqual
2436 # Like the previous test, but the subject line is the last
2437 # header.
2438 msg = email.message_from_string("""\
2439From: aperson@dom.ain
2440To: bperson@dom.ain
2441Date: Mon, 8 Apr 2002 15:09:19 -0400
2442Message-ID: spam
2443Subject: the next line has a space on it
2444\x20
2445
2446Here's the message body
2447""")
2448 eq(msg['subject'], 'the next line has a space on it\n ')
2449 eq(msg['message-id'], 'spam')
2450 eq(msg.get_payload(), "Here's the message body\n")
2451
2452 def test_crlf_separation(self):
2453 eq = self.assertEqual
Guido van Rossum98297ee2007-11-06 21:34:58 +00002454 with openfile('msg_26.txt', newline='\n') as fp:
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002455 msg = Parser().parse(fp)
2456 eq(len(msg.get_payload()), 2)
2457 part1 = msg.get_payload(0)
2458 eq(part1.get_content_type(), 'text/plain')
2459 eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n')
2460 part2 = msg.get_payload(1)
2461 eq(part2.get_content_type(), 'application/riscos')
2462
2463 def test_multipart_digest_with_extra_mime_headers(self):
2464 eq = self.assertEqual
2465 neq = self.ndiffAssertEqual
2466 with openfile('msg_28.txt') as fp:
2467 msg = email.message_from_file(fp)
2468 # Structure is:
2469 # multipart/digest
2470 # message/rfc822
2471 # text/plain
2472 # message/rfc822
2473 # text/plain
2474 eq(msg.is_multipart(), 1)
2475 eq(len(msg.get_payload()), 2)
2476 part1 = msg.get_payload(0)
2477 eq(part1.get_content_type(), 'message/rfc822')
2478 eq(part1.is_multipart(), 1)
2479 eq(len(part1.get_payload()), 1)
2480 part1a = part1.get_payload(0)
2481 eq(part1a.is_multipart(), 0)
2482 eq(part1a.get_content_type(), 'text/plain')
2483 neq(part1a.get_payload(), 'message 1\n')
2484 # next message/rfc822
2485 part2 = msg.get_payload(1)
2486 eq(part2.get_content_type(), 'message/rfc822')
2487 eq(part2.is_multipart(), 1)
2488 eq(len(part2.get_payload()), 1)
2489 part2a = part2.get_payload(0)
2490 eq(part2a.is_multipart(), 0)
2491 eq(part2a.get_content_type(), 'text/plain')
2492 neq(part2a.get_payload(), 'message 2\n')
2493
2494 def test_three_lines(self):
2495 # A bug report by Andrew McNamara
2496 lines = ['From: Andrew Person <aperson@dom.ain',
2497 'Subject: Test',
2498 'Date: Tue, 20 Aug 2002 16:43:45 +1000']
2499 msg = email.message_from_string(NL.join(lines))
2500 self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000')
2501
2502 def test_strip_line_feed_and_carriage_return_in_headers(self):
2503 eq = self.assertEqual
2504 # For [ 1002475 ] email message parser doesn't handle \r\n correctly
2505 value1 = 'text'
2506 value2 = 'more text'
2507 m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % (
2508 value1, value2)
2509 msg = email.message_from_string(m)
2510 eq(msg.get('Header'), value1)
2511 eq(msg.get('Next-Header'), value2)
2512
2513 def test_rfc2822_header_syntax(self):
2514 eq = self.assertEqual
2515 m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2516 msg = email.message_from_string(m)
2517 eq(len(msg), 3)
2518 eq(sorted(field for field in msg), ['!"#QUX;~', '>From', 'From'])
2519 eq(msg.get_payload(), 'body')
2520
2521 def test_rfc2822_space_not_allowed_in_header(self):
2522 eq = self.assertEqual
2523 m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2524 msg = email.message_from_string(m)
2525 eq(len(msg.keys()), 0)
2526
2527 def test_rfc2822_one_character_header(self):
2528 eq = self.assertEqual
2529 m = 'A: first header\nB: second header\nCC: third header\n\nbody'
2530 msg = email.message_from_string(m)
2531 headers = msg.keys()
2532 headers.sort()
2533 eq(headers, ['A', 'B', 'CC'])
2534 eq(msg.get_payload(), 'body')
2535
2536
2537
2538class TestBase64(unittest.TestCase):
2539 def test_len(self):
2540 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002541 eq(base64mime.header_length('hello'),
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002542 len(base64mime.body_encode(b'hello', eol='')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002543 for size in range(15):
2544 if size == 0 : bsize = 0
2545 elif size <= 3 : bsize = 4
2546 elif size <= 6 : bsize = 8
2547 elif size <= 9 : bsize = 12
2548 elif size <= 12: bsize = 16
2549 else : bsize = 20
Guido van Rossum9604e662007-08-30 03:46:43 +00002550 eq(base64mime.header_length('x' * size), bsize)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002551
2552 def test_decode(self):
2553 eq = self.assertEqual
Barry Warsaw2cc1f6d2007-08-30 14:28:55 +00002554 eq(base64mime.decode(''), b'')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002555 eq(base64mime.decode('aGVsbG8='), b'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002556
2557 def test_encode(self):
2558 eq = self.assertEqual
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002559 eq(base64mime.body_encode(b''), b'')
2560 eq(base64mime.body_encode(b'hello'), 'aGVsbG8=\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002561 # Test the binary flag
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002562 eq(base64mime.body_encode(b'hello\n'), 'aGVsbG8K\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002563 # Test the maxlinelen arg
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002564 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002565eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2566eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2567eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2568eHh4eCB4eHh4IA==
2569""")
2570 # Test the eol argument
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002571 eq(base64mime.body_encode(b'xxxx ' * 20, maxlinelen=40, eol='\r\n'),
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002572 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002573eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2574eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2575eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2576eHh4eCB4eHh4IA==\r
2577""")
2578
2579 def test_header_encode(self):
2580 eq = self.assertEqual
2581 he = base64mime.header_encode
2582 eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=')
Guido van Rossum9604e662007-08-30 03:46:43 +00002583 eq(he('hello\r\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=')
2584 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002585 # Test the charset option
2586 eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=')
2587 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002588
2589
2590
2591class TestQuopri(unittest.TestCase):
2592 def setUp(self):
2593 # Set of characters (as byte integers) that don't need to be encoded
2594 # in headers.
2595 self.hlit = list(chain(
2596 range(ord('a'), ord('z') + 1),
2597 range(ord('A'), ord('Z') + 1),
2598 range(ord('0'), ord('9') + 1),
Guido van Rossum9604e662007-08-30 03:46:43 +00002599 (c for c in b'!*+-/')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002600 # Set of characters (as byte integers) that do need to be encoded in
2601 # headers.
2602 self.hnon = [c for c in range(256) if c not in self.hlit]
2603 assert len(self.hlit) + len(self.hnon) == 256
2604 # Set of characters (as byte integers) that don't need to be encoded
2605 # in bodies.
2606 self.blit = list(range(ord(' '), ord('~') + 1))
2607 self.blit.append(ord('\t'))
2608 self.blit.remove(ord('='))
2609 # Set of characters (as byte integers) that do need to be encoded in
2610 # bodies.
2611 self.bnon = [c for c in range(256) if c not in self.blit]
2612 assert len(self.blit) + len(self.bnon) == 256
2613
Guido van Rossum9604e662007-08-30 03:46:43 +00002614 def test_quopri_header_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002615 for c in self.hlit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002616 self.failIf(quoprimime.header_check(c),
2617 'Should not be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002618 for c in self.hnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002619 self.failUnless(quoprimime.header_check(c),
2620 'Should be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002621
Guido van Rossum9604e662007-08-30 03:46:43 +00002622 def test_quopri_body_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002623 for c in self.blit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002624 self.failIf(quoprimime.body_check(c),
2625 'Should not be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002626 for c in self.bnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002627 self.failUnless(quoprimime.body_check(c),
2628 'Should be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002629
2630 def test_header_quopri_len(self):
2631 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002632 eq(quoprimime.header_length(b'hello'), 5)
2633 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002634 eq(len(quoprimime.header_encode(b'hello', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00002635 quoprimime.header_length(b'hello') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002636 # =?xxx?q?...?= means 10 extra characters
2637 10)
Guido van Rossum9604e662007-08-30 03:46:43 +00002638 eq(quoprimime.header_length(b'h@e@l@l@o@'), 20)
2639 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002640 eq(len(quoprimime.header_encode(b'h@e@l@l@o@', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00002641 quoprimime.header_length(b'h@e@l@l@o@') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002642 # =?xxx?q?...?= means 10 extra characters
2643 10)
2644 for c in self.hlit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002645 eq(quoprimime.header_length(bytes([c])), 1,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002646 'expected length 1 for %r' % chr(c))
2647 for c in self.hnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002648 # Space is special; it's encoded to _
2649 if c == ord(' '):
2650 continue
2651 eq(quoprimime.header_length(bytes([c])), 3,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002652 'expected length 3 for %r' % chr(c))
Guido van Rossum9604e662007-08-30 03:46:43 +00002653 eq(quoprimime.header_length(b' '), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002654
2655 def test_body_quopri_len(self):
2656 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002657 for c in self.blit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002658 eq(quoprimime.body_length(bytes([c])), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002659 for c in self.bnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002660 eq(quoprimime.body_length(bytes([c])), 3)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002661
2662 def test_quote_unquote_idempotent(self):
2663 for x in range(256):
2664 c = chr(x)
2665 self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c)
2666
2667 def test_header_encode(self):
2668 eq = self.assertEqual
2669 he = quoprimime.header_encode
2670 eq(he(b'hello'), '=?iso-8859-1?q?hello?=')
2671 eq(he(b'hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=')
2672 eq(he(b'hello\nworld'), '=?iso-8859-1?q?hello=0Aworld?=')
2673 # Test a non-ASCII character
2674 eq(he(b'hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=')
2675
2676 def test_decode(self):
2677 eq = self.assertEqual
2678 eq(quoprimime.decode(''), '')
2679 eq(quoprimime.decode('hello'), 'hello')
2680 eq(quoprimime.decode('hello', 'X'), 'hello')
2681 eq(quoprimime.decode('hello\nworld', 'X'), 'helloXworld')
2682
2683 def test_encode(self):
2684 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002685 eq(quoprimime.body_encode(''), '')
2686 eq(quoprimime.body_encode('hello'), 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002687 # Test the binary flag
Guido van Rossum9604e662007-08-30 03:46:43 +00002688 eq(quoprimime.body_encode('hello\r\nworld'), 'hello\nworld')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002689 # Test the maxlinelen arg
Guido van Rossum9604e662007-08-30 03:46:43 +00002690 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002691xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
2692 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
2693x xxxx xxxx xxxx xxxx=20""")
2694 # Test the eol argument
Guido van Rossum9604e662007-08-30 03:46:43 +00002695 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'),
2696 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002697xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
2698 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
2699x xxxx xxxx xxxx xxxx=20""")
Guido van Rossum9604e662007-08-30 03:46:43 +00002700 eq(quoprimime.body_encode("""\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002701one line
2702
2703two line"""), """\
2704one line
2705
2706two line""")
2707
2708
2709
2710# Test the Charset class
2711class TestCharset(unittest.TestCase):
2712 def tearDown(self):
2713 from email import charset as CharsetModule
2714 try:
2715 del CharsetModule.CHARSETS['fake']
2716 except KeyError:
2717 pass
2718
Guido van Rossum9604e662007-08-30 03:46:43 +00002719 def test_codec_encodeable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002720 eq = self.assertEqual
2721 # Make sure us-ascii = no Unicode conversion
2722 c = Charset('us-ascii')
Guido van Rossum9604e662007-08-30 03:46:43 +00002723 eq(c.header_encode('Hello World!'), 'Hello World!')
2724 # Test 8-bit idempotency with us-ascii
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002725 s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
Guido van Rossum9604e662007-08-30 03:46:43 +00002726 self.assertRaises(UnicodeError, c.header_encode, s)
2727 c = Charset('utf-8')
2728 eq(c.header_encode(s), '=?utf-8?b?wqTCosKkwqTCpMKmwqTCqMKkwqo=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002729
2730 def test_body_encode(self):
2731 eq = self.assertEqual
2732 # Try a charset with QP body encoding
2733 c = Charset('iso-8859-1')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002734 eq('hello w=F6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002735 # Try a charset with Base64 body encoding
2736 c = Charset('utf-8')
Martin v. Löwis15b16a32008-12-02 06:00:15 +00002737 eq('aGVsbG8gd29ybGQ=\n', c.body_encode(b'hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002738 # Try a charset with None body encoding
2739 c = Charset('us-ascii')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002740 eq('hello world', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002741 # Try the convert argument, where input codec != output codec
2742 c = Charset('euc-jp')
2743 # With apologies to Tokio Kikuchi ;)
Barry Warsawbef9d212007-08-31 10:55:37 +00002744 # XXX FIXME
2745## try:
2746## eq('\x1b$B5FCO;~IW\x1b(B',
2747## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
2748## eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
2749## c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
2750## except LookupError:
2751## # We probably don't have the Japanese codecs installed
2752## pass
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002753 # Testing SF bug #625509, which we have to fake, since there are no
2754 # built-in encodings where the header encoding is QP but the body
2755 # encoding is not.
2756 from email import charset as CharsetModule
2757 CharsetModule.add_charset('fake', CharsetModule.QP, None)
2758 c = Charset('fake')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002759 eq('hello w\xf6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002760
2761 def test_unicode_charset_name(self):
2762 charset = Charset('us-ascii')
2763 self.assertEqual(str(charset), 'us-ascii')
2764 self.assertRaises(errors.CharsetError, Charset, 'asc\xffii')
2765
2766
2767
2768# Test multilingual MIME headers.
2769class TestHeader(TestEmailBase):
2770 def test_simple(self):
2771 eq = self.ndiffAssertEqual
2772 h = Header('Hello World!')
2773 eq(h.encode(), 'Hello World!')
2774 h.append(' Goodbye World!')
2775 eq(h.encode(), 'Hello World! Goodbye World!')
2776
2777 def test_simple_surprise(self):
2778 eq = self.ndiffAssertEqual
2779 h = Header('Hello World!')
2780 eq(h.encode(), 'Hello World!')
2781 h.append('Goodbye World!')
2782 eq(h.encode(), 'Hello World! Goodbye World!')
2783
2784 def test_header_needs_no_decoding(self):
2785 h = 'no decoding needed'
2786 self.assertEqual(decode_header(h), [(h, None)])
2787
2788 def test_long(self):
2789 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.",
2790 maxlinelen=76)
2791 for l in h.encode(splitchars=' ').split('\n '):
2792 self.failUnless(len(l) <= 76)
2793
2794 def test_multilingual(self):
2795 eq = self.ndiffAssertEqual
2796 g = Charset("iso-8859-1")
2797 cz = Charset("iso-8859-2")
2798 utf8 = Charset("utf-8")
2799 g_head = (b'Die Mieter treten hier ein werden mit einem '
2800 b'Foerderband komfortabel den Korridor entlang, '
2801 b'an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, '
2802 b'gegen die rotierenden Klingen bef\xf6rdert. ')
2803 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
2804 b'd\xf9vtipu.. ')
2805 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
2806 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
2807 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
2808 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
2809 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
2810 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
2811 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
2812 '\u3044\u307e\u3059\u3002')
2813 h = Header(g_head, g)
2814 h.append(cz_head, cz)
2815 h.append(utf8_head, utf8)
Guido van Rossum9604e662007-08-30 03:46:43 +00002816 enc = h.encode(maxlinelen=76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002817 eq(enc, """\
Guido van Rossum9604e662007-08-30 03:46:43 +00002818=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_kom?=
2819 =?iso-8859-1?q?fortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wand?=
2820 =?iso-8859-1?q?gem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6r?=
2821 =?iso-8859-1?q?dert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002822 =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
2823 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
2824 =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
2825 =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
Guido van Rossum9604e662007-08-30 03:46:43 +00002826 =?utf-8?b?IE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5k?=
2827 =?utf-8?b?IGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIA=?=
2828 =?utf-8?b?44Gj44Gm44GE44G+44GZ44CC?=""")
2829 decoded = decode_header(enc)
2830 eq(len(decoded), 3)
2831 eq(decoded[0], (g_head, 'iso-8859-1'))
2832 eq(decoded[1], (cz_head, 'iso-8859-2'))
2833 eq(decoded[2], (utf8_head.encode('utf-8'), 'utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002834 ustr = str(h)
Guido van Rossum9604e662007-08-30 03:46:43 +00002835 eq(ustr,
2836 (b'Die Mieter treten hier ein werden mit einem Foerderband '
2837 b'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
2838 b'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
2839 b'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
2840 b'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
2841 b'\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
2842 b'\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
2843 b'\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
2844 b'\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
2845 b'\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
2846 b'\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
2847 b'\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
2848 b'\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
2849 b'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
2850 b'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
2851 b'\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82'
2852 ).decode('utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002853 # Test make_header()
2854 newh = make_header(decode_header(enc))
Guido van Rossum9604e662007-08-30 03:46:43 +00002855 eq(newh, h)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002856
2857 def test_empty_header_encode(self):
2858 h = Header()
2859 self.assertEqual(h.encode(), '')
Barry Warsaw8b3d6592007-08-30 02:10:49 +00002860
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002861 def test_header_ctor_default_args(self):
2862 eq = self.ndiffAssertEqual
2863 h = Header()
2864 eq(h, '')
2865 h.append('foo', Charset('iso-8859-1'))
Guido van Rossum9604e662007-08-30 03:46:43 +00002866 eq(h, 'foo')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002867
2868 def test_explicit_maxlinelen(self):
2869 eq = self.ndiffAssertEqual
2870 hstr = ('A very long line that must get split to something other '
2871 'than at the 76th character boundary to test the non-default '
2872 'behavior')
2873 h = Header(hstr)
2874 eq(h.encode(), '''\
2875A very long line that must get split to something other than at the 76th
2876 character boundary to test the non-default behavior''')
2877 eq(str(h), hstr)
2878 h = Header(hstr, header_name='Subject')
2879 eq(h.encode(), '''\
2880A very long line that must get split to something other than at the
2881 76th character boundary to test the non-default behavior''')
2882 eq(str(h), hstr)
2883 h = Header(hstr, maxlinelen=1024, header_name='Subject')
2884 eq(h.encode(), hstr)
2885 eq(str(h), hstr)
2886
Guido van Rossum9604e662007-08-30 03:46:43 +00002887 def test_quopri_splittable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002888 eq = self.ndiffAssertEqual
2889 h = Header(charset='iso-8859-1', maxlinelen=20)
Guido van Rossum9604e662007-08-30 03:46:43 +00002890 x = 'xxxx ' * 20
2891 h.append(x)
2892 s = h.encode()
2893 eq(s, """\
2894=?iso-8859-1?q?xxx?=
2895 =?iso-8859-1?q?x_?=
2896 =?iso-8859-1?q?xx?=
2897 =?iso-8859-1?q?xx?=
2898 =?iso-8859-1?q?_x?=
2899 =?iso-8859-1?q?xx?=
2900 =?iso-8859-1?q?x_?=
2901 =?iso-8859-1?q?xx?=
2902 =?iso-8859-1?q?xx?=
2903 =?iso-8859-1?q?_x?=
2904 =?iso-8859-1?q?xx?=
2905 =?iso-8859-1?q?x_?=
2906 =?iso-8859-1?q?xx?=
2907 =?iso-8859-1?q?xx?=
2908 =?iso-8859-1?q?_x?=
2909 =?iso-8859-1?q?xx?=
2910 =?iso-8859-1?q?x_?=
2911 =?iso-8859-1?q?xx?=
2912 =?iso-8859-1?q?xx?=
2913 =?iso-8859-1?q?_x?=
2914 =?iso-8859-1?q?xx?=
2915 =?iso-8859-1?q?x_?=
2916 =?iso-8859-1?q?xx?=
2917 =?iso-8859-1?q?xx?=
2918 =?iso-8859-1?q?_x?=
2919 =?iso-8859-1?q?xx?=
2920 =?iso-8859-1?q?x_?=
2921 =?iso-8859-1?q?xx?=
2922 =?iso-8859-1?q?xx?=
2923 =?iso-8859-1?q?_x?=
2924 =?iso-8859-1?q?xx?=
2925 =?iso-8859-1?q?x_?=
2926 =?iso-8859-1?q?xx?=
2927 =?iso-8859-1?q?xx?=
2928 =?iso-8859-1?q?_x?=
2929 =?iso-8859-1?q?xx?=
2930 =?iso-8859-1?q?x_?=
2931 =?iso-8859-1?q?xx?=
2932 =?iso-8859-1?q?xx?=
2933 =?iso-8859-1?q?_x?=
2934 =?iso-8859-1?q?xx?=
2935 =?iso-8859-1?q?x_?=
2936 =?iso-8859-1?q?xx?=
2937 =?iso-8859-1?q?xx?=
2938 =?iso-8859-1?q?_x?=
2939 =?iso-8859-1?q?xx?=
2940 =?iso-8859-1?q?x_?=
2941 =?iso-8859-1?q?xx?=
2942 =?iso-8859-1?q?xx?=
2943 =?iso-8859-1?q?_?=""")
2944 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002945 h = Header(charset='iso-8859-1', maxlinelen=40)
2946 h.append('xxxx ' * 20)
Guido van Rossum9604e662007-08-30 03:46:43 +00002947 s = h.encode()
2948 eq(s, """\
2949=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xxx?=
2950 =?iso-8859-1?q?x_xxxx_xxxx_xxxx_xxxx_?=
2951 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
2952 =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
2953 =?iso-8859-1?q?_xxxx_xxxx_?=""")
2954 eq(x, str(make_header(decode_header(s))))
2955
2956 def test_base64_splittable(self):
2957 eq = self.ndiffAssertEqual
2958 h = Header(charset='koi8-r', maxlinelen=20)
2959 x = 'xxxx ' * 20
2960 h.append(x)
2961 s = h.encode()
2962 eq(s, """\
2963=?koi8-r?b?eHh4?=
2964 =?koi8-r?b?eCB4?=
2965 =?koi8-r?b?eHh4?=
2966 =?koi8-r?b?IHh4?=
2967 =?koi8-r?b?eHgg?=
2968 =?koi8-r?b?eHh4?=
2969 =?koi8-r?b?eCB4?=
2970 =?koi8-r?b?eHh4?=
2971 =?koi8-r?b?IHh4?=
2972 =?koi8-r?b?eHgg?=
2973 =?koi8-r?b?eHh4?=
2974 =?koi8-r?b?eCB4?=
2975 =?koi8-r?b?eHh4?=
2976 =?koi8-r?b?IHh4?=
2977 =?koi8-r?b?eHgg?=
2978 =?koi8-r?b?eHh4?=
2979 =?koi8-r?b?eCB4?=
2980 =?koi8-r?b?eHh4?=
2981 =?koi8-r?b?IHh4?=
2982 =?koi8-r?b?eHgg?=
2983 =?koi8-r?b?eHh4?=
2984 =?koi8-r?b?eCB4?=
2985 =?koi8-r?b?eHh4?=
2986 =?koi8-r?b?IHh4?=
2987 =?koi8-r?b?eHgg?=
2988 =?koi8-r?b?eHh4?=
2989 =?koi8-r?b?eCB4?=
2990 =?koi8-r?b?eHh4?=
2991 =?koi8-r?b?IHh4?=
2992 =?koi8-r?b?eHgg?=
2993 =?koi8-r?b?eHh4?=
2994 =?koi8-r?b?eCB4?=
2995 =?koi8-r?b?eHh4?=
2996 =?koi8-r?b?IA==?=""")
2997 eq(x, str(make_header(decode_header(s))))
2998 h = Header(charset='koi8-r', maxlinelen=40)
2999 h.append(x)
3000 s = h.encode()
3001 eq(s, """\
3002=?koi8-r?b?eHh4eCB4eHh4IHh4eHggeHh4?=
3003 =?koi8-r?b?eCB4eHh4IHh4eHggeHh4eCB4?=
3004 =?koi8-r?b?eHh4IHh4eHggeHh4eCB4eHh4?=
3005 =?koi8-r?b?IHh4eHggeHh4eCB4eHh4IHh4?=
3006 =?koi8-r?b?eHggeHh4eCB4eHh4IHh4eHgg?=
3007 =?koi8-r?b?eHh4eCB4eHh4IA==?=""")
3008 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003009
3010 def test_us_ascii_header(self):
3011 eq = self.assertEqual
3012 s = 'hello'
3013 x = decode_header(s)
3014 eq(x, [('hello', None)])
3015 h = make_header(x)
3016 eq(s, h.encode())
3017
3018 def test_string_charset(self):
3019 eq = self.assertEqual
3020 h = Header()
3021 h.append('hello', 'iso-8859-1')
Guido van Rossum9604e662007-08-30 03:46:43 +00003022 eq(h, 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003023
3024## def test_unicode_error(self):
3025## raises = self.assertRaises
3026## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')
3027## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')
3028## h = Header()
3029## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')
3030## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')
3031## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')
3032
3033 def test_utf8_shortest(self):
3034 eq = self.assertEqual
3035 h = Header('p\xf6stal', 'utf-8')
3036 eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
3037 h = Header('\u83ca\u5730\u6642\u592b', 'utf-8')
3038 eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
3039
3040 def test_bad_8bit_header(self):
3041 raises = self.assertRaises
3042 eq = self.assertEqual
3043 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
3044 raises(UnicodeError, Header, x)
3045 h = Header()
3046 raises(UnicodeError, h.append, x)
3047 e = x.decode('utf-8', 'replace')
3048 eq(str(Header(x, errors='replace')), e)
3049 h.append(x, errors='replace')
3050 eq(str(h), e)
3051
3052 def test_encoded_adjacent_nonencoded(self):
3053 eq = self.assertEqual
3054 h = Header()
3055 h.append('hello', 'iso-8859-1')
3056 h.append('world')
3057 s = h.encode()
3058 eq(s, '=?iso-8859-1?q?hello?= world')
3059 h = make_header(decode_header(s))
3060 eq(h.encode(), s)
3061
3062 def test_whitespace_eater(self):
3063 eq = self.assertEqual
3064 s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
3065 parts = decode_header(s)
3066 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)])
3067 hdr = make_header(parts)
3068 eq(hdr.encode(),
3069 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
3070
3071 def test_broken_base64_header(self):
3072 raises = self.assertRaises
3073 s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3IQ?='
3074 raises(errors.HeaderParseError, decode_header, s)
3075
3076
3077
3078# Test RFC 2231 header parameters (en/de)coding
3079class TestRFC2231(TestEmailBase):
3080 def test_get_param(self):
3081 eq = self.assertEqual
3082 msg = self._msgobj('msg_29.txt')
3083 eq(msg.get_param('title'),
3084 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3085 eq(msg.get_param('title', unquote=False),
3086 ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
3087
3088 def test_set_param(self):
3089 eq = self.ndiffAssertEqual
3090 msg = Message()
3091 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3092 charset='us-ascii')
3093 eq(msg.get_param('title'),
3094 ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
3095 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3096 charset='us-ascii', language='en')
3097 eq(msg.get_param('title'),
3098 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3099 msg = self._msgobj('msg_01.txt')
3100 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3101 charset='us-ascii', language='en')
3102 eq(msg.as_string(maxheaderlen=78), """\
3103Return-Path: <bbb@zzz.org>
3104Delivered-To: bbb@zzz.org
3105Received: by mail.zzz.org (Postfix, from userid 889)
3106\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3107MIME-Version: 1.0
3108Content-Transfer-Encoding: 7bit
3109Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3110From: bbb@ddd.com (John X. Doe)
3111To: bbb@zzz.org
3112Subject: This is a test message
3113Date: Fri, 4 May 2001 14:05:44 -0400
3114Content-Type: text/plain; charset=us-ascii;
3115 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3116
3117
3118Hi,
3119
3120Do you like this message?
3121
3122-Me
3123""")
3124
3125 def test_del_param(self):
3126 eq = self.ndiffAssertEqual
3127 msg = self._msgobj('msg_01.txt')
3128 msg.set_param('foo', 'bar', charset='us-ascii', language='en')
3129 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3130 charset='us-ascii', language='en')
3131 msg.del_param('foo', header='Content-Type')
3132 eq(msg.as_string(maxheaderlen=78), """\
3133Return-Path: <bbb@zzz.org>
3134Delivered-To: bbb@zzz.org
3135Received: by mail.zzz.org (Postfix, from userid 889)
3136\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3137MIME-Version: 1.0
3138Content-Transfer-Encoding: 7bit
3139Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3140From: bbb@ddd.com (John X. Doe)
3141To: bbb@zzz.org
3142Subject: This is a test message
3143Date: Fri, 4 May 2001 14:05:44 -0400
3144Content-Type: text/plain; charset="us-ascii";
3145 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3146
3147
3148Hi,
3149
3150Do you like this message?
3151
3152-Me
3153""")
3154
3155 def test_rfc2231_get_content_charset(self):
3156 eq = self.assertEqual
3157 msg = self._msgobj('msg_32.txt')
3158 eq(msg.get_content_charset(), 'us-ascii')
3159
3160 def test_rfc2231_no_language_or_charset(self):
3161 m = '''\
3162Content-Transfer-Encoding: 8bit
3163Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
3164Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
3165
3166'''
3167 msg = email.message_from_string(m)
3168 param = msg.get_param('NAME')
3169 self.failIf(isinstance(param, tuple))
3170 self.assertEqual(
3171 param,
3172 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
3173
3174 def test_rfc2231_no_language_or_charset_in_filename(self):
3175 m = '''\
3176Content-Disposition: inline;
3177\tfilename*0*="''This%20is%20even%20more%20";
3178\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3179\tfilename*2="is it not.pdf"
3180
3181'''
3182 msg = email.message_from_string(m)
3183 self.assertEqual(msg.get_filename(),
3184 'This is even more ***fun*** is it not.pdf')
3185
3186 def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
3187 m = '''\
3188Content-Disposition: inline;
3189\tfilename*0*="''This%20is%20even%20more%20";
3190\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3191\tfilename*2="is it not.pdf"
3192
3193'''
3194 msg = email.message_from_string(m)
3195 self.assertEqual(msg.get_filename(),
3196 'This is even more ***fun*** is it not.pdf')
3197
3198 def test_rfc2231_partly_encoded(self):
3199 m = '''\
3200Content-Disposition: inline;
3201\tfilename*0="''This%20is%20even%20more%20";
3202\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3203\tfilename*2="is it not.pdf"
3204
3205'''
3206 msg = email.message_from_string(m)
3207 self.assertEqual(
3208 msg.get_filename(),
3209 'This%20is%20even%20more%20***fun*** is it not.pdf')
3210
3211 def test_rfc2231_partly_nonencoded(self):
3212 m = '''\
3213Content-Disposition: inline;
3214\tfilename*0="This%20is%20even%20more%20";
3215\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
3216\tfilename*2="is it not.pdf"
3217
3218'''
3219 msg = email.message_from_string(m)
3220 self.assertEqual(
3221 msg.get_filename(),
3222 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
3223
3224 def test_rfc2231_no_language_or_charset_in_boundary(self):
3225 m = '''\
3226Content-Type: multipart/alternative;
3227\tboundary*0*="''This%20is%20even%20more%20";
3228\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
3229\tboundary*2="is it not.pdf"
3230
3231'''
3232 msg = email.message_from_string(m)
3233 self.assertEqual(msg.get_boundary(),
3234 'This is even more ***fun*** is it not.pdf')
3235
3236 def test_rfc2231_no_language_or_charset_in_charset(self):
3237 # This is a nonsensical charset value, but tests the code anyway
3238 m = '''\
3239Content-Type: text/plain;
3240\tcharset*0*="This%20is%20even%20more%20";
3241\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
3242\tcharset*2="is it not.pdf"
3243
3244'''
3245 msg = email.message_from_string(m)
3246 self.assertEqual(msg.get_content_charset(),
3247 'this is even more ***fun*** is it not.pdf')
3248
3249 def test_rfc2231_bad_encoding_in_filename(self):
3250 m = '''\
3251Content-Disposition: inline;
3252\tfilename*0*="bogus'xx'This%20is%20even%20more%20";
3253\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3254\tfilename*2="is it not.pdf"
3255
3256'''
3257 msg = email.message_from_string(m)
3258 self.assertEqual(msg.get_filename(),
3259 'This is even more ***fun*** is it not.pdf')
3260
3261 def test_rfc2231_bad_encoding_in_charset(self):
3262 m = """\
3263Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
3264
3265"""
3266 msg = email.message_from_string(m)
3267 # This should return None because non-ascii characters in the charset
3268 # are not allowed.
3269 self.assertEqual(msg.get_content_charset(), None)
3270
3271 def test_rfc2231_bad_character_in_charset(self):
3272 m = """\
3273Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D
3274
3275"""
3276 msg = email.message_from_string(m)
3277 # This should return None because non-ascii characters in the charset
3278 # are not allowed.
3279 self.assertEqual(msg.get_content_charset(), None)
3280
3281 def test_rfc2231_bad_character_in_filename(self):
3282 m = '''\
3283Content-Disposition: inline;
3284\tfilename*0*="ascii'xx'This%20is%20even%20more%20";
3285\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3286\tfilename*2*="is it not.pdf%E2"
3287
3288'''
3289 msg = email.message_from_string(m)
3290 self.assertEqual(msg.get_filename(),
3291 'This is even more ***fun*** is it not.pdf\ufffd')
3292
3293 def test_rfc2231_unknown_encoding(self):
3294 m = """\
3295Content-Transfer-Encoding: 8bit
3296Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
3297
3298"""
3299 msg = email.message_from_string(m)
3300 self.assertEqual(msg.get_filename(), 'myfile.txt')
3301
3302 def test_rfc2231_single_tick_in_filename_extended(self):
3303 eq = self.assertEqual
3304 m = """\
3305Content-Type: application/x-foo;
3306\tname*0*=\"Frank's\"; name*1*=\" Document\"
3307
3308"""
3309 msg = email.message_from_string(m)
3310 charset, language, s = msg.get_param('name')
3311 eq(charset, None)
3312 eq(language, None)
3313 eq(s, "Frank's Document")
3314
3315 def test_rfc2231_single_tick_in_filename(self):
3316 m = """\
3317Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
3318
3319"""
3320 msg = email.message_from_string(m)
3321 param = msg.get_param('name')
3322 self.failIf(isinstance(param, tuple))
3323 self.assertEqual(param, "Frank's Document")
3324
3325 def test_rfc2231_tick_attack_extended(self):
3326 eq = self.assertEqual
3327 m = """\
3328Content-Type: application/x-foo;
3329\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
3330
3331"""
3332 msg = email.message_from_string(m)
3333 charset, language, s = msg.get_param('name')
3334 eq(charset, 'us-ascii')
3335 eq(language, 'en-us')
3336 eq(s, "Frank's Document")
3337
3338 def test_rfc2231_tick_attack(self):
3339 m = """\
3340Content-Type: application/x-foo;
3341\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
3342
3343"""
3344 msg = email.message_from_string(m)
3345 param = msg.get_param('name')
3346 self.failIf(isinstance(param, tuple))
3347 self.assertEqual(param, "us-ascii'en-us'Frank's Document")
3348
3349 def test_rfc2231_no_extended_values(self):
3350 eq = self.assertEqual
3351 m = """\
3352Content-Type: application/x-foo; name=\"Frank's Document\"
3353
3354"""
3355 msg = email.message_from_string(m)
3356 eq(msg.get_param('name'), "Frank's Document")
3357
3358 def test_rfc2231_encoded_then_unencoded_segments(self):
3359 eq = self.assertEqual
3360 m = """\
3361Content-Type: application/x-foo;
3362\tname*0*=\"us-ascii'en-us'My\";
3363\tname*1=\" Document\";
3364\tname*2*=\" For You\"
3365
3366"""
3367 msg = email.message_from_string(m)
3368 charset, language, s = msg.get_param('name')
3369 eq(charset, 'us-ascii')
3370 eq(language, 'en-us')
3371 eq(s, 'My Document For You')
3372
3373 def test_rfc2231_unencoded_then_encoded_segments(self):
3374 eq = self.assertEqual
3375 m = """\
3376Content-Type: application/x-foo;
3377\tname*0=\"us-ascii'en-us'My\";
3378\tname*1*=\" Document\";
3379\tname*2*=\" For You\"
3380
3381"""
3382 msg = email.message_from_string(m)
3383 charset, language, s = msg.get_param('name')
3384 eq(charset, 'us-ascii')
3385 eq(language, 'en-us')
3386 eq(s, 'My Document For You')
3387
3388
3389
3390def _testclasses():
3391 mod = sys.modules[__name__]
3392 return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
3393
3394
3395def suite():
3396 suite = unittest.TestSuite()
3397 for testclass in _testclasses():
3398 suite.addTest(unittest.makeSuite(testclass))
3399 return suite
3400
3401
3402def test_main():
3403 for testclass in _testclasses():
3404 run_unittest(testclass)
3405
3406
3407
3408if __name__ == '__main__':
3409 unittest.main(defaultTest='suite')