105 lines
3.8 KiB
Python
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()
|