diff --git a/src/api/login.py b/src/api/login.py index 2431548..1bb745d 100644 --- a/src/api/login.py +++ b/src/api/login.py @@ -14,7 +14,7 @@ SessionConfig, r, SecurityConfig, - send_email, + queue_email, all_ids, regenerate_ids, SignupConfig, @@ -75,8 +75,7 @@ async def forgot_password( } ), ) - background_tasks.add_task( - send_email, + queue_email( "ChangePassword", user["email"], code=unique_id, diff --git a/src/api/signup.py b/src/api/signup.py index 69985c4..8cf4e1c 100644 --- a/src/api/signup.py +++ b/src/api/signup.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, Response, BackgroundTasks, HTTPException, Request from api.model import UserSignupRequest, LoginResponse, ConfirmEmailCodeRequest -from tools import send_email +from tools import queue_email import json from tools import SignupConfig, SessionConfig from tools import all_ids, regenerate_ids, r @@ -66,8 +66,7 @@ async def signup( ) # Generate and send confirmation email - background_tasks.add_task( - send_email, + queue_email( "ConfirmEmail", signup_form.email, code=unique_id, diff --git a/src/crud/user.py b/src/crud/user.py index 5f5e2a2..cf67fab 100644 --- a/src/crud/user.py +++ b/src/crud/user.py @@ -2,7 +2,7 @@ from tools import ( users_collection, SignupConfig, - send_email, + queue_email, InternalConfig, insecure_cols, AccountFeaturesConfig, @@ -155,15 +155,13 @@ def update_public_user( # Get a unique ID for confirmation email unique_id = all_ids.pop() # Generate and send confirmation email - background_tasks.add_task( - send_email, + queue_email( "ConfirmEmail", data["email"], **existing_user, code=unique_id, time=SignupConfig.conf_code_expiry, ) - r.setex( "emailchange:" + str(existing_user["_id"]), SignupConfig.conf_code_expiry * 60, @@ -335,7 +333,7 @@ def create_user( # User Created (Create Session Token and send Welcome Email) session_token = sessions.create_login_session(user_db.inserted_id, request) if SignupConfig.enable_welcome_email: - background_tasks.add_task(send_email, "WelcomeEmail", data["email"], **data) + queue_email("WelcomeEmail", data["email"], **data) return session_token diff --git a/src/tools/__init__.py b/src/tools/__init__.py index c20e4f6..1cdc6c2 100644 --- a/src/tools/__init__.py +++ b/src/tools/__init__.py @@ -15,7 +15,7 @@ EmailConfig, SignupConfig, ) -from .mail import send_email, broadcast_emails +from .mail import broadcast_emails, queue_email from .confirmation_codes import all_ids, regenerate_ids __all__ = [ @@ -30,7 +30,7 @@ "AccountFeaturesConfig", "insecure_cols", "SecurityConfig", - "send_email", + "queue_email", "broadcast_emails", "all_ids", "regenerate_ids", diff --git a/src/tools/mail.py b/src/tools/mail.py index a012b2b..78e4e80 100644 --- a/src/tools/mail.py +++ b/src/tools/mail.py @@ -6,13 +6,31 @@ from threading import Lock from tools import users_collection, InternalConfig import importlib +import re +import threading +import queue -smtp = smtplib.SMTP_SSL(EmailConfig.smtp_host, EmailConfig.smtp_port) +smtp = smtplib.SMTP_SSL(EmailConfig.smtp_host, EmailConfig.smtp_port, timeout=1) +email_queue = queue.Queue() + + +def queue_email(template_name: str, to: str, **kwargs): + email_queue.put_nowait(lambda: send_email(template_name, to, **kwargs)) + + +def fill_placeholders(template: str, data: dict) -> str: + pattern = re.compile(r"\{\{\s*(\w+)\s*\}\}") + result = pattern.sub( + lambda match: str(data.get(match.group(1), match.group(0))), template + ) + return result def load_template(template_name: str, **kwargs) -> str: with open(f"/src/app/config/email/{template_name}.html", "r") as file: template = file.read() + if template.find("") == -1: + raise ValueError("Template does not contain a title tag") subject = template[template.find("<title>") + 7 : template.find("")] # Try to find template_name + ".py" and execute the function "preprocess" try: @@ -29,7 +47,7 @@ def load_template(template_name: str, **kwargs) -> str: logger.error( f"Failed to execute preprocess function in {template_name}.py: {e}" ) - formatted_template = template.format(**kwargs) + formatted_template = fill_placeholders(template, kwargs) return formatted_template, subject @@ -48,7 +66,6 @@ def send_email(template_name: str, to: str, **kwargs): # Send email smtp.login(EmailConfig.login_usr, EmailConfig.login_pwd) smtp.send_message(msg) - smtp.quit() logger.info(f"Email sent to {to} with subject: {subject}") @@ -72,3 +89,17 @@ def broadcast_emails( raise e finally: email_task_lock.release_lock() + + +def email_worker(): + logger.info("\u001b[32m- E-Mail Worker started\u001b[0m") + while True: + email_task = email_queue.get() + try: + email_task() + except Exception as e: + logger.error(f"Failed to send email: {e}") + email_queue.task_done() + + +threading.Thread(target=email_worker).start()