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 = `

Neue Audit-Anfrage

Name
${escapeHtml(safeName)}
Firma
${escapeHtml(safeCompany)}
E-Mail
${escapeHtml(safeEmail)}
Seite
${escapeHtml(singleLine(url))}
Typ
${escapeHtml(roleLabel)}
B2B
bestätigt (§ 14 BGB)

Motivation

${escapeHtml(motivation).replaceAll('\n', '
')}

` await transporter.sendMail({ from, to, replyTo: safeEmail, subject: `Audit-Anfrage: ${safeCompany} (${safeName})`, text: textBody, html: htmlBody, }) // Deliberately no recipient address, no payload - keep logs free of // personal data (Art. 5 GDPR, data minimisation). console.log('Audit request forwarded.') } export async function sendQuickcheckRequestEmail({ name, email }) { const transporter = getTransporter() const to = env('AUDIT_RECIPIENT') const from = env('SMTP_FROM', env('SMTP_USER')) if (!to) { throw new Error('AUDIT_RECIPIENT is not configured.') } const safeName = singleLine(name || 'Nicht angegeben') const safeEmail = singleLine(email) await transporter.sendMail({ from, to, replyTo: safeEmail, subject: `Schnellcheck-Anfrage: ${safeEmail}`, text: [ 'Neue Schnellcheck-Anfrage', '', `Name: ${safeName}`, `E-Mail: ${safeEmail}`, 'B2B: bestätigt (§ 14 BGB)', 'Datenschutz: bestätigt', ].join('\n'), html: `

Neue Schnellcheck-Anfrage

Name
${escapeHtml(safeName)}
E-Mail
${escapeHtml(safeEmail)}
B2B
bestätigt (§ 14 BGB)
Datenschutz
bestätigt
`, }) console.log('Quickcheck request forwarded.') }