# SEC1 — SMTP-Mail-Fundament (Detail-Prompt) > Claude-Code-Prompt für **einen** Entwickler. Basis `dev` → Branch **`dev/sec1-mail-smtp`** (PR-Ziel `dev`). > Ziel: zuverlässiger, gebrandeter, **asynchroner** Mail-Versand über **SMTP** — Fundament für Passwort-Reset (SEC2), Einladung, MFA-Hinweise (SEC3) und Ticket-/Freigabe-/Fristen-Benachrichtigungen. Stack vorhanden: **BullMQ + Redis**, Next.js/TS, Prisma, i18n (de/en), Task/TaskComment, Audit-Log, `TenantSettings`. --- ## 1. Architektur (Zielbild) ``` Aufrufer (Action/Event) Worker-Prozess │ enqueueMail({template,to, ┌───────────────────────────┐ │ locale,vars,tenantId}) │ BullMQ "mail"-Queue │ ▼ │ → render(template,locale) │ MailService ──push job──► Redis ───────► │ → MailProvider.send() │ │ │ → MailLog(status) │ └─ MailLog(pending) │ → Retry/Backoff/DLQ │ └───────────────────────────┘ Provider-Interface ← SMTP-Impl (nodemailer) [später austauschbar] ``` Trennung in vier Schichten: **Provider** (SMTP), **Templates** (Render), **Queue/Worker** (Zustellung + Retry), **Service/Trigger** (Aufruf + Regeln + Log). --- ## 2. Datenmodelle (Prisma, mit Migration + RLS) - **`MailLog`** (Auditierbarkeit + Idempotenz): `id`, `tenantId` (nullable → Plattform-Mails), `to`, `template`, `locale`, `status` (`pending|sent|failed|bounced|suppressed`), `providerMessageId?`, `error?`, `dedupeKey?` (unique, für Idempotenz), `createdAt`, `sentAt?`. → in `TENANT_MODELS`+RLS (Plattform-Zeilen mit `tenantId=null`, `scope=platform`). - **`NotificationPreference`**: `userId`, `eventType`, `email` (bool, default true), `locale?`. Default = opt-in; UI kommt später — jetzt Modell + Default-Auflösung. - Migrationen im Prisma-7-Flow (RLS-DO-Block manuell anhängen). --- ## 3. Konfiguration (Env / Secret-Store — nichts ins Repo) `SMTP_HOST`, `SMTP_PORT`, `SMTP_SECURE` (true=TLS/465, false=STARTTLS/587), `SMTP_USER`, `SMTP_PASS`, `MAIL_FROM` (`no-reply@certvia.de`), `MAIL_FROM_NAME` (`Certvia`), `APP_BASE_URL`, `MAIL_REPLY_TO?`. - **Boot-Validierung** (z. B. zod): fehlt Konfig → klarer Fehler, Versand deaktiviert (Jobs bleiben `pending`, Warnung im Log). Kein stiller Fehlversand. --- ## 4. Provider & Service - **Interface** `src/server/mail/provider.ts`: `send(msg: {from,to,replyTo?,subject,html,text,headers?}): Promise<{messageId}>`. So bleibt der Provider austauschbar (heute SMTP, später API). - **SMTP-Impl** `src/server/mail/provider-smtp.ts`: **nodemailer** (Pool, TLS, Timeouts). Verbindung wiederverwenden. - **Service** `src/server/mail/service.ts`: - `enqueueMail(input)` → `dedupeKey` bilden (z. B. `template:to:refId`), `MailLog(pending)` anlegen (unique verhindert Doppelversand), Job in Queue. - `renderMail(template, locale, vars)` → `{subject, html, text}` (Abschnitt 5). - Action-Datei in `scripts/check-module-guards.ts` als **`EXEMPT`** eintragen (Infrastruktur, kein gegatetes Modul). --- ## 5. Templates (gebrandet, de/en, HTML + Text) - **Basis-Layout** `src/server/mail/templates/_layout.*`: Certvia-Kopf (Logo/Wortmarke), Markenfarben (Violett `#5d52a3` / Magenta-Akzent `#812d80`), **Inline-CSS** (E-Mail-Client-tauglich; MJML oder handgepflegtes Table-Layout), Fußzeile „**Certvia — ein Produkt von GEFIM**" + Impressum/Abmelde-Hinweis. Immer **Text-Alternative** mitliefern. - **i18n:** je Template `de`/`en` über die bestehenden Message-Kataloge; `locale` aus Nutzer/`NotificationPreference`, Fallback `de`. - **Template-Set (Erststufe):** | Template-Key | Anlass | Kern-Variablen | Auslöser | |---|---|---|---| | `invitation` | Nutzer-Onboarding | `name, tenantName, actionUrl, expires` | SEC1/SEC4 (Einladung) | | `password_reset` | Reset angefordert | `name, actionUrl, expires` | **SEC2** (Token wird dort erzeugt) | | `password_changed` | Passwort geändert | `name, when, ip?` | SEC2 | | `email_change_verify` | E-Mail-Änderung bestätigen | `name, actionUrl, expires` | SEC2 | | `mfa_changed` | MFA aktiviert/deaktiviert/Recovery neu | `name, change, when` | SEC3 | | `notification` | Ticket/Freigabe/Fälligkeit | `name, subject, body, actionUrl, taskType` | Task-Events (Abschnitt 6) | > **Wichtig:** Reset-/Verify-/Einladungs-**Tokens** werden von SEC2/SEC3/SEC4 erzeugt (single-use, gehasht). SEC1 liefert nur Template + Versand und bekommt die fertige `actionUrl` als Variable — **keine** Klartext-Secrets ins MailLog/Log schreiben. --- ## 6. Benachrichtigungs-Trigger (Ticket-/Freigabe-/Fristen) - **Task-Ereignisse** (bestehendes `Task`/`TaskComment` + `submitForApproval`): bei **Zuweisung**, **Freigabe-Anfrage**, **Freigabe-Entscheidung** (angenommen/abgelehnt) → `notification`-Mail an den jeweils Zuständigen, sofern `NotificationPreference.email` aktiv. - **Fristen-Erinnerung:** **BullMQ Repeatable Job** (z. B. täglich) prüft fällige/überfällige Aufgaben (`dueDate`) und versendet gebündelte Erinnerungen (keine Spam-Schleifen — je Aufgabe max. definierte Frequenz, `dedupeKey`). - Alle Trigger **mandantenisoliert**; Sprache je Empfänger. --- ## 7. Queue / Worker / Robustheit - **Queue** `mail` (BullMQ). **Worker** `src/server/mail/worker.ts`: `render → provider.send → MailLog(sent|failed)`. - **Retry:** z. B. 5 Versuche, exponentielles Backoff; nach Ausschöpfung `status=failed` + **Dead-Letter** (separate Queue/Flag) + Alarm-Logeintrag. - **Rate-Limit** je Empfänger/Domain (BullMQ Limiter), **Concurrency** begrenzt, **Graceful Shutdown**. - **Idempotenz:** `dedupeKey`-Unique + „bereits gesendet"-Kurzschluss. - **Betriebsmodus dokumentieren:** Worker als eigener Prozess (Coolify) **oder** im App-Container gestartet — entscheiden und im README festhalten. --- ## 8. Sicherheit / Datenschutz - **TLS erzwingen** (STARTTLS/implicit), Zertifikatsprüfung an. Secrets nur aus Env/Secret-Store. - **Keine sensiblen Inhalte** in Mails über das Nötige hinaus; **keine Tokens** in Logs/MailLog (nur `dedupeKey`/`providerMessageId`). - **Abmelde-/Präferenz-Hinweis** in Benachrichtigungs-Mails (nicht in sicherheitskritischen Transaktionsmails wie Reset). - **Enumeration-Schutz** ist Sache von SEC2 (Reset) — SEC1 versendet nur, was ihm übergeben wird. - **Audit:** Versandereignisse (ohne Inhalt) ins Security-Audit (Erweiterung in SEC5 kompatibel halten). - **DNS-Vorbedingung:** SPF, DKIM, DMARC für die Absenderdomain (Ops; vor Produktivversand). --- ## 9. Admin-Testversand - Aktion „Test-Mail senden" im Adminportal (an eigene Adresse), zeigt Ergebnis/MailLog-Status → schnelle Zustell-/DKIM-Prüfung. --- ## 10. Akzeptanzkriterien - `enqueueMail(...)` legt `MailLog(pending)` an und stellt asynchron über SMTP zu; bei Erfolg `sent` + `providerMessageId`, bei Fehler Retry→`failed`/DLQ. - Templates rendern **de und en**, HTML **und** Text, mit Certvia-Branding und „ein Produkt von GEFIM". - **Ticket-Benachrichtigung** wird bei Task-Zuweisung/Freigabe ausgelöst und respektiert `NotificationPreference` + Mandantenisolation. - **Fristen-Erinnerung** läuft als wiederkehrender Job ohne Doppelversand. - Fehlende SMTP-Konfig → klarer Fehler, kein stiller Fehlversand; keine Secrets/Tokens im Repo oder Log. - Lokaler Test gegen **Mailpit/Mailhog** dokumentiert; Admin-Testversand funktioniert. ## 11. Definition of Done `npx tsc --noEmit` → `npm run lint` → `npm run build` (Guard-Check) grün · Mail-Action als `EXEMPT` registriert · `MailLog`/`NotificationPreference` in `TENANT_MODELS`+RLS+Migration (RLS-DO-Block) · Worker-Betriebsmodus im README · lokaler Mailpit-Testnachweis (Screenshots) · Demo-Umgebung lauffähig. ## 12. Testplan (Kurz) 1. **Lokal Mailpit** (`docker run mailpit`) als SMTP-Ziel; Env setzen; Test-Mail senden → in Mailpit sichtbar (HTML+Text, de/en). 2. **Retry:** Provider künstlich fehlschlagen lassen → Job retryt, landet nach N Versuchen in DLQ, `MailLog=failed`. 3. **Idempotenz:** zweimal gleicher `dedupeKey` → nur eine Mail. 4. **Trigger:** Aufgabe zuweisen / Freigabe anfragen → `notification`-Mail; `NotificationPreference.email=false` → keine Mail. 5. **Fristen-Job:** überfällige Aufgabe → genau eine Erinnerung je Zyklus. ## 13. Bibliotheken `nodemailer` (SMTP) · `bullmq` (vorhanden) · optional `mjml` für Template-Rendering · `zod` (Env-Validierung, i. d. R. vorhanden). Nächste Pakete bauen darauf auf: **SEC2** (Reset/Change nutzen `password_reset`/`password_changed`/`email_change_verify`), **SEC3** (`mfa_changed`), **SEC4** (`invitation`).