From 9c92eb79531711b68f252e3130f4b3d3c627ba6e Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:19:22 -0800 Subject: [PATCH] feat: Add SMTP support for self-hosted Postal mail server --- backend/services/email_service.py | 55 ++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/backend/services/email_service.py b/backend/services/email_service.py index b933391..96b3b7b 100644 --- a/backend/services/email_service.py +++ b/backend/services/email_service.py @@ -1,9 +1,12 @@ """ -Email Service - Mailgun (primary) with AWS SES fallback +Email Service - Postal SMTP (primary), Mailgun, or AWS SES fallback """ import os import httpx import secrets +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart from datetime import datetime, timedelta from typing import Optional @@ -18,7 +21,14 @@ except ImportError: class EmailService: def __init__(self): - # Mailgun settings (primary) + # Postal SMTP settings (primary - self-hosted) + self.smtp_host = os.getenv("SMTP_HOST") + self.smtp_port = int(os.getenv("SMTP_PORT", "25")) + self.smtp_username = os.getenv("SMTP_USERNAME") + self.smtp_password = os.getenv("SMTP_PASSWORD") + self.smtp_use_tls = os.getenv("SMTP_USE_TLS", "true").lower() == "true" + + # Mailgun settings (alternative) self.mailgun_api_key = os.getenv("MAILGUN_API_KEY") self.mailgun_domain = os.getenv("MAILGUN_DOMAIN") self.mailgun_api_base = os.getenv("MAILGUN_API_BASE", "https://api.mailgun.net/v3") @@ -32,8 +42,11 @@ class EmailService: self.email_from = os.getenv("EMAIL_FROM", "noreply@elmeg.xyz") self.frontend_url = os.getenv("FRONTEND_URL", "https://elmeg.xyz") - # Determine which provider to use - if self.mailgun_api_key and self.mailgun_domain: + # Determine which provider to use (priority: SMTP -> Mailgun -> SES -> Dummy) + if self.smtp_host and self.smtp_username and self.smtp_password: + self.provider = "smtp" + print(f"Email service: Using SMTP ({self.smtp_host}:{self.smtp_port})") + elif self.mailgun_api_key and self.mailgun_domain: self.provider = "mailgun" print(f"Email service: Using Mailgun ({self.mailgun_domain})") elif BOTO3_AVAILABLE and self.aws_access_key_id and self.aws_secret_access_key: @@ -51,13 +64,45 @@ class EmailService: def send_email(self, to_email: str, subject: str, html_content: str, text_content: str): """Send an email using configured provider""" - if self.provider == "mailgun": + if self.provider == "smtp": + return self._send_smtp(to_email, subject, html_content, text_content) + elif self.provider == "mailgun": return self._send_mailgun(to_email, subject, html_content, text_content) elif self.provider == "ses": return self._send_ses(to_email, subject, html_content, text_content) else: return self._send_dummy(to_email, subject, text_content) + def _send_smtp(self, to_email: str, subject: str, html_content: str, text_content: str): + """Send email via SMTP (Postal or any SMTP server)""" + try: + # Create message + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = f"Elmeg <{self.email_from}>" + msg["To"] = to_email + + # Attach text and HTML parts + msg.attach(MIMEText(text_content, "plain")) + msg.attach(MIMEText(html_content, "html")) + + # Connect and send + if self.smtp_use_tls: + server = smtplib.SMTP(self.smtp_host, self.smtp_port) + server.starttls() + else: + server = smtplib.SMTP(self.smtp_host, self.smtp_port) + + server.login(self.smtp_username, self.smtp_password) + server.sendmail(self.email_from, to_email, msg.as_string()) + server.quit() + + print(f"Email sent via SMTP to {to_email}") + return True + except Exception as e: + print(f"Error sending email via SMTP: {e}") + return False + def _send_mailgun(self, to_email: str, subject: str, html_content: str, text_content: str): """Send email via Mailgun API""" try: