apple-mail-mcp 2.10.15 → 2.10.16

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.
Files changed (3) hide show
  1. package/README.md +21 -0
  2. package/build/index.js +238 -142
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -838,11 +838,26 @@ it**. An id the server hasn't seen listed is accepted only when exactly one mail
838
838
  if several do, that id fails with the candidate mailboxes named instead of being applied to an
839
839
  arbitrary copy. `imap:…` ids carry their own account + mailbox + UID and are never ambiguous.
840
840
 
841
+ **Say which mailbox with `sourceMailbox` / `sourceAccount`.** The binding above is remembered
842
+ per running server, so a client that reconnects, restarts, or replays a saved list of ids has
843
+ nothing recorded and every id takes the slower whole-tree path — where, on a label store, it is
844
+ likely to be refused as ambiguous. Passing the source mailbox explicitly is the reliable way to
845
+ stay scoped, and it overrides the remembered location. These parameters name where the ids **came
846
+ from**; for `batch-move-messages` that is distinct from `mailbox`, the destination.
847
+
848
+ `sourceMailbox` is the one that scopes, and it works on its own: omitting `sourceAccount` means
849
+ that mailbox **in the default account**, exactly as an omitted `account` does elsewhere. If the
850
+ default account cannot be determined, the ids fail with an error asking for an explicit
851
+ `sourceAccount` — a scope the server can't honor is never quietly downgraded to the guess-the-copy
852
+ walk. `sourceAccount` by itself pins nothing, since the mailbox is what an id is scoped to.
853
+
841
854
  #### `batch-delete-messages`
842
855
 
843
856
  | Parameter | Type | Required | Description |
844
857
  |-----------|------|----------|-------------|
845
858
  | `ids` | string[] | Yes | Message IDs to delete (max 100) |
859
+ | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
860
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Defaults to the default account; on its own it pins nothing — pair it with `sourceMailbox`. |
846
861
 
847
862
  **⚠️ Safety:** Destructive. Requires explicit user confirmation; search/list first to confirm the message ids.
848
863
 
@@ -853,12 +868,16 @@ arbitrary copy. `imap:…` ids carry their own account + mailbox + UID and are n
853
868
  | `ids` | string[] | Yes | Message IDs to move (max 100) |
854
869
  | `mailbox` | string | Yes | Destination mailbox |
855
870
  | `account` | string | No | Account containing mailbox |
871
+ | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
872
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Defaults to the default account; on its own it pins nothing — pair it with `sourceMailbox`. |
856
873
 
857
874
  #### `batch-mark-as-read` / `batch-mark-as-unread`
858
875
 
859
876
  | Parameter | Type | Required | Description |
860
877
  |-----------|------|----------|-------------|
861
878
  | `ids` | string[] | Yes | Message IDs (max 100) |
879
+ | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
880
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Defaults to the default account; on its own it pins nothing — pair it with `sourceMailbox`. |
862
881
 
863
882
  #### `batch-flag-messages` / `batch-unflag-messages`
864
883
 
@@ -866,6 +885,8 @@ arbitrary copy. `imap:…` ids carry their own account + mailbox + UID and are n
866
885
  |-----------|------|----------|-------------|
867
886
  | `ids` | string[] | Yes | Message IDs (max 100) |
868
887
  | `color` | string | No | (`batch-flag-messages` only) Flag color — see [`flag-message`](#flag-message--unflag-message). Applied on both routes, so a mixed batch of numeric and `imap:` ids all end up colored. |
888
+ | `sourceMailbox` | string | No | Mailbox the **numeric** ids were listed from — pins them to it. Ignored for `imap:` ids. |
889
+ | `sourceAccount` | string | No | Account the numeric ids were listed from. Defaults to the default account; on its own it pins nothing — pair it with `sourceMailbox`. |
869
890
 
870
891
  ---
871
892
 
package/build/index.js CHANGED
@@ -78581,6 +78581,18 @@ var AppleMailManager = class {
78581
78581
  locationFor(id) {
78582
78582
  return this.idLocationIndex.get(String(id));
78583
78583
  }
78584
+ /**
78585
+ * Publicly record where a message id lives.
78586
+ *
78587
+ * The index fills itself from list/search results, but that is per-process
78588
+ * state: a caller that carried ids across a process boundary (a stored triage
78589
+ * list, a scheduled job resuming) starts with an empty index, so every id is
78590
+ * "unlocated" and a label-store id gets refused as ambiguous. Registering the
78591
+ * known location restores scoped resolution.
78592
+ */
78593
+ noteMessageLocation(id, account, mailbox) {
78594
+ this.rememberLocation(id, account, mailbox);
78595
+ }
78584
78596
  /**
78585
78597
  * AppleScript fragment resolving `account` + `mailbox` into `_tmb`, leaving
78586
78598
  * `_tmb` as `missing value` when it can't be pinned down. Exact-name match
@@ -79815,10 +79827,12 @@ var AppleMailManager = class {
79815
79827
  * Mail.app numeric ids are per-mailbox, and on a label store (Gmail, iCloud)
79816
79828
  * ONE message is present in several mailboxes under the SAME id — INBOX,
79817
79829
  * "Important" and "All Mail" all report id 75816 for the same mail. This used
79818
- * to walk every account's every mailbox and mutate the FIRST hit; because
79819
- * `mailboxes of account` yields INBOX late, an id listed from INBOX was
79820
- * reliably mutated in "Important" instead, so the op reported success while
79821
- * the INBOX copy stayed put and a different copy was moved/deleted (#152).
79830
+ * to walk every account's every mailbox and mutate the FIRST hit, so whichever
79831
+ * copy `mailboxes of <account>` happened to reach first won and the mailbox the
79832
+ * id was listed from lost whenever an alias came earlier in that (store-
79833
+ * dependent) order the op reported success while the copy the caller meant
79834
+ * stayed put and a different one was moved/deleted (#152). See
79835
+ * runBatchOperation for the observed ordering on the reporting account.
79822
79836
  *
79823
79837
  * Which mailbox a mutation lands in is semantic — deleting the INBOX copy and
79824
79838
  * deleting the "All Mail" copy are different operations — so scope to the
@@ -80116,6 +80130,43 @@ var AppleMailManager = class {
80116
80130
  // ===========================================================================
80117
80131
  // Batch Operations
80118
80132
  // ===========================================================================
80133
+ /**
80134
+ * Turn a caller-supplied batch source scope into an account+mailbox pair.
80135
+ *
80136
+ * `mailbox` is what does the scoping, and it works ON ITS OWN: an omitted
80137
+ * `account` means "the default account" here exactly as it does for every
80138
+ * other tool in this server (see resolveAccount), never "ignore the argument
80139
+ * you were given". Requiring both used to make a lone `sourceMailbox` a silent
80140
+ * no-op — the ids fell back to the whole-tree path and were then refused as
80141
+ * ambiguous, which is the very failure the parameter exists to prevent.
80142
+ *
80143
+ * The safety property is absolute: when the account cannot be determined there
80144
+ * is NO fallback to the scan-and-guess walk. The caller gets an error naming
80145
+ * the mailbox it asked for, so it can retry with an explicit `sourceAccount`.
80146
+ * (An `account` with no `mailbox` cannot pin anything, so it scopes nothing —
80147
+ * those ids still go through the index / ambiguity-checked path.)
80148
+ */
80149
+ resolveBatchScope(scope) {
80150
+ const mailbox = scope?.mailbox?.trim();
80151
+ if (!mailbox) return { kind: "none" };
80152
+ const account = scope?.account?.trim();
80153
+ if (account) return { kind: "scoped", account, mailbox };
80154
+ const known = this.getCachedAccounts();
80155
+ if (known.length === 0) {
80156
+ return {
80157
+ kind: "unresolvable",
80158
+ error: `Cannot scope to source mailbox "${mailbox}": no sourceAccount was given and no Mail account could be read (Mail returned none, or the AppleScript transport failed). Retry with an explicit sourceAccount.`
80159
+ };
80160
+ }
80161
+ const chosen = this.resolveAccount();
80162
+ if (!known.some((a) => a.name === chosen)) {
80163
+ return {
80164
+ kind: "unresolvable",
80165
+ error: `Cannot scope to source mailbox "${mailbox}": no sourceAccount was given and the default account could not be determined. Retry with an explicit sourceAccount (available: ${known.map((a) => a.name).join(", ")}).`
80166
+ };
80167
+ }
80168
+ return { kind: "scoped", account: chosen, mailbox };
80169
+ }
80119
80170
  /**
80120
80171
  * Run one operation over many message IDs in a SINGLE osascript invocation.
80121
80172
  *
@@ -80132,18 +80183,23 @@ var AppleMailManager = class {
80132
80183
  * numeric id is unique only within a mailbox, and a label store (Gmail,
80133
80184
  * iCloud) exposes one message in several mailboxes under the same id — INBOX,
80134
80185
  * "Important" and "All Mail" all report id 75816 for the same mail. The old
80135
- * tree walk applied `operation` to the FIRST mailbox that matched, and
80136
- * `mailboxes of account` yields INBOX late, so a batch of ids listed from
80137
- * INBOX was reliably applied to the "Important" copies instead: every id
80138
- * reported `ok` while the INBOX messages stayed put and other copies were
80139
- * moved/deleted. Grouping by recorded source mailbox makes the op land on the
80140
- * copy the caller actually listed; ids with no recorded mailbox are refused
80141
- * when ambiguous rather than applied to an arbitrary copy.
80186
+ * tree walk applied `operation` to the FIRST mailbox that matched while
80187
+ * iterating `mailboxes of <account>`, so whichever copy that iteration reached
80188
+ * first won and the ids' real source mailbox lost whenever an alias came
80189
+ * earlier. Observed on the reporting account (`list-mailboxes`, 2026-08-13):
80190
+ * INBOX 1, "[Gmail]/All Mail" 5, "[Gmail]/Important" 9, "Sales Spam" 12 so a
80191
+ * batch listed from "Sales Spam" was applied to the All Mail copies while the
80192
+ * Sales Spam messages stayed put, and every id still reported `ok`. That order
80193
+ * is a property of the store, not a guarantee: do not rely on it in either
80194
+ * direction — any mailbox the walk reaches late loses the same way. Grouping by
80195
+ * recorded source mailbox makes the op land on the copy the caller actually
80196
+ * listed; ids with no recorded mailbox are refused when ambiguous rather than
80197
+ * applied to an arbitrary copy.
80142
80198
  *
80143
80199
  * `setup` runs once up front (used by move to resolve the destination); it may
80144
80200
  * bail the whole batch by returning a `BATCH_FATAL`-prefixed string.
80145
80201
  */
80146
- runBatchOperation(ids, operation, setup = "") {
80202
+ runBatchOperation(ids, operation, setup = "", scope) {
80147
80203
  const valid = [];
80148
80204
  for (const id of ids) {
80149
80205
  const num = Number(id);
@@ -80152,11 +80208,16 @@ var AppleMailManager = class {
80152
80208
  if (valid.length === 0) {
80153
80209
  return ids.map((id) => ({ id, success: false, error: "Invalid message ID" }));
80154
80210
  }
80211
+ const resolved = this.resolveBatchScope(scope);
80212
+ if (resolved.kind === "unresolvable") {
80213
+ return ids.map((id) => ({ id, success: false, error: resolved.error }));
80214
+ }
80215
+ const callerScope = resolved.kind === "scoped" ? resolved : void 0;
80155
80216
  const groups = /* @__PURE__ */ new Map();
80156
80217
  const unlocated = [];
80157
80218
  valid.forEach((v, i) => {
80158
80219
  const pos = i + 1;
80159
- const loc = this.locationFor(v.id);
80220
+ const loc = callerScope ?? this.locationFor(v.id);
80160
80221
  if (!loc) {
80161
80222
  unlocated.push({ num: v.num, pos });
80162
80223
  return;
@@ -80284,8 +80345,8 @@ var AppleMailManager = class {
80284
80345
  /**
80285
80346
  * Delete multiple messages at once (single tree walk — see runBatchOperation).
80286
80347
  */
80287
- batchDeleteMessages(ids) {
80288
- return this.runBatchOperation(ids, "delete _msg");
80348
+ batchDeleteMessages(ids, scope) {
80349
+ return this.runBatchOperation(ids, "delete _msg", "", scope);
80289
80350
  }
80290
80351
  /**
80291
80352
  * Move multiple messages to a mailbox at once (single tree walk).
@@ -80294,7 +80355,7 @@ var AppleMailManager = class {
80294
80355
  * matching more than one mailbox fails the whole batch rather than guessing),
80295
80356
  * then every matched message is moved in the same walk.
80296
80357
  */
80297
- batchMoveMessages(ids, mailbox, account) {
80358
+ batchMoveMessages(ids, mailbox, account, scope) {
80298
80359
  const targetAccount = this.resolveAccount(account);
80299
80360
  const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
80300
80361
  const safeMailbox = escapeForAppleScript(targetMailbox);
@@ -80308,31 +80369,31 @@ var AppleMailManager = class {
80308
80369
  if (count of destMatches) is 0 then return "${BATCH_FATAL}Destination mailbox \\"" & destName & "\\" not found in account \\"${safeAccount}\\""
80309
80370
  if (count of destMatches) > 1 then return "${BATCH_FATAL}Destination mailbox \\"" & destName & "\\" is ambiguous (" & (count of destMatches) & " matches) in account \\"${safeAccount}\\"; move by full path"
80310
80371
  set destMailbox to item 1 of destMatches`;
80311
- return this.runBatchOperation(ids, "move _msg to destMailbox", setup);
80372
+ return this.runBatchOperation(ids, "move _msg to destMailbox", setup, scope);
80312
80373
  }
80313
80374
  /**
80314
80375
  * Mark multiple messages as read at once (single tree walk).
80315
80376
  */
80316
- batchMarkAsRead(ids) {
80317
- return this.runBatchOperation(ids, "set read status of _msg to true");
80377
+ batchMarkAsRead(ids, scope) {
80378
+ return this.runBatchOperation(ids, "set read status of _msg to true", "", scope);
80318
80379
  }
80319
80380
  /**
80320
80381
  * Mark multiple messages as unread at once (single tree walk).
80321
80382
  */
80322
- batchMarkAsUnread(ids) {
80323
- return this.runBatchOperation(ids, "set read status of _msg to false");
80383
+ batchMarkAsUnread(ids, scope) {
80384
+ return this.runBatchOperation(ids, "set read status of _msg to false", "", scope);
80324
80385
  }
80325
80386
  /**
80326
80387
  * Flag multiple messages at once (single tree walk).
80327
80388
  */
80328
- batchFlagMessages(ids, colorIndex) {
80329
- return this.runBatchOperation(ids, this.flagOperation("_msg", colorIndex));
80389
+ batchFlagMessages(ids, colorIndex, scope) {
80390
+ return this.runBatchOperation(ids, this.flagOperation("_msg", colorIndex), "", scope);
80330
80391
  }
80331
80392
  /**
80332
80393
  * Unflag multiple messages at once (single tree walk).
80333
80394
  */
80334
- batchUnflagMessages(ids) {
80335
- return this.runBatchOperation(ids, "set flagged status of _msg to false");
80395
+ batchUnflagMessages(ids, scope) {
80396
+ return this.runBatchOperation(ids, "set flagged status of _msg to false", "", scope);
80336
80397
  }
80337
80398
  /**
80338
80399
  * List attachments for a message.
@@ -82997,6 +83058,57 @@ function withErrorHandling(handler, errorPrefix) {
82997
83058
  };
82998
83059
  }
82999
83060
 
83061
+ // src/tools/batchResults.ts
83062
+ async function hybridBatchCounts(ids, appleFn, imapFn) {
83063
+ const imapIds = ids.filter((i) => i.startsWith("imap:"));
83064
+ const numericIds = ids.filter((i) => !i.startsWith("imap:"));
83065
+ let success = 0;
83066
+ let fail = 0;
83067
+ const errors = [];
83068
+ if (numericIds.length > 0) {
83069
+ const res = appleFn(numericIds);
83070
+ const s = res.filter((r) => r.success).length;
83071
+ success += s;
83072
+ fail += res.length - s;
83073
+ errors.push(...res.filter((r) => !r.success && r.error).map((r) => r.error));
83074
+ }
83075
+ if (imapIds.length > 0) {
83076
+ const r = await imapFn(imapIds);
83077
+ success += r.success;
83078
+ fail += r.failed;
83079
+ errors.push(...r.errors);
83080
+ }
83081
+ return { success, fail, errors };
83082
+ }
83083
+ function distinctErrors(errors) {
83084
+ return [...new Set(errors.filter(Boolean))];
83085
+ }
83086
+ function formatBatchErrors(errors, max = 5) {
83087
+ const distinct = distinctErrors(errors);
83088
+ if (distinct.length === 0) return "";
83089
+ const shown = distinct.slice(0, max);
83090
+ const more = distinct.length - shown.length;
83091
+ return `: ${shown.join("; ")}${more > 0 ? ` (+${more} more)` : ""}`;
83092
+ }
83093
+ var MAX_STRUCTURED_BATCH_ERRORS = 20;
83094
+ function batchResponse(counts, messages, extra = {}) {
83095
+ const { success, fail, errors } = counts;
83096
+ const distinct = distinctErrors(errors);
83097
+ const reported = distinct.slice(0, MAX_STRUCTURED_BATCH_ERRORS);
83098
+ const structured = {
83099
+ ok: fail === 0,
83100
+ success,
83101
+ failed: fail,
83102
+ ...extra,
83103
+ ...reported.length > 0 ? { errors: reported } : {},
83104
+ ...distinct.length > reported.length ? { errorsTruncated: true } : {}
83105
+ };
83106
+ const suffix = formatBatchErrors(distinct);
83107
+ if (fail === 0) return successResponse(messages.allSucceeded(success), structured);
83108
+ if (success === 0) return errorResponse(`${messages.allFailed(fail)}${suffix}`, structured);
83109
+ return successResponse(`${messages.partial(success, fail)}${suffix}`, structured);
83110
+ }
83111
+
83000
83112
  // src/services/imapMultiAccount.ts
83001
83113
  function normalizeMessageId2(row) {
83002
83114
  const raw = typeof row.messageId === "string" ? row.messageId.trim() : "";
@@ -83588,6 +83700,12 @@ function withJsonSchema2020_12(transport2) {
83588
83700
  loadFileConfig();
83589
83701
  var MESSAGE_ID_SCHEMA = external_exports.string().regex(/^(\d+|imap:[A-Za-z0-9_-]+)$/, "Message ID must be numeric or an IMAP id (imap:\u2026)");
83590
83702
  var BATCH_IDS_SCHEMA = external_exports.array(MESSAGE_ID_SCHEMA).min(1, "At least one message ID is required").max(100, "Cannot process more than 100 messages in a single batch");
83703
+ var BATCH_SOURCE_MAILBOX_SCHEMA = external_exports.string().optional().describe(
83704
+ "Mailbox the numeric ids were listed from (e.g. 'INBOX'). Pins each id to that mailbox \u2014 strongly recommended, since one numeric id can match in several mailboxes. Works on its own: without sourceAccount it means that mailbox in the default account. Ignored for imap: ids."
83705
+ );
83706
+ var BATCH_SOURCE_ACCOUNT_SCHEMA = external_exports.string().optional().describe(
83707
+ "Account the numeric ids were listed from. Defaults to the default account. On its own it pins nothing \u2014 pair it with sourceMailbox."
83708
+ );
83591
83709
  var FLAG_COLOR_INDEX = {
83592
83710
  red: 0,
83593
83711
  orange: 1,
@@ -83637,7 +83755,19 @@ var BATCH_COUNT_OUTPUT_SCHEMA = {
83637
83755
  ok: external_exports.boolean().optional(),
83638
83756
  success: external_exports.number().optional(),
83639
83757
  failed: external_exports.number().optional(),
83640
- mailbox: external_exports.string().optional()
83758
+ mailbox: external_exports.string().optional(),
83759
+ // Declared so the failure channel is part of the tool's advertised CONTRACT:
83760
+ // a client can rely on `errors` being string[] and code against it, and it
83761
+ // shows up in generated types and docs. Declaring is not what makes it
83762
+ // deliverable — registerTool() wraps every outputSchema in
83763
+ // `z.object(shape).passthrough()`, so these tools advertise
83764
+ // `additionalProperties: true` (verified against the built server) and an
83765
+ // undeclared key would be carried through, not rejected. Enumerating it is a
83766
+ // promise to callers, not a workaround for a validator.
83767
+ errors: external_exports.array(external_exports.string()).optional(),
83768
+ // Set when `errors` was capped (MAX_STRUCTURED_BATCH_ERRORS distinct reasons),
83769
+ // so a short list is never mistaken for the complete one.
83770
+ errorsTruncated: external_exports.boolean().optional()
83641
83771
  };
83642
83772
  var CHECK_ITEM_SCHEMA = external_exports.object({}).passthrough();
83643
83773
  var require2 = createRequire(import.meta.url);
@@ -83718,26 +83848,6 @@ function registerTool(name, config2, cb) {
83718
83848
  }
83719
83849
  var mailManager = new AppleMailManager();
83720
83850
  registerResourcesAndPrompts(server, mailManager);
83721
- async function hybridBatchCounts(ids, appleFn, imapFn) {
83722
- const imapIds = ids.filter((i) => i.startsWith("imap:"));
83723
- const numericIds = ids.filter((i) => !i.startsWith("imap:"));
83724
- let success = 0;
83725
- let fail = 0;
83726
- const errors = [];
83727
- if (numericIds.length > 0) {
83728
- const res = appleFn(numericIds);
83729
- const s = res.filter((r) => r.success).length;
83730
- success += s;
83731
- fail += res.length - s;
83732
- }
83733
- if (imapIds.length > 0) {
83734
- const r = await imapFn(imapIds);
83735
- success += r.success;
83736
- fail += r.failed;
83737
- errors.push(...r.errors);
83738
- }
83739
- return { success, fail, errors };
83740
- }
83741
83851
  registerTool(
83742
83852
  "search-messages",
83743
83853
  {
@@ -84515,173 +84625,159 @@ registerTool(
84515
84625
  registerTool(
84516
84626
  "batch-delete-messages",
84517
84627
  {
84518
- description: "Use when: deleting multiple messages in one call (1\u2013100 ids; moves them to Trash).\nReturns: counts of how many were deleted and how many failed.\nDo not use when: deleting just one (use delete-message) or filing messages away (use batch-move-messages).\nSafety: destructive and applies to many messages at once \u2014 require explicit user confirmation, and search-messages/list-messages first to confirm every id is correct before deleting.",
84628
+ description: "Use when: deleting multiple messages in one call (1\u2013100 ids; moves them to Trash).\nReturns: counts of how many were deleted and how many failed, plus the distinct reasons for any failures.\nDo not use when: deleting just one (use delete-message) or filing messages away (use batch-move-messages).\nSafety: destructive and applies to many messages at once \u2014 require explicit user confirmation, and search-messages/list-messages first to confirm every id is correct before deleting. Pass sourceMailbox/sourceAccount (the mailbox you listed the ids from) so each numeric id is pinned to that mailbox; an id that matches in several mailboxes is refused, not guessed.",
84519
84629
  inputSchema: {
84520
- ids: BATCH_IDS_SCHEMA
84630
+ ids: BATCH_IDS_SCHEMA,
84631
+ sourceMailbox: BATCH_SOURCE_MAILBOX_SCHEMA,
84632
+ sourceAccount: BATCH_SOURCE_ACCOUNT_SCHEMA
84521
84633
  },
84522
84634
  outputSchema: BATCH_COUNT_OUTPUT_SCHEMA
84523
84635
  },
84524
- withErrorHandling(async ({ ids }) => {
84525
- const { success: successCount, fail: failCount } = await hybridBatchCounts(
84636
+ withErrorHandling(async ({ ids, sourceMailbox, sourceAccount }) => {
84637
+ const counts = await hybridBatchCounts(
84526
84638
  ids,
84527
- (n) => mailManager.batchDeleteMessages(n),
84639
+ (n) => mailManager.batchDeleteMessages(n, { account: sourceAccount, mailbox: sourceMailbox }),
84528
84640
  (im) => imapBatchDelete(im)
84529
84641
  );
84530
- const structured = { ok: failCount === 0, success: successCount, failed: failCount };
84531
- if (failCount === 0) {
84532
- return successResponse(`Successfully deleted ${successCount} message(s)`, structured);
84533
- } else if (successCount === 0) {
84534
- return errorResponse(`Failed to delete all ${failCount} message(s)`);
84535
- } else {
84536
- return successResponse(`Deleted ${successCount} message(s), ${failCount} failed`, structured);
84537
- }
84642
+ return batchResponse(counts, {
84643
+ allSucceeded: (n) => `Successfully deleted ${n} message(s)`,
84644
+ allFailed: (n) => `Failed to delete all ${n} message(s)`,
84645
+ partial: (ok, failed) => `Deleted ${ok} message(s), ${failed} failed`
84646
+ });
84538
84647
  }, "Error batch deleting messages")
84539
84648
  );
84540
84649
  registerTool(
84541
84650
  "batch-move-messages",
84542
84651
  {
84543
- description: "Use when: moving multiple messages (1\u2013100 ids) into the same destination mailbox/folder in one call, e.g. bulk archiving.\nReturns: counts of how many were moved and how many failed.\nDo not use when: moving just one (use move-message) or deleting (use batch-delete-messages). Use list-mailboxes to confirm the destination name exists.\nSafety: moves many real messages at once \u2014 confirm the destination mailbox, and search-messages/list-messages first to confirm the ids.",
84652
+ description: "Use when: moving multiple messages (1\u2013100 ids) into the same destination mailbox/folder in one call, e.g. bulk archiving.\nReturns: counts of how many were moved and how many failed, plus the distinct reasons for any failures.\nDo not use when: moving just one (use move-message) or deleting (use batch-delete-messages). Use list-mailboxes to confirm the destination name exists.\nSafety: moves many real messages at once \u2014 confirm the destination mailbox, and search-messages/list-messages first to confirm the ids. Pass sourceMailbox/sourceAccount (the mailbox you listed the ids from \u2014 not the destination) so each numeric id is pinned to that mailbox; an id that matches in several mailboxes is refused, not guessed.",
84544
84653
  inputSchema: {
84545
84654
  ids: BATCH_IDS_SCHEMA,
84546
84655
  mailbox: external_exports.string().min(1, "Destination mailbox is required"),
84547
- account: external_exports.string().optional().describe("Account containing the destination mailbox")
84656
+ account: external_exports.string().optional().describe("Account containing the destination mailbox"),
84657
+ sourceMailbox: BATCH_SOURCE_MAILBOX_SCHEMA,
84658
+ sourceAccount: BATCH_SOURCE_ACCOUNT_SCHEMA
84548
84659
  },
84549
84660
  outputSchema: BATCH_COUNT_OUTPUT_SCHEMA
84550
84661
  },
84551
- withErrorHandling(async ({ ids, mailbox, account }) => {
84552
- const { success: successCount, fail: failCount } = await hybridBatchCounts(
84662
+ withErrorHandling(async ({ ids, mailbox, account, sourceMailbox, sourceAccount }) => {
84663
+ const counts = await hybridBatchCounts(
84553
84664
  ids,
84554
- (n) => mailManager.batchMoveMessages(n, mailbox, account),
84665
+ (n) => mailManager.batchMoveMessages(n, mailbox, account, {
84666
+ account: sourceAccount,
84667
+ mailbox: sourceMailbox
84668
+ }),
84555
84669
  (im) => imapBatchMove(im, mailbox, { account })
84556
84670
  );
84557
- const structured = { ok: failCount === 0, success: successCount, failed: failCount, mailbox };
84558
- if (failCount === 0) {
84559
- return successResponse(
84560
- `Successfully moved ${successCount} message(s) to "${mailbox}"`,
84561
- structured
84562
- );
84563
- } else if (successCount === 0) {
84564
- return errorResponse(`Failed to move all ${failCount} message(s)`);
84565
- } else {
84566
- return successResponse(
84567
- `Moved ${successCount} message(s) to "${mailbox}", ${failCount} failed`,
84568
- structured
84569
- );
84570
- }
84671
+ return batchResponse(
84672
+ counts,
84673
+ {
84674
+ allSucceeded: (n) => `Successfully moved ${n} message(s) to "${mailbox}"`,
84675
+ allFailed: (n) => `Failed to move all ${n} message(s)`,
84676
+ partial: (ok, failed) => `Moved ${ok} message(s) to "${mailbox}", ${failed} failed`
84677
+ },
84678
+ { mailbox }
84679
+ );
84571
84680
  }, "Error batch moving messages")
84572
84681
  );
84573
84682
  registerTool(
84574
84683
  "batch-mark-as-read",
84575
84684
  {
84576
- description: "Use when: marking multiple messages (1\u2013100 ids) as read in one call.\nReturns: counts of how many were marked read and how many failed.\nDo not use when: marking just one (use mark-as-read) or marking unread (use batch-mark-as-unread). Get the ids from search-messages or list-messages first.",
84685
+ description: "Use when: marking multiple messages (1\u2013100 ids) as read in one call.\nReturns: counts of how many were marked read and how many failed.\nDo not use when: marking just one (use mark-as-read) or marking unread (use batch-mark-as-unread). Get the ids from search-messages or list-messages first. Pass sourceMailbox/sourceAccount (the mailbox you listed the ids from) so each numeric id is pinned to that mailbox; an id that matches in several mailboxes is refused, not guessed.",
84577
84686
  inputSchema: {
84578
- ids: BATCH_IDS_SCHEMA
84687
+ ids: BATCH_IDS_SCHEMA,
84688
+ sourceMailbox: BATCH_SOURCE_MAILBOX_SCHEMA,
84689
+ sourceAccount: BATCH_SOURCE_ACCOUNT_SCHEMA
84579
84690
  },
84580
84691
  outputSchema: BATCH_COUNT_OUTPUT_SCHEMA
84581
84692
  },
84582
- withErrorHandling(async ({ ids }) => {
84583
- const { success: successCount, fail: failCount } = await hybridBatchCounts(
84693
+ withErrorHandling(async ({ ids, sourceMailbox, sourceAccount }) => {
84694
+ const counts = await hybridBatchCounts(
84584
84695
  ids,
84585
- (n) => mailManager.batchMarkAsRead(n),
84696
+ (n) => mailManager.batchMarkAsRead(n, { account: sourceAccount, mailbox: sourceMailbox }),
84586
84697
  (im) => imapBatchMarkRead(im)
84587
84698
  );
84588
- const structured = { ok: failCount === 0, success: successCount, failed: failCount };
84589
- if (failCount === 0) {
84590
- return successResponse(`Successfully marked ${successCount} message(s) as read`, structured);
84591
- } else if (successCount === 0) {
84592
- return errorResponse(`Failed to mark all ${failCount} message(s) as read`);
84593
- } else {
84594
- return successResponse(
84595
- `Marked ${successCount} message(s) as read, ${failCount} failed`,
84596
- structured
84597
- );
84598
- }
84699
+ return batchResponse(counts, {
84700
+ allSucceeded: (n) => `Successfully marked ${n} message(s) as read`,
84701
+ allFailed: (n) => `Failed to mark all ${n} message(s) as read`,
84702
+ partial: (ok, failed) => `Marked ${ok} message(s) as read, ${failed} failed`
84703
+ });
84599
84704
  }, "Error batch marking messages as read")
84600
84705
  );
84601
84706
  registerTool(
84602
84707
  "batch-mark-as-unread",
84603
84708
  {
84604
- description: "Use when: marking multiple messages (1\u2013100 ids) as unread in one call.\nReturns: counts of how many were marked unread and how many failed.\nDo not use when: marking just one (use mark-as-unread) or marking read (use batch-mark-as-read). Get the ids from search-messages or list-messages first.",
84709
+ description: "Use when: marking multiple messages (1\u2013100 ids) as unread in one call.\nReturns: counts of how many were marked unread and how many failed.\nDo not use when: marking just one (use mark-as-unread) or marking read (use batch-mark-as-read). Get the ids from search-messages or list-messages first. Pass sourceMailbox/sourceAccount (the mailbox you listed the ids from) so each numeric id is pinned to that mailbox; an id that matches in several mailboxes is refused, not guessed.",
84605
84710
  inputSchema: {
84606
- ids: BATCH_IDS_SCHEMA
84711
+ ids: BATCH_IDS_SCHEMA,
84712
+ sourceMailbox: BATCH_SOURCE_MAILBOX_SCHEMA,
84713
+ sourceAccount: BATCH_SOURCE_ACCOUNT_SCHEMA
84607
84714
  },
84608
84715
  outputSchema: BATCH_COUNT_OUTPUT_SCHEMA
84609
84716
  },
84610
- withErrorHandling(async ({ ids }) => {
84611
- const { success: successCount, fail: failCount } = await hybridBatchCounts(
84717
+ withErrorHandling(async ({ ids, sourceMailbox, sourceAccount }) => {
84718
+ const counts = await hybridBatchCounts(
84612
84719
  ids,
84613
- (n) => mailManager.batchMarkAsUnread(n),
84720
+ (n) => mailManager.batchMarkAsUnread(n, { account: sourceAccount, mailbox: sourceMailbox }),
84614
84721
  (im) => imapBatchMarkUnread(im)
84615
84722
  );
84616
- const structured = { ok: failCount === 0, success: successCount, failed: failCount };
84617
- if (failCount === 0) {
84618
- return successResponse(
84619
- `Successfully marked ${successCount} message(s) as unread`,
84620
- structured
84621
- );
84622
- } else if (successCount === 0) {
84623
- return errorResponse(`Failed to mark all ${failCount} message(s) as unread`);
84624
- } else {
84625
- return successResponse(
84626
- `Marked ${successCount} message(s) as unread, ${failCount} failed`,
84627
- structured
84628
- );
84629
- }
84723
+ return batchResponse(counts, {
84724
+ allSucceeded: (n) => `Successfully marked ${n} message(s) as unread`,
84725
+ allFailed: (n) => `Failed to mark all ${n} message(s) as unread`,
84726
+ partial: (ok, failed) => `Marked ${ok} message(s) as unread, ${failed} failed`
84727
+ });
84630
84728
  }, "Error batch marking messages as unread")
84631
84729
  );
84632
84730
  registerTool(
84633
84731
  "batch-flag-messages",
84634
84732
  {
84635
- description: "Use when: flagging multiple messages (1\u2013100 ids) in one call, optionally with a color (red/orange/yellow/green/blue/purple/gray).\nReturns: counts of how many were flagged and how many failed.\nDo not use when: flagging just one (use flag-message) or removing flags (use batch-unflag-messages). Get the ids from search-messages or list-messages first.\nNote: the color is applied on both routes \u2014 AppleScript sets the flag index, IMAP writes the equivalent $MailFlagBit0/1/2 keywords Mail.app reads \u2014 so a mixed batch of numeric and `imap:` ids all end up colored.",
84733
+ description: "Use when: flagging multiple messages (1\u2013100 ids) in one call, optionally with a color (red/orange/yellow/green/blue/purple/gray).\nReturns: counts of how many were flagged and how many failed.\nDo not use when: flagging just one (use flag-message) or removing flags (use batch-unflag-messages). Get the ids from search-messages or list-messages first. Pass sourceMailbox/sourceAccount (the mailbox you listed the ids from) so each numeric id is pinned to that mailbox; an id that matches in several mailboxes is refused, not guessed.\nNote: the color is applied on both routes \u2014 AppleScript sets the flag index, IMAP writes the equivalent $MailFlagBit0/1/2 keywords Mail.app reads \u2014 so a mixed batch of numeric and `imap:` ids all end up colored.",
84636
84734
  inputSchema: {
84637
84735
  ids: BATCH_IDS_SCHEMA,
84638
- color: FLAG_COLOR_SCHEMA
84736
+ color: FLAG_COLOR_SCHEMA,
84737
+ sourceMailbox: BATCH_SOURCE_MAILBOX_SCHEMA,
84738
+ sourceAccount: BATCH_SOURCE_ACCOUNT_SCHEMA
84639
84739
  },
84640
84740
  outputSchema: BATCH_COUNT_OUTPUT_SCHEMA
84641
84741
  },
84642
- withErrorHandling(async ({ ids, color }) => {
84742
+ withErrorHandling(async ({ ids, color, sourceMailbox, sourceAccount }) => {
84643
84743
  const colorIndex = color ? FLAG_COLOR_INDEX[color] : void 0;
84644
- const { success: successCount, fail: failCount } = await hybridBatchCounts(
84744
+ const counts = await hybridBatchCounts(
84645
84745
  ids,
84646
- (n) => mailManager.batchFlagMessages(n, colorIndex),
84746
+ (n) => mailManager.batchFlagMessages(n, colorIndex, {
84747
+ account: sourceAccount,
84748
+ mailbox: sourceMailbox
84749
+ }),
84647
84750
  (im) => imapBatchFlag(im, colorIndex)
84648
84751
  );
84649
- const structured = { ok: failCount === 0, success: successCount, failed: failCount };
84650
- if (failCount === 0) {
84651
- return successResponse(`Successfully flagged ${successCount} message(s)`, structured);
84652
- } else if (successCount === 0) {
84653
- return errorResponse(`Failed to flag all ${failCount} message(s)`);
84654
- } else {
84655
- return successResponse(`Flagged ${successCount} message(s), ${failCount} failed`, structured);
84656
- }
84752
+ return batchResponse(counts, {
84753
+ allSucceeded: (n) => `Successfully flagged ${n} message(s)`,
84754
+ allFailed: (n) => `Failed to flag all ${n} message(s)`,
84755
+ partial: (ok, failed) => `Flagged ${ok} message(s), ${failed} failed`
84756
+ });
84657
84757
  }, "Error batch flagging messages")
84658
84758
  );
84659
84759
  registerTool(
84660
84760
  "batch-unflag-messages",
84661
84761
  {
84662
- description: "Use when: removing flags from multiple messages (1\u2013100 ids) in one call.\nReturns: counts of how many were unflagged and how many failed.\nDo not use when: unflagging just one (use unflag-message) or adding flags (use batch-flag-messages). Get the ids from search-messages or list-messages first.",
84762
+ description: "Use when: removing flags from multiple messages (1\u2013100 ids) in one call.\nReturns: counts of how many were unflagged and how many failed.\nDo not use when: unflagging just one (use unflag-message) or adding flags (use batch-flag-messages). Get the ids from search-messages or list-messages first. Pass sourceMailbox/sourceAccount (the mailbox you listed the ids from) so each numeric id is pinned to that mailbox; an id that matches in several mailboxes is refused, not guessed.",
84663
84763
  inputSchema: {
84664
- ids: BATCH_IDS_SCHEMA
84764
+ ids: BATCH_IDS_SCHEMA,
84765
+ sourceMailbox: BATCH_SOURCE_MAILBOX_SCHEMA,
84766
+ sourceAccount: BATCH_SOURCE_ACCOUNT_SCHEMA
84665
84767
  },
84666
84768
  outputSchema: BATCH_COUNT_OUTPUT_SCHEMA
84667
84769
  },
84668
- withErrorHandling(async ({ ids }) => {
84669
- const { success: successCount, fail: failCount } = await hybridBatchCounts(
84770
+ withErrorHandling(async ({ ids, sourceMailbox, sourceAccount }) => {
84771
+ const counts = await hybridBatchCounts(
84670
84772
  ids,
84671
- (n) => mailManager.batchUnflagMessages(n),
84773
+ (n) => mailManager.batchUnflagMessages(n, { account: sourceAccount, mailbox: sourceMailbox }),
84672
84774
  (im) => imapBatchUnflag(im)
84673
84775
  );
84674
- const structured = { ok: failCount === 0, success: successCount, failed: failCount };
84675
- if (failCount === 0) {
84676
- return successResponse(`Successfully unflagged ${successCount} message(s)`, structured);
84677
- } else if (successCount === 0) {
84678
- return errorResponse(`Failed to unflag all ${failCount} message(s)`);
84679
- } else {
84680
- return successResponse(
84681
- `Unflagged ${successCount} message(s), ${failCount} failed`,
84682
- structured
84683
- );
84684
- }
84776
+ return batchResponse(counts, {
84777
+ allSucceeded: (n) => `Successfully unflagged ${n} message(s)`,
84778
+ allFailed: (n) => `Failed to unflag all ${n} message(s)`,
84779
+ partial: (ok, failed) => `Unflagged ${ok} message(s), ${failed} failed`
84780
+ });
84685
84781
  }, "Error batch unflagging messages")
84686
84782
  );
84687
84783
  registerTool(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.10.15",
3
+ "version": "2.10.16",
4
4
  "description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",