Kuma/src/lib/mail/templates/layout.ts
2026-10-02 17:31:08 +02:00

100 lines
3.9 KiB
TypeScript

/**
* Gemeinsame HTML-Shell für alle E-Mail-Templates.
*
* Tabellen-basiertes Layout mit ausschließlich inline-Styles (Outlook-Kompatibilität,
* kein <style>-Block, kein Flexbox/Grid, keine CSS-Gradients).
*/
import { escapeHtml, sanitizeColor, sanitizeUrl } from "./utils";
import { EMAIL_TOKENS, KUMASY_BRANDING, type EmailBranding } from "./tokens";
export interface RenderEmailLayoutParams {
title: string;
/** H1-Text im Header-Band. Vom Aufrufer bereits escaped/vertrauenswürdig. */
headerHeading: string;
/** Optionale volle Reihe ÜBER dem Header-Band (z.B. Cover-Bild). */
bannerHtml?: string;
/** Fertiger Inner-Content, wird unverändert in die Content-Zelle eingesetzt. */
bodyHtml: string;
branding?: EmailBranding;
/**
* Ersetzt den Hinweis "automatisch generiert, bitte nicht antworten" im
* Footer - z.B. bei Werbe-Mails, auf die Kunden antworten dürfen und die
* einen Abmeldelink brauchen. Vom Aufrufer bereits escaped.
*/
footerNoteHtml?: string;
}
export function renderEmailLayout({
title,
headerHeading,
bannerHtml,
bodyHtml,
branding = KUMASY_BRANDING,
footerNoteHtml,
}: RenderEmailLayoutParams): string {
const safeTitle = escapeHtml(title);
const safeCompanyName = escapeHtml(branding.companyName);
const safeLogoAlt = escapeHtml(branding.logoAlt);
const safePrimaryColor = sanitizeColor(branding.primaryColor, KUMASY_BRANDING.primaryColor);
const headerLogoHtml = branding.logoUrl
? `<img src="${sanitizeUrl(branding.logoUrl)}" alt="${safeLogoAlt}" width="160" style="display: block; margin: 0 auto 16px; max-width: 160px; height: auto;" />`
: `<p style="margin: 0 0 12px; color: #ffffff; font-size: 15px; font-weight: 700; letter-spacing: 0.5px; text-transform: uppercase;">${safeCompanyName}</p>`;
return `
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${safeTitle}</title>
</head>
<body style="margin: 0; padding: 0; font-family: ${EMAIL_TOKENS.fontFamily}; background-color: ${EMAIL_TOKENS.colors.bodyBackground};">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: ${EMAIL_TOKENS.colors.bodyBackground}; padding: ${EMAIL_TOKENS.spacing.pagePadding}px 0;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: ${EMAIL_TOKENS.colors.cardBackground}; border-radius: ${EMAIL_TOKENS.radius.card}; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
${bannerHtml ?? ""}
<!-- Header -->
<tr>
<td style="background-color: ${safePrimaryColor}; padding: 40px; text-align: center;">
${headerLogoHtml}
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: 600;">
${headerHeading}
</h1>
</td>
</tr>
<!-- Content -->
<tr>
<td style="padding: ${EMAIL_TOKENS.spacing.cardPadding}px;">
${bodyHtml}
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: ${EMAIL_TOKENS.colors.footerBackground}; padding: ${EMAIL_TOKENS.spacing.footerPadding}px; text-align: center; border-top: 1px solid ${EMAIL_TOKENS.colors.border};">
<p style="margin: 0 0 10px; color: ${EMAIL_TOKENS.colors.textSecondary}; font-size: 14px;">
${safeCompanyName}
</p>
<p style="margin: 0 0 10px; color: ${EMAIL_TOKENS.colors.textMuted}; font-size: 12px;">
${footerNoteHtml ?? "Diese E-Mail wurde automatisch generiert. Bitte antworten Sie nicht auf diese E-Mail."}
</p>
<p style="margin: 0; color: ${EMAIL_TOKENS.colors.textMuted}; font-size: 11px;">
Powered by Kumasy
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`;
}