apple-mail-mcp 2.8.5 → 2.8.7

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/index.js CHANGED
@@ -20707,7 +20707,7 @@ var require_thread_stream = __commonJS({
20707
20707
  var { version: version3 } = require_package2();
20708
20708
  var { EventEmitter } = __require("events");
20709
20709
  var { Worker } = __require("worker_threads");
20710
- var { join: join5 } = __require("path");
20710
+ var { join: join6 } = __require("path");
20711
20711
  var { pathToFileURL } = __require("url");
20712
20712
  var { wait } = require_wait();
20713
20713
  var {
@@ -20758,7 +20758,7 @@ var require_thread_stream = __commonJS({
20758
20758
  function createWorker(stream, opts) {
20759
20759
  const { filename, workerData } = opts;
20760
20760
  const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
20761
- const toExecute = bundlerOverrides["thread-stream-worker"] || join5(__dirname, "lib", "worker.js");
20761
+ const toExecute = bundlerOverrides["thread-stream-worker"] || join6(__dirname, "lib", "worker.js");
20762
20762
  const worker = new Worker(toExecute, {
20763
20763
  ...opts.workerOpts,
20764
20764
  name: opts.workerOpts?.name || "thread-stream",
@@ -21224,9 +21224,9 @@ var require_transport = __commonJS({
21224
21224
  "node_modules/.pnpm/pino@10.3.1/node_modules/pino/lib/transport.js"(exports, module) {
21225
21225
  "use strict";
21226
21226
  var { createRequire: createRequire2 } = __require("module");
21227
- var { existsSync: existsSync5 } = __require("node:fs");
21227
+ var { existsSync: existsSync6 } = __require("node:fs");
21228
21228
  var getCallers = require_caller();
21229
- var { join: join5, isAbsolute: isAbsolute3, sep: sep2 } = __require("node:path");
21229
+ var { join: join6, isAbsolute: isAbsolute3, sep: sep2 } = __require("node:path");
21230
21230
  var { fileURLToPath } = __require("node:url");
21231
21231
  var sleep2 = require_atomic_sleep();
21232
21232
  var onExit = require_on_exit_leak_free();
@@ -21298,7 +21298,7 @@ var require_transport = __commonJS({
21298
21298
  return false;
21299
21299
  }
21300
21300
  }
21301
- return isAbsolute3(path) && !existsSync5(path);
21301
+ return isAbsolute3(path) && !existsSync6(path);
21302
21302
  }
21303
21303
  function stripQuotes(value) {
21304
21304
  const first = value[0];
@@ -21379,7 +21379,7 @@ var require_transport = __commonJS({
21379
21379
  throw new Error("only one of target or targets can be specified");
21380
21380
  }
21381
21381
  if (targets) {
21382
- target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
21382
+ target = bundlerOverrides["pino-worker"] || join6(__dirname, "worker.js");
21383
21383
  options.targets = targets.filter((dest) => dest.target).map((dest) => {
21384
21384
  return {
21385
21385
  ...dest,
@@ -21397,7 +21397,7 @@ var require_transport = __commonJS({
21397
21397
  });
21398
21398
  });
21399
21399
  } else if (pipeline) {
21400
- target = bundlerOverrides["pino-worker"] || join5(__dirname, "worker.js");
21400
+ target = bundlerOverrides["pino-worker"] || join6(__dirname, "worker.js");
21401
21401
  options.pipelines = [pipeline.map((dest) => {
21402
21402
  return {
21403
21403
  ...dest,
@@ -21420,7 +21420,7 @@ var require_transport = __commonJS({
21420
21420
  return origin;
21421
21421
  }
21422
21422
  if (origin === "pino/file") {
21423
- return join5(__dirname, "..", "file.js");
21423
+ return join6(__dirname, "..", "file.js");
21424
21424
  }
21425
21425
  let fixTarget2;
21426
21426
  for (const filePath of callers) {
@@ -22400,7 +22400,7 @@ var require_safe_stable_stringify = __commonJS({
22400
22400
  return circularValue;
22401
22401
  }
22402
22402
  let res = "";
22403
- let join5 = ",";
22403
+ let join6 = ",";
22404
22404
  const originalIndentation = indentation;
22405
22405
  if (Array.isArray(value)) {
22406
22406
  if (value.length === 0) {
@@ -22414,7 +22414,7 @@ var require_safe_stable_stringify = __commonJS({
22414
22414
  indentation += spacer;
22415
22415
  res += `
22416
22416
  ${indentation}`;
22417
- join5 = `,
22417
+ join6 = `,
22418
22418
  ${indentation}`;
22419
22419
  }
22420
22420
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -22422,13 +22422,13 @@ ${indentation}`;
22422
22422
  for (; i < maximumValuesToStringify - 1; i++) {
22423
22423
  const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
22424
22424
  res += tmp2 !== void 0 ? tmp2 : "null";
22425
- res += join5;
22425
+ res += join6;
22426
22426
  }
22427
22427
  const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
22428
22428
  res += tmp !== void 0 ? tmp : "null";
22429
22429
  if (value.length - 1 > maximumBreadth) {
22430
22430
  const removedKeys = value.length - maximumBreadth - 1;
22431
- res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
22431
+ res += `${join6}"... ${getItemCount(removedKeys)} not stringified"`;
22432
22432
  }
22433
22433
  if (spacer !== "") {
22434
22434
  res += `
@@ -22449,7 +22449,7 @@ ${originalIndentation}`;
22449
22449
  let separator = "";
22450
22450
  if (spacer !== "") {
22451
22451
  indentation += spacer;
22452
- join5 = `,
22452
+ join6 = `,
22453
22453
  ${indentation}`;
22454
22454
  whitespace = " ";
22455
22455
  }
@@ -22463,13 +22463,13 @@ ${indentation}`;
22463
22463
  const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
22464
22464
  if (tmp !== void 0) {
22465
22465
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
22466
- separator = join5;
22466
+ separator = join6;
22467
22467
  }
22468
22468
  }
22469
22469
  if (keyLength > maximumBreadth) {
22470
22470
  const removedKeys = keyLength - maximumBreadth;
22471
22471
  res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
22472
- separator = join5;
22472
+ separator = join6;
22473
22473
  }
22474
22474
  if (spacer !== "" && separator.length > 1) {
22475
22475
  res = `
@@ -22510,7 +22510,7 @@ ${originalIndentation}`;
22510
22510
  }
22511
22511
  const originalIndentation = indentation;
22512
22512
  let res = "";
22513
- let join5 = ",";
22513
+ let join6 = ",";
22514
22514
  if (Array.isArray(value)) {
22515
22515
  if (value.length === 0) {
22516
22516
  return "[]";
@@ -22523,7 +22523,7 @@ ${originalIndentation}`;
22523
22523
  indentation += spacer;
22524
22524
  res += `
22525
22525
  ${indentation}`;
22526
- join5 = `,
22526
+ join6 = `,
22527
22527
  ${indentation}`;
22528
22528
  }
22529
22529
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
@@ -22531,13 +22531,13 @@ ${indentation}`;
22531
22531
  for (; i < maximumValuesToStringify - 1; i++) {
22532
22532
  const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
22533
22533
  res += tmp2 !== void 0 ? tmp2 : "null";
22534
- res += join5;
22534
+ res += join6;
22535
22535
  }
22536
22536
  const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
22537
22537
  res += tmp !== void 0 ? tmp : "null";
22538
22538
  if (value.length - 1 > maximumBreadth) {
22539
22539
  const removedKeys = value.length - maximumBreadth - 1;
22540
- res += `${join5}"... ${getItemCount(removedKeys)} not stringified"`;
22540
+ res += `${join6}"... ${getItemCount(removedKeys)} not stringified"`;
22541
22541
  }
22542
22542
  if (spacer !== "") {
22543
22543
  res += `
@@ -22550,7 +22550,7 @@ ${originalIndentation}`;
22550
22550
  let whitespace = "";
22551
22551
  if (spacer !== "") {
22552
22552
  indentation += spacer;
22553
- join5 = `,
22553
+ join6 = `,
22554
22554
  ${indentation}`;
22555
22555
  whitespace = " ";
22556
22556
  }
@@ -22559,7 +22559,7 @@ ${indentation}`;
22559
22559
  const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
22560
22560
  if (tmp !== void 0) {
22561
22561
  res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
22562
- separator = join5;
22562
+ separator = join6;
22563
22563
  }
22564
22564
  }
22565
22565
  if (spacer !== "" && separator.length > 1) {
@@ -22617,20 +22617,20 @@ ${originalIndentation}`;
22617
22617
  indentation += spacer;
22618
22618
  let res2 = `
22619
22619
  ${indentation}`;
22620
- const join6 = `,
22620
+ const join7 = `,
22621
22621
  ${indentation}`;
22622
22622
  const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
22623
22623
  let i = 0;
22624
22624
  for (; i < maximumValuesToStringify - 1; i++) {
22625
22625
  const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
22626
22626
  res2 += tmp2 !== void 0 ? tmp2 : "null";
22627
- res2 += join6;
22627
+ res2 += join7;
22628
22628
  }
22629
22629
  const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
22630
22630
  res2 += tmp !== void 0 ? tmp : "null";
22631
22631
  if (value.length - 1 > maximumBreadth) {
22632
22632
  const removedKeys = value.length - maximumBreadth - 1;
22633
- res2 += `${join6}"... ${getItemCount(removedKeys)} not stringified"`;
22633
+ res2 += `${join7}"... ${getItemCount(removedKeys)} not stringified"`;
22634
22634
  }
22635
22635
  res2 += `
22636
22636
  ${originalIndentation}`;
@@ -22646,16 +22646,16 @@ ${originalIndentation}`;
22646
22646
  return '"[Object]"';
22647
22647
  }
22648
22648
  indentation += spacer;
22649
- const join5 = `,
22649
+ const join6 = `,
22650
22650
  ${indentation}`;
22651
22651
  let res = "";
22652
22652
  let separator = "";
22653
22653
  let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
22654
22654
  if (isTypedArrayWithEntries(value)) {
22655
- res += stringifyTypedArray(value, join5, maximumBreadth);
22655
+ res += stringifyTypedArray(value, join6, maximumBreadth);
22656
22656
  keys = keys.slice(value.length);
22657
22657
  maximumPropertiesToStringify -= value.length;
22658
- separator = join5;
22658
+ separator = join6;
22659
22659
  }
22660
22660
  if (deterministic) {
22661
22661
  keys = sort(keys, comparator);
@@ -22666,13 +22666,13 @@ ${indentation}`;
22666
22666
  const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
22667
22667
  if (tmp !== void 0) {
22668
22668
  res += `${separator}${strEscape(key2)}: ${tmp}`;
22669
- separator = join5;
22669
+ separator = join6;
22670
22670
  }
22671
22671
  }
22672
22672
  if (keyLength > maximumBreadth) {
22673
22673
  const removedKeys = keyLength - maximumBreadth;
22674
22674
  res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
22675
- separator = join5;
22675
+ separator = join6;
22676
22676
  }
22677
22677
  if (separator !== "") {
22678
22678
  res = `
@@ -76147,9 +76147,9 @@ var StdioServerTransport = class {
76147
76147
 
76148
76148
  // src/services/appleMailManager.ts
76149
76149
  import { spawnSync as spawnSync2 } from "child_process";
76150
- import { existsSync as existsSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync2, mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "fs";
76151
- import { isAbsolute, resolve, sep, join as join3 } from "path";
76152
- import { homedir as homedir2 } from "os";
76150
+ import { existsSync as existsSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync2, mkdtempSync as mkdtempSync2, rmSync as rmSync2 } from "fs";
76151
+ import { isAbsolute, resolve, sep, join as join4 } from "path";
76152
+ import { homedir as homedir3 } from "os";
76153
76153
 
76154
76154
  // src/utils/applescript.ts
76155
76155
  import { execSync, spawnSync } from "child_process";
@@ -76219,7 +76219,7 @@ var ERROR_MAPPINGS = [
76219
76219
  // Permission errors
76220
76220
  {
76221
76221
  pattern: /not authorized|not permitted|access.*denied/i,
76222
- message: "Permission denied. Grant automation access in System Preferences > Privacy & Security > Automation."
76222
+ message: "Permission denied. Grant automation access in System Settings > Privacy & Security > Automation."
76223
76223
  },
76224
76224
  // Application not running
76225
76225
  {
@@ -76685,6 +76685,114 @@ function materializeAttachments(attachments) {
76685
76685
  };
76686
76686
  }
76687
76687
 
76688
+ // src/utils/contactsDb.ts
76689
+ import { existsSync as existsSync2, readdirSync } from "fs";
76690
+ import { join as join3 } from "path";
76691
+ import { homedir as homedir2 } from "os";
76692
+ function loadSqlite() {
76693
+ try {
76694
+ const mod = __require("node:sqlite");
76695
+ if (!mod || typeof mod.DatabaseSync !== "function") {
76696
+ console.error(
76697
+ "[contactsDb] node:sqlite unavailable (no DatabaseSync export); returning no contacts"
76698
+ );
76699
+ return null;
76700
+ }
76701
+ return mod;
76702
+ } catch (err) {
76703
+ console.error(
76704
+ `[contactsDb] node:sqlite not available (requires Node >= 22.5); returning no contacts: ${err instanceof Error ? err.message : String(err)}`
76705
+ );
76706
+ return null;
76707
+ }
76708
+ }
76709
+ function resolveContactsDbPaths(baseDir) {
76710
+ const root = baseDir ?? join3(homedir2(), "Library", "Application Support", "AddressBook");
76711
+ const paths = [];
76712
+ const topLevel = join3(root, "AddressBook-v22.abcddb");
76713
+ if (existsSync2(topLevel)) paths.push(topLevel);
76714
+ const sourcesDir = join3(root, "Sources");
76715
+ if (existsSync2(sourcesDir)) {
76716
+ let entries = [];
76717
+ try {
76718
+ entries = readdirSync(sourcesDir);
76719
+ } catch {
76720
+ entries = [];
76721
+ }
76722
+ for (const entry of entries) {
76723
+ const candidate = join3(sourcesDir, entry, "AddressBook-v22.abcddb");
76724
+ if (existsSync2(candidate)) paths.push(candidate);
76725
+ }
76726
+ }
76727
+ return paths;
76728
+ }
76729
+ var RECORD_SQL = `
76730
+ SELECT r.Z_PK pk,
76731
+ TRIM(COALESCE(r.ZFIRSTNAME,'')||' '||COALESCE(r.ZLASTNAME,'')) fullname,
76732
+ r.ZORGANIZATION org, r.ZNICKNAME nick
76733
+ FROM ZABCDRECORD r
76734
+ WHERE lower(TRIM(COALESCE(r.ZFIRSTNAME,'')||' '||COALESCE(r.ZLASTNAME,''))) LIKE ?
76735
+ OR lower(COALESCE(r.ZORGANIZATION,'')) LIKE ?
76736
+ OR lower(COALESCE(r.ZNICKNAME,'')) LIKE ?
76737
+ OR r.Z_PK IN (SELECT ZOWNER FROM ZABCDEMAILADDRESS WHERE lower(COALESCE(ZADDRESS,'')) LIKE ?)
76738
+ `;
76739
+ var EMAILS_SQL = `SELECT ZADDRESS AS v FROM ZABCDEMAILADDRESS WHERE ZOWNER = ? AND ZADDRESS IS NOT NULL`;
76740
+ var PHONES_SQL = `SELECT ZFULLNUMBER AS v FROM ZABCDPHONENUMBER WHERE ZOWNER = ? AND ZFULLNUMBER IS NOT NULL`;
76741
+ function displayName(row) {
76742
+ const fullname = typeof row.fullname === "string" ? row.fullname.trim() : "";
76743
+ if (fullname) return fullname;
76744
+ const org = typeof row.org === "string" ? row.org.trim() : "";
76745
+ if (org) return org;
76746
+ const nick = typeof row.nick === "string" ? row.nick.trim() : "";
76747
+ if (nick) return nick;
76748
+ return "(no name)";
76749
+ }
76750
+ function searchOneDb(sqlite, dbPath, like) {
76751
+ let db = null;
76752
+ try {
76753
+ db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
76754
+ const records = db.prepare(RECORD_SQL).all(like, like, like, like);
76755
+ const emailStmt = db.prepare(EMAILS_SQL);
76756
+ const phoneStmt = db.prepare(PHONES_SQL);
76757
+ const contacts = [];
76758
+ for (const rec of records) {
76759
+ const pk = rec.pk;
76760
+ const emails = emailStmt.all(pk).map((r) => r.v).filter((v) => typeof v === "string" && v.length > 0);
76761
+ const phones = phoneStmt.all(pk).map((r) => r.v).filter((v) => typeof v === "string" && v.length > 0);
76762
+ contacts.push({ name: displayName(rec), emails, phones });
76763
+ }
76764
+ return contacts;
76765
+ } catch (err) {
76766
+ console.error(
76767
+ `[contactsDb] skipping unreadable Contacts DB ${dbPath}: ${err instanceof Error ? err.message : String(err)}`
76768
+ );
76769
+ return [];
76770
+ } finally {
76771
+ try {
76772
+ db?.close();
76773
+ } catch {
76774
+ }
76775
+ }
76776
+ }
76777
+ function searchContactsDb(query, opts) {
76778
+ if (!query || !query.trim()) return [];
76779
+ const sqlite = loadSqlite();
76780
+ if (!sqlite) return [];
76781
+ const dbPaths = opts?.dbPaths ?? resolveContactsDbPaths();
76782
+ const like = `%${query.toLowerCase()}%`;
76783
+ const seen = /* @__PURE__ */ new Set();
76784
+ const results = [];
76785
+ for (const dbPath of dbPaths) {
76786
+ for (const contact of searchOneDb(sqlite, dbPath, like)) {
76787
+ const key = `${contact.name} ${[...contact.emails].sort().join(",")}`;
76788
+ if (seen.has(key)) continue;
76789
+ seen.add(key);
76790
+ results.push(contact);
76791
+ }
76792
+ }
76793
+ return results;
76794
+ }
76795
+
76688
76796
  // src/services/appleMailManager.ts
76689
76797
  function getMailboxScanThreshold() {
76690
76798
  const raw = process.env.APPLE_MAIL_MAX_SEARCH_MAILBOX;
@@ -76740,7 +76848,7 @@ function splitSearchDiagnostics(output, account) {
76740
76848
  }
76741
76849
  return { payload, diagnostics };
76742
76850
  }
76743
- var ALLOWED_SAVE_ROOTS = [homedir2(), "/tmp", "/private/tmp", "/Volumes"];
76851
+ var ALLOWED_SAVE_ROOTS = [homedir3(), "/tmp", "/private/tmp", "/Volumes"];
76744
76852
  function isPathWithinAllowedRoots(resolvedPath) {
76745
76853
  return ALLOWED_SAVE_ROOTS.some((root) => {
76746
76854
  const base = root.endsWith(sep) ? root.slice(0, -1) : root;
@@ -76784,7 +76892,7 @@ function buildAttachmentCommands(attachments) {
76784
76892
  if (!isAbsolute(filePath)) {
76785
76893
  throw new Error(`Attachment path must be absolute: "${filePath}"`);
76786
76894
  }
76787
- if (!existsSync2(filePath)) {
76895
+ if (!existsSync3(filePath)) {
76788
76896
  throw new Error(`Attachment file not found: "${filePath}"`);
76789
76897
  }
76790
76898
  }
@@ -78606,7 +78714,7 @@ var AppleMailManager = class {
78606
78714
  let dir = null;
78607
78715
  try {
78608
78716
  dir = mkdtempSync2("/private/tmp/amcp-fetch-");
78609
- const dest = join3(dir, attachmentName.replace(/[/\\]/g, "_"));
78717
+ const dest = join4(dir, attachmentName.replace(/[/\\]/g, "_"));
78610
78718
  const ok = this.saveAttachment(id, attachmentName, dest);
78611
78719
  if (!ok) {
78612
78720
  return {
@@ -79040,60 +79148,16 @@ ${actionStmts.join("\n")}
79040
79148
  // Contacts Integration
79041
79149
  // ===========================================================================
79042
79150
  /**
79043
- * Search contacts by name or email.
79151
+ * Search contacts by name, organization, nickname, or email address.
79152
+ *
79153
+ * Reads the macOS Contacts (AddressBook) SQLite databases directly via Full
79154
+ * Disk Access — it does NOT drive Contacts.app over AppleScript, so it raises
79155
+ * no Automation / Apple-Events permission prompt (the old path would hang on
79156
+ * that unanswerable prompt on headless/scheduled hosts). See
79157
+ * {@link searchContactsDb}.
79044
79158
  */
79045
79159
  searchContacts(query) {
79046
- const safeQuery = escapeForAppleScript(query);
79047
- const script = `
79048
- tell application "Contacts"
79049
- set matchedContacts to {}
79050
- set foundPeople to (every person whose name contains "${safeQuery}") & (every person whose value of emails contains "${safeQuery}")
79051
-
79052
- -- Deduplicate by tracking IDs
79053
- set seenIds to {}
79054
- repeat with p in foundPeople
79055
- set pid to id of p
79056
- if seenIds does not contain pid then
79057
- set end of seenIds to pid
79058
- -- Per-person try: one malformed contact (e.g. no emails) must not
79059
- -- abort the whole search and return an empty list (audit finding #13).
79060
- try
79061
- set pName to name of p
79062
- set pEmails to ""
79063
- repeat with e in emails of p
79064
- if pEmails is not "" then set pEmails to pEmails & ","
79065
- set pEmails to pEmails & (value of e)
79066
- end repeat
79067
- set pPhones to ""
79068
- repeat with ph in phones of p
79069
- if pPhones is not "" then set pPhones to pPhones & ","
79070
- set pPhones to pPhones & (value of ph)
79071
- end repeat
79072
- set end of matchedContacts to pName & "${FIELD_SEP}" & pEmails & "${FIELD_SEP}" & pPhones
79073
- end try
79074
- end if
79075
- end repeat
79076
-
79077
- set AppleScript's text item delimiters to "${RECORD_SEP}"
79078
- return matchedContacts as text
79079
- end tell
79080
- `;
79081
- const result = executeAppleScript(script);
79082
- if (!result.success || !result.output.trim()) {
79083
- return [];
79084
- }
79085
- const items = result.output.split(RECORD_SEP);
79086
- const contacts = [];
79087
- for (const item of items) {
79088
- const parts = item.split(FIELD_SEP);
79089
- if (parts.length < 3) continue;
79090
- contacts.push({
79091
- name: parts[0],
79092
- emails: parts[1] ? parts[1].split(",").filter(Boolean) : [],
79093
- phones: parts[2] ? parts[2].split(",").filter(Boolean) : []
79094
- });
79095
- }
79096
- return contacts;
79160
+ return searchContactsDb(query);
79097
79161
  }
79098
79162
  // ===========================================================================
79099
79163
  // Email Templates
@@ -79153,7 +79217,7 @@ ${actionStmts.join("\n")}
79153
79217
  message: "Mail.app is accessible"
79154
79218
  });
79155
79219
  } else {
79156
- const errorHint = mailCheck.error?.includes("not authorized") ? " (check Automation permissions in System Preferences)" : "";
79220
+ const errorHint = mailCheck.error?.includes("not authorized") ? " (check System Settings > Privacy & Security > Automation)" : "";
79157
79221
  checks.push({
79158
79222
  name: "mail_app",
79159
79223
  passed: false,
@@ -79173,7 +79237,7 @@ ${actionStmts.join("\n")}
79173
79237
  checks.push({
79174
79238
  name: "permissions",
79175
79239
  passed: !isPermError,
79176
- message: isPermError ? "AppleScript permissions denied. Grant access in System Preferences > Privacy & Security > Automation" : `Permission check returned: ${permCheck.error}`
79240
+ message: isPermError ? "AppleScript permissions denied. Grant access in System Settings > Privacy & Security > Automation" : `Permission check returned: ${permCheck.error}`
79177
79241
  });
79178
79242
  if (isPermError) {
79179
79243
  return { healthy: false, checks };
@@ -79408,7 +79472,13 @@ import { resolve as resolvePath, join as joinPath } from "path";
79408
79472
  var import_nodemailer = __toESM(require_nodemailer(), 1);
79409
79473
  import { execFileSync } from "child_process";
79410
79474
  import { isAbsolute as isAbsolute2 } from "path";
79411
- import { existsSync as existsSync3 } from "fs";
79475
+ import { existsSync as existsSync4 } from "fs";
79476
+
79477
+ // src/utils/docsUrls.ts
79478
+ var SETUP_GUIDE_URL = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
79479
+ var SETUP_HINT = `Setup guide: ${SETUP_GUIDE_URL} \u2014 run the "doctor" tool to check your setup.`;
79480
+
79481
+ // src/services/smtpMailer.ts
79412
79482
  var SMTP_ENV = {
79413
79483
  host: "APPLE_MAIL_MCP_SMTP_HOST",
79414
79484
  port: "APPLE_MAIL_MCP_SMTP_PORT",
@@ -79451,7 +79521,7 @@ function resolveSmtpConfig(env = process.env) {
79451
79521
  if (!user) missing.push(SMTP_ENV.user);
79452
79522
  if (missing.length > 0) {
79453
79523
  throw new Error(
79454
- `SMTP transport is not configured. Set ${missing.join(" and ")} (plus a password via ${SMTP_ENV.password} or the Keychain). See the README "SMTP transport" section.`
79524
+ `SMTP transport is not configured. Set ${missing.join(" and ")} (plus a password via ${SMTP_ENV.password} or the Keychain). ` + SETUP_HINT
79455
79525
  );
79456
79526
  }
79457
79527
  const secure = /^(1|true|yes)$/i.test(env[SMTP_ENV.secure]?.trim() ?? "");
@@ -79468,7 +79538,7 @@ function resolveSmtpConfig(env = process.env) {
79468
79538
  }
79469
79539
  if (!pass) {
79470
79540
  throw new Error(
79471
- `No SMTP password found. Set ${SMTP_ENV.password}, or store an internet password in the Keychain for service "${env[SMTP_ENV.keychainService]?.trim() || host}" / account "${env[SMTP_ENV.keychainAccount]?.trim() || user}".`
79541
+ `No SMTP password found. Set ${SMTP_ENV.password}, or store an internet password in the Keychain for service "${env[SMTP_ENV.keychainService]?.trim() || host}" / account "${env[SMTP_ENV.keychainAccount]?.trim() || user}". ` + SETUP_HINT
79472
79542
  );
79473
79543
  }
79474
79544
  return { host, port, secure, user, pass, from };
@@ -79478,7 +79548,7 @@ function buildAttachments(attachments) {
79478
79548
  return attachments.map((a) => {
79479
79549
  if (typeof a === "string") {
79480
79550
  if (!isAbsolute2(a)) throw new Error(`Attachment path must be absolute: "${a}"`);
79481
- if (!existsSync3(a)) throw new Error(`Attachment file not found: "${a}"`);
79551
+ if (!existsSync4(a)) throw new Error(`Attachment file not found: "${a}"`);
79482
79552
  return { path: a };
79483
79553
  }
79484
79554
  if (!a.filename || !a.contentBase64) {
@@ -79811,7 +79881,9 @@ function resolveImapConfigs(env = process.env) {
79811
79881
  function resolveImapConfig(env = process.env, account) {
79812
79882
  const specs = listImapAccountSpecs(env);
79813
79883
  if (specs.length === 0) {
79814
- throw new Error(`IMAP not configured. Set ${IMAP_ENV.user} (login address) to enable it.`);
79884
+ throw new Error(
79885
+ `IMAP not configured. Set ${IMAP_ENV.user} (login address) to enable it. ${SETUP_HINT}`
79886
+ );
79815
79887
  }
79816
79888
  let spec;
79817
79889
  if (account) {
@@ -80757,7 +80829,6 @@ async function routeMessage(id, opts) {
80757
80829
  }
80758
80830
 
80759
80831
  // src/tools/doctor.ts
80760
- var SETUP_GUIDE = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
80761
80832
  var CONFIG_FILE_HINT = "If your MCP host ignores the server 'env' block (e.g. Claude Desktop), put these in ~/Library/Application Support/apple-mail-mcp/config.json instead";
80762
80833
  async function runDoctor(mailManager2) {
80763
80834
  const checks = [];
@@ -80790,7 +80861,7 @@ async function runDoctor(mailManager2) {
80790
80861
  checks.push({
80791
80862
  name: "IMAP backend",
80792
80863
  status: "warn",
80793
- detail: `not configured \u2014 AppleScript is used for all accounts. Set ${IMAP_ENV.user} (+ Keychain/password), or ${IMAP_ENV.accounts} for multiple accounts, to enable server-side search and server-mailbox ops. ${CONFIG_FILE_HINT}. Setup guide: ${SETUP_GUIDE}`
80864
+ detail: `not configured \u2014 AppleScript is used for all accounts. Set ${IMAP_ENV.user} (+ Keychain/password), or ${IMAP_ENV.accounts} for multiple accounts, to enable server-side search and server-mailbox ops. ${CONFIG_FILE_HINT}. Setup guide: ${SETUP_GUIDE_URL}`
80794
80865
  });
80795
80866
  } else {
80796
80867
  for (const label of imapAccounts) {
@@ -80806,7 +80877,7 @@ async function runDoctor(mailManager2) {
80806
80877
  checks.push({
80807
80878
  name: "SMTP transport",
80808
80879
  status: isSmtpConfigured() ? "ok" : "warn",
80809
- detail: isSmtpConfigured() ? `configured (${smtpHost}); send-email auto-prefers clean SMTP (no Mail.app Sent-folder copy; a non-email "account" label still routes to AppleScript). Pass transport:"applescript" to force Mail.app. The apple-mail-send CLI is also available.` : `not configured \u2014 send-email uses AppleScript (subject to macOS 15+ blockquote wrapping). Set ${SMTP_ENV.host} and ${SMTP_ENV.user} (+ password via Keychain) to enable. ${CONFIG_FILE_HINT}. Setup guide: ${SETUP_GUIDE}`
80880
+ detail: isSmtpConfigured() ? `configured (${smtpHost}); send-email auto-prefers clean SMTP (no Mail.app Sent-folder copy; a non-email "account" label still routes to AppleScript). Pass transport:"applescript" to force Mail.app. The apple-mail-send CLI is also available.` : `not configured \u2014 send-email uses AppleScript (subject to macOS 15+ blockquote wrapping). Set ${SMTP_ENV.host} and ${SMTP_ENV.user} (+ password via Keychain) to enable. ${CONFIG_FILE_HINT}. Setup guide: ${SETUP_GUIDE_URL}`
80810
80881
  });
80811
80882
  const healthy = !checks.some((c) => c.status === "fail");
80812
80883
  return { healthy, checks };
@@ -81071,18 +81142,18 @@ var ImapIdleWatcher = class {
81071
81142
  };
81072
81143
 
81073
81144
  // src/services/fileConfig.ts
81074
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
81075
- import { join as join4 } from "path";
81076
- import { homedir as homedir3 } from "os";
81145
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
81146
+ import { join as join5 } from "path";
81147
+ import { homedir as homedir4 } from "os";
81077
81148
  function fileConfigPath(env = process.env) {
81078
81149
  const override = env.APPLE_MAIL_MCP_CONFIG_FILE;
81079
81150
  if (override && override.trim()) return override.trim();
81080
- return join4(homedir3(), "Library", "Application Support", "apple-mail-mcp", "config.json");
81151
+ return join5(homedir4(), "Library", "Application Support", "apple-mail-mcp", "config.json");
81081
81152
  }
81082
81153
  function loadFileConfig(env = process.env, path = fileConfigPath(env)) {
81083
81154
  const applied = [];
81084
81155
  try {
81085
- if (!existsSync4(path)) return applied;
81156
+ if (!existsSync5(path)) return applied;
81086
81157
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
81087
81158
  if (!parsed || typeof parsed !== "object") return applied;
81088
81159
  for (const [k, v] of Object.entries(parsed)) {
@@ -82690,7 +82761,7 @@ server.registerTool(
82690
82761
  server.registerTool(
82691
82762
  "search-contacts",
82692
82763
  {
82693
- description: "Use when: looking up a person in Contacts.app by name to find their email address(es) before composing or sending mail.\nReturns: matching contacts with their names and email addresses.\nDo not use when: searching email messages (use search-messages) \u2014 this queries Contacts, not the mailbox.",
82764
+ description: "Use when: looking up a person in Contacts by name, organization, nickname, or email to find their email address(es)/phone(s) before composing or sending mail. Reads the macOS Contacts database directly (needs Full Disk Access; does NOT require Contacts.app to be running or an Automation / Apple-Events grant).\nReturns: matching contacts with their names, email addresses, and phone numbers.\nDo not use when: searching email messages (use search-messages) \u2014 this queries Contacts, not the mailbox.",
82694
82765
  inputSchema: {
82695
82766
  query: external_exports.string().min(1, "Search query is required")
82696
82767
  },