diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index a49b8ef2..eaaee321 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -159,7 +159,7 @@ export async function getUserEmailCredentials(userId: string, accountId?: string where: { AND: [ { userId }, - { email: accountId } + accountId ? { id: accountId } : {} ] } }); @@ -272,265 +272,90 @@ export async function getEmails( searchQuery: string = '', accountId?: string ): Promise { + console.log(`Fetching emails for user ${userId}${accountId ? ` account ${accountId}` : ''} in folder ${folder}`); + // Try to get from cache first - if (!searchQuery) { - const cacheKey = accountId ? `${userId}:${accountId}:${folder}` : `${userId}:${folder}`; - const cachedResult = await getCachedEmailList(userId, accountId || 'default', folder, page, perPage); - if (cachedResult) { - console.log(`Using cached email list for ${cacheKey}:${page}:${perPage}`); - return cachedResult; - } + const cacheKey = accountId + ? `email:list:${userId}:${accountId}:${folder}:${page}:${perPage}:${searchQuery}` + : `email:list:${userId}:${folder}:${page}:${perPage}:${searchQuery}`; + + const cached = await getCachedEmailList(cacheKey); + if (cached) { + console.log(`Using cached email list for ${cacheKey}`); + return cached; } - - console.log(`Cache miss for emails ${userId}:${folder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}, fetching from IMAP`); - - // If accountId is provided, connect to that specific account - let client: ImapFlow; - - if (accountId) { - try { - // Get account from database - const account = await prisma.mailCredentials.findFirst({ - where: { - AND: [ - { userId }, - { email: accountId } - ] - } - }); - - if (!account) { - throw new Error(`Account with ID ${accountId} not found`); - } - - // Connect to IMAP server for this specific account - client = new ImapFlow({ - host: account.host, - port: account.port, - secure: true, // Default to secure connection - auth: { - user: account.email, - pass: account.password, - }, - logger: false, - tls: { - rejectUnauthorized: false - } - }); - - await client.connect(); - } catch (error) { - console.error(`Error connecting to account ${accountId}:`, error); - // Fallback to default connection - client = await getImapConnection(userId); - } - } else { - // Use the default connection logic - client = await getImapConnection(userId); + + // Get IMAP connection for the specific account + const client = await getImapConnection(userId, accountId); + if (!client) { + throw new Error('Failed to get IMAP connection'); } - - let mailboxes: string[] = []; - + try { - console.log(`[DEBUG] Fetching mailboxes for user ${userId}`); - // Get list of mailboxes first - try { - mailboxes = await getMailboxes(client); - console.log(`[DEBUG] Found ${mailboxes.length} mailboxes:`, mailboxes); - - // Save mailboxes in session data - const cachedSession = await getCachedImapSession(userId); - await cacheImapSession(userId, { - ...(cachedSession || { lastActive: Date.now() }), - mailboxes - }); - console.log(`[DEBUG] Updated cached session with mailboxes for user ${userId}`); - } catch (mailboxError) { - console.error(`[ERROR] Failed to fetch mailboxes:`, mailboxError); - } + // Select the mailbox + await client.mailboxOpen(folder); - // Open mailbox - const mailboxData = await client.mailboxOpen(folder); - const totalMessages = mailboxData.exists; + // Get total count + const totalEmails = await client.mailbox.messages.total; + const totalPages = Math.ceil(totalEmails / perPage); - // Calculate range based on total messages - const endIdx = page * perPage; - const startIdx = (page - 1) * perPage + 1; - const from = Math.max(totalMessages - endIdx + 1, 1); - const to = Math.max(totalMessages - startIdx + 1, 1); - - // Empty result if no messages - if (totalMessages === 0 || from > to) { - const result = { - emails: [], - totalEmails: 0, - page, - perPage, - totalPages: 0, - folder, - mailboxes - }; - - // Cache even empty results - if (!searchQuery) { - await cacheEmailList(userId, accountId || 'default', folder, page, perPage, result); - } - - return result; - } - - // Search if needed - let messageIds: any[] = []; - if (searchQuery) { - messageIds = await client.search({ body: searchQuery }); - messageIds = messageIds.filter(id => id >= from && id <= to); - } else { - messageIds = Array.from({ length: to - from + 1 }, (_, i) => from + i); - } + // Calculate range for this page + const start = (page - 1) * perPage + 1; + const end = Math.min(start + perPage - 1, totalEmails); // Fetch messages + const messages = await client.fetch(`${start}:${end}`, { + envelope: true, + flags: true, + bodyStructure: true, + internalDate: true, + size: true, + bodyParts: [ + { part: 'TEXT', query: { headers: true } } + ] + }); + + // Process messages const emails: EmailMessage[] = []; - - for (const id of messageIds) { - try { - // Define fetch options with proper typing - const fetchOptions: any = { - envelope: true, - flags: true, - bodyStructure: true, - internalDate: true, - size: true, - bodyParts: [{ - part: '1', - query: { type: "text" }, - limit: 5000 - }] - }; - - const message = await client.fetchOne(id, fetchOptions); - - if (!message) continue; - - const { envelope, flags, bodyStructure, internalDate, size, bodyParts } = message; - - // Extract preview content - let preview = ''; - if (bodyParts && typeof bodyParts === 'object') { - // Convert to array if it's a Map - const partsArray = Array.isArray(bodyParts) - ? bodyParts - : Array.from(bodyParts.values()); - - const textPart = partsArray.find((part: any) => part.type === 'text/plain'); - const htmlPart = partsArray.find((part: any) => part.type === 'text/html'); - const content = textPart?.content || htmlPart?.content || ''; - - if (typeof content === 'string') { - preview = content.substring(0, 150) + '...'; - } else if (Buffer.isBuffer(content)) { - preview = content.toString('utf-8', 0, 150) + '...'; - } - } - - // Process attachments - const attachments: EmailAttachment[] = []; - - const processAttachments = (node: any, path: Array = []) => { - if (!node) return; - - if (node.type === 'attachment') { - attachments.push({ - contentId: node.contentId, - filename: node.filename || 'attachment', - contentType: node.contentType, - size: node.size, - path: [...path, node.part].join('.') - }); - } - - if (node.childNodes) { - node.childNodes.forEach((child: any, index: number) => { - processAttachments(child, [...path, node.part || index + 1]); - }); - } - }; - - if (bodyStructure) { - processAttachments(bodyStructure); - } - - // Convert flags from Set to boolean checks - const flagsArray = Array.from(flags as Set); - - emails.push({ - id: id.toString(), - 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: internalDate || new Date(), - flags: { - seen: flagsArray.includes("\\Seen"), - flagged: flagsArray.includes("\\Flagged"), - answered: flagsArray.includes("\\Answered"), - deleted: flagsArray.includes("\\Deleted"), - draft: flagsArray.includes("\\Draft"), - }, - hasAttachments: attachments.length > 0, - attachments, - size, - preview, - folder, - contentFetched: false - }); - } catch (error) { - console.error(`Error fetching message ${id}:`, error); - } + for await (const message of messages) { + const email: EmailMessage = { + id: message.uid.toString(), + from: message.envelope.from[0]?.address || '', + to: message.envelope.to.map(addr => addr.address).join(', '), + subject: message.envelope.subject || '', + date: message.internalDate, + flags: { + seen: message.flags.has('\\Seen'), + answered: message.flags.has('\\Answered'), + flagged: message.flags.has('\\Flagged'), + draft: message.flags.has('\\Draft'), + deleted: message.flags.has('\\Deleted') + }, + size: message.size, + hasAttachments: message.bodyStructure?.childNodes?.some(node => node.disposition === 'attachment') || false + }; + emails.push(email); } - - // Sort by date, newest first - emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - - const result = { + + const result: EmailListResult = { emails, - totalEmails: totalMessages, + totalEmails, page, perPage, - totalPages: Math.ceil(totalMessages / perPage), + totalPages, folder, - mailboxes + mailboxes: await getMailboxes(client) }; - - // Always cache the result if it's not a search query, even for pagination - if (!searchQuery) { - console.log(`Caching email list for ${userId}:${folder}:${page}:${perPage}`); - await cacheEmailList(userId, accountId || 'default', folder, page, perPage, result); - } - + + // Cache the result + await cacheEmailList(cacheKey, result); + return result; + } catch (error) { + console.error(`Error fetching emails for ${userId}${accountId ? ` account ${accountId}` : ''}:`, error); + throw error; } finally { - // Don't logout, keep connection in pool - if (folder !== 'INBOX') { - try { - await client.mailboxClose(); - } catch (error) { - console.error('Error closing mailbox:', error); - } - } + // Don't close the connection, it's managed by the connection pool } }