fix(imap-idle): IDLE-renew 25min→10min + NOOP-heartbeat (GMX silent-drop fix)

User-test: Casino-mail an Chahine@gmx.net wurde nicht geblockt obwohl
daemon "connected" zeigte. Mo's diagnose: GMX dropped IDLE-connection
silent (kein TCP-error, kein logout). ImapFlow.idle() hängt unbegrenzt
ohne reject — exists-events kommen nie an, daemon ist faktisch tot.

2 Fixes:
1) IDLE_RENEW_INTERVAL_MS: 25 min → 10 min. GMX timeout-window ist
   ~10-15min, 25min war zu lang. Trade-off: alle 10min full reconnect.
2) NOOP-heartbeat alle 2min während IDLE-loop. Wenn NOOP fail
   (= silent-drop detected) → close → reconnect-loop. Early-detection.

Andere provider (Gmail/iCloud/Outlook) sind unaffected — die haben
~29min IDLE-timeout, also passt 10min auch dort safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-09 23:42:09 +02:00
parent a81ba2e54a
commit 01420eaa09

View File

@ -35,7 +35,14 @@ const ADMIN_SECRET =
process.env.NUXT_ADMIN_SECRET || process.env.ADMIN_SECRET || "";
const DB_REFRESH_INTERVAL_MS = 5 * 60 * 1000; // 5 min — neue Connections entdecken
const IDLE_RENEW_INTERVAL_MS = 25 * 60 * 1000; // 25 min — RFC 3501 max = 29min
// IDLE_RENEW von 25min → 10min: GMX dropped IDLE-connections silent vor 25min
// → exists-events kommen nie an + ImapFlow.idle() hängt ohne reject. 10min
// deckt alle bekannten Provider-Timeouts ab (GMX ~10-15min, Gmail ~29min,
// iCloud ~29min, Outlook ~29min). Trade-off: alle 10min full reconnect-cycle.
const IDLE_RENEW_INTERVAL_MS = 10 * 60 * 1000; // 10 min (war 25)
// NOOP-heartbeat alle 2min während IDLE: defensive check ob connection wirklich
// alive ist. Wenn NOOP fehlschlägt → close + reconnect-loop.
const IDLE_NOOP_INTERVAL_MS = 2 * 60 * 1000; // 2 min — silent-drop early-detection
const RECONNECT_DELAYS_MS = [1000, 5000, 30_000]; // exponential backoff, danach 60s loop
const RECONNECT_LOOP_DELAY_MS = 60 * 1000;
@ -205,15 +212,29 @@ async function runSession(conn) {
};
imap.on("exists", onExists);
// IDLE nach 25min erneuern (RFC 3501: Server darf nach 29min droppen)
// IDLE nach 10min erneuern (war 25; GMX dropped silent vor 25min)
const renewTimer = setTimeout(() => {
log(conn.email, "idle renewing (25min threshold)");
log(conn.email, "idle renewing (10min threshold)");
imap.close(); // Unterbricht idle() → Loop iteriert → reconnect
}, IDLE_RENEW_INTERVAL_MS);
// NOOP-heartbeat alle 2min: detect silent-IDLE-drops (GMX-pattern).
// Wenn NOOP fehlschlägt → close → loop iteriert → reconnect.
const noopTimer = setInterval(async () => {
try {
await imap.noop();
// Optional: verbose-log für debugging — aktuell silent
// log(conn.email, "noop ok");
} catch (err) {
logError(conn.email, "noop failed — connection dead, force reconnect", err);
imap.close();
}
}, IDLE_NOOP_INTERVAL_MS);
try {
await idlePromise;
} finally {
clearInterval(noopTimer);
clearTimeout(renewTimer);
imap.removeListener("exists", onExists);
}