299 lines
9 KiB
JavaScript
299 lines
9 KiB
JavaScript
/**
|
|
* List folders functionality
|
|
*/
|
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
const { ensureAuthenticated } = require('../auth');
|
|
|
|
/**
|
|
* List folders handler
|
|
* @param {object} args - Tool arguments
|
|
* @returns {object} - MCP response
|
|
*/
|
|
async function handleListFolders(args) {
|
|
const includeItemCounts = args.includeItemCounts === true || args.includeItemCounts === 'true';
|
|
const includeChildren = args.includeChildren === true || args.includeChildren === 'true';
|
|
|
|
try {
|
|
// Get access token
|
|
const accessToken = await ensureAuthenticated();
|
|
|
|
// Get all mail folders
|
|
const folders = await getAllFoldersHierarchy(accessToken, includeItemCounts);
|
|
|
|
// If including children, format as hierarchy
|
|
if (includeChildren) {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: formatFolderHierarchy(folders, includeItemCounts)
|
|
}]
|
|
};
|
|
} else {
|
|
// Otherwise, format as flat list
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: formatFolderList(folders, includeItemCounts)
|
|
}]
|
|
};
|
|
}
|
|
} catch (error) {
|
|
if (error.message === 'Authentication required') {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: "Authentication required. Please use the 'authenticate' tool first."
|
|
}]
|
|
};
|
|
}
|
|
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: `Error listing folders: ${error.message}`
|
|
}]
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all mail folders with hierarchy information
|
|
* @param {string} accessToken - Access token
|
|
* @param {boolean} includeItemCounts - Include item counts in response
|
|
* @returns {Promise<Array>} - Array of folder objects with hierarchy
|
|
*/
|
|
async function getAllFoldersHierarchy(accessToken, includeItemCounts) {
|
|
try {
|
|
// Determine select fields based on whether to include counts
|
|
const selectFields = includeItemCounts
|
|
? 'id,displayName,parentFolderId,childFolderCount,totalItemCount,unreadItemCount'
|
|
: 'id,displayName,parentFolderId,childFolderCount';
|
|
|
|
// Get all mail folders
|
|
const response = await callGraphAPI(
|
|
accessToken,
|
|
'GET',
|
|
'me/mailFolders',
|
|
null,
|
|
{
|
|
$top: 100,
|
|
$select: selectFields
|
|
}
|
|
);
|
|
|
|
if (!response.value) {
|
|
return [];
|
|
}
|
|
|
|
// Get child folders for folders with children
|
|
const foldersWithChildren = response.value.filter(f => f.childFolderCount > 0);
|
|
|
|
const childFolderPromises = foldersWithChildren.map(async (folder) => {
|
|
try {
|
|
const childResponse = await callGraphAPI(
|
|
accessToken,
|
|
'GET',
|
|
`me/mailFolders/${folder.id}/childFolders`,
|
|
null,
|
|
{ $select: selectFields }
|
|
);
|
|
|
|
// Add parent folder info to each child
|
|
const childFolders = childResponse.value || [];
|
|
childFolders.forEach(child => {
|
|
child.parentFolder = folder.displayName;
|
|
});
|
|
|
|
return childFolders;
|
|
} catch (error) {
|
|
console.error(`Error getting child folders for "${folder.displayName}": ${error.message}`);
|
|
return [];
|
|
}
|
|
});
|
|
|
|
const childFolders = await Promise.all(childFolderPromises);
|
|
const allChildFolders = childFolders.flat();
|
|
|
|
// Add top-level flag to parent folders
|
|
const topLevelFolders = response.value.map(folder => ({
|
|
...folder,
|
|
isTopLevel: true
|
|
}));
|
|
|
|
// Combine all folders and deduplicate by ID. Some folders can appear both as
|
|
// top-level entries and as children of other folders (e.g. Junk Email under
|
|
// RecoverableItems), causing duplicate output.
|
|
const combined = [...topLevelFolders, ...allChildFolders];
|
|
const deduped = Array.from(
|
|
new Map(combined.map(folder => [folder.id, folder])).values()
|
|
);
|
|
|
|
// Mark folders whose displayName is shared by multiple distinct IDs so the
|
|
// formatter can include the ID to tell them apart.
|
|
const displayNameCounts = new Map();
|
|
for (const folder of deduped) {
|
|
displayNameCounts.set(folder.displayName, (displayNameCounts.get(folder.displayName) || 0) + 1);
|
|
}
|
|
for (const folder of deduped) {
|
|
if (displayNameCounts.get(folder.displayName) > 1) {
|
|
folder.hasDuplicateName = true;
|
|
}
|
|
}
|
|
|
|
return deduped;
|
|
} catch (error) {
|
|
console.error(`Error getting all folders: ${error.message}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format folders as a flat list
|
|
* @param {Array} folders - Array of folder objects
|
|
* @param {boolean} includeItemCounts - Whether to include item counts
|
|
* @returns {string} - Formatted list
|
|
*/
|
|
function formatFolderList(folders, includeItemCounts) {
|
|
if (!folders || folders.length === 0) {
|
|
return "No folders found.";
|
|
}
|
|
|
|
// Sort folders alphabetically, with well-known folders first
|
|
const wellKnownFolderNames = ['Inbox', 'Drafts', 'Sent Items', 'Deleted Items', 'Junk Email', 'Archive'];
|
|
|
|
const sortedFolders = [...folders].sort((a, b) => {
|
|
// Well-known folders come first
|
|
const aIsWellKnown = wellKnownFolderNames.includes(a.displayName);
|
|
const bIsWellKnown = wellKnownFolderNames.includes(b.displayName);
|
|
|
|
if (aIsWellKnown && !bIsWellKnown) return -1;
|
|
if (!aIsWellKnown && bIsWellKnown) return 1;
|
|
|
|
if (aIsWellKnown && bIsWellKnown) {
|
|
// Sort well-known folders by their index in the array
|
|
return wellKnownFolderNames.indexOf(a.displayName) - wellKnownFolderNames.indexOf(b.displayName);
|
|
}
|
|
|
|
// Sort other folders alphabetically
|
|
return a.displayName.localeCompare(b.displayName);
|
|
});
|
|
|
|
// Format each folder
|
|
const folderLines = sortedFolders.map(folder => {
|
|
let folderInfo = folder.displayName;
|
|
|
|
// Include the raw folder id when multiple distinct folders share the
|
|
// same displayName (e.g. two Junk Email folders).
|
|
if (folder.hasDuplicateName) {
|
|
folderInfo += ` [id: ${folder.id}]`;
|
|
}
|
|
|
|
// Add parent folder info if available
|
|
if (folder.parentFolder) {
|
|
folderInfo += ` (in ${folder.parentFolder})`;
|
|
}
|
|
|
|
// Add item counts if requested
|
|
if (includeItemCounts) {
|
|
const unreadCount = folder.unreadItemCount || 0;
|
|
const totalCount = folder.totalItemCount || 0;
|
|
folderInfo += ` - ${totalCount} items`;
|
|
|
|
if (unreadCount > 0) {
|
|
folderInfo += ` (${unreadCount} unread)`;
|
|
}
|
|
}
|
|
|
|
return folderInfo;
|
|
});
|
|
|
|
return `Found ${folders.length} folders:\n\n${folderLines.join('\n')}`;
|
|
}
|
|
|
|
/**
|
|
* Format folders as a hierarchical tree
|
|
* @param {Array} folders - Array of folder objects
|
|
* @param {boolean} includeItemCounts - Whether to include item counts
|
|
* @returns {string} - Formatted hierarchy
|
|
*/
|
|
function formatFolderHierarchy(folders, includeItemCounts) {
|
|
if (!folders || folders.length === 0) {
|
|
return "No folders found.";
|
|
}
|
|
|
|
// Build folder hierarchy
|
|
const folderMap = new Map();
|
|
const rootFolders = [];
|
|
|
|
// First pass: create map of all folders
|
|
folders.forEach(folder => {
|
|
folderMap.set(folder.id, {
|
|
...folder,
|
|
children: []
|
|
});
|
|
|
|
if (folder.isTopLevel) {
|
|
rootFolders.push(folder.id);
|
|
}
|
|
});
|
|
|
|
// Second pass: build hierarchy
|
|
folders.forEach(folder => {
|
|
if (!folder.isTopLevel && folder.parentFolderId) {
|
|
const parent = folderMap.get(folder.parentFolderId);
|
|
if (parent) {
|
|
parent.children.push(folder.id);
|
|
} else {
|
|
// Fallback for orphaned folders
|
|
rootFolders.push(folder.id);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Format hierarchy recursively
|
|
function formatSubtree(folderId, level = 0) {
|
|
const folder = folderMap.get(folderId);
|
|
if (!folder) return '';
|
|
|
|
const indent = ' '.repeat(level);
|
|
let line = `${indent}${folder.displayName}`;
|
|
|
|
// Include the raw folder id when multiple distinct folders share the
|
|
// same displayName (e.g. two Junk Email folders).
|
|
if (folder.hasDuplicateName) {
|
|
line += ` [id: ${folder.id}]`;
|
|
}
|
|
|
|
// Add item counts if requested
|
|
if (includeItemCounts) {
|
|
const unreadCount = folder.unreadItemCount || 0;
|
|
const totalCount = folder.totalItemCount || 0;
|
|
line += ` - ${totalCount} items`;
|
|
|
|
if (unreadCount > 0) {
|
|
line += ` (${unreadCount} unread)`;
|
|
}
|
|
}
|
|
|
|
// Add children
|
|
const childLines = folder.children
|
|
.map(childId => formatSubtree(childId, level + 1))
|
|
.filter(line => line.length > 0)
|
|
.join('\n');
|
|
|
|
return childLines.length > 0 ? `${line}\n${childLines}` : line;
|
|
}
|
|
|
|
// Format all root folders
|
|
const formattedHierarchy = rootFolders
|
|
.map(folderId => formatSubtree(folderId))
|
|
.join('\n');
|
|
|
|
return `Folder Hierarchy:\n\n${formattedHierarchy}`;
|
|
}
|
|
|
|
module.exports = {
|
|
handleListFolders,
|
|
formatFolderList,
|
|
formatFolderHierarchy
|
|
};
|