/** * Uebersetzt Fehler beim Mailversand ueber Microsoft 365 (OAuth 2.0, siehe * lib/mail.ts) in eine konkrete Ursache mit deutscher Handlungsanweisung. * Der Versand selbst verschluckt Fehler bewusst — ohne diese Einordnung sieht * man nur "keine Mails kommen an". */ import { Ms365TokenError } from "@/lib/ms365-oauth"; export type SmtpFailureCause = | "oauth-token" | "oauth-mailbox-access" | "smtp-auth-disabled" | "send-as-denied" | "network" | "tls" | "unknown"; export interface SmtpDiagnosis { cause: SmtpFailureCause; hint: string; /** Originalmeldung des Servers, gekuerzt und ohne Geheimnisse. */ detail: string; } const HINTS: Record = { "oauth-token": "Microsoft Entra ID gibt kein Zugriffstoken aus. Prüfe ENTRA_TENANT_ID, ENTRA_CLIENT_ID und ENTRA_CLIENT_SECRET — dieselben Werte wie für „Mit Microsoft anmelden“.", "oauth-mailbox-access": "Das Token ist gültig, aber Exchange lässt die App nicht mit dem Postfach aus SMTP_USER senden. Prüfe in dieser Reihenfolge: (1) API-Berechtigung „Office 365 Exchange Online → SMTP.SendAsApp“ (Anwendung) mit Administratorzustimmung an der SSO-App-Registrierung, (2) New-ServicePrincipal -AppId -ObjectId in Exchange Online PowerShell, (3) Add-MailboxPermission -Identity -User -AccessRights FullAccess. Nach Änderungen bis zu 30 Minuten warten.", "smtp-auth-disabled": "„Authentifiziertes SMTP“ ist für den Tenant oder das Postfach ausgeschaltet — auch OAuth 2.0 läuft darüber. Exchange Online PowerShell: Set-CASMailbox -Identity -SmtpClientAuthenticationDisabled $false. Die Postfach-Einstellung hat Vorrang vor der des Tenants; Änderungen können bis zu einer Stunde dauern.", "send-as-denied": "Die App darf nicht mit der Absenderadresse (SMTP_FROM) senden. SMTP_FROM muss die Adresse des Postfachs aus SMTP_USER sein, oder das Postfach braucht die Berechtigung „Senden als“ für diese Adresse. Für einen Alias: Set-OrganizationConfig -SendFromAliasEnabled $true.", network: "smtp.office365.com ist nicht erreichbar. Die Netzwerkprüfung oben zeigt, welcher Weg blockiert ist: Der Cluster muss ausgehend per IPv4 smtp.office365.com auf Port 587 und login.microsoftonline.com auf Port 443 erreichen (Firewall des Hosters, NetworkPolicy). Manche Hoster sperren Mail-Ports für neue Server — dann dort die Freigabe von Port 587 beantragen.", tls: "Die verschlüsselte Verbindung zu smtp.office365.com ist fehlgeschlagen (STARTTLS auf Port 587). Prüfe Proxy oder TLS-Inspektion auf dem Netzweg.", unknown: "Unbekannter Fehler. Die Originalmeldung des Mailservers steht unten.", }; /** Konkrete Anweisung zu den haeufigsten Entra-Fehlernummern beim Token-Abruf. */ const AAD_HINTS: Record = { "7000215": "Das Client-Secret ist falsch. In ENTRA_CLIENT_SECRET gehört der „Wert“ des geheimen Clientschlüssels, nicht die „Geheimnis-ID“. Der Wert wird nur direkt nach dem Anlegen angezeigt — sonst einen neuen Schlüssel erstellen.", "7000222": "Das Client-Secret ist abgelaufen — das betrifft auch „Mit Microsoft anmelden“. Entra ID → App-Registrierungen → Zertifikate & Geheimnisse → neuen geheimen Clientschlüssel anlegen, ENTRA_CLIENT_SECRET im Kubernetes-Secret ersetzen, Pod neu starten. Ablaufdatum notieren.", "700016": "Die App-Registrierung wurde in diesem Tenant nicht gefunden. Prüfe ENTRA_CLIENT_ID (Anwendungs-ID/Client-ID) und ob ENTRA_TENANT_ID zum selben Tenant gehört.", "90002": "Der Tenant existiert nicht. Prüfe ENTRA_TENANT_ID (Verzeichnis-ID/Mandanten-ID).", "900023": "Die Tenant-Angabe ist ungültig. Prüfe ENTRA_TENANT_ID (Verzeichnis-ID/Mandanten-ID).", }; const MAX_DETAIL_LENGTH = 300; function readError(error: unknown): { message: string; code: string } { if (error instanceof Error) { const code = (error as Error & { code?: unknown }).code; return { message: error.message, code: typeof code === "string" ? code : "" }; } return { message: typeof error === "string" ? error : "", code: "" }; } function classify(error: unknown, message: string, code: string): SmtpFailureCause { if (error instanceof Ms365TokenError) return "oauth-token"; const text = message.toLowerCase(); if (text.includes("smtpclientauthentication is disabled")) return "smtp-auth-disabled"; if (text.includes("sendasdenied") || text.includes("5.2.252") || text.includes("5.7.60")) return "send-as-denied"; // Mit OAuth gibt es kein Passwort, das falsch sein koennte: ein abgelehntes // XOAUTH2-Token heisst fast immer fehlender Postfachzugriff der App. if (code === "EAUTH" || text.includes("535") || text.includes("authentication unsuccessful")) return "oauth-mailbox-access"; if (["ETIMEDOUT", "ECONNREFUSED", "ECONNECTION", "EDNS", "ENOTFOUND", "ESOCKET", "ECONNRESET"].includes(code) || /etimedout|econnrefused|enotfound/.test(text)) { return "network"; } if (code === "ETLS" || text.includes("certificate") || text.includes("ssl") || text.includes("tls")) return "tls"; return "unknown"; } interface ExplainOptions { /** Werte, die nie in der Ausgabe erscheinen duerfen (Client-Secret, Token). */ secrets?: (string | undefined)[]; } export function explainSmtpError(error: unknown, options: ExplainOptions = {}): SmtpDiagnosis { const { message, code } = readError(error); const cause = classify(error, message, code); const aadHint = error instanceof Ms365TokenError && error.aadCode ? AAD_HINTS[error.aadCode] : undefined; const redacted = (options.secrets ?? []).reduce((text, secret) => (secret ? text.split(secret).join("***") : text), message); return { cause, hint: aadHint ?? HINTS[cause], detail: redacted.slice(0, MAX_DETAIL_LENGTH) }; }