"""SMTP-backed transactional email. Sends multipart/alternative: a plain-text body for accessibility / minimal clients and an HTML body for richer rendering. Designed for cross-client robustness: - Inline styles on every element (Outlook desktop ignores
Your {brand} sign-in code — {code} — expires in {ttl_minutes} minutes.
▰ {brand_upper}
 
Your sign-in code
 
{code}
 
This code expires in {ttl_minutes} minutes. If you didn’t request it, you can safely ignore this email — no changes will be made to any account.
 
 
Sent automatically by {brand} · do not reply
""" def _html_template_filled(code: str, ttl_minutes: int) -> str: """Substitute palette + content into the OTP HTML template.""" return _OTP_HTML_TEMPLATE.format( code=code, ttl_minutes=ttl_minutes, brand=branding.BRAND_NAME, brand_upper=branding.BRAND_NAME.upper(), FONT_MONO=branding.FONT_MONO, **{f"L_{k.replace('-', '_')}": v for k, v in branding.LIGHT.items()}, **{f"D_{k.replace('-', '_')}": v for k, v in branding.DARK.items()}, ) _OTP_TEXT_TEMPLATE = """\ {brand_upper} — sign in Your verification code: {code} This code expires in {ttl_minutes} minutes. If you didn't request it, you can safely ignore this email — no changes will be made to any account. — Sent automatically by {brand} · do not reply """ def render_otp_email(code: str, ttl_minutes: int) -> tuple[str, str, str]: """Returns (subject, text_body, html_body). Subject embeds the code so users can read it directly from the inbox list without opening the message — common practice for OTP emails (Notion, Substack). The lock-screen exposure tradeoff is minimal: anyone with phone access who could see the notification could also open the email.""" subject = f"{branding.BRAND_NAME} sign-in: {code}" text = _OTP_TEXT_TEMPLATE.format( code=code, ttl_minutes=ttl_minutes, brand=branding.BRAND_NAME, brand_upper=branding.BRAND_NAME.upper(), ) html = _html_template_filled(code=code, ttl_minutes=ttl_minutes) return subject, text, html async def send_otp(to: str, code: str, ttl_minutes: int) -> None: subject, text, html = render_otp_email(code, ttl_minutes) await send_email(to, subject, text, html_body=html) # --------------------------------------------------------------------------- # Welcome email — sent once on first successful login. # --------------------------------------------------------------------------- _WELCOME_HTML_TEMPLATE = """\ Welcome to {brand}
▰ {brand_upper}
 
Welcome to {brand}.
 
You’re signed in. The dashboard is at {app_url_short} — a rolling news feed, cross-asset indicator panels, and a written strategic read of the session, all updated through the day.
 
 
About the email digest
 
We send one Sunday digest to every account — the week behind + the week ahead. Paid subscribers also get a short daily digest (Mon–Sat), each a ~600-word read of the session. You’re opted in by default; you can switch the digest off at any time on the Settings page, or use the one-click unsubscribe link in every digest email.
 
 
Sent automatically by {brand} · do not reply
""" _WELCOME_TEXT_TEMPLATE = """\ {brand_upper} — welcome You're signed in. The dashboard is at {app_url}: a rolling news feed, cross-asset indicator panels, and a written strategic read of the session, all updated through the day. About the email digest ---------------------- We send one Sunday digest to every account (the week behind + the week ahead). Paid subscribers also get a short daily digest (Mon-Sat), each a ~600-word read of the session. You're opted in by default; switch it off any time at {settings_url}, or use the one-click unsubscribe link in every digest email. — Sent automatically by {brand} · do not reply """ def render_welcome_email() -> tuple[str, str, str]: """Returns (subject, text_body, html_body) for the post-signup welcome. Single-shot email, sent the first time a user successfully verifies an OTP. Explains the digest (which is opt-in by default) and how to turn it off — replaces the old verify-page checkbox which appeared on every login and was misleading.""" subject = f"Welcome to {branding.BRAND_NAME}" fmt = dict( brand=branding.BRAND_NAME, brand_upper=branding.BRAND_NAME.upper(), app_url=branding.APP_URL, app_url_short=branding.APP_URL.replace("https://", "").replace("http://", ""), settings_url=f"{branding.APP_URL}/settings", FONT_MONO=branding.FONT_MONO, **{f"L_{k.replace('-', '_')}": v for k, v in branding.LIGHT.items()}, **{f"D_{k.replace('-', '_')}": v for k, v in branding.DARK.items()}, ) text = _WELCOME_TEXT_TEMPLATE.format(**fmt) html = _WELCOME_HTML_TEMPLATE.format(**fmt) return subject, text, html async def send_welcome_email(to: str) -> None: subject, text, html = render_welcome_email() await send_email(to, subject, text, html_body=html)