diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index d36e3681..a6ecb9b5 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -30,68 +30,94 @@ export interface EmailListResult { mailboxes: string[]; } -// Connection pool management +// Connection pool to reuse IMAP clients const connectionPool: Record = {}; const CONNECTION_TIMEOUT = 5 * 60 * 1000; // 5 minutes // Clean up idle connections periodically setInterval(() => { const now = Date.now(); + Object.entries(connectionPool).forEach(([key, { client, lastUsed }]) => { if (now - lastUsed > CONNECTION_TIMEOUT) { console.log(`Closing idle IMAP connection for ${key}`); - if (client && client.usable) { - client.logout().catch(err => { - console.error(`Error closing connection for ${key}:`, err); - }); - } + client.logout().catch(err => { + console.error(`Error closing connection for ${key}:`, err); + }); delete connectionPool[key]; } }); }, 60 * 1000); // Check every minute /** - * Get IMAP connection for a user + * Get IMAP connection for a user, reusing existing connections when possible */ -export async function getImapConnection(userId: string, accountId?: string): Promise { - const key = `${userId}:${accountId || 'default'}`; +export async function getImapConnection( + userId: string, + accountId?: string +): Promise { + console.log(`Getting IMAP connection for user ${userId}${accountId ? ` account ${accountId}` : ''}`); - // Check if we have a valid connection in the pool - const existingConnection = connectionPool[key]; - if (existingConnection && existingConnection.client.usable) { - existingConnection.lastUsed = Date.now(); - return existingConnection.client; - } + // First try to get credentials from Redis cache + let credentials = accountId + ? await getCachedEmailCredentials(userId, accountId) + : await getCachedEmailCredentials(userId, 'default'); - // Get credentials from cache or database - const credentials = await getCachedEmailCredentials(userId, accountId || 'default'); + // If not in cache, get from database and cache them if (!credentials) { - // If no credentials found with default accountId, try to get from database - const dbCredentials = await prisma.mailCredentials.findFirst({ - where: { userId } - }); - - if (!dbCredentials) { + 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'); } - // Cache the credentials with the email as accountId - await cacheEmailCredentials(userId, dbCredentials.email, { - email: dbCredentials.email, - password: dbCredentials.password, - host: dbCredentials.host, - port: dbCredentials.port - }); - - // Try to get credentials again with the email as accountId - const retryCredentials = await getCachedEmailCredentials(userId, dbCredentials.email); - if (!retryCredentials) { - throw new Error('Failed to cache email credentials'); - } - - return getImapConnection(userId, dbCredentials.email); + // Cache credentials for future use + await cacheEmailCredentials(userId, accountId || 'default', 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'); + } + + // Use accountId in connection key to ensure different accounts get different connections + const connectionKey = `${userId}:${accountId || 'default'}`; + const existingConnection = connectionPool[connectionKey]; + + // Try to get session data from Redis + const sessionData = await getCachedImapSession(userId); + + // Return existing connection if available and connected + if (existingConnection) { + try { + if (existingConnection.client.usable) { + existingConnection.lastUsed = Date.now(); + console.log(`Reusing existing IMAP connection for ${connectionKey}`); + + // Update session data in Redis + if (sessionData) { + await cacheImapSession(userId, { + ...sessionData, + lastActive: Date.now() + }); + } + + return existingConnection.client; + } + } catch (error) { + console.warn(`Existing connection for ${connectionKey} is not usable, creating new connection`); + // Will create a new connection below + } + } + + console.log(`Creating new IMAP connection for ${connectionKey}`); + // Create new connection const client = new ImapFlow({ host: credentials.host, @@ -99,18 +125,30 @@ export async function getImapConnection(userId: string, accountId?: string): Pro secure: true, auth: { user: credentials.email, - pass: credentials.password + pass: credentials.password, }, - logger: false + logger: false, + emitLogs: false, + tls: { + rejectUnauthorized: false + } }); try { await client.connect(); - connectionPool[key] = { client, lastUsed: Date.now() }; + console.log(`Successfully connected to IMAP server for ${connectionKey}`); + + // Store in connection pool + connectionPool[connectionKey] = { + client, + lastUsed: Date.now() + }; + return client; - } catch (error) { - console.error('Error connecting to IMAP:', error); - throw error; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + console.error(`IMAP connection error for ${connectionKey}:`, errorMessage); + throw new Error(`Failed to connect to IMAP server: ${errorMessage}`); } } @@ -243,7 +281,7 @@ export async function getEmails( // Get IMAP connection client = await getImapConnection(userId, accountId); - if (!client || !client.usable) { + if (!client) { throw new Error('Failed to establish IMAP connection'); } @@ -343,7 +381,7 @@ export async function getEmails( console.error('Error fetching emails:', error); throw error; } finally { - if (client && client.usable) { + if (client) { try { await client.mailboxClose(); } catch (error) { @@ -434,10 +472,9 @@ export async function getEmailContent( contentType: att.contentType, size: att.size || 0 })), - content: { - text: parsedEmail.text || '', - html: rawHtml || '' - }, + html: rawHtml, + text: parsedEmail.text || undefined, + content: rawHtml || parsedEmail.text || '', folder, contentFetched: true, size: size || 0