blob: 0a6b8083a880ebbb90843d9509ed4718c454effe [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
13from email.Parser import Parser
14from email.Generator import Generator, DecodedGenerator
15from email.Message import Message
Barry Warsaw65279d02001-09-26 05:47:08 +000016from email.MIMEText import MIMEText
17from email.MIMEImage import MIMEImage
Barry Warsaw41075852001-09-23 03:18:13 +000018from email.MIMEBase import MIMEBase
Barry Warsaw65279d02001-09-26 05:47:08 +000019from email.MIMEMessage import MIMEMessage
Barry Warsaw41075852001-09-23 03:18:13 +000020from email import Utils
21from email import Errors
22from email import Encoders
23from email import Iterators
24
25import test.regrtest
26
27NL = '\n'
28EMPTYSTRING = ''
29
30
Barry Warsaw08a534d2001-10-04 17:58:50 +000031
Barry Warsaw41075852001-09-23 03:18:13 +000032def openfile(filename):
33 path = os.path.join(os.path.dirname(test.regrtest.__file__),
34 'data', filename)
35 return open(path)
36
37
Barry Warsaw08a534d2001-10-04 17:58:50 +000038
Barry Warsaw41075852001-09-23 03:18:13 +000039# Base test class
40class TestEmailBase(unittest.TestCase):
41 def _msgobj(self, filename):
42 fp = openfile(filename)
43 try:
Barry Warsaw65279d02001-09-26 05:47:08 +000044 msg = email.message_from_file(fp)
Barry Warsaw41075852001-09-23 03:18:13 +000045 finally:
46 fp.close()
Barry Warsaw65279d02001-09-26 05:47:08 +000047 return msg
Barry Warsaw41075852001-09-23 03:18:13 +000048
49
Barry Warsaw08a534d2001-10-04 17:58:50 +000050
Barry Warsaw41075852001-09-23 03:18:13 +000051# Test various aspects of the Message class's API
52class TestMessageAPI(TestEmailBase):
53 def test_get_charsets(self):
54 eq = self.assertEqual
Tim Peters527e64f2001-10-04 05:36:56 +000055
Barry Warsaw65279d02001-09-26 05:47:08 +000056 msg = self._msgobj('msg_08.txt')
57 charsets = msg.get_charsets()
58 eq(charsets, [None, 'us-ascii', 'iso-8859-1', 'iso-8859-2', 'koi8-r'])
Barry Warsaw41075852001-09-23 03:18:13 +000059
Barry Warsaw65279d02001-09-26 05:47:08 +000060 msg = self._msgobj('msg_09.txt')
61 charsets = msg.get_charsets('dingbat')
62 eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat',
63 'koi8-r'])
Barry Warsaw41075852001-09-23 03:18:13 +000064
Barry Warsaw65279d02001-09-26 05:47:08 +000065 msg = self._msgobj('msg_12.txt')
66 charsets = msg.get_charsets()
67 eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2',
68 'iso-8859-3', 'us-ascii', 'koi8-r'])
Barry Warsaw41075852001-09-23 03:18:13 +000069
70 def test_get_filename(self):
71 eq = self.assertEqual
72
Barry Warsaw65279d02001-09-26 05:47:08 +000073 msg = self._msgobj('msg_04.txt')
74 filenames = [p.get_filename() for p in msg.get_payload()]
Barry Warsaw41075852001-09-23 03:18:13 +000075 eq(filenames, ['msg.txt', 'msg.txt'])
76
Barry Warsaw65279d02001-09-26 05:47:08 +000077 msg = self._msgobj('msg_07.txt')
78 subpart = msg.get_payload(1)
Barry Warsaw41075852001-09-23 03:18:13 +000079 eq(subpart.get_filename(), 'dingusfish.gif')
80
81 def test_get_boundary(self):
82 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +000083 msg = self._msgobj('msg_07.txt')
Barry Warsaw41075852001-09-23 03:18:13 +000084 # No quotes!
Barry Warsaw65279d02001-09-26 05:47:08 +000085 eq(msg.get_boundary(), 'BOUNDARY')
Barry Warsaw41075852001-09-23 03:18:13 +000086
87 def test_set_boundary(self):
88 eq = self.assertEqual
89 # This one has no existing boundary parameter, but the Content-Type:
90 # header appears fifth.
Barry Warsaw65279d02001-09-26 05:47:08 +000091 msg = self._msgobj('msg_01.txt')
92 msg.set_boundary('BOUNDARY')
93 header, value = msg.items()[4]
Barry Warsaw41075852001-09-23 03:18:13 +000094 eq(header.lower(), 'content-type')
95 eq(value, 'text/plain; charset=us-ascii; boundary="BOUNDARY"')
96 # This one has a Content-Type: header, with a boundary, stuck in the
97 # middle of its headers. Make sure the order is preserved; it should
98 # be fifth.
Barry Warsaw65279d02001-09-26 05:47:08 +000099 msg = self._msgobj('msg_04.txt')
100 msg.set_boundary('BOUNDARY')
101 header, value = msg.items()[4]
Barry Warsaw41075852001-09-23 03:18:13 +0000102 eq(header.lower(), 'content-type')
103 eq(value, 'multipart/mixed; boundary="BOUNDARY"')
104 # And this one has no Content-Type: header at all.
Barry Warsaw65279d02001-09-26 05:47:08 +0000105 msg = self._msgobj('msg_03.txt')
Barry Warsaw41075852001-09-23 03:18:13 +0000106 self.assertRaises(Errors.HeaderParseError,
Barry Warsaw65279d02001-09-26 05:47:08 +0000107 msg.set_boundary, 'BOUNDARY')
Barry Warsaw41075852001-09-23 03:18:13 +0000108
109 def test_get_decoded_payload(self):
110 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000111 msg = self._msgobj('msg_10.txt')
Barry Warsaw41075852001-09-23 03:18:13 +0000112 # The outer message is a multipart
Barry Warsaw65279d02001-09-26 05:47:08 +0000113 eq(msg.get_payload(decode=1), None)
Barry Warsaw41075852001-09-23 03:18:13 +0000114 # Subpart 1 is 7bit encoded
Barry Warsaw65279d02001-09-26 05:47:08 +0000115 eq(msg.get_payload(0).get_payload(decode=1),
Barry Warsaw41075852001-09-23 03:18:13 +0000116 'This is a 7bit encoded message.\n')
117 # Subpart 2 is quopri
Barry Warsaw65279d02001-09-26 05:47:08 +0000118 eq(msg.get_payload(1).get_payload(decode=1),
Barry Warsaw41075852001-09-23 03:18:13 +0000119 '\xa1This is a Quoted Printable encoded message!\n')
120 # Subpart 3 is base64
Barry Warsaw65279d02001-09-26 05:47:08 +0000121 eq(msg.get_payload(2).get_payload(decode=1),
Barry Warsaw41075852001-09-23 03:18:13 +0000122 'This is a Base64 encoded message.')
123 # Subpart 4 has no Content-Transfer-Encoding: header.
Barry Warsaw65279d02001-09-26 05:47:08 +0000124 eq(msg.get_payload(3).get_payload(decode=1),
Barry Warsaw41075852001-09-23 03:18:13 +0000125 'This has no Content-Transfer-Encoding: header.\n')
126
127 def test_decoded_generator(self):
128 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000129 msg = self._msgobj('msg_07.txt')
130 fp = openfile('msg_17.txt')
131 try:
132 text = fp.read()
133 finally:
134 fp.close()
Barry Warsaw41075852001-09-23 03:18:13 +0000135 s = StringIO()
136 g = DecodedGenerator(s)
Barry Warsaw65279d02001-09-26 05:47:08 +0000137 g(msg)
138 eq(s.getvalue(), text)
Barry Warsaw41075852001-09-23 03:18:13 +0000139
140 def test__contains__(self):
141 msg = Message()
142 msg['From'] = 'Me'
143 msg['to'] = 'You'
144 # Check for case insensitivity
145 self.failUnless('from' in msg)
146 self.failUnless('From' in msg)
147 self.failUnless('FROM' in msg)
148 self.failUnless('to' in msg)
149 self.failUnless('To' in msg)
150 self.failUnless('TO' in msg)
151
152 def test_as_string(self):
153 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000154 msg = self._msgobj('msg_01.txt')
Barry Warsaw41075852001-09-23 03:18:13 +0000155 fp = openfile('msg_01.txt')
156 try:
157 text = fp.read()
158 finally:
159 fp.close()
Barry Warsaw65279d02001-09-26 05:47:08 +0000160 eq(text, msg.as_string())
161 fullrepr = str(msg)
Barry Warsaw41075852001-09-23 03:18:13 +0000162 lines = fullrepr.split('\n')
163 self.failUnless(lines[0].startswith('From '))
164 eq(text, NL.join(lines[1:]))
165
166 def test_bad_param(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000167 msg = email.message_from_string("Content-Type: blarg; baz; boo\n")
Barry Warsaw41075852001-09-23 03:18:13 +0000168 self.assertEqual(msg.get_param('baz'), '')
169
170 def test_missing_filename(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000171 msg = email.message_from_string("From: foo\n")
Barry Warsaw41075852001-09-23 03:18:13 +0000172 self.assertEqual(msg.get_filename(), None)
173
174 def test_bogus_filename(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000175 msg = email.message_from_string(
176 "Content-Disposition: blarg; filename\n")
Barry Warsaw41075852001-09-23 03:18:13 +0000177 self.assertEqual(msg.get_filename(), '')
Tim Peters527e64f2001-10-04 05:36:56 +0000178
Barry Warsaw41075852001-09-23 03:18:13 +0000179 def test_missing_boundary(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000180 msg = email.message_from_string("From: foo\n")
Barry Warsaw41075852001-09-23 03:18:13 +0000181 self.assertEqual(msg.get_boundary(), None)
182
Barry Warsaw65279d02001-09-26 05:47:08 +0000183 def test_get_params(self):
184 eq = self.assertEqual
185 msg = email.message_from_string(
186 'X-Header: foo=one; bar=two; baz=three\n')
187 eq(msg.get_params(header='x-header'),
188 [('foo', 'one'), ('bar', 'two'), ('baz', 'three')])
189 msg = email.message_from_string(
190 'X-Header: foo; bar=one; baz=two\n')
191 eq(msg.get_params(header='x-header'),
192 [('foo', ''), ('bar', 'one'), ('baz', 'two')])
193 eq(msg.get_params(), None)
194 msg = email.message_from_string(
195 'X-Header: foo; bar="one"; baz=two\n')
196 eq(msg.get_params(header='x-header'),
197 [('foo', ''), ('bar', 'one'), ('baz', 'two')])
198
199 def test_get_param(self):
200 eq = self.assertEqual
201 msg = email.message_from_string(
202 "X-Header: foo=one; bar=two; baz=three\n")
203 eq(msg.get_param('bar', header='x-header'), 'two')
204 eq(msg.get_param('quuz', header='x-header'), None)
205 eq(msg.get_param('quuz'), None)
206 msg = email.message_from_string(
207 'X-Header: foo; bar="one"; baz=two\n')
208 eq(msg.get_param('foo', header='x-header'), '')
209 eq(msg.get_param('bar', header='x-header'), 'one')
210 eq(msg.get_param('baz', header='x-header'), 'two')
211
212 def test_has_key(self):
213 msg = email.message_from_string('Header: exists')
214 self.failUnless(msg.has_key('header'))
215 self.failUnless(msg.has_key('Header'))
216 self.failUnless(msg.has_key('HEADER'))
217 self.failIf(msg.has_key('headeri'))
218
Barry Warsaw41075852001-09-23 03:18:13 +0000219
Barry Warsaw08a534d2001-10-04 17:58:50 +0000220
Barry Warsaw41075852001-09-23 03:18:13 +0000221# Test the email.Encoders module
222class TestEncoders(unittest.TestCase):
223 def test_encode_noop(self):
224 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000225 msg = MIMEText('hello world', _encoder=Encoders.encode_noop)
Barry Warsaw41075852001-09-23 03:18:13 +0000226 eq(msg.get_payload(), 'hello world\n')
227 eq(msg['content-transfer-encoding'], None)
228
229 def test_encode_7bit(self):
230 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000231 msg = MIMEText('hello world', _encoder=Encoders.encode_7or8bit)
Barry Warsaw41075852001-09-23 03:18:13 +0000232 eq(msg.get_payload(), 'hello world\n')
233 eq(msg['content-transfer-encoding'], '7bit')
Barry Warsaw65279d02001-09-26 05:47:08 +0000234 msg = MIMEText('hello \x7f world', _encoder=Encoders.encode_7or8bit)
Barry Warsaw41075852001-09-23 03:18:13 +0000235 eq(msg.get_payload(), 'hello \x7f world\n')
236 eq(msg['content-transfer-encoding'], '7bit')
237
238 def test_encode_8bit(self):
239 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000240 msg = MIMEText('hello \x80 world', _encoder=Encoders.encode_7or8bit)
Barry Warsaw41075852001-09-23 03:18:13 +0000241 eq(msg.get_payload(), 'hello \x80 world\n')
242 eq(msg['content-transfer-encoding'], '8bit')
243
244 def test_encode_base64(self):
245 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000246 msg = MIMEText('hello world', _encoder=Encoders.encode_base64)
Barry Warsaw41075852001-09-23 03:18:13 +0000247 eq(msg.get_payload(), 'aGVsbG8gd29ybGQK\n')
248 eq(msg['content-transfer-encoding'], 'base64')
249
250 def test_encode_quoted_printable(self):
251 eq = self.assertEqual
Barry Warsaw65279d02001-09-26 05:47:08 +0000252 msg = MIMEText('hello world', _encoder=Encoders.encode_quopri)
Barry Warsaw41075852001-09-23 03:18:13 +0000253 eq(msg.get_payload(), 'hello=20world\n')
254 eq(msg['content-transfer-encoding'], 'quoted-printable')
255
256
Barry Warsaw08a534d2001-10-04 17:58:50 +0000257
258# Test long header wrapping
Barry Warsaw41075852001-09-23 03:18:13 +0000259class TestLongHeaders(unittest.TestCase):
260 def test_header_splitter(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000261 msg = MIMEText('')
Barry Warsaw41075852001-09-23 03:18:13 +0000262 # It'd be great if we could use add_header() here, but that doesn't
263 # guarantee an order of the parameters.
264 msg['X-Foobar-Spoink-Defrobnit'] = (
265 'wasnipoop; giraffes="very-long-necked-animals"; '
266 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"')
267 sfp = StringIO()
268 g = Generator(sfp)
269 g(msg)
Barry Warsaw08a534d2001-10-04 17:58:50 +0000270 self.assertEqual(sfp.getvalue(), openfile('msg_18.txt').read())
Barry Warsaw41075852001-09-23 03:18:13 +0000271
272
Barry Warsaw08a534d2001-10-04 17:58:50 +0000273
274# Test mangling of "From " lines in the body of a message
Barry Warsaw41075852001-09-23 03:18:13 +0000275class TestFromMangling(unittest.TestCase):
276 def setUp(self):
277 self.msg = Message()
278 self.msg['From'] = 'aaa@bbb.org'
279 self.msg.add_payload("""\
280From the desk of A.A.A.:
281Blah blah blah
282""")
283
284 def test_mangled_from(self):
285 s = StringIO()
286 g = Generator(s, mangle_from_=1)
287 g(self.msg)
288 self.assertEqual(s.getvalue(), """\
289From: aaa@bbb.org
290
291>From the desk of A.A.A.:
292Blah blah blah
293""")
294
295 def test_dont_mangle_from(self):
296 s = StringIO()
297 g = Generator(s, mangle_from_=0)
298 g(self.msg)
299 self.assertEqual(s.getvalue(), """\
300From: aaa@bbb.org
301
302From the desk of A.A.A.:
303Blah blah blah
304""")
305
306
Barry Warsaw08a534d2001-10-04 17:58:50 +0000307
Barry Warsaw65279d02001-09-26 05:47:08 +0000308# Test the basic MIMEImage class
309class TestMIMEImage(unittest.TestCase):
Barry Warsaw41075852001-09-23 03:18:13 +0000310 def setUp(self):
311 fp = openfile('PyBanner048.gif')
312 try:
313 self._imgdata = fp.read()
314 finally:
315 fp.close()
Barry Warsaw65279d02001-09-26 05:47:08 +0000316 self._im = MIMEImage(self._imgdata)
Barry Warsaw41075852001-09-23 03:18:13 +0000317
318 def test_guess_minor_type(self):
319 self.assertEqual(self._im.get_type(), 'image/gif')
320
321 def test_encoding(self):
322 payload = self._im.get_payload()
323 self.assertEqual(base64.decodestring(payload), self._imgdata)
324
325 def checkSetMinor(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000326 im = MIMEImage(self._imgdata, 'fish')
Barry Warsaw41075852001-09-23 03:18:13 +0000327 self.assertEqual(im.get_type(), 'image/fish')
328
329 def test_custom_encoder(self):
330 eq = self.assertEqual
331 def encoder(msg):
332 orig = msg.get_payload()
333 msg.set_payload(0)
334 msg['Content-Transfer-Encoding'] = 'broken64'
Barry Warsaw65279d02001-09-26 05:47:08 +0000335 im = MIMEImage(self._imgdata, _encoder=encoder)
Barry Warsaw41075852001-09-23 03:18:13 +0000336 eq(im.get_payload(), 0)
337 eq(im['content-transfer-encoding'], 'broken64')
338
339 def test_add_header(self):
340 eq = self.assertEqual
341 unless = self.failUnless
342 self._im.add_header('Content-Disposition', 'attachment',
343 filename='dingusfish.gif')
344 eq(self._im['content-disposition'],
345 'attachment; filename="dingusfish.gif"')
346 eq(self._im.get_params(header='content-disposition'),
Barry Warsaw65279d02001-09-26 05:47:08 +0000347 [('attachment', ''), ('filename', 'dingusfish.gif')])
Barry Warsaw41075852001-09-23 03:18:13 +0000348 eq(self._im.get_param('filename', header='content-disposition'),
349 'dingusfish.gif')
350 missing = []
Barry Warsaw65279d02001-09-26 05:47:08 +0000351 eq(self._im.get_param('attachment', header='content-disposition'), '')
352 unless(self._im.get_param('foo', failobj=missing,
Barry Warsaw41075852001-09-23 03:18:13 +0000353 header='content-disposition') is missing)
354 # Try some missing stuff
355 unless(self._im.get_param('foobar', missing) is missing)
356 unless(self._im.get_param('attachment', missing,
357 header='foobar') is missing)
358
359
Barry Warsaw08a534d2001-10-04 17:58:50 +0000360
Barry Warsaw65279d02001-09-26 05:47:08 +0000361# Test the basic MIMEText class
362class TestMIMEText(unittest.TestCase):
Barry Warsaw41075852001-09-23 03:18:13 +0000363 def setUp(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000364 self._msg = MIMEText('hello there')
Barry Warsaw41075852001-09-23 03:18:13 +0000365
366 def test_types(self):
367 eq = self.assertEqual
368 unless = self.failUnless
369 eq(self._msg.get_type(), 'text/plain')
370 eq(self._msg.get_param('charset'), 'us-ascii')
371 missing = []
372 unless(self._msg.get_param('foobar', missing) is missing)
373 unless(self._msg.get_param('charset', missing, header='foobar')
374 is missing)
375
376 def test_payload(self):
377 self.assertEqual(self._msg.get_payload(), 'hello there\n')
378 self.failUnless(not self._msg.is_multipart())
379
380
Barry Warsaw08a534d2001-10-04 17:58:50 +0000381
382# Test a more complicated multipart/mixed type message
Barry Warsaw41075852001-09-23 03:18:13 +0000383class TestMultipartMixed(unittest.TestCase):
384 def setUp(self):
385 fp = openfile('PyBanner048.gif')
386 try:
387 data = fp.read()
388 finally:
389 fp.close()
390
391 container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY')
Barry Warsaw65279d02001-09-26 05:47:08 +0000392 image = MIMEImage(data, name='dingusfish.gif')
Barry Warsaw41075852001-09-23 03:18:13 +0000393 image.add_header('content-disposition', 'attachment',
394 filename='dingusfish.gif')
Barry Warsaw65279d02001-09-26 05:47:08 +0000395 intro = MIMEText('''\
Barry Warsaw41075852001-09-23 03:18:13 +0000396Hi there,
397
398This is the dingus fish.
399''')
400 container.add_payload(intro)
401 container.add_payload(image)
402 container['From'] = 'Barry <barry@digicool.com>'
403 container['To'] = 'Dingus Lovers <cravindogs@cravindogs.com>'
404 container['Subject'] = 'Here is your dingus fish'
Tim Peters527e64f2001-10-04 05:36:56 +0000405
Barry Warsaw41075852001-09-23 03:18:13 +0000406 now = 987809702.54848599
407 timetuple = time.localtime(now)
408 if timetuple[-1] == 0:
409 tzsecs = time.timezone
410 else:
411 tzsecs = time.altzone
412 if tzsecs > 0:
413 sign = '-'
414 else:
415 sign = '+'
416 tzoffset = ' %s%04d' % (sign, tzsecs / 36)
417 container['Date'] = time.strftime(
418 '%a, %d %b %Y %H:%M:%S',
419 time.localtime(now)) + tzoffset
420 self._msg = container
421 self._im = image
422 self._txt = intro
423
424 def test_hierarchy(self):
425 # convenience
426 eq = self.assertEqual
427 unless = self.failUnless
428 raises = self.assertRaises
429 # tests
430 m = self._msg
431 unless(m.is_multipart())
432 eq(m.get_type(), 'multipart/mixed')
433 eq(len(m.get_payload()), 2)
434 raises(IndexError, m.get_payload, 2)
435 m0 = m.get_payload(0)
436 m1 = m.get_payload(1)
437 unless(m0 is self._txt)
438 unless(m1 is self._im)
439 eq(m.get_payload(), [m0, m1])
440 unless(not m0.is_multipart())
441 unless(not m1.is_multipart())
442
443
Barry Warsaw08a534d2001-10-04 17:58:50 +0000444
445# Test some badly formatted messages
Barry Warsaw41075852001-09-23 03:18:13 +0000446class TestNonConformant(TestEmailBase):
447 def test_parse_missing_minor_type(self):
448 eq = self.assertEqual
449 msg = self._msgobj('msg_14.txt')
450 eq(msg.get_type(), 'text')
451 eq(msg.get_main_type(), 'text')
452 self.failUnless(msg.get_subtype() is None)
453
454 def test_bogus_boundary(self):
455 fp = openfile('msg_15.txt')
456 try:
457 data = fp.read()
458 finally:
459 fp.close()
460 p = Parser()
461 # Note, under a future non-strict parsing mode, this would parse the
462 # message into the intended message tree.
463 self.assertRaises(Errors.BoundaryError, p.parsestr, data)
464
465
Barry Warsaw08a534d2001-10-04 17:58:50 +0000466
467# Test RFC 2047 header encoding and decoding
Barry Warsaw41075852001-09-23 03:18:13 +0000468class TestRFC2047(unittest.TestCase):
469 def test_iso_8859_1(self):
470 eq = self.assertEqual
471 s = '=?iso-8859-1?q?this=20is=20some=20text?='
472 eq(Utils.decode(s), 'this is some text')
473 s = '=?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?='
474 eq(Utils.decode(s), u'Keld_J\xf8rn_Simonsen')
475 s = '=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=' \
476 '=?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?='
477 eq(Utils.decode(s), 'If you can read this you understand the example.')
478 s = '=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?='
479 eq(Utils.decode(s),
480 u'\u05dd\u05d5\u05dc\u05e9 \u05df\u05d1 \u05d9\u05dc\u05d8\u05e4\u05e0')
481 s = '=?iso-8859-1?q?this=20is?= =?iso-8859-1?q?some=20text?='
482 eq(Utils.decode(s), u'this is some text')
483
484 def test_encode_header(self):
485 eq = self.assertEqual
486 s = 'this is some text'
487 eq(Utils.encode(s), '=?iso-8859-1?q?this=20is=20some=20text?=')
488 s = 'Keld_J\xf8rn_Simonsen'
489 eq(Utils.encode(s), '=?iso-8859-1?q?Keld_J=F8rn_Simonsen?=')
490 s1 = 'If you can read this yo'
491 s2 = 'u understand the example.'
492 eq(Utils.encode(s1, encoding='b'),
493 '=?iso-8859-1?b?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=')
494 eq(Utils.encode(s2, charset='iso-8859-2', encoding='b'),
495 '=?iso-8859-2?b?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=')
496
497
Barry Warsaw08a534d2001-10-04 17:58:50 +0000498
499# Test the MIMEMessage class
Barry Warsaw65279d02001-09-26 05:47:08 +0000500class TestMIMEMessage(TestEmailBase):
Barry Warsaw41075852001-09-23 03:18:13 +0000501 def setUp(self):
502 fp = openfile('msg_11.txt')
503 self._text = fp.read()
504 fp.close()
505
506 def test_type_error(self):
Barry Warsaw65279d02001-09-26 05:47:08 +0000507 self.assertRaises(TypeError, MIMEMessage, 'a plain string')
Barry Warsaw41075852001-09-23 03:18:13 +0000508
509 def test_valid_argument(self):
510 eq = self.assertEqual
511 subject = 'A sub-message'
512 m = Message()
513 m['Subject'] = subject
Barry Warsaw65279d02001-09-26 05:47:08 +0000514 r = MIMEMessage(m)
Barry Warsaw41075852001-09-23 03:18:13 +0000515 eq(r.get_type(), 'message/rfc822')
516 self.failUnless(r.get_payload() is m)
517 eq(r.get_payload()['subject'], subject)
518
519 def test_generate(self):
520 # First craft the message to be encapsulated
521 m = Message()
522 m['Subject'] = 'An enclosed message'
523 m.add_payload('Here is the body of the message.\n')
Barry Warsaw65279d02001-09-26 05:47:08 +0000524 r = MIMEMessage(m)
Barry Warsaw41075852001-09-23 03:18:13 +0000525 r['Subject'] = 'The enclosing message'
526 s = StringIO()
527 g = Generator(s)
528 g(r)
Barry Warsaw65279d02001-09-26 05:47:08 +0000529 self.assertEqual(s.getvalue(), """\
530Content-Type: message/rfc822
531MIME-Version: 1.0
532Subject: The enclosing message
533
534Subject: An enclosed message
535
536Here is the body of the message.
537""")
538
539 def test_parse_message_rfc822(self):
540 eq = self.assertEqual
541 msg = self._msgobj('msg_11.txt')
542 eq(msg.get_type(), 'message/rfc822')
543 eq(len(msg.get_payload()), 1)
544 submsg = msg.get_payload()
545 self.failUnless(isinstance(submsg, Message))
546 eq(submsg['subject'], 'An enclosed message')
547 eq(submsg.get_payload(), 'Here is the body of the message.\n')
548
549 def test_dsn(self):
550 eq = self.assertEqual
551 unless = self.failUnless
552 # msg 16 is a Delivery Status Notification, see RFC XXXX
553 msg = self._msgobj('msg_16.txt')
554 eq(msg.get_type(), 'multipart/report')
555 unless(msg.is_multipart())
556 eq(len(msg.get_payload()), 3)
557 # Subpart 1 is a text/plain, human readable section
558 subpart = msg.get_payload(0)
559 eq(subpart.get_type(), 'text/plain')
560 eq(subpart.get_payload(), """\
561This report relates to a message you sent with the following header fields:
562
563 Message-id: <002001c144a6$8752e060$56104586@oxy.edu>
564 Date: Sun, 23 Sep 2001 20:10:55 -0700
565 From: "Ian T. Henry" <henryi@oxy.edu>
566 To: SoCal Raves <scr@socal-raves.org>
567 Subject: [scr] yeah for Ians!!
568
569Your message cannot be delivered to the following recipients:
570
571 Recipient address: jangel1@cougar.noc.ucla.edu
572 Reason: recipient reached disk quota
573
574""")
575 # Subpart 2 contains the machine parsable DSN information. It
576 # consists of two blocks of headers, represented by two nested Message
577 # objects.
578 subpart = msg.get_payload(1)
579 eq(subpart.get_type(), 'message/delivery-status')
580 eq(len(subpart.get_payload()), 2)
581 # message/delivery-status should treat each block as a bunch of
582 # headers, i.e. a bunch of Message objects.
583 dsn1 = subpart.get_payload(0)
584 unless(isinstance(dsn1, Message))
585 eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu')
586 eq(dsn1.get_param('dns', header='reporting-mta'), '')
587 # Try a missing one <wink>
588 eq(dsn1.get_param('nsd', header='reporting-mta'), None)
589 dsn2 = subpart.get_payload(1)
590 unless(isinstance(dsn2, Message))
591 eq(dsn2['action'], 'failed')
592 eq(dsn2.get_params(header='original-recipient'),
593 [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')])
594 eq(dsn2.get_param('rfc822', header='final-recipient'), '')
595 # Subpart 3 is the original message
596 subpart = msg.get_payload(2)
597 eq(subpart.get_type(), 'message/rfc822')
598 subsubpart = subpart.get_payload()
599 unless(isinstance(subsubpart, Message))
600 eq(subsubpart.get_type(), 'text/plain')
601 eq(subsubpart['message-id'],
602 '<002001c144a6$8752e060$56104586@oxy.edu>')
Barry Warsaw41075852001-09-23 03:18:13 +0000603
604
Barry Warsaw08a534d2001-10-04 17:58:50 +0000605
606# A general test of parser->model->generator idempotency. IOW, read a message
607# in, parse it into a message object tree, then without touching the tree,
608# regenerate the plain text. The original text and the transformed text
609# should be identical. Note: that we ignore the Unix-From since that may
610# contain a changed date.
Barry Warsaw41075852001-09-23 03:18:13 +0000611class TestIdempotent(unittest.TestCase):
612 def _msgobj(self, filename):
613 fp = openfile(filename)
614 try:
615 data = fp.read()
616 finally:
617 fp.close()
Barry Warsaw65279d02001-09-26 05:47:08 +0000618 msg = email.message_from_string(data)
619 return msg, data
Barry Warsaw41075852001-09-23 03:18:13 +0000620
621 def _idempotent(self, msg, text):
622 eq = self.assertEquals
623 s = StringIO()
624 g = Generator(s, maxheaderlen=0)
625 g(msg)
626 eq(text, s.getvalue())
627
628 def test_parse_text_message(self):
629 eq = self.assertEquals
630 msg, text = self._msgobj('msg_01.txt')
631 eq(msg.get_type(), 'text/plain')
632 eq(msg.get_main_type(), 'text')
633 eq(msg.get_subtype(), 'plain')
Barry Warsaw65279d02001-09-26 05:47:08 +0000634 eq(msg.get_params()[1], ('charset', 'us-ascii'))
Barry Warsaw41075852001-09-23 03:18:13 +0000635 eq(msg.get_param('charset'), 'us-ascii')
636 eq(msg.preamble, None)
637 eq(msg.epilogue, None)
638 self._idempotent(msg, text)
639
640 def test_parse_untyped_message(self):
641 eq = self.assertEquals
642 msg, text = self._msgobj('msg_03.txt')
643 eq(msg.get_type(), None)
644 eq(msg.get_params(), None)
645 eq(msg.get_param('charset'), None)
646 self._idempotent(msg, text)
647
648 def test_simple_multipart(self):
649 msg, text = self._msgobj('msg_04.txt')
650 self._idempotent(msg, text)
651
652 def test_MIME_digest(self):
653 msg, text = self._msgobj('msg_02.txt')
654 self._idempotent(msg, text)
655
656 def test_mixed_with_image(self):
657 msg, text = self._msgobj('msg_06.txt')
658 self._idempotent(msg, text)
Tim Peters527e64f2001-10-04 05:36:56 +0000659
Barry Warsaw41075852001-09-23 03:18:13 +0000660 def test_multipart_report(self):
661 msg, text = self._msgobj('msg_05.txt')
662 self._idempotent(msg, text)
Barry Warsaw65279d02001-09-26 05:47:08 +0000663
664 def test_dsn(self):
665 msg, text = self._msgobj('msg_16.txt')
666 self._idempotent(msg, text)
Tim Peters527e64f2001-10-04 05:36:56 +0000667
Barry Warsaw41075852001-09-23 03:18:13 +0000668 def test_content_type(self):
669 eq = self.assertEquals
670 # Get a message object and reset the seek pointer for other tests
671 msg, text = self._msgobj('msg_05.txt')
672 eq(msg.get_type(), 'multipart/report')
673 # Test the Content-Type: parameters
674 params = {}
Barry Warsaw65279d02001-09-26 05:47:08 +0000675 for pk, pv in msg.get_params():
Barry Warsaw41075852001-09-23 03:18:13 +0000676 params[pk] = pv
677 eq(params['report-type'], 'delivery-status')
Barry Warsaw65279d02001-09-26 05:47:08 +0000678 eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com')
Barry Warsaw41075852001-09-23 03:18:13 +0000679 eq(msg.preamble, 'This is a MIME-encapsulated message.\n\n')
680 eq(msg.epilogue, '\n\n')
681 eq(len(msg.get_payload()), 3)
682 # Make sure the subparts are what we expect
683 msg1 = msg.get_payload(0)
684 eq(msg1.get_type(), 'text/plain')
685 eq(msg1.get_payload(), 'Yadda yadda yadda\n')
686 msg2 = msg.get_payload(1)
687 eq(msg2.get_type(), None)
688 eq(msg2.get_payload(), 'Yadda yadda yadda\n')
689 msg3 = msg.get_payload(2)
690 eq(msg3.get_type(), 'message/rfc822')
691 self.failUnless(isinstance(msg3, Message))
692 msg4 = msg3.get_payload()
693 self.failUnless(isinstance(msg4, Message))
694 eq(msg4.get_payload(), 'Yadda yadda yadda\n')
695
696 def test_parser(self):
697 eq = self.assertEquals
698 msg, text = self._msgobj('msg_06.txt')
699 # Check some of the outer headers
700 eq(msg.get_type(), 'message/rfc822')
701 # Make sure there's exactly one thing in the payload and that's a
702 # sub-Message object of type text/plain
703 msg1 = msg.get_payload()
704 self.failUnless(isinstance(msg1, Message))
705 eq(msg1.get_type(), 'text/plain')
706 self.failUnless(isinstance(msg1.get_payload(), StringType))
707 eq(msg1.get_payload(), '\n')
Barry Warsaw41075852001-09-23 03:18:13 +0000708
Tim Peters527e64f2001-10-04 05:36:56 +0000709
Barry Warsaw08a534d2001-10-04 17:58:50 +0000710
711# Test various other bits of the package's functionality
Barry Warsaw41075852001-09-23 03:18:13 +0000712class TestMiscellaneous(unittest.TestCase):
713 def test_message_from_string(self):
714 fp = openfile('msg_01.txt')
715 try:
716 text = fp.read()
717 finally:
718 fp.close()
719 msg = email.message_from_string(text)
720 s = StringIO()
721 # Don't wrap/continue long headers since we're trying to test
722 # idempotency.
723 g = Generator(s, maxheaderlen=0)
724 g(msg)
725 self.assertEqual(text, s.getvalue())
726
727 def test_message_from_file(self):
728 fp = openfile('msg_01.txt')
729 try:
730 text = fp.read()
731 fp.seek(0)
732 msg = email.message_from_file(fp)
733 s = StringIO()
734 # Don't wrap/continue long headers since we're trying to test
735 # idempotency.
736 g = Generator(s, maxheaderlen=0)
737 g(msg)
738 self.assertEqual(text, s.getvalue())
739 finally:
740 fp.close()
741
742 def test_message_from_string_with_class(self):
743 unless = self.failUnless
744 fp = openfile('msg_01.txt')
745 try:
746 text = fp.read()
747 finally:
748 fp.close()
749 # Create a subclass
750 class MyMessage(Message):
751 pass
Tim Peters527e64f2001-10-04 05:36:56 +0000752
Barry Warsaw41075852001-09-23 03:18:13 +0000753 msg = email.message_from_string(text, MyMessage)
754 unless(isinstance(msg, MyMessage))
755 # Try something more complicated
756 fp = openfile('msg_02.txt')
757 try:
758 text = fp.read()
759 finally:
760 fp.close()
761 msg = email.message_from_string(text, MyMessage)
762 for subpart in msg.walk():
763 unless(isinstance(subpart, MyMessage))
764
765
766 def test_message_from_file_with_class(self):
767 unless = self.failUnless
768 # Create a subclass
769 class MyMessage(Message):
770 pass
Tim Peters527e64f2001-10-04 05:36:56 +0000771
Barry Warsaw41075852001-09-23 03:18:13 +0000772 fp = openfile('msg_01.txt')
773 try:
774 msg = email.message_from_file(fp, MyMessage)
775 finally:
776 fp.close()
777 unless(isinstance(msg, MyMessage))
778 # Try something more complicated
779 fp = openfile('msg_02.txt')
780 try:
781 msg = email.message_from_file(fp, MyMessage)
782 finally:
783 fp.close()
784 for subpart in msg.walk():
785 unless(isinstance(subpart, MyMessage))
786
787
Barry Warsaw08a534d2001-10-04 17:58:50 +0000788
789# Test the iterator/generators
Barry Warsaw41075852001-09-23 03:18:13 +0000790class TestIterators(TestEmailBase):
791 def test_body_line_iterator(self):
792 eq = self.assertEqual
793 # First a simple non-multipart message
794 msg = self._msgobj('msg_01.txt')
795 it = Iterators.body_line_iterator(msg)
Barry Warsawd1de6ea2001-10-04 18:18:37 +0000796 lines = list(it)
Barry Warsaw41075852001-09-23 03:18:13 +0000797 eq(len(lines), 6)
798 eq(EMPTYSTRING.join(lines), msg.get_payload())
799 # Now a more complicated multipart
800 msg = self._msgobj('msg_02.txt')
801 it = Iterators.body_line_iterator(msg)
Barry Warsawd1de6ea2001-10-04 18:18:37 +0000802 lines = list(it)
Barry Warsaw41075852001-09-23 03:18:13 +0000803 eq(len(lines), 43)
Barry Warsaw08a534d2001-10-04 17:58:50 +0000804 eq(EMPTYSTRING.join(lines), openfile('msg_19.txt').read())
Barry Warsaw41075852001-09-23 03:18:13 +0000805
806 def test_typed_subpart_iterator(self):
807 eq = self.assertEqual
808 msg = self._msgobj('msg_04.txt')
809 it = Iterators.typed_subpart_iterator(msg, 'text')
Barry Warsawd1de6ea2001-10-04 18:18:37 +0000810 lines = [subpart.get_payload() for subpart in it]
811 eq(len(lines), 2)
Barry Warsaw41075852001-09-23 03:18:13 +0000812 eq(EMPTYSTRING.join(lines), """\
813a simple kind of mirror
814to reflect upon our own
815a simple kind of mirror
816to reflect upon our own
817""")
818
819
Barry Warsaw08a534d2001-10-04 17:58:50 +0000820
Barry Warsaw41075852001-09-23 03:18:13 +0000821def suite():
822 suite = unittest.TestSuite()
823 suite.addTest(unittest.makeSuite(TestMessageAPI))
824 suite.addTest(unittest.makeSuite(TestEncoders))
825 suite.addTest(unittest.makeSuite(TestLongHeaders))
826 suite.addTest(unittest.makeSuite(TestFromMangling))
Barry Warsaw65279d02001-09-26 05:47:08 +0000827 suite.addTest(unittest.makeSuite(TestMIMEImage))
828 suite.addTest(unittest.makeSuite(TestMIMEText))
Barry Warsaw41075852001-09-23 03:18:13 +0000829 suite.addTest(unittest.makeSuite(TestMultipartMixed))
830 suite.addTest(unittest.makeSuite(TestNonConformant))
831 suite.addTest(unittest.makeSuite(TestRFC2047))
Barry Warsaw65279d02001-09-26 05:47:08 +0000832 suite.addTest(unittest.makeSuite(TestMIMEMessage))
Barry Warsaw41075852001-09-23 03:18:13 +0000833 suite.addTest(unittest.makeSuite(TestIdempotent))
834 suite.addTest(unittest.makeSuite(TestMiscellaneous))
835 suite.addTest(unittest.makeSuite(TestIterators))
836 return suite
837
838
Barry Warsaw08a534d2001-10-04 17:58:50 +0000839
Barry Warsaw41075852001-09-23 03:18:13 +0000840if __name__ == '__main__':
841 unittest.main(defaultTest='suite')
842else:
843 from test_support import run_suite
844 run_suite(suite())