courrier multi account restore compose
This commit is contained in:
parent
6bdc3bba5a
commit
764f194a72
@ -43,13 +43,17 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: 'Email ID is required' }, { status: 400 });
|
return NextResponse.json({ error: 'Email ID is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { folder = 'INBOX', accountId } = await request.json();
|
const { folder = 'INBOX', accountId, isRead = true } = await request.json();
|
||||||
|
|
||||||
|
// Log operation details for debugging
|
||||||
|
console.log(`Marking email ${emailId} as ${isRead ? 'read' : 'unread'} in folder ${folder}${accountId ? ` for account ${accountId}` : ''}`);
|
||||||
|
|
||||||
const success = await markEmailReadStatus(
|
const success = await markEmailReadStatus(
|
||||||
session.user.id,
|
session.user.id,
|
||||||
emailId,
|
emailId,
|
||||||
true,
|
isRead,
|
||||||
folder
|
folder,
|
||||||
|
accountId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
|||||||
@ -334,6 +334,51 @@ export const useCourrier = () => {
|
|||||||
}
|
}
|
||||||
}, [currentFolder]);
|
}, [currentFolder]);
|
||||||
|
|
||||||
|
// Mark an email as read/unread
|
||||||
|
const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean) => {
|
||||||
|
try {
|
||||||
|
// Find the email to get its accountId
|
||||||
|
const emailToMark = emails.find(e => e.id === emailId);
|
||||||
|
if (!emailToMark) {
|
||||||
|
throw new Error('Email not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the accountId from the email
|
||||||
|
const emailAccountId = emailToMark.accountId;
|
||||||
|
|
||||||
|
const response = await fetch(`/api/courrier/${emailId}/mark-read`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
isRead,
|
||||||
|
folder: currentFolder,
|
||||||
|
accountId: emailAccountId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to mark email as read');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the email in the list
|
||||||
|
setEmails(emails.map(email =>
|
||||||
|
email.id === emailId ? { ...email, flags: { ...email.flags, seen: isRead } } : email
|
||||||
|
));
|
||||||
|
|
||||||
|
// If the selected email is the one being marked, update it too
|
||||||
|
if (selectedEmail && selectedEmail.id === emailId) {
|
||||||
|
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
|
// 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) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@ -373,42 +418,7 @@ export const useCourrier = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [emails, fetchEmailContent, toast]);
|
}, [emails, fetchEmailContent, markEmailAsRead, toast]);
|
||||||
|
|
||||||
// Mark an email as read/unread
|
|
||||||
const markEmailAsRead = useCallback(async (emailId: string, isRead: boolean) => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/courrier/${emailId}/mark-read`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
isRead,
|
|
||||||
folder: currentFolder
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to mark email as read');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the email in the list
|
|
||||||
setEmails(emails.map(email =>
|
|
||||||
email.id === emailId ? { ...email, flags: { ...email.flags, seen: isRead } } : email
|
|
||||||
));
|
|
||||||
|
|
||||||
// If the selected email is the one being marked, update it too
|
|
||||||
if (selectedEmail && selectedEmail.id === emailId) {
|
|
||||||
setSelectedEmail({ ...selectedEmail, flags: { ...selectedEmail.flags, seen: isRead } });
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error marking email as read:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, [emails, selectedEmail, currentFolder]);
|
|
||||||
|
|
||||||
// Toggle starred status for an email
|
// Toggle starred status for an email
|
||||||
const toggleStarred = useCallback(async (emailId: string) => {
|
const toggleStarred = useCallback(async (emailId: string) => {
|
||||||
|
|||||||
@ -423,53 +423,55 @@ export async function getEmailContent(
|
|||||||
throw new Error('Email ID must be a number');
|
throw new Error('Email ID must be a number');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get from cache first, using account-specific cache key
|
// Remove accountId prefix if present in folder name
|
||||||
const cacheKey = accountId ? `${accountId}:${folder}` : folder;
|
const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder;
|
||||||
|
// Extract account ID from folder name if present and none was explicitly provided
|
||||||
|
const effectiveAccountId = folder.includes(':') && !accountId ? folder.split(':')[0] : accountId;
|
||||||
|
|
||||||
|
// Use normalized folder name for cache key
|
||||||
|
const cacheKey = effectiveAccountId || 'default';
|
||||||
const cachedEmail = await getCachedEmailContent(userId, cacheKey, emailId);
|
const cachedEmail = await getCachedEmailContent(userId, cacheKey, emailId);
|
||||||
if (cachedEmail) {
|
if (cachedEmail) {
|
||||||
console.log(`Using cached email content for ${userId}:${accountId}:${emailId}`);
|
console.log(`Using cached email content for ${userId}:${cacheKey}:${emailId}`);
|
||||||
return cachedEmail;
|
return cachedEmail;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Cache miss for email content ${userId}:${accountId}:${emailId}, fetching from IMAP`);
|
console.log(`Cache miss for email content ${userId}:${cacheKey}:${emailId}, fetching from IMAP`);
|
||||||
|
|
||||||
const client = await getImapConnection(userId, accountId);
|
const client = await getImapConnection(userId, effectiveAccountId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Remove accountId prefix if present in folder name
|
|
||||||
const actualFolder = folder.includes(':') ? folder.split(':')[1] : folder;
|
|
||||||
|
|
||||||
// Log connection details with account context
|
// Log connection details with account context
|
||||||
console.log(`[DEBUG] Fetching email ${emailId} from folder ${actualFolder} for account ${accountId || 'default'}`);
|
console.log(`[DEBUG] Fetching email ${emailId} from folder ${actualFolder} for account ${effectiveAccountId || 'default'}`);
|
||||||
|
|
||||||
// Open mailbox with error handling
|
// Open mailbox with error handling
|
||||||
const mailbox = await client.mailboxOpen(actualFolder);
|
const mailbox = await client.mailboxOpen(actualFolder);
|
||||||
if (!mailbox || typeof mailbox === 'boolean') {
|
if (!mailbox || typeof mailbox === 'boolean') {
|
||||||
throw new Error(`Failed to open mailbox: ${actualFolder} for account ${accountId || 'default'}`);
|
throw new Error(`Failed to open mailbox: ${actualFolder} for account ${effectiveAccountId || 'default'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log mailbox status with account context
|
// Log mailbox status with account context
|
||||||
console.log(`[DEBUG] Mailbox ${actualFolder} opened for account ${accountId || 'default'}, total messages: ${mailbox.exists}`);
|
console.log(`[DEBUG] Mailbox ${actualFolder} opened for account ${effectiveAccountId || 'default'}, 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 ${accountId || 'default'}`);
|
console.log(`[DEBUG] Mailbox UIDVALIDITY: ${uidValidity}, UIDNEXT: ${uidNext} for account ${effectiveAccountId || 'default'}`);
|
||||||
|
|
||||||
// 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 ${accountId || 'default'}`);
|
throw new Error(`Email ID ${numericId} is greater than or equal to the highest UID in mailbox (${uidNext}) for account ${effectiveAccountId || 'default'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 ${accountId || 'default'}`);
|
throw new Error(`Email with UID ${numericId} not found in folder ${actualFolder} for account ${effectiveAccountId || 'default'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sequenceNumber = searchResult[0];
|
const sequenceNumber = searchResult[0];
|
||||||
console.log(`[DEBUG] Found sequence number ${sequenceNumber} for UID ${numericId} in account ${accountId || 'default'}`);
|
console.log(`[DEBUG] Found sequence number ${sequenceNumber} for UID ${numericId} in account ${effectiveAccountId || 'default'}`);
|
||||||
|
|
||||||
// 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(), {
|
||||||
@ -480,7 +482,7 @@ export async function getEmailContent(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
throw new Error(`Email not found with sequence number ${sequenceNumber} in folder ${actualFolder} for account ${accountId || 'default'}`);
|
throw new Error(`Email not found with sequence number ${sequenceNumber} in folder ${actualFolder} for account ${effectiveAccountId || 'default'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { source, envelope, flags, size } = message;
|
const { source, envelope, flags, size } = message;
|
||||||
@ -538,7 +540,7 @@ export async function getEmailContent(
|
|||||||
folder: actualFolder,
|
folder: actualFolder,
|
||||||
contentFetched: true,
|
contentFetched: true,
|
||||||
size: size || 0,
|
size: size || 0,
|
||||||
accountId: accountId || 'default'
|
accountId: effectiveAccountId || 'default'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cache the email content with account-specific key
|
// Cache the email content with account-specific key
|
||||||
@ -549,8 +551,8 @@ export async function getEmailContent(
|
|||||||
console.error('[ERROR] Email fetch failed:', {
|
console.error('[ERROR] Email fetch failed:', {
|
||||||
userId,
|
userId,
|
||||||
emailId,
|
emailId,
|
||||||
folder,
|
folder: actualFolder,
|
||||||
accountId,
|
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
|
||||||
});
|
});
|
||||||
@ -571,12 +573,18 @@ export async function markEmailReadStatus(
|
|||||||
userId: string,
|
userId: string,
|
||||||
emailId: string,
|
emailId: string,
|
||||||
isRead: boolean,
|
isRead: boolean,
|
||||||
folder: string = 'INBOX'
|
folder: string = 'INBOX',
|
||||||
|
accountId?: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const client = await getImapConnection(userId);
|
// 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
|
||||||
|
const effectiveAccountId = folder.includes(':') && !accountId ? folder.split(':')[0] : accountId;
|
||||||
|
|
||||||
|
const client = await getImapConnection(userId, effectiveAccountId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.mailboxOpen(folder);
|
await client.mailboxOpen(actualFolder);
|
||||||
|
|
||||||
if (isRead) {
|
if (isRead) {
|
||||||
await client.messageFlagsAdd(emailId, ['\\Seen']);
|
await client.messageFlagsAdd(emailId, ['\\Seen']);
|
||||||
@ -584,11 +592,14 @@ 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, folder, emailId);
|
await invalidateEmailContentCache(userId, cacheKey, emailId);
|
||||||
|
|
||||||
// Also invalidate folder cache because unread counts may have changed
|
// Also invalidate folder cache because unread counts may have changed
|
||||||
await invalidateFolderCache(userId, folder, folder);
|
await invalidateFolderCache(userId, cacheKey, actualFolder);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -68,17 +68,26 @@ export async function getCachedEmailsWithTimeout(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize folder name by removing account prefix if present
|
||||||
|
// This ensures consistent cache key format regardless of how folder name is passed
|
||||||
|
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
|
||||||
|
|
||||||
|
// Log the normalization for debugging
|
||||||
|
if (folder !== normalizedFolder) {
|
||||||
|
console.log(`Normalized folder name from ${folder} to ${normalizedFolder} for cache lookup`);
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
console.log(`Cache access timeout for ${userId}:${folder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}`);
|
console.log(`Cache access timeout for ${userId}:${normalizedFolder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}`);
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
|
|
||||||
getCachedEmailList(userId, accountId || 'default', folder, page, perPage)
|
getCachedEmailList(userId, accountId || 'default', normalizedFolder, page, perPage)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
if (result) {
|
if (result) {
|
||||||
console.log(`Using cached data for ${userId}:${folder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}`);
|
console.log(`Using cached data for ${userId}:${normalizedFolder}:${page}:${perPage}${accountId ? ` for account ${accountId}` : ''}`);
|
||||||
resolve(result);
|
resolve(result);
|
||||||
} else {
|
} else {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
@ -100,9 +109,14 @@ export async function refreshEmailsInBackground(
|
|||||||
userId: string,
|
userId: string,
|
||||||
folder: string = 'INBOX',
|
folder: string = 'INBOX',
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
perPage: number = 20
|
perPage: number = 20,
|
||||||
|
accountId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const prefetchKey = `refresh:${folder}:${page}`;
|
// Normalize folder name by removing account prefix if present
|
||||||
|
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
|
||||||
|
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
|
||||||
|
|
||||||
|
const prefetchKey = `refresh:${normalizedFolder}:${page}:${folderAccountId || ''}`;
|
||||||
|
|
||||||
// Skip if already in progress or in cooldown
|
// Skip if already in progress or in cooldown
|
||||||
if (!shouldPrefetch(userId, prefetchKey)) {
|
if (!shouldPrefetch(userId, prefetchKey)) {
|
||||||
@ -112,9 +126,9 @@ export async function refreshEmailsInBackground(
|
|||||||
// Use setTimeout to ensure this runs after current execution context
|
// Use setTimeout to ensure this runs after current execution context
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
console.log(`Background refresh for ${userId}:${folder}:${page}:${perPage}`);
|
console.log(`Background refresh for ${userId}:${normalizedFolder}:${page}:${perPage}${folderAccountId ? ` for account ${folderAccountId}` : ''}`);
|
||||||
const freshData = await getEmails(userId, folder, page, perPage);
|
const freshData = await getEmails(userId, normalizedFolder, page, perPage, folderAccountId);
|
||||||
console.log(`Background refresh completed for ${userId}:${folder}`);
|
console.log(`Background refresh completed for ${userId}:${normalizedFolder}${folderAccountId ? ` for account ${folderAccountId}` : ''}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Background refresh error:', error);
|
console.error('Background refresh error:', error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -218,7 +232,11 @@ export async function prefetchFolderEmails(
|
|||||||
startPage: number = 1,
|
startPage: number = 1,
|
||||||
accountId?: string
|
accountId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const prefetchKey = `folder:${folder}:${startPage}${accountId ? `:${accountId}` : ''}`;
|
// Normalize folder name by removing account prefix if present
|
||||||
|
const normalizedFolder = folder.includes(':') ? folder.split(':')[1] : folder;
|
||||||
|
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
|
||||||
|
|
||||||
|
const prefetchKey = `folder:${normalizedFolder}:${startPage}:${folderAccountId || ''}`;
|
||||||
|
|
||||||
// Skip if already in progress or in cooldown
|
// Skip if already in progress or in cooldown
|
||||||
if (!shouldPrefetch(userId, prefetchKey)) {
|
if (!shouldPrefetch(userId, prefetchKey)) {
|
||||||
@ -226,7 +244,7 @@ export async function prefetchFolderEmails(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`Prefetching ${pages} pages of emails for folder ${folder} starting from page ${startPage}${accountId ? ` for account ${accountId}` : ''}`);
|
console.log(`Prefetching ${pages} pages of emails for folder ${normalizedFolder} starting from page ${startPage}${folderAccountId ? ` for account ${folderAccountId}` : ''}`);
|
||||||
|
|
||||||
// Calculate the range of pages to prefetch
|
// Calculate the range of pages to prefetch
|
||||||
const pagesToFetch = Array.from(
|
const pagesToFetch = Array.from(
|
||||||
@ -239,21 +257,21 @@ export async function prefetchFolderEmails(
|
|||||||
// Fetch multiple pages in parallel
|
// Fetch multiple pages in parallel
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
pagesToFetch.map(page =>
|
pagesToFetch.map(page =>
|
||||||
getEmails(userId, folder, page, 20)
|
getEmails(userId, normalizedFolder, page, 20, folderAccountId)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
console.log(`Successfully prefetched and cached page ${page} of ${folder} with ${result.emails.length} emails`);
|
console.log(`Successfully prefetched and cached page ${page} of ${normalizedFolder} with ${result.emails.length} emails`);
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(`Error prefetching page ${page} of ${folder}:`, err);
|
console.error(`Error prefetching page ${page} of ${normalizedFolder}:`, err);
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`Completed prefetching ${pages} pages of ${folder}`);
|
console.log(`Completed prefetching ${pages} pages for ${normalizedFolder}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error prefetching folder ${folder}:`, error);
|
console.error(`Error during folder prefetch:`, error);
|
||||||
} finally {
|
} finally {
|
||||||
markPrefetchCompleted(userId, prefetchKey);
|
markPrefetchCompleted(userId, prefetchKey);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user