import { useState, useCallback, useEffect } from 'react'; import { useSession } from 'next-auth/react'; import { useToast } from './use-toast'; import { formatEmailForReplyOrForward } from '@/lib/utils/email-utils'; import { getCachedEmailsWithTimeout, refreshEmailsInBackground } from '@/lib/services/prefetch-service'; import { EmailAddress, EmailAttachment } from '@/lib/types'; export interface Email { id: string; from: EmailAddress[]; to: EmailAddress[]; cc?: EmailAddress[]; bcc?: EmailAddress[]; subject: string; content: { text: string; html: string; }; preview?: string; date: Date; flags: { seen: boolean; answered: boolean; flagged: boolean; draft: boolean; deleted: boolean; }; size: number; hasAttachments: boolean; folder: string; contentFetched: boolean; accountId?: string; messageId?: string; attachments?: EmailAttachment[]; } export interface EmailListResult { emails: Email[]; totalEmails: number; page: number; perPage: number; totalPages: number; folder: string; mailboxes: string[]; } export interface EmailData { to: string; cc?: string; bcc?: string; subject: string; body: string; attachments?: Array<{ name: string; content: string; type: string; }>; } export type MailFolder = string; // Hook for managing email operations export const useCourrier = () => { // State for email data const [emails, setEmails] = useState([]); const [selectedEmail, setSelectedEmail] = useState(null); const [selectedEmailIds, setSelectedEmailIds] = useState([]); const [currentFolder, setCurrentFolder] = useState('INBOX'); const [mailboxes, setMailboxes] = useState([]); // State for UI const [isLoading, setIsLoading] = useState(false); const [isSending, setIsSending] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [page, setPage] = useState(1); const [perPage, setPerPage] = useState(20); const [totalEmails, setTotalEmails] = useState(0); const [totalPages, setTotalPages] = useState(0); // Auth and notifications const { data: session } = useSession(); const { toast } = useToast(); // Load emails from the server const loadEmails = useCallback(async (isLoadMore = false, accountId?: string) => { if (!session?.user?.id) return; setIsLoading(true); setError(null); try { // CRITICAL FIX: When an account ID is explicitly provided, ALWAYS prioritize it // over any account ID that might be extracted from the currentFolder let normalizedFolder = currentFolder; let effectiveAccountId: string; // Start by logging exactly what parameters we received console.log(`[loadEmails] Called with isLoadMore=${isLoadMore}, accountId=${accountId || 'undefined'}, currentFolder=${currentFolder}`); if (currentFolder.includes(':')) { const parts = currentFolder.split(':'); const folderAccountId = parts[0]; const baseFolderName = parts[1]; // CRITICAL FIX: If an account ID is explicitly provided, use it with highest priority // This ensures account switching always works correctly if (accountId) { console.log(`[loadEmails] Explicit accountId provided (${accountId}), overriding folder prefix (${folderAccountId})`); effectiveAccountId = accountId; normalizedFolder = baseFolderName; } else { // No explicit account ID, use the one from the folder effectiveAccountId = folderAccountId; normalizedFolder = baseFolderName; } } else { // Folder doesn't have a prefix normalizedFolder = currentFolder; effectiveAccountId = accountId || 'default'; } console.log(`[loadEmails] Using normalized folder: ${normalizedFolder}, effectiveAccountId: ${effectiveAccountId}`); // Construct query parameters with normalized values const queryParams = new URLSearchParams({ folder: normalizedFolder, // Use normalized folder without account prefix for API page: page.toString(), perPage: perPage.toString() }); if (searchQuery) { queryParams.set('search', searchQuery); } // Always add accountId to query params queryParams.set('accountId', effectiveAccountId); // Log the exact query parameters being used console.log(`[loadEmails] Query parameters: ${queryParams.toString()}`); // Try to get cached emails first const currentRequestPage = page; // CRITICAL FIX: Use consistent cache key format with explicit account ID const folderForCache = `${effectiveAccountId}:${normalizedFolder}`; console.log(`[loadEmails] Checking cache with key: ${folderForCache}, accountId=${effectiveAccountId}`); const cachedEmails = await getCachedEmailsWithTimeout( session.user.id, // userId: string folderForCache, // folder: string - use consistently prefixed folder for cache key currentRequestPage, // page: number perPage, // perPage: number 100, // timeoutMs: number effectiveAccountId // accountId?: string - always pass the effective account ID ); if (cachedEmails) { // Ensure cached data has emails array property if (Array.isArray(cachedEmails.emails)) { if (isLoadMore) { // When loading more, always append to the existing list setEmails(prevEmails => { // Create a Set of existing email IDs to avoid duplicates const existingIds = new Set(prevEmails.map(email => email.id)); // Filter out any duplicates before appending const newEmails = cachedEmails.emails.filter((email: Email) => !existingIds.has(email.id)); // Log pagination info console.log(`Added ${newEmails.length} cached emails from page ${currentRequestPage} to existing ${prevEmails.length} emails`); // Combine emails and sort them by date (newest first) const combinedEmails = [...prevEmails, ...newEmails]; return combinedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()); }); } else { // For initial load, replace emails console.log(`Setting ${cachedEmails.emails.length} cached emails for page ${currentRequestPage}`); // Ensure emails are sorted by date (newest first) setEmails(cachedEmails.emails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime())); } // Set pagination info from cache if available if (cachedEmails.totalEmails) setTotalEmails(cachedEmails.totalEmails); if (cachedEmails.totalPages) setTotalPages(cachedEmails.totalPages); // Update available mailboxes if provided if (cachedEmails.mailboxes && cachedEmails.mailboxes.length > 0) { setMailboxes(cachedEmails.mailboxes); } } else if (Array.isArray(cachedEmails)) { // Direct array response if (isLoadMore) { setEmails(prevEmails => { // Create a Set of existing email IDs to avoid duplicates const existingIds = new Set(prevEmails.map(email => email.id)); // Filter out any duplicates before appending const newEmails = cachedEmails.filter((email: Email) => !existingIds.has(email.id)); // Log pagination info console.log(`Added ${newEmails.length} cached emails from page ${currentRequestPage} to existing ${prevEmails.length} emails`); // Combine emails and sort them by date (newest first) const combinedEmails = [...prevEmails, ...newEmails]; return combinedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()); }); } else { // For initial load, replace emails console.log(`Setting ${cachedEmails.length} cached emails for page ${currentRequestPage}`); // Ensure emails are sorted by date (newest first) setEmails(cachedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime())); } } else { console.warn('Invalid cache format:', cachedEmails); } setIsLoading(false); // Still refresh in background for fresh data // CRITICAL FIX: Use the normalized folder and explicit account ID together for background refresh // This prevents issues with account switching where the wrong folder prefix remains console.log(`Starting background refresh with normalized folder=${normalizedFolder}, account=${effectiveAccountId}`); refreshEmailsInBackground( session.user.id, normalizedFolder, // CRITICAL FIX: Use the normalized folder WITHOUT prefix currentRequestPage, perPage, effectiveAccountId // Always pass the effective account ID explicitly ).catch(err => { console.error('Background refresh error:', err); }); return; } // Fetch emails from API console.log(`Fetching emails from API with params: ${queryParams.toString()}`); const response = await fetch(`/api/courrier?${queryParams.toString()}`); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to fetch emails'); } const data: EmailListResult = await response.json(); // CRITICAL FIX: Ensure all emails have the proper account ID and folder format for consistent lookup if (Array.isArray(data.emails)) { data.emails.forEach(email => { // If email doesn't have an accountId, set it to the effective one if (!email.accountId) { email.accountId = effectiveAccountId; } // Ensure folder has the proper prefix format if (email.folder && !email.folder.includes(':')) { email.folder = `${email.accountId}:${email.folder}`; } }); } // Update state with the fetched data if (isLoadMore) { setEmails(prev => { // Create a Set of existing email IDs to avoid duplicates const existingIds = new Set(prev.map(email => email.id)); // Filter out any duplicates before appending const newEmails = data.emails.filter((email: Email) => !existingIds.has(email.id)); // Log pagination info console.log(`Added ${newEmails.length} fetched emails from page ${currentRequestPage} to existing ${prev.length} emails`); // Combine emails and sort them by date (newest first) const combinedEmails = [...prev, ...newEmails]; return combinedEmails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime()); }); } else { // Ensure we always set an array even if API returns invalid data console.log(`Setting ${data.emails?.length || 0} fetched emails for page ${currentRequestPage}`); // Ensure emails are sorted by date (newest first) if (Array.isArray(data.emails)) { setEmails(data.emails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime())); } else { setEmails([]); } } setTotalEmails(data.totalEmails); setTotalPages(data.totalPages); // Update available mailboxes if provided if (data.mailboxes && data.mailboxes.length > 0) { setMailboxes(data.mailboxes); } // Clear selection if not loading more if (!isLoadMore) { setSelectedEmail(null); setSelectedEmailIds([]); } } catch (err) { console.error(`Error loading emails for page ${page}:`, err); // Set emails to empty array on error to prevent runtime issues if (!isLoadMore) { setEmails([]); } setError(err instanceof Error ? err.message : 'Failed to load emails'); toast({ variant: "destructive", title: "Error", description: err instanceof Error ? err.message : 'Failed to load emails' }); } finally { setIsLoading(false); } }, [currentFolder, page, perPage, searchQuery, session?.user?.id, toast]); // Change folder and load emails from that folder const changeFolder = useCallback(async (folder: string, accountId?: string) => { console.log(`[changeFolder] Called with folder=${folder}, accountId=${accountId || 'default'}`); try { // Reset selected email and selection state immediately to avoid race conditions setSelectedEmail(null); setSelectedEmailIds([]); setEmails([]); // Clear existing emails right away setIsLoading(true); // Show loading state immediately // CRITICAL FIX: Clear and precise parameter handling with detailed logging let normalizedFolder: string; let effectiveAccountId: string; // Parse input folder parameter if (folder.includes(':')) { // Folder has a prefix, extract components const parts = folder.split(':'); const folderAccountId = parts[0]; normalizedFolder = parts[1]; // CRITICAL FIX: If an explicit accountId is provided, it ALWAYS takes precedence // This is the key to fixing account switching issues if (accountId) { console.log(`[changeFolder] Explicit accountId (${accountId}) overrides folder prefix (${folderAccountId})`); effectiveAccountId = accountId; } else { console.log(`[changeFolder] Using account ID from folder prefix: ${folderAccountId}`); effectiveAccountId = folderAccountId; } } else { // Folder has no prefix normalizedFolder = folder; effectiveAccountId = accountId || 'default'; console.log(`[changeFolder] No folder prefix, using accountId=${effectiveAccountId}, folder=${normalizedFolder}`); } // CRITICAL FIX: Always create a consistently formatted folder name with the EFFECTIVE account prefix const prefixedFolder = `${effectiveAccountId}:${normalizedFolder}`; console.log(`[changeFolder] Normalized parameters: folder=${normalizedFolder}, accountId=${effectiveAccountId}, prefixedFolder=${prefixedFolder}`); // Reset search query when changing folders setSearchQuery(''); // Reset to page 1 setPage(1); // Set currentFolder state console.log(`[changeFolder] Setting currentFolder to: ${prefixedFolder}`); setCurrentFolder(prefixedFolder); // CRITICAL FIX: Create a local implementation of loadEmails that doesn't use currentFolder state // This completely avoids the race condition with React state updates const loadEmailsForFolder = async () => { try { console.log(`[changeFolder] Loading emails with fixed parameters: folder=${normalizedFolder}, accountId=${effectiveAccountId}`); // CRITICAL FIX: Use a timeout to give UI time to update but avoid state race conditions await new Promise(resolve => setTimeout(resolve, 50)); setIsLoading(true); setError(null); // Try to get cached emails first with the correct parameters // This uses our local variables, not the potentially stale state const cachedEmails = await getCachedEmailsWithTimeout( session?.user?.id || '', // userId `${effectiveAccountId}:${normalizedFolder}`, // folderForCache - use local variables 1, // page perPage, // perPage 100, // timeoutMs effectiveAccountId // accountId - use local variable ); if (cachedEmails) { // Process cached emails similar to loadEmails if (Array.isArray(cachedEmails.emails)) { // Set emails from cache using the local function console.log(`[changeFolder] Setting ${cachedEmails.emails.length} cached emails for folder ${normalizedFolder}`); setEmails(cachedEmails.emails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime())); // Set other state from cache if (cachedEmails.totalEmails) setTotalEmails(cachedEmails.totalEmails); if (cachedEmails.totalPages) setTotalPages(cachedEmails.totalPages); if (cachedEmails.mailboxes && cachedEmails.mailboxes.length > 0) { setMailboxes(cachedEmails.mailboxes); } } // Start background refresh with the correct parameters console.log(`[changeFolder] Starting background refresh with folder=${normalizedFolder}, accountId=${effectiveAccountId}`); refreshEmailsInBackground( session?.user?.id || '', normalizedFolder, // Use normalized folder name without prefix 1, // page perPage, // perPage effectiveAccountId // Use effective account ID ).catch(err => { console.error('[changeFolder] Background refresh error:', err); }); } else { // No cache hit, perform direct API call similar to loadEmails // Construct query parameters with the correct values const queryParams = new URLSearchParams({ folder: normalizedFolder, page: '1', perPage: perPage.toString(), accountId: effectiveAccountId }); console.log(`[changeFolder] Fetching emails from API with params: ${queryParams.toString()}`); const response = await fetch(`/api/courrier?${queryParams.toString()}`); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to fetch emails'); } const data = await response.json(); // Update state with fetched data if (Array.isArray(data.emails)) { setEmails(data.emails.sort((a: Email, b: Email) => new Date(b.date).getTime() - new Date(a.date).getTime())); if (data.totalEmails) setTotalEmails(data.totalEmails); if (data.totalPages) setTotalPages(data.totalPages); if (data.mailboxes && data.mailboxes.length > 0) { setMailboxes(data.mailboxes); } } else { setEmails([]); } } console.log(`[changeFolder] Finished changing to folder=${prefixedFolder}`); } catch (error) { console.error(`[changeFolder] Error loading emails:`, error); setError(error instanceof Error ? error.message : 'Error loading emails'); } finally { setIsLoading(false); } }; // Execute our local function that doesn't depend on state updates loadEmailsForFolder().catch(error => { console.error(`[changeFolder] Unhandled error:`, error); setError(error instanceof Error ? error.message : 'Unknown error'); setIsLoading(false); }); } catch (error) { console.error(`[changeFolder] Error changing to folder ${folder}:`, error); setError(error instanceof Error ? error.message : 'Unknown error'); setIsLoading(false); } }, [loadEmails, setSearchQuery, setPage, setCurrentFolder, setEmails, setSelectedEmail, setSelectedEmailIds, setIsLoading, setError]); // Load emails when page changes for pagination only useEffect(() => { // We ONLY want this to run when page changes, not when currentFolder changes // This prevents race conditions when switching folders if (session?.user?.id && page > 1) { // Log what we're doing console.log(`[PAGINATION] Loading page ${page} for folder ${currentFolder}`); // CRITICAL FIX: Extract account ID from current folder to ensure pagination uses the correct account let accountId: string | undefined; if (currentFolder && currentFolder.includes(':')) { const parts = currentFolder.split(':'); accountId = parts[0]; console.log(`[PAGINATION] Extracted account ID ${accountId} from folder ${currentFolder}`); } // Call changeFolder with explicit account ID when available changeFolder(currentFolder, accountId) .catch(err => { console.error(`[PAGINATION] Error loading more emails:`, err); }); } }, [page, session?.user?.id, changeFolder, currentFolder]); // Include currentFolder for correct account ID extraction // ADDING DEBUG LOGS to track currentFolder changes useEffect(() => { console.log(`[DEBUG] currentFolder changed to: ${currentFolder}`); }, [currentFolder]); // 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: normalizedFolder, }); // 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}`); } const data = await response.json(); return data; } catch (error) { console.error('Error fetching email content:', error); throw error; } }, [currentFolder]); // Mark an email as read/unread const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean, providedAccountId?: string) => { try { // CRITICAL FIX: If an account ID is provided, use it directly // Otherwise, find the email to get its accountId let emailAccountId = providedAccountId; let emailFolder = ''; if (!emailAccountId) { // Find the email in the current list const emailToMark = emails.find(e => e.id === emailId); if (!emailToMark) { throw new Error('Email not found'); } // Get the accountId from the email emailAccountId = emailToMark.accountId || 'default'; emailFolder = emailToMark.folder; } else { // If providedAccountId exists but we don't have folder info, // try to find the email in the list to get its folder const emailToMark = emails.find(e => e.id === emailId && e.accountId === providedAccountId); if (emailToMark) { emailFolder = emailToMark.folder; } } // Normalize folder name by removing account prefix if present let normalizedFolder = emailFolder; if (emailFolder && emailFolder.includes(':')) { normalizedFolder = emailFolder.split(':')[1]; } else if (!emailFolder) { // If folder isn't available from the email object, try to extract it from currentFolder if (currentFolder.includes(':')) { normalizedFolder = currentFolder.split(':')[1]; } else { normalizedFolder = currentFolder; } } 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', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ isRead, folder: normalizedFolder, accountId: emailAccountId }) }); if (!response.ok) { throw new Error('Failed to mark email as read'); } // Update the email in the list - only update the specific email with matching ID AND account ID setEmails(emails.map(email => (email.id === emailId && (!providedAccountId || email.accountId === providedAccountId)) ? { ...email, flags: { ...email.flags, seen: isRead } } : email )); // If the selected email is the one being marked, update it too if (selectedEmail && selectedEmail.id === emailId && (!providedAccountId || selectedEmail.accountId === providedAccountId)) { 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) => { // Enhanced logging for better debugging console.log(`[handleEmailSelect] Selecting email ${emailId} from account [${accountId}] in folder [${folderOverride}]`); // Skip processing if emailId is empty (used when deselecting emails) if (!emailId) { console.log('[handleEmailSelect] No email ID provided, clearing selection'); setSelectedEmail(null); return; } // CRITICAL FIX: Validate required parameters if (!accountId) { console.error('[handleEmailSelect] Account ID is required for email selection'); toast({ variant: "destructive", title: "Error", description: "Missing account information when selecting email" }); return; } setIsLoading(true); try { // CRITICAL FIX: Normalize folder name with detailed logging let normalizedFolder: string; let prefixedFolder: string; if (folderOverride.includes(':')) { // Extract parts if folder already has a prefix const parts = folderOverride.split(':'); const folderAccountId = parts[0]; normalizedFolder = parts[1]; console.log(`[handleEmailSelect] Folder has prefix '${folderAccountId}', normalized to ${normalizedFolder}`); // CRITICAL FIX: ALWAYS use the provided accountId, never use the one from the folder if (folderAccountId !== accountId) { console.log(`[handleEmailSelect] WARNING: Folder account ID (${folderAccountId}) doesn't match provided account ID (${accountId})`); } // Create folder name with consistent account ID prefixedFolder = `${accountId}:${normalizedFolder}`; } else { // No prefix, add one with the provided account ID normalizedFolder = folderOverride; prefixedFolder = `${accountId}:${normalizedFolder}`; console.log(`[handleEmailSelect] Folder has no prefix, created prefixed version: ${prefixedFolder}`); } console.log(`[handleEmailSelect] Looking for email with ID=${emailId}, account=${accountId}, folder=${prefixedFolder}`); // CRITICAL FIX: Find email by exact match with the provided account ID // This prevents mixing emails across accounts let email = emails.find(e => e.id === emailId && e.accountId === accountId && ( e.folder === prefixedFolder || e.folder === normalizedFolder || (e.folder?.includes(':') && e.folder.split(':')[1] === normalizedFolder) ) ); if (email) { console.log(`[handleEmailSelect] Found matching email in current list`); } else { console.log(`[handleEmailSelect] Email ${emailId} not found in current list for account ${accountId}, fetching from API`); try { // CRITICAL FIX: Always use explicit parameters for API request console.log(`[handleEmailSelect] Fetching email ${emailId} from API with folder=${normalizedFolder}, accountId=${accountId}`); const fullEmail = await fetchEmailContent(emailId, accountId, normalizedFolder); if (fullEmail) { // CRITICAL FIX: Ensure the email has proper account and folder context fullEmail.accountId = accountId; // Make sure folder has the proper prefix for consistent lookup if (fullEmail.folder && !fullEmail.folder.includes(':')) { fullEmail.folder = `${accountId}:${fullEmail.folder}`; } console.log(`[handleEmailSelect] Successfully fetched email from API`); setSelectedEmail(fullEmail); } else { throw new Error('Email content not found'); } } catch (error) { const fetchError = error instanceof Error ? error : new Error(String(error)); console.error(`[handleEmailSelect] Error fetching email from API: ${fetchError.message}`); throw fetchError; } return; } // If content is not fetched, get the full content if (!email.contentFetched) { console.log(`[handleEmailSelect] Email found but content not fetched. Getting full content for ${emailId}`); try { console.log(`[handleEmailSelect] Fetching content with explicit parameters: emailId=${emailId}, accountId=${accountId}, folder=${normalizedFolder}`); // CRITICAL FIX: Use explicit parameters for API request const fullEmail = await fetchEmailContent( emailId, accountId, normalizedFolder ); if (fullEmail) { // CRITICAL FIX: Force correct account ID fullEmail.accountId = accountId; // CRITICAL FIX: Create an updated email with explicit account context const updatedEmail = { ...email, accountId: accountId, // Use provided accountId folder: prefixedFolder, // Use consistent prefixed folder content: fullEmail.content, attachments: fullEmail.attachments, contentFetched: true }; // Update the email in the list - only match emails with the same ID AND account setEmails(emails.map(e => (e.id === emailId && e.accountId === accountId) ? updatedEmail : e )); setSelectedEmail(updatedEmail); console.log(`[handleEmailSelect] Email content loaded successfully`); } } catch (error) { const contentError = error instanceof Error ? error : new Error(String(error)); console.error(`[handleEmailSelect] Error fetching email content: ${contentError.message}`); throw contentError; } } else { console.log(`[handleEmailSelect] Email found with content already fetched, selecting directly`); // CRITICAL FIX: Even if already fetched, ensure consistent account context email = { ...email, accountId: accountId, // Force the correct account ID folder: prefixedFolder // Force the consistent folder prefix }; setSelectedEmail(email); } // Mark the email as read if it's not already if (!email.flags.seen) { console.log(`[handleEmailSelect] Marking email ${emailId} as read for account ${accountId}`); markEmailAsRead(emailId, true, accountId).catch(err => { console.error(`[handleEmailSelect] Failed to mark email as read: ${err.message}`); }); } } catch (err) { console.error(`[handleEmailSelect] Error: ${err instanceof Error ? err.message : String(err)}`); toast({ variant: "destructive", title: "Error", description: "Could not load email content" }); } finally { setIsLoading(false); } }, [emails, fetchEmailContent, markEmailAsRead, toast]); // Toggle starred status for an email const toggleStarred = useCallback(async (emailId: string) => { // Find the email in the emails array const email = emails.find(e => e.id === emailId); if (!email) return; const newStarredStatus = !email.flags.flagged; // CRITICAL FIX: Extract the account ID from the email object const emailAccountId = email.accountId; if (!emailAccountId) { console.error('Cannot toggle star without account ID'); return; } // Extract normalized folder from folder with potential prefix let normalizedFolder: string; if (email.folder && email.folder.includes(':')) { normalizedFolder = email.folder.split(':')[1]; } else if (currentFolder.includes(':')) { normalizedFolder = currentFolder.split(':')[1]; } else { normalizedFolder = email.folder || currentFolder; } try { const response = await fetch(`/api/courrier/${emailId}/star`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ starred: newStarredStatus, folder: normalizedFolder, accountId: emailAccountId // CRITICAL FIX: Always include account ID in requests }) }); if (!response.ok) { throw new Error('Failed to toggle star status'); } // Update the email in the list - match both ID and account ID setEmails(emails.map(email => (email.id === emailId && email.accountId === emailAccountId) ? { ...email, flags: { ...email.flags, flagged: newStarredStatus } } : email )); // If the selected email is the one being starred, update it too if (selectedEmail && selectedEmail.id === emailId && selectedEmail.accountId === emailAccountId) { setSelectedEmail({ ...selectedEmail, flags: { ...selectedEmail.flags, flagged: newStarredStatus } }); } } catch (error) { console.error('Error toggling star status:', error); toast({ variant: "destructive", title: "Error", description: "Could not update star status" }); } }, [emails, selectedEmail, currentFolder, toast]); // Send an email const sendEmail = useCallback(async (emailData: EmailData) => { if (!session?.user?.id) { toast({ variant: "destructive", title: "Error", description: "You must be logged in to send emails" }); return { success: false, error: "Not authenticated" }; } setIsSending(true); try { const response = await fetch('/api/courrier/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(emailData) }); const result = await response.json(); if (!response.ok) { throw new Error(result.error || 'Failed to send email'); } toast({ title: "Success", description: "Email sent successfully" }); return { success: true, messageId: result.messageId }; } catch (error) { console.error('Error sending email:', error); toast({ variant: "destructive", title: "Error", description: error instanceof Error ? error.message : 'Failed to send email' }); return { success: false, error: error instanceof Error ? error.message : 'Failed to send email' }; } finally { setIsSending(false); } }, [session?.user?.id, toast]); // Delete selected emails const deleteEmails = useCallback(async (emailIds: string[]) => { if (emailIds.length === 0) return; setIsDeleting(true); try { // CRITICAL FIX: Extract normalized folder and account ID from currentFolder let normalizedFolder = currentFolder; let accountId = 'default'; if (currentFolder.includes(':')) { const parts = currentFolder.split(':'); accountId = parts[0]; normalizedFolder = parts[1]; } // Filter email IDs based on the current account context // Only delete emails that belong to the current account const emailsInCurrentAccount = emails.filter(email => emailIds.includes(email.id) && (!email.accountId || email.accountId === accountId) ); const filteredEmailIds = emailsInCurrentAccount.map(email => email.id); if (filteredEmailIds.length === 0) { console.log('No emails to delete in the current account context'); return; } console.log(`Deleting ${filteredEmailIds.length} emails from account ${accountId} in folder ${normalizedFolder}`); const response = await fetch('/api/courrier/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ emailIds: filteredEmailIds, folder: normalizedFolder, accountId }) }); if (!response.ok) { throw new Error('Failed to delete emails'); } // Remove the deleted emails from the list setEmails(emails.filter(email => !filteredEmailIds.includes(email.id) || (email.accountId && email.accountId !== accountId) )); // Clear selection if the selected email was deleted if (selectedEmail && filteredEmailIds.includes(selectedEmail.id) && (!selectedEmail.accountId || selectedEmail.accountId === accountId)) { setSelectedEmail(null); } // Clear selected IDs setSelectedEmailIds(prevIds => prevIds.filter(id => !filteredEmailIds.includes(id))); toast({ title: "Success", description: `${filteredEmailIds.length} email(s) deleted` }); } catch (error) { console.error('Error deleting emails:', error); toast({ variant: "destructive", title: "Error", description: "Failed to delete emails" }); } finally { setIsDeleting(false); } }, [emails, selectedEmail, currentFolder, toast]); // Toggle selection of an email const toggleEmailSelection = useCallback((emailId: string) => { setSelectedEmailIds(prev => { if (prev.includes(emailId)) { return prev.filter(id => id !== emailId); } else { return [...prev, emailId]; } }); }, []); // Select all emails const toggleSelectAll = useCallback(() => { if (selectedEmailIds.length === emails.length) { setSelectedEmailIds([]); } else { setSelectedEmailIds(emails.map(email => email.id)); } }, [emails, selectedEmailIds]); // Search emails const searchEmails = useCallback((query: string) => { setSearchQuery(query); setPage(1); // CRITICAL FIX: Extract account ID from currentFolder when searching let accountId = 'default'; if (currentFolder.includes(':')) { const parts = currentFolder.split(':'); accountId = parts[0]; } // Call loadEmails with the correct account context loadEmails(false, accountId); }, [loadEmails, currentFolder]); // Format an email for reply or forward const formatEmailForAction = useCallback((email: Email) => { return { id: email.id, subject: email.subject, from: email.from[0]?.address || '', to: email.to.map(addr => addr.address).join(', '), cc: email.cc?.map(addr => addr.address).join(', '), bcc: email.bcc?.map(addr => addr.address).join(', '), content: email.content.text || email.content.html || '', attachments: email.attachments }; }, []); // Return all the functionality and state values return { // Data emails, selectedEmail, selectedEmailIds, currentFolder, mailboxes, isLoading, isSending, isDeleting, error, searchQuery, page, perPage, totalEmails, totalPages, // Functions loadEmails, handleEmailSelect, markEmailAsRead, toggleStarred, sendEmail, deleteEmails, toggleEmailSelection, toggleSelectAll, changeFolder, searchEmails, formatEmailForAction, setPage, setPerPage, setSearchQuery, // Added email state setter setEmails, }; };