import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { getImapConnection } from '@/lib/services/email-service'; import { prisma } from '@/lib/prisma'; import { getRedisClient } from '@/lib/redis'; // Cache TTL for unread counts (increased to 2 minutes for better performance) const UNREAD_COUNTS_CACHE_TTL = 120; // Key for unread counts cache const UNREAD_COUNTS_CACHE_KEY = (userId: string) => `email:unread:${userId}`; // Refresh lock key to prevent parallel refreshes const REFRESH_LOCK_KEY = (userId: string) => `email:unread-refresh:${userId}`; // Lock TTL to prevent stuck locks (30 seconds) const REFRESH_LOCK_TTL = 30; /** * API route for fetching unread counts for email folders * Optimized with proper caching, connection reuse, and background refresh */ export async function GET(request: Request) { try { // Authenticate user const session = await getServerSession(authOptions); if (!session || !session.user?.id) { return NextResponse.json( { error: "Not authenticated" }, { status: 401 } ); } const userId = session.user.id; const redis = getRedisClient(); // First try to get from cache const cachedCounts = await redis.get(UNREAD_COUNTS_CACHE_KEY(userId)); if (cachedCounts) { // Use cached results if available console.log(`[UNREAD_API] Using cached unread counts for user ${userId}`); // If the cache is about to expire, schedule a background refresh const ttl = await redis.ttl(UNREAD_COUNTS_CACHE_KEY(userId)); if (ttl < UNREAD_COUNTS_CACHE_TTL / 2) { // Only refresh if not already refreshing (use a lock) const lockAcquired = await redis.set( REFRESH_LOCK_KEY(userId), Date.now().toString(), 'EX', REFRESH_LOCK_TTL, 'NX' // Set only if key doesn't exist ); if (lockAcquired) { console.log(`[UNREAD_API] Scheduling background refresh for user ${userId}`); // Use Promise to run in background setTimeout(() => { refreshUnreadCounts(userId, redis) .catch(err => console.error(`[UNREAD_API] Background refresh error: ${err}`)) .finally(() => { // Release lock regardless of outcome redis.del(REFRESH_LOCK_KEY(userId)).catch(() => {}); }); }, 0); } } return NextResponse.json(JSON.parse(cachedCounts)); } console.log(`[UNREAD_API] Cache miss for user ${userId}, fetching unread counts`); // Try to acquire lock to prevent parallel refreshes const lockAcquired = await redis.set( REFRESH_LOCK_KEY(userId), Date.now().toString(), 'EX', REFRESH_LOCK_TTL, 'NX' // Set only if key doesn't exist ); if (!lockAcquired) { console.log(`[UNREAD_API] Another process is refreshing unread counts for ${userId}`); // Return empty counts with short cache time if we can't acquire lock // The next request will likely get cached data return NextResponse.json({ _status: 'pending_refresh' }); } try { // Fetch new counts const unreadCounts = await fetchUnreadCounts(userId); // Save to cache with longer TTL (2 minutes) await redis.set( UNREAD_COUNTS_CACHE_KEY(userId), JSON.stringify(unreadCounts), 'EX', UNREAD_COUNTS_CACHE_TTL ); return NextResponse.json(unreadCounts); } finally { // Always release lock await redis.del(REFRESH_LOCK_KEY(userId)); } } catch (error: any) { console.error("[UNREAD_API] Error fetching unread counts:", error); return NextResponse.json( { error: "Failed to fetch unread counts", message: error.message }, { status: 500 } ); } } /** * Background refresh function to update cache without blocking the API response */ async function refreshUnreadCounts(userId: string, redis: any): Promise { try { console.log(`[UNREAD_API] Background refresh started for user ${userId}`); const unreadCounts = await fetchUnreadCounts(userId); // Save to cache await redis.set( UNREAD_COUNTS_CACHE_KEY(userId), JSON.stringify(unreadCounts), 'EX', UNREAD_COUNTS_CACHE_TTL ); console.log(`[UNREAD_API] Background refresh completed for user ${userId}`); } catch (error) { console.error(`[UNREAD_API] Background refresh failed for user ${userId}:`, error); throw error; } } /** * Core function to fetch unread counts from IMAP */ async function fetchUnreadCounts(userId: string): Promise>> { // Get all accounts from the database directly const accounts = await prisma.mailCredentials.findMany({ where: { userId }, select: { id: true, email: true } }); console.log(`[UNREAD_API] Found ${accounts.length} accounts for user ${userId}`); if (accounts.length === 0) { return { default: {} }; } // Mapping to hold the unread counts const unreadCounts: Record> = {}; // For each account, get the unread counts for standard folders for (const account of accounts) { const accountId = account.id; try { // Get IMAP connection for this account console.log(`[UNREAD_API] Processing account ${accountId} (${account.email})`); const client = await getImapConnection(userId, accountId); unreadCounts[accountId] = {}; // Standard folders to check const standardFolders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk', 'Spam', 'Archive', 'Sent Items', 'Archives', 'Notes', 'Éléments supprimés']; // Get mailboxes for this account to check if folders exist const mailboxes = await client.list(); const availableFolders = mailboxes.map(mb => mb.path); // Check each standard folder if it exists for (const folder of standardFolders) { // Skip if folder doesn't exist in this account if (!availableFolders.includes(folder) && !availableFolders.some(f => f.toLowerCase() === folder.toLowerCase())) { continue; } try { // Check folder status without opening it (more efficient) const status = await client.status(folder, { unseen: true }); if (status && typeof status.unseen === 'number') { // Store the unread count unreadCounts[accountId][folder] = status.unseen; // Also store with prefixed version for consistency unreadCounts[accountId][`${accountId}:${folder}`] = status.unseen; console.log(`[UNREAD_API] Account ${accountId}, folder ${folder}: ${status.unseen} unread`); } } catch (folderError) { console.error(`[UNREAD_API] Error getting unread count for ${accountId}:${folder}:`, folderError); // Continue to next folder even if this one fails } } // Don't close the connection - let the connection pool handle it } catch (accountError) { console.error(`[UNREAD_API] Error processing account ${accountId}:`, accountError); } } return unreadCounts; } /** * Helper to get all account IDs for a user */ async function getUserAccountIds(userId: string): Promise { try { // Get credentials for all accounts from the email service // This is a simplified version - you should replace this with your actual logic // to retrieve the user's accounts // First try the default account const defaultClient = await getImapConnection(userId, 'default'); const accounts = ['default']; try { // Try to get other accounts if they exist // This is just a placeholder - implement your actual account retrieval logic // Close the default connection await defaultClient.logout(); } catch (error) { console.error('[UNREAD_API] Error getting additional accounts:', error); } return accounts; } catch (error) { console.error('[UNREAD_API] Error getting account IDs:', error); return ['default']; // Return at least the default account } }