outlook-mcp/email/read-multiple.js
Seton Carmichael e70840552d feat(outlook-mcp): shared mailbox targeting, discovery, and scopes (v1.1.0)
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.
2026-08-24 08:41:05 -04:00

152 lines
4.8 KiB
JavaScript

/**
* Read multiple emails functionality
*/
const config = require('../config');
const { callGraphAPI } = require('../utils/graph-api');
const { ensureAuthenticated } = require('../auth');
const { cleanBody } = require('../utils/bodyParser');
const { formatDateTime } = require('../utils/time-formatter');
const { normalizeMailbox, buildPath, withMailboxHeaders } = require('../utils/mailbox');
/**
* Format a single email for display
* @param {object} email - Email object from Graph API
* @param {string} emailId - Email ID for error context
* @returns {string} - Formatted email text
*/
function formatEmail(email, emailId) {
if (!email) {
return `Email ID ${emailId}: Not found or inaccessible`;
}
try {
const sender = email.from ? `${email.from.emailAddress.name} (${email.from.emailAddress.address})` : 'Unknown';
const to = email.toRecipients ? email.toRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
const cc = email.ccRecipients && email.ccRecipients.length > 0 ? email.ccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
const bcc = email.bccRecipients && email.bccRecipients.length > 0 ? email.bccRecipients.map(r => `${r.emailAddress.name} (${r.emailAddress.address})`).join(", ") : 'None';
const date = formatDateTime(email.receivedDateTime);
let body = '';
if (email.body) {
body = cleanBody(email.body.content);
} else {
body = cleanBody(email.bodyPreview) || 'No content';
}
return `From: ${sender}
To: ${to}
${cc !== 'None' ? `CC: ${cc}\n` : ''}${bcc !== 'None' ? `BCC: ${bcc}\n` : ''}Subject: ${email.subject}
Date: ${date}
Importance: ${email.importance || 'normal'}
Has Attachments: ${email.hasAttachments ? 'Yes' : 'No'}
${body}`;
} catch (error) {
return `Email ID ${emailId}: Error formatting email - ${error.message}`;
}
}
/**
* Read multiple emails handler
* @param {object} args - Tool arguments
* @returns {object} - MCP response
*/
async function handleReadMultipleEmails(args) {
const emailIds = args.ids;
const mb = normalizeMailbox(args.mailbox);
const opts = { headers: withMailboxHeaders(mb) };
if (!emailIds || !Array.isArray(emailIds) || emailIds.length === 0) {
return {
content: [{
type: "text",
text: "Email IDs array is required and must contain at least one ID."
}]
};
}
const maxEmails = 10;
if (emailIds.length > maxEmails) {
return {
content: [{
type: "text",
text: `Too many email IDs provided. Maximum allowed is ${maxEmails}, but ${emailIds.length} were provided.`
}]
};
}
try {
const accessToken = await ensureAuthenticated();
const emailPromises = emailIds.map(async (emailId) => {
try {
const endpoint = buildPath(mb, `messages/${emailId}`);
const queryParams = {
$select: config.EMAIL_DETAIL_FIELDS
};
const email = await callGraphAPI(accessToken, 'GET', endpoint, null, queryParams, opts);
return { emailId, email, error: null };
} catch (error) {
console.error(`Error reading email ${emailId}: ${error.message}`);
return { emailId, email: null, error: error.message };
}
});
const results = await Promise.all(emailPromises);
const mailboxNote = mb.kind === 'user' ? ` Mailbox: ${mb.smtpOrUpn}.` : '';
const formattedEmails = results.map((result, index) => {
const emailNumber = index + 1;
const separator = "=".repeat(80);
if (result.error) {
return `${separator}
EMAIL ${emailNumber} (ID: ${result.emailId})
${separator}
Error: ${result.error}`;
} else {
const formattedEmail = formatEmail(result.email, result.emailId);
return `${separator}
EMAIL ${emailNumber} (ID: ${result.emailId})
${separator}
${formattedEmail}`;
}
});
const successCount = results.filter(r => !r.error && r.email).length;
const errorCount = results.filter(r => r.error || !r.email).length;
const summary = `Retrieved ${successCount} email(s) successfully${errorCount > 0 ? `, ${errorCount} failed` : ''}.${mailboxNote}
`;
return {
content: [
{
type: "text",
text: summary + formattedEmails.join('\n\n')
}
]
};
} 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 accessing emails: ${error.message}`
}]
};
}
}
module.exports = handleReadMultipleEmails;