outlook-mcp/email/read-multiple.js
Seton Carmichael 68a64461eb fix(outlook-mcp): P0 audit fixes
- Handle empty 2xx Graph API responses without JSON parse errors
- Fix list-rules handler import (handleListRules named export)
- Add timezone-aware display formatting using MS_TIMEZONE / DEFAULT_TIMEZONE
- Return created event ID in create-event success message
- Change default timezone from Windows name to IANA (America/New_York)
2026-06-21 20:28:11 -04:00

156 lines
No EOL
5 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');
/**
* 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 {
// Format sender, recipients, etc.
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);
// Extract and clean body content (cleanBody handles both HTML and plain text)
let body = '';
if (email.body) {
body = cleanBody(email.body.content);
} else {
body = cleanBody(email.bodyPreview) || 'No content';
}
// Format the email
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;
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."
}]
};
}
// Limit the number of emails to prevent overwhelming responses
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 {
// Get access token
const accessToken = await ensureAuthenticated();
// Create concurrent API calls for all email IDs
const emailPromises = emailIds.map(async (emailId) => {
try {
const endpoint = `me/messages/${encodeURIComponent(emailId)}`;
const queryParams = {
$select: config.EMAIL_DETAIL_FIELDS
};
const email = await callGraphAPI(accessToken, 'GET', endpoint, null, queryParams);
return { emailId, email, error: null };
} catch (error) {
console.error(`Error reading email ${emailId}: ${error.message}`);
return { emailId, email: null, error: error.message };
}
});
// Wait for all API calls to complete
const results = await Promise.all(emailPromises);
// Format all emails
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}`;
}
});
// Count successful vs failed reads
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` : ''}.
`;
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;