Basis: Certvia dev@a48c5fb als Fundament für Craftvia
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>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { BRAND, BRAND_COLORS } from "@/lib/brand";
|
||||
import { DOCUMENT_THEME } from "@/lib/document-brand";
|
||||
|
||||
/**
|
||||
* Brandfähige E-Mail-Template-Basis (Story S6).
|
||||
*
|
||||
* SMTP und der Einladungs-/Reset-Flow kommen erst mit Paket 4 — dieses Modul
|
||||
* liefert vorab das Certvia-Gerüst, damit die Templates dann nur noch Inhalt
|
||||
* einsetzen müssen und das CD nicht neu erfunden wird.
|
||||
*
|
||||
* Bewusste Einschränkungen für E-Mail-Clients:
|
||||
* - keine CSS-Variablen, keine externen Stylesheets → alles inline
|
||||
* - Tabellen-Layout statt Flex/Grid (Outlook)
|
||||
* - Logo als absolute URL, kein Inline-SVG (viele Clients rendern es nicht)
|
||||
* - immer eine Plaintext-Alternative mitschicken (`renderTextEmail`)
|
||||
*/
|
||||
|
||||
export type EmailContent = {
|
||||
/** Betreff — wird nicht in den Body gerendert, nur durchgereicht */
|
||||
subject: string;
|
||||
/** Überschrift im Body */
|
||||
heading: string;
|
||||
/** Absätze; jeder Eintrag wird ein <p> */
|
||||
paragraphs: string[];
|
||||
/** Optionaler Call-to-Action */
|
||||
action?: { label: string; url: string };
|
||||
/** Kleingedrucktes über der Fußzeile (z. B. Gültigkeitsdauer eines Links) */
|
||||
note?: string;
|
||||
/**
|
||||
* Zusatzzeile in der Fußzeile — für den Abmelde-/Präferenzhinweis bei
|
||||
* Benachrichtigungsmails. Sicherheitsrelevante Transaktionsmails (Reset,
|
||||
* Passwortwechsel) setzen ihn bewusst NICHT: sie sind nicht abbestellbar.
|
||||
*/
|
||||
footerNote?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Basis-URL für absolute Links/Bilder in Mails. Relative Pfade funktionieren in
|
||||
* E-Mail-Clients nicht.
|
||||
*/
|
||||
function baseUrl(): string {
|
||||
return (process.env.AUTH_URL ?? process.env.NEXTAUTH_URL ?? "https://app.certvia.de").replace(
|
||||
/\/+$/,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
/** Absenderzeile für den `From`-Header. */
|
||||
export function emailSender(address: string): string {
|
||||
return `${BRAND.name} <${address}>`;
|
||||
}
|
||||
|
||||
/** Signatur-/Fußzeilentext — enthält immer den Dachmarken-Hinweis. */
|
||||
export function emailSignature(): string {
|
||||
return `${BRAND.name} — ${BRAND.byline}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendert eine Mail im Certvia-CD (heller Grund, Marken-Violett, Poppins-Kopf).
|
||||
* Rückgabe ist ein vollständiges HTML-Dokument.
|
||||
*/
|
||||
export function renderHtmlEmail(content: EmailContent): string {
|
||||
const url = baseUrl();
|
||||
const t = DOCUMENT_THEME;
|
||||
|
||||
const paragraphs = content.paragraphs
|
||||
.map(
|
||||
(p) =>
|
||||
`<p style="margin:0 0 14px;font-family:${t.bodyFont};font-size:15px;line-height:1.6;color:${t.text};">${escapeHtml(p)}</p>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const action = content.action
|
||||
? `<table role="presentation" cellpadding="0" cellspacing="0" style="margin:24px 0;">
|
||||
<tr><td style="background:${BRAND_COLORS.violet};border-radius:8px;">
|
||||
<a href="${escapeHtml(content.action.url)}" style="display:inline-block;padding:12px 22px;font-family:${t.headingFont};font-size:15px;font-weight:600;color:#ffffff;text-decoration:none;">${escapeHtml(content.action.label)}</a>
|
||||
</td></tr>
|
||||
</table>`
|
||||
: "";
|
||||
|
||||
const note = content.note
|
||||
? `<p style="margin:0;font-family:${t.bodyFont};font-size:12px;line-height:1.5;color:${t.textMuted};">${escapeHtml(content.note)}</p>`
|
||||
: "";
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="de">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(content.subject)}</title></head>
|
||||
<body style="margin:0;padding:0;background:#f4f2fa;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f4f2fa;padding:28px 12px;">
|
||||
<tr><td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width:560px;background:${t.pageBackground};border-radius:14px;overflow:hidden;border:1px solid ${t.rule};">
|
||||
<tr><td style="padding:26px 30px 0;">
|
||||
<img src="${url}/assets/logo/certvia-lockup-positiv.svg" alt="${BRAND.name}" height="34" style="height:34px;width:auto;display:block;border:0;">
|
||||
</td></tr>
|
||||
<tr><td style="padding:22px 30px 4px;">
|
||||
<h1 style="margin:0 0 14px;font-family:${t.headingFont};font-size:20px;font-weight:600;color:${t.text};">${escapeHtml(content.heading)}</h1>
|
||||
${paragraphs}
|
||||
${action}
|
||||
${note}
|
||||
</td></tr>
|
||||
<tr><td style="padding:22px 30px 26px;">
|
||||
<div style="border-top:1px solid ${t.rule};padding-top:14px;font-family:${t.bodyFont};font-size:12px;color:${t.textMuted};">
|
||||
${escapeHtml(emailSignature())}${
|
||||
content.footerNote
|
||||
? `<br><span style="font-size:11px;">${escapeHtml(content.footerNote)}</span>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/** Plaintext-Alternative zur HTML-Mail (immer als `text` mitschicken). */
|
||||
export function renderTextEmail(content: EmailContent): string {
|
||||
const lines = [content.heading, "", ...content.paragraphs];
|
||||
if (content.action) lines.push("", `${content.action.label}: ${content.action.url}`);
|
||||
if (content.note) lines.push("", content.note);
|
||||
lines.push("", "—", emailSignature());
|
||||
if (content.footerNote) lines.push(content.footerNote);
|
||||
return lines.join("\n");
|
||||
}
|
||||
Reference in New Issue
Block a user