sender.py 11 KB

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