import { NextResponse } from 'next/server'; import { ImapFlow } from 'imapflow'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { prisma } from '@/lib/prisma'; import { simpleParser } from 'mailparser'; // Type definitions interface EmailCacheEntry { data: any; timestamp: number; } interface CredentialsCacheEntry { client: any; // Use any for ImapFlow to avoid type issues timestamp: number; } // Email cache structure const emailListCache: Record = {}; const credentialsCache: Record = {}; // Cache TTL in milliseconds const CACHE_TTL = 5 * 60 * 1000; // 5 minutes // Helper function to get credentials with caching async function getCredentialsWithCache(userId: string) { // Check if we have fresh cached credentials const cachedCreds = credentialsCache[userId]; const now = Date.now(); if (cachedCreds && now - cachedCreds.timestamp < CACHE_TTL) { return cachedCreds.client; } // Otherwise fetch from database const credentials = await prisma.mailCredentials.findUnique({ where: { userId } }); // Cache the result if (credentials) { credentialsCache[userId] = { client: credentialsCache[userId]?.client || new ImapFlow({ host: credentials.host, port: credentials.port, secure: true, auth: { user: credentials.email, pass: credentials.password, }, logger: false, }), timestamp: now }; } return credentialsCache[userId]?.client || null; } // Retry logic for IMAP operations async function retryOperation(operation: () => Promise, maxAttempts = 3, delay = 1000): Promise { let lastError: Error; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error as Error; console.warn(`Operation failed (attempt ${attempt}/${maxAttempts}):`, error); if (attempt < maxAttempts) { // Exponential backoff const backoffDelay = delay * Math.pow(2, attempt - 1); console.log(`Retrying in ${backoffDelay}ms...`); await new Promise(resolve => setTimeout(resolve, backoffDelay)); } } } throw lastError!; } export async function GET(request: Request) { try { const session = await getServerSession(authOptions); if (!session || !session.user?.id) { return NextResponse.json( { error: "Not authenticated" }, { status: 401 } ); } // Get mail credentials const credentials = await prisma.mailCredentials.findUnique({ where: { userId: session.user.id, }, }); if (!credentials) { return NextResponse.json( { error: "No mail credentials found" }, { status: 404 } ); } const { searchParams } = new URL(request.url); const page = parseInt(searchParams.get("page") || "1"); const perPage = parseInt(searchParams.get("perPage") || "20"); const folder = searchParams.get("folder") || "INBOX"; const searchQuery = searchParams.get("search") || ""; // Check for entry in emailCache const cacheKey = `${session.user.id}:${folder}:${page}:${perPage}:${searchQuery}`; const cachedEmails = emailListCache[cacheKey]; if (cachedEmails) { console.log(`Using cached emails for ${cacheKey}`); return NextResponse.json(cachedEmails.data); } console.log(`Cache miss for ${cacheKey}, fetching from IMAP`); // Fetch from IMAP const cacheCredKey = `credentials:${session.user.id}`; let imapClient: any = credentialsCache[cacheCredKey]?.client || null; if (!imapClient) { // Create IMAP client const connectWithRetry = async (retries = 3, delay = 1000): Promise => { try { console.log(`Attempting to connect to IMAP server (${credentials.host}:${credentials.port})...`); const client = new ImapFlow({ host: credentials.host, port: credentials.port, secure: true, auth: { user: credentials.email, pass: credentials.password, }, logger: false, }); await client.connect(); console.log("Successfully connected to IMAP server"); return client; } catch (error) { if (retries > 0) { console.log(`Connection failed, retrying... (${retries} attempts left)`); await new Promise((resolve) => setTimeout(resolve, delay)); return connectWithRetry(retries - 1, delay * 1.5); } throw error; } }; try { imapClient = await connectWithRetry(); // Cache for future use credentialsCache[cacheCredKey] = { client: imapClient, timestamp: Date.now() }; } catch (error: any) { console.error("Failed to connect to IMAP server after retries:", error); return NextResponse.json( { error: "Failed to connect to IMAP server", message: error.message }, { status: 500 } ); } } else { console.log("Using cached IMAP client connection"); } // Function to get mailboxes const getMailboxes = async () => { try { console.log("Getting list of mailboxes..."); const mailboxes = []; const list = await imapClient.list(); console.log(`Found ${list.length} mailboxes from IMAP server`); for (const mailbox of list) { mailboxes.push(mailbox.path); } console.log("Available mailboxes:", mailboxes); return mailboxes; } catch (error) { console.error("Error listing mailboxes:", error); return []; } }; // Setup paging const startIdx = (page - 1) * perPage + 1; const endIdx = page * perPage; let emails: any[] = []; let mailboxData = null; try { // Select and lock mailbox mailboxData = await imapClient.mailboxOpen(folder); console.log(`Opened mailbox ${folder}, ${mailboxData.exists} messages total`); // Calculate range based on total messages const totalMessages = mailboxData.exists; const from = Math.max(totalMessages - endIdx + 1, 1); const to = Math.max(totalMessages - startIdx + 1, 1); // Skip if no messages or invalid range if (totalMessages === 0 || from > to) { console.log("No messages in range, returning empty array"); const result = { emails: [], totalEmails: 0, page, perPage, totalPages: 0, folder, mailboxes: await getMailboxes(), }; emailListCache[cacheKey] = { data: result, timestamp: Date.now() }; return NextResponse.json(result); } console.log(`Fetching messages ${from}:${to} (page ${page}, ${perPage} per page)`); // Search if needed let messageIds: any[] = []; if (searchQuery) { console.log(`Searching for: "${searchQuery}"`); messageIds = await imapClient.search({ body: searchQuery }); // Filter to our page range messageIds = messageIds.filter(id => id >= from && id <= to); console.log(`Found ${messageIds.length} messages matching search`); } else { messageIds = Array.from( { length: to - from + 1 }, (_, i) => from + i ); } // Fetch messages with their content for (const id of messageIds) { try { const message = await imapClient.fetchOne(id, { envelope: true, flags: true, bodyStructure: true, internalDate: true, size: true, // Only fetch a preview of the body initially for faster loading bodyParts: [ { query: { type: "text", }, limit: 5000, // Limit to first 5KB of text } ] }); if (!message) continue; const { envelope, flags, bodyStructure, internalDate, size, bodyParts } = message; // Extract content from the body parts for a preview let preview = ''; if (bodyParts && bodyParts.length > 0) { const textPart = bodyParts.find((part: any) => part.type === 'text/plain'); const htmlPart = bodyParts.find((part: any) => part.type === 'text/html'); // Prefer text for preview as it's smaller and faster to process 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) + '...'; } } // Convert attachments to our format const attachments: Array<{ contentId?: string; filename: string; contentType: string; size: number; path: string; }> = []; 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, 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, // Just include the preview instead of the full content initially preview, // Store the fetched state to know we only have preview contentFetched: false }); } catch (messageError) { console.error(`Error fetching message ${id}:`, messageError); } } // Sort by date, newest first emails.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); const result = { emails, totalEmails: totalMessages, page, perPage, totalPages: Math.ceil(totalMessages / perPage), folder, mailboxes: await getMailboxes(), }; // Cache the result emailListCache[cacheKey] = { data: result, timestamp: Date.now() }; return NextResponse.json(result); } finally { // If we opened a mailbox, close it if (mailboxData) { await imapClient.mailboxClose(); } } } catch (error: any) { console.error("Error in GET:", error); return NextResponse.json( { error: "Internal server error", message: error.message }, { status: 500 } ); } } export async function POST(request: Request) { try { const session = await getServerSession(authOptions); if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { emailId, folderName, action } = await request.json(); if (!emailId) { return NextResponse.json({ error: 'Missing emailId parameter' }, { status: 400 }); } // Invalidate cache entries for this folder const userId = session.user.id; // If folder is specified, only invalidate that folder's cache if (folderName) { Object.keys(emailListCache).forEach(key => { if (key.includes(`${userId}:${folderName}`)) { delete emailListCache[key]; } }); } else { // Otherwise invalidate all cache entries for this user Object.keys(emailListCache).forEach(key => { if (key.startsWith(`${userId}:`)) { delete emailListCache[key]; } }); } return NextResponse.json({ success: true }); } catch (error) { console.error('Error in POST handler:', error); return NextResponse.json({ error: 'An unexpected error occurred' }, { status: 500 }); } }