import Imap from 'imap'; import { getCredentialsFromSession, decryptCredentials } from './encryption'; interface ConnectionPoolItem { connection: any; sessionId: string; lastUsed: number; isConnected: boolean; } class IMAPConnectionPool { private connections: Map = new Map(); private maxConnections = 5; private connectionTimeout = 300000; // 5 minutes private reconnectAttempts = 3; private reconnectDelay = 1000; // 1 second private cleanupInterval: NodeJS.Timeout; constructor() { // Cleanup expired connections every minute this.cleanupInterval = setInterval(() => { this.cleanupExpiredConnections(); }, 60000); } async getConnection(sessionId: string): Promise { // Check if we have an existing connection const existing = this.connections.get(sessionId); if (existing && existing.isConnected) { // Test connection health if (await this.testConnection(existing.connection)) { existing.lastUsed = Date.now(); return existing.connection; } else { // Connection is dead, remove it this.removeConnection(sessionId); } } // Create new connection return await this.createConnection(sessionId); } private async createConnection(sessionId: string): Promise { const encryptedCredentials = getCredentialsFromSession(sessionId); if (!encryptedCredentials) { throw new Error('No credentials found for session'); } let credentials; try { credentials = decryptCredentials(encryptedCredentials); } catch (error) { throw new Error('Failed to decrypt credentials'); } const imapConfig = { user: credentials.email, password: credentials.password, host: process.env.NUXT_EMAIL_IMAP_HOST || 'mail.portnimara.com', port: parseInt(process.env.NUXT_EMAIL_IMAP_PORT || '993'), tls: true, tlsOptions: { rejectUnauthorized: false }, connTimeout: 15000, // 15 seconds connection timeout authTimeout: 10000, // 10 seconds auth timeout keepalive: { interval: 10000, // Send keepalive every 10 seconds idleInterval: 300000, // IDLE for 5 minutes forceNoop: true } }; return new Promise((resolve, reject) => { const imap = new Imap(imapConfig); let connectionAttempts = 0; const attemptConnection = () => { connectionAttempts++; imap.once('ready', () => { console.log(`[IMAPPool] Connection established for session ${sessionId}`); // Store connection this.connections.set(sessionId, { connection: imap, sessionId, lastUsed: Date.now(), isConnected: true }); // Set up error handlers imap.on('error', (err: any) => { console.error(`[IMAPPool] Connection error for session ${sessionId}:`, err); this.markConnectionAsDead(sessionId); }); imap.on('end', () => { console.log(`[IMAPPool] Connection ended for session ${sessionId}`); this.removeConnection(sessionId); }); resolve(imap); }); imap.once('error', (err: any) => { console.error(`[IMAPPool] Connection attempt ${connectionAttempts} failed:`, err); if (connectionAttempts < this.reconnectAttempts) { setTimeout(() => { try { imap.connect(); } catch (connectError) { console.error(`[IMAPPool] Reconnect attempt failed:`, connectError); attemptConnection(); } }, this.reconnectDelay * connectionAttempts); } else { reject(err); } }); try { imap.connect(); } catch (connectError) { console.error(`[IMAPPool] Initial connect failed:`, connectError); if (connectionAttempts < this.reconnectAttempts) { setTimeout(attemptConnection, this.reconnectDelay); } else { reject(connectError); } } }; attemptConnection(); }); } private async testConnection(imap: any): Promise { return new Promise((resolve) => { try { if (!imap || imap.state !== 'authenticated') { resolve(false); return; } // Test with a simple NOOP command imap.seq.fetch('1:1', { bodies: 'HEADER' }, (err: any) => { // Even if fetch fails, if no connection error then connection is likely OK resolve(!err || err.code !== 'ECONNRESET'); }); // Timeout the test after 5 seconds setTimeout(() => resolve(false), 5000); } catch (error) { resolve(false); } }); } private markConnectionAsDead(sessionId: string): void { const connection = this.connections.get(sessionId); if (connection) { connection.isConnected = false; } } private removeConnection(sessionId: string): void { const connection = this.connections.get(sessionId); if (connection) { try { if (connection.connection && typeof connection.connection.end === 'function') { connection.connection.end(); } } catch (error) { console.error(`[IMAPPool] Error closing connection:`, error); } this.connections.delete(sessionId); } } private cleanupExpiredConnections(): void { const now = Date.now(); for (const [sessionId, connection] of this.connections.entries()) { if (now - connection.lastUsed > this.connectionTimeout) { console.log(`[IMAPPool] Cleaning up expired connection for session ${sessionId}`); this.removeConnection(sessionId); } } } async closeConnection(sessionId: string): Promise { this.removeConnection(sessionId); } async closeAllConnections(): Promise { for (const sessionId of this.connections.keys()) { this.removeConnection(sessionId); } } destroy(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.closeAllConnections(); } } // Global instance let globalIMAPPool: IMAPConnectionPool | null = null; export function getIMAPPool(): IMAPConnectionPool { if (!globalIMAPPool) { globalIMAPPool = new IMAPConnectionPool(); } return globalIMAPPool; } export function destroyIMAPPool(): void { if (globalIMAPPool) { globalIMAPPool.destroy(); globalIMAPPool = null; } }