69 lines
2.7 KiB
TypeScript
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)}`;
|
|
}
|