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

169 lines
4.3 KiB
JavaScript

/**
* Send email functionality
*/
const config = require('../config');
const { callGraphAPI } = require('../utils/graph-api');
const { ensureAuthenticated } = require('../auth');
const { normalizeMailbox, buildPath, withMailboxHeaders } = require('../utils/mailbox');
/**
* Send email handler
* @param {object} args - Tool arguments
* @returns {object} - MCP response
*/
async function handleSendEmail(args) {
const {
to,
cc,
bcc,
subject,
body,
importance = 'normal',
saveToSentItems = true,
from: fromArg,
onBehalfOf = false
} = args;
const mb = normalizeMailbox(args.mailbox);
const onBehalf = onBehalfOf === true || onBehalfOf === 'true';
if (!to) {
return {
content: [{
type: "text",
text: "Recipient (to) is required."
}]
};
}
if (!subject) {
return {
content: [{
type: "text",
text: "Subject is required."
}]
};
}
if (!body) {
return {
content: [{
type: "text",
text: "Body content is required."
}]
};
}
try {
const accessToken = await ensureAuthenticated();
const toRecipients = to.split(',').map(email => {
email = email.trim();
return {
emailAddress: {
address: email
}
};
});
const ccRecipients = cc ? cc.split(',').map(email => {
email = email.trim();
return {
emailAddress: {
address: email
}
};
}) : [];
const bccRecipients = bcc ? bcc.split(',').map(email => {
email = email.trim();
return {
emailAddress: {
address: email
}
};
}) : [];
const fromAddr = (fromArg && String(fromArg).trim())
|| (mb.kind === 'user' ? mb.smtpOrUpn : null);
// Default: when mailbox is a shared UPN, send via users/{upn}/sendMail (Send As style).
// onBehalfOf=true forces me/sendMail with from=shared (Send on Behalf style).
let sendPath = 'me/sendMail';
let anchorMb = mb;
if (mb.kind === 'user' && !onBehalf) {
sendPath = buildPath(mb, 'sendMail');
} else if (fromAddr && onBehalf) {
sendPath = 'me/sendMail';
anchorMb = normalizeMailbox(fromAddr);
} else if (mb.kind === 'user') {
sendPath = buildPath(mb, 'sendMail');
}
const message = {
subject,
body: {
// New Outlook + Graph: prefer HTML when the body looks like a document.
// Match <html, <!doctype html, or a leading HTML fragment with common tags.
contentType: /<!DOCTYPE\s+html|<html[\s>]|<(?:table|div|h[1-6]|p)\b/i.test(body || '')
? 'HTML'
: 'Text',
content: body
},
toRecipients,
ccRecipients: ccRecipients.length > 0 ? ccRecipients : undefined,
bccRecipients: bccRecipients.length > 0 ? bccRecipients : undefined,
importance
};
if (fromAddr) {
message.from = {
emailAddress: {
address: fromAddr
}
};
}
const emailObject = {
message,
saveToSentItems
};
await callGraphAPI(
accessToken,
'POST',
sendPath,
emailObject,
null,
{ headers: withMailboxHeaders(anchorMb) }
);
const fromNote = fromAddr ? `\nFrom: ${fromAddr}${onBehalf ? ' (on behalf)' : ''}` : '';
const mailboxNote = mb.kind === 'user' ? `\nMailbox: ${mb.smtpOrUpn}` : '';
return {
content: [{
type: "text",
text: `Email sent successfully!${mailboxNote}${fromNote}\n\nSubject: ${subject}\nRecipients: ${toRecipients.length}${ccRecipients.length > 0 ? ` + ${ccRecipients.length} CC` : ''}${bccRecipients.length > 0 ? ` + ${bccRecipients.length} BCC` : ''}\nMessage Length: ${body.length} characters`
}]
};
} 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 sending email: ${error.message}`
}]
};
}
}
module.exports = handleSendEmail;