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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user