Add optional mailbox (UPN/SMTP) routing on email/folder/thread tools via
users/{upn}/... and X-AnchorMailbox. New list-mailboxes probes primary,
OUTLOOK_SHARED_MAILBOXES seeds, cache, and candidates. Send supports
mailbox-rooted sendMail and onBehalfOf. MSAL requests Mail.*.Shared;
check-auth-status reports token scp gaps. Docs, env example, tests.
192 lines
6 KiB
JavaScript
192 lines
6 KiB
JavaScript
/**
|
|
* 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<string>} - 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<string|null>} - 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>} - 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
|
|
};
|