PageRenderTime 27ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/Lib/test/test_email/test_email.py

https://github.com/albertz/CPython
Python | 1193 lines | 1123 code | 39 blank | 31 comment | 10 complexity | 13e42dedca567722c7382803d8688c3a MD5 | raw file
  1. # Copyright (C) 2001-2010 Python Software Foundation
  2. # Contact: email-sig@python.org
  3. # email package unit tests
  4. import re
  5. import time
  6. import base64
  7. import unittest
  8. import textwrap
  9. from io import StringIO, BytesIO
  10. from itertools import chain
  11. from random import choice
  12. from socket import getfqdn
  13. from threading import Thread
  14. import email
  15. import email.policy
  16. from email.charset import Charset
  17. from email.header import Header, decode_header, make_header
  18. from email.parser import Parser, HeaderParser
  19. from email.generator import Generator, DecodedGenerator, BytesGenerator
  20. from email.message import Message
  21. from email.mime.application import MIMEApplication
  22. from email.mime.audio import MIMEAudio
  23. from email.mime.text import MIMEText
  24. from email.mime.image import MIMEImage
  25. from email.mime.base import MIMEBase
  26. from email.mime.message import MIMEMessage
  27. from email.mime.multipart import MIMEMultipart
  28. from email.mime.nonmultipart import MIMENonMultipart
  29. from email import utils
  30. from email import errors
  31. from email import encoders
  32. from email import iterators
  33. from email import base64mime
  34. from email import quoprimime
  35. from test.support import unlink, start_threads
  36. from test.test_email import openfile, TestEmailBase
  37. # These imports are documented to work, but we are testing them using a
  38. # different path, so we import them here just to make sure they are importable.
  39. from email.parser import FeedParser, BytesFeedParser
  40. NL = '\n'
  41. EMPTYSTRING = ''
  42. SPACE = ' '
  43. # Test various aspects of the Message class's API
  44. class TestMessageAPI(TestEmailBase):
  45. def test_get_all(self):
  46. eq = self.assertEqual
  47. msg = self._msgobj('msg_20.txt')
  48. eq(msg.get_all('cc'), ['ccc@zzz.org', 'ddd@zzz.org', 'eee@zzz.org'])
  49. eq(msg.get_all('xx', 'n/a'), 'n/a')
  50. def test_getset_charset(self):
  51. eq = self.assertEqual
  52. msg = Message()
  53. eq(msg.get_charset(), None)
  54. charset = Charset('iso-8859-1')
  55. msg.set_charset(charset)
  56. eq(msg['mime-version'], '1.0')
  57. eq(msg.get_content_type(), 'text/plain')
  58. eq(msg['content-type'], 'text/plain; charset="iso-8859-1"')
  59. eq(msg.get_param('charset'), 'iso-8859-1')
  60. eq(msg['content-transfer-encoding'], 'quoted-printable')
  61. eq(msg.get_charset().input_charset, 'iso-8859-1')
  62. # Remove the charset
  63. msg.set_charset(None)
  64. eq(msg.get_charset(), None)
  65. eq(msg['content-type'], 'text/plain')
  66. # Try adding a charset when there's already MIME headers present
  67. msg = Message()
  68. msg['MIME-Version'] = '2.0'
  69. msg['Content-Type'] = 'text/x-weird'
  70. msg['Content-Transfer-Encoding'] = 'quinted-puntable'
  71. msg.set_charset(charset)
  72. eq(msg['mime-version'], '2.0')
  73. eq(msg['content-type'], 'text/x-weird; charset="iso-8859-1"')
  74. eq(msg['content-transfer-encoding'], 'quinted-puntable')
  75. def test_set_charset_from_string(self):
  76. eq = self.assertEqual
  77. msg = Message()
  78. msg.set_charset('us-ascii')
  79. eq(msg.get_charset().input_charset, 'us-ascii')
  80. eq(msg['content-type'], 'text/plain; charset="us-ascii"')
  81. def test_set_payload_with_charset(self):
  82. msg = Message()
  83. charset = Charset('iso-8859-1')
  84. msg.set_payload('This is a string payload', charset)
  85. self.assertEqual(msg.get_charset().input_charset, 'iso-8859-1')
  86. def test_set_payload_with_8bit_data_and_charset(self):
  87. data = b'\xd0\x90\xd0\x91\xd0\x92'
  88. charset = Charset('utf-8')
  89. msg = Message()
  90. msg.set_payload(data, charset)
  91. self.assertEqual(msg['content-transfer-encoding'], 'base64')
  92. self.assertEqual(msg.get_payload(decode=True), data)
  93. self.assertEqual(msg.get_payload(), '0JDQkdCS\n')
  94. def test_set_payload_with_non_ascii_and_charset_body_encoding_none(self):
  95. data = b'\xd0\x90\xd0\x91\xd0\x92'
  96. charset = Charset('utf-8')
  97. charset.body_encoding = None # Disable base64 encoding
  98. msg = Message()
  99. msg.set_payload(data.decode('utf-8'), charset)
  100. self.assertEqual(msg['content-transfer-encoding'], '8bit')
  101. self.assertEqual(msg.get_payload(decode=True), data)
  102. def test_set_payload_with_8bit_data_and_charset_body_encoding_none(self):
  103. data = b'\xd0\x90\xd0\x91\xd0\x92'
  104. charset = Charset('utf-8')
  105. charset.body_encoding = None # Disable base64 encoding
  106. msg = Message()
  107. msg.set_payload(data, charset)
  108. self.assertEqual(msg['content-transfer-encoding'], '8bit')
  109. self.assertEqual(msg.get_payload(decode=True), data)
  110. def test_set_payload_to_list(self):
  111. msg = Message()
  112. msg.set_payload([])
  113. self.assertEqual(msg.get_payload(), [])
  114. def test_attach_when_payload_is_string(self):
  115. msg = Message()
  116. msg['Content-Type'] = 'multipart/mixed'
  117. msg.set_payload('string payload')
  118. sub_msg = MIMEMessage(Message())
  119. self.assertRaisesRegex(TypeError, "[Aa]ttach.*non-multipart",
  120. msg.attach, sub_msg)
  121. def test_get_charsets(self):
  122. eq = self.assertEqual
  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. msg = self._msgobj('msg_09.txt')
  127. charsets = msg.get_charsets('dingbat')
  128. eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat',
  129. 'koi8-r'])
  130. msg = self._msgobj('msg_12.txt')
  131. charsets = msg.get_charsets()
  132. eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2',
  133. 'iso-8859-3', 'us-ascii', 'koi8-r'])
  134. def test_get_filename(self):
  135. eq = self.assertEqual
  136. msg = self._msgobj('msg_04.txt')
  137. filenames = [p.get_filename() for p in msg.get_payload()]
  138. eq(filenames, ['msg.txt', 'msg.txt'])
  139. msg = self._msgobj('msg_07.txt')
  140. subpart = msg.get_payload(1)
  141. eq(subpart.get_filename(), 'dingusfish.gif')
  142. def test_get_filename_with_name_parameter(self):
  143. eq = self.assertEqual
  144. msg = self._msgobj('msg_44.txt')
  145. filenames = [p.get_filename() for p in msg.get_payload()]
  146. eq(filenames, ['msg.txt', 'msg.txt'])
  147. def test_get_boundary(self):
  148. eq = self.assertEqual
  149. msg = self._msgobj('msg_07.txt')
  150. # No quotes!
  151. eq(msg.get_boundary(), 'BOUNDARY')
  152. def test_set_boundary(self):
  153. eq = self.assertEqual
  154. # This one has no existing boundary parameter, but the Content-Type:
  155. # header appears fifth.
  156. msg = self._msgobj('msg_01.txt')
  157. msg.set_boundary('BOUNDARY')
  158. header, value = msg.items()[4]
  159. eq(header.lower(), 'content-type')
  160. eq(value, 'text/plain; charset="us-ascii"; boundary="BOUNDARY"')
  161. # This one has a Content-Type: header, with a boundary, stuck in the
  162. # middle of its headers. Make sure the order is preserved; it should
  163. # be fifth.
  164. msg = self._msgobj('msg_04.txt')
  165. msg.set_boundary('BOUNDARY')
  166. header, value = msg.items()[4]
  167. eq(header.lower(), 'content-type')
  168. eq(value, 'multipart/mixed; boundary="BOUNDARY"')
  169. # And this one has no Content-Type: header at all.
  170. msg = self._msgobj('msg_03.txt')
  171. self.assertRaises(errors.HeaderParseError,
  172. msg.set_boundary, 'BOUNDARY')
  173. def test_make_boundary(self):
  174. msg = MIMEMultipart('form-data')
  175. # Note that when the boundary gets created is an implementation
  176. # detail and might change.
  177. self.assertEqual(msg.items()[0][1], 'multipart/form-data')
  178. # Trigger creation of boundary
  179. msg.as_string()
  180. self.assertEqual(msg.items()[0][1][:33],
  181. 'multipart/form-data; boundary="==')
  182. # XXX: there ought to be tests of the uniqueness of the boundary, too.
  183. def test_message_rfc822_only(self):
  184. # Issue 7970: message/rfc822 not in multipart parsed by
  185. # HeaderParser caused an exception when flattened.
  186. with openfile('msg_46.txt') as fp:
  187. msgdata = fp.read()
  188. parser = HeaderParser()
  189. msg = parser.parsestr(msgdata)
  190. out = StringIO()
  191. gen = Generator(out, True, 0)
  192. gen.flatten(msg, False)
  193. self.assertEqual(out.getvalue(), msgdata)
  194. def test_byte_message_rfc822_only(self):
  195. # Make sure new bytes header parser also passes this.
  196. with openfile('msg_46.txt') as fp:
  197. msgdata = fp.read().encode('ascii')
  198. parser = email.parser.BytesHeaderParser()
  199. msg = parser.parsebytes(msgdata)
  200. out = BytesIO()
  201. gen = email.generator.BytesGenerator(out)
  202. gen.flatten(msg)
  203. self.assertEqual(out.getvalue(), msgdata)
  204. def test_get_decoded_payload(self):
  205. eq = self.assertEqual
  206. msg = self._msgobj('msg_10.txt')
  207. # The outer message is a multipart
  208. eq(msg.get_payload(decode=True), None)
  209. # Subpart 1 is 7bit encoded
  210. eq(msg.get_payload(0).get_payload(decode=True),
  211. b'This is a 7bit encoded message.\n')
  212. # Subpart 2 is quopri
  213. eq(msg.get_payload(1).get_payload(decode=True),
  214. b'\xa1This is a Quoted Printable encoded message!\n')
  215. # Subpart 3 is base64
  216. eq(msg.get_payload(2).get_payload(decode=True),
  217. b'This is a Base64 encoded message.')
  218. # Subpart 4 is base64 with a trailing newline, which
  219. # used to be stripped (issue 7143).
  220. eq(msg.get_payload(3).get_payload(decode=True),
  221. b'This is a Base64 encoded message.\n')
  222. # Subpart 5 has no Content-Transfer-Encoding: header.
  223. eq(msg.get_payload(4).get_payload(decode=True),
  224. b'This has no Content-Transfer-Encoding: header.\n')
  225. def test_get_decoded_uu_payload(self):
  226. eq = self.assertEqual
  227. msg = Message()
  228. msg.set_payload('begin 666 -\n+:&5L;&\\@=V]R;&0 \n \nend\n')
  229. for cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):
  230. msg['content-transfer-encoding'] = cte
  231. eq(msg.get_payload(decode=True), b'hello world')
  232. # Now try some bogus data
  233. msg.set_payload('foo')
  234. eq(msg.get_payload(decode=True), b'foo')
  235. def test_get_payload_n_raises_on_non_multipart(self):
  236. msg = Message()
  237. self.assertRaises(TypeError, msg.get_payload, 1)
  238. def test_decoded_generator(self):
  239. eq = self.assertEqual
  240. msg = self._msgobj('msg_07.txt')
  241. with openfile('msg_17.txt') as fp:
  242. text = fp.read()
  243. s = StringIO()
  244. g = DecodedGenerator(s)
  245. g.flatten(msg)
  246. eq(s.getvalue(), text)
  247. def test__contains__(self):
  248. msg = Message()
  249. msg['From'] = 'Me'
  250. msg['to'] = 'You'
  251. # Check for case insensitivity
  252. self.assertIn('from', msg)
  253. self.assertIn('From', msg)
  254. self.assertIn('FROM', msg)
  255. self.assertIn('to', msg)
  256. self.assertIn('To', msg)
  257. self.assertIn('TO', msg)
  258. def test_as_string(self):
  259. msg = self._msgobj('msg_01.txt')
  260. with openfile('msg_01.txt') as fp:
  261. text = fp.read()
  262. self.assertEqual(text, str(msg))
  263. fullrepr = msg.as_string(unixfrom=True)
  264. lines = fullrepr.split('\n')
  265. self.assertTrue(lines[0].startswith('From '))
  266. self.assertEqual(text, NL.join(lines[1:]))
  267. def test_as_string_policy(self):
  268. msg = self._msgobj('msg_01.txt')
  269. newpolicy = msg.policy.clone(linesep='\r\n')
  270. fullrepr = msg.as_string(policy=newpolicy)
  271. s = StringIO()
  272. g = Generator(s, policy=newpolicy)
  273. g.flatten(msg)
  274. self.assertEqual(fullrepr, s.getvalue())
  275. def test_as_bytes(self):
  276. msg = self._msgobj('msg_01.txt')
  277. with openfile('msg_01.txt') as fp:
  278. data = fp.read().encode('ascii')
  279. self.assertEqual(data, bytes(msg))
  280. fullrepr = msg.as_bytes(unixfrom=True)
  281. lines = fullrepr.split(b'\n')
  282. self.assertTrue(lines[0].startswith(b'From '))
  283. self.assertEqual(data, b'\n'.join(lines[1:]))
  284. def test_as_bytes_policy(self):
  285. msg = self._msgobj('msg_01.txt')
  286. newpolicy = msg.policy.clone(linesep='\r\n')
  287. fullrepr = msg.as_bytes(policy=newpolicy)
  288. s = BytesIO()
  289. g = BytesGenerator(s,policy=newpolicy)
  290. g.flatten(msg)
  291. self.assertEqual(fullrepr, s.getvalue())
  292. # test_headerregistry.TestContentTypeHeader.bad_params
  293. def test_bad_param(self):
  294. msg = email.message_from_string("Content-Type: blarg; baz; boo\n")
  295. self.assertEqual(msg.get_param('baz'), '')
  296. def test_missing_filename(self):
  297. msg = email.message_from_string("From: foo\n")
  298. self.assertEqual(msg.get_filename(), None)
  299. def test_bogus_filename(self):
  300. msg = email.message_from_string(
  301. "Content-Disposition: blarg; filename\n")
  302. self.assertEqual(msg.get_filename(), '')
  303. def test_missing_boundary(self):
  304. msg = email.message_from_string("From: foo\n")
  305. self.assertEqual(msg.get_boundary(), None)
  306. def test_get_params(self):
  307. eq = self.assertEqual
  308. msg = email.message_from_string(
  309. 'X-Header: foo=one; bar=two; baz=three\n')
  310. eq(msg.get_params(header='x-header'),
  311. [('foo', 'one'), ('bar', 'two'), ('baz', 'three')])
  312. msg = email.message_from_string(
  313. 'X-Header: foo; bar=one; baz=two\n')
  314. eq(msg.get_params(header='x-header'),
  315. [('foo', ''), ('bar', 'one'), ('baz', 'two')])
  316. eq(msg.get_params(), None)
  317. msg = email.message_from_string(
  318. 'X-Header: foo; bar="one"; baz=two\n')
  319. eq(msg.get_params(header='x-header'),
  320. [('foo', ''), ('bar', 'one'), ('baz', 'two')])
  321. # test_headerregistry.TestContentTypeHeader.spaces_around_param_equals
  322. def test_get_param_liberal(self):
  323. msg = Message()
  324. msg['Content-Type'] = 'Content-Type: Multipart/mixed; boundary = "CPIMSSMTPC06p5f3tG"'
  325. self.assertEqual(msg.get_param('boundary'), 'CPIMSSMTPC06p5f3tG')
  326. def test_get_param(self):
  327. eq = self.assertEqual
  328. msg = email.message_from_string(
  329. "X-Header: foo=one; bar=two; baz=three\n")
  330. eq(msg.get_param('bar', header='x-header'), 'two')
  331. eq(msg.get_param('quuz', header='x-header'), None)
  332. eq(msg.get_param('quuz'), None)
  333. msg = email.message_from_string(
  334. 'X-Header: foo; bar="one"; baz=two\n')
  335. eq(msg.get_param('foo', header='x-header'), '')
  336. eq(msg.get_param('bar', header='x-header'), 'one')
  337. eq(msg.get_param('baz', header='x-header'), 'two')
  338. # XXX: We are not RFC-2045 compliant! We cannot parse:
  339. # msg["Content-Type"] = 'text/plain; weird="hey; dolly? [you] @ <\\"home\\">?"'
  340. # msg.get_param("weird")
  341. # yet.
  342. # test_headerregistry.TestContentTypeHeader.spaces_around_semis
  343. def test_get_param_funky_continuation_lines(self):
  344. msg = self._msgobj('msg_22.txt')
  345. self.assertEqual(msg.get_payload(1).get_param('name'), 'wibble.JPG')
  346. # test_headerregistry.TestContentTypeHeader.semis_inside_quotes
  347. def test_get_param_with_semis_in_quotes(self):
  348. msg = email.message_from_string(
  349. 'Content-Type: image/pjpeg; name="Jim&amp;&amp;Jill"\n')
  350. self.assertEqual(msg.get_param('name'), 'Jim&amp;&amp;Jill')
  351. self.assertEqual(msg.get_param('name', unquote=False),
  352. '"Jim&amp;&amp;Jill"')
  353. # test_headerregistry.TestContentTypeHeader.quotes_inside_rfc2231_value
  354. def test_get_param_with_quotes(self):
  355. msg = email.message_from_string(
  356. 'Content-Type: foo; bar*0="baz\\"foobar"; bar*1="\\"baz"')
  357. self.assertEqual(msg.get_param('bar'), 'baz"foobar"baz')
  358. msg = email.message_from_string(
  359. "Content-Type: foo; bar*0=\"baz\\\"foobar\"; bar*1=\"\\\"baz\"")
  360. self.assertEqual(msg.get_param('bar'), 'baz"foobar"baz')
  361. def test_field_containment(self):
  362. msg = email.message_from_string('Header: exists')
  363. self.assertIn('header', msg)
  364. self.assertIn('Header', msg)
  365. self.assertIn('HEADER', msg)
  366. self.assertNotIn('headerx', msg)
  367. def test_set_param(self):
  368. eq = self.assertEqual
  369. msg = Message()
  370. msg.set_param('charset', 'iso-2022-jp')
  371. eq(msg.get_param('charset'), 'iso-2022-jp')
  372. msg.set_param('importance', 'high value')
  373. eq(msg.get_param('importance'), 'high value')
  374. eq(msg.get_param('importance', unquote=False), '"high value"')
  375. eq(msg.get_params(), [('text/plain', ''),
  376. ('charset', 'iso-2022-jp'),
  377. ('importance', 'high value')])
  378. eq(msg.get_params(unquote=False), [('text/plain', ''),
  379. ('charset', '"iso-2022-jp"'),
  380. ('importance', '"high value"')])
  381. msg.set_param('charset', 'iso-9999-xx', header='X-Jimmy')
  382. eq(msg.get_param('charset', header='X-Jimmy'), 'iso-9999-xx')
  383. def test_del_param(self):
  384. eq = self.assertEqual
  385. msg = self._msgobj('msg_05.txt')
  386. eq(msg.get_params(),
  387. [('multipart/report', ''), ('report-type', 'delivery-status'),
  388. ('boundary', 'D1690A7AC1.996856090/mail.example.com')])
  389. old_val = msg.get_param("report-type")
  390. msg.del_param("report-type")
  391. eq(msg.get_params(),
  392. [('multipart/report', ''),
  393. ('boundary', 'D1690A7AC1.996856090/mail.example.com')])
  394. msg.set_param("report-type", old_val)
  395. eq(msg.get_params(),
  396. [('multipart/report', ''),
  397. ('boundary', 'D1690A7AC1.996856090/mail.example.com'),
  398. ('report-type', old_val)])
  399. def test_del_param_on_other_header(self):
  400. msg = Message()
  401. msg.add_header('Content-Disposition', 'attachment', filename='bud.gif')
  402. msg.del_param('filename', 'content-disposition')
  403. self.assertEqual(msg['content-disposition'], 'attachment')
  404. def test_del_param_on_nonexistent_header(self):
  405. msg = Message()
  406. # Deleting param on empty msg should not raise exception.
  407. msg.del_param('filename', 'content-disposition')
  408. def test_del_nonexistent_param(self):
  409. msg = Message()
  410. msg.add_header('Content-Type', 'text/plain', charset='utf-8')
  411. existing_header = msg['Content-Type']
  412. msg.del_param('foobar', header='Content-Type')
  413. self.assertEqual(msg['Content-Type'], existing_header)
  414. def test_set_type(self):
  415. eq = self.assertEqual
  416. msg = Message()
  417. self.assertRaises(ValueError, msg.set_type, 'text')
  418. msg.set_type('text/plain')
  419. eq(msg['content-type'], 'text/plain')
  420. msg.set_param('charset', 'us-ascii')
  421. eq(msg['content-type'], 'text/plain; charset="us-ascii"')
  422. msg.set_type('text/html')
  423. eq(msg['content-type'], 'text/html; charset="us-ascii"')
  424. def test_set_type_on_other_header(self):
  425. msg = Message()
  426. msg['X-Content-Type'] = 'text/plain'
  427. msg.set_type('application/octet-stream', 'X-Content-Type')
  428. self.assertEqual(msg['x-content-type'], 'application/octet-stream')
  429. def test_get_content_type_missing(self):
  430. msg = Message()
  431. self.assertEqual(msg.get_content_type(), 'text/plain')
  432. def test_get_content_type_missing_with_default_type(self):
  433. msg = Message()
  434. msg.set_default_type('message/rfc822')
  435. self.assertEqual(msg.get_content_type(), 'message/rfc822')
  436. def test_get_content_type_from_message_implicit(self):
  437. msg = self._msgobj('msg_30.txt')
  438. self.assertEqual(msg.get_payload(0).get_content_type(),
  439. 'message/rfc822')
  440. def test_get_content_type_from_message_explicit(self):
  441. msg = self._msgobj('msg_28.txt')
  442. self.assertEqual(msg.get_payload(0).get_content_type(),
  443. 'message/rfc822')
  444. def test_get_content_type_from_message_text_plain_implicit(self):
  445. msg = self._msgobj('msg_03.txt')
  446. self.assertEqual(msg.get_content_type(), 'text/plain')
  447. def test_get_content_type_from_message_text_plain_explicit(self):
  448. msg = self._msgobj('msg_01.txt')
  449. self.assertEqual(msg.get_content_type(), 'text/plain')
  450. def test_get_content_maintype_missing(self):
  451. msg = Message()
  452. self.assertEqual(msg.get_content_maintype(), 'text')
  453. def test_get_content_maintype_missing_with_default_type(self):
  454. msg = Message()
  455. msg.set_default_type('message/rfc822')
  456. self.assertEqual(msg.get_content_maintype(), 'message')
  457. def test_get_content_maintype_from_message_implicit(self):
  458. msg = self._msgobj('msg_30.txt')
  459. self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message')
  460. def test_get_content_maintype_from_message_explicit(self):
  461. msg = self._msgobj('msg_28.txt')
  462. self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message')
  463. def test_get_content_maintype_from_message_text_plain_implicit(self):
  464. msg = self._msgobj('msg_03.txt')
  465. self.assertEqual(msg.get_content_maintype(), 'text')
  466. def test_get_content_maintype_from_message_text_plain_explicit(self):
  467. msg = self._msgobj('msg_01.txt')
  468. self.assertEqual(msg.get_content_maintype(), 'text')
  469. def test_get_content_subtype_missing(self):
  470. msg = Message()
  471. self.assertEqual(msg.get_content_subtype(), 'plain')
  472. def test_get_content_subtype_missing_with_default_type(self):
  473. msg = Message()
  474. msg.set_default_type('message/rfc822')
  475. self.assertEqual(msg.get_content_subtype(), 'rfc822')
  476. def test_get_content_subtype_from_message_implicit(self):
  477. msg = self._msgobj('msg_30.txt')
  478. self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822')
  479. def test_get_content_subtype_from_message_explicit(self):
  480. msg = self._msgobj('msg_28.txt')
  481. self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822')
  482. def test_get_content_subtype_from_message_text_plain_implicit(self):
  483. msg = self._msgobj('msg_03.txt')
  484. self.assertEqual(msg.get_content_subtype(), 'plain')
  485. def test_get_content_subtype_from_message_text_plain_explicit(self):
  486. msg = self._msgobj('msg_01.txt')
  487. self.assertEqual(msg.get_content_subtype(), 'plain')
  488. def test_get_content_maintype_error(self):
  489. msg = Message()
  490. msg['Content-Type'] = 'no-slash-in-this-string'
  491. self.assertEqual(msg.get_content_maintype(), 'text')
  492. def test_get_content_subtype_error(self):
  493. msg = Message()
  494. msg['Content-Type'] = 'no-slash-in-this-string'
  495. self.assertEqual(msg.get_content_subtype(), 'plain')
  496. def test_replace_header(self):
  497. eq = self.assertEqual
  498. msg = Message()
  499. msg.add_header('First', 'One')
  500. msg.add_header('Second', 'Two')
  501. msg.add_header('Third', 'Three')
  502. eq(msg.keys(), ['First', 'Second', 'Third'])
  503. eq(msg.values(), ['One', 'Two', 'Three'])
  504. msg.replace_header('Second', 'Twenty')
  505. eq(msg.keys(), ['First', 'Second', 'Third'])
  506. eq(msg.values(), ['One', 'Twenty', 'Three'])
  507. msg.add_header('First', 'Eleven')
  508. msg.replace_header('First', 'One Hundred')
  509. eq(msg.keys(), ['First', 'Second', 'Third', 'First'])
  510. eq(msg.values(), ['One Hundred', 'Twenty', 'Three', 'Eleven'])
  511. self.assertRaises(KeyError, msg.replace_header, 'Fourth', 'Missing')
  512. def test_get_content_disposition(self):
  513. msg = Message()
  514. self.assertIsNone(msg.get_content_disposition())
  515. msg.add_header('Content-Disposition', 'attachment',
  516. filename='random.avi')
  517. self.assertEqual(msg.get_content_disposition(), 'attachment')
  518. msg.replace_header('Content-Disposition', 'inline')
  519. self.assertEqual(msg.get_content_disposition(), 'inline')
  520. msg.replace_header('Content-Disposition', 'InlinE')
  521. self.assertEqual(msg.get_content_disposition(), 'inline')
  522. # test_defect_handling:test_invalid_chars_in_base64_payload
  523. def test_broken_base64_payload(self):
  524. x = 'AwDp0P7//y6LwKEAcPa/6Q=9'
  525. msg = Message()
  526. msg['content-type'] = 'audio/x-midi'
  527. msg['content-transfer-encoding'] = 'base64'
  528. msg.set_payload(x)
  529. self.assertEqual(msg.get_payload(decode=True),
  530. (b'\x03\x00\xe9\xd0\xfe\xff\xff.\x8b\xc0'
  531. b'\xa1\x00p\xf6\xbf\xe9\x0f'))
  532. self.assertIsInstance(msg.defects[0],
  533. errors.InvalidBase64CharactersDefect)
  534. def test_broken_unicode_payload(self):
  535. # This test improves coverage but is not a compliance test.
  536. # The behavior in this situation is currently undefined by the API.
  537. x = 'this is a br\xf6ken thing to do'
  538. msg = Message()
  539. msg['content-type'] = 'text/plain'
  540. msg['content-transfer-encoding'] = '8bit'
  541. msg.set_payload(x)
  542. self.assertEqual(msg.get_payload(decode=True),
  543. bytes(x, 'raw-unicode-escape'))
  544. def test_questionable_bytes_payload(self):
  545. # This test improves coverage but is not a compliance test,
  546. # since it involves poking inside the black box.
  547. x = 'this is a quéstionable thing to do'.encode('utf-8')
  548. msg = Message()
  549. msg['content-type'] = 'text/plain; charset="utf-8"'
  550. msg['content-transfer-encoding'] = '8bit'
  551. msg._payload = x
  552. self.assertEqual(msg.get_payload(decode=True), x)
  553. # Issue 1078919
  554. def test_ascii_add_header(self):
  555. msg = Message()
  556. msg.add_header('Content-Disposition', 'attachment',
  557. filename='bud.gif')
  558. self.assertEqual('attachment; filename="bud.gif"',
  559. msg['Content-Disposition'])
  560. def test_noascii_add_header(self):
  561. msg = Message()
  562. msg.add_header('Content-Disposition', 'attachment',
  563. filename="Fußballer.ppt")
  564. self.assertEqual(
  565. 'attachment; filename*=utf-8\'\'Fu%C3%9Fballer.ppt',
  566. msg['Content-Disposition'])
  567. def test_nonascii_add_header_via_triple(self):
  568. msg = Message()
  569. msg.add_header('Content-Disposition', 'attachment',
  570. filename=('iso-8859-1', '', 'Fußballer.ppt'))
  571. self.assertEqual(
  572. 'attachment; filename*=iso-8859-1\'\'Fu%DFballer.ppt',
  573. msg['Content-Disposition'])
  574. def test_ascii_add_header_with_tspecial(self):
  575. msg = Message()
  576. msg.add_header('Content-Disposition', 'attachment',
  577. filename="windows [filename].ppt")
  578. self.assertEqual(
  579. 'attachment; filename="windows [filename].ppt"',
  580. msg['Content-Disposition'])
  581. def test_nonascii_add_header_with_tspecial(self):
  582. msg = Message()
  583. msg.add_header('Content-Disposition', 'attachment',
  584. filename="Fußballer [filename].ppt")
  585. self.assertEqual(
  586. "attachment; filename*=utf-8''Fu%C3%9Fballer%20%5Bfilename%5D.ppt",
  587. msg['Content-Disposition'])
  588. def test_binary_quopri_payload(self):
  589. for charset in ('latin-1', 'ascii'):
  590. msg = Message()
  591. msg['content-type'] = 'text/plain; charset=%s' % charset
  592. msg['content-transfer-encoding'] = 'quoted-printable'
  593. msg.set_payload(b'foo=e6=96=87bar')
  594. self.assertEqual(
  595. msg.get_payload(decode=True),
  596. b'foo\xe6\x96\x87bar',
  597. 'get_payload returns wrong result with charset %s.' % charset)
  598. def test_binary_base64_payload(self):
  599. for charset in ('latin-1', 'ascii'):
  600. msg = Message()
  601. msg['content-type'] = 'text/plain; charset=%s' % charset
  602. msg['content-transfer-encoding'] = 'base64'
  603. msg.set_payload(b'Zm9v5paHYmFy')
  604. self.assertEqual(
  605. msg.get_payload(decode=True),
  606. b'foo\xe6\x96\x87bar',
  607. 'get_payload returns wrong result with charset %s.' % charset)
  608. def test_binary_uuencode_payload(self):
  609. for charset in ('latin-1', 'ascii'):
  610. for encoding in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):
  611. msg = Message()
  612. msg['content-type'] = 'text/plain; charset=%s' % charset
  613. msg['content-transfer-encoding'] = encoding
  614. msg.set_payload(b"begin 666 -\n)9F]OYI:'8F%R\n \nend\n")
  615. self.assertEqual(
  616. msg.get_payload(decode=True),
  617. b'foo\xe6\x96\x87bar',
  618. str(('get_payload returns wrong result ',
  619. 'with charset {0} and encoding {1}.')).\
  620. format(charset, encoding))
  621. def test_add_header_with_name_only_param(self):
  622. msg = Message()
  623. msg.add_header('Content-Disposition', 'inline', foo_bar=None)
  624. self.assertEqual("inline; foo-bar", msg['Content-Disposition'])
  625. def test_add_header_with_no_value(self):
  626. msg = Message()
  627. msg.add_header('X-Status', None)
  628. self.assertEqual('', msg['X-Status'])
  629. # Issue 5871: reject an attempt to embed a header inside a header value
  630. # (header injection attack).
  631. def test_embedded_header_via_Header_rejected(self):
  632. msg = Message()
  633. msg['Dummy'] = Header('dummy\nX-Injected-Header: test')
  634. self.assertRaises(errors.HeaderParseError, msg.as_string)
  635. def test_embedded_header_via_string_rejected(self):
  636. msg = Message()
  637. msg['Dummy'] = 'dummy\nX-Injected-Header: test'
  638. self.assertRaises(errors.HeaderParseError, msg.as_string)
  639. def test_unicode_header_defaults_to_utf8_encoding(self):
  640. # Issue 14291
  641. m = MIMEText('abc\n')
  642. m['Subject'] = 'É test'
  643. self.assertEqual(str(m),textwrap.dedent("""\
  644. Content-Type: text/plain; charset="us-ascii"
  645. MIME-Version: 1.0
  646. Content-Transfer-Encoding: 7bit
  647. Subject: =?utf-8?q?=C3=89_test?=
  648. abc
  649. """))
  650. def test_unicode_body_defaults_to_utf8_encoding(self):
  651. # Issue 14291
  652. m = MIMEText('É testabc\n')
  653. self.assertEqual(str(m),textwrap.dedent("""\
  654. Content-Type: text/plain; charset="utf-8"
  655. MIME-Version: 1.0
  656. Content-Transfer-Encoding: base64
  657. w4kgdGVzdGFiYwo=
  658. """))
  659. # Test the email.encoders module
  660. class TestEncoders(unittest.TestCase):
  661. def test_EncodersEncode_base64(self):
  662. with openfile('PyBanner048.gif', 'rb') as fp:
  663. bindata = fp.read()
  664. mimed = email.mime.image.MIMEImage(bindata)
  665. base64ed = mimed.get_payload()
  666. # the transfer-encoded body lines should all be <=76 characters
  667. lines = base64ed.split('\n')
  668. self.assertLessEqual(max([ len(x) for x in lines ]), 76)
  669. def test_encode_empty_payload(self):
  670. eq = self.assertEqual
  671. msg = Message()
  672. msg.set_charset('us-ascii')
  673. eq(msg['content-transfer-encoding'], '7bit')
  674. def test_default_cte(self):
  675. eq = self.assertEqual
  676. # 7bit data and the default us-ascii _charset
  677. msg = MIMEText('hello world')
  678. eq(msg['content-transfer-encoding'], '7bit')
  679. # Similar, but with 8bit data
  680. msg = MIMEText('hello \xf8 world')
  681. eq(msg['content-transfer-encoding'], 'base64')
  682. # And now with a different charset
  683. msg = MIMEText('hello \xf8 world', _charset='iso-8859-1')
  684. eq(msg['content-transfer-encoding'], 'quoted-printable')
  685. def test_encode7or8bit(self):
  686. # Make sure a charset whose input character set is 8bit but
  687. # whose output character set is 7bit gets a transfer-encoding
  688. # of 7bit.
  689. eq = self.assertEqual
  690. msg = MIMEText('文\n', _charset='euc-jp')
  691. eq(msg['content-transfer-encoding'], '7bit')
  692. eq(msg.as_string(), textwrap.dedent("""\
  693. MIME-Version: 1.0
  694. Content-Type: text/plain; charset="iso-2022-jp"
  695. Content-Transfer-Encoding: 7bit
  696. \x1b$BJ8\x1b(B
  697. """))
  698. def test_qp_encode_latin1(self):
  699. msg = MIMEText('\xe1\xf6\n', 'text', 'ISO-8859-1')
  700. self.assertEqual(str(msg), textwrap.dedent("""\
  701. MIME-Version: 1.0
  702. Content-Type: text/text; charset="iso-8859-1"
  703. Content-Transfer-Encoding: quoted-printable
  704. =E1=F6
  705. """))
  706. def test_qp_encode_non_latin1(self):
  707. # Issue 16948
  708. msg = MIMEText('\u017c\n', 'text', 'ISO-8859-2')
  709. self.assertEqual(str(msg), textwrap.dedent("""\
  710. MIME-Version: 1.0
  711. Content-Type: text/text; charset="iso-8859-2"
  712. Content-Transfer-Encoding: quoted-printable
  713. =BF
  714. """))
  715. # Test long header wrapping
  716. class TestLongHeaders(TestEmailBase):
  717. maxDiff = None
  718. def test_split_long_continuation(self):
  719. eq = self.ndiffAssertEqual
  720. msg = email.message_from_string("""\
  721. Subject: bug demonstration
  722. \t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
  723. \tmore text
  724. test
  725. """)
  726. sfp = StringIO()
  727. g = Generator(sfp)
  728. g.flatten(msg)
  729. eq(sfp.getvalue(), """\
  730. Subject: bug demonstration
  731. \t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
  732. \tmore text
  733. test
  734. """)
  735. def test_another_long_almost_unsplittable_header(self):
  736. eq = self.ndiffAssertEqual
  737. hstr = """\
  738. bug demonstration
  739. \t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
  740. \tmore text"""
  741. h = Header(hstr, continuation_ws='\t')
  742. eq(h.encode(), """\
  743. bug demonstration
  744. \t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
  745. \tmore text""")
  746. h = Header(hstr.replace('\t', ' '))
  747. eq(h.encode(), """\
  748. bug demonstration
  749. 12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789
  750. more text""")
  751. def test_long_nonstring(self):
  752. eq = self.ndiffAssertEqual
  753. g = Charset("iso-8859-1")
  754. cz = Charset("iso-8859-2")
  755. utf8 = Charset("utf-8")
  756. g_head = (b'Die Mieter treten hier ein werden mit einem Foerderband '
  757. b'komfortabel den Korridor entlang, an s\xfcdl\xfcndischen '
  758. b'Wandgem\xe4lden vorbei, gegen die rotierenden Klingen '
  759. b'bef\xf6rdert. ')
  760. cz_head = (b'Finan\xe8ni metropole se hroutily pod tlakem jejich '
  761. b'd\xf9vtipu.. ')
  762. utf8_head = ('\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f'
  763. '\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00'
  764. '\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c'
  765. '\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067'
  766. '\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das '
  767. 'Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder '
  768. 'die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066'
  769. '\u3044\u307e\u3059\u3002')
  770. h = Header(g_head, g, header_name='Subject')
  771. h.append(cz_head, cz)
  772. h.append(utf8_head, utf8)
  773. msg = Message()
  774. msg['Subject'] = h
  775. sfp = StringIO()
  776. g = Generator(sfp)
  777. g.flatten(msg)
  778. eq(sfp.getvalue(), """\
  779. Subject: =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderb?=
  780. =?iso-8859-1?q?and_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen?=
  781. =?iso-8859-1?q?_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef?=
  782. =?iso-8859-1?q?=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hrouti?=
  783. =?iso-8859-2?q?ly_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?=
  784. =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC5LiA?=
  785. =?utf-8?b?6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn44Gf44KJ?=
  786. =?utf-8?b?44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFzIE51bnN0dWNr?=
  787. =?utf-8?b?IGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWloZXJodW5kIGRhcyBPZGVyIGRp?=
  788. =?utf-8?b?ZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI3jgajoqIDjgaPjgabjgYTjgb7jgZk=?=
  789. =?utf-8?b?44CC?=
  790. """)
  791. eq(h.encode(maxlinelen=76), """\
  792. =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerde?=
  793. =?iso-8859-1?q?rband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndis?=
  794. =?iso-8859-1?q?chen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klinge?=
  795. =?iso-8859-1?q?n_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se?=
  796. =?iso-8859-2?q?_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?=
  797. =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb?=
  798. =?utf-8?b?44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go?=
  799. =?utf-8?b?44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBp?=
  800. =?utf-8?b?c3QgZGFzIE51bnN0dWNrIGdpdCB1bmQgU2xvdGVybWV5ZXI/IEphISBCZWlo?=
  801. =?utf-8?b?ZXJodW5kIGRhcyBPZGVyIGRpZSBGbGlwcGVyd2FsZHQgZ2Vyc3B1dC7jgI0=?=
  802. =?utf-8?b?44Go6KiA44Gj44Gm44GE44G+44GZ44CC?=""")
  803. def test_long_header_encode(self):
  804. eq = self.ndiffAssertEqual
  805. h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
  806. 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
  807. header_name='X-Foobar-Spoink-Defrobnit')
  808. eq(h.encode(), '''\
  809. wasnipoop; giraffes="very-long-necked-animals";
  810. spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
  811. def test_long_header_encode_with_tab_continuation_is_just_a_hint(self):
  812. eq = self.ndiffAssertEqual
  813. h = Header('wasnipoop; giraffes="very-long-necked-animals"; '
  814. 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
  815. header_name='X-Foobar-Spoink-Defrobnit',
  816. continuation_ws='\t')
  817. eq(h.encode(), '''\
  818. wasnipoop; giraffes="very-long-necked-animals";
  819. spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
  820. def test_long_header_encode_with_tab_continuation(self):
  821. eq = self.ndiffAssertEqual
  822. h = Header('wasnipoop; giraffes="very-long-necked-animals";\t'
  823. 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"',
  824. header_name='X-Foobar-Spoink-Defrobnit',
  825. continuation_ws='\t')
  826. eq(h.encode(), '''\
  827. wasnipoop; giraffes="very-long-necked-animals";
  828. \tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''')
  829. def test_header_encode_with_different_output_charset(self):
  830. h = Header('文', 'euc-jp')
  831. self.assertEqual(h.encode(), "=?iso-2022-jp?b?GyRCSjgbKEI=?=")
  832. def test_long_header_encode_with_different_output_charset(self):
  833. h = Header(b'test-ja \xa4\xd8\xc5\xea\xb9\xc6\xa4\xb5\xa4\xec\xa4'
  834. b'\xbf\xa5\xe1\xa1\xbc\xa5\xeb\xa4\xcf\xbb\xca\xb2\xf1\xbc\xd4'
  835. b'\xa4\xce\xbe\xb5\xc7\xa7\xa4\xf2\xc2\xd4\xa4\xc3\xa4\xc6\xa4'
  836. b'\xa4\xa4\xde\xa4\xb9'.decode('euc-jp'), 'euc-jp')
  837. res = """\
  838. =?iso-2022-jp?b?dGVzdC1qYSAbJEIkWEVqOUYkNSRsJD8lYSE8JWskTztKMnE8VCROPjUbKEI=?=
  839. =?iso-2022-jp?b?GyRCRyckckJUJEMkRiQkJF4kORsoQg==?="""
  840. self.assertEqual(h.encode(), res)
  841. def test_header_splitter(self):
  842. eq = self.ndiffAssertEqual
  843. msg = MIMEText('')
  844. # It'd be great if we could use add_header() here, but that doesn't
  845. # guarantee an order of the parameters.
  846. msg['X-Foobar-Spoink-Defrobnit'] = (
  847. 'wasnipoop; giraffes="very-long-necked-animals"; '
  848. 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"')
  849. sfp = StringIO()
  850. g = Generator(sfp)
  851. g.flatten(msg)
  852. eq(sfp.getvalue(), '''\
  853. Content-Type: text/plain; charset="us-ascii"
  854. MIME-Version: 1.0
  855. Content-Transfer-Encoding: 7bit
  856. X-Foobar-Spoink-Defrobnit: wasnipoop; giraffes="very-long-necked-animals";
  857. spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"
  858. ''')
  859. def test_no_semis_header_splitter(self):
  860. eq = self.ndiffAssertEqual
  861. msg = Message()
  862. msg['From'] = 'test@dom.ain'
  863. msg['References'] = SPACE.join('<%d@dom.ain>' % i for i in range(10))
  864. msg.set_payload('Test')
  865. sfp = StringIO()
  866. g = Generator(sfp)
  867. g.flatten(msg)
  868. eq(sfp.getvalue(), """\
  869. From: test@dom.ain
  870. References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain>
  871. <5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain>
  872. Test""")
  873. def test_last_split_chunk_does_not_fit(self):
  874. eq = self.ndiffAssertEqual
  875. h = Header('Subject: the first part of this is short, but_the_second'
  876. '_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line'
  877. '_all_by_itself')
  878. eq(h.encode(), """\
  879. Subject: the first part of this is short,
  880. but_the_second_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself""")
  881. def test_splittable_leading_char_followed_by_overlong_unsplitable(self):
  882. eq = self.ndiffAssertEqual
  883. h = Header(', but_the_second'
  884. '_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line'
  885. '_all_by_itself')
  886. eq(h.encode(), """\
  887. ,
  888. but_the_second_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself""")
  889. def test_multiple_splittable_leading_char_followed_by_overlong_unsplitable(self):
  890. eq = self.ndiffAssertEqual
  891. h = Header(', , but_the_second'
  892. '_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line'
  893. '_all_by_itself')
  894. eq(h.encode(), """\
  895. , ,
  896. but_the_second_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself""")
  897. def test_trailing_splitable_on_overlong_unsplitable(self):
  898. eq = self.ndiffAssertEqual
  899. h = Header('this_part_does_not_fit_within_maxlinelen_and_thus_should_'
  900. 'be_on_a_line_all_by_itself;')
  901. eq(h.encode(), "this_part_does_not_fit_within_maxlinelen_and_thus_should_"
  902. "be_on_a_line_all_by_itself;")
  903. def test_trailing_splitable_on_overlong_unsplitable_with_leading_splitable(self):
  904. eq = self.ndiffAssertEqual
  905. h = Header('; '
  906. 'this_part_does_not_fit_within_maxlinelen_and_thus_should_'
  907. 'be_on_a_line_all_by_itself; ')
  908. eq(h.encode(), """\
  909. ;
  910. this_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself; """)
  911. def test_long_header_with_multiple_sequential_split_chars(self):
  912. eq = self.ndiffAssertEqual
  913. h = Header('This is a long line that has two whitespaces in a row. '
  914. 'This used to cause truncation of the header when folded')
  915. eq(h.encode(), """\
  916. This is a long line that has two whitespaces in a row. This used to cause
  917. truncation of the header when folded""")
  918. def test_splitter_split_on_punctuation_only_if_fws_with_header(self):
  919. eq = self.ndiffAssertEqual
  920. h = Header('thisverylongheaderhas;semicolons;and,commas,but'
  921. 'they;arenotlegal;fold,points')
  922. eq(h.encode(), "thisverylongheaderhas;semicolons;and,commas,butthey;"
  923. "arenotlegal;fold,points")
  924. def test_leading_splittable_in_the_middle_just_before_overlong_last_part(self):
  925. eq = self.ndiffAssertEqual
  926. h = Header('this is a test where we need to have more than one line '
  927. 'before; our final line that is just too big to fit;; '
  928. 'this_part_does_not_fit_within_maxlinelen_and_thus_should_'
  929. 'be_on_a_line_all_by_itself;')
  930. eq(h.encode(), """\
  931. this is a test where we need to have more than one line before;
  932. our final line that is just too big to fit;;
  933. this_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself;""")
  934. def test_overlong_last_part_followed_by_split_point(self):
  935. eq = self.ndiffAssertEqual
  936. h = Header('this_part_does_not_fit_within_maxlinelen_and_thus_should_'
  937. 'be_on_a_line_all_by_itself ')
  938. eq(h.encode(), "this_part_does_not_fit_within_maxlinelen_and_thus_"
  939. "should_be_on_a_line_all_by_itself ")
  940. def test_multiline_with_overlong_parts_separated_by_two_split_points(self):
  941. eq = self.ndiffAssertEqual
  942. h = Header('this_is_a__test_where_we_need_to_have_more_than_one_line_'
  943. 'before_our_final_line_; ; '
  944. 'this_part_does_not_fit_within_maxlinelen_and_thus_should_'
  945. 'be_on_a_line_all_by_itself; ')
  946. eq(h.encode(), """\
  947. this_is_a__test_where_we_need_to_have_more_than_one_line_before_our_final_line_;
  948. ;
  949. this_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself; """)
  950. def test_multiline_with_overlong_last_part_followed_by_split_point(self):
  951. eq = self.ndiffAssertEqual
  952. h = Header('this is a test where we need to have more than one line '
  953. 'before our final line; ; '
  954. 'this_part_does_not_fit_within_maxlinelen_and_thus_should_'
  955. 'be_on_a_line_all_by_itself; ')
  956. eq(h.encode(), """\
  957. this is a test where we need to have more than one line before our final line;
  958. ;
  959. this_part_does_not_fit_within_maxlinelen_and_thus_should_be_on_a_line_all_by_itself; """)
  960. def test_long_header_with_whitespace_runs(self):
  961. eq = self.ndiffAssertEqual
  962. msg = Message()
  963. msg['From'] = 'test@dom.ain'
  964. msg['References'] = SPACE.join(['<foo@dom.ain> '] * 10)
  965. msg.set_payload('Test')
  966. sfp = StringIO()
  967. g = Generator(sfp)
  968. g.flatten(msg)
  969. eq(sfp.getvalue(), """\
  970. From: test@dom.ain
  971. References: <foo@dom.ain> <foo@dom.ain> <foo@dom.ain> <foo@dom.ain>
  972. <foo@dom.ain> <foo@dom.ain> <foo@dom.ain> <foo@dom.ain>
  973. <foo@dom.ain> <foo@dom.ain>\x20\x20
  974. Test""")
  975. def test_long_run_with_semi_header_splitter(self):
  976. eq = self.ndiffAssertEqual
  977. msg = Message()
  978. msg['From'] = 'test@dom.ain'
  979. msg['References'] = SPACE.join(['<foo@dom.ain>'] * 10) + '; abc'
  980. msg.set_payload('Test')
  981. sfp = StringIO()
  982. g = Generator(sfp)
  983. g.flatten(msg)
  984. eq(sfp.getvalue(), """\
  985. From: test@dom.ain
  986. References: <foo@dom.ain> <foo@dom.ain> <foo@dom.ain> <foo@dom.ain>
  987. <foo@dom.ain> <foo@dom.ain> <foo@dom.ain> <foo@dom.ain> <foo@dom.ain>
  988. <foo@dom.ain>; abc
  989. Test""")
  990. def test_splitter_split_on_punctuation_only_if_fws(self):
  991. eq = self.ndiffAssertEqual
  992. msg = Message()
  993. msg['From'] = 'test@dom.ain'
  994. msg['References'] = ('thisverylongheaderhas;semicolons;and,commas,but'
  995. 'they;arenotlegal;fold,points')
  996. msg.set_payload('Test')
  997. sfp = StringIO()
  998. g = Generator(sfp)
  999. g.flatten(msg)
  1000. # XXX the space after the header should not be there.
  1001. eq(sfp.getvalue(), """\
  1002. From: test@dom.ain
  1003. References:\x20
  1004. thisverylongheaderhas;semicolons;and,commas,butthey;arenotlegal;fold,points
  1005. Test""")
  1006. def test_no_split_long_header(self):
  1007. eq = self.ndiffAssertEqual
  1008. hstr = 'References: ' + 'x' * 80
  1009. h = Header(hstr)
  1010. # These come on two lines because Headers are really field value
  1011. # classes and don't really know about their field names.
  1012. eq(h.encode(), """\
  1013. References:
  1014. xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""")
  1015. h = Header('x' * 80)
  1016. eq(h.encode(), 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx')
  1017. def test_splitting_multiple_long_lines(self):
  1018. eq = self.ndiffAssertEqual
  1019. hstr = """\
  1020. from babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for <mailman-admin@babylon.socal-raves.org>; Sat, 2 Feb 2002 17:00:06 -0800 (PST)
  1021. \tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for <mailman-admin@babylon.socal-raves.org>; Sat, 2 Feb 2002 17:00:06 -0800 (PST)
  1022. \tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for <mailman-admin@babylon.socal-raves.org>; Sat, 2 Feb 2002 17:00:06 -0800 (PST)
  1023. """
  1024. h = Header(hstr, continuation_ws='\t')
  1025. eq(h.encode(), """\
  1026. from babylon.socal-raves.org (localhost [127.0.0.1]);
  1027. by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
  1028. for <mailman-admin@babylon.socal-raves.org>;
  1029. Sat, 2 Feb 2002 17:00:06 -0800 (PST)
  1030. \tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
  1031. by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
  1032. for <mailman-admin@babylon.socal-raves.org>;
  1033. Sat, 2 Feb 2002 17:00:06 -0800 (PST)
  1034. \tfrom babylon.socal-raves.org (localhost [127.0.0.1]);
  1035. by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81;
  1036. for <mailman-admin@babylon.socal-raves.org>;
  1037. Sat, 2 Feb 2002 17:00:06 -0800 (PST)""")
  1038. def test_splitting_first_line_only_is_long(self):
  1039. eq = self.ndiffAssertEqual
  1040. hstr = """\
  1041. from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca)
  1042. \tby kronos.mems-exchange.org with esmtp (Exim 4.05)
  1043. \tid 17k4h5-00034i-00
  1044. \tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400"""
  1045. h = Header(hstr, maxlinelen=78, header_name='Receiv