IMAP-Fehler: zentraler Humanizer + Auth-Fehler explizit

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 15:28:43 +02:00
co-authored by Claude Opus 4.7
parent be19e60a1b
commit f2703ed6b7
3 changed files with 77 additions and 37 deletions
@@ -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);
}
}
+6 -3
View File
@@ -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),
};
}
}
+69 -8
View File
@@ -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)}`,
);
}