diff --git a/app/api/courrier/route.ts b/app/api/courrier/route.ts index e51b3d3e..342fb7c2 100644 --- a/app/api/courrier/route.ts +++ b/app/api/courrier/route.ts @@ -39,9 +39,10 @@ export async function GET(request: Request) { const folder = searchParams.get("folder") || "INBOX"; const searchQuery = searchParams.get("search") || ""; const accountId = searchParams.get("accountId") || ""; + const checkOnly = searchParams.get("checkOnly") === "true"; // CRITICAL FIX: Log exact parameters received by the API - console.log(`[API] Received request with: folder=${folder}, accountId=${accountId}, page=${page}`); + console.log(`[API] Received request with: folder=${folder}, accountId=${accountId}, page=${page}, checkOnly=${checkOnly}`); // CRITICAL FIX: More robust parameter normalization // 1. If folder contains an account prefix, extract it but DO NOT use it @@ -61,8 +62,8 @@ export async function GET(request: Request) { // CRITICAL FIX: Enhanced logging for parameter resolution console.log(`[API] Using normalized parameters: folder=${normalizedFolder}, accountId=${effectiveAccountId}`); - // Try to get from Redis cache first, but only if it's not a search query - if (!searchQuery) { + // Try to get from Redis cache first, but only if it's not a search query and not checkOnly + if (!searchQuery && !checkOnly) { // CRITICAL FIX: Use consistent cache key format with the correct account ID console.log(`[API] Checking Redis cache for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}`); const cachedEmails = await getCachedEmailList( @@ -87,13 +88,14 @@ export async function GET(request: Request) { normalizedFolder, // folder (without prefix) page, // page perPage, // perPage - effectiveAccountId // accountId + effectiveAccountId, // accountId + checkOnly // checkOnly flag - only check for new emails without loading full content ); // CRITICAL FIX: Log when emails are returned from IMAP console.log(`[API] Successfully fetched ${emailsResult.emails.length} emails from IMAP for account ${effectiveAccountId}`); - // The result is already cached in the getEmails function + // The result is already cached in the getEmails function (if not checkOnly) return NextResponse.json(emailsResult); } catch (error: any) { console.error("[API] Error fetching emails:", error); diff --git a/hooks/use-email-state.ts b/hooks/use-email-state.ts index fa08e11c..a3cd438b 100644 --- a/hooks/use-email-state.ts +++ b/hooks/use-email-state.ts @@ -964,6 +964,68 @@ export const useEmailState = () => { } }, [dispatch, session?.user, state.isLoadingUnreadCounts, logEmailOp]); + // Function to check for new emails without disrupting the user + const checkForNewEmails = useCallback(async () => { + if (!session?.user?.id || !state.selectedAccount) return; + + // Don't check if already loading emails + if (state.isLoading) return; + + try { + // Get normalized parameters using helper function + const accountId = state.selectedAccount ? state.selectedAccount.id : undefined; + const { normalizedFolder, effectiveAccountId, prefixedFolder } = + normalizeFolderAndAccount(state.currentFolder, accountId); + + logEmailOp('CHECK_NEW_EMAILS', `Checking for new emails in ${prefixedFolder}`); + + // Quietly check for new emails with a special parameter + const queryParams = new URLSearchParams({ + folder: normalizedFolder, + page: '1', + perPage: '10', // Just get the top 10 to check if any are new + accountId: effectiveAccountId, + checkOnly: 'true' // Special parameter to indicate this is just a check + }); + + const response = await fetch(`/api/courrier/emails?${queryParams.toString()}`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + cache: 'no-cache' + }); + + if (!response.ok) { + throw new Error(`Failed to check for new emails: ${response.status}`); + } + + const data = await response.json(); + + // If we have emails and the newest one is different from our current newest + if (data.emails && data.emails.length > 0 && state.emails.length > 0) { + const newestEmailId = data.emails[0].id; + const currentNewestEmailId = state.emails[0].id; + + if (newestEmailId !== currentNewestEmailId) { + logEmailOp('NEW_EMAILS', `Found new emails, newest ID: ${newestEmailId}`); + + // Show a toast notification + toast({ + title: "New emails", + description: "You have new emails in your inbox", + duration: 5000 + }); + + // Refresh the current view to show the new emails + loadEmails(state.page, state.perPage, false); + } else { + logEmailOp('CHECK_NEW_EMAILS', 'No new emails found'); + } + } + } catch (error) { + console.error('Error checking for new emails:', error); + } + }, [session?.user?.id, state.selectedAccount, state.currentFolder, state.isLoading, state.emails, state.page, state.perPage, toast, loadEmails, logEmailOp]); + // Calculate and update unread counts const updateUnreadCounts = useCallback(() => { // Skip if no emails or accounts @@ -1023,6 +1085,23 @@ export const useEmailState = () => { // Deliberately exclude unreadCountMap to prevent infinite loops }, [state.emails, updateUnreadCounts]); + // Set up periodic check for new emails + useEffect(() => { + if (!state.emails || state.emails.length === 0) return; + + // Set up a periodic check for new emails at the same interval as unread counts + const checkNewEmailsId = setInterval(() => { + if (document.visibilityState === 'visible') { + checkForNewEmails(); + } + }, 60000); // 1 minute - same as unread count refresh + + // Cleanup interval on unmount or state change + return () => { + clearInterval(checkNewEmailsId); + }; + }, [state.emails, checkForNewEmails]); + // Tracking when an email is viewed to optimize unread count refreshes const lastViewedEmailRef = useRef(null); const fetchFailuresRef = useRef(0); @@ -1076,6 +1155,7 @@ export const useEmailState = () => { selectAccount, handleLoadMore, fetchUnreadCounts, - viewEmail + viewEmail, + checkForNewEmails }; }; \ No newline at end of file diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 94fd30ab..8ce2fc66 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -28,6 +28,7 @@ export interface EmailListResult { totalPages: number; folder: string; mailboxes: string[]; + newestEmailId: number; } // Connection pool to reuse IMAP clients @@ -482,10 +483,11 @@ export async function getEmails( folder: string, page: number = 1, perPage: number = 20, - accountId?: string + accountId?: string, + checkOnly: boolean = false ): Promise { // Normalize folder name and handle account ID - console.log(`[getEmails] Processing request for folder: ${folder}, normalized to ${folder}, account: ${accountId || 'default'}`); + console.log(`[getEmails] Processing request for folder: ${folder}, normalized to ${folder}, account: ${accountId || 'default'}, checkOnly: ${checkOnly}`); try { // The getImapConnection function already handles 'default' accountId by finding the first available account @@ -517,21 +519,57 @@ export async function getEmails( perPage, totalPages: 0, folder, - mailboxes + mailboxes, + newestEmailId: 0 }; - await cacheEmailList( - userId, - resolvedAccountId, // Use the guaranteed string account ID - folder, - page, - perPage, - emptyResult - ); + // Only cache if not in checkOnly mode + if (!checkOnly) { + await cacheEmailList( + userId, + resolvedAccountId, // Use the guaranteed string account ID + folder, + page, + perPage, + emptyResult + ); + } return emptyResult; } + // If checkOnly mode, we just fetch the most recent email's ID to compare + if (checkOnly) { + console.log(`[getEmails] checkOnly mode: fetching only the most recent email ID`); + + // Get the most recent message (highest sequence number) + const lastMessageSequence = totalEmails.toString(); + console.log(`[getEmails] Fetching latest message with sequence: ${lastMessageSequence}`); + + const messages = await client.fetch(lastMessageSequence, { + uid: true + }); + + let newestEmailId = 0; + for await (const message of messages) { + newestEmailId = message.uid; + } + + console.log(`[getEmails] Latest email UID: ${newestEmailId}`); + + // Return minimal result with just the newest email ID + return { + emails: [], + totalEmails, + page, + perPage, + totalPages, + folder, + mailboxes, + newestEmailId + }; + } + // Calculate message range for pagination const start = Math.max(1, totalEmails - (page * perPage) + 1); const end = Math.max(1, totalEmails - ((page - 1) * perPage)); @@ -541,11 +579,19 @@ export async function getEmails( const messages = await client.fetch(`${start}:${end}`, { envelope: true, flags: true, - bodyStructure: true + bodyStructure: true, + uid: true }); const emails: EmailMessage[] = []; + let newestEmailId = 0; + for await (const message of messages) { + // Track the newest email ID (highest UID) + if (message.uid > newestEmailId) { + newestEmailId = message.uid; + } + const email: EmailMessage = { id: message.uid.toString(), from: message.envelope.from?.map(addr => ({ @@ -578,33 +624,31 @@ export async function getEmails( 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 { + // Prepare the result + const result = { emails, - totalEmails: totalEmails, + totalEmails, page, perPage, totalPages: Math.ceil(totalEmails / perPage), - folder: folder, - mailboxes: mailboxes + folder, + mailboxes, + newestEmailId }; + + // Cache the result with the effective account ID (only if not in checkOnly mode) + if (!checkOnly) { + await cacheEmailList( + userId, + resolvedAccountId, // Use the guaranteed string account ID + folder, + page, + perPage, + result + ); + } + + return result; } catch (error) { console.error('Error fetching emails:', error); throw error;