"""Transactional email via Resend (stdlib only) + professional templates. If ``RESEND_API_KEY`` is unset, emails are logged instead of sent (dev mode). A 403 from Resend almost always means ``EMAIL_FROM`` is not on a verified domain (resend.com/domains). """ from __future__ import annotations import json import urllib.error import urllib.request from urllib.parse import quote from app.core.config import Settings, get_settings from app.core.logging import get_logger logger = get_logger(__name__) _RESEND_ENDPOINT = "https://api.resend.com/emails" def app_base(origin: str | None, settings: Settings | None = None) -> str: """Resolve the public base URL for links: trust the caller's origin only if it's allow-listed (prevents activation/reset links pointing at an attacker domain); else fall back to config.""" settings = settings or get_settings() if origin: o = origin.strip().rstrip("/") if o in settings.allowed_app_origins: return o return settings.public_app_url.rstrip("/") def build_link(path: str, token: str, base: str) -> str: return f"{base.rstrip('/')}{path}?token={quote(token)}" def _send_via_sdk(to: str, subject: str, html: str, settings: Settings) -> bool | None: """Send via the official Resend SDK. Returns True/False, or None if the SDK isn't available.""" try: import resend # type: ignore except Exception: # noqa: BLE001 — SDK not installed; caller falls back to HTTP return None try: resend.api_key = settings.resend_api_key resp = resend.Emails.send({"from": settings.email_from, "to": [to], "subject": subject, "html": html}) ok = bool(resp and (resp.get("id") if isinstance(resp, dict) else getattr(resp, "id", None))) if not ok: logger.error("Resend SDK returned no id: %r", resp) return ok except Exception as exc: # noqa: BLE001 logger.error("Resend SDK send failed: %s", exc) return False def _send_via_http(to: str, subject: str, html: str, settings: Settings) -> bool: """Fallback HTTP path with a real User-Agent (Cloudflare 1010 blocks Python-urllib's UA).""" body = json.dumps({"from": settings.email_from, "to": [to], "subject": subject, "html": html}).encode("utf-8") req = urllib.request.Request( _RESEND_ENDPOINT, data=body, method="POST", headers={ "Authorization": f"Bearer {settings.resend_api_key}", "Content-Type": "application/json", "Accept": "application/json", "User-Agent": "matrix-builder/1.0 (+https://build.matrixhub.io)", }, ) try: with urllib.request.urlopen(req, timeout=10) as resp: return 200 <= resp.status < 300 except urllib.error.HTTPError as exc: detail = "" try: detail = exc.read().decode("utf-8", "replace")[:500] except Exception: # noqa: BLE001 pass logger.error("Resend HTTP send failed: HTTP %s — %s", exc.code, detail) return False except Exception as exc: # noqa: BLE001 logger.error("Resend HTTP send failed: %s", exc) return False def send_email(to: str, subject: str, html: str, settings: Settings | None = None) -> bool: """Send one email. Returns True on success (or when logged in dev), False on failure. Prefers the official ``resend`` SDK (its User-Agent passes Cloudflare); falls back to a direct HTTPS call with a proper User-Agent if the SDK isn't installed. """ settings = settings or get_settings() if not settings.resend_api_key: logger.warning("RESEND_API_KEY unset; would email %s — subject=%r", to, subject) return True # dev mode: treat as delivered so local flows don't fail via_sdk = _send_via_sdk(to, subject, html, settings) return via_sdk if via_sdk is not None else _send_via_http(to, subject, html, settings) def _shell(title: str, intro: str, cta_label: str, link: str, footer: str) -> str: return f"""
|
Matrix Builder · Give AI coders a contract, not a prompt. |