apple-mail-mcp 2.10.6 → 2.10.8
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 +17 -1
- package/build/index.js +153 -41
- package/docs/IMAP-SETUP.md +17 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -486,6 +486,7 @@ for an explicitly-named IMAP account, never on an omitted account.
|
|
|
486
486
|
| `APPLE_MAIL_MCP_IMAP_IDLE` | No | `0` | Set `1` to enable IMAP IDLE push notifications (new-mail alerts) for every configured account |
|
|
487
487
|
| `APPLE_MAIL_MCP_IMAP_IDLE_MS` | No | `30000` | Idle timeout (ms) before a pooled IMAP connection is closed (`0` = never close) |
|
|
488
488
|
| `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 |
|
|
489
|
+
| `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | No | `50000` | Overall wall-clock deadline for one `get-mail-stats` call (minimum `2000`), covering account enumeration **and** every per-account read. Keep it below your client's request timeout |
|
|
489
490
|
|
|
490
491
|
**Multiple IMAP accounts (C2):** set `APPLE_MAIL_MCP_IMAP_ACCOUNTS` to a JSON array, e.g.
|
|
491
492
|
`[{"account":"Work","user":"me@co.com","host":"imap.co.com","keychainService":"imap.co.com"}]`.
|
|
@@ -775,9 +776,15 @@ Move a message to a different mailbox.
|
|
|
775
776
|
| Parameter | Type | Required | Description |
|
|
776
777
|
|-----------|------|----------|-------------|
|
|
777
778
|
| `id` | string | Yes | Message ID |
|
|
778
|
-
| `mailbox` | string | Yes | Destination mailbox |
|
|
779
|
+
| `mailbox` | string | Yes | Destination mailbox — full path (`Work/Archive`) or a leaf name that is unique on the account |
|
|
779
780
|
| `account` | string | No | Account containing mailbox |
|
|
780
781
|
|
|
782
|
+
A destination is matched first as a full path, then as a leaf name. If a leaf
|
|
783
|
+
name matches **more than one** mailbox (e.g. `Archive` under both `Work` and
|
|
784
|
+
`Thornlands`), the move is refused with an error naming every candidate — pass
|
|
785
|
+
the full path. The same applies to `batch-move-messages`, `delete-mailbox` and
|
|
786
|
+
`rename-mailbox`.
|
|
787
|
+
|
|
781
788
|
---
|
|
782
789
|
|
|
783
790
|
#### `list-attachments`
|
|
@@ -1157,6 +1164,15 @@ overruns is reported via `partial: true` + `failedAccounts` rather than being
|
|
|
1157
1164
|
folded in as a silent zero; a scoped call to a single account returns an error
|
|
1158
1165
|
naming the budget instead. Raise the budget if you have a very large account.
|
|
1159
1166
|
|
|
1167
|
+
The whole call is additionally bounded by one wall-clock deadline,
|
|
1168
|
+
`APPLE_MAIL_MCP_STATS_DEADLINE_MS` (default `50000`), which covers the Mail.app
|
|
1169
|
+
account enumeration as well as every per-account read. Per-step budgets alone
|
|
1170
|
+
were not enough: their worst cases **add up**, and the sum could exceed a
|
|
1171
|
+
client's request timeout, so the call died with nothing returned instead of
|
|
1172
|
+
degrading. Keep the deadline below your MCP client's request timeout — whatever
|
|
1173
|
+
cannot be read inside it is named in `failedAccounts`, so you always get a
|
|
1174
|
+
partial answer rather than a dead call.
|
|
1175
|
+
|
|
1160
1176
|
---
|
|
1161
1177
|
|
|
1162
1178
|
#### `get-sync-status`
|
package/build/index.js
CHANGED
|
@@ -78388,6 +78388,12 @@ var AppleMailManager = class {
|
|
|
78388
78388
|
* presenting a fallback zero/empty as a real answer. (#130)
|
|
78389
78389
|
*/
|
|
78390
78390
|
lastAccountsError = null;
|
|
78391
|
+
/**
|
|
78392
|
+
* Same, for the mailbox listing — read via `listMailboxesChecked()`. Kept
|
|
78393
|
+
* separate from `lastAccountsError` so a failed mailbox read on one account
|
|
78394
|
+
* can't be misread as the account enumeration having failed. (#135)
|
|
78395
|
+
*/
|
|
78396
|
+
lastMailboxesError = null;
|
|
78391
78397
|
/**
|
|
78392
78398
|
* Remembers where each message id was last seen: id → {account, mailbox}.
|
|
78393
78399
|
*
|
|
@@ -78417,12 +78423,12 @@ var AppleMailManager = class {
|
|
|
78417
78423
|
/**
|
|
78418
78424
|
* Returns cached accounts or fetches fresh data if cache is expired/empty.
|
|
78419
78425
|
*/
|
|
78420
|
-
getCachedAccounts() {
|
|
78426
|
+
getCachedAccounts(options = {}) {
|
|
78421
78427
|
const now = Date.now();
|
|
78422
78428
|
if (this.cache.accounts && now < this.cache.accounts.expiry) {
|
|
78423
78429
|
return this.cache.accounts.data;
|
|
78424
78430
|
}
|
|
78425
|
-
const accounts = this.fetchAccounts();
|
|
78431
|
+
const accounts = this.fetchAccounts(options);
|
|
78426
78432
|
if (accounts === null) {
|
|
78427
78433
|
return this.cache.accounts?.data ?? [];
|
|
78428
78434
|
}
|
|
@@ -80187,7 +80193,7 @@ var AppleMailManager = class {
|
|
|
80187
80193
|
/**
|
|
80188
80194
|
* List all mailboxes for an account.
|
|
80189
80195
|
*/
|
|
80190
|
-
listMailboxes(account) {
|
|
80196
|
+
listMailboxes(account, options = {}) {
|
|
80191
80197
|
const targetAccount = this.resolveAccount(account);
|
|
80192
80198
|
const listCommand = `
|
|
80193
80199
|
set mailboxList to {}
|
|
@@ -80201,9 +80207,10 @@ var AppleMailManager = class {
|
|
|
80201
80207
|
return mailboxList as text
|
|
80202
80208
|
`;
|
|
80203
80209
|
const script = buildAccountScopedScript(targetAccount, listCommand);
|
|
80204
|
-
const result = executeAppleScript(script, { timeoutMs: 6e4 });
|
|
80210
|
+
const result = executeAppleScript(script, { timeoutMs: options.timeoutMs ?? 6e4 });
|
|
80205
80211
|
if (!result.success) {
|
|
80206
80212
|
console.error(`Failed to list mailboxes: ${result.error}`);
|
|
80213
|
+
this.lastMailboxesError = result.error ?? "AppleScript transport failed";
|
|
80207
80214
|
return [];
|
|
80208
80215
|
}
|
|
80209
80216
|
if (!result.output.trim()) return [];
|
|
@@ -80221,6 +80228,20 @@ var AppleMailManager = class {
|
|
|
80221
80228
|
}
|
|
80222
80229
|
return mailboxes;
|
|
80223
80230
|
}
|
|
80231
|
+
/**
|
|
80232
|
+
* listMailboxes() plus whether the underlying AppleScript read actually worked.
|
|
80233
|
+
*
|
|
80234
|
+
* An empty list is ambiguous on its own — Mail with no mailboxes and a timed-out
|
|
80235
|
+
* transport both produce `[]`, and folding the second into a total as 0 is the
|
|
80236
|
+
* silent-zero class #130 fixed elsewhere. A caller summing counts across
|
|
80237
|
+
* accounts needs to tell them apart. (#135)
|
|
80238
|
+
*/
|
|
80239
|
+
listMailboxesChecked(account, options = {}) {
|
|
80240
|
+
this.lastMailboxesError = null;
|
|
80241
|
+
const mailboxes = this.listMailboxes(account, options);
|
|
80242
|
+
const error2 = this.lastMailboxesError;
|
|
80243
|
+
return error2 ? { mailboxes, failed: true, error: error2 } : { mailboxes, failed: false };
|
|
80244
|
+
}
|
|
80224
80245
|
/**
|
|
80225
80246
|
* Get unread count for a mailbox.
|
|
80226
80247
|
*/
|
|
@@ -80756,18 +80777,22 @@ end tell`;
|
|
|
80756
80777
|
/**
|
|
80757
80778
|
* List all mail accounts (uses cache).
|
|
80758
80779
|
*/
|
|
80759
|
-
listAccounts() {
|
|
80760
|
-
return this.getCachedAccounts();
|
|
80780
|
+
listAccounts(options = {}) {
|
|
80781
|
+
return this.getCachedAccounts(options);
|
|
80761
80782
|
}
|
|
80762
80783
|
/**
|
|
80763
80784
|
* listAccounts() plus whether the underlying AppleScript read actually worked.
|
|
80764
80785
|
*
|
|
80765
80786
|
* `failed: true` means the list is a fallback (stale cache or empty) because the
|
|
80766
80787
|
* transport errored — NOT that Mail has no accounts. (#130)
|
|
80788
|
+
*
|
|
80789
|
+
* `timeoutMs` bounds the AppleScript read when the cache is cold, so a caller
|
|
80790
|
+
* working to an overall deadline can spend a known slice here instead of the
|
|
80791
|
+
* blanket 30s. A cache hit costs nothing and ignores it. (#135)
|
|
80767
80792
|
*/
|
|
80768
|
-
listAccountsChecked() {
|
|
80793
|
+
listAccountsChecked(options = {}) {
|
|
80769
80794
|
this.lastAccountsError = null;
|
|
80770
|
-
const accounts = this.getCachedAccounts();
|
|
80795
|
+
const accounts = this.getCachedAccounts(options);
|
|
80771
80796
|
const error2 = this.lastAccountsError;
|
|
80772
80797
|
return error2 ? { accounts, failed: true, error: error2 } : { accounts, failed: false };
|
|
80773
80798
|
}
|
|
@@ -80792,7 +80817,7 @@ end tell`;
|
|
|
80792
80817
|
* "Mail answered, and there genuinely are no accounts" — collapsing the two is
|
|
80793
80818
|
* what let a wedged transport report a confident "No Mail accounts found". (#130)
|
|
80794
80819
|
*/
|
|
80795
|
-
fetchAccounts() {
|
|
80820
|
+
fetchAccounts(options = {}) {
|
|
80796
80821
|
const script = buildAppLevelScript(`
|
|
80797
80822
|
set accountList to {}
|
|
80798
80823
|
repeat with acct in accounts
|
|
@@ -80808,7 +80833,10 @@ end tell`;
|
|
|
80808
80833
|
set AppleScript's text item delimiters to "${RECORD_SEP}"
|
|
80809
80834
|
return accountList as text
|
|
80810
80835
|
`);
|
|
80811
|
-
const result = executeAppleScript(
|
|
80836
|
+
const result = executeAppleScript(
|
|
80837
|
+
script,
|
|
80838
|
+
options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {}
|
|
80839
|
+
);
|
|
80812
80840
|
if (!result.success) {
|
|
80813
80841
|
console.error(`Failed to list accounts: ${result.error}`);
|
|
80814
80842
|
this.lastAccountsError = result.error ?? "AppleScript transport failed";
|
|
@@ -81653,9 +81681,8 @@ function sameImapAccount(left, right, deps) {
|
|
|
81653
81681
|
if (aliases.has(left) && aliases.has(right)) return true;
|
|
81654
81682
|
}
|
|
81655
81683
|
const specs = listImapAccountSpecs();
|
|
81656
|
-
const
|
|
81657
|
-
const
|
|
81658
|
-
const rightSpec = specs.find((spec) => matches(right, spec));
|
|
81684
|
+
const leftSpec = specs.find((spec) => specMatchesSelector(spec, left));
|
|
81685
|
+
const rightSpec = specs.find((spec) => specMatchesSelector(spec, right));
|
|
81659
81686
|
return leftSpec !== void 0 && leftSpec === rightSpec;
|
|
81660
81687
|
}
|
|
81661
81688
|
function depsForAccount(account, deps) {
|
|
@@ -81667,14 +81694,21 @@ function depsForAccount(account, deps) {
|
|
|
81667
81694
|
function depsForMessageRef(ref, deps) {
|
|
81668
81695
|
return depsForAccount(ref.account, deps);
|
|
81669
81696
|
}
|
|
81697
|
+
function specMatchesSelector(spec, selector) {
|
|
81698
|
+
return spec.accountLabel === selector || spec.user === selector || (spec.aliases?.includes(selector) ?? false);
|
|
81699
|
+
}
|
|
81670
81700
|
function str(v) {
|
|
81671
81701
|
return typeof v === "string" && v.trim() ? v.trim() : void 0;
|
|
81672
81702
|
}
|
|
81703
|
+
function imapIdentityKey(spec) {
|
|
81704
|
+
return `${spec.host.trim().toLowerCase()}:${spec.port}:${spec.user.trim()}`;
|
|
81705
|
+
}
|
|
81673
81706
|
function listImapAccountSpecs(env = process.env) {
|
|
81674
81707
|
const specs = [];
|
|
81708
|
+
const seen = /* @__PURE__ */ new Set();
|
|
81675
81709
|
const user = env[IMAP_ENV.user]?.trim();
|
|
81676
81710
|
if (user) {
|
|
81677
|
-
|
|
81711
|
+
const legacy = {
|
|
81678
81712
|
accountLabel: env[IMAP_ENV.account]?.trim() || user,
|
|
81679
81713
|
user,
|
|
81680
81714
|
host: env[IMAP_ENV.host]?.trim() || "imap.gmail.com",
|
|
@@ -81682,7 +81716,9 @@ function listImapAccountSpecs(env = process.env) {
|
|
|
81682
81716
|
password: env[IMAP_ENV.password],
|
|
81683
81717
|
keychainService: env[IMAP_ENV.keychainService]?.trim(),
|
|
81684
81718
|
keychainAccount: env[IMAP_ENV.keychainAccount]?.trim()
|
|
81685
|
-
}
|
|
81719
|
+
};
|
|
81720
|
+
specs.push(legacy);
|
|
81721
|
+
seen.add(imapIdentityKey(legacy));
|
|
81686
81722
|
}
|
|
81687
81723
|
const json = env[IMAP_ENV.accounts]?.trim();
|
|
81688
81724
|
if (json) {
|
|
@@ -81694,12 +81730,22 @@ function listImapAccountSpecs(env = process.env) {
|
|
|
81694
81730
|
const u = str(a.user);
|
|
81695
81731
|
if (!u) continue;
|
|
81696
81732
|
const label = str(a.account) || str(a.accountLabel) || u;
|
|
81697
|
-
|
|
81733
|
+
const host = str(a.host) || "imap.gmail.com";
|
|
81698
81734
|
const port = a.port ? Number(a.port) : 993;
|
|
81735
|
+
const key = imapIdentityKey({ host, port, user: u });
|
|
81736
|
+
if (seen.has(key)) {
|
|
81737
|
+
const owner = specs.find((s) => imapIdentityKey(s) === key);
|
|
81738
|
+
if (owner && owner.accountLabel !== label && !owner.aliases?.includes(label)) {
|
|
81739
|
+
(owner.aliases ??= []).push(label);
|
|
81740
|
+
}
|
|
81741
|
+
continue;
|
|
81742
|
+
}
|
|
81743
|
+
if (specs.some((s) => s.accountLabel === label)) continue;
|
|
81744
|
+
seen.add(key);
|
|
81699
81745
|
specs.push({
|
|
81700
81746
|
accountLabel: label,
|
|
81701
81747
|
user: u,
|
|
81702
|
-
host
|
|
81748
|
+
host,
|
|
81703
81749
|
port,
|
|
81704
81750
|
password: str(a.password),
|
|
81705
81751
|
keychainService: str(a.keychainService),
|
|
@@ -81737,7 +81783,7 @@ function specToConfig(spec) {
|
|
|
81737
81783
|
}
|
|
81738
81784
|
function isImapAccount(account, env = process.env) {
|
|
81739
81785
|
if (!account) return false;
|
|
81740
|
-
return listImapAccountSpecs(env).some((s) => s
|
|
81786
|
+
return listImapAccountSpecs(env).some((s) => specMatchesSelector(s, account));
|
|
81741
81787
|
}
|
|
81742
81788
|
function shouldUseImap(account, env = process.env) {
|
|
81743
81789
|
return listImapAccountSpecs(env).length > 0 && (account === void 0 || isImapAccount(account, env));
|
|
@@ -81760,12 +81806,12 @@ function resolveImapConfig(env = process.env, account) {
|
|
|
81760
81806
|
const specs = listImapAccountSpecs(env);
|
|
81761
81807
|
if (specs.length === 0) {
|
|
81762
81808
|
throw new Error(
|
|
81763
|
-
`IMAP not configured. Set ${IMAP_ENV.user} (login address) to enable it. ${SETUP_HINT}`
|
|
81809
|
+
`IMAP not configured. Set ${IMAP_ENV.user} (login address), or ${IMAP_ENV.accounts} for multiple accounts, to enable it. ${SETUP_HINT}`
|
|
81764
81810
|
);
|
|
81765
81811
|
}
|
|
81766
81812
|
let spec;
|
|
81767
81813
|
if (account) {
|
|
81768
|
-
spec = specs.find((s) => s
|
|
81814
|
+
spec = specs.find((s) => specMatchesSelector(s, account));
|
|
81769
81815
|
if (!spec) {
|
|
81770
81816
|
throw new Error(
|
|
81771
81817
|
`No IMAP account matching "${account}". Configured: ${specs.map((s) => s.accountLabel).join(", ")}.`
|
|
@@ -81977,7 +82023,7 @@ function errText(e) {
|
|
|
81977
82023
|
var poolConnect = defaultConnect;
|
|
81978
82024
|
var pools = /* @__PURE__ */ new Map();
|
|
81979
82025
|
function poolKey(cfg) {
|
|
81980
|
-
return
|
|
82026
|
+
return imapIdentityKey(cfg);
|
|
81981
82027
|
}
|
|
81982
82028
|
function imapIdleMs() {
|
|
81983
82029
|
const raw = process.env.APPLE_MAIL_MCP_IMAP_IDLE_MS;
|
|
@@ -82035,7 +82081,7 @@ async function acquirePooled(cfg) {
|
|
|
82035
82081
|
}
|
|
82036
82082
|
}
|
|
82037
82083
|
async function imapHealthCheck(deps = {}) {
|
|
82038
|
-
if (!deps.config &&
|
|
82084
|
+
if (!deps.config && listImapAccountSpecs().length === 0) {
|
|
82039
82085
|
return { configured: false, ok: false };
|
|
82040
82086
|
}
|
|
82041
82087
|
let cfg;
|
|
@@ -82095,13 +82141,26 @@ async function useClient(deps, fn, retryOnDrop = false) {
|
|
|
82095
82141
|
function withClient(deps, fn) {
|
|
82096
82142
|
return useClient(deps, fn);
|
|
82097
82143
|
}
|
|
82098
|
-
async function
|
|
82144
|
+
async function resolveMailbox(client, name) {
|
|
82099
82145
|
const wanted = name.trim().toLowerCase();
|
|
82100
82146
|
const boxes = await client.list();
|
|
82101
82147
|
const byPath = boxes.find((b) => b.path.toLowerCase() === wanted);
|
|
82102
|
-
if (byPath) return byPath.path;
|
|
82103
|
-
const byName = boxes.
|
|
82104
|
-
|
|
82148
|
+
if (byPath) return { kind: "found", path: byPath.path };
|
|
82149
|
+
const byName = boxes.filter((b) => b.name.toLowerCase() === wanted);
|
|
82150
|
+
if (byName.length === 1) return { kind: "found", path: byName[0].path };
|
|
82151
|
+
if (byName.length > 1) {
|
|
82152
|
+
return { kind: "ambiguous", candidates: byName.map((b) => b.path).sort() };
|
|
82153
|
+
}
|
|
82154
|
+
return { kind: "none" };
|
|
82155
|
+
}
|
|
82156
|
+
function ambiguousMailboxError(name, candidates, accountLabel) {
|
|
82157
|
+
const where = accountLabel ? ` on IMAP account ${accountLabel}` : "";
|
|
82158
|
+
return `Mailbox "${name}" is ambiguous${where} \u2014 it matches ${candidates.map((c) => `"${c}"`).join(" and ")}. Pass the full path.`;
|
|
82159
|
+
}
|
|
82160
|
+
async function findMailboxPathOrThrow(client, name) {
|
|
82161
|
+
const res = await resolveMailbox(client, name);
|
|
82162
|
+
if (res.kind === "ambiguous") throw new Error(ambiguousMailboxError(name, res.candidates));
|
|
82163
|
+
return res.kind === "found" ? res.path : null;
|
|
82105
82164
|
}
|
|
82106
82165
|
function imapCreateMailbox(name, deps = {}) {
|
|
82107
82166
|
return withClient(deps, async (client) => {
|
|
@@ -82115,13 +82174,20 @@ function imapCreateMailbox(name, deps = {}) {
|
|
|
82115
82174
|
}
|
|
82116
82175
|
function imapDeleteMailbox(name, deps = {}) {
|
|
82117
82176
|
return withClient(deps, async (client, cfg) => {
|
|
82118
|
-
const
|
|
82119
|
-
if (
|
|
82177
|
+
const res = await resolveMailbox(client, name);
|
|
82178
|
+
if (res.kind === "ambiguous") {
|
|
82179
|
+
return {
|
|
82180
|
+
success: false,
|
|
82181
|
+
error: ambiguousMailboxError(name, res.candidates, cfg.accountLabel)
|
|
82182
|
+
};
|
|
82183
|
+
}
|
|
82184
|
+
if (res.kind === "none") {
|
|
82120
82185
|
return {
|
|
82121
82186
|
success: false,
|
|
82122
82187
|
error: `Mailbox "${name}" not found on IMAP account ${cfg.accountLabel}.`
|
|
82123
82188
|
};
|
|
82124
82189
|
}
|
|
82190
|
+
const path = res.path;
|
|
82125
82191
|
try {
|
|
82126
82192
|
await client.mailboxDelete(path);
|
|
82127
82193
|
return {
|
|
@@ -82135,13 +82201,20 @@ function imapDeleteMailbox(name, deps = {}) {
|
|
|
82135
82201
|
}
|
|
82136
82202
|
function imapRenameMailbox(oldName, newName, deps = {}) {
|
|
82137
82203
|
return withClient(deps, async (client, cfg) => {
|
|
82138
|
-
const
|
|
82139
|
-
if (
|
|
82204
|
+
const found = await resolveMailbox(client, oldName);
|
|
82205
|
+
if (found.kind === "ambiguous") {
|
|
82206
|
+
return {
|
|
82207
|
+
success: false,
|
|
82208
|
+
error: ambiguousMailboxError(oldName, found.candidates, cfg.accountLabel)
|
|
82209
|
+
};
|
|
82210
|
+
}
|
|
82211
|
+
if (found.kind === "none") {
|
|
82140
82212
|
return {
|
|
82141
82213
|
success: false,
|
|
82142
82214
|
error: `Mailbox "${oldName}" not found on IMAP account ${cfg.accountLabel}.`
|
|
82143
82215
|
};
|
|
82144
82216
|
}
|
|
82217
|
+
const path = found.path;
|
|
82145
82218
|
try {
|
|
82146
82219
|
const res = await client.mailboxRename(path, newName);
|
|
82147
82220
|
return { success: true, info: `Renamed "${res.path}" to "${res.newPath}" via IMAP.` };
|
|
@@ -82274,8 +82347,15 @@ function imapUnflagMessage(id, deps = {}) {
|
|
|
82274
82347
|
async function imapMoveMessageById(id, destMailbox, deps = {}) {
|
|
82275
82348
|
const ref = decodeImapId(id);
|
|
82276
82349
|
if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
|
|
82277
|
-
return withClient(depsForMessageRef(ref, deps), async (client) => {
|
|
82278
|
-
const
|
|
82350
|
+
return withClient(depsForMessageRef(ref, deps), async (client, cfg) => {
|
|
82351
|
+
const dest = await resolveMailbox(client, destMailbox);
|
|
82352
|
+
if (dest.kind === "ambiguous") {
|
|
82353
|
+
return {
|
|
82354
|
+
success: false,
|
|
82355
|
+
error: ambiguousMailboxError(destMailbox, dest.candidates, cfg.accountLabel)
|
|
82356
|
+
};
|
|
82357
|
+
}
|
|
82358
|
+
const destPath = dest.kind === "found" ? dest.path : resolveMailboxPath(destMailbox, "list");
|
|
82279
82359
|
const lock = await client.getMailboxLock(ref.path);
|
|
82280
82360
|
try {
|
|
82281
82361
|
await client.messageMove([ref.uid], destPath, { uid: true });
|
|
@@ -82450,7 +82530,7 @@ var imapBatchDelete = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids, p
|
|
|
82450
82530
|
});
|
|
82451
82531
|
function imapBatchMove(ids, destMailbox, deps = {}) {
|
|
82452
82532
|
return imapBatch(ids, deps, async (c, uids) => {
|
|
82453
|
-
const dest = await
|
|
82533
|
+
const dest = await findMailboxPathOrThrow(c, destMailbox) ?? resolveMailboxPath(destMailbox, "list");
|
|
82454
82534
|
await c.messageMove(uids, dest, { uid: true });
|
|
82455
82535
|
});
|
|
82456
82536
|
}
|
|
@@ -82773,7 +82853,11 @@ async function runDoctor(mailManager2) {
|
|
|
82773
82853
|
checks.push({
|
|
82774
82854
|
name: `IMAP: ${label}`,
|
|
82775
82855
|
status: h.ok ? "ok" : "fail",
|
|
82776
|
-
|
|
82856
|
+
// `h.error` is optional on the health-check result, so interpolating it
|
|
82857
|
+
// bare printed the literal string "connection failed: undefined" for
|
|
82858
|
+
// any failure that carried no message (issue #138). Never render that:
|
|
82859
|
+
// an unexplained failure is still worth naming, but as words.
|
|
82860
|
+
detail: h.ok ? `connected to ${h.host}` : `connection failed: ${h.error ?? "the health check reported no detail"}. Check the Keychain password and host/port.`
|
|
82777
82861
|
});
|
|
82778
82862
|
}
|
|
82779
82863
|
}
|
|
@@ -85075,16 +85159,20 @@ registerTool(
|
|
|
85075
85159
|
},
|
|
85076
85160
|
withErrorHandling(async ({ account }) => {
|
|
85077
85161
|
const budgetMs = Math.max(1e3, Number(process.env.APPLE_MAIL_MCP_STATS_BUDGET_MS ?? 25e3));
|
|
85162
|
+
const deadlineMs = Math.max(
|
|
85163
|
+
2e3,
|
|
85164
|
+
Number(process.env.APPLE_MAIL_MCP_STATS_DEADLINE_MS ?? 5e4)
|
|
85165
|
+
);
|
|
85166
|
+
const startedAt = Date.now();
|
|
85167
|
+
const remainingMs = () => Math.max(0, deadlineMs - (Date.now() - startedAt));
|
|
85078
85168
|
const withBudget = async (work, label) => {
|
|
85169
|
+
const ms = Math.min(budgetMs, remainingMs());
|
|
85079
85170
|
let timer;
|
|
85080
85171
|
try {
|
|
85081
85172
|
return await Promise.race([
|
|
85082
85173
|
work,
|
|
85083
85174
|
new Promise((_, reject) => {
|
|
85084
|
-
timer = setTimeout(
|
|
85085
|
-
() => reject(new Error(`${label} timed out after ${budgetMs}ms`)),
|
|
85086
|
-
budgetMs
|
|
85087
|
-
);
|
|
85175
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
85088
85176
|
})
|
|
85089
85177
|
]);
|
|
85090
85178
|
} finally {
|
|
@@ -85118,8 +85206,21 @@ registerTool(
|
|
|
85118
85206
|
let totalUnread = 0;
|
|
85119
85207
|
const recent = { last24h: 0, last7d: 0, last30d: 0 };
|
|
85120
85208
|
const perAccount = [];
|
|
85121
|
-
const sources = planCountSources(mailManager.listAccounts(), resolveImapConfigs());
|
|
85122
85209
|
const failedAccounts = [];
|
|
85210
|
+
const imapConfigs = resolveImapConfigs();
|
|
85211
|
+
const enumerateMs = Math.max(1e3, Math.min(1e4, Math.floor(remainingMs() * 0.3)));
|
|
85212
|
+
const enumerated = mailManager.listAccountsChecked({ timeoutMs: enumerateMs });
|
|
85213
|
+
const sources = enumerated.failed ? imapConfigs.map((config2) => ({
|
|
85214
|
+
kind: "imap",
|
|
85215
|
+
config: config2,
|
|
85216
|
+
label: config2.accountLabel
|
|
85217
|
+
})) : planCountSources(enumerated.accounts, imapConfigs);
|
|
85218
|
+
if (enumerated.failed) {
|
|
85219
|
+
console.error(
|
|
85220
|
+
`Mail.app account enumeration failed for get-mail-stats: ${enumerated.error}`
|
|
85221
|
+
);
|
|
85222
|
+
failedAccounts.push("Mail.app accounts (AppleScript enumeration)");
|
|
85223
|
+
}
|
|
85123
85224
|
const settled = await Promise.all(
|
|
85124
85225
|
sources.filter((s) => s.kind === "imap").map(async (src) => {
|
|
85125
85226
|
try {
|
|
@@ -85154,9 +85255,20 @@ registerTool(
|
|
|
85154
85255
|
}
|
|
85155
85256
|
for (const src of sources) {
|
|
85156
85257
|
if (src.kind === "imap") continue;
|
|
85258
|
+
const left = remainingMs();
|
|
85259
|
+
if (left < 1e3) {
|
|
85260
|
+
failedAccounts.push(src.label);
|
|
85261
|
+
continue;
|
|
85262
|
+
}
|
|
85263
|
+
const read = mailManager.listMailboxesChecked(src.account.name, { timeoutMs: left });
|
|
85264
|
+
if (read.failed) {
|
|
85265
|
+
console.error(`AppleScript mail-stats failed for "${src.label}": ${read.error}`);
|
|
85266
|
+
failedAccounts.push(src.label);
|
|
85267
|
+
continue;
|
|
85268
|
+
}
|
|
85157
85269
|
let m = 0;
|
|
85158
85270
|
let u = 0;
|
|
85159
|
-
for (const mb of
|
|
85271
|
+
for (const mb of read.mailboxes) {
|
|
85160
85272
|
m += mb.messageCount;
|
|
85161
85273
|
u += mb.unreadCount;
|
|
85162
85274
|
}
|
|
@@ -85188,7 +85300,7 @@ registerTool(
|
|
|
85188
85300
|
if (failedAccounts.length > 0) {
|
|
85189
85301
|
lines2.push(
|
|
85190
85302
|
``,
|
|
85191
|
-
`\u26A0\uFE0F PARTIAL: ${failedAccounts.length} account(s) could not be read (${failedAccounts.join(", ")}), so the real totals are higher. They either failed
|
|
85303
|
+
`\u26A0\uFE0F PARTIAL: ${failedAccounts.length} account(s) could not be read (${failedAccounts.join(", ")}), so the real totals are higher. They either failed, exceeded the ${budgetMs}ms per-account budget, or ran out of the ${deadlineMs}ms overall deadline \u2014 raise APPLE_MAIL_MCP_STATS_BUDGET_MS and/or APPLE_MAIL_MCP_STATS_DEADLINE_MS if an account is simply large, or run the "doctor" tool to check the connection.`
|
|
85192
85304
|
);
|
|
85193
85305
|
}
|
|
85194
85306
|
return successResponse(lines2.join("\n"), {
|
package/docs/IMAP-SETUP.md
CHANGED
|
@@ -199,6 +199,20 @@ Each array entry accepts: `account`, `user`, `host`, `port`, `password`
|
|
|
199
199
|
(discouraged — prefer Keychain), `keychainService`, `keychainAccount`. Each
|
|
200
200
|
account keeps its own pooled IMAP connection.
|
|
201
201
|
|
|
202
|
+
**The array alone is enough.** You do not need the legacy single-account vars:
|
|
203
|
+
listing every account in `APPLE_MAIL_MCP_IMAP_ACCOUNTS` and setting none of
|
|
204
|
+
`APPLE_MAIL_MCP_IMAP_USER` / `_ACCOUNT` / `_HOST` is fully supported, and the
|
|
205
|
+
first array entry becomes the default account. (Before 2.10.7 that shape worked
|
|
206
|
+
for every tool but made `doctor` report `connection failed: undefined` for each
|
|
207
|
+
account — see #138.)
|
|
208
|
+
|
|
209
|
+
**Don't declare the same mailbox twice.** If the legacy vars already describe a
|
|
210
|
+
mailbox, do not also give it an array entry — even under a different `account`
|
|
211
|
+
nickname. An account's identity is its resolved `(host, port, user)`, not its
|
|
212
|
+
label, so the duplicate is recognised and collapsed rather than counted twice
|
|
213
|
+
(the extra nickname still works as an alias). Before 2.10.7 it was counted
|
|
214
|
+
twice, inflating `get-unread-count` and `get-mail-stats`.
|
|
215
|
+
|
|
202
216
|
---
|
|
203
217
|
|
|
204
218
|
## Step 4 (optional) — SMTP sending
|
|
@@ -333,17 +347,18 @@ GUI is ignoring.
|
|
|
333
347
|
| Variable | Purpose |
|
|
334
348
|
|----------|---------|
|
|
335
349
|
| `APPLE_MAIL_MCP_DEFAULT_ACCOUNT` | Account used when a tool omits `account` (name or email). |
|
|
336
|
-
| `APPLE_MAIL_MCP_IMAP_USER` | Primary IMAP login
|
|
350
|
+
| `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. |
|
|
337
351
|
| `APPLE_MAIL_MCP_IMAP_ACCOUNT` | Mail.app account name to match for routing (default = USER). |
|
|
338
352
|
| `APPLE_MAIL_MCP_IMAP_HOST` | IMAP host (default `imap.gmail.com`). |
|
|
339
353
|
| `APPLE_MAIL_MCP_IMAP_PORT` | IMAP port (default `993`, implicit TLS). |
|
|
340
354
|
| `APPLE_MAIL_MCP_IMAP_PASSWORD` | Password (discouraged; prefer Keychain). |
|
|
341
355
|
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_SERVICE` | Keychain item service/server name. |
|
|
342
356
|
| `APPLE_MAIL_MCP_IMAP_KEYCHAIN_ACCOUNT` | Keychain item account (default = USER). |
|
|
343
|
-
| `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | JSON array of
|
|
357
|
+
| `APPLE_MAIL_MCP_IMAP_ACCOUNTS` | JSON array of accounts (multi-account). Sufficient on its own; also enables IMAP. |
|
|
344
358
|
| `APPLE_MAIL_MCP_IMAP_IDLE` | `1` to enable IMAP IDLE new-mail push. |
|
|
345
359
|
| `APPLE_MAIL_MCP_IMAP_IDLE_MS` | Pooled-connection idle timeout in ms (default `30000`; `0` = never close). |
|
|
346
360
|
| `APPLE_MAIL_MCP_STATS_BUDGET_MS` | Per-account wall-clock budget for `get-mail-stats` in ms (default `25000`, minimum `1000`). |
|
|
361
|
+
| `APPLE_MAIL_MCP_STATS_DEADLINE_MS` | Overall wall-clock deadline for one `get-mail-stats` call in ms (default `50000`, minimum `2000`) — covers the Mail.app account enumeration as well as every per-account read. Keep it below your MCP client's request timeout. |
|
|
347
362
|
| `APPLE_MAIL_MCP_SMTP_HOST` | SMTP host; setting it enables `transport:"smtp"`. |
|
|
348
363
|
| `APPLE_MAIL_MCP_SMTP_PORT` | SMTP port (`465` if secure, else `587`). |
|
|
349
364
|
| `APPLE_MAIL_MCP_SMTP_SECURE` | `true` for implicit TLS (465); else STARTTLS. |
|
package/package.json
CHANGED