import nodemailer from 'nodemailer'
/**
* Fehler-Meldung mit zwei Stufen — statt eines Sammel-Etiketts.
*
* **Warum das geaendert wurde (26.09.):** `server.js` ersetzte global
* `console.error` durch dieses Modul, und der Ersatz schrieb pauschal
* `"Fatal error"`. Damit hiess **jeder** Fehler im Prozess „Fatal" — auch eine
* abgelaufene Browser-Session (`jwt expired`, im Dev-Log 18-mal) oder eine
* fehlgeschlagene PDF-Vorschau. Zur Fehlersuche war das Rauschen in genau der
* Datei, in der man nach Signalen sucht.
*
* Zweites Problem: `server.js` setzt in Produktion `console.log` auf eine leere
* Funktion. Da der alte Ersatz ueber `console.log` schrieb, verschwand die Zeile
* dort komplett — blieb die Mail unkonfiguriert, war der Fehler **unsichtbar**.
*
* Drittes Problem: der Mail-Versand hing am globalen `console.error`. Jeder der
* 34 `console.error`-Aufrufe im Code loeste damit eine Mail aus — und
* `errorLogger` (jeder protokollierte Fehler) ueber den Hijack ebenfalls. Ohne
* Drosselung und ohne `verify()`.
*
* Jetzt gilt:
* - {@link logError} — Betriebsfehler (Request, Upload, Auth). Geht nach stderr,
* **keine** Mail.
* - {@link logFatal} — echter Fatal (Startfehler). Geht nach stderr **und**
* mailt, wenn konfiguriert und nicht gedrosselt.
*
* `console.error` bleibt unangetastet: Node schreibt es nach stderr, und stderr
* wird durch die `console.log`-Abschaltung in Produktion **nicht** beruehrt.
*
* Der Dateiname ist bewusst unveraendert (`errorHander` ohne „l"): er steht in
* generierten Dokus (`docuviewer/`, `documentation/`). Eine Umbenennung waere
* nur Doku-Drift.
*
* Der Datei-Log bleibt die dauerhafte Spur: `errorLogger` schreibt zusaetzlich
* nach `logs/db/error.log` (rotierend, siehe `utils/requestLogger.js`).
*/
/**
* Liest die Mail-Konfiguration — oder `null`, wenn sie unvollstaendig ist.
*
* Der alte Guard lautete
* `if(process.env.mailHost && process.env.mailPort && process.env.mailUser && process.env.mailPassword, process.env.errorMail)`.
* Das ist ein **Komma-Operator**: der linke Ausdruck wird ausgewertet und
* verworfen, geprueft wird nur `errorMail`. Eine halb gesetzte Konfiguration
* lief damit in `createTransport` und warf erst beim Senden.
*
* @returns {{host: string, port: string, user: string, password: string, to: string} | null}
*/
const mailConfig = () => {
const host = process.env.mailHost
const port = process.env.mailPort
const user = process.env.mailUser
const password = process.env.mailPassword
const to = process.env.errorMail
if (!host || !port || !user || !password || !to) return null
return { host, port, user, password, to }
}
/** Zeitpunkt der letzten Fehler-Mail (ms). */
let lastMailAt = 0
/**
* Macht Fehler mailbar.
*
* `JSON.stringify(new Error('x'))` ergibt `{}` — `message` und `stack` sind
* nicht-enumerable und fielen damit aus dem Mailtext heraus. Die Fehler-Mail
* enthielt deshalb nur `[{}]` und war als Meldung wertlos.
*
* @param {unknown[]} errors
* @returns {string}
*/
const serializeErrors = (errors) => JSON.stringify(errors, (_key, value) => {
if (value instanceof Error) {
return { name: value.name, message: value.message, stack: value.stack }
}
return value
}, 2)
/**
* Mindestabstand zwischen zwei Fehler-Mails.
*
* Ein Fehlersturm darf kein Mailsturm werden: `errorLogger` wird pro
* fehlgeschlagenem Request gerufen, und ein einziger kaputter Aufrufpfad kann
* das im Sekundentakt ausloesen.
*/
const MAIL_THROTTLE_MS = 60_000
/**
* Verschickt eine Fehler-Mail — fire-and-forget.
*
* Ein Fehler im Mailer darf die Fehlerbehandlung nicht aufhalten oder gar
* werfen, deshalb ist alles umschlossen und der Versand wird nicht abgewartet.
*
* @param {{host: string, port: string, user: string, password: string, to: string}} cfg
* @param {string} label - `FATAL` (nur hierfuer wird gemailt)
* @param {unknown[]} errors
* @returns {void}
*/
const sendErrorMail = (cfg, label, errors) => {
const now = Date.now()
if (now - lastMailAt < MAIL_THROTTLE_MS) return
lastMailAt = now
try {
const transporter = nodemailer.createTransport({
host: cfg.host,
port: cfg.port,
secure: true, // true for 465, false for other ports
auth: {
user: cfg.user,
pass: cfg.password
}
})
transporter.sendMail({
from: {
name: process.env.mailUserName,
address: cfg.user
},
replyTo: {
address: cfg.to
},
to: cfg.to,
subject: `!!!!kpe20 error report kpe20${process.env.baseUrl ?? ''} [${label}]`,
text: serializeErrors(errors)
})
.catch(mailErr => {
console.error('[errorHandler] Fehler-Mail konnte nicht gesendet werden:', mailErr)
})
} catch (mailSetupErr) {
console.error('[errorHandler] Fehler-Mail konnte nicht aufgebaut werden:', mailSetupErr)
}
}
/**
* Betriebsfehler: protokollieren, **nicht** mailen.
*
* @param {...unknown} err
* @returns {void}
*/
export const logError = (...err) => {
console.error('[ERROR]', ...err)
}
/**
* Echter Fatal: protokollieren **und** mailen (gedrosselt, wenn konfiguriert).
*
* Gedacht fuer den Startpfad — wenn der Server nicht hochkommt, ist das die
* einzige Meldung, die jemand mitbekommt.
*
* @param {...unknown} err
* @returns {void}
*/
export const logFatal = (...err) => {
console.error('[FATAL]', ...err)
const cfg = mailConfig()
if (cfg) sendErrorMail(cfg, 'FATAL', err)
}
// Standard-Export bleibt `logError`: er ist die richtige Stufe fuer einen
// generischen Fehler-Sink und haelt bestehende Importe gueltig.
export default logError