- 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>
178 lines
8.2 KiB
Markdown
178 lines
8.2 KiB
Markdown
# SEC1 — SMTP-Mail-Fundament
|
|
|
|
> Branch `dev-sec1-mail-smtp` (Basis `dev`) · Grundlage: `docs/sicherheit/SEC1-Mail-Fundament-Detail.md`
|
|
> Fundament für Passwort-Reset (SEC2), Einladung (SEC4), MFA-Hinweise (SEC3) und
|
|
> Ticket-/Freigabe-/Fristen-Benachrichtigungen.
|
|
|
|
---
|
|
|
|
## 1. Architektur
|
|
|
|
```
|
|
Aufrufer (Server-Action / Event)
|
|
│ enqueueMail({template,to,vars,tenantId,locale,dedupeKey})
|
|
▼
|
|
service.ts ──► MailLog(pending) Worker-Prozess (npm run worker:mail)
|
|
│ (dedupeKey = Sperre) ┌──────────────────────────────────┐
|
|
├─ Queue erreichbar? ──ja──► Redis ──►│ BullMQ "mail" │
|
|
│ │ → renderTemplate(locale) │
|
|
└─ nein ─────────────────────────────►│ → provider.send() (deliver.ts) │
|
|
(inline, gleicher deliver-Pfad) │ → MailLog(sent|failed) │
|
|
│ → Retry/Backoff → mail-dead-letter
|
|
└──────────────────────────────────┘
|
|
Provider-Interface ← SMTP (nodemailer, Pool + TLS)
|
|
```
|
|
|
|
Vier Schichten, wie im Aufgabenpaket vorgesehen: **Provider** (`provider.ts`,
|
|
`provider-smtp.ts`), **Templates** (`templates.ts` + `src/lib/email-brand.ts`),
|
|
**Queue/Worker** (`queue.ts`, `worker.ts`), **Service/Trigger** (`service.ts`,
|
|
`notifications.ts`).
|
|
|
|
---
|
|
|
|
## 2. Betriebsmodus (Entscheidung)
|
|
|
|
Das Aufgabenpaket verlangt eine Festlegung — hier ist sie:
|
|
|
|
| `REDIS_URL` | Modus | Verhalten |
|
|
|---|---|---|
|
|
| gesetzt **und** erreichbar | **Queue** (Produktivmodus) | Asynchron über BullMQ; ein **eigener Worker-Prozess** (`npm run worker:mail`, eigener Container in Coolify) stellt zu. Retry mit exponentiellem Backoff (5 Versuche), Dead-Letter-Queue, Limiter (20 Mails/10 s, Concurrency 5), Graceful Shutdown. |
|
|
| gesetzt, aber **nicht erreichbar** | Inline (degradiert) | Der Versand fällt auf den direkten Pfad zurück, damit bei einem Redis-Ausfall keine Mail verloren geht — **ohne** Retry/DLQ. Wird im Log vermerkt. |
|
|
| nicht gesetzt | Inline | Lokale Entwicklung und Demo laufen ohne Redis. |
|
|
|
|
Die Erreichbarkeit wird **vor** dem Einstellen geprüft (`isQueueReady()`). Ein
|
|
Fallback *nach* einem fehlgeschlagenen `add()` wäre riskant: der Job könnte
|
|
angekommen sein und nur die Bestätigung verloren gegangen — das ergäbe einen
|
|
Doppelversand. Deshalb wird ein Fehler beim Einstellen nur protokolliert.
|
|
|
|
Der Worker registriert zusätzlich den **Fristen-Job** (täglich 07:00
|
|
Europe/Berlin) auf einer **eigenen** Queue `mail-scheduler`. Grund: ein
|
|
BullMQ-Worker konsumiert alle Jobs *seiner* Queue unabhängig vom Job-Namen —
|
|
auf einer gemeinsamen Queue könnte der Zustell-Worker den Fristen-Job abgreifen.
|
|
|
|
---
|
|
|
|
## 3. Konfiguration
|
|
|
|
Führend sind die bereits im Repo etablierten Namen; die im Aufgabenpaket
|
|
genannten Aliasse werden zusätzlich akzeptiert.
|
|
|
|
| Variable | Alias | Zweck |
|
|
|---|---|---|
|
|
| `SMTP_HOST`, `SMTP_PORT` | — | Relay |
|
|
| `SMTP_SECURE` | — | `true` = implizites TLS (465), sonst STARTTLS. Ohne Angabe aus dem Port abgeleitet. |
|
|
| `SMTP_USER`, `SMTP_PASSWORD` | `SMTP_PASS` | Authentifizierung (optional) |
|
|
| `SMTP_FROM` | `MAIL_FROM` | Absenderadresse |
|
|
| `MAIL_FROM_NAME` | — | Anzeigename (Default `Certvia`) |
|
|
| `MAIL_REPLY_TO` | — | optionale Antwortadresse |
|
|
| `APP_BASE_URL` | `AUTH_URL` | Basis für absolute Links in Mails |
|
|
|
|
**Fehlt die Konfiguration, wird nicht still versendet:** `getMailConfig()`
|
|
liefert `null` samt Begründung, das MailLog bleibt `pending` mit Fehlertext, und
|
|
die Admin-Konsole zeigt „Nicht konfiguriert". TLS wird erzwungen — gelockert nur
|
|
gegen `localhost`/`mailpit`/`mailhog`, weil der lokale Test-SMTP kein gültiges
|
|
Zertifikat hat.
|
|
|
|
---
|
|
|
|
## 4. Datenmodelle
|
|
|
|
- **`MailLog`** — Versandprotokoll: Empfänger, Template, Locale, Status
|
|
(`pending|sent|failed|bounced|suppressed`), `providerMessageId`, Fehlertext,
|
|
Versuchszähler, `dedupeKey` (unique). `tenantId` nullable für Plattform-Mails
|
|
(`scope=platform`, analog `AuditLog`).
|
|
**Bewusst nicht gespeichert:** Mail-Inhalt und jegliche Tokens.
|
|
- **`NotificationPreference`** — je Nutzer und Ereignistyp. **Default opt-in:**
|
|
fehlt die Zeile, wird versendet; erst ein ausdrückliches `email = false`
|
|
unterdrückt. Die Pflege-UI folgt später — Modell und Auflösung stehen.
|
|
|
|
Beide in `TENANT_MODELS` und unter RLS (ENABLE + FORCE + `USING`/`WITH CHECK`,
|
|
Migration `20260730180000_mail_fundament`, inkl. `GRANT` für `isms_app`).
|
|
|
|
---
|
|
|
|
## 5. Templates
|
|
|
|
Acht Templates, jeweils **de und en**, **HTML und Text**:
|
|
`invitation`, `password_reset`, `password_changed`, `email_change_verify`,
|
|
`email_changed_notice`, `mfa_changed`, `notification`, `test`.
|
|
|
|
Layout, Farben und die Fußzeile „Certvia — ein Produkt von GEFIM" kommen aus
|
|
`src/lib/email-brand.ts` (Inline-Styles, Tabellenlayout für Outlook, absolute
|
|
Asset-URLs).
|
|
|
|
**i18n-Abweichung mit Begründung:** die Sprachen liegen in einem eigenen
|
|
Katalog (`src/server/mail/templates.ts`), nicht in `messages/*.json`. Die Mails
|
|
werden im **Worker** gerendert — außerhalb eines Requests; die
|
|
next-intl-Server-APIs (`getTranslations`) setzen einen Request-Scope voraus und
|
|
stehen dort nicht zur Verfügung.
|
|
|
|
**Tokens:** Templates bekommen fertige `actionUrl`s. Erzeugt werden die Tokens
|
|
in SEC2/SEC3/SEC4 — sie erscheinen weder im MailLog noch im Server-Log.
|
|
|
|
---
|
|
|
|
## 6. Benachrichtigungen
|
|
|
|
| Ereignis | Auslöser | Empfänger |
|
|
|---|---|---|
|
|
| `task_assigned` | `createTask`, `updateTask` (nur bei **Wechsel** des Zuständigen) | neuer Zuständiger |
|
|
| `task_approval_requested` | `submitForApproval` (Richtlinien) | ausgewählter Freigeber |
|
|
| `task_decided` | `approveTask` / `rejectTask` | Einreicher |
|
|
| `task_due` | Fristen-Job (täglich) | Zuständiger |
|
|
|
|
Regeln: strikt mandantenisoliert, Sprache je Empfänger (Präferenz → Mandanten-
|
|
Locale → `de`), **kein Selbstversand** über die eigene Handlung, Idempotenz über
|
|
`dedupeKey` (der Fristen-Job hängt das Datum an → höchstens eine Erinnerung je
|
|
Aufgabe und Tag). Fehler beim Versand werden geloggt, kippen aber nie die
|
|
auslösende Fachaktion.
|
|
|
|
Benachrichtigungen tragen einen Präferenzhinweis in der Fußzeile;
|
|
sicherheitsrelevante Transaktionsmails (Reset, Passwortwechsel) bewusst **nicht**
|
|
— sie sind nicht abbestellbar.
|
|
|
|
---
|
|
|
|
## 7. Admin-Testversand
|
|
|
|
`/admin` zeigt Konfigurationszustand, Modus (Queue/inline) und die letzten zehn
|
|
MailLog-Zeilen; der Button „Test-Mail senden" schickt an die **eigene** Adresse
|
|
des angemeldeten Plattform-Admins (kein Empfängerfeld — die Funktion ist eine
|
|
Zustellprüfung, kein Relay). Autorisierung über `requirePlatformSession()`;
|
|
`mail.ts` ist in `scripts/check-module-guards.ts` als `EXEMPT` registriert.
|
|
|
|
---
|
|
|
|
## 8. Verifikation
|
|
|
|
```bash
|
|
docker compose up -d mailhog # SMTP :1025, Weboberfläche :8025
|
|
npx tsx scripts/test-mail.ts # Abnahmetest
|
|
```
|
|
|
|
Der Test prüft: Rendering aller Templates in de/en (HTML + Text, Branding +
|
|
Dachmarken-Fußzeile), Präferenzhinweis nur bei Benachrichtigungen, echten
|
|
Versand mit `MailLog=sent` + `providerMessageId`, Idempotenz über `dedupeKey`
|
|
und das Verhalten bei fehlender SMTP-Konfiguration.
|
|
|
|
Nachgewiesen am 30.07.2026 gegen Mailhog: alle Prüfungen grün, Nachricht kommt
|
|
als `multipart/alternative` (HTML + Text) mit Absender `Certvia <…>`,
|
|
`Auto-Submitted: auto-generated` und korrekter Fußzeile an.
|
|
|
|
Zusätzlich: `npx tsc --noEmit` → `npm run lint` → `npm run build` (Guard-Check).
|
|
|
|
---
|
|
|
|
## 9. Offene Punkte / Vorbedingungen
|
|
|
|
- **DNS:** SPF, DKIM und DMARC für die Absenderdomain müssen **vor** dem ersten
|
|
Produktivversand aktiv sein (Ops, kein Code).
|
|
- **Bounce-Verarbeitung:** `MailLog.status` kennt `bounced`, es gibt aber noch
|
|
keinen Rückkanal (Webhook/IMAP). Erst mit einem Provider sinnvoll, der
|
|
Bounces meldet.
|
|
- **Präferenz-UI:** `NotificationPreference` ist modelliert und wird ausgewertet;
|
|
die Pflegeoberfläche im Profil fehlt noch.
|
|
- **Worker-Deployment:** `npm run worker:mail` muss in
|
|
`docker-compose.coolify.yml` als eigener Service ergänzt werden (der
|
|
vorhandene `worker`-Service im lokalen Compose zeigt noch auf keinen Befehl).
|