test_inline_media.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. import base64
  2. from redmail import EmailSender
  3. import re
  4. from pathlib import Path
  5. from io import BytesIO
  6. import pytest
  7. # Importing Pandas from utils (is None if missing package)
  8. from redmail.email.utils import pd
  9. from resources import get_mpl_fig, get_pil_image
  10. from convert import remove_extra_lines, payloads_to_dict
  11. def compare_image_mime(mime_part, mime_part_html, orig_image:bytes, type_="image/png"):
  12. assert type_ == mime_part.get_content_type()
  13. image_bytes = mime_part.get_content()
  14. assert orig_image == image_bytes
  15. # Check the HTML mime has the image
  16. image_info = dict(mime_part.items())
  17. cid_parts = image_info['Content-ID'][1:-1].split(".")
  18. cid = "{}.{}.=\n{}.{domain}".format(*cid_parts[:3], domain='.'.join(cid_parts[3:]))
  19. cid = image_info['Content-ID'][1:-1]
  20. mime_part_html_cleaned = mime_part_html.get_payload().replace("=\n", "")
  21. assert f'<img src=3D"cid:{cid}">' in mime_part_html_cleaned or f'<img src="cid:{cid}">' in mime_part_html_cleaned
  22. @pytest.mark.parametrize(
  23. "get_image_obj", [
  24. pytest.param(lambda x: str(x), id="Path (str)"),
  25. pytest.param(lambda x: Path(str(x)), id="Path (pathlib)"),
  26. pytest.param(lambda x: open(str(x), 'rb').read(), id="Bytes (bytes)"),
  27. pytest.param(lambda x: BytesIO(open(str(x), 'rb').read()), id="Bytes (BytesIO)"),
  28. pytest.param(lambda x: {"maintype": "image", "subtype": "png", "content": open(str(x), 'rb').read()}, id="Dict specs"),
  29. ]
  30. )
  31. def test_with_image_file(get_image_obj, dummy_png):
  32. with open(str(dummy_png), "rb") as f:
  33. dummy_bytes = f.read()
  34. image_obj = get_image_obj(dummy_png)
  35. sender = EmailSender(host=None, port=1234)
  36. msg = sender.get_message(
  37. sender="me@gmail.com",
  38. receivers="you@gmail.com",
  39. subject="Some news",
  40. html='<h1>Hi,</h1> Nice to meet you. Look at this: {{ my_image }}',
  41. body_images={"my_image": image_obj}
  42. )
  43. assert "multipart/mixed" == msg.get_content_type()
  44. alternative = msg.get_payload()[0]
  45. related = alternative.get_payload()[0]
  46. mime_html, mime_image = related.get_payload()
  47. compare_image_mime(mime_image, mime_html, orig_image=dummy_bytes)
  48. # Test receivers etc.
  49. headers = {
  50. key: val
  51. for key, val in msg.items()
  52. if key not in ('Message-ID', 'Date')
  53. }
  54. assert {
  55. 'From': 'me@gmail.com',
  56. 'Subject': 'Some news',
  57. 'To': 'you@gmail.com',
  58. #'MIME-Version': '1.0',
  59. 'Content-Type': 'multipart/mixed'
  60. } == headers
  61. def test_with_image_dict_jpeg():
  62. img_data = '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD5rooor8DP9oD/2Q=='
  63. img_bytes = base64.b64decode(img_data)
  64. sender = EmailSender(host=None, port=1234)
  65. msg = sender.get_message(
  66. sender="me@gmail.com",
  67. receivers="you@gmail.com",
  68. subject="Some news",
  69. html='<h1>Hi,</h1> Nice to meet you. Look at this: {{ my_image }}',
  70. body_images={
  71. 'my_image': {
  72. "content": img_bytes,
  73. 'subtype': 'jpg'
  74. }
  75. }
  76. )
  77. # Validate structure
  78. structure = payloads_to_dict(msg)
  79. assert structure == {
  80. "multipart/mixed": {
  81. "multipart/alternative": {
  82. "multipart/related": {
  83. "text/html": structure["multipart/mixed"]["multipart/alternative"]["multipart/related"]["text/html"],
  84. "image/jpg": structure["multipart/mixed"]["multipart/alternative"]["multipart/related"]["image/jpg"],
  85. }
  86. }
  87. }
  88. }
  89. assert "multipart/mixed" == msg.get_content_type()
  90. alternative = msg.get_payload()[0]
  91. related = alternative.get_payload()[0]
  92. mime_html, mime_image = related.get_payload()
  93. compare_image_mime(mime_image, mime_html, orig_image=img_bytes, type_="image/jpg")
  94. # Test receivers etc.
  95. headers = {
  96. key: val
  97. for key, val in msg.items()
  98. if key not in ('Message-ID', 'Date')
  99. }
  100. assert {
  101. 'From': 'me@gmail.com',
  102. 'Subject': 'Some news',
  103. 'To': 'you@gmail.com',
  104. #'MIME-Version': '1.0',
  105. 'Content-Type': 'multipart/mixed'
  106. } == headers
  107. @pytest.mark.parametrize(
  108. "get_image_obj", [
  109. pytest.param(get_mpl_fig, id="Matplotlib figure"),
  110. pytest.param(get_pil_image, id="PIL image"),
  111. ]
  112. )
  113. def test_with_image_obj(get_image_obj):
  114. image_obj, image_bytes = get_image_obj()
  115. sender = EmailSender(host=None, port=1234)
  116. msg = sender.get_message(
  117. sender="me@gmail.com",
  118. receivers="you@gmail.com",
  119. subject="Some news",
  120. html='<h1>Hi,</h1> Nice to meet you. Look at this: <img src="{{ my_image }}">',
  121. body_images={"my_image": image_obj}
  122. )
  123. assert "multipart/mixed" == msg.get_content_type()
  124. alternative = msg.get_payload()[0]
  125. related = alternative.get_payload()[0]
  126. mime_html, mime_image = related.get_payload()
  127. compare_image_mime(mime_image, mime_html, orig_image=image_bytes)
  128. # Test receivers etc.
  129. headers = {
  130. key: val
  131. for key, val in msg.items()
  132. if key not in ('Message-ID', 'Date')
  133. }
  134. assert {
  135. 'From': 'me@gmail.com',
  136. 'Subject': 'Some news',
  137. 'To': 'you@gmail.com',
  138. #'MIME-Version': '1.0',
  139. 'Content-Type': 'multipart/mixed'
  140. } == headers
  141. def test_with_image_error():
  142. sender = EmailSender(host=None, port=1234)
  143. with pytest.raises(ValueError):
  144. msg = sender.get_message(
  145. sender="me@gmail.com",
  146. receivers="you@gmail.com",
  147. subject="Some news",
  148. html='<h1>Hi,</h1> Nice to meet you. Look at this: <img src="{{ my_image }}">',
  149. body_images={"my_image": "this is invalid"}
  150. )
  151. invalid_type_obj = type("TempClass", (), {})()
  152. with pytest.raises(TypeError):
  153. msg = sender.get_message(
  154. sender="me@gmail.com",
  155. receivers="you@gmail.com",
  156. subject="Some news",
  157. html='<h1>Hi,</h1> Nice to meet you. Look at this: <img src="{{ my_image }}">',
  158. body_images={"my_image": invalid_type_obj}
  159. )
  160. with pytest.raises(KeyError):
  161. msg = sender.get_message(
  162. sender="me@gmail.com",
  163. receivers="you@gmail.com",
  164. subject="Some news",
  165. html='<h1>Hi,</h1> Nice to meet you. Look at this:>',
  166. body_images={"my_image": {}}
  167. )
  168. @pytest.mark.parametrize(
  169. "get_df,", [
  170. pytest.param(
  171. lambda: pd.DataFrame(
  172. [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
  173. columns=pd.Index(["first", "second", "third"]),
  174. ),
  175. id="Simple dataframe"
  176. ),
  177. pytest.param(
  178. lambda: pd.DataFrame(
  179. [[1], [2], [3]],
  180. columns=pd.Index(["first"]),
  181. ),
  182. id="Single column datafram"
  183. ),
  184. pytest.param(
  185. lambda: pd.DataFrame(
  186. [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
  187. columns=pd.Index(["first", "second", "third"]),
  188. index=pd.Index(["a", "b", "c"], name="category")
  189. ),
  190. id="Simple dataframe with index"
  191. ),
  192. pytest.param(
  193. lambda: pd.DataFrame(
  194. [[1, 2, 3, "a"], [4, 5, 6, "b"], [7, 8, 9, "c"], [10, 11, 12, "d"]],
  195. columns=pd.MultiIndex.from_tuples([("parent a", "child a"), ("parent a", "child b"), ("parent b", "child a"), ("parent c", "child a")], names=["lvl 1", "lvl 2"]),
  196. index=pd.MultiIndex.from_tuples([("row a", "sub a"), ("row a", "sub b"), ("row b", "sub a"), ("row c", "sub a")], names=["cat 1", "cat 2"]),
  197. ),
  198. id="Complex dataframe"
  199. ),
  200. pytest.param(
  201. lambda: pd.DataFrame(
  202. [[1, 2], [4, 5]],
  203. columns=pd.MultiIndex.from_tuples([("col a", "child b", "subchild a"), ("col a", "child b", "subchild a")]),
  204. index=pd.MultiIndex.from_tuples([("row a", "child b", "subchild a"), ("row a", "child b", "subchild a")]),
  205. ),
  206. id="Multiindex end with spanned"
  207. ),
  208. pytest.param(
  209. lambda: pd.DataFrame(
  210. [[1, 2], [4, 5]],
  211. columns=pd.MultiIndex.from_tuples([("col a", "child b", "subchild a"), ("col a", "child b", "subchild a")]),
  212. index=pd.MultiIndex.from_tuples([("row a", "child b", "subchild a"), ("row a", "child b", "subchild a")]),
  213. ).style.set_caption("A caption"),
  214. id="With styler"
  215. ),
  216. pytest.param(
  217. lambda: pd.DataFrame(
  218. [],
  219. columns=pd.Index(["first", "second", "third"]),
  220. ),
  221. id="Empty datafram"
  222. ),
  223. ]
  224. )
  225. def test_with_html_table_no_error(get_df, tmpdir):
  226. pytest.importorskip("pandas")
  227. df = get_df()
  228. sender = EmailSender(host=None, port=1234)
  229. msg = sender.get_message(
  230. sender="me@gmail.com",
  231. receivers="you@gmail.com",
  232. subject="Some news",
  233. html='The table {{my_table}}',
  234. body_tables={"my_table": df}
  235. )
  236. assert "multipart/mixed" == msg.get_content_type()
  237. alternative = msg.get_payload()[0]
  238. mime_html = alternative.get_payload()[0]
  239. #mime_text = msg.get_payload()[0]
  240. html = remove_extra_lines(mime_html.get_payload()).replace("=20", "").replace('"3D', "")
  241. #tmpdir.join("email.html").write(html)
  242. # TODO: Test the HTML is as required
  243. assert html
  244. def test_embed_tables_pandas_missing():
  245. sender = EmailSender(host=None, port=1234)
  246. from redmail.email import body
  247. pd_mdl = body.pd # This may be already None if env does not have Pandas
  248. try:
  249. # src uses this to reference Pandas (if missing --> None)
  250. body.pd = None
  251. with pytest.raises(ImportError):
  252. msg = sender.get_message(
  253. sender="me@gmail.com",
  254. receivers="you@gmail.com",
  255. subject="Some news",
  256. html='The table {{my_table}}',
  257. body_tables={"my_table": [{"col1": 1, "col2": "a"}, {"col1": 2, "col2": "b"}]}
  258. )
  259. finally:
  260. body.pd = pd_mdl
  261. def test_styled_tables_dependency_missing():
  262. pd = pytest.importorskip("pandas")
  263. sender = EmailSender(host=None, port=1234)
  264. from redmail.email import body
  265. mdl = body.css_inline # This may be already None if env does not have Pandas
  266. try:
  267. # src uses this to reference Pandas (if missing --> None)
  268. body.css_inline = None
  269. style = pd.DataFrame(
  270. {"col1": ["a", "b", "c"], "col2": [1, 2, 3]}
  271. ).style.set_caption("A caption")
  272. with pytest.raises(ImportError):
  273. msg = sender.get_message(
  274. sender="me@gmail.com",
  275. receivers="you@gmail.com",
  276. subject="Some news",
  277. html='The table {{my_table}}',
  278. body_tables={"my_table": style}
  279. )
  280. finally:
  281. body.css_inline = mdl