proton mail IMAP reconnection, wait to re-gen the QR code until daemon has fully started up

This commit is contained in:
Egor 2026-01-22 13:29:10 -08:00
parent 0e3eb43531
commit 128aa9ffe5
3 changed files with 52 additions and 20 deletions

View file

@ -60,7 +60,7 @@ const cspConfig = {
defaultSrc: ["'self'"], defaultSrc: ["'self'"],
scriptSrc: ["'self'"], scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https://api.qrserver.com'], imgSrc: ["'self'", 'data:'],
formAction: ["'self'"], formAction: ["'self'"],
frameAncestors: ["'none'"], frameAncestors: ["'none'"],
objectSrc: ["'none'"], objectSrc: ["'none'"],

View file

@ -12,9 +12,18 @@ import { logError, logInfo, logSuccess, logVerbose, logWarn } from '@/utils/log'
let imapConnected = false; let imapConnected = false;
let monitorStartTime = 0; let monitorStartTime = 0;
let reconnectAttempts = 0;
const MAX_RECONNECT_DELAY = 300000; // 5 minutes
export const isImapConnected = () => imapConnected; export const isImapConnected = () => imapConnected;
const getReconnectDelay = () => {
const baseDelay = 10000; // 10 seconds
const delay = Math.min(baseDelay * 2 ** reconnectAttempts, MAX_RECONNECT_DELAY);
reconnectAttempts++;
return delay;
};
export async function startProtonMonitor() { export async function startProtonMonitor() {
if (!PROTON_IMAP_USERNAME || !PROTON_IMAP_PASSWORD) { if (!PROTON_IMAP_USERNAME || !PROTON_IMAP_PASSWORD) {
logError('Missing required env vars: PROTON_IMAP_USERNAME and PROTON_IMAP_PASSWORD'); logError('Missing required env vars: PROTON_IMAP_USERNAME and PROTON_IMAP_PASSWORD');
@ -114,6 +123,7 @@ export async function startProtonMonitor() {
const nameMatch = rawFrom.match(/^"?([^"<]+)"?\s*<?/); const nameMatch = rawFrom.match(/^"?([^"<]+)"?\s*<?/);
const from = nameMatch?.[1]?.trim() || rawFrom; const from = nameMatch?.[1]?.trim() || rawFrom;
reconnectAttempts = 0;
sendNotification(from, subject); sendNotification(from, subject);
}); });
}); });
@ -123,6 +133,7 @@ export async function startProtonMonitor() {
imap.on('ready', () => { imap.on('ready', () => {
imapConnected = true; imapConnected = true;
reconnectAttempts = 0;
logSuccess('IMAP is ready'); logSuccess('IMAP is ready');
openInbox(); openInbox();
}); });
@ -132,14 +143,26 @@ export async function startProtonMonitor() {
logError('IMAP error:', err.message); logError('IMAP error:', err.message);
}); });
imap.on('close', (hadError: boolean) => { const handleReconnect = (reason: string) => {
imapConnected = false; imapConnected = false;
logError(`IMAP connection closed (hadError: ${hadError})`); logError(reason);
const delay = getReconnectDelay();
logInfo(`Attempting to reconnect in ${delay / 1000}s (attempt ${reconnectAttempts})...`);
setTimeout(() => {
if (!imapConnected) {
logInfo('Reconnecting to Proton Bridge...');
imap.connect();
}
}, delay);
};
imap.on('close', (hadError: boolean) => {
handleReconnect(`IMAP connection closed (hadError: ${hadError})`);
}); });
imap.on('end', () => { imap.on('end', () => {
imapConnected = false; handleReconnect('IMAP connection ended by server');
logError('IMAP connection ended by server');
}); });
imap.connect(); imap.connect();

View file

@ -161,27 +161,36 @@ const handleQRSection = async () => {
if ((!cachedQR || now - qrCacheTime > QR_CACHE_TTL) && !generatingPromise) { if ((!cachedQR || now - qrCacheTime > QR_CACHE_TTL) && !generatingPromise) {
generatingPromise = (async () => { generatingPromise = (async () => {
const qr = await generateLinkQR(); try {
cachedQR = qr; const qr = await generateLinkQR();
qrCacheTime = Date.now(); cachedQR = qr;
qrCacheTime = Date.now();
finishLink() finishLink()
.then(async () => { .then(async () => {
await initSignal(); await initSignal();
}) })
.catch(() => {}) .catch(() => {})
.finally(() => { .finally(() => {
generatingPromise = null; generatingPromise = null;
cachedQR = null; cachedQR = null;
qrCacheTime = 0; qrCacheTime = 0;
}); });
return qr; return qr;
} catch (error) {
generatingPromise = null;
throw error;
}
})(); })();
} }
if (generatingPromise && !cachedQR) { if (generatingPromise && !cachedQR) {
await generatingPromise; try {
await generatingPromise;
} catch {
return '<p>Signal daemon is starting up, please refresh in a few seconds...</p>';
}
} }
return ` return `