From b340d7ee0379a59b9b709c7db232d1ecac3e3031 Mon Sep 17 00:00:00 2001 From: Rajpreet0 Date: Fri, 14 Aug 2026 13:37:37 +0200 Subject: [PATCH] unify email design --- src/lib/mail/supabase-templates/README.md | 52 +++++ .../mail/supabase-templates/change-email.html | 90 ++++++++ .../supabase-templates/confirm-signup.html | 81 ++++++++ .../supabase-templates/reset-password.html | 84 ++++++++ .../mail/templates/booking-confirmation.ts | 195 ++++++------------ src/lib/mail/templates/booking-refund.ts | 115 ++++------- src/lib/mail/templates/components.ts | 97 +++++++++ src/lib/mail/templates/employee-onboarding.ts | 129 +++++------- src/lib/mail/templates/layout.ts | 92 +++++++++ src/lib/mail/templates/onboarding-complete.ts | 114 ++++------ src/lib/mail/templates/tokens.ts | 65 ++++++ 11 files changed, 753 insertions(+), 361 deletions(-) create mode 100644 src/lib/mail/supabase-templates/README.md create mode 100644 src/lib/mail/supabase-templates/change-email.html create mode 100644 src/lib/mail/supabase-templates/confirm-signup.html create mode 100644 src/lib/mail/supabase-templates/reset-password.html create mode 100644 src/lib/mail/templates/components.ts create mode 100644 src/lib/mail/templates/layout.ts create mode 100644 src/lib/mail/templates/tokens.ts diff --git a/src/lib/mail/supabase-templates/README.md b/src/lib/mail/supabase-templates/README.md new file mode 100644 index 0000000..12c45eb --- /dev/null +++ b/src/lib/mail/supabase-templates/README.md @@ -0,0 +1,52 @@ +# Supabase Auth E-Mail-Templates + +Diese HTML-Dateien sind **nicht** Teil des Laufzeitpfads der App. Supabase Auth versendet seine +eigenen E-Mails (Registrierung, Passwort-Reset, E-Mail-Änderung) über einen Mechanismus, der +komplett getrennt von `src/lib/mail/` (Resend) läuft und ausschließlich über das +**Supabase Dashboard** konfiguriert wird – es gibt in diesem Repo kein `supabase/`-Verzeichnis +und keine lokale Supabase-CLI-Konfiguration, über die diese Templates automatisch deployed werden +könnten. + +Die Dateien hier sind manuell ins Dashboard einzutragen, im selben Kumasy-Design (Navy `#1E293B` / +Gold `#F4C00C`) wie die Resend-Templates unter `src/lib/mail/templates/`. + +## Welche Vorlage gehört wohin + +| Datei | Supabase-Template | Wird ausgelöst durch | +| --------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `confirm-signup.html` | **Confirm signup** | `supabase.auth.signUp()` in [register/route.ts](../../../app/api/auth/register/route.ts) (User) und [register/admin/route.ts](../../../app/api/auth/register/admin/route.ts) (Manager) | +| `reset-password.html` | **Reset password** | `supabase.auth.resetPasswordForEmail()` in [forgot-password-view.tsx](../../../modules/auth/ui/views/forgot-password-view.tsx) | +| `change-email.html` | **Change email address** | `supabase.auth.updateUser({ email })` in [personal-settings-actions.ts](../../../modules/settings/actions/personal-settings-actions.ts) | + +Hinweis: Mitarbeiter-Accounts ([register/employee/route.ts](../../../app/api/auth/register/employee/route.ts)) +nutzen bewusst `admin.createUser({ email_confirm: true })` und lösen **keine** dieser Mails aus – die +Identität ist dort bereits über den Resend-Onboarding-Link bestätigt. + +## Einrichtung im Dashboard + +Für **jedes** Supabase-Projekt separat wiederholen (Production + Staging `pyxjpsalchmnlyqfnzfw`): + +1. Supabase Dashboard → **Authentication → Email Templates**. +2. Für jede der 3 Vorlagen: entsprechende Datei öffnen, Inhalt komplett kopieren, im Dashboard-Editor + der passenden Vorlage einfügen und speichern. +3. Betreff-Zeile im Dashboard separat pflegen (nicht Teil der HTML-Datei), z.B.: + - Confirm signup: „Bestätige deine E-Mail-Adresse bei Kumasy" + - Reset password: „Passwort zurücksetzen – Kumasy" + - Change email address: „Bestätige deine neue E-Mail-Adresse" + +## Platzhalter + +Supabase nutzt Go-Template-Syntax (`{{ .Variable }}`), kein JS/TS – die Escaping-Helfer aus +`../templates/utils.ts` sind hier nicht anwendbar, da Supabase die Variablen serverseitig selbst +rendert. Verwendete Platzhalter: + +- `{{ .ConfirmationURL }}` – vorgefertigter Bestätigungs-/Reset-Link (in allen 3 Templates) +- `{{ .NewEmail }}` – nur in `change-email.html`, zeigt die neue Adresse zur Kontrolle an + +## Design-Konsistenz + +Da Supabase kein gemeinsames Layout-Modul wie `layout.ts`/`components.ts` unterstützt (jede Vorlage +wird als eigenständiger String im Dashboard gespeichert), ist die visuelle Konsistenz zu den +Resend-Mails hier manuell hergestellt (gleiche Farben/Radien/Spacing hart kodiert in jeder Datei). +Bei künftigen Design-Änderungen an `src/lib/mail/templates/tokens.ts` müssen diese 3 Dateien separat +nachgezogen werden. diff --git a/src/lib/mail/supabase-templates/change-email.html b/src/lib/mail/supabase-templates/change-email.html new file mode 100644 index 0000000..370a38c --- /dev/null +++ b/src/lib/mail/supabase-templates/change-email.html @@ -0,0 +1,90 @@ + + + + + + E-Mail-Änderung bestätigen + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ Kumasy +

+ E-Mail-Änderung bestätigen +

+
+

+ Hallo, +

+ +

+ für dein Kumasy-Konto wurde eine Änderung der E-Mail-Adresse angefordert. Bitte bestätige die neue Adresse, damit die Änderung wirksam wird. +

+ + + + + +
+

Neue E-Mail-Adresse

+

{{ .NewEmail }}

+
+ + + + + +
+ + E-Mail-Änderung bestätigen + +
+ +

+ Oder kopiere diesen Link in deinen Browser: +

+

+ {{ .ConfirmationURL }} +

+ +
+

+ Falls du diese Änderung nicht angefordert hast, kannst du diese E-Mail ignorieren – deine aktuelle E-Mail-Adresse bleibt dann unverändert. +

+
+
+

+ Kumasy +

+

+ Diese E-Mail wurde automatisch generiert. Bitte antworte nicht auf diese E-Mail. +

+

+ Powered by Kumasy +

+
+
+ + diff --git a/src/lib/mail/supabase-templates/confirm-signup.html b/src/lib/mail/supabase-templates/confirm-signup.html new file mode 100644 index 0000000..ed61649 --- /dev/null +++ b/src/lib/mail/supabase-templates/confirm-signup.html @@ -0,0 +1,81 @@ + + + + + + Bestätige deine E-Mail-Adresse + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ Kumasy +

+ Willkommen bei Kumasy! +

+
+

+ Hallo, +

+ +

+ vielen Dank für deine Registrierung bei Kumasy. Bitte bestätige deine E-Mail-Adresse, um dein Konto zu aktivieren. +

+ + + + + +
+ + E-Mail-Adresse bestätigen + +
+ +

+ Oder kopiere diesen Link in deinen Browser: +

+

+ {{ .ConfirmationURL }} +

+ +
+

+ Falls du dich nicht bei Kumasy registriert hast, kannst du diese E-Mail ignorieren. +

+
+
+

+ Kumasy +

+

+ Diese E-Mail wurde automatisch generiert. Bitte antworte nicht auf diese E-Mail. +

+

+ Powered by Kumasy +

+
+
+ + diff --git a/src/lib/mail/supabase-templates/reset-password.html b/src/lib/mail/supabase-templates/reset-password.html new file mode 100644 index 0000000..aa28987 --- /dev/null +++ b/src/lib/mail/supabase-templates/reset-password.html @@ -0,0 +1,84 @@ + + + + + + Passwort zurücksetzen + + + + + + +
+ + + + + + + + + + + + + + + + + +
+ Kumasy +

+ Passwort zurücksetzen +

+
+

+ Hallo, +

+ +

+ wir haben eine Anfrage erhalten, das Passwort für dein Kumasy-Konto zurückzusetzen. Klicke auf den Button unten, um ein neues Passwort zu vergeben. +

+ + + + + +
+ + Neues Passwort vergeben + +
+ +

+ Oder kopiere diesen Link in deinen Browser: +

+

+ {{ .ConfirmationURL }} +

+ +
+

+ Wichtig: Dieser Link ist aus Sicherheitsgründen zeitlich begrenzt gültig. +

+

+ Falls du kein neues Passwort angefordert hast, kannst du diese E-Mail ignorieren – dein Passwort bleibt unverändert. +

+
+
+

+ Kumasy +

+

+ Diese E-Mail wurde automatisch generiert. Bitte antworte nicht auf diese E-Mail. +

+

+ Powered by Kumasy +

+
+
+ + diff --git a/src/lib/mail/templates/booking-confirmation.ts b/src/lib/mail/templates/booking-confirmation.ts index c20a9d1..944436a 100644 --- a/src/lib/mail/templates/booking-confirmation.ts +++ b/src/lib/mail/templates/booking-confirmation.ts @@ -6,6 +6,9 @@ */ import { escapeHtml, sanitizeUrl, singleLine } from "./utils"; +import { renderButton, renderDivider, renderInfoBox, renderPriceRow } from "./components"; +import { renderEmailLayout } from "./layout"; +import { KUMASY_BRANDING, type EmailBranding } from "./tokens"; interface BookingConfirmationEmailParams { courseName: string; @@ -18,6 +21,7 @@ interface BookingConfirmationEmailParams { myCoursesUrl: string; coverImageUrl?: string | null; companyName?: string; + branding?: EmailBranding; } function formatDate(date: Date): string { @@ -48,6 +52,7 @@ export function generateBookingConfirmationEmail({ myCoursesUrl, coverImageUrl, companyName = "Ihr Unternehmen", + branding = KUMASY_BRANDING, }: BookingConfirmationEmailParams): { subject: string; html: string; text: string } { // Defense gegen Markup-Injection: alle dynamischen Werte vor dem HTML-Einsetzen escapen. const safeCourseName = escapeHtml(courseName); @@ -55,7 +60,6 @@ export function generateBookingConfirmationEmail({ const safeBusinessAddress = businessAddress ? escapeHtml(businessAddress) : ""; const safeTimeFrom = escapeHtml(timeFrom); const safeTimeTo = escapeHtml(timeTo); - const safeCompanyName = escapeHtml(companyName); const safeMyCoursesUrl = sanitizeUrl(myCoursesUrl); const safeCoverImageUrl = coverImageUrl ? sanitizeUrl(coverImageUrl) : null; @@ -63,140 +67,67 @@ export function generateBookingConfirmationEmail({ const formattedDate = formatDate(courseDate); const formattedPrice = formatPrice(amountPaidCents); - const html = ` - - - - - - Buchungsbestätigung - - - + const bannerHtml = safeCoverImageUrl + ? ` - - -
- - - ${ - safeCoverImageUrl - ? ` - - - - - ` - : "" - } - - - - - - - - - - - - - - - - -
- ${safeCourseName} -
-

- Buchung bestätigt ✓ -

-
-

- Hallo, -

- -

- vielen Dank für Ihre Buchung. Ihre Zahlung wurde erfolgreich verarbeitet. - Hier sind die Details Ihres Kurses: -

- - - - - - -
-

Kurs

-

${safeCourseName}

- -

Anbieter

-

${safeBusinessName}

- -

Termin

-

${formattedDate}

-

${safeTimeFrom} – ${safeTimeTo} Uhr

- - ${ - safeBusinessAddress - ? ` -

Ort

-

${safeBusinessAddress}

- ` - : "" - } - -
- - - - - -
Bezahlt${formattedPrice}
-
-
- -

- Wir wünschen Ihnen viel Spaß im Kurs! -

- - - - - - -
- - Zu meinen Kursen - -
- -

- Oder kopieren Sie diesen Link in Ihren Browser: -

-

- ${safeMyCoursesUrl} -

- -
-

- Bei Fragen zu Ihrer Buchung wenden Sie sich bitte direkt an den Anbieter ${safeBusinessName}. -

-
-
-

- ${safeCompanyName} -

-

- Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht auf diese E-Mail. -

-
+
+ ${safeCourseName}
- - + ` + : ""; + + const bodyHtml = ` +

+ Hallo, +

+ +

+ vielen Dank für Ihre Buchung. Ihre Zahlung wurde erfolgreich verarbeitet. + Hier sind die Details Ihres Kurses: +

+ + ${renderInfoBox({ + rows: [ + { + label: "Kurs", + valueHtml: `${safeCourseName}`, + }, + { label: "Anbieter", valueHtml: safeBusinessName }, + { label: "Termin", valueHtml: `${formattedDate}
${safeTimeFrom} – ${safeTimeTo} Uhr` }, + ...(safeBusinessAddress ? [{ label: "Ort", valueHtml: safeBusinessAddress }] : []), + ], + })} + + ${renderDivider(renderPriceRow({ label: "Bezahlt", amountFormatted: formattedPrice }))} + +

+ Wir wünschen Ihnen viel Spaß im Kurs! +

+ + ${renderButton({ label: "Zu meinen Kursen", href: myCoursesUrl, branding })} + +

+ Oder kopieren Sie diesen Link in Ihren Browser: +

+

+ ${safeMyCoursesUrl} +

+ + ${renderDivider(` +

+ Bei Fragen zu Ihrer Buchung wenden Sie sich bitte direkt an den Anbieter ${safeBusinessName}. +

+ `)} `; + const html = renderEmailLayout({ + title: "Buchungsbestätigung", + headerHeading: "Buchung bestätigt ✓", + bannerHtml, + bodyHtml, + branding, + }); + const text = ` Buchung bestätigt – ${courseName} @@ -217,6 +148,8 @@ Bei Fragen zu Ihrer Buchung wenden Sie sich bitte direkt an den Anbieter ${busin ${companyName} Diese E-Mail wurde automatisch generiert. + +Powered by Kumasy `; return { subject, html, text }; diff --git a/src/lib/mail/templates/booking-refund.ts b/src/lib/mail/templates/booking-refund.ts index 78db6a9..9c51b46 100644 --- a/src/lib/mail/templates/booking-refund.ts +++ b/src/lib/mail/templates/booking-refund.ts @@ -10,6 +10,9 @@ import { escapeHtml, singleLine } from "./utils"; import { formatPrice, formatPercent } from "@/src/lib/currency"; +import { renderDivider, renderInfoBox } from "./components"; +import { renderEmailLayout } from "./layout"; +import { KUMASY_BRANDING, type EmailBranding } from "./tokens"; export type BookingRefundReason = "overbooked" | "cancelled_by_user" | "cancelled_by_manager"; @@ -21,6 +24,7 @@ interface BookingRefundEmailParams { reason: BookingRefundReason; originalAmountCents?: number; // nur relevant/angezeigt wenn refundPercent < 100 companyName?: string; + branding?: EmailBranding; } const REASON_COPY: Record< @@ -64,11 +68,11 @@ export function generateBookingRefundEmail({ reason, originalAmountCents, companyName = "Ihr Unternehmen", + branding = KUMASY_BRANDING, }: BookingRefundEmailParams): { subject: string; html: string; text: string } { // Defense gegen Markup-Injection in der Mail. const safeCourseName = escapeHtml(courseName); const safeBusinessName = escapeHtml(businessName); - const safeCompanyName = escapeHtml(companyName); const hasRefund = refundPercent > 0 && amountRefundedCents > 0; const isPartial = hasRefund && refundPercent < 100 && originalAmountCents !== undefined; @@ -112,94 +116,47 @@ export function generateBookingRefundEmail({ : ""; const refundBoxHtml = hasRefund - ? ` - - - -
- - - - - -
Erstatteter Betrag${formattedPrice}
-
` + ? renderInfoBox({ + variant: "danger", + rows: [{ label: "Erstatteter Betrag", valueHtml: formattedPrice }], + }) : ""; const refundBoxText = hasRefund ? `Erstatteter Betrag: ${formattedPrice}\n\n` : ""; - const html = ` - - - - - - ${hasRefund ? "Rückerstattung" : "Stornierung"} - - - - - - -
- + const bodyHtml = ` +

+ Hallo, +

- - - - +

+ ${introHtml} +

- - - - - - - - - - -
-

- ${hasRefund ? "Rückerstattung wurde veranlasst" : "Stornierung bestätigt"} -

-
-

- Hallo, -

+ ${refundStatusHtml} -

- ${introHtml} -

+ ${policyLineHtml} - ${refundStatusHtml} + ${refundBoxHtml} - ${policyLineHtml} +

+ Sie können gerne einen anderen freien Kurs bei ${safeBusinessName} buchen. +

- ${refundBoxHtml} - -

- Sie können gerne einen anderen freien Kurs bei ${safeBusinessName} buchen. -

- -
-

- Bei Fragen zur Rückerstattung antworten Sie nicht auf diese E-Mail, sondern - wenden Sie sich an Ihre Bank oder Ihren Karten-Anbieter. -

-
-
-

- ${safeCompanyName} -

-

- Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht auf diese E-Mail. -

-
-
- - + ${renderDivider(` +

+ Bei Fragen zur Rückerstattung antworten Sie nicht auf diese E-Mail, sondern + wenden Sie sich an Ihre Bank oder Ihren Karten-Anbieter. +

+ `)} `; + const html = renderEmailLayout({ + title: hasRefund ? "Rückerstattung" : "Stornierung", + headerHeading: hasRefund ? "Rückerstattung wurde veranlasst" : "Stornierung bestätigt", + bodyHtml, + branding, + }); + const text = ` ${hasRefund ? "Rückerstattung" : "Stornierung bestätigt"} – ${courseName} @@ -215,6 +172,8 @@ Bei Fragen zur Rückerstattung wenden Sie sich an Ihre Bank oder Ihren Karten-An ${companyName} Diese E-Mail wurde automatisch generiert. + +Powered by Kumasy `; return { subject, html, text }; diff --git a/src/lib/mail/templates/components.ts b/src/lib/mail/templates/components.ts new file mode 100644 index 0000000..6c5c3ff --- /dev/null +++ b/src/lib/mail/templates/components.ts @@ -0,0 +1,97 @@ +/** + * Wiederverwendbare HTML-Fragmente für E-Mail-Templates. + * + * Jede Funktion gibt einen HTML-String-Fragment zurück (kein vollständiges Dokument). + */ + +import { escapeHtml, sanitizeUrl } from "./utils"; +import { EMAIL_TOKENS, KUMASY_BRANDING, type EmailBranding } from "./tokens"; + +interface InfoBoxRow { + label: string; + /** Bereits fertiges HTML (z.B. inkl. ). Caller ist für Escaping verantwortlich. */ + valueHtml: string; +} + +export function renderInfoBox({ + rows, + variant = "default", +}: { + rows: InfoBoxRow[]; + variant?: "default" | "danger"; +}): string { + const background = + variant === "danger" + ? EMAIL_TOKENS.colors.dangerBackground + : EMAIL_TOKENS.colors.infoBoxBackground; + const border = variant === "danger" ? `1px solid ${EMAIL_TOKENS.colors.dangerBorder}` : "none"; + + const rowsHtml = rows + .map( + (row, index) => ` +

${escapeHtml(row.label)}

+

${row.valueHtml}

` + ) + .join(""); + + return ` + + + + +
+ ${rowsHtml} +
`; +} + +export function renderButton({ + label, + href, + branding = KUMASY_BRANDING, +}: { + label: string; + href: string; + branding?: EmailBranding; +}): string { + const safeLabel = escapeHtml(label); + const safeHref = sanitizeUrl(href); + + return ` + + + + +
+ + ${safeLabel} + +
`; +} + +export function renderDivider(innerHtml: string): string { + return ` +
+ ${innerHtml} +
`; +} + +export function renderPriceRow({ + label, + amountFormatted, + emphasis = false, +}: { + label: string; + amountFormatted: string; + emphasis?: boolean; +}): string { + const valueColor = emphasis ? EMAIL_TOKENS.colors.dangerText : EMAIL_TOKENS.colors.textPrimary; + + return ` + + + + + +
${escapeHtml(label)}${escapeHtml(amountFormatted)}
`; +} diff --git a/src/lib/mail/templates/employee-onboarding.ts b/src/lib/mail/templates/employee-onboarding.ts index bc8b4c2..3d389d6 100644 --- a/src/lib/mail/templates/employee-onboarding.ts +++ b/src/lib/mail/templates/employee-onboarding.ts @@ -4,10 +4,16 @@ * Dieses Template wird an neue Mitarbeiter gesendet, um ihnen den Onboarding-Link zu schicken. */ +import { escapeHtml, sanitizeUrl, singleLine } from "./utils"; +import { renderButton, renderDivider } from "./components"; +import { renderEmailLayout } from "./layout"; +import { KUMASY_BRANDING, type EmailBranding } from "./tokens"; + interface OnboardingEmailParams { email: string; onboardingUrl: string; companyName?: string; + branding?: EmailBranding; } /** @@ -17,99 +23,54 @@ export function generateOnboardingEmail({ email, onboardingUrl, companyName = "Ihr Unternehmen", + branding = KUMASY_BRANDING, }: OnboardingEmailParams): { subject: string; html: string; text: string } { - const subject = `Willkommen bei ${companyName} - Vervollständigen Sie Ihr Profil`; + const safeEmail = escapeHtml(email); + const safeOnboardingUrl = sanitizeUrl(onboardingUrl); + const safeCompanyName = escapeHtml(companyName); - const html = ` - - - - - - Willkommen - - - - - - -
- - + const subject = singleLine(`Willkommen bei ${companyName} - Vervollständigen Sie Ihr Profil`); - - - - + const bodyHtml = ` +

+ Hallo, +

- - - - - - - - - - -
-

- Willkommen bei ${companyName}! -

-
-

- Hallo, -

+

+ Sie wurden als Mitarbeiter bei ${safeCompanyName} registriert. Um Ihr Profil zu vervollständigen und Zugang zum System zu erhalten, klicken Sie bitte auf den Button unten. +

-

- Sie wurden als Mitarbeiter bei ${companyName} registriert. Um Ihr Profil zu vervollständigen und Zugang zum System zu erhalten, klicken Sie bitte auf den Button unten. -

+

+ Ihre registrierte E-Mail-Adresse: ${safeEmail} +

-

- Ihre registrierte E-Mail-Adresse: ${email} -

+ ${renderButton({ label: "Profil vervollständigen", href: onboardingUrl, branding })} - - - - - -
- - Profil vervollständigen - -
+

+ Oder kopieren Sie diesen Link in Ihren Browser: +

-

- Oder kopieren Sie diesen Link in Ihren Browser: -

+

+ ${safeOnboardingUrl} +

-

- ${onboardingUrl} -

- -
-

- Wichtig: Dieser Link ist 7 Tage gültig. -

-

- Falls Sie diese E-Mail nicht erwartet haben, ignorieren Sie sie bitte. -

-
-
-

- ${companyName} -

-

- Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht auf diese E-Mail. -

-
-
- - + ${renderDivider(` +

+ Wichtig: Dieser Link ist 7 Tage gültig. +

+

+ Falls Sie diese E-Mail nicht erwartet haben, ignorieren Sie sie bitte. +

+ `)} `; + const html = renderEmailLayout({ + title: "Willkommen", + headerHeading: `Willkommen bei ${safeCompanyName}!`, + bodyHtml, + branding, + }); + const text = ` Willkommen bei ${companyName}! @@ -117,7 +78,7 @@ Hallo, Sie wurden als Mitarbeiter bei ${companyName} registriert. Um Ihr Profil zu vervollständigen und Zugang zum System zu erhalten, öffnen Sie bitte den folgenden Link: -${onboardingUrl} +${safeOnboardingUrl} Ihre registrierte E-Mail-Adresse: ${email} @@ -127,6 +88,8 @@ Falls Sie diese E-Mail nicht erwartet haben, ignorieren Sie sie bitte. ${companyName} Diese E-Mail wurde automatisch generiert. + +Powered by Kumasy `; return { subject, html, text }; diff --git a/src/lib/mail/templates/layout.ts b/src/lib/mail/templates/layout.ts new file mode 100644 index 0000000..70abd5b --- /dev/null +++ b/src/lib/mail/templates/layout.ts @@ -0,0 +1,92 @@ +/** + * Gemeinsame HTML-Shell für alle E-Mail-Templates. + * + * Tabellen-basiertes Layout mit ausschließlich inline-Styles (Outlook-Kompatibilität, + * kein