Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# 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`).
|
||||
Reference in New Issue
Block a user