'use server'; import 'server-only'; import { ImapFlow } from 'imapflow'; import nodemailer from 'nodemailer'; import { prisma } from '@/lib/prisma'; import { simpleParser } from 'mailparser'; import { cacheEmailCredentials, getCachedEmailCredentials, cacheEmailList, getCachedEmailList, cacheEmailContent, getCachedEmailContent, cacheImapSession, getCachedImapSession, invalidateFolderCache, invalidateEmailContentCache } from '@/lib/redis'; import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@/lib/types'; // Types specific to this service export interface EmailListResult { emails: EmailMessage[]; totalEmails: number; page: number; perPage: number; totalPages: number; folder: string; mailboxes: string[]; } // Connection pool to reuse IMAP clients 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(); // CRITICAL PERFORMANCE FIX: Increase idle timeout from 15 minutes to 30 minutes // This will keep connections alive longer and reduce reconnection delays const CONNECTION_TIMEOUT = 30 * 60 * 1000; // Increased to 30 minutes (was 15 minutes) const MAX_POOL_SIZE = 20; // Maximum number of connections to keep in the pool const CONNECTION_CHECK_INTERVAL = 60 * 1000; // Check every minute const MIN_POOL_SIZE = 2; // Keep at least this many active connections per user // Clean up idle connections periodically 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; } // PERFORMANCE FIX: Group connections by user for better management const connectionsByUser: Record = {}; connectionKeys.forEach(key => { const userId = key.split(':')[0]; if (!connectionsByUser[userId]) { connectionsByUser[userId] = []; } connectionsByUser[userId].push(key); }); // PERFORMANCE FIX: Manage pool size per user Object.entries(connectionsByUser).forEach(([userId, userConnections]) => { // Sort connections by last used (oldest first) const sortedConnections = userConnections .map(key => ({ key, lastUsed: connectionPool[key].lastUsed })) .sort((a, b) => a.lastUsed - b.lastUsed); // Keep the most recently used connections up to the min pool size const connectionsToKeep = sortedConnections.slice(-MIN_POOL_SIZE); const keepKeys = new Set(connectionsToKeep.map(conn => conn.key)); // Check the rest for idle timeout sortedConnections.forEach(({ key, lastUsed }) => { // Skip connections to keep and those that are in the process of connecting if (keepKeys.has(key) || connectionPool[key].isConnecting) { return; } // Only close connections idle for too long if (now - lastUsed > CONNECTION_TIMEOUT) { console.log(`Closing idle IMAP connection for ${key} (idle for ${Math.round((now - lastUsed)/1000)}s)`); try { if (connectionPool[key].client.usable) { connectionPool[key].client.logout().catch(err => { console.error(`Error closing idle connection for ${key}:`, err); }); } } catch (error) { console.error(`Error checking connection status for ${key}:`, error); } finally { delete connectionPool[key]; console.log(`Removed idle connection for ${key} from pool (pool size: ${Object.keys(connectionPool).length})`); } } }); }); // Log connection pool status with more details const activeCount = connectionKeys.filter(key => { const conn = connectionPool[key]; return !conn.isConnecting && (conn.client?.usable || false); }).length; const connectingCount = connectionKeys.filter(key => connectionPool[key].isConnecting).length; console.log(`[IMAP POOL] Size: ${connectionKeys.length}, Active: ${activeCount}, Connecting: ${connectingCount}, Max: ${MAX_POOL_SIZE}`); }, CONNECTION_CHECK_INTERVAL); /** * Get IMAP connection for a user, reusing existing connections when possible * with improved connection handling and error recovery */ 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 if (!accountId || accountId === 'default') { console.log(`No specific account provided or 'default' requested, trying to find first account for user ${userId}`); // Try getting the account ID from cache to avoid database query const sessionData = await getCachedImapSession(userId); if (sessionData && sessionData.defaultAccountId) { accountId = sessionData.defaultAccountId; console.log(`Using cached default account ID: ${accountId}`); } else { // Query to find all accounts for this user const accounts = await prisma.mailCredentials.findMany({ where: { userId }, orderBy: { createdAt: 'asc' }, take: 1 }); if (accounts && accounts.length > 0) { const firstAccount = accounts[0]; console.log(`Using first available account: ${firstAccount.id} (${firstAccount.email})`); accountId = firstAccount.id; // Cache default account ID for future use if (sessionData) { await cacheImapSession(userId, { ...sessionData, defaultAccountId: accountId, lastActive: Date.now() }); } else { await cacheImapSession(userId, { lastActive: Date.now(), defaultAccountId: accountId }); } } else { totalConnectionErrors++; throw new Error('No email accounts configured for this user'); } } } // Use accountId in connection key to ensure different accounts get different connections const connectionKey = `${userId}:${accountId}`; // If we already have a connection for this key if (connectionPool[connectionKey]) { const connection = connectionPool[connectionKey]; // If a connection is being established, wait for it if (connection.isConnecting && connection.connectionPromise) { console.log(`Connection in progress for ${connectionKey}, waiting for existing connection`); 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); // Fall through to create new connection } } // Try to use existing connection if it's usable try { // PERFORMANCE FIX: More robust connection status checking if (connection.client && connection.client.usable) { // Touch the connection to mark it as recently used connection.lastUsed = Date.now(); console.log(`Reusing existing IMAP connection for ${connectionKey}`); // 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`); // Will create a new connection below } } catch (error) { console.warn(`Error checking existing connection for ${connectionKey}:`, error); // Will create a new connection below } } // If we get here, we need a new connection console.log(`Creating new IMAP connection for ${connectionKey}`); // First try to get credentials from Redis cache let credentials = await getCachedEmailCredentials(userId, accountId); // If not in cache, get from database and cache them if (!credentials) { console.log(`Credentials not found in cache for ${userId}${accountId ? ` account ${accountId}` : ''}, attempting database lookup`); credentials = await getUserEmailCredentials(userId, accountId); if (!credentials) { console.error(`No credentials found for user ${userId}${accountId ? ` account ${accountId}` : ''}`); totalConnectionErrors++; throw new Error('Email account credentials not found'); } // Cache the credentials for future use await cacheEmailCredentials(userId, accountId, credentials); } // Initialize connection tracking connectionPool[connectionKey] = { client: null as any, lastUsed: Date.now(), isConnecting: true, connectionAttempts: (connectionPool[connectionKey]?.connectionAttempts || 0) + 1 }; // PERFORMANCE FIX: Add connection timeout to prevent hanging connections let connectionTimeout: NodeJS.Timeout | null = setTimeout(() => { console.error(`[IMAP] Connection for ${connectionKey} timed out after 60 seconds`); if (connectionPool[connectionKey]?.isConnecting) { delete connectionPool[connectionKey]; totalConnectionErrors++; } }, 60 * 1000); // 60 seconds timeout // 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(); // Clear timeout since connection was successful if (connectionTimeout) { clearTimeout(connectionTimeout); connectionTimeout = null; } // 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 => { // Clear timeout to prevent double errors if (connectionTimeout) { clearTimeout(connectionTimeout); connectionTimeout = null; } // 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; return connectionPromise; } /** * Helper function to create a new IMAP connection */ async function createImapConnection(credentials: EmailCredentials, connectionKey: string): Promise { const client = new ImapFlow({ host: credentials.host, port: credentials.port, secure: credentials.secure ?? true, auth: { user: credentials.email, pass: credentials.password, }, logger: false, emitLogs: false, tls: { rejectUnauthorized: false }, // Connection timeout settings disableAutoIdle: false // Keep idle to auto-refresh connection }); await client.connect(); // Add error handler client.on('error', (err) => { console.error(`IMAP connection error for ${connectionKey}:`, err); // Remove from pool on error if (connectionPool[connectionKey]) { delete connectionPool[connectionKey]; } }); return client; } /** * Update session data in Redis */ async function updateSessionData(userId: string, accountId?: string): Promise { const sessionData = await getCachedImapSession(userId); if (sessionData) { await cacheImapSession(userId, { ...sessionData, lastActive: Date.now(), ...(accountId && { defaultAccountId: accountId }) }); } else { await cacheImapSession(userId, { lastActive: Date.now(), ...(accountId && { defaultAccountId: accountId }) }); } } /** * Get user's email credentials from database */ export async function getUserEmailCredentials(userId: string, accountId?: string): Promise { const credentials = await prisma.mailCredentials.findFirst({ where: { AND: [ { userId }, accountId ? { id: accountId } : {} ] } }); if (!credentials) return null; const mailCredentials = credentials as unknown as { email: string; password: string; host: string; port: number; secure: boolean; smtp_host: string | null; smtp_port: number | null; smtp_secure: boolean | null; display_name: string | null; color: string | null; }; return { email: mailCredentials.email, password: mailCredentials.password, host: mailCredentials.host, port: mailCredentials.port, secure: mailCredentials.secure, smtp_host: mailCredentials.smtp_host || undefined, smtp_port: mailCredentials.smtp_port || undefined, smtp_secure: mailCredentials.smtp_secure ?? false, display_name: mailCredentials.display_name || undefined, color: mailCredentials.color || undefined }; } /** * Save or update user's email credentials */ export async function saveUserEmailCredentials( userId: string, accountId: string, credentials: EmailCredentials ): Promise { console.log('Saving credentials for user:', userId, 'account:', accountId); if (!credentials) { throw new Error('No credentials provided'); } // Extract only the fields that exist in the database schema const dbCredentials = { email: credentials.email, password: credentials.password ?? '', host: credentials.host, port: credentials.port, secure: credentials.secure ?? true, smtp_host: credentials.smtp_host || null, smtp_port: credentials.smtp_port || null, smtp_secure: credentials.smtp_secure ?? false, display_name: credentials.display_name || null, color: credentials.color || null }; try { // Save to database using the unique constraint on [userId, email] await prisma.mailCredentials.upsert({ where: { id: await prisma.mailCredentials.findFirst({ where: { AND: [ { userId }, { email: accountId } ] }, select: { id: true } }).then(result => result?.id ?? '') }, update: dbCredentials, create: { userId, ...dbCredentials } }); // Cache the full credentials object in Redis await cacheEmailCredentials(userId, accountId, credentials); console.log('Successfully saved and cached credentials for user:', userId); } catch (error) { console.error('Error saving credentials:', error); throw error; } } // Helper type for IMAP fetch options interface FetchOptions { envelope: boolean; flags: boolean; bodyStructure: boolean; internalDate: boolean; size: boolean; bodyParts: { part: string; query: any; limit?: number }[]; } /** * Get list of emails for a user */ export async function getEmails( userId: string, folder: string, page: number = 1, perPage: number = 20, accountId?: string ): Promise { // Normalize folder name and handle account ID console.log(`[getEmails] Processing request for folder: ${folder}, normalized to ${folder}, account: ${accountId || 'default'}`); try { // The getImapConnection function already handles 'default' accountId by finding the first available account const client = await getImapConnection(userId, accountId); // At this point, accountId has been resolved to an actual account ID by getImapConnection // Store the resolved accountId in a variable that is guaranteed to be a string const resolvedAccountId = accountId || 'default'; // Attempt to select the mailbox try { const mailboxInfo = await client.mailboxOpen(folder); console.log(`Opened mailbox ${folder} with ${mailboxInfo.exists} messages`); // Get list of all mailboxes for UI const mailboxes = await getMailboxes(client, resolvedAccountId); // Calculate pagination const totalEmails = mailboxInfo.exists || 0; const totalPages = Math.ceil(totalEmails / perPage); // Check if mailbox is empty if (totalEmails === 0) { // Cache the empty result const emptyResult = { emails: [], totalEmails: 0, page, perPage, totalPages: 0, folder, mailboxes }; await cacheEmailList( userId, resolvedAccountId, // Use the guaranteed string account ID folder, page, perPage, emptyResult ); return emptyResult; } // Calculate message range for pagination const start = Math.max(1, totalEmails - (page * perPage) + 1); const end = Math.max(1, totalEmails - ((page - 1) * perPage)); console.log(`Fetching messages ${start}:${end} from ${folder} for account ${resolvedAccountId}`); // Fetch messages const messages = await client.fetch(`${start}:${end}`, { envelope: true, flags: true, bodyStructure: true }); const emails: EmailMessage[] = []; for await (const message of messages) { const email: EmailMessage = { id: message.uid.toString(), from: message.envelope.from?.map(addr => ({ name: addr.name || '', address: addr.address || '' })) || [], to: message.envelope.to?.map(addr => ({ name: addr.name || '', address: addr.address || '' })) || [], subject: message.envelope.subject || '', date: message.envelope.date || new Date(), flags: { seen: message.flags.has('\\Seen'), flagged: message.flags.has('\\Flagged'), answered: message.flags.has('\\Answered'), draft: message.flags.has('\\Draft'), deleted: message.flags.has('\\Deleted') }, size: message.size || 0, hasAttachments: message.bodyStructure?.childNodes?.some(node => node.disposition === 'attachment') || false, folder: folder, contentFetched: false, accountId: resolvedAccountId, content: { text: '', html: '' } }; emails.push(email); } // Cache the result with the effective account ID await cacheEmailList( userId, resolvedAccountId, // Use the guaranteed string account ID folder, page, perPage, { emails, totalEmails: totalEmails, page, perPage, totalPages: Math.ceil(totalEmails / perPage), folder: folder, mailboxes: mailboxes } ); return { emails, totalEmails: totalEmails, page, perPage, totalPages: Math.ceil(totalEmails / perPage), folder: folder, mailboxes: mailboxes }; } catch (error) { console.error('Error fetching emails:', error); throw error; } } catch (error) { console.error('Error fetching emails:', error); throw error; } } /** * Get a single email with full content */ export async function getEmailContent( userId: string, emailId: string, folder: string = 'INBOX', accountId?: string ): Promise { // Validate parameters if (!userId || !emailId || !folder) { throw new Error('Missing required parameters'); } // Validate UID format if (!/^\d+$/.test(emailId)) { throw new Error('Invalid email ID format: must be a numeric UID'); } // Convert to number for IMAP const numericId = parseInt(emailId, 10); if (isNaN(numericId)) { throw new Error('Email ID must be a number'); } // Extract account ID from folder name if present and none was explicitly provided const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; // Use the most specific account ID available const effectiveAccountId = folderAccountId || accountId || 'default'; // Normalize folder name by removing account prefix if present const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; console.log(`[getEmailContent] Fetching email ${emailId} from folder ${normalizedFolder}, account ${effectiveAccountId}`); // Use normalized folder name and effective account ID for cache key const cachedEmail = await getCachedEmailContent(userId, effectiveAccountId, emailId); if (cachedEmail) { console.log(`Using cached email content for ${userId}:${effectiveAccountId}:${emailId}`); return cachedEmail; } console.log(`Cache miss for email content ${userId}:${effectiveAccountId}:${emailId}, fetching from IMAP`); const client = await getImapConnection(userId, effectiveAccountId); try { // Log connection details with account context console.log(`[DEBUG] Fetching email ${emailId} from folder ${normalizedFolder} for account ${effectiveAccountId}`); // Open mailbox with error handling const mailbox = await client.mailboxOpen(normalizedFolder); if (!mailbox || typeof mailbox === 'boolean') { throw new Error(`Failed to open mailbox: ${normalizedFolder} for account ${effectiveAccountId}`); } // Log mailbox status with account context console.log(`[DEBUG] Mailbox ${normalizedFolder} opened for account ${effectiveAccountId}, total messages: ${mailbox.exists}`); // Get the UIDVALIDITY and UIDNEXT values const uidValidity = mailbox.uidValidity; const uidNext = mailbox.uidNext; console.log(`[DEBUG] Mailbox UIDVALIDITY: ${uidValidity}, UIDNEXT: ${uidNext} for account ${effectiveAccountId}`); // Validate UID exists in mailbox if (numericId >= uidNext) { throw new Error(`Email ID ${numericId} is greater than or equal to the highest UID in mailbox (${uidNext}) for account ${effectiveAccountId}`); } // First, try to get the sequence number for this UID const searchResult = await client.search({ uid: numericId.toString() }); if (!searchResult || searchResult.length === 0) { throw new Error(`Email with UID ${numericId} not found in folder ${normalizedFolder} for account ${effectiveAccountId}`); } const sequenceNumber = searchResult[0]; console.log(`[DEBUG] Found sequence number ${sequenceNumber} for UID ${numericId} in account ${effectiveAccountId}`); // Now fetch using the sequence number const message = await client.fetchOne(sequenceNumber.toString(), { source: true, envelope: true, flags: true, size: true }); if (!message) { throw new Error(`Email not found with sequence number ${sequenceNumber} in folder ${normalizedFolder} for account ${effectiveAccountId}`); } const { source, envelope, flags, size } = message; // Parse the email content, ensuring all styles and structure are preserved const parsedEmail = await simpleParser(source.toString(), { skipHtmlToText: true, keepCidLinks: true }); // Convert flags from Set to boolean checks const flagsArray = Array.from(flags as Set); // Preserve the raw HTML exactly as it was in the original email const rawHtml = parsedEmail.html || ''; const email: EmailMessage = { id: emailId, messageId: envelope.messageId, subject: envelope.subject || "(No Subject)", from: envelope.from.map((f: any) => ({ name: f.name || f.address, address: f.address, })), to: envelope.to.map((t: any) => ({ name: t.name || t.address, address: t.address, })), cc: (envelope.cc || []).map((c: any) => ({ name: c.name || c.address, address: c.address, })), bcc: (envelope.bcc || []).map((b: any) => ({ name: b.name || b.address, address: b.address, })), date: envelope.date || new Date(), flags: { seen: flagsArray.includes("\\Seen"), flagged: flagsArray.includes("\\Flagged"), answered: flagsArray.includes("\\Answered"), deleted: flagsArray.includes("\\Deleted"), draft: flagsArray.includes("\\Draft"), }, hasAttachments: parsedEmail.attachments?.length > 0, attachments: parsedEmail.attachments?.map(att => ({ filename: att.filename || 'attachment', contentType: att.contentType, size: att.size || 0 })), content: { text: parsedEmail.text || '', html: rawHtml || '', isHtml: !!rawHtml, direction: 'ltr' // Default to left-to-right }, folder: normalizedFolder, contentFetched: true, size: size || 0, accountId: effectiveAccountId }; // Cache the email content with effective account ID await cacheEmailContent(userId, effectiveAccountId, emailId, email); return email; } catch (error) { console.error('[ERROR] Email fetch failed:', { userId, emailId, folder: normalizedFolder, accountId: effectiveAccountId, error: error instanceof Error ? error.message : 'Unknown error', details: error instanceof Error ? error.stack : undefined }); throw error; } finally { try { await client.mailboxClose(); } catch (error) { console.error('Error closing mailbox:', error); } } } /** * Mark an email as read or unread */ export async function markEmailReadStatus( userId: string, emailId: string, isRead: boolean, folder: string = 'INBOX', accountId?: string ): Promise { // Extract account ID from folder name if present and none was explicitly provided const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; // Use the most specific account ID available const effectiveAccountId = folderAccountId || accountId || 'default'; // Normalize folder name by removing account prefix if present const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; console.log(`[markEmailReadStatus] Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${normalizedFolder}, account ${effectiveAccountId}`); const client = await getImapConnection(userId, effectiveAccountId); try { await client.mailboxOpen(normalizedFolder); if (isRead) { await client.messageFlagsAdd(emailId, ['\\Seen']); } else { await client.messageFlagsRemove(emailId, ['\\Seen']); } // Invalidate content cache since the flags changed await invalidateEmailContentCache(userId, effectiveAccountId, emailId); // Also invalidate folder cache because unread counts may have changed await invalidateFolderCache(userId, effectiveAccountId, normalizedFolder); return true; } catch (error) { console.error(`Error marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${normalizedFolder}, account ${effectiveAccountId}:`, error); return false; } finally { try { await client.mailboxClose(); } catch (error) { console.error('Error closing mailbox:', error); } } } /** * Send an email */ export async function sendEmail( userId: string, emailData: { to: string; cc?: string; bcc?: string; subject: string; body: string; attachments?: Array<{ name: string; content: string; type: string; }>; } ): Promise<{ success: boolean; messageId?: string; error?: string }> { const credentials = await getUserEmailCredentials(userId); if (!credentials) { return { success: false, error: 'No email credentials found' }; } // Create SMTP transporter with user's SMTP settings if available const transporter = nodemailer.createTransport({ host: credentials.smtp_host || 'smtp.infomaniak.com', port: credentials.smtp_port || 587, secure: credentials.smtp_secure || false, auth: { user: credentials.email, pass: credentials.password, }, tls: { rejectUnauthorized: false } }); try { const info = await transporter.sendMail({ from: credentials.email, to: emailData.to, cc: emailData.cc, bcc: emailData.bcc, subject: emailData.subject, text: emailData.body, html: emailData.body, attachments: emailData.attachments?.map(att => ({ filename: att.name, content: att.content, contentType: att.type })), }); return { success: true, messageId: info.messageId }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } /** * Get list of mailboxes from an IMAP connection */ export async function getMailboxes(client: ImapFlow, accountId?: string): Promise { try { const mailboxes = await client.list(); // If we have an accountId, prefix the folder names to prevent namespace collisions if (accountId) { return mailboxes.map(mailbox => `${accountId}:${mailbox.path}`); } // For backward compatibility, return unprefixed names when no accountId return mailboxes.map(mailbox => mailbox.path); } catch (error) { console.error('Error fetching mailboxes:', error); // Return empty array on error to avoid showing incorrect folders return []; } } /** * Test IMAP and SMTP connections for an email account */ export async function testEmailConnection(credentials: EmailCredentials): Promise<{ imap: boolean; smtp?: boolean; error?: string; folders?: string[]; }> { console.log('Testing connection with:', { ...credentials, password: '***' }); // Test IMAP connection try { console.log(`Testing IMAP connection to ${credentials.host}:${credentials.port} for ${credentials.email}`); const client = new ImapFlow({ host: credentials.host, port: credentials.port, secure: credentials.secure ?? true, auth: { user: credentials.email, pass: credentials.password, }, logger: false, tls: { rejectUnauthorized: false } }); await client.connect(); const folders = await getMailboxes(client); await client.logout(); console.log(`IMAP connection successful for ${credentials.email}`); console.log(`Found ${folders.length} folders:`, folders); // Test SMTP connection if SMTP settings are provided let smtpSuccess = false; if (credentials.smtp_host && credentials.smtp_port) { try { console.log(`Testing SMTP connection to ${credentials.smtp_host}:${credentials.smtp_port}`); const transporter = nodemailer.createTransport({ host: credentials.smtp_host, port: credentials.smtp_port, secure: credentials.smtp_secure ?? false, auth: { user: credentials.email, pass: credentials.password, }, tls: { rejectUnauthorized: false } }); await transporter.verify(); console.log(`SMTP connection successful for ${credentials.email}`); smtpSuccess = true; } catch (smtpError) { console.error(`SMTP connection failed for ${credentials.email}:`, smtpError); return { imap: true, smtp: false, error: `SMTP connection failed: ${smtpError instanceof Error ? smtpError.message : 'Unknown error'}`, folders }; } } return { imap: true, smtp: smtpSuccess, folders }; } catch (error) { console.error(`IMAP connection failed for ${credentials.email}:`, error); return { imap: false, error: `IMAP connection failed: ${error instanceof Error ? error.message : 'Unknown error'}` }; } }