apple-mail-mcp 2.19.16 → 2.19.18
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 +34 -12
- package/build/cli.js +292 -67
- package/build/index.js +328 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -280,19 +280,27 @@ human reader; the same information is also returned as structured fields on
|
|
|
280
280
|
`search-messages` and `list-messages`, so a caller can tell _"nothing matched"_
|
|
281
281
|
apart from _"I did not look everywhere"_ without parsing the text:
|
|
282
282
|
|
|
283
|
-
| Field | Type | Meaning
|
|
284
|
-
| ----------------------- | -------- |
|
|
285
|
-
| `partial` | boolean | Coverage was incomplete — **the result is not a confirmed "no such mail"**. True whenever any field below is non-empty.
|
|
286
|
-
| `skippedLargeMailboxes` | string[] | Mailboxes never scanned because their message count exceeded `APPLE_MAIL_MAX_SEARCH_MAILBOX`, formatted `"Account / Mailbox (count)"` — e.g. `"iCloud / Archive (90694)"`.
|
|
287
|
-
| `notSearchedMailboxes` | string[] | Mailboxes that _were_ reached but timed out or errored mid-scan, formatted `"Account / Mailbox"`. Also carries the IMAP path's `failedMailboxes`.
|
|
288
|
-
| `timedOutAccounts` | string[] | Accounts whose whole-account AppleScript was killed by the per-account time budget — nothing from that account was searched.
|
|
289
|
-
| `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.
|
|
283
|
+
| Field | Type | Meaning |
|
|
284
|
+
| ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
285
|
+
| `partial` | boolean | Coverage was incomplete — **the result is not a confirmed "no such mail"**. True whenever any field below is non-empty. |
|
|
286
|
+
| `skippedLargeMailboxes` | string[] | Mailboxes never scanned because their message count exceeded `APPLE_MAIL_MAX_SEARCH_MAILBOX`, formatted `"Account / Mailbox (count)"` — e.g. `"iCloud / Archive (90694)"`. |
|
|
287
|
+
| `notSearchedMailboxes` | string[] | Mailboxes that _were_ reached but timed out or errored mid-scan, formatted `"Account / Mailbox"`. Also carries the IMAP path's `failedMailboxes`. |
|
|
288
|
+
| `timedOutAccounts` | string[] | Accounts whose whole-account AppleScript was killed by the per-account time budget — nothing from that account was searched. |
|
|
289
|
+
| `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. |
|
|
290
|
+
| `failedMailboxReasons` | object | The IMAP server's own error text for each entry in `failedMailboxes`, keyed the same way. |
|
|
291
|
+
| `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)) |
|
|
290
292
|
|
|
291
293
|
Treat a non-empty `skippedLargeMailboxes` as actionable rather than
|
|
292
294
|
informational: re-run scoped to the named mailbox with a `dateFrom`/`dateTo`
|
|
293
295
|
window, or configure the [IMAP backend](#imap-backend--opt-in), which searches
|
|
294
|
-
those mailboxes server-side.
|
|
295
|
-
coverage was complete.
|
|
296
|
+
those mailboxes server-side. The list fields are optional and are omitted (or
|
|
297
|
+
empty) when coverage was complete.
|
|
298
|
+
|
|
299
|
+
A row the IMAP backend could only read with a reduced item set (its full
|
|
300
|
+
`BODYSTRUCTURE`, or its whole `FETCH` response, was unparseable — e.g. a message
|
|
301
|
+
forwarded as an attachment many levels deep) is still returned, with a
|
|
302
|
+
`metadataIncomplete` note saying what was missing; its `hasAttachments` is then
|
|
303
|
+
inferred from the top-level `Content-Type`.
|
|
296
304
|
|
|
297
305
|
---
|
|
298
306
|
|
|
@@ -826,7 +834,7 @@ Reply to an existing message.
|
|
|
826
834
|
| Parameter | Type | Required | Description |
|
|
827
835
|
| ----------- | ------- | -------- | ---------------------------------------------------------------------------------------------- |
|
|
828
836
|
| `id` | string | Yes | Message ID to reply to |
|
|
829
|
-
| `body` | string | Yes | Reply body (plain text; HTML tags such as `<br>` are not rendered)
|
|
837
|
+
| `body` | string | Yes | Reply body (plain text; HTML tags such as `<br>` are not rendered) |
|
|
830
838
|
| `replyAll` | boolean | No | Reply to all recipients (default: false) |
|
|
831
839
|
| `send` | boolean | No | Send immediately (default: true, false = save as draft) |
|
|
832
840
|
| `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
|
|
@@ -1081,17 +1089,31 @@ Matching ignores letter case **and Unicode normalization form**: a typed
|
|
|
1081
1089
|
precomposed `México` (NFC) finds a mailbox the server stores decomposed (NFD —
|
|
1082
1090
|
common for folders created on a Mac, and what iCloud keeps), and the server's
|
|
1083
1091
|
own stored spelling is what gets selected. Two mailboxes whose names differ
|
|
1084
|
-
|
|
1092
|
+
_only_ in case or normalization look identical in any listing, so a name that
|
|
1085
1093
|
matches both is refused with an error saying so — rename one of them. For the
|
|
1086
1094
|
same reason `create-mailbox` treats a normalization-equivalent existing name as
|
|
1087
1095
|
already existing, and `rename-mailbox` refuses to create such a twin.
|
|
1088
1096
|
|
|
1089
1097
|
**Messages awaiting expunge are not listed.** On IMAP accounts, search,
|
|
1090
|
-
list, thread and mail-stats queries only match messages
|
|
1098
|
+
list, thread and mail-stats queries only match messages _not_ flagged
|
|
1091
1099
|
`\Deleted` (IMAP `UNDELETED`). iCloud keeps flagged-but-never-expunged
|
|
1092
1100
|
messages out of its message counts yet still returns them from `UID SEARCH`,
|
|
1093
1101
|
which made a filter-less `list-messages` fail on such a mailbox before 2.19.16.
|
|
1094
1102
|
|
|
1103
|
+
**Very large IMAP mailboxes are read newest-first in bounded windows**
|
|
1104
|
+
([#256](https://github.com/sweetrb/apple-mail-mcp/issues/256)). Above 10,000
|
|
1105
|
+
messages, a filter-less `list-messages`/`search-messages` pages by message
|
|
1106
|
+
sequence number from the top of the mailbox — it fetches UIDs and flags for
|
|
1107
|
+
just enough of the newest messages to fill `limit` + `offset` (skipping
|
|
1108
|
+
`\Deleted` ones) and full rows for the page alone, so `limit: 1` on a
|
|
1109
|
+
793,614-message mailbox costs one small `FETCH`, not a whole-mailbox `SEARCH`.
|
|
1110
|
+
A filtered search there runs the same criteria over newest-first sequence
|
|
1111
|
+
windows (5,000 messages, growing to 50,000) and stops once the page is full;
|
|
1112
|
+
the reported total then reads `at least N` unless the walk reached the bottom
|
|
1113
|
+
of the mailbox. A `SEARCH` or `FETCH` that fails names the mailbox, its size and
|
|
1114
|
+
the server's own reason (or a likely timeout) instead of reporting "no
|
|
1115
|
+
messages".
|
|
1116
|
+
|
|
1095
1117
|
**Mail's local "On My Mac" mailboxes** are not children of any account — they
|
|
1096
1118
|
hang off the application — so they are reported under the synthetic account label
|
|
1097
1119
|
**`On My Mac`**. An unscoped call includes them (listed last); `account="On My
|
package/build/cli.js
CHANGED
|
@@ -58045,6 +58045,7 @@ var imapClient_exports = {};
|
|
|
58045
58045
|
__export(imapClient_exports, {
|
|
58046
58046
|
HEADER_WINDOW_BYTES: () => HEADER_WINDOW_BYTES,
|
|
58047
58047
|
IMAP_ENV: () => IMAP_ENV,
|
|
58048
|
+
LARGE_MAILBOX_MESSAGES: () => LARGE_MAILBOX_MESSAGES,
|
|
58048
58049
|
MAX_COMPOSE_SOURCE_BYTES: () => MAX_COMPOSE_SOURCE_BYTES,
|
|
58049
58050
|
MAX_RFC822_FILE_BYTES: () => MAX_RFC822_FILE_BYTES,
|
|
58050
58051
|
MAX_RFC822_INLINE_BYTES: () => MAX_RFC822_INLINE_BYTES,
|
|
@@ -58091,6 +58092,7 @@ __export(imapClient_exports, {
|
|
|
58091
58092
|
mailFlagColorIndex: () => mailFlagColorIndex,
|
|
58092
58093
|
matchMailbox: () => matchMailbox,
|
|
58093
58094
|
normalizeMessageId: () => normalizeMessageId,
|
|
58095
|
+
omittedNote: () => omittedNote,
|
|
58094
58096
|
resolveImapConfig: () => resolveImapConfig,
|
|
58095
58097
|
resolveImapConfigs: () => resolveImapConfigs,
|
|
58096
58098
|
resolveMailboxPath: () => resolveMailboxPath,
|
|
@@ -58401,7 +58403,12 @@ function structuredRow(m, account, path) {
|
|
|
58401
58403
|
// a caller from "no attachments", so every IMAP-sourced message claimed to
|
|
58402
58404
|
// have none. Falls back to false only when the fetch carried no
|
|
58403
58405
|
// BODYSTRUCTURE at all.
|
|
58404
|
-
|
|
58406
|
+
// A row read without BODYSTRUCTURE (#256 follow-up) judges by its top-level
|
|
58407
|
+
// Content-Type instead, and says so in `metadataIncomplete`.
|
|
58408
|
+
hasAttachments: m.bodyStructure ? bodyStructureHasAttachments(m.bodyStructure) : m.degraded ? contentTypeSuggestsAttachments(m) : false,
|
|
58409
|
+
...m.degraded ? {
|
|
58410
|
+
metadataIncomplete: m.degraded === "bodystructure" ? "BODYSTRUCTURE unreadable; hasAttachments inferred from Content-Type" : "FETCH response unreadable; row rebuilt from raw headers, hasAttachments inferred from Content-Type"
|
|
58411
|
+
} : {},
|
|
58405
58412
|
// Message-ID (when the envelope carries it) is the strongest cross-/intra-
|
|
58406
58413
|
// backend dedup key for the multi-account merge (imapMultiAccount.ts). The
|
|
58407
58414
|
// AppleScript path does not expose it, so cross-backend dedup falls back to
|
|
@@ -58409,6 +58416,13 @@ function structuredRow(m, account, path) {
|
|
|
58409
58416
|
...env.messageId ? { messageId: env.messageId } : {}
|
|
58410
58417
|
};
|
|
58411
58418
|
}
|
|
58419
|
+
function omittedNote(omitted, merged) {
|
|
58420
|
+
if (omitted.length === 0) return "";
|
|
58421
|
+
const list = omitted.map((o) => `UID ${o.uid} in "${o.mailbox}" (${o.id})`).join(", ");
|
|
58422
|
+
return `
|
|
58423
|
+
|
|
58424
|
+
Partial result. ${omitted.length} message(s) ${merged ? "that may belong" : "that belong"} on this page could not be read and are not listed: ${list}. Reason: ${[...new Set(omitted.map((o) => o.reason))].join("; ")}. get-message or get-message-headers with those ids may still read them.`;
|
|
58425
|
+
}
|
|
58412
58426
|
function describeMailboxFailure(error) {
|
|
58413
58427
|
const raw = errText(error);
|
|
58414
58428
|
const oneLine = raw.split("\n")[0].trim();
|
|
@@ -58428,44 +58442,215 @@ function messageIdentity(entry) {
|
|
|
58428
58442
|
const messageId = raw.replace(/^<+|>+$/g, "").trim().toLowerCase();
|
|
58429
58443
|
return messageId ? `mid:${messageId}` : `${entry.path}\0${entry.message.uid}`;
|
|
58430
58444
|
}
|
|
58431
|
-
|
|
58432
|
-
const
|
|
58445
|
+
function isUnfiltered(criteria) {
|
|
58446
|
+
const keys = Object.keys(criteria);
|
|
58447
|
+
return keys.length === 1 && criteria.deleted === false;
|
|
58448
|
+
}
|
|
58449
|
+
async function statusMessageCount(client, path) {
|
|
58433
58450
|
try {
|
|
58434
|
-
const
|
|
58435
|
-
|
|
58436
|
-
|
|
58437
|
-
|
|
58438
|
-
|
|
58451
|
+
const st = await client.status(path, { messages: true });
|
|
58452
|
+
return typeof st.messages === "number" && st.messages >= 0 ? st.messages : void 0;
|
|
58453
|
+
} catch {
|
|
58454
|
+
return void 0;
|
|
58455
|
+
}
|
|
58456
|
+
}
|
|
58457
|
+
async function searchUids(client, path, criteria, size) {
|
|
58458
|
+
client.takeLastCommandError?.();
|
|
58459
|
+
const found = await client.search(criteria, { uid: true });
|
|
58460
|
+
if (Array.isArray(found)) return found;
|
|
58461
|
+
const cause = client.takeLastCommandError?.();
|
|
58462
|
+
const sized = size === void 0 ? "" : ` (${size.toLocaleString("en-US")} messages)`;
|
|
58463
|
+
throw new Error(
|
|
58464
|
+
`IMAP SEARCH on "${path}"${sized} failed: ` + (cause ? errText(cause) : "the server rejected it or the connection dropped before it answered (on a mailbox this size, usually a server-side timeout)")
|
|
58465
|
+
);
|
|
58466
|
+
}
|
|
58467
|
+
function mergeFetched(into, msg) {
|
|
58468
|
+
const prev = into.get(msg.uid);
|
|
58469
|
+
if (!prev) {
|
|
58470
|
+
into.set(msg.uid, msg);
|
|
58471
|
+
return;
|
|
58472
|
+
}
|
|
58473
|
+
const merged = { ...prev };
|
|
58474
|
+
for (const [key, value] of Object.entries(msg)) {
|
|
58475
|
+
if (value !== void 0 && value !== null)
|
|
58476
|
+
merged[key] = value;
|
|
58477
|
+
}
|
|
58478
|
+
into.set(msg.uid, merged);
|
|
58479
|
+
}
|
|
58480
|
+
async function fetchInto(client, uids, query, into) {
|
|
58481
|
+
const wanted = new Set(uids);
|
|
58482
|
+
for await (const msg of client.fetch(uids.join(","), query, { uid: true })) {
|
|
58483
|
+
if (typeof msg?.uid === "number" && wanted.has(msg.uid)) mergeFetched(into, msg);
|
|
58484
|
+
}
|
|
58485
|
+
}
|
|
58486
|
+
function fetchedHeaders(m) {
|
|
58487
|
+
return m.headers ? parseHeaderBlock(decodeHeaderBytes(asBuffer(m.headers))) : void 0;
|
|
58488
|
+
}
|
|
58489
|
+
function envelopeFromHeaders(m) {
|
|
58490
|
+
const h = fetchedHeaders(m);
|
|
58491
|
+
if (!h || h.headers.length === 0) return void 0;
|
|
58492
|
+
const env = {};
|
|
58493
|
+
if (h.subject) env.subject = h.subject;
|
|
58494
|
+
if (h.from) {
|
|
58495
|
+
const angle = h.from.match(/^\s*"?([^"<]*?)"?\s*<([^>]+)>/);
|
|
58496
|
+
env.from = angle ? [{ ...angle[1].trim() ? { name: angle[1].trim() } : {}, address: angle[2].trim() }] : [{ address: h.from.trim() }];
|
|
58497
|
+
}
|
|
58498
|
+
if (h.messageId) env.messageId = `<${h.messageId}>`;
|
|
58499
|
+
if (h.inReplyTo) env.inReplyTo = `<${h.inReplyTo}>`;
|
|
58500
|
+
if (h.dateHeader) env.date = h.dateHeader;
|
|
58501
|
+
return env;
|
|
58502
|
+
}
|
|
58503
|
+
function contentTypeSuggestsAttachments(m) {
|
|
58504
|
+
const type = fetchedHeaders(m)?.headers.find((h) => h.name.toLowerCase() === "content-type");
|
|
58505
|
+
return /^\s*multipart\/mixed\b/i.test(type?.value ?? "");
|
|
58506
|
+
}
|
|
58507
|
+
async function fetchRows(client, uids) {
|
|
58508
|
+
if (uids.length === 0) return { messages: [], omitted: [] };
|
|
58509
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
58510
|
+
const causes = [];
|
|
58511
|
+
const noteCause = () => {
|
|
58512
|
+
const err = client.takeLastCommandError?.();
|
|
58513
|
+
if (err !== void 0) {
|
|
58514
|
+
const text = describeMailboxFailure(err);
|
|
58515
|
+
if (!causes.includes(text)) causes.push(text);
|
|
58516
|
+
}
|
|
58517
|
+
};
|
|
58518
|
+
const missing = () => uids.filter((uid) => !byUid.get(uid)?.envelope);
|
|
58519
|
+
client.takeLastCommandError?.();
|
|
58520
|
+
await fetchInto(client, uids, ROW_QUERY, byUid);
|
|
58521
|
+
noteCause();
|
|
58522
|
+
let gap = missing();
|
|
58523
|
+
if (gap.length > 0) {
|
|
58524
|
+
await fetchInto(client, gap, ROW_QUERY, byUid);
|
|
58525
|
+
noteCause();
|
|
58526
|
+
gap = missing();
|
|
58527
|
+
}
|
|
58528
|
+
if (gap.length > 0) {
|
|
58529
|
+
const reduced = /* @__PURE__ */ new Map();
|
|
58530
|
+
await fetchInto(client, gap, ROW_QUERY_NO_STRUCTURE, reduced);
|
|
58531
|
+
noteCause();
|
|
58532
|
+
for (const msg of reduced.values()) {
|
|
58533
|
+
if (!msg.envelope) continue;
|
|
58534
|
+
mergeFetched(byUid, { ...msg, degraded: "bodystructure" });
|
|
58535
|
+
}
|
|
58536
|
+
gap = missing();
|
|
58537
|
+
}
|
|
58538
|
+
if (gap.length > 0) {
|
|
58539
|
+
for (const uid of gap) {
|
|
58540
|
+
const bare = /* @__PURE__ */ new Map();
|
|
58541
|
+
await fetchInto(client, [uid], ROW_QUERY_HEADERS_ONLY, bare);
|
|
58542
|
+
noteCause();
|
|
58543
|
+
const msg = bare.get(uid);
|
|
58544
|
+
const envelope = msg ? envelopeFromHeaders(msg) : void 0;
|
|
58545
|
+
if (msg && envelope) {
|
|
58546
|
+
mergeFetched(byUid, { ...msg, envelope, degraded: "envelope" });
|
|
58547
|
+
}
|
|
58548
|
+
}
|
|
58549
|
+
gap = missing();
|
|
58550
|
+
}
|
|
58551
|
+
const omitted = new Set(gap);
|
|
58552
|
+
return {
|
|
58553
|
+
messages: uids.filter((uid) => !omitted.has(uid)).map((uid) => byUid.get(uid)).filter((message) => message !== void 0),
|
|
58554
|
+
omitted: gap,
|
|
58555
|
+
...causes.length > 0 ? { cause: causes.join("; ") } : {}
|
|
58556
|
+
};
|
|
58557
|
+
}
|
|
58558
|
+
function pageRows(fetch) {
|
|
58559
|
+
return {
|
|
58560
|
+
messages: fetch.messages,
|
|
58561
|
+
omitted: fetch.omitted,
|
|
58562
|
+
...fetch.omitted.length > 0 ? { omittedReason: omissionReason(fetch) } : {}
|
|
58563
|
+
};
|
|
58564
|
+
}
|
|
58565
|
+
function omissionReason(fetch) {
|
|
58566
|
+
return "the server's FETCH response for this message could not be read, even without BODYSTRUCTURE or ENVELOPE" + (fetch.cause ? ` (${fetch.cause})` : "");
|
|
58567
|
+
}
|
|
58568
|
+
async function walkWindows(exists, page, firstWindow, readWindow) {
|
|
58569
|
+
const wanted = page.skip + page.take;
|
|
58570
|
+
const uids = [];
|
|
58571
|
+
let seen = 0;
|
|
58572
|
+
let matched = 0;
|
|
58573
|
+
let hi = exists;
|
|
58574
|
+
let width = Math.max(1, Math.min(firstWindow, MAX_WINDOW));
|
|
58575
|
+
while (hi >= 1 && seen < wanted) {
|
|
58576
|
+
const lo = Math.max(1, hi - width + 1);
|
|
58577
|
+
const live = (await readWindow(lo, hi === exists ? "*" : hi, hi - lo + 1)).slice().sort((a, b) => b - a);
|
|
58578
|
+
matched += live.length;
|
|
58579
|
+
for (const uid of live) {
|
|
58580
|
+
if (seen >= wanted) break;
|
|
58581
|
+
if (seen >= page.skip) uids.push(uid);
|
|
58582
|
+
seen++;
|
|
58583
|
+
}
|
|
58584
|
+
hi = lo - 1;
|
|
58585
|
+
width = Math.min(width * 4, MAX_WINDOW);
|
|
58586
|
+
}
|
|
58587
|
+
return { uids, matched, exhausted: hi < 1 };
|
|
58588
|
+
}
|
|
58589
|
+
async function listLargeMailbox(client, exists, page) {
|
|
58590
|
+
let deletedSeen = 0;
|
|
58591
|
+
const walk = await walkWindows(
|
|
58592
|
+
exists,
|
|
58593
|
+
page,
|
|
58594
|
+
// Room for a few ghosts on the first read without a second round trip.
|
|
58595
|
+
page.skip + page.take + 64,
|
|
58596
|
+
async (lo, hi) => {
|
|
58597
|
+
const live = [];
|
|
58598
|
+
for await (const msg of client.fetch(`${lo}:${hi}`, { uid: true, flags: true })) {
|
|
58599
|
+
if (msg.flags?.has("\\Deleted")) deletedSeen++;
|
|
58600
|
+
else live.push(msg.uid);
|
|
58601
|
+
}
|
|
58602
|
+
return live;
|
|
58603
|
+
}
|
|
58604
|
+
);
|
|
58605
|
+
return {
|
|
58606
|
+
...pageRows(await fetchRows(client, walk.uids)),
|
|
58607
|
+
total: Math.max(0, exists - deletedSeen),
|
|
58608
|
+
totalExact: true
|
|
58609
|
+
};
|
|
58610
|
+
}
|
|
58611
|
+
async function searchLargeMailbox(client, path, criteria, exists, page) {
|
|
58612
|
+
const walk = await walkWindows(exists, page, FIRST_SEARCH_WINDOW, async (lo, hi, width) => {
|
|
58613
|
+
const found = await searchUids(client, path, { ...criteria, seq: `${lo}:${hi}` }, exists);
|
|
58614
|
+
if (found.length > width) {
|
|
58439
58615
|
throw new Error(
|
|
58440
|
-
`IMAP SEARCH on "${path}" reported ${
|
|
58616
|
+
`IMAP SEARCH on "${path}" reported ${found.length} matches in a ${width}-message window \u2014 discarding as corrupted rather than trusting it (see #246).`
|
|
58441
58617
|
);
|
|
58442
58618
|
}
|
|
58443
|
-
|
|
58444
|
-
|
|
58445
|
-
|
|
58446
|
-
|
|
58447
|
-
|
|
58448
|
-
|
|
58449
|
-
|
|
58450
|
-
|
|
58451
|
-
|
|
58452
|
-
|
|
58453
|
-
|
|
58454
|
-
|
|
58455
|
-
|
|
58456
|
-
|
|
58457
|
-
|
|
58458
|
-
|
|
58459
|
-
|
|
58460
|
-
|
|
58461
|
-
|
|
58462
|
-
|
|
58463
|
-
|
|
58619
|
+
return found;
|
|
58620
|
+
});
|
|
58621
|
+
return {
|
|
58622
|
+
...pageRows(await fetchRows(client, walk.uids)),
|
|
58623
|
+
total: walk.matched,
|
|
58624
|
+
totalExact: walk.exhausted
|
|
58625
|
+
};
|
|
58626
|
+
}
|
|
58627
|
+
async function fetchMailboxMatches(client, path, criteria, page) {
|
|
58628
|
+
const lock = await client.getMailboxLock(path);
|
|
58629
|
+
try {
|
|
58630
|
+
const exists = await statusMessageCount(client, path);
|
|
58631
|
+
if (exists === 0) return { messages: [], omitted: [], total: 0, totalExact: true };
|
|
58632
|
+
if (exists !== void 0 && exists > LARGE_MAILBOX_MESSAGES) {
|
|
58633
|
+
try {
|
|
58634
|
+
return isUnfiltered(criteria) ? await listLargeMailbox(client, exists, page) : await searchLargeMailbox(client, path, criteria, exists, page);
|
|
58635
|
+
} catch (error) {
|
|
58636
|
+
const detail = errText(error);
|
|
58637
|
+
throw new Error(
|
|
58638
|
+
detail.includes(`"${path}" (`) ? detail : `reading the newest messages of "${path}" (${exists.toLocaleString("en-US")} messages) failed: ${detail}`
|
|
58639
|
+
);
|
|
58640
|
+
}
|
|
58464
58641
|
}
|
|
58465
|
-
|
|
58466
|
-
|
|
58467
|
-
total: uids.length
|
|
58468
|
-
}
|
|
58642
|
+
const uids = await searchUids(client, path, criteria, exists);
|
|
58643
|
+
if (uids.length === 0 || page.take === 0) {
|
|
58644
|
+
return { messages: [], omitted: [], total: uids.length, totalExact: true };
|
|
58645
|
+
}
|
|
58646
|
+
const messages = exists ?? (await client.status(path, { messages: true })).messages ?? void 0;
|
|
58647
|
+
if (typeof messages === "number" && uids.length > messages) {
|
|
58648
|
+
throw new Error(
|
|
58649
|
+
`IMAP SEARCH on "${path}" reported ${uids.length} matches, more than the mailbox's own ${messages} messages \u2014 discarding as corrupted rather than trusting it (see #246).`
|
|
58650
|
+
);
|
|
58651
|
+
}
|
|
58652
|
+
const newest = uids.slice().reverse().slice(page.skip, page.skip + page.take);
|
|
58653
|
+
return { ...pageRows(await fetchRows(client, newest)), total: uids.length, totalExact: true };
|
|
58469
58654
|
} finally {
|
|
58470
58655
|
lock.release();
|
|
58471
58656
|
}
|
|
@@ -58494,16 +58679,27 @@ async function run(args, listMode, deps) {
|
|
|
58494
58679
|
const limit = args.limit ?? 50;
|
|
58495
58680
|
const offset = args.offset ?? 0;
|
|
58496
58681
|
const criteria = buildCriteria(args, listMode);
|
|
58497
|
-
const
|
|
58682
|
+
const page = unscopedSearch ? { skip: 0, take: offset + limit } : { skip: offset, take: limit };
|
|
58498
58683
|
const fetched = [];
|
|
58499
58684
|
const failedMailboxes = [];
|
|
58500
58685
|
const failedMailboxReasons = {};
|
|
58686
|
+
const omittedMessages = [];
|
|
58501
58687
|
let totalMatched = 0;
|
|
58688
|
+
let totalExact = true;
|
|
58502
58689
|
for (const path of paths) {
|
|
58503
58690
|
try {
|
|
58504
|
-
const result = await fetchMailboxMatches(client, path, criteria,
|
|
58691
|
+
const result = await fetchMailboxMatches(client, path, criteria, page);
|
|
58505
58692
|
totalMatched += result.total;
|
|
58693
|
+
totalExact &&= result.totalExact;
|
|
58506
58694
|
fetched.push(...result.messages.map((message) => ({ message, path })));
|
|
58695
|
+
for (const uid of result.omitted) {
|
|
58696
|
+
omittedMessages.push({
|
|
58697
|
+
id: encodeImapId(cfg.accountLabel, path, uid),
|
|
58698
|
+
mailbox: path,
|
|
58699
|
+
uid,
|
|
58700
|
+
reason: result.omittedReason ?? "the server's FETCH response could not be read"
|
|
58701
|
+
});
|
|
58702
|
+
}
|
|
58507
58703
|
} catch (error) {
|
|
58508
58704
|
failedMailboxes.push(path);
|
|
58509
58705
|
failedMailboxReasons[path] = describeMailboxFailure(error);
|
|
@@ -58527,18 +58723,17 @@ async function run(args, listMode, deps) {
|
|
|
58527
58723
|
if (!unique.has(key)) unique.set(key, entry);
|
|
58528
58724
|
}
|
|
58529
58725
|
ordered = [...unique.values()].slice(offset, offset + limit);
|
|
58530
|
-
} else {
|
|
58531
|
-
ordered = fetched.slice(offset, offset + limit);
|
|
58532
58726
|
}
|
|
58533
58727
|
const rows = ordered.map(({ message, path }) => formatRow(message, cfg.accountLabel, path));
|
|
58534
58728
|
const messages = ordered.map(
|
|
58535
58729
|
({ message, path }) => structuredRow(message, cfg.accountLabel, path)
|
|
58536
58730
|
);
|
|
58537
|
-
const partial = failedMailboxes.length > 0;
|
|
58538
|
-
const failureNote =
|
|
58731
|
+
const partial = failedMailboxes.length > 0 || omittedMessages.length > 0;
|
|
58732
|
+
const failureNote = (failedMailboxes.length > 0 ? `
|
|
58539
58733
|
|
|
58540
|
-
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
|
|
58734
|
+
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch);
|
|
58541
58735
|
const verb = listMode ? "listed" : "matched";
|
|
58736
|
+
const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
|
|
58542
58737
|
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
58543
58738
|
if (messages.length === 0) {
|
|
58544
58739
|
return {
|
|
@@ -58547,10 +58742,11 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
|
|
|
58547
58742
|
count: 0,
|
|
58548
58743
|
partial,
|
|
58549
58744
|
failedMailboxes,
|
|
58550
|
-
failedMailboxReasons
|
|
58745
|
+
failedMailboxReasons,
|
|
58746
|
+
omittedMessages
|
|
58551
58747
|
};
|
|
58552
58748
|
}
|
|
58553
|
-
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${
|
|
58749
|
+
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalText} ${verb}):
|
|
58554
58750
|
` + rows.join("\n") + `
|
|
58555
58751
|
|
|
58556
58752
|
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;
|
|
@@ -58560,7 +58756,8 @@ Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutatio
|
|
|
58560
58756
|
count: messages.length,
|
|
58561
58757
|
partial,
|
|
58562
58758
|
failedMailboxes,
|
|
58563
|
-
failedMailboxReasons
|
|
58759
|
+
failedMailboxReasons,
|
|
58760
|
+
omittedMessages
|
|
58564
58761
|
};
|
|
58565
58762
|
},
|
|
58566
58763
|
true
|
|
@@ -59520,31 +59717,21 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59520
59717
|
}
|
|
59521
59718
|
if (uidSet.size <= 1) return null;
|
|
59522
59719
|
const uids = [...uidSet].slice(0, limit);
|
|
59523
|
-
const
|
|
59524
|
-
|
|
59525
|
-
|
|
59526
|
-
|
|
59527
|
-
|
|
59528
|
-
|
|
59529
|
-
|
|
59530
|
-
|
|
59531
|
-
// sanity-checked exactly as a row's dateSent is (#234).
|
|
59532
|
-
{
|
|
59533
|
-
envelope: true,
|
|
59534
|
-
flags: true,
|
|
59535
|
-
bodyStructure: true,
|
|
59536
|
-
internalDate: true,
|
|
59537
|
-
headers: ["date"]
|
|
59538
|
-
},
|
|
59539
|
-
{ uid: true }
|
|
59540
|
-
)) {
|
|
59541
|
-
msgs.push(msg);
|
|
59542
|
-
}
|
|
59720
|
+
const rows = await fetchRows(client, uids);
|
|
59721
|
+
const msgs = rows.messages.slice();
|
|
59722
|
+
const omittedMessages = rows.omitted.map((uid) => ({
|
|
59723
|
+
id: encodeImapId(ref.account, ref.path, uid),
|
|
59724
|
+
mailbox: ref.path,
|
|
59725
|
+
uid,
|
|
59726
|
+
reason: omissionReason(rows)
|
|
59727
|
+
}));
|
|
59543
59728
|
msgs.sort((a, b) => dateMs(a) - dateMs(b));
|
|
59544
59729
|
const subject = seed.envelope?.subject || "(no subject)";
|
|
59545
59730
|
const structured = {
|
|
59546
59731
|
subject,
|
|
59547
59732
|
count: msgs.length,
|
|
59733
|
+
partial: omittedMessages.length > 0,
|
|
59734
|
+
omittedMessages,
|
|
59548
59735
|
messages: msgs.map((m) => ({
|
|
59549
59736
|
id: encodeImapId(ref.account, ref.path, m.uid),
|
|
59550
59737
|
subject: m.envelope?.subject || "(no subject)",
|
|
@@ -59559,7 +59746,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59559
59746
|
}))
|
|
59560
59747
|
};
|
|
59561
59748
|
const text = `Thread "${subject}" \u2014 ${msgs.length} message(s) via IMAP (References-linked, oldest first):
|
|
59562
|
-
` + msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n");
|
|
59749
|
+
` + msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n") + omittedNote(omittedMessages, false);
|
|
59563
59750
|
return { count: msgs.length, text, structured };
|
|
59564
59751
|
} finally {
|
|
59565
59752
|
lock.release();
|
|
@@ -59568,7 +59755,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59568
59755
|
true
|
|
59569
59756
|
);
|
|
59570
59757
|
}
|
|
59571
|
-
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
|
|
59758
|
+
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
|
|
59572
59759
|
var init_imapClient = __esm({
|
|
59573
59760
|
"src/services/imapClient.ts"() {
|
|
59574
59761
|
"use strict";
|
|
@@ -59594,7 +59781,24 @@ var init_imapClient = __esm({
|
|
|
59594
59781
|
accounts: "APPLE_MAIL_MCP_IMAP_ACCOUNTS"
|
|
59595
59782
|
};
|
|
59596
59783
|
defaultConnect = async (cfg) => {
|
|
59597
|
-
|
|
59784
|
+
let lastCommandError;
|
|
59785
|
+
const keepErr = (entry) => {
|
|
59786
|
+
if (entry && typeof entry === "object" && "err" in entry) {
|
|
59787
|
+
lastCommandError = entry.err;
|
|
59788
|
+
}
|
|
59789
|
+
};
|
|
59790
|
+
const noop = () => void 0;
|
|
59791
|
+
const client = new import_imapflow.ImapFlow({
|
|
59792
|
+
...buildImapConnectionOptions(cfg),
|
|
59793
|
+
logger: { trace: noop, debug: noop, info: noop, warn: keepErr, error: keepErr, fatal: keepErr }
|
|
59794
|
+
});
|
|
59795
|
+
Object.assign(client, {
|
|
59796
|
+
takeLastCommandError: () => {
|
|
59797
|
+
const err = lastCommandError;
|
|
59798
|
+
lastCommandError = void 0;
|
|
59799
|
+
return err;
|
|
59800
|
+
}
|
|
59801
|
+
});
|
|
59598
59802
|
client.on("error", () => {
|
|
59599
59803
|
});
|
|
59600
59804
|
try {
|
|
@@ -59622,6 +59826,27 @@ var init_imapClient = __esm({
|
|
|
59622
59826
|
starred: "\\flagged"
|
|
59623
59827
|
};
|
|
59624
59828
|
NOT_DELETED = { deleted: false };
|
|
59829
|
+
LARGE_MAILBOX_MESSAGES = 1e4;
|
|
59830
|
+
FIRST_SEARCH_WINDOW = 5e3;
|
|
59831
|
+
MAX_WINDOW = 5e4;
|
|
59832
|
+
ROW_QUERY = {
|
|
59833
|
+
envelope: true,
|
|
59834
|
+
flags: true,
|
|
59835
|
+
bodyStructure: true,
|
|
59836
|
+
internalDate: true,
|
|
59837
|
+
headers: ["date"]
|
|
59838
|
+
};
|
|
59839
|
+
ROW_QUERY_NO_STRUCTURE = {
|
|
59840
|
+
envelope: true,
|
|
59841
|
+
flags: true,
|
|
59842
|
+
internalDate: true,
|
|
59843
|
+
headers: ["date", "content-type"]
|
|
59844
|
+
};
|
|
59845
|
+
ROW_QUERY_HEADERS_ONLY = {
|
|
59846
|
+
flags: true,
|
|
59847
|
+
internalDate: true,
|
|
59848
|
+
headers: ["date", "from", "subject", "message-id", "in-reply-to", "content-type"]
|
|
59849
|
+
};
|
|
59625
59850
|
poolConnect = defaultConnect;
|
|
59626
59851
|
pools = /* @__PURE__ */ new Map();
|
|
59627
59852
|
connecting = /* @__PURE__ */ new Map();
|
package/build/index.js
CHANGED
|
@@ -65374,6 +65374,7 @@ var imapClient_exports = {};
|
|
|
65374
65374
|
__export(imapClient_exports, {
|
|
65375
65375
|
HEADER_WINDOW_BYTES: () => HEADER_WINDOW_BYTES,
|
|
65376
65376
|
IMAP_ENV: () => IMAP_ENV,
|
|
65377
|
+
LARGE_MAILBOX_MESSAGES: () => LARGE_MAILBOX_MESSAGES,
|
|
65377
65378
|
MAX_COMPOSE_SOURCE_BYTES: () => MAX_COMPOSE_SOURCE_BYTES,
|
|
65378
65379
|
MAX_RFC822_FILE_BYTES: () => MAX_RFC822_FILE_BYTES,
|
|
65379
65380
|
MAX_RFC822_INLINE_BYTES: () => MAX_RFC822_INLINE_BYTES,
|
|
@@ -65420,6 +65421,7 @@ __export(imapClient_exports, {
|
|
|
65420
65421
|
mailFlagColorIndex: () => mailFlagColorIndex,
|
|
65421
65422
|
matchMailbox: () => matchMailbox,
|
|
65422
65423
|
normalizeMessageId: () => normalizeMessageId,
|
|
65424
|
+
omittedNote: () => omittedNote,
|
|
65423
65425
|
resolveImapConfig: () => resolveImapConfig,
|
|
65424
65426
|
resolveImapConfigs: () => resolveImapConfigs,
|
|
65425
65427
|
resolveMailboxPath: () => resolveMailboxPath,
|
|
@@ -65730,7 +65732,12 @@ function structuredRow(m, account, path) {
|
|
|
65730
65732
|
// a caller from "no attachments", so every IMAP-sourced message claimed to
|
|
65731
65733
|
// have none. Falls back to false only when the fetch carried no
|
|
65732
65734
|
// BODYSTRUCTURE at all.
|
|
65733
|
-
|
|
65735
|
+
// A row read without BODYSTRUCTURE (#256 follow-up) judges by its top-level
|
|
65736
|
+
// Content-Type instead, and says so in `metadataIncomplete`.
|
|
65737
|
+
hasAttachments: m.bodyStructure ? bodyStructureHasAttachments(m.bodyStructure) : m.degraded ? contentTypeSuggestsAttachments(m) : false,
|
|
65738
|
+
...m.degraded ? {
|
|
65739
|
+
metadataIncomplete: m.degraded === "bodystructure" ? "BODYSTRUCTURE unreadable; hasAttachments inferred from Content-Type" : "FETCH response unreadable; row rebuilt from raw headers, hasAttachments inferred from Content-Type"
|
|
65740
|
+
} : {},
|
|
65734
65741
|
// Message-ID (when the envelope carries it) is the strongest cross-/intra-
|
|
65735
65742
|
// backend dedup key for the multi-account merge (imapMultiAccount.ts). The
|
|
65736
65743
|
// AppleScript path does not expose it, so cross-backend dedup falls back to
|
|
@@ -65738,6 +65745,13 @@ function structuredRow(m, account, path) {
|
|
|
65738
65745
|
...env.messageId ? { messageId: env.messageId } : {}
|
|
65739
65746
|
};
|
|
65740
65747
|
}
|
|
65748
|
+
function omittedNote(omitted, merged) {
|
|
65749
|
+
if (omitted.length === 0) return "";
|
|
65750
|
+
const list = omitted.map((o) => `UID ${o.uid} in "${o.mailbox}" (${o.id})`).join(", ");
|
|
65751
|
+
return `
|
|
65752
|
+
|
|
65753
|
+
Partial result. ${omitted.length} message(s) ${merged ? "that may belong" : "that belong"} on this page could not be read and are not listed: ${list}. Reason: ${[...new Set(omitted.map((o) => o.reason))].join("; ")}. get-message or get-message-headers with those ids may still read them.`;
|
|
65754
|
+
}
|
|
65741
65755
|
function describeMailboxFailure(error2) {
|
|
65742
65756
|
const raw = errText(error2);
|
|
65743
65757
|
const oneLine = raw.split("\n")[0].trim();
|
|
@@ -65757,44 +65771,215 @@ function messageIdentity(entry) {
|
|
|
65757
65771
|
const messageId = raw.replace(/^<+|>+$/g, "").trim().toLowerCase();
|
|
65758
65772
|
return messageId ? `mid:${messageId}` : `${entry.path}\0${entry.message.uid}`;
|
|
65759
65773
|
}
|
|
65760
|
-
|
|
65761
|
-
const
|
|
65774
|
+
function isUnfiltered(criteria) {
|
|
65775
|
+
const keys = Object.keys(criteria);
|
|
65776
|
+
return keys.length === 1 && criteria.deleted === false;
|
|
65777
|
+
}
|
|
65778
|
+
async function statusMessageCount(client, path) {
|
|
65762
65779
|
try {
|
|
65763
|
-
const
|
|
65764
|
-
|
|
65765
|
-
|
|
65766
|
-
|
|
65767
|
-
|
|
65780
|
+
const st = await client.status(path, { messages: true });
|
|
65781
|
+
return typeof st.messages === "number" && st.messages >= 0 ? st.messages : void 0;
|
|
65782
|
+
} catch {
|
|
65783
|
+
return void 0;
|
|
65784
|
+
}
|
|
65785
|
+
}
|
|
65786
|
+
async function searchUids(client, path, criteria, size) {
|
|
65787
|
+
client.takeLastCommandError?.();
|
|
65788
|
+
const found = await client.search(criteria, { uid: true });
|
|
65789
|
+
if (Array.isArray(found)) return found;
|
|
65790
|
+
const cause = client.takeLastCommandError?.();
|
|
65791
|
+
const sized = size === void 0 ? "" : ` (${size.toLocaleString("en-US")} messages)`;
|
|
65792
|
+
throw new Error(
|
|
65793
|
+
`IMAP SEARCH on "${path}"${sized} failed: ` + (cause ? errText(cause) : "the server rejected it or the connection dropped before it answered (on a mailbox this size, usually a server-side timeout)")
|
|
65794
|
+
);
|
|
65795
|
+
}
|
|
65796
|
+
function mergeFetched(into, msg) {
|
|
65797
|
+
const prev = into.get(msg.uid);
|
|
65798
|
+
if (!prev) {
|
|
65799
|
+
into.set(msg.uid, msg);
|
|
65800
|
+
return;
|
|
65801
|
+
}
|
|
65802
|
+
const merged = { ...prev };
|
|
65803
|
+
for (const [key, value] of Object.entries(msg)) {
|
|
65804
|
+
if (value !== void 0 && value !== null)
|
|
65805
|
+
merged[key] = value;
|
|
65806
|
+
}
|
|
65807
|
+
into.set(msg.uid, merged);
|
|
65808
|
+
}
|
|
65809
|
+
async function fetchInto(client, uids, query, into) {
|
|
65810
|
+
const wanted = new Set(uids);
|
|
65811
|
+
for await (const msg of client.fetch(uids.join(","), query, { uid: true })) {
|
|
65812
|
+
if (typeof msg?.uid === "number" && wanted.has(msg.uid)) mergeFetched(into, msg);
|
|
65813
|
+
}
|
|
65814
|
+
}
|
|
65815
|
+
function fetchedHeaders(m) {
|
|
65816
|
+
return m.headers ? parseHeaderBlock(decodeHeaderBytes(asBuffer(m.headers))) : void 0;
|
|
65817
|
+
}
|
|
65818
|
+
function envelopeFromHeaders(m) {
|
|
65819
|
+
const h = fetchedHeaders(m);
|
|
65820
|
+
if (!h || h.headers.length === 0) return void 0;
|
|
65821
|
+
const env = {};
|
|
65822
|
+
if (h.subject) env.subject = h.subject;
|
|
65823
|
+
if (h.from) {
|
|
65824
|
+
const angle = h.from.match(/^\s*"?([^"<]*?)"?\s*<([^>]+)>/);
|
|
65825
|
+
env.from = angle ? [{ ...angle[1].trim() ? { name: angle[1].trim() } : {}, address: angle[2].trim() }] : [{ address: h.from.trim() }];
|
|
65826
|
+
}
|
|
65827
|
+
if (h.messageId) env.messageId = `<${h.messageId}>`;
|
|
65828
|
+
if (h.inReplyTo) env.inReplyTo = `<${h.inReplyTo}>`;
|
|
65829
|
+
if (h.dateHeader) env.date = h.dateHeader;
|
|
65830
|
+
return env;
|
|
65831
|
+
}
|
|
65832
|
+
function contentTypeSuggestsAttachments(m) {
|
|
65833
|
+
const type = fetchedHeaders(m)?.headers.find((h) => h.name.toLowerCase() === "content-type");
|
|
65834
|
+
return /^\s*multipart\/mixed\b/i.test(type?.value ?? "");
|
|
65835
|
+
}
|
|
65836
|
+
async function fetchRows(client, uids) {
|
|
65837
|
+
if (uids.length === 0) return { messages: [], omitted: [] };
|
|
65838
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
65839
|
+
const causes = [];
|
|
65840
|
+
const noteCause = () => {
|
|
65841
|
+
const err = client.takeLastCommandError?.();
|
|
65842
|
+
if (err !== void 0) {
|
|
65843
|
+
const text = describeMailboxFailure(err);
|
|
65844
|
+
if (!causes.includes(text)) causes.push(text);
|
|
65845
|
+
}
|
|
65846
|
+
};
|
|
65847
|
+
const missing = () => uids.filter((uid) => !byUid.get(uid)?.envelope);
|
|
65848
|
+
client.takeLastCommandError?.();
|
|
65849
|
+
await fetchInto(client, uids, ROW_QUERY, byUid);
|
|
65850
|
+
noteCause();
|
|
65851
|
+
let gap = missing();
|
|
65852
|
+
if (gap.length > 0) {
|
|
65853
|
+
await fetchInto(client, gap, ROW_QUERY, byUid);
|
|
65854
|
+
noteCause();
|
|
65855
|
+
gap = missing();
|
|
65856
|
+
}
|
|
65857
|
+
if (gap.length > 0) {
|
|
65858
|
+
const reduced = /* @__PURE__ */ new Map();
|
|
65859
|
+
await fetchInto(client, gap, ROW_QUERY_NO_STRUCTURE, reduced);
|
|
65860
|
+
noteCause();
|
|
65861
|
+
for (const msg of reduced.values()) {
|
|
65862
|
+
if (!msg.envelope) continue;
|
|
65863
|
+
mergeFetched(byUid, { ...msg, degraded: "bodystructure" });
|
|
65864
|
+
}
|
|
65865
|
+
gap = missing();
|
|
65866
|
+
}
|
|
65867
|
+
if (gap.length > 0) {
|
|
65868
|
+
for (const uid of gap) {
|
|
65869
|
+
const bare = /* @__PURE__ */ new Map();
|
|
65870
|
+
await fetchInto(client, [uid], ROW_QUERY_HEADERS_ONLY, bare);
|
|
65871
|
+
noteCause();
|
|
65872
|
+
const msg = bare.get(uid);
|
|
65873
|
+
const envelope = msg ? envelopeFromHeaders(msg) : void 0;
|
|
65874
|
+
if (msg && envelope) {
|
|
65875
|
+
mergeFetched(byUid, { ...msg, envelope, degraded: "envelope" });
|
|
65876
|
+
}
|
|
65877
|
+
}
|
|
65878
|
+
gap = missing();
|
|
65879
|
+
}
|
|
65880
|
+
const omitted = new Set(gap);
|
|
65881
|
+
return {
|
|
65882
|
+
messages: uids.filter((uid) => !omitted.has(uid)).map((uid) => byUid.get(uid)).filter((message) => message !== void 0),
|
|
65883
|
+
omitted: gap,
|
|
65884
|
+
...causes.length > 0 ? { cause: causes.join("; ") } : {}
|
|
65885
|
+
};
|
|
65886
|
+
}
|
|
65887
|
+
function pageRows(fetch) {
|
|
65888
|
+
return {
|
|
65889
|
+
messages: fetch.messages,
|
|
65890
|
+
omitted: fetch.omitted,
|
|
65891
|
+
...fetch.omitted.length > 0 ? { omittedReason: omissionReason(fetch) } : {}
|
|
65892
|
+
};
|
|
65893
|
+
}
|
|
65894
|
+
function omissionReason(fetch) {
|
|
65895
|
+
return "the server's FETCH response for this message could not be read, even without BODYSTRUCTURE or ENVELOPE" + (fetch.cause ? ` (${fetch.cause})` : "");
|
|
65896
|
+
}
|
|
65897
|
+
async function walkWindows(exists, page, firstWindow, readWindow) {
|
|
65898
|
+
const wanted = page.skip + page.take;
|
|
65899
|
+
const uids = [];
|
|
65900
|
+
let seen = 0;
|
|
65901
|
+
let matched = 0;
|
|
65902
|
+
let hi = exists;
|
|
65903
|
+
let width = Math.max(1, Math.min(firstWindow, MAX_WINDOW));
|
|
65904
|
+
while (hi >= 1 && seen < wanted) {
|
|
65905
|
+
const lo = Math.max(1, hi - width + 1);
|
|
65906
|
+
const live = (await readWindow(lo, hi === exists ? "*" : hi, hi - lo + 1)).slice().sort((a, b) => b - a);
|
|
65907
|
+
matched += live.length;
|
|
65908
|
+
for (const uid of live) {
|
|
65909
|
+
if (seen >= wanted) break;
|
|
65910
|
+
if (seen >= page.skip) uids.push(uid);
|
|
65911
|
+
seen++;
|
|
65912
|
+
}
|
|
65913
|
+
hi = lo - 1;
|
|
65914
|
+
width = Math.min(width * 4, MAX_WINDOW);
|
|
65915
|
+
}
|
|
65916
|
+
return { uids, matched, exhausted: hi < 1 };
|
|
65917
|
+
}
|
|
65918
|
+
async function listLargeMailbox(client, exists, page) {
|
|
65919
|
+
let deletedSeen = 0;
|
|
65920
|
+
const walk = await walkWindows(
|
|
65921
|
+
exists,
|
|
65922
|
+
page,
|
|
65923
|
+
// Room for a few ghosts on the first read without a second round trip.
|
|
65924
|
+
page.skip + page.take + 64,
|
|
65925
|
+
async (lo, hi) => {
|
|
65926
|
+
const live = [];
|
|
65927
|
+
for await (const msg of client.fetch(`${lo}:${hi}`, { uid: true, flags: true })) {
|
|
65928
|
+
if (msg.flags?.has("\\Deleted")) deletedSeen++;
|
|
65929
|
+
else live.push(msg.uid);
|
|
65930
|
+
}
|
|
65931
|
+
return live;
|
|
65932
|
+
}
|
|
65933
|
+
);
|
|
65934
|
+
return {
|
|
65935
|
+
...pageRows(await fetchRows(client, walk.uids)),
|
|
65936
|
+
total: Math.max(0, exists - deletedSeen),
|
|
65937
|
+
totalExact: true
|
|
65938
|
+
};
|
|
65939
|
+
}
|
|
65940
|
+
async function searchLargeMailbox(client, path, criteria, exists, page) {
|
|
65941
|
+
const walk = await walkWindows(exists, page, FIRST_SEARCH_WINDOW, async (lo, hi, width) => {
|
|
65942
|
+
const found = await searchUids(client, path, { ...criteria, seq: `${lo}:${hi}` }, exists);
|
|
65943
|
+
if (found.length > width) {
|
|
65768
65944
|
throw new Error(
|
|
65769
|
-
`IMAP SEARCH on "${path}" reported ${
|
|
65945
|
+
`IMAP SEARCH on "${path}" reported ${found.length} matches in a ${width}-message window \u2014 discarding as corrupted rather than trusting it (see #246).`
|
|
65770
65946
|
);
|
|
65771
65947
|
}
|
|
65772
|
-
|
|
65773
|
-
|
|
65774
|
-
|
|
65775
|
-
|
|
65776
|
-
|
|
65777
|
-
|
|
65778
|
-
|
|
65779
|
-
|
|
65780
|
-
|
|
65781
|
-
|
|
65782
|
-
|
|
65783
|
-
|
|
65784
|
-
|
|
65785
|
-
|
|
65786
|
-
|
|
65787
|
-
|
|
65788
|
-
|
|
65789
|
-
|
|
65790
|
-
|
|
65791
|
-
|
|
65792
|
-
|
|
65948
|
+
return found;
|
|
65949
|
+
});
|
|
65950
|
+
return {
|
|
65951
|
+
...pageRows(await fetchRows(client, walk.uids)),
|
|
65952
|
+
total: walk.matched,
|
|
65953
|
+
totalExact: walk.exhausted
|
|
65954
|
+
};
|
|
65955
|
+
}
|
|
65956
|
+
async function fetchMailboxMatches(client, path, criteria, page) {
|
|
65957
|
+
const lock = await client.getMailboxLock(path);
|
|
65958
|
+
try {
|
|
65959
|
+
const exists = await statusMessageCount(client, path);
|
|
65960
|
+
if (exists === 0) return { messages: [], omitted: [], total: 0, totalExact: true };
|
|
65961
|
+
if (exists !== void 0 && exists > LARGE_MAILBOX_MESSAGES) {
|
|
65962
|
+
try {
|
|
65963
|
+
return isUnfiltered(criteria) ? await listLargeMailbox(client, exists, page) : await searchLargeMailbox(client, path, criteria, exists, page);
|
|
65964
|
+
} catch (error2) {
|
|
65965
|
+
const detail = errText(error2);
|
|
65966
|
+
throw new Error(
|
|
65967
|
+
detail.includes(`"${path}" (`) ? detail : `reading the newest messages of "${path}" (${exists.toLocaleString("en-US")} messages) failed: ${detail}`
|
|
65968
|
+
);
|
|
65969
|
+
}
|
|
65793
65970
|
}
|
|
65794
|
-
|
|
65795
|
-
|
|
65796
|
-
total: uids.length
|
|
65797
|
-
}
|
|
65971
|
+
const uids = await searchUids(client, path, criteria, exists);
|
|
65972
|
+
if (uids.length === 0 || page.take === 0) {
|
|
65973
|
+
return { messages: [], omitted: [], total: uids.length, totalExact: true };
|
|
65974
|
+
}
|
|
65975
|
+
const messages = exists ?? (await client.status(path, { messages: true })).messages ?? void 0;
|
|
65976
|
+
if (typeof messages === "number" && uids.length > messages) {
|
|
65977
|
+
throw new Error(
|
|
65978
|
+
`IMAP SEARCH on "${path}" reported ${uids.length} matches, more than the mailbox's own ${messages} messages \u2014 discarding as corrupted rather than trusting it (see #246).`
|
|
65979
|
+
);
|
|
65980
|
+
}
|
|
65981
|
+
const newest = uids.slice().reverse().slice(page.skip, page.skip + page.take);
|
|
65982
|
+
return { ...pageRows(await fetchRows(client, newest)), total: uids.length, totalExact: true };
|
|
65798
65983
|
} finally {
|
|
65799
65984
|
lock.release();
|
|
65800
65985
|
}
|
|
@@ -65823,16 +66008,27 @@ async function run(args, listMode, deps) {
|
|
|
65823
66008
|
const limit = args.limit ?? 50;
|
|
65824
66009
|
const offset = args.offset ?? 0;
|
|
65825
66010
|
const criteria = buildCriteria(args, listMode);
|
|
65826
|
-
const
|
|
66011
|
+
const page = unscopedSearch ? { skip: 0, take: offset + limit } : { skip: offset, take: limit };
|
|
65827
66012
|
const fetched = [];
|
|
65828
66013
|
const failedMailboxes = [];
|
|
65829
66014
|
const failedMailboxReasons = {};
|
|
66015
|
+
const omittedMessages = [];
|
|
65830
66016
|
let totalMatched = 0;
|
|
66017
|
+
let totalExact = true;
|
|
65831
66018
|
for (const path of paths) {
|
|
65832
66019
|
try {
|
|
65833
|
-
const result = await fetchMailboxMatches(client, path, criteria,
|
|
66020
|
+
const result = await fetchMailboxMatches(client, path, criteria, page);
|
|
65834
66021
|
totalMatched += result.total;
|
|
66022
|
+
totalExact &&= result.totalExact;
|
|
65835
66023
|
fetched.push(...result.messages.map((message) => ({ message, path })));
|
|
66024
|
+
for (const uid of result.omitted) {
|
|
66025
|
+
omittedMessages.push({
|
|
66026
|
+
id: encodeImapId(cfg.accountLabel, path, uid),
|
|
66027
|
+
mailbox: path,
|
|
66028
|
+
uid,
|
|
66029
|
+
reason: result.omittedReason ?? "the server's FETCH response could not be read"
|
|
66030
|
+
});
|
|
66031
|
+
}
|
|
65836
66032
|
} catch (error2) {
|
|
65837
66033
|
failedMailboxes.push(path);
|
|
65838
66034
|
failedMailboxReasons[path] = describeMailboxFailure(error2);
|
|
@@ -65856,18 +66052,17 @@ async function run(args, listMode, deps) {
|
|
|
65856
66052
|
if (!unique.has(key)) unique.set(key, entry);
|
|
65857
66053
|
}
|
|
65858
66054
|
ordered = [...unique.values()].slice(offset, offset + limit);
|
|
65859
|
-
} else {
|
|
65860
|
-
ordered = fetched.slice(offset, offset + limit);
|
|
65861
66055
|
}
|
|
65862
66056
|
const rows = ordered.map(({ message, path }) => formatRow(message, cfg.accountLabel, path));
|
|
65863
66057
|
const messages = ordered.map(
|
|
65864
66058
|
({ message, path }) => structuredRow(message, cfg.accountLabel, path)
|
|
65865
66059
|
);
|
|
65866
|
-
const partial2 = failedMailboxes.length > 0;
|
|
65867
|
-
const failureNote =
|
|
66060
|
+
const partial2 = failedMailboxes.length > 0 || omittedMessages.length > 0;
|
|
66061
|
+
const failureNote = (failedMailboxes.length > 0 ? `
|
|
65868
66062
|
|
|
65869
|
-
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
|
|
66063
|
+
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch);
|
|
65870
66064
|
const verb = listMode ? "listed" : "matched";
|
|
66065
|
+
const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
|
|
65871
66066
|
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
65872
66067
|
if (messages.length === 0) {
|
|
65873
66068
|
return {
|
|
@@ -65876,10 +66071,11 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
|
|
|
65876
66071
|
count: 0,
|
|
65877
66072
|
partial: partial2,
|
|
65878
66073
|
failedMailboxes,
|
|
65879
|
-
failedMailboxReasons
|
|
66074
|
+
failedMailboxReasons,
|
|
66075
|
+
omittedMessages
|
|
65880
66076
|
};
|
|
65881
66077
|
}
|
|
65882
|
-
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${
|
|
66078
|
+
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalText} ${verb}):
|
|
65883
66079
|
` + rows.join("\n") + `
|
|
65884
66080
|
|
|
65885
66081
|
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;
|
|
@@ -65889,7 +66085,8 @@ Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutatio
|
|
|
65889
66085
|
count: messages.length,
|
|
65890
66086
|
partial: partial2,
|
|
65891
66087
|
failedMailboxes,
|
|
65892
|
-
failedMailboxReasons
|
|
66088
|
+
failedMailboxReasons,
|
|
66089
|
+
omittedMessages
|
|
65893
66090
|
};
|
|
65894
66091
|
},
|
|
65895
66092
|
true
|
|
@@ -66849,31 +67046,21 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66849
67046
|
}
|
|
66850
67047
|
if (uidSet.size <= 1) return null;
|
|
66851
67048
|
const uids = [...uidSet].slice(0, limit);
|
|
66852
|
-
const
|
|
66853
|
-
|
|
66854
|
-
|
|
66855
|
-
|
|
66856
|
-
|
|
66857
|
-
|
|
66858
|
-
|
|
66859
|
-
|
|
66860
|
-
// sanity-checked exactly as a row's dateSent is (#234).
|
|
66861
|
-
{
|
|
66862
|
-
envelope: true,
|
|
66863
|
-
flags: true,
|
|
66864
|
-
bodyStructure: true,
|
|
66865
|
-
internalDate: true,
|
|
66866
|
-
headers: ["date"]
|
|
66867
|
-
},
|
|
66868
|
-
{ uid: true }
|
|
66869
|
-
)) {
|
|
66870
|
-
msgs.push(msg);
|
|
66871
|
-
}
|
|
67049
|
+
const rows = await fetchRows(client, uids);
|
|
67050
|
+
const msgs = rows.messages.slice();
|
|
67051
|
+
const omittedMessages = rows.omitted.map((uid) => ({
|
|
67052
|
+
id: encodeImapId(ref.account, ref.path, uid),
|
|
67053
|
+
mailbox: ref.path,
|
|
67054
|
+
uid,
|
|
67055
|
+
reason: omissionReason(rows)
|
|
67056
|
+
}));
|
|
66872
67057
|
msgs.sort((a, b) => dateMs(a) - dateMs(b));
|
|
66873
67058
|
const subject = seed.envelope?.subject || "(no subject)";
|
|
66874
67059
|
const structured = {
|
|
66875
67060
|
subject,
|
|
66876
67061
|
count: msgs.length,
|
|
67062
|
+
partial: omittedMessages.length > 0,
|
|
67063
|
+
omittedMessages,
|
|
66877
67064
|
messages: msgs.map((m) => ({
|
|
66878
67065
|
id: encodeImapId(ref.account, ref.path, m.uid),
|
|
66879
67066
|
subject: m.envelope?.subject || "(no subject)",
|
|
@@ -66888,7 +67075,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66888
67075
|
}))
|
|
66889
67076
|
};
|
|
66890
67077
|
const text = `Thread "${subject}" \u2014 ${msgs.length} message(s) via IMAP (References-linked, oldest first):
|
|
66891
|
-
` + msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n");
|
|
67078
|
+
` + msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n") + omittedNote(omittedMessages, false);
|
|
66892
67079
|
return { count: msgs.length, text, structured };
|
|
66893
67080
|
} finally {
|
|
66894
67081
|
lock.release();
|
|
@@ -66897,7 +67084,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66897
67084
|
true
|
|
66898
67085
|
);
|
|
66899
67086
|
}
|
|
66900
|
-
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
|
|
67087
|
+
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
|
|
66901
67088
|
var init_imapClient = __esm({
|
|
66902
67089
|
"src/services/imapClient.ts"() {
|
|
66903
67090
|
"use strict";
|
|
@@ -66923,7 +67110,24 @@ var init_imapClient = __esm({
|
|
|
66923
67110
|
accounts: "APPLE_MAIL_MCP_IMAP_ACCOUNTS"
|
|
66924
67111
|
};
|
|
66925
67112
|
defaultConnect = async (cfg) => {
|
|
66926
|
-
|
|
67113
|
+
let lastCommandError;
|
|
67114
|
+
const keepErr = (entry) => {
|
|
67115
|
+
if (entry && typeof entry === "object" && "err" in entry) {
|
|
67116
|
+
lastCommandError = entry.err;
|
|
67117
|
+
}
|
|
67118
|
+
};
|
|
67119
|
+
const noop = () => void 0;
|
|
67120
|
+
const client = new import_imapflow.ImapFlow({
|
|
67121
|
+
...buildImapConnectionOptions(cfg),
|
|
67122
|
+
logger: { trace: noop, debug: noop, info: noop, warn: keepErr, error: keepErr, fatal: keepErr }
|
|
67123
|
+
});
|
|
67124
|
+
Object.assign(client, {
|
|
67125
|
+
takeLastCommandError: () => {
|
|
67126
|
+
const err = lastCommandError;
|
|
67127
|
+
lastCommandError = void 0;
|
|
67128
|
+
return err;
|
|
67129
|
+
}
|
|
67130
|
+
});
|
|
66927
67131
|
client.on("error", () => {
|
|
66928
67132
|
});
|
|
66929
67133
|
try {
|
|
@@ -66951,6 +67155,27 @@ var init_imapClient = __esm({
|
|
|
66951
67155
|
starred: "\\flagged"
|
|
66952
67156
|
};
|
|
66953
67157
|
NOT_DELETED = { deleted: false };
|
|
67158
|
+
LARGE_MAILBOX_MESSAGES = 1e4;
|
|
67159
|
+
FIRST_SEARCH_WINDOW = 5e3;
|
|
67160
|
+
MAX_WINDOW = 5e4;
|
|
67161
|
+
ROW_QUERY = {
|
|
67162
|
+
envelope: true,
|
|
67163
|
+
flags: true,
|
|
67164
|
+
bodyStructure: true,
|
|
67165
|
+
internalDate: true,
|
|
67166
|
+
headers: ["date"]
|
|
67167
|
+
};
|
|
67168
|
+
ROW_QUERY_NO_STRUCTURE = {
|
|
67169
|
+
envelope: true,
|
|
67170
|
+
flags: true,
|
|
67171
|
+
internalDate: true,
|
|
67172
|
+
headers: ["date", "content-type"]
|
|
67173
|
+
};
|
|
67174
|
+
ROW_QUERY_HEADERS_ONLY = {
|
|
67175
|
+
flags: true,
|
|
67176
|
+
internalDate: true,
|
|
67177
|
+
headers: ["date", "from", "subject", "message-id", "in-reply-to", "content-type"]
|
|
67178
|
+
};
|
|
66954
67179
|
poolConnect = defaultConnect;
|
|
66955
67180
|
pools = /* @__PURE__ */ new Map();
|
|
66956
67181
|
connecting = /* @__PURE__ */ new Map();
|
|
@@ -87452,6 +87677,7 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
|
|
|
87452
87677
|
const accountsFailed = [];
|
|
87453
87678
|
const failedMailboxes = [];
|
|
87454
87679
|
const failedMailboxReasons = {};
|
|
87680
|
+
const omittedMessages = [];
|
|
87455
87681
|
for (const config2 of configs) {
|
|
87456
87682
|
const perAccountArgs = { ...args, account: void 0 };
|
|
87457
87683
|
try {
|
|
@@ -87464,12 +87690,25 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
|
|
|
87464
87690
|
for (const [mailbox, reason] of Object.entries(res.failedMailboxReasons)) {
|
|
87465
87691
|
failedMailboxReasons[`${config2.accountLabel} / ${mailbox}`] = reason;
|
|
87466
87692
|
}
|
|
87693
|
+
omittedMessages.push(
|
|
87694
|
+
...(res.omittedMessages ?? []).map((o) => ({
|
|
87695
|
+
...o,
|
|
87696
|
+
mailbox: `${config2.accountLabel} / ${o.mailbox}`
|
|
87697
|
+
}))
|
|
87698
|
+
);
|
|
87467
87699
|
} catch (e) {
|
|
87468
87700
|
accountsFailed.push(config2.accountLabel);
|
|
87469
87701
|
console.error(`IMAP fan-out failed for account "${config2.accountLabel}": ${String(e)}`);
|
|
87470
87702
|
}
|
|
87471
87703
|
}
|
|
87472
|
-
return {
|
|
87704
|
+
return {
|
|
87705
|
+
rows,
|
|
87706
|
+
accountsQueried,
|
|
87707
|
+
accountsFailed,
|
|
87708
|
+
failedMailboxes,
|
|
87709
|
+
failedMailboxReasons,
|
|
87710
|
+
omittedMessages
|
|
87711
|
+
};
|
|
87473
87712
|
}
|
|
87474
87713
|
function configMatchesAccount(config2, account) {
|
|
87475
87714
|
const name = account.name.trim().toLowerCase();
|
|
@@ -88072,7 +88311,10 @@ var LIST_OUTPUT_SCHEMA = {
|
|
|
88072
88311
|
failedMailboxes: external_exports.array(external_exports.string()).optional(),
|
|
88073
88312
|
// Underlying error text per entry in `failedMailboxes`, same keys (#246
|
|
88074
88313
|
// follow-up) — declared so a client can rely on it rather than parse text.
|
|
88075
|
-
failedMailboxReasons: external_exports.record(external_exports.string(), external_exports.string()).optional()
|
|
88314
|
+
failedMailboxReasons: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
88315
|
+
// Messages that belong on the page but could not be read, each with its
|
|
88316
|
+
// imap: id and why (#256 follow-up). Non-empty implies `partial: true`.
|
|
88317
|
+
omittedMessages: external_exports.array(external_exports.object({ id: external_exports.string(), mailbox: external_exports.string(), uid: external_exports.number(), reason: external_exports.string() })).optional()
|
|
88076
88318
|
};
|
|
88077
88319
|
var BATCH_COUNT_OUTPUT_SCHEMA = {
|
|
88078
88320
|
ok: external_exports.boolean().optional(),
|
|
@@ -88151,7 +88393,7 @@ function mergedMessageResponse(fan, apple, limit, verb) {
|
|
|
88151
88393
|
const merged = mergeMessages(fan.rows, apple.rows, limit);
|
|
88152
88394
|
const diagnostics = {
|
|
88153
88395
|
...apple.diagnostics,
|
|
88154
|
-
partial: apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0,
|
|
88396
|
+
partial: apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0 || fan.omittedMessages.length > 0,
|
|
88155
88397
|
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
|
|
88156
88398
|
notSearchedMailboxes: [...apple.diagnostics.notSearchedMailboxes, ...fan.failedMailboxes]
|
|
88157
88399
|
};
|
|
@@ -88163,9 +88405,10 @@ function mergedMessageResponse(fan, apple, limit, verb) {
|
|
|
88163
88405
|
notSearchedMailboxes: diagnostics.notSearchedMailboxes,
|
|
88164
88406
|
timedOutAccounts: diagnostics.timedOutAccounts,
|
|
88165
88407
|
failedMailboxes: fan.failedMailboxes,
|
|
88166
|
-
failedMailboxReasons: fan.failedMailboxReasons
|
|
88408
|
+
failedMailboxReasons: fan.failedMailboxReasons,
|
|
88409
|
+
omittedMessages: fan.omittedMessages
|
|
88167
88410
|
};
|
|
88168
|
-
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
88411
|
+
const coverageBlock = partialCoverageBlock(diagnostics) + omittedNote(fan.omittedMessages, true);
|
|
88169
88412
|
if (merged.length === 0) {
|
|
88170
88413
|
const base = diagnostics.partial ? `No messages found in the portions that were ${verb === "matched" ? "searched" : "listed"}.` : "No messages found";
|
|
88171
88414
|
return successResponse(`${base}${coverageBlock}`, structured);
|
|
@@ -88266,7 +88509,8 @@ registerTool(
|
|
|
88266
88509
|
count: r.count,
|
|
88267
88510
|
partial: r.partial,
|
|
88268
88511
|
failedMailboxes: r.failedMailboxes,
|
|
88269
|
-
failedMailboxReasons: r.failedMailboxReasons
|
|
88512
|
+
failedMailboxReasons: r.failedMailboxReasons,
|
|
88513
|
+
omittedMessages: r.omittedMessages
|
|
88270
88514
|
});
|
|
88271
88515
|
}
|
|
88272
88516
|
const fan = await fanOutImapMessages(imapArgs, "search");
|
|
@@ -88620,7 +88864,9 @@ registerTool(
|
|
|
88620
88864
|
messages: external_exports.array(MESSAGE_ROW_SCHEMA).optional(),
|
|
88621
88865
|
count: external_exports.number().optional(),
|
|
88622
88866
|
partial: external_exports.boolean().optional(),
|
|
88623
|
-
failedMailboxes: external_exports.array(external_exports.string()).optional()
|
|
88867
|
+
failedMailboxes: external_exports.array(external_exports.string()).optional(),
|
|
88868
|
+
failedMailboxReasons: LIST_OUTPUT_SCHEMA.failedMailboxReasons,
|
|
88869
|
+
omittedMessages: LIST_OUTPUT_SCHEMA.omittedMessages
|
|
88624
88870
|
}
|
|
88625
88871
|
},
|
|
88626
88872
|
withErrorHandling(async ({ id, account, mailbox, limit = 50 }) => {
|
|
@@ -88650,7 +88896,8 @@ ${r.text}`, {
|
|
|
88650
88896
|
count: r.count,
|
|
88651
88897
|
partial: r.partial,
|
|
88652
88898
|
failedMailboxes: r.failedMailboxes,
|
|
88653
|
-
failedMailboxReasons: r.failedMailboxReasons
|
|
88899
|
+
failedMailboxReasons: r.failedMailboxReasons,
|
|
88900
|
+
omittedMessages: r.omittedMessages
|
|
88654
88901
|
});
|
|
88655
88902
|
}
|
|
88656
88903
|
const fan = await fanOutImapMessages({ subject: base, mailbox, limit }, "search");
|
|
@@ -88675,20 +88922,21 @@ ${r.text}`, {
|
|
|
88675
88922
|
const orderedRows = mergedNewestFirst.slice().reverse().sort(
|
|
88676
88923
|
(a, b) => (a.dateReceived ? new Date(a.dateReceived).getTime() : 0) - (b.dateReceived ? new Date(b.dateReceived).getTime() : 0)
|
|
88677
88924
|
);
|
|
88678
|
-
const partial2 = apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0;
|
|
88925
|
+
const partial2 = apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0 || fan.omittedMessages.length > 0;
|
|
88679
88926
|
const coverage = partialCoverageBlock({
|
|
88680
88927
|
...apple.diagnostics,
|
|
88681
88928
|
partial: partial2,
|
|
88682
88929
|
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
|
|
88683
88930
|
notSearchedMailboxes: [...apple.diagnostics.notSearchedMailboxes, ...fan.failedMailboxes]
|
|
88684
|
-
});
|
|
88931
|
+
}) + omittedNote(fan.omittedMessages, true);
|
|
88685
88932
|
const structured2 = {
|
|
88686
88933
|
subject: base,
|
|
88687
88934
|
messages: orderedRows,
|
|
88688
88935
|
count: orderedRows.length,
|
|
88689
88936
|
partial: partial2,
|
|
88690
88937
|
failedMailboxes: fan.failedMailboxes,
|
|
88691
|
-
failedMailboxReasons: fan.failedMailboxReasons
|
|
88938
|
+
failedMailboxReasons: fan.failedMailboxReasons,
|
|
88939
|
+
omittedMessages: fan.omittedMessages
|
|
88692
88940
|
};
|
|
88693
88941
|
if (orderedRows.length === 0) {
|
|
88694
88942
|
return successResponse(`No messages found in thread "${base}".${coverage}`, structured2);
|
|
@@ -88753,7 +89001,8 @@ registerTool(
|
|
|
88753
89001
|
count: r.count,
|
|
88754
89002
|
partial: r.partial,
|
|
88755
89003
|
failedMailboxes: r.failedMailboxes,
|
|
88756
|
-
failedMailboxReasons: r.failedMailboxReasons
|
|
89004
|
+
failedMailboxReasons: r.failedMailboxReasons,
|
|
89005
|
+
omittedMessages: r.omittedMessages
|
|
88757
89006
|
});
|
|
88758
89007
|
}
|
|
88759
89008
|
const fan = await fanOutImapMessages({ mailbox, limit, offset, from, unreadOnly }, "list");
|
package/package.json
CHANGED