2026-07-16 20:59:59 +02:00
|
|
|
"""
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
2026-07-17 21:40:42 +02:00
|
|
|
_TEMPLATES = {
|
|
|
|
|
"nb": {
|
|
|
|
|
"subject": "Logg inn på TeeCup",
|
|
|
|
|
"body": (
|
|
|
|
|
"Hei,\n\n"
|
|
|
|
|
"Bruk denne koden for å logge inn på TeeCup innen {minutes} minutter:\n\n"
|
|
|
|
|
"{token}\n\n"
|
|
|
|
|
"Ba du ikke om dette, kan du se bort fra e-posten.\n"
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
"en": {
|
|
|
|
|
"subject": "Log in to TeeCup",
|
|
|
|
|
"body": (
|
|
|
|
|
"Hi,\n\n"
|
|
|
|
|
"Use this code to log in to TeeCup within {minutes} minutes:\n\n"
|
|
|
|
|
"{token}\n\n"
|
|
|
|
|
"If you didn't request this, you can ignore this email.\n"
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def send_magic_link_email(to_email: str, raw_token: str, locale: str = "nb") -> None:
|
|
|
|
|
"""Sender selve magic-link-tokenet som klartekst, på angitt språk.
|
2026-07-16 20:59:59 +02:00
|
|
|
|
|
|
|
|
MERK: ingen klikkbar lenke ennå -- TeeCup har ikke noe frontend å peke
|
|
|
|
|
på (se plan). Når frontend bygges, oppdater malen til en ekte URL.
|
2026-07-17 21:40:42 +02:00
|
|
|
`locale` valideres allerede av Pydantic (Literal["nb","en"]) på vei inn i
|
|
|
|
|
routeren, men faller trygt tilbake til "nb" her også hvis den skulle
|
|
|
|
|
komme fra et annet kall uten den garantien.
|
2026-07-16 20:59:59 +02:00
|
|
|
"""
|
2026-07-17 21:40:42 +02:00
|
|
|
template = _TEMPLATES.get(locale, _TEMPLATES["nb"])
|
|
|
|
|
subject = template["subject"]
|
|
|
|
|
body = template["body"].format(minutes=settings.MAGIC_LINK_MAX_AGE_MINUTES, token=raw_token)
|
2026-07-16 20:59:59 +02:00
|
|
|
await to_thread(_send_sync, to_email, subject, body)
|