diff --git a/lib/redis.ts b/lib/redis.ts index 1e336078..d722684f 100644 --- a/lib/redis.ts +++ b/lib/redis.ts @@ -157,6 +157,7 @@ interface ImapSessionData { lastActive: number; mailboxes?: string[]; lastVisit?: number; + defaultAccountId?: string; } /** diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 962d12e9..9bec9fb3 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -36,8 +36,16 @@ const connectionPool: Record; + connectionAttempts?: number; }> = {}; +// Track overall connection metrics +let totalConnectionRequests = 0; +let totalNewConnections = 0; +let totalReuseConnections = 0; +let totalConnectionErrors = 0; +let lastMetricsReset = Date.now(); + const CONNECTION_TIMEOUT = 15 * 60 * 1000; // Increased to 15 minutes for long-lived connections const MAX_POOL_SIZE = 20; // Maximum number of connections to keep in the pool const CONNECTION_CHECK_INTERVAL = 60 * 1000; // Check every minute @@ -47,6 +55,16 @@ setInterval(() => { const now = Date.now(); const connectionKeys = Object.keys(connectionPool); + // If we've been collecting metrics for more than an hour, log and reset + if (now - lastMetricsReset > 60 * 60 * 1000) { + console.log(`[IMAP METRICS] Total requests: ${totalConnectionRequests}, New connections: ${totalNewConnections}, Reused: ${totalReuseConnections}, Errors: ${totalConnectionErrors}, Success rate: ${((totalReuseConnections + totalNewConnections) / totalConnectionRequests * 100).toFixed(2)}%`); + totalConnectionRequests = 0; + totalNewConnections = 0; + totalReuseConnections = 0; + totalConnectionErrors = 0; + lastMetricsReset = now; + } + // If we're over the pool size limit, sort by last used and remove oldest if (connectionKeys.length > MAX_POOL_SIZE) { const sortedConnections = connectionKeys @@ -94,6 +112,9 @@ setInterval(() => { } } }); + + // Log connection pool status + console.log(`[IMAP POOL] Current size: ${connectionKeys.length}, Max: ${MAX_POOL_SIZE}`); }, CONNECTION_CHECK_INTERVAL); /** @@ -104,6 +125,9 @@ export async function getImapConnection( userId: string, accountId?: string ): Promise { + const startTime = Date.now(); + totalConnectionRequests++; + console.log(`Getting IMAP connection for user ${userId}${accountId ? ` account ${accountId}` : ''}`); // Special handling for 'default' accountId - find the first available account @@ -142,6 +166,7 @@ export async function getImapConnection( }); } } else { + totalConnectionErrors++; throw new Error('No email accounts configured for this user'); } } @@ -160,6 +185,8 @@ export async function getImapConnection( try { const client = await connection.connectionPromise; connection.lastUsed = Date.now(); + totalReuseConnections++; + console.log(`[IMAP] Reused pending connection for ${connectionKey} in ${Date.now() - startTime}ms`); return client; } catch (error) { console.error(`Error waiting for connection for ${connectionKey}:`, error); @@ -177,6 +204,8 @@ export async function getImapConnection( // Update session data in Redis await updateSessionData(userId, accountId); + totalReuseConnections++; + console.log(`[IMAP] Successfully reused connection for ${connectionKey} in ${Date.now() - startTime}ms`); return connection.client; } else { console.log(`Existing connection for ${connectionKey} not usable, recreating`); @@ -198,60 +227,54 @@ export async function getImapConnection( if (!credentials) { console.log(`Credentials not found in cache for ${userId}${accountId ? ` account ${accountId}` : ''}, attempting database lookup`); credentials = await getUserEmailCredentials(userId, accountId); + if (!credentials) { - throw new Error('No email credentials found'); + console.error(`No credentials found for user ${userId}${accountId ? ` account ${accountId}` : ''}`); + totalConnectionErrors++; + throw new Error('Email account credentials not found'); } - // Cache credentials for future use + // Cache the credentials for future use await cacheEmailCredentials(userId, accountId, credentials); } - // Validate credentials - if (!credentials.password) { - console.error(`Missing password in credentials for user ${userId}${accountId ? ` account ${accountId}` : ''}`); - throw new Error('No password configured'); - } - - if (!credentials.email || !credentials.host) { - console.error(`Incomplete credentials for user ${userId}${accountId ? ` account ${accountId}` : ''}`); - throw new Error('Invalid email credentials configuration'); - } - - // Create connection record with connecting state + // Initialize connection tracking connectionPool[connectionKey] = { - client: null as any, // Will be set once connected + client: null as any, lastUsed: Date.now(), - isConnecting: true + isConnecting: true, + connectionAttempts: (connectionPool[connectionKey]?.connectionAttempts || 0) + 1 }; - // Create the connection promise - const connectionPromise = createImapConnection(credentials, connectionKey); + // Create connection promise + const connectionPromise = createImapConnection(credentials, connectionKey) + .then(client => { + // Update connection pool entry + connectionPool[connectionKey].client = client; + connectionPool[connectionKey].isConnecting = false; + connectionPool[connectionKey].lastUsed = Date.now(); + + // Update session data + updateSessionData(userId, accountId).catch(err => { + console.error(`Failed to update session data: ${err.message}`); + }); + + totalNewConnections++; + console.log(`[IMAP] Created new connection for ${connectionKey} in ${Date.now() - startTime}ms (attempt #${connectionPool[connectionKey].connectionAttempts})`); + return client; + }) + .catch(error => { + // Handle connection error + console.error(`Failed to create IMAP connection for ${connectionKey}:`, error); + delete connectionPool[connectionKey]; + totalConnectionErrors++; + throw error; + }); + + // Save the promise to allow other requests to wait for this connection connectionPool[connectionKey].connectionPromise = connectionPromise; - try { - const client = await connectionPromise; - console.log(`Successfully connected to IMAP server for ${connectionKey}`); - - // Update connection record - connectionPool[connectionKey] = { - client, - lastUsed: Date.now(), - isConnecting: false - }; - - // Update session data in Redis - await updateSessionData(userId, accountId); - - return client; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - console.error(`IMAP connection error for ${connectionKey}:`, errorMessage); - - // Clean up failed connection - delete connectionPool[connectionKey]; - - throw new Error(`Failed to connect to IMAP server: ${errorMessage}`); - } + return connectionPromise; } /** @@ -272,10 +295,7 @@ async function createImapConnection(credentials: EmailCredentials, connectionKey rejectUnauthorized: false }, // Connection timeout settings - disableAutoIdle: false, // Keep idle to auto-refresh connection - idleTimeout: 60000, // 1 minute - idleRefreshTimeout: 30000, // 30 seconds - idleRefreshIntervalMs: 30 * 1000, // 30 seconds + disableAutoIdle: false // Keep idle to auto-refresh connection }); await client.connect(); @@ -302,12 +322,12 @@ async function updateSessionData(userId: string, accountId?: string): Promise