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