118 lines
3.1 KiB
JavaScript
118 lines
3.1 KiB
JavaScript
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 = `
|
|
<h2>Neue Audit-Anfrage</h2>
|
|
<dl>
|
|
<dt><strong>Name</strong></dt><dd>${escapeHtml(safeName)}</dd>
|
|
<dt><strong>Firma</strong></dt><dd>${escapeHtml(safeCompany)}</dd>
|
|
<dt><strong>E-Mail</strong></dt><dd>${escapeHtml(safeEmail)}</dd>
|
|
<dt><strong>Seite</strong></dt><dd>${escapeHtml(singleLine(url))}</dd>
|
|
<dt><strong>Typ</strong></dt><dd>${escapeHtml(roleLabel)}</dd>
|
|
<dt><strong>B2B</strong></dt><dd>bestätigt (§ 14 BGB)</dd>
|
|
</dl>
|
|
<h3>Motivation</h3>
|
|
<p>${escapeHtml(motivation).replaceAll('\n', '<br>')}</p>
|
|
`
|
|
|
|
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.')
|
|
}
|