blob: 3134a88e013b36c28411626433ee3dba4d416663 [file] [log] [blame]
Barry Warsaw41075852001-09-23 03:18:13 +00001# Copyright (C) 2001 Python Software Foundation
2# email package unit tests
3
4import os
5import time
6import unittest
7import base64
8from cStringIO import StringIO
9from types import StringType
10
11import email
12
Barry Warsawbf7a59d2001-10-11 15:44:50 +000013from email.Parser import Parser, HeaderParser
Barry Warsaw41075852001-09-23 03:18:13 +000014from email.Generator import Generator, DecodedGenerator
15from email.Message import Message
Barry Warsawfee435a2001-10-09 19:23:57 +000016from email.MIMEAudio import MIMEAudio
Barry Warsaw65279d02001-09-26 05:47:08 +000017from email.MIMEText import MIMEText
18from email.MIMEImage import MIMEImage
Barry Warsaw41075852001-09-23 03:18:13 +000019from email.MIMEBase import MIMEBase
Barry Warsaw65279d02001-09-26 05:47:08 +000020from email.MIMEMessage import MIMEMessage
Barry Warsaw41075852001-09-23 03:18:13 +000021from email import Utils
22from email import Errors
23from email import Encoders
24from email import Iterators
25
Barry Warsawfee435a2001-10-09 19:23:57 +000026import test_email
27from test_support import findfile
Barry Warsaw41075852001-09-23 03:18:13 +000028
29NL = '\n'
30EMPTYSTRING = ''
31
32
Barry Warsaw08a534d2001-10-04 17:58:50 +000033
Barry Warsaw41075852001-09-23 03:18:13 +000034def openfile(filename):
Barry Warsawfee435a2001-10-09 19:23:57 +000035 path = os.path.join(os.path.dirname(test_email.__file__), 'data', filename)
Barry Warsaw41075852001-09-23 03:18:13 +000036 return open(path)
37
38
Barry Warsaw08a534d2001-10-04 17:58:50 +000039
Barry Warsaw41075852001-09-23 03:18:13 +000040# Base test class
41class TestEmailBase(unittest.TestCase):
42 def _msgobj(self, filename):
43 fp = openfile(filename)
44 try:
Barry Warsaw65279d02001-09-26 05:47:08 +000045 msg = email.message_from_file(fp)
Barry Warsaw41075852001-09-23 03:18:13 +000046 finally:
47 fp.close()
Barry Warsaw65279d02001-09-26 05:47:08 +000048 return msg
Barry Warsaw41075852001-09-23 03:18:13 +000049
50
Barry Warsaw08a534d2001-10-04 17:58:50 +000051
Barry Warsaw41075852001-09-23 03:18:13 +000052# Test various aspects of the Message class's API
53class TestMessageAPI(TestEmailBase):
Barry Warsaw2f6a0b02001-10-09 15:49:35 +000054 def test_get_all(self):
55 eq = self.assertEqual
56 msg = self._msgobj('msg_20.txt')
57 eq(msg.get_all('cc'), ['ccc@zzz.org', 'ddd@zzz.org', 'eee@zzz.org'])
58 eq(msg.get_all('xx', 'n/a'), 'n/a')
59
Barry Warsaw41075852001-09-23 03:18:13 +000060 def test_get_charsets(self):
61 eq = self.assertEqual
Tim Peters527e64f2001-10-04 05:36:56 +000062
Barry Warsaw65279d02001-09-26 05:47:08 +000063 msg = self._msgobj('msg_08.txt')
64 charsets = msg.get_charsets()
65 eq(charsets, [None, 'us-ascii', 'iso-8859-1', 'iso-8859-2', 'koi8-r'])
Barry Warsaw41075852001-09-23 03:18:13 +000066
Barry Warsaw65279d02001-09-26 05:47:08 +000067 msg = self._msgobj('msg_09.txt')
68 charsets = msg.get_charsets('dingbat')
69 eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat',
70 'koi8-r'])
Barry Warsaw41075852001-09-23 03:18:13 +000071
Barry Warsaw65279d02001-09-26 05:47:08 +000072 msg = self._msgobj('msg_12.txt')
73 charsets = msg.get_charsets()
74 eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2',
75 'iso-8859-3', 'us-ascii', 'koi8-r'])
Barry Warsaw41075852001-09-23 03:18:13 +000076
77 def test_get_filename(self):
78 eq = self.assertEqual
79
Barry Warsaw65279d02001-09-26 05:47:08 +000080 msg = self._msgobj('msg_04.txt')
81 filenames = [p.get_filename() for p in msg.get_payload()]
Barry Warsaw41075852001-09-23 03:18:13 +000082 eq(filenames, ['msg.txt', 'msg.txt'])
83
Barry Warsaw65279d02001-09-26 05:47:08 +000084 msg = self._msgobj('msg_07.txt')
85 subpart = msg.get_payload(1)
Barry Warsaw41075852001-09-23 03:18:13 +000086 eq(subpart.get_filename(), 'dingusfish.gif')
87
88 def test_get_boundary(self):
89 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +000090 msg = self._msgobj('msg_07.txt')
Barry Warsaw41075852001-09-23 03:18:13 +000091 # No quotes!
Barry Warsaw65279d02001-09-26 05:47:08 +000092 eq(msg.get_boundary(), 'BOUNDARY')
Barry Warsaw41075852001-09-23 03:18:13 +000093
94 def test_set_boundary(self):
95 eq = self.assertEqual
96 # This one has no existing boundary parameter, but the Content-Type:
97 # header appears fifth.
Barry Warsaw65279d02001-09-26 05:47:08 +000098 msg = self._msgobj('msg_01.txt')
99 msg.set_boundary('BOUNDARY')
100 header, value = msg.items()[4]
Barry Warsaw41075852001-09-23 03:18:13 +0000101 eq(header.lower(), 'content-type')
102 eq(value, 'text/plain; charset=us-ascii; boundary="BOUNDARY"')
103 # This one has a Content-Type: header, with a boundary, stuck in the
104 # middle of its headers. Make sure the order is preserved; it should
105 # be fifth.
Barry Warsaw65279d02001-09-26 05:47:08 +0000106 msg = self._msgobj('msg_04.txt')
107 msg.set_boundary('BOUNDARY')
108 header, value = msg.items()[4]
Barry Warsaw41075852001-09-23 03:18:13 +0000109 eq(header.lower(), 'content-type')
110 eq(value, 'multipart/mixed; boundary="BOUNDARY"')
111 # And this one has no Content-Type: header at all.
Barry Warsaw65279d02001-09-26 05:47:08 +0000112 msg = self._msgobj('msg_03.txt')
Barry Warsaw41075852001-09-23 03:18:13 +0000113 self.assertRaises(Errors.HeaderParseError,
Barry Warsaw65279d02001-09-26 05:47:08 +0000114 msg.set_boundary, 'BOUNDARY')
Barry Warsaw41075852001-09-23 03:18:13 +0000115
116 def test_get_decoded_payload(self):
117 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000118 msg = self._msgobj('msg_10.txt')
Barry Warsaw41075852001-09-23 03:18:13 +0000119 # The outer message is a multipart
Barry Warsaw65279d02001-09-26 05:47:08 +0000120 eq(msg.get_payload(decode=1), None)
Barry Warsaw41075852001-09-23 03:18:13 +0000121 # Subpart 1 is 7bit encoded
Barry Warsaw65279d02001-09-26 05:47:08 +0000122 eq(msg.get_payload(0).get_payload(decode=1),
Barry Warsaw41075852001-09-23 03:18:13 +0000123 'This is a 7bit encoded message.\n')
124 # Subpart 2 is quopri
Barry Warsaw65279d02001-09-26 05:47:08 +0000125 eq(msg.get_payload(1).get_payload(decode=1),
Barry Warsaw41075852001-09-23 03:18:13 +0000126 '\xa1This is a Quoted Printable encoded message!\n')
127 # Subpart 3 is base64
Barry Warsaw65279d02001-09-26 05:47:08 +0000128 eq(msg.get_payload(2).get_payload(decode=1),
Barry Warsaw41075852001-09-23 03:18:13 +0000129 'This is a Base64 encoded message.')
130 # Subpart 4 has no Content-Transfer-Encoding: header.
Barry Warsaw65279d02001-09-26 05:47:08 +0000131 eq(msg.get_payload(3).get_payload(decode=1),
Barry Warsaw41075852001-09-23 03:18:13 +0000132 'This has no Content-Transfer-Encoding: header.\n')
133
134 def test_decoded_generator(self):
135 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000136 msg = self._msgobj('msg_07.txt')
137 fp = openfile('msg_17.txt')
138 try:
139 text = fp.read()
140 finally:
141 fp.close()
Barry Warsaw41075852001-09-23 03:18:13 +0000142 s = StringIO()
143 g = DecodedGenerator(s)
Barry Warsaw65279d02001-09-26 05:47:08 +0000144 g(msg)
145 eq(s.getvalue(), text)
Barry Warsaw41075852001-09-23 03:18:13 +0000146
147 def test__contains__(self):
148 msg = Message()
149 msg['From'] = 'Me'
150 msg['to'] = 'You'
151 # Check for case insensitivity
152 self.failUnless('from' in msg)
153 self.failUnless('From' in msg)
154 self.failUnless('FROM' in msg)
155 self.failUnless('to' in msg)
156 self.failUnless('To' in msg)
157 self.failUnless('TO' in msg)
158
159 def test_as_string(self):
160 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000161 msg = self._msgobj('msg_01.txt')
Barry Warsaw41075852001-09-23 03:18:13 +0000162 fp = openfile('msg_01.txt')
163 try:
164 text = fp.read()
165 finally:
166 fp.close()
Barry Warsaw65279d02001-09-26 05:47:08 +0000167 eq(text, msg.as_string())
168 fullrepr = str(msg)
Barry Warsaw41075852001-09-23 03:18:13 +0000169 lines = fullrepr.split('\n')
170 self.failUnless(lines[0].startswith('From '))
171 eq(text, NL.join(lines[1:]))
172
173 def test_bad_param(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000174 msg = email.message_from_string("Content-Type: blarg; baz; boo\n")
Barry Warsaw41075852001-09-23 03:18:13 +0000175 self.assertEqual(msg.get_param('baz'), '')
176
177 def test_missing_filename(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000178 msg = email.message_from_string("From: foo\n")
Barry Warsaw41075852001-09-23 03:18:13 +0000179 self.assertEqual(msg.get_filename(), None)
180
181 def test_bogus_filename(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000182 msg = email.message_from_string(
183 "Content-Disposition: blarg; filename\n")
Barry Warsaw41075852001-09-23 03:18:13 +0000184 self.assertEqual(msg.get_filename(), '')
Tim Peters527e64f2001-10-04 05:36:56 +0000185
Barry Warsaw41075852001-09-23 03:18:13 +0000186 def test_missing_boundary(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000187 msg = email.message_from_string("From: foo\n")
Barry Warsaw41075852001-09-23 03:18:13 +0000188 self.assertEqual(msg.get_boundary(), None)
189
Barry Warsaw65279d02001-09-26 05:47:08 +0000190 def test_get_params(self):
191 eq = self.assertEqual
192 msg = email.message_from_string(
193 'X-Header: foo=one; bar=two; baz=three\n')
194 eq(msg.get_params(header='x-header'),
195 [('foo', 'one'), ('bar', 'two'), ('baz', 'three')])
196 msg = email.message_from_string(
197 'X-Header: foo; bar=one; baz=two\n')
198 eq(msg.get_params(header='x-header'),
199 [('foo', ''), ('bar', 'one'), ('baz', 'two')])
200 eq(msg.get_params(), None)
201 msg = email.message_from_string(
202 'X-Header: foo; bar="one"; baz=two\n')
203 eq(msg.get_params(header='x-header'),
204 [('foo', ''), ('bar', 'one'), ('baz', 'two')])
205
206 def test_get_param(self):
207 eq = self.assertEqual
208 msg = email.message_from_string(
209 "X-Header: foo=one; bar=two; baz=three\n")
210 eq(msg.get_param('bar', header='x-header'), 'two')
211 eq(msg.get_param('quuz', header='x-header'), None)
212 eq(msg.get_param('quuz'), None)
213 msg = email.message_from_string(
214 'X-Header: foo; bar="one"; baz=two\n')
215 eq(msg.get_param('foo', header='x-header'), '')
216 eq(msg.get_param('bar', header='x-header'), 'one')
217 eq(msg.get_param('baz', header='x-header'), 'two')
218
219 def test_has_key(self):
220 msg = email.message_from_string('Header: exists')
221 self.failUnless(msg.has_key('header'))
222 self.failUnless(msg.has_key('Header'))
223 self.failUnless(msg.has_key('HEADER'))
224 self.failIf(msg.has_key('headeri'))
225
Barry Warsaw41075852001-09-23 03:18:13 +0000226
Barry Warsaw08a534d2001-10-04 17:58:50 +0000227
Barry Warsaw41075852001-09-23 03:18:13 +0000228# Test the email.Encoders module
229class TestEncoders(unittest.TestCase):
230 def test_encode_noop(self):
231 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000232 msg = MIMEText('hello world', _encoder=Encoders.encode_noop)
Barry Warsaw41075852001-09-23 03:18:13 +0000233 eq(msg.get_payload(), 'hello world\n')
234 eq(msg['content-transfer-encoding'], None)
235
236 def test_encode_7bit(self):
237 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000238 msg = MIMEText('hello world', _encoder=Encoders.encode_7or8bit)
Barry Warsaw41075852001-09-23 03:18:13 +0000239 eq(msg.get_payload(), 'hello world\n')
240 eq(msg['content-transfer-encoding'], '7bit')
Barry Warsaw65279d02001-09-26 05:47:08 +0000241 msg = MIMEText('hello \x7f world', _encoder=Encoders.encode_7or8bit)
Barry Warsaw41075852001-09-23 03:18:13 +0000242 eq(msg.get_payload(), 'hello \x7f world\n')
243 eq(msg['content-transfer-encoding'], '7bit')
244
245 def test_encode_8bit(self):
246 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000247 msg = MIMEText('hello \x80 world', _encoder=Encoders.encode_7or8bit)
Barry Warsaw41075852001-09-23 03:18:13 +0000248 eq(msg.get_payload(), 'hello \x80 world\n')
249 eq(msg['content-transfer-encoding'], '8bit')
250
251 def test_encode_base64(self):
252 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000253 msg = MIMEText('hello world', _encoder=Encoders.encode_base64)
Barry Warsaw41075852001-09-23 03:18:13 +0000254 eq(msg.get_payload(), 'aGVsbG8gd29ybGQK\n')
255 eq(msg['content-transfer-encoding'], 'base64')
256
257 def test_encode_quoted_printable(self):
258 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000259 msg = MIMEText('hello world', _encoder=Encoders.encode_quopri)
Barry Warsaw41075852001-09-23 03:18:13 +0000260 eq(msg.get_payload(), 'hello=20world\n')
261 eq(msg['content-transfer-encoding'], 'quoted-printable')
262
263
Barry Warsaw08a534d2001-10-04 17:58:50 +0000264
265# Test long header wrapping
Barry Warsaw41075852001-09-23 03:18:13 +0000266class TestLongHeaders(unittest.TestCase):
267 def test_header_splitter(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000268 msg = MIMEText('')
Barry Warsaw41075852001-09-23 03:18:13 +0000269 # It'd be great if we could use add_header() here, but that doesn't
270 # guarantee an order of the parameters.
271 msg['X-Foobar-Spoink-Defrobnit'] = (
272 'wasnipoop; giraffes="very-long-necked-animals"; '
273 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"')
274 sfp = StringIO()
275 g = Generator(sfp)
276 g(msg)
Barry Warsaw08a534d2001-10-04 17:58:50 +0000277 self.assertEqual(sfp.getvalue(), openfile('msg_18.txt').read())
Barry Warsaw41075852001-09-23 03:18:13 +0000278
279
Barry Warsaw08a534d2001-10-04 17:58:50 +0000280
281# Test mangling of "From " lines in the body of a message
Barry Warsaw41075852001-09-23 03:18:13 +0000282class TestFromMangling(unittest.TestCase):
283 def setUp(self):
284 self.msg = Message()
285 self.msg['From'] = 'aaa@bbb.org'
286 self.msg.add_payload("""\
287From the desk of A.A.A.:
288Blah blah blah
289""")
290
291 def test_mangled_from(self):
292 s = StringIO()
293 g = Generator(s, mangle_from_=1)
294 g(self.msg)
295 self.assertEqual(s.getvalue(), """\
296From: aaa@bbb.org
297
298>From the desk of A.A.A.:
299Blah blah blah
300""")
301
302 def test_dont_mangle_from(self):
303 s = StringIO()
304 g = Generator(s, mangle_from_=0)
305 g(self.msg)
306 self.assertEqual(s.getvalue(), """\
307From: aaa@bbb.org
308
309From the desk of A.A.A.:
310Blah blah blah
311""")
312
313
Barry Warsaw08a534d2001-10-04 17:58:50 +0000314
Barry Warsawfee435a2001-10-09 19:23:57 +0000315# Test the basic MIMEAudio class
316class TestMIMEAudio(unittest.TestCase):
317 def setUp(self):
318 # In Python, audiotest.au lives in Lib/test not Lib/test/data
319 fp = open(findfile('audiotest.au'))
320 try:
321 self._audiodata = fp.read()
322 finally:
323 fp.close()
324 self._au = MIMEAudio(self._audiodata)
325
326 def test_guess_minor_type(self):
327 self.assertEqual(self._au.get_type(), 'audio/basic')
328
329 def test_encoding(self):
330 payload = self._au.get_payload()
331 self.assertEqual(base64.decodestring(payload), self._audiodata)
332
333 def checkSetMinor(self):
334 au = MIMEAudio(self._audiodata, 'fish')
335 self.assertEqual(im.get_type(), 'audio/fish')
336
337 def test_custom_encoder(self):
338 eq = self.assertEqual
339 def encoder(msg):
340 orig = msg.get_payload()
341 msg.set_payload(0)
342 msg['Content-Transfer-Encoding'] = 'broken64'
343 au = MIMEAudio(self._audiodata, _encoder=encoder)
344 eq(au.get_payload(), 0)
345 eq(au['content-transfer-encoding'], 'broken64')
346
347 def test_add_header(self):
348 eq = self.assertEqual
349 unless = self.failUnless
350 self._au.add_header('Content-Disposition', 'attachment',
351 filename='audiotest.au')
352 eq(self._au['content-disposition'],
353 'attachment; filename="audiotest.au"')
354 eq(self._au.get_params(header='content-disposition'),
355 [('attachment', ''), ('filename', 'audiotest.au')])
356 eq(self._au.get_param('filename', header='content-disposition'),
357 'audiotest.au')
358 missing = []
359 eq(self._au.get_param('attachment', header='content-disposition'), '')
360 unless(self._au.get_param('foo', failobj=missing,
361 header='content-disposition') is missing)
362 # Try some missing stuff
363 unless(self._au.get_param('foobar', missing) is missing)
364 unless(self._au.get_param('attachment', missing,
365 header='foobar') is missing)
366
367
368
Barry Warsaw65279d02001-09-26 05:47:08 +0000369# Test the basic MIMEImage class
370class TestMIMEImage(unittest.TestCase):
Barry Warsaw41075852001-09-23 03:18:13 +0000371 def setUp(self):
372 fp = openfile('PyBanner048.gif')
373 try:
374 self._imgdata = fp.read()
375 finally:
376 fp.close()
Barry Warsaw65279d02001-09-26 05:47:08 +0000377 self._im = MIMEImage(self._imgdata)
Barry Warsaw41075852001-09-23 03:18:13 +0000378
379 def test_guess_minor_type(self):
380 self.assertEqual(self._im.get_type(), 'image/gif')
381
382 def test_encoding(self):
383 payload = self._im.get_payload()
384 self.assertEqual(base64.decodestring(payload), self._imgdata)
385
386 def checkSetMinor(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000387 im = MIMEImage(self._imgdata, 'fish')
Barry Warsaw41075852001-09-23 03:18:13 +0000388 self.assertEqual(im.get_type(), 'image/fish')
389
390 def test_custom_encoder(self):
391 eq = self.assertEqual
392 def encoder(msg):
393 orig = msg.get_payload()
394 msg.set_payload(0)
395 msg['Content-Transfer-Encoding'] = 'broken64'
Barry Warsaw65279d02001-09-26 05:47:08 +0000396 im = MIMEImage(self._imgdata, _encoder=encoder)
Barry Warsaw41075852001-09-23 03:18:13 +0000397 eq(im.get_payload(), 0)
398 eq(im['content-transfer-encoding'], 'broken64')
399
400 def test_add_header(self):
401 eq = self.assertEqual
402 unless = self.failUnless
403 self._im.add_header('Content-Disposition', 'attachment',
404 filename='dingusfish.gif')
405 eq(self._im['content-disposition'],
406 'attachment; filename="dingusfish.gif"')
407 eq(self._im.get_params(header='content-disposition'),
Barry Warsaw65279d02001-09-26 05:47:08 +0000408 [('attachment', ''), ('filename', 'dingusfish.gif')])
Barry Warsaw41075852001-09-23 03:18:13 +0000409 eq(self._im.get_param('filename', header='content-disposition'),
410 'dingusfish.gif')
411 missing = []
Barry Warsaw65279d02001-09-26 05:47:08 +0000412 eq(self._im.get_param('attachment', header='content-disposition'), '')
413 unless(self._im.get_param('foo', failobj=missing,
Barry Warsaw41075852001-09-23 03:18:13 +0000414 header='content-disposition') is missing)
415 # Try some missing stuff
416 unless(self._im.get_param('foobar', missing) is missing)
417 unless(self._im.get_param('attachment', missing,
418 header='foobar') is missing)
419
420
Barry Warsaw08a534d2001-10-04 17:58:50 +0000421
Barry Warsaw65279d02001-09-26 05:47:08 +0000422# Test the basic MIMEText class
423class TestMIMEText(unittest.TestCase):
Barry Warsaw41075852001-09-23 03:18:13 +0000424 def setUp(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000425 self._msg = MIMEText('hello there')
Barry Warsaw41075852001-09-23 03:18:13 +0000426
427 def test_types(self):
428 eq = self.assertEqual
429 unless = self.failUnless
430 eq(self._msg.get_type(), 'text/plain')
431 eq(self._msg.get_param('charset'), 'us-ascii')
432 missing = []
433 unless(self._msg.get_param('foobar', missing) is missing)
434 unless(self._msg.get_param('charset', missing, header='foobar')
435 is missing)
436
437 def test_payload(self):
438 self.assertEqual(self._msg.get_payload(), 'hello there\n')
439 self.failUnless(not self._msg.is_multipart())
440
441
Barry Warsaw08a534d2001-10-04 17:58:50 +0000442
443# Test a more complicated multipart/mixed type message
Barry Warsaw41075852001-09-23 03:18:13 +0000444class TestMultipartMixed(unittest.TestCase):
445 def setUp(self):
446 fp = openfile('PyBanner048.gif')
447 try:
448 data = fp.read()
449 finally:
450 fp.close()
451
452 container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
Barry Warsaw65279d02001-09-26 05:47:08 +0000453 image = MIMEImage(data, name='dingusfish.gif')
Barry Warsaw41075852001-09-23 03:18:13 +0000454 image.add_header('content-disposition', 'attachment',
455 filename='dingusfish.gif')
Barry Warsaw65279d02001-09-26 05:47:08 +0000456 intro = MIMEText('''\
Barry Warsaw41075852001-09-23 03:18:13 +0000457Hi there,
458
459This is the dingus fish.
460''')
461 container.add_payload(intro)
462 container.add_payload(image)
463 container['From'] = 'Barry <barry@digicool.com>'
464 container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>'
465 container['Subject'] = 'Here is your dingus fish'
Tim Peters527e64f2001-10-04 05:36:56 +0000466
Barry Warsaw41075852001-09-23 03:18:13 +0000467 now = 987809702.54848599
468 timetuple = time.localtime(now)
469 if timetuple[-1] == 0:
470 tzsecs = time.timezone
471 else:
472 tzsecs = time.altzone
473 if tzsecs > 0:
474 sign = '-'
475 else:
476 sign = '+'
477 tzoffset = ' %s%04d' % (sign, tzsecs / 36)
478 container['Date'] = time.strftime(
479 '%a, %d %b %Y %H:%M:%S',
480 time.localtime(now)) + tzoffset
481 self._msg = container
482 self._im = image
483 self._txt = intro
484
485 def test_hierarchy(self):
486 # convenience
487 eq = self.assertEqual
488 unless = self.failUnless
489 raises = self.assertRaises
490 # tests
491 m = self._msg
492 unless(m.is_multipart())
493 eq(m.get_type(), 'multipart/mixed')
494 eq(len(m.get_payload()), 2)
495 raises(IndexError, m.get_payload, 2)
496 m0 = m.get_payload(0)
497 m1 = m.get_payload(1)
498 unless(m0 is self._txt)
499 unless(m1 is self._im)
500 eq(m.get_payload(), [m0, m1])
501 unless(not m0.is_multipart())
502 unless(not m1.is_multipart())
503
504
Barry Warsaw08a534d2001-10-04 17:58:50 +0000505
506# Test some badly formatted messages
Barry Warsaw41075852001-09-23 03:18:13 +0000507class TestNonConformant(TestEmailBase):
508 def test_parse_missing_minor_type(self):
509 eq = self.assertEqual
510 msg = self._msgobj('msg_14.txt')
511 eq(msg.get_type(), 'text')
512 eq(msg.get_main_type(), 'text')
513 self.failUnless(msg.get_subtype() is None)
514
515 def test_bogus_boundary(self):
516 fp = openfile('msg_15.txt')
517 try:
518 data = fp.read()
519 finally:
520 fp.close()
521 p = Parser()
522 # Note, under a future non-strict parsing mode, this would parse the
523 # message into the intended message tree.
524 self.assertRaises(Errors.BoundaryError, p.parsestr, data)
525
526
Barry Warsaw08a534d2001-10-04 17:58:50 +0000527
528# Test RFC 2047 header encoding and decoding
Barry Warsaw41075852001-09-23 03:18:13 +0000529class TestRFC2047(unittest.TestCase):
530 def test_iso_8859_1(self):
531 eq = self.assertEqual
532 s = '=?iso-8859-1?q?this=20is=20some=20text?='
533 eq(Utils.decode(s), 'this is some text')
534 s = '=?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?='
535 eq(Utils.decode(s), u'Keld_J\xf8rn_Simonsen')
536 s = '=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=' \
537 '=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?='
538 eq(Utils.decode(s), 'If you can read this you understand the example.')
539 s = '=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?='
540 eq(Utils.decode(s),
541 u'\u05dd\u05d5\u05dc\u05e9 \u05df\u05d1 \u05d9\u05dc\u05d8\u05e4\u05e0')
542 s = '=?iso-8859-1?q?this=20is?= =?iso-8859-1?q?some=20text?='
543 eq(Utils.decode(s), u'this is some text')
544
545 def test_encode_header(self):
546 eq = self.assertEqual
547 s = 'this is some text'
548 eq(Utils.encode(s), '=?iso-8859-1?q?this=20is=20some=20text?=')
549 s = 'Keld_J\xf8rn_Simonsen'
550 eq(Utils.encode(s), '=?iso-8859-1?q?Keld_J=F8rn_Simonsen?=')
551 s1 = 'If you can read this yo'
552 s2 = 'u understand the example.'
553 eq(Utils.encode(s1, encoding='b'),
554 '=?iso-8859-1?b?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=')
555 eq(Utils.encode(s2, charset='iso-8859-2', encoding='b'),
556 '=?iso-8859-2?b?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=')
557
558
Barry Warsaw08a534d2001-10-04 17:58:50 +0000559
560# Test the MIMEMessage class
Barry Warsaw65279d02001-09-26 05:47:08 +0000561class TestMIMEMessage(TestEmailBase):
Barry Warsaw41075852001-09-23 03:18:13 +0000562 def setUp(self):
563 fp = openfile('msg_11.txt')
564 self._text = fp.read()
565 fp.close()
566
567 def test_type_error(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000568 self.assertRaises(TypeError, MIMEMessage, 'a plain string')
Barry Warsaw41075852001-09-23 03:18:13 +0000569
570 def test_valid_argument(self):
571 eq = self.assertEqual
572 subject = 'A sub-message'
573 m = Message()
574 m['Subject'] = subject
Barry Warsaw65279d02001-09-26 05:47:08 +0000575 r = MIMEMessage(m)
Barry Warsaw41075852001-09-23 03:18:13 +0000576 eq(r.get_type(), 'message/rfc822')
577 self.failUnless(r.get_payload() is m)
578 eq(r.get_payload()['subject'], subject)
579
580 def test_generate(self):
581 # First craft the message to be encapsulated
582 m = Message()
583 m['Subject'] = 'An enclosed message'
584 m.add_payload('Here is the body of the message.\n')
Barry Warsaw65279d02001-09-26 05:47:08 +0000585 r = MIMEMessage(m)
Barry Warsaw41075852001-09-23 03:18:13 +0000586 r['Subject'] = 'The enclosing message'
587 s = StringIO()
588 g = Generator(s)
589 g(r)
Barry Warsaw65279d02001-09-26 05:47:08 +0000590 self.assertEqual(s.getvalue(), """\
591Content-Type: message/rfc822
592MIME-Version: 1.0
593Subject: The enclosing message
594
595Subject: An enclosed message
596
597Here is the body of the message.
598""")
599
600 def test_parse_message_rfc822(self):
601 eq = self.assertEqual
602 msg = self._msgobj('msg_11.txt')
603 eq(msg.get_type(), 'message/rfc822')
604 eq(len(msg.get_payload()), 1)
605 submsg = msg.get_payload()
606 self.failUnless(isinstance(submsg, Message))
607 eq(submsg['subject'], 'An enclosed message')
608 eq(submsg.get_payload(), 'Here is the body of the message.\n')
609
610 def test_dsn(self):
611 eq = self.assertEqual
612 unless = self.failUnless
613 # msg 16 is a Delivery Status Notification, see RFC XXXX
614 msg = self._msgobj('msg_16.txt')
615 eq(msg.get_type(), 'multipart/report')
616 unless(msg.is_multipart())
617 eq(len(msg.get_payload()), 3)
618 # Subpart 1 is a text/plain, human readable section
619 subpart = msg.get_payload(0)
620 eq(subpart.get_type(), 'text/plain')
621 eq(subpart.get_payload(), """\
622This report relates to a message you sent with the following header fields:
623
624 Message-id: <002001c144a6$8752e060$56104586@oxy.edu>
625 Date: Sun, 23 Sep 2001 20:10:55 -0700
626 From: "Ian T. Henry" <henryi@oxy.edu>
627 To: SoCal Raves <scr@socal-raves.org>
628 Subject: [scr] yeah for Ians!!
629
630Your message cannot be delivered to the following recipients:
631
632 Recipient address: jangel1@cougar.noc.ucla.edu
633 Reason: recipient reached disk quota
634
635""")
636 # Subpart 2 contains the machine parsable DSN information. It
637 # consists of two blocks of headers, represented by two nested Message
638 # objects.
639 subpart = msg.get_payload(1)
640 eq(subpart.get_type(), 'message/delivery-status')
641 eq(len(subpart.get_payload()), 2)
642 # message/delivery-status should treat each block as a bunch of
643 # headers, i.e. a bunch of Message objects.
644 dsn1 = subpart.get_payload(0)
645 unless(isinstance(dsn1, Message))
646 eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu')
647 eq(dsn1.get_param('dns', header='reporting-mta'), '')
648 # Try a missing one <wink>
649 eq(dsn1.get_param('nsd', header='reporting-mta'), None)
650 dsn2 = subpart.get_payload(1)
651 unless(isinstance(dsn2, Message))
652 eq(dsn2['action'], 'failed')
653 eq(dsn2.get_params(header='original-recipient'),
654 [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')])
655 eq(dsn2.get_param('rfc822', header='final-recipient'), '')
656 # Subpart 3 is the original message
657 subpart = msg.get_payload(2)
658 eq(subpart.get_type(), 'message/rfc822')
659 subsubpart = subpart.get_payload()
660 unless(isinstance(subsubpart, Message))
661 eq(subsubpart.get_type(), 'text/plain')
662 eq(subsubpart['message-id'],
663 '<002001c144a6$8752e060$56104586@oxy.edu>')
Barry Warsaw41075852001-09-23 03:18:13 +0000664
665
Barry Warsaw08a534d2001-10-04 17:58:50 +0000666
667# A general test of parser->model->generator idempotency. IOW, read a message
668# in, parse it into a message object tree, then without touching the tree,
669# regenerate the plain text. The original text and the transformed text
670# should be identical. Note: that we ignore the Unix-From since that may
671# contain a changed date.
Barry Warsaw41075852001-09-23 03:18:13 +0000672class TestIdempotent(unittest.TestCase):
673 def _msgobj(self, filename):
674 fp = openfile(filename)
675 try:
676 data = fp.read()
677 finally:
678 fp.close()
Barry Warsaw65279d02001-09-26 05:47:08 +0000679 msg = email.message_from_string(data)
680 return msg, data
Barry Warsaw41075852001-09-23 03:18:13 +0000681
682 def _idempotent(self, msg, text):
683 eq = self.assertEquals
684 s = StringIO()
685 g = Generator(s, maxheaderlen=0)
686 g(msg)
687 eq(text, s.getvalue())
688
689 def test_parse_text_message(self):
690 eq = self.assertEquals
691 msg, text = self._msgobj('msg_01.txt')
692 eq(msg.get_type(), 'text/plain')
693 eq(msg.get_main_type(), 'text')
694 eq(msg.get_subtype(), 'plain')
Barry Warsaw65279d02001-09-26 05:47:08 +0000695 eq(msg.get_params()[1], ('charset', 'us-ascii'))
Barry Warsaw41075852001-09-23 03:18:13 +0000696 eq(msg.get_param('charset'), 'us-ascii')
697 eq(msg.preamble, None)
698 eq(msg.epilogue, None)
699 self._idempotent(msg, text)
700
701 def test_parse_untyped_message(self):
702 eq = self.assertEquals
703 msg, text = self._msgobj('msg_03.txt')
704 eq(msg.get_type(), None)
705 eq(msg.get_params(), None)
706 eq(msg.get_param('charset'), None)
707 self._idempotent(msg, text)
708
709 def test_simple_multipart(self):
710 msg, text = self._msgobj('msg_04.txt')
711 self._idempotent(msg, text)
712
713 def test_MIME_digest(self):
714 msg, text = self._msgobj('msg_02.txt')
715 self._idempotent(msg, text)
716
717 def test_mixed_with_image(self):
718 msg, text = self._msgobj('msg_06.txt')
719 self._idempotent(msg, text)
Tim Peters527e64f2001-10-04 05:36:56 +0000720
Barry Warsaw41075852001-09-23 03:18:13 +0000721 def test_multipart_report(self):
722 msg, text = self._msgobj('msg_05.txt')
723 self._idempotent(msg, text)
Barry Warsaw65279d02001-09-26 05:47:08 +0000724
725 def test_dsn(self):
726 msg, text = self._msgobj('msg_16.txt')
727 self._idempotent(msg, text)
Tim Peters527e64f2001-10-04 05:36:56 +0000728
Barry Warsaw41075852001-09-23 03:18:13 +0000729 def test_content_type(self):
730 eq = self.assertEquals
731 # Get a message object and reset the seek pointer for other tests
732 msg, text = self._msgobj('msg_05.txt')
733 eq(msg.get_type(), 'multipart/report')
734 # Test the Content-Type: parameters
735 params = {}
Barry Warsaw65279d02001-09-26 05:47:08 +0000736 for pk, pv in msg.get_params():
Barry Warsaw41075852001-09-23 03:18:13 +0000737 params[pk] = pv
738 eq(params['report-type'], 'delivery-status')
Barry Warsaw65279d02001-09-26 05:47:08 +0000739 eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
Barry Warsaw41075852001-09-23 03:18:13 +0000740 eq(msg.preamble, 'This is a MIME-encapsulated message.\n\n')
741 eq(msg.epilogue, '\n\n')
742 eq(len(msg.get_payload()), 3)
743 # Make sure the subparts are what we expect
744 msg1 = msg.get_payload(0)
745 eq(msg1.get_type(), 'text/plain')
746 eq(msg1.get_payload(), 'Yadda yadda yadda\n')
747 msg2 = msg.get_payload(1)
748 eq(msg2.get_type(), None)
749 eq(msg2.get_payload(), 'Yadda yadda yadda\n')
750 msg3 = msg.get_payload(2)
751 eq(msg3.get_type(), 'message/rfc822')
752 self.failUnless(isinstance(msg3, Message))
753 msg4 = msg3.get_payload()
754 self.failUnless(isinstance(msg4, Message))
755 eq(msg4.get_payload(), 'Yadda yadda yadda\n')
756
757 def test_parser(self):
758 eq = self.assertEquals
759 msg, text = self._msgobj('msg_06.txt')
760 # Check some of the outer headers
761 eq(msg.get_type(), 'message/rfc822')
762 # Make sure there's exactly one thing in the payload and that's a
763 # sub-Message object of type text/plain
764 msg1 = msg.get_payload()
765 self.failUnless(isinstance(msg1, Message))
766 eq(msg1.get_type(), 'text/plain')
767 self.failUnless(isinstance(msg1.get_payload(), StringType))
768 eq(msg1.get_payload(), '\n')
Barry Warsaw41075852001-09-23 03:18:13 +0000769
Tim Peters527e64f2001-10-04 05:36:56 +0000770
Barry Warsaw08a534d2001-10-04 17:58:50 +0000771
772# Test various other bits of the package's functionality
Barry Warsaw41075852001-09-23 03:18:13 +0000773class TestMiscellaneous(unittest.TestCase):
774 def test_message_from_string(self):
775 fp = openfile('msg_01.txt')
776 try:
777 text = fp.read()
778 finally:
779 fp.close()
780 msg = email.message_from_string(text)
781 s = StringIO()
782 # Don't wrap/continue long headers since we're trying to test
783 # idempotency.
784 g = Generator(s, maxheaderlen=0)
785 g(msg)
786 self.assertEqual(text, s.getvalue())
787
788 def test_message_from_file(self):
789 fp = openfile('msg_01.txt')
790 try:
791 text = fp.read()
792 fp.seek(0)
793 msg = email.message_from_file(fp)
794 s = StringIO()
795 # Don't wrap/continue long headers since we're trying to test
796 # idempotency.
797 g = Generator(s, maxheaderlen=0)
798 g(msg)
799 self.assertEqual(text, s.getvalue())
800 finally:
801 fp.close()
802
803 def test_message_from_string_with_class(self):
804 unless = self.failUnless
805 fp = openfile('msg_01.txt')
806 try:
807 text = fp.read()
808 finally:
809 fp.close()
810 # Create a subclass
811 class MyMessage(Message):
812 pass
Tim Peters527e64f2001-10-04 05:36:56 +0000813
Barry Warsaw41075852001-09-23 03:18:13 +0000814 msg = email.message_from_string(text, MyMessage)
815 unless(isinstance(msg, MyMessage))
816 # Try something more complicated
817 fp = openfile('msg_02.txt')
818 try:
819 text = fp.read()
820 finally:
821 fp.close()
822 msg = email.message_from_string(text, MyMessage)
823 for subpart in msg.walk():
824 unless(isinstance(subpart, MyMessage))
825
Barry Warsaw41075852001-09-23 03:18:13 +0000826 def test_message_from_file_with_class(self):
827 unless = self.failUnless
828 # Create a subclass
829 class MyMessage(Message):
830 pass
Tim Peters527e64f2001-10-04 05:36:56 +0000831
Barry Warsaw41075852001-09-23 03:18:13 +0000832 fp = openfile('msg_01.txt')
833 try:
834 msg = email.message_from_file(fp, MyMessage)
835 finally:
836 fp.close()
837 unless(isinstance(msg, MyMessage))
838 # Try something more complicated
839 fp = openfile('msg_02.txt')
840 try:
841 msg = email.message_from_file(fp, MyMessage)
842 finally:
843 fp.close()
844 for subpart in msg.walk():
845 unless(isinstance(subpart, MyMessage))
846
Barry Warsawfee435a2001-10-09 19:23:57 +0000847 def test__all__(self):
848 module = __import__('email')
849 all = module.__all__
850 all.sort()
851 self.assertEqual(all, ['Encoders', 'Errors', 'Generator', 'Iterators',
852 'MIMEAudio', 'MIMEBase', 'MIMEImage',
853 'MIMEMessage', 'MIMEText', 'Message', 'Parser',
854 'Utils',
855 'message_from_file', 'message_from_string'])
856
Barry Warsaw41075852001-09-23 03:18:13 +0000857
Barry Warsaw08a534d2001-10-04 17:58:50 +0000858
859# Test the iterator/generators
Barry Warsaw41075852001-09-23 03:18:13 +0000860class TestIterators(TestEmailBase):
861 def test_body_line_iterator(self):
862 eq = self.assertEqual
863 # First a simple non-multipart message
864 msg = self._msgobj('msg_01.txt')
865 it = Iterators.body_line_iterator(msg)
Barry Warsawd1de6ea2001-10-04 18:18:37 +0000866 lines = list(it)
Barry Warsaw41075852001-09-23 03:18:13 +0000867 eq(len(lines), 6)
868 eq(EMPTYSTRING.join(lines), msg.get_payload())
869 # Now a more complicated multipart
870 msg = self._msgobj('msg_02.txt')
871 it = Iterators.body_line_iterator(msg)
Barry Warsawd1de6ea2001-10-04 18:18:37 +0000872 lines = list(it)
Barry Warsaw41075852001-09-23 03:18:13 +0000873 eq(len(lines), 43)
Barry Warsaw08a534d2001-10-04 17:58:50 +0000874 eq(EMPTYSTRING.join(lines), openfile('msg_19.txt').read())
Barry Warsaw41075852001-09-23 03:18:13 +0000875
876 def test_typed_subpart_iterator(self):
877 eq = self.assertEqual
878 msg = self._msgobj('msg_04.txt')
879 it = Iterators.typed_subpart_iterator(msg, 'text')
Barry Warsawd1de6ea2001-10-04 18:18:37 +0000880 lines = [subpart.get_payload() for subpart in it]
881 eq(len(lines), 2)
Barry Warsaw41075852001-09-23 03:18:13 +0000882 eq(EMPTYSTRING.join(lines), """\
883a simple kind of mirror
884to reflect upon our own
885a simple kind of mirror
886to reflect upon our own
887""")
888
889
Barry Warsaw08a534d2001-10-04 17:58:50 +0000890
Barry Warsawbf7a59d2001-10-11 15:44:50 +0000891class TestParsers(unittest.TestCase):
892 def test_header_parser(self):
893 eq = self.assertEqual
894 # Parse only the headers of a complex multipart MIME document
895 p = HeaderParser()
896 fp = openfile('msg_02.txt')
897 msg = p.parse(fp)
898 eq(msg['from'], 'ppp-request@zzz.org')
899 eq(msg['to'], 'ppp@zzz.org')
900 eq(msg.get_type(), 'multipart/mixed')
901 eq(msg.is_multipart(), 0)
902 self.failUnless(isinstance(msg.get_payload(), StringType))
903
904
905
Barry Warsaw41075852001-09-23 03:18:13 +0000906def suite():
907 suite = unittest.TestSuite()
908 suite.addTest(unittest.makeSuite(TestMessageAPI))
909 suite.addTest(unittest.makeSuite(TestEncoders))
910 suite.addTest(unittest.makeSuite(TestLongHeaders))
911 suite.addTest(unittest.makeSuite(TestFromMangling))
Barry Warsawfee435a2001-10-09 19:23:57 +0000912 suite.addTest(unittest.makeSuite(TestMIMEAudio))
Barry Warsaw65279d02001-09-26 05:47:08 +0000913 suite.addTest(unittest.makeSuite(TestMIMEImage))
914 suite.addTest(unittest.makeSuite(TestMIMEText))
Barry Warsaw41075852001-09-23 03:18:13 +0000915 suite.addTest(unittest.makeSuite(TestMultipartMixed))
916 suite.addTest(unittest.makeSuite(TestNonConformant))
917 suite.addTest(unittest.makeSuite(TestRFC2047))
Barry Warsaw65279d02001-09-26 05:47:08 +0000918 suite.addTest(unittest.makeSuite(TestMIMEMessage))
Barry Warsaw41075852001-09-23 03:18:13 +0000919 suite.addTest(unittest.makeSuite(TestIdempotent))
920 suite.addTest(unittest.makeSuite(TestMiscellaneous))
921 suite.addTest(unittest.makeSuite(TestIterators))
Barry Warsawbf7a59d2001-10-11 15:44:50 +0000922 suite.addTest(unittest.makeSuite(TestParsers))
Barry Warsaw41075852001-09-23 03:18:13 +0000923 return suite
924
925
Barry Warsaw08a534d2001-10-04 17:58:50 +0000926
Barry Warsaw41075852001-09-23 03:18:13 +0000927if __name__ == '__main__':
928 unittest.main(defaultTest='suite')
929else:
930 from test_support import run_suite
931 run_suite(suite())