from email.message import EmailMessage from typing import Callable, Dict, Optional, Union import jinja2 from redmail.email.attachment import Attachments from redmail.email.body import HTMLBody, TextBody from redmail.models import EmailAddress, Error from .envs import get_span, is_last_group_row import smtplib from pathlib import Path from platform import node from getpass import getuser import datetime class EmailSender: """Email sender Parameters ---------- host : str SMTP host address. port : int Port to the SMTP server. user : str, callable User name to authenticate on the server. password : str, callable User password to authenticate on the server. Examples -------- .. code-block:: python mymail = EmailSender(server="smtp.mymail.com", port=123) mymail.set_credentials( user=lambda: read_yaml("C:/config/email.yaml")["mymail"]["user"], password=lambda: read_yaml("C:/config/email.yaml")["mymail"]["password"] ) mymail.send( subject="Important email", html="

Important

", body_images={'nice_pic': 'path/to/pic.jpg'}, ) """ default_html_theme = "modest.html" default_text_theme = "pandas.txt" templates_html = jinja2.Environment(loader=jinja2.FileSystemLoader(str(Path(__file__).parent / "templates/html"))) templates_html_table = jinja2.Environment(loader=jinja2.FileSystemLoader(str(Path(__file__).parent / "templates/html/table"))) templates_text = jinja2.Environment(loader=jinja2.FileSystemLoader(str(Path(__file__).parent / "templates/text"))) templates_text_table = jinja2.Environment(loader=jinja2.FileSystemLoader(str(Path(__file__).parent / "templates/text/table"))) # Set globals templates_html_table.globals["get_span"] = get_span templates_text_table.globals["get_span"] = get_span templates_html_table.globals["is_last_group_row"] = is_last_group_row templates_text_table.globals["is_last_group_row"] = is_last_group_row attachment_encoding = 'UTF-8' def __init__(self, host:str, port:int, user_name:str=None, password:str=None): self.host = host self.port = port self.user_name = user_name self.password = password # Defaults self.sender = None self.receivers = None self.subject = None self.text = None self.html = None self.html_template = None self.text_template = None def send(self, **kwargs): """Send an email message. Parameters ---------- subject : str Subject of the email. receivers : list, optional Receivers of the email. sender : str, optional Sender of the email. cc : list, optional Cc or Carbon Copy of the email. Extra recipients of the email. bcc : list, optional Blind Carbon Copy of the email. Extra recipients of the email that don't see who else got the email. html : str, optional HTML body of the email. May contain Jinja templated variables of the tables, images and other variables. text_body : str, optional Text body of the email. body_images : dict of bytes, path-likes and figures, optional HTML images to embed with the html. The key should be as Jinja variables in the html and the values represent images (path to an image, bytes of an image or image object). body_tables : Dict[str, pd.DataFrame], optional HTML tables to embed with the html. The key should be as Jinja variables in the html and the values are Pandas DataFrames. html_params : dict, optional Extra parameters passed to html_table as Jinja parameters. Examples -------- >>> sender = EmailSender(host="myserver", port=1234) >>> sender.send( sender="me@gmail.com", receiver="you@gmail.com", subject="Some news", html='

Hi,

Nice to meet you. Look at this: ', body_images={"my_image": Path("C:/path/to/img.png")} ) >>> sender.send( sender="me@gmail.com", receiver="you@gmail.com", subject="Some news", html='

Hi {{ name }},

Nice to meet you. Look at this table: ', body_images={"my_image": Path("C:/path/to/img.png")}, html_params={"name": "Jack"}, ) Returns ------- EmailMessage Email message. """ msg = self.get_message(**kwargs) self.send_message(msg) return msg def get_message(self, subject:str=None, receivers:list=None, sender:str=None, cc:list=None, bcc:list=None, html:str=None, text:str=None, html_template=None, text_template=None, body_images:Dict[str, str]=None, body_tables:Dict[str, str]=None, body_params:dict=None, attachments:dict=None) -> EmailMessage: """Get the email message.""" subject = subject or self.subject sender = sender or self.sender or self.user_name receivers = receivers or self.receivers html = html or self.html text = text or self.text html_template = html_template or self.html_template text_template = text_template or self.text_template if subject is None: raise ValueError("Email must have a subject") msg = self._create_body( subject=subject, sender=sender, receivers=receivers, cc=cc, bcc=bcc, ) if text is not None or text_template is not None: body = TextBody( template=self.get_text_template(text_template), table_template=self.get_text_table_template(), ) body.attach( msg, text, tables=body_tables, jinja_params=self.get_text_params(extra=body_params, sender=sender), ) if html is not None or html_template is not None: body = HTMLBody( template=self.get_html_template(html_template), table_template=self.get_html_table_template(), ) body.attach( msg, html=html, images=body_images, tables=body_tables, jinja_params=self.get_html_params(extra=body_params, sender=sender) ) if attachments: att = Attachments(attachments, encoding=self.attachment_encoding) att.attach(msg) return msg def _create_body(self, subject, sender, receivers=None, cc=None, bcc=None) -> EmailMessage: msg = EmailMessage() msg["from"] = sender msg["subject"] = subject # To whoom the email goes if receivers: msg["to"] = receivers if cc: msg['cc'] = cc if bcc: msg['bcc'] = bcc return msg def send_message(self, msg): "Send the created message" user = self.user_name password = self.password server = smtplib.SMTP(self.host, self.port) server.starttls() if user is not None or password is not None: server.login(user, password) server.send_message(msg) server.quit() def get_params(self, sender:str): "Get Jinja parametes passed to template" # TODO: Add receivers to params return { "node": node(), "user": getuser(), "now": datetime.datetime.now(), "sender": EmailAddress(sender), } def get_html_params(self, extra:Optional[dict]=None, **kwargs): params = self.get_params(**kwargs) params.update({ "error": Error(content_type='html-inline') }) if extra: params.update(extra) return params def get_text_params(self, extra:Optional[dict]=None, **kwargs): params = self.get_params(**kwargs) params.update({ "error": Error(content_type='text') }) if extra: params.update(extra) return params def get_html_table_template(self, layout=None) -> jinja2.Template: layout = self.default_html_theme if layout is None else layout if layout is None: return None return self.templates_html_table.get_template(layout) def get_html_template(self, layout=None) -> jinja2.Template: if layout is None: return None return self.templates_html.get_template(layout) def get_text_table_template(self, layout=None) -> jinja2.Template: layout = self.default_text_theme if layout is None else layout if layout is None: return None return self.templates_text_table.get_template(layout) def get_text_template(self, layout=None) -> jinja2.Template: if layout is None: return None return self.templates_text.get_template(layout) def set_template_paths(self, html=None, text=None, html_table=None, text_table=None): """Create Jinja envs for body templates using given paths This is a shortcut for manually setting them like: .. clode-block:: python sender.templates_html = jinja2.Environment(loader=jinja2.FileSystemLoader(...)) sender.templates_text = jinja2.Environment(loader=jinja2.FileSystemLoader(...)) ... """ if html is not None: self.templates_html = jinja2.Environment(loader=jinja2.FileSystemLoader(html)) if text is not None: self.templates_text = jinja2.Environment(loader=jinja2.FileSystemLoader(text)) if html_table is not None: self.templates_html_table = jinja2.Environment(loader=jinja2.FileSystemLoader(html_table)) if text_table is not None: self.templates_text_table = jinja2.Environment(loader=jinja2.FileSystemLoader(text_table))