240 lines
8.2 KiB
TypeScript
240 lines
8.2 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { getServerSession } from 'next-auth';
|
|
import { authOptions } from "@/app/api/auth/options";
|
|
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<void> {
|
|
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<Record<string, Record<string, number>>> {
|
|
// 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<string, Record<string, number>> = {};
|
|
|
|
// 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<string[]> {
|
|
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
|
|
}
|
|
}
|