apple-mail-mcp 2.19.14 → 2.19.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.
package/README.md CHANGED
@@ -1077,6 +1077,21 @@ 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
+
1089
+ **Messages awaiting expunge are not listed.** On IMAP accounts, search,
1090
+ list, thread and mail-stats queries only match messages *not* flagged
1091
+ `\Deleted` (IMAP `UNDELETED`). iCloud keeps flagged-but-never-expunged
1092
+ messages out of its message counts yet still returns them from `UID SEARCH`,
1093
+ which made a filter-less `list-messages` fail on such a mailbox before 2.19.16.
1094
+
1080
1095
  **Mail's local "On My Mac" mailboxes** are not children of any account — they
1081
1096
  hang off the application — so they are reported under the synthetic account label
1082
1097
  **`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,21 +58304,26 @@ 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
  }
58306
58325
  function buildCriteria(a, listMode) {
58307
- const c = {};
58326
+ const c = { ...NOT_DELETED };
58308
58327
  if (a.query) c.or = [{ subject: a.query }, { from: a.query }];
58309
58328
  if (a.body) c.body = a.body;
58310
58329
  if (a.from) c.from = a.from;
@@ -58316,7 +58335,6 @@ function buildCriteria(a, listMode) {
58316
58335
  if (a.isFlagged === false) c.unflagged = true;
58317
58336
  if (a.dateFrom) c.since = new Date(a.dateFrom);
58318
58337
  if (a.dateTo) c.before = new Date(a.dateTo);
58319
- if (Object.keys(c).length === 0) c.all = true;
58320
58338
  return c;
58321
58339
  }
58322
58340
  function validDate(d) {
@@ -58392,7 +58410,7 @@ function structuredRow(m, account, path) {
58392
58410
  };
58393
58411
  }
58394
58412
  function describeMailboxFailure(error) {
58395
- const raw = error instanceof Error ? error.message : String(error);
58413
+ const raw = errText(error);
58396
58414
  const oneLine = raw.split("\n")[0].trim();
58397
58415
  const looksSensitive = /pass(word)?\s*[:=]|authorization:\s*\S|bearer\s+\S{10,}/i.test(oneLine);
58398
58416
  const safe = looksSensitive ? "IMAP error (detail redacted \u2014 response text looked like it might contain a credential)" : oneLine || "unknown error";
@@ -58610,7 +58628,10 @@ function imapMailStats(deps = {}) {
58610
58628
  try {
58611
58629
  const lock = await client.getMailboxLock("INBOX");
58612
58630
  try {
58613
- const found = await client.search({ since: since(days) }, { uid: true });
58631
+ const found = await client.search(
58632
+ { since: since(days), ...NOT_DELETED },
58633
+ { uid: true }
58634
+ );
58614
58635
  return Array.isArray(found) ? found.length : 0;
58615
58636
  } finally {
58616
58637
  lock.release();
@@ -58630,7 +58651,13 @@ function imapMailStats(deps = {}) {
58630
58651
  );
58631
58652
  }
58632
58653
  function errText(e) {
58633
- return e instanceof Error ? e.message : String(e);
58654
+ if (!(e instanceof Error)) return String(e);
58655
+ const x = e;
58656
+ if (typeof x.responseStatus !== "string" || !x.responseStatus) return e.message;
58657
+ const code = typeof x.serverResponseCode === "string" && x.serverResponseCode;
58658
+ const text = typeof x.responseText === "string" ? x.responseText.trim() : "";
58659
+ const server = [x.responseStatus, code ? `[${code}]` : "", text].filter(Boolean).join(" ");
58660
+ return e.message && e.message !== "Command failed" ? `${e.message}: ${server}` : server;
58634
58661
  }
58635
58662
  function assertMutated(result, what) {
58636
58663
  if (!result) throw new Error(`${what}: server rejected the command (IMAP NO/BAD)`);
@@ -58782,20 +58809,30 @@ function withClient(deps, fn) {
58782
58809
  return useClient(deps, fn);
58783
58810
  }
58784
58811
  async function resolveMailbox(client, name) {
58785
- const wanted = name.trim().toLowerCase();
58786
58812
  const boxes = await client.list();
58787
- const byPath = boxes.find((b) => b.path.toLowerCase() === wanted);
58788
- if (byPath) return { kind: "found", path: byPath.path };
58789
- const byName = boxes.filter((b) => b.name.toLowerCase() === wanted);
58790
- if (byName.length === 1) return { kind: "found", path: byName[0].path };
58791
- if (byName.length > 1) {
58792
- return { kind: "ambiguous", candidates: byName.map((b) => b.path).sort() };
58813
+ return matchMailbox(boxes, name);
58814
+ }
58815
+ function matchMailbox(boxes, name) {
58816
+ const exact = boxes.find((b) => b.path === name || b.path === name.trim());
58817
+ if (exact) return { kind: "found", path: exact.path };
58818
+ const wanted = mailboxNameKey(name);
58819
+ const tiers = [
58820
+ boxes.filter((b) => mailboxNameKey(b.path) === wanted),
58821
+ boxes.filter((b) => mailboxNameKey(b.name) === wanted)
58822
+ ];
58823
+ for (const hits of tiers) {
58824
+ const paths = [...new Set(hits.map((b) => b.path))];
58825
+ if (paths.length === 1) return { kind: "found", path: paths[0] };
58826
+ if (paths.length > 1) return { kind: "ambiguous", candidates: paths.sort() };
58793
58827
  }
58794
58828
  return { kind: "none" };
58795
58829
  }
58796
58830
  function ambiguousMailboxError(name, candidates, accountLabel) {
58797
58831
  const where = accountLabel ? ` on IMAP account ${accountLabel}` : "";
58798
- return `Mailbox "${name}" is ambiguous${where} \u2014 it matches ${candidates.map((c) => `"${c}"`).join(" and ")}. Pass the full path.`;
58832
+ const listed = candidates.map((c) => `"${c}"`).join(" and ");
58833
+ const indistinguishable = new Set(candidates.map(mailboxNameKey)).size === 1;
58834
+ 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.";
58835
+ return `Mailbox "${name}" is ambiguous${where} \u2014 it matches ${listed}.${hint}`;
58799
58836
  }
58800
58837
  async function findMailboxPathOrThrow(client, name) {
58801
58838
  const res = await resolveMailbox(client, name);
@@ -58804,6 +58841,15 @@ async function findMailboxPathOrThrow(client, name) {
58804
58841
  }
58805
58842
  function imapCreateMailbox(name, deps = {}) {
58806
58843
  return withClient(deps, async (client) => {
58844
+ let twin;
58845
+ try {
58846
+ const wanted = nfc(name);
58847
+ twin = (await client.list()).find((b) => nfc(b.path) === wanted)?.path;
58848
+ } catch {
58849
+ }
58850
+ if (twin !== void 0) {
58851
+ return { success: true, info: `Mailbox "${twin}" already existed.` };
58852
+ }
58807
58853
  try {
58808
58854
  const res = await client.mailboxCreate(name);
58809
58855
  return res.created ? { success: true, info: `Created mailbox "${res.path}".` } : { success: true, info: `Mailbox "${res.path}" already existed.` };
@@ -58855,6 +58901,14 @@ function imapRenameMailbox(oldName, newName, deps = {}) {
58855
58901
  };
58856
58902
  }
58857
58903
  const path = found.path;
58904
+ const wantedNew = nfc(newName);
58905
+ const clash = (await client.list()).find((b) => b.path !== path && nfc(b.path) === wantedNew);
58906
+ if (clash) {
58907
+ return {
58908
+ success: false,
58909
+ 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).`
58910
+ };
58911
+ }
58858
58912
  try {
58859
58913
  const res = await client.mailboxRename(path, newName);
58860
58914
  return { success: true, info: `Renamed "${res.path}" to "${res.newPath}" via IMAP.` };
@@ -59446,11 +59500,23 @@ async function imapThread(id, deps = {}, limit = 50) {
59446
59500
  if (Array.isArray(found)) found.forEach((u) => uidSet.add(u));
59447
59501
  };
59448
59502
  if (seedMsgId) {
59449
- addFound(await client.search({ header: { references: seedMsgId } }, { uid: true }));
59450
- addFound(await client.search({ header: { "in-reply-to": seedMsgId } }, { uid: true }));
59503
+ addFound(
59504
+ await client.search(
59505
+ { header: { references: seedMsgId }, ...NOT_DELETED },
59506
+ { uid: true }
59507
+ )
59508
+ );
59509
+ addFound(
59510
+ await client.search(
59511
+ { header: { "in-reply-to": seedMsgId }, ...NOT_DELETED },
59512
+ { uid: true }
59513
+ )
59514
+ );
59451
59515
  }
59452
59516
  for (const mid of [...refIds].slice(0, 20)) {
59453
- addFound(await client.search({ header: { "message-id": mid } }, { uid: true }));
59517
+ addFound(
59518
+ await client.search({ header: { "message-id": mid }, ...NOT_DELETED }, { uid: true })
59519
+ );
59454
59520
  }
59455
59521
  if (uidSet.size <= 1) return null;
59456
59522
  const uids = [...uidSet].slice(0, limit);
@@ -59502,13 +59568,14 @@ async function imapThread(id, deps = {}, limit = 50) {
59502
59568
  true
59503
59569
  );
59504
59570
  }
59505
- var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
59571
+ var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
59506
59572
  var init_imapClient = __esm({
59507
59573
  "src/services/imapClient.ts"() {
59508
59574
  "use strict";
59509
59575
  import_imapflow = __toESM(require_imap_flow(), 1);
59510
59576
  init_smtpMailer();
59511
59577
  init_docsUrls();
59578
+ init_mailboxName();
59512
59579
  init_mimeParse();
59513
59580
  init_headers();
59514
59581
  init_auditLog();
@@ -59554,6 +59621,7 @@ var init_imapClient = __esm({
59554
59621
  junk: "\\junk",
59555
59622
  starred: "\\flagged"
59556
59623
  };
59624
+ NOT_DELETED = { deleted: false };
59557
59625
  poolConnect = defaultConnect;
59558
59626
  pools = /* @__PURE__ */ new Map();
59559
59627
  connecting = /* @__PURE__ */ new Map();
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,21 +65633,26 @@ 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
  }
65635
65654
  function buildCriteria(a, listMode) {
65636
- const c = {};
65655
+ const c = { ...NOT_DELETED };
65637
65656
  if (a.query) c.or = [{ subject: a.query }, { from: a.query }];
65638
65657
  if (a.body) c.body = a.body;
65639
65658
  if (a.from) c.from = a.from;
@@ -65645,7 +65664,6 @@ function buildCriteria(a, listMode) {
65645
65664
  if (a.isFlagged === false) c.unflagged = true;
65646
65665
  if (a.dateFrom) c.since = new Date(a.dateFrom);
65647
65666
  if (a.dateTo) c.before = new Date(a.dateTo);
65648
- if (Object.keys(c).length === 0) c.all = true;
65649
65667
  return c;
65650
65668
  }
65651
65669
  function validDate(d) {
@@ -65721,7 +65739,7 @@ function structuredRow(m, account, path) {
65721
65739
  };
65722
65740
  }
65723
65741
  function describeMailboxFailure(error2) {
65724
- const raw = error2 instanceof Error ? error2.message : String(error2);
65742
+ const raw = errText(error2);
65725
65743
  const oneLine = raw.split("\n")[0].trim();
65726
65744
  const looksSensitive = /pass(word)?\s*[:=]|authorization:\s*\S|bearer\s+\S{10,}/i.test(oneLine);
65727
65745
  const safe = looksSensitive ? "IMAP error (detail redacted \u2014 response text looked like it might contain a credential)" : oneLine || "unknown error";
@@ -65939,7 +65957,10 @@ function imapMailStats(deps = {}) {
65939
65957
  try {
65940
65958
  const lock = await client.getMailboxLock("INBOX");
65941
65959
  try {
65942
- const found = await client.search({ since: since(days) }, { uid: true });
65960
+ const found = await client.search(
65961
+ { since: since(days), ...NOT_DELETED },
65962
+ { uid: true }
65963
+ );
65943
65964
  return Array.isArray(found) ? found.length : 0;
65944
65965
  } finally {
65945
65966
  lock.release();
@@ -65959,7 +65980,13 @@ function imapMailStats(deps = {}) {
65959
65980
  );
65960
65981
  }
65961
65982
  function errText(e) {
65962
- return e instanceof Error ? e.message : String(e);
65983
+ if (!(e instanceof Error)) return String(e);
65984
+ const x = e;
65985
+ if (typeof x.responseStatus !== "string" || !x.responseStatus) return e.message;
65986
+ const code = typeof x.serverResponseCode === "string" && x.serverResponseCode;
65987
+ const text = typeof x.responseText === "string" ? x.responseText.trim() : "";
65988
+ const server2 = [x.responseStatus, code ? `[${code}]` : "", text].filter(Boolean).join(" ");
65989
+ return e.message && e.message !== "Command failed" ? `${e.message}: ${server2}` : server2;
65963
65990
  }
65964
65991
  function assertMutated(result, what) {
65965
65992
  if (!result) throw new Error(`${what}: server rejected the command (IMAP NO/BAD)`);
@@ -66111,20 +66138,30 @@ function withClient(deps, fn) {
66111
66138
  return useClient(deps, fn);
66112
66139
  }
66113
66140
  async function resolveMailbox(client, name) {
66114
- const wanted = name.trim().toLowerCase();
66115
66141
  const boxes = await client.list();
66116
- const byPath = boxes.find((b) => b.path.toLowerCase() === wanted);
66117
- if (byPath) return { kind: "found", path: byPath.path };
66118
- const byName = boxes.filter((b) => b.name.toLowerCase() === wanted);
66119
- if (byName.length === 1) return { kind: "found", path: byName[0].path };
66120
- if (byName.length > 1) {
66121
- return { kind: "ambiguous", candidates: byName.map((b) => b.path).sort() };
66142
+ return matchMailbox(boxes, name);
66143
+ }
66144
+ function matchMailbox(boxes, name) {
66145
+ const exact = boxes.find((b) => b.path === name || b.path === name.trim());
66146
+ if (exact) return { kind: "found", path: exact.path };
66147
+ const wanted = mailboxNameKey(name);
66148
+ const tiers = [
66149
+ boxes.filter((b) => mailboxNameKey(b.path) === wanted),
66150
+ boxes.filter((b) => mailboxNameKey(b.name) === wanted)
66151
+ ];
66152
+ for (const hits of tiers) {
66153
+ const paths = [...new Set(hits.map((b) => b.path))];
66154
+ if (paths.length === 1) return { kind: "found", path: paths[0] };
66155
+ if (paths.length > 1) return { kind: "ambiguous", candidates: paths.sort() };
66122
66156
  }
66123
66157
  return { kind: "none" };
66124
66158
  }
66125
66159
  function ambiguousMailboxError(name, candidates, accountLabel) {
66126
66160
  const where = accountLabel ? ` on IMAP account ${accountLabel}` : "";
66127
- return `Mailbox "${name}" is ambiguous${where} \u2014 it matches ${candidates.map((c) => `"${c}"`).join(" and ")}. Pass the full path.`;
66161
+ const listed = candidates.map((c) => `"${c}"`).join(" and ");
66162
+ const indistinguishable = new Set(candidates.map(mailboxNameKey)).size === 1;
66163
+ 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.";
66164
+ return `Mailbox "${name}" is ambiguous${where} \u2014 it matches ${listed}.${hint}`;
66128
66165
  }
66129
66166
  async function findMailboxPathOrThrow(client, name) {
66130
66167
  const res = await resolveMailbox(client, name);
@@ -66133,6 +66170,15 @@ async function findMailboxPathOrThrow(client, name) {
66133
66170
  }
66134
66171
  function imapCreateMailbox(name, deps = {}) {
66135
66172
  return withClient(deps, async (client) => {
66173
+ let twin;
66174
+ try {
66175
+ const wanted = nfc(name);
66176
+ twin = (await client.list()).find((b) => nfc(b.path) === wanted)?.path;
66177
+ } catch {
66178
+ }
66179
+ if (twin !== void 0) {
66180
+ return { success: true, info: `Mailbox "${twin}" already existed.` };
66181
+ }
66136
66182
  try {
66137
66183
  const res = await client.mailboxCreate(name);
66138
66184
  return res.created ? { success: true, info: `Created mailbox "${res.path}".` } : { success: true, info: `Mailbox "${res.path}" already existed.` };
@@ -66184,6 +66230,14 @@ function imapRenameMailbox(oldName, newName, deps = {}) {
66184
66230
  };
66185
66231
  }
66186
66232
  const path = found.path;
66233
+ const wantedNew = nfc(newName);
66234
+ const clash = (await client.list()).find((b) => b.path !== path && nfc(b.path) === wantedNew);
66235
+ if (clash) {
66236
+ return {
66237
+ success: false,
66238
+ 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).`
66239
+ };
66240
+ }
66187
66241
  try {
66188
66242
  const res = await client.mailboxRename(path, newName);
66189
66243
  return { success: true, info: `Renamed "${res.path}" to "${res.newPath}" via IMAP.` };
@@ -66775,11 +66829,23 @@ async function imapThread(id, deps = {}, limit = 50) {
66775
66829
  if (Array.isArray(found)) found.forEach((u) => uidSet.add(u));
66776
66830
  };
66777
66831
  if (seedMsgId) {
66778
- addFound(await client.search({ header: { references: seedMsgId } }, { uid: true }));
66779
- addFound(await client.search({ header: { "in-reply-to": seedMsgId } }, { uid: true }));
66832
+ addFound(
66833
+ await client.search(
66834
+ { header: { references: seedMsgId }, ...NOT_DELETED },
66835
+ { uid: true }
66836
+ )
66837
+ );
66838
+ addFound(
66839
+ await client.search(
66840
+ { header: { "in-reply-to": seedMsgId }, ...NOT_DELETED },
66841
+ { uid: true }
66842
+ )
66843
+ );
66780
66844
  }
66781
66845
  for (const mid of [...refIds].slice(0, 20)) {
66782
- addFound(await client.search({ header: { "message-id": mid } }, { uid: true }));
66846
+ addFound(
66847
+ await client.search({ header: { "message-id": mid }, ...NOT_DELETED }, { uid: true })
66848
+ );
66783
66849
  }
66784
66850
  if (uidSet.size <= 1) return null;
66785
66851
  const uids = [...uidSet].slice(0, limit);
@@ -66831,13 +66897,14 @@ async function imapThread(id, deps = {}, limit = 50) {
66831
66897
  true
66832
66898
  );
66833
66899
  }
66834
- var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
66900
+ var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
66835
66901
  var init_imapClient = __esm({
66836
66902
  "src/services/imapClient.ts"() {
66837
66903
  "use strict";
66838
66904
  import_imapflow = __toESM(require_imap_flow(), 1);
66839
66905
  init_smtpMailer();
66840
66906
  init_docsUrls();
66907
+ init_mailboxName();
66841
66908
  init_mimeParse();
66842
66909
  init_headers();
66843
66910
  init_auditLog();
@@ -66883,6 +66950,7 @@ var init_imapClient = __esm({
66883
66950
  junk: "\\junk",
66884
66951
  starred: "\\flagged"
66885
66952
  };
66953
+ NOT_DELETED = { deleted: false };
66886
66954
  poolConnect = defaultConnect;
66887
66955
  pools = /* @__PURE__ */ new Map();
66888
66956
  connecting = /* @__PURE__ */ new Map();
@@ -81931,6 +81999,7 @@ function executeAppleScript(script, options = {}) {
81931
81999
  init_docsUrls();
81932
82000
  init_mimeParse();
81933
82001
  init_headers();
82002
+ init_mailboxName();
81934
82003
 
81935
82004
  // src/services/templateStore.ts
81936
82005
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -82566,23 +82635,28 @@ function mailboxLeaf(path) {
82566
82635
  function resolveAppleMailboxPath(mailbox, actualPaths) {
82567
82636
  if (actualPaths.length === 0) return mailbox;
82568
82637
  const candidates = [mailbox, ...MAILBOX_ALIASES[mailbox.toLowerCase()] ?? []];
82638
+ const ambiguous = (matches) => {
82639
+ const sorted = [...new Set(matches)].sort();
82640
+ const paths = sorted.map((path) => `"${path}"`).join(" and ");
82641
+ const twins = new Set(sorted.map(mailboxNameKey)).size === 1;
82642
+ 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.";
82643
+ return new Error(`Mailbox "${mailbox}" is ambiguous \u2014 it matches ${paths}.${hint}`);
82644
+ };
82569
82645
  for (const candidate of candidates) {
82570
82646
  const exact = actualPaths.find((path) => path === candidate);
82571
82647
  if (exact) return exact;
82572
- const folded = actualPaths.find((path) => path.toLowerCase() === candidate.toLowerCase());
82573
- if (folded) return folded;
82648
+ const key = mailboxNameKey(candidate);
82649
+ const folded = [...new Set(actualPaths.filter((path) => mailboxNameKey(path) === key))];
82650
+ if (folded.length === 1) return folded[0];
82651
+ if (folded.length > 1) throw ambiguous(folded);
82574
82652
  }
82575
82653
  for (const candidate of candidates) {
82576
- const leafMatches = actualPaths.filter(
82577
- (path) => mailboxLeaf(path).toLowerCase() === candidate.toLowerCase()
82578
- );
82654
+ const key = mailboxNameKey(candidate);
82655
+ const leafMatches = [
82656
+ ...new Set(actualPaths.filter((path) => mailboxNameKey(mailboxLeaf(path)) === key))
82657
+ ];
82579
82658
  if (leafMatches.length === 1) return leafMatches[0];
82580
- if (leafMatches.length > 1) {
82581
- const paths = [...leafMatches].sort().map((path) => `"${path}"`).join(" and ");
82582
- throw new Error(
82583
- `Mailbox "${mailbox}" is ambiguous \u2014 it matches ${paths}. Pass the full path.`
82584
- );
82585
- }
82659
+ if (leafMatches.length > 1) throw ambiguous(leafMatches);
82586
82660
  }
82587
82661
  return mailbox;
82588
82662
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.19.14",
3
+ "version": "2.19.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",