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