/** * Email folder utilities */ const { callGraphAPI } = require('../utils/graph-api'); const { normalizeMailbox, buildPath, withMailboxHeaders } = require('../utils/mailbox'); /** * Cache of folder information to reduce API calls * Format: { userId: { folderName: { id, path } } } */ const folderCache = {}; /** * Resolve a folder name to its endpoint path * @param {string} accessToken - Access token * @param {string} folderName - Folder name to resolve * @param {string|object|null} mailbox - Optional shared mailbox UPN/SMTP or normalizeMailbox ctx * @returns {Promise} - Resolved endpoint path */ async function resolveFolderPath(accessToken, folderName, mailbox = null) { const mb = typeof mailbox === 'object' && mailbox && mailbox.graphRoot ? mailbox : normalizeMailbox(mailbox); const headers = { headers: withMailboxHeaders(mb) }; // Default to inbox if no folder specified if (!folderName) { return buildPath(mb, 'messages'); } // Handle well-known folder names (case-insensitive) // Note: 'inbox' uses the messages shortcut; SentItems aliases included for search UX const wellKnownRelative = { 'inbox': 'messages', 'drafts': 'mailFolders/drafts/messages', 'sent': 'mailFolders/sentItems/messages', 'sentitems': 'mailFolders/sentItems/messages', 'deleted': 'mailFolders/deletedItems/messages', 'deleteditems': 'mailFolders/deletedItems/messages', 'junk': 'mailFolders/junkemail/messages', 'junkemail': 'mailFolders/junkemail/messages', 'archive': 'mailFolders/archive/messages' }; const lowerFolderName = folderName.toLowerCase(); if (wellKnownRelative[lowerFolderName]) { const p = buildPath(mb, wellKnownRelative[lowerFolderName]); console.error(`Using well-known folder path for "${folderName}": ${p}`); return p; } try { const folderId = await getFolderIdByName(accessToken, folderName, mb); if (folderId) { const p = buildPath(mb, `mailFolders/${folderId}/messages`); console.error(`Resolved folder "${folderName}" to path: ${p}`); return p; } console.error(`Couldn't find folder "${folderName}", falling back to inbox`); return buildPath(mb, 'messages'); } catch (error) { console.error(`Error resolving folder "${folderName}": ${error.message}`); return buildPath(mb, 'messages'); } } /** * Get the ID of a mail folder by its name * @param {string} accessToken - Access token * @param {string} folderName - Name of the folder to find * @param {string|object|null} mailbox * @returns {Promise} - Folder ID or null if not found */ async function getFolderIdByName(accessToken, folderName, mailbox = null) { const mb = typeof mailbox === 'object' && mailbox && mailbox.graphRoot ? mailbox : normalizeMailbox(mailbox); const opts = { headers: withMailboxHeaders(mb) }; try { console.error(`Looking for folder with name "${folderName}" in ${mb.graphRoot}`); const response = await callGraphAPI( accessToken, 'GET', buildPath(mb, 'mailFolders'), null, { $filter: `displayName eq '${folderName}'` }, opts ); if (response.value && response.value.length > 0) { console.error(`Found folder "${folderName}" with ID: ${response.value[0].id}`); return response.value[0].id; } console.error(`No exact match found for "${folderName}", trying case-insensitive search`); const allFoldersResponse = await callGraphAPI( accessToken, 'GET', buildPath(mb, 'mailFolders'), null, { $top: 100 }, opts ); if (allFoldersResponse.value) { const lowerFolderName = folderName.toLowerCase(); const matchingFolder = allFoldersResponse.value.find( folder => folder.displayName.toLowerCase() === lowerFolderName ); if (matchingFolder) { console.error(`Found case-insensitive match for "${folderName}" with ID: ${matchingFolder.id}`); return matchingFolder.id; } } console.error(`No folder found matching "${folderName}"`); return null; } catch (error) { console.error(`Error finding folder "${folderName}": ${error.message}`); return null; } } /** * Get all mail folders * @param {string} accessToken - Access token * @param {string|object|null} mailbox * @returns {Promise} - Array of folder objects */ async function getAllFolders(accessToken, mailbox = null) { const mb = typeof mailbox === 'object' && mailbox && mailbox.graphRoot ? mailbox : normalizeMailbox(mailbox); const opts = { headers: withMailboxHeaders(mb) }; try { const response = await callGraphAPI( accessToken, 'GET', buildPath(mb, 'mailFolders'), null, { $top: 100, $select: 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount' }, opts ); if (!response.value) { return []; } const foldersWithChildren = response.value.filter(f => f.childFolderCount > 0); const childFolderPromises = foldersWithChildren.map(async (folder) => { try { const childResponse = await callGraphAPI( accessToken, 'GET', buildPath(mb, `mailFolders/${folder.id}/childFolders`), null, { $select: 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount' }, opts ); return childResponse.value || []; } catch (error) { console.error(`Error getting child folders for "${folder.displayName}": ${error.message}`); return []; } }); const childFolders = await Promise.all(childFolderPromises); return [...response.value, ...childFolders.flat()]; } catch (error) { console.error(`Error getting all folders: ${error.message}`); return []; } } module.exports = { resolveFolderPath, getFolderIdByName, getAllFolders, folderCache };