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