/** * nextcloud-alert-mail — reiner Baustein der Benachrichtigungsmail * (quick-261002-kxc, L-05, D-K6). Kein Nest, kein Zugriff auf die Uhr oder * den Versand: Betreff und Klartext entstehen aus den Eingaben. Die Mail ist * wie die Erinnerungsmails nur Deutsch, Zeit in Europe/Berlin. */ import type { NextcloudRating } from './nextcloud-rating'; export interface NextcloudAlertMailInput { kind: 'down' | 'up'; customerName: string; baseUrl: string; rating: NextcloudRating; errorKind: string | null; errorDetail: string | null; at: Date; } const SUBJECT_MAX = 150; const CERT_NAME_CODES = new Set(['ERR_TLS_CERT_ALTNAME_INVALID', 'HOSTNAME_MISMATCH']); const CERT_EXPIRED_CODES = new Set(['CERT_HAS_EXPIRED']); const CERT_UNTRUSTED_CODES = new Set([ 'DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', 'CERT_UNTRUSTED', 'CERT_NOT_YET_VALID', 'CERT_REVOKED', ]); const DNS_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN']); const TIMEOUT_CODES = new Set(['ETIMEDOUT', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT']); /** * Uebersetzt Fehlerart und Kurzkennung eines fehlgeschlagenen Abrufs in einen * lesbaren deutschen Hinweis. Die Rohkennung bleibt in der Datenbank; die * Kachel (Web) bildet dieselben Regeln ab. */ export function describeCheckError( errorKind: string | null | undefined, errorDetail: string | null | undefined, ): string { const detail = errorDetail ?? ''; if (errorKind === 'tls') { if (CERT_NAME_CODES.has(detail)) return 'Zertifikat passt nicht zur Adresse'; if (CERT_EXPIRED_CODES.has(detail)) return 'Zertifikat abgelaufen'; if (CERT_UNTRUSTED_CODES.has(detail)) return 'Zertifikat nicht vertrauenswürdig'; return 'Verbindungsfehler'; } if (errorKind === 'timeout') return 'Zeitüberschreitung'; if (errorKind === 'http-status') { const match = /^HTTP (\d{3})$/.exec(detail); return match ? `Server antwortet mit Fehler ${match[1]}` : 'Verbindungsfehler'; } if (errorKind === 'network') { if (DNS_CODES.has(detail)) return 'Adresse nicht gefunden'; if (detail === 'ECONNREFUSED') return 'Verbindung abgelehnt'; if (TIMEOUT_CODES.has(detail)) return 'Zeitüberschreitung'; } return 'Verbindungsfehler'; } function subjectWording(input: NextcloudAlertMailInput): string { if (input.kind === 'up') return 'wieder in Ordnung'; switch (input.rating.reason) { case 'invalid-response': return 'keine gültige Antwort'; case 'maintenance': return 'im Wartungsmodus'; case 'needs-db-upgrade': return 'Datenbank-Aktualisierung ausstehend'; case 'eol-passed': return 'Support abgelaufen'; default: return 'nicht erreichbar'; } } /** 'YYYY-MM-DD' als 'TT.MM.JJJJ'. */ function germanDay(day: string): string { const [y, m, d] = day.split('-'); return `${d}.${m}.${y}`; } function reasonLine(input: NextcloudAlertMailInput): string { const { rating } = input; if (input.kind === 'up') { if (rating.reason === 'update-available' && rating.updateTo) { return `Aktueller Stand: Update auf ${rating.updateTo} verfügbar`; } if (rating.reason === 'eol-soon' && rating.eolDate) { return `Aktueller Stand: Support endet am ${germanDay(rating.eolDate)}`; } return 'Aktueller Stand: Aktuell'; } switch (rating.reason) { case 'invalid-response': return 'Grund: Keine gültige Nextcloud-Antwort'; case 'maintenance': return 'Grund: Wartungsmodus eingeschaltet'; case 'needs-db-upgrade': return 'Grund: Datenbank-Aktualisierung ausstehend'; case 'eol-passed': return rating.eolDate ? `Grund: Support abgelaufen seit ${germanDay(rating.eolDate)}` : 'Grund: Support abgelaufen'; default: return `Grund: Nicht erreichbar (${describeCheckError(input.errorKind, input.errorDetail)})`; } } export function buildNextcloudAlertMail( input: NextcloudAlertMailInput, appUrl: string, ): { subject: string; text: string } { const subject = `Nextcloud ${input.customerName}: ${subjectWording(input)}` .replace(/[\r\n]+/g, ' ') .slice(0, SUBJECT_MAX); const when = `${new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', dateStyle: 'full', timeStyle: 'short', }).format(input.at)} Uhr`; const name = input.customerName.replace(/[\r\n]+/g, ' '); const sentence = input.kind === 'down' ? `die Nextcloud "${name}" hat seit ${when} ein Problem.` : `die Nextcloud "${name}" ist seit ${when} wieder in Ordnung.`; const lines = [ 'Guten Tag,', '', sentence, '', reasonLine(input), `Adresse: ${input.baseUrl}`, `Zeitpunkt: ${when}`, '', `Zum Modul: ${appUrl}/modules/nextcloud-status`, '', 'Sie erhalten diese Nachricht, weil für diese Cloud "Benachrichtigen" eingeschaltet ist. Mit der Glocke auf der Kachel können Sie das jederzeit ausschalten.', ]; return { subject, text: lines.join('\n') }; }