blob: ef11e1730efc3b2f3d276797ea0ce26ce5148577 [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
1015 msg = MIMEApplication('\xfa\xfb\xfc\xfd\xfe\xff')
1016 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')
2558 eq(base64mime.body_encode('hello\n', 0), 'aGVsbG8NCg==\n')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002559 # Test the maxlinelen arg
Guido van Rossum9604e662007-08-30 03:46:43 +00002560 eq(base64mime.body_encode('xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002561eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2562eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2563eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg
2564eHh4eCB4eHh4IA==
2565""")
2566 # Test the eol argument
2567 eq(base64mime.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\
2568eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\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')
2684 eq(quoprimime.body_encode('hello\r\nworld', 0), 'hello\nworld')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002685 # Test the maxlinelen arg
Guido van Rossum9604e662007-08-30 03:46:43 +00002686 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40), """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002687xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=
2688 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=
2689x xxxx xxxx xxxx xxxx=20""")
2690 # Test the eol argument
Guido van Rossum9604e662007-08-30 03:46:43 +00002691 eq(quoprimime.body_encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'),
2692 """\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002693xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r
2694 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r
2695x xxxx xxxx xxxx xxxx=20""")
Guido van Rossum9604e662007-08-30 03:46:43 +00002696 eq(quoprimime.body_encode("""\
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002697one line
2698
2699two line"""), """\
2700one line
2701
2702two line""")
2703
2704
2705
2706# Test the Charset class
2707class TestCharset(unittest.TestCase):
2708 def tearDown(self):
2709 from email import charset as CharsetModule
2710 try:
2711 del CharsetModule.CHARSETS['fake']
2712 except KeyError:
2713 pass
2714
Guido van Rossum9604e662007-08-30 03:46:43 +00002715 def test_codec_encodeable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002716 eq = self.assertEqual
2717 # Make sure us-ascii = no Unicode conversion
2718 c = Charset('us-ascii')
Guido van Rossum9604e662007-08-30 03:46:43 +00002719 eq(c.header_encode('Hello World!'), 'Hello World!')
2720 # Test 8-bit idempotency with us-ascii
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002721 s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa'
Guido van Rossum9604e662007-08-30 03:46:43 +00002722 self.assertRaises(UnicodeError, c.header_encode, s)
2723 c = Charset('utf-8')
2724 eq(c.header_encode(s), '=?utf-8?b?wqTCosKkwqTCpMKmwqTCqMKkwqo=?=')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002725
2726 def test_body_encode(self):
2727 eq = self.assertEqual
2728 # Try a charset with QP body encoding
2729 c = Charset('iso-8859-1')
2730 eq('hello w=F6rld', c.body_encode(b'hello w\xf6rld'))
2731 # Try a charset with Base64 body encoding
2732 c = Charset('utf-8')
2733 eq('aGVsbG8gd29ybGQ=\n', c.body_encode(b'hello world'))
2734 # Try a charset with None body encoding
2735 c = Charset('us-ascii')
2736 eq('hello world', c.body_encode(b'hello world'))
2737 # Try the convert argument, where input codec != output codec
2738 c = Charset('euc-jp')
2739 # With apologies to Tokio Kikuchi ;)
2740 try:
2741 eq('\x1b$B5FCO;~IW\x1b(B',
2742 c.body_encode(b'\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7'))
2743 eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7',
2744 c.body_encode(b'\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False))
2745 except LookupError:
2746 # We probably don't have the Japanese codecs installed
2747 pass
2748 # Testing SF bug #625509, which we have to fake, since there are no
2749 # built-in encodings where the header encoding is QP but the body
2750 # encoding is not.
2751 from email import charset as CharsetModule
2752 CharsetModule.add_charset('fake', CharsetModule.QP, None)
2753 c = Charset('fake')
2754 eq('hello w\xf6rld', c.body_encode(b'hello w\xf6rld'))
2755
2756 def test_unicode_charset_name(self):
2757 charset = Charset('us-ascii')
2758 self.assertEqual(str(charset), 'us-ascii')
2759 self.assertRaises(errors.CharsetError, Charset, 'asc\xffii')
2760
2761
2762
2763# Test multilingual MIME headers.
2764class TestHeader(TestEmailBase):
2765 def test_simple(self):
2766 eq = self.ndiffAssertEqual
2767 h = Header('Hello World!')
2768 eq(h.encode(), 'Hello World!')
2769 h.append(' Goodbye World!')
2770 eq(h.encode(), 'Hello World! Goodbye World!')
2771
2772 def test_simple_surprise(self):
2773 eq = self.ndiffAssertEqual
2774 h = Header('Hello World!')
2775 eq(h.encode(), 'Hello World!')
2776 h.append('Goodbye World!')
2777 eq(h.encode(), 'Hello World! Goodbye World!')
2778
2779 def test_header_needs_no_decoding(self):
2780 h = 'no decoding needed'
2781 self.assertEqual(decode_header(h), [(h, None)])
2782
2783 def test_long(self):
2784 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.",
2785 maxlinelen=76)
2786 for l in h.encode(splitchars=' ').split('\n '):
2787 self.failUnless(len(l) <= 76)
2788
2789 def test_multilingual(self):
2790 eq = self.ndiffAssertEqual
2791 g = Charset("iso-8859-1")
2792 cz = Charset("iso-8859-2")
2793 utf8 = Charset("utf-8")
2794 g_head = (b'Die Mieter treten hier ein werden mit einem '
2795 b'Foerderband komfortabel den Korridor entlang, '
2796 b'an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, '
2797 b'gegen die rotierenden Klingen bef\xf6rdert. ')
2798 cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
2799 b'd\xf9vtipu.. ')
2800 utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
2801 '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
2802 '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
2803 '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
2804 '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
2805 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
2806 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
2807 '\u3044\u307e\u3059\u3002')
2808 h = Header(g_head, g)
2809 h.append(cz_head, cz)
2810 h.append(utf8_head, utf8)
Guido van Rossum9604e662007-08-30 03:46:43 +00002811 enc = h.encode(maxlinelen=76)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002812 eq(enc, """\
Guido van Rossum9604e662007-08-30 03:46:43 +00002813=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_kom?=
2814 =?iso-8859-1?q?fortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wand?=
2815 =?iso-8859-1?q?gem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6r?=
2816 =?iso-8859-1?q?dert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?=
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002817 =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
2818 =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?=
2819 =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?=
2820 =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?=
Guido van Rossum9604e662007-08-30 03:46:43 +00002821 =?utf-8?b?IE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5k?=
2822 =?utf-8?b?IGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIA=?=
2823 =?utf-8?b?44Gj44Gm44GE44G+44GZ44CC?=""")
2824 decoded = decode_header(enc)
2825 eq(len(decoded), 3)
2826 eq(decoded[0], (g_head, 'iso-8859-1'))
2827 eq(decoded[1], (cz_head, 'iso-8859-2'))
2828 eq(decoded[2], (utf8_head.encode('utf-8'), 'utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002829 ustr = str(h)
Guido van Rossum9604e662007-08-30 03:46:43 +00002830 eq(ustr,
2831 (b'Die Mieter treten hier ein werden mit einem Foerderband '
2832 b'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen '
2833 b'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen '
2834 b'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod '
2835 b'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81'
2836 b'\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3'
2837 b'\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3'
2838 b'\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83'
2839 b'\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e'
2840 b'\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3'
2841 b'\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82'
2842 b'\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b'
2843 b'\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git '
2844 b'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt '
2845 b'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81'
2846 b'\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82'
2847 ).decode('utf-8'))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002848 # Test make_header()
2849 newh = make_header(decode_header(enc))
Guido van Rossum9604e662007-08-30 03:46:43 +00002850 eq(newh, h)
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002851
2852 def test_empty_header_encode(self):
2853 h = Header()
2854 self.assertEqual(h.encode(), '')
Barry Warsaw8b3d6592007-08-30 02:10:49 +00002855
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002856 def test_header_ctor_default_args(self):
2857 eq = self.ndiffAssertEqual
2858 h = Header()
2859 eq(h, '')
2860 h.append('foo', Charset('iso-8859-1'))
Guido van Rossum9604e662007-08-30 03:46:43 +00002861 eq(h, 'foo')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002862
2863 def test_explicit_maxlinelen(self):
2864 eq = self.ndiffAssertEqual
2865 hstr = ('A very long line that must get split to something other '
2866 'than at the 76th character boundary to test the non-default '
2867 'behavior')
2868 h = Header(hstr)
2869 eq(h.encode(), '''\
2870A very long line that must get split to something other than at the 76th
2871 character boundary to test the non-default behavior''')
2872 eq(str(h), hstr)
2873 h = Header(hstr, header_name='Subject')
2874 eq(h.encode(), '''\
2875A very long line that must get split to something other than at the
2876 76th character boundary to test the non-default behavior''')
2877 eq(str(h), hstr)
2878 h = Header(hstr, maxlinelen=1024, header_name='Subject')
2879 eq(h.encode(), hstr)
2880 eq(str(h), hstr)
2881
Guido van Rossum9604e662007-08-30 03:46:43 +00002882 def test_quopri_splittable(self):
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002883 eq = self.ndiffAssertEqual
2884 h = Header(charset='iso-8859-1', maxlinelen=20)
Guido van Rossum9604e662007-08-30 03:46:43 +00002885 x = 'xxxx ' * 20
2886 h.append(x)
2887 s = h.encode()
2888 eq(s, """\
2889=?iso-8859-1?q?xxx?=
2890 =?iso-8859-1?q?x_?=
2891 =?iso-8859-1?q?xx?=
2892 =?iso-8859-1?q?xx?=
2893 =?iso-8859-1?q?_x?=
2894 =?iso-8859-1?q?xx?=
2895 =?iso-8859-1?q?x_?=
2896 =?iso-8859-1?q?xx?=
2897 =?iso-8859-1?q?xx?=
2898 =?iso-8859-1?q?_x?=
2899 =?iso-8859-1?q?xx?=
2900 =?iso-8859-1?q?x_?=
2901 =?iso-8859-1?q?xx?=
2902 =?iso-8859-1?q?xx?=
2903 =?iso-8859-1?q?_x?=
2904 =?iso-8859-1?q?xx?=
2905 =?iso-8859-1?q?x_?=
2906 =?iso-8859-1?q?xx?=
2907 =?iso-8859-1?q?xx?=
2908 =?iso-8859-1?q?_x?=
2909 =?iso-8859-1?q?xx?=
2910 =?iso-8859-1?q?x_?=
2911 =?iso-8859-1?q?xx?=
2912 =?iso-8859-1?q?xx?=
2913 =?iso-8859-1?q?_x?=
2914 =?iso-8859-1?q?xx?=
2915 =?iso-8859-1?q?x_?=
2916 =?iso-8859-1?q?xx?=
2917 =?iso-8859-1?q?xx?=
2918 =?iso-8859-1?q?_x?=
2919 =?iso-8859-1?q?xx?=
2920 =?iso-8859-1?q?x_?=
2921 =?iso-8859-1?q?xx?=
2922 =?iso-8859-1?q?xx?=
2923 =?iso-8859-1?q?_x?=
2924 =?iso-8859-1?q?xx?=
2925 =?iso-8859-1?q?x_?=
2926 =?iso-8859-1?q?xx?=
2927 =?iso-8859-1?q?xx?=
2928 =?iso-8859-1?q?_x?=
2929 =?iso-8859-1?q?xx?=
2930 =?iso-8859-1?q?x_?=
2931 =?iso-8859-1?q?xx?=
2932 =?iso-8859-1?q?xx?=
2933 =?iso-8859-1?q?_x?=
2934 =?iso-8859-1?q?xx?=
2935 =?iso-8859-1?q?x_?=
2936 =?iso-8859-1?q?xx?=
2937 =?iso-8859-1?q?xx?=
2938 =?iso-8859-1?q?_?=""")
2939 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00002940 h = Header(charset='iso-8859-1', maxlinelen=40)
2941 h.append('xxxx ' * 20)
Guido van Rossum9604e662007-08-30 03:46:43 +00002942 s = h.encode()
2943 eq(s, """\
2944=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xxx?=
2945 =?iso-8859-1?q?x_xxxx_xxxx_xxxx_xxxx_?=
2946 =?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=
2947 =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=
2948 =?iso-8859-1?q?_xxxx_xxxx_?=""")
2949 eq(x, str(make_header(decode_header(s))))
2950
2951 def test_base64_splittable(self):
2952 eq = self.ndiffAssertEqual
2953 h = Header(charset='koi8-r', maxlinelen=20)
2954 x = 'xxxx ' * 20
2955 h.append(x)
2956 s = h.encode()
2957 eq(s, """\
2958=?koi8-r?b?eHh4?=
2959 =?koi8-r?b?eCB4?=
2960 =?koi8-r?b?eHh4?=
2961 =?koi8-r?b?IHh4?=
2962 =?koi8-r?b?eHgg?=
2963 =?koi8-r?b?eHh4?=
2964 =?koi8-r?b?eCB4?=
2965 =?koi8-r?b?eHh4?=
2966 =?koi8-r?b?IHh4?=
2967 =?koi8-r?b?eHgg?=
2968 =?koi8-r?b?eHh4?=
2969 =?koi8-r?b?eCB4?=
2970 =?koi8-r?b?eHh4?=
2971 =?koi8-r?b?IHh4?=
2972 =?koi8-r?b?eHgg?=
2973 =?koi8-r?b?eHh4?=
2974 =?koi8-r?b?eCB4?=
2975 =?koi8-r?b?eHh4?=
2976 =?koi8-r?b?IHh4?=
2977 =?koi8-r?b?eHgg?=
2978 =?koi8-r?b?eHh4?=
2979 =?koi8-r?b?eCB4?=
2980 =?koi8-r?b?eHh4?=
2981 =?koi8-r?b?IHh4?=
2982 =?koi8-r?b?eHgg?=
2983 =?koi8-r?b?eHh4?=
2984 =?koi8-r?b?eCB4?=
2985 =?koi8-r?b?eHh4?=
2986 =?koi8-r?b?IHh4?=
2987 =?koi8-r?b?eHgg?=
2988 =?koi8-r?b?eHh4?=
2989 =?koi8-r?b?eCB4?=
2990 =?koi8-r?b?eHh4?=
2991 =?koi8-r?b?IA==?=""")
2992 eq(x, str(make_header(decode_header(s))))
2993 h = Header(charset='koi8-r', maxlinelen=40)
2994 h.append(x)
2995 s = h.encode()
2996 eq(s, """\
2997=?koi8-r?b?eHh4eCB4eHh4IHh4eHggeHh4?=
2998 =?koi8-r?b?eCB4eHh4IHh4eHggeHh4eCB4?=
2999 =?koi8-r?b?eHh4IHh4eHggeHh4eCB4eHh4?=
3000 =?koi8-r?b?IHh4eHggeHh4eCB4eHh4IHh4?=
3001 =?koi8-r?b?eHggeHh4eCB4eHh4IHh4eHgg?=
3002 =?koi8-r?b?eHh4eCB4eHh4IA==?=""")
3003 eq(x, str(make_header(decode_header(s))))
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003004
3005 def test_us_ascii_header(self):
3006 eq = self.assertEqual
3007 s = 'hello'
3008 x = decode_header(s)
3009 eq(x, [('hello', None)])
3010 h = make_header(x)
3011 eq(s, h.encode())
3012
3013 def test_string_charset(self):
3014 eq = self.assertEqual
3015 h = Header()
3016 h.append('hello', 'iso-8859-1')
Guido van Rossum9604e662007-08-30 03:46:43 +00003017 eq(h, 'hello')
Guido van Rossum8b3febe2007-08-30 01:15:14 +00003018
3019## def test_unicode_error(self):
3020## raises = self.assertRaises
3021## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii')
3022## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii')
3023## h = Header()
3024## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii')
3025## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii')
3026## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1')
3027
3028 def test_utf8_shortest(self):
3029 eq = self.assertEqual
3030 h = Header('p\xf6stal', 'utf-8')
3031 eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=')
3032 h = Header('\u83ca\u5730\u6642\u592b', 'utf-8')
3033 eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=')
3034
3035 def test_bad_8bit_header(self):
3036 raises = self.assertRaises
3037 eq = self.assertEqual
3038 x = b'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big'
3039 raises(UnicodeError, Header, x)
3040 h = Header()
3041 raises(UnicodeError, h.append, x)
3042 e = x.decode('utf-8', 'replace')
3043 eq(str(Header(x, errors='replace')), e)
3044 h.append(x, errors='replace')
3045 eq(str(h), e)
3046
3047 def test_encoded_adjacent_nonencoded(self):
3048 eq = self.assertEqual
3049 h = Header()
3050 h.append('hello', 'iso-8859-1')
3051 h.append('world')
3052 s = h.encode()
3053 eq(s, '=?iso-8859-1?q?hello?= world')
3054 h = make_header(decode_header(s))
3055 eq(h.encode(), s)
3056
3057 def test_whitespace_eater(self):
3058 eq = self.assertEqual
3059 s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.'
3060 parts = decode_header(s)
3061 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)])
3062 hdr = make_header(parts)
3063 eq(hdr.encode(),
3064 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.')
3065
3066 def test_broken_base64_header(self):
3067 raises = self.assertRaises
3068 s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3IQ?='
3069 raises(errors.HeaderParseError, decode_header, s)
3070
3071
3072
3073# Test RFC 2231 header parameters (en/de)coding
3074class TestRFC2231(TestEmailBase):
3075 def test_get_param(self):
3076 eq = self.assertEqual
3077 msg = self._msgobj('msg_29.txt')
3078 eq(msg.get_param('title'),
3079 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3080 eq(msg.get_param('title', unquote=False),
3081 ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"'))
3082
3083 def test_set_param(self):
3084 eq = self.ndiffAssertEqual
3085 msg = Message()
3086 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3087 charset='us-ascii')
3088 eq(msg.get_param('title'),
3089 ('us-ascii', '', 'This is even more ***fun*** isn\'t it!'))
3090 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3091 charset='us-ascii', language='en')
3092 eq(msg.get_param('title'),
3093 ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!'))
3094 msg = self._msgobj('msg_01.txt')
3095 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3096 charset='us-ascii', language='en')
3097 eq(msg.as_string(maxheaderlen=78), """\
3098Return-Path: <bbb@zzz.org>
3099Delivered-To: bbb@zzz.org
3100Received: by mail.zzz.org (Postfix, from userid 889)
3101\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3102MIME-Version: 1.0
3103Content-Transfer-Encoding: 7bit
3104Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3105From: bbb@ddd.com (John X. Doe)
3106To: bbb@zzz.org
3107Subject: This is a test message
3108Date: Fri, 4 May 2001 14:05:44 -0400
3109Content-Type: text/plain; charset=us-ascii;
3110 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3111
3112
3113Hi,
3114
3115Do you like this message?
3116
3117-Me
3118""")
3119
3120 def test_del_param(self):
3121 eq = self.ndiffAssertEqual
3122 msg = self._msgobj('msg_01.txt')
3123 msg.set_param('foo', 'bar', charset='us-ascii', language='en')
3124 msg.set_param('title', 'This is even more ***fun*** isn\'t it!',
3125 charset='us-ascii', language='en')
3126 msg.del_param('foo', header='Content-Type')
3127 eq(msg.as_string(maxheaderlen=78), """\
3128Return-Path: <bbb@zzz.org>
3129Delivered-To: bbb@zzz.org
3130Received: by mail.zzz.org (Postfix, from userid 889)
3131\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT)
3132MIME-Version: 1.0
3133Content-Transfer-Encoding: 7bit
3134Message-ID: <15090.61304.110929.45684@aaa.zzz.org>
3135From: bbb@ddd.com (John X. Doe)
3136To: bbb@zzz.org
3137Subject: This is a test message
3138Date: Fri, 4 May 2001 14:05:44 -0400
3139Content-Type: text/plain; charset="us-ascii";
3140 title*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21"
3141
3142
3143Hi,
3144
3145Do you like this message?
3146
3147-Me
3148""")
3149
3150 def test_rfc2231_get_content_charset(self):
3151 eq = self.assertEqual
3152 msg = self._msgobj('msg_32.txt')
3153 eq(msg.get_content_charset(), 'us-ascii')
3154
3155 def test_rfc2231_no_language_or_charset(self):
3156 m = '''\
3157Content-Transfer-Encoding: 8bit
3158Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm"
3159Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm
3160
3161'''
3162 msg = email.message_from_string(m)
3163 param = msg.get_param('NAME')
3164 self.failIf(isinstance(param, tuple))
3165 self.assertEqual(
3166 param,
3167 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm')
3168
3169 def test_rfc2231_no_language_or_charset_in_filename(self):
3170 m = '''\
3171Content-Disposition: inline;
3172\tfilename*0*="''This%20is%20even%20more%20";
3173\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3174\tfilename*2="is it not.pdf"
3175
3176'''
3177 msg = email.message_from_string(m)
3178 self.assertEqual(msg.get_filename(),
3179 'This is even more ***fun*** is it not.pdf')
3180
3181 def test_rfc2231_no_language_or_charset_in_filename_encoded(self):
3182 m = '''\
3183Content-Disposition: inline;
3184\tfilename*0*="''This%20is%20even%20more%20";
3185\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3186\tfilename*2="is it not.pdf"
3187
3188'''
3189 msg = email.message_from_string(m)
3190 self.assertEqual(msg.get_filename(),
3191 'This is even more ***fun*** is it not.pdf')
3192
3193 def test_rfc2231_partly_encoded(self):
3194 m = '''\
3195Content-Disposition: inline;
3196\tfilename*0="''This%20is%20even%20more%20";
3197\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3198\tfilename*2="is it not.pdf"
3199
3200'''
3201 msg = email.message_from_string(m)
3202 self.assertEqual(
3203 msg.get_filename(),
3204 'This%20is%20even%20more%20***fun*** is it not.pdf')
3205
3206 def test_rfc2231_partly_nonencoded(self):
3207 m = '''\
3208Content-Disposition: inline;
3209\tfilename*0="This%20is%20even%20more%20";
3210\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20";
3211\tfilename*2="is it not.pdf"
3212
3213'''
3214 msg = email.message_from_string(m)
3215 self.assertEqual(
3216 msg.get_filename(),
3217 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf')
3218
3219 def test_rfc2231_no_language_or_charset_in_boundary(self):
3220 m = '''\
3221Content-Type: multipart/alternative;
3222\tboundary*0*="''This%20is%20even%20more%20";
3223\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20";
3224\tboundary*2="is it not.pdf"
3225
3226'''
3227 msg = email.message_from_string(m)
3228 self.assertEqual(msg.get_boundary(),
3229 'This is even more ***fun*** is it not.pdf')
3230
3231 def test_rfc2231_no_language_or_charset_in_charset(self):
3232 # This is a nonsensical charset value, but tests the code anyway
3233 m = '''\
3234Content-Type: text/plain;
3235\tcharset*0*="This%20is%20even%20more%20";
3236\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20";
3237\tcharset*2="is it not.pdf"
3238
3239'''
3240 msg = email.message_from_string(m)
3241 self.assertEqual(msg.get_content_charset(),
3242 'this is even more ***fun*** is it not.pdf')
3243
3244 def test_rfc2231_bad_encoding_in_filename(self):
3245 m = '''\
3246Content-Disposition: inline;
3247\tfilename*0*="bogus'xx'This%20is%20even%20more%20";
3248\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3249\tfilename*2="is it not.pdf"
3250
3251'''
3252 msg = email.message_from_string(m)
3253 self.assertEqual(msg.get_filename(),
3254 'This is even more ***fun*** is it not.pdf')
3255
3256 def test_rfc2231_bad_encoding_in_charset(self):
3257 m = """\
3258Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D
3259
3260"""
3261 msg = email.message_from_string(m)
3262 # This should return None because non-ascii characters in the charset
3263 # are not allowed.
3264 self.assertEqual(msg.get_content_charset(), None)
3265
3266 def test_rfc2231_bad_character_in_charset(self):
3267 m = """\
3268Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D
3269
3270"""
3271 msg = email.message_from_string(m)
3272 # This should return None because non-ascii characters in the charset
3273 # are not allowed.
3274 self.assertEqual(msg.get_content_charset(), None)
3275
3276 def test_rfc2231_bad_character_in_filename(self):
3277 m = '''\
3278Content-Disposition: inline;
3279\tfilename*0*="ascii'xx'This%20is%20even%20more%20";
3280\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20";
3281\tfilename*2*="is it not.pdf%E2"
3282
3283'''
3284 msg = email.message_from_string(m)
3285 self.assertEqual(msg.get_filename(),
3286 'This is even more ***fun*** is it not.pdf\ufffd')
3287
3288 def test_rfc2231_unknown_encoding(self):
3289 m = """\
3290Content-Transfer-Encoding: 8bit
3291Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt
3292
3293"""
3294 msg = email.message_from_string(m)
3295 self.assertEqual(msg.get_filename(), 'myfile.txt')
3296
3297 def test_rfc2231_single_tick_in_filename_extended(self):
3298 eq = self.assertEqual
3299 m = """\
3300Content-Type: application/x-foo;
3301\tname*0*=\"Frank's\"; name*1*=\" Document\"
3302
3303"""
3304 msg = email.message_from_string(m)
3305 charset, language, s = msg.get_param('name')
3306 eq(charset, None)
3307 eq(language, None)
3308 eq(s, "Frank's Document")
3309
3310 def test_rfc2231_single_tick_in_filename(self):
3311 m = """\
3312Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\"
3313
3314"""
3315 msg = email.message_from_string(m)
3316 param = msg.get_param('name')
3317 self.failIf(isinstance(param, tuple))
3318 self.assertEqual(param, "Frank's Document")
3319
3320 def test_rfc2231_tick_attack_extended(self):
3321 eq = self.assertEqual
3322 m = """\
3323Content-Type: application/x-foo;
3324\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\"
3325
3326"""
3327 msg = email.message_from_string(m)
3328 charset, language, s = msg.get_param('name')
3329 eq(charset, 'us-ascii')
3330 eq(language, 'en-us')
3331 eq(s, "Frank's Document")
3332
3333 def test_rfc2231_tick_attack(self):
3334 m = """\
3335Content-Type: application/x-foo;
3336\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\"
3337
3338"""
3339 msg = email.message_from_string(m)
3340 param = msg.get_param('name')
3341 self.failIf(isinstance(param, tuple))
3342 self.assertEqual(param, "us-ascii'en-us'Frank's Document")
3343
3344 def test_rfc2231_no_extended_values(self):
3345 eq = self.assertEqual
3346 m = """\
3347Content-Type: application/x-foo; name=\"Frank's Document\"
3348
3349"""
3350 msg = email.message_from_string(m)
3351 eq(msg.get_param('name'), "Frank's Document")
3352
3353 def test_rfc2231_encoded_then_unencoded_segments(self):
3354 eq = self.assertEqual
3355 m = """\
3356Content-Type: application/x-foo;
3357\tname*0*=\"us-ascii'en-us'My\";
3358\tname*1=\" Document\";
3359\tname*2*=\" For You\"
3360
3361"""
3362 msg = email.message_from_string(m)
3363 charset, language, s = msg.get_param('name')
3364 eq(charset, 'us-ascii')
3365 eq(language, 'en-us')
3366 eq(s, 'My Document For You')
3367
3368 def test_rfc2231_unencoded_then_encoded_segments(self):
3369 eq = self.assertEqual
3370 m = """\
3371Content-Type: application/x-foo;
3372\tname*0=\"us-ascii'en-us'My\";
3373\tname*1*=\" Document\";
3374\tname*2*=\" For You\"
3375
3376"""
3377 msg = email.message_from_string(m)
3378 charset, language, s = msg.get_param('name')
3379 eq(charset, 'us-ascii')
3380 eq(language, 'en-us')
3381 eq(s, 'My Document For You')
3382
3383
3384
3385def _testclasses():
3386 mod = sys.modules[__name__]
3387 return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')]
3388
3389
3390def suite():
3391 suite = unittest.TestSuite()
3392 for testclass in _testclasses():
3393 suite.addTest(unittest.makeSuite(testclass))
3394 return suite
3395
3396
3397def test_main():
3398 for testclass in _testclasses():
3399 run_unittest(testclass)
3400
3401
3402
3403if __name__ == '__main__':
3404 unittest.main(defaultTest='suite')