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

8.6 KiB

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