sender.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. from email.message import EmailMessage
  2. from typing import Callable, Dict, Union
  3. import jinja2
  4. from redmail.email.body import HTMLBody, TextBody
  5. from .address import EmailAddress
  6. from .envs import get_span, is_last_group_row
  7. import smtplib
  8. from pathlib import Path
  9. from platform import node
  10. from getpass import getuser
  11. import datetime
  12. class EmailSender:
  13. """Email sender
  14. Parameters
  15. ----------
  16. server : str
  17. SMTP server address.
  18. port : int
  19. Port to the SMTP server.
  20. user : str, callable
  21. User name to authenticate on the server.
  22. password : str, callable
  23. User password to authenticate on the server.
  24. Examples
  25. --------
  26. mymail = EmailSender(server="smtp.mymail.com", port=123)
  27. mymail.set_credentials(
  28. user=lambda: read_yaml("C:/config/email.yaml")["mymail"]["user"],
  29. password=lambda: read_yaml("C:/config/email.yaml")["mymail"]["password"]
  30. )
  31. mymail.send_email(
  32. subject="Important email",
  33. html_body="<h1>Important</h1><img src={{ nice_pic }}>",
  34. body_images={'nice_pic': 'path/to/pic.jpg'},
  35. )
  36. """
  37. default_html_theme = "modest.html"
  38. default_text_theme = "pandas.txt"
  39. templates_html = jinja2.Environment(loader=jinja2.FileSystemLoader(Path(__file__).parent / "templates/html"))
  40. templates_html_table = jinja2.Environment(loader=jinja2.FileSystemLoader(Path(__file__).parent / "templates/html/table"))
  41. templates_text = jinja2.Environment(loader=jinja2.FileSystemLoader(Path(__file__).parent / "templates/text"))
  42. templates_text_table = jinja2.Environment(loader=jinja2.FileSystemLoader(Path(__file__).parent / "templates/text/table"))
  43. # Set globals
  44. templates_html_table.globals["get_span"] = get_span
  45. templates_text_table.globals["get_span"] = get_span
  46. templates_html_table.globals["is_last_group_row"] = is_last_group_row
  47. templates_text_table.globals["is_last_group_row"] = is_last_group_row
  48. def __init__(self, server:str, port:int, user_name:str=None, password:str=None):
  49. self.server = server
  50. self.port = port
  51. self.user_name = user_name
  52. self.password = password
  53. def send_email(self, **kwargs):
  54. """Send an email message.
  55. Parameters
  56. ----------
  57. subject : str
  58. Subject of the email.
  59. receiver : list, optional
  60. Receivers of the email.
  61. sender : str, optional
  62. Sender of the email.
  63. cc : list, optional
  64. Cc or Carbon Copy of the email.
  65. Extra recipients of the email.
  66. bcc : list, optional
  67. Blind Carbon Copy of the email.
  68. Extra recipients of the email that
  69. don't see who else got the email.
  70. html_body : str, optional
  71. HTML body of the email. May contain
  72. Jinja templated variables of the
  73. tables, images and other variables.
  74. text_body : str, optional
  75. Text body of the email.
  76. body_images : dict of bytes, path-likes and figures, optional
  77. HTML images to embed with the html_body. The key should be
  78. as Jinja variables in the html_body and the values represent
  79. images (path to an image, bytes of an image or image object).
  80. body_tables : Dict[str, pd.DataFrame], optional
  81. HTML tables to embed with the html_body. The key should be
  82. as Jinja variables in the html_body and the values are Pandas
  83. DataFrames.
  84. html_params : dict, optional
  85. Extra parameters passed to html_table as Jinja parameters.
  86. Examples
  87. --------
  88. >>> sender = EmailSender(server="myserver", port=1234)
  89. >>> sender.send_email(
  90. sender="me@gmail.com",
  91. receiver="you@gmail.com",
  92. subject="Some news",
  93. html_body='<h1>Hi,</h1> Nice to meet you. Look at this: <img src="{{ my_image }}">',
  94. body_images={"my_image": Path("C:/path/to/img.png")}
  95. )
  96. >>> sender.send_email(
  97. sender="me@gmail.com",
  98. receiver="you@gmail.com",
  99. subject="Some news",
  100. html_body='<h1>Hi {{ name }},</h1> Nice to meet you. Look at this table: <img src="{{ my_table }}">',
  101. body_images={"my_image": Path("C:/path/to/img.png")},
  102. html_params={"name": "Jack"},
  103. )
  104. Returns
  105. -------
  106. EmailMessage
  107. Email message.
  108. """
  109. msg = self.get_message(**kwargs)
  110. self.send_message(msg)
  111. return msg
  112. def get_message(self,
  113. subject:str,
  114. receiver:list=None,
  115. sender:str=None,
  116. cc:list=None,
  117. bcc:list=None,
  118. html_body:str=None,
  119. text_body:str=None,
  120. html_template=None,
  121. text_template=None,
  122. body_images:Dict[str, str]=None,
  123. body_tables:Dict[str, str]=None,
  124. body_params:dict=None) -> EmailMessage:
  125. """Get the email message."""
  126. sender = sender or self.user_name
  127. msg = self._create_body(
  128. subject=subject,
  129. sender=sender,
  130. receiver=receiver,
  131. cc=cc,
  132. bcc=bcc,
  133. )
  134. jinja_params = self.get_template_params(sender=sender)
  135. jinja_params.update(body_params if body_params is not None else {})
  136. if text_body is not None or text_template is not None:
  137. body = TextBody(
  138. template=self.get_text_body_template(text_template),
  139. table_template=self.get_text_table_template(),
  140. )
  141. body.attach(
  142. msg,
  143. text_body,
  144. tables=body_tables,
  145. jinja_params=jinja_params,
  146. )
  147. if html_body is not None or html_template is not None:
  148. body = HTMLBody(
  149. template=self.get_html_body_template(html_template),
  150. table_template=self.get_html_table_template(),
  151. )
  152. body.attach(
  153. msg,
  154. html=html_body,
  155. images=body_images,
  156. tables=body_tables,
  157. jinja_params=jinja_params
  158. )
  159. return msg
  160. def _create_body(self, subject, sender, receiver=None, cc=None, bcc=None) -> EmailMessage:
  161. msg = EmailMessage()
  162. msg["from"] = sender
  163. msg["subject"] = subject
  164. # To whoom the email goes
  165. if receiver:
  166. msg["to"] = receiver
  167. if cc:
  168. msg['cc'] = cc
  169. if bcc:
  170. msg['bcc'] = bcc
  171. return msg
  172. def send_message(self, msg):
  173. "Send the created message"
  174. user = self.user_name
  175. password = self.password
  176. server = smtplib.SMTP(self.server, self.port)
  177. server.starttls()
  178. server.login(user, password)
  179. server.send_message(msg)
  180. server.quit()
  181. def get_template_params(self, sender:str):
  182. "Get Jinja parametes passed to template"
  183. # TODO: Add receivers to params
  184. return {
  185. "node": node(),
  186. "user": getuser(),
  187. "now": datetime.datetime.now(),
  188. "sender": EmailAddress(sender),
  189. }
  190. def get_html_table_template(self, layout=None) -> jinja2.Template:
  191. layout = self.default_html_theme if layout is None else layout
  192. if layout is None:
  193. return None
  194. return self.templates_html_table.get_template(layout)
  195. def get_html_body_template(self, layout=None) -> jinja2.Template:
  196. if layout is None:
  197. return None
  198. return self.templates_html.get_template(layout)
  199. def get_text_table_template(self, layout=None) -> jinja2.Template:
  200. layout = self.default_text_theme if layout is None else layout
  201. if layout is None:
  202. return None
  203. return self.templates_text_table.get_template(layout)
  204. def get_text_body_template(self, layout=None) -> jinja2.Template:
  205. if layout is None:
  206. return None
  207. return self.templates_text.get_template(layout)
  208. def set_template_paths(self, html=None, text=None, html_table=None, text_table=None):
  209. """Create Jinja envs for body templates using given paths
  210. This is a shortcut for manually setting them like:
  211. .. clode-block:: python
  212. sender.templates_html = jinja2.Environment(loader=jinja2.FileSystemLoader(...))
  213. sender.templates_text = jinja2.Environment(loader=jinja2.FileSystemLoader(...))
  214. ...
  215. """
  216. if html is not None:
  217. self.templates_html = jinja2.Environment(loader=jinja2.FileSystemLoader(html))
  218. if text is not None:
  219. self.templates_text = jinja2.Environment(loader=jinja2.FileSystemLoader(text))
  220. if html_table is not None:
  221. self.templates_html_table = jinja2.Environment(loader=jinja2.FileSystemLoader(html_table))
  222. if text_table is not None:
  223. self.templates_text_table = jinja2.Environment(loader=jinja2.FileSystemLoader(text_table))