Fix: IMAP/SMTP mit älteren TLS-Versionen zulassen

Der Fehler 'Client network socket disconnected before secure TLS connection
was established' tritt auf, wenn der Mailserver nur alte TLS-Versionen (1.0/1.1)
oder legacy Cipher-Suites anbietet - Node.js 20+ schließt dann den Socket, noch
bevor überhaupt ein Zertifikat gesehen wird. Das Häkchen 'Selbstsignierte
Zertifikate erlauben' greift zu spät, weil der Handshake gar nicht startet.

Fix: Wenn 'Selbstsignierte Zertifikate erlauben' aktiv ist, setzen wir gleich
auch minVersion='TLSv1' und ciphers='DEFAULT:@SECLEVEL=0'. Damit akzeptiert
Node.js auch alte Cipher-Suites und TLS-Versionen des Mailservers.

Bei aktivem 'allowSelfSignedCerts' heißt das zusammen:
- rejectUnauthorized: false (Zertifikate akzeptieren auch wenn selbstsigniert)
- minVersion: 'TLSv1' (auch alte TLS-Versionen zulassen)
- ciphers: 'DEFAULT:@SECLEVEL=0' (auch schwache Ciphers zulassen)

Refactor:
- imapService: neuer Helper buildTlsOptions() – ersetzt 8 identische
  Inline-Setups, damit die Fix-Logik zentral gepflegt wird
- smtpService: tls-Type erweitert (minVersion/ciphers), gleiche Logik

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck 2026-04-23 15:05:04 +02:00
parent fd55f3129f
commit 1de8fb9847
2 changed files with 38 additions and 12 deletions

View File

@ -16,6 +16,27 @@ export interface ImapCredentials {
allowSelfSignedCerts?: boolean; // Selbstsignierte Zertifikate erlauben allowSelfSignedCerts?: boolean; // Selbstsignierte Zertifikate erlauben
} }
/**
* TLS-Optionen für IMAP-Verbindungen zusammenbauen.
* Wenn `allowSelfSignedCerts` aktiv ist, werden zusätzlich ältere TLS-Versionen
* (TLS 1.0+) und legacy Cipher-Suites erlaubt hilfreich bei älteren Mailservern,
* die sonst den Socket sofort nach Connect schließen.
*/
function buildTlsOptions(credentials: ImapCredentials): Record<string, unknown> | undefined {
const encryption = credentials.encryption ?? 'SSL';
if (encryption === 'NONE') return undefined;
const rejectUnauthorized = !credentials.allowSelfSignedCerts;
const options: Record<string, unknown> = { rejectUnauthorized };
if (credentials.allowSelfSignedCerts) {
options.minVersion = 'TLSv1';
options.ciphers = 'DEFAULT:@SECLEVEL=0';
}
return options;
}
export interface FetchedEmail { export interface FetchedEmail {
uid: number; uid: number;
messageId: string; messageId: string;
@ -106,7 +127,7 @@ export async function fetchEmails(
// TLS-Optionen nur wenn nicht NONE // TLS-Optionen nur wenn nicht NONE
if (encryption !== 'NONE') { if (encryption !== 'NONE') {
clientOptions.tls = { rejectUnauthorized }; const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
} }
// Debug-Logging // Debug-Logging
@ -260,7 +281,7 @@ export async function testImapConnection(credentials: ImapCredentials): Promise<
}; };
if (encryption !== 'NONE') { if (encryption !== 'NONE') {
clientOptions.tls = { rejectUnauthorized }; const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
} }
const client = new ImapFlow(clientOptions); const client = new ImapFlow(clientOptions);
@ -325,7 +346,7 @@ export async function getHighestUid(
}; };
if (encryption !== 'NONE') { if (encryption !== 'NONE') {
clientOptions.tls = { rejectUnauthorized }; const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
} }
const client = new ImapFlow(clientOptions); const client = new ImapFlow(clientOptions);
@ -414,12 +435,12 @@ async function fetchAttachmentInner(
}; };
if (encryption !== 'NONE') { if (encryption !== 'NONE') {
clientOptions.tls = { rejectUnauthorized }; const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
} }
const client = new ImapFlow(clientOptions); const client = new ImapFlow(clientOptions);
console.log(`[fetchAttachment] Host: ${credentials.host}:${credentials.port} | User: ${credentials.user} | Folder: ${folder} | UID: ${uid} | File: ${attachmentFilename}`); console.log(`[fetchAttachment] Host: ${credentials.host}:${credentials.port} | User: ${credentials.user} | Folder: ${folder} | UID: ${uid} | File: ${attachmentFilename} | AllowSelfSigned: ${credentials.allowSelfSignedCerts}`);
try { try {
await client.connect(); await client.connect();
@ -527,7 +548,7 @@ export async function appendToSent(
}; };
if (encryption !== 'NONE') { if (encryption !== 'NONE') {
clientOptions.tls = { rejectUnauthorized }; const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
} }
console.log(`[IMAP] Appending email to ${sentFolder} folder...`); console.log(`[IMAP] Appending email to ${sentFolder} folder...`);
@ -613,7 +634,7 @@ export async function fetchAttachmentList(
}; };
if (encryption !== 'NONE') { if (encryption !== 'NONE') {
clientOptions.tls = { rejectUnauthorized }; const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
} }
const client = new ImapFlow(clientOptions); const client = new ImapFlow(clientOptions);
@ -705,7 +726,7 @@ export async function moveToTrash(
}; };
if (encryption !== 'NONE') { if (encryption !== 'NONE') {
clientOptions.tls = { rejectUnauthorized }; const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
} }
const client = new ImapFlow(clientOptions); const client = new ImapFlow(clientOptions);
@ -778,7 +799,7 @@ export async function restoreFromTrash(
}; };
if (encryption !== 'NONE') { if (encryption !== 'NONE') {
clientOptions.tls = { rejectUnauthorized }; const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
} }
const client = new ImapFlow(clientOptions); const client = new ImapFlow(clientOptions);
@ -849,7 +870,7 @@ export async function permanentDelete(
}; };
if (encryption !== 'NONE') { if (encryption !== 'NONE') {
clientOptions.tls = { rejectUnauthorized }; const __tls = buildTlsOptions(credentials); if (__tls) clientOptions.tls = __tls as any;
} }
const client = new ImapFlow(clientOptions); const client = new ImapFlow(clientOptions);

View File

@ -69,7 +69,7 @@ export async function sendEmail(
port: number; port: number;
secure: boolean; secure: boolean;
auth: { user: string; pass: string }; auth: { user: string; pass: string };
tls?: { rejectUnauthorized: boolean }; tls?: { rejectUnauthorized: boolean; minVersion?: string; ciphers?: string };
ignoreTLS?: boolean; ignoreTLS?: boolean;
requireTLS?: boolean; requireTLS?: boolean;
connectionTimeout: number; connectionTimeout: number;
@ -91,6 +91,11 @@ export async function sendEmail(
// TLS-Optionen nur wenn nicht NONE // TLS-Optionen nur wenn nicht NONE
if (encryption !== 'NONE') { if (encryption !== 'NONE') {
transportOptions.tls = { rejectUnauthorized }; transportOptions.tls = { rejectUnauthorized };
if (credentials.allowSelfSignedCerts) {
// Auch ältere TLS-Versionen + legacy Cipher-Suites für alte Server zulassen
transportOptions.tls.minVersion = 'TLSv1';
transportOptions.tls.ciphers = 'DEFAULT:@SECLEVEL=0';
}
} else { } else {
// Keine Verschlüsselung: STARTTLS ignorieren // Keine Verschlüsselung: STARTTLS ignorieren
transportOptions.ignoreTLS = true; transportOptions.ignoreTLS = true;
@ -273,7 +278,7 @@ export async function testSmtpConnection(credentials: SmtpCredentials): Promise<
port: number; port: number;
secure: boolean; secure: boolean;
auth: { user: string; pass: string }; auth: { user: string; pass: string };
tls?: { rejectUnauthorized: boolean }; tls?: { rejectUnauthorized: boolean; minVersion?: string; ciphers?: string };
ignoreTLS?: boolean; ignoreTLS?: boolean;
connectionTimeout: number; connectionTimeout: number;
greetingTimeout: number; greetingTimeout: number;