"""EmailService over a provider interface (Resend in prod, console locally). Rule: no em dashes in any copy. Broadcast bodies are plain text and get HTML escaped. """ import hashlib import hmac import html import logging from abc import ABC, abstractmethod from dataclasses import dataclass from app.config import settings log = logging.getLogger(__name__) @dataclass class OutgoingEmail: to: str subject: str html: str text: str headers: dict[str, str] | None = None class BaseEmailProvider(ABC): @abstractmethod def send(self, email: OutgoingEmail) -> str | None: ... class ResendEmailProvider(BaseEmailProvider): def __init__(self, api_key: str): import resend resend.api_key = api_key self._resend = resend def send(self, email: OutgoingEmail) -> str | None: params = { "from": settings.from_email, "to": [email.to], "subject": email.subject, "html": email.html, "text": email.text, } if email.headers: params["headers"] = email.headers result = self._resend.Emails.send(params) return result.get("id") if isinstance(result, dict) else None class ConsoleEmailProvider(BaseEmailProvider): sent: list[OutgoingEmail] = [] def send(self, email: OutgoingEmail) -> str | None: ConsoleEmailProvider.sent.append(email) # Warning, not info: locally this is the only place a sign up or reset code shows up. log.warning("[email] to=%s subject=%s\n%s", email.to, email.subject, email.text) return None def make_unsubscribe_token(user_id: str) -> str: sig = hmac.new(settings.unsubscribe_secret.encode(), user_id.encode(), hashlib.sha256).hexdigest()[:32] return f"{user_id}.{sig}" def verify_unsubscribe_token(token: str) -> str | None: user_id, _, _sig = token.partition(".") if user_id and hmac.compare_digest(make_unsubscribe_token(user_id), token): return user_id return None def _layout(title: str, body_html: str, footer_html: str = "") -> str: # Inline styles only; email clients ignore