Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/integrations/email/personalization.ts
T
2026-07-08 16:15:37 +02:00

69 lines
2.7 KiB
TypeScript

// AK-P14b (P14 email marketing) — pure personalization + footer injection.
//
// Replaces `{{token}}` placeholders in subject/body with per-recipient values
// (standard contact tokens + tenant custom fields as `{{cf.<key>}}`), and injects
// the DSGVO-mandatory footer (Impressum + unsubscribe link) when missing. PURE:
// no DB, no React — fully unit-testable. Used by the send-engine (S3) and the
// pre-send checklist (S5).
/** Standard token source for a recipient. */
export interface RecipientTokenSource {
vorname?: string | null;
nachname?: string | null;
email?: string | null;
orgName?: string | null;
/** Tenant custom-field values by field key (rendered as `{{cf.<key>}}`). */
customFields?: Record<string, string>;
}
/** Build the `{{token}} → value` map for a recipient. Missing values render
* as empty strings so a template never leaks a raw `{{token}}`. */
export function buildTokenMap(source: RecipientTokenSource): Record<string, string> {
const map: Record<string, string> = {
firstName: source.vorname ?? "",
lastName: source.nachname ?? "",
email: source.email ?? "",
company_name: source.orgName ?? "",
};
for (const [key, value] of Object.entries(source.customFields ?? {})) {
map[`cf.${key}`] = value;
}
return map;
}
const TOKEN_PATTERN = /\{\{\s*([\w.]+)\s*\}\}/g;
/** Replace every `{{token}}` with its mapped value. Unknown tokens collapse to
* an empty string (never left raw in a sent mail). */
export function replaceTokens(template: string, tokens: Record<string, string>): string {
return template.replace(TOKEN_PATTERN, (_match, key: string) => tokens[key] ?? "");
}
/** Marker the footer carries so re-injection is idempotent. */
export const FOOTER_MARKER = "data-hg-footer";
/** True when the html already contains an injected footer. */
export function hasFooter(html: string): boolean {
return html.includes(FOOTER_MARKER);
}
/**
* Append the mandatory footer to the html body if not already present. The
* footer carries the Impressum + the unsubscribe link; `{{unsubscribeUrl}}` in
* the footer template is replaced with the per-recipient URL.
*/
export function injectFooter(html: string, footerHtml: string, unsubscribeUrl: string): string {
if (hasFooter(html)) {
return replaceTokens(html, { unsubscribeUrl });
}
const footer = replaceTokens(footerHtml, { unsubscribeUrl });
const wrapped = `<div ${FOOTER_MARKER}="1">${footer}</div>`;
return `${html}${wrapped}`;
}
/** Build the public one-click unsubscribe URL for a signed token. */
export function buildUnsubscribeUrl(baseUrl: string, token: string): string {
const trimmed = baseUrl.replace(/\/+$/, "");
return `${trimmed}/unsubscribe/${encodeURIComponent(token)}`;
}