source: python/trunk/Lib/email/test/test_email.py

Last change on this file was 391, checked in by dmik, 11 years ago

python: Merge vendor 2.7.6 to trunk.

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