PageRenderTime 60ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/Lib/test/test_email/test_contentmanager.py

https://github.com/albertz/CPython
Python | 796 lines | 777 code | 7 blank | 12 comment | 3 complexity | 0bf3deeac7af22a818b49d3ad1b6c2da MD5 | raw file
  1. import unittest
  2. from test.test_email import TestEmailBase, parameterize
  3. import textwrap
  4. from email import policy
  5. from email.message import EmailMessage
  6. from email.contentmanager import ContentManager, raw_data_manager
  7. @parameterize
  8. class TestContentManager(TestEmailBase):
  9. policy = policy.default
  10. message = EmailMessage
  11. get_key_params = {
  12. 'full_type': (1, 'text/plain',),
  13. 'maintype_only': (2, 'text',),
  14. 'null_key': (3, '',),
  15. }
  16. def get_key_as_get_content_key(self, order, key):
  17. def foo_getter(msg, foo=None):
  18. bar = msg['X-Bar-Header']
  19. return foo, bar
  20. cm = ContentManager()
  21. cm.add_get_handler(key, foo_getter)
  22. m = self._make_message()
  23. m['Content-Type'] = 'text/plain'
  24. m['X-Bar-Header'] = 'foo'
  25. self.assertEqual(cm.get_content(m, foo='bar'), ('bar', 'foo'))
  26. def get_key_as_get_content_key_order(self, order, key):
  27. def bar_getter(msg):
  28. return msg['X-Bar-Header']
  29. def foo_getter(msg):
  30. return msg['X-Foo-Header']
  31. cm = ContentManager()
  32. cm.add_get_handler(key, foo_getter)
  33. for precedence, key in self.get_key_params.values():
  34. if precedence > order:
  35. cm.add_get_handler(key, bar_getter)
  36. m = self._make_message()
  37. m['Content-Type'] = 'text/plain'
  38. m['X-Bar-Header'] = 'bar'
  39. m['X-Foo-Header'] = 'foo'
  40. self.assertEqual(cm.get_content(m), ('foo'))
  41. def test_get_content_raises_if_unknown_mimetype_and_no_default(self):
  42. cm = ContentManager()
  43. m = self._make_message()
  44. m['Content-Type'] = 'text/plain'
  45. with self.assertRaisesRegex(KeyError, 'text/plain'):
  46. cm.get_content(m)
  47. class BaseThing(str):
  48. pass
  49. baseobject_full_path = __name__ + '.' + 'TestContentManager.BaseThing'
  50. class Thing(BaseThing):
  51. pass
  52. testobject_full_path = __name__ + '.' + 'TestContentManager.Thing'
  53. set_key_params = {
  54. 'type': (0, Thing,),
  55. 'full_path': (1, testobject_full_path,),
  56. 'qualname': (2, 'TestContentManager.Thing',),
  57. 'name': (3, 'Thing',),
  58. 'base_type': (4, BaseThing,),
  59. 'base_full_path': (5, baseobject_full_path,),
  60. 'base_qualname': (6, 'TestContentManager.BaseThing',),
  61. 'base_name': (7, 'BaseThing',),
  62. 'str_type': (8, str,),
  63. 'str_full_path': (9, 'builtins.str',),
  64. 'str_name': (10, 'str',), # str name and qualname are the same
  65. 'null_key': (11, None,),
  66. }
  67. def set_key_as_set_content_key(self, order, key):
  68. def foo_setter(msg, obj, foo=None):
  69. msg['X-Foo-Header'] = foo
  70. msg.set_payload(obj)
  71. cm = ContentManager()
  72. cm.add_set_handler(key, foo_setter)
  73. m = self._make_message()
  74. msg_obj = self.Thing()
  75. cm.set_content(m, msg_obj, foo='bar')
  76. self.assertEqual(m['X-Foo-Header'], 'bar')
  77. self.assertEqual(m.get_payload(), msg_obj)
  78. def set_key_as_set_content_key_order(self, order, key):
  79. def foo_setter(msg, obj):
  80. msg['X-FooBar-Header'] = 'foo'
  81. msg.set_payload(obj)
  82. def bar_setter(msg, obj):
  83. msg['X-FooBar-Header'] = 'bar'
  84. cm = ContentManager()
  85. cm.add_set_handler(key, foo_setter)
  86. for precedence, key in self.get_key_params.values():
  87. if precedence > order:
  88. cm.add_set_handler(key, bar_setter)
  89. m = self._make_message()
  90. msg_obj = self.Thing()
  91. cm.set_content(m, msg_obj)
  92. self.assertEqual(m['X-FooBar-Header'], 'foo')
  93. self.assertEqual(m.get_payload(), msg_obj)
  94. def test_set_content_raises_if_unknown_type_and_no_default(self):
  95. cm = ContentManager()
  96. m = self._make_message()
  97. msg_obj = self.Thing()
  98. with self.assertRaisesRegex(KeyError, self.testobject_full_path):
  99. cm.set_content(m, msg_obj)
  100. def test_set_content_raises_if_called_on_multipart(self):
  101. cm = ContentManager()
  102. m = self._make_message()
  103. m['Content-Type'] = 'multipart/foo'
  104. with self.assertRaises(TypeError):
  105. cm.set_content(m, 'test')
  106. def test_set_content_calls_clear_content(self):
  107. m = self._make_message()
  108. m['Content-Foo'] = 'bar'
  109. m['Content-Type'] = 'text/html'
  110. m['To'] = 'test'
  111. m.set_payload('abc')
  112. cm = ContentManager()
  113. cm.add_set_handler(str, lambda *args, **kw: None)
  114. m.set_content('xyz', content_manager=cm)
  115. self.assertIsNone(m['Content-Foo'])
  116. self.assertIsNone(m['Content-Type'])
  117. self.assertEqual(m['To'], 'test')
  118. self.assertIsNone(m.get_payload())
  119. @parameterize
  120. class TestRawDataManager(TestEmailBase):
  121. # Note: these tests are dependent on the order in which headers are added
  122. # to the message objects by the code. There's no defined ordering in
  123. # RFC5322/MIME, so this makes the tests more fragile than the standards
  124. # require. However, if the header order changes it is best to understand
  125. # *why*, and make sure it isn't a subtle bug in whatever change was
  126. # applied.
  127. policy = policy.default.clone(max_line_length=60,
  128. content_manager=raw_data_manager)
  129. message = EmailMessage
  130. def test_get_text_plain(self):
  131. m = self._str_msg(textwrap.dedent("""\
  132. Content-Type: text/plain
  133. Basic text.
  134. """))
  135. self.assertEqual(raw_data_manager.get_content(m), "Basic text.\n")
  136. def test_get_text_html(self):
  137. m = self._str_msg(textwrap.dedent("""\
  138. Content-Type: text/html
  139. <p>Basic text.</p>
  140. """))
  141. self.assertEqual(raw_data_manager.get_content(m),
  142. "<p>Basic text.</p>\n")
  143. def test_get_text_plain_latin1(self):
  144. m = self._bytes_msg(textwrap.dedent("""\
  145. Content-Type: text/plain; charset=latin1
  146. Basìc tëxt.
  147. """).encode('latin1'))
  148. self.assertEqual(raw_data_manager.get_content(m), "Basìc tëxt.\n")
  149. def test_get_text_plain_latin1_quoted_printable(self):
  150. m = self._str_msg(textwrap.dedent("""\
  151. Content-Type: text/plain; charset="latin-1"
  152. Content-Transfer-Encoding: quoted-printable
  153. Bas=ECc t=EBxt.
  154. """))
  155. self.assertEqual(raw_data_manager.get_content(m), "Basìc tëxt.\n")
  156. def test_get_text_plain_utf8_base64(self):
  157. m = self._str_msg(textwrap.dedent("""\
  158. Content-Type: text/plain; charset="utf8"
  159. Content-Transfer-Encoding: base64
  160. QmFzw6xjIHTDq3h0Lgo=
  161. """))
  162. self.assertEqual(raw_data_manager.get_content(m), "Basìc tëxt.\n")
  163. def test_get_text_plain_bad_utf8_quoted_printable(self):
  164. m = self._str_msg(textwrap.dedent("""\
  165. Content-Type: text/plain; charset="utf8"
  166. Content-Transfer-Encoding: quoted-printable
  167. Bas=c3=acc t=c3=abxt=fd.
  168. """))
  169. self.assertEqual(raw_data_manager.get_content(m), "Basìc tëxt�.\n")
  170. def test_get_text_plain_bad_utf8_quoted_printable_ignore_errors(self):
  171. m = self._str_msg(textwrap.dedent("""\
  172. Content-Type: text/plain; charset="utf8"
  173. Content-Transfer-Encoding: quoted-printable
  174. Bas=c3=acc t=c3=abxt=fd.
  175. """))
  176. self.assertEqual(raw_data_manager.get_content(m, errors='ignore'),
  177. "Basìc tëxt.\n")
  178. def test_get_text_plain_utf8_base64_recoverable_bad_CTE_data(self):
  179. m = self._str_msg(textwrap.dedent("""\
  180. Content-Type: text/plain; charset="utf8"
  181. Content-Transfer-Encoding: base64
  182. QmFzw6xjIHTDq3h0Lgo\xFF=
  183. """))
  184. self.assertEqual(raw_data_manager.get_content(m, errors='ignore'),
  185. "Basìc tëxt.\n")
  186. def test_get_text_invalid_keyword(self):
  187. m = self._str_msg(textwrap.dedent("""\
  188. Content-Type: text/plain
  189. Basic text.
  190. """))
  191. with self.assertRaises(TypeError):
  192. raw_data_manager.get_content(m, foo='ignore')
  193. def test_get_non_text(self):
  194. template = textwrap.dedent("""\
  195. Content-Type: {}
  196. Content-Transfer-Encoding: base64
  197. Ym9ndXMgZGF0YQ==
  198. """)
  199. for maintype in 'audio image video application'.split():
  200. with self.subTest(maintype=maintype):
  201. m = self._str_msg(template.format(maintype+'/foo'))
  202. self.assertEqual(raw_data_manager.get_content(m), b"bogus data")
  203. def test_get_non_text_invalid_keyword(self):
  204. m = self._str_msg(textwrap.dedent("""\
  205. Content-Type: image/jpg
  206. Content-Transfer-Encoding: base64
  207. Ym9ndXMgZGF0YQ==
  208. """))
  209. with self.assertRaises(TypeError):
  210. raw_data_manager.get_content(m, errors='ignore')
  211. def test_get_raises_on_multipart(self):
  212. m = self._str_msg(textwrap.dedent("""\
  213. Content-Type: multipart/mixed; boundary="==="
  214. --===
  215. --===--
  216. """))
  217. with self.assertRaises(KeyError):
  218. raw_data_manager.get_content(m)
  219. def test_get_message_rfc822_and_external_body(self):
  220. template = textwrap.dedent("""\
  221. Content-Type: message/{}
  222. To: foo@example.com
  223. From: bar@example.com
  224. Subject: example
  225. an example message
  226. """)
  227. for subtype in 'rfc822 external-body'.split():
  228. with self.subTest(subtype=subtype):
  229. m = self._str_msg(template.format(subtype))
  230. sub_msg = raw_data_manager.get_content(m)
  231. self.assertIsInstance(sub_msg, self.message)
  232. self.assertEqual(raw_data_manager.get_content(sub_msg),
  233. "an example message\n")
  234. self.assertEqual(sub_msg['to'], 'foo@example.com')
  235. self.assertEqual(sub_msg['from'].addresses[0].username, 'bar')
  236. def test_get_message_non_rfc822_or_external_body_yields_bytes(self):
  237. m = self._str_msg(textwrap.dedent("""\
  238. Content-Type: message/partial
  239. To: foo@example.com
  240. From: bar@example.com
  241. Subject: example
  242. The real body is in another message.
  243. """))
  244. self.assertEqual(raw_data_manager.get_content(m)[:10], b'To: foo@ex')
  245. def test_set_text_plain(self):
  246. m = self._make_message()
  247. content = "Simple message.\n"
  248. raw_data_manager.set_content(m, content)
  249. self.assertEqual(str(m), textwrap.dedent("""\
  250. Content-Type: text/plain; charset="utf-8"
  251. Content-Transfer-Encoding: 7bit
  252. Simple message.
  253. """))
  254. self.assertEqual(m.get_payload(decode=True).decode('utf-8'), content)
  255. self.assertEqual(m.get_content(), content)
  256. def test_set_text_html(self):
  257. m = self._make_message()
  258. content = "<p>Simple message.</p>\n"
  259. raw_data_manager.set_content(m, content, subtype='html')
  260. self.assertEqual(str(m), textwrap.dedent("""\
  261. Content-Type: text/html; charset="utf-8"
  262. Content-Transfer-Encoding: 7bit
  263. <p>Simple message.</p>
  264. """))
  265. self.assertEqual(m.get_payload(decode=True).decode('utf-8'), content)
  266. self.assertEqual(m.get_content(), content)
  267. def test_set_text_charset_latin_1(self):
  268. m = self._make_message()
  269. content = "Simple message.\n"
  270. raw_data_manager.set_content(m, content, charset='latin-1')
  271. self.assertEqual(str(m), textwrap.dedent("""\
  272. Content-Type: text/plain; charset="iso-8859-1"
  273. Content-Transfer-Encoding: 7bit
  274. Simple message.
  275. """))
  276. self.assertEqual(m.get_payload(decode=True).decode('utf-8'), content)
  277. self.assertEqual(m.get_content(), content)
  278. def test_set_text_short_line_minimal_non_ascii_heuristics(self):
  279. m = self._make_message()
  280. content = "et là il est monté sur moi et il commence à m'éto.\n"
  281. raw_data_manager.set_content(m, content)
  282. self.assertEqual(bytes(m), textwrap.dedent("""\
  283. Content-Type: text/plain; charset="utf-8"
  284. Content-Transfer-Encoding: 8bit
  285. et il est monté sur moi et il commence à m'éto.
  286. """).encode('utf-8'))
  287. self.assertEqual(m.get_payload(decode=True).decode('utf-8'), content)
  288. self.assertEqual(m.get_content(), content)
  289. def test_set_text_long_line_minimal_non_ascii_heuristics(self):
  290. m = self._make_message()
  291. content = ("j'ai un problème de python. il est sorti de son"
  292. " vivarium. et là il est monté sur moi et il commence"
  293. " à m'éto.\n")
  294. raw_data_manager.set_content(m, content)
  295. self.assertEqual(bytes(m), textwrap.dedent("""\
  296. Content-Type: text/plain; charset="utf-8"
  297. Content-Transfer-Encoding: quoted-printable
  298. j'ai un probl=C3=A8me de python. il est sorti de son vivari=
  299. um. et l=C3=A0 il est mont=C3=A9 sur moi et il commence =
  300. =C3=A0 m'=C3=A9to.
  301. """).encode('utf-8'))
  302. self.assertEqual(m.get_payload(decode=True).decode('utf-8'), content)
  303. self.assertEqual(m.get_content(), content)
  304. def test_set_text_11_lines_long_line_minimal_non_ascii_heuristics(self):
  305. m = self._make_message()
  306. content = '\n'*10 + (
  307. "j'ai un problème de python. il est sorti de son"
  308. " vivarium. et là il est monté sur moi et il commence"
  309. " à m'éto.\n")
  310. raw_data_manager.set_content(m, content)
  311. self.assertEqual(bytes(m), textwrap.dedent("""\
  312. Content-Type: text/plain; charset="utf-8"
  313. Content-Transfer-Encoding: quoted-printable
  314. """ + '\n'*10 + """
  315. j'ai un probl=C3=A8me de python. il est sorti de son vivari=
  316. um. et l=C3=A0 il est mont=C3=A9 sur moi et il commence =
  317. =C3=A0 m'=C3=A9to.
  318. """).encode('utf-8'))
  319. self.assertEqual(m.get_payload(decode=True).decode('utf-8'), content)
  320. self.assertEqual(m.get_content(), content)
  321. def test_set_text_maximal_non_ascii_heuristics(self):
  322. m = self._make_message()
  323. content = "áàäéèęöő.\n"
  324. raw_data_manager.set_content(m, content)
  325. self.assertEqual(bytes(m), textwrap.dedent("""\
  326. Content-Type: text/plain; charset="utf-8"
  327. Content-Transfer-Encoding: 8bit
  328. áàäéèęöő.
  329. """).encode('utf-8'))
  330. self.assertEqual(m.get_payload(decode=True).decode('utf-8'), content)
  331. self.assertEqual(m.get_content(), content)
  332. def test_set_text_11_lines_maximal_non_ascii_heuristics(self):
  333. m = self._make_message()
  334. content = '\n'*10 + "áàäéèęöő.\n"
  335. raw_data_manager.set_content(m, content)
  336. self.assertEqual(bytes(m), textwrap.dedent("""\
  337. Content-Type: text/plain; charset="utf-8"
  338. Content-Transfer-Encoding: 8bit
  339. """ + '\n'*10 + """
  340. áàäéèęöő.
  341. """).encode('utf-8'))
  342. self.assertEqual(m.get_payload(decode=True).decode('utf-8'), content)
  343. self.assertEqual(m.get_content(), content)
  344. def test_set_text_long_line_maximal_non_ascii_heuristics(self):
  345. m = self._make_message()
  346. content = ("áàäéèęöőáàäéèęöőáàäéèęöőáàäéèęöő"
  347. "áàäéèęöőáàäéèęöőáàäéèęöőáàäéèęöő"
  348. "áàäéèęöőáàäéèęöőáàäéèęöőáàäéèęöő.\n")
  349. raw_data_manager.set_content(m, content)
  350. self.assertEqual(bytes(m), textwrap.dedent("""\
  351. Content-Type: text/plain; charset="utf-8"
  352. Content-Transfer-Encoding: base64
  353. w6HDoMOkw6nDqMSZw7bFkcOhw6DDpMOpw6jEmcO2xZHDocOgw6TDqcOoxJnD
  354. tsWRw6HDoMOkw6nDqMSZw7bFkcOhw6DDpMOpw6jEmcO2xZHDocOgw6TDqcOo
  355. xJnDtsWRw6HDoMOkw6nDqMSZw7bFkcOhw6DDpMOpw6jEmcO2xZHDocOgw6TD
  356. qcOoxJnDtsWRw6HDoMOkw6nDqMSZw7bFkcOhw6DDpMOpw6jEmcO2xZHDocOg
  357. w6TDqcOoxJnDtsWRLgo=
  358. """).encode('utf-8'))
  359. self.assertEqual(m.get_payload(decode=True).decode('utf-8'), content)
  360. self.assertEqual(m.get_content(), content)
  361. def test_set_text_11_lines_long_line_maximal_non_ascii_heuristics(self):
  362. # Yes, it chooses "wrong" here. It's a heuristic. So this result
  363. # could change if we come up with a better heuristic.
  364. m = self._make_message()
  365. content = ('\n'*10 +
  366. "áàäéèęöőáàäéèęöőáàäéèęöőáàäéèęöő"
  367. "áàäéèęöőáàäéèęöőáàäéèęöőáàäéèęöő"
  368. "áàäéèęöőáàäéèęöőáàäéèęöőáàäéèęöő.\n")
  369. raw_data_manager.set_content(m, "\n"*10 +
  370. "áàäéèęöőáàäéèęöőáàäéèęöőáàäéèęöő"
  371. "áàäéèęöőáàäéèęöőáàäéèęöőáàäéèęöő"
  372. "áàäéèęöőáàäéèęöőáàäéèęöőáàäéèęöő.\n")
  373. self.assertEqual(bytes(m), textwrap.dedent("""\
  374. Content-Type: text/plain; charset="utf-8"
  375. Content-Transfer-Encoding: quoted-printable
  376. """ + '\n'*10 + """
  377. =C3=A1=C3=A0=C3=A4=C3=A9=C3=A8=C4=99=C3=B6=C5=91=C3=A1=C3=
  378. =A0=C3=A4=C3=A9=C3=A8=C4=99=C3=B6=C5=91=C3=A1=C3=A0=C3=A4=
  379. =C3=A9=C3=A8=C4=99=C3=B6=C5=91=C3=A1=C3=A0=C3=A4=C3=A9=C3=
  380. =A8=C4=99=C3=B6=C5=91=C3=A1=C3=A0=C3=A4=C3=A9=C3=A8=C4=99=
  381. =C3=B6=C5=91=C3=A1=C3=A0=C3=A4=C3=A9=C3=A8=C4=99=C3=B6=C5=
  382. =91=C3=A1=C3=A0=C3=A4=C3=A9=C3=A8=C4=99=C3=B6=C5=91=C3=A1=
  383. =C3=A0=C3=A4=C3=A9=C3=A8=C4=99=C3=B6=C5=91=C3=A1=C3=A0=C3=
  384. =A4=C3=A9=C3=A8=C4=99=C3=B6=C5=91=C3=A1=C3=A0=C3=A4=C3=A9=
  385. =C3=A8=C4=99=C3=B6=C5=91=C3=A1=C3=A0=C3=A4=C3=A9=C3=A8=C4=
  386. =99=C3=B6=C5=91=C3=A1=C3=A0=C3=A4=C3=A9=C3=A8=C4=99=C3=B6=
  387. =C5=91.
  388. """).encode('utf-8'))
  389. self.assertEqual(m.get_payload(decode=True).decode('utf-8'), content)
  390. self.assertEqual(m.get_content(), content)
  391. def test_set_text_non_ascii_with_cte_7bit_raises(self):
  392. m = self._make_message()
  393. with self.assertRaises(UnicodeError):
  394. raw_data_manager.set_content(m,"áàäéèęöő.\n", cte='7bit')
  395. def test_set_text_non_ascii_with_charset_ascii_raises(self):
  396. m = self._make_message()
  397. with self.assertRaises(UnicodeError):
  398. raw_data_manager.set_content(m,"áàäéèęöő.\n", charset='ascii')
  399. def test_set_text_non_ascii_with_cte_7bit_and_charset_ascii_raises(self):
  400. m = self._make_message()
  401. with self.assertRaises(UnicodeError):
  402. raw_data_manager.set_content(m,"áàäéèęöő.\n", cte='7bit', charset='ascii')
  403. def test_set_message(self):
  404. m = self._make_message()
  405. m['Subject'] = "Forwarded message"
  406. content = self._make_message()
  407. content['To'] = 'python@vivarium.org'
  408. content['From'] = 'police@monty.org'
  409. content['Subject'] = "get back in your box"
  410. content.set_content("Or face the comfy chair.")
  411. raw_data_manager.set_content(m, content)
  412. self.assertEqual(str(m), textwrap.dedent("""\
  413. Subject: Forwarded message
  414. Content-Type: message/rfc822
  415. Content-Transfer-Encoding: 8bit
  416. To: python@vivarium.org
  417. From: police@monty.org
  418. Subject: get back in your box
  419. Content-Type: text/plain; charset="utf-8"
  420. Content-Transfer-Encoding: 7bit
  421. MIME-Version: 1.0
  422. Or face the comfy chair.
  423. """))
  424. payload = m.get_payload(0)
  425. self.assertIsInstance(payload, self.message)
  426. self.assertEqual(str(payload), str(content))
  427. self.assertIsInstance(m.get_content(), self.message)
  428. self.assertEqual(str(m.get_content()), str(content))
  429. def test_set_message_with_non_ascii_and_coercion_to_7bit(self):
  430. m = self._make_message()
  431. m['Subject'] = "Escape report"
  432. content = self._make_message()
  433. content['To'] = 'police@monty.org'
  434. content['From'] = 'victim@monty.org'
  435. content['Subject'] = "Help"
  436. content.set_content("j'ai un problème de python. il est sorti de son"
  437. " vivarium.")
  438. raw_data_manager.set_content(m, content)
  439. self.assertEqual(bytes(m), textwrap.dedent("""\
  440. Subject: Escape report
  441. Content-Type: message/rfc822
  442. Content-Transfer-Encoding: 8bit
  443. To: police@monty.org
  444. From: victim@monty.org
  445. Subject: Help
  446. Content-Type: text/plain; charset="utf-8"
  447. Content-Transfer-Encoding: 8bit
  448. MIME-Version: 1.0
  449. j'ai un problème de python. il est sorti de son vivarium.
  450. """).encode('utf-8'))
  451. # The choice of base64 for the body encoding is because generator
  452. # doesn't bother with heuristics and uses it unconditionally for utf-8
  453. # text.
  454. # XXX: the first cte should be 7bit, too...that's a generator bug.
  455. # XXX: the line length in the body also looks like a generator bug.
  456. self.assertEqual(m.as_string(maxheaderlen=self.policy.max_line_length),
  457. textwrap.dedent("""\
  458. Subject: Escape report
  459. Content-Type: message/rfc822
  460. Content-Transfer-Encoding: 8bit
  461. To: police@monty.org
  462. From: victim@monty.org
  463. Subject: Help
  464. Content-Type: text/plain; charset="utf-8"
  465. Content-Transfer-Encoding: base64
  466. MIME-Version: 1.0
  467. aidhaSB1biBwcm9ibMOobWUgZGUgcHl0aG9uLiBpbCBlc3Qgc29ydGkgZGUgc29uIHZpdmFyaXVt
  468. Lgo=
  469. """))
  470. self.assertIsInstance(m.get_content(), self.message)
  471. self.assertEqual(str(m.get_content()), str(content))
  472. def test_set_message_invalid_cte_raises(self):
  473. m = self._make_message()
  474. content = self._make_message()
  475. for cte in 'quoted-printable base64'.split():
  476. for subtype in 'rfc822 external-body'.split():
  477. with self.subTest(cte=cte, subtype=subtype):
  478. with self.assertRaises(ValueError) as ar:
  479. m.set_content(content, subtype, cte=cte)
  480. exc = str(ar.exception)
  481. self.assertIn(cte, exc)
  482. self.assertIn(subtype, exc)
  483. subtype = 'external-body'
  484. for cte in '8bit binary'.split():
  485. with self.subTest(cte=cte, subtype=subtype):
  486. with self.assertRaises(ValueError) as ar:
  487. m.set_content(content, subtype, cte=cte)
  488. exc = str(ar.exception)
  489. self.assertIn(cte, exc)
  490. self.assertIn(subtype, exc)
  491. def test_set_image_jpg(self):
  492. for content in (b"bogus content",
  493. bytearray(b"bogus content"),
  494. memoryview(b"bogus content")):
  495. with self.subTest(content=content):
  496. m = self._make_message()
  497. raw_data_manager.set_content(m, content, 'image', 'jpeg')
  498. self.assertEqual(str(m), textwrap.dedent("""\
  499. Content-Type: image/jpeg
  500. Content-Transfer-Encoding: base64
  501. Ym9ndXMgY29udGVudA==
  502. """))
  503. self.assertEqual(m.get_payload(decode=True), content)
  504. self.assertEqual(m.get_content(), content)
  505. def test_set_audio_aif_with_quoted_printable_cte(self):
  506. # Why you would use qp, I don't know, but it is technically supported.
  507. # XXX: the incorrect line length is because binascii.b2a_qp doesn't
  508. # support a line length parameter, but we must use it to get newline
  509. # encoding.
  510. # XXX: what about that lack of tailing newline? Do we actually handle
  511. # that correctly in all cases? That is, if the *source* has an
  512. # unencoded newline, do we add an extra newline to the returned payload
  513. # or not? And can that actually be disambiguated based on the RFC?
  514. m = self._make_message()
  515. content = b'b\xFFgus\tcon\nt\rent ' + b'z'*100
  516. m.set_content(content, 'audio', 'aif', cte='quoted-printable')
  517. self.assertEqual(bytes(m), textwrap.dedent("""\
  518. Content-Type: audio/aif
  519. Content-Transfer-Encoding: quoted-printable
  520. MIME-Version: 1.0
  521. b=FFgus=09con=0At=0Dent=20zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz=
  522. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz""").encode('latin-1'))
  523. self.assertEqual(m.get_payload(decode=True), content)
  524. self.assertEqual(m.get_content(), content)
  525. def test_set_video_mpeg_with_binary_cte(self):
  526. m = self._make_message()
  527. content = b'b\xFFgus\tcon\nt\rent ' + b'z'*100
  528. m.set_content(content, 'video', 'mpeg', cte='binary')
  529. self.assertEqual(bytes(m), textwrap.dedent("""\
  530. Content-Type: video/mpeg
  531. Content-Transfer-Encoding: binary
  532. MIME-Version: 1.0
  533. """).encode('ascii') +
  534. # XXX: the second \n ought to be a \r, but generator gets it wrong.
  535. # THIS MEANS WE DON'T ACTUALLY SUPPORT THE 'binary' CTE.
  536. b'b\xFFgus\tcon\nt\nent zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz' +
  537. b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz')
  538. self.assertEqual(m.get_payload(decode=True), content)
  539. self.assertEqual(m.get_content(), content)
  540. def test_set_application_octet_stream_with_8bit_cte(self):
  541. # In 8bit mode, universal line end logic applies. It is up to the
  542. # application to make sure the lines are short enough; we don't check.
  543. m = self._make_message()
  544. content = b'b\xFFgus\tcon\nt\rent\n' + b'z'*60 + b'\n'
  545. m.set_content(content, 'application', 'octet-stream', cte='8bit')
  546. self.assertEqual(bytes(m), textwrap.dedent("""\
  547. Content-Type: application/octet-stream
  548. Content-Transfer-Encoding: 8bit
  549. MIME-Version: 1.0
  550. """).encode('ascii') +
  551. b'b\xFFgus\tcon\nt\nent\n' +
  552. b'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\n')
  553. self.assertEqual(m.get_payload(decode=True), content)
  554. self.assertEqual(m.get_content(), content)
  555. def test_set_headers_from_header_objects(self):
  556. m = self._make_message()
  557. content = "Simple message.\n"
  558. header_factory = self.policy.header_factory
  559. raw_data_manager.set_content(m, content, headers=(
  560. header_factory("To", "foo@example.com"),
  561. header_factory("From", "foo@example.com"),
  562. header_factory("Subject", "I'm talking to myself.")))
  563. self.assertEqual(str(m), textwrap.dedent("""\
  564. Content-Type: text/plain; charset="utf-8"
  565. To: foo@example.com
  566. From: foo@example.com
  567. Subject: I'm talking to myself.
  568. Content-Transfer-Encoding: 7bit
  569. Simple message.
  570. """))
  571. def test_set_headers_from_strings(self):
  572. m = self._make_message()
  573. content = "Simple message.\n"
  574. raw_data_manager.set_content(m, content, headers=(
  575. "X-Foo-Header: foo",
  576. "X-Bar-Header: bar",))
  577. self.assertEqual(str(m), textwrap.dedent("""\
  578. Content-Type: text/plain; charset="utf-8"
  579. X-Foo-Header: foo
  580. X-Bar-Header: bar
  581. Content-Transfer-Encoding: 7bit
  582. Simple message.
  583. """))
  584. def test_set_headers_with_invalid_duplicate_string_header_raises(self):
  585. m = self._make_message()
  586. content = "Simple message.\n"
  587. with self.assertRaisesRegex(ValueError, 'Content-Type'):
  588. raw_data_manager.set_content(m, content, headers=(
  589. "Content-Type: foo/bar",)
  590. )
  591. def test_set_headers_with_invalid_duplicate_header_header_raises(self):
  592. m = self._make_message()
  593. content = "Simple message.\n"
  594. header_factory = self.policy.header_factory
  595. with self.assertRaisesRegex(ValueError, 'Content-Type'):
  596. raw_data_manager.set_content(m, content, headers=(
  597. header_factory("Content-Type", " foo/bar"),)
  598. )
  599. def test_set_headers_with_defective_string_header_raises(self):
  600. m = self._make_message()
  601. content = "Simple message.\n"
  602. with self.assertRaisesRegex(ValueError, 'a@fairly@@invalid@address'):
  603. raw_data_manager.set_content(m, content, headers=(
  604. 'To: a@fairly@@invalid@address',)
  605. )
  606. print(m['To'].defects)
  607. def test_set_headers_with_defective_header_header_raises(self):
  608. m = self._make_message()
  609. content = "Simple message.\n"
  610. header_factory = self.policy.header_factory
  611. with self.assertRaisesRegex(ValueError, 'a@fairly@@invalid@address'):
  612. raw_data_manager.set_content(m, content, headers=(
  613. header_factory('To', 'a@fairly@@invalid@address'),)
  614. )
  615. print(m['To'].defects)
  616. def test_set_disposition_inline(self):
  617. m = self._make_message()
  618. m.set_content('foo', disposition='inline')
  619. self.assertEqual(m['Content-Disposition'], 'inline')
  620. def test_set_disposition_attachment(self):
  621. m = self._make_message()
  622. m.set_content('foo', disposition='attachment')
  623. self.assertEqual(m['Content-Disposition'], 'attachment')
  624. def test_set_disposition_foo(self):
  625. m = self._make_message()
  626. m.set_content('foo', disposition='foo')
  627. self.assertEqual(m['Content-Disposition'], 'foo')
  628. # XXX: we should have a 'strict' policy mode (beyond raise_on_defect) that
  629. # would cause 'foo' above to raise.
  630. def test_set_filename(self):
  631. m = self._make_message()
  632. m.set_content('foo', filename='bar.txt')
  633. self.assertEqual(m['Content-Disposition'],
  634. 'attachment; filename="bar.txt"')
  635. def test_set_filename_and_disposition_inline(self):
  636. m = self._make_message()
  637. m.set_content('foo', disposition='inline', filename='bar.txt')
  638. self.assertEqual(m['Content-Disposition'], 'inline; filename="bar.txt"')
  639. def test_set_non_ascii_filename(self):
  640. m = self._make_message()
  641. m.set_content('foo', filename='ábárî.txt')
  642. self.assertEqual(bytes(m), textwrap.dedent("""\
  643. Content-Type: text/plain; charset="utf-8"
  644. Content-Transfer-Encoding: 7bit
  645. Content-Disposition: attachment;
  646. filename*=utf-8''%C3%A1b%C3%A1r%C3%AE.txt
  647. MIME-Version: 1.0
  648. foo
  649. """).encode('ascii'))
  650. content_object_params = {
  651. 'text_plain': ('content', ()),
  652. 'text_html': ('content', ('html',)),
  653. 'application_octet_stream': (b'content',
  654. ('application', 'octet_stream')),
  655. 'image_jpeg': (b'content', ('image', 'jpeg')),
  656. 'message_rfc822': (message(), ()),
  657. 'message_external_body': (message(), ('external-body',)),
  658. }
  659. def content_object_as_header_receiver(self, obj, mimetype):
  660. m = self._make_message()
  661. m.set_content(obj, *mimetype, headers=(
  662. 'To: foo@example.com',
  663. 'From: bar@simple.net'))
  664. self.assertEqual(m['to'], 'foo@example.com')
  665. self.assertEqual(m['from'], 'bar@simple.net')
  666. def content_object_as_disposition_inline_receiver(self, obj, mimetype):
  667. m = self._make_message()
  668. m.set_content(obj, *mimetype, disposition='inline')
  669. self.assertEqual(m['Content-Disposition'], 'inline')
  670. def content_object_as_non_ascii_filename_receiver(self, obj, mimetype):
  671. m = self._make_message()
  672. m.set_content(obj, *mimetype, disposition='inline', filename='bár.txt')
  673. self.assertEqual(m['Content-Disposition'], 'inline; filename="bár.txt"')
  674. self.assertEqual(m.get_filename(), "bár.txt")
  675. self.assertEqual(m['Content-Disposition'].params['filename'], "bár.txt")
  676. def content_object_as_cid_receiver(self, obj, mimetype):
  677. m = self._make_message()
  678. m.set_content(obj, *mimetype, cid='some_random_stuff')
  679. self.assertEqual(m['Content-ID'], 'some_random_stuff')
  680. def content_object_as_params_receiver(self, obj, mimetype):
  681. m = self._make_message()
  682. params = {'foo': 'bár', 'abc': 'xyz'}
  683. m.set_content(obj, *mimetype, params=params)
  684. if isinstance(obj, str):
  685. params['charset'] = 'utf-8'
  686. self.assertEqual(m['Content-Type'].params, params)
  687. if __name__ == '__main__':
  688. unittest.main()