Bygget: ny app/email.py (send_magic_link_email, kjører smtplib via asyncio.to_thread siden det er synkront, håndterer både implisitt TLS (port 465) og STARTTLS dynamisk siden jeg bevisst ikke leste TEECUP_SMTP_PORT-verdien). app/config.py fikk nye, valgfrie innstillinger — SMTP_CONFIGURED er IKKE _required, så scratch-/dev-testing fortsatt fungerer uendret uten SMTP satt opp. Sikkerhetsdesign: en driftsfeil i selve utsendingen (feil passord, SMTP nede, eller ingenting konfigurert) logges kun server-side og endrer aldri klientresponsen — bevarer request-link sitt anti-enumereringsvern. Verifisert i to trinn: Dev-log-flyten uendret uten SMTP satt (regresjonstest). Én ekte test-e-post sendt til hei@erol.no, med credentials videreført fra .env til scratch-containeren uten at jeg noensinne leste verdiene — du bekreftet mottak. Dette er første gang noe i prosjektet er bevist ved ekte, ekstern levering fremfor bare curl/scratch-container. CLAUDE.md/FEATURE_BACKLOG.md oppdatert.
54 lines
1.9 KiB
Python
54 lines
1.9 KiB
Python
"""
|
|
E-postutsending for TeeCup. I dag kun magic-link-innlogging.
|
|
|
|
Egne, uavhengige SMTP-credentials fra teeoff (ADR-009) -- se .env
|
|
(TEECUP_SMTP_SERVER/PORT/USER/PASS, TEECUP_FROM_EMAIL). `smtplib` er
|
|
synkron, så selve sendingen kjøres i en tråd (asyncio.to_thread) for ikke å
|
|
blokkere event loop-en -- samme mønster som teeoffs egen, fungerende
|
|
magic-link-utsending.
|
|
|
|
Håndterer BEGGE vanlige SMTP-tilkoblingsmåter siden porten ikke er antatt på
|
|
forhånd: implisitt TLS (SMTP_SSL, typisk port 465) eller STARTTLS på en
|
|
vanlig tilkobling (typisk port 587/25).
|
|
"""
|
|
|
|
import smtplib
|
|
from asyncio import to_thread
|
|
from email.message import EmailMessage
|
|
|
|
from .config import settings
|
|
|
|
|
|
def _send_sync(to_email: str, subject: str, body: str) -> None:
|
|
msg = EmailMessage()
|
|
msg["Subject"] = subject
|
|
msg["From"] = settings.FROM_EMAIL
|
|
msg["To"] = to_email
|
|
msg.set_content(body)
|
|
|
|
if settings.SMTP_PORT == 465:
|
|
with smtplib.SMTP_SSL(settings.SMTP_SERVER, settings.SMTP_PORT) as smtp:
|
|
smtp.login(settings.SMTP_USER, settings.SMTP_PASS)
|
|
smtp.send_message(msg)
|
|
else:
|
|
with smtplib.SMTP(settings.SMTP_SERVER, settings.SMTP_PORT) as smtp:
|
|
smtp.starttls()
|
|
smtp.login(settings.SMTP_USER, settings.SMTP_PASS)
|
|
smtp.send_message(msg)
|
|
|
|
|
|
async def send_magic_link_email(to_email: str, raw_token: str) -> None:
|
|
"""Sender selve magic-link-tokenet som klartekst.
|
|
|
|
MERK: ingen klikkbar lenke ennå -- TeeCup har ikke noe frontend å peke
|
|
på (se plan). Når frontend bygges, oppdater malen til en ekte URL.
|
|
"""
|
|
subject = "Logg inn på TeeCup"
|
|
body = (
|
|
"Hei,\n\n"
|
|
f"Bruk denne koden for å logge inn på TeeCup innen "
|
|
f"{settings.MAGIC_LINK_MAX_AGE_MINUTES} minutter:\n\n"
|
|
f"{raw_token}\n\n"
|
|
"Ba du ikke om dette, kan du se bort fra e-posten.\n"
|
|
)
|
|
await to_thread(_send_sync, to_email, subject, body)
|