sender.py 10 KB

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