import nodemailer from 'nodemailer' import { env } from './env.js' let cachedTransporter = null function getTransporter() { if (cachedTransporter) return cachedTransporter const host = env('SMTP_HOST') const user = env('SMTP_USER') const pass = env('SMTP_PASS') if (!host || !user || !pass) { throw new Error('SMTP configuration is incomplete.') } const port = Number(env('SMTP_PORT', '587')) if (!Number.isInteger(port) || port <= 0 || port > 65535) { throw new Error(`SMTP_PORT is not a valid port: ${env('SMTP_PORT')}`) } cachedTransporter = nodemailer.createTransport({ host, port, secure: port === 465, // implicit TLS on 465, STARTTLS otherwise auth: { user, pass }, }) return cachedTransporter } /** * Strips CR and LF so a submitted value can never inject extra mail headers * when it is used in Subject or Reply-To. */ function singleLine(value) { return String(value).replace(/[\r\n]+/g, ' ').trim() } /** Minimal HTML escaping - the payload is attacker-controlled by definition. */ function escapeHtml(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", ''') } const ROLE_LABELS = { agency: 'Agentur / Dienstleister', client: 'Endkunde (eigene Website)', } export async function sendAuditRequestEmail({ name, company, email, url, role, motivation, }) { const transporter = getTransporter() const to = env('AUDIT_RECIPIENT') // Falls back to the authenticated mailbox when SMTP_FROM is left blank. const from = env('SMTP_FROM', env('SMTP_USER')) if (!to) { throw new Error('AUDIT_RECIPIENT is not configured.') } const safeName = singleLine(name) const safeCompany = singleLine(company) const safeEmail = singleLine(email) const roleLabel = ROLE_LABELS[role] ?? role const textBody = [ `Name: ${safeName}`, `Firma: ${safeCompany}`, `E-Mail: ${safeEmail}`, `Seite: ${singleLine(url)}`, `Typ: ${roleLabel}`, 'B2B: bestätigt (§ 14 BGB)', '', 'Motivation:', motivation, ].join('\n') const htmlBody = `
${escapeHtml(motivation).replaceAll('\n', '
')}