diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 7e1c49f..b567b1c 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -960,6 +960,14 @@ export async function getEmails( skip ); + logger.debug('[EMAIL] Graph API returned emails', { + userId, + folder, + mailCredentialId: graphCheck.mailCredentialId, + count: graphResult.value?.length || 0, + hasNextLink: !!graphResult['@odata.nextLink'], + }); + // Get mailboxes (folders) const graphFolders = await fetchGraphMailFolders(graphCheck.mailCredentialId); const mailboxes = graphFolders.map(f => f.displayName); @@ -969,6 +977,21 @@ export async function getEmails( convertGraphMessageToEmailMessage(msg, folder, accountId || 'default') ); + // Ensure emails are sorted by date (newest first) - Graph API should already do this, but double-check + emails.sort((a, b) => { + const dateA = a.date instanceof Date ? a.date.getTime() : new Date(a.date).getTime(); + const dateB = b.date instanceof Date ? b.date.getTime() : new Date(b.date).getTime(); + return dateB - dateA; // Descending order (newest first) + }); + + logger.debug('[EMAIL] Converted and sorted emails', { + userId, + folder, + count: emails.length, + firstEmailDate: emails[0]?.date, + lastEmailDate: emails[emails.length - 1]?.date, + }); + // Calculate total (Graph API doesn't provide total count directly, so we estimate) const totalEmails = graphResult['@odata.nextLink'] ? (page * perPage) + 1 // Has more pages diff --git a/lib/services/microsoft-graph-mail.ts b/lib/services/microsoft-graph-mail.ts index 329b3f8..51d5972 100644 --- a/lib/services/microsoft-graph-mail.ts +++ b/lib/services/microsoft-graph-mail.ts @@ -99,13 +99,15 @@ export interface GraphMailFolder { /** * Fetch emails from a Microsoft mailbox folder using Graph API + * Note: Microsoft Graph API doesn't support $skip for pagination, only $top and $skipToken */ export async function fetchGraphEmails( mailCredentialId: string, folderId: string = 'Inbox', top: number = 50, skip: number = 0, - filter?: string + filter?: string, + skipToken?: string ): Promise<{ value: GraphMailMessage[]; '@odata.nextLink'?: string; @@ -117,19 +119,47 @@ export async function fetchGraphEmails( let url = `/me/mailFolders/${folderId}/messages`; const params = new URLSearchParams({ '$top': top.toString(), - '$skip': skip.toString(), '$orderby': 'receivedDateTime desc', '$select': 'id,subject,from,toRecipients,ccRecipients,body,bodyPreview,receivedDateTime,sentDateTime,isRead,hasAttachments,importance,flag', }); + // Microsoft Graph API supports $skip for messages endpoint, but it's more reliable to use $skipToken + // For the first page (skip=0), don't use $skip + // For subsequent pages, we can use $skip but $skipToken is preferred + if (skip > 0 && !skipToken) { + params.append('$skip', skip.toString()); + } + + // Use skipToken if provided (for server-driven pagination from @odata.nextLink) + // This is the preferred method for pagination in Graph API + if (skipToken) { + params.append('$skiptoken', skipToken); + } + if (filter) { params.append('$filter', filter); } url += `?${params.toString()}`; + logger.debug('Fetching emails from Microsoft Graph API', { + mailCredentialId, + folderId, + top, + skip, + skipToken: skipToken ? 'present' : 'none', + url, + }); + const response = await client.get(url); + logger.debug('Microsoft Graph API response', { + mailCredentialId, + folderId, + emailCount: response.data?.value?.length || 0, + hasNextLink: !!response.data?.['@odata.nextLink'], + }); + return response.data; } catch (error: any) { logger.error('Error fetching emails from Microsoft Graph', {