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