courrier multi account restore compose

This commit is contained in:
alma 2025-04-29 11:29:48 +02:00
parent 0368bd1069
commit 2fada01eba
4 changed files with 169 additions and 81 deletions

View File

@ -45,15 +45,24 @@ export async function POST(
const { folder = 'INBOX', accountId, isRead = true } = await request.json(); const { folder = 'INBOX', accountId, isRead = true } = await request.json();
// Extract account ID from folder name if present and none was explicitly provided
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
// Use the most specific account ID available
const effectiveAccountId = folderAccountId || accountId || 'default';
// Normalize folder name by removing account prefix if present
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
// Log operation details for debugging // Log operation details for debugging
console.log(`Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${folder}${accountId ? ` for account ${accountId}` : ''}`); console.log(`Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${normalizedFolder} for account ${effectiveAccountId}`);
const success = await markEmailReadStatus( const success = await markEmailReadStatus(
session.user.id, session.user.id,
emailId, emailId,
isRead, isRead,
folder, normalizedFolder,
accountId effectiveAccountId
); );
if (!success) { if (!success) {

View File

@ -37,31 +37,42 @@ export async function GET(request: Request) {
const searchQuery = searchParams.get("search") || ""; const searchQuery = searchParams.get("search") || "";
const accountId = searchParams.get("accountId") || ""; const accountId = searchParams.get("accountId") || "";
// Extract account ID from folder name if present and none was explicitly provided
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
// Use the most specific account ID available
const effectiveAccountId = folderAccountId || accountId || 'default';
// Normalize folder name by removing account prefix if present
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
// Log the request details for debugging
console.log(`Email request: user=${session.user.id}, folder=${normalizedFolder}, account=${effectiveAccountId}, page=${page}`);
// Try to get from Redis cache first, but only if it's not a search query // Try to get from Redis cache first, but only if it's not a search query
if (!searchQuery) { if (!searchQuery) {
const cacheKey = accountId ? `${session.user.id}:${accountId}:${folder}` : `${session.user.id}:${folder}`;
const cachedEmails = await getCachedEmailList( const cachedEmails = await getCachedEmailList(
session.user.id, session.user.id,
accountId || 'default', effectiveAccountId,
folder, normalizedFolder,
page, page,
perPage perPage
); );
if (cachedEmails) { if (cachedEmails) {
console.log(`Using Redis cached emails for ${cacheKey}:${page}:${perPage}`); console.log(`Using Redis cached emails for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}`);
return NextResponse.json(cachedEmails); return NextResponse.json(cachedEmails);
} }
} }
console.log(`Redis cache miss for ${session.user.id}:${folder}:${page}:${perPage}, fetching emails from IMAP`); console.log(`Redis cache miss for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}, fetching emails from IMAP`);
// Use the email service to fetch emails, passing the accountId if provided // Use the email service to fetch emails with the normalized folder and effective account ID
const emailsResult = await getEmails( const emailsResult = await getEmails(
session.user.id, session.user.id,
folder, normalizedFolder,
page, page,
perPage, perPage,
accountId || undefined effectiveAccountId
); );
// The result is already cached in the getEmails function // The result is already cached in the getEmails function

View File

@ -266,11 +266,22 @@ export const useCourrier = () => {
const changeFolder = useCallback(async (folder: string, accountId?: string) => { const changeFolder = useCallback(async (folder: string, accountId?: string) => {
console.log(`Changing folder to ${folder} for account ${accountId || 'default'}`); console.log(`Changing folder to ${folder} for account ${accountId || 'default'}`);
try { try {
// Extract account ID from folder name if present and none was explicitly provided
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
// Use the most specific account ID available
const effectiveAccountId = folderAccountId || accountId || 'default';
// Normalize folder name by removing account prefix if present
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
console.log(`Folder change normalized: ${normalizedFolder}, account: ${effectiveAccountId}`);
// Reset selected email // Reset selected email
setSelectedEmail(null); setSelectedEmail(null);
setSelectedEmailIds([]); setSelectedEmailIds([]);
// Record the new folder // Record the new folder (preserving account prefix if present)
setCurrentFolder(folder); setCurrentFolder(folder);
// Reset search query when changing folders // Reset search query when changing folders
@ -289,8 +300,8 @@ export const useCourrier = () => {
// This helps prevent race conditions when multiple folders are clicked quickly // This helps prevent race conditions when multiple folders are clicked quickly
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
// Call loadEmails with correct boolean parameter type // Call loadEmails with correct boolean parameter type and account ID
await loadEmails(false, accountId); await loadEmails(false, effectiveAccountId);
} catch (error) { } catch (error) {
console.error(`Error changing to folder ${folder}:`, error); console.error(`Error changing to folder ${folder}:`, error);
setError(error instanceof Error ? error.message : 'Unknown error'); setError(error instanceof Error ? error.message : 'Unknown error');
@ -323,11 +334,27 @@ export const useCourrier = () => {
// Fetch a single email's content // Fetch a single email's content
const fetchEmailContent = useCallback(async (emailId: string, accountId?: string, folderOverride?: string) => { const fetchEmailContent = useCallback(async (emailId: string, accountId?: string, folderOverride?: string) => {
try { try {
// Use the provided folder or current folder
const folderToUse = folderOverride || currentFolder; 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({ const query = new URLSearchParams({
folder: folderToUse, folder: normalizedFolder,
}); });
if (accountId) query.set('accountId', accountId);
// Always include account ID in query params
query.set('accountId', effectiveAccountId);
const response = await fetch(`/api/courrier/${emailId}?${query.toString()}`); const response = await fetch(`/api/courrier/${emailId}?${query.toString()}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch email content: ${response.status}`); throw new Error(`Failed to fetch email content: ${response.status}`);
@ -350,7 +377,14 @@ export const useCourrier = () => {
} }
// Get the accountId from the email // Get the accountId from the email
const emailAccountId = emailToMark.accountId; const emailAccountId = emailToMark.accountId || 'default';
// Normalize folder name by removing account prefix if present
const normalizedFolder = emailToMark.folder.includes(':')
? emailToMark.folder.split(':')[1]
: emailToMark.folder;
console.log(`Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${normalizedFolder}, account: ${emailAccountId}`);
const response = await fetch(`/api/courrier/${emailId}/mark-read`, { const response = await fetch(`/api/courrier/${emailId}/mark-read`, {
method: 'POST', method: 'POST',
@ -359,7 +393,7 @@ export const useCourrier = () => {
}, },
body: JSON.stringify({ body: JSON.stringify({
isRead, isRead,
folder: currentFolder, folder: normalizedFolder,
accountId: emailAccountId accountId: emailAccountId
}) })
}); });
@ -383,20 +417,39 @@ export const useCourrier = () => {
console.error('Error marking email as read:', error); console.error('Error marking email as read:', error);
return false; return false;
} }
}, [emails, selectedEmail, currentFolder]); }, [emails, selectedEmail]);
// Select an email to view // Select an email to view
const handleEmailSelect = useCallback(async (emailId: string, accountId: string, folderOverride: string) => { const handleEmailSelect = useCallback(async (emailId: string, accountId: string, folderOverride: string) => {
console.log(`Selecting email ${emailId} from account ${accountId} in folder ${folderOverride}`);
setIsLoading(true); setIsLoading(true);
try { try {
// Normalize account ID if not provided
const effectiveAccountId = accountId || 'default';
// Normalize folder name by removing account prefix if present
const normalizedFolder = folderOverride.includes(':') ? folderOverride.split(':')[1] : folderOverride;
// Find the email in the current list // Find the email in the current list
const email = emails.find(e => e.id === emailId && e.accountId === accountId && e.folder === folderOverride); const email = emails.find(e =>
e.id === emailId &&
(e.accountId === effectiveAccountId) &&
(e.folder === normalizedFolder || e.folder === folderOverride)
);
if (!email) { if (!email) {
throw new Error('Email not found'); console.log(`Email ${emailId} not found in current list. Fetching from API.`);
const fullEmail = await fetchEmailContent(emailId, effectiveAccountId, normalizedFolder);
setSelectedEmail(fullEmail);
return;
} }
// If content is not fetched, get the full content // If content is not fetched, get the full content
if (!email.contentFetched) { if (!email.contentFetched) {
const fullEmail = await fetchEmailContent(emailId, accountId, folderOverride); console.log(`Fetching content for email ${emailId}`);
const fullEmail = await fetchEmailContent(emailId, effectiveAccountId, normalizedFolder);
// Merge the full content with the email // Merge the full content with the email
const updatedEmail = { const updatedEmail = {
...email, ...email,
@ -404,12 +457,14 @@ export const useCourrier = () => {
attachments: fullEmail.attachments, attachments: fullEmail.attachments,
contentFetched: true contentFetched: true
}; };
// Update the email in the list // Update the email in the list
setEmails(emails.map(e => e.id === emailId ? updatedEmail : e)); setEmails(emails.map(e => e.id === emailId ? updatedEmail : e));
setSelectedEmail(updatedEmail); setSelectedEmail(updatedEmail);
} else { } else {
setSelectedEmail(email); setSelectedEmail(email);
} }
// Mark the email as read if it's not already // Mark the email as read if it's not already
if (!email.flags.seen) { if (!email.flags.seen) {
markEmailAsRead(emailId, true); markEmailAsRead(emailId, true);

View File

@ -195,7 +195,7 @@ export async function getUserEmailCredentials(userId: string, accountId?: string
secure: mailCredentials.secure, secure: mailCredentials.secure,
smtp_host: mailCredentials.smtp_host || undefined, smtp_host: mailCredentials.smtp_host || undefined,
smtp_port: mailCredentials.smtp_port || undefined, smtp_port: mailCredentials.smtp_port || undefined,
smtp_secure: mailCredentials.smtp_secure || false, smtp_secure: mailCredentials.smtp_secure ?? false,
display_name: mailCredentials.display_name || undefined, display_name: mailCredentials.display_name || undefined,
color: mailCredentials.color || undefined color: mailCredentials.color || undefined
}; };
@ -282,26 +282,33 @@ export async function getEmails(
let client: ImapFlow | undefined; let client: ImapFlow | undefined;
try { try {
// Extract the actual folder name (remove account prefix if present) // Extract account ID from folder name if present and none was explicitly provided
const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder; const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
console.log(`Fetching emails for folder: ${folder} (actual: ${actualFolder})`);
// Get IMAP connection // Use the most specific account ID available
client = await getImapConnection(userId, accountId); const effectiveAccountId = folderAccountId || accountId || 'default';
// Normalize folder name by removing account prefix if present
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
console.log(`[getEmails] Processing request for folder: ${folder}, normalized to ${normalizedFolder}, account: ${effectiveAccountId}`);
// Get IMAP connection using the effective account ID
client = await getImapConnection(userId, effectiveAccountId);
if (!client) { if (!client) {
throw new Error('Failed to establish IMAP connection'); throw new Error('Failed to establish IMAP connection');
} }
// Open mailbox with the actual folder name // Open mailbox with the normalized folder name
await client.mailboxOpen(actualFolder); await client.mailboxOpen(normalizedFolder);
const mailbox = client.mailbox; const mailbox = client.mailbox;
if (!mailbox || typeof mailbox === 'boolean') { if (!mailbox || typeof mailbox === 'boolean') {
throw new Error(`Failed to open mailbox: ${actualFolder}`); throw new Error(`Failed to open mailbox: ${normalizedFolder}`);
} }
// Get total messages // Get total messages
const total = mailbox.exists || 0; const total = mailbox.exists || 0;
console.log(`Total messages in ${actualFolder}: ${total}`); console.log(`Total messages in ${normalizedFolder} for account ${effectiveAccountId}: ${total}`);
// If no messages, return empty result // If no messages, return empty result
if (total === 0) { if (total === 0) {
@ -311,7 +318,7 @@ export async function getEmails(
page, page,
perPage, perPage,
totalPages: 0, totalPages: 0,
folder: actualFolder, folder: normalizedFolder,
mailboxes: [] mailboxes: []
}; };
} }
@ -319,7 +326,7 @@ export async function getEmails(
// Calculate message range for pagination // Calculate message range for pagination
const start = Math.max(1, total - (page * perPage) + 1); const start = Math.max(1, total - (page * perPage) + 1);
const end = Math.max(1, total - ((page - 1) * perPage)); const end = Math.max(1, total - ((page - 1) * perPage));
console.log(`Fetching messages ${start}:${end} from ${actualFolder}`); console.log(`Fetching messages ${start}:${end} from ${normalizedFolder} for account ${effectiveAccountId}`);
// Fetch messages // Fetch messages
const messages = await client.fetch(`${start}:${end}`, { const messages = await client.fetch(`${start}:${end}`, {
@ -351,9 +358,9 @@ export async function getEmails(
}, },
size: message.size || 0, size: message.size || 0,
hasAttachments: message.bodyStructure?.childNodes?.some(node => node.disposition === 'attachment') || false, hasAttachments: message.bodyStructure?.childNodes?.some(node => node.disposition === 'attachment') || false,
folder: actualFolder, folder: normalizedFolder,
contentFetched: false, contentFetched: false,
accountId: accountId || 'default', accountId: effectiveAccountId,
content: { content: {
text: '', text: '',
html: '' html: ''
@ -362,18 +369,16 @@ export async function getEmails(
emails.push(email); emails.push(email);
} }
// Cache the result if we have an accountId // Cache the result with the effective account ID
if (accountId) { await cacheEmailList(userId, effectiveAccountId, normalizedFolder, page, perPage, {
await cacheEmailList(userId, accountId, actualFolder, page, perPage, { emails,
emails, totalEmails: total,
totalEmails: total, page,
page, perPage,
perPage, totalPages: Math.ceil(total / perPage),
totalPages: Math.ceil(total / perPage), folder: normalizedFolder,
folder: actualFolder, mailboxes: []
mailboxes: [] });
});
}
return { return {
emails, emails,
@ -381,7 +386,7 @@ export async function getEmails(
page, page,
perPage, perPage,
totalPages: Math.ceil(total / perPage), totalPages: Math.ceil(total / perPage),
folder: actualFolder, folder: normalizedFolder,
mailboxes: [] mailboxes: []
}; };
} catch (error) { } catch (error) {
@ -423,55 +428,60 @@ export async function getEmailContent(
throw new Error('Email ID must be a number'); throw new Error('Email ID must be a number');
} }
// Remove accountId prefix if present in folder name
const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder;
// Extract account ID from folder name if present and none was explicitly provided // Extract account ID from folder name if present and none was explicitly provided
const effectiveAccountId = folder.includes(':') && !accountId ? folder.split(':')[0] : accountId; const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
// Use normalized folder name for cache key // Use the most specific account ID available
const cacheKey = effectiveAccountId || 'default'; const effectiveAccountId = folderAccountId || accountId || 'default';
const cachedEmail = await getCachedEmailContent(userId, cacheKey, emailId);
// Normalize folder name by removing account prefix if present
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
console.log(`[getEmailContent] Fetching email ${emailId} from folder ${normalizedFolder}, account ${effectiveAccountId}`);
// Use normalized folder name and effective account ID for cache key
const cachedEmail = await getCachedEmailContent(userId, effectiveAccountId, emailId);
if (cachedEmail) { if (cachedEmail) {
console.log(`Using cached email content for ${userId}:${cacheKey}:${emailId}`); console.log(`Using cached email content for ${userId}:${effectiveAccountId}:${emailId}`);
return cachedEmail; return cachedEmail;
} }
console.log(`Cache miss for email content ${userId}:${cacheKey}:${emailId}, fetching from IMAP`); console.log(`Cache miss for email content ${userId}:${effectiveAccountId}:${emailId}, fetching from IMAP`);
const client = await getImapConnection(userId, effectiveAccountId); const client = await getImapConnection(userId, effectiveAccountId);
try { try {
// Log connection details with account context // Log connection details with account context
console.log(`[DEBUG] Fetching email ${emailId} from folder ${actualFolder} for account ${effectiveAccountId || 'default'}`); console.log(`[DEBUG] Fetching email ${emailId} from folder ${normalizedFolder} for account ${effectiveAccountId}`);
// Open mailbox with error handling // Open mailbox with error handling
const mailbox = await client.mailboxOpen(actualFolder); const mailbox = await client.mailboxOpen(normalizedFolder);
if (!mailbox || typeof mailbox === 'boolean') { if (!mailbox || typeof mailbox === 'boolean') {
throw new Error(`Failed to open mailbox: ${actualFolder} for account ${effectiveAccountId || 'default'}`); throw new Error(`Failed to open mailbox: ${normalizedFolder} for account ${effectiveAccountId}`);
} }
// Log mailbox status with account context // Log mailbox status with account context
console.log(`[DEBUG] Mailbox ${actualFolder} opened for account ${effectiveAccountId || 'default'}, total messages: ${mailbox.exists}`); console.log(`[DEBUG] Mailbox ${normalizedFolder} opened for account ${effectiveAccountId}, total messages: ${mailbox.exists}`);
// Get the UIDVALIDITY and UIDNEXT values // Get the UIDVALIDITY and UIDNEXT values
const uidValidity = mailbox.uidValidity; const uidValidity = mailbox.uidValidity;
const uidNext = mailbox.uidNext; const uidNext = mailbox.uidNext;
console.log(`[DEBUG] Mailbox UIDVALIDITY: ${uidValidity}, UIDNEXT: ${uidNext} for account ${effectiveAccountId || 'default'}`); console.log(`[DEBUG] Mailbox UIDVALIDITY: ${uidValidity}, UIDNEXT: ${uidNext} for account ${effectiveAccountId}`);
// Validate UID exists in mailbox // Validate UID exists in mailbox
if (numericId >= uidNext) { if (numericId >= uidNext) {
throw new Error(`Email ID ${numericId} is greater than or equal to the highest UID in mailbox (${uidNext}) for account ${effectiveAccountId || 'default'}`); throw new Error(`Email ID ${numericId} is greater than or equal to the highest UID in mailbox (${uidNext}) for account ${effectiveAccountId}`);
} }
// First, try to get the sequence number for this UID // First, try to get the sequence number for this UID
const searchResult = await client.search({ uid: numericId.toString() }); const searchResult = await client.search({ uid: numericId.toString() });
if (!searchResult || searchResult.length === 0) { if (!searchResult || searchResult.length === 0) {
throw new Error(`Email with UID ${numericId} not found in folder ${actualFolder} for account ${effectiveAccountId || 'default'}`); throw new Error(`Email with UID ${numericId} not found in folder ${normalizedFolder} for account ${effectiveAccountId}`);
} }
const sequenceNumber = searchResult[0]; const sequenceNumber = searchResult[0];
console.log(`[DEBUG] Found sequence number ${sequenceNumber} for UID ${numericId} in account ${effectiveAccountId || 'default'}`); console.log(`[DEBUG] Found sequence number ${sequenceNumber} for UID ${numericId} in account ${effectiveAccountId}`);
// Now fetch using the sequence number // Now fetch using the sequence number
const message = await client.fetchOne(sequenceNumber.toString(), { const message = await client.fetchOne(sequenceNumber.toString(), {
@ -482,7 +492,7 @@ export async function getEmailContent(
}); });
if (!message) { if (!message) {
throw new Error(`Email not found with sequence number ${sequenceNumber} in folder ${actualFolder} for account ${effectiveAccountId || 'default'}`); throw new Error(`Email not found with sequence number ${sequenceNumber} in folder ${normalizedFolder} for account ${effectiveAccountId}`);
} }
const { source, envelope, flags, size } = message; const { source, envelope, flags, size } = message;
@ -537,21 +547,21 @@ export async function getEmailContent(
text: parsedEmail.text || '', text: parsedEmail.text || '',
html: rawHtml || '' html: rawHtml || ''
}, },
folder: actualFolder, folder: normalizedFolder,
contentFetched: true, contentFetched: true,
size: size || 0, size: size || 0,
accountId: effectiveAccountId || 'default' accountId: effectiveAccountId
}; };
// Cache the email content with account-specific key // Cache the email content with effective account ID
await cacheEmailContent(userId, cacheKey, emailId, email); await cacheEmailContent(userId, effectiveAccountId, emailId, email);
return email; return email;
} catch (error) { } catch (error) {
console.error('[ERROR] Email fetch failed:', { console.error('[ERROR] Email fetch failed:', {
userId, userId,
emailId, emailId,
folder: actualFolder, folder: normalizedFolder,
accountId: effectiveAccountId, accountId: effectiveAccountId,
error: error instanceof Error ? error.message : 'Unknown error', error: error instanceof Error ? error.message : 'Unknown error',
details: error instanceof Error ? error.stack : undefined details: error instanceof Error ? error.stack : undefined
@ -576,15 +586,21 @@ export async function markEmailReadStatus(
folder: string = 'INBOX', folder: string = 'INBOX',
accountId?: string accountId?: string
): Promise<boolean> { ): Promise<boolean> {
// Normalize folder name by removing account prefix if present
const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder;
// Extract account ID from folder name if present and none was explicitly provided // Extract account ID from folder name if present and none was explicitly provided
const effectiveAccountId = folder.includes(':') && !accountId ? folder.split(':')[0] : accountId; const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
// Use the most specific account ID available
const effectiveAccountId = folderAccountId || accountId || 'default';
// Normalize folder name by removing account prefix if present
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
console.log(`[markEmailReadStatus] Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${normalizedFolder}, account ${effectiveAccountId}`);
const client = await getImapConnection(userId, effectiveAccountId); const client = await getImapConnection(userId, effectiveAccountId);
try { try {
await client.mailboxOpen(actualFolder); await client.mailboxOpen(normalizedFolder);
if (isRead) { if (isRead) {
await client.messageFlagsAdd(emailId, ['\\Seen']); await client.messageFlagsAdd(emailId, ['\\Seen']);
@ -592,18 +608,15 @@ export async function markEmailReadStatus(
await client.messageFlagsRemove(emailId, ['\\Seen']); await client.messageFlagsRemove(emailId, ['\\Seen']);
} }
// Use normalized cacheKey for consistency
const cacheKey = effectiveAccountId || 'default';
// Invalidate content cache since the flags changed // Invalidate content cache since the flags changed
await invalidateEmailContentCache(userId, cacheKey, emailId); await invalidateEmailContentCache(userId, effectiveAccountId, emailId);
// Also invalidate folder cache because unread counts may have changed // Also invalidate folder cache because unread counts may have changed
await invalidateFolderCache(userId, cacheKey, actualFolder); await invalidateFolderCache(userId, effectiveAccountId, normalizedFolder);
return true; return true;
} catch (error) { } catch (error) {
console.error(`Error marking email ${emailId} as ${isRead ? 'read' : 'unread'}:`, error); console.error(`Error marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${normalizedFolder}, account ${effectiveAccountId}:`, error);
return false; return false;
} finally { } finally {
try { try {