diff --git a/app/api/courrier/[id]/mark-read/route.ts b/app/api/courrier/[id]/mark-read/route.ts index 8565501b..87cf95e8 100644 --- a/app/api/courrier/[id]/mark-read/route.ts +++ b/app/api/courrier/[id]/mark-read/route.ts @@ -45,15 +45,24 @@ export async function POST( const { folder = 'INBOX', accountId, isRead = true } = await request.json(); + // Extract account ID from folder name if present and none was explicitly provided + const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; + + // Use the most specific account ID available + const effectiveAccountId = folderAccountId || accountId || 'default'; + + // Normalize folder name by removing account prefix if present + const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; + // Log operation details for debugging - console.log(`Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${folder}${accountId ? ` for account ${accountId}` : ''}`); + console.log(`Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${normalizedFolder} for account ${effectiveAccountId}`); const success = await markEmailReadStatus( session.user.id, emailId, isRead, - folder, - accountId + normalizedFolder, + effectiveAccountId ); if (!success) { diff --git a/app/api/courrier/route.ts b/app/api/courrier/route.ts index 6f20901c..57c605b7 100644 --- a/app/api/courrier/route.ts +++ b/app/api/courrier/route.ts @@ -37,31 +37,42 @@ export async function GET(request: Request) { const searchQuery = searchParams.get("search") || ""; const accountId = searchParams.get("accountId") || ""; + // Extract account ID from folder name if present and none was explicitly provided + const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; + + // Use the most specific account ID available + const effectiveAccountId = folderAccountId || accountId || 'default'; + + // Normalize folder name by removing account prefix if present + const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; + + // Log the request details for debugging + console.log(`Email request: user=${session.user.id}, folder=${normalizedFolder}, account=${effectiveAccountId}, page=${page}`); + // Try to get from Redis cache first, but only if it's not a search query if (!searchQuery) { - const cacheKey = accountId ? `${session.user.id}:${accountId}:${folder}` : `${session.user.id}:${folder}`; const cachedEmails = await getCachedEmailList( session.user.id, - accountId || 'default', - folder, + effectiveAccountId, + normalizedFolder, page, perPage ); if (cachedEmails) { - console.log(`Using Redis cached emails for ${cacheKey}:${page}:${perPage}`); + console.log(`Using Redis cached emails for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}`); return NextResponse.json(cachedEmails); } } - console.log(`Redis cache miss for ${session.user.id}:${folder}:${page}:${perPage}, fetching emails from IMAP`); + console.log(`Redis cache miss for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}, fetching emails from IMAP`); - // Use the email service to fetch emails, passing the accountId if provided + // Use the email service to fetch emails with the normalized folder and effective account ID const emailsResult = await getEmails( session.user.id, - folder, + normalizedFolder, page, perPage, - accountId || undefined + effectiveAccountId ); // The result is already cached in the getEmails function diff --git a/hooks/use-courrier.ts b/hooks/use-courrier.ts index 34df81e7..df8cda85 100644 --- a/hooks/use-courrier.ts +++ b/hooks/use-courrier.ts @@ -266,11 +266,22 @@ export const useCourrier = () => { const changeFolder = useCallback(async (folder: string, accountId?: string) => { console.log(`Changing folder to ${folder} for account ${accountId || 'default'}`); try { + // Extract account ID from folder name if present and none was explicitly provided + const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; + + // Use the most specific account ID available + const effectiveAccountId = folderAccountId || accountId || 'default'; + + // Normalize folder name by removing account prefix if present + const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; + + console.log(`Folder change normalized: ${normalizedFolder}, account: ${effectiveAccountId}`); + // Reset selected email setSelectedEmail(null); setSelectedEmailIds([]); - // Record the new folder + // Record the new folder (preserving account prefix if present) setCurrentFolder(folder); // Reset search query when changing folders @@ -289,8 +300,8 @@ export const useCourrier = () => { // This helps prevent race conditions when multiple folders are clicked quickly await new Promise(resolve => setTimeout(resolve, 100)); - // Call loadEmails with correct boolean parameter type - await loadEmails(false, accountId); + // Call loadEmails with correct boolean parameter type and account ID + await loadEmails(false, effectiveAccountId); } catch (error) { console.error(`Error changing to folder ${folder}:`, error); setError(error instanceof Error ? error.message : 'Unknown error'); @@ -323,11 +334,27 @@ export const useCourrier = () => { // Fetch a single email's content const fetchEmailContent = useCallback(async (emailId: string, accountId?: string, folderOverride?: string) => { try { + // Use the provided folder or current folder const folderToUse = folderOverride || currentFolder; + + // Extract account ID from folder name if present and none was explicitly provided + const folderAccountId = folderToUse.includes(':') ? folderToUse.split(':')[0] : accountId; + + // Use the most specific account ID available + const effectiveAccountId = folderAccountId || accountId || 'default'; + + // Normalize folder name by removing account prefix if present + const normalizedFolder = folderToUse.includes(':') ? folderToUse.split(':')[1] : folderToUse; + + console.log(`Fetching email content for ID ${emailId} from folder ${normalizedFolder}, account: ${effectiveAccountId}`); + const query = new URLSearchParams({ - folder: folderToUse, + folder: normalizedFolder, }); - if (accountId) query.set('accountId', accountId); + + // Always include account ID in query params + query.set('accountId', effectiveAccountId); + const response = await fetch(`/api/courrier/${emailId}?${query.toString()}`); if (!response.ok) { throw new Error(`Failed to fetch email content: ${response.status}`); @@ -350,7 +377,14 @@ export const useCourrier = () => { } // Get the accountId from the email - const emailAccountId = emailToMark.accountId; + const emailAccountId = emailToMark.accountId || 'default'; + + // Normalize folder name by removing account prefix if present + const normalizedFolder = emailToMark.folder.includes(':') + ? emailToMark.folder.split(':')[1] + : emailToMark.folder; + + console.log(`Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${normalizedFolder}, account: ${emailAccountId}`); const response = await fetch(`/api/courrier/${emailId}/mark-read`, { method: 'POST', @@ -359,7 +393,7 @@ export const useCourrier = () => { }, body: JSON.stringify({ isRead, - folder: currentFolder, + folder: normalizedFolder, accountId: emailAccountId }) }); @@ -383,20 +417,39 @@ export const useCourrier = () => { console.error('Error marking email as read:', error); return false; } - }, [emails, selectedEmail, currentFolder]); + }, [emails, selectedEmail]); // Select an email to view const handleEmailSelect = useCallback(async (emailId: string, accountId: string, folderOverride: string) => { + console.log(`Selecting email ${emailId} from account ${accountId} in folder ${folderOverride}`); + setIsLoading(true); try { + // Normalize account ID if not provided + const effectiveAccountId = accountId || 'default'; + + // Normalize folder name by removing account prefix if present + const normalizedFolder = folderOverride.includes(':') ? folderOverride.split(':')[1] : folderOverride; + // Find the email in the current list - const email = emails.find(e => e.id === emailId && e.accountId === accountId && e.folder === folderOverride); + const email = emails.find(e => + e.id === emailId && + (e.accountId === effectiveAccountId) && + (e.folder === normalizedFolder || e.folder === folderOverride) + ); + if (!email) { - throw new Error('Email not found'); + console.log(`Email ${emailId} not found in current list. Fetching from API.`); + const fullEmail = await fetchEmailContent(emailId, effectiveAccountId, normalizedFolder); + setSelectedEmail(fullEmail); + return; } + // If content is not fetched, get the full content if (!email.contentFetched) { - const fullEmail = await fetchEmailContent(emailId, accountId, folderOverride); + console.log(`Fetching content for email ${emailId}`); + const fullEmail = await fetchEmailContent(emailId, effectiveAccountId, normalizedFolder); + // Merge the full content with the email const updatedEmail = { ...email, @@ -404,12 +457,14 @@ export const useCourrier = () => { attachments: fullEmail.attachments, contentFetched: true }; + // Update the email in the list setEmails(emails.map(e => e.id === emailId ? updatedEmail : e)); setSelectedEmail(updatedEmail); } else { setSelectedEmail(email); } + // Mark the email as read if it's not already if (!email.flags.seen) { markEmailAsRead(emailId, true); diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 2bdbf2b5..640214a4 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -195,7 +195,7 @@ export async function getUserEmailCredentials(userId: string, accountId?: string secure: mailCredentials.secure, smtp_host: mailCredentials.smtp_host || undefined, smtp_port: mailCredentials.smtp_port || undefined, - smtp_secure: mailCredentials.smtp_secure || false, + smtp_secure: mailCredentials.smtp_secure ?? false, display_name: mailCredentials.display_name || undefined, color: mailCredentials.color || undefined }; @@ -282,26 +282,33 @@ export async function getEmails( let client: ImapFlow | undefined; try { - // Extract the actual folder name (remove account prefix if present) - const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder; - console.log(`Fetching emails for folder: ${folder} (actual: ${actualFolder})`); + // Extract account ID from folder name if present and none was explicitly provided + const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; + + // Use the most specific account ID available + const effectiveAccountId = folderAccountId || accountId || 'default'; + + // Normalize folder name by removing account prefix if present + const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; + + console.log(`[getEmails] Processing request for folder: ${folder}, normalized to ${normalizedFolder}, account: ${effectiveAccountId}`); - // Get IMAP connection - client = await getImapConnection(userId, accountId); + // Get IMAP connection using the effective account ID + client = await getImapConnection(userId, effectiveAccountId); if (!client) { throw new Error('Failed to establish IMAP connection'); } - // Open mailbox with the actual folder name - await client.mailboxOpen(actualFolder); + // Open mailbox with the normalized folder name + await client.mailboxOpen(normalizedFolder); const mailbox = client.mailbox; if (!mailbox || typeof mailbox === 'boolean') { - throw new Error(`Failed to open mailbox: ${actualFolder}`); + throw new Error(`Failed to open mailbox: ${normalizedFolder}`); } // Get total messages const total = mailbox.exists || 0; - console.log(`Total messages in ${actualFolder}: ${total}`); + console.log(`Total messages in ${normalizedFolder} for account ${effectiveAccountId}: ${total}`); // If no messages, return empty result if (total === 0) { @@ -311,7 +318,7 @@ export async function getEmails( page, perPage, totalPages: 0, - folder: actualFolder, + folder: normalizedFolder, mailboxes: [] }; } @@ -319,7 +326,7 @@ export async function getEmails( // Calculate message range for pagination const start = Math.max(1, total - (page * perPage) + 1); const end = Math.max(1, total - ((page - 1) * perPage)); - console.log(`Fetching messages ${start}:${end} from ${actualFolder}`); + console.log(`Fetching messages ${start}:${end} from ${normalizedFolder} for account ${effectiveAccountId}`); // Fetch messages const messages = await client.fetch(`${start}:${end}`, { @@ -351,9 +358,9 @@ export async function getEmails( }, size: message.size || 0, hasAttachments: message.bodyStructure?.childNodes?.some(node => node.disposition === 'attachment') || false, - folder: actualFolder, + folder: normalizedFolder, contentFetched: false, - accountId: accountId || 'default', + accountId: effectiveAccountId, content: { text: '', html: '' @@ -362,18 +369,16 @@ export async function getEmails( emails.push(email); } - // Cache the result if we have an accountId - if (accountId) { - await cacheEmailList(userId, accountId, actualFolder, page, perPage, { - emails, - totalEmails: total, - page, - perPage, - totalPages: Math.ceil(total / perPage), - folder: actualFolder, - mailboxes: [] - }); - } + // Cache the result with the effective account ID + await cacheEmailList(userId, effectiveAccountId, normalizedFolder, page, perPage, { + emails, + totalEmails: total, + page, + perPage, + totalPages: Math.ceil(total / perPage), + folder: normalizedFolder, + mailboxes: [] + }); return { emails, @@ -381,7 +386,7 @@ export async function getEmails( page, perPage, totalPages: Math.ceil(total / perPage), - folder: actualFolder, + folder: normalizedFolder, mailboxes: [] }; } catch (error) { @@ -423,55 +428,60 @@ export async function getEmailContent( throw new Error('Email ID must be a number'); } - // 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; + const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; - // Use normalized folder name for cache key - const cacheKey = effectiveAccountId || 'default'; - const cachedEmail = await getCachedEmailContent(userId, cacheKey, emailId); + // Use the most specific account ID available + const effectiveAccountId = folderAccountId || accountId || 'default'; + + // Normalize folder name by removing account prefix if present + const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; + + console.log(`[getEmailContent] Fetching email ${emailId} from folder ${normalizedFolder}, account ${effectiveAccountId}`); + + // Use normalized folder name and effective account ID for cache key + const cachedEmail = await getCachedEmailContent(userId, effectiveAccountId, emailId); if (cachedEmail) { - console.log(`Using cached email content for ${userId}:${cacheKey}:${emailId}`); + console.log(`Using cached email content for ${userId}:${effectiveAccountId}:${emailId}`); return cachedEmail; } - console.log(`Cache miss for email content ${userId}:${cacheKey}:${emailId}, fetching from IMAP`); + console.log(`Cache miss for email content ${userId}:${effectiveAccountId}:${emailId}, fetching from IMAP`); const client = await getImapConnection(userId, effectiveAccountId); try { // Log connection details with account context - console.log(`[DEBUG] Fetching email ${emailId} from folder ${actualFolder} for account ${effectiveAccountId || 'default'}`); + console.log(`[DEBUG] Fetching email ${emailId} from folder ${normalizedFolder} for account ${effectiveAccountId}`); // Open mailbox with error handling - const mailbox = await client.mailboxOpen(actualFolder); + const mailbox = await client.mailboxOpen(normalizedFolder); if (!mailbox || typeof mailbox === 'boolean') { - throw new Error(`Failed to open mailbox: ${actualFolder} for account ${effectiveAccountId || 'default'}`); + throw new Error(`Failed to open mailbox: ${normalizedFolder} for account ${effectiveAccountId}`); } // Log mailbox status with account context - console.log(`[DEBUG] Mailbox ${actualFolder} opened for account ${effectiveAccountId || 'default'}, total messages: ${mailbox.exists}`); + console.log(`[DEBUG] Mailbox ${normalizedFolder} opened for account ${effectiveAccountId}, 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 ${effectiveAccountId || 'default'}`); + console.log(`[DEBUG] Mailbox UIDVALIDITY: ${uidValidity}, UIDNEXT: ${uidNext} for account ${effectiveAccountId}`); // 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 ${effectiveAccountId || 'default'}`); + throw new Error(`Email ID ${numericId} is greater than or equal to the highest UID in mailbox (${uidNext}) for account ${effectiveAccountId}`); } // 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 ${effectiveAccountId || 'default'}`); + throw new Error(`Email with UID ${numericId} not found in folder ${normalizedFolder} for account ${effectiveAccountId}`); } const sequenceNumber = searchResult[0]; - console.log(`[DEBUG] Found sequence number ${sequenceNumber} for UID ${numericId} in account ${effectiveAccountId || 'default'}`); + console.log(`[DEBUG] Found sequence number ${sequenceNumber} for UID ${numericId} in account ${effectiveAccountId}`); // Now fetch using the sequence number const message = await client.fetchOne(sequenceNumber.toString(), { @@ -482,7 +492,7 @@ export async function getEmailContent( }); if (!message) { - throw new Error(`Email not found with sequence number ${sequenceNumber} in folder ${actualFolder} for account ${effectiveAccountId || 'default'}`); + throw new Error(`Email not found with sequence number ${sequenceNumber} in folder ${normalizedFolder} for account ${effectiveAccountId}`); } const { source, envelope, flags, size } = message; @@ -537,21 +547,21 @@ export async function getEmailContent( text: parsedEmail.text || '', html: rawHtml || '' }, - folder: actualFolder, + folder: normalizedFolder, contentFetched: true, size: size || 0, - accountId: effectiveAccountId || 'default' + accountId: effectiveAccountId }; - // Cache the email content with account-specific key - await cacheEmailContent(userId, cacheKey, emailId, email); + // Cache the email content with effective account ID + await cacheEmailContent(userId, effectiveAccountId, emailId, email); return email; } catch (error) { console.error('[ERROR] Email fetch failed:', { userId, emailId, - folder: actualFolder, + folder: normalizedFolder, accountId: effectiveAccountId, error: error instanceof Error ? error.message : 'Unknown error', details: error instanceof Error ? error.stack : undefined @@ -576,15 +586,21 @@ export async function markEmailReadStatus( folder: string = 'INBOX', accountId?: string ): Promise { - // 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 folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; + + // Use the most specific account ID available + const effectiveAccountId = folderAccountId || accountId || 'default'; + + // Normalize folder name by removing account prefix if present + const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder; + + console.log(`[markEmailReadStatus] Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${normalizedFolder}, account ${effectiveAccountId}`); const client = await getImapConnection(userId, effectiveAccountId); try { - await client.mailboxOpen(actualFolder); + await client.mailboxOpen(normalizedFolder); if (isRead) { await client.messageFlagsAdd(emailId, ['\\Seen']); @@ -592,18 +608,15 @@ 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, cacheKey, emailId); + await invalidateEmailContentCache(userId, effectiveAccountId, emailId); // Also invalidate folder cache because unread counts may have changed - await invalidateFolderCache(userId, cacheKey, actualFolder); + await invalidateFolderCache(userId, effectiveAccountId, normalizedFolder); return true; } catch (error) { - console.error(`Error marking email ${emailId} as ${isRead ? 'read' : 'unread'}:`, error); + console.error(`Error marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${normalizedFolder}, account ${effectiveAccountId}:`, error); return false; } finally { try {