import asyncio import smtplib import ssl from email.message import EmailMessage from . import vault # Receipt email. SMTP creds live in the vault (admin-only). stdlib smtplib in a thread so the # event loop isn't blocked — no new dependency. ponytail: one sender, swap to API mail if needed. class MailUnconfigured(Exception): pass async def _creds(db): host = await vault.get_secret(db, "smtp_host") if not host: raise MailUnconfigured("SMTP not configured — set smtp_* in admin → Connections") user = await vault.get_secret(db, "smtp_user") or "" return { "host": host.strip(), "port": int((await vault.get_secret(db, "smtp_port") or "587").strip() or 587), "user": user, "pass": await vault.get_secret(db, "smtp_pass") or "", "from": (await vault.get_secret(db, "smtp_from") or user).strip(), "from_name": await vault.get_secret(db, "smtp_from_name") or "RecordGod", } def _send_sync(c, to, subject, html): msg = EmailMessage() msg["Subject"] = subject msg["From"] = f'{c["from_name"]} <{c["from"]}>' if c["from_name"] else c["from"] msg["To"] = to msg.set_content("Your receipt is below — view this email in an HTML-capable client.") msg.add_alternative(html, subtype="html") ctx = ssl.create_default_context() if c["port"] == 465: with smtplib.SMTP_SSL(c["host"], c["port"], context=ctx, timeout=20) as s: if c["user"]: s.login(c["user"], c["pass"]) s.send_message(msg) else: with smtplib.SMTP(c["host"], c["port"], timeout=20) as s: s.ehlo() try: s.starttls(context=ctx) s.ehlo() except smtplib.SMTPNotSupportedError: pass if c["user"]: s.login(c["user"], c["pass"]) s.send_message(msg) async def send_mail(db, to, subject, html): c = await _creds(db) await asyncio.to_thread(_send_sync, c, to, subject, html) return c["from"]