apple-mail-mcp 2.20.21 → 2.20.23

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/build/cli.js CHANGED
@@ -12265,44 +12265,77 @@ function sensitiveRoots() {
12265
12265
  }
12266
12266
  });
12267
12267
  }
12268
+ function homeCanonical() {
12269
+ try {
12270
+ return canonicalize(homedir());
12271
+ } catch {
12272
+ return void 0;
12273
+ }
12274
+ }
12268
12275
  function isWithinRoot(candidate, root) {
12269
12276
  return candidate === root || candidate.startsWith(root + sep);
12270
12277
  }
12271
12278
  function hasHiddenPathSegment(candidate) {
12272
12279
  return candidate.split(sep).some((segment) => segment.startsWith(".") && segment.length > 1);
12273
12280
  }
12274
- function isProtectedPath(candidate) {
12281
+ function userLibraryTail(candidate) {
12282
+ const segments = candidate.split(sep).filter(Boolean);
12283
+ const lower = segments.map((segment) => segment.toLowerCase());
12284
+ const volumeTops = [
12285
+ 0,
12286
+ lower[0] === "volumes" ? 2 : -1,
12287
+ lower.slice(0, 3).join("/") === "system/volumes/data" ? 3 : -1
12288
+ ];
12289
+ for (const i of volumeTops) {
12290
+ if (i >= 0 && lower[i] === "users" && lower[i + 2] === "library") return lower.slice(i + 3);
12291
+ }
12292
+ const home = homeCanonical() ?? resolve3(homedir());
12293
+ if (home === sep || !isWithinRoot(candidate, home)) return void 0;
12294
+ const relative = candidate.slice(home.length).split(sep).filter(Boolean);
12295
+ if (relative[0]?.toLowerCase() !== "library") return void 0;
12296
+ return relative.slice(1).map((segment) => segment.toLowerCase());
12297
+ }
12298
+ function isLibraryDocument(tail) {
12299
+ return LIBRARY_DOCUMENT_FOLDERS.some((folder) => folder.every((name2, i) => tail[i] === name2));
12300
+ }
12301
+ function isProtectedPath(candidate, exemptRoots = []) {
12275
12302
  if (hasHiddenPathSegment(candidate)) return true;
12276
12303
  if (sensitiveRoots().some((root) => isWithinRoot(candidate, root))) return true;
12277
- let home;
12304
+ const tail = userLibraryTail(candidate);
12305
+ if (!tail || isLibraryDocument(tail)) return false;
12306
+ const home = homeCanonical();
12307
+ return !exemptRoots.some(
12308
+ (root) => isWithinRoot(candidate, root) && !(home && isWithinRoot(home, root))
12309
+ );
12310
+ }
12311
+ function canonicalTmpdir() {
12278
12312
  try {
12279
- home = canonicalize(homedir());
12313
+ return [canonicalize(tmpdir())];
12280
12314
  } catch {
12281
- home = resolve3(homedir());
12315
+ return [];
12282
12316
  }
12283
- if (!isWithinRoot(candidate, home)) return false;
12284
- const relative = candidate.slice(home.length).split(sep).filter(Boolean);
12285
- return relative.length >= 4 && relative[0].toLowerCase() === "library" && relative[1].toLowerCase() === "application support" && relative.at(-1)?.toLowerCase() === "config.json";
12286
12317
  }
12287
- function configuredRoots(env) {
12288
- const raw = env[ATTACHMENT_READ_ROOTS_ENV];
12289
- const extraRoots = raw === void 0 ? [] : raw.split(delimiter2).map((root) => root.trim()).filter(Boolean);
12290
- const requested = [...DEFAULT_ATTACHMENT_READ_ROOTS, ...extraRoots];
12291
- for (const root of requested) {
12292
- if (!isAbsolute(root)) {
12293
- throw new Error(`${ATTACHMENT_READ_ROOTS_ENV} entries must be absolute paths.`);
12294
- }
12295
- }
12318
+ function canonicalRoots(requested) {
12296
12319
  const resolved = [];
12297
12320
  for (const root of requested) {
12298
12321
  try {
12299
12322
  const canonical = canonicalize(resolve3(root));
12300
- if (!resolved.includes(canonical)) resolved.push(canonical);
12323
+ if (canonical !== sep && !resolved.includes(canonical)) resolved.push(canonical);
12301
12324
  } catch {
12302
12325
  }
12303
12326
  }
12304
12327
  return resolved;
12305
12328
  }
12329
+ function explicitRoots(env) {
12330
+ const raw = env[ATTACHMENT_READ_ROOTS_ENV];
12331
+ const extraRoots = raw === void 0 ? [] : raw.split(delimiter2).map((root) => root.trim()).filter(Boolean);
12332
+ for (const root of extraRoots) {
12333
+ if (!isAbsolute(root)) {
12334
+ throw new Error(`${ATTACHMENT_READ_ROOTS_ENV} entries must be absolute paths.`);
12335
+ }
12336
+ }
12337
+ return canonicalRoots(extraRoots);
12338
+ }
12306
12339
  function resolveAttachmentReadPath(filePath, env = process.env) {
12307
12340
  if (!isAbsolute(filePath)) {
12308
12341
  throw new Error(`Attachment path must be absolute: "${filePath}"`);
@@ -12321,19 +12354,22 @@ function resolveAttachmentReadPath(filePath, env = process.env) {
12321
12354
  if (error2 instanceof Error && error2.message.includes("not a regular file")) throw error2;
12322
12355
  throw new Error(`Attachment file not found: "${filePath}"`);
12323
12356
  }
12324
- if (isProtectedPath(canonical)) {
12357
+ const explicit = explicitRoots(env);
12358
+ const exempt = [...explicit, ...canonicalTmpdir()];
12359
+ if (isProtectedPath(canonical, exempt) || isProtectedPath(resolve3(filePath), exempt)) {
12325
12360
  throw new Error(
12326
- `Attachment path is in a protected location: "${filePath}". Hidden files and credential/configuration locations cannot be sent as attachments.`
12361
+ `Attachment path is in a protected location: "${filePath}". Hidden files, credential locations and user Library folders (except iCloud Drive and CloudStorage) cannot be sent as attachments unless ${ATTACHMENT_READ_ROOTS_ENV} names a root for them.`
12327
12362
  );
12328
12363
  }
12329
- if (!configuredRoots(env).some((root) => isWithinRoot(canonical, root))) {
12364
+ const roots = [...canonicalRoots(DEFAULT_ATTACHMENT_READ_ROOTS), ...explicit];
12365
+ if (!roots.some((root) => isWithinRoot(canonical, root))) {
12330
12366
  throw new Error(
12331
12367
  `Attachment path is outside the allowed read roots: "${filePath}". Use an ordinary home-directory, /Volumes, or temporary path, or configure ${ATTACHMENT_READ_ROOTS_ENV} for an additional explicit root.`
12332
12368
  );
12333
12369
  }
12334
12370
  return canonical;
12335
12371
  }
12336
- var ATTACHMENT_READ_ROOTS_ENV, DEFAULT_ATTACHMENT_READ_ROOTS, SENSITIVE_HOME_ROOTS;
12372
+ var ATTACHMENT_READ_ROOTS_ENV, DEFAULT_ATTACHMENT_READ_ROOTS, SENSITIVE_HOME_ROOTS, LIBRARY_DOCUMENT_FOLDERS;
12337
12373
  var init_attachmentReadPolicy = __esm({
12338
12374
  "src/utils/attachmentReadPolicy.ts"() {
12339
12375
  "use strict";
@@ -12345,6 +12381,7 @@ var init_attachmentReadPolicy = __esm({
12345
12381
  join(homedir(), ".config", "gh"),
12346
12382
  join(homedir(), "Library", "Keychains")
12347
12383
  ];
12384
+ LIBRARY_DOCUMENT_FOLDERS = [["mobile documents", "com~apple~clouddocs"], ["cloudstorage"]];
12348
12385
  }
12349
12386
  });
12350
12387
 
package/build/index.js CHANGED
@@ -64830,44 +64830,77 @@ function sensitiveRoots() {
64830
64830
  }
64831
64831
  });
64832
64832
  }
64833
+ function homeCanonical() {
64834
+ try {
64835
+ return canonicalize(homedir());
64836
+ } catch {
64837
+ return void 0;
64838
+ }
64839
+ }
64833
64840
  function isWithinRoot(candidate, root) {
64834
64841
  return candidate === root || candidate.startsWith(root + sep);
64835
64842
  }
64836
64843
  function hasHiddenPathSegment(candidate) {
64837
64844
  return candidate.split(sep).some((segment) => segment.startsWith(".") && segment.length > 1);
64838
64845
  }
64839
- function isProtectedPath(candidate) {
64846
+ function userLibraryTail(candidate) {
64847
+ const segments = candidate.split(sep).filter(Boolean);
64848
+ const lower = segments.map((segment) => segment.toLowerCase());
64849
+ const volumeTops = [
64850
+ 0,
64851
+ lower[0] === "volumes" ? 2 : -1,
64852
+ lower.slice(0, 3).join("/") === "system/volumes/data" ? 3 : -1
64853
+ ];
64854
+ for (const i of volumeTops) {
64855
+ if (i >= 0 && lower[i] === "users" && lower[i + 2] === "library") return lower.slice(i + 3);
64856
+ }
64857
+ const home = homeCanonical() ?? resolve3(homedir());
64858
+ if (home === sep || !isWithinRoot(candidate, home)) return void 0;
64859
+ const relative = candidate.slice(home.length).split(sep).filter(Boolean);
64860
+ if (relative[0]?.toLowerCase() !== "library") return void 0;
64861
+ return relative.slice(1).map((segment) => segment.toLowerCase());
64862
+ }
64863
+ function isLibraryDocument(tail) {
64864
+ return LIBRARY_DOCUMENT_FOLDERS.some((folder) => folder.every((name2, i) => tail[i] === name2));
64865
+ }
64866
+ function isProtectedPath(candidate, exemptRoots = []) {
64840
64867
  if (hasHiddenPathSegment(candidate)) return true;
64841
64868
  if (sensitiveRoots().some((root) => isWithinRoot(candidate, root))) return true;
64842
- let home;
64869
+ const tail = userLibraryTail(candidate);
64870
+ if (!tail || isLibraryDocument(tail)) return false;
64871
+ const home = homeCanonical();
64872
+ return !exemptRoots.some(
64873
+ (root) => isWithinRoot(candidate, root) && !(home && isWithinRoot(home, root))
64874
+ );
64875
+ }
64876
+ function canonicalTmpdir() {
64843
64877
  try {
64844
- home = canonicalize(homedir());
64878
+ return [canonicalize(tmpdir())];
64845
64879
  } catch {
64846
- home = resolve3(homedir());
64880
+ return [];
64847
64881
  }
64848
- if (!isWithinRoot(candidate, home)) return false;
64849
- const relative = candidate.slice(home.length).split(sep).filter(Boolean);
64850
- return relative.length >= 4 && relative[0].toLowerCase() === "library" && relative[1].toLowerCase() === "application support" && relative.at(-1)?.toLowerCase() === "config.json";
64851
64882
  }
64852
- function configuredRoots(env) {
64853
- const raw = env[ATTACHMENT_READ_ROOTS_ENV];
64854
- const extraRoots = raw === void 0 ? [] : raw.split(delimiter2).map((root) => root.trim()).filter(Boolean);
64855
- const requested = [...DEFAULT_ATTACHMENT_READ_ROOTS, ...extraRoots];
64856
- for (const root of requested) {
64857
- if (!isAbsolute(root)) {
64858
- throw new Error(`${ATTACHMENT_READ_ROOTS_ENV} entries must be absolute paths.`);
64859
- }
64860
- }
64883
+ function canonicalRoots(requested) {
64861
64884
  const resolved = [];
64862
64885
  for (const root of requested) {
64863
64886
  try {
64864
64887
  const canonical = canonicalize(resolve3(root));
64865
- if (!resolved.includes(canonical)) resolved.push(canonical);
64888
+ if (canonical !== sep && !resolved.includes(canonical)) resolved.push(canonical);
64866
64889
  } catch {
64867
64890
  }
64868
64891
  }
64869
64892
  return resolved;
64870
64893
  }
64894
+ function explicitRoots(env) {
64895
+ const raw = env[ATTACHMENT_READ_ROOTS_ENV];
64896
+ const extraRoots = raw === void 0 ? [] : raw.split(delimiter2).map((root) => root.trim()).filter(Boolean);
64897
+ for (const root of extraRoots) {
64898
+ if (!isAbsolute(root)) {
64899
+ throw new Error(`${ATTACHMENT_READ_ROOTS_ENV} entries must be absolute paths.`);
64900
+ }
64901
+ }
64902
+ return canonicalRoots(extraRoots);
64903
+ }
64871
64904
  function resolveAttachmentReadPath(filePath, env = process.env) {
64872
64905
  if (!isAbsolute(filePath)) {
64873
64906
  throw new Error(`Attachment path must be absolute: "${filePath}"`);
@@ -64886,19 +64919,22 @@ function resolveAttachmentReadPath(filePath, env = process.env) {
64886
64919
  if (error3 instanceof Error && error3.message.includes("not a regular file")) throw error3;
64887
64920
  throw new Error(`Attachment file not found: "${filePath}"`);
64888
64921
  }
64889
- if (isProtectedPath(canonical)) {
64922
+ const explicit = explicitRoots(env);
64923
+ const exempt = [...explicit, ...canonicalTmpdir()];
64924
+ if (isProtectedPath(canonical, exempt) || isProtectedPath(resolve3(filePath), exempt)) {
64890
64925
  throw new Error(
64891
- `Attachment path is in a protected location: "${filePath}". Hidden files and credential/configuration locations cannot be sent as attachments.`
64926
+ `Attachment path is in a protected location: "${filePath}". Hidden files, credential locations and user Library folders (except iCloud Drive and CloudStorage) cannot be sent as attachments unless ${ATTACHMENT_READ_ROOTS_ENV} names a root for them.`
64892
64927
  );
64893
64928
  }
64894
- if (!configuredRoots(env).some((root) => isWithinRoot(canonical, root))) {
64929
+ const roots = [...canonicalRoots(DEFAULT_ATTACHMENT_READ_ROOTS), ...explicit];
64930
+ if (!roots.some((root) => isWithinRoot(canonical, root))) {
64895
64931
  throw new Error(
64896
64932
  `Attachment path is outside the allowed read roots: "${filePath}". Use an ordinary home-directory, /Volumes, or temporary path, or configure ${ATTACHMENT_READ_ROOTS_ENV} for an additional explicit root.`
64897
64933
  );
64898
64934
  }
64899
64935
  return canonical;
64900
64936
  }
64901
- var ATTACHMENT_READ_ROOTS_ENV, DEFAULT_ATTACHMENT_READ_ROOTS, SENSITIVE_HOME_ROOTS;
64937
+ var ATTACHMENT_READ_ROOTS_ENV, DEFAULT_ATTACHMENT_READ_ROOTS, SENSITIVE_HOME_ROOTS, LIBRARY_DOCUMENT_FOLDERS;
64902
64938
  var init_attachmentReadPolicy = __esm({
64903
64939
  "src/utils/attachmentReadPolicy.ts"() {
64904
64940
  "use strict";
@@ -64910,6 +64946,7 @@ var init_attachmentReadPolicy = __esm({
64910
64946
  join(homedir(), ".config", "gh"),
64911
64947
  join(homedir(), "Library", "Keychains")
64912
64948
  ];
64949
+ LIBRARY_DOCUMENT_FOLDERS = [["mobile documents", "com~apple~clouddocs"], ["cloudstorage"]];
64913
64950
  }
64914
64951
  });
64915
64952
 
@@ -83781,19 +83818,32 @@ function mailboxPathFragment(mailboxVar, outputVar) {
83781
83818
  set _pathParent to _pathNext
83782
83819
  end repeat`;
83783
83820
  }
83784
- function mailboxLookupFragment(collExpr, path3, outputVar) {
83785
- return `
83786
- set ${outputVar} to missing value
83821
+ function mailboxLookupFragment(collExpr, path3, outputVar, hintIndex) {
83822
+ const safePath = escapeForAppleScript(path3);
83823
+ const hint = hintIndex === void 0 ? "" : `
83824
+ try
83825
+ set _mbc to item ${hintIndex} of (${collExpr})
83826
+ set _mbcPath to ""
83827
+ ${mailboxPathFragment("_mbc", "_mbcPath")}
83828
+ ignoring case
83829
+ if _mbcPath is "${safePath}" then set ${outputVar} to _mbc
83830
+ end ignoring
83831
+ end try`;
83832
+ const walk = `
83787
83833
  repeat with _mbc in (${collExpr})
83788
83834
  set _mbcPath to ""
83789
83835
  ${mailboxPathFragment("_mbc", "_mbcPath")}
83790
83836
  ignoring case
83791
- if _mbcPath is "${escapeForAppleScript(path3)}" then
83837
+ if _mbcPath is "${safePath}" then
83792
83838
  set ${outputVar} to _mbc
83793
83839
  exit repeat
83794
83840
  end if
83795
83841
  end ignoring
83796
83842
  end repeat`;
83843
+ return `
83844
+ set ${outputVar} to missing value${hint}${hintIndex === void 0 ? walk : `
83845
+ if ${outputVar} is missing value then${walk}
83846
+ end if`}`;
83797
83847
  }
83798
83848
  var MAILBOX_ALIASES = {
83799
83849
  inbox: ["INBOX", "Inbox", "inbox"],
@@ -84789,6 +84839,17 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
84789
84839
  resolveMailbox(mailbox, account) {
84790
84840
  return resolveAppleMailboxPath(mailbox, this.getCachedMailboxNames(account));
84791
84841
  }
84842
+ /**
84843
+ * The 1-based position of `path` in the account's cached mailbox list, which is
84844
+ * built by walking the account's `mailboxes` in order. `mailboxLookupFragment`
84845
+ * checks that one mailbox first instead of working out the path of every one.
84846
+ * Undefined when the path is not in the list; a stale position is harmless, the
84847
+ * fragment verifies the path and falls back to the full walk.
84848
+ */
84849
+ mailboxIndexHint(account, path3) {
84850
+ const index = this.getCachedMailboxNames(account).indexOf(path3);
84851
+ return index >= 0 ? index + 1 : void 0;
84852
+ }
84792
84853
  /**
84793
84854
  * Non-throwing `resolveMailbox`, for callers comparing mailbox names AFTER
84794
84855
  * a destructive op already ran (the forensics `sameMailbox` closures). An
@@ -84955,7 +85016,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
84955
85016
  ${dateSetup}set outputText to ""
84956
85017
  set _timedOut to false
84957
85018
  set _notSearched to ""
84958
- ${mailboxLookupFragment(mbIter, targetMailbox, "theMailbox")}
85019
+ ${mailboxLookupFragment(mbIter, targetMailbox, "theMailbox", this.mailboxIndexHint(targetAccount, targetMailbox))}
84959
85020
  set msgCount to 0
84960
85021
  if theMailbox is missing value then
84961
85022
  set _timedOut to true
@@ -85687,7 +85748,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
85687
85748
  set outputText to ""
85688
85749
  set _timedOut to false
85689
85750
  set _notSearched to ""
85690
- ${mailboxLookupFragment(mbIter, targetMailbox, "theMailbox")}
85751
+ ${mailboxLookupFragment(mbIter, targetMailbox, "theMailbox", this.mailboxIndexHint(targetAccount, targetMailbox))}
85691
85752
  set msgCount to 0
85692
85753
  set skipped to 0
85693
85754
  if theMailbox is missing value then
@@ -87183,7 +87244,7 @@ ${this.errorEmit(" ")}
87183
87244
  const targetAccount = this.resolveAccount(account);
87184
87245
  const targetMailbox = this.resolveMailbox(mailbox || "INBOX", targetAccount);
87185
87246
  const command = `
87186
- ${mailboxLookupFragment("mailboxes", targetMailbox, "theMailbox")}
87247
+ ${mailboxLookupFragment("mailboxes", targetMailbox, "theMailbox", this.mailboxIndexHint(targetAccount, targetMailbox))}
87187
87248
  if theMailbox is missing value then error "Mailbox \\"${escapeForAppleScript(targetMailbox)}\\" not found"
87188
87249
  return unread count of theMailbox`;
87189
87250
  const script = buildAccountScopedScript(targetAccount, command);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.20.21",
3
+ "version": "2.20.23",
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",