diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 1606594..7e1c49f 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -982,7 +982,9 @@ export async function getEmails( totalPages: Math.ceil(totalEmails / perPage), folder, mailboxes, - newestEmailId: emails.length > 0 ? parseInt(emails[0].id) || 0 : 0, + // For Graph API, IDs are strings, so we can't use parseInt + // Use a hash or just use the first email's ID as string, then convert to number if needed + newestEmailId: emails.length > 0 ? (emails[0].id.match(/^\d+$/) ? parseInt(emails[0].id) : 0) : 0, }; // Cache the result @@ -1219,6 +1221,7 @@ function mapAddresses(addresses: any[] | undefined): Array<{ name: string; addre /** * Get a single email with full content + * Supports both IMAP (numeric UIDs) and Microsoft Graph API (string IDs) */ export async function getEmailContent( userId: string, @@ -1231,17 +1234,6 @@ export async function getEmailContent( throw new Error('Missing required parameters'); } - // Validate UID format - if (!/^\d+$/.test(emailId)) { - throw new Error('Invalid email ID format: must be a numeric UID'); - } - - // Convert to number for IMAP - const numericId = parseInt(emailId, 10); - if (isNaN(numericId)) { - throw new Error('Email ID must be a number'); - } - // Extract account ID from folder name if present and none was explicitly provided const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId; @@ -1258,6 +1250,99 @@ export async function getEmailContent( accountId: effectiveAccountId, }); + // Check if this is a Microsoft account that should use Graph API + // Graph API uses string IDs (not numeric UIDs), so if emailId is not numeric, it's likely Graph + const isGraphId = !/^\d+$/.test(emailId); + const graphCheck = await shouldUseGraphAPI(userId, effectiveAccountId); + + if (isGraphId && graphCheck.useGraph && graphCheck.mailCredentialId) { + // Use Microsoft Graph API for Microsoft accounts with Graph IDs + logger.debug('[EMAIL] Fetching email content via Microsoft Graph API', { + userId, + emailId, + mailCredentialId: graphCheck.mailCredentialId, + }); + + try { + // Use normalized folder name and effective account ID for cache key + const cachedEmail = await getCachedEmailContent(userId, effectiveAccountId, emailId); + if (cachedEmail) { + logger.debug('[EMAIL] Using cached email content (Graph)', { + userId, + accountId: effectiveAccountId, + emailId, + }); + return cachedEmail; + } + + // Fetch from Graph API + const { fetchGraphEmail } = await import('./microsoft-graph-mail'); + const graphMessage = await fetchGraphEmail(graphCheck.mailCredentialId, emailId); + + // Convert Graph message to EmailMessage format + const email: EmailMessage = { + id: graphMessage.id, + from: graphMessage.from ? [{ + name: graphMessage.from.emailAddress.name || '', + address: graphMessage.from.emailAddress.address, + }] : [], + to: graphMessage.toRecipients?.map(recipient => ({ + name: recipient.emailAddress.name || '', + address: recipient.emailAddress.address, + })) || [], + cc: graphMessage.ccRecipients?.map(recipient => ({ + name: recipient.emailAddress.name || '', + address: recipient.emailAddress.address, + })), + subject: graphMessage.subject || '', + date: new Date(graphMessage.receivedDateTime), + flags: { + seen: graphMessage.isRead, + flagged: graphMessage.flag?.flagStatus === 'flagged' || graphMessage.flag?.flagStatus === 'complete', + answered: false, + draft: false, + deleted: false, + }, + size: 0, + hasAttachments: graphMessage.hasAttachments || false, + folder: normalizedFolder, + contentFetched: true, + accountId: effectiveAccountId, + content: { + text: graphMessage.body?.contentType === 'text' ? (graphMessage.body.content || '') : '', + html: graphMessage.body?.contentType === 'html' ? (graphMessage.body.content || '') : (graphMessage.bodyPreview || ''), + isHtml: graphMessage.body?.contentType === 'html', + direction: 'ltr', + }, + preview: graphMessage.bodyPreview, + }; + + // Cache the email content + await cacheEmailContent(userId, effectiveAccountId, emailId, email); + + return email; + } catch (error) { + logger.error('[EMAIL] Error fetching email content from Graph API', { + userId, + emailId, + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + } + + // Use IMAP for non-Microsoft accounts or numeric UIDs + // Validate UID format for IMAP + if (!/^\d+$/.test(emailId)) { + throw new Error('Invalid email ID format: must be a numeric UID for IMAP accounts'); + } + + // Convert to number for IMAP + const numericId = parseInt(emailId, 10); + if (isNaN(numericId)) { + throw new Error('Email ID must be a number for IMAP accounts'); + } + // Use normalized folder name and effective account ID for cache key const cachedEmail = await getCachedEmailContent(userId, effectiveAccountId, emailId); if (cachedEmail) { @@ -1441,6 +1526,7 @@ export async function getEmailContent( /** * Mark an email as read or unread + * Supports both IMAP (numeric UIDs) and Microsoft Graph API (string IDs) */ export async function markEmailReadStatus( userId: string, @@ -1466,6 +1552,41 @@ export async function markEmailReadStatus( accountId: effectiveAccountId, }); + // Check if this is a Microsoft account that should use Graph API + // Graph API uses string IDs (not numeric UIDs), so if emailId is not numeric, it's likely Graph + const isGraphId = !/^\d+$/.test(emailId); + const graphCheck = await shouldUseGraphAPI(userId, effectiveAccountId); + + if (isGraphId && graphCheck.useGraph && graphCheck.mailCredentialId) { + // Use Microsoft Graph API for Microsoft accounts with Graph IDs + logger.debug('[EMAIL] Marking email as read/unread via Microsoft Graph API', { + userId, + emailId, + isRead, + mailCredentialId: graphCheck.mailCredentialId, + }); + + try { + const { markGraphEmailAsRead } = await import('./microsoft-graph-mail'); + await markGraphEmailAsRead(graphCheck.mailCredentialId, emailId, isRead); + return true; + } catch (error) { + logger.error('[EMAIL] Error marking email as read/unread via Graph API', { + userId, + emailId, + isRead, + error: error instanceof Error ? error.message : String(error), + }); + return false; + } + } + + // Use IMAP for non-Microsoft accounts or numeric UIDs + // Validate UID format for IMAP + if (!/^\d+$/.test(emailId)) { + throw new Error('Invalid email ID format: must be a numeric UID for IMAP accounts'); + } + const client = await getImapConnection(userId, effectiveAccountId); try {