From cea615fe633aad5f62d3fdeeee8a1cf02d382844 Mon Sep 17 00:00:00 2001 From: Max Metz Date: Tue, 30 Jul 2024 11:41:46 +0200 Subject: [PATCH] building out and preparing the EmailHandler's adaptive content. Using HTML formatting. --- src/server/BreCal/services/email_handling.py | 67 ++++++++++++++++++-- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/src/server/BreCal/services/email_handling.py b/src/server/BreCal/services/email_handling.py index e06021d..ba21027 100644 --- a/src/server/BreCal/services/email_handling.py +++ b/src/server/BreCal/services/email_handling.py @@ -12,6 +12,12 @@ import email from email.mime.multipart import MIMEMultipart from email.mime.application import MIMEApplication +import subprocess +import sys +import time +from tempfile import NamedTemporaryFile +import json +from cryptography.fernet import Fernet class EmailHandler(): """ @@ -36,7 +42,7 @@ class EmailHandler(): self.mail_port = mail_port self.mail_address = mail_address - self.server = smtplib.SMTP_SSL(self.mail_server, self.mail_port) # alternatively, SMTP + self.server = smtplib.SMTP_SSL(self.mail_server, self.mail_port) # alternatively, use smtplib.SMTP def check_state(self): """check, whether the server login took place and is open.""" @@ -59,7 +65,7 @@ class EmailHandler(): user = self.server.__dict__.get("user",None) return user is not None - def login(self, interactive:bool=True): + def login(self, interactive:bool=True, pwd=typing.Optional[bytes]): """ login on the determined mail server's mail address. By default, this function opens an interactive window to type the password without echoing (printing '*******' instead of readable characters). @@ -71,19 +77,30 @@ class EmailHandler(): (status_code, status_msg) = self.server.login(self.mail_address, password=getpass()) else: # fernet + password file - raise NotImplementedError() + assert pwd is not None, f"when non-interactive login is selected, one must provide a password" + assert isinstance(pwd, bytes), "please provide only byte-encrypted secure passwords. Those should be Fernet encoded." + + fernet_key_path = os.path.join(os.path.expanduser("~"), "secure", "email_login_fernet_key.json") + assert os.path.exists(fernet_key_path), f"cannot find fernet key file at path: {fernet_key_path}" + + with open(fernet_key_path, "r") as jr: + json_content = json.load(jr) + assert "fernet_key" in json_content + key = json_content.get("fernet_key").encode("utf8") + + (status_code, status_msg) = self.server.login(self.mail_address, password=Fernet(key).decrypt(pwd).decode()) return (status_code, status_msg) # should be: (235, b'2.7.0 Authentication successful') - def create_email(self, subject:str, message_body:str)->EmailMessage: + def create_email(self, subject:str, message_body:str, subtype:typing.Optional[str]=None, sender_address:typing.Optional[str]=None)->EmailMessage: """ Create an EmailMessage object, which contains the Email's header ("Subject"), content ("Message Body") and the sender's address ("From"). The EmailMessage object does not contain the recipients yet, as these will be defined upon sending the Email. """ msg = EmailMessage() msg["Subject"] = subject - msg["From"] = self.mail_address + msg["From"] = self.mail_address if sender_address is None else sender_address #msg["To"] = email_tgts # will be defined in self.send_email - msg.set_content(message_body) + msg.set_content(message_body, subtype=subtype) if subtype is not None else msg.set_content(message_body, subtype=subtype) return msg def build_recipients(self, email_tgts:list[str]): @@ -172,3 +189,41 @@ class EmailHandler(): self.server.quit() return + def preview_html_content(self, html:str, delete_after_s_seconds:typing.Optional[float]=None, file_path_dict:dict={}): + """ + Given an HTML-formatted text string, this method creates a temporary .html file and + spawns the local default webbrowser to preview the content. + + This method is useful to design or debug HTML files before sending them via the EmailHandler. + + When providing a floating point to the 'delete_after_s_seconds' argument, the temporary file will be + automatically removed after those seconds. The python script is blocked for the duration (using time.sleep) + + args: + file_path_dict: + it is common to refer to images via 'cid:FILE_ID' within the HTML content. The preview cannot + display this, as the attached files are missing. To circumvent this, one can provide a dictionary, which + replaced the referred key + (e.g., 'cid:FILE_ID') + with the actual path, such as a logo or remote absolute path + (e.g., 'file:///C:/Users/User/brecal/misc/logo_bremen_calling.png') + + Inspired by: https://stackoverflow.com/questions/53452322/is-there-a-way-that-i-can-preview-my-html-file + User: https://stackoverflow.com/users/355230/martineau + """ + for k, v in file_path_dict.items(): + html = html.replace(k, v) + + with NamedTemporaryFile(mode='wt', suffix='.html', delete=False, encoding="utf-8") as temp_file: + temp_file.write(html) + temp_filename = temp_file.name # Save temp file's name. + + command = f"{sys.executable} -m webbrowser -n {temp_filename}" + browser = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + if delete_after_s_seconds is not None: + assert isinstance(delete_after_s_seconds, float) + time.sleep(delete_after_s_seconds) + if os.path.exists(temp_filename): + os.remove(temp_filename) + return \ No newline at end of file