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