apple-mail-mcp 2.19.13 → 2.19.15

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/README.md CHANGED
@@ -1077,6 +1077,15 @@ refused rather than guessed; pass the full path to disambiguate. This also
1077
1077
  means Gmail's nested special mailboxes now report their real path, e.g.
1078
1078
  `[Gmail]/All Mail` rather than `All Mail` — a visible change from before 2.17.0.
1079
1079
 
1080
+ Matching ignores letter case **and Unicode normalization form**: a typed
1081
+ precomposed `México` (NFC) finds a mailbox the server stores decomposed (NFD —
1082
+ common for folders created on a Mac, and what iCloud keeps), and the server's
1083
+ own stored spelling is what gets selected. Two mailboxes whose names differ
1084
+ *only* in case or normalization look identical in any listing, so a name that
1085
+ matches both is refused with an error saying so — rename one of them. For the
1086
+ same reason `create-mailbox` treats a normalization-equivalent existing name as
1087
+ already existing, and `rename-mailbox` refuses to create such a twin.
1088
+
1080
1089
  **Mail's local "On My Mac" mailboxes** are not children of any account — they
1081
1090
  hang off the application — so they are reported under the synthetic account label
1082
1091
  **`On My Mac`**. An unscoped call includes them (listed last); `account="On My
package/build/cli.js CHANGED
@@ -57647,6 +57647,19 @@ var require_imap_flow = __commonJS({
57647
57647
  }
57648
57648
  });
57649
57649
 
57650
+ // src/utils/mailboxName.ts
57651
+ function mailboxNameKey(name) {
57652
+ return name.trim().normalize("NFC").toLowerCase();
57653
+ }
57654
+ function nfc(name) {
57655
+ return name.trim().normalize("NFC");
57656
+ }
57657
+ var init_mailboxName = __esm({
57658
+ "src/utils/mailboxName.ts"() {
57659
+ "use strict";
57660
+ }
57661
+ });
57662
+
57650
57663
  // src/utils/mimeParse.ts
57651
57664
  function extractBoundary(source) {
57652
57665
  const match = source.match(/boundary="?([^";\s\r\n]+)"?/i);
@@ -58076,6 +58089,7 @@ __export(imapClient_exports, {
58076
58089
  listImapAccountLabels: () => listImapAccountLabels,
58077
58090
  mailFlagBitsFor: () => mailFlagBitsFor,
58078
58091
  mailFlagColorIndex: () => mailFlagColorIndex,
58092
+ matchMailbox: () => matchMailbox,
58079
58093
  normalizeMessageId: () => normalizeMessageId,
58080
58094
  resolveImapConfig: () => resolveImapConfig,
58081
58095
  resolveImapConfigs: () => resolveImapConfigs,
@@ -58290,16 +58304,21 @@ function staticMailboxAlias(mailbox) {
58290
58304
  }
58291
58305
  async function resolveMailboxPath(client, mailbox, _mode) {
58292
58306
  if (!mailbox) return "INBOX";
58307
+ let boxes;
58293
58308
  try {
58294
- const resolved = await resolveMailbox(client, mailbox);
58295
- if (resolved.kind === "found") return resolved.path;
58296
- const flag = SPECIAL_USE_ALIASES[mailbox.trim().toLowerCase()];
58297
- if (flag) {
58298
- const boxes = await client.list();
58299
- const special = boxes.find((b) => b.specialUse?.toLowerCase() === flag);
58300
- if (special) return special.path;
58301
- }
58309
+ boxes = await client.list();
58302
58310
  } catch {
58311
+ return staticMailboxAlias(mailbox);
58312
+ }
58313
+ const resolved = matchMailbox(boxes, mailbox);
58314
+ if (resolved.kind === "found") return resolved.path;
58315
+ const flag = SPECIAL_USE_ALIASES[mailbox.trim().toLowerCase()];
58316
+ if (flag) {
58317
+ const special = boxes.find((b) => b.specialUse?.toLowerCase() === flag);
58318
+ if (special) return special.path;
58319
+ }
58320
+ if (resolved.kind === "ambiguous") {
58321
+ throw new Error(ambiguousMailboxError(mailbox, resolved.candidates));
58303
58322
  }
58304
58323
  return staticMailboxAlias(mailbox);
58305
58324
  }
@@ -58391,6 +58410,13 @@ function structuredRow(m, account, path) {
58391
58410
  ...env.messageId ? { messageId: env.messageId } : {}
58392
58411
  };
58393
58412
  }
58413
+ function describeMailboxFailure(error) {
58414
+ const raw = errText(error);
58415
+ const oneLine = raw.split("\n")[0].trim();
58416
+ const looksSensitive = /pass(word)?\s*[:=]|authorization:\s*\S|bearer\s+\S{10,}/i.test(oneLine);
58417
+ const safe = looksSensitive ? "IMAP error (detail redacted \u2014 response text looked like it might contain a credential)" : oneLine || "unknown error";
58418
+ return safe.length > 300 ? `${safe.slice(0, 300)}\u2026` : safe;
58419
+ }
58394
58420
  function hasMailboxFlag(mailbox, wanted) {
58395
58421
  const normalized = wanted.toLowerCase();
58396
58422
  return [...mailbox.flags ?? []].some((flag) => flag.toLowerCase() === normalized);
@@ -58472,6 +58498,7 @@ async function run(args, listMode, deps) {
58472
58498
  const newestPerMailbox = offset + limit;
58473
58499
  const fetched = [];
58474
58500
  const failedMailboxes = [];
58501
+ const failedMailboxReasons = {};
58475
58502
  let totalMatched = 0;
58476
58503
  for (const path of paths) {
58477
58504
  try {
@@ -58480,14 +58507,16 @@ async function run(args, listMode, deps) {
58480
58507
  fetched.push(...result.messages.map((message) => ({ message, path })));
58481
58508
  } catch (error) {
58482
58509
  failedMailboxes.push(path);
58510
+ failedMailboxReasons[path] = describeMailboxFailure(error);
58483
58511
  console.error(
58484
58512
  `IMAP ${listMode ? "list" : "search"} failed for account "${cfg.accountLabel}", mailbox "${path}": ${String(error)}`
58485
58513
  );
58486
58514
  }
58487
58515
  }
58488
58516
  if (failedMailboxes.length === paths.length) {
58517
+ const detail = failedMailboxes.map((path) => `${path} (${failedMailboxReasons[path]})`).join(", ");
58489
58518
  throw new Error(
58490
- `IMAP ${listMode ? "list" : "search"} failed in every requested mailbox for account ${cfg.accountLabel}: ${failedMailboxes.join(", ")}.`
58519
+ `IMAP ${listMode ? "list" : "search"} failed in every requested mailbox for account ${cfg.accountLabel}: ${detail}.`
58491
58520
  );
58492
58521
  }
58493
58522
  let ordered = fetched;
@@ -58509,7 +58538,7 @@ async function run(args, listMode, deps) {
58509
58538
  const partial = failedMailboxes.length > 0;
58510
58539
  const failureNote = partial ? `
58511
58540
 
58512
- Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}"`).join(", ")}.` : "";
58541
+ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
58513
58542
  const verb = listMode ? "listed" : "matched";
58514
58543
  const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
58515
58544
  if (messages.length === 0) {
@@ -58518,14 +58547,22 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
58518
58547
  messages,
58519
58548
  count: 0,
58520
58549
  partial,
58521
- failedMailboxes
58550
+ failedMailboxes,
58551
+ failedMailboxReasons
58522
58552
  };
58523
58553
  }
58524
58554
  const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalMatched} total ${verb}):
58525
58555
  ` + rows.join("\n") + `
58526
58556
 
58527
58557
  Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutations (mark/flag/move/delete-message), which route back to IMAP.` + failureNote;
58528
- return { text, messages, count: messages.length, partial, failedMailboxes };
58558
+ return {
58559
+ text,
58560
+ messages,
58561
+ count: messages.length,
58562
+ partial,
58563
+ failedMailboxes,
58564
+ failedMailboxReasons
58565
+ };
58529
58566
  },
58530
58567
  true
58531
58568
  );
@@ -58612,7 +58649,13 @@ function imapMailStats(deps = {}) {
58612
58649
  );
58613
58650
  }
58614
58651
  function errText(e) {
58615
- return e instanceof Error ? e.message : String(e);
58652
+ if (!(e instanceof Error)) return String(e);
58653
+ const x = e;
58654
+ if (typeof x.responseStatus !== "string" || !x.responseStatus) return e.message;
58655
+ const code = typeof x.serverResponseCode === "string" && x.serverResponseCode;
58656
+ const text = typeof x.responseText === "string" ? x.responseText.trim() : "";
58657
+ const server = [x.responseStatus, code ? `[${code}]` : "", text].filter(Boolean).join(" ");
58658
+ return e.message && e.message !== "Command failed" ? `${e.message}: ${server}` : server;
58616
58659
  }
58617
58660
  function assertMutated(result, what) {
58618
58661
  if (!result) throw new Error(`${what}: server rejected the command (IMAP NO/BAD)`);
@@ -58764,20 +58807,30 @@ function withClient(deps, fn) {
58764
58807
  return useClient(deps, fn);
58765
58808
  }
58766
58809
  async function resolveMailbox(client, name) {
58767
- const wanted = name.trim().toLowerCase();
58768
58810
  const boxes = await client.list();
58769
- const byPath = boxes.find((b) => b.path.toLowerCase() === wanted);
58770
- if (byPath) return { kind: "found", path: byPath.path };
58771
- const byName = boxes.filter((b) => b.name.toLowerCase() === wanted);
58772
- if (byName.length === 1) return { kind: "found", path: byName[0].path };
58773
- if (byName.length > 1) {
58774
- return { kind: "ambiguous", candidates: byName.map((b) => b.path).sort() };
58811
+ return matchMailbox(boxes, name);
58812
+ }
58813
+ function matchMailbox(boxes, name) {
58814
+ const exact = boxes.find((b) => b.path === name || b.path === name.trim());
58815
+ if (exact) return { kind: "found", path: exact.path };
58816
+ const wanted = mailboxNameKey(name);
58817
+ const tiers = [
58818
+ boxes.filter((b) => mailboxNameKey(b.path) === wanted),
58819
+ boxes.filter((b) => mailboxNameKey(b.name) === wanted)
58820
+ ];
58821
+ for (const hits of tiers) {
58822
+ const paths = [...new Set(hits.map((b) => b.path))];
58823
+ if (paths.length === 1) return { kind: "found", path: paths[0] };
58824
+ if (paths.length > 1) return { kind: "ambiguous", candidates: paths.sort() };
58775
58825
  }
58776
58826
  return { kind: "none" };
58777
58827
  }
58778
58828
  function ambiguousMailboxError(name, candidates, accountLabel) {
58779
58829
  const where = accountLabel ? ` on IMAP account ${accountLabel}` : "";
58780
- return `Mailbox "${name}" is ambiguous${where} \u2014 it matches ${candidates.map((c) => `"${c}"`).join(" and ")}. Pass the full path.`;
58830
+ const listed = candidates.map((c) => `"${c}"`).join(" and ");
58831
+ const indistinguishable = new Set(candidates.map(mailboxNameKey)).size === 1;
58832
+ const hint = indistinguishable ? " They differ only in letter case or Unicode normalization (precomposed vs decomposed accents), so no typed name can tell them apart. Rename one of them." : " Pass the full path.";
58833
+ return `Mailbox "${name}" is ambiguous${where} \u2014 it matches ${listed}.${hint}`;
58781
58834
  }
58782
58835
  async function findMailboxPathOrThrow(client, name) {
58783
58836
  const res = await resolveMailbox(client, name);
@@ -58786,6 +58839,15 @@ async function findMailboxPathOrThrow(client, name) {
58786
58839
  }
58787
58840
  function imapCreateMailbox(name, deps = {}) {
58788
58841
  return withClient(deps, async (client) => {
58842
+ let twin;
58843
+ try {
58844
+ const wanted = nfc(name);
58845
+ twin = (await client.list()).find((b) => nfc(b.path) === wanted)?.path;
58846
+ } catch {
58847
+ }
58848
+ if (twin !== void 0) {
58849
+ return { success: true, info: `Mailbox "${twin}" already existed.` };
58850
+ }
58789
58851
  try {
58790
58852
  const res = await client.mailboxCreate(name);
58791
58853
  return res.created ? { success: true, info: `Created mailbox "${res.path}".` } : { success: true, info: `Mailbox "${res.path}" already existed.` };
@@ -58837,6 +58899,14 @@ function imapRenameMailbox(oldName, newName, deps = {}) {
58837
58899
  };
58838
58900
  }
58839
58901
  const path = found.path;
58902
+ const wantedNew = nfc(newName);
58903
+ const clash = (await client.list()).find((b) => b.path !== path && nfc(b.path) === wantedNew);
58904
+ if (clash) {
58905
+ return {
58906
+ success: false,
58907
+ error: `Cannot rename "${path}" to "${newName}": mailbox "${clash.path}" already exists on IMAP account ${cfg.accountLabel} (the same name, differing only in how its accents are encoded).`
58908
+ };
58909
+ }
58840
58910
  try {
58841
58911
  const res = await client.mailboxRename(path, newName);
58842
58912
  return { success: true, info: `Renamed "${res.path}" to "${res.newPath}" via IMAP.` };
@@ -59491,6 +59561,7 @@ var init_imapClient = __esm({
59491
59561
  import_imapflow = __toESM(require_imap_flow(), 1);
59492
59562
  init_smtpMailer();
59493
59563
  init_docsUrls();
59564
+ init_mailboxName();
59494
59565
  init_mimeParse();
59495
59566
  init_headers();
59496
59567
  init_auditLog();
package/build/index.js CHANGED
@@ -7681,6 +7681,19 @@ var init_headers = __esm({
7681
7681
  }
7682
7682
  });
7683
7683
 
7684
+ // src/utils/mailboxName.ts
7685
+ function mailboxNameKey(name) {
7686
+ return name.trim().normalize("NFC").toLowerCase();
7687
+ }
7688
+ function nfc(name) {
7689
+ return name.trim().normalize("NFC");
7690
+ }
7691
+ var init_mailboxName = __esm({
7692
+ "src/utils/mailboxName.ts"() {
7693
+ "use strict";
7694
+ }
7695
+ });
7696
+
7684
7697
  // src/utils/attachmentLimits.ts
7685
7698
  function isInlineAttachmentBase64WithinLimit(contentBase64) {
7686
7699
  if (contentBase64.length > MAX_INLINE_ATTACHMENT_BASE64_INPUT_CHARS) return false;
@@ -65405,6 +65418,7 @@ __export(imapClient_exports, {
65405
65418
  listImapAccountLabels: () => listImapAccountLabels,
65406
65419
  mailFlagBitsFor: () => mailFlagBitsFor,
65407
65420
  mailFlagColorIndex: () => mailFlagColorIndex,
65421
+ matchMailbox: () => matchMailbox,
65408
65422
  normalizeMessageId: () => normalizeMessageId,
65409
65423
  resolveImapConfig: () => resolveImapConfig,
65410
65424
  resolveImapConfigs: () => resolveImapConfigs,
@@ -65619,16 +65633,21 @@ function staticMailboxAlias(mailbox) {
65619
65633
  }
65620
65634
  async function resolveMailboxPath(client, mailbox, _mode) {
65621
65635
  if (!mailbox) return "INBOX";
65636
+ let boxes;
65622
65637
  try {
65623
- const resolved = await resolveMailbox(client, mailbox);
65624
- if (resolved.kind === "found") return resolved.path;
65625
- const flag = SPECIAL_USE_ALIASES[mailbox.trim().toLowerCase()];
65626
- if (flag) {
65627
- const boxes = await client.list();
65628
- const special = boxes.find((b) => b.specialUse?.toLowerCase() === flag);
65629
- if (special) return special.path;
65630
- }
65638
+ boxes = await client.list();
65631
65639
  } catch {
65640
+ return staticMailboxAlias(mailbox);
65641
+ }
65642
+ const resolved = matchMailbox(boxes, mailbox);
65643
+ if (resolved.kind === "found") return resolved.path;
65644
+ const flag = SPECIAL_USE_ALIASES[mailbox.trim().toLowerCase()];
65645
+ if (flag) {
65646
+ const special = boxes.find((b) => b.specialUse?.toLowerCase() === flag);
65647
+ if (special) return special.path;
65648
+ }
65649
+ if (resolved.kind === "ambiguous") {
65650
+ throw new Error(ambiguousMailboxError(mailbox, resolved.candidates));
65632
65651
  }
65633
65652
  return staticMailboxAlias(mailbox);
65634
65653
  }
@@ -65720,6 +65739,13 @@ function structuredRow(m, account, path) {
65720
65739
  ...env.messageId ? { messageId: env.messageId } : {}
65721
65740
  };
65722
65741
  }
65742
+ function describeMailboxFailure(error2) {
65743
+ const raw = errText(error2);
65744
+ const oneLine = raw.split("\n")[0].trim();
65745
+ const looksSensitive = /pass(word)?\s*[:=]|authorization:\s*\S|bearer\s+\S{10,}/i.test(oneLine);
65746
+ const safe = looksSensitive ? "IMAP error (detail redacted \u2014 response text looked like it might contain a credential)" : oneLine || "unknown error";
65747
+ return safe.length > 300 ? `${safe.slice(0, 300)}\u2026` : safe;
65748
+ }
65723
65749
  function hasMailboxFlag(mailbox, wanted) {
65724
65750
  const normalized = wanted.toLowerCase();
65725
65751
  return [...mailbox.flags ?? []].some((flag) => flag.toLowerCase() === normalized);
@@ -65801,6 +65827,7 @@ async function run(args, listMode, deps) {
65801
65827
  const newestPerMailbox = offset + limit;
65802
65828
  const fetched = [];
65803
65829
  const failedMailboxes = [];
65830
+ const failedMailboxReasons = {};
65804
65831
  let totalMatched = 0;
65805
65832
  for (const path of paths) {
65806
65833
  try {
@@ -65809,14 +65836,16 @@ async function run(args, listMode, deps) {
65809
65836
  fetched.push(...result.messages.map((message) => ({ message, path })));
65810
65837
  } catch (error2) {
65811
65838
  failedMailboxes.push(path);
65839
+ failedMailboxReasons[path] = describeMailboxFailure(error2);
65812
65840
  console.error(
65813
65841
  `IMAP ${listMode ? "list" : "search"} failed for account "${cfg.accountLabel}", mailbox "${path}": ${String(error2)}`
65814
65842
  );
65815
65843
  }
65816
65844
  }
65817
65845
  if (failedMailboxes.length === paths.length) {
65846
+ const detail = failedMailboxes.map((path) => `${path} (${failedMailboxReasons[path]})`).join(", ");
65818
65847
  throw new Error(
65819
- `IMAP ${listMode ? "list" : "search"} failed in every requested mailbox for account ${cfg.accountLabel}: ${failedMailboxes.join(", ")}.`
65848
+ `IMAP ${listMode ? "list" : "search"} failed in every requested mailbox for account ${cfg.accountLabel}: ${detail}.`
65820
65849
  );
65821
65850
  }
65822
65851
  let ordered = fetched;
@@ -65838,7 +65867,7 @@ async function run(args, listMode, deps) {
65838
65867
  const partial2 = failedMailboxes.length > 0;
65839
65868
  const failureNote = partial2 ? `
65840
65869
 
65841
- Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}"`).join(", ")}.` : "";
65870
+ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
65842
65871
  const verb = listMode ? "listed" : "matched";
65843
65872
  const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
65844
65873
  if (messages.length === 0) {
@@ -65847,14 +65876,22 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
65847
65876
  messages,
65848
65877
  count: 0,
65849
65878
  partial: partial2,
65850
- failedMailboxes
65879
+ failedMailboxes,
65880
+ failedMailboxReasons
65851
65881
  };
65852
65882
  }
65853
65883
  const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalMatched} total ${verb}):
65854
65884
  ` + rows.join("\n") + `
65855
65885
 
65856
65886
  Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutations (mark/flag/move/delete-message), which route back to IMAP.` + failureNote;
65857
- return { text, messages, count: messages.length, partial: partial2, failedMailboxes };
65887
+ return {
65888
+ text,
65889
+ messages,
65890
+ count: messages.length,
65891
+ partial: partial2,
65892
+ failedMailboxes,
65893
+ failedMailboxReasons
65894
+ };
65858
65895
  },
65859
65896
  true
65860
65897
  );
@@ -65941,7 +65978,13 @@ function imapMailStats(deps = {}) {
65941
65978
  );
65942
65979
  }
65943
65980
  function errText(e) {
65944
- return e instanceof Error ? e.message : String(e);
65981
+ if (!(e instanceof Error)) return String(e);
65982
+ const x = e;
65983
+ if (typeof x.responseStatus !== "string" || !x.responseStatus) return e.message;
65984
+ const code = typeof x.serverResponseCode === "string" && x.serverResponseCode;
65985
+ const text = typeof x.responseText === "string" ? x.responseText.trim() : "";
65986
+ const server2 = [x.responseStatus, code ? `[${code}]` : "", text].filter(Boolean).join(" ");
65987
+ return e.message && e.message !== "Command failed" ? `${e.message}: ${server2}` : server2;
65945
65988
  }
65946
65989
  function assertMutated(result, what) {
65947
65990
  if (!result) throw new Error(`${what}: server rejected the command (IMAP NO/BAD)`);
@@ -66093,20 +66136,30 @@ function withClient(deps, fn) {
66093
66136
  return useClient(deps, fn);
66094
66137
  }
66095
66138
  async function resolveMailbox(client, name) {
66096
- const wanted = name.trim().toLowerCase();
66097
66139
  const boxes = await client.list();
66098
- const byPath = boxes.find((b) => b.path.toLowerCase() === wanted);
66099
- if (byPath) return { kind: "found", path: byPath.path };
66100
- const byName = boxes.filter((b) => b.name.toLowerCase() === wanted);
66101
- if (byName.length === 1) return { kind: "found", path: byName[0].path };
66102
- if (byName.length > 1) {
66103
- return { kind: "ambiguous", candidates: byName.map((b) => b.path).sort() };
66140
+ return matchMailbox(boxes, name);
66141
+ }
66142
+ function matchMailbox(boxes, name) {
66143
+ const exact = boxes.find((b) => b.path === name || b.path === name.trim());
66144
+ if (exact) return { kind: "found", path: exact.path };
66145
+ const wanted = mailboxNameKey(name);
66146
+ const tiers = [
66147
+ boxes.filter((b) => mailboxNameKey(b.path) === wanted),
66148
+ boxes.filter((b) => mailboxNameKey(b.name) === wanted)
66149
+ ];
66150
+ for (const hits of tiers) {
66151
+ const paths = [...new Set(hits.map((b) => b.path))];
66152
+ if (paths.length === 1) return { kind: "found", path: paths[0] };
66153
+ if (paths.length > 1) return { kind: "ambiguous", candidates: paths.sort() };
66104
66154
  }
66105
66155
  return { kind: "none" };
66106
66156
  }
66107
66157
  function ambiguousMailboxError(name, candidates, accountLabel) {
66108
66158
  const where = accountLabel ? ` on IMAP account ${accountLabel}` : "";
66109
- return `Mailbox "${name}" is ambiguous${where} \u2014 it matches ${candidates.map((c) => `"${c}"`).join(" and ")}. Pass the full path.`;
66159
+ const listed = candidates.map((c) => `"${c}"`).join(" and ");
66160
+ const indistinguishable = new Set(candidates.map(mailboxNameKey)).size === 1;
66161
+ const hint = indistinguishable ? " They differ only in letter case or Unicode normalization (precomposed vs decomposed accents), so no typed name can tell them apart. Rename one of them." : " Pass the full path.";
66162
+ return `Mailbox "${name}" is ambiguous${where} \u2014 it matches ${listed}.${hint}`;
66110
66163
  }
66111
66164
  async function findMailboxPathOrThrow(client, name) {
66112
66165
  const res = await resolveMailbox(client, name);
@@ -66115,6 +66168,15 @@ async function findMailboxPathOrThrow(client, name) {
66115
66168
  }
66116
66169
  function imapCreateMailbox(name, deps = {}) {
66117
66170
  return withClient(deps, async (client) => {
66171
+ let twin;
66172
+ try {
66173
+ const wanted = nfc(name);
66174
+ twin = (await client.list()).find((b) => nfc(b.path) === wanted)?.path;
66175
+ } catch {
66176
+ }
66177
+ if (twin !== void 0) {
66178
+ return { success: true, info: `Mailbox "${twin}" already existed.` };
66179
+ }
66118
66180
  try {
66119
66181
  const res = await client.mailboxCreate(name);
66120
66182
  return res.created ? { success: true, info: `Created mailbox "${res.path}".` } : { success: true, info: `Mailbox "${res.path}" already existed.` };
@@ -66166,6 +66228,14 @@ function imapRenameMailbox(oldName, newName, deps = {}) {
66166
66228
  };
66167
66229
  }
66168
66230
  const path = found.path;
66231
+ const wantedNew = nfc(newName);
66232
+ const clash = (await client.list()).find((b) => b.path !== path && nfc(b.path) === wantedNew);
66233
+ if (clash) {
66234
+ return {
66235
+ success: false,
66236
+ error: `Cannot rename "${path}" to "${newName}": mailbox "${clash.path}" already exists on IMAP account ${cfg.accountLabel} (the same name, differing only in how its accents are encoded).`
66237
+ };
66238
+ }
66169
66239
  try {
66170
66240
  const res = await client.mailboxRename(path, newName);
66171
66241
  return { success: true, info: `Renamed "${res.path}" to "${res.newPath}" via IMAP.` };
@@ -66820,6 +66890,7 @@ var init_imapClient = __esm({
66820
66890
  import_imapflow = __toESM(require_imap_flow(), 1);
66821
66891
  init_smtpMailer();
66822
66892
  init_docsUrls();
66893
+ init_mailboxName();
66823
66894
  init_mimeParse();
66824
66895
  init_headers();
66825
66896
  init_auditLog();
@@ -81913,6 +81984,7 @@ function executeAppleScript(script, options = {}) {
81913
81984
  init_docsUrls();
81914
81985
  init_mimeParse();
81915
81986
  init_headers();
81987
+ init_mailboxName();
81916
81988
 
81917
81989
  // src/services/templateStore.ts
81918
81990
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -82548,23 +82620,28 @@ function mailboxLeaf(path) {
82548
82620
  function resolveAppleMailboxPath(mailbox, actualPaths) {
82549
82621
  if (actualPaths.length === 0) return mailbox;
82550
82622
  const candidates = [mailbox, ...MAILBOX_ALIASES[mailbox.toLowerCase()] ?? []];
82623
+ const ambiguous = (matches) => {
82624
+ const sorted = [...new Set(matches)].sort();
82625
+ const paths = sorted.map((path) => `"${path}"`).join(" and ");
82626
+ const twins = new Set(sorted.map(mailboxNameKey)).size === 1;
82627
+ const hint = twins ? " They differ only in letter case or Unicode normalization (precomposed vs decomposed accents), so no typed name can tell them apart. Rename one of them." : " Pass the full path.";
82628
+ return new Error(`Mailbox "${mailbox}" is ambiguous \u2014 it matches ${paths}.${hint}`);
82629
+ };
82551
82630
  for (const candidate of candidates) {
82552
82631
  const exact = actualPaths.find((path) => path === candidate);
82553
82632
  if (exact) return exact;
82554
- const folded = actualPaths.find((path) => path.toLowerCase() === candidate.toLowerCase());
82555
- if (folded) return folded;
82633
+ const key = mailboxNameKey(candidate);
82634
+ const folded = [...new Set(actualPaths.filter((path) => mailboxNameKey(path) === key))];
82635
+ if (folded.length === 1) return folded[0];
82636
+ if (folded.length > 1) throw ambiguous(folded);
82556
82637
  }
82557
82638
  for (const candidate of candidates) {
82558
- const leafMatches = actualPaths.filter(
82559
- (path) => mailboxLeaf(path).toLowerCase() === candidate.toLowerCase()
82560
- );
82639
+ const key = mailboxNameKey(candidate);
82640
+ const leafMatches = [
82641
+ ...new Set(actualPaths.filter((path) => mailboxNameKey(mailboxLeaf(path)) === key))
82642
+ ];
82561
82643
  if (leafMatches.length === 1) return leafMatches[0];
82562
- if (leafMatches.length > 1) {
82563
- const paths = [...leafMatches].sort().map((path) => `"${path}"`).join(" and ");
82564
- throw new Error(
82565
- `Mailbox "${mailbox}" is ambiguous \u2014 it matches ${paths}. Pass the full path.`
82566
- );
82567
- }
82644
+ if (leafMatches.length > 1) throw ambiguous(leafMatches);
82568
82645
  }
82569
82646
  return mailbox;
82570
82647
  }
@@ -87359,6 +87436,7 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
87359
87436
  const accountsQueried = [];
87360
87437
  const accountsFailed = [];
87361
87438
  const failedMailboxes = [];
87439
+ const failedMailboxReasons = {};
87362
87440
  for (const config2 of configs) {
87363
87441
  const perAccountArgs = { ...args, account: void 0 };
87364
87442
  try {
@@ -87368,12 +87446,15 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
87368
87446
  failedMailboxes.push(
87369
87447
  ...res.failedMailboxes.map((mailbox) => `${config2.accountLabel} / ${mailbox}`)
87370
87448
  );
87449
+ for (const [mailbox, reason] of Object.entries(res.failedMailboxReasons)) {
87450
+ failedMailboxReasons[`${config2.accountLabel} / ${mailbox}`] = reason;
87451
+ }
87371
87452
  } catch (e) {
87372
87453
  accountsFailed.push(config2.accountLabel);
87373
87454
  console.error(`IMAP fan-out failed for account "${config2.accountLabel}": ${String(e)}`);
87374
87455
  }
87375
87456
  }
87376
- return { rows, accountsQueried, accountsFailed, failedMailboxes };
87457
+ return { rows, accountsQueried, accountsFailed, failedMailboxes, failedMailboxReasons };
87377
87458
  }
87378
87459
  function configMatchesAccount(config2, account) {
87379
87460
  const name = account.name.trim().toLowerCase();
@@ -87973,7 +88054,10 @@ var LIST_OUTPUT_SCHEMA = {
87973
88054
  skippedLargeMailboxes: external_exports.array(external_exports.string()).optional(),
87974
88055
  notSearchedMailboxes: external_exports.array(external_exports.string()).optional(),
87975
88056
  timedOutAccounts: external_exports.array(external_exports.string()).optional(),
87976
- failedMailboxes: external_exports.array(external_exports.string()).optional()
88057
+ failedMailboxes: external_exports.array(external_exports.string()).optional(),
88058
+ // Underlying error text per entry in `failedMailboxes`, same keys (#246
88059
+ // follow-up) — declared so a client can rely on it rather than parse text.
88060
+ failedMailboxReasons: external_exports.record(external_exports.string(), external_exports.string()).optional()
87977
88061
  };
87978
88062
  var BATCH_COUNT_OUTPUT_SCHEMA = {
87979
88063
  ok: external_exports.boolean().optional(),
@@ -88063,7 +88147,8 @@ function mergedMessageResponse(fan, apple, limit, verb) {
88063
88147
  skippedLargeMailboxes: diagnostics.skippedLargeMailboxes,
88064
88148
  notSearchedMailboxes: diagnostics.notSearchedMailboxes,
88065
88149
  timedOutAccounts: diagnostics.timedOutAccounts,
88066
- failedMailboxes: fan.failedMailboxes
88150
+ failedMailboxes: fan.failedMailboxes,
88151
+ failedMailboxReasons: fan.failedMailboxReasons
88067
88152
  };
88068
88153
  const coverageBlock = partialCoverageBlock(diagnostics);
88069
88154
  if (merged.length === 0) {
@@ -88165,7 +88250,8 @@ registerTool(
88165
88250
  messages: r.messages,
88166
88251
  count: r.count,
88167
88252
  partial: r.partial,
88168
- failedMailboxes: r.failedMailboxes
88253
+ failedMailboxes: r.failedMailboxes,
88254
+ failedMailboxReasons: r.failedMailboxReasons
88169
88255
  });
88170
88256
  }
88171
88257
  const fan = await fanOutImapMessages(imapArgs, "search");
@@ -88548,7 +88634,8 @@ ${r.text}`, {
88548
88634
  messages: r.messages,
88549
88635
  count: r.count,
88550
88636
  partial: r.partial,
88551
- failedMailboxes: r.failedMailboxes
88637
+ failedMailboxes: r.failedMailboxes,
88638
+ failedMailboxReasons: r.failedMailboxReasons
88552
88639
  });
88553
88640
  }
88554
88641
  const fan = await fanOutImapMessages({ subject: base, mailbox, limit }, "search");
@@ -88585,7 +88672,8 @@ ${r.text}`, {
88585
88672
  messages: orderedRows,
88586
88673
  count: orderedRows.length,
88587
88674
  partial: partial2,
88588
- failedMailboxes: fan.failedMailboxes
88675
+ failedMailboxes: fan.failedMailboxes,
88676
+ failedMailboxReasons: fan.failedMailboxReasons
88589
88677
  };
88590
88678
  if (orderedRows.length === 0) {
88591
88679
  return successResponse(`No messages found in thread "${base}".${coverage}`, structured2);
@@ -88649,7 +88737,8 @@ registerTool(
88649
88737
  messages: r.messages,
88650
88738
  count: r.count,
88651
88739
  partial: r.partial,
88652
- failedMailboxes: r.failedMailboxes
88740
+ failedMailboxes: r.failedMailboxes,
88741
+ failedMailboxReasons: r.failedMailboxReasons
88653
88742
  });
88654
88743
  }
88655
88744
  const fan = await fanOutImapMessages({ mailbox, limit, offset, from, unreadOnly }, "list");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.19.13",
3
+ "version": "2.19.15",
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",