apple-mail-mcp 2.17.0 → 2.17.1
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 +14 -5
- package/build/index.js +141 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -470,11 +470,20 @@ matching `account` is passed. There are three cases:
|
|
|
470
470
|
sort newest-first; count tools (`get-unread-count`, `get-mail-stats`) count each
|
|
471
471
|
account via exactly one backend so a coverage mismatch can never double- (or
|
|
472
472
|
under-) count.
|
|
473
|
-
- **
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
`[Gmail]/All Mail`
|
|
477
|
-
|
|
473
|
+
- **An omitted mailbox on `search-messages` searches the account's entire
|
|
474
|
+
store, not just one default folder** (v2.17.1, [#199](https://github.com/sweetrb/apple-mail-mcp/issues/199)).
|
|
475
|
+
Per account, the fan-out uses the server-advertised RFC 6154 `\All`
|
|
476
|
+
mailbox when one exists (Gmail/Workspace's `[Gmail]/All Mail`); otherwise
|
|
477
|
+
it searches every selectable mailbox the server lists (iCloud, generic
|
|
478
|
+
IMAP), merges the matches, de-duplicates by Message-ID, and sorts
|
|
479
|
+
newest-first before applying `limit`/`offset`. A mailbox that can't be
|
|
480
|
+
selected or searched is named in the result instead of silently dropping
|
|
481
|
+
coverage. Scanning every mailbox on a large, deeply-nested account costs
|
|
482
|
+
one `SEARCH` + a bounded `FETCH` per mailbox over the pooled IMAP
|
|
483
|
+
connection — pin a `mailbox` to skip the fan-out when you already know
|
|
484
|
+
where to look. `list-messages` (no query) still defaults an omitted
|
|
485
|
+
mailbox to `INBOX` on every provider — only unscoped *search* scans the
|
|
486
|
+
whole account.
|
|
478
487
|
|
|
479
488
|
If IMAP is **not** configured at all, every read behaves exactly as before
|
|
480
489
|
(pure AppleScript). The three mailbox-**write** ops (`create-mailbox`,
|
package/build/index.js
CHANGED
|
@@ -84030,8 +84030,8 @@ var defaultConnect = async (cfg) => {
|
|
|
84030
84030
|
}
|
|
84031
84031
|
return client;
|
|
84032
84032
|
};
|
|
84033
|
-
function resolveMailboxPath(mailbox,
|
|
84034
|
-
if (!mailbox) return
|
|
84033
|
+
function resolveMailboxPath(mailbox, _mode) {
|
|
84034
|
+
if (!mailbox) return "INBOX";
|
|
84035
84035
|
const map = {
|
|
84036
84036
|
"all mail": "[Gmail]/All Mail",
|
|
84037
84037
|
"sent mail": "[Gmail]/Sent Mail",
|
|
@@ -84094,49 +84094,127 @@ function structuredRow(m, account, path) {
|
|
|
84094
84094
|
...env.messageId ? { messageId: env.messageId } : {}
|
|
84095
84095
|
};
|
|
84096
84096
|
}
|
|
84097
|
+
function hasMailboxFlag(mailbox, wanted) {
|
|
84098
|
+
const normalized = wanted.toLowerCase();
|
|
84099
|
+
return [...mailbox.flags ?? []].some((flag) => flag.toLowerCase() === normalized);
|
|
84100
|
+
}
|
|
84101
|
+
function messageDateEpoch(message) {
|
|
84102
|
+
if (!message.envelope?.date) return 0;
|
|
84103
|
+
const epoch = new Date(message.envelope.date).getTime();
|
|
84104
|
+
return Number.isNaN(epoch) ? 0 : epoch;
|
|
84105
|
+
}
|
|
84106
|
+
function messageIdentity(entry) {
|
|
84107
|
+
const raw = entry.message.envelope?.messageId?.trim() ?? "";
|
|
84108
|
+
const messageId = raw.replace(/^<+|>+$/g, "").trim().toLowerCase();
|
|
84109
|
+
return messageId ? `mid:${messageId}` : `${entry.path}\0${entry.message.uid}`;
|
|
84110
|
+
}
|
|
84111
|
+
async function fetchMailboxMatches(client, path, criteria, newestCount) {
|
|
84112
|
+
const lock = await client.getMailboxLock(path);
|
|
84113
|
+
try {
|
|
84114
|
+
const found = await client.search(criteria, { uid: true });
|
|
84115
|
+
const uids = Array.isArray(found) ? found : [];
|
|
84116
|
+
if (uids.length === 0 || newestCount === 0) return { messages: [], total: uids.length };
|
|
84117
|
+
const newest = uids.slice().reverse().slice(0, newestCount);
|
|
84118
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
84119
|
+
for await (const msg of client.fetch(
|
|
84120
|
+
newest.join(","),
|
|
84121
|
+
// BODYSTRUCTURE rides along so `hasAttachments` is computed rather
|
|
84122
|
+
// than assumed. Measured on 50 real messages: ~390ms -> ~465ms for
|
|
84123
|
+
// the fetch (~17%), same single round trip, no extra request.
|
|
84124
|
+
{ envelope: true, flags: true, bodyStructure: true },
|
|
84125
|
+
{ uid: true }
|
|
84126
|
+
)) {
|
|
84127
|
+
byUid.set(msg.uid, msg);
|
|
84128
|
+
}
|
|
84129
|
+
return {
|
|
84130
|
+
messages: newest.map((uid) => byUid.get(uid)).filter((message) => message !== void 0),
|
|
84131
|
+
total: uids.length
|
|
84132
|
+
};
|
|
84133
|
+
} finally {
|
|
84134
|
+
lock.release();
|
|
84135
|
+
}
|
|
84136
|
+
}
|
|
84097
84137
|
async function run(args, listMode, deps) {
|
|
84098
84138
|
return useClient(
|
|
84099
84139
|
{ ...deps, account: deps.account ?? args.account },
|
|
84100
84140
|
async (client, cfg) => {
|
|
84101
|
-
const
|
|
84102
|
-
|
|
84103
|
-
|
|
84104
|
-
|
|
84105
|
-
const
|
|
84106
|
-
|
|
84107
|
-
|
|
84108
|
-
|
|
84109
|
-
|
|
84110
|
-
|
|
84111
|
-
|
|
84112
|
-
|
|
84141
|
+
const unscopedSearch = !listMode && !args.mailbox;
|
|
84142
|
+
let paths;
|
|
84143
|
+
let allMailboxCount = 0;
|
|
84144
|
+
if (unscopedSearch) {
|
|
84145
|
+
const listed = await client.list();
|
|
84146
|
+
const selectable = listed.filter((mailbox) => !hasMailboxFlag(mailbox, "\\Noselect"));
|
|
84147
|
+
const allMailbox = selectable.find(
|
|
84148
|
+
(mailbox) => mailbox.specialUse?.toLowerCase() === "\\all"
|
|
84149
|
+
);
|
|
84150
|
+
paths = allMailbox ? [allMailbox.path] : selectable.map((mailbox) => mailbox.path);
|
|
84151
|
+
allMailboxCount = paths.length;
|
|
84152
|
+
if (paths.length === 0) {
|
|
84153
|
+
throw new Error(`No selectable IMAP mailboxes found for account ${cfg.accountLabel}.`);
|
|
84113
84154
|
}
|
|
84114
|
-
|
|
84115
|
-
|
|
84116
|
-
|
|
84117
|
-
|
|
84118
|
-
|
|
84119
|
-
|
|
84120
|
-
|
|
84121
|
-
|
|
84122
|
-
|
|
84123
|
-
|
|
84124
|
-
|
|
84125
|
-
|
|
84126
|
-
|
|
84155
|
+
} else {
|
|
84156
|
+
paths = [resolveMailboxPath(args.mailbox, listMode ? "list" : "search")];
|
|
84157
|
+
}
|
|
84158
|
+
const limit = args.limit ?? 50;
|
|
84159
|
+
const offset = args.offset ?? 0;
|
|
84160
|
+
const criteria = buildCriteria(args, listMode);
|
|
84161
|
+
const newestPerMailbox = offset + limit;
|
|
84162
|
+
const fetched = [];
|
|
84163
|
+
const failedMailboxes = [];
|
|
84164
|
+
let totalMatched = 0;
|
|
84165
|
+
for (const path of paths) {
|
|
84166
|
+
try {
|
|
84167
|
+
const result = await fetchMailboxMatches(client, path, criteria, newestPerMailbox);
|
|
84168
|
+
totalMatched += result.total;
|
|
84169
|
+
fetched.push(...result.messages.map((message) => ({ message, path })));
|
|
84170
|
+
} catch (error2) {
|
|
84171
|
+
failedMailboxes.push(path);
|
|
84172
|
+
console.error(
|
|
84173
|
+
`IMAP ${listMode ? "list" : "search"} failed for account "${cfg.accountLabel}", mailbox "${path}": ${String(error2)}`
|
|
84174
|
+
);
|
|
84127
84175
|
}
|
|
84128
|
-
|
|
84129
|
-
|
|
84130
|
-
|
|
84131
|
-
|
|
84132
|
-
|
|
84133
|
-
|
|
84176
|
+
}
|
|
84177
|
+
if (failedMailboxes.length === paths.length) {
|
|
84178
|
+
throw new Error(
|
|
84179
|
+
`IMAP ${listMode ? "list" : "search"} failed in every requested mailbox for account ${cfg.accountLabel}: ${failedMailboxes.join(", ")}.`
|
|
84180
|
+
);
|
|
84181
|
+
}
|
|
84182
|
+
let ordered = fetched;
|
|
84183
|
+
if (unscopedSearch) {
|
|
84184
|
+
ordered = fetched.slice().sort((a, b) => messageDateEpoch(b.message) - messageDateEpoch(a.message));
|
|
84185
|
+
const unique = /* @__PURE__ */ new Map();
|
|
84186
|
+
for (const entry of ordered) {
|
|
84187
|
+
const key = messageIdentity(entry);
|
|
84188
|
+
if (!unique.has(key)) unique.set(key, entry);
|
|
84189
|
+
}
|
|
84190
|
+
ordered = [...unique.values()].slice(offset, offset + limit);
|
|
84191
|
+
} else {
|
|
84192
|
+
ordered = fetched.slice(offset, offset + limit);
|
|
84193
|
+
}
|
|
84194
|
+
const rows = ordered.map(({ message, path }) => formatRow(message, cfg.accountLabel, path));
|
|
84195
|
+
const messages = ordered.map(
|
|
84196
|
+
({ message, path }) => structuredRow(message, cfg.accountLabel, path)
|
|
84197
|
+
);
|
|
84198
|
+
const partial2 = failedMailboxes.length > 0;
|
|
84199
|
+
const failureNote = partial2 ? `
|
|
84134
84200
|
|
|
84135
|
-
|
|
84136
|
-
|
|
84137
|
-
}
|
|
84138
|
-
|
|
84201
|
+
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}"`).join(", ")}.` : "";
|
|
84202
|
+
const verb = listMode ? "listed" : "matched";
|
|
84203
|
+
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
84204
|
+
if (messages.length === 0) {
|
|
84205
|
+
return {
|
|
84206
|
+
text: `No messages found via IMAP in ${scope} (account ${cfg.accountLabel}).${failureNote}`,
|
|
84207
|
+
messages,
|
|
84208
|
+
count: 0,
|
|
84209
|
+
partial: partial2,
|
|
84210
|
+
failedMailboxes
|
|
84211
|
+
};
|
|
84139
84212
|
}
|
|
84213
|
+
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalMatched} total ${verb}):
|
|
84214
|
+
` + rows.join("\n") + `
|
|
84215
|
+
|
|
84216
|
+
Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutations (mark/flag/move/delete-message), which route back to IMAP.` + failureNote;
|
|
84217
|
+
return { text, messages, count: messages.length, partial: partial2, failedMailboxes };
|
|
84140
84218
|
},
|
|
84141
84219
|
true
|
|
84142
84220
|
);
|
|
@@ -85240,26 +85318,26 @@ function mergeMessages(imapRows, appleRows, limit) {
|
|
|
85240
85318
|
merged.sort((a, b) => dateEpoch(b) - dateEpoch(a));
|
|
85241
85319
|
return limit >= 0 ? merged.slice(0, limit) : merged;
|
|
85242
85320
|
}
|
|
85243
|
-
function isGmailHost(host) {
|
|
85244
|
-
return /(^|\.)gmail\.com$/i.test(host.trim());
|
|
85245
|
-
}
|
|
85246
85321
|
async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapConfigs()) {
|
|
85247
85322
|
const rows = [];
|
|
85248
85323
|
const accountsQueried = [];
|
|
85249
85324
|
const accountsFailed = [];
|
|
85325
|
+
const failedMailboxes = [];
|
|
85250
85326
|
for (const config2 of configs) {
|
|
85251
|
-
const
|
|
85252
|
-
const perAccountArgs = { ...args, account: void 0, mailbox };
|
|
85327
|
+
const perAccountArgs = { ...args, account: void 0 };
|
|
85253
85328
|
try {
|
|
85254
85329
|
const res = kind === "search" ? await imapSearchMessages(perAccountArgs, { ...deps, config: config2 }) : await imapListMessages(perAccountArgs, { ...deps, config: config2 });
|
|
85255
85330
|
rows.push(...res.messages);
|
|
85256
85331
|
accountsQueried.push(config2.accountLabel);
|
|
85332
|
+
failedMailboxes.push(
|
|
85333
|
+
...res.failedMailboxes.map((mailbox) => `${config2.accountLabel} / ${mailbox}`)
|
|
85334
|
+
);
|
|
85257
85335
|
} catch (e) {
|
|
85258
85336
|
accountsFailed.push(config2.accountLabel);
|
|
85259
85337
|
console.error(`IMAP fan-out failed for account "${config2.accountLabel}": ${String(e)}`);
|
|
85260
85338
|
}
|
|
85261
85339
|
}
|
|
85262
|
-
return { rows, accountsQueried, accountsFailed };
|
|
85340
|
+
return { rows, accountsQueried, accountsFailed, failedMailboxes };
|
|
85263
85341
|
}
|
|
85264
85342
|
function configMatchesAccount(config2, account) {
|
|
85265
85343
|
const name = account.name.trim().toLowerCase();
|
|
@@ -85847,7 +85925,8 @@ var LIST_OUTPUT_SCHEMA = {
|
|
|
85847
85925
|
partial: external_exports.boolean().optional(),
|
|
85848
85926
|
skippedLargeMailboxes: external_exports.array(external_exports.string()).optional(),
|
|
85849
85927
|
notSearchedMailboxes: external_exports.array(external_exports.string()).optional(),
|
|
85850
|
-
timedOutAccounts: external_exports.array(external_exports.string()).optional()
|
|
85928
|
+
timedOutAccounts: external_exports.array(external_exports.string()).optional(),
|
|
85929
|
+
failedMailboxes: external_exports.array(external_exports.string()).optional()
|
|
85851
85930
|
};
|
|
85852
85931
|
var BATCH_COUNT_OUTPUT_SCHEMA = {
|
|
85853
85932
|
ok: external_exports.boolean().optional(),
|
|
@@ -85926,8 +86005,9 @@ function mergedMessageResponse(fan, apple, limit, verb) {
|
|
|
85926
86005
|
const merged = mergeMessages(fan.rows, apple.rows, limit);
|
|
85927
86006
|
const diagnostics = {
|
|
85928
86007
|
...apple.diagnostics,
|
|
85929
|
-
partial: apple.diagnostics.partial || fan.accountsFailed.length > 0,
|
|
85930
|
-
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed]
|
|
86008
|
+
partial: apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0,
|
|
86009
|
+
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
|
|
86010
|
+
notSearchedMailboxes: [...apple.diagnostics.notSearchedMailboxes, ...fan.failedMailboxes]
|
|
85931
86011
|
};
|
|
85932
86012
|
const structured = {
|
|
85933
86013
|
messages: merged,
|
|
@@ -85935,7 +86015,8 @@ function mergedMessageResponse(fan, apple, limit, verb) {
|
|
|
85935
86015
|
partial: diagnostics.partial,
|
|
85936
86016
|
skippedLargeMailboxes: diagnostics.skippedLargeMailboxes,
|
|
85937
86017
|
notSearchedMailboxes: diagnostics.notSearchedMailboxes,
|
|
85938
|
-
timedOutAccounts: diagnostics.timedOutAccounts
|
|
86018
|
+
timedOutAccounts: diagnostics.timedOutAccounts,
|
|
86019
|
+
failedMailboxes: fan.failedMailboxes
|
|
85939
86020
|
};
|
|
85940
86021
|
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
85941
86022
|
if (merged.length === 0) {
|
|
@@ -86031,7 +86112,8 @@ registerTool(
|
|
|
86031
86112
|
return successResponse(r.text, {
|
|
86032
86113
|
messages: r.messages,
|
|
86033
86114
|
count: r.count,
|
|
86034
|
-
partial: r.partial
|
|
86115
|
+
partial: r.partial,
|
|
86116
|
+
failedMailboxes: r.failedMailboxes
|
|
86035
86117
|
});
|
|
86036
86118
|
}
|
|
86037
86119
|
const fan = await fanOutImapMessages(imapArgs, "search");
|
|
@@ -86178,7 +86260,8 @@ registerTool(
|
|
|
86178
86260
|
subject: external_exports.string().optional(),
|
|
86179
86261
|
messages: external_exports.array(MESSAGE_ROW_SCHEMA).optional(),
|
|
86180
86262
|
count: external_exports.number().optional(),
|
|
86181
|
-
partial: external_exports.boolean().optional()
|
|
86263
|
+
partial: external_exports.boolean().optional(),
|
|
86264
|
+
failedMailboxes: external_exports.array(external_exports.string()).optional()
|
|
86182
86265
|
}
|
|
86183
86266
|
},
|
|
86184
86267
|
withErrorHandling(async ({ id, account, mailbox, limit = 50 }) => {
|
|
@@ -86206,7 +86289,8 @@ ${r.text}`, {
|
|
|
86206
86289
|
subject: base,
|
|
86207
86290
|
messages: r.messages,
|
|
86208
86291
|
count: r.count,
|
|
86209
|
-
partial: r.partial
|
|
86292
|
+
partial: r.partial,
|
|
86293
|
+
failedMailboxes: r.failedMailboxes
|
|
86210
86294
|
});
|
|
86211
86295
|
}
|
|
86212
86296
|
const fan = await fanOutImapMessages({ subject: base, mailbox, limit }, "search");
|
|
@@ -86231,17 +86315,19 @@ ${r.text}`, {
|
|
|
86231
86315
|
const orderedRows = mergedNewestFirst.slice().reverse().sort(
|
|
86232
86316
|
(a, b) => (a.dateReceived ? new Date(a.dateReceived).getTime() : 0) - (b.dateReceived ? new Date(b.dateReceived).getTime() : 0)
|
|
86233
86317
|
);
|
|
86234
|
-
const partial2 = apple.diagnostics.partial || fan.accountsFailed.length > 0;
|
|
86318
|
+
const partial2 = apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0;
|
|
86235
86319
|
const coverage = partialCoverageBlock({
|
|
86236
86320
|
...apple.diagnostics,
|
|
86237
86321
|
partial: partial2,
|
|
86238
|
-
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed]
|
|
86322
|
+
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
|
|
86323
|
+
notSearchedMailboxes: [...apple.diagnostics.notSearchedMailboxes, ...fan.failedMailboxes]
|
|
86239
86324
|
});
|
|
86240
86325
|
const structured2 = {
|
|
86241
86326
|
subject: base,
|
|
86242
86327
|
messages: orderedRows,
|
|
86243
86328
|
count: orderedRows.length,
|
|
86244
|
-
partial: partial2
|
|
86329
|
+
partial: partial2,
|
|
86330
|
+
failedMailboxes: fan.failedMailboxes
|
|
86245
86331
|
};
|
|
86246
86332
|
if (orderedRows.length === 0) {
|
|
86247
86333
|
return successResponse(`No messages found in thread "${base}".${coverage}`, structured2);
|
|
@@ -86304,7 +86390,8 @@ registerTool(
|
|
|
86304
86390
|
return successResponse(r.text, {
|
|
86305
86391
|
messages: r.messages,
|
|
86306
86392
|
count: r.count,
|
|
86307
|
-
partial: r.partial
|
|
86393
|
+
partial: r.partial,
|
|
86394
|
+
failedMailboxes: r.failedMailboxes
|
|
86308
86395
|
});
|
|
86309
86396
|
}
|
|
86310
86397
|
const fan = await fanOutImapMessages({ mailbox, limit, offset, from, unreadOnly }, "list");
|
package/package.json
CHANGED