@chrischall/office-outlook-mcp 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +74 -19
- package/dist/bundle.js +765 -39
- package/dist/client.js +16 -1
- package/dist/tools/_invite.js +24 -0
- package/dist/tools/calendar.js +41 -13
- package/dist/tools/directory.js +9 -0
- package/dist/tools/mail.js +295 -4
- package/dist/tools/writes.js +525 -17
- package/dist/version.js +1 -1
- package/dist/view.js +40 -7
- package/mint.yaml +3 -2
- package/package.json +1 -1
- package/server.json +3 -3
package/dist/bundle.js
CHANGED
|
@@ -35616,6 +35616,11 @@ function textResult(data) {
|
|
|
35616
35616
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
|
|
35617
35617
|
};
|
|
35618
35618
|
}
|
|
35619
|
+
function imageResult(base643, mimeType) {
|
|
35620
|
+
return {
|
|
35621
|
+
content: [{ type: "image", data: base643, mimeType }]
|
|
35622
|
+
};
|
|
35623
|
+
}
|
|
35619
35624
|
function errorResult(message) {
|
|
35620
35625
|
return {
|
|
35621
35626
|
content: [{ type: "text", text: redactSecrets(message) }],
|
|
@@ -37474,6 +37479,18 @@ var init_verify_after_write = __esm({
|
|
|
37474
37479
|
});
|
|
37475
37480
|
|
|
37476
37481
|
// node_modules/@chrischall/mcp-utils/dist/concurrency/index.js
|
|
37482
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
37483
|
+
const results = new Array(items.length);
|
|
37484
|
+
let next = 0;
|
|
37485
|
+
const poolSize = Math.min(Math.max(limit, 1), items.length);
|
|
37486
|
+
await Promise.all(Array.from({ length: poolSize }, async () => {
|
|
37487
|
+
while (next < items.length) {
|
|
37488
|
+
const i = next++;
|
|
37489
|
+
results[i] = await fn(items[i], i);
|
|
37490
|
+
}
|
|
37491
|
+
}));
|
|
37492
|
+
return results;
|
|
37493
|
+
}
|
|
37477
37494
|
var init_concurrency = __esm({
|
|
37478
37495
|
"node_modules/@chrischall/mcp-utils/dist/concurrency/index.js"() {
|
|
37479
37496
|
init_single_flight();
|
|
@@ -37571,7 +37588,7 @@ var init_version = __esm({
|
|
|
37571
37588
|
"src/version.ts"() {
|
|
37572
37589
|
"use strict";
|
|
37573
37590
|
PACKAGE_NAME = "office-outlook-mcp";
|
|
37574
|
-
VERSION = "0.
|
|
37591
|
+
VERSION = "0.5.0";
|
|
37575
37592
|
}
|
|
37576
37593
|
});
|
|
37577
37594
|
|
|
@@ -48663,6 +48680,10 @@ function toBearerTokens(accessToken) {
|
|
|
48663
48680
|
}
|
|
48664
48681
|
return { accessToken, refreshToken: RECAPTURE_SENTINEL, expiresAt };
|
|
48665
48682
|
}
|
|
48683
|
+
function isCredentialFailure(e) {
|
|
48684
|
+
const kind = errorKindOf(e);
|
|
48685
|
+
return kind === "no_credential" || kind === "credential_rejected" || errorStatusOf(e) === 401;
|
|
48686
|
+
}
|
|
48666
48687
|
var OutlookClient = class {
|
|
48667
48688
|
#baseUrl;
|
|
48668
48689
|
/**
|
|
@@ -48690,7 +48711,8 @@ var OutlookClient = class {
|
|
|
48690
48711
|
this.#configError = new McpToolError(
|
|
48691
48712
|
"No Outlook credential is configured.",
|
|
48692
48713
|
{
|
|
48693
|
-
hint: "Set OUTLOOK_ACCESS_TOKEN, or unset OUTLOOK_DISABLE_FETCHPROXY so the token can be captured from a signed-in Outlook browser tab."
|
|
48714
|
+
hint: "Set OUTLOOK_ACCESS_TOKEN, or unset OUTLOOK_DISABLE_FETCHPROXY so the token can be captured from a signed-in Outlook browser tab.",
|
|
48715
|
+
kind: "no_credential"
|
|
48694
48716
|
}
|
|
48695
48717
|
);
|
|
48696
48718
|
}
|
|
@@ -48721,7 +48743,9 @@ var OutlookClient = class {
|
|
|
48721
48743
|
refresh: async () => {
|
|
48722
48744
|
if (direct !== void 0) {
|
|
48723
48745
|
throw new McpToolError("OUTLOOK_ACCESS_TOKEN was rejected by Outlook.", {
|
|
48724
|
-
hint: "A directly-supplied token cannot be refreshed automatically. Replace OUTLOOK_ACCESS_TOKEN with a current one, or unset it to capture from a signed-in Outlook browser tab instead."
|
|
48746
|
+
hint: "A directly-supplied token cannot be refreshed automatically. Replace OUTLOOK_ACCESS_TOKEN with a current one, or unset it to capture from a signed-in Outlook browser tab instead.",
|
|
48747
|
+
status: 401,
|
|
48748
|
+
kind: "credential_rejected"
|
|
48725
48749
|
});
|
|
48726
48750
|
}
|
|
48727
48751
|
return toBearerTokens(await this.#capture());
|
|
@@ -48760,7 +48784,9 @@ var OutlookClient = class {
|
|
|
48760
48784
|
timeout: 6e4,
|
|
48761
48785
|
baseHeaders: { Accept: "application/json" },
|
|
48762
48786
|
onUnauthorized: () => new McpToolError("Outlook rejected the access token (401).", {
|
|
48763
|
-
hint: "The token has expired or been revoked. Open a signed-in Outlook tab and retry \u2014 the next call re-captures automatically."
|
|
48787
|
+
hint: "The token has expired or been revoked. Open a signed-in Outlook tab and retry \u2014 the next call re-captures automatically.",
|
|
48788
|
+
status: 401,
|
|
48789
|
+
kind: "credential_rejected"
|
|
48764
48790
|
})
|
|
48765
48791
|
});
|
|
48766
48792
|
return this.#api;
|
|
@@ -48883,12 +48909,21 @@ function addrs(list) {
|
|
|
48883
48909
|
if (!Array.isArray(list) || list.length === 0) return void 0;
|
|
48884
48910
|
return list.map((r) => addr(r)).filter((s) => s !== void 0);
|
|
48885
48911
|
}
|
|
48912
|
+
var MAX_RECIPIENTS = 20;
|
|
48913
|
+
function cappedRecipients(key, list) {
|
|
48914
|
+
if (list === void 0 || list.length <= MAX_RECIPIENTS) return { [key]: list };
|
|
48915
|
+
return {
|
|
48916
|
+
[key]: list.slice(0, MAX_RECIPIENTS),
|
|
48917
|
+
[`${key}Count`]: list.length,
|
|
48918
|
+
[`${key}Truncated`]: true
|
|
48919
|
+
};
|
|
48920
|
+
}
|
|
48886
48921
|
function compactMessage(m) {
|
|
48887
48922
|
return pruned({
|
|
48888
48923
|
Id: m.Id,
|
|
48889
48924
|
Subject: m.Subject,
|
|
48890
48925
|
From: addr(m.From ?? m.Sender),
|
|
48891
|
-
To
|
|
48926
|
+
...cappedRecipients("To", addrs(m.ToRecipients)),
|
|
48892
48927
|
Received: m.ReceivedDateTime,
|
|
48893
48928
|
IsRead: m.IsRead,
|
|
48894
48929
|
HasAttachments: m.HasAttachments || void 0,
|
|
@@ -48901,7 +48936,7 @@ function compactMessage(m) {
|
|
|
48901
48936
|
function fullMessage(m) {
|
|
48902
48937
|
return pruned({
|
|
48903
48938
|
...compactMessage(m),
|
|
48904
|
-
Cc
|
|
48939
|
+
...cappedRecipients("Cc", addrs(m.CcRecipients)),
|
|
48905
48940
|
Sent: m.SentDateTime,
|
|
48906
48941
|
Importance: m.Importance,
|
|
48907
48942
|
IsDraft: m.IsDraft,
|
|
@@ -48920,14 +48955,19 @@ function compactEvent(e) {
|
|
|
48920
48955
|
TimeZone: e.Start?.TimeZone,
|
|
48921
48956
|
Location: e.Location?.DisplayName || void 0,
|
|
48922
48957
|
Organizer: addr(e.Organizer),
|
|
48923
|
-
IsAllDay: e.IsAllDay
|
|
48924
|
-
IsCancelled: e.IsCancelled
|
|
48958
|
+
IsAllDay: e.IsAllDay,
|
|
48959
|
+
IsCancelled: e.IsCancelled,
|
|
48960
|
+
IsOrganizer: e.IsOrganizer,
|
|
48961
|
+
MyResponse: e.ResponseStatus?.Response,
|
|
48962
|
+
ResponseRequested: e.ResponseRequested,
|
|
48963
|
+
Type: e.Type,
|
|
48964
|
+
ShowAs: e.ShowAs
|
|
48925
48965
|
});
|
|
48926
48966
|
}
|
|
48927
48967
|
function fullEvent(e) {
|
|
48928
48968
|
return pruned({
|
|
48929
48969
|
...compactEvent(e),
|
|
48930
|
-
|
|
48970
|
+
SeriesMasterId: e.SeriesMasterId,
|
|
48931
48971
|
Attendees: e.Attendees?.map(
|
|
48932
48972
|
(a) => pruned({ Who: addr(a), Response: a.Status?.Response })
|
|
48933
48973
|
),
|
|
@@ -48975,6 +49015,17 @@ function projectCollection(payload, project, label) {
|
|
|
48975
49015
|
});
|
|
48976
49016
|
}
|
|
48977
49017
|
|
|
49018
|
+
// src/tools/_invite.ts
|
|
49019
|
+
var EVENT_EXPAND = "Microsoft.OutlookServices.EventMessage/Event";
|
|
49020
|
+
function resolvedEvent(e) {
|
|
49021
|
+
return e?.Id ? e : void 0;
|
|
49022
|
+
}
|
|
49023
|
+
var INVITE_GONE = "the meeting is no longer on your calendar (already declined, cancelled, or deleted), so there is nothing to respond to";
|
|
49024
|
+
var MEETING_MESSAGE_TYPE = "Microsoft.OutlookServices.EventMessage/MeetingMessageType";
|
|
49025
|
+
function isMeetingMessage(m) {
|
|
49026
|
+
return !!m?.MeetingMessageType || (m?.["@odata.type"] ?? "").includes("EventMessage");
|
|
49027
|
+
}
|
|
49028
|
+
|
|
48978
49029
|
// src/tools/_paging.ts
|
|
48979
49030
|
init_zod();
|
|
48980
49031
|
var nextLinkParam = external_exports.string().min(1).optional().describe(
|
|
@@ -49016,6 +49067,91 @@ var LIST_SELECT = "Id,Subject,From,ToRecipients,ReceivedDateTime,IsRead,HasAttac
|
|
|
49016
49067
|
function folderSegment(folder) {
|
|
49017
49068
|
return encodeURIComponent(folder);
|
|
49018
49069
|
}
|
|
49070
|
+
var UNREAD_SELECT = [
|
|
49071
|
+
"Id",
|
|
49072
|
+
"Subject",
|
|
49073
|
+
"From",
|
|
49074
|
+
"ToRecipients",
|
|
49075
|
+
"CcRecipients",
|
|
49076
|
+
"ReceivedDateTime",
|
|
49077
|
+
"Importance",
|
|
49078
|
+
"HasAttachments",
|
|
49079
|
+
"Categories",
|
|
49080
|
+
"Flag",
|
|
49081
|
+
"ConversationId",
|
|
49082
|
+
MEETING_MESSAGE_TYPE
|
|
49083
|
+
].join(",");
|
|
49084
|
+
var BODY_CONCURRENCY = 4;
|
|
49085
|
+
function triageKind(m) {
|
|
49086
|
+
switch (m.MeetingMessageType) {
|
|
49087
|
+
case "MeetingRequest":
|
|
49088
|
+
return "meetingRequest";
|
|
49089
|
+
case "MeetingCancelled":
|
|
49090
|
+
return "meetingCancelled";
|
|
49091
|
+
case "MeetingAccepted":
|
|
49092
|
+
case "MeetingTenativelyAccepted":
|
|
49093
|
+
// sic — Outlook's spelling
|
|
49094
|
+
case "MeetingTentativelyAccepted":
|
|
49095
|
+
case "MeetingDeclined":
|
|
49096
|
+
return "meetingResponse";
|
|
49097
|
+
}
|
|
49098
|
+
const type = m["@odata.type"] ?? "";
|
|
49099
|
+
if (!type.includes("EventMessage")) return "mail";
|
|
49100
|
+
if (/Request$/.test(type)) return "meetingRequest";
|
|
49101
|
+
if (/Cancel/.test(type)) return "meetingCancelled";
|
|
49102
|
+
return "meetingResponse";
|
|
49103
|
+
}
|
|
49104
|
+
function triageEvent(e) {
|
|
49105
|
+
return stripUndefined({
|
|
49106
|
+
id: e.Id,
|
|
49107
|
+
subject: e.Subject,
|
|
49108
|
+
start: e.Start?.DateTime,
|
|
49109
|
+
end: e.End?.DateTime,
|
|
49110
|
+
timeZone: e.Start?.TimeZone,
|
|
49111
|
+
location: e.Location?.DisplayName || void 0,
|
|
49112
|
+
organizer: addr(e.Organizer),
|
|
49113
|
+
responseStatus: e.ResponseStatus?.Response,
|
|
49114
|
+
isCancelled: e.IsCancelled
|
|
49115
|
+
});
|
|
49116
|
+
}
|
|
49117
|
+
var INVITE_UNRESOLVED_HINT = `This invite's ${INVITE_GONE}.`;
|
|
49118
|
+
function truncateChars(text, max) {
|
|
49119
|
+
const end = /[\uD800-\uDBFF]/.test(text.charAt(max - 1)) ? max - 1 : max;
|
|
49120
|
+
return text.slice(0, end);
|
|
49121
|
+
}
|
|
49122
|
+
function stripUndefined(o) {
|
|
49123
|
+
return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
49124
|
+
}
|
|
49125
|
+
var ATTACHMENT_META_SELECT = "Id,Name,ContentType,Size,IsInline";
|
|
49126
|
+
var MAX_INLINE_BYTES = 5 * 1024 * 1024;
|
|
49127
|
+
var MAX_TEXT_BYTES = 200 * 1024;
|
|
49128
|
+
var TEXT_TYPE = /^text\/|^application\/(json|xml|csv|ics|calendar)\b|\+(json|xml)$/i;
|
|
49129
|
+
var TEXT_EXT = /\.(txt|csv|tsv|json|xml|ics|md|log|html?)$/i;
|
|
49130
|
+
function attachmentKind(a) {
|
|
49131
|
+
const type = a["@odata.type"] ?? "";
|
|
49132
|
+
if (/ItemAttachment$/.test(type)) return "item";
|
|
49133
|
+
if (/ReferenceAttachment$/.test(type)) return "reference";
|
|
49134
|
+
return "file";
|
|
49135
|
+
}
|
|
49136
|
+
function isImage(contentType) {
|
|
49137
|
+
return /^image\//i.test(contentType) && !/svg/i.test(contentType);
|
|
49138
|
+
}
|
|
49139
|
+
function isText(contentType, name) {
|
|
49140
|
+
return TEXT_TYPE.test(contentType) || /svg/i.test(contentType) || TEXT_EXT.test(name);
|
|
49141
|
+
}
|
|
49142
|
+
function base64DecodedLength(b64) {
|
|
49143
|
+
const padding = b64.endsWith("==") ? 2 : b64.endsWith("=") ? 1 : 0;
|
|
49144
|
+
return Math.floor(b64.length * 3 / 4) - padding;
|
|
49145
|
+
}
|
|
49146
|
+
function utf8Boundary(buf, max) {
|
|
49147
|
+
if (buf.length <= max) return buf.length;
|
|
49148
|
+
let end = max;
|
|
49149
|
+
while (end > 0 && (buf[end] & 192) === 128) end--;
|
|
49150
|
+
return end;
|
|
49151
|
+
}
|
|
49152
|
+
function sinceLiteral(hours) {
|
|
49153
|
+
return new Date(Date.now() - hours * 36e5).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
49154
|
+
}
|
|
49019
49155
|
function registerMailTools(server, client2) {
|
|
49020
49156
|
server.registerTool(
|
|
49021
49157
|
"outlook_list_folders",
|
|
@@ -49044,7 +49180,7 @@ function registerMailTools(server, client2) {
|
|
|
49044
49180
|
server.registerTool(
|
|
49045
49181
|
"outlook_list_messages",
|
|
49046
49182
|
{
|
|
49047
|
-
description:
|
|
49183
|
+
description: 'List messages in a folder, newest first. Defaults to the inbox. Use `unreadOnly` for a triage view. Bodies are NOT included \u2014 call outlook_get_message for one. Note `search` and `unreadOnly` cannot be combined on a first-page request (the API rejects $search with $filter); pass `search` alone to search the whole mailbox. When following a `nextLink`, both are ignored, so echoing them back is harmless. Each message\'s To list (and Cc, in the full view) holds at most 20 addresses; a longer list adds ToCount/ToTruncated (CcCount/CcTruncated) with the full count \u2014 pass view:"raw" for every address. ' + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
49048
49184
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49049
49185
|
inputSchema: external_exports.object({
|
|
49050
49186
|
view: viewParam(VIEWS2),
|
|
@@ -49090,7 +49226,7 @@ function registerMailTools(server, client2) {
|
|
|
49090
49226
|
server.registerTool(
|
|
49091
49227
|
"outlook_get_message",
|
|
49092
49228
|
{
|
|
49093
|
-
description: `Get one message including its body. The body is requested as plain text rather than HTML (measured ~9x smaller and far easier to read); pass view:"raw" to get Outlook's untouched record. ` + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
49229
|
+
description: `Get one message including its body. The body is requested as plain text rather than HTML (measured ~9x smaller and far easier to read); pass view:"raw" to get Outlook's untouched record. To and Cc list at most 20 addresses; a longer list adds ToCount/ToTruncated (CcCount/CcTruncated) with the full count \u2014 view:"raw" returns every address. ` + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
49094
49230
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49095
49231
|
inputSchema: external_exports.object({
|
|
49096
49232
|
view: viewParam(VIEWS2),
|
|
@@ -49107,6 +49243,91 @@ function registerMailTools(server, client2) {
|
|
|
49107
49243
|
return mailboxUntrusted(v === "full" ? fullMessage(data) : compactMessage(data));
|
|
49108
49244
|
}
|
|
49109
49245
|
);
|
|
49246
|
+
server.registerTool(
|
|
49247
|
+
"outlook_get_unread",
|
|
49248
|
+
{
|
|
49249
|
+
description: 'Triage batch: the unread messages in a folder (default inbox), newest first, each WITH its plain-text body (truncated to `maxBodyChars`) in one call \u2014 no follow-up outlook_get_message needed. Every item has a `kind`: "mail", "meetingRequest", "meetingCancelled" or "meetingResponse" (a reply to an invite you sent). Requests and cancellations also carry `event` (id, time, organizer, your current `responseStatus`) so the invite can be answered by its event id (when the meeting is no longer on your calendar \u2014 already declined, cancelled or deleted \u2014 the item has a `hint` instead and there is nothing to respond to). `to`/`cc` list at most 20 addresses; a longer list adds `toCount`/`ccCount` and `toTruncated`/`ccTruncated`. This does NOT mark anything read. Processing loop: read the batch \u2192 act on each item (reply, respond to the invite, file, flag \u2014 or leave it) \u2192 call outlook_mark_read on the ids you handled, so the next call returns only what is still new. Follow `nextLink` for more. ' + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
49250
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49251
|
+
inputSchema: external_exports.object({
|
|
49252
|
+
folder: external_exports.string().min(1).optional().describe('Folder id or well-known name (default "inbox")'),
|
|
49253
|
+
limit: external_exports.number().int().min(1).max(50).optional().describe("Max messages (default 25, max 50)"),
|
|
49254
|
+
sinceHours: external_exports.number().positive().max(24 * 365).optional().describe("Only mail received in the last N hours"),
|
|
49255
|
+
includeBody: external_exports.boolean().optional().describe("Fetch each plain-text body (default true). Invite events are resolved either way."),
|
|
49256
|
+
maxBodyChars: external_exports.number().int().min(100).max(1e5).optional().describe("Truncate each body to this many characters (default 4000); `bodyTruncated` marks a cut"),
|
|
49257
|
+
nextLink: nextLinkParam
|
|
49258
|
+
})
|
|
49259
|
+
},
|
|
49260
|
+
async ({ folder, limit, sinceHours, includeBody, maxBodyChars, nextLink }) => {
|
|
49261
|
+
const filter = [
|
|
49262
|
+
sinceHours !== void 0 ? `ReceivedDateTime ge ${sinceLiteral(sinceHours)}` : void 0,
|
|
49263
|
+
"IsRead eq false"
|
|
49264
|
+
].filter((c) => c !== void 0).join(" and ");
|
|
49265
|
+
const data = await fetchPage(
|
|
49266
|
+
client2,
|
|
49267
|
+
nextLink,
|
|
49268
|
+
`/me/mailfolders/${folderSegment(folder ?? "inbox")}/messages`,
|
|
49269
|
+
{
|
|
49270
|
+
query: {
|
|
49271
|
+
$top: limit ?? 25,
|
|
49272
|
+
$select: UNREAD_SELECT,
|
|
49273
|
+
$filter: filter,
|
|
49274
|
+
$orderby: "ReceivedDateTime desc"
|
|
49275
|
+
}
|
|
49276
|
+
}
|
|
49277
|
+
);
|
|
49278
|
+
const rows = Array.isArray(data?.value) ? data.value : [];
|
|
49279
|
+
const wantBody = includeBody !== false;
|
|
49280
|
+
const cap = maxBodyChars ?? 4e3;
|
|
49281
|
+
const items = await mapWithConcurrency(rows, BODY_CONCURRENCY, async (m) => {
|
|
49282
|
+
const kind = triageKind(m);
|
|
49283
|
+
const item = stripUndefined({
|
|
49284
|
+
id: m.Id,
|
|
49285
|
+
kind,
|
|
49286
|
+
receivedAt: m.ReceivedDateTime,
|
|
49287
|
+
from: addr(m.From ?? m.Sender),
|
|
49288
|
+
...cappedRecipients("to", addrs(m.ToRecipients)),
|
|
49289
|
+
...cappedRecipients("cc", addrs(m.CcRecipients)),
|
|
49290
|
+
subject: m.Subject,
|
|
49291
|
+
importance: m.Importance,
|
|
49292
|
+
hasAttachments: m.HasAttachments || void 0,
|
|
49293
|
+
categories: m.Categories?.length ? m.Categories : void 0,
|
|
49294
|
+
flag: m.Flag?.FlagStatus,
|
|
49295
|
+
conversationId: m.ConversationId
|
|
49296
|
+
});
|
|
49297
|
+
const wantEvent = kind === "meetingRequest" || kind === "meetingCancelled";
|
|
49298
|
+
if (!wantBody && !wantEvent || m.Id === void 0) return item;
|
|
49299
|
+
try {
|
|
49300
|
+
const one = await client2.get(
|
|
49301
|
+
`/me/messages/${encodeURIComponent(m.Id)}`,
|
|
49302
|
+
{
|
|
49303
|
+
text: wantBody,
|
|
49304
|
+
query: {
|
|
49305
|
+
$select: wantBody ? "Id,Body" : "Id",
|
|
49306
|
+
$expand: wantEvent ? EVENT_EXPAND : void 0
|
|
49307
|
+
}
|
|
49308
|
+
}
|
|
49309
|
+
);
|
|
49310
|
+
if (wantBody) {
|
|
49311
|
+
const text = (one.Body?.Content ?? "").trim();
|
|
49312
|
+
item.body = text.length > cap ? truncateChars(text, cap) : text;
|
|
49313
|
+
if (text.length > cap) item.bodyTruncated = true;
|
|
49314
|
+
}
|
|
49315
|
+
if (wantEvent) {
|
|
49316
|
+
const event = resolvedEvent(one.Event);
|
|
49317
|
+
if (event) item.event = triageEvent(event);
|
|
49318
|
+
else item.hint = INVITE_UNRESOLVED_HINT;
|
|
49319
|
+
}
|
|
49320
|
+
} catch (e) {
|
|
49321
|
+
item.error = e instanceof Error ? e.message : String(e);
|
|
49322
|
+
}
|
|
49323
|
+
return item;
|
|
49324
|
+
});
|
|
49325
|
+
const out = { count: items.length, items };
|
|
49326
|
+
const next = data?.["@odata.nextLink"];
|
|
49327
|
+
if (next) out.nextLink = next;
|
|
49328
|
+
return mailboxUntrusted(out);
|
|
49329
|
+
}
|
|
49330
|
+
);
|
|
49110
49331
|
server.registerTool(
|
|
49111
49332
|
"outlook_list_attachments",
|
|
49112
49333
|
{
|
|
@@ -49127,6 +49348,70 @@ function registerMailTools(server, client2) {
|
|
|
49127
49348
|
return minifiedResult(plainCollection(data));
|
|
49128
49349
|
}
|
|
49129
49350
|
);
|
|
49351
|
+
server.registerTool(
|
|
49352
|
+
"outlook_get_attachment",
|
|
49353
|
+
{
|
|
49354
|
+
description: "Get one attachment's content (ids from outlook_list_attachments). An image comes back as image content the model can see; a text-like file (text/*, JSON, CSV, XML, .ics) is decoded to `text`, capped at 200 KB (`textTruncated` marks a cut). Any other binary (PDF, Office, archives) and anything over 5 MB is returned as metadata only \u2014 its bytes are not inlined. An attached Outlook item (a forwarded email or event) or a cloud-file link has no bytes to return; the result says which. " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
49355
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49356
|
+
inputSchema: external_exports.object({
|
|
49357
|
+
messageId: external_exports.string().min(1).describe("Message Id"),
|
|
49358
|
+
attachmentId: external_exports.string().min(1).describe("Attachment Id from outlook_list_attachments")
|
|
49359
|
+
})
|
|
49360
|
+
},
|
|
49361
|
+
async ({ messageId, attachmentId }) => {
|
|
49362
|
+
const path = `/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`;
|
|
49363
|
+
const meta3 = await client2.get(path, { query: { $select: ATTACHMENT_META_SELECT } });
|
|
49364
|
+
const kind = attachmentKind(meta3);
|
|
49365
|
+
const contentType = meta3.ContentType ?? "";
|
|
49366
|
+
const name = meta3.Name ?? "";
|
|
49367
|
+
const out = stripUndefined({
|
|
49368
|
+
messageId,
|
|
49369
|
+
attachmentId,
|
|
49370
|
+
name: meta3.Name,
|
|
49371
|
+
contentType: meta3.ContentType ?? void 0,
|
|
49372
|
+
size: meta3.Size,
|
|
49373
|
+
isInline: meta3.IsInline || void 0,
|
|
49374
|
+
kind,
|
|
49375
|
+
inlined: false
|
|
49376
|
+
});
|
|
49377
|
+
if (kind === "item") {
|
|
49378
|
+
out.hint = "This is an attached Outlook item (an email, event or contact), not a file, so it has no bytes to return. Ask the sender for it, or open the parent message in Outlook.";
|
|
49379
|
+
return mailboxUntrusted(out);
|
|
49380
|
+
}
|
|
49381
|
+
if (kind === "reference") {
|
|
49382
|
+
out.hint = "This is a link to a cloud file (OneDrive/SharePoint), not a file stored in the message, so there are no bytes to return.";
|
|
49383
|
+
return mailboxUntrusted(out);
|
|
49384
|
+
}
|
|
49385
|
+
const image = isImage(contentType);
|
|
49386
|
+
if (!image && !isText(contentType, name)) {
|
|
49387
|
+
out.hint = "Binary content is not inlined: only images and text-like files are returned. Open it in Outlook to read it.";
|
|
49388
|
+
return mailboxUntrusted(out);
|
|
49389
|
+
}
|
|
49390
|
+
if ((meta3.Size ?? 0) > MAX_INLINE_BYTES) {
|
|
49391
|
+
out.hint = "Content is not inlined above 5 MB. Open it in Outlook to read it.";
|
|
49392
|
+
return mailboxUntrusted(out);
|
|
49393
|
+
}
|
|
49394
|
+
const full = await client2.get(path);
|
|
49395
|
+
const bytes = full.ContentBytes;
|
|
49396
|
+
if (typeof bytes !== "string" || bytes.length === 0) {
|
|
49397
|
+
out.hint = "Outlook returned no content for this attachment.";
|
|
49398
|
+
return mailboxUntrusted(out);
|
|
49399
|
+
}
|
|
49400
|
+
if (base64DecodedLength(bytes) > MAX_INLINE_BYTES) {
|
|
49401
|
+
out.hint = "Content is not inlined above 5 MB. Open it in Outlook to read it.";
|
|
49402
|
+
return mailboxUntrusted(out);
|
|
49403
|
+
}
|
|
49404
|
+
const buf = Buffer.from(bytes, "base64");
|
|
49405
|
+
out.inlined = true;
|
|
49406
|
+
if (image) {
|
|
49407
|
+
const env = mailboxUntrusted(out);
|
|
49408
|
+
return { ...env, content: [...env.content, ...imageResult(bytes, contentType).content] };
|
|
49409
|
+
}
|
|
49410
|
+
out.text = buf.subarray(0, utf8Boundary(buf, MAX_TEXT_BYTES)).toString("utf8");
|
|
49411
|
+
if (buf.length > MAX_TEXT_BYTES) out.textTruncated = true;
|
|
49412
|
+
return mailboxUntrusted(out);
|
|
49413
|
+
}
|
|
49414
|
+
);
|
|
49130
49415
|
}
|
|
49131
49416
|
|
|
49132
49417
|
// src/tools/calendar.ts
|
|
@@ -49153,7 +49438,7 @@ async function mailboxTimeZone(client2, opts = {}) {
|
|
|
49153
49438
|
}
|
|
49154
49439
|
|
|
49155
49440
|
// src/tools/calendar.ts
|
|
49156
|
-
var EVENT_SELECT = "Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,OnlineMeeting,BodyPreview";
|
|
49441
|
+
var EVENT_SELECT = "Id,Subject,Start,End,Location,Organizer,IsAllDay,IsCancelled,ShowAs,OnlineMeetingUrl,OnlineMeeting,BodyPreview,IsOrganizer,ResponseStatus,ResponseRequested,Type,SeriesMasterId";
|
|
49157
49442
|
function dateTimeTimeZone(value, zone) {
|
|
49158
49443
|
if (/(Z|[+-]\d{2}:?\d{2})$/i.test(value)) {
|
|
49159
49444
|
const d = new Date(value);
|
|
@@ -49258,12 +49543,13 @@ function registerCalendarTools(server, client2) {
|
|
|
49258
49543
|
server.registerTool(
|
|
49259
49544
|
"outlook_find_meeting_times",
|
|
49260
49545
|
{
|
|
49261
|
-
description: "Find times when people can meet \u2014 Outlook's Scheduling Assistant. Give the attendees, a search window and a duration; Outlook checks everyone's free/busy (and the signed-in user's, as organizer) and returns ranked candidate slots with each attendee's availability. By default only working hours are searched. Use this to answer \"when can X and Y meet?\"; then book with outlook_create_event. `start`/`end` are local wall-clock times in `timeZone` (default: the mailbox zone), and returned slots are in that zone too. " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
49546
|
+
description: "Find times when people can meet \u2014 Outlook's Scheduling Assistant. Give the attendees, a search window and a duration; Outlook checks everyone's free/busy (and the signed-in user's, as organizer) and returns ranked candidate slots with each attendee's availability. By default only working hours are searched. Use this to answer \"when can X and Y meet?\"; then book with outlook_create_event \u2014 each suggestion carries `createEventArgs` (times, zone and attendees; `subject` too when given here) to pass to it as-is. `start`/`end` are local wall-clock times in `timeZone` (default: the mailbox zone), and returned slots are in that zone too. view: 'raw' returns Outlook's response as-is, without `createEventArgs`. " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
49262
49547
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49263
49548
|
inputSchema: external_exports.object({
|
|
49264
49549
|
view: viewParam(VIEWS2),
|
|
49265
49550
|
attendees: attendeeList("required attendees").min(1),
|
|
49266
49551
|
optionalAttendees: attendeeList("optional attendees").optional(),
|
|
49552
|
+
subject: external_exports.string().optional().describe("Meeting title, carried into each suggestion's createEventArgs; not sent to Outlook"),
|
|
49267
49553
|
start: external_exports.string().min(1).describe("Earliest the meeting may start, e.g. 2026-10-12T09:00:00"),
|
|
49268
49554
|
end: external_exports.string().min(1).describe("Latest the meeting may end, e.g. 2026-10-16T17:00:00"),
|
|
49269
49555
|
durationMinutes: external_exports.number().int().min(5).max(1440).describe("Meeting length in minutes"),
|
|
@@ -49299,19 +49585,37 @@ function registerCalendarTools(server, client2) {
|
|
|
49299
49585
|
};
|
|
49300
49586
|
const data = await client2.post("/me/findmeetingtimes", body, { prefer: `outlook.timezone="${zone}"` });
|
|
49301
49587
|
if (resolveView(args.view, VIEWS2) === "raw") return mailboxUntrusted(data);
|
|
49302
|
-
const suggestions = (data?.MeetingTimeSuggestions ?? []).map((s) =>
|
|
49303
|
-
|
|
49304
|
-
|
|
49305
|
-
|
|
49306
|
-
|
|
49307
|
-
|
|
49308
|
-
|
|
49309
|
-
|
|
49310
|
-
|
|
49311
|
-
|
|
49312
|
-
|
|
49313
|
-
|
|
49314
|
-
|
|
49588
|
+
const suggestions = (data?.MeetingTimeSuggestions ?? []).map((s) => {
|
|
49589
|
+
const start = trimTime(s.MeetingTimeSlot?.Start?.DateTime);
|
|
49590
|
+
const end = trimTime(s.MeetingTimeSlot?.End?.DateTime);
|
|
49591
|
+
return {
|
|
49592
|
+
Start: start,
|
|
49593
|
+
End: end,
|
|
49594
|
+
Confidence: s.Confidence,
|
|
49595
|
+
Organizer: s.OrganizerAvailability,
|
|
49596
|
+
Attendees: Object.fromEntries(
|
|
49597
|
+
(s.AttendeeAvailability ?? []).map((a) => [
|
|
49598
|
+
a.Attendee?.EmailAddress?.Address ?? a.Attendee?.EmailAddress?.Name ?? "?",
|
|
49599
|
+
a.Availability
|
|
49600
|
+
])
|
|
49601
|
+
),
|
|
49602
|
+
Reason: s.SuggestionReason,
|
|
49603
|
+
// outlook_create_event's input for this slot, so booking it is a
|
|
49604
|
+
// straight hand-off. The slot came back in `zone` (the Prefer header),
|
|
49605
|
+
// which is also the zone create reads wall-clock times in. A slot
|
|
49606
|
+
// without times cannot be booked, so it gets no args.
|
|
49607
|
+
...start && end ? {
|
|
49608
|
+
createEventArgs: {
|
|
49609
|
+
...args.subject !== void 0 ? { subject: args.subject } : {},
|
|
49610
|
+
start,
|
|
49611
|
+
end,
|
|
49612
|
+
timeZone: zone,
|
|
49613
|
+
attendees: args.attendees,
|
|
49614
|
+
...args.optionalAttendees?.length ? { optionalAttendees: args.optionalAttendees } : {}
|
|
49615
|
+
}
|
|
49616
|
+
} : {}
|
|
49617
|
+
};
|
|
49618
|
+
});
|
|
49315
49619
|
return mailboxUntrusted({
|
|
49316
49620
|
timeZone: zone,
|
|
49317
49621
|
count: suggestions.length,
|
|
@@ -49478,6 +49782,21 @@ function registerDirectoryTools(server, client2) {
|
|
|
49478
49782
|
return minifiedResult(collection(data, resolveView(view, VIEWS2) === "raw"));
|
|
49479
49783
|
}
|
|
49480
49784
|
);
|
|
49785
|
+
server.registerTool(
|
|
49786
|
+
"outlook_list_categories",
|
|
49787
|
+
{
|
|
49788
|
+
description: "List the mailbox's categories (the master list) with their colours \u2014 the names outlook_update_message can add or remove.",
|
|
49789
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
49790
|
+
inputSchema: external_exports.object({})
|
|
49791
|
+
},
|
|
49792
|
+
async () => {
|
|
49793
|
+
const data = await client2.get(
|
|
49794
|
+
"/me/outlook/masterCategories"
|
|
49795
|
+
);
|
|
49796
|
+
const categories = (data?.value ?? []).map((c) => ({ name: c.DisplayName, color: c.Color }));
|
|
49797
|
+
return minifiedResult({ count: categories.length, categories });
|
|
49798
|
+
}
|
|
49799
|
+
);
|
|
49481
49800
|
}
|
|
49482
49801
|
|
|
49483
49802
|
// src/tools/writes.ts
|
|
@@ -49530,9 +49849,72 @@ async function writeOrUnknown(run, checkWhere) {
|
|
|
49530
49849
|
};
|
|
49531
49850
|
}
|
|
49532
49851
|
}
|
|
49852
|
+
var INVITE_EVENT_SELECT = "Id,Subject,IsOrganizer,IsCancelled,Start,End,Organizer,ResponseStatus";
|
|
49853
|
+
var DELETE_EVENT_SELECT = "Id,Subject,IsOrganizer,IsCancelled,ResponseStatus,Type,SeriesMasterId,Start,End,Organizer,Attendees";
|
|
49854
|
+
var INVITE_RESPONSES = {
|
|
49855
|
+
accept: { verb: "accept", status: "Accepted" },
|
|
49856
|
+
tentative: { verb: "tentativelyaccept", status: "TentativelyAccepted" },
|
|
49857
|
+
decline: { verb: "decline", status: "Declined" }
|
|
49858
|
+
};
|
|
49533
49859
|
var naiveMs = (dt) => Date.parse(`${dt.replace(/\.\d+$/, "")}Z`);
|
|
49534
49860
|
var fromNaiveMs = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "");
|
|
49535
49861
|
var trimTime2 = (dt) => dt?.replace(/\.\d+$/, "");
|
|
49862
|
+
var REPLY_MODES = {
|
|
49863
|
+
reply: { send: "reply", draft: "createreply", prefix: "RE: " },
|
|
49864
|
+
replyAll: { send: "replyall", draft: "createreplyall", prefix: "RE: " },
|
|
49865
|
+
forward: { send: "forward", draft: "createforward", prefix: "FW: " }
|
|
49866
|
+
};
|
|
49867
|
+
function addresses(...lists) {
|
|
49868
|
+
const seen = /* @__PURE__ */ new Map();
|
|
49869
|
+
for (const a of lists.flat()) {
|
|
49870
|
+
const v = a?.EmailAddress?.Address;
|
|
49871
|
+
if (v && !seen.has(v.toLowerCase())) seen.set(v.toLowerCase(), v);
|
|
49872
|
+
}
|
|
49873
|
+
return [...seen.values()];
|
|
49874
|
+
}
|
|
49875
|
+
var escapeHtml = (t) => t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
49876
|
+
function bodyWithComment(body, comment) {
|
|
49877
|
+
const existing = body?.Content ?? "";
|
|
49878
|
+
if (body?.ContentType?.toLowerCase() !== "html") {
|
|
49879
|
+
return { ContentType: "Text", Content: existing ? `${comment}
|
|
49880
|
+
|
|
49881
|
+
${existing}` : comment };
|
|
49882
|
+
}
|
|
49883
|
+
const block = `<div>${escapeHtml(comment).replace(/\r?\n/g, "<br>")}</div><br>`;
|
|
49884
|
+
const open2 = /<body[^>]*>/i.exec(existing);
|
|
49885
|
+
const Content = open2 ? existing.slice(0, open2.index + open2[0].length) + block + existing.slice(open2.index + open2[0].length) : block + existing;
|
|
49886
|
+
return { ContentType: "HTML", Content };
|
|
49887
|
+
}
|
|
49888
|
+
var FLAG_STATUS = { flagged: "Flagged", complete: "Complete", none: "NotFlagged" };
|
|
49889
|
+
var UPDATE_CONCURRENCY = 4;
|
|
49890
|
+
var sameCategories = (a, b) => [...a ?? []].map((c) => c.toLowerCase()).sort().join("\n") === [...b ?? []].map((c) => c.toLowerCase()).sort().join("\n");
|
|
49891
|
+
function editCategories(current, add, remove) {
|
|
49892
|
+
const gone = new Set(remove.map((c) => c.toLowerCase()));
|
|
49893
|
+
const next = (current ?? []).filter((c) => !gone.has(c.toLowerCase()));
|
|
49894
|
+
for (const c of add) {
|
|
49895
|
+
if (!next.some((n) => n.toLowerCase() === c.toLowerCase())) next.push(c);
|
|
49896
|
+
}
|
|
49897
|
+
return next;
|
|
49898
|
+
}
|
|
49899
|
+
function unchangedFields(payload, after) {
|
|
49900
|
+
const out = [];
|
|
49901
|
+
if (payload.IsRead !== void 0 && after.IsRead !== payload.IsRead) out.push("IsRead");
|
|
49902
|
+
if (payload.Flag && after.Flag?.FlagStatus !== payload.Flag.FlagStatus) out.push("Flag");
|
|
49903
|
+
if (payload.Categories && !sameCategories(after.Categories, payload.Categories)) out.push("Categories");
|
|
49904
|
+
return out;
|
|
49905
|
+
}
|
|
49906
|
+
var createEventInput = external_exports.object({
|
|
49907
|
+
subject: external_exports.string().describe("Event title"),
|
|
49908
|
+
start: external_exports.string().min(1).describe("Start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
|
|
49909
|
+
end: external_exports.string().min(1).describe("End, ISO 8601 local time"),
|
|
49910
|
+
timeZone: external_exports.string().optional().describe("Windows time-zone name. Defaults to the MAILBOX time zone."),
|
|
49911
|
+
location: external_exports.string().optional().describe("Location display name"),
|
|
49912
|
+
body: external_exports.string().optional().describe("Event description"),
|
|
49913
|
+
attendees: recipientList.describe("Required attendees' email addresses"),
|
|
49914
|
+
optionalAttendees: recipientList.describe("Optional attendees' email addresses"),
|
|
49915
|
+
teamsMeeting: teamsMeetingParam,
|
|
49916
|
+
confirmToken: confirmTokenParam
|
|
49917
|
+
});
|
|
49536
49918
|
function registerWriteTools(server, client2) {
|
|
49537
49919
|
server.registerTool(
|
|
49538
49920
|
"outlook_send_mail",
|
|
@@ -49651,6 +50033,85 @@ function registerWriteTools(server, client2) {
|
|
|
49651
50033
|
});
|
|
49652
50034
|
}
|
|
49653
50035
|
);
|
|
50036
|
+
server.registerTool(
|
|
50037
|
+
"outlook_update_message",
|
|
50038
|
+
{
|
|
50039
|
+
description: 'Flag, categorise and/or mark read up to 50 messages at once \u2014 the filing step of the triage loop (outlook_get_unread \u2192 act \u2192 update). `flag`: "flagged", "complete" or "none". `categories` replaces a message\'s categories; `addCategories`/`removeCategories` edit each message\'s current list instead (names from outlook_list_categories). Duplicate ids are collapsed, so each message is written once. Each id gets its own result `{ id, ok, error? }`, verified against what Outlook stored; one failure does not stop the rest, but a rejected or missing credential fails the whole call. Only your mailbox changes \u2014 nobody is notified. ' + CONFIRM_FLOW_SENTENCE,
|
|
50040
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
50041
|
+
inputSchema: external_exports.object({
|
|
50042
|
+
messageIds: external_exports.array(external_exports.string().min(1)).min(1).max(50).describe("Message Ids (1 to 50)"),
|
|
50043
|
+
flag: external_exports.enum(["flagged", "complete", "none"]).optional().describe("Follow-up flag to set"),
|
|
50044
|
+
categories: external_exports.array(external_exports.string().min(1)).optional().describe("Replace the categories with exactly these ([] clears them)"),
|
|
50045
|
+
addCategories: external_exports.array(external_exports.string().min(1)).optional().describe("Categories to add"),
|
|
50046
|
+
removeCategories: external_exports.array(external_exports.string().min(1)).optional().describe("Categories to remove"),
|
|
50047
|
+
isRead: external_exports.boolean().optional().describe("true to mark read, false to mark unread"),
|
|
50048
|
+
confirmToken: confirmTokenParam
|
|
50049
|
+
})
|
|
50050
|
+
},
|
|
50051
|
+
async ({ messageIds, flag, categories, addCategories, removeCategories, isRead, confirmToken }, ctx) => {
|
|
50052
|
+
const add = addCategories ?? [];
|
|
50053
|
+
const remove = removeCategories ?? [];
|
|
50054
|
+
const editing = add.length > 0 || remove.length > 0;
|
|
50055
|
+
if (categories !== void 0 && editing) {
|
|
50056
|
+
throw new McpToolError("Pass `categories` or `addCategories`/`removeCategories`, not both.", {
|
|
50057
|
+
hint: "`categories` replaces the whole list; the add/remove pair edits the current one."
|
|
50058
|
+
});
|
|
50059
|
+
}
|
|
50060
|
+
const fixed = {
|
|
50061
|
+
...isRead !== void 0 ? { IsRead: isRead } : {},
|
|
50062
|
+
...flag ? { Flag: { FlagStatus: FLAG_STATUS[flag] } } : {},
|
|
50063
|
+
...categories !== void 0 ? { Categories: categories } : {}
|
|
50064
|
+
};
|
|
50065
|
+
if (Object.keys(fixed).length === 0 && !editing) {
|
|
50066
|
+
throw new McpToolError("Nothing to update.", {
|
|
50067
|
+
hint: "Pass at least one of flag, isRead, categories, addCategories or removeCategories."
|
|
50068
|
+
});
|
|
50069
|
+
}
|
|
50070
|
+
const ids = [...new Set(messageIds)];
|
|
50071
|
+
const pathOf = (id) => `/me/messages/${encodeURIComponent(id)}`;
|
|
50072
|
+
const changes = [
|
|
50073
|
+
...flag ? [`flag ${flag}`] : [],
|
|
50074
|
+
...isRead !== void 0 ? [isRead ? "mark read" : "mark unread"] : [],
|
|
50075
|
+
...categories !== void 0 ? [`set categories [${categories.join(", ")}]`] : [],
|
|
50076
|
+
...add.length ? [`add categories [${add.join(", ")}]`] : [],
|
|
50077
|
+
...remove.length ? [`remove categories [${remove.join(", ")}]`] : []
|
|
50078
|
+
];
|
|
50079
|
+
const gate = await confirmWrite(ctx, {
|
|
50080
|
+
tool: "outlook_update_message",
|
|
50081
|
+
action: "mail.update",
|
|
50082
|
+
message: "Review and confirm this change:",
|
|
50083
|
+
summary: `Update ${ids.length} message${ids.length === 1 ? "" : "s"}: ${changes.join("; ")}`,
|
|
50084
|
+
// One signed-in mailbox per server process.
|
|
50085
|
+
account: void 0,
|
|
50086
|
+
target: ids.join(","),
|
|
50087
|
+
request: {
|
|
50088
|
+
method: "PATCH",
|
|
50089
|
+
path: ids.length === 1 ? pathOf(ids[0]) : "/me/messages/{id}",
|
|
50090
|
+
body: editing ? { ...fixed, Categories: `(each message's current categories${add.length ? ` + ${add.join(", ")}` : ""}${remove.length ? ` - ${remove.join(", ")}` : ""})` } : fixed
|
|
50091
|
+
},
|
|
50092
|
+
confirmToken
|
|
50093
|
+
});
|
|
50094
|
+
if (gate) return gate;
|
|
50095
|
+
const results = await mapWithConcurrency(ids, UPDATE_CONCURRENCY, async (id) => {
|
|
50096
|
+
try {
|
|
50097
|
+
const payload = { ...fixed };
|
|
50098
|
+
if (editing) {
|
|
50099
|
+
const current = await client2.get(`${pathOf(id)}?$select=Categories`);
|
|
50100
|
+
payload.Categories = editCategories(current?.Categories, add, remove);
|
|
50101
|
+
}
|
|
50102
|
+
const echoed = await client2.write("PATCH", pathOf(id), payload);
|
|
50103
|
+
const after = echoed && typeof echoed === "object" ? echoed : await client2.get(`${pathOf(id)}?$select=IsRead,Flag,Categories`) ?? {};
|
|
50104
|
+
const unchanged = unchangedFields(payload, after);
|
|
50105
|
+
return unchanged.length ? { id, ok: false, error: `Outlook accepted the write but these did not change: ${unchanged.join(", ")}.` } : { id, ok: true };
|
|
50106
|
+
} catch (e) {
|
|
50107
|
+
if (isCredentialFailure(e)) throw e;
|
|
50108
|
+
return { id, ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
50109
|
+
}
|
|
50110
|
+
});
|
|
50111
|
+
const updated = results.filter((r) => r.ok).length;
|
|
50112
|
+
return minifiedResult({ updated, failed: results.length - updated, results });
|
|
50113
|
+
}
|
|
50114
|
+
);
|
|
49654
50115
|
server.registerTool(
|
|
49655
50116
|
"outlook_move_message",
|
|
49656
50117
|
{
|
|
@@ -49693,20 +50154,9 @@ function registerWriteTools(server, client2) {
|
|
|
49693
50154
|
server.registerTool(
|
|
49694
50155
|
"outlook_create_event",
|
|
49695
50156
|
{
|
|
49696
|
-
description: 'Create a calendar event or meeting. A Microsoft Teams meeting is attached by default (`teamsMeeting: false` to skip) and its join link is returned. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name. Attendees are emailed an invitation. To pick a time first, use outlook_find_meeting_times. ' + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
50157
|
+
description: 'Create a calendar event or meeting. A Microsoft Teams meeting is attached by default (`teamsMeeting: false` to skip) and its join link is returned. `timeZone` takes a WINDOWS zone name such as "Eastern Standard Time", not an IANA name. Attendees are emailed an invitation. To pick a time first, use outlook_find_meeting_times: each suggestion it returns carries `createEventArgs` that can be passed here as-is (add `subject` if the search did not set one). ' + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
49697
50158
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
49698
|
-
inputSchema:
|
|
49699
|
-
subject: external_exports.string().describe("Event title"),
|
|
49700
|
-
start: external_exports.string().min(1).describe("Start, ISO 8601 local time e.g. 2026-09-22T15:00:00"),
|
|
49701
|
-
end: external_exports.string().min(1).describe("End, ISO 8601 local time"),
|
|
49702
|
-
timeZone: external_exports.string().optional().describe("Windows time-zone name. Defaults to the MAILBOX time zone."),
|
|
49703
|
-
location: external_exports.string().optional().describe("Location display name"),
|
|
49704
|
-
body: external_exports.string().optional().describe("Event description"),
|
|
49705
|
-
attendees: recipientList.describe("Required attendees' email addresses"),
|
|
49706
|
-
optionalAttendees: recipientList.describe("Optional attendees' email addresses"),
|
|
49707
|
-
teamsMeeting: teamsMeetingParam,
|
|
49708
|
-
confirmToken: confirmTokenParam
|
|
49709
|
-
})
|
|
50159
|
+
inputSchema: createEventInput
|
|
49710
50160
|
},
|
|
49711
50161
|
async ({ subject, start, end, timeZone, location, body, attendees, optionalAttendees, teamsMeeting, confirmToken }, ctx) => {
|
|
49712
50162
|
const wantTeams = teamsMeeting !== false;
|
|
@@ -49874,6 +50324,282 @@ function registerWriteTools(server, client2) {
|
|
|
49874
50324
|
});
|
|
49875
50325
|
}
|
|
49876
50326
|
);
|
|
50327
|
+
server.registerTool(
|
|
50328
|
+
"outlook_respond_to_invite",
|
|
50329
|
+
{
|
|
50330
|
+
description: "Accept, tentatively accept or decline a meeting invitation. Pass the invite's `messageId` (from outlook_get_unread, which marks it kind \"meetingRequest\") or the event's `eventId` \u2014 exactly one. The organizer is sent your response unless `sendResponse: false`, in which case only your calendar changes and no confirmation is asked. Cancelled meetings and meetings you organize are refused. The result is verified by re-reading the event's response status; a declined meeting may leave your calendar entirely. " + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
50331
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
50332
|
+
inputSchema: external_exports.object({
|
|
50333
|
+
messageId: external_exports.string().min(1).optional().describe("Id of the invite message (resolved to its event). Give this or eventId."),
|
|
50334
|
+
eventId: external_exports.string().min(1).optional().describe("Event Id. Give this or messageId."),
|
|
50335
|
+
response: external_exports.enum(["accept", "tentative", "decline"]).describe("Your answer"),
|
|
50336
|
+
comment: external_exports.string().optional().describe("Note to the organizer sent with the response"),
|
|
50337
|
+
sendResponse: external_exports.boolean().optional().describe("Tell the organizer (default true). false changes only your calendar."),
|
|
50338
|
+
confirmToken: confirmTokenParam
|
|
50339
|
+
})
|
|
50340
|
+
},
|
|
50341
|
+
async ({ messageId, eventId, response, comment, sendResponse, confirmToken }, ctx) => {
|
|
50342
|
+
if (messageId === void 0 === (eventId === void 0)) {
|
|
50343
|
+
throw new McpToolError("Pass exactly one of messageId or eventId.", {
|
|
50344
|
+
hint: "messageId is the invite in your inbox; eventId is the meeting on your calendar."
|
|
50345
|
+
});
|
|
50346
|
+
}
|
|
50347
|
+
let event;
|
|
50348
|
+
if (messageId !== void 0) {
|
|
50349
|
+
const msg = await client2.get(
|
|
50350
|
+
`/me/messages/${encodeURIComponent(messageId)}`,
|
|
50351
|
+
{ query: { $select: `Id,${MEETING_MESSAGE_TYPE}`, $expand: EVENT_EXPAND } }
|
|
50352
|
+
);
|
|
50353
|
+
event = resolvedEvent(msg?.Event);
|
|
50354
|
+
if (!event) {
|
|
50355
|
+
if (isMeetingMessage(msg)) {
|
|
50356
|
+
throw new McpToolError(
|
|
50357
|
+
"This invite's meeting is no longer on your calendar (already declined, cancelled or deleted) \u2014 there is nothing to respond to.",
|
|
50358
|
+
{ hint: "Mark the invite read or file it; answering it again is not possible." }
|
|
50359
|
+
);
|
|
50360
|
+
}
|
|
50361
|
+
throw new McpToolError("That message is not a meeting invite.", {
|
|
50362
|
+
hint: 'Only messages outlook_get_unread marks kind "meetingRequest" carry an event to answer.'
|
|
50363
|
+
});
|
|
50364
|
+
}
|
|
50365
|
+
} else {
|
|
50366
|
+
event = await client2.get(
|
|
50367
|
+
`/me/events/${encodeURIComponent(eventId)}?$select=${INVITE_EVENT_SELECT}`
|
|
50368
|
+
);
|
|
50369
|
+
}
|
|
50370
|
+
const id = event?.Id ?? eventId;
|
|
50371
|
+
if (event?.IsCancelled === true) {
|
|
50372
|
+
throw new McpToolError("This meeting has been cancelled; there is nothing to respond to.", {
|
|
50373
|
+
hint: "Remove it from your calendar with outlook_delete_event if it is still shown."
|
|
50374
|
+
});
|
|
50375
|
+
}
|
|
50376
|
+
if (event?.IsOrganizer === true || event?.ResponseStatus?.Response === "Organizer") {
|
|
50377
|
+
throw new McpToolError("You organize this meeting, so you cannot respond to it.", {
|
|
50378
|
+
hint: "To change or call it off, use outlook_update_event or Outlook itself."
|
|
50379
|
+
});
|
|
50380
|
+
}
|
|
50381
|
+
const { verb, status } = INVITE_RESPONSES[response];
|
|
50382
|
+
const notify = sendResponse !== false;
|
|
50383
|
+
if (!notify && comment) {
|
|
50384
|
+
throw new McpToolError("A comment can only go with a response the organizer receives.", {
|
|
50385
|
+
hint: "Drop the comment, or leave sendResponse at its default (true) so the organizer gets both."
|
|
50386
|
+
});
|
|
50387
|
+
}
|
|
50388
|
+
const path = `/me/events/${encodeURIComponent(id)}/${verb}`;
|
|
50389
|
+
const payload = { ...comment ? { Comment: comment } : {}, SendResponse: notify };
|
|
50390
|
+
if (notify) {
|
|
50391
|
+
const when = `${trimTime2(event?.Start?.DateTime) ?? "?"} to ${trimTime2(event?.End?.DateTime) ?? "?"}${event?.Start?.TimeZone ? ` (${event.Start.TimeZone})` : ""}`;
|
|
50392
|
+
const organizer = event?.Organizer?.EmailAddress?.Address ?? "the organizer";
|
|
50393
|
+
const gate = await confirmWrite(ctx, {
|
|
50394
|
+
tool: "outlook_respond_to_invite",
|
|
50395
|
+
action: "calendar.respond_to_invite",
|
|
50396
|
+
message: "Review and confirm this response (the organizer is notified):",
|
|
50397
|
+
summary: `${response === "tentative" ? "Tentatively accept" : response === "accept" ? "Accept" : "Decline"} "${event?.Subject ?? id}" ${when}, organized by ${organizer}` + (comment ? `, with comment: "${comment}"` : ""),
|
|
50398
|
+
// One signed-in mailbox per server process.
|
|
50399
|
+
account: void 0,
|
|
50400
|
+
target: id,
|
|
50401
|
+
request: { method: "POST", path, body: payload },
|
|
50402
|
+
confirmToken
|
|
50403
|
+
});
|
|
50404
|
+
if (gate) return gate;
|
|
50405
|
+
}
|
|
50406
|
+
const write = await writeOrUnknown(
|
|
50407
|
+
() => client2.write("POST", path, payload),
|
|
50408
|
+
"the event's response (outlook_get_event)"
|
|
50409
|
+
);
|
|
50410
|
+
if (!write.ok) return write.result;
|
|
50411
|
+
let after;
|
|
50412
|
+
try {
|
|
50413
|
+
after = await client2.get(
|
|
50414
|
+
`/me/events/${encodeURIComponent(id)}?$select=ResponseStatus`
|
|
50415
|
+
);
|
|
50416
|
+
} catch (e) {
|
|
50417
|
+
if (response === "decline" && errorStatusOf(e) === 404) {
|
|
50418
|
+
return minifiedResult({
|
|
50419
|
+
responded: true,
|
|
50420
|
+
eventId: id,
|
|
50421
|
+
response: status,
|
|
50422
|
+
sentResponse: notify,
|
|
50423
|
+
note: "The declined meeting is no longer on your calendar."
|
|
50424
|
+
});
|
|
50425
|
+
}
|
|
50426
|
+
return minifiedResult({
|
|
50427
|
+
responded: null,
|
|
50428
|
+
eventId: id,
|
|
50429
|
+
sentResponse: notify,
|
|
50430
|
+
warning: `Outlook accepted the response but re-reading the event failed (${e instanceof Error ? e.message : String(e)}), so it is unverified.`
|
|
50431
|
+
});
|
|
50432
|
+
}
|
|
50433
|
+
const got = after?.ResponseStatus?.Response;
|
|
50434
|
+
return minifiedResult({
|
|
50435
|
+
responded: got === status,
|
|
50436
|
+
eventId: id,
|
|
50437
|
+
response: got,
|
|
50438
|
+
sentResponse: notify,
|
|
50439
|
+
...got === status ? {} : { warning: `Outlook accepted the response but the status did not change (still ${got ?? "unknown"}).` }
|
|
50440
|
+
});
|
|
50441
|
+
}
|
|
50442
|
+
);
|
|
50443
|
+
server.registerTool(
|
|
50444
|
+
"outlook_delete_event",
|
|
50445
|
+
{
|
|
50446
|
+
description: `Delete a calendar event. An occurrence's id deletes only that occurrence; a series master's id (Type "SeriesMaster" in outlook_get_event) deletes the WHOLE series. Only the organizer can delete a meeting \u2014 for an invite you received, decline it with outlook_respond_to_invite instead (a meeting the organizer already cancelled can be removed here). Deleting a meeting that has attendees sends each of them a cancellation, so it is confirmed first; your own appointment with no attendees, or a cancelled meeting, is removed without asking because only your calendar changes. The result is verified by re-reading the event. ` + CONFIRM_FLOW_SENTENCE + " " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE,
|
|
50447
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
50448
|
+
inputSchema: external_exports.object({
|
|
50449
|
+
id: external_exports.string().min(1).describe("Event Id from outlook_list_events or outlook_get_event (an occurrence, or a series master)"),
|
|
50450
|
+
confirmToken: confirmTokenParam
|
|
50451
|
+
})
|
|
50452
|
+
},
|
|
50453
|
+
async ({ id, confirmToken }, ctx) => {
|
|
50454
|
+
const path = `/me/events/${encodeURIComponent(id)}`;
|
|
50455
|
+
const zone = await mailboxTimeZone(client2);
|
|
50456
|
+
const event = await client2.get(
|
|
50457
|
+
`${path}?$select=${DELETE_EVENT_SELECT}`,
|
|
50458
|
+
zone ? { prefer: `outlook.timezone="${zone}"` } : {}
|
|
50459
|
+
);
|
|
50460
|
+
const organizer = event?.IsOrganizer === true || event?.ResponseStatus?.Response === "Organizer";
|
|
50461
|
+
const cancelled = event?.IsCancelled === true;
|
|
50462
|
+
if (!organizer && !cancelled) {
|
|
50463
|
+
throw new McpToolError("Only the organizer can delete this meeting.", {
|
|
50464
|
+
hint: 'You are an attendee: deleting your copy would not tell the organizer. To drop it, decline with outlook_respond_to_invite (response: "decline"), which also removes it from your calendar.'
|
|
50465
|
+
});
|
|
50466
|
+
}
|
|
50467
|
+
const scope = event?.Type === "SeriesMaster" ? "the whole series (every occurrence)" : event?.Type === "Occurrence" || event?.Type === "Exception" ? "only this occurrence (the rest of the series stays)" : "this event";
|
|
50468
|
+
const attendeeCount = event?.Attendees?.length ?? 0;
|
|
50469
|
+
const notifies = organizer && !cancelled && attendeeCount > 0;
|
|
50470
|
+
if (notifies) {
|
|
50471
|
+
const when = `${trimTime2(event?.Start?.DateTime) ?? "?"} to ${trimTime2(event?.End?.DateTime) ?? "?"}${event?.Start?.TimeZone ? ` (${event.Start.TimeZone})` : ""}`;
|
|
50472
|
+
const gate = await confirmWrite(ctx, {
|
|
50473
|
+
tool: "outlook_delete_event",
|
|
50474
|
+
action: "calendar.delete_event",
|
|
50475
|
+
message: "Review and confirm this deletion (every attendee is sent a cancellation):",
|
|
50476
|
+
summary: `Delete "${event?.Subject ?? id}" ${when} \u2014 ${scope}; ${attendeeCount} attendee${attendeeCount === 1 ? "" : "s"} will be sent a cancellation`,
|
|
50477
|
+
// One signed-in mailbox per server process.
|
|
50478
|
+
account: void 0,
|
|
50479
|
+
target: id,
|
|
50480
|
+
request: { method: "DELETE", path },
|
|
50481
|
+
preview: { scope, attendees: attendeeCount },
|
|
50482
|
+
confirmToken
|
|
50483
|
+
});
|
|
50484
|
+
if (gate) return gate;
|
|
50485
|
+
}
|
|
50486
|
+
const write = await writeOrUnknown(
|
|
50487
|
+
() => client2.write("DELETE", path),
|
|
50488
|
+
"the calendar (outlook_get_event)"
|
|
50489
|
+
);
|
|
50490
|
+
if (!write.ok) return write.result;
|
|
50491
|
+
const base = { Id: id, Subject: event?.Subject, scope, cancellationSent: notifies };
|
|
50492
|
+
let after;
|
|
50493
|
+
try {
|
|
50494
|
+
after = await client2.get(`${path}?$select=Id`);
|
|
50495
|
+
} catch (e) {
|
|
50496
|
+
if (errorStatusOf(e) === 404) return minifiedResult({ deleted: true, ...base });
|
|
50497
|
+
return minifiedResult({
|
|
50498
|
+
deleted: null,
|
|
50499
|
+
...base,
|
|
50500
|
+
warning: `Outlook accepted the delete but re-reading the event failed (${e instanceof Error ? e.message : String(e)}), so it is unverified.`
|
|
50501
|
+
});
|
|
50502
|
+
}
|
|
50503
|
+
return minifiedResult({
|
|
50504
|
+
deleted: false,
|
|
50505
|
+
...base,
|
|
50506
|
+
warning: `Outlook accepted the delete but the event is still on the calendar${after?.Id ? ` (${after.Id})` : ""}.`
|
|
50507
|
+
});
|
|
50508
|
+
}
|
|
50509
|
+
);
|
|
50510
|
+
server.registerTool(
|
|
50511
|
+
"outlook_reply",
|
|
50512
|
+
{
|
|
50513
|
+
description: "Reply, reply-all or forward a message, with `comment` as your new text above the quoted original. Outlook picks the recipients for reply (the sender, or its Reply-To) and reply-all (sender plus the original To and Cc, minus you); a forward needs `to`. `draftOnly: true` leaves the reply in Drafts and returns its id, sending nothing and asking no confirmation; otherwise it is sent at once. " + CONFIRM_FLOW_SENTENCE + " The preview shows the recipients, subject and comment. " + TIMEOUT_SENTENCE + " " + CONFIRM_INJECTION_RULE + " " + UNTRUSTED_DESCRIPTION_SUFFIX,
|
|
50514
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
50515
|
+
inputSchema: external_exports.object({
|
|
50516
|
+
messageId: external_exports.string().min(1).describe("Id of the message to answer or forward"),
|
|
50517
|
+
mode: external_exports.enum(["reply", "replyAll", "forward"]).describe("reply (sender only), replyAll, or forward"),
|
|
50518
|
+
comment: external_exports.string().describe("Your text, placed above the quoted original"),
|
|
50519
|
+
to: recipientList.describe("Forward recipients' email addresses (forward only, required there)"),
|
|
50520
|
+
draftOnly: external_exports.boolean().optional().describe("Save to Drafts instead of sending (default false)"),
|
|
50521
|
+
confirmToken: confirmTokenParam
|
|
50522
|
+
})
|
|
50523
|
+
},
|
|
50524
|
+
async ({ messageId, mode, comment, to, draftOnly, confirmToken }, ctx) => {
|
|
50525
|
+
if (mode === "forward" && !to?.length) {
|
|
50526
|
+
throw new McpToolError("A forward needs at least one recipient.", {
|
|
50527
|
+
hint: "Pass `to` with the addresses to forward to."
|
|
50528
|
+
});
|
|
50529
|
+
}
|
|
50530
|
+
if (mode !== "forward" && to?.length) {
|
|
50531
|
+
throw new McpToolError("`to` applies only to a forward.", {
|
|
50532
|
+
hint: 'A reply goes where Outlook computes; use mode "forward" to send it somewhere else, or outlook_send_mail.'
|
|
50533
|
+
});
|
|
50534
|
+
}
|
|
50535
|
+
const { send, draft, prefix } = REPLY_MODES[mode];
|
|
50536
|
+
const base = `/me/messages/${encodeURIComponent(messageId)}`;
|
|
50537
|
+
const forwardTo = mode === "forward" ? { ToRecipients: toRecipients(to) } : {};
|
|
50538
|
+
if (draftOnly === true) {
|
|
50539
|
+
const create = await writeOrUnknown(
|
|
50540
|
+
() => client2.write(
|
|
50541
|
+
"POST",
|
|
50542
|
+
`${base}/${draft}`,
|
|
50543
|
+
{}
|
|
50544
|
+
),
|
|
50545
|
+
"Drafts"
|
|
50546
|
+
);
|
|
50547
|
+
if (!create.ok) return create.result;
|
|
50548
|
+
const created = create.value;
|
|
50549
|
+
if (!created?.Id) {
|
|
50550
|
+
throw new McpToolError("Outlook did not return the draft it created.", {
|
|
50551
|
+
hint: "Check the Drafts folder before trying again."
|
|
50552
|
+
});
|
|
50553
|
+
}
|
|
50554
|
+
try {
|
|
50555
|
+
await client2.write("PATCH", `/me/messages/${encodeURIComponent(created.Id)}`, {
|
|
50556
|
+
Body: bodyWithComment(created.Body, comment),
|
|
50557
|
+
...forwardTo
|
|
50558
|
+
});
|
|
50559
|
+
} catch (e) {
|
|
50560
|
+
return minifiedResult({
|
|
50561
|
+
drafted: true,
|
|
50562
|
+
mode,
|
|
50563
|
+
draftId: created.Id,
|
|
50564
|
+
WebLink: created.WebLink,
|
|
50565
|
+
warning: `The draft was created but adding your comment${mode === "forward" ? " and recipients" : ""} failed (${e instanceof Error ? e.message : String(e)}). Edit it in Drafts, or delete it before trying again.`
|
|
50566
|
+
});
|
|
50567
|
+
}
|
|
50568
|
+
return minifiedResult({ drafted: true, mode, draftId: created.Id, WebLink: created.WebLink });
|
|
50569
|
+
}
|
|
50570
|
+
const orig = await client2.get(base, {
|
|
50571
|
+
query: { $select: "Subject,From,ReplyTo,ToRecipients,CcRecipients" }
|
|
50572
|
+
});
|
|
50573
|
+
const sender = orig?.ReplyTo?.length ? orig.ReplyTo : orig?.From ? [orig.From] : [];
|
|
50574
|
+
const recipients = mode === "forward" ? { to: to ?? [], cc: [] } : mode === "replyAll" ? { to: addresses(sender, orig?.ToRecipients), cc: addresses(orig?.CcRecipients) } : { to: addresses(sender), cc: [] };
|
|
50575
|
+
const subject = `${prefix}${orig?.Subject ?? ""}`;
|
|
50576
|
+
const path = `${base}/${send}`;
|
|
50577
|
+
const payload = { Comment: comment, ...forwardTo };
|
|
50578
|
+
const everyone = [...recipients.to, ...recipients.cc.map((c) => `cc ${c}`)];
|
|
50579
|
+
const who = everyone.length > MAX_RECIPIENTS ? `${everyone.slice(0, MAX_RECIPIENTS).join(", ")} and ${(everyone.length - MAX_RECIPIENTS).toLocaleString("en-US")} more` : everyone.join(", ");
|
|
50580
|
+
const gate = await confirmWrite(ctx, {
|
|
50581
|
+
tool: "outlook_reply",
|
|
50582
|
+
action: `mail.${send}`,
|
|
50583
|
+
message: "Review and confirm this email before it is sent:",
|
|
50584
|
+
summary: `${mode === "forward" ? "Forward" : mode === "replyAll" ? "Reply all" : "Reply"} "${subject}" to ${who || "(no recipients)"}${mode === "replyAll" ? " (Outlook leaves out your own address)" : ""}, with comment: "${comment}"`,
|
|
50585
|
+
// One signed-in mailbox per server process.
|
|
50586
|
+
account: void 0,
|
|
50587
|
+
target: messageId,
|
|
50588
|
+
request: { method: "POST", path, body: payload },
|
|
50589
|
+
confirmToken
|
|
50590
|
+
});
|
|
50591
|
+
if (gate) return gate;
|
|
50592
|
+
const sent = await writeOrUnknown(() => client2.write("POST", path, payload), "Sent Items");
|
|
50593
|
+
if (!sent.ok) return sent.result;
|
|
50594
|
+
return mailboxUntrusted({
|
|
50595
|
+
sent: true,
|
|
50596
|
+
mode,
|
|
50597
|
+
subject,
|
|
50598
|
+
...cappedRecipients("to", recipients.to),
|
|
50599
|
+
...cappedRecipients("cc", recipients.cc)
|
|
50600
|
+
});
|
|
50601
|
+
}
|
|
50602
|
+
);
|
|
49877
50603
|
}
|
|
49878
50604
|
|
|
49879
50605
|
// src/tools/healthcheck.ts
|