courrier multi account restore compose

This commit is contained in:
alma 2025-04-30 12:47:52 +02:00
parent 346b766b7f
commit 9f0ca0e6f5
2 changed files with 232 additions and 129 deletions

View File

@ -7,6 +7,9 @@ import {
cacheEmailList,
invalidateFolderCache
} from '@/lib/redis';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Simple in-memory cache (will be removed in a future update)
interface EmailCacheEntry {
@ -38,11 +41,32 @@ export async function GET(request: Request) {
const accountId = searchParams.get("accountId") || "";
// Extract account ID from folder name if present and none was explicitly provided
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : "";
// Use the most specific account ID available
// First from the folder, then from the explicit parameter, then default
let effectiveAccountId = folderAccountId || accountId || 'default';
// CRITICAL FIX: If effectiveAccountId is still 'default', try to find the first account for the user
if (effectiveAccountId === 'default') {
try {
const accounts = await prisma.mailCredentials.findMany({
where: { userId: session.user.id },
orderBy: { createdAt: 'asc' },
take: 1,
select: { id: true }
});
if (accounts && accounts.length > 0) {
effectiveAccountId = accounts[0].id;
console.log(`No specific account provided, using first available account: ${effectiveAccountId}`);
}
} catch (error) {
console.error("Error finding default account:", error);
// Continue with 'default' if there's an error
}
}
// Normalize folder name by removing account prefix if present
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;

View File

@ -95,15 +95,23 @@ export const useCourrier = () => {
// The currentFolder should already have the account prefix in format "accountId:folder"
// We need to extract the base folder name for the API request
let normalizedFolder = currentFolder;
let effectiveAccountId = accountId || 'default';
if (currentFolder.includes(':')) {
// CRITICAL FIX: Handle account ID determination more robustly
// If specific account ID is provided, always use it
// Otherwise extract from currentFolder if possible
let effectiveAccountId: string;
if (accountId) {
// Use explicitly provided accountId
effectiveAccountId = accountId;
} else if (currentFolder.includes(':')) {
// Extract from folder format
const parts = currentFolder.split(':');
// If no explicit accountId was provided, use the one from the folder name
if (!accountId) {
effectiveAccountId = parts[0];
}
effectiveAccountId = parts[0];
normalizedFolder = parts[1];
} else {
// Default case
effectiveAccountId = 'default';
}
console.log(`Load emails - using normalized folder: ${normalizedFolder}, effectiveAccountId: ${effectiveAccountId}`);
@ -125,12 +133,14 @@ export const useCourrier = () => {
// Try to get cached emails first
const currentRequestPage = page;
// FIXED: Use the correct parameter order and normalized values
// Function signature: (userId: string, folder: string, page: number, perPage: number, timeoutMs: number = 100, accountId?: string)
console.log(`Getting cached emails for user ${session.user.id}, folder ${currentFolder}, normalizedFolder: ${normalizedFolder}, page ${currentRequestPage}, accountId ${effectiveAccountId}`);
// CRITICAL FIX: Use the correct cache key format
// We pass the full prefixed folder to ensure proper cache key consistency
const folderForCache = `${effectiveAccountId}:${normalizedFolder}`;
console.log(`Getting cached emails for user ${session.user.id}, folder ${folderForCache}, page ${currentRequestPage}, accountId ${effectiveAccountId}`);
const cachedEmails = await getCachedEmailsWithTimeout(
session.user.id, // userId: string
currentFolder, // folder: string - use full prefixed folder for cache key
folderForCache, // folder: string - use consistently prefixed folder for cache key
currentRequestPage, // page: number
perPage, // perPage: number
100, // timeoutMs: number
@ -299,8 +309,13 @@ export const useCourrier = () => {
const changeFolder = useCallback(async (folder: string, accountId?: string) => {
console.log(`Changing folder to ${folder} for account ${accountId || 'default'}`);
try {
// CRITICAL FIX: Better folder and account ID handling
// Extract account ID from folder name if present and none was explicitly provided
// 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: Extract account ID from folder name if present and none was explicitly provided
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
// Use the most specific account ID available
@ -314,13 +329,8 @@ export const useCourrier = () => {
console.log(`Folder change: original=${folder}, normalized=${normalizedFolder}, accountId=${effectiveAccountId}, prefixed=${prefixedFolder}`);
// Reset selected email
setSelectedEmail(null);
setSelectedEmailIds([]);
// Use the consistently prefixed folder name for state
// CRITICAL FIX: Always use the properly prefixed folder name in state
setCurrentFolder(prefixedFolder);
// CRITICAL FIX: Store the current account ID in state for all subsequent operations
// This ensures operations like markAsRead use the correct account context
// Reset search query when changing folders
setSearchQuery('');
@ -328,19 +338,16 @@ export const useCourrier = () => {
// Reset to page 1
setPage(1);
// Clear existing emails before loading new ones to prevent UI flicker
setEmails([]);
// Show loading state
setIsLoading(true);
// CRITICAL FIX: We set the currentFolder state AFTER we have prepared all parameters
// This ensures any effects or functions triggered by currentFolder change have the correct context
setCurrentFolder(prefixedFolder);
// Use a small delay to ensure state updates have propagated
// This helps prevent race conditions when multiple folders are clicked quickly
await new Promise(resolve => setTimeout(resolve, 100));
// Call loadEmails with correct boolean parameter type and account ID
// CRITICAL FIX: Pass the properly formatted folder name to the cache lookup functions
console.log(`Loading emails for prefixed folder: ${prefixedFolder} with accountId: ${effectiveAccountId}`);
// CRITICAL FIX: Wait for the loadEmails operation to complete before considering the folder change done
// This prevents multiple concurrent folder changes from interfering with each other
await loadEmails(false, effectiveAccountId);
} catch (error) {
console.error(`Error changing to folder ${folder}:`, error);
@ -408,24 +415,47 @@ export const useCourrier = () => {
}, [currentFolder]);
// Mark an email as read/unread
const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean) => {
const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean, providedAccountId?: string) => {
try {
// Find the email to get its accountId
const emailToMark = emails.find(e => e.id === emailId);
if (!emailToMark) {
throw new Error('Email not found');
// 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;
}
}
// Get the accountId from the email
const emailAccountId = emailToMark.accountId || 'default';
// Normalize folder name by removing account prefix if present
const normalizedFolder = emailToMark.folder.includes(':')
? emailToMark.folder.split(':')[1]
: emailToMark.folder;
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: {
@ -437,27 +467,29 @@ export const useCourrier = () => {
accountId: emailAccountId
})
});
if (!response.ok) {
throw new Error('Failed to mark email as read');
}
// Update the email in the list
// Update the email in the list - only update the specific email with matching ID AND account ID
setEmails(emails.map(email =>
email.id === emailId ? { ...email, flags: { ...email.flags, seen: isRead } } : 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) {
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]);
}, [emails, selectedEmail, currentFolder]);
// Select an email to view
const handleEmailSelect = useCallback(async (emailId: string, accountId: string, folderOverride: string) => {
@ -473,8 +505,11 @@ export const useCourrier = () => {
setIsLoading(true);
try {
// Normalize account ID if not provided
const effectiveAccountId = accountId || 'default';
// CRITICAL FIX: Always use the provided accountId, never use default
// This ensures we consistently use the correct account throughout the email selection process
if (!accountId) {
throw new Error('Account ID is required for email selection');
}
// Normalize folder name handling - ensure consistent format
let normalizedFolder: string;
@ -483,76 +518,59 @@ export const useCourrier = () => {
if (folderOverride.includes(':')) {
// Extract parts if folder already has a prefix
const parts = folderOverride.split(':');
const folderAccountId = parts[0];
normalizedFolder = parts[1];
// CRITICAL FIX: Always use the provided accountId instead of the one in the folder
// CRITICAL FIX: Always use the explicitly provided accountId
// This ensures we're looking in the right account when an email is clicked
prefixedFolder = `${effectiveAccountId}:${normalizedFolder}`;
if (folderAccountId !== effectiveAccountId) {
console.log(`WARNING: Folder account prefix mismatch. Folder has ${folderAccountId}, but using ${effectiveAccountId}`);
}
prefixedFolder = `${accountId}:${normalizedFolder}`;
} else {
// No prefix, add one
// No prefix, add one using the provided account ID
normalizedFolder = folderOverride;
prefixedFolder = `${effectiveAccountId}:${normalizedFolder}`;
prefixedFolder = `${accountId}:${normalizedFolder}`;
}
console.log(`Email selection with normalized values: folder=${normalizedFolder}, prefixed=${prefixedFolder}, accountId=${effectiveAccountId}`);
console.log(`Email selection with normalized values: folder=${normalizedFolder}, prefixed=${prefixedFolder}, accountId=${accountId}`);
// More flexible email finding with detailed logging
console.log(`Looking for email with ID=${emailId}, account=${effectiveAccountId}, normalized folder=${normalizedFolder}, prefixed=${prefixedFolder}`);
// CRITICAL FIX: First search for email in current list using the EXACT account provided
// This ensures we don't mix emails from different accounts
console.log(`Looking for email with ID=${emailId}, account=${accountId}, normalized folder=${normalizedFolder}, prefixed=${prefixedFolder}`);
// First, try to find by exact match with account and folder
// First, try to find by exact match with the provided account and folder
let email = emails.find(e =>
e.id === emailId &&
e.accountId === effectiveAccountId &&
e.accountId === accountId &&
(
e.folder === prefixedFolder ||
e.folder === normalizedFolder ||
e.folder === folderOverride ||
// Also check for case where email folder has its own prefix but with the same normalized folder
(e.folder?.includes(':') && e.folder.split(':')[1] === normalizedFolder)
)
);
// If not found, try to find just by ID as fallback
// CRITICAL FIX: If not found, we do NOT try finding by ID only
// This prevents mixing emails across accounts
if (!email) {
console.log(`No exact match found. Looking for email just by ID=${emailId}`);
email = emails.find(e => e.id === emailId);
if (email) {
console.log(`Found email by ID only. Account=${email.accountId}, folder=${email.folder}`);
}
}
if (!email) {
console.log(`Email ${emailId} not found in current list (searched ${emails.length} emails). Fetching from API.`);
console.log(`Email ${emailId} not found in current list for account ${accountId} (searched ${emails.length} emails). Fetching from API.`);
try {
// CRITICAL FIX: Pass both normalized folder and account ID to fetch
// Using normalized folder (without prefix) for API compatibility
console.log(`Fetching email ${emailId} directly from API with normalizedFolder=${normalizedFolder}, accountId=${effectiveAccountId}`);
const fullEmail = await fetchEmailContent(emailId, effectiveAccountId, normalizedFolder);
// Use the provided account ID and normalized folder for the API request
console.log(`Fetching email ${emailId} directly from API with normalizedFolder=${normalizedFolder}, accountId=${accountId}`);
const fullEmail = await fetchEmailContent(emailId, accountId, normalizedFolder);
// Ensure the returned email has the proper accountId and prefixed folder name
// CRITICAL FIX: Always set the accountId correctly
if (fullEmail) {
if (!fullEmail.accountId) {
fullEmail.accountId = effectiveAccountId;
}
fullEmail.accountId = accountId;
// Make sure folder has the proper prefix for consistent lookup
if (fullEmail.folder && !fullEmail.folder.includes(':')) {
fullEmail.folder = `${fullEmail.accountId}:${fullEmail.folder}`;
fullEmail.folder = `${accountId}:${fullEmail.folder}`;
}
console.log(`Successfully fetched email from API:`, {
id: fullEmail.id,
account: fullEmail.accountId,
folder: fullEmail.folder
});
setSelectedEmail(fullEmail);
}
console.log(`Successfully fetched email from API:`, {
id: fullEmail.id,
account: fullEmail.accountId,
folder: fullEmail.folder
});
setSelectedEmail(fullEmail);
} catch (error) {
// Type the error properly
const fetchError = error instanceof Error ? error : new Error(String(error));
@ -566,43 +584,35 @@ export const useCourrier = () => {
if (!email.contentFetched) {
console.log(`Email found but content not fetched. Getting full content for ${emailId}`);
try {
// CRITICAL FIX: Extract normalized folder from email's folder if it has a prefix
const emailFolder = email.folder || normalizedFolder;
let emailNormalizedFolder = emailFolder;
// CRITICAL FIX: Use the provided accountId for fetching content, not the one from the email
// This ensures consistent account context
if (emailFolder.includes(':')) {
emailNormalizedFolder = emailFolder.split(':')[1];
}
console.log(`Fetching content for email ${emailId} with accountId=${accountId}, folder=${normalizedFolder}`);
// Always use the email's own accountId if available
const emailAccountId = email.accountId || effectiveAccountId;
console.log(`Fetching content for email ${emailId} with accountId=${emailAccountId}, folder=${emailNormalizedFolder}`);
// Use the email's own accountId and normalized folder for fetching
// Use the provided accountId and normalized folder for fetching
const fullEmail = await fetchEmailContent(
emailId,
emailAccountId,
emailNormalizedFolder
accountId,
normalizedFolder
);
// Ensure the returned email has consistent format
if (fullEmail && !fullEmail.accountId) {
fullEmail.accountId = emailAccountId;
// CRITICAL FIX: Ensure the returned email has the correct account ID
if (fullEmail) {
fullEmail.accountId = accountId;
}
// Merge the full content with the email
const updatedEmail = {
...email,
accountId: emailAccountId, // Ensure account ID is preserved
folder: email.folder, // Preserve original folder name with prefix
accountId, // CRITICAL FIX: Use the provided accountId
folder: prefixedFolder, // CRITICAL FIX: Use the consistently prefixed folder
content: fullEmail.content,
attachments: fullEmail.attachments,
contentFetched: true
};
// Update the email in the list
setEmails(emails.map(e => e.id === emailId ? updatedEmail : e));
setEmails(emails.map(e => e.id === emailId && e.accountId === accountId ? updatedEmail : e));
setSelectedEmail(updatedEmail);
console.log(`Successfully updated email with content`);
} catch (error) {
@ -613,13 +623,21 @@ export const useCourrier = () => {
}
} else {
console.log(`Email found with content already fetched, selecting directly`);
// CRITICAL FIX: Ensure the email has the correct account ID before selecting
email = {
...email,
accountId, // Always use the provided accountId
folder: prefixedFolder // Always use the consistently prefixed folder
};
setSelectedEmail(email);
}
// Mark the email as read if it's not already
if (!email.flags.seen) {
console.log(`Marking email ${emailId} as read`);
markEmailAsRead(emailId, true).catch(err => {
console.log(`Marking email ${emailId} as read for account ${accountId}`);
markEmailAsRead(emailId, true, accountId).catch(err => {
console.error(`Failed to mark email as read: ${err.message}`);
});
}
@ -637,11 +655,29 @@ export const useCourrier = () => {
// 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',
@ -650,7 +686,8 @@ export const useCourrier = () => {
},
body: JSON.stringify({
starred: newStarredStatus,
folder: currentFolder
folder: normalizedFolder,
accountId: emailAccountId // CRITICAL FIX: Always include account ID in requests
})
});
@ -658,13 +695,15 @@ export const useCourrier = () => {
throw new Error('Failed to toggle star status');
}
// Update the email in the list
// Update the email in the list - match both ID and account ID
setEmails(emails.map(email =>
email.id === emailId ? { ...email, flags: { ...email.flags, flagged: newStarredStatus } } : 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) {
if (selectedEmail && selectedEmail.id === emailId && selectedEmail.accountId === emailAccountId) {
setSelectedEmail({ ...selectedEmail, flags: { ...selectedEmail.flags, flagged: newStarredStatus } });
}
} catch (error) {
@ -736,14 +775,41 @@ export const useCourrier = () => {
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,
folder: currentFolder
emailIds: filteredEmailIds,
folder: normalizedFolder,
accountId
})
});
@ -752,19 +818,23 @@ export const useCourrier = () => {
}
// Remove the deleted emails from the list
setEmails(emails.filter(email => !emailIds.includes(email.id)));
setEmails(emails.filter(email =>
!filteredEmailIds.includes(email.id) ||
(email.accountId && email.accountId !== accountId)
));
// Clear selection if the selected email was deleted
if (selectedEmail && emailIds.includes(selectedEmail.id)) {
if (selectedEmail && filteredEmailIds.includes(selectedEmail.id) &&
(!selectedEmail.accountId || selectedEmail.accountId === accountId)) {
setSelectedEmail(null);
}
// Clear selected IDs
setSelectedEmailIds([]);
setSelectedEmailIds(prevIds => prevIds.filter(id => !filteredEmailIds.includes(id)));
toast({
title: "Success",
description: `${emailIds.length} email(s) deleted`
description: `${filteredEmailIds.length} email(s) deleted`
});
} catch (error) {
console.error('Error deleting emails:', error);
@ -803,8 +873,17 @@ export const useCourrier = () => {
const searchEmails = useCallback((query: string) => {
setSearchQuery(query);
setPage(1);
loadEmails();
}, [loadEmails]);
// 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) => {