NeahNew/hooks/use-courrier.ts
2025-05-03 14:17:46 +02:00

1075 lines
41 KiB
TypeScript

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<Email[]>([]);
const [selectedEmail, setSelectedEmail] = useState<Email | null>(null);
const [selectedEmailIds, setSelectedEmailIds] = useState<string[]>([]);
const [currentFolder, setCurrentFolder] = useState<MailFolder>('INBOX');
const [mailboxes, setMailboxes] = useState<string[]>([]);
// State for UI
const [isLoading, setIsLoading] = useState(false);
const [isSending, setIsSending] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [error, setError] = useState<string | null>(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,
};
};