proton-mail-bridge-client 2.7.3 → 2.8.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/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { ensureValidEmails, foldQuotedHistory, isTextLikeMimeType, isValidEmail,
18
18
  import { AccountManager } from "./services/account-manager.js";
19
19
  import { logger } from "./utils/logger.js";
20
20
  import { ensureDestructiveConfirmed, ensureEmailActionAllowed, ensureFlagChangeAllowed, ensureMailboxWriteAllowed, ensureOutboundRecipientsAllowed, ensureRemoteDraftSyncAllowed, ensureSendAllowed, resolveRemoteDraftSync, sanitizeRuntimeConfig, } from "./utils/runtime-policy.js";
21
+ import { InviteError, buildInviteReply, findMyAttendee, parseInvite } from "./utils/ical-reply.js";
21
22
  const require = createRequire(import.meta.url);
22
23
  const packageJson = require("../package.json");
23
24
  const PACKAGE_VERSION = packageJson.version ?? process.env.npm_package_version ?? "0.0.0";
@@ -33,6 +34,11 @@ const ALL_EMAIL_ACTIONS = [
33
34
  "move",
34
35
  "delete",
35
36
  ];
37
+ // Triage and statistics tools cover every configured account unless told which one.
38
+ const ACCOUNT_FILTER_PROPERTY = {
39
+ type: "string",
40
+ description: "Only this account (its address or slug). Without it the tool covers every configured account.",
41
+ };
36
42
  const TOOLS = [
37
43
  {
38
44
  name: "send_email",
@@ -100,6 +106,7 @@ const TOOLS = [
100
106
  inputSchema: {
101
107
  type: "object",
102
108
  properties: {
109
+ account: ACCOUNT_FILTER_PROPERTY,
103
110
  status: { type: "string", enum: ["pending", "sending", "sent", "canceled", "failed"], description: "Filter to one status. Omit to list everything." },
104
111
  limit: { type: "number", description: "Records per page, newest first. Default 50.", default: 50 },
105
112
  offset: { type: "number", description: "Skip this many records (paging). Check hasMore for more." },
@@ -146,6 +153,61 @@ const TOOLS = [
146
153
  required: ["to"],
147
154
  },
148
155
  },
156
+ {
157
+ name: "set_reply_reminder",
158
+ description: "Remind yourself to follow up if nobody answers a message by a date. Usually used on a message you sent: it is kept as a local note, and get_inbox_digest and list_reply_reminders show it as due once the date has passed and nobody but you has written in the thread. A reply cancels the wait by itself. Setting a reminder again for the same message moves it to the new date. Changes nothing in the mailbox. Use get_follow_up_candidates instead to find sent threads that have been unanswered for a while without setting anything.",
159
+ annotations: { destructiveHint: false },
160
+ inputSchema: {
161
+ type: "object",
162
+ properties: {
163
+ emailId: { type: "string", description: "The message to be answered (typically one from Sent)." },
164
+ afterDays: { type: "number", description: "Remind after this many days (1-365). Default 3. Give this or remindAt." },
165
+ remindAt: { type: "string", description: "Remind at this date and time (ISO 8601, in the future). Give this or afterDays." },
166
+ note: { type: "string", description: "Optional note shown with the reminder (up to 500 characters)." },
167
+ },
168
+ required: ["emailId"],
169
+ },
170
+ },
171
+ {
172
+ name: "list_reply_reminders",
173
+ description: "List your reply reminders with their state: waiting (the date has not come), due (the date has passed and nobody has answered) or answered (someone other than you wrote in the thread after the reminder was set). By default only waiting and due ones are listed, due first. The state is worked out from the local index when you ask; run sync_emails first if the index may be stale.",
174
+ annotations: { readOnlyHint: true },
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ account: ACCOUNT_FILTER_PROPERTY,
179
+ status: { type: "string", enum: ["open", "due", "answered", "all"], description: "open = waiting and due (default).", default: "open" },
180
+ limit: { type: "number", description: "Maximum reminders to return.", default: 50 },
181
+ },
182
+ },
183
+ },
184
+ {
185
+ name: "cancel_reply_reminder",
186
+ description: "Delete a reply reminder by the id set_reply_reminder or list_reply_reminders gave.",
187
+ annotations: { destructiveHint: false },
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: { id: { type: "string", description: "Reminder id." } },
191
+ required: ["id"],
192
+ },
193
+ },
194
+ {
195
+ name: "respond_to_invite",
196
+ description: "Answer a calendar invitation in a message: accept, decline or tentatively accept. Reads the invitation (the text/calendar attachment of emailId), then sends the organizer a standard calendar reply (iTIP METHOD:REPLY) from the address the invitation was sent to, so their calendar updates your status. Refuses a cancelled event, a message that is not an invitation, and an invitation without an organizer. Use dryRun to see who would be answered and what the reply says before sending. Subject to the same send settings as reply_to_email (PROTONMAIL_ALLOW_SEND, confirmation, RESTRICT_OUTBOUND_TO_SELF, send delay). Use get_email_by_id or list_attachments first if unsure whether a message holds an invitation.",
197
+ annotations: { destructiveHint: true },
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ emailId: { type: "string", description: "The message that holds the invitation." },
202
+ response: { type: "string", enum: ["accept", "decline", "tentative"], description: "Your answer." },
203
+ comment: { type: "string", description: "Optional note to the organizer (up to 1000 characters)." },
204
+ dryRun: { type: "boolean", description: "Show who would be answered and what the reply says, without sending.", default: false },
205
+ undoWindowSeconds: { type: "number", description: "Override PROTONMAIL_SEND_DELAY_SECONDS for this one send: queue it for this many seconds (cancelable via cancel_send). 0 sends immediately." },
206
+ confirmed: { type: "boolean", description: "Set to true to confirm this irreversible send when PROTONMAIL_CONFIRM_DESTRUCTIVE is enabled." },
207
+ },
208
+ required: ["emailId", "response"],
209
+ },
210
+ },
149
211
  {
150
212
  name: "reply_to_email",
151
213
  description: "Immediately send a reply to an existing email, threading it correctly via In-Reply-To and References headers. Use when you have an emailId and want to send the reply right away. Prefer create_reply_draft to save the reply for review first, or create_thread_reply_draft when replying from a threadId. Use reply_all_email to reply to all original recipients. Provide body (plain text, or HTML with isHtml) or markdownBody. Requires PROTONMAIL_ALLOW_SEND.",
@@ -396,6 +458,7 @@ const TOOLS = [
396
458
  inputSchema: {
397
459
  type: "object",
398
460
  properties: {
461
+ account: ACCOUNT_FILTER_PROPERTY,
399
462
  includeSent: { type: "boolean", description: "Include drafts already sent.", default: false },
400
463
  limit: { type: "number", description: "Maximum drafts to return. Defaults to 20; check hasMore and page with offset for more." },
401
464
  offset: { type: "number", description: "Number of drafts to skip, newest-updated first.", default: 0 },
@@ -409,6 +472,7 @@ const TOOLS = [
409
472
  inputSchema: {
410
473
  type: "object",
411
474
  properties: {
475
+ account: ACCOUNT_FILTER_PROPERTY,
412
476
  limit: { type: "number", description: "Maximum drafts to return.", default: 50 },
413
477
  offset: { type: "number", description: "Pagination offset.", default: 0 },
414
478
  },
@@ -542,6 +606,7 @@ const TOOLS = [
542
606
  inputSchema: {
543
607
  type: "object",
544
608
  properties: {
609
+ account: ACCOUNT_FILTER_PROPERTY,
545
610
  folder: { type: "string", description: "Folder name.", default: "INBOX" },
546
611
  limit: { type: "number", description: "Number of emails to return (1-250; larger values are capped at 250, use hasMore and offset/beforeUid to page).", default: 50, maximum: 250 },
547
612
  offset: { type: "number", description: "Pagination offset from newest first.", default: 0 },
@@ -623,7 +688,7 @@ const TOOLS = [
623
688
  name: "get_folders",
624
689
  description: "Return all mailbox folders with message counts and unseen counts from the live IMAP session. Use to discover available folder names before targeting get_emails, move_email, or create_folder. Prefer sync_folders to force a fresh fetch when the folder list appears stale. Folders with noselect:true cannot be used for IMAP operations.",
625
690
  annotations: { readOnlyHint: true },
626
- inputSchema: { type: "object", properties: {} },
691
+ inputSchema: { type: "object", properties: { account: ACCOUNT_FILTER_PROPERTY } },
627
692
  },
628
693
  {
629
694
  name: "sync_folders",
@@ -785,6 +850,7 @@ const TOOLS = [
785
850
  inputSchema: {
786
851
  type: "object",
787
852
  properties: {
853
+ account: ACCOUNT_FILTER_PROPERTY,
788
854
  status: { type: "string", enum: ["pending", "waking", "woken", "canceled", "failed"], description: "Filter to one status. Omit to list everything (waking = being moved back right now)." },
789
855
  },
790
856
  },
@@ -915,6 +981,7 @@ const TOOLS = [
915
981
  inputSchema: {
916
982
  type: "object",
917
983
  properties: {
984
+ account: ACCOUNT_FILTER_PROPERTY,
918
985
  folder: { type: "string", description: "Folder to count in. Defaults to INBOX." },
919
986
  query: { type: "string", description: "Free-text filter." },
920
987
  from: { type: "string", description: "Sender filter." },
@@ -939,6 +1006,7 @@ const TOOLS = [
939
1006
  inputSchema: {
940
1007
  type: "object",
941
1008
  properties: {
1009
+ account: ACCOUNT_FILTER_PROPERTY,
942
1010
  folder: { type: "string", description: "Folder path. Defaults to INBOX.", default: "INBOX" },
943
1011
  },
944
1012
  },
@@ -1032,6 +1100,7 @@ const TOOLS = [
1032
1100
  inputSchema: {
1033
1101
  type: "object",
1034
1102
  properties: {
1103
+ account: ACCOUNT_FILTER_PROPERTY,
1035
1104
  folder: { type: "string", description: "Folder to analyse. Defaults to INBOX." },
1036
1105
  since: { type: "string", description: "ISO date lower bound." },
1037
1106
  before: { type: "string", description: "ISO date upper bound." },
@@ -1225,6 +1294,7 @@ const TOOLS = [
1225
1294
  inputSchema: {
1226
1295
  type: "object",
1227
1296
  properties: {
1297
+ account: ACCOUNT_FILTER_PROPERTY,
1228
1298
  days: { type: "number", description: "Trailing days window.", default: 30 },
1229
1299
  limit: { type: "number", description: "Maximum messages to sample.", default: 2000 },
1230
1300
  },
@@ -1237,6 +1307,7 @@ const TOOLS = [
1237
1307
  inputSchema: {
1238
1308
  type: "object",
1239
1309
  properties: {
1310
+ account: ACCOUNT_FILTER_PROPERTY,
1240
1311
  days: { type: "number", description: "Trailing days window.", default: 30 },
1241
1312
  limit: { type: "number", description: "Maximum messages to sample.", default: 2000 },
1242
1313
  },
@@ -1249,6 +1320,7 @@ const TOOLS = [
1249
1320
  inputSchema: {
1250
1321
  type: "object",
1251
1322
  properties: {
1323
+ account: ACCOUNT_FILTER_PROPERTY,
1252
1324
  limit: { type: "number", description: "Maximum contacts to return.", default: 100 },
1253
1325
  },
1254
1326
  },
@@ -1260,6 +1332,7 @@ const TOOLS = [
1260
1332
  inputSchema: {
1261
1333
  type: "object",
1262
1334
  properties: {
1335
+ account: ACCOUNT_FILTER_PROPERTY,
1263
1336
  days: { type: "number", description: "Number of trailing days to include.", default: 30 },
1264
1337
  },
1265
1338
  },
@@ -1356,7 +1429,7 @@ const TOOLS = [
1356
1429
  name: "get_index_status",
1357
1430
  description: "Return metadata about the local SQLite email index: row count, last sync timestamp, index schema version, and per-folder coverage. Use to verify the index is fresh and complete before querying it with search_indexed_emails or get_threads. If the index is empty or stale, call sync_emails first.",
1358
1431
  annotations: { readOnlyHint: true },
1359
- inputSchema: { type: "object", properties: {} },
1432
+ inputSchema: { type: "object", properties: { account: ACCOUNT_FILTER_PROPERTY } },
1360
1433
  },
1361
1434
  {
1362
1435
  name: "search_indexed_emails",
@@ -1393,6 +1466,7 @@ const TOOLS = [
1393
1466
  inputSchema: {
1394
1467
  type: "object",
1395
1468
  properties: {
1469
+ account: ACCOUNT_FILTER_PROPERTY,
1396
1470
  limit: { type: "number", description: "Maximum labels to return. Capped at 250; use get_folders for the complete live folder list.", default: 250 },
1397
1471
  },
1398
1472
  },
@@ -1404,6 +1478,7 @@ const TOOLS = [
1404
1478
  inputSchema: {
1405
1479
  type: "object",
1406
1480
  properties: {
1481
+ account: ACCOUNT_FILTER_PROPERTY,
1407
1482
  query: { type: "string", description: "Free-text filter across subject, participants, and labels." },
1408
1483
  label: { type: "string", description: "Require a normalized label on the thread." },
1409
1484
  folder: { type: "string", description: "Only threads with a message in this folder, by its exact path as get_folders lists it (e.g. INBOX, Archive, Trash, Folders/Receipts). With several accounts it applies to each." },
@@ -1419,6 +1494,7 @@ const TOOLS = [
1419
1494
  inputSchema: {
1420
1495
  type: "object",
1421
1496
  properties: {
1497
+ account: ACCOUNT_FILTER_PROPERTY,
1422
1498
  query: { type: "string", description: "Free-text filter across subject, latest preview, senders, and labels." },
1423
1499
  label: { type: "string", description: "Require a normalized label on the thread." },
1424
1500
  pendingOn: {
@@ -1449,6 +1525,7 @@ const TOOLS = [
1449
1525
  inputSchema: {
1450
1526
  type: "object",
1451
1527
  properties: {
1528
+ account: ACCOUNT_FILTER_PROPERTY,
1452
1529
  limit: { type: "number", description: "Maximum threads per digest section.", default: 10 },
1453
1530
  offset: { type: "number", description: "Skip this many threads in each section (use paging.<section>.nextOffset from the previous response to continue).", default: 0 },
1454
1531
  minAgeHours: {
@@ -1471,6 +1548,7 @@ const TOOLS = [
1471
1548
  inputSchema: {
1472
1549
  type: "object",
1473
1550
  properties: {
1551
+ account: ACCOUNT_FILTER_PROPERTY,
1474
1552
  limit: { type: "number", description: "Maximum candidate threads to return.", default: 25 },
1475
1553
  offset: { type: "number", description: "Skip this many candidates (use nextOffset from the previous response to page).", default: 0 },
1476
1554
  minAgeHours: { type: "number", description: "Minimum thread age in hours.", default: 24 },
@@ -1495,6 +1573,7 @@ const TOOLS = [
1495
1573
  inputSchema: {
1496
1574
  type: "object",
1497
1575
  properties: {
1576
+ account: ACCOUNT_FILTER_PROPERTY,
1498
1577
  category: {
1499
1578
  type: "string",
1500
1579
  enum: ["document", "invoice", "contract", "travel", "calendar"],
@@ -1518,6 +1597,7 @@ const TOOLS = [
1518
1597
  inputSchema: {
1519
1598
  type: "object",
1520
1599
  properties: {
1600
+ account: ACCOUNT_FILTER_PROPERTY,
1521
1601
  person: { type: "string", description: "Person name or email fragment to match." },
1522
1602
  domain: { type: "string", description: "Domain to match, such as example.com." },
1523
1603
  limit: { type: "number", description: "Maximum threads to include.", default: 10 },
@@ -3782,6 +3862,37 @@ export function createServer(config, options = {}) {
3782
3862
  }
3783
3863
  return found;
3784
3864
  }
3865
+ // The accounts a triage or statistics tool covers: the one named by `account`, else all of them.
3866
+ function selectedBundles(args) {
3867
+ const one = resolveAccountArg(args);
3868
+ return one ? [one] : accountManager.all();
3869
+ }
3870
+ // A reminder as a caller sees it: ids carry the account's prefix, like every other id.
3871
+ function presentReminder(bundle, record) {
3872
+ const slug = responseSlug(bundle);
3873
+ return { ...record, id: withAccountPrefix(slug, record.id), emailId: withAccountPrefix(slug, record.emailId) };
3874
+ }
3875
+ // Each open reminder with its state, from the local index: answered (somebody else wrote in the thread since it
3876
+ // was set), due (the date has passed) or waiting.
3877
+ async function evaluateReminders(bundles) {
3878
+ const own = allAccountAddresses();
3879
+ const now = Date.now();
3880
+ const perAccount = await Promise.all(bundles.map(async (bundle) => {
3881
+ const records = await bundle.replyReminderService.list();
3882
+ return Promise.all(records.map(async (record) => {
3883
+ const replies = await bundle.localIndexService.getRepliesSince(record.messageId, record.createdAt, own);
3884
+ const first = [...replies].sort((left, right) => (left.internalDate || left.date || "").localeCompare(right.internalDate || right.date || ""))[0];
3885
+ const state = first ? "answered" : new Date(record.remindAt).getTime() <= now ? "due" : "waiting";
3886
+ return {
3887
+ ...presentReminder(bundle, record),
3888
+ state,
3889
+ ...(state === "due" ? { daysOverdue: Math.floor((now - new Date(record.remindAt).getTime()) / 86_400_000) } : {}),
3890
+ ...(first ? { answeredAt: first.internalDate || first.date, answeredBy: first.from[0]?.address } : {}),
3891
+ };
3892
+ }));
3893
+ }));
3894
+ return perAccount.flat();
3895
+ }
3785
3896
  // Every configured account address — reply helpers treat all of them as "self".
3786
3897
  function allAccountAddresses() {
3787
3898
  return accountManager.all().map((bundle) => bundle.account.address);
@@ -4168,7 +4279,7 @@ export function createServer(config, options = {}) {
4168
4279
  // a draft — see redactQueueRecordAttachments' comment. Redacted for every
4169
4280
  // record regardless of status (including canceled ones, which otherwise
4170
4281
  // stayed just as bloated once queued).
4171
- const perAccount = await withAudit(auditService, name, args, async () => Promise.all(accountManager.all().map(async (bundle) => ({
4282
+ const perAccount = await withAudit(auditService, name, args, async () => Promise.all(selectedBundles(args).map(async (bundle) => ({
4172
4283
  bundle,
4173
4284
  items: await bundle.deliveryQueueService.list(),
4174
4285
  }))));
@@ -4451,6 +4562,151 @@ export function createServer(config, options = {}) {
4451
4562
  sentCopy: sentCopyTokenRa,
4452
4563
  }, false, [emailSource({ ...detailRa, id: prefixedIdFor(readBundleRa, detailRa.id) })]);
4453
4564
  }
4565
+ case "set_reply_reminder": {
4566
+ ensureMailboxWriteAllowed(config.runtime);
4567
+ const { bundle: reminderBundle, rest: reminderEmailId } = resolveAccountForEmailId(requireString(args, "emailId"));
4568
+ const afterDays = optionalNumber(args.afterDays, 1, 365);
4569
+ const remindAtArg = optionalString(args, "remindAt");
4570
+ if (afterDays !== undefined && remindAtArg !== undefined) {
4571
+ throw new McpError(ErrorCode.InvalidParams, "Give afterDays or remindAt, not both.");
4572
+ }
4573
+ const now = new Date();
4574
+ const remindAt = remindAtArg !== undefined ? new Date(remindAtArg) : new Date(now.getTime() + (afterDays ?? 3) * 24 * 60 * 60 * 1000);
4575
+ if (Number.isNaN(remindAt.getTime())) {
4576
+ throw new McpError(ErrorCode.InvalidParams, "remindAt must be a date and time such as 2026-10-20T09:00:00Z.");
4577
+ }
4578
+ const reminderDetail = await reminderBundle.imapService.getEmailById(reminderEmailId);
4579
+ if (!reminderDetail.messageId) {
4580
+ throw new McpError(ErrorCode.InvalidParams, "This message has no Message-ID header, so replies to it cannot be recognised.");
4581
+ }
4582
+ const record = await withAudit(reminderBundle.auditService, name, args, () => reminderBundle.replyReminderService.set({
4583
+ emailId: reminderEmailId,
4584
+ messageId: reminderDetail.messageId,
4585
+ subject: reminderDetail.subject,
4586
+ to: reminderDetail.to.map((address) => address.address ?? "").filter(Boolean),
4587
+ remindAt,
4588
+ note: optionalString(args, "note"),
4589
+ now,
4590
+ }));
4591
+ return createTextResult({ ...presentReminder(reminderBundle, record), state: "waiting" });
4592
+ }
4593
+ case "list_reply_reminders": {
4594
+ const status = optionalString(args, "status") ?? "open";
4595
+ if (!["open", "due", "answered", "all"].includes(status)) {
4596
+ throw new McpError(ErrorCode.InvalidParams, "status must be open, due, answered or all.");
4597
+ }
4598
+ const limit = normalizeLimit(args.limit, 50, 1, 500);
4599
+ const bundles = selectedBundles(args);
4600
+ await Promise.all(bundles.map((bundle) => maybeRefreshLocalIndex(bundle.imapService, bundle.localIndexService, { force: false, folder: "INBOX", limitPerFolder: 100 })));
4601
+ const all = await evaluateReminders(bundles);
4602
+ const wanted = all.filter((reminder) => (status === "all" ? true : status === "open" ? reminder.state !== "answered" : reminder.state === status));
4603
+ const rank = { due: 0, waiting: 1, answered: 2 };
4604
+ wanted.sort((left, right) => rank[left.state] - rank[right.state] || left.remindAt.localeCompare(right.remindAt));
4605
+ return createTextResult({
4606
+ total: wanted.length,
4607
+ due: all.filter((reminder) => reminder.state === "due").length,
4608
+ reminders: wanted.slice(0, limit),
4609
+ });
4610
+ }
4611
+ case "cancel_reply_reminder": {
4612
+ ensureMailboxWriteAllowed(config.runtime);
4613
+ const { bundle: cancelBundle, rest: reminderId } = resolveAccountForEmailId(requireString(args, "id"));
4614
+ const result = await withAudit(cancelBundle.auditService, name, args, () => cancelBundle.replyReminderService.cancel(reminderId));
4615
+ return createTextResult({ id: requireString(args, "id"), canceled: result.canceled });
4616
+ }
4617
+ case "respond_to_invite": {
4618
+ ensureDestructiveConfirmed(config.runtime, normalizeBoolean(args.confirmed, false), `Answer calendar invitation ${String(args.emailId ?? "?")}`);
4619
+ ensureSendAllowed(config.runtime);
4620
+ const { bundle: inviteBundle, rest: inviteEmailId } = resolveAccountForEmailId(requireString(args, "emailId"));
4621
+ const responseArg = requireString(args, "response");
4622
+ if (responseArg !== "accept" && responseArg !== "decline" && responseArg !== "tentative") {
4623
+ throw new McpError(ErrorCode.InvalidParams, "response must be accept, decline or tentative.");
4624
+ }
4625
+ const inviteComment = optionalString(args, "comment");
4626
+ if (inviteComment !== undefined && inviteComment.length > 1000) {
4627
+ throw new McpError(ErrorCode.InvalidParams, "comment must be at most 1000 characters.");
4628
+ }
4629
+ const inviteDetail = await inviteBundle.imapService.getEmailById(inviteEmailId);
4630
+ const candidates = (inviteDetail.attachments ?? []).filter((attachment) => Boolean(attachment.id) &&
4631
+ (attachment.isCalendarInvite || attachment.kind === "calendar" || /text\/calendar/i.test(attachment.contentType ?? "") || /\.ics$/i.test(attachment.filename ?? "")));
4632
+ if (candidates.length === 0) {
4633
+ throw new McpError(ErrorCode.InvalidParams, "This message has no calendar invitation attachment.");
4634
+ }
4635
+ let invite;
4636
+ let inviteProblem;
4637
+ for (const candidate of candidates) {
4638
+ const content = await inviteBundle.imapService.getAttachmentContent(inviteEmailId, candidate.id, { includeBase64: true });
4639
+ try {
4640
+ invite = parseInvite(Buffer.from(content.base64 ?? "", "base64").toString("utf8"));
4641
+ break;
4642
+ }
4643
+ catch (error) {
4644
+ if (!(error instanceof InviteError))
4645
+ throw error;
4646
+ inviteProblem ??= error.message;
4647
+ }
4648
+ }
4649
+ if (!invite) {
4650
+ throw new McpError(ErrorCode.InvalidParams, inviteProblem ?? "No invitation could be read from this message.");
4651
+ }
4652
+ // Answer as the account that holds the message. Only an attendee address that is exactly one of our
4653
+ // accounts is used; a "+tag" or alias address in a hostile invite never picks the sender.
4654
+ const myAddresses = allAccountAddresses();
4655
+ const myAttendee = findMyAttendee(invite, (address) => myAddresses.some((own) => own.toLowerCase() === address.toLowerCase()));
4656
+ const attendeeBundle = myAttendee ? accountManager.byAddress(myAttendee.address) : undefined;
4657
+ const inviteSendBundle = attendeeBundle ?? inviteBundle;
4658
+ const answeringAs = inviteSendBundle.config.smtp.username;
4659
+ const inviteReply = buildInviteReply(invite, {
4660
+ attendeeAddress: answeringAs,
4661
+ attendeeName: myAttendee?.name,
4662
+ response: responseArg,
4663
+ comment: inviteComment,
4664
+ });
4665
+ const inviteTo = [inviteReply.to];
4666
+ ensureValidEmails(inviteTo, "to");
4667
+ ensureOutboundRecipientsAllowed(config.runtime, inviteSendBundle.config.smtp.username, inviteTo);
4668
+ const senderDomains = (inviteDetail.from ?? []).map((entry) => (entry.address ?? "").split("@").pop()?.toLowerCase());
4669
+ const organizerDomain = invite.organizer.address.split("@").pop()?.toLowerCase();
4670
+ const organizerMismatch = !senderDomains.includes(organizerDomain);
4671
+ const inviteSummary = {
4672
+ event: invite.summary ?? "(no title)",
4673
+ ...(organizerMismatch
4674
+ ? { warning: `The organizer (${invite.organizer.address}) is not the sender of this message; the answer goes to the organizer, so check that you trust them.` }
4675
+ : {}),
4676
+ organizer: invite.organizer.address,
4677
+ answeringAs,
4678
+ response: responseArg,
4679
+ previousStatus: myAttendee?.partstat,
4680
+ ...(invite.hasRecurrence ? { note: "This invitation repeats; the answer applies to the whole series." } : {}),
4681
+ };
4682
+ if (normalizeBoolean(args.dryRun, false)) {
4683
+ return createTextResult({ dryRun: true, wouldSendTo: inviteTo, subject: inviteReply.subject, ...inviteSummary, replyCalendar: inviteReply.ics.split("\r\n").filter(Boolean), note: "No email was sent." });
4684
+ }
4685
+ const invitePayload = {
4686
+ to: inviteTo,
4687
+ subject: inviteReply.subject,
4688
+ body: inviteReply.body,
4689
+ from: inviteSendBundle.config.smtp.username === answeringAs ? undefined : answeringAs,
4690
+ inReplyTo: inviteDetail.messageId,
4691
+ references: replyReferences(inviteDetail),
4692
+ icalEvent: { method: "REPLY", content: inviteReply.ics },
4693
+ appendSignature: false,
4694
+ };
4695
+ const inviteUndo = resolveUndoWindowSeconds(args);
4696
+ if (inviteUndo > 0) {
4697
+ return queueUndoSend(inviteSendBundle, invitePayload, inviteUndo, name, args, { to: inviteTo, ...inviteSummary });
4698
+ }
4699
+ const inviteResult = await withAudit(auditService, name, args, async () => inviteSendBundle.smtpService.sendEmail(invitePayload));
4700
+ return createTextResult({
4701
+ ...inviteSummary,
4702
+ to: inviteTo,
4703
+ subject: inviteReply.subject,
4704
+ messageId: inviteResult.messageId,
4705
+ accepted: inviteResult.accepted,
4706
+ rejected: inviteResult.rejected,
4707
+ response_: inviteResult.response,
4708
+ }, false, [emailSource({ ...inviteDetail, id: prefixedIdFor(inviteBundle, inviteDetail.id) })]);
4709
+ }
4454
4710
  case "forward_email": {
4455
4711
  ensureDestructiveConfirmed(config.runtime, normalizeBoolean(args.confirmed, false), `Forward email ${String(args.emailId ?? "?")} to ${String(args.to ?? "?")}`);
4456
4712
  ensureSendAllowed(config.runtime);
@@ -4814,9 +5070,10 @@ export function createServer(config, options = {}) {
4814
5070
  // account's own list already arrives that way). Single-account guard
4815
5071
  // keeps byte-identical output for the common case.
4816
5072
  const includeSent = normalizeBoolean(args.includeSent, false);
5073
+ resolveAccountArg(args);
4817
5074
  const allDrafts = accountManager.all().length === 1
4818
5075
  ? (await draftStore.listDrafts(includeSent)).map((draft) => ({ bundle: primaryBundle, draft }))
4819
- : (await Promise.all(accountManager.all().map(async (bundle) => {
5076
+ : (await Promise.all(selectedBundles(args).map(async (bundle) => {
4820
5077
  const drafts = await bundle.draftStore.listDrafts(includeSent);
4821
5078
  return drafts.map((draft) => ({ bundle, draft: presentDraft(bundle, draft) }));
4822
5079
  })))
@@ -4843,7 +5100,11 @@ export function createServer(config, options = {}) {
4843
5100
  case "list_remote_drafts": {
4844
5101
  // Scope limitation (follow-up round): stays scoped to the primary account's
4845
5102
  // remote Drafts mailbox only — see list_drafts' identical note.
4846
- const result = await imapService.listRemoteDrafts(normalizeLimit(args.limit, 50), normalizeLimit(args.offset, 0, 0, 10_000));
5103
+ // `account` picks another account's Drafts mailbox; its ids carry that account's prefix like everywhere else.
5104
+ const draftsBundle = resolveAccountArg(args) ?? primaryBundle;
5105
+ const draftsSlug = responseSlug(draftsBundle);
5106
+ const rawResult = await draftsBundle.imapService.listRemoteDrafts(normalizeLimit(args.limit, 50), normalizeLimit(args.offset, 0, 0, 10_000));
5107
+ const result = { ...rawResult, emails: rawResult.emails.map((email) => ({ ...email, id: withAccountPrefix(draftsSlug, email.id) })) };
4847
5108
  // projectFields (called with no fields arg) still trims each email's
4848
5109
  // attachments to the essentials — same fix as every other list-style
4849
5110
  // tool, this one just didn't route through projectFields at all before.
@@ -5321,6 +5582,7 @@ export function createServer(config, options = {}) {
5321
5582
  includeSnippet: normalizeBoolean(args.includeSnippet, false),
5322
5583
  };
5323
5584
  if (accountManager.all().length === 1) {
5585
+ resolveAccountArg(args);
5324
5586
  const result = await imapService.getEmails(getEmailsInput);
5325
5587
  return createTextResult({
5326
5588
  ...result,
@@ -5362,7 +5624,7 @@ export function createServer(config, options = {}) {
5362
5624
  // than inferring it from a count that can be either capped-by-fetch or
5363
5625
  // filter-blind depending on which one you pick.
5364
5626
  const perAccountFetchLimit = requestedOffset + effectiveLimit + 1;
5365
- const perAccount = await Promise.all(accountManager.all().map(async (bundle) => {
5627
+ const perAccount = await Promise.all(selectedBundles(args).map(async (bundle) => {
5366
5628
  const result = await bundle.imapService.getEmails({ ...getEmailsInput, offset: 0, limit: perAccountFetchLimit });
5367
5629
  return { bundle, result };
5368
5630
  }));
@@ -5582,6 +5844,7 @@ export function createServer(config, options = {}) {
5582
5844
  sizeSmaller: optionalNumber(args.sizeSmaller, 0, Number.MAX_SAFE_INTEGER),
5583
5845
  };
5584
5846
  if (accountManager.all().length === 1) {
5847
+ resolveAccountArg(args);
5585
5848
  const result = await imapService.countMessages(countInput);
5586
5849
  return createTextResult(result);
5587
5850
  }
@@ -5590,7 +5853,7 @@ export function createServer(config, options = {}) {
5590
5853
  // folder name), with a per-account breakdown alongside the total.
5591
5854
  // One account lacking the folder or label (or unreachable) must not fail the whole count, as in
5592
5855
  // search_emails: report it and return the rest. Every account failing still throws.
5593
- const settled = await Promise.allSettled(accountManager.all().map(async (bundle) => ({
5856
+ const settled = await Promise.allSettled(selectedBundles(args).map(async (bundle) => ({
5594
5857
  slug: bundle.account.slug,
5595
5858
  ...(await bundle.imapService.countMessages(countInput)),
5596
5859
  })));
@@ -5599,7 +5862,7 @@ export function createServer(config, options = {}) {
5599
5862
  throw settled[0].reason;
5600
5863
  }
5601
5864
  const failedAccounts = settled.flatMap((entry, index) => entry.status === "rejected"
5602
- ? [{ account: accountManager.all()[index].account.slug, error: entry.reason instanceof Error ? entry.reason.message : String(entry.reason) }]
5865
+ ? [{ account: selectedBundles(args)[index].account.slug, error: entry.reason instanceof Error ? entry.reason.message : String(entry.reason) }]
5603
5866
  : []);
5604
5867
  return createTextResult({
5605
5868
  folder: countInput.folder ?? "INBOX",
@@ -5612,13 +5875,14 @@ export function createServer(config, options = {}) {
5612
5875
  case "folder_stats": {
5613
5876
  const folderStatsFolder = optionalString(args, "folder");
5614
5877
  if (accountManager.all().length === 1) {
5878
+ resolveAccountArg(args);
5615
5879
  const result = await imapService.getFolderStats(folderStatsFolder);
5616
5880
  return createTextResult(result);
5617
5881
  }
5618
5882
  // Merge strategy: fetch stats for the same folder in every account and
5619
5883
  // sum the numeric fields (total/unseen); uidNext/uidValidity are
5620
5884
  // per-account concepts and only make sense in the breakdown.
5621
- const perAccount = await Promise.all(accountManager.all().map(async (bundle) => ({
5885
+ const perAccount = await Promise.all(selectedBundles(args).map(async (bundle) => ({
5622
5886
  slug: bundle.account.slug,
5623
5887
  ...(await bundle.imapService.getFolderStats(folderStatsFolder)),
5624
5888
  })));
@@ -5938,6 +6202,7 @@ export function createServer(config, options = {}) {
5938
6202
  excludeSelf: normalizeBoolean(args.excludeSelf, true),
5939
6203
  };
5940
6204
  if (accountManager.all().length === 1) {
6205
+ resolveAccountArg(args);
5941
6206
  const result = await imapService.topSenders(topSendersInput);
5942
6207
  return createTextResult(result);
5943
6208
  }
@@ -5946,7 +6211,7 @@ export function createServer(config, options = {}) {
5946
6211
  // re-sort and re-apply the requested top-N limit over the merged set.
5947
6212
  // Each account is asked for its WHOLE table, not just its own top N: a sender in the middle of every
5948
6213
  // account's table can still be first in total, and cutting each table first made it disappear.
5949
- const perAccount = await Promise.all(accountManager.all().map(async (bundle) => ({
6214
+ const perAccount = await Promise.all(selectedBundles(args).map(async (bundle) => ({
5950
6215
  bundle,
5951
6216
  result: await bundle.imapService.topSenders({ ...topSendersInput, limit: 10_000 }),
5952
6217
  })));
@@ -6065,6 +6330,7 @@ export function createServer(config, options = {}) {
6065
6330
  }
6066
6331
  case "get_folders": {
6067
6332
  if (accountManager.all().length === 1) {
6333
+ resolveAccountArg(args);
6068
6334
  return createTextResult(await imapService.getFolders());
6069
6335
  }
6070
6336
  // Merge strategy: fetch each account's own folder list (LIVE IMAP) and
@@ -6072,7 +6338,7 @@ export function createServer(config, options = {}) {
6072
6338
  // which is left bare) so the merged list stays unambiguous while a
6073
6339
  // single-account setup's own folder paths are completely untouched.
6074
6340
  const primarySlug = accountManager.primary().account.slug;
6075
- const perAccount = await Promise.all(accountManager.all().map(async (bundle) => ({ bundle, folders: await bundle.imapService.getFolders() })));
6341
+ const perAccount = await Promise.all(selectedBundles(args).map(async (bundle) => ({ bundle, folders: await bundle.imapService.getFolders() })));
6076
6342
  const merged = perAccount.flatMap(({ bundle, folders }) => folders.map((folder) => {
6077
6343
  const slug = accountSlugForTag(bundle, primarySlug);
6078
6344
  return slug ? { ...folder, path: `${slug}::${folder.path}`, account: slug } : folder;
@@ -6248,11 +6514,12 @@ export function createServer(config, options = {}) {
6248
6514
  // independent snoozeService/JSON file. Single-account guard keeps
6249
6515
  // byte-identical output for the common case.
6250
6516
  if (accountManager.all().length === 1) {
6517
+ resolveAccountArg(args);
6251
6518
  const all = await withAudit(auditService, name, args, async () => snoozeService.list());
6252
6519
  const filtered = statusFilter ? all.filter((item) => item.status === statusFilter) : all;
6253
6520
  return createTextResult(filtered);
6254
6521
  }
6255
- const perAccount = await withAudit(auditService, name, args, async () => Promise.all(accountManager.all().map(async (bundle) => ({
6522
+ const perAccount = await withAudit(auditService, name, args, async () => Promise.all(selectedBundles(args).map(async (bundle) => ({
6256
6523
  bundle,
6257
6524
  items: await bundle.snoozeService.list(),
6258
6525
  }))));
@@ -6377,6 +6644,7 @@ export function createServer(config, options = {}) {
6377
6644
  const statsDays = normalizeLimit(args.days, 30, 1, 365);
6378
6645
  const statsLimit = normalizeLimit(args.limit, 2000, 1, 10_000);
6379
6646
  if (accountManager.all().length === 1) {
6647
+ resolveAccountArg(args);
6380
6648
  const folders = await imapService.getFolders();
6381
6649
  const sample = await getAnalyticsSampleFromIndex(imapService, localIndexService, statsDays, statsLimit);
6382
6650
  return createTextResult(analyticsService.getEmailStats(sample, folders, config.smtp.username));
@@ -6386,7 +6654,7 @@ export function createServer(config, options = {}) {
6386
6654
  // numeric mailbox/sample fields and merge folder lists (non-primary
6387
6655
  // folder paths tagged with their account slug to avoid collisions).
6388
6656
  const primarySlugForStats = accountManager.primary().account.slug;
6389
- const statsPerAccount = await Promise.all(accountManager.all().map(async (bundle) => {
6657
+ const statsPerAccount = await Promise.all(selectedBundles(args).map(async (bundle) => {
6390
6658
  const folders = await bundle.imapService.getFolders();
6391
6659
  const sample = await getAnalyticsSampleFromIndex(bundle.imapService, bundle.localIndexService, statsDays, statsLimit);
6392
6660
  return {
@@ -6418,6 +6686,7 @@ export function createServer(config, options = {}) {
6418
6686
  const analyticsDays = normalizeLimit(args.days, 30, 1, 365);
6419
6687
  const analyticsLimit = normalizeLimit(args.limit, 2000, 1, 10_000);
6420
6688
  if (accountManager.all().length === 1) {
6689
+ resolveAccountArg(args);
6421
6690
  const sample = await getAnalyticsSampleFromIndex(imapService, localIndexService, analyticsDays, analyticsLimit);
6422
6691
  return createTextResult(analyticsService.getEmailAnalytics(sample, config.smtp.username, analyticsDays));
6423
6692
  }
@@ -6426,7 +6695,7 @@ export function createServer(config, options = {}) {
6426
6695
  // top-N breakdowns (hours/senders/domains) by summing counts per key
6427
6696
  // and re-sorting, and recompute busiestDay/insights from the merged
6428
6697
  // results.
6429
- const analyticsPerAccount = await Promise.all(accountManager.all().map(async (bundle) => {
6698
+ const analyticsPerAccount = await Promise.all(selectedBundles(args).map(async (bundle) => {
6430
6699
  const sample = await getAnalyticsSampleFromIndex(bundle.imapService, bundle.localIndexService, analyticsDays, analyticsLimit);
6431
6700
  return analyticsService.getEmailAnalytics(sample, bundle.config.smtp.username, analyticsDays);
6432
6701
  }));
@@ -6465,6 +6734,7 @@ export function createServer(config, options = {}) {
6465
6734
  // meant limit: 5 silently ranked contacts from only 5 messages total.
6466
6735
  const limit = normalizeLimit(args.limit, 100);
6467
6736
  if (accountManager.all().length === 1) {
6737
+ resolveAccountArg(args);
6468
6738
  const sample = await getAnalyticsSampleFromIndex(imapService, localIndexService, 30, 3000);
6469
6739
  return createTextResult(analyticsService.getContacts(sample, limit, config.smtp.username));
6470
6740
  }
@@ -6473,7 +6743,7 @@ export function createServer(config, options = {}) {
6473
6743
  // address across accounts by summing incoming/outgoing/totalMessages
6474
6744
  // and keeping the most recent lastContactAt, and re-apply the
6475
6745
  // requested limit over the merged, re-sorted set.
6476
- const contactsPerAccount = await Promise.all(accountManager.all().map(async (bundle) => {
6746
+ const contactsPerAccount = await Promise.all(selectedBundles(args).map(async (bundle) => {
6477
6747
  const sample = await getAnalyticsSampleFromIndex(bundle.imapService, bundle.localIndexService, 30, 3000);
6478
6748
  return analyticsService.getContacts(sample, Number.MAX_SAFE_INTEGER, bundle.config.smtp.username);
6479
6749
  }));
@@ -6483,13 +6753,14 @@ export function createServer(config, options = {}) {
6483
6753
  const days = normalizeLimit(args.days, 30, 1, 365);
6484
6754
  // Counted in SQL over the whole index (a sample of the newest 3000 messages cut the older days short).
6485
6755
  if (accountManager.all().length === 1) {
6756
+ resolveAccountArg(args);
6486
6757
  await maybeRefreshLocalIndex(imapService, localIndexService, {});
6487
6758
  return createTextResult(await localIndexService.getDailyVolume(days));
6488
6759
  }
6489
6760
  // Merge strategy: compute each account's own daily trend points
6490
6761
  // independently, then sum count/unreadCount/starredCount/attachmentCount
6491
6762
  // for matching dates across accounts.
6492
- const trendsPerAccount = await Promise.all(accountManager.all().map(async (bundle) => {
6763
+ const trendsPerAccount = await Promise.all(selectedBundles(args).map(async (bundle) => {
6493
6764
  await maybeRefreshLocalIndex(bundle.imapService, bundle.localIndexService, {});
6494
6765
  return bundle.localIndexService.getDailyVolume(days);
6495
6766
  }));
@@ -6890,8 +7161,9 @@ export function createServer(config, options = {}) {
6890
7161
  }
6891
7162
  case "get_index_status":
6892
7163
  {
6893
- const indexStatus = await localIndexService.getStatus();
6894
- const syncCfg = backgroundSyncService.getStatus();
7164
+ const statusBundle = resolveAccountArg(args) ?? primaryBundle;
7165
+ const indexStatus = await statusBundle.localIndexService.getStatus();
7166
+ const syncCfg = statusBundle.backgroundSyncService.getStatus();
6895
7167
  return createTextResult({
6896
7168
  ...indexStatus,
6897
7169
  // What keeps this index current, so a stale/missing message can be explained:
@@ -6962,6 +7234,7 @@ export function createServer(config, options = {}) {
6962
7234
  {
6963
7235
  const labelsLimit = normalizeLimit(args.limit, 250);
6964
7236
  if (accountManager.all().length === 1) {
7237
+ resolveAccountArg(args);
6965
7238
  const labels = await localIndexService.getLabels(labelsLimit);
6966
7239
  return createTextResult({
6967
7240
  total: labels.length,
@@ -6972,7 +7245,7 @@ export function createServer(config, options = {}) {
6972
7245
  // messageCount/unreadCount/threadCount for labels that share the same
6973
7246
  // name+type across accounts, so e.g. "INBOX" folders from two accounts
6974
7247
  // combine into one row rather than appearing as unrelated duplicates.
6975
- const perAccountLabels = await Promise.all(accountManager.all().map((bundle) => bundle.localIndexService.getLabels(labelsLimit)));
7248
+ const perAccountLabels = await Promise.all(selectedBundles(args).map((bundle) => bundle.localIndexService.getLabels(labelsLimit)));
6976
7249
  const mergedLabels = mergeMailboxLabels(perAccountLabels).slice(0, labelsLimit);
6977
7250
  return createTextResult({
6978
7251
  total: mergedLabels.length,
@@ -6989,6 +7262,7 @@ export function createServer(config, options = {}) {
6989
7262
  offset: optionalInteger(args.offset, 0, 1_000_000),
6990
7263
  };
6991
7264
  if (accountManager.all().length === 1) {
7265
+ resolveAccountArg(args);
6992
7266
  await maybeRefreshLocalIndex(imapService, localIndexService, {
6993
7267
  folder: "INBOX",
6994
7268
  limitPerFolder: 100,
@@ -7002,7 +7276,7 @@ export function createServer(config, options = {}) {
7002
7276
  // re-sort newest-first by latestDate, then re-apply the requested
7003
7277
  // limit across the merged set and sum totals/hasMore.
7004
7278
  const primarySlugForThreads = accountManager.primary().account.slug;
7005
- const threadsPerAccount = await Promise.all(accountManager.all().map(async (bundle) => {
7279
+ const threadsPerAccount = await Promise.all(selectedBundles(args).map(async (bundle) => {
7006
7280
  await maybeRefreshLocalIndex(bundle.imapService, bundle.localIndexService, {
7007
7281
  folder: "INBOX",
7008
7282
  limitPerFolder: 100,
@@ -7046,7 +7320,7 @@ export function createServer(config, options = {}) {
7046
7320
  // makes this correct: any thread in the true global top-`limit` must already
7047
7321
  // rank within its own account's top-`limit`, since a lower rank there would mean
7048
7322
  // at least `limit` better-or-equal threads exist ahead of it account-wide alone.
7049
- const perAccount = await Promise.all(accountManager.all().map(async (bundle) => {
7323
+ const perAccount = await Promise.all(selectedBundles(args).map(async (bundle) => {
7050
7324
  const refresh = await maybeRefreshLocalIndex(bundle.imapService, bundle.localIndexService, {
7051
7325
  force: normalizeBoolean(args.syncBefore, false),
7052
7326
  folder: "INBOX",
@@ -7102,7 +7376,7 @@ export function createServer(config, options = {}) {
7102
7376
  // top `limit` of the MERGED list (never per-account — see get_actionable_threads
7103
7377
  // for why requesting `limit`, not limit/accounts, per account is what makes this
7104
7378
  // correct). `counts` is summed across accounts field-by-field.
7105
- const perAccount = await Promise.all(accountManager.all().map(async (bundle) => {
7379
+ const perAccount = await Promise.all(selectedBundles(args).map(async (bundle) => {
7106
7380
  await maybeRefreshLocalIndex(bundle.imapService, bundle.localIndexService, {
7107
7381
  force: normalizeBoolean(args.syncBefore, false),
7108
7382
  folder: "INBOX",
@@ -7137,6 +7411,8 @@ export function createServer(config, options = {}) {
7137
7411
  for (const key of countKeys) {
7138
7412
  counts[key] = perAccount.reduce((sum, entry) => sum + (entry.counts[key] ?? 0), 0);
7139
7413
  }
7414
+ // Reply reminders whose date has passed with no answer: they belong in a triage summary.
7415
+ const dueReminders = (await evaluateReminders(selectedBundles(args))).filter((reminder) => reminder.state === "due");
7140
7416
  const result = {
7141
7417
  generatedAt: new Date().toISOString(),
7142
7418
  indexUpdatedAt: perAccount.map((entry) => entry.indexUpdatedAt).filter(Boolean).sort().reverse()[0],
@@ -7146,6 +7422,7 @@ export function createServer(config, options = {}) {
7146
7422
  : {}),
7147
7423
  topThreads,
7148
7424
  staleAwaitingYou,
7425
+ ...(dueReminders.length > 0 ? { repliesDue: dueReminders.slice(0, 10), repliesDueTotal: dueReminders.length } : {}),
7149
7426
  // Each section pages independently with the same offset/limit: pass nextOffset back as
7150
7427
  // `offset` to continue (a section with hasMore:false is exhausted).
7151
7428
  paging: {
@@ -7165,7 +7442,7 @@ export function createServer(config, options = {}) {
7165
7442
  // score), tag ids with the account's slug, concatenate, re-sort by that same
7166
7443
  // (ageHours, then score) order, and take the top `limit` of the MERGED set —
7167
7444
  // same correctness argument as get_actionable_threads' fan-out.
7168
- const perAccount = await Promise.all(accountManager.all().map(async (bundle) => {
7445
+ const perAccount = await Promise.all(selectedBundles(args).map(async (bundle) => {
7169
7446
  await maybeRefreshLocalIndex(bundle.imapService, bundle.localIndexService, {
7170
7447
  force: normalizeBoolean(args.syncBefore, false),
7171
7448
  folder: "INBOX",
@@ -7224,7 +7501,7 @@ export function createServer(config, options = {}) {
7224
7501
  args.category === "calendar"
7225
7502
  ? args.category
7226
7503
  : "document";
7227
- const perAccount = await Promise.all(accountManager.all().map(async (bundle) => {
7504
+ const perAccount = await Promise.all(selectedBundles(args).map(async (bundle) => {
7228
7505
  await maybeRefreshLocalIndex(bundle.imapService, bundle.localIndexService, {
7229
7506
  force: normalizeBoolean(args.syncBefore, false),
7230
7507
  folder: "INBOX",
@@ -7268,7 +7545,7 @@ export function createServer(config, options = {}) {
7268
7545
  // local-index-service.ts), tag ids with the account's slug, concatenate,
7269
7546
  // re-sort by that same recency order, and take the top `limit` of the
7270
7547
  // MERGED set — same correctness argument as get_actionable_threads' fan-out.
7271
- const perAccount = await Promise.all(accountManager.all().map(async (bundle) => {
7548
+ const perAccount = await Promise.all(selectedBundles(args).map(async (bundle) => {
7272
7549
  await maybeRefreshLocalIndex(bundle.imapService, bundle.localIndexService, {
7273
7550
  force: normalizeBoolean(args.syncBefore, false),
7274
7551
  folder: "INBOX",