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