Files
craftvia/docs/_certvia-archiv/sicherheit/SEC1-Mail-Fundament-Detail.md
T
msolarczekandClaude Opus 5 cadaedc6cc L10b Betrieb & Aufräumen: Deploy – craftvia-worker, CI-Testjob, DEPLOY.md, Certvia-Doku archiviert
- docker-compose.coolify(.prebuilt).yml: Service craftvia-worker (Target worker, Chromium,
  shm_size 1gb, gleiche Härtung), Craftvia-Variablen für app und worker.
- Dockerfile: worker-Stage mit HOME=/home/app (Chromium-Profil für non-root); lokaler
  docker build der Targets runner und worker erfolgreich, PDF-Erzeugung im Image geprüft.
- .env.example/.env.prod.example/.env.coolify.example: alle Craftvia-Variablen inkl. RLS,
  KI-Provider, PDF_CHROMIUM_PATH, OFFLINE_MAX_DAYS, API_RATE_LIMIT_*, AI_GENERATION_RETENTION_DAYS,
  AI_MONTHLY_TOKEN_LIMIT.
- CI (.github, .gitea): Job gate mit Postgres (pgvector) und Redis als Service: migrate deploy,
  seed, Passwort für craftvia_app, tsc, lint, build, npm run test.
- docs/craftvia/DEPLOY.md (aus den Certvia-Deploy-Docs abgeleitet): Architektur, Domains, Secrets,
  Worker, Migrationen, RLS-Aktivierung, Backup/Restore, KI, Rate Limits, Aufbewahrung, Smoke,
  Update/Rollback. build-and-push-images.sh baut craftvia-worker.
- Certvia-/ISMS-Dokumente aus docs/ nach docs/_certvia-archiv/ (mit README); Verweise in README.md
  und Skript-Kommentaren angepasst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:19 +02:00

126 lines
8.6 KiB
Markdown

# 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`).