outlook-mcp/email/read.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

127 lines
3.9 KiB
JavaScript

/**
* Read email 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');
/**
* Read email handler
* @param {object} args - Tool arguments
* @returns {object} - MCP response
*/
async function handleReadEmail(args) {
const emailId = args.id;
const mb = normalizeMailbox(args.mailbox);
if (!emailId) {
return {
content: [{
type: "text",
text: "Email ID is required."
}]
};
}
try {
const accessToken = await ensureAuthenticated();
// Do not pre-encode the ID — callGraphAPI encodes each path segment once.
const endpoint = buildPath(mb, `messages/${emailId}`);
const queryParams = {
$select: config.EMAIL_DETAIL_FIELDS
};
const opts = { headers: withMailboxHeaders(mb) };
try {
const email = await callGraphAPI(accessToken, 'GET', endpoint, null, queryParams, opts);
if (!email) {
return {
content: [
{
type: "text",
text: `Email with ID ${emailId} not found.`
}
]
};
}
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';
}
const mailboxLine = mb.kind === 'user' ? `Mailbox: ${mb.smtpOrUpn}\n` : '';
const formattedEmail = `${mailboxLine}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}`;
return {
content: [
{
type: "text",
text: formattedEmail
}
]
};
} catch (error) {
console.error(`Error reading email: ${error.message}`);
if (error.message.includes("doesn't belong to the targeted mailbox")) {
return {
content: [
{
type: "text",
text: `The email ID seems invalid or doesn't belong to the targeted mailbox${mb.kind === 'user' ? ` (${mb.smtpOrUpn})` : ''}. Pass the same mailbox used when listing/searching, or try a different email ID.`
}
]
};
} else {
return {
content: [
{
type: "text",
text: `Failed to read email: ${error.message}`
}
]
};
}
}
} 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 email: ${error.message}`
}]
};
}
}
module.exports = handleReadEmail;