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 stressfreiEmailService from '../services/stressfreiEmail.service.js';
|
||||||
import * as invoiceService from '../services/invoice.service.js';
|
import * as invoiceService from '../services/invoice.service.js';
|
||||||
import { sendEmail, SmtpCredentials, SendEmailParams, EmailAttachment } from '../services/smtpService.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 { getImapSmtpSettings } from '../services/emailProvider/emailProviderService.js';
|
||||||
import { decrypt } from '../utils/encryption.js';
|
import { decrypt } from '../utils/encryption.js';
|
||||||
import { sanitizeNotes, stripHtml, validateContractDocumentType, validateOptionalIsoDate, assertSafePdf } from '../utils/sanitize.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);
|
res.send(attachment.content);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('downloadAttachment error:', 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({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Fehler beim Herunterladen des Anhangs: ${friendly}`,
|
error: `Fehler beim Herunterladen des Anhangs: ${humanizeImapError(error)}`,
|
||||||
} as ApiResponse);
|
} as ApiResponse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { CachedEmail, Prisma, EmailFolder } from '@prisma/client';
|
import { CachedEmail, Prisma, EmailFolder } from '@prisma/client';
|
||||||
import prisma from '../lib/prisma.js';
|
import prisma from '../lib/prisma.js';
|
||||||
import { decrypt } from '../utils/encryption.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';
|
import { getImapSmtpSettings } from './emailProvider/emailProviderService.js';
|
||||||
|
|
||||||
// ==================== TYPES ====================
|
// ==================== TYPES ====================
|
||||||
@@ -155,12 +155,15 @@ export async function syncEmailsForAccount(
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('syncEmailsForAccount error:', 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 {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
newEmails: 0,
|
newEmails: 0,
|
||||||
totalEmails: 0,
|
totalEmails: 0,
|
||||||
error: errorMessage,
|
error: humanizeImapError(error),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,69 @@ import { simpleParser, ParsedMail, AddressObject } from 'mailparser';
|
|||||||
// Verschlüsselungstyp
|
// Verschlüsselungstyp
|
||||||
export type MailEncryption = 'SSL' | 'STARTTLS' | 'NONE';
|
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 {
|
export interface ImapCredentials {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
@@ -531,15 +594,13 @@ async function fetchAttachmentInner(
|
|||||||
}
|
}
|
||||||
} catch (fetchErr) {
|
} catch (fetchErr) {
|
||||||
// imapflow wirft bei IMAP-NO/BAD immer nur `new Error('Command failed')`,
|
// imapflow wirft bei IMAP-NO/BAD immer nur `new Error('Command failed')`,
|
||||||
// die eigentliche Server-Antwort steht in `.responseText`. Ohne die
|
// die eigentliche Server-Antwort steht in `.responseText`. Zentraler
|
||||||
// Anreicherung sähe der User oben im UI nur "Command failed" ohne
|
// Helper `humanizeImapError` sorgt dafür, dass Anhang-Download UND
|
||||||
// Kontext (Ursache 2026-07-11).
|
// Sync die gleiche klare Meldung liefern (2026-07-11: Ursache war
|
||||||
const anyErr = fetchErr as { message?: string; responseText?: string; responseStatus?: string; response?: unknown };
|
// ein abgelaufenes Postfach-Passwort, kam als "Command failed" durch).
|
||||||
const detail = anyErr?.responseText || anyErr?.message || 'unbekannter Fehler';
|
console.error(`[fetchAttachment] fetch(UID ${uid}) failed:`, fetchErr);
|
||||||
const statusTag = anyErr?.responseStatus ? `${anyErr.responseStatus} ` : '';
|
|
||||||
console.error(`[fetchAttachment] fetch(UID ${uid}) failed [${statusTag}${detail}]:`, fetchErr);
|
|
||||||
throw new Error(
|
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