Files
teamwallet/docs/superpowers/specs/2026-07-31-mail-versand-design.md
Bastian Wagner ed5a00a577 docs: correct mail design spec after deeper env/infra investigation
.env already exists locally with valid creds (the deleted repo-root
.env was an unused duplicate), and no MailDev infra exists for the
e2e mail tests, so the plan shifts to a MailService unit test plus
one manual send instead of touching .env or standing up e2e infra.
2026-07-31 22:09:59 +02:00

8.6 KiB

Mailversand reparieren + Templates neu gestalten

Status: approved Datum: 2026-07-31

Kontext

Backend: NestJS (myteamwallet_backend), basierend auf nestjs-boilerplate. Mailversand über @nestjs-modules/mailer (Nodemailer) mit Handlebars-Templates. E-Mails werden für zwei Flows verschickt: Registrierung (Bestätigungsmail) und Passwort vergessen.

Der Mailversand funktioniert aktuell nicht. Ursache (nach genauerer Prüfung — korrigiert gegenüber einer ersten Analyse):

  • src/mail/mail.service.ts — sowohl userSignUp() als auch forgotPassword() haben ein totes return; als erste Anweisung, noch vor dem eigentlichen mailerService.sendMail(...) Aufruf. Der Versand-Code wird nie erreicht. Das ist die alleinige Ursache.

Korrektur: die im Repo-Root gelöschte .env (Commit 1d4546d "styling", korrekt aus dem Git-Tracking entfernt und zu .gitignore hinzugefügt) ist eine andere, von der App nicht genutzte Datei. Die tatsächlich verwendete myteamwallet_backend/.env (das Arbeitsverzeichnis der NestJS-App) existiert bereits lokal, ist bereits in myteamwallet_backend/.gitignore ausgeschlossen und enthält bereits gültige MAIL_*/DATABASE_* Werte (Strato-SMTP, lokale MySQL-Instanz läuft). Es ist keine Aktion an .env nötig.

Zusätzlich bestehende Baustellen, die in diesem Zug mit erledigt werden:

  • E-Mail-Templates (activation.hbs, reset-password.hbs) sind unverändertes, unstyled Boilerplate aus dem nestjs-boilerplate Scaffold (graue Kopfzeile, generischer Button, kein Branding).
  • Die E-Mail-Texte laufen aktuell über nestjs-i18n, das ausschließlich für diese zwei Mails genutzt wird und nur eine en-Locale hat, obwohl das Produkt (myteamwallet.de) deutschsprachig ist. Entscheidung (siehe unten): nestjs-i18n komplett entfernen, Texte direkt auf Deutsch im Code/Template.

Das Frontend (myteamwallet_frontend_modern, das aktuell aktiv gebaute/deployte Frontend laut Dockerfile) hat kein fertiges Bild-Logo — nur generische Angular-Default-Icons. Als Markenfarbe dient das dort verwendete Grün #2e7d32 (Material theme-color), Schriftart Roboto, Wordmark-Schreibweise „TeamWallet".

Entscheidungen aus dem Brainstorming

  • SMTP-Zugangsdaten: bleiben unangetastet — myteamwallet_backend/.env existiert bereits lokal mit gültigen Strato-Werten (keine Rotation im Rahmen dieser Änderung).
  • Testing-Infrastruktur: kein lokaler MailDev-Container/e2e-Aufbau in diesem Zug (siehe Testing-Abschnitt) — Unit-Test + einmalige manuelle Verifikation mit echtem Versand stattdessen.
  • Sprachen: nur Deutsch. Keine mehrsprachige i18n-Infrastruktur für Mails.
  • i18n-Mechanismus: kein Sprachdatei-System — deutsche Texte direkt im Code/Template. nestjs-i18n wird komplett entfernt (Modul-Registrierung, src/i18n/, Dependency, I18n-Nutzung in mail.service.ts), da es im Backend ausschließlich für die zwei Mails verwendet wurde.
  • Branding: Grün #2e7d32 (aus myteamwallet_frontend_modern), Textwordmark „TeamWallet" (kein Bild-Logo), Roboto mit System-Font-Fallback, abgerundete Card-Optik passend zum „fintech-lite" Look der App.
  • Personalisierung: Anrede mit Vornamen („Hallo Max,"), da firstName an beiden Aufrufstellen im AuthService bereits verfügbar ist.

Architektur / Komponenten

1. Bugfix mail.service.ts

  • Die zwei toten return; Statements entfernen (vor sendMail in userSignUp() und forgotPassword()).
  • I18n/I18nRequestScopeService Constructor-Injection entfernen.
  • Betreffzeilen als deutsche String-Literale direkt im Service ('E-Mail bestätigen', 'Passwort zurücksetzen').
  • MailData<T> Interface (src/mail/interfaces/mail-data.interface.ts) bleibt strukturell gleich ({ to: string; data: T }), T wird pro Aufruf um firstName?: string erweitert: MailData<{ hash: string; firstName?: string }>.

2. Aufrufstellen auth.service.ts

  • register(): dto.firstName zusätzlich in mailData.data durchreichen.
  • forgotPassword(): user.firstName zusätzlich in mailData.data durchreichen.
  • Keine Änderung an Kontrollfluss, Fehlerbehandlung oder DB-Zugriffen — nur die zusätzliche Datenübergabe.

3. i18n-Entfernung

  • app.module.ts: I18nModule.forRootAsync(...) Import und Registrierung entfernen (HeaderResolver-Import ebenfalls, falls sonst ungenutzt).
  • src/i18n/ Ordner komplett löschen (nur von Mails genutzt, siehe Analyse).
  • nestjs-i18n Dependency aus package.json entfernen (npm uninstall, damit Lockfile konsistent bleibt).
  • app.config.ts Felder fallbackLanguage/headerLanguage (APP_FALLBACK_LANGUAGE, APP_HEADER_LANGUAGE) werden mit entfernt: geprüft, sie werden ausschließlich in app.module.ts für I18nModule gelesen (kein anderer Konsument im Code) — würden sonst toten Config-Code hinterlassen.

4. Templates

  • Neues Handlebars-Partial src/mail/mail-templates/partials/layout-header.hbs und layout-footer.hbs (oder ein kombiniertes layout.hbs, falls das mit dem HandlebarsAdapter sauberer registrierbar ist) — gemeinsamer Rahmen: Header mit „TeamWallet"-Wordmark auf grünem/hellem Grund, Footer mit Kontakt-/Legal-Hinweis (z. B. „Diese E-Mail wurde automatisch von TeamWallet verschickt.").
  • activation.hbs: nutzt das Layout, Inhalt: Begrüßung mit {{firstName}} (Fallback ohne Namen falls nicht vorhanden), kurzer Erklärtext, grüner CTA-Button „E-Mail bestätigen" → {{url}}.
  • reset-password.hbs: nutzt das Layout, Inhalt: Begrüßung, Erklärtext, grüner CTA-Button „Passwort zurücksetzen" → {{url}}, Hinweis auf zeitliche Begrenzung des Links und dass die Mail ignoriert werden kann, falls nicht selbst angefragt.
  • Styling-Konstanten: Akzent #2e7d32, Radius ~12px auf Card-Container, max-width: 600px, Inline-CSS (kein externes Stylesheet — E-Mail-Client-Kompatibilität), tabellenbasiertes Layout wie bisher (kein MJML — unnötige Build-Komplexität für zwei Templates), Roboto mit Fallback-Stack (Roboto, Helvetica, Arial, sans-serif, da Web-Fonts in vielen Mail-Clients nicht geladen werden).
  • Kontext-Variablen, die mail.service.ts an Handlebars übergibt: app_name ("TeamWallet"), firstName, url, ggf. year fürs Footer-Copyright — Rest (Texte, Button-Label, Titel) fest im Template.

5. .env

Keine Aktion nötig — myteamwallet_backend/.env existiert bereits lokal mit gültigen Werten (siehe Kontext oben).

Fehlerbehandlung

  • Kein Verhaltensunterschied zu heute beabsichtigt: mailerService.sendMail(...) wirft bei SMTP-Fehlern eine Exception, die aktuell nicht speziell abgefangen wird (weder vorher noch nachher) — das bleibt so, ist außerhalb des Scopes dieser Änderung. Falls beim Testen ein SMTP- Fehler auftritt (z. B. Strato blockiert alte/rotierte Zugangsdaten), wird das separat besprochen statt stillschweigend Fehlerbehandlung hinzuzufügen.

Testing

Korrektur gegenüber einer ersten Analyse: der bestehende e2e-Test test/user/auth.e2e-spec.ts ("Confirm email") pollt eine MailDev-HTTP-Inbox-API (MAIL_HOST:MAIL_CLIENT_PORT). Dafür existiert im Repo keine Infrastruktur (kein docker-compose.yml, kein MailDev-Container), und die echte .env zeigt auf produktives Strato-SMTP statt auf eine lokale Test-Mailbox — dieser Test kann unabhängig vom Bugfix nicht grün werden, ohne separate Test-Infrastruktur aufzusetzen. Entscheidung: kein MailDev-Aufbau in diesem Zug (zu viel Zusatzaufwand für den aktuellen Scope).

Stattdessen:

  • Neuer Jest Unit-Test für MailService (gemockter MailerService/ConfigService), der verifiziert, dass userSignUp() und forgotPassword() sendMail(...) mit den erwarteten Parametern (to, subject, template, context) aufrufen. Dieser Test hätte den return;-Bug erkannt und ist die primäre automatisierte Regression-Absicherung.
  • Manuelle Verifikation: Backend lokal starten (bestehende .env, laufende lokale MySQL-Instanz vorhanden), einmal Registrierung und einmal Passwort-vergessen über die echten Endpunkte auslösen und den tatsächlichen Mailversand über Strato-SMTP mit einer echten Test-Mailadresse prüfen.
  • Der bestehende e2e-Test bleibt unverändert im Repo (nicht Teil dieses Scopes, weiterhin ohne lokale MailDev-Instanz nicht lauffähig — vorbestehende Einschränkung, nicht durch diese Änderung verursacht). Kein neuer e2e-Test für forgot-password (gleiche Infrastruktur-Einschränkung).

Out of Scope

  • Rotation der SMTP-Zugangsdaten (User macht das ggf. später selbst).
  • Mehrsprachigkeit / weitere Locales.
  • Redesign der Frontend-Flows, die auf /confirm-email/:hash bzw. /password-change/:hash linken.
  • MJML- oder Build-Pipeline-Einführung für Templates.