blob: 104840ac85f1fd078328ff7e8e943163591ef0a9 [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
37from test.test_support import findfile, run_unittest
38from 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>,
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000772\t"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
817 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
818Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by
819\throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
820\tWed, 05 Mar 2003 18:10:18 -0700
821Received-2: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by
822\throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP;
823\tWed, 05 Mar 2003 18:10:18 -0700
824
825""")
826
827 def test_string_headerinst_eq(self):
828 h = ('<15975.17901.207240.414604@sgigritzmann1.mathematik.'
829 'tu-muenchen.de> (David Bremner\'s message of '
830 '"Thu, 6 Mar 2003 13:58:21 +0100")')
831 msg = Message()
832 msg['Received-1'] = Header(h, header_name='Received-1',
833 continuation_ws='\t')
834 msg['Received-2'] = h
835 self.ndiffAssertEqual(msg.as_string(maxheaderlen=78), """\
836Received-1: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de>
Guido van Rossum9604e662007-08-30 03:46:43 +0000837 (David Bremner's message of \"Thu, 6 Mar 2003 13:58:21 +0100\")
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000838Received-2: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de>
Guido van Rossum9604e662007-08-30 03:46:43 +0000839 (David Bremner's message of \"Thu, 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')
851 eq(msg.as_string(maxheaderlen=78), """\
852Face-1: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
853 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
854Face-2: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9
855 locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp
856
857""")
858
859 def test_another_long_multiline_header(self):
860 eq = self.ndiffAssertEqual
861 m = ('Received: from siimage.com '
862 '([172.25.1.3]) by zima.siliconimage.com with '
Guido van Rossum9604e662007-08-30 03:46:43 +0000863 'Microsoft SMTPSVC(5.0.2195.4905); '
864 'Wed, 16 Oct 2002 07:41:11 -0700')
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000865 msg = email.message_from_string(m)
866 eq(msg.as_string(maxheaderlen=78), '''\
867Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with
Guido van Rossum9604e662007-08-30 03:46:43 +0000868 Microsoft SMTPSVC(5.0.2195.4905); Wed, 16 Oct 2002 07:41:11 -0700
Guido van Rossum8b3febe2007-08-30 01:15:14 +0000869
870''')
871
872 def test_long_lines_with_different_header(self):
873 eq = self.ndiffAssertEqual
874 h = ('List-Unsubscribe: '
875 '<http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,'
876 ' <mailto:spamassassin-talk-request@lists.sourceforge.net'
877 '?subject=unsubscribe>')
878 msg = Message()
879 msg['List'] = h
880 msg['List'] = Header(h, header_name='List')
881 eq(msg.as_string(maxheaderlen=78), """\
882List: List-Unsubscribe: <http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
883\t<mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
884List: List-Unsubscribe: <http://lists.sourceforge.net/lists/listinfo/spamassassin-talk>,
885 <mailto:spamassassin-talk-request@lists.sourceforge.net?subject=unsubscribe>
886
887""")
888
889
890
891# Test mangling of "From " lines in the body of a message
892class TestFromMangling(unittest.TestCase):
893 def setUp(self):
894 self.msg = Message()
895 self.msg['From'] = 'aaa@bbb.org'
896 self.msg.set_payload("""\
897From the desk of A.A.A.:
898Blah blah blah
899""")
900
901 def test_mangled_from(self):
902 s = StringIO()
903 g = Generator(s, mangle_from_=True)
904 g.flatten(self.msg)
905 self.assertEqual(s.getvalue(), """\
906From: aaa@bbb.org
907
908>From the desk of A.A.A.:
909Blah blah blah
910""")
911
912 def test_dont_mangle_from(self):
913 s = StringIO()
914 g = Generator(s, mangle_from_=False)
915 g.flatten(self.msg)
916 self.assertEqual(s.getvalue(), """\
917From: aaa@bbb.org
918
919From the desk of A.A.A.:
920Blah blah blah
921""")
922
923
924
925# Test the basic MIMEAudio class
926class TestMIMEAudio(unittest.TestCase):
927 def setUp(self):
928 # Make sure we pick up the audiotest.au that lives in email/test/data.
929 # In Python, there's an audiotest.au living in Lib/test but that isn't
930 # included in some binary distros that don't include the test
931 # package. The trailing empty string on the .join() is significant
932 # since findfile() will do a dirname().
933 datadir = os.path.join(os.path.dirname(landmark), 'data', '')
934 with open(findfile('audiotest.au', datadir), 'rb') as fp:
935 self._audiodata = fp.read()
936 self._au = MIMEAudio(self._audiodata)
937
938 def test_guess_minor_type(self):
939 self.assertEqual(self._au.get_content_type(), 'audio/basic')
940
941 def test_encoding(self):
942 payload = self._au.get_payload()
943 self.assertEqual(base64.decodestring(payload), self._audiodata)
944
945 def test_checkSetMinor(self):
946 au = MIMEAudio(self._audiodata, 'fish')
947 self.assertEqual(au.get_content_type(), 'audio/fish')
948
949 def test_add_header(self):
950 eq = self.assertEqual
951 unless = self.failUnless
952 self._au.add_header('Content-Disposition', 'attachment',
953 filename='audiotest.au')
954 eq(self._au['content-disposition'],
955 'attachment; filename="audiotest.au"')
956 eq(self._au.get_params(header='content-disposition'),
957 [('attachment', ''), ('filename', 'audiotest.au')])
958 eq(self._au.get_param('filename', header='content-disposition'),
959 'audiotest.au')
960 missing = []
961 eq(self._au.get_param('attachment', header='content-disposition'), '')
962 unless(self._au.get_param('foo', failobj=missing,
963 header='content-disposition') is missing)
964 # Try some missing stuff
965 unless(self._au.get_param('foobar', missing) is missing)
966 unless(self._au.get_param('attachment', missing,
967 header='foobar') is missing)
968
969
970
971# Test the basic MIMEImage class
972class TestMIMEImage(unittest.TestCase):
973 def setUp(self):
974 with openfile('PyBanner048.gif', 'rb') as fp:
975 self._imgdata = fp.read()
976 self._im = MIMEImage(self._imgdata)
977
978 def test_guess_minor_type(self):
979 self.assertEqual(self._im.get_content_type(), 'image/gif')
980
981 def test_encoding(self):
982 payload = self._im.get_payload()
983 self.assertEqual(base64.decodestring(payload), self._imgdata)
984
985 def test_checkSetMinor(self):
986 im = MIMEImage(self._imgdata, 'fish')
987 self.assertEqual(im.get_content_type(), 'image/fish')
988
989 def test_add_header(self):
990 eq = self.assertEqual
991 unless = self.failUnless
992 self._im.add_header('Content-Disposition', 'attachment',
993 filename='dingusfish.gif')
994 eq(self._im['content-disposition'],
995 'attachment; filename="dingusfish.gif"')
996 eq(self._im.get_params(header='content-disposition'),
997 [('attachment', ''), ('filename', 'dingusfish.gif')])
998 eq(self._im.get_param('filename', header='content-disposition'),
999 'dingusfish.gif')
1000 missing = []
1001 eq(self._im.get_param('attachment', header='content-disposition'), '')
1002 unless(self._im.get_param('foo', failobj=missing,
1003 header='content-disposition') is missing)
1004 # Try some missing stuff
1005 unless(self._im.get_param('foobar', missing) is missing)
1006 unless(self._im.get_param('attachment', missing,
1007 header='foobar') is missing)
1008
1009
1010
1011# Test the basic MIMEApplication class
1012class TestMIMEApplication(unittest.TestCase):
1013 def test_headers(self):
1014 eq = self.assertEqual
Barry Warsaw8b2af272007-08-31 03:04:26 +00001015 msg = MIMEApplication(b'\xfa\xfb\xfc\xfd\xfe\xff')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001016 eq(msg.get_content_type(), 'application/octet-stream')
1017 eq(msg['content-transfer-encoding'], 'base64')
1018
1019 def test_body(self):
1020 eq = self.assertEqual
Barry Warsaw8c571042007-08-30 19:17:18 +00001021 bytes = b'\xfa\xfb\xfc\xfd\xfe\xff'
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001022 msg = MIMEApplication(bytes)
Barry Warsaw8c571042007-08-30 19:17:18 +00001023 eq(msg.get_payload(), b'+vv8/f7/')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001024 eq(msg.get_payload(decode=True), bytes)
1025
1026
1027
1028# Test the basic MIMEText class
1029class TestMIMEText(unittest.TestCase):
1030 def setUp(self):
1031 self._msg = MIMEText('hello there')
1032
1033 def test_types(self):
1034 eq = self.assertEqual
1035 unless = self.failUnless
1036 eq(self._msg.get_content_type(), 'text/plain')
1037 eq(self._msg.get_param('charset'), 'us-ascii')
1038 missing = []
1039 unless(self._msg.get_param('foobar', missing) is missing)
1040 unless(self._msg.get_param('charset', missing, header='foobar')
1041 is missing)
1042
1043 def test_payload(self):
1044 self.assertEqual(self._msg.get_payload(), 'hello there')
1045 self.failUnless(not self._msg.is_multipart())
1046
1047 def test_charset(self):
1048 eq = self.assertEqual
1049 msg = MIMEText('hello there', _charset='us-ascii')
1050 eq(msg.get_charset().input_charset, 'us-ascii')
1051 eq(msg['content-type'], 'text/plain; charset="us-ascii"')
1052
1053
1054
1055# Test complicated multipart/* messages
1056class TestMultipart(TestEmailBase):
1057 def setUp(self):
1058 with openfile('PyBanner048.gif', 'rb') as fp:
1059 data = fp.read()
1060 container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
1061 image = MIMEImage(data, name='dingusfish.gif')
1062 image.add_header('content-disposition', 'attachment',
1063 filename='dingusfish.gif')
1064 intro = MIMEText('''\
1065Hi there,
1066
1067This is the dingus fish.
1068''')
1069 container.attach(intro)
1070 container.attach(image)
1071 container['From'] = 'Barry <barry@digicool.com>'
1072 container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>'
1073 container['Subject'] = 'Here is your dingus fish'
1074
1075 now = 987809702.54848599
1076 timetuple = time.localtime(now)
1077 if timetuple[-1] == 0:
1078 tzsecs = time.timezone
1079 else:
1080 tzsecs = time.altzone
1081 if tzsecs > 0:
1082 sign = '-'
1083 else:
1084 sign = '+'
1085 tzoffset = ' %s%04d' % (sign, tzsecs / 36)
1086 container['Date'] = time.strftime(
1087 '%a, %d %b %Y %H:%M:%S',
1088 time.localtime(now)) + tzoffset
1089 self._msg = container
1090 self._im = image
1091 self._txt = intro
1092
1093 def test_hierarchy(self):
1094 # convenience
1095 eq = self.assertEqual
1096 unless = self.failUnless
1097 raises = self.assertRaises
1098 # tests
1099 m = self._msg
1100 unless(m.is_multipart())
1101 eq(m.get_content_type(), 'multipart/mixed')
1102 eq(len(m.get_payload()), 2)
1103 raises(IndexError, m.get_payload, 2)
1104 m0 = m.get_payload(0)
1105 m1 = m.get_payload(1)
1106 unless(m0 is self._txt)
1107 unless(m1 is self._im)
1108 eq(m.get_payload(), [m0, m1])
1109 unless(not m0.is_multipart())
1110 unless(not m1.is_multipart())
1111
1112 def test_empty_multipart_idempotent(self):
1113 text = """\
1114Content-Type: multipart/mixed; boundary="BOUNDARY"
1115MIME-Version: 1.0
1116Subject: A subject
1117To: aperson@dom.ain
1118From: bperson@dom.ain
1119
1120
1121--BOUNDARY
1122
1123
1124--BOUNDARY--
1125"""
1126 msg = Parser().parsestr(text)
1127 self.ndiffAssertEqual(text, msg.as_string())
1128
1129 def test_no_parts_in_a_multipart_with_none_epilogue(self):
1130 outer = MIMEBase('multipart', 'mixed')
1131 outer['Subject'] = 'A subject'
1132 outer['To'] = 'aperson@dom.ain'
1133 outer['From'] = 'bperson@dom.ain'
1134 outer.set_boundary('BOUNDARY')
1135 self.ndiffAssertEqual(outer.as_string(), '''\
1136Content-Type: multipart/mixed; boundary="BOUNDARY"
1137MIME-Version: 1.0
1138Subject: A subject
1139To: aperson@dom.ain
1140From: bperson@dom.ain
1141
1142--BOUNDARY
1143
1144--BOUNDARY--''')
1145
1146 def test_no_parts_in_a_multipart_with_empty_epilogue(self):
1147 outer = MIMEBase('multipart', 'mixed')
1148 outer['Subject'] = 'A subject'
1149 outer['To'] = 'aperson@dom.ain'
1150 outer['From'] = 'bperson@dom.ain'
1151 outer.preamble = ''
1152 outer.epilogue = ''
1153 outer.set_boundary('BOUNDARY')
1154 self.ndiffAssertEqual(outer.as_string(), '''\
1155Content-Type: multipart/mixed; boundary="BOUNDARY"
1156MIME-Version: 1.0
1157Subject: A subject
1158To: aperson@dom.ain
1159From: bperson@dom.ain
1160
1161
1162--BOUNDARY
1163
1164--BOUNDARY--
1165''')
1166
1167 def test_one_part_in_a_multipart(self):
1168 eq = self.ndiffAssertEqual
1169 outer = MIMEBase('multipart', 'mixed')
1170 outer['Subject'] = 'A subject'
1171 outer['To'] = 'aperson@dom.ain'
1172 outer['From'] = 'bperson@dom.ain'
1173 outer.set_boundary('BOUNDARY')
1174 msg = MIMEText('hello world')
1175 outer.attach(msg)
1176 eq(outer.as_string(), '''\
1177Content-Type: multipart/mixed; boundary="BOUNDARY"
1178MIME-Version: 1.0
1179Subject: A subject
1180To: aperson@dom.ain
1181From: bperson@dom.ain
1182
1183--BOUNDARY
1184Content-Type: text/plain; charset="us-ascii"
1185MIME-Version: 1.0
1186Content-Transfer-Encoding: 7bit
1187
1188hello world
1189--BOUNDARY--''')
1190
1191 def test_seq_parts_in_a_multipart_with_empty_preamble(self):
1192 eq = self.ndiffAssertEqual
1193 outer = MIMEBase('multipart', 'mixed')
1194 outer['Subject'] = 'A subject'
1195 outer['To'] = 'aperson@dom.ain'
1196 outer['From'] = 'bperson@dom.ain'
1197 outer.preamble = ''
1198 msg = MIMEText('hello world')
1199 outer.attach(msg)
1200 outer.set_boundary('BOUNDARY')
1201 eq(outer.as_string(), '''\
1202Content-Type: multipart/mixed; boundary="BOUNDARY"
1203MIME-Version: 1.0
1204Subject: A subject
1205To: aperson@dom.ain
1206From: bperson@dom.ain
1207
1208
1209--BOUNDARY
1210Content-Type: text/plain; charset="us-ascii"
1211MIME-Version: 1.0
1212Content-Transfer-Encoding: 7bit
1213
1214hello world
1215--BOUNDARY--''')
1216
1217
1218 def test_seq_parts_in_a_multipart_with_none_preamble(self):
1219 eq = self.ndiffAssertEqual
1220 outer = MIMEBase('multipart', 'mixed')
1221 outer['Subject'] = 'A subject'
1222 outer['To'] = 'aperson@dom.ain'
1223 outer['From'] = 'bperson@dom.ain'
1224 outer.preamble = None
1225 msg = MIMEText('hello world')
1226 outer.attach(msg)
1227 outer.set_boundary('BOUNDARY')
1228 eq(outer.as_string(), '''\
1229Content-Type: multipart/mixed; boundary="BOUNDARY"
1230MIME-Version: 1.0
1231Subject: A subject
1232To: aperson@dom.ain
1233From: bperson@dom.ain
1234
1235--BOUNDARY
1236Content-Type: text/plain; charset="us-ascii"
1237MIME-Version: 1.0
1238Content-Transfer-Encoding: 7bit
1239
1240hello world
1241--BOUNDARY--''')
1242
1243
1244 def test_seq_parts_in_a_multipart_with_none_epilogue(self):
1245 eq = self.ndiffAssertEqual
1246 outer = MIMEBase('multipart', 'mixed')
1247 outer['Subject'] = 'A subject'
1248 outer['To'] = 'aperson@dom.ain'
1249 outer['From'] = 'bperson@dom.ain'
1250 outer.epilogue = None
1251 msg = MIMEText('hello world')
1252 outer.attach(msg)
1253 outer.set_boundary('BOUNDARY')
1254 eq(outer.as_string(), '''\
1255Content-Type: multipart/mixed; boundary="BOUNDARY"
1256MIME-Version: 1.0
1257Subject: A subject
1258To: aperson@dom.ain
1259From: bperson@dom.ain
1260
1261--BOUNDARY
1262Content-Type: text/plain; charset="us-ascii"
1263MIME-Version: 1.0
1264Content-Transfer-Encoding: 7bit
1265
1266hello world
1267--BOUNDARY--''')
1268
1269
1270 def test_seq_parts_in_a_multipart_with_empty_epilogue(self):
1271 eq = self.ndiffAssertEqual
1272 outer = MIMEBase('multipart', 'mixed')
1273 outer['Subject'] = 'A subject'
1274 outer['To'] = 'aperson@dom.ain'
1275 outer['From'] = 'bperson@dom.ain'
1276 outer.epilogue = ''
1277 msg = MIMEText('hello world')
1278 outer.attach(msg)
1279 outer.set_boundary('BOUNDARY')
1280 eq(outer.as_string(), '''\
1281Content-Type: multipart/mixed; boundary="BOUNDARY"
1282MIME-Version: 1.0
1283Subject: A subject
1284To: aperson@dom.ain
1285From: bperson@dom.ain
1286
1287--BOUNDARY
1288Content-Type: text/plain; charset="us-ascii"
1289MIME-Version: 1.0
1290Content-Transfer-Encoding: 7bit
1291
1292hello world
1293--BOUNDARY--
1294''')
1295
1296
1297 def test_seq_parts_in_a_multipart_with_nl_epilogue(self):
1298 eq = self.ndiffAssertEqual
1299 outer = MIMEBase('multipart', 'mixed')
1300 outer['Subject'] = 'A subject'
1301 outer['To'] = 'aperson@dom.ain'
1302 outer['From'] = 'bperson@dom.ain'
1303 outer.epilogue = '\n'
1304 msg = MIMEText('hello world')
1305 outer.attach(msg)
1306 outer.set_boundary('BOUNDARY')
1307 eq(outer.as_string(), '''\
1308Content-Type: multipart/mixed; boundary="BOUNDARY"
1309MIME-Version: 1.0
1310Subject: A subject
1311To: aperson@dom.ain
1312From: bperson@dom.ain
1313
1314--BOUNDARY
1315Content-Type: text/plain; charset="us-ascii"
1316MIME-Version: 1.0
1317Content-Transfer-Encoding: 7bit
1318
1319hello world
1320--BOUNDARY--
1321
1322''')
1323
1324 def test_message_external_body(self):
1325 eq = self.assertEqual
1326 msg = self._msgobj('msg_36.txt')
1327 eq(len(msg.get_payload()), 2)
1328 msg1 = msg.get_payload(1)
1329 eq(msg1.get_content_type(), 'multipart/alternative')
1330 eq(len(msg1.get_payload()), 2)
1331 for subpart in msg1.get_payload():
1332 eq(subpart.get_content_type(), 'message/external-body')
1333 eq(len(subpart.get_payload()), 1)
1334 subsubpart = subpart.get_payload(0)
1335 eq(subsubpart.get_content_type(), 'text/plain')
1336
1337 def test_double_boundary(self):
1338 # msg_37.txt is a multipart that contains two dash-boundary's in a
1339 # row. Our interpretation of RFC 2046 calls for ignoring the second
1340 # and subsequent boundaries.
1341 msg = self._msgobj('msg_37.txt')
1342 self.assertEqual(len(msg.get_payload()), 3)
1343
1344 def test_nested_inner_contains_outer_boundary(self):
1345 eq = self.ndiffAssertEqual
1346 # msg_38.txt has an inner part that contains outer boundaries. My
1347 # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say
1348 # these are illegal and should be interpreted as unterminated inner
1349 # parts.
1350 msg = self._msgobj('msg_38.txt')
1351 sfp = StringIO()
1352 iterators._structure(msg, sfp)
1353 eq(sfp.getvalue(), """\
1354multipart/mixed
1355 multipart/mixed
1356 multipart/alternative
1357 text/plain
1358 text/plain
1359 text/plain
1360 text/plain
1361""")
1362
1363 def test_nested_with_same_boundary(self):
1364 eq = self.ndiffAssertEqual
1365 # msg 39.txt is similarly evil in that it's got inner parts that use
1366 # the same boundary as outer parts. Again, I believe the way this is
1367 # parsed is closest to the spirit of RFC 2046
1368 msg = self._msgobj('msg_39.txt')
1369 sfp = StringIO()
1370 iterators._structure(msg, sfp)
1371 eq(sfp.getvalue(), """\
1372multipart/mixed
1373 multipart/mixed
1374 multipart/alternative
1375 application/octet-stream
1376 application/octet-stream
1377 text/plain
1378""")
1379
1380 def test_boundary_in_non_multipart(self):
1381 msg = self._msgobj('msg_40.txt')
1382 self.assertEqual(msg.as_string(), '''\
1383MIME-Version: 1.0
1384Content-Type: text/html; boundary="--961284236552522269"
1385
1386----961284236552522269
1387Content-Type: text/html;
1388Content-Transfer-Encoding: 7Bit
1389
1390<html></html>
1391
1392----961284236552522269--
1393''')
1394
1395 def test_boundary_with_leading_space(self):
1396 eq = self.assertEqual
1397 msg = email.message_from_string('''\
1398MIME-Version: 1.0
1399Content-Type: multipart/mixed; boundary=" XXXX"
1400
1401-- XXXX
1402Content-Type: text/plain
1403
1404
1405-- XXXX
1406Content-Type: text/plain
1407
1408-- XXXX--
1409''')
1410 self.failUnless(msg.is_multipart())
1411 eq(msg.get_boundary(), ' XXXX')
1412 eq(len(msg.get_payload()), 2)
1413
1414 def test_boundary_without_trailing_newline(self):
1415 m = Parser().parsestr("""\
1416Content-Type: multipart/mixed; boundary="===============0012394164=="
1417MIME-Version: 1.0
1418
1419--===============0012394164==
1420Content-Type: image/file1.jpg
1421MIME-Version: 1.0
1422Content-Transfer-Encoding: base64
1423
1424YXNkZg==
1425--===============0012394164==--""")
1426 self.assertEquals(m.get_payload(0).get_payload(), 'YXNkZg==')
1427
1428
1429
1430# Test some badly formatted messages
1431class TestNonConformant(TestEmailBase):
1432 def test_parse_missing_minor_type(self):
1433 eq = self.assertEqual
1434 msg = self._msgobj('msg_14.txt')
1435 eq(msg.get_content_type(), 'text/plain')
1436 eq(msg.get_content_maintype(), 'text')
1437 eq(msg.get_content_subtype(), 'plain')
1438
1439 def test_same_boundary_inner_outer(self):
1440 unless = self.failUnless
1441 msg = self._msgobj('msg_15.txt')
1442 # XXX We can probably eventually do better
1443 inner = msg.get_payload(0)
1444 unless(hasattr(inner, 'defects'))
1445 self.assertEqual(len(inner.defects), 1)
1446 unless(isinstance(inner.defects[0],
1447 errors.StartBoundaryNotFoundDefect))
1448
1449 def test_multipart_no_boundary(self):
1450 unless = self.failUnless
1451 msg = self._msgobj('msg_25.txt')
1452 unless(isinstance(msg.get_payload(), str))
1453 self.assertEqual(len(msg.defects), 2)
1454 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1455 unless(isinstance(msg.defects[1],
1456 errors.MultipartInvariantViolationDefect))
1457
1458 def test_invalid_content_type(self):
1459 eq = self.assertEqual
1460 neq = self.ndiffAssertEqual
1461 msg = Message()
1462 # RFC 2045, $5.2 says invalid yields text/plain
1463 msg['Content-Type'] = 'text'
1464 eq(msg.get_content_maintype(), 'text')
1465 eq(msg.get_content_subtype(), 'plain')
1466 eq(msg.get_content_type(), 'text/plain')
1467 # Clear the old value and try something /really/ invalid
1468 del msg['content-type']
1469 msg['Content-Type'] = 'foo'
1470 eq(msg.get_content_maintype(), 'text')
1471 eq(msg.get_content_subtype(), 'plain')
1472 eq(msg.get_content_type(), 'text/plain')
1473 # Still, make sure that the message is idempotently generated
1474 s = StringIO()
1475 g = Generator(s)
1476 g.flatten(msg)
1477 neq(s.getvalue(), 'Content-Type: foo\n\n')
1478
1479 def test_no_start_boundary(self):
1480 eq = self.ndiffAssertEqual
1481 msg = self._msgobj('msg_31.txt')
1482 eq(msg.get_payload(), """\
1483--BOUNDARY
1484Content-Type: text/plain
1485
1486message 1
1487
1488--BOUNDARY
1489Content-Type: text/plain
1490
1491message 2
1492
1493--BOUNDARY--
1494""")
1495
1496 def test_no_separating_blank_line(self):
1497 eq = self.ndiffAssertEqual
1498 msg = self._msgobj('msg_35.txt')
1499 eq(msg.as_string(), """\
1500From: aperson@dom.ain
1501To: bperson@dom.ain
1502Subject: here's something interesting
1503
1504counter to RFC 2822, there's no separating newline here
1505""")
1506
1507 def test_lying_multipart(self):
1508 unless = self.failUnless
1509 msg = self._msgobj('msg_41.txt')
1510 unless(hasattr(msg, 'defects'))
1511 self.assertEqual(len(msg.defects), 2)
1512 unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect))
1513 unless(isinstance(msg.defects[1],
1514 errors.MultipartInvariantViolationDefect))
1515
1516 def test_missing_start_boundary(self):
1517 outer = self._msgobj('msg_42.txt')
1518 # The message structure is:
1519 #
1520 # multipart/mixed
1521 # text/plain
1522 # message/rfc822
1523 # multipart/mixed [*]
1524 #
1525 # [*] This message is missing its start boundary
1526 bad = outer.get_payload(1).get_payload(0)
1527 self.assertEqual(len(bad.defects), 1)
1528 self.failUnless(isinstance(bad.defects[0],
1529 errors.StartBoundaryNotFoundDefect))
1530
1531 def test_first_line_is_continuation_header(self):
1532 eq = self.assertEqual
1533 m = ' Line 1\nLine 2\nLine 3'
1534 msg = email.message_from_string(m)
1535 eq(msg.keys(), [])
1536 eq(msg.get_payload(), 'Line 2\nLine 3')
1537 eq(len(msg.defects), 1)
1538 self.failUnless(isinstance(msg.defects[0],
1539 errors.FirstHeaderLineIsContinuationDefect))
1540 eq(msg.defects[0].line, ' Line 1\n')
1541
1542
1543
1544# Test RFC 2047 header encoding and decoding
Guido van Rossum9604e662007-08-30 03:46:43 +00001545class TestRFC2047(TestEmailBase):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001546 def test_rfc2047_multiline(self):
1547 eq = self.assertEqual
1548 s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz
1549 foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?="""
1550 dh = decode_header(s)
1551 eq(dh, [
1552 (b'Re:', None),
1553 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland'),
1554 (b'baz foo bar', None),
1555 (b'r\x8aksm\x9arg\x8cs', 'mac-iceland')])
1556 header = make_header(dh)
1557 eq(str(header),
1558 'Re: r\xe4ksm\xf6rg\xe5s baz foo bar r\xe4ksm\xf6rg\xe5s')
Barry Warsaw00b34222007-08-31 02:35:00 +00001559 self.ndiffAssertEqual(header.encode(maxlinelen=76), """\
Guido van Rossum9604e662007-08-30 03:46:43 +00001560Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar =?mac-iceland?q?r=8Aksm?=
1561 =?mac-iceland?q?=9Arg=8Cs?=""")
Guido van Rossum8b3febe2007-08-30 01:15:14 +00001562
1563 def test_whitespace_eater_unicode(self):
1564 eq = self.assertEqual
1565 s = '=?ISO-8859-1?Q?Andr=E9?= Pirard <pirard@dom.ain>'
1566 dh = decode_header(s)
1567 eq(dh, [(b'Andr\xe9', 'iso-8859-1'),
1568 (b'Pirard <pirard@dom.ain>', None)])
1569 header = str(make_header(dh))
1570 eq(header, 'Andr\xe9 Pirard <pirard@dom.ain>')
1571
1572 def test_whitespace_eater_unicode_2(self):
1573 eq = self.assertEqual
1574 s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?='
1575 dh = decode_header(s)
1576 eq(dh, [(b'The', None), (b'quick brown fox', 'iso-8859-1'),
1577 (b'jumped over the', None), (b'lazy dog', 'iso-8859-1')])
1578 hu = str(make_header(dh))
1579 eq(hu, 'The quick brown fox jumped over the lazy dog')
1580
1581 def test_rfc2047_missing_whitespace(self):
1582 s = 'Sm=?ISO-8859-1?B?9g==?=rg=?ISO-8859-1?B?5Q==?=sbord'
1583 dh = decode_header(s)
1584 self.assertEqual(dh, [(s, None)])
1585
1586 def test_rfc2047_with_whitespace(self):
1587 s = 'Sm =?ISO-8859-1?B?9g==?= rg =?ISO-8859-1?B?5Q==?= sbord'
1588 dh = decode_header(s)
1589 self.assertEqual(dh, [(b'Sm', None), (b'\xf6', 'iso-8859-1'),
1590 (b'rg', None), (b'\xe5', 'iso-8859-1'),
1591 (b'sbord', None)])
1592
1593
1594
1595# Test the MIMEMessage class
1596class TestMIMEMessage(TestEmailBase):
1597 def setUp(self):
1598 with openfile('msg_11.txt') as fp:
1599 self._text = fp.read()
1600
1601 def test_type_error(self):
1602 self.assertRaises(TypeError, MIMEMessage, 'a plain string')
1603
1604 def test_valid_argument(self):
1605 eq = self.assertEqual
1606 unless = self.failUnless
1607 subject = 'A sub-message'
1608 m = Message()
1609 m['Subject'] = subject
1610 r = MIMEMessage(m)
1611 eq(r.get_content_type(), 'message/rfc822')
1612 payload = r.get_payload()
1613 unless(isinstance(payload, list))
1614 eq(len(payload), 1)
1615 subpart = payload[0]
1616 unless(subpart is m)
1617 eq(subpart['subject'], subject)
1618
1619 def test_bad_multipart(self):
1620 eq = self.assertEqual
1621 msg1 = Message()
1622 msg1['Subject'] = 'subpart 1'
1623 msg2 = Message()
1624 msg2['Subject'] = 'subpart 2'
1625 r = MIMEMessage(msg1)
1626 self.assertRaises(errors.MultipartConversionError, r.attach, msg2)
1627
1628 def test_generate(self):
1629 # First craft the message to be encapsulated
1630 m = Message()
1631 m['Subject'] = 'An enclosed message'
1632 m.set_payload('Here is the body of the message.\n')
1633 r = MIMEMessage(m)
1634 r['Subject'] = 'The enclosing message'
1635 s = StringIO()
1636 g = Generator(s)
1637 g.flatten(r)
1638 self.assertEqual(s.getvalue(), """\
1639Content-Type: message/rfc822
1640MIME-Version: 1.0
1641Subject: The enclosing message
1642
1643Subject: An enclosed message
1644
1645Here is the body of the message.
1646""")
1647
1648 def test_parse_message_rfc822(self):
1649 eq = self.assertEqual
1650 unless = self.failUnless
1651 msg = self._msgobj('msg_11.txt')
1652 eq(msg.get_content_type(), 'message/rfc822')
1653 payload = msg.get_payload()
1654 unless(isinstance(payload, list))
1655 eq(len(payload), 1)
1656 submsg = payload[0]
1657 self.failUnless(isinstance(submsg, Message))
1658 eq(submsg['subject'], 'An enclosed message')
1659 eq(submsg.get_payload(), 'Here is the body of the message.\n')
1660
1661 def test_dsn(self):
1662 eq = self.assertEqual
1663 unless = self.failUnless
1664 # msg 16 is a Delivery Status Notification, see RFC 1894
1665 msg = self._msgobj('msg_16.txt')
1666 eq(msg.get_content_type(), 'multipart/report')
1667 unless(msg.is_multipart())
1668 eq(len(msg.get_payload()), 3)
1669 # Subpart 1 is a text/plain, human readable section
1670 subpart = msg.get_payload(0)
1671 eq(subpart.get_content_type(), 'text/plain')
1672 eq(subpart.get_payload(), """\
1673This report relates to a message you sent with the following header fields:
1674
1675 Message-id: <002001c144a6$8752e060$56104586@oxy.edu>
1676 Date: Sun, 23 Sep 2001 20:10:55 -0700
1677 From: "Ian T. Henry" <henryi@oxy.edu>
1678 To: SoCal Raves <scr@socal-raves.org>
1679 Subject: [scr] yeah for Ians!!
1680
1681Your message cannot be delivered to the following recipients:
1682
1683 Recipient address: jangel1@cougar.noc.ucla.edu
1684 Reason: recipient reached disk quota
1685
1686""")
1687 # Subpart 2 contains the machine parsable DSN information. It
1688 # consists of two blocks of headers, represented by two nested Message
1689 # objects.
1690 subpart = msg.get_payload(1)
1691 eq(subpart.get_content_type(), 'message/delivery-status')
1692 eq(len(subpart.get_payload()), 2)
1693 # message/delivery-status should treat each block as a bunch of
1694 # headers, i.e. a bunch of Message objects.
1695 dsn1 = subpart.get_payload(0)
1696 unless(isinstance(dsn1, Message))
1697 eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu')
1698 eq(dsn1.get_param('dns', header='reporting-mta'), '')
1699 # Try a missing one <wink>
1700 eq(dsn1.get_param('nsd', header='reporting-mta'), None)
1701 dsn2 = subpart.get_payload(1)
1702 unless(isinstance(dsn2, Message))
1703 eq(dsn2['action'], 'failed')
1704 eq(dsn2.get_params(header='original-recipient'),
1705 [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')])
1706 eq(dsn2.get_param('rfc822', header='final-recipient'), '')
1707 # Subpart 3 is the original message
1708 subpart = msg.get_payload(2)
1709 eq(subpart.get_content_type(), 'message/rfc822')
1710 payload = subpart.get_payload()
1711 unless(isinstance(payload, list))
1712 eq(len(payload), 1)
1713 subsubpart = payload[0]
1714 unless(isinstance(subsubpart, Message))
1715 eq(subsubpart.get_content_type(), 'text/plain')
1716 eq(subsubpart['message-id'],
1717 '<002001c144a6$8752e060$56104586@oxy.edu>')
1718
1719 def test_epilogue(self):
1720 eq = self.ndiffAssertEqual
1721 with openfile('msg_21.txt') as fp:
1722 text = fp.read()
1723 msg = Message()
1724 msg['From'] = 'aperson@dom.ain'
1725 msg['To'] = 'bperson@dom.ain'
1726 msg['Subject'] = 'Test'
1727 msg.preamble = 'MIME message'
1728 msg.epilogue = 'End of MIME message\n'
1729 msg1 = MIMEText('One')
1730 msg2 = MIMEText('Two')
1731 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1732 msg.attach(msg1)
1733 msg.attach(msg2)
1734 sfp = StringIO()
1735 g = Generator(sfp)
1736 g.flatten(msg)
1737 eq(sfp.getvalue(), text)
1738
1739 def test_no_nl_preamble(self):
1740 eq = self.ndiffAssertEqual
1741 msg = Message()
1742 msg['From'] = 'aperson@dom.ain'
1743 msg['To'] = 'bperson@dom.ain'
1744 msg['Subject'] = 'Test'
1745 msg.preamble = 'MIME message'
1746 msg.epilogue = ''
1747 msg1 = MIMEText('One')
1748 msg2 = MIMEText('Two')
1749 msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY')
1750 msg.attach(msg1)
1751 msg.attach(msg2)
1752 eq(msg.as_string(), """\
1753From: aperson@dom.ain
1754To: bperson@dom.ain
1755Subject: Test
1756Content-Type: multipart/mixed; boundary="BOUNDARY"
1757
1758MIME message
1759--BOUNDARY
1760Content-Type: text/plain; charset="us-ascii"
1761MIME-Version: 1.0
1762Content-Transfer-Encoding: 7bit
1763
1764One
1765--BOUNDARY
1766Content-Type: text/plain; charset="us-ascii"
1767MIME-Version: 1.0
1768Content-Transfer-Encoding: 7bit
1769
1770Two
1771--BOUNDARY--
1772""")
1773
1774 def test_default_type(self):
1775 eq = self.assertEqual
1776 with openfile('msg_30.txt') as fp:
1777 msg = email.message_from_file(fp)
1778 container1 = msg.get_payload(0)
1779 eq(container1.get_default_type(), 'message/rfc822')
1780 eq(container1.get_content_type(), 'message/rfc822')
1781 container2 = msg.get_payload(1)
1782 eq(container2.get_default_type(), 'message/rfc822')
1783 eq(container2.get_content_type(), 'message/rfc822')
1784 container1a = container1.get_payload(0)
1785 eq(container1a.get_default_type(), 'text/plain')
1786 eq(container1a.get_content_type(), 'text/plain')
1787 container2a = container2.get_payload(0)
1788 eq(container2a.get_default_type(), 'text/plain')
1789 eq(container2a.get_content_type(), 'text/plain')
1790
1791 def test_default_type_with_explicit_container_type(self):
1792 eq = self.assertEqual
1793 with openfile('msg_28.txt') as fp:
1794 msg = email.message_from_file(fp)
1795 container1 = msg.get_payload(0)
1796 eq(container1.get_default_type(), 'message/rfc822')
1797 eq(container1.get_content_type(), 'message/rfc822')
1798 container2 = msg.get_payload(1)
1799 eq(container2.get_default_type(), 'message/rfc822')
1800 eq(container2.get_content_type(), 'message/rfc822')
1801 container1a = container1.get_payload(0)
1802 eq(container1a.get_default_type(), 'text/plain')
1803 eq(container1a.get_content_type(), 'text/plain')
1804 container2a = container2.get_payload(0)
1805 eq(container2a.get_default_type(), 'text/plain')
1806 eq(container2a.get_content_type(), 'text/plain')
1807
1808 def test_default_type_non_parsed(self):
1809 eq = self.assertEqual
1810 neq = self.ndiffAssertEqual
1811 # Set up container
1812 container = MIMEMultipart('digest', 'BOUNDARY')
1813 container.epilogue = ''
1814 # Set up subparts
1815 subpart1a = MIMEText('message 1\n')
1816 subpart2a = MIMEText('message 2\n')
1817 subpart1 = MIMEMessage(subpart1a)
1818 subpart2 = MIMEMessage(subpart2a)
1819 container.attach(subpart1)
1820 container.attach(subpart2)
1821 eq(subpart1.get_content_type(), 'message/rfc822')
1822 eq(subpart1.get_default_type(), 'message/rfc822')
1823 eq(subpart2.get_content_type(), 'message/rfc822')
1824 eq(subpart2.get_default_type(), 'message/rfc822')
1825 neq(container.as_string(0), '''\
1826Content-Type: multipart/digest; boundary="BOUNDARY"
1827MIME-Version: 1.0
1828
1829--BOUNDARY
1830Content-Type: message/rfc822
1831MIME-Version: 1.0
1832
1833Content-Type: text/plain; charset="us-ascii"
1834MIME-Version: 1.0
1835Content-Transfer-Encoding: 7bit
1836
1837message 1
1838
1839--BOUNDARY
1840Content-Type: message/rfc822
1841MIME-Version: 1.0
1842
1843Content-Type: text/plain; charset="us-ascii"
1844MIME-Version: 1.0
1845Content-Transfer-Encoding: 7bit
1846
1847message 2
1848
1849--BOUNDARY--
1850''')
1851 del subpart1['content-type']
1852 del subpart1['mime-version']
1853 del subpart2['content-type']
1854 del subpart2['mime-version']
1855 eq(subpart1.get_content_type(), 'message/rfc822')
1856 eq(subpart1.get_default_type(), 'message/rfc822')
1857 eq(subpart2.get_content_type(), 'message/rfc822')
1858 eq(subpart2.get_default_type(), 'message/rfc822')
1859 neq(container.as_string(0), '''\
1860Content-Type: multipart/digest; boundary="BOUNDARY"
1861MIME-Version: 1.0
1862
1863--BOUNDARY
1864
1865Content-Type: text/plain; charset="us-ascii"
1866MIME-Version: 1.0
1867Content-Transfer-Encoding: 7bit
1868
1869message 1
1870
1871--BOUNDARY
1872
1873Content-Type: text/plain; charset="us-ascii"
1874MIME-Version: 1.0
1875Content-Transfer-Encoding: 7bit
1876
1877message 2
1878
1879--BOUNDARY--
1880''')
1881
1882 def test_mime_attachments_in_constructor(self):
1883 eq = self.assertEqual
1884 text1 = MIMEText('')
1885 text2 = MIMEText('')
1886 msg = MIMEMultipart(_subparts=(text1, text2))
1887 eq(len(msg.get_payload()), 2)
1888 eq(msg.get_payload(0), text1)
1889 eq(msg.get_payload(1), text2)
1890
1891
1892
1893# A general test of parser->model->generator idempotency. IOW, read a message
1894# in, parse it into a message object tree, then without touching the tree,
1895# regenerate the plain text. The original text and the transformed text
1896# should be identical. Note: that we ignore the Unix-From since that may
1897# contain a changed date.
1898class TestIdempotent(TestEmailBase):
1899 def _msgobj(self, filename):
1900 with openfile(filename) as fp:
1901 data = fp.read()
1902 msg = email.message_from_string(data)
1903 return msg, data
1904
1905 def _idempotent(self, msg, text):
1906 eq = self.ndiffAssertEqual
1907 s = StringIO()
1908 g = Generator(s, maxheaderlen=0)
1909 g.flatten(msg)
1910 eq(text, s.getvalue())
1911
1912 def test_parse_text_message(self):
1913 eq = self.assertEquals
1914 msg, text = self._msgobj('msg_01.txt')
1915 eq(msg.get_content_type(), 'text/plain')
1916 eq(msg.get_content_maintype(), 'text')
1917 eq(msg.get_content_subtype(), 'plain')
1918 eq(msg.get_params()[1], ('charset', 'us-ascii'))
1919 eq(msg.get_param('charset'), 'us-ascii')
1920 eq(msg.preamble, None)
1921 eq(msg.epilogue, None)
1922 self._idempotent(msg, text)
1923
1924 def test_parse_untyped_message(self):
1925 eq = self.assertEquals
1926 msg, text = self._msgobj('msg_03.txt')
1927 eq(msg.get_content_type(), 'text/plain')
1928 eq(msg.get_params(), None)
1929 eq(msg.get_param('charset'), None)
1930 self._idempotent(msg, text)
1931
1932 def test_simple_multipart(self):
1933 msg, text = self._msgobj('msg_04.txt')
1934 self._idempotent(msg, text)
1935
1936 def test_MIME_digest(self):
1937 msg, text = self._msgobj('msg_02.txt')
1938 self._idempotent(msg, text)
1939
1940 def test_long_header(self):
1941 msg, text = self._msgobj('msg_27.txt')
1942 self._idempotent(msg, text)
1943
1944 def test_MIME_digest_with_part_headers(self):
1945 msg, text = self._msgobj('msg_28.txt')
1946 self._idempotent(msg, text)
1947
1948 def test_mixed_with_image(self):
1949 msg, text = self._msgobj('msg_06.txt')
1950 self._idempotent(msg, text)
1951
1952 def test_multipart_report(self):
1953 msg, text = self._msgobj('msg_05.txt')
1954 self._idempotent(msg, text)
1955
1956 def test_dsn(self):
1957 msg, text = self._msgobj('msg_16.txt')
1958 self._idempotent(msg, text)
1959
1960 def test_preamble_epilogue(self):
1961 msg, text = self._msgobj('msg_21.txt')
1962 self._idempotent(msg, text)
1963
1964 def test_multipart_one_part(self):
1965 msg, text = self._msgobj('msg_23.txt')
1966 self._idempotent(msg, text)
1967
1968 def test_multipart_no_parts(self):
1969 msg, text = self._msgobj('msg_24.txt')
1970 self._idempotent(msg, text)
1971
1972 def test_no_start_boundary(self):
1973 msg, text = self._msgobj('msg_31.txt')
1974 self._idempotent(msg, text)
1975
1976 def test_rfc2231_charset(self):
1977 msg, text = self._msgobj('msg_32.txt')
1978 self._idempotent(msg, text)
1979
1980 def test_more_rfc2231_parameters(self):
1981 msg, text = self._msgobj('msg_33.txt')
1982 self._idempotent(msg, text)
1983
1984 def test_text_plain_in_a_multipart_digest(self):
1985 msg, text = self._msgobj('msg_34.txt')
1986 self._idempotent(msg, text)
1987
1988 def test_nested_multipart_mixeds(self):
1989 msg, text = self._msgobj('msg_12a.txt')
1990 self._idempotent(msg, text)
1991
1992 def test_message_external_body_idempotent(self):
1993 msg, text = self._msgobj('msg_36.txt')
1994 self._idempotent(msg, text)
1995
1996 def test_content_type(self):
1997 eq = self.assertEquals
1998 unless = self.failUnless
1999 # Get a message object and reset the seek pointer for other tests
2000 msg, text = self._msgobj('msg_05.txt')
2001 eq(msg.get_content_type(), 'multipart/report')
2002 # Test the Content-Type: parameters
2003 params = {}
2004 for pk, pv in msg.get_params():
2005 params[pk] = pv
2006 eq(params['report-type'], 'delivery-status')
2007 eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
2008 eq(msg.preamble, 'This is a MIME-encapsulated message.\n')
2009 eq(msg.epilogue, '\n')
2010 eq(len(msg.get_payload()), 3)
2011 # Make sure the subparts are what we expect
2012 msg1 = msg.get_payload(0)
2013 eq(msg1.get_content_type(), 'text/plain')
2014 eq(msg1.get_payload(), 'Yadda yadda yadda\n')
2015 msg2 = msg.get_payload(1)
2016 eq(msg2.get_content_type(), 'text/plain')
2017 eq(msg2.get_payload(), 'Yadda yadda yadda\n')
2018 msg3 = msg.get_payload(2)
2019 eq(msg3.get_content_type(), 'message/rfc822')
2020 self.failUnless(isinstance(msg3, Message))
2021 payload = msg3.get_payload()
2022 unless(isinstance(payload, list))
2023 eq(len(payload), 1)
2024 msg4 = payload[0]
2025 unless(isinstance(msg4, Message))
2026 eq(msg4.get_payload(), 'Yadda yadda yadda\n')
2027
2028 def test_parser(self):
2029 eq = self.assertEquals
2030 unless = self.failUnless
2031 msg, text = self._msgobj('msg_06.txt')
2032 # Check some of the outer headers
2033 eq(msg.get_content_type(), 'message/rfc822')
2034 # Make sure the payload is a list of exactly one sub-Message, and that
2035 # that submessage has a type of text/plain
2036 payload = msg.get_payload()
2037 unless(isinstance(payload, list))
2038 eq(len(payload), 1)
2039 msg1 = payload[0]
2040 self.failUnless(isinstance(msg1, Message))
2041 eq(msg1.get_content_type(), 'text/plain')
2042 self.failUnless(isinstance(msg1.get_payload(), str))
2043 eq(msg1.get_payload(), '\n')
2044
2045
2046
2047# Test various other bits of the package's functionality
2048class TestMiscellaneous(TestEmailBase):
2049 def test_message_from_string(self):
2050 with openfile('msg_01.txt') as fp:
2051 text = fp.read()
2052 msg = email.message_from_string(text)
2053 s = StringIO()
2054 # Don't wrap/continue long headers since we're trying to test
2055 # idempotency.
2056 g = Generator(s, maxheaderlen=0)
2057 g.flatten(msg)
2058 self.assertEqual(text, s.getvalue())
2059
2060 def test_message_from_file(self):
2061 with openfile('msg_01.txt') as fp:
2062 text = fp.read()
2063 fp.seek(0)
2064 msg = email.message_from_file(fp)
2065 s = StringIO()
2066 # Don't wrap/continue long headers since we're trying to test
2067 # idempotency.
2068 g = Generator(s, maxheaderlen=0)
2069 g.flatten(msg)
2070 self.assertEqual(text, s.getvalue())
2071
2072 def test_message_from_string_with_class(self):
2073 unless = self.failUnless
2074 with openfile('msg_01.txt') as fp:
2075 text = fp.read()
2076
2077 # Create a subclass
2078 class MyMessage(Message):
2079 pass
2080
2081 msg = email.message_from_string(text, MyMessage)
2082 unless(isinstance(msg, MyMessage))
2083 # Try something more complicated
2084 with openfile('msg_02.txt') as fp:
2085 text = fp.read()
2086 msg = email.message_from_string(text, MyMessage)
2087 for subpart in msg.walk():
2088 unless(isinstance(subpart, MyMessage))
2089
2090 def test_message_from_file_with_class(self):
2091 unless = self.failUnless
2092 # Create a subclass
2093 class MyMessage(Message):
2094 pass
2095
2096 with openfile('msg_01.txt') as fp:
2097 msg = email.message_from_file(fp, MyMessage)
2098 unless(isinstance(msg, MyMessage))
2099 # Try something more complicated
2100 with openfile('msg_02.txt') as fp:
2101 msg = email.message_from_file(fp, MyMessage)
2102 for subpart in msg.walk():
2103 unless(isinstance(subpart, MyMessage))
2104
2105 def test__all__(self):
2106 module = __import__('email')
2107 # Can't use sorted() here due to Python 2.3 compatibility
2108 all = module.__all__[:]
2109 all.sort()
2110 self.assertEqual(all, [
2111 'base64mime', 'charset', 'encoders', 'errors', 'generator',
2112 'header', 'iterators', 'message', 'message_from_file',
2113 'message_from_string', 'mime', 'parser',
2114 'quoprimime', 'utils',
2115 ])
2116
2117 def test_formatdate(self):
2118 now = time.time()
2119 self.assertEqual(utils.parsedate(utils.formatdate(now))[:6],
2120 time.gmtime(now)[:6])
2121
2122 def test_formatdate_localtime(self):
2123 now = time.time()
2124 self.assertEqual(
2125 utils.parsedate(utils.formatdate(now, localtime=True))[:6],
2126 time.localtime(now)[:6])
2127
2128 def test_formatdate_usegmt(self):
2129 now = time.time()
2130 self.assertEqual(
2131 utils.formatdate(now, localtime=False),
2132 time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now)))
2133 self.assertEqual(
2134 utils.formatdate(now, localtime=False, usegmt=True),
2135 time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now)))
2136
2137 def test_parsedate_none(self):
2138 self.assertEqual(utils.parsedate(''), None)
2139
2140 def test_parsedate_compact(self):
2141 # The FWS after the comma is optional
2142 self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'),
2143 utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800'))
2144
2145 def test_parsedate_no_dayofweek(self):
2146 eq = self.assertEqual
2147 eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'),
2148 (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800))
2149
2150 def test_parsedate_compact_no_dayofweek(self):
2151 eq = self.assertEqual
2152 eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'),
2153 (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800))
2154
2155 def test_parsedate_acceptable_to_time_functions(self):
2156 eq = self.assertEqual
2157 timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800')
2158 t = int(time.mktime(timetup))
2159 eq(time.localtime(t)[:6], timetup[:6])
2160 eq(int(time.strftime('%Y', timetup)), 2003)
2161 timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800')
2162 t = int(time.mktime(timetup[:9]))
2163 eq(time.localtime(t)[:6], timetup[:6])
2164 eq(int(time.strftime('%Y', timetup[:9])), 2003)
2165
2166 def test_parseaddr_empty(self):
2167 self.assertEqual(utils.parseaddr('<>'), ('', ''))
2168 self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '')
2169
2170 def test_noquote_dump(self):
2171 self.assertEqual(
2172 utils.formataddr(('A Silly Person', 'person@dom.ain')),
2173 'A Silly Person <person@dom.ain>')
2174
2175 def test_escape_dump(self):
2176 self.assertEqual(
2177 utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')),
2178 r'"A \(Very\) Silly Person" <person@dom.ain>')
2179 a = r'A \(Special\) Person'
2180 b = 'person@dom.ain'
2181 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2182
2183 def test_escape_backslashes(self):
2184 self.assertEqual(
2185 utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')),
2186 r'"Arthur \\Backslash\\ Foobar" <person@dom.ain>')
2187 a = r'Arthur \Backslash\ Foobar'
2188 b = 'person@dom.ain'
2189 self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b))
2190
2191 def test_name_with_dot(self):
2192 x = 'John X. Doe <jxd@example.com>'
2193 y = '"John X. Doe" <jxd@example.com>'
2194 a, b = ('John X. Doe', 'jxd@example.com')
2195 self.assertEqual(utils.parseaddr(x), (a, b))
2196 self.assertEqual(utils.parseaddr(y), (a, b))
2197 # formataddr() quotes the name if there's a dot in it
2198 self.assertEqual(utils.formataddr((a, b)), y)
2199
2200 def test_multiline_from_comment(self):
2201 x = """\
2202Foo
2203\tBar <foo@example.com>"""
2204 self.assertEqual(utils.parseaddr(x), ('Foo Bar', 'foo@example.com'))
2205
2206 def test_quote_dump(self):
2207 self.assertEqual(
2208 utils.formataddr(('A Silly; Person', 'person@dom.ain')),
2209 r'"A Silly; Person" <person@dom.ain>')
2210
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002211 def test_charset_richcomparisons(self):
2212 eq = self.assertEqual
2213 ne = self.failIfEqual
2214 cset1 = Charset()
2215 cset2 = Charset()
2216 eq(cset1, 'us-ascii')
2217 eq(cset1, 'US-ASCII')
2218 eq(cset1, 'Us-AsCiI')
2219 eq('us-ascii', cset1)
2220 eq('US-ASCII', cset1)
2221 eq('Us-AsCiI', cset1)
2222 ne(cset1, 'usascii')
2223 ne(cset1, 'USASCII')
2224 ne(cset1, 'UsAsCiI')
2225 ne('usascii', cset1)
2226 ne('USASCII', cset1)
2227 ne('UsAsCiI', cset1)
2228 eq(cset1, cset2)
2229 eq(cset2, cset1)
2230
2231 def test_getaddresses(self):
2232 eq = self.assertEqual
2233 eq(utils.getaddresses(['aperson@dom.ain (Al Person)',
2234 'Bud Person <bperson@dom.ain>']),
2235 [('Al Person', 'aperson@dom.ain'),
2236 ('Bud Person', 'bperson@dom.ain')])
2237
2238 def test_getaddresses_nasty(self):
2239 eq = self.assertEqual
2240 eq(utils.getaddresses(['foo: ;']), [('', '')])
2241 eq(utils.getaddresses(
2242 ['[]*-- =~$']),
2243 [('', ''), ('', ''), ('', '*--')])
2244 eq(utils.getaddresses(
2245 ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
2246 [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
2247
2248 def test_getaddresses_embedded_comment(self):
2249 """Test proper handling of a nested comment"""
2250 eq = self.assertEqual
2251 addrs = utils.getaddresses(['User ((nested comment)) <foo@bar.com>'])
2252 eq(addrs[0][1], 'foo@bar.com')
2253
2254 def test_utils_quote_unquote(self):
2255 eq = self.assertEqual
2256 msg = Message()
2257 msg.add_header('content-disposition', 'attachment',
2258 filename='foo\\wacky"name')
2259 eq(msg.get_filename(), 'foo\\wacky"name')
2260
2261 def test_get_body_encoding_with_bogus_charset(self):
2262 charset = Charset('not a charset')
2263 self.assertEqual(charset.get_body_encoding(), 'base64')
2264
2265 def test_get_body_encoding_with_uppercase_charset(self):
2266 eq = self.assertEqual
2267 msg = Message()
2268 msg['Content-Type'] = 'text/plain; charset=UTF-8'
2269 eq(msg['content-type'], 'text/plain; charset=UTF-8')
2270 charsets = msg.get_charsets()
2271 eq(len(charsets), 1)
2272 eq(charsets[0], 'utf-8')
2273 charset = Charset(charsets[0])
2274 eq(charset.get_body_encoding(), 'base64')
2275 msg.set_payload('hello world', charset=charset)
2276 eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n')
2277 eq(msg.get_payload(decode=True), b'hello world')
2278 eq(msg['content-transfer-encoding'], 'base64')
2279 # Try another one
2280 msg = Message()
2281 msg['Content-Type'] = 'text/plain; charset="US-ASCII"'
2282 charsets = msg.get_charsets()
2283 eq(len(charsets), 1)
2284 eq(charsets[0], 'us-ascii')
2285 charset = Charset(charsets[0])
2286 eq(charset.get_body_encoding(), encoders.encode_7or8bit)
2287 msg.set_payload('hello world', charset=charset)
2288 eq(msg.get_payload(), 'hello world')
2289 eq(msg['content-transfer-encoding'], '7bit')
2290
2291 def test_charsets_case_insensitive(self):
2292 lc = Charset('us-ascii')
2293 uc = Charset('US-ASCII')
2294 self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding())
2295
2296 def test_partial_falls_inside_message_delivery_status(self):
2297 eq = self.ndiffAssertEqual
2298 # The Parser interface provides chunks of data to FeedParser in 8192
2299 # byte gulps. SF bug #1076485 found one of those chunks inside
2300 # message/delivery-status header block, which triggered an
2301 # unreadline() of NeedMoreData.
2302 msg = self._msgobj('msg_43.txt')
2303 sfp = StringIO()
2304 iterators._structure(msg, sfp)
2305 eq(sfp.getvalue(), """\
2306multipart/report
2307 text/plain
2308 message/delivery-status
2309 text/plain
2310 text/plain
2311 text/plain
2312 text/plain
2313 text/plain
2314 text/plain
2315 text/plain
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/rfc822-headers
2336""")
2337
2338
2339
2340# Test the iterator/generators
2341class TestIterators(TestEmailBase):
2342 def test_body_line_iterator(self):
2343 eq = self.assertEqual
2344 neq = self.ndiffAssertEqual
2345 # First a simple non-multipart message
2346 msg = self._msgobj('msg_01.txt')
2347 it = iterators.body_line_iterator(msg)
2348 lines = list(it)
2349 eq(len(lines), 6)
2350 neq(EMPTYSTRING.join(lines), msg.get_payload())
2351 # Now a more complicated multipart
2352 msg = self._msgobj('msg_02.txt')
2353 it = iterators.body_line_iterator(msg)
2354 lines = list(it)
2355 eq(len(lines), 43)
2356 with openfile('msg_19.txt') as fp:
2357 neq(EMPTYSTRING.join(lines), fp.read())
2358
2359 def test_typed_subpart_iterator(self):
2360 eq = self.assertEqual
2361 msg = self._msgobj('msg_04.txt')
2362 it = iterators.typed_subpart_iterator(msg, 'text')
2363 lines = []
2364 subparts = 0
2365 for subpart in it:
2366 subparts += 1
2367 lines.append(subpart.get_payload())
2368 eq(subparts, 2)
2369 eq(EMPTYSTRING.join(lines), """\
2370a simple kind of mirror
2371to reflect upon our own
2372a simple kind of mirror
2373to reflect upon our own
2374""")
2375
2376 def test_typed_subpart_iterator_default_type(self):
2377 eq = self.assertEqual
2378 msg = self._msgobj('msg_03.txt')
2379 it = iterators.typed_subpart_iterator(msg, 'text', 'plain')
2380 lines = []
2381 subparts = 0
2382 for subpart in it:
2383 subparts += 1
2384 lines.append(subpart.get_payload())
2385 eq(subparts, 1)
2386 eq(EMPTYSTRING.join(lines), """\
2387
2388Hi,
2389
2390Do you like this message?
2391
2392-Me
2393""")
2394
2395
2396
2397class TestParsers(TestEmailBase):
2398 def test_header_parser(self):
2399 eq = self.assertEqual
2400 # Parse only the headers of a complex multipart MIME document
2401 with openfile('msg_02.txt') as fp:
2402 msg = HeaderParser().parse(fp)
2403 eq(msg['from'], 'ppp-request@zzz.org')
2404 eq(msg['to'], 'ppp@zzz.org')
2405 eq(msg.get_content_type(), 'multipart/mixed')
2406 self.failIf(msg.is_multipart())
2407 self.failUnless(isinstance(msg.get_payload(), str))
2408
2409 def test_whitespace_continuation(self):
2410 eq = self.assertEqual
2411 # This message contains a line after the Subject: header that has only
2412 # whitespace, but it is not empty!
2413 msg = email.message_from_string("""\
2414From: aperson@dom.ain
2415To: bperson@dom.ain
2416Subject: the next line has a space on it
2417\x20
2418Date: Mon, 8 Apr 2002 15:09:19 -0400
2419Message-ID: spam
2420
2421Here's the message body
2422""")
2423 eq(msg['subject'], 'the next line has a space on it\n ')
2424 eq(msg['message-id'], 'spam')
2425 eq(msg.get_payload(), "Here's the message body\n")
2426
2427 def test_whitespace_continuation_last_header(self):
2428 eq = self.assertEqual
2429 # Like the previous test, but the subject line is the last
2430 # header.
2431 msg = email.message_from_string("""\
2432From: aperson@dom.ain
2433To: bperson@dom.ain
2434Date: Mon, 8 Apr 2002 15:09:19 -0400
2435Message-ID: spam
2436Subject: the next line has a space on it
2437\x20
2438
2439Here's the message body
2440""")
2441 eq(msg['subject'], 'the next line has a space on it\n ')
2442 eq(msg['message-id'], 'spam')
2443 eq(msg.get_payload(), "Here's the message body\n")
2444
2445 def test_crlf_separation(self):
2446 eq = self.assertEqual
2447 # XXX When Guido fixes TextIOWrapper.read() to act just like
2448 # .readlines(), open this in 'rb' mode with newlines='\n'.
2449 with openfile('msg_26.txt', mode='rb') as fp:
2450 msg = Parser().parse(fp)
2451 eq(len(msg.get_payload()), 2)
2452 part1 = msg.get_payload(0)
2453 eq(part1.get_content_type(), 'text/plain')
2454 eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n')
2455 part2 = msg.get_payload(1)
2456 eq(part2.get_content_type(), 'application/riscos')
2457
2458 def test_multipart_digest_with_extra_mime_headers(self):
2459 eq = self.assertEqual
2460 neq = self.ndiffAssertEqual
2461 with openfile('msg_28.txt') as fp:
2462 msg = email.message_from_file(fp)
2463 # Structure is:
2464 # multipart/digest
2465 # message/rfc822
2466 # text/plain
2467 # message/rfc822
2468 # text/plain
2469 eq(msg.is_multipart(), 1)
2470 eq(len(msg.get_payload()), 2)
2471 part1 = msg.get_payload(0)
2472 eq(part1.get_content_type(), 'message/rfc822')
2473 eq(part1.is_multipart(), 1)
2474 eq(len(part1.get_payload()), 1)
2475 part1a = part1.get_payload(0)
2476 eq(part1a.is_multipart(), 0)
2477 eq(part1a.get_content_type(), 'text/plain')
2478 neq(part1a.get_payload(), 'message 1\n')
2479 # next message/rfc822
2480 part2 = msg.get_payload(1)
2481 eq(part2.get_content_type(), 'message/rfc822')
2482 eq(part2.is_multipart(), 1)
2483 eq(len(part2.get_payload()), 1)
2484 part2a = part2.get_payload(0)
2485 eq(part2a.is_multipart(), 0)
2486 eq(part2a.get_content_type(), 'text/plain')
2487 neq(part2a.get_payload(), 'message 2\n')
2488
2489 def test_three_lines(self):
2490 # A bug report by Andrew McNamara
2491 lines = ['From: Andrew Person <aperson@dom.ain',
2492 'Subject: Test',
2493 'Date: Tue, 20 Aug 2002 16:43:45 +1000']
2494 msg = email.message_from_string(NL.join(lines))
2495 self.assertEqual(msg['date'], 'Tue, 20 Aug 2002 16:43:45 +1000')
2496
2497 def test_strip_line_feed_and_carriage_return_in_headers(self):
2498 eq = self.assertEqual
2499 # For [ 1002475 ] email message parser doesn't handle \r\n correctly
2500 value1 = 'text'
2501 value2 = 'more text'
2502 m = 'Header: %s\r\nNext-Header: %s\r\n\r\nBody\r\n\r\n' % (
2503 value1, value2)
2504 msg = email.message_from_string(m)
2505 eq(msg.get('Header'), value1)
2506 eq(msg.get('Next-Header'), value2)
2507
2508 def test_rfc2822_header_syntax(self):
2509 eq = self.assertEqual
2510 m = '>From: foo\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2511 msg = email.message_from_string(m)
2512 eq(len(msg), 3)
2513 eq(sorted(field for field in msg), ['!"#QUX;~', '>From', 'From'])
2514 eq(msg.get_payload(), 'body')
2515
2516 def test_rfc2822_space_not_allowed_in_header(self):
2517 eq = self.assertEqual
2518 m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody'
2519 msg = email.message_from_string(m)
2520 eq(len(msg.keys()), 0)
2521
2522 def test_rfc2822_one_character_header(self):
2523 eq = self.assertEqual
2524 m = 'A: first header\nB: second header\nCC: third header\n\nbody'
2525 msg = email.message_from_string(m)
2526 headers = msg.keys()
2527 headers.sort()
2528 eq(headers, ['A', 'B', 'CC'])
2529 eq(msg.get_payload(), 'body')
2530
2531
2532
2533class TestBase64(unittest.TestCase):
2534 def test_len(self):
2535 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002536 eq(base64mime.header_length('hello'),
2537 len(base64mime.body_encode('hello', eol='')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002538 for size in range(15):
2539 if size == 0 : bsize = 0
2540 elif size <= 3 : bsize = 4
2541 elif size <= 6 : bsize = 8
2542 elif size <= 9 : bsize = 12
2543 elif size <= 12: bsize = 16
2544 else : bsize = 20
Guido van Rossum9604e662007-08-30 03:46:43 +00002545 eq(base64mime.header_length('x' * size), bsize)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002546
2547 def test_decode(self):
2548 eq = self.assertEqual
Barry Warsaw2cc1f6d2007-08-30 14:28:55 +00002549 eq(base64mime.decode(''), b'')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002550 eq(base64mime.decode('aGVsbG8='), b'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002551
2552 def test_encode(self):
2553 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002554 eq(base64mime.body_encode(''), '')
2555 eq(base64mime.body_encode('hello'), 'aGVsbG8=\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002556 # Test the binary flag
Guido van Rossum9604e662007-08-30 03:46:43 +00002557 eq(base64mime.body_encode('hello\n'), 'aGVsbG8K\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002558 # Test the maxlinelen arg
Guido van Rossum9604e662007-08-30 03:46:43 +00002559 eq(base64mime.body_encode('xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002560eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2561eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2562eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2563eHh4eCB4eHh4IA==
2564""")
2565 # Test the eol argument
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002566 eq(base64mime.body_encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'),
2567 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002568eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2569eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2570eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r
2571eHh4eCB4eHh4IA==\r
2572""")
2573
2574 def test_header_encode(self):
2575 eq = self.assertEqual
2576 he = base64mime.header_encode
2577 eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=')
Guido van Rossum9604e662007-08-30 03:46:43 +00002578 eq(he('hello\r\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=')
2579 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002580 # Test the charset option
2581 eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=')
2582 eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002583
2584
2585
2586class TestQuopri(unittest.TestCase):
2587 def setUp(self):
2588 # Set of characters (as byte integers) that don't need to be encoded
2589 # in headers.
2590 self.hlit = list(chain(
2591 range(ord('a'), ord('z') + 1),
2592 range(ord('A'), ord('Z') + 1),
2593 range(ord('0'), ord('9') + 1),
Guido van Rossum9604e662007-08-30 03:46:43 +00002594 (c for c in b'!*+-/')))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002595 # Set of characters (as byte integers) that do need to be encoded in
2596 # headers.
2597 self.hnon = [c for c in range(256) if c not in self.hlit]
2598 assert len(self.hlit) + len(self.hnon) == 256
2599 # Set of characters (as byte integers) that don't need to be encoded
2600 # in bodies.
2601 self.blit = list(range(ord(' '), ord('~') + 1))
2602 self.blit.append(ord('\t'))
2603 self.blit.remove(ord('='))
2604 # Set of characters (as byte integers) that do need to be encoded in
2605 # bodies.
2606 self.bnon = [c for c in range(256) if c not in self.blit]
2607 assert len(self.blit) + len(self.bnon) == 256
2608
Guido van Rossum9604e662007-08-30 03:46:43 +00002609 def test_quopri_header_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002610 for c in self.hlit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002611 self.failIf(quoprimime.header_check(c),
2612 'Should not be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002613 for c in self.hnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002614 self.failUnless(quoprimime.header_check(c),
2615 'Should be header quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002616
Guido van Rossum9604e662007-08-30 03:46:43 +00002617 def test_quopri_body_check(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002618 for c in self.blit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002619 self.failIf(quoprimime.body_check(c),
2620 'Should not be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002621 for c in self.bnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002622 self.failUnless(quoprimime.body_check(c),
2623 'Should be body quopri encoded: %s' % chr(c))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002624
2625 def test_header_quopri_len(self):
2626 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002627 eq(quoprimime.header_length(b'hello'), 5)
2628 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002629 eq(len(quoprimime.header_encode(b'hello', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00002630 quoprimime.header_length(b'hello') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002631 # =?xxx?q?...?= means 10 extra characters
2632 10)
Guido van Rossum9604e662007-08-30 03:46:43 +00002633 eq(quoprimime.header_length(b'h@e@l@l@o@'), 20)
2634 # RFC 2047 chrome is not included in header_length().
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002635 eq(len(quoprimime.header_encode(b'h@e@l@l@o@', charset='xxx')),
Guido van Rossum9604e662007-08-30 03:46:43 +00002636 quoprimime.header_length(b'h@e@l@l@o@') +
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002637 # =?xxx?q?...?= means 10 extra characters
2638 10)
2639 for c in self.hlit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002640 eq(quoprimime.header_length(bytes([c])), 1,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002641 'expected length 1 for %r' % chr(c))
2642 for c in self.hnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002643 # Space is special; it's encoded to _
2644 if c == ord(' '):
2645 continue
2646 eq(quoprimime.header_length(bytes([c])), 3,
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002647 'expected length 3 for %r' % chr(c))
Guido van Rossum9604e662007-08-30 03:46:43 +00002648 eq(quoprimime.header_length(b' '), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002649
2650 def test_body_quopri_len(self):
2651 eq = self.assertEqual
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002652 for c in self.blit:
Guido van Rossum9604e662007-08-30 03:46:43 +00002653 eq(quoprimime.body_length(bytes([c])), 1)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002654 for c in self.bnon:
Guido van Rossum9604e662007-08-30 03:46:43 +00002655 eq(quoprimime.body_length(bytes([c])), 3)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002656
2657 def test_quote_unquote_idempotent(self):
2658 for x in range(256):
2659 c = chr(x)
2660 self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c)
2661
2662 def test_header_encode(self):
2663 eq = self.assertEqual
2664 he = quoprimime.header_encode
2665 eq(he(b'hello'), '=?iso-8859-1?q?hello?=')
2666 eq(he(b'hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=')
2667 eq(he(b'hello\nworld'), '=?iso-8859-1?q?hello=0Aworld?=')
2668 # Test a non-ASCII character
2669 eq(he(b'hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=')
2670
2671 def test_decode(self):
2672 eq = self.assertEqual
2673 eq(quoprimime.decode(''), '')
2674 eq(quoprimime.decode('hello'), 'hello')
2675 eq(quoprimime.decode('hello', 'X'), 'hello')
2676 eq(quoprimime.decode('hello\nworld', 'X'), 'helloXworld')
2677
2678 def test_encode(self):
2679 eq = self.assertEqual
Guido van Rossum9604e662007-08-30 03:46:43 +00002680 eq(quoprimime.body_encode(''), '')
2681 eq(quoprimime.body_encode('hello'), 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002682 # Test the binary flag
Guido van Rossum9604e662007-08-30 03:46:43 +00002683 eq(quoprimime.body_encode('hello\r\nworld'), 'hello\nworld')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002684 # Test the maxlinelen arg
Guido van Rossum9604e662007-08-30 03:46:43 +00002685 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002686xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
2687 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
2688x xxxx xxxx xxxx xxxx=20""")
2689 # Test the eol argument
Guido van Rossum9604e662007-08-30 03:46:43 +00002690 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'),
2691 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002692xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
2693 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
2694x xxxx xxxx xxxx xxxx=20""")
Guido van Rossum9604e662007-08-30 03:46:43 +00002695 eq(quoprimime.body_encode("""\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002696one line
2697
2698two line"""), """\
2699one line
2700
2701two line""")
2702
2703
2704
2705# Test the Charset class
2706class TestCharset(unittest.TestCase):
2707 def tearDown(self):
2708 from email import charset as CharsetModule
2709 try:
2710 del CharsetModule.CHARSETS['fake']
2711 except KeyError:
2712 pass
2713
Guido van Rossum9604e662007-08-30 03:46:43 +00002714 def test_codec_encodeable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002715 eq = self.assertEqual
2716 # Make sure us-ascii = no Unicode conversion
2717 c = Charset('us-ascii')
Guido van Rossum9604e662007-08-30 03:46:43 +00002718 eq(c.header_encode('Hello World!'), 'Hello World!')
2719 # Test 8-bit idempotency with us-ascii
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002720 s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
Guido van Rossum9604e662007-08-30 03:46:43 +00002721 self.assertRaises(UnicodeError, c.header_encode, s)
2722 c = Charset('utf-8')
2723 eq(c.header_encode(s), '=?utf-8?b?wqTCosKkwqTCpMKmwqTCqMKkwqo=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002724
2725 def test_body_encode(self):
2726 eq = self.assertEqual
2727 # Try a charset with QP body encoding
2728 c = Charset('iso-8859-1')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002729 eq('hello w=F6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002730 # Try a charset with Base64 body encoding
2731 c = Charset('utf-8')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002732 eq('aGVsbG8gd29ybGQ=\n', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002733 # Try a charset with None body encoding
2734 c = Charset('us-ascii')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002735 eq('hello world', c.body_encode('hello world'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002736 # Try the convert argument, where input codec != output codec
2737 c = Charset('euc-jp')
2738 # With apologies to Tokio Kikuchi ;)
2739 try:
2740 eq('\x1b$B5FCO;~IW\x1b(B',
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002741 c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002742 eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002743 c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002744 except LookupError:
2745 # We probably don't have the Japanese codecs installed
2746 pass
2747 # Testing SF bug #625509, which we have to fake, since there are no
2748 # built-in encodings where the header encoding is QP but the body
2749 # encoding is not.
2750 from email import charset as CharsetModule
2751 CharsetModule.add_charset('fake', CharsetModule.QP, None)
2752 c = Charset('fake')
Barry Warsaw7aa02e62007-08-31 03:26:19 +00002753 eq('hello w\xf6rld', c.body_encode('hello w\xf6rld'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002754
2755 def test_unicode_charset_name(self):
2756 charset = Charset('us-ascii')
2757 self.assertEqual(str(charset), 'us-ascii')
2758 self.assertRaises(errors.CharsetError, Charset, 'asc\xffii')
2759
2760
2761
2762# Test multilingual MIME headers.
2763class TestHeader(TestEmailBase):
2764 def test_simple(self):
2765 eq = self.ndiffAssertEqual
2766 h = Header('Hello World!')
2767 eq(h.encode(), 'Hello World!')
2768 h.append(' Goodbye World!')
2769 eq(h.encode(), 'Hello World! Goodbye World!')
2770
2771 def test_simple_surprise(self):
2772 eq = self.ndiffAssertEqual
2773 h = Header('Hello World!')
2774 eq(h.encode(), 'Hello World!')
2775 h.append('Goodbye World!')
2776 eq(h.encode(), 'Hello World! Goodbye World!')
2777
2778 def test_header_needs_no_decoding(self):
2779 h = 'no decoding needed'
2780 self.assertEqual(decode_header(h), [(h, None)])
2781
2782 def test_long(self):
2783 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.",
2784 maxlinelen=76)
2785 for l in h.encode(splitchars=' ').split('\n '):
2786 self.failUnless(len(l) <= 76)
2787
2788 def test_multilingual(self):
2789 eq = self.ndiffAssertEqual
2790 g = Charset("iso-8859-1")
2791 cz = Charset("iso-8859-2")
2792 utf8 = Charset("utf-8")
2793 g_head = (b'Die Mieter treten hier ein werden mit einem '
2794 b'Foerderband komfortabel den Korridor entlang, '
2795 b'an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, '
2796 b'gegen die rotierenden Klingen bef\xf6rdert. ')
2797 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
2798 b'd\xf9vtipu.. ')
2799 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
2800 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
2801 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
2802 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
2803 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
2804 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
2805 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
2806 '\u3044\u307e\u3059\u3002')
2807 h = Header(g_head, g)
2808 h.append(cz_head, cz)
2809 h.append(utf8_head, utf8)
Guido van Rossum9604e662007-08-30 03:46:43 +00002810 enc = h.encode(maxlinelen=76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002811 eq(enc, """\
Guido van Rossum9604e662007-08-30 03:46:43 +00002812=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_kom?=
2813 =?iso-8859-1?q?fortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wand?=
2814 =?iso-8859-1?q?gem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6r?=
2815 =?iso-8859-1?q?dert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002816 =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
2817 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
2818 =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
2819 =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
Guido van Rossum9604e662007-08-30 03:46:43 +00002820 =?utf-8?b?IE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5k?=
2821 =?utf-8?b?IGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIA=?=
2822 =?utf-8?b?44Gj44Gm44GE44G+44GZ44CC?=""")
2823 decoded = decode_header(enc)
2824 eq(len(decoded), 3)
2825 eq(decoded[0], (g_head, 'iso-8859-1'))
2826 eq(decoded[1], (cz_head, 'iso-8859-2'))
2827 eq(decoded[2], (utf8_head.encode('utf-8'), 'utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002828 ustr = str(h)
Guido van Rossum9604e662007-08-30 03:46:43 +00002829 eq(ustr,
2830 (b'Die Mieter treten hier ein werden mit einem Foerderband '
2831 b'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
2832 b'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
2833 b'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
2834 b'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
2835 b'\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
2836 b'\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
2837 b'\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
2838 b'\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
2839 b'\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
2840 b'\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
2841 b'\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
2842 b'\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
2843 b'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
2844 b'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
2845 b'\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82'
2846 ).decode('utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002847 # Test make_header()
2848 newh = make_header(decode_header(enc))
Guido van Rossum9604e662007-08-30 03:46:43 +00002849 eq(newh, h)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002850
2851 def test_empty_header_encode(self):
2852 h = Header()
2853 self.assertEqual(h.encode(), '')
Barry Warsaw8b3d6592007-08-30 02:10:49 +00002854
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002855 def test_header_ctor_default_args(self):
2856 eq = self.ndiffAssertEqual
2857 h = Header()
2858 eq(h, '')
2859 h.append('foo', Charset('iso-8859-1'))
Guido van Rossum9604e662007-08-30 03:46:43 +00002860 eq(h, 'foo')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002861
2862 def test_explicit_maxlinelen(self):
2863 eq = self.ndiffAssertEqual
2864 hstr = ('A very long line that must get split to something other '
2865 'than at the 76th character boundary to test the non-default '
2866 'behavior')
2867 h = Header(hstr)
2868 eq(h.encode(), '''\
2869A very long line that must get split to something other than at the 76th
2870 character boundary to test the non-default behavior''')
2871 eq(str(h), hstr)
2872 h = Header(hstr, header_name='Subject')
2873 eq(h.encode(), '''\
2874A very long line that must get split to something other than at the
2875 76th character boundary to test the non-default behavior''')
2876 eq(str(h), hstr)
2877 h = Header(hstr, maxlinelen=1024, header_name='Subject')
2878 eq(h.encode(), hstr)
2879 eq(str(h), hstr)
2880
Guido van Rossum9604e662007-08-30 03:46:43 +00002881 def test_quopri_splittable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002882 eq = self.ndiffAssertEqual
2883 h = Header(charset='iso-8859-1', maxlinelen=20)
Guido van Rossum9604e662007-08-30 03:46:43 +00002884 x = 'xxxx ' * 20
2885 h.append(x)
2886 s = h.encode()
2887 eq(s, """\
2888=?iso-8859-1?q?xxx?=
2889 =?iso-8859-1?q?x_?=
2890 =?iso-8859-1?q?xx?=
2891 =?iso-8859-1?q?xx?=
2892 =?iso-8859-1?q?_x?=
2893 =?iso-8859-1?q?xx?=
2894 =?iso-8859-1?q?x_?=
2895 =?iso-8859-1?q?xx?=
2896 =?iso-8859-1?q?xx?=
2897 =?iso-8859-1?q?_x?=
2898 =?iso-8859-1?q?xx?=
2899 =?iso-8859-1?q?x_?=
2900 =?iso-8859-1?q?xx?=
2901 =?iso-8859-1?q?xx?=
2902 =?iso-8859-1?q?_x?=
2903 =?iso-8859-1?q?xx?=
2904 =?iso-8859-1?q?x_?=
2905 =?iso-8859-1?q?xx?=
2906 =?iso-8859-1?q?xx?=
2907 =?iso-8859-1?q?_x?=
2908 =?iso-8859-1?q?xx?=
2909 =?iso-8859-1?q?x_?=
2910 =?iso-8859-1?q?xx?=
2911 =?iso-8859-1?q?xx?=
2912 =?iso-8859-1?q?_x?=
2913 =?iso-8859-1?q?xx?=
2914 =?iso-8859-1?q?x_?=
2915 =?iso-8859-1?q?xx?=
2916 =?iso-8859-1?q?xx?=
2917 =?iso-8859-1?q?_x?=
2918 =?iso-8859-1?q?xx?=
2919 =?iso-8859-1?q?x_?=
2920 =?iso-8859-1?q?xx?=
2921 =?iso-8859-1?q?xx?=
2922 =?iso-8859-1?q?_x?=
2923 =?iso-8859-1?q?xx?=
2924 =?iso-8859-1?q?x_?=
2925 =?iso-8859-1?q?xx?=
2926 =?iso-8859-1?q?xx?=
2927 =?iso-8859-1?q?_x?=
2928 =?iso-8859-1?q?xx?=
2929 =?iso-8859-1?q?x_?=
2930 =?iso-8859-1?q?xx?=
2931 =?iso-8859-1?q?xx?=
2932 =?iso-8859-1?q?_x?=
2933 =?iso-8859-1?q?xx?=
2934 =?iso-8859-1?q?x_?=
2935 =?iso-8859-1?q?xx?=
2936 =?iso-8859-1?q?xx?=
2937 =?iso-8859-1?q?_?=""")
2938 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002939 h = Header(charset='iso-8859-1', maxlinelen=40)
2940 h.append('xxxx ' * 20)
Guido van Rossum9604e662007-08-30 03:46:43 +00002941 s = h.encode()
2942 eq(s, """\
2943=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xxx?=
2944 =?iso-8859-1?q?x_xxxx_xxxx_xxxx_xxxx_?=
2945 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
2946 =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
2947 =?iso-8859-1?q?_xxxx_xxxx_?=""")
2948 eq(x, str(make_header(decode_header(s))))
2949
2950 def test_base64_splittable(self):
2951 eq = self.ndiffAssertEqual
2952 h = Header(charset='koi8-r', maxlinelen=20)
2953 x = 'xxxx ' * 20
2954 h.append(x)
2955 s = h.encode()
2956 eq(s, """\
2957=?koi8-r?b?eHh4?=
2958 =?koi8-r?b?eCB4?=
2959 =?koi8-r?b?eHh4?=
2960 =?koi8-r?b?IHh4?=
2961 =?koi8-r?b?eHgg?=
2962 =?koi8-r?b?eHh4?=
2963 =?koi8-r?b?eCB4?=
2964 =?koi8-r?b?eHh4?=
2965 =?koi8-r?b?IHh4?=
2966 =?koi8-r?b?eHgg?=
2967 =?koi8-r?b?eHh4?=
2968 =?koi8-r?b?eCB4?=
2969 =?koi8-r?b?eHh4?=
2970 =?koi8-r?b?IHh4?=
2971 =?koi8-r?b?eHgg?=
2972 =?koi8-r?b?eHh4?=
2973 =?koi8-r?b?eCB4?=
2974 =?koi8-r?b?eHh4?=
2975 =?koi8-r?b?IHh4?=
2976 =?koi8-r?b?eHgg?=
2977 =?koi8-r?b?eHh4?=
2978 =?koi8-r?b?eCB4?=
2979 =?koi8-r?b?eHh4?=
2980 =?koi8-r?b?IHh4?=
2981 =?koi8-r?b?eHgg?=
2982 =?koi8-r?b?eHh4?=
2983 =?koi8-r?b?eCB4?=
2984 =?koi8-r?b?eHh4?=
2985 =?koi8-r?b?IHh4?=
2986 =?koi8-r?b?eHgg?=
2987 =?koi8-r?b?eHh4?=
2988 =?koi8-r?b?eCB4?=
2989 =?koi8-r?b?eHh4?=
2990 =?koi8-r?b?IA==?=""")
2991 eq(x, str(make_header(decode_header(s))))
2992 h = Header(charset='koi8-r', maxlinelen=40)
2993 h.append(x)
2994 s = h.encode()
2995 eq(s, """\
2996=?koi8-r?b?eHh4eCB4eHh4IHh4eHggeHh4?=
2997 =?koi8-r?b?eCB4eHh4IHh4eHggeHh4eCB4?=
2998 =?koi8-r?b?eHh4IHh4eHggeHh4eCB4eHh4?=
2999 =?koi8-r?b?IHh4eHggeHh4eCB4eHh4IHh4?=
3000 =?koi8-r?b?eHggeHh4eCB4eHh4IHh4eHgg?=
3001 =?koi8-r?b?eHh4eCB4eHh4IA==?=""")
3002 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003003
3004 def test_us_ascii_header(self):
3005 eq = self.assertEqual
3006 s = 'hello'
3007 x = decode_header(s)
3008 eq(x, [('hello', None)])
3009 h = make_header(x)
3010 eq(s, h.encode())
3011
3012 def test_string_charset(self):
3013 eq = self.assertEqual
3014 h = Header()
3015 h.append('hello', 'iso-8859-1')
Guido van Rossum9604e662007-08-30 03:46:43 +00003016 eq(h, 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003017
3018## def test_unicode_error(self):
3019## raises = self.assertRaises
3020## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')
3021## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')
3022## h = Header()
3023## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')
3024## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')
3025## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')
3026
3027 def test_utf8_shortest(self):
3028 eq = self.assertEqual
3029 h = Header('p\xf6stal', 'utf-8')
3030 eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
3031 h = Header('\u83ca\u5730\u6642\u592b', 'utf-8')
3032 eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
3033
3034 def test_bad_8bit_header(self):
3035 raises = self.assertRaises
3036 eq = self.assertEqual
3037 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
3038 raises(UnicodeError, Header, x)
3039 h = Header()
3040 raises(UnicodeError, h.append, x)
3041 e = x.decode('utf-8', 'replace')
3042 eq(str(Header(x, errors='replace')), e)
3043 h.append(x, errors='replace')
3044 eq(str(h), e)
3045
3046 def test_encoded_adjacent_nonencoded(self):
3047 eq = self.assertEqual
3048 h = Header()
3049 h.append('hello', 'iso-8859-1')
3050 h.append('world')
3051 s = h.encode()
3052 eq(s, '=?iso-8859-1?q?hello?= world')
3053 h = make_header(decode_header(s))
3054 eq(h.encode(), s)
3055
3056 def test_whitespace_eater(self):
3057 eq = self.assertEqual
3058 s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
3059 parts = decode_header(s)
3060 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)])
3061 hdr = make_header(parts)
3062 eq(hdr.encode(),
3063 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
3064
3065 def test_broken_base64_header(self):
3066 raises = self.assertRaises
3067 s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3IQ?='
3068 raises(errors.HeaderParseError, decode_header, s)
3069
3070
3071
3072# Test RFC 2231 header parameters (en/de)coding
3073class TestRFC2231(TestEmailBase):
3074 def test_get_param(self):
3075 eq = self.assertEqual
3076 msg = self._msgobj('msg_29.txt')
3077 eq(msg.get_param('title'),
3078 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3079 eq(msg.get_param('title', unquote=False),
3080 ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
3081
3082 def test_set_param(self):
3083 eq = self.ndiffAssertEqual
3084 msg = Message()
3085 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3086 charset='us-ascii')
3087 eq(msg.get_param('title'),
3088 ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
3089 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3090 charset='us-ascii', language='en')
3091 eq(msg.get_param('title'),
3092 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3093 msg = self._msgobj('msg_01.txt')
3094 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3095 charset='us-ascii', language='en')
3096 eq(msg.as_string(maxheaderlen=78), """\
3097Return-Path: <bbb@zzz.org>
3098Delivered-To: bbb@zzz.org
3099Received: by mail.zzz.org (Postfix, from userid 889)
3100\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3101MIME-Version: 1.0
3102Content-Transfer-Encoding: 7bit
3103Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3104From: bbb@ddd.com (John X. Doe)
3105To: bbb@zzz.org
3106Subject: This is a test message
3107Date: Fri, 4 May 2001 14:05:44 -0400
3108Content-Type: text/plain; charset=us-ascii;
3109 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3110
3111
3112Hi,
3113
3114Do you like this message?
3115
3116-Me
3117""")
3118
3119 def test_del_param(self):
3120 eq = self.ndiffAssertEqual
3121 msg = self._msgobj('msg_01.txt')
3122 msg.set_param('foo', 'bar', charset='us-ascii', language='en')
3123 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3124 charset='us-ascii', language='en')
3125 msg.del_param('foo', header='Content-Type')
3126 eq(msg.as_string(maxheaderlen=78), """\
3127Return-Path: <bbb@zzz.org>
3128Delivered-To: bbb@zzz.org
3129Received: by mail.zzz.org (Postfix, from userid 889)
3130\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3131MIME-Version: 1.0
3132Content-Transfer-Encoding: 7bit
3133Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3134From: bbb@ddd.com (John X. Doe)
3135To: bbb@zzz.org
3136Subject: This is a test message
3137Date: Fri, 4 May 2001 14:05:44 -0400
3138Content-Type: text/plain; charset="us-ascii";
3139 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3140
3141
3142Hi,
3143
3144Do you like this message?
3145
3146-Me
3147""")
3148
3149 def test_rfc2231_get_content_charset(self):
3150 eq = self.assertEqual
3151 msg = self._msgobj('msg_32.txt')
3152 eq(msg.get_content_charset(), 'us-ascii')
3153
3154 def test_rfc2231_no_language_or_charset(self):
3155 m = '''\
3156Content-Transfer-Encoding: 8bit
3157Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
3158Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
3159
3160'''
3161 msg = email.message_from_string(m)
3162 param = msg.get_param('NAME')
3163 self.failIf(isinstance(param, tuple))
3164 self.assertEqual(
3165 param,
3166 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
3167
3168 def test_rfc2231_no_language_or_charset_in_filename(self):
3169 m = '''\
3170Content-Disposition: inline;
3171\tfilename*0*="''This%20is%20even%20more%20";
3172\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3173\tfilename*2="is it not.pdf"
3174
3175'''
3176 msg = email.message_from_string(m)
3177 self.assertEqual(msg.get_filename(),
3178 'This is even more ***fun*** is it not.pdf')
3179
3180 def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
3181 m = '''\
3182Content-Disposition: inline;
3183\tfilename*0*="''This%20is%20even%20more%20";
3184\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3185\tfilename*2="is it not.pdf"
3186
3187'''
3188 msg = email.message_from_string(m)
3189 self.assertEqual(msg.get_filename(),
3190 'This is even more ***fun*** is it not.pdf')
3191
3192 def test_rfc2231_partly_encoded(self):
3193 m = '''\
3194Content-Disposition: inline;
3195\tfilename*0="''This%20is%20even%20more%20";
3196\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3197\tfilename*2="is it not.pdf"
3198
3199'''
3200 msg = email.message_from_string(m)
3201 self.assertEqual(
3202 msg.get_filename(),
3203 'This%20is%20even%20more%20***fun*** is it not.pdf')
3204
3205 def test_rfc2231_partly_nonencoded(self):
3206 m = '''\
3207Content-Disposition: inline;
3208\tfilename*0="This%20is%20even%20more%20";
3209\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
3210\tfilename*2="is it not.pdf"
3211
3212'''
3213 msg = email.message_from_string(m)
3214 self.assertEqual(
3215 msg.get_filename(),
3216 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
3217
3218 def test_rfc2231_no_language_or_charset_in_boundary(self):
3219 m = '''\
3220Content-Type: multipart/alternative;
3221\tboundary*0*="''This%20is%20even%20more%20";
3222\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
3223\tboundary*2="is it not.pdf"
3224
3225'''
3226 msg = email.message_from_string(m)
3227 self.assertEqual(msg.get_boundary(),
3228 'This is even more ***fun*** is it not.pdf')
3229
3230 def test_rfc2231_no_language_or_charset_in_charset(self):
3231 # This is a nonsensical charset value, but tests the code anyway
3232 m = '''\
3233Content-Type: text/plain;
3234\tcharset*0*="This%20is%20even%20more%20";
3235\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
3236\tcharset*2="is it not.pdf"
3237
3238'''
3239 msg = email.message_from_string(m)
3240 self.assertEqual(msg.get_content_charset(),
3241 'this is even more ***fun*** is it not.pdf')
3242
3243 def test_rfc2231_bad_encoding_in_filename(self):
3244 m = '''\
3245Content-Disposition: inline;
3246\tfilename*0*="bogus'xx'This%20is%20even%20more%20";
3247\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3248\tfilename*2="is it not.pdf"
3249
3250'''
3251 msg = email.message_from_string(m)
3252 self.assertEqual(msg.get_filename(),
3253 'This is even more ***fun*** is it not.pdf')
3254
3255 def test_rfc2231_bad_encoding_in_charset(self):
3256 m = """\
3257Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
3258
3259"""
3260 msg = email.message_from_string(m)
3261 # This should return None because non-ascii characters in the charset
3262 # are not allowed.
3263 self.assertEqual(msg.get_content_charset(), None)
3264
3265 def test_rfc2231_bad_character_in_charset(self):
3266 m = """\
3267Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D
3268
3269"""
3270 msg = email.message_from_string(m)
3271 # This should return None because non-ascii characters in the charset
3272 # are not allowed.
3273 self.assertEqual(msg.get_content_charset(), None)
3274
3275 def test_rfc2231_bad_character_in_filename(self):
3276 m = '''\
3277Content-Disposition: inline;
3278\tfilename*0*="ascii'xx'This%20is%20even%20more%20";
3279\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3280\tfilename*2*="is it not.pdf%E2"
3281
3282'''
3283 msg = email.message_from_string(m)
3284 self.assertEqual(msg.get_filename(),
3285 'This is even more ***fun*** is it not.pdf\ufffd')
3286
3287 def test_rfc2231_unknown_encoding(self):
3288 m = """\
3289Content-Transfer-Encoding: 8bit
3290Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
3291
3292"""
3293 msg = email.message_from_string(m)
3294 self.assertEqual(msg.get_filename(), 'myfile.txt')
3295
3296 def test_rfc2231_single_tick_in_filename_extended(self):
3297 eq = self.assertEqual
3298 m = """\
3299Content-Type: application/x-foo;
3300\tname*0*=\"Frank's\"; name*1*=\" Document\"
3301
3302"""
3303 msg = email.message_from_string(m)
3304 charset, language, s = msg.get_param('name')
3305 eq(charset, None)
3306 eq(language, None)
3307 eq(s, "Frank's Document")
3308
3309 def test_rfc2231_single_tick_in_filename(self):
3310 m = """\
3311Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
3312
3313"""
3314 msg = email.message_from_string(m)
3315 param = msg.get_param('name')
3316 self.failIf(isinstance(param, tuple))
3317 self.assertEqual(param, "Frank's Document")
3318
3319 def test_rfc2231_tick_attack_extended(self):
3320 eq = self.assertEqual
3321 m = """\
3322Content-Type: application/x-foo;
3323\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
3324
3325"""
3326 msg = email.message_from_string(m)
3327 charset, language, s = msg.get_param('name')
3328 eq(charset, 'us-ascii')
3329 eq(language, 'en-us')
3330 eq(s, "Frank's Document")
3331
3332 def test_rfc2231_tick_attack(self):
3333 m = """\
3334Content-Type: application/x-foo;
3335\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
3336
3337"""
3338 msg = email.message_from_string(m)
3339 param = msg.get_param('name')
3340 self.failIf(isinstance(param, tuple))
3341 self.assertEqual(param, "us-ascii'en-us'Frank's Document")
3342
3343 def test_rfc2231_no_extended_values(self):
3344 eq = self.assertEqual
3345 m = """\
3346Content-Type: application/x-foo; name=\"Frank's Document\"
3347
3348"""
3349 msg = email.message_from_string(m)
3350 eq(msg.get_param('name'), "Frank's Document")
3351
3352 def test_rfc2231_encoded_then_unencoded_segments(self):
3353 eq = self.assertEqual
3354 m = """\
3355Content-Type: application/x-foo;
3356\tname*0*=\"us-ascii'en-us'My\";
3357\tname*1=\" Document\";
3358\tname*2*=\" For You\"
3359
3360"""
3361 msg = email.message_from_string(m)
3362 charset, language, s = msg.get_param('name')
3363 eq(charset, 'us-ascii')
3364 eq(language, 'en-us')
3365 eq(s, 'My Document For You')
3366
3367 def test_rfc2231_unencoded_then_encoded_segments(self):
3368 eq = self.assertEqual
3369 m = """\
3370Content-Type: application/x-foo;
3371\tname*0=\"us-ascii'en-us'My\";
3372\tname*1*=\" Document\";
3373\tname*2*=\" For You\"
3374
3375"""
3376 msg = email.message_from_string(m)
3377 charset, language, s = msg.get_param('name')
3378 eq(charset, 'us-ascii')
3379 eq(language, 'en-us')
3380 eq(s, 'My Document For You')
3381
3382
3383
3384def _testclasses():
3385 mod = sys.modules[__name__]
3386 return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
3387
3388
3389def suite():
3390 suite = unittest.TestSuite()
3391 for testclass in _testclasses():
3392 suite.addTest(unittest.makeSuite(testclass))
3393 return suite
3394
3395
3396def test_main():
3397 for testclass in _testclasses():
3398 run_unittest(testclass)
3399
3400
3401
3402if __name__ == '__main__':
3403 unittest.main(defaultTest='suite')