diff --git a/app/api/courrier/[id]/mark-read/route.ts b/app/api/courrier/[id]/mark-read/route.ts index 7ca438c5..8565501b 100644 --- a/app/api/courrier/[id]/mark-read/route.ts +++ b/app/api/courrier/[id]/mark-read/route.ts @@ -43,13 +43,17 @@ export async function POST( return NextResponse.json({ error: 'Email ID is required' }, { status: 400 }); } - const { folder = 'INBOX', accountId } = await request.json(); + const { folder = 'INBOX', accountId, isRead = true } = await request.json(); + + // Log operation details for debugging + console.log(`Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${folder}${accountId ? ` for account ${accountId}` : ''}`); const success = await markEmailReadStatus( session.user.id, emailId, - true, - folder + isRead, + folder, + accountId ); if (!success) { diff --git a/hooks/use-courrier.ts b/hooks/use-courrier.ts index 2dc2a818..87485754 100644 --- a/hooks/use-courrier.ts +++ b/hooks/use-courrier.ts @@ -334,6 +334,51 @@ export const useCourrier = () => { } }, [currentFolder]); + // Mark an email as read/unread + const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean) => { + try { + // Find the email to get its accountId + const emailToMark = emails.find(e => e.id === emailId); + if (!emailToMark) { + throw new Error('Email not found'); + } + + // Get the accountId from the email + const emailAccountId = emailToMark.accountId; + + const response = await fetch(`/api/courrier/${emailId}/mark-read`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + isRead, + folder: currentFolder, + accountId: emailAccountId + }) + }); + + if (!response.ok) { + throw new Error('Failed to mark email as read'); + } + + // Update the email in the list + setEmails(emails.map(email => + email.id === emailId ? { ...email, flags: { ...email.flags, seen: isRead } } : email + )); + + // If the selected email is the one being marked, update it too + if (selectedEmail && selectedEmail.id === emailId) { + setSelectedEmail({ ...selectedEmail, flags: { ...selectedEmail.flags, seen: isRead } }); + } + + return true; + } catch (error) { + console.error('Error marking email as read:', error); + return false; + } + }, [emails, selectedEmail, currentFolder]); + // Select an email to view const handleEmailSelect = useCallback(async (emailId: string, accountId: string, folderOverride: string) => { setIsLoading(true); @@ -373,42 +418,7 @@ export const useCourrier = () => { } finally { setIsLoading(false); } - }, [emails, fetchEmailContent, toast]); - - // Mark an email as read/unread - const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean) => { - try { - const response = await fetch(`/api/courrier/${emailId}/mark-read`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - isRead, - folder: currentFolder - }) - }); - - if (!response.ok) { - throw new Error('Failed to mark email as read'); - } - - // Update the email in the list - setEmails(emails.map(email => - email.id === emailId ? { ...email, flags: { ...email.flags, seen: isRead } } : email - )); - - // If the selected email is the one being marked, update it too - if (selectedEmail && selectedEmail.id === emailId) { - setSelectedEmail({ ...selectedEmail, flags: { ...selectedEmail.flags, seen: isRead } }); - } - - return true; - } catch (error) { - console.error('Error marking email as read:', error); - return false; - } - }, [emails, selectedEmail, currentFolder]); + }, [emails, fetchEmailContent, markEmailAsRead, toast]); // Toggle starred status for an email const toggleStarred = useCallback(async (emailId: string) => { diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index f31dcace..2bdbf2b5 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -423,53 +423,55 @@ export async function getEmailContent( throw new Error('Email ID must be a number'); } - // Try to get from cache first, using account-specific cache key - const cacheKey = accountId ? `${accountId}:${folder}` : folder; + // Remove accountId prefix if present in folder name + const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder; + // Extract account ID from folder name if present and none was explicitly provided + const effectiveAccountId = folder.includes(':') && !accountId ? folder.split(':')[0] : accountId; + + // Use normalized folder name for cache key + const cacheKey = effectiveAccountId || 'default'; const cachedEmail = await getCachedEmailContent(userId, cacheKey, emailId); if (cachedEmail) { - console.log(`Using cached email content for ${userId}:${accountId}:${emailId}`); + console.log(`Using cached email content for ${userId}:${cacheKey}:${emailId}`); return cachedEmail; } - console.log(`Cache miss for email content ${userId}:${accountId}:${emailId}, fetching from IMAP`); + console.log(`Cache miss for email content ${userId}:${cacheKey}:${emailId}, fetching from IMAP`); - const client = await getImapConnection(userId, accountId); + const client = await getImapConnection(userId, effectiveAccountId); try { - // Remove accountId prefix if present in folder name - const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder; - // Log connection details with account context - console.log(`[DEBUG] Fetching email ${emailId} from folder ${actualFolder} for account ${accountId || 'default'}`); + console.log(`[DEBUG] Fetching email ${emailId} from folder ${actualFolder} for account ${effectiveAccountId || 'default'}`); // Open mailbox with error handling const mailbox = await client.mailboxOpen(actualFolder); if (!mailbox || typeof mailbox === 'boolean') { - throw new Error(`Failed to open mailbox: ${actualFolder} for account ${accountId || 'default'}`); + throw new Error(`Failed to open mailbox: ${actualFolder} for account ${effectiveAccountId || 'default'}`); } // Log mailbox status with account context - console.log(`[DEBUG] Mailbox ${actualFolder} opened for account ${accountId || 'default'}, total messages: ${mailbox.exists}`); + console.log(`[DEBUG] Mailbox ${actualFolder} opened for account ${effectiveAccountId || 'default'}, total messages: ${mailbox.exists}`); // Get the UIDVALIDITY and UIDNEXT values const uidValidity = mailbox.uidValidity; const uidNext = mailbox.uidNext; - console.log(`[DEBUG] Mailbox UIDVALIDITY: ${uidValidity}, UIDNEXT: ${uidNext} for account ${accountId || 'default'}`); + console.log(`[DEBUG] Mailbox UIDVALIDITY: ${uidValidity}, UIDNEXT: ${uidNext} for account ${effectiveAccountId || 'default'}`); // Validate UID exists in mailbox if (numericId >= uidNext) { - throw new Error(`Email ID ${numericId} is greater than or equal to the highest UID in mailbox (${uidNext}) for account ${accountId || 'default'}`); + throw new Error(`Email ID ${numericId} is greater than or equal to the highest UID in mailbox (${uidNext}) for account ${effectiveAccountId || 'default'}`); } // First, try to get the sequence number for this UID const searchResult = await client.search({ uid: numericId.toString() }); if (!searchResult || searchResult.length === 0) { - throw new Error(`Email with UID ${numericId} not found in folder ${actualFolder} for account ${accountId || 'default'}`); + throw new Error(`Email with UID ${numericId} not found in folder ${actualFolder} for account ${effectiveAccountId || 'default'}`); } const sequenceNumber = searchResult[0]; - console.log(`[DEBUG] Found sequence number ${sequenceNumber} for UID ${numericId} in account ${accountId || 'default'}`); + console.log(`[DEBUG] Found sequence number ${sequenceNumber} for UID ${numericId} in account ${effectiveAccountId || 'default'}`); // Now fetch using the sequence number const message = await client.fetchOne(sequenceNumber.toString(), { @@ -480,7 +482,7 @@ export async function getEmailContent( }); if (!message) { - throw new Error(`Email not found with sequence number ${sequenceNumber} in folder ${actualFolder} for account ${accountId || 'default'}`); + throw new Error(`Email not found with sequence number ${sequenceNumber} in folder ${actualFolder} for account ${effectiveAccountId || 'default'}`); } const { source, envelope, flags, size } = message; @@ -538,7 +540,7 @@ export async function getEmailContent( folder: actualFolder, contentFetched: true, size: size || 0, - accountId: accountId || 'default' + accountId: effectiveAccountId || 'default' }; // Cache the email content with account-specific key @@ -549,8 +551,8 @@ export async function getEmailContent( console.error('[ERROR] Email fetch failed:', { userId, emailId, - folder, - accountId, + folder: actualFolder, + accountId: effectiveAccountId, error: error instanceof Error ? error.message : 'Unknown error', details: error instanceof Error ? error.stack : undefined }); @@ -571,12 +573,18 @@ export async function markEmailReadStatus( userId: string, emailId: string, isRead: boolean, - folder: string = 'INBOX' + folder: string = 'INBOX', + accountId?: string ): Promise { - const client = await getImapConnection(userId); + // Normalize folder name by removing account prefix if present + const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder; + // Extract account ID from folder name if present and none was explicitly provided + const effectiveAccountId = folder.includes(':') && !accountId ? folder.split(':')[0] : accountId; + + const client = await getImapConnection(userId, effectiveAccountId); try { - await client.mailboxOpen(folder); + await client.mailboxOpen(actualFolder); if (isRead) { await client.messageFlagsAdd(emailId, ['\\Seen']); @@ -584,11 +592,14 @@ export async function markEmailReadStatus( await client.messageFlagsRemove(emailId, ['\\Seen']); } + // Use normalized cacheKey for consistency + const cacheKey = effectiveAccountId || 'default'; + // Invalidate content cache since the flags changed - await invalidateEmailContentCache(userId, folder, emailId); + await invalidateEmailContentCache(userId, cacheKey, emailId); // Also invalidate folder cache because unread counts may have changed - await invalidateFolderCache(userId, folder, folder); + await invalidateFolderCache(userId, cacheKey, actualFolder); return true; } catch (error) { diff --git a/lib/services/prefetch-service.ts b/lib/services/prefetch-service.ts index 3ea4381f..3e886dbb 100644 --- a/lib/services/prefetch-service.ts +++ b/lib/services/prefetch-service.ts @@ -68,17 +68,26 @@ export async function getCachedEmailsWithTimeout( return null; } + // Normalize folder name by removing account prefix if present + // This ensures consistent cache key format regardless of how folder name is passed + const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; + + // Log the normalization for debugging + if (folder !== normalizedFolder) { + console.log(`Normalized folder name from ${folder} to ${normalizedFolder} for cache lookup`); + } + return new Promise((resolve) => { const timeoutId = setTimeout(() => { - console.log(`Cache access timeout for ${userId}:${folder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}`); + console.log(`Cache access timeout for ${userId}:${normalizedFolder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}`); resolve(null); }, timeoutMs); - getCachedEmailList(userId, accountId || 'default', folder, page, perPage) + getCachedEmailList(userId, accountId || 'default', normalizedFolder, page, perPage) .then(result => { clearTimeout(timeoutId); if (result) { - console.log(`Using cached data for ${userId}:${folder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}`); + console.log(`Using cached data for ${userId}:${normalizedFolder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}`); resolve(result); } else { resolve(null); @@ -100,9 +109,14 @@ export async function refreshEmailsInBackground( userId: string, folder: string = 'INBOX', page: number = 1, - perPage: number = 20 + perPage: number = 20, + accountId?: string ): Promise { - const prefetchKey = `refresh:${folder}:${page}`; + // Normalize folder name by removing account prefix if present + const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; + const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; + + const prefetchKey = `refresh:${normalizedFolder}:${page}:${folderAccountId || ''}`; // Skip if already in progress or in cooldown if (!shouldPrefetch(userId, prefetchKey)) { @@ -112,9 +126,9 @@ export async function refreshEmailsInBackground( // Use setTimeout to ensure this runs after current execution context setTimeout(async () => { try { - console.log(`Background refresh for ${userId}:${folder}:${page}:${perPage}`); - const freshData = await getEmails(userId, folder, page, perPage); - console.log(`Background refresh completed for ${userId}:${folder}`); + console.log(`Background refresh for ${userId}:${normalizedFolder}:${page}:${perPage}${folderAccountId ? ` for account ${folderAccountId}` : ''}`); + const freshData = await getEmails(userId, normalizedFolder, page, perPage, folderAccountId); + console.log(`Background refresh completed for ${userId}:${normalizedFolder}${folderAccountId ? ` for account ${folderAccountId}` : ''}`); } catch (error) { console.error('Background refresh error:', error); } finally { @@ -218,7 +232,11 @@ export async function prefetchFolderEmails( startPage: number = 1, accountId?: string ): Promise { - const prefetchKey = `folder:${folder}:${startPage}${accountId ? `:${accountId}` : ''}`; + // Normalize folder name by removing account prefix if present + const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; + const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; + + const prefetchKey = `folder:${normalizedFolder}:${startPage}:${folderAccountId || ''}`; // Skip if already in progress or in cooldown if (!shouldPrefetch(userId, prefetchKey)) { @@ -226,7 +244,7 @@ export async function prefetchFolderEmails( } try { - console.log(`Prefetching ${pages} pages of emails for folder ${folder} starting from page ${startPage}${accountId ? ` for account ${accountId}` : ''}`); + console.log(`Prefetching ${pages} pages of emails for folder ${normalizedFolder} starting from page ${startPage}${folderAccountId ? ` for account ${folderAccountId}` : ''}`); // Calculate the range of pages to prefetch const pagesToFetch = Array.from( @@ -239,21 +257,21 @@ export async function prefetchFolderEmails( // Fetch multiple pages in parallel await Promise.allSettled( pagesToFetch.map(page => - getEmails(userId, folder, page, 20) + getEmails(userId, normalizedFolder, page, 20, folderAccountId) .then(result => { - console.log(`Successfully prefetched and cached page ${page} of ${folder} with ${result.emails.length} emails`); + console.log(`Successfully prefetched and cached page ${page} of ${normalizedFolder} with ${result.emails.length} emails`); return result; }) .catch(err => { - console.error(`Error prefetching page ${page} of ${folder}:`, err); + console.error(`Error prefetching page ${page} of ${normalizedFolder}:`, err); return null; }) ) ); - console.log(`Completed prefetching ${pages} pages of ${folder}`); + console.log(`Completed prefetching ${pages} pages for ${normalizedFolder}`); } catch (error) { - console.error(`Error prefetching folder ${folder}:`, error); + console.error(`Error during folder prefetch:`, error); } finally { markPrefetchCompleted(userId, prefetchKey); }