courrier preview
This commit is contained in:
parent
96e16ea80d
commit
2eaca502b2
@ -39,9 +39,10 @@ export async function GET(request: Request) {
|
|||||||
const folder = searchParams.get("folder") || "INBOX";
|
const folder = searchParams.get("folder") || "INBOX";
|
||||||
const searchQuery = searchParams.get("search") || "";
|
const searchQuery = searchParams.get("search") || "";
|
||||||
const accountId = searchParams.get("accountId") || "";
|
const accountId = searchParams.get("accountId") || "";
|
||||||
|
const checkOnly = searchParams.get("checkOnly") === "true";
|
||||||
|
|
||||||
// CRITICAL FIX: Log exact parameters received by the API
|
// CRITICAL FIX: Log exact parameters received by the API
|
||||||
console.log(`[API] Received request with: folder=${folder}, accountId=${accountId}, page=${page}`);
|
console.log(`[API] Received request with: folder=${folder}, accountId=${accountId}, page=${page}, checkOnly=${checkOnly}`);
|
||||||
|
|
||||||
// CRITICAL FIX: More robust parameter normalization
|
// CRITICAL FIX: More robust parameter normalization
|
||||||
// 1. If folder contains an account prefix, extract it but DO NOT use it
|
// 1. If folder contains an account prefix, extract it but DO NOT use it
|
||||||
@ -61,8 +62,8 @@ export async function GET(request: Request) {
|
|||||||
// CRITICAL FIX: Enhanced logging for parameter resolution
|
// CRITICAL FIX: Enhanced logging for parameter resolution
|
||||||
console.log(`[API] Using normalized parameters: folder=${normalizedFolder}, accountId=${effectiveAccountId}`);
|
console.log(`[API] Using normalized parameters: folder=${normalizedFolder}, accountId=${effectiveAccountId}`);
|
||||||
|
|
||||||
// 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 and not checkOnly
|
||||||
if (!searchQuery) {
|
if (!searchQuery && !checkOnly) {
|
||||||
// CRITICAL FIX: Use consistent cache key format with the correct account ID
|
// CRITICAL FIX: Use consistent cache key format with the correct account ID
|
||||||
console.log(`[API] Checking Redis cache for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}`);
|
console.log(`[API] Checking Redis cache for ${session.user.id}:${effectiveAccountId}:${normalizedFolder}:${page}:${perPage}`);
|
||||||
const cachedEmails = await getCachedEmailList(
|
const cachedEmails = await getCachedEmailList(
|
||||||
@ -87,13 +88,14 @@ export async function GET(request: Request) {
|
|||||||
normalizedFolder, // folder (without prefix)
|
normalizedFolder, // folder (without prefix)
|
||||||
page, // page
|
page, // page
|
||||||
perPage, // perPage
|
perPage, // perPage
|
||||||
effectiveAccountId // accountId
|
effectiveAccountId, // accountId
|
||||||
|
checkOnly // checkOnly flag - only check for new emails without loading full content
|
||||||
);
|
);
|
||||||
|
|
||||||
// CRITICAL FIX: Log when emails are returned from IMAP
|
// CRITICAL FIX: Log when emails are returned from IMAP
|
||||||
console.log(`[API] Successfully fetched ${emailsResult.emails.length} emails from IMAP for account ${effectiveAccountId}`);
|
console.log(`[API] Successfully fetched ${emailsResult.emails.length} emails from IMAP for account ${effectiveAccountId}`);
|
||||||
|
|
||||||
// The result is already cached in the getEmails function
|
// The result is already cached in the getEmails function (if not checkOnly)
|
||||||
return NextResponse.json(emailsResult);
|
return NextResponse.json(emailsResult);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("[API] Error fetching emails:", error);
|
console.error("[API] Error fetching emails:", error);
|
||||||
|
|||||||
@ -964,6 +964,68 @@ export const useEmailState = () => {
|
|||||||
}
|
}
|
||||||
}, [dispatch, session?.user, state.isLoadingUnreadCounts, logEmailOp]);
|
}, [dispatch, session?.user, state.isLoadingUnreadCounts, logEmailOp]);
|
||||||
|
|
||||||
|
// Function to check for new emails without disrupting the user
|
||||||
|
const checkForNewEmails = useCallback(async () => {
|
||||||
|
if (!session?.user?.id || !state.selectedAccount) return;
|
||||||
|
|
||||||
|
// Don't check if already loading emails
|
||||||
|
if (state.isLoading) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get normalized parameters using helper function
|
||||||
|
const accountId = state.selectedAccount ? state.selectedAccount.id : undefined;
|
||||||
|
const { normalizedFolder, effectiveAccountId, prefixedFolder } =
|
||||||
|
normalizeFolderAndAccount(state.currentFolder, accountId);
|
||||||
|
|
||||||
|
logEmailOp('CHECK_NEW_EMAILS', `Checking for new emails in ${prefixedFolder}`);
|
||||||
|
|
||||||
|
// Quietly check for new emails with a special parameter
|
||||||
|
const queryParams = new URLSearchParams({
|
||||||
|
folder: normalizedFolder,
|
||||||
|
page: '1',
|
||||||
|
perPage: '10', // Just get the top 10 to check if any are new
|
||||||
|
accountId: effectiveAccountId,
|
||||||
|
checkOnly: 'true' // Special parameter to indicate this is just a check
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`/api/courrier/emails?${queryParams.toString()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
cache: 'no-cache'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to check for new emails: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// If we have emails and the newest one is different from our current newest
|
||||||
|
if (data.emails && data.emails.length > 0 && state.emails.length > 0) {
|
||||||
|
const newestEmailId = data.emails[0].id;
|
||||||
|
const currentNewestEmailId = state.emails[0].id;
|
||||||
|
|
||||||
|
if (newestEmailId !== currentNewestEmailId) {
|
||||||
|
logEmailOp('NEW_EMAILS', `Found new emails, newest ID: ${newestEmailId}`);
|
||||||
|
|
||||||
|
// Show a toast notification
|
||||||
|
toast({
|
||||||
|
title: "New emails",
|
||||||
|
description: "You have new emails in your inbox",
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh the current view to show the new emails
|
||||||
|
loadEmails(state.page, state.perPage, false);
|
||||||
|
} else {
|
||||||
|
logEmailOp('CHECK_NEW_EMAILS', 'No new emails found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking for new emails:', error);
|
||||||
|
}
|
||||||
|
}, [session?.user?.id, state.selectedAccount, state.currentFolder, state.isLoading, state.emails, state.page, state.perPage, toast, loadEmails, logEmailOp]);
|
||||||
|
|
||||||
// Calculate and update unread counts
|
// Calculate and update unread counts
|
||||||
const updateUnreadCounts = useCallback(() => {
|
const updateUnreadCounts = useCallback(() => {
|
||||||
// Skip if no emails or accounts
|
// Skip if no emails or accounts
|
||||||
@ -1023,6 +1085,23 @@ export const useEmailState = () => {
|
|||||||
// Deliberately exclude unreadCountMap to prevent infinite loops
|
// Deliberately exclude unreadCountMap to prevent infinite loops
|
||||||
}, [state.emails, updateUnreadCounts]);
|
}, [state.emails, updateUnreadCounts]);
|
||||||
|
|
||||||
|
// Set up periodic check for new emails
|
||||||
|
useEffect(() => {
|
||||||
|
if (!state.emails || state.emails.length === 0) return;
|
||||||
|
|
||||||
|
// Set up a periodic check for new emails at the same interval as unread counts
|
||||||
|
const checkNewEmailsId = setInterval(() => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
checkForNewEmails();
|
||||||
|
}
|
||||||
|
}, 60000); // 1 minute - same as unread count refresh
|
||||||
|
|
||||||
|
// Cleanup interval on unmount or state change
|
||||||
|
return () => {
|
||||||
|
clearInterval(checkNewEmailsId);
|
||||||
|
};
|
||||||
|
}, [state.emails, checkForNewEmails]);
|
||||||
|
|
||||||
// Tracking when an email is viewed to optimize unread count refreshes
|
// Tracking when an email is viewed to optimize unread count refreshes
|
||||||
const lastViewedEmailRef = useRef<number | null>(null);
|
const lastViewedEmailRef = useRef<number | null>(null);
|
||||||
const fetchFailuresRef = useRef<number>(0);
|
const fetchFailuresRef = useRef<number>(0);
|
||||||
@ -1076,6 +1155,7 @@ export const useEmailState = () => {
|
|||||||
selectAccount,
|
selectAccount,
|
||||||
handleLoadMore,
|
handleLoadMore,
|
||||||
fetchUnreadCounts,
|
fetchUnreadCounts,
|
||||||
viewEmail
|
viewEmail,
|
||||||
|
checkForNewEmails
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -28,6 +28,7 @@ export interface EmailListResult {
|
|||||||
totalPages: number;
|
totalPages: number;
|
||||||
folder: string;
|
folder: string;
|
||||||
mailboxes: string[];
|
mailboxes: string[];
|
||||||
|
newestEmailId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection pool to reuse IMAP clients
|
// Connection pool to reuse IMAP clients
|
||||||
@ -482,10 +483,11 @@ export async function getEmails(
|
|||||||
folder: string,
|
folder: string,
|
||||||
page: number = 1,
|
page: number = 1,
|
||||||
perPage: number = 20,
|
perPage: number = 20,
|
||||||
accountId?: string
|
accountId?: string,
|
||||||
|
checkOnly: boolean = false
|
||||||
): Promise<EmailListResult> {
|
): Promise<EmailListResult> {
|
||||||
// Normalize folder name and handle account ID
|
// Normalize folder name and handle account ID
|
||||||
console.log(`[getEmails] Processing request for folder: ${folder}, normalized to ${folder}, account: ${accountId || 'default'}`);
|
console.log(`[getEmails] Processing request for folder: ${folder}, normalized to ${folder}, account: ${accountId || 'default'}, checkOnly: ${checkOnly}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// The getImapConnection function already handles 'default' accountId by finding the first available account
|
// The getImapConnection function already handles 'default' accountId by finding the first available account
|
||||||
@ -517,21 +519,57 @@ export async function getEmails(
|
|||||||
perPage,
|
perPage,
|
||||||
totalPages: 0,
|
totalPages: 0,
|
||||||
folder,
|
folder,
|
||||||
mailboxes
|
mailboxes,
|
||||||
|
newestEmailId: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
await cacheEmailList(
|
// Only cache if not in checkOnly mode
|
||||||
userId,
|
if (!checkOnly) {
|
||||||
resolvedAccountId, // Use the guaranteed string account ID
|
await cacheEmailList(
|
||||||
folder,
|
userId,
|
||||||
page,
|
resolvedAccountId, // Use the guaranteed string account ID
|
||||||
perPage,
|
folder,
|
||||||
emptyResult
|
page,
|
||||||
);
|
perPage,
|
||||||
|
emptyResult
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return emptyResult;
|
return emptyResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If checkOnly mode, we just fetch the most recent email's ID to compare
|
||||||
|
if (checkOnly) {
|
||||||
|
console.log(`[getEmails] checkOnly mode: fetching only the most recent email ID`);
|
||||||
|
|
||||||
|
// Get the most recent message (highest sequence number)
|
||||||
|
const lastMessageSequence = totalEmails.toString();
|
||||||
|
console.log(`[getEmails] Fetching latest message with sequence: ${lastMessageSequence}`);
|
||||||
|
|
||||||
|
const messages = await client.fetch(lastMessageSequence, {
|
||||||
|
uid: true
|
||||||
|
});
|
||||||
|
|
||||||
|
let newestEmailId = 0;
|
||||||
|
for await (const message of messages) {
|
||||||
|
newestEmailId = message.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[getEmails] Latest email UID: ${newestEmailId}`);
|
||||||
|
|
||||||
|
// Return minimal result with just the newest email ID
|
||||||
|
return {
|
||||||
|
emails: [],
|
||||||
|
totalEmails,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
totalPages,
|
||||||
|
folder,
|
||||||
|
mailboxes,
|
||||||
|
newestEmailId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate message range for pagination
|
// Calculate message range for pagination
|
||||||
const start = Math.max(1, totalEmails - (page * perPage) + 1);
|
const start = Math.max(1, totalEmails - (page * perPage) + 1);
|
||||||
const end = Math.max(1, totalEmails - ((page - 1) * perPage));
|
const end = Math.max(1, totalEmails - ((page - 1) * perPage));
|
||||||
@ -541,11 +579,19 @@ export async function getEmails(
|
|||||||
const messages = await client.fetch(`${start}:${end}`, {
|
const messages = await client.fetch(`${start}:${end}`, {
|
||||||
envelope: true,
|
envelope: true,
|
||||||
flags: true,
|
flags: true,
|
||||||
bodyStructure: true
|
bodyStructure: true,
|
||||||
|
uid: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const emails: EmailMessage[] = [];
|
const emails: EmailMessage[] = [];
|
||||||
|
let newestEmailId = 0;
|
||||||
|
|
||||||
for await (const message of messages) {
|
for await (const message of messages) {
|
||||||
|
// Track the newest email ID (highest UID)
|
||||||
|
if (message.uid > newestEmailId) {
|
||||||
|
newestEmailId = message.uid;
|
||||||
|
}
|
||||||
|
|
||||||
const email: EmailMessage = {
|
const email: EmailMessage = {
|
||||||
id: message.uid.toString(),
|
id: message.uid.toString(),
|
||||||
from: message.envelope.from?.map(addr => ({
|
from: message.envelope.from?.map(addr => ({
|
||||||
@ -578,33 +624,31 @@ export async function getEmails(
|
|||||||
emails.push(email);
|
emails.push(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the result with the effective account ID
|
// Prepare the result
|
||||||
await cacheEmailList(
|
const result = {
|
||||||
userId,
|
|
||||||
resolvedAccountId, // Use the guaranteed string account ID
|
|
||||||
folder,
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
{
|
|
||||||
emails,
|
|
||||||
totalEmails: totalEmails,
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
totalPages: Math.ceil(totalEmails / perPage),
|
|
||||||
folder: folder,
|
|
||||||
mailboxes: mailboxes
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
emails,
|
emails,
|
||||||
totalEmails: totalEmails,
|
totalEmails,
|
||||||
page,
|
page,
|
||||||
perPage,
|
perPage,
|
||||||
totalPages: Math.ceil(totalEmails / perPage),
|
totalPages: Math.ceil(totalEmails / perPage),
|
||||||
folder: folder,
|
folder,
|
||||||
mailboxes: mailboxes
|
mailboxes,
|
||||||
|
newestEmailId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cache the result with the effective account ID (only if not in checkOnly mode)
|
||||||
|
if (!checkOnly) {
|
||||||
|
await cacheEmailList(
|
||||||
|
userId,
|
||||||
|
resolvedAccountId, // Use the guaranteed string account ID
|
||||||
|
folder,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
result
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching emails:', error);
|
console.error('Error fetching emails:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user