1075 lines
41 KiB
TypeScript
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,
|
|
};
|
|
};
|