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