Status: Accepted Datum: 2026-05-16 Oblast: Infrastruktura / E-mail / Notifikace Supersedes: — Navazuje na: UC-01005 (Forgotten Password), UC-11001 (Join Waitlist)
Context
Stávající stav — tři e-mailové stuby
TalkIDE má tři místa, kde uživatel přirozeně očekává e-mail, ale žádný se neposílá:
| Stub | UC | Stav |
|---|---|---|
| Waitlist confirmation | UC-11001 | UI zobrazuje “We sent a confirmation to {email}”, žádný e-mail neodchází |
| Forgot-password reset link | UC-01005 | MVP: reset link se loguje pouze do konzoly BE |
| Billing spending alert (80 % / 100 %) | UC-10005 | MVP: Slf4j log only |
Billing alert je mimo scope tohoto ADR — řeší se samostatně v rámci Stopy C (real Stripe live mode). Aktuálně zůstává jako stub.
Požadavky
- Přejít z MVP no-op / log-only na reálné odeslání pro forgot-password a waitlist confirmation.
- Abstrakce umožňující snadné testování (dev/test profil odesílá do
/dev/null, nevolá žádné API). - Auditní log odeslaných e-mailů v DB (pro debugging doručitelnosti a budoucí billing usage tracking).
- Sending doména na subdoméně
mail.talkide.app— neovlivňuje MX záznamy hlavní domény. - Konfigurovatelný API base URL (US vs EU region).
Decision
1. Provider — Mailgun
Zvolena platforma Mailgun (Sinch). Důvody:
- Existující firemní účet — nulový onboarding čas.
- Robustní HTTP API (
https://api.mailgun.net/v3/{domain}/messages) s formátemmultipart/form-data. - Webhook events (delivered, failed, bounced) pro budoucí doručitelnostní monitoring.
- Alternativy zvažovány, ale odmítnuty (viz sekce Alternatives).
2. Transport — HTTP API, nikoli SMTP
Integrace probíhá výhradně přes Mailgun HTTP API. Závislost spring-boot-starter-mail (JavaMail/SMTP) se nepřidává. Důvody:
- HTTP API je synchronní, snadněji testovatelné a nevyžaduje správu SMTP connection pool.
- Jednodušší retry logika (HTTP status kódy vs. SMTP error codes).
- Přímočará autentizace (
Authorization: Basic api:<key>).
3. Sending doména — mail.talkide.app
E-maily odcházejí z adresy noreply@mail.talkide.app, zobrazovaný název odesílatele je TalkIDE. Používá se subdoména, aby:
- DNS záznamy pro e-mail (SPF, DKIM, tracking CNAME) neovlivňovaly hlavní doménu
talkide.app. - Mailgun verifikace domény nezasahovala do stávajícího DNS setupu.
DNS prerequisite (manuální konfigurace Mailgunu před prvním prod odesláním):
| Typ záznamu | Hodnota |
|---|---|
| TXT (SPF) | v=spf1 include:mailgun.org ~all na mail.talkide.app |
| TXT (DKIM) | k=rsa; p=<public_key> na pic._domainkey.mail.talkide.app |
| CNAME (tracking) | mailgun.org na email.mail.talkide.app |
| MX (volitelné) | pouze pokud chceme příjmové e-maily na subdoméně |
DNS záznamy jsou generovány Mailgunem po přidání domény do panelu. Až do jejich ověření Mailgunem prod odesílání nefunguje.
4. Konfigurovatelný API base URL
# application.yaml
talkide:
email:
provider: mailgun
mailgun:
base-url: https://api.mailgun.net # US region; EU: https://api.eu.mailgun.net
domain: mail.talkide.app
from-name: TalkIDE
from-address: noreply@mail.talkide.app
base-url je konfigurovatelný přes env, bez nutnosti kódu měnit region.
5. EmailSender abstrakce
interface EmailSender {
fun send(to: String, subject: String, htmlBody: String, textBody: String): EmailSendResult
}
Dvě implementace:
| Implementace | Profil | Chování |
|---|---|---|
MailgunEmailSender | production | Skutečné HTTP volání Mailgun API; loguje provider_message_id z odpovědi |
NoOpEmailSender | !production (všechny ne-production profily — local, test, default, dev, …) | Žádné HTTP volání; loguje kompletní e-mail (headers + body) do konzoly na úrovni INFO |
Konzumenti (use cases) pracují pouze s rozhraním EmailSender — neví, která implementace je aktivní.
6. Audit log — tabulka email_log
Nová Liquibase migrace 0028-create-email-log.xml:
CREATE TABLE email_log (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(50) NOT NULL, -- FORGOT_PASSWORD | WAITLIST_CONFIRMATION | BILLING_ALERT
recipient VARCHAR(255) NOT NULL,
subject VARCHAR(500) NOT NULL,
provider_message_id VARCHAR(255), -- Mailgun-Message-Id z response header; null u NoOp
status VARCHAR(20) NOT NULL, -- SENT | FAILED
error TEXT, -- null pokud SENT; chybová zpráva pokud FAILED
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Composite index optimalizovaný pro throttle lookup (najdi poslední SENT WAITLIST_CONFIRMATION
-- pro daného příjemce): filtruje po recipient + type, řadí/limituje dle created_at.
CREATE INDEX idx_email_log_recipient_type_created ON email_log (recipient, type, created_at);
Záznam se persistuje vždy — i při selhání odeslání (status=FAILED, error=…). Tabulka slouží pro debugging doručitelnosti a budoucí compliance.
7. Secrets pattern — K8s Secret mailgun-creds
# kubectl create secret generic mailgun-creds -n talkide \
# --from-literal=api-key=key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
BE pod injectuje secret jako env proměnnou:
env:
- name: TALKIDE_EMAIL_MAILGUN_API_KEY
valueFrom:
secretKeyRef:
name: mailgun-creds
key: api-key
Secret se spravuje ručně (stejný vzor jako db-creds a anthropic-creds). Secret template není součástí Helm chartu.
8. Chování pro jednotlivé stuby
| UC | Trigger | Template | Chování při selhání |
|---|---|---|---|
| UC-01005 | BE generuje reset token | Subject: “Reset your TalkIDE password”, text s linkem | Selhání loge, ignoruje — response je vždy 200 OK (anti-enumeration) |
| UC-11001 | Po 201 vždy; po idempotentním 200 jen pokud email_log neobsahuje úspěšně odeslaný WAITLIST_CONFIRMATION pro tohoto příjemce za posledních WAITLIST_CONFIRMATION_RESEND_WINDOW_HOURS = 24 h (throttled resend) | Subject: “You’re on the TalkIDE waitlist!”, text s potvrzením + referral link | Selhání loge, ignoruje — join request NESMÍ selhat kvůli e-mailu |
| UC-10005 (billing alert) | Spending limit 80 %/100 % | MIMO SCOPE tohoto ADR | Zůstává Slf4j log stub |
Consequences
Pozitivní
- Waitlist uživatelé obdrží reálné potvrzení e-mailem — eliminuje nesoulad UI copy vs. realita.
- Forgot-password flow je kompletní end-to-end bez manuálního vyhledávání v BE logu.
- Auditní tabulka
email_logumožňuje ověřit doručení bez přístupu do Mailgun dashboardu. NoOpEmailSenderzajišťuje, že ve všech ne-production prostředích (!production) neodcházejí reálné e-maily; reset link je stále viditelný v konzole.- Abstrakce
EmailSenderumožňuje v budoucnu vyměnit Mailgun za jiný provider bez změny business logiky.
Negativní / Rizika
- DNS prerequisite: prod odesílání nefunguje, dokud nejsou ověřeny DNS záznamy Mailgunu pro
mail.talkide.app. Toto je manuální krok mimo kód. - Nový K8s Secret
mailgun-credsmusí být vytvořen před deploym na prod; absence způsobí chybu startupového ENV injection (pod nepůjde nastartovat pokud jerequired: true). - Billing alert zůstává stub — přidáváme email_log tabulku, ale
BILLING_ALERTtype se zatím nezapisuje (připraveno pro budoucí implementaci). - Bez retry logiky v MVP — pokud Mailgun API vrátí 5xx,
MailgunEmailSenderzaloguje chybu a zapíše FAILED doemail_log. Žádný exponential backoff / dead-letter queue. Toto je akceptovatelné pro aktuální objem. - Profil MUSÍ být přesně
production— chybný název profilu (např.prod) způsobí tichý fallback naNoOpEmailSendera žádné e-maily se neposílají. Tato chyba nastala v produkci a byla opravena v commitu 13002d2.
Alternatives Considered
| Provider | Důvod odmítnutí |
|---|---|
| Resend | Žádný existující účet; modernější DX, ale zbytečný onboarding overhead |
| Postmark | Silný v transakčních e-mailech, dobrá reputace, ale opět nový účet + vyšší cena pro nízký objem |
| AWS SES | Výhodný pro vysoký objem, ale komplikovanější setup (IAM, sandbox vyjímka) a přidaná DO↔AWS závislost |
| SMTP (spring-boot-starter-mail) | Přidává JavaMail stack; SMTP session management je zbytečně složitý pro nízký objem; horší testovatelnost |
| SendGrid | Vlastněno Twiliem; cena a složitost API v porovnání s Mailgunem bez výhod pro náš use case |
Prerequisites Checklist (před prod deploym)
- Mailgun doména
mail.talkide.apppřidána a ověřena v Mailgun panelu (DNS záznamy SPF, DKIM, tracking CNAME propagovány) - K8s Secret
mailgun-credsvytvořen v namespacetalkides klíčemapi-key - BE Helm chart / deployment YAML aktualizován o env injection
TALKIDE_EMAIL_MAILGUN_API_KEY - Liquibase migrace
0028-create-email-log.xmlaplikována (stane se automaticky při prvním startu BE) - Smoke test: ruční
POST /api/v1/auth/forgot-passwordna prod → ověřit příchod e-mailu a záznam vemail_log
Thanks for the feedback.