apple-mail-mcp 2.20.4 → 2.20.6
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 +38 -24
- package/build/cli.js +93 -6
- package/build/index.js +237 -64
- package/docs/IMAP-SETUP.md +27 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -281,15 +281,28 @@ human reader; the same information is also returned as structured fields on
|
|
|
281
281
|
`search-messages` and `list-messages`, so a caller can tell _"nothing matched"_
|
|
282
282
|
apart from _"I did not look everywhere"_ without parsing the text:
|
|
283
283
|
|
|
284
|
-
| Field | Type | Meaning
|
|
285
|
-
| ----------------------- | -------- |
|
|
286
|
-
| `partial` | boolean | Coverage was incomplete — **the result is not a confirmed "no such mail"**. True whenever any field below is non-empty.
|
|
287
|
-
| `skippedLargeMailboxes` | string[] | Mailboxes never scanned because their message count exceeded `APPLE_MAIL_MAX_SEARCH_MAILBOX`, formatted `"Account / Mailbox (count)"` — e.g. `"iCloud / Archive (90694)"`.
|
|
288
|
-
| `notSearchedMailboxes` | string[] | Mailboxes that _were_ reached but timed out or errored mid-scan, formatted `"Account / Mailbox"`. Also carries the IMAP path's `failedMailboxes`.
|
|
289
|
-
| `timedOutAccounts` | string[] | Accounts whose whole-account AppleScript was killed by the per-account time budget — nothing from that account was searched.
|
|
290
|
-
| `failedMailboxes` | string[] | IMAP-backend mailboxes that errored. These are merged into `notSearchedMailboxes` as well; read that field unless you need to attribute the failure to the IMAP path specifically.
|
|
291
|
-
| `failedMailboxReasons` | object | The IMAP server's own error text for each entry in `failedMailboxes`, keyed the same way.
|
|
292
|
-
| `omittedMessages` | object[] | IMAP messages that belong on the page but whose `FETCH` response could not be read even with a reduced item set — `{ id, mailbox, uid, reason }`. The page is short by exactly these; the `id` still works with `get-message` / `get-message-headers`. ([#256](https://github.com/sweetrb/apple-mail-mcp/issues/256))
|
|
284
|
+
| Field | Type | Meaning |
|
|
285
|
+
| ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
286
|
+
| `partial` | boolean | Coverage was incomplete — **the result is not a confirmed "no such mail"**. True whenever any field below is non-empty. |
|
|
287
|
+
| `skippedLargeMailboxes` | string[] | Mailboxes never scanned because their message count exceeded `APPLE_MAIL_MAX_SEARCH_MAILBOX`, formatted `"Account / Mailbox (count)"` — e.g. `"iCloud / Archive (90694)"`. |
|
|
288
|
+
| `notSearchedMailboxes` | string[] | Mailboxes that _were_ reached but timed out or errored mid-scan, formatted `"Account / Mailbox"`. Also carries the IMAP path's `failedMailboxes`. |
|
|
289
|
+
| `timedOutAccounts` | string[] | Accounts whose whole-account AppleScript was killed by the per-account time budget — nothing from that account was searched. |
|
|
290
|
+
| `failedMailboxes` | string[] | IMAP-backend mailboxes that errored. These are merged into `notSearchedMailboxes` as well; read that field unless you need to attribute the failure to the IMAP path specifically. |
|
|
291
|
+
| `failedMailboxReasons` | object | The IMAP server's own error text for each entry in `failedMailboxes`, keyed the same way. |
|
|
292
|
+
| `omittedMessages` | object[] | IMAP messages that belong on the page but whose `FETCH` response could not be read even with a reduced item set — `{ id, mailbox, uid, reason }`. The page is short by exactly these; the `id` still works with `get-message` / `get-message-headers`. ([#256](https://github.com/sweetrb/apple-mail-mcp/issues/256)) |
|
|
293
|
+
| `timedOutMailboxes` | string[] | IMAP mailboxes the search did not finish because the call was **cancelled** (`notifications/cancelled`) or reached its **deadline** (`APPLE_MAIL_MCP_SEARCH_DEADLINE_MS`, default 45s), formatted `"Account / Mailbox"`; an account never reached is `"Account / *"`. Results from the mailboxes that did finish are still returned. ([#276](https://github.com/sweetrb/apple-mail-mcp/issues/276)) |
|
|
294
|
+
| `stoppedBy` | string | `"deadline"` or `"cancelled"` — present only with `timedOutMailboxes`. |
|
|
295
|
+
|
|
296
|
+
**Deadline and cancellation ([#276](https://github.com/sweetrb/apple-mail-mcp/issues/276)).** Tool
|
|
297
|
+
calls run one at a time (so they cannot race into Mail.app), which means a long search holds every
|
|
298
|
+
later call behind it. An IMAP `search-messages` therefore stops at
|
|
299
|
+
`APPLE_MAIL_MCP_SEARCH_DEADLINE_MS` (default `45000`, minimum `2000`, measured from when the request
|
|
300
|
+
arrived — time queued behind other calls counts) and returns what it found plus `timedOutMailboxes`,
|
|
301
|
+
and it stops the same way when the client cancels the request. The mailbox being searched at that
|
|
302
|
+
moment is abandoned by closing its IMAP connection (the next call reconnects), so the queue is
|
|
303
|
+
released promptly rather than after a server-side `SEARCH` that can take a minute and a half on a
|
|
304
|
+
very large mailbox. A request cancelled while still queued is skipped. Keep the deadline below your
|
|
305
|
+
client's own request timeout.
|
|
293
306
|
|
|
294
307
|
Treat a non-empty `skippedLargeMailboxes` as actionable rather than
|
|
295
308
|
informational: re-run scoped to the named mailbox with a `dateFrom`/`dateTo`
|
|
@@ -620,21 +633,22 @@ If IMAP is **not** configured at all, every read behaves exactly as before
|
|
|
620
633
|
`delete-mailbox`, `rename-mailbox`) remain conservative — they route to IMAP only
|
|
621
634
|
for an explicitly-named IMAP account, never on an omitted account.
|
|
622
635
|
|
|
623
|
-
| Variable | Required | Default | Description
|
|
624
|
-
| -------------------------------------- | -------- | ---------------- |
|
|
625
|
-
| `APPLE_MAIL_MCP_IMAP_USER` | Yes | — | Login address; setting it enables IMAP
|
|
626
|
-
| `APPLE_MAIL_MCP_IMAP_ACCOUNT` | No | = user | Mail account name to match for routing
|
|
627
|
-
| `APPLE_MAIL_MCP_IMAP_HOST` | No | `imap.gmail.com` | IMAP server hostname
|
|
628
|
-
| `APPLE_MAIL_MCP_IMAP_PORT` | No | `993` | IMAP port (993 = implicit TLS)
|
|
629
|
-
| `APPLE_MAIL_MCP_IMAP_ALLOW_PLAINTEXT` | No | `0` | Set `1` only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required
|
|
630
|
-
| `APPLE_MAIL_MCP_IMAP_PASSWORD` | No | — | Password (if set, used instead of the Keychain)
|
|
631
|
-
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | No | — | Keychain item service/server name
|
|
632
|
-
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account
|
|
633
|
-
| `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | No | — | JSON array of **additional** IMAP accounts for multi-account setups (see below)
|
|
634
|
-
| `APPLE_MAIL_MCP_IMAP_IDLE` | No | `0` | Set `1` to enable IMAP IDLE push notifications (new-mail alerts) for every configured account
|
|
635
|
-
| `APPLE_MAIL_MCP_IMAP_IDLE_MS` | No | `30000` | Idle timeout (ms) before a pooled IMAP connection is closed (`0` = never close)
|
|
636
|
-
| `APPLE_MAIL_MCP_STATS_BUDGET_MS` | No | `25000` | Per-account wall-clock budget for `get-mail-stats` (minimum `1000`). Raise it for very large accounts
|
|
637
|
-
| `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | No | `50000` | Overall wall-clock deadline for one `get-mail-stats` call (minimum `2000`), measured from when the request arrived and covering time queued behind other tool calls, account enumeration **and** every per-account read. Keep it below your client's request timeout
|
|
636
|
+
| Variable | Required | Default | Description |
|
|
637
|
+
| -------------------------------------- | -------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
638
|
+
| `APPLE_MAIL_MCP_IMAP_USER` | Yes | — | Login address; setting it enables IMAP |
|
|
639
|
+
| `APPLE_MAIL_MCP_IMAP_ACCOUNT` | No | = user | Mail account name to match for routing |
|
|
640
|
+
| `APPLE_MAIL_MCP_IMAP_HOST` | No | `imap.gmail.com` | IMAP server hostname |
|
|
641
|
+
| `APPLE_MAIL_MCP_IMAP_PORT` | No | `993` | IMAP port (993 = implicit TLS) |
|
|
642
|
+
| `APPLE_MAIL_MCP_IMAP_ALLOW_PLAINTEXT` | No | `0` | Set `1` only for an explicitly trusted plaintext test/server; otherwise STARTTLS is required |
|
|
643
|
+
| `APPLE_MAIL_MCP_IMAP_PASSWORD` | No | — | Password (if set, used instead of the Keychain) |
|
|
644
|
+
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | No | — | Keychain item service/server name |
|
|
645
|
+
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | No | = user | Keychain item account |
|
|
646
|
+
| `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | No | — | JSON array of **additional** IMAP accounts for multi-account setups (see below) |
|
|
647
|
+
| `APPLE_MAIL_MCP_IMAP_IDLE` | No | `0` | Set `1` to enable IMAP IDLE push notifications (new-mail alerts) for every configured account |
|
|
648
|
+
| `APPLE_MAIL_MCP_IMAP_IDLE_MS` | No | `30000` | Idle timeout (ms) before a pooled IMAP connection is closed (`0` = never close) |
|
|
649
|
+
| `APPLE_MAIL_MCP_STATS_BUDGET_MS` | No | `25000` | Per-account wall-clock budget for `get-mail-stats` (minimum `1000`). Raise it for very large accounts |
|
|
650
|
+
| `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | No | `50000` | Overall wall-clock deadline for one `get-mail-stats` call (minimum `2000`), measured from when the request arrived and covering time queued behind other tool calls, account enumeration **and** every per-account read. Keep it below your client's request timeout |
|
|
651
|
+
| `APPLE_MAIL_MCP_SEARCH_DEADLINE_MS` | No | `45000` | Overall wall-clock deadline for one IMAP `search-messages` call (minimum `2000`), measured from when the request arrived. On expiry the search returns partial results plus `timedOutMailboxes` instead of holding the tool-call queue. Keep it below your client's request timeout |
|
|
638
652
|
|
|
639
653
|
**Multiple IMAP accounts (C2):** set `APPLE_MAIL_MCP_IMAP_ACCOUNTS` to a JSON array, e.g.
|
|
640
654
|
`[{"account":"Work","user":"me@co.com","host":"imap.co.com","keychainService":"imap.co.com"}]`.
|
package/build/cli.js
CHANGED
|
@@ -12259,6 +12259,51 @@ var init_docsUrls = __esm({
|
|
|
12259
12259
|
}
|
|
12260
12260
|
});
|
|
12261
12261
|
|
|
12262
|
+
// src/utils/callStop.ts
|
|
12263
|
+
function describeStop(signal) {
|
|
12264
|
+
const reason = signal?.reason;
|
|
12265
|
+
if (reason && typeof reason === "object" && reason.kind === "deadline") {
|
|
12266
|
+
return `the ${Math.round(reason.deadlineMs / 1e3)}s search deadline (${reason.envVar})`;
|
|
12267
|
+
}
|
|
12268
|
+
return "the request being cancelled";
|
|
12269
|
+
}
|
|
12270
|
+
function stoppedByDeadline(signal) {
|
|
12271
|
+
const reason = signal?.reason;
|
|
12272
|
+
return !!reason && typeof reason === "object" && reason.kind === "deadline";
|
|
12273
|
+
}
|
|
12274
|
+
function raceStop(work, signal) {
|
|
12275
|
+
if (!signal) return work;
|
|
12276
|
+
if (signal.aborted) {
|
|
12277
|
+
work.catch(() => void 0);
|
|
12278
|
+
return Promise.resolve(STOPPED);
|
|
12279
|
+
}
|
|
12280
|
+
return new Promise((resolve4, reject) => {
|
|
12281
|
+
const onAbort = () => {
|
|
12282
|
+
work.catch(() => void 0);
|
|
12283
|
+
resolve4(STOPPED);
|
|
12284
|
+
};
|
|
12285
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
12286
|
+
work.then(
|
|
12287
|
+
(value) => {
|
|
12288
|
+
signal.removeEventListener("abort", onAbort);
|
|
12289
|
+
resolve4(value);
|
|
12290
|
+
},
|
|
12291
|
+
(error2) => {
|
|
12292
|
+
signal.removeEventListener("abort", onAbort);
|
|
12293
|
+
reject(error2);
|
|
12294
|
+
}
|
|
12295
|
+
);
|
|
12296
|
+
});
|
|
12297
|
+
}
|
|
12298
|
+
var SEARCH_DEADLINE_ENV, STOPPED;
|
|
12299
|
+
var init_callStop = __esm({
|
|
12300
|
+
"src/utils/callStop.ts"() {
|
|
12301
|
+
"use strict";
|
|
12302
|
+
SEARCH_DEADLINE_ENV = "APPLE_MAIL_MCP_SEARCH_DEADLINE_MS";
|
|
12303
|
+
STOPPED = /* @__PURE__ */ Symbol("stopped");
|
|
12304
|
+
}
|
|
12305
|
+
});
|
|
12306
|
+
|
|
12262
12307
|
// node_modules/.pnpm/pino-std-serializers@7.1.0/node_modules/pino-std-serializers/lib/err-helpers.js
|
|
12263
12308
|
var require_err_helpers = __commonJS({
|
|
12264
12309
|
"node_modules/.pnpm/pino-std-serializers@7.1.0/node_modules/pino-std-serializers/lib/err-helpers.js"(exports, module) {
|
|
@@ -58037,6 +58082,7 @@ __export(imapClient_exports, {
|
|
|
58037
58082
|
resolveImapConfigs: () => resolveImapConfigs,
|
|
58038
58083
|
resolveMailboxPath: () => resolveMailboxPath,
|
|
58039
58084
|
shouldUseImap: () => shouldUseImap,
|
|
58085
|
+
stoppedNote: () => stoppedNote,
|
|
58040
58086
|
unscopedSearchOffsetError: () => unscopedSearchOffsetError
|
|
58041
58087
|
});
|
|
58042
58088
|
import { createHash } from "node:crypto";
|
|
@@ -58357,6 +58403,12 @@ function structuredRow(m, account, path3) {
|
|
|
58357
58403
|
...env.messageId ? { messageId: env.messageId } : {}
|
|
58358
58404
|
};
|
|
58359
58405
|
}
|
|
58406
|
+
function stoppedNote(timedOut, signal) {
|
|
58407
|
+
if (timedOut.length === 0) return "";
|
|
58408
|
+
return `
|
|
58409
|
+
|
|
58410
|
+
Partial result. Stopped by ${describeStop(signal)} before finishing ${timedOut.length} mailbox(es): ${timedOut.map((m) => `"${m}"`).join(", ")}. This is NOT a confirmed "no such mail" \u2014 narrow the search with \`mailbox\` and/or \`dateFrom\`, or raise ${SEARCH_DEADLINE_ENV}.`;
|
|
58411
|
+
}
|
|
58360
58412
|
function omittedNote(omitted, merged) {
|
|
58361
58413
|
if (omitted.length === 0) return "";
|
|
58362
58414
|
const list = omitted.map((o) => `UID ${o.uid} in "${o.mailbox}" (${o.id})`).join(", ");
|
|
@@ -58646,9 +58698,23 @@ async function run(args, listMode, deps) {
|
|
|
58646
58698
|
const omittedMessages = [];
|
|
58647
58699
|
let totalMatched = 0;
|
|
58648
58700
|
let totalExact = true;
|
|
58649
|
-
|
|
58701
|
+
const timedOutMailboxes = [];
|
|
58702
|
+
for (const [index, path3] of paths.entries()) {
|
|
58703
|
+
if (deps.signal?.aborted) {
|
|
58704
|
+
timedOutMailboxes.push(...paths.slice(index));
|
|
58705
|
+
break;
|
|
58706
|
+
}
|
|
58650
58707
|
try {
|
|
58651
|
-
const
|
|
58708
|
+
const outcome = await raceStop(
|
|
58709
|
+
fetchMailboxMatches(client, path3, criteria, page),
|
|
58710
|
+
deps.signal
|
|
58711
|
+
);
|
|
58712
|
+
if (outcome === STOPPED) {
|
|
58713
|
+
abandonClient(client, cfg, deps);
|
|
58714
|
+
timedOutMailboxes.push(...paths.slice(index));
|
|
58715
|
+
break;
|
|
58716
|
+
}
|
|
58717
|
+
const result = outcome;
|
|
58652
58718
|
totalMatched += result.total;
|
|
58653
58719
|
totalExact &&= result.totalExact;
|
|
58654
58720
|
fetched.push(...result.messages.map((message) => ({ message, path: path3 })));
|
|
@@ -58688,10 +58754,14 @@ async function run(args, listMode, deps) {
|
|
|
58688
58754
|
const messages = ordered.map(
|
|
58689
58755
|
({ message, path: path3 }) => structuredRow(message, cfg.accountLabel, path3)
|
|
58690
58756
|
);
|
|
58691
|
-
const partial = failedMailboxes.length > 0 || omittedMessages.length > 0;
|
|
58757
|
+
const partial = failedMailboxes.length > 0 || omittedMessages.length > 0 || timedOutMailboxes.length > 0;
|
|
58758
|
+
const stopped = timedOutMailboxes.length > 0 ? {
|
|
58759
|
+
timedOutMailboxes,
|
|
58760
|
+
stoppedBy: stoppedByDeadline(deps.signal) ? "deadline" : "cancelled"
|
|
58761
|
+
} : {};
|
|
58692
58762
|
const failureNote = (failedMailboxes.length > 0 ? `
|
|
58693
58763
|
|
|
58694
|
-
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path3) => `"${path3}" (${failedMailboxReasons[path3]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch);
|
|
58764
|
+
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path3) => `"${path3}" (${failedMailboxReasons[path3]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch) + stoppedNote(timedOutMailboxes, deps.signal);
|
|
58695
58765
|
const verb = listMode ? "listed" : "matched";
|
|
58696
58766
|
const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
|
|
58697
58767
|
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
@@ -58703,7 +58773,8 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path3) => `
|
|
|
58703
58773
|
partial,
|
|
58704
58774
|
failedMailboxes,
|
|
58705
58775
|
failedMailboxReasons,
|
|
58706
|
-
omittedMessages
|
|
58776
|
+
omittedMessages,
|
|
58777
|
+
...stopped
|
|
58707
58778
|
};
|
|
58708
58779
|
}
|
|
58709
58780
|
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalText} ${verb}):
|
|
@@ -58717,7 +58788,8 @@ Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutatio
|
|
|
58717
58788
|
partial,
|
|
58718
58789
|
failedMailboxes,
|
|
58719
58790
|
failedMailboxReasons,
|
|
58720
|
-
omittedMessages
|
|
58791
|
+
omittedMessages,
|
|
58792
|
+
...stopped
|
|
58721
58793
|
};
|
|
58722
58794
|
},
|
|
58723
58795
|
true
|
|
@@ -58860,6 +58932,20 @@ async function dropPool(key) {
|
|
|
58860
58932
|
await e.client.logout().catch(() => void 0);
|
|
58861
58933
|
e.client.close?.();
|
|
58862
58934
|
}
|
|
58935
|
+
function abandonClient(client, cfg, deps) {
|
|
58936
|
+
if (!deps.connect) {
|
|
58937
|
+
const key = poolKey(cfg);
|
|
58938
|
+
const entry = pools.get(key);
|
|
58939
|
+
if (entry && entry.client === client) {
|
|
58940
|
+
if (entry.idle) clearTimeout(entry.idle);
|
|
58941
|
+
pools.delete(key);
|
|
58942
|
+
}
|
|
58943
|
+
}
|
|
58944
|
+
try {
|
|
58945
|
+
client.close?.();
|
|
58946
|
+
} catch {
|
|
58947
|
+
}
|
|
58948
|
+
}
|
|
58863
58949
|
async function dropAllPools() {
|
|
58864
58950
|
await Promise.all([...pools.keys()].map((k) => dropPool(k)));
|
|
58865
58951
|
}
|
|
@@ -59858,6 +59944,7 @@ var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED,
|
|
|
59858
59944
|
var init_imapClient = __esm({
|
|
59859
59945
|
"src/services/imapClient.ts"() {
|
|
59860
59946
|
"use strict";
|
|
59947
|
+
init_callStop();
|
|
59861
59948
|
import_imapflow = __toESM(require_imap_flow(), 1);
|
|
59862
59949
|
init_smtpMailer();
|
|
59863
59950
|
init_docsUrls();
|
package/build/index.js
CHANGED
|
@@ -19994,6 +19994,84 @@ var init_nodemailer = __esm({
|
|
|
19994
19994
|
}
|
|
19995
19995
|
});
|
|
19996
19996
|
|
|
19997
|
+
// src/utils/callStop.ts
|
|
19998
|
+
function searchDeadlineMs(env = process.env) {
|
|
19999
|
+
const raw = env[SEARCH_DEADLINE_ENV];
|
|
20000
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_SEARCH_DEADLINE_MS;
|
|
20001
|
+
const n = Number(raw);
|
|
20002
|
+
if (!Number.isFinite(n)) return DEFAULT_SEARCH_DEADLINE_MS;
|
|
20003
|
+
return Math.max(MIN_SEARCH_DEADLINE_MS, Math.floor(n));
|
|
20004
|
+
}
|
|
20005
|
+
function createCallStop(timing, deadlineMs, envVar = SEARCH_DEADLINE_ENV) {
|
|
20006
|
+
const controller = new AbortController();
|
|
20007
|
+
const upstream = timing?.signal;
|
|
20008
|
+
const onCancel = () => {
|
|
20009
|
+
if (!controller.signal.aborted) controller.abort({ kind: "cancelled" });
|
|
20010
|
+
};
|
|
20011
|
+
if (upstream?.aborted) onCancel();
|
|
20012
|
+
else upstream?.addEventListener("abort", onCancel, { once: true });
|
|
20013
|
+
const startedAt = timing?.arrivedAt ?? Date.now();
|
|
20014
|
+
const remaining = Math.max(0, deadlineMs - (Date.now() - startedAt));
|
|
20015
|
+
const timer = setTimeout(() => {
|
|
20016
|
+
if (!controller.signal.aborted) {
|
|
20017
|
+
controller.abort({ kind: "deadline", deadlineMs, envVar });
|
|
20018
|
+
}
|
|
20019
|
+
}, remaining);
|
|
20020
|
+
timer.unref?.();
|
|
20021
|
+
return {
|
|
20022
|
+
signal: controller.signal,
|
|
20023
|
+
dispose() {
|
|
20024
|
+
clearTimeout(timer);
|
|
20025
|
+
upstream?.removeEventListener("abort", onCancel);
|
|
20026
|
+
}
|
|
20027
|
+
};
|
|
20028
|
+
}
|
|
20029
|
+
function describeStop(signal) {
|
|
20030
|
+
const reason = signal?.reason;
|
|
20031
|
+
if (reason && typeof reason === "object" && reason.kind === "deadline") {
|
|
20032
|
+
return `the ${Math.round(reason.deadlineMs / 1e3)}s search deadline (${reason.envVar})`;
|
|
20033
|
+
}
|
|
20034
|
+
return "the request being cancelled";
|
|
20035
|
+
}
|
|
20036
|
+
function stoppedByDeadline(signal) {
|
|
20037
|
+
const reason = signal?.reason;
|
|
20038
|
+
return !!reason && typeof reason === "object" && reason.kind === "deadline";
|
|
20039
|
+
}
|
|
20040
|
+
function raceStop(work, signal) {
|
|
20041
|
+
if (!signal) return work;
|
|
20042
|
+
if (signal.aborted) {
|
|
20043
|
+
work.catch(() => void 0);
|
|
20044
|
+
return Promise.resolve(STOPPED);
|
|
20045
|
+
}
|
|
20046
|
+
return new Promise((resolve5, reject) => {
|
|
20047
|
+
const onAbort = () => {
|
|
20048
|
+
work.catch(() => void 0);
|
|
20049
|
+
resolve5(STOPPED);
|
|
20050
|
+
};
|
|
20051
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
20052
|
+
work.then(
|
|
20053
|
+
(value) => {
|
|
20054
|
+
signal.removeEventListener("abort", onAbort);
|
|
20055
|
+
resolve5(value);
|
|
20056
|
+
},
|
|
20057
|
+
(error3) => {
|
|
20058
|
+
signal.removeEventListener("abort", onAbort);
|
|
20059
|
+
reject(error3);
|
|
20060
|
+
}
|
|
20061
|
+
);
|
|
20062
|
+
});
|
|
20063
|
+
}
|
|
20064
|
+
var DEFAULT_SEARCH_DEADLINE_MS, MIN_SEARCH_DEADLINE_MS, SEARCH_DEADLINE_ENV, STOPPED;
|
|
20065
|
+
var init_callStop = __esm({
|
|
20066
|
+
"src/utils/callStop.ts"() {
|
|
20067
|
+
"use strict";
|
|
20068
|
+
DEFAULT_SEARCH_DEADLINE_MS = 45e3;
|
|
20069
|
+
MIN_SEARCH_DEADLINE_MS = 2e3;
|
|
20070
|
+
SEARCH_DEADLINE_ENV = "APPLE_MAIL_MCP_SEARCH_DEADLINE_MS";
|
|
20071
|
+
STOPPED = /* @__PURE__ */ Symbol("stopped");
|
|
20072
|
+
}
|
|
20073
|
+
});
|
|
20074
|
+
|
|
19997
20075
|
// node_modules/.pnpm/pino-std-serializers@7.1.0/node_modules/pino-std-serializers/lib/err-helpers.js
|
|
19998
20076
|
var require_err_helpers = __commonJS({
|
|
19999
20077
|
"node_modules/.pnpm/pino-std-serializers@7.1.0/node_modules/pino-std-serializers/lib/err-helpers.js"(exports, module) {
|
|
@@ -65379,6 +65457,7 @@ __export(imapClient_exports, {
|
|
|
65379
65457
|
resolveImapConfigs: () => resolveImapConfigs,
|
|
65380
65458
|
resolveMailboxPath: () => resolveMailboxPath,
|
|
65381
65459
|
shouldUseImap: () => shouldUseImap,
|
|
65460
|
+
stoppedNote: () => stoppedNote,
|
|
65382
65461
|
unscopedSearchOffsetError: () => unscopedSearchOffsetError
|
|
65383
65462
|
});
|
|
65384
65463
|
import { createHash } from "node:crypto";
|
|
@@ -65699,6 +65778,12 @@ function structuredRow(m, account, path3) {
|
|
|
65699
65778
|
...env.messageId ? { messageId: env.messageId } : {}
|
|
65700
65779
|
};
|
|
65701
65780
|
}
|
|
65781
|
+
function stoppedNote(timedOut, signal) {
|
|
65782
|
+
if (timedOut.length === 0) return "";
|
|
65783
|
+
return `
|
|
65784
|
+
|
|
65785
|
+
Partial result. Stopped by ${describeStop(signal)} before finishing ${timedOut.length} mailbox(es): ${timedOut.map((m) => `"${m}"`).join(", ")}. This is NOT a confirmed "no such mail" \u2014 narrow the search with \`mailbox\` and/or \`dateFrom\`, or raise ${SEARCH_DEADLINE_ENV}.`;
|
|
65786
|
+
}
|
|
65702
65787
|
function omittedNote(omitted, merged) {
|
|
65703
65788
|
if (omitted.length === 0) return "";
|
|
65704
65789
|
const list = omitted.map((o) => `UID ${o.uid} in "${o.mailbox}" (${o.id})`).join(", ");
|
|
@@ -65988,9 +66073,23 @@ async function run(args, listMode, deps) {
|
|
|
65988
66073
|
const omittedMessages = [];
|
|
65989
66074
|
let totalMatched = 0;
|
|
65990
66075
|
let totalExact = true;
|
|
65991
|
-
|
|
66076
|
+
const timedOutMailboxes = [];
|
|
66077
|
+
for (const [index, path3] of paths.entries()) {
|
|
66078
|
+
if (deps.signal?.aborted) {
|
|
66079
|
+
timedOutMailboxes.push(...paths.slice(index));
|
|
66080
|
+
break;
|
|
66081
|
+
}
|
|
65992
66082
|
try {
|
|
65993
|
-
const
|
|
66083
|
+
const outcome = await raceStop(
|
|
66084
|
+
fetchMailboxMatches(client, path3, criteria, page),
|
|
66085
|
+
deps.signal
|
|
66086
|
+
);
|
|
66087
|
+
if (outcome === STOPPED) {
|
|
66088
|
+
abandonClient(client, cfg, deps);
|
|
66089
|
+
timedOutMailboxes.push(...paths.slice(index));
|
|
66090
|
+
break;
|
|
66091
|
+
}
|
|
66092
|
+
const result = outcome;
|
|
65994
66093
|
totalMatched += result.total;
|
|
65995
66094
|
totalExact &&= result.totalExact;
|
|
65996
66095
|
fetched.push(...result.messages.map((message) => ({ message, path: path3 })));
|
|
@@ -66030,10 +66129,14 @@ async function run(args, listMode, deps) {
|
|
|
66030
66129
|
const messages = ordered.map(
|
|
66031
66130
|
({ message, path: path3 }) => structuredRow(message, cfg.accountLabel, path3)
|
|
66032
66131
|
);
|
|
66033
|
-
const partial2 = failedMailboxes.length > 0 || omittedMessages.length > 0;
|
|
66132
|
+
const partial2 = failedMailboxes.length > 0 || omittedMessages.length > 0 || timedOutMailboxes.length > 0;
|
|
66133
|
+
const stopped = timedOutMailboxes.length > 0 ? {
|
|
66134
|
+
timedOutMailboxes,
|
|
66135
|
+
stoppedBy: stoppedByDeadline(deps.signal) ? "deadline" : "cancelled"
|
|
66136
|
+
} : {};
|
|
66034
66137
|
const failureNote = (failedMailboxes.length > 0 ? `
|
|
66035
66138
|
|
|
66036
|
-
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path3) => `"${path3}" (${failedMailboxReasons[path3]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch);
|
|
66139
|
+
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path3) => `"${path3}" (${failedMailboxReasons[path3]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch) + stoppedNote(timedOutMailboxes, deps.signal);
|
|
66037
66140
|
const verb = listMode ? "listed" : "matched";
|
|
66038
66141
|
const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
|
|
66039
66142
|
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
@@ -66045,7 +66148,8 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path3) => `
|
|
|
66045
66148
|
partial: partial2,
|
|
66046
66149
|
failedMailboxes,
|
|
66047
66150
|
failedMailboxReasons,
|
|
66048
|
-
omittedMessages
|
|
66151
|
+
omittedMessages,
|
|
66152
|
+
...stopped
|
|
66049
66153
|
};
|
|
66050
66154
|
}
|
|
66051
66155
|
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalText} ${verb}):
|
|
@@ -66059,7 +66163,8 @@ Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutatio
|
|
|
66059
66163
|
partial: partial2,
|
|
66060
66164
|
failedMailboxes,
|
|
66061
66165
|
failedMailboxReasons,
|
|
66062
|
-
omittedMessages
|
|
66166
|
+
omittedMessages,
|
|
66167
|
+
...stopped
|
|
66063
66168
|
};
|
|
66064
66169
|
},
|
|
66065
66170
|
true
|
|
@@ -66202,6 +66307,20 @@ async function dropPool(key) {
|
|
|
66202
66307
|
await e.client.logout().catch(() => void 0);
|
|
66203
66308
|
e.client.close?.();
|
|
66204
66309
|
}
|
|
66310
|
+
function abandonClient(client, cfg, deps) {
|
|
66311
|
+
if (!deps.connect) {
|
|
66312
|
+
const key = poolKey(cfg);
|
|
66313
|
+
const entry = pools.get(key);
|
|
66314
|
+
if (entry && entry.client === client) {
|
|
66315
|
+
if (entry.idle) clearTimeout(entry.idle);
|
|
66316
|
+
pools.delete(key);
|
|
66317
|
+
}
|
|
66318
|
+
}
|
|
66319
|
+
try {
|
|
66320
|
+
client.close?.();
|
|
66321
|
+
} catch {
|
|
66322
|
+
}
|
|
66323
|
+
}
|
|
66205
66324
|
async function dropAllPools() {
|
|
66206
66325
|
await Promise.all([...pools.keys()].map((k) => dropPool(k)));
|
|
66207
66326
|
}
|
|
@@ -67200,6 +67319,7 @@ var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED,
|
|
|
67200
67319
|
var init_imapClient = __esm({
|
|
67201
67320
|
"src/services/imapClient.ts"() {
|
|
67202
67321
|
"use strict";
|
|
67322
|
+
init_callStop();
|
|
67203
67323
|
import_imapflow = __toESM(require_imap_flow(), 1);
|
|
67204
67324
|
init_smtpMailer();
|
|
67205
67325
|
init_docsUrls();
|
|
@@ -82615,6 +82735,7 @@ function getByIdScanThreshold() {
|
|
|
82615
82735
|
return 5e4;
|
|
82616
82736
|
}
|
|
82617
82737
|
var BYID_SCAN_BUDGET_SECONDS = 9;
|
|
82738
|
+
var BYID_PROBE_TIMEOUT_SECONDS = 3;
|
|
82618
82739
|
var BYID_COVERAGE_MARKER = "COV";
|
|
82619
82740
|
var SCOPED_MISS_MARKER = "SCOPED";
|
|
82620
82741
|
function describeUnscopedMiss(id, raw) {
|
|
@@ -84416,16 +84537,20 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84416
84537
|
*/
|
|
84417
84538
|
unscopedByIdScript(id, innerAction, opts = {}) {
|
|
84418
84539
|
const threshold = getByIdScanThreshold();
|
|
84419
|
-
const sizeGuard = (mbVar, acctNameExpr) => threshold > 0 ? `set _mbCount to
|
|
84540
|
+
const sizeGuard = (mbVar, acctNameExpr) => threshold > 0 ? `set _mbCount to -1
|
|
84420
84541
|
try
|
|
84421
|
-
|
|
84542
|
+
with timeout of ${BYID_PROBE_TIMEOUT_SECONDS} seconds
|
|
84543
|
+
set _mbCount to count of messages of ${mbVar}
|
|
84544
|
+
end timeout
|
|
84422
84545
|
end try
|
|
84423
|
-
if _mbCount > ${threshold} then
|
|
84546
|
+
if _mbCount < 0 or _mbCount > ${threshold} then
|
|
84424
84547
|
set _skipPath to ""
|
|
84425
84548
|
try
|
|
84426
84549
|
${mailboxPathFragment(mbVar, "_skipPath")}
|
|
84427
84550
|
end try
|
|
84428
|
-
set
|
|
84551
|
+
set _sizeLabel to "size unknown (count probe exceeded ${BYID_PROBE_TIMEOUT_SECONDS}s)"
|
|
84552
|
+
if _mbCount > -1 then set _sizeLabel to (_mbCount as string)
|
|
84553
|
+
set _skipped to _skipped & ${acctNameExpr} & " / " & _skipPath & " (" & _sizeLabel & ")${DIAG_ITEM_SEP}"
|
|
84429
84554
|
set _probe to false
|
|
84430
84555
|
end if` : "";
|
|
84431
84556
|
const budgetCheck = `if ((current date) - _startedAt) > ${BYID_SCAN_BUDGET_SECONDS} then
|
|
@@ -87600,10 +87725,14 @@ function currentCallTiming() {
|
|
|
87600
87725
|
return callTiming.getStore();
|
|
87601
87726
|
}
|
|
87602
87727
|
function withErrorHandling(handler, errorPrefix) {
|
|
87603
|
-
return async (params) => {
|
|
87728
|
+
return async (params, extra) => {
|
|
87604
87729
|
const arrivedAt = Date.now();
|
|
87730
|
+
const signal = extra?.signal;
|
|
87605
87731
|
return serializeAppleScript(async () => {
|
|
87606
|
-
|
|
87732
|
+
if (signal?.aborted) {
|
|
87733
|
+
return errorResponse(`${errorPrefix}: request was cancelled before it started`);
|
|
87734
|
+
}
|
|
87735
|
+
const timing = { arrivedAt, queueWaitMs: Date.now() - arrivedAt, signal };
|
|
87607
87736
|
return callTiming.run(timing, async () => {
|
|
87608
87737
|
try {
|
|
87609
87738
|
return await handler(params);
|
|
@@ -87864,6 +87993,9 @@ function unlistableStoreError(account, error3, knownAccounts) {
|
|
|
87864
87993
|
return parts.join("\n\n");
|
|
87865
87994
|
}
|
|
87866
87995
|
|
|
87996
|
+
// src/index.ts
|
|
87997
|
+
init_callStop();
|
|
87998
|
+
|
|
87867
87999
|
// src/tools/batchResults.ts
|
|
87868
88000
|
async function hybridBatchCounts(ids, appleFn, imapFn) {
|
|
87869
88001
|
const distinctIds = [...new Set(ids)];
|
|
@@ -88042,12 +88174,17 @@ function mergeMessages(imapRows, appleRows, limit) {
|
|
|
88042
88174
|
}
|
|
88043
88175
|
async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapConfigs()) {
|
|
88044
88176
|
const rows = [];
|
|
88177
|
+
const timedOutMailboxes = [];
|
|
88045
88178
|
const accountsQueried = [];
|
|
88046
88179
|
const accountsFailed = [];
|
|
88047
88180
|
const failedMailboxes = [];
|
|
88048
88181
|
const failedMailboxReasons = {};
|
|
88049
88182
|
const omittedMessages = [];
|
|
88050
88183
|
for (const config2 of configs) {
|
|
88184
|
+
if (deps.signal?.aborted) {
|
|
88185
|
+
timedOutMailboxes.push(`${config2.accountLabel} / *`);
|
|
88186
|
+
continue;
|
|
88187
|
+
}
|
|
88051
88188
|
const perAccountArgs = { ...args, account: void 0 };
|
|
88052
88189
|
try {
|
|
88053
88190
|
const res = kind === "search" ? await imapSearchMessages(perAccountArgs, { ...deps, config: config2 }) : await imapListMessages(perAccountArgs, { ...deps, config: config2 });
|
|
@@ -88059,6 +88196,9 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
|
|
|
88059
88196
|
for (const [mailbox, reason] of Object.entries(res.failedMailboxReasons)) {
|
|
88060
88197
|
failedMailboxReasons[`${config2.accountLabel} / ${mailbox}`] = reason;
|
|
88061
88198
|
}
|
|
88199
|
+
timedOutMailboxes.push(
|
|
88200
|
+
...(res.timedOutMailboxes ?? []).map((mailbox) => `${config2.accountLabel} / ${mailbox}`)
|
|
88201
|
+
);
|
|
88062
88202
|
omittedMessages.push(
|
|
88063
88203
|
...(res.omittedMessages ?? []).map((o) => ({
|
|
88064
88204
|
...o,
|
|
@@ -88076,7 +88216,8 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
|
|
|
88076
88216
|
accountsFailed,
|
|
88077
88217
|
failedMailboxes,
|
|
88078
88218
|
failedMailboxReasons,
|
|
88079
|
-
omittedMessages
|
|
88219
|
+
omittedMessages,
|
|
88220
|
+
timedOutMailboxes
|
|
88080
88221
|
};
|
|
88081
88222
|
}
|
|
88082
88223
|
function configMatchesAccount(config2, account) {
|
|
@@ -88694,7 +88835,11 @@ var LIST_OUTPUT_SCHEMA = {
|
|
|
88694
88835
|
failedMailboxReasons: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
88695
88836
|
// Messages that belong on the page but could not be read, each with its
|
|
88696
88837
|
// imap: id and why (#256 follow-up). Non-empty implies `partial: true`.
|
|
88697
|
-
omittedMessages: external_exports.array(external_exports.object({ id: external_exports.string(), mailbox: external_exports.string(), uid: external_exports.number(), reason: external_exports.string() })).optional()
|
|
88838
|
+
omittedMessages: external_exports.array(external_exports.object({ id: external_exports.string(), mailbox: external_exports.string(), uid: external_exports.number(), reason: external_exports.string() })).optional(),
|
|
88839
|
+
// Mailboxes a cancelled or out-of-time search-messages never finished (#276),
|
|
88840
|
+
// and why. Non-empty implies `partial: true`.
|
|
88841
|
+
timedOutMailboxes: external_exports.array(external_exports.string()).optional(),
|
|
88842
|
+
stoppedBy: external_exports.enum(["deadline", "cancelled"]).optional()
|
|
88698
88843
|
};
|
|
88699
88844
|
var BATCH_COUNT_OUTPUT_SCHEMA = {
|
|
88700
88845
|
ok: external_exports.boolean().optional(),
|
|
@@ -88769,11 +88914,12 @@ function appleScanForAccounts(accounts, scan) {
|
|
|
88769
88914
|
}
|
|
88770
88915
|
return { rows, diagnostics };
|
|
88771
88916
|
}
|
|
88772
|
-
function mergedMessageResponse(fan, apple, limit, verb) {
|
|
88917
|
+
function mergedMessageResponse(fan, apple, limit, verb, stopSignal) {
|
|
88773
88918
|
const merged = mergeMessages(fan.rows, apple.rows, limit);
|
|
88919
|
+
const timedOutMailboxes = fan.timedOutMailboxes ?? [];
|
|
88774
88920
|
const diagnostics = {
|
|
88775
88921
|
...apple.diagnostics,
|
|
88776
|
-
partial: apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0 || fan.omittedMessages.length > 0,
|
|
88922
|
+
partial: apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0 || fan.omittedMessages.length > 0 || timedOutMailboxes.length > 0,
|
|
88777
88923
|
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
|
|
88778
88924
|
notSearchedMailboxes: [...apple.diagnostics.notSearchedMailboxes, ...fan.failedMailboxes]
|
|
88779
88925
|
};
|
|
@@ -88786,9 +88932,13 @@ function mergedMessageResponse(fan, apple, limit, verb) {
|
|
|
88786
88932
|
timedOutAccounts: diagnostics.timedOutAccounts,
|
|
88787
88933
|
failedMailboxes: fan.failedMailboxes,
|
|
88788
88934
|
failedMailboxReasons: fan.failedMailboxReasons,
|
|
88789
|
-
omittedMessages: fan.omittedMessages
|
|
88935
|
+
omittedMessages: fan.omittedMessages,
|
|
88936
|
+
...timedOutMailboxes.length > 0 ? {
|
|
88937
|
+
timedOutMailboxes,
|
|
88938
|
+
stoppedBy: stoppedByDeadline(stopSignal) ? "deadline" : "cancelled"
|
|
88939
|
+
} : {}
|
|
88790
88940
|
};
|
|
88791
|
-
const coverageBlock = partialCoverageBlock(diagnostics) + omittedNote(fan.omittedMessages, true);
|
|
88941
|
+
const coverageBlock = partialCoverageBlock(diagnostics) + omittedNote(fan.omittedMessages, true) + stoppedNote(timedOutMailboxes, stopSignal);
|
|
88792
88942
|
if (merged.length === 0) {
|
|
88793
88943
|
const base2 = diagnostics.partial ? `No messages found in the portions that were ${verb === "matched" ? "searched" : "listed"}.` : "No messages found";
|
|
88794
88944
|
return successResponse(`${base2}${coverageBlock}`, structured);
|
|
@@ -88835,7 +88985,7 @@ function collectForensics(tool, args) {
|
|
|
88835
88985
|
registerTool(
|
|
88836
88986
|
"search-messages",
|
|
88837
88987
|
{
|
|
88838
|
-
description: "Use when: finding messages by query/sender/subject/date/read/flag filters and you need their ids for follow-up operations.\nReturns: matching messages with id, date, subject, sender, and read state (plus partial-coverage diagnostics when some mailboxes were skipped).\nDo not use when: you want a plain mailbox listing without filters (use list-messages), already have an id and want the body (use get-message), or want a whole conversation (use get-thread).\nPrefer this first to obtain the message ids that get-message/mark-as-read/delete-message/move-message and the batch tools require.",
|
|
88988
|
+
description: "Use when: finding messages by query/sender/subject/date/read/flag filters and you need their ids for follow-up operations.\nReturns: matching messages with id, date, subject, sender, and read state (plus partial-coverage diagnostics when some mailboxes were skipped). An IMAP search stops at its deadline (APPLE_MAIL_MCP_SEARCH_DEADLINE_MS, default 45s) or when the request is cancelled, returning the matches found so far with partial: true and timedOutMailboxes \u2014 narrow with mailbox/dateFrom rather than repeating it.\nDo not use when: you want a plain mailbox listing without filters (use list-messages), already have an id and want the body (use get-message), or want a whole conversation (use get-thread).\nPrefer this first to obtain the message ids that get-message/mark-as-read/delete-message/move-message and the batch tools require.",
|
|
88839
88989
|
inputSchema: {
|
|
88840
88990
|
query: external_exports.string().optional().describe("Text to search for in subject or sender"),
|
|
88841
88991
|
body: external_exports.string().optional().describe(
|
|
@@ -88878,6 +89028,15 @@ registerTool(
|
|
|
88878
89028
|
if (tooDeep) return errorResponse(tooDeep);
|
|
88879
89029
|
}
|
|
88880
89030
|
if (shouldUseImap(account)) {
|
|
89031
|
+
const stop = createCallStop(currentCallTiming(), searchDeadlineMs());
|
|
89032
|
+
try {
|
|
89033
|
+
return await searchViaImap(stop.signal);
|
|
89034
|
+
} finally {
|
|
89035
|
+
stop.dispose();
|
|
89036
|
+
}
|
|
89037
|
+
}
|
|
89038
|
+
return searchViaAppleScript();
|
|
89039
|
+
async function searchViaImap(signal) {
|
|
88881
89040
|
const imapArgs = {
|
|
88882
89041
|
query,
|
|
88883
89042
|
body,
|
|
@@ -88892,14 +89051,15 @@ registerTool(
|
|
|
88892
89051
|
isFlagged
|
|
88893
89052
|
};
|
|
88894
89053
|
if (account !== void 0) {
|
|
88895
|
-
const r = await imapSearchMessages({ ...imapArgs, account });
|
|
89054
|
+
const r = await imapSearchMessages({ ...imapArgs, account }, { signal });
|
|
88896
89055
|
return successResponse(r.text, {
|
|
88897
89056
|
messages: r.messages,
|
|
88898
89057
|
count: r.count,
|
|
88899
89058
|
partial: r.partial,
|
|
88900
89059
|
failedMailboxes: r.failedMailboxes,
|
|
88901
89060
|
failedMailboxReasons: r.failedMailboxReasons,
|
|
88902
|
-
omittedMessages: r.omittedMessages
|
|
89061
|
+
omittedMessages: r.omittedMessages,
|
|
89062
|
+
...r.timedOutMailboxes?.length ? { timedOutMailboxes: r.timedOutMailboxes, stoppedBy: r.stoppedBy } : {}
|
|
88903
89063
|
});
|
|
88904
89064
|
}
|
|
88905
89065
|
const { appleScriptOnly } = partitionAccountsForCounts(
|
|
@@ -88911,7 +89071,7 @@ registerTool(
|
|
|
88911
89071
|
`offset is supported only on IMAP accounts, and ${appleScriptOnly.map((a) => `"${a.name}"`).join(", ")} ${appleScriptOnly.length === 1 ? "is" : "are"} AppleScript-only. Pass an IMAP account to page, or omit offset.`
|
|
88912
89072
|
);
|
|
88913
89073
|
}
|
|
88914
|
-
const fan = await fanOutImapMessages(imapArgs, "search");
|
|
89074
|
+
const fan = await fanOutImapMessages(imapArgs, "search", { signal });
|
|
88915
89075
|
if (body) {
|
|
88916
89076
|
const apple2 = {
|
|
88917
89077
|
rows: [],
|
|
@@ -88923,7 +89083,18 @@ registerTool(
|
|
|
88923
89083
|
)
|
|
88924
89084
|
}
|
|
88925
89085
|
};
|
|
88926
|
-
return mergedMessageResponse(fan, apple2, limit, "matched");
|
|
89086
|
+
return mergedMessageResponse(fan, apple2, limit, "matched", signal);
|
|
89087
|
+
}
|
|
89088
|
+
if (signal.aborted && appleScriptOnly.length > 0) {
|
|
89089
|
+
const apple2 = {
|
|
89090
|
+
rows: [],
|
|
89091
|
+
diagnostics: {
|
|
89092
|
+
...emptyDiagnostics(),
|
|
89093
|
+
partial: true,
|
|
89094
|
+
timedOutAccounts: appleScriptOnly.map((a) => a.name)
|
|
89095
|
+
}
|
|
89096
|
+
};
|
|
89097
|
+
return mergedMessageResponse(fan, apple2, limit, "matched", signal);
|
|
88927
89098
|
}
|
|
88928
89099
|
const apple = appleScanForAccounts(
|
|
88929
89100
|
appleScriptOnly,
|
|
@@ -88940,51 +89111,53 @@ registerTool(
|
|
|
88940
89111
|
isFlagged
|
|
88941
89112
|
)
|
|
88942
89113
|
);
|
|
88943
|
-
return mergedMessageResponse(fan, apple, limit, "matched");
|
|
89114
|
+
return mergedMessageResponse(fan, apple, limit, "matched", signal);
|
|
88944
89115
|
}
|
|
88945
|
-
|
|
88946
|
-
|
|
88947
|
-
|
|
89116
|
+
function searchViaAppleScript() {
|
|
89117
|
+
if (offset > 0) {
|
|
89118
|
+
return errorResponse(
|
|
89119
|
+
`offset is supported only on IMAP accounts; IMAP is not configured for ${account ? `account "${account}"` : "any account"}. Omit offset, or narrow the search with dateFrom/dateTo instead.`
|
|
89120
|
+
);
|
|
89121
|
+
}
|
|
89122
|
+
if (body) {
|
|
89123
|
+
return errorResponse(
|
|
89124
|
+
`Body search requires the IMAP backend, which is not configured for ${account ? `account "${account}"` : "any account"}. Configure IMAP (see the IMAP backend section of the README) or search by query/subject/from instead.`
|
|
89125
|
+
);
|
|
89126
|
+
}
|
|
89127
|
+
const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(
|
|
89128
|
+
query,
|
|
89129
|
+
mailbox,
|
|
89130
|
+
account,
|
|
89131
|
+
limit,
|
|
89132
|
+
dateFrom,
|
|
89133
|
+
dateTo,
|
|
89134
|
+
from,
|
|
89135
|
+
subject,
|
|
89136
|
+
isRead,
|
|
89137
|
+
isFlagged
|
|
88948
89138
|
);
|
|
88949
|
-
|
|
88950
|
-
|
|
88951
|
-
|
|
88952
|
-
|
|
89139
|
+
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
89140
|
+
const structured = {
|
|
89141
|
+
messages: messages.map(messageSummary),
|
|
89142
|
+
count: messages.length,
|
|
89143
|
+
partial: diagnostics.partial,
|
|
89144
|
+
skippedLargeMailboxes: diagnostics.skippedLargeMailboxes,
|
|
89145
|
+
notSearchedMailboxes: diagnostics.notSearchedMailboxes,
|
|
89146
|
+
timedOutAccounts: diagnostics.timedOutAccounts
|
|
89147
|
+
};
|
|
89148
|
+
if (messages.length === 0) {
|
|
89149
|
+
const base2 = diagnostics.partial ? "No messages found in the portions that were searched." : "No messages found matching criteria";
|
|
89150
|
+
return successResponse(`${base2}${coverageBlock}`, structured);
|
|
89151
|
+
}
|
|
89152
|
+
const messageList = messages.map(
|
|
89153
|
+
(m) => ` - ID: ${m.id} | ${m.dateReceived.toLocaleDateString()} | ${m.subject} (from: ${m.sender}) [${m.isRead ? "read" : "unread"}]`
|
|
89154
|
+
).join("\n");
|
|
89155
|
+
return successResponse(
|
|
89156
|
+
`Found ${messages.length} message(s):
|
|
89157
|
+
${messageList}${coverageBlock}`,
|
|
89158
|
+
structured
|
|
88953
89159
|
);
|
|
88954
89160
|
}
|
|
88955
|
-
const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(
|
|
88956
|
-
query,
|
|
88957
|
-
mailbox,
|
|
88958
|
-
account,
|
|
88959
|
-
limit,
|
|
88960
|
-
dateFrom,
|
|
88961
|
-
dateTo,
|
|
88962
|
-
from,
|
|
88963
|
-
subject,
|
|
88964
|
-
isRead,
|
|
88965
|
-
isFlagged
|
|
88966
|
-
);
|
|
88967
|
-
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
88968
|
-
const structured = {
|
|
88969
|
-
messages: messages.map(messageSummary),
|
|
88970
|
-
count: messages.length,
|
|
88971
|
-
partial: diagnostics.partial,
|
|
88972
|
-
skippedLargeMailboxes: diagnostics.skippedLargeMailboxes,
|
|
88973
|
-
notSearchedMailboxes: diagnostics.notSearchedMailboxes,
|
|
88974
|
-
timedOutAccounts: diagnostics.timedOutAccounts
|
|
88975
|
-
};
|
|
88976
|
-
if (messages.length === 0) {
|
|
88977
|
-
const base2 = diagnostics.partial ? "No messages found in the portions that were searched." : "No messages found matching criteria";
|
|
88978
|
-
return successResponse(`${base2}${coverageBlock}`, structured);
|
|
88979
|
-
}
|
|
88980
|
-
const messageList = messages.map(
|
|
88981
|
-
(m) => ` - ID: ${m.id} | ${m.dateReceived.toLocaleDateString()} | ${m.subject} (from: ${m.sender}) [${m.isRead ? "read" : "unread"}]`
|
|
88982
|
-
).join("\n");
|
|
88983
|
-
return successResponse(
|
|
88984
|
-
`Found ${messages.length} message(s):
|
|
88985
|
-
${messageList}${coverageBlock}`,
|
|
88986
|
-
structured
|
|
88987
|
-
);
|
|
88988
89161
|
},
|
|
88989
89162
|
"Error searching messages"
|
|
88990
89163
|
)
|
package/docs/IMAP-SETUP.md
CHANGED
|
@@ -346,29 +346,30 @@ GUI is ignoring.
|
|
|
346
346
|
|
|
347
347
|
## Full environment variable reference
|
|
348
348
|
|
|
349
|
-
| Variable | Purpose
|
|
350
|
-
| ------------------------------------------------------------ |
|
|
351
|
-
| `APPLE_MAIL_MCP_DEFAULT_ACCOUNT` | Account used when a tool omits `account` (name or email).
|
|
352
|
-
| `APPLE_MAIL_MCP_IMAP_USER` | Primary IMAP login. Setting it enables IMAP — but so does `APPLE_MAIL_MCP_IMAP_ACCOUNTS` on its own; either is sufficient.
|
|
353
|
-
| `APPLE_MAIL_MCP_IMAP_ACCOUNT` | Mail.app account name to match for routing (default = USER).
|
|
354
|
-
| `APPLE_MAIL_MCP_IMAP_HOST` | IMAP host (default `imap.gmail.com`).
|
|
355
|
-
| `APPLE_MAIL_MCP_IMAP_PORT` | IMAP port (default `993`, implicit TLS).
|
|
356
|
-
| `APPLE_MAIL_MCP_IMAP_PASSWORD` | Password (discouraged; prefer Keychain).
|
|
357
|
-
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | Keychain item service/server name.
|
|
358
|
-
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | Keychain item account (default = USER).
|
|
359
|
-
| `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | JSON array of accounts (multi-account). Sufficient on its own; also enables IMAP.
|
|
360
|
-
| `APPLE_MAIL_MCP_IMAP_IDLE` | `1` to enable IMAP IDLE new-mail push.
|
|
361
|
-
| `APPLE_MAIL_MCP_IMAP_IDLE_MS` | Pooled-connection idle timeout in ms (default `30000`; `0` = never close).
|
|
362
|
-
| `APPLE_MAIL_MCP_STATS_BUDGET_MS` | Per-account wall-clock budget for `get-mail-stats` in ms (default `25000`, minimum `1000`).
|
|
363
|
-
| `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | Overall wall-clock deadline for one `get-mail-stats` call in ms (default `50000`, minimum `2000`) — measured from when the request arrived, and covering time spent queued behind other tool calls as well as the Mail.app account enumeration and every per-account read. Keep it below your MCP client's request timeout.
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
366
|
-
| `
|
|
367
|
-
| `
|
|
368
|
-
| `
|
|
369
|
-
| `
|
|
370
|
-
| `
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
349
|
+
| Variable | Purpose |
|
|
350
|
+
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
351
|
+
| `APPLE_MAIL_MCP_DEFAULT_ACCOUNT` | Account used when a tool omits `account` (name or email). |
|
|
352
|
+
| `APPLE_MAIL_MCP_IMAP_USER` | Primary IMAP login. Setting it enables IMAP — but so does `APPLE_MAIL_MCP_IMAP_ACCOUNTS` on its own; either is sufficient. |
|
|
353
|
+
| `APPLE_MAIL_MCP_IMAP_ACCOUNT` | Mail.app account name to match for routing (default = USER). |
|
|
354
|
+
| `APPLE_MAIL_MCP_IMAP_HOST` | IMAP host (default `imap.gmail.com`). |
|
|
355
|
+
| `APPLE_MAIL_MCP_IMAP_PORT` | IMAP port (default `993`, implicit TLS). |
|
|
356
|
+
| `APPLE_MAIL_MCP_IMAP_PASSWORD` | Password (discouraged; prefer Keychain). |
|
|
357
|
+
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | Keychain item service/server name. |
|
|
358
|
+
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | Keychain item account (default = USER). |
|
|
359
|
+
| `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | JSON array of accounts (multi-account). Sufficient on its own; also enables IMAP. |
|
|
360
|
+
| `APPLE_MAIL_MCP_IMAP_IDLE` | `1` to enable IMAP IDLE new-mail push. |
|
|
361
|
+
| `APPLE_MAIL_MCP_IMAP_IDLE_MS` | Pooled-connection idle timeout in ms (default `30000`; `0` = never close). |
|
|
362
|
+
| `APPLE_MAIL_MCP_STATS_BUDGET_MS` | Per-account wall-clock budget for `get-mail-stats` in ms (default `25000`, minimum `1000`). |
|
|
363
|
+
| `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | Overall wall-clock deadline for one `get-mail-stats` call in ms (default `50000`, minimum `2000`) — measured from when the request arrived, and covering time spent queued behind other tool calls as well as the Mail.app account enumeration and every per-account read. Keep it below your MCP client's request timeout. |
|
|
364
|
+
| `APPLE_MAIL_MCP_SEARCH_DEADLINE_MS` | Overall wall-clock deadline for one IMAP `search-messages` call in ms (default `45000`, minimum `2000`), measured from when the request arrived. On expiry — or when the client cancels the request — the search abandons the mailbox in flight and returns partial results plus `timedOutMailboxes`. Keep it below your MCP client's request timeout. |
|
|
365
|
+
| `APPLE_MAIL_MCP_SMTP_HOST` | SMTP host; setting it enables `transport:"smtp"`. |
|
|
366
|
+
| `APPLE_MAIL_MCP_SMTP_PORT` | SMTP port (`465` if secure, else `587`). |
|
|
367
|
+
| `APPLE_MAIL_MCP_SMTP_SECURE` | `true` for implicit TLS (465); else STARTTLS. |
|
|
368
|
+
| `APPLE_MAIL_MCP_SMTP_USER` / `_FROM` | SMTP login / From address. |
|
|
369
|
+
| `APPLE_MAIL_MCP_SMTP_PASSWORD` | Password (discouraged; prefer Keychain). |
|
|
370
|
+
| `APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE` / `_KEYCHAIN_ACCOUNT` | SMTP Keychain reference. |
|
|
371
|
+
| `APPLE_MAIL_MCP_CONFIG_FILE` | Path to the config JSON (default app-support dir). |
|
|
372
|
+
| `APPLE_MAIL_MCP_TEMPLATES_FILE` | Email-templates store path (default `~/Library/Application Support/apple-mail-mcp/templates.json`). |
|
|
373
|
+
| `APPLE_MAIL_MCP_MAX_BUFFER` | Max AppleScript (`osascript`) output buffer in bytes (default 64 MiB). |
|
|
374
|
+
| `APPLE_MAIL_MAX_SEARCH_MAILBOX` | Per-mailbox message-count guard for unscoped AppleScript search (default `5000`; `0` disables). Note: no `_MCP` in the name. |
|
|
375
|
+
| `APPLE_MAIL_MAX_BYID_SCAN_MAILBOX` | Per-mailbox message-count guard for the cross-mailbox scan a numeric message id falls back to (default `50000`; `0` disables). Skipped mailboxes are named in the not-found error. Note: no `_MCP` in the name. |
|
package/package.json
CHANGED