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