From f2703ed6b7cd6946dabfe7c5a89965d6d0d7bb72 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 11 Jul 2026 15:28:43 +0200 Subject: [PATCH] IMAP-Fehler: zentraler Humanizer + Auth-Fehler explizit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der User sah beim Postfach-Sync stumpf "Command failed" – realer Grund war ein abgelaufenes Postfach-Passwort. Der Bug betraf Anhang-Download UND Sync und war der gleiche wie 2 Commits zuvor: imapflow wirft `new Error('Command failed')` und legt Details in `.responseText`/`.responseStatus` ab, wir haben sie nirgends gelesen. Fix: humanizeImapError() als zentraler Helper in imapService: - Extrahiert responseText/responseStatus aus imapflow-Errors - Erkennt Auth-Fehler ("authentication failed", "invalid credentials", NO+auth) und liefert klare Meldung mit Handlungsanweisung: "Passwort stimmt nicht mehr, bitte Zugangsdaten synchronisieren". - Deckt zusätzlich Netzwerk/TLS/Timeout/UID-Stale ab. Angewendet auf: - fetchAttachmentInner (Anhang-Fetch) - downloadAttachment-Controller (Response an UI) - syncEmailsForAccount (Sync-Ergebnis + Toast) Damit sieht der User im Toast künftig statt "Command failed" die tatsächliche Ursache – gleicher Mechanismus für Sync und Anhang-Download. Co-Authored-By: Claude Opus 4.7 --- .../src/controllers/cachedEmail.controller.ts | 28 +------ backend/src/services/cachedEmail.service.ts | 9 ++- backend/src/services/imapService.ts | 77 +++++++++++++++++-- 3 files changed, 77 insertions(+), 37 deletions(-) diff --git a/backend/src/controllers/cachedEmail.controller.ts b/backend/src/controllers/cachedEmail.controller.ts index 025a9504..6024c307 100644 --- a/backend/src/controllers/cachedEmail.controller.ts +++ b/backend/src/controllers/cachedEmail.controller.ts @@ -5,7 +5,7 @@ import * as cachedEmailService from '../services/cachedEmail.service.js'; import * as stressfreiEmailService from '../services/stressfreiEmail.service.js'; import * as invoiceService from '../services/invoice.service.js'; import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '../services/smtpService.js'; -import { fetchAttachment, appendToSent, ImapCredentials } from '../services/imapService.js'; +import { fetchAttachment, appendToSent, ImapCredentials, humanizeImapError } from '../services/imapService.js'; import { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js'; import { decrypt } from '../utils/encryption.js'; import { sanitizeNotes, stripHtml, validateContractDocumentType, validateOptionalIsoDate, assertSafePdf } from '../utils/sanitize.js'; @@ -797,33 +797,9 @@ export async function downloadAttachment(req: AuthRequest, res: Response): Promi res.send(attachment.content); } catch (error) { console.error('downloadAttachment error:', error); - // imapflow versteckt die eigentliche Server-Antwort in - // `.responseText`/`.responseStatus` und setzt `.message` immer nur auf - // `'Command failed'`. Wenn wir nur die `.message` ausgeben, sieht der - // Endnutzer im UI nichts Verwertbares. - const anyErr = error as { message?: string; responseText?: string; responseStatus?: string }; - const rawMsg = anyErr?.responseText - ? `${anyErr.responseStatus ? anyErr.responseStatus + ' ' : ''}${anyErr.responseText}` - : error instanceof Error ? error.message : 'Unbekannter Fehler'; - const lower = rawMsg.toLowerCase(); - - let friendly = rawMsg; - if (lower.includes('socket disconnected') && lower.includes('tls')) { - friendly = - 'IMAP-Server hat die TLS-Verbindung abgelehnt. Mögliche Ursache: selbstsigniertes Zertifikat. Bitte in den E-Mail-Provider-Einstellungen "Selbstsignierte Zertifikate erlauben" aktivieren.'; - } else if (lower.includes('econnrefused')) { - friendly = 'IMAP-Server ist nicht erreichbar (Verbindung verweigert). Bitte Server/Port prüfen.'; - } else if (lower.includes('etimedout')) { - friendly = 'Zeitüberschreitung beim Verbinden zum IMAP-Server. Bitte später erneut versuchen.'; - } else if (lower.includes('authentication') || lower.includes('auth')) { - friendly = 'IMAP-Authentifizierung fehlgeschlagen. Bitte Zugangsdaten prüfen.'; - } else if (lower.includes('no longer exist') || lower.includes('no such message') || lower.includes('unknown')) { - friendly = 'Nachricht existiert im IMAP-Postfach nicht mehr (wahrscheinlich verschoben oder gelöscht). Bitte E-Mail-Liste neu synchronisieren.'; - } - res.status(500).json({ success: false, - error: `Fehler beim Herunterladen des Anhangs: ${friendly}`, + error: `Fehler beim Herunterladen des Anhangs: ${humanizeImapError(error)}`, } as ApiResponse); } } diff --git a/backend/src/services/cachedEmail.service.ts b/backend/src/services/cachedEmail.service.ts index fb95266a..ccbcb207 100644 --- a/backend/src/services/cachedEmail.service.ts +++ b/backend/src/services/cachedEmail.service.ts @@ -4,7 +4,7 @@ import { CachedEmail, Prisma, EmailFolder } from '@prisma/client'; import prisma from '../lib/prisma.js'; import { decrypt } from '../utils/encryption.js'; -import { fetchEmails, ImapCredentials, FetchedEmail, moveToTrash, restoreFromTrash, permanentDelete } from './imapService.js'; +import { fetchEmails, ImapCredentials, FetchedEmail, moveToTrash, restoreFromTrash, permanentDelete, humanizeImapError } from './imapService.js'; import { getImapSmtpSettings } from './emailProvider/emailProviderService.js'; // ==================== TYPES ==================== @@ -155,12 +155,15 @@ export async function syncEmailsForAccount( }; } catch (error) { console.error('syncEmailsForAccount error:', error); - const errorMessage = error instanceof Error ? error.message : 'Unbekannter Fehler'; + // humanizeImapError zieht die eigentliche Server-Antwort aus dem + // imapflow-Error und erkennt insbesondere Auth-Fehler ("Passwort + // stimmt nicht mehr") mit klarer Handlungsanweisung – vorher sah + // der User im Sync-Toast nur "Command failed". return { success: false, newEmails: 0, totalEmails: 0, - error: errorMessage, + error: humanizeImapError(error), }; } } diff --git a/backend/src/services/imapService.ts b/backend/src/services/imapService.ts index d35a3fe1..8f4117e1 100644 --- a/backend/src/services/imapService.ts +++ b/backend/src/services/imapService.ts @@ -7,6 +7,69 @@ import { simpleParser, ParsedMail, AddressObject } from 'mailparser'; // Verschlüsselungstyp export type MailEncryption = 'SSL' | 'STARTTLS' | 'NONE'; +/** + * Zieht eine menschenlesbare Fehlermeldung aus einem imapflow-Error. + * `imapflow` wirft bei jedem IMAP-NO/BAD-Response immer nur + * `new Error('Command failed')` und legt den echten Grund in + * `err.responseText` / `err.responseStatus` ab. Ohne diese Extraktion + * sähe der User im UI nur "Command failed" ohne Kontext (das war der + * ursprüngliche Bug 2026-07-11: Passwort war falsch, gemeldet wurde + * nur "Command failed"). + * + * Zusätzlich mappen wir häufige Fehler-Klassen auf klare Meldungen + * mit Handlungsanweisung – gilt sowohl für Anhang-Download als auch + * für Postfach-Sync, damit der User weiß was zu tun ist. + */ +export function humanizeImapError(error: unknown): string { + const anyErr = error as { + message?: string; + responseText?: string; + responseStatus?: string; + authenticationFailed?: boolean; + code?: string; + }; + const rawText = anyErr?.responseText || anyErr?.message || 'Unbekannter Fehler'; + const statusTag = anyErr?.responseStatus ? `${anyErr.responseStatus} ` : ''; + const combined = `${statusTag}${rawText}`; + const lower = combined.toLowerCase(); + + // 1) Auth-Fehler explizit erkennen – häufigste Ursache in der Praxis. + // Passwort im CRM stimmt nicht mehr mit dem am Provider überein + // (Passwort geändert / abgelaufen / Postfach neu angelegt). + if ( + anyErr?.authenticationFailed + || lower.includes('authentication failed') + || lower.includes('auth failed') + || lower.includes('invalid credentials') + || lower.includes('login failed') + || lower.includes('logindisabled') + || (anyErr?.responseStatus === 'NO' && lower.includes('auth')) + ) { + return 'IMAP-Login abgelehnt. Das gespeicherte Postfach-Passwort stimmt nicht mehr. Bitte im Postfach "Zugangsdaten synchronisieren" ausführen oder das Passwort neu setzen.'; + } + + // 2) UID-Stale-Fehler beim Fetch: E-Mail existiert IMAP-seitig nicht mehr. + if (lower.includes('no longer exist') || lower.includes('no such message')) { + return 'Nachricht existiert im IMAP-Postfach nicht mehr (wahrscheinlich verschoben oder gelöscht). Bitte E-Mail-Liste neu synchronisieren.'; + } + + // 3) Netzwerk-/TLS-Fehler + if (lower.includes('socket disconnected') && lower.includes('tls')) { + return 'IMAP-Server hat die TLS-Verbindung abgelehnt. Mögliche Ursache: selbstsigniertes Zertifikat. Bitte in den E-Mail-Provider-Einstellungen "Selbstsignierte Zertifikate erlauben" aktivieren.'; + } + if (lower.includes('econnrefused')) { + return 'IMAP-Server ist nicht erreichbar (Verbindung verweigert). Bitte Server/Port prüfen.'; + } + if (lower.includes('etimedout')) { + return 'Zeitüberschreitung beim Verbinden zum IMAP-Server. Bitte später erneut versuchen.'; + } + + // 4) Fallback: den vom Server gelieferten Text nehmen – der ist meist + // schon aussagekräftig ("BAD Session expired." etc.). Wenn wir hier + // gar nichts hätten, käme jetzt zumindest der Status-Tag durch. + return combined; +} + export interface ImapCredentials { host: string; port: number; @@ -531,15 +594,13 @@ async function fetchAttachmentInner( } } catch (fetchErr) { // imapflow wirft bei IMAP-NO/BAD immer nur `new Error('Command failed')`, - // die eigentliche Server-Antwort steht in `.responseText`. Ohne die - // Anreicherung sähe der User oben im UI nur "Command failed" ohne - // Kontext (Ursache 2026-07-11). - const anyErr = fetchErr as { message?: string; responseText?: string; responseStatus?: string; response?: unknown }; - const detail = anyErr?.responseText || anyErr?.message || 'unbekannter Fehler'; - const statusTag = anyErr?.responseStatus ? `${anyErr.responseStatus} ` : ''; - console.error(`[fetchAttachment] fetch(UID ${uid}) failed [${statusTag}${detail}]:`, fetchErr); + // die eigentliche Server-Antwort steht in `.responseText`. Zentraler + // Helper `humanizeImapError` sorgt dafür, dass Anhang-Download UND + // Sync die gleiche klare Meldung liefern (2026-07-11: Ursache war + // ein abgelaufenes Postfach-Passwort, kam als "Command failed" durch). + console.error(`[fetchAttachment] fetch(UID ${uid}) failed:`, fetchErr); throw new Error( - `Nachricht mit UID ${uid} konnte nicht geladen werden: ${statusTag}${detail}. Möglicherweise wurde sie im IMAP-Postfach verschoben oder gelöscht.`, + `Nachricht mit UID ${uid}: ${humanizeImapError(fetchErr)}`, ); }