import { readdirSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { config } from '../config.js'
/**
* HTML-Mails aus den Vorlagen in server/mail-templates (Claude-Design-Projekt
* "Webklar Bestätigungsmail gestalten"; K = an Kunden, T = intern ans Team).
*
* Jede Vorlage beschreibt sich im Kopfkommentar selbst:
* Betreff: Text mit {{platzhaltern}}
* Platzhalter: {{name}}, {{email}}, ...
* Optionale Blöcke (...): angebot, grund -> ...
* renderMail() setzt die Werte HTML-sicher ein, entfernt nicht zutreffende Blöcke samt Inhalt und
* liefert Betreff, HTML und eine Textfassung für Mailprogramme ohne HTML. Eine neue Fassung aus dem
* Design kann eine Datei 1:1 ersetzen, solange Platzhalter und Blöcke gleich heißen.
*/
const TEMPLATE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'mail-templates')
// In den Vorlagen fest eingetragene Portal-Adresse; beim Rendern gilt config.publicBaseUrl
const DESIGN_BASE_URL = 'https://project.webklar.com'
const cache = new Map()
/** IDs aller Vorlagen (K01, K02, ..., T11) */
export function templateIds() {
return readdirSync(TEMPLATE_DIR)
.filter((f) => /^[KT]\d{2} .*\.html$/.test(f))
.map((f) => f.slice(0, 3))
.sort()
}
const nameList = (line) =>
String(line || '')
.split(',')
.map((s) => s.trim().replace(/^\{\{|\}\}$/g, ''))
.filter(Boolean)
/** Vorlage lesen und Kopfkommentar auswerten (einmal je Prozess). */
export function loadTemplate(id) {
if (cache.has(id)) return cache.get(id)
const file = readdirSync(TEMPLATE_DIR).find((f) => f.startsWith(`${id} `) && f.endsWith('.html'))
if (!file) throw new Error(`Mail-Vorlage ${id} fehlt`)
const source = readFileSync(path.join(TEMPLATE_DIR, file), 'utf8')
const header = source.match(/\n?/)
if (header?.[1] !== id) throw new Error(`Mail-Vorlage ${file}: Kopfkommentar fehlt`)
const field = (label) => header[0].match(new RegExp(`^\\s*${label}[^:\\n]*:[ \\t]*(.*)$`, 'm'))?.[1]?.trim() || ''
const template = {
id,
file,
subject: field('Betreff'),
placeholders: nameList(field('Platzhalter')),
blocks: nameList(field('Optionale Blöcke')),
body: source.replace(header[0], ''),
}
if (!template.subject) throw new Error(`Mail-Vorlage ${file}: Betreff fehlt`)
cache.set(id, template)
return template
}
export const escapeHtml = (value) =>
String(value)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
/** ... : Inhalt behalten (ohne Markierungen) oder ganz entfernen */
function applyBlocks(html, blocks) {
const block = /\n?([\s\S]*?)\n?/g
// Von außen nach innen, bis keine Markierung mehr übrig ist (Blöcke dürfen verschachtelt sein)
for (let i = 0; i < 10 && html.includes('\n?/g, '')
.replaceAll(DESIGN_BASE_URL, config.publicBaseUrl)
html = html
.replace(/\{\{(\w+)\}\}/g, (_, key) => escapeHtml(valueOf(key)).replace(/\r?\n/g, '
'))
// Anrede ohne bekannten Namen
.replace(/Guten Tag ,/g, 'Guten Tag,')
const subject = template.subject
.replace(/\{\{(\w+)\}\}/g, (_, key) => valueOf(key))
.replace(/\s+/g, ' ')
.trim()
return { id, subject, html, text: htmlToText(html), problems: [...new Set(problems)] }
}
// ---------------------------------------------------------------------------
// Textfassung (multipart/alternative): aus dem gerenderten HTML abgeleitet,
// damit beide Fassungen immer denselben Inhalt haben
// ---------------------------------------------------------------------------
const NAMED_ENTITIES = { nbsp: ' ', amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", rarr: '→' }
function decodeEntities(text) {
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (entity, code) => {
if (code[0] !== '#') return NAMED_ENTITIES[code.toLowerCase()] ?? entity
const point = code[1].toLowerCase() === 'x' ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10)
return point === 847 ? '' : String.fromCodePoint(point)
})
}
function linkText({ href, label, button }) {
const url = decodeEntities(href)
const text = label.replace(/\s*→\s*$/, '').trim()
if (/^(mailto|tel):/i.test(url) || !url) return text
if (!text || text === url) return url
return button ? `\n${text}: ${url}\n` : `${text} (${url})`
}
/** HTML der Vorlagen (Tabellen-Layout) in lesbaren Text umwandeln. */
export function htmlToText(html) {
const body = (html.match(/