105 lines
3.8 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from email.message import EmailMessage
import smtplib
from typing import Iterable
from ..settings import settings
class MailerError(RuntimeError):
pass
@dataclass(frozen=True)
class SentEmail:
ok: bool
detail: str = ""
class Mailer:
"""Send onboarding and notification email through configured SMTP."""
def __init__(self) -> None:
self._host = settings.smtp_host
self._port = settings.smtp_port
self._username = settings.smtp_username
self._password = settings.smtp_password
self._from_addr = settings.smtp_from
self._starttls = settings.smtp_starttls
self._use_tls = settings.smtp_use_tls
self._timeout = settings.smtp_timeout_sec
def send(self, subject: str, to_addrs: Iterable[str], text_body: str, html_body: str | None = None) -> SentEmail:
if not self._host:
raise MailerError("smtp host not configured")
message = EmailMessage()
message["Subject"] = subject
message["From"] = self._from_addr
message["To"] = ", ".join(to_addrs)
message.set_content(text_body)
if html_body:
message.add_alternative(html_body, subtype="html")
try:
if self._use_tls:
server: smtplib.SMTP = smtplib.SMTP_SSL(self._host, self._port, timeout=self._timeout)
else:
server = smtplib.SMTP(self._host, self._port, timeout=self._timeout)
with server:
server.ehlo()
if self._starttls:
server.starttls()
server.ehlo()
if self._username:
server.login(self._username, self._password)
server.send_message(message)
return SentEmail(ok=True, detail="sent")
except Exception as exc:
raise MailerError(str(exc)) from exc
def send_welcome(self, to_addr: str, request_code: str, onboarding_url: str, username: str | None = None) -> SentEmail:
display = username or "there"
subject = "Welcome to Titan Lab"
text_body = "\n".join(
[
f"Hi {display},",
"",
"Your Titan Lab access is approved.",
f"Complete onboarding here: {onboarding_url}",
"",
f"Request code: {request_code}",
"",
"If you did not request access, ignore this email.",
"",
"— Titan Lab",
]
)
html_body = f"""
<html>
<body style="font-family: Arial, sans-serif; background:#f6f6f4; padding:24px;">
<table style="max-width:600px; background:#ffffff; padding:24px; border-radius:12px; margin:0 auto; box-shadow:0 6px 24px rgba(0,0,0,0.08);">
<tr><td>
<h1 style="margin:0 0 8px; font-size:24px; color:#111827;">Welcome to Titan Lab</h1>
<p style="margin:0 0 16px; color:#4b5563;">Hi {display}, your access has been approved.</p>
<p style="margin:0 0 24px; color:#111827;">
<a href="{onboarding_url}" style="display:inline-block; background:#111827; color:#ffffff; padding:10px 16px; border-radius:8px; text-decoration:none;">
Start onboarding
</a>
</p>
<p style="margin:0 0 8px; color:#6b7280; font-size:14px;">Request code: <strong>{request_code}</strong></p>
<p style="margin:0; color:#9ca3af; font-size:12px;">If you did not request access, ignore this email.</p>
</td></tr>
</table>
</body>
</html>
""".strip()
return self.send(subject, [to_addr], text_body, html_body)
mailer = Mailer()