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>
8.6 KiB
SEC1 — SMTP-Mail-Fundament (Detail-Prompt)
Claude-Code-Prompt für einen Entwickler. Basis
dev→ Branchdev/sec1-mail-smtp(PR-Zieldev). 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?. → inTENANT_MODELS+RLS (Plattform-Zeilen mittenantId=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)→dedupeKeybilden (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.tsalsEXEMPTeintragen (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;localeaus Nutzer/NotificationPreference, Fallbackde. - 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
actionUrlals 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, sofernNotificationPreference.emailaktiv. - 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). Workersrc/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(...)legtMailLog(pending)an und stellt asynchron über SMTP zu; bei Erfolgsent+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)
- Lokal Mailpit (
docker run mailpit) als SMTP-Ziel; Env setzen; Test-Mail senden → in Mailpit sichtbar (HTML+Text, de/en). - Retry: Provider künstlich fehlschlagen lassen → Job retryt, landet nach N Versuchen in DLQ,
MailLog=failed. - Idempotenz: zweimal gleicher
dedupeKey→ nur eine Mail. - Trigger: Aufgabe zuweisen / Freigabe anfragen →
notification-Mail;NotificationPreference.email=false→ keine Mail. - 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).