apple-mail-mcp 2.19.14 → 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
  }
@@ -58392,7 +58411,7 @@ function structuredRow(m, account, path) {
58392
58411
  };
58393
58412
  }
58394
58413
  function describeMailboxFailure(error) {
58395
- const raw = error instanceof Error ? error.message : String(error);
58414
+ const raw = errText(error);
58396
58415
  const oneLine = raw.split("\n")[0].trim();
58397
58416
  const looksSensitive = /pass(word)?\s*[:=]|authorization:\s*\S|bearer\s+\S{10,}/i.test(oneLine);
58398
58417
  const safe = looksSensitive ? "IMAP error (detail redacted \u2014 response text looked like it might contain a credential)" : oneLine || "unknown error";
@@ -58630,7 +58649,13 @@ function imapMailStats(deps = {}) {
58630
58649
  );
58631
58650
  }
58632
58651
  function errText(e) {
58633
- 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;
58634
58659
  }
58635
58660
  function assertMutated(result, what) {
58636
58661
  if (!result) throw new Error(`${what}: server rejected the command (IMAP NO/BAD)`);
@@ -58782,20 +58807,30 @@ function withClient(deps, fn) {
58782
58807
  return useClient(deps, fn);
58783
58808
  }
58784
58809
  async function resolveMailbox(client, name) {
58785
- const wanted = name.trim().toLowerCase();
58786
58810
  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() };
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() };
58793
58825
  }
58794
58826
  return { kind: "none" };
58795
58827
  }
58796
58828
  function ambiguousMailboxError(name, candidates, accountLabel) {
58797
58829
  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.`;
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}`;
58799
58834
  }
58800
58835
  async function findMailboxPathOrThrow(client, name) {
58801
58836
  const res = await resolveMailbox(client, name);
@@ -58804,6 +58839,15 @@ async function findMailboxPathOrThrow(client, name) {
58804
58839
  }
58805
58840
  function imapCreateMailbox(name, deps = {}) {
58806
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
+ }
58807
58851
  try {
58808
58852
  const res = await client.mailboxCreate(name);
58809
58853
  return res.created ? { success: true, info: `Created mailbox "${res.path}".` } : { success: true, info: `Mailbox "${res.path}" already existed.` };
@@ -58855,6 +58899,14 @@ function imapRenameMailbox(oldName, newName, deps = {}) {
58855
58899
  };
58856
58900
  }
58857
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
+ }
58858
58910
  try {
58859
58911
  const res = await client.mailboxRename(path, newName);
58860
58912
  return { success: true, info: `Renamed "${res.path}" to "${res.newPath}" via IMAP.` };
@@ -59509,6 +59561,7 @@ var init_imapClient = __esm({
59509
59561
  import_imapflow = __toESM(require_imap_flow(), 1);
59510
59562
  init_smtpMailer();
59511
59563
  init_docsUrls();
59564
+ init_mailboxName();
59512
59565
  init_mimeParse();
59513
59566
  init_headers();
59514
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
  }
@@ -65721,7 +65740,7 @@ function structuredRow(m, account, path) {
65721
65740
  };
65722
65741
  }
65723
65742
  function describeMailboxFailure(error2) {
65724
- const raw = error2 instanceof Error ? error2.message : String(error2);
65743
+ const raw = errText(error2);
65725
65744
  const oneLine = raw.split("\n")[0].trim();
65726
65745
  const looksSensitive = /pass(word)?\s*[:=]|authorization:\s*\S|bearer\s+\S{10,}/i.test(oneLine);
65727
65746
  const safe = looksSensitive ? "IMAP error (detail redacted \u2014 response text looked like it might contain a credential)" : oneLine || "unknown error";
@@ -65959,7 +65978,13 @@ function imapMailStats(deps = {}) {
65959
65978
  );
65960
65979
  }
65961
65980
  function errText(e) {
65962
- 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;
65963
65988
  }
65964
65989
  function assertMutated(result, what) {
65965
65990
  if (!result) throw new Error(`${what}: server rejected the command (IMAP NO/BAD)`);
@@ -66111,20 +66136,30 @@ function withClient(deps, fn) {
66111
66136
  return useClient(deps, fn);
66112
66137
  }
66113
66138
  async function resolveMailbox(client, name) {
66114
- const wanted = name.trim().toLowerCase();
66115
66139
  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() };
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() };
66122
66154
  }
66123
66155
  return { kind: "none" };
66124
66156
  }
66125
66157
  function ambiguousMailboxError(name, candidates, accountLabel) {
66126
66158
  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.`;
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}`;
66128
66163
  }
66129
66164
  async function findMailboxPathOrThrow(client, name) {
66130
66165
  const res = await resolveMailbox(client, name);
@@ -66133,6 +66168,15 @@ async function findMailboxPathOrThrow(client, name) {
66133
66168
  }
66134
66169
  function imapCreateMailbox(name, deps = {}) {
66135
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
+ }
66136
66180
  try {
66137
66181
  const res = await client.mailboxCreate(name);
66138
66182
  return res.created ? { success: true, info: `Created mailbox "${res.path}".` } : { success: true, info: `Mailbox "${res.path}" already existed.` };
@@ -66184,6 +66228,14 @@ function imapRenameMailbox(oldName, newName, deps = {}) {
66184
66228
  };
66185
66229
  }
66186
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
+ }
66187
66239
  try {
66188
66240
  const res = await client.mailboxRename(path, newName);
66189
66241
  return { success: true, info: `Renamed "${res.path}" to "${res.newPath}" via IMAP.` };
@@ -66838,6 +66890,7 @@ var init_imapClient = __esm({
66838
66890
  import_imapflow = __toESM(require_imap_flow(), 1);
66839
66891
  init_smtpMailer();
66840
66892
  init_docsUrls();
66893
+ init_mailboxName();
66841
66894
  init_mimeParse();
66842
66895
  init_headers();
66843
66896
  init_auditLog();
@@ -81931,6 +81984,7 @@ function executeAppleScript(script, options = {}) {
81931
81984
  init_docsUrls();
81932
81985
  init_mimeParse();
81933
81986
  init_headers();
81987
+ init_mailboxName();
81934
81988
 
81935
81989
  // src/services/templateStore.ts
81936
81990
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -82566,23 +82620,28 @@ function mailboxLeaf(path) {
82566
82620
  function resolveAppleMailboxPath(mailbox, actualPaths) {
82567
82621
  if (actualPaths.length === 0) return mailbox;
82568
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
+ };
82569
82630
  for (const candidate of candidates) {
82570
82631
  const exact = actualPaths.find((path) => path === candidate);
82571
82632
  if (exact) return exact;
82572
- const folded = actualPaths.find((path) => path.toLowerCase() === candidate.toLowerCase());
82573
- 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);
82574
82637
  }
82575
82638
  for (const candidate of candidates) {
82576
- const leafMatches = actualPaths.filter(
82577
- (path) => mailboxLeaf(path).toLowerCase() === candidate.toLowerCase()
82578
- );
82639
+ const key = mailboxNameKey(candidate);
82640
+ const leafMatches = [
82641
+ ...new Set(actualPaths.filter((path) => mailboxNameKey(mailboxLeaf(path)) === key))
82642
+ ];
82579
82643
  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
- }
82644
+ if (leafMatches.length > 1) throw ambiguous(leafMatches);
82586
82645
  }
82587
82646
  return mailbox;
82588
82647
  }
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.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",