apple-mail-mcp 2.19.17 → 2.19.19
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 +32 -14
- package/build/cli.js +195 -64
- package/build/index.js +253 -79
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -240,6 +240,7 @@ Search for messages matching criteria. Searches all accounts by default.
|
|
|
240
240
|
| `dateFrom` | string | No | Start date filter (e.g., "January 1, 2026") |
|
|
241
241
|
| `dateTo` | string | No | End date filter (e.g., "March 1, 2026") |
|
|
242
242
|
| `limit` | number | No | Max results, 1–500 (default: 50) |
|
|
243
|
+
| `offset` | number | No | Newest matches to skip, ≥ 0 (for pagination). IMAP accounts only; without a `mailbox`, `offset` + `limit` may not exceed 5,000 |
|
|
243
244
|
|
|
244
245
|
**Returns:** List of matching messages with ID, date, subject, sender, and read state.
|
|
245
246
|
|
|
@@ -280,19 +281,27 @@ human reader; the same information is also returned as structured fields on
|
|
|
280
281
|
`search-messages` and `list-messages`, so a caller can tell _"nothing matched"_
|
|
281
282
|
apart from _"I did not look everywhere"_ without parsing the text:
|
|
282
283
|
|
|
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.
|
|
284
|
+
| Field | Type | Meaning |
|
|
285
|
+
| ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
286
|
+
| `partial` | boolean | Coverage was incomplete — **the result is not a confirmed "no such mail"**. True whenever any field below is non-empty. |
|
|
287
|
+
| `skippedLargeMailboxes` | string[] | Mailboxes never scanned because their message count exceeded `APPLE_MAIL_MAX_SEARCH_MAILBOX`, formatted `"Account / Mailbox (count)"` — e.g. `"iCloud / Archive (90694)"`. |
|
|
288
|
+
| `notSearchedMailboxes` | string[] | Mailboxes that _were_ reached but timed out or errored mid-scan, formatted `"Account / Mailbox"`. Also carries the IMAP path's `failedMailboxes`. |
|
|
289
|
+
| `timedOutAccounts` | string[] | Accounts whose whole-account AppleScript was killed by the per-account time budget — nothing from that account was searched. |
|
|
290
|
+
| `failedMailboxes` | string[] | IMAP-backend mailboxes that errored. These are merged into `notSearchedMailboxes` as well; read that field unless you need to attribute the failure to the IMAP path specifically. |
|
|
291
|
+
| `failedMailboxReasons` | object | The IMAP server's own error text for each entry in `failedMailboxes`, keyed the same way. |
|
|
292
|
+
| `omittedMessages` | object[] | IMAP messages that belong on the page but whose `FETCH` response could not be read even with a reduced item set — `{ id, mailbox, uid, reason }`. The page is short by exactly these; the `id` still works with `get-message` / `get-message-headers`. ([#256](https://github.com/sweetrb/apple-mail-mcp/issues/256)) |
|
|
290
293
|
|
|
291
294
|
Treat a non-empty `skippedLargeMailboxes` as actionable rather than
|
|
292
295
|
informational: re-run scoped to the named mailbox with a `dateFrom`/`dateTo`
|
|
293
296
|
window, or configure the [IMAP backend](#imap-backend--opt-in), which searches
|
|
294
|
-
those mailboxes server-side.
|
|
295
|
-
coverage was complete.
|
|
297
|
+
those mailboxes server-side. The list fields are optional and are omitted (or
|
|
298
|
+
empty) when coverage was complete.
|
|
299
|
+
|
|
300
|
+
A row the IMAP backend could only read with a reduced item set (its full
|
|
301
|
+
`BODYSTRUCTURE`, or its whole `FETCH` response, was unparseable — e.g. a message
|
|
302
|
+
forwarded as an attachment many levels deep) is still returned, with a
|
|
303
|
+
`metadataIncomplete` note saying what was missing; its `hasAttachments` is then
|
|
304
|
+
inferred from the top-level `Content-Type`.
|
|
296
305
|
|
|
297
306
|
---
|
|
298
307
|
|
|
@@ -1095,14 +1104,23 @@ which made a filter-less `list-messages` fail on such a mailbox before 2.19.16.
|
|
|
1095
1104
|
**Very large IMAP mailboxes are read newest-first in bounded windows**
|
|
1096
1105
|
([#256](https://github.com/sweetrb/apple-mail-mcp/issues/256)). Above 10,000
|
|
1097
1106
|
messages, a filter-less `list-messages`/`search-messages` pages by message
|
|
1098
|
-
sequence number
|
|
1099
|
-
|
|
1100
|
-
`\Deleted` ones
|
|
1101
|
-
|
|
1107
|
+
sequence number: page `offset` of `limit` is sequence numbers
|
|
1108
|
+
`N-offset-limit+1 .. N-offset`, so it fetches UIDs and flags for that range
|
|
1109
|
+
alone (plus a small margin; `\Deleted` ones are skipped and the page topped up
|
|
1110
|
+
from below) and full rows for the page. The cost is the page, not the offset:
|
|
1111
|
+
`limit: 1` at offset 790,000 of a 793,614-message mailbox is one ~33-message
|
|
1112
|
+
`FETCH`, not a whole-mailbox `SEARCH` or a walk over every skipped message.
|
|
1113
|
+
Offsets count sequence positions. iCloud keeps messages awaiting expunge out of
|
|
1114
|
+
the sequence space, so there they are exact; on a server that keeps them in it,
|
|
1115
|
+
a page shifts by the number of such messages above it (they are never
|
|
1116
|
+
returned).
|
|
1102
1117
|
A filtered search there runs the same criteria over newest-first sequence
|
|
1103
1118
|
windows (5,000 messages, growing to 50,000) and stops once the page is full;
|
|
1104
1119
|
the reported total then reads `at least N` unless the walk reached the bottom
|
|
1105
|
-
of the mailbox. A
|
|
1120
|
+
of the mailbox. A filtered search's deep `offset` has no such shortcut — which
|
|
1121
|
+
messages match is only known by searching the skipped windows — so its cost
|
|
1122
|
+
still grows with the offset (in 50,000-message `SEARCH` windows); narrow it
|
|
1123
|
+
with `dateTo` instead. A `SEARCH` or `FETCH` that fails names the mailbox, its size and
|
|
1106
1124
|
the server's own reason (or a likely timeout) instead of reporting "no
|
|
1107
1125
|
messages".
|
|
1108
1126
|
|
package/build/cli.js
CHANGED
|
@@ -58049,6 +58049,7 @@ __export(imapClient_exports, {
|
|
|
58049
58049
|
MAX_COMPOSE_SOURCE_BYTES: () => MAX_COMPOSE_SOURCE_BYTES,
|
|
58050
58050
|
MAX_RFC822_FILE_BYTES: () => MAX_RFC822_FILE_BYTES,
|
|
58051
58051
|
MAX_RFC822_INLINE_BYTES: () => MAX_RFC822_INLINE_BYTES,
|
|
58052
|
+
MAX_UNSCOPED_SEARCH_DEPTH: () => MAX_UNSCOPED_SEARCH_DEPTH,
|
|
58052
58053
|
__resetPool: () => __resetPool,
|
|
58053
58054
|
__setPoolConnect: () => __setPoolConnect,
|
|
58054
58055
|
bodyStructureHasAttachments: () => bodyStructureHasAttachments,
|
|
@@ -58092,10 +58093,12 @@ __export(imapClient_exports, {
|
|
|
58092
58093
|
mailFlagColorIndex: () => mailFlagColorIndex,
|
|
58093
58094
|
matchMailbox: () => matchMailbox,
|
|
58094
58095
|
normalizeMessageId: () => normalizeMessageId,
|
|
58096
|
+
omittedNote: () => omittedNote,
|
|
58095
58097
|
resolveImapConfig: () => resolveImapConfig,
|
|
58096
58098
|
resolveImapConfigs: () => resolveImapConfigs,
|
|
58097
58099
|
resolveMailboxPath: () => resolveMailboxPath,
|
|
58098
|
-
shouldUseImap: () => shouldUseImap
|
|
58100
|
+
shouldUseImap: () => shouldUseImap,
|
|
58101
|
+
unscopedSearchOffsetError: () => unscopedSearchOffsetError
|
|
58099
58102
|
});
|
|
58100
58103
|
import { createHash } from "node:crypto";
|
|
58101
58104
|
function encodeImapId(account, path, uid) {
|
|
@@ -58402,7 +58405,12 @@ function structuredRow(m, account, path) {
|
|
|
58402
58405
|
// a caller from "no attachments", so every IMAP-sourced message claimed to
|
|
58403
58406
|
// have none. Falls back to false only when the fetch carried no
|
|
58404
58407
|
// BODYSTRUCTURE at all.
|
|
58405
|
-
|
|
58408
|
+
// A row read without BODYSTRUCTURE (#256 follow-up) judges by its top-level
|
|
58409
|
+
// Content-Type instead, and says so in `metadataIncomplete`.
|
|
58410
|
+
hasAttachments: m.bodyStructure ? bodyStructureHasAttachments(m.bodyStructure) : m.degraded ? contentTypeSuggestsAttachments(m) : false,
|
|
58411
|
+
...m.degraded ? {
|
|
58412
|
+
metadataIncomplete: m.degraded === "bodystructure" ? "BODYSTRUCTURE unreadable; hasAttachments inferred from Content-Type" : "FETCH response unreadable; row rebuilt from raw headers, hasAttachments inferred from Content-Type"
|
|
58413
|
+
} : {},
|
|
58406
58414
|
// Message-ID (when the envelope carries it) is the strongest cross-/intra-
|
|
58407
58415
|
// backend dedup key for the multi-account merge (imapMultiAccount.ts). The
|
|
58408
58416
|
// AppleScript path does not expose it, so cross-backend dedup falls back to
|
|
@@ -58410,6 +58418,13 @@ function structuredRow(m, account, path) {
|
|
|
58410
58418
|
...env.messageId ? { messageId: env.messageId } : {}
|
|
58411
58419
|
};
|
|
58412
58420
|
}
|
|
58421
|
+
function omittedNote(omitted, merged) {
|
|
58422
|
+
if (omitted.length === 0) return "";
|
|
58423
|
+
const list = omitted.map((o) => `UID ${o.uid} in "${o.mailbox}" (${o.id})`).join(", ");
|
|
58424
|
+
return `
|
|
58425
|
+
|
|
58426
|
+
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.`;
|
|
58427
|
+
}
|
|
58413
58428
|
function describeMailboxFailure(error) {
|
|
58414
58429
|
const raw = errText(error);
|
|
58415
58430
|
const oneLine = raw.split("\n")[0].trim();
|
|
@@ -58429,6 +58444,10 @@ function messageIdentity(entry) {
|
|
|
58429
58444
|
const messageId = raw.replace(/^<+|>+$/g, "").trim().toLowerCase();
|
|
58430
58445
|
return messageId ? `mid:${messageId}` : `${entry.path}\0${entry.message.uid}`;
|
|
58431
58446
|
}
|
|
58447
|
+
function unscopedSearchOffsetError(offset, limit) {
|
|
58448
|
+
if (offset <= 0 || offset + limit <= MAX_UNSCOPED_SEARCH_DEPTH) return void 0;
|
|
58449
|
+
return `search-messages without a mailbox merges every mailbox's newest offset+limit matches, so it pages at most ${MAX_UNSCOPED_SEARCH_DEPTH.toLocaleString("en-US")} deep (offset ${offset} + limit ${limit} asked). Name a mailbox (e.g. the account's All Mail or Archive) to page deeper, or narrow the search with dateTo.`;
|
|
58450
|
+
}
|
|
58432
58451
|
function isUnfiltered(criteria) {
|
|
58433
58452
|
const keys = Object.keys(criteria);
|
|
58434
58453
|
return keys.length === 1 && criteria.deleted === false;
|
|
@@ -58451,41 +58470,117 @@ async function searchUids(client, path, criteria, size) {
|
|
|
58451
58470
|
`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)")
|
|
58452
58471
|
);
|
|
58453
58472
|
}
|
|
58473
|
+
function mergeFetched(into, msg) {
|
|
58474
|
+
const prev = into.get(msg.uid);
|
|
58475
|
+
if (!prev) {
|
|
58476
|
+
into.set(msg.uid, msg);
|
|
58477
|
+
return;
|
|
58478
|
+
}
|
|
58479
|
+
const merged = { ...prev };
|
|
58480
|
+
for (const [key, value] of Object.entries(msg)) {
|
|
58481
|
+
if (value !== void 0 && value !== null)
|
|
58482
|
+
merged[key] = value;
|
|
58483
|
+
}
|
|
58484
|
+
into.set(msg.uid, merged);
|
|
58485
|
+
}
|
|
58486
|
+
async function fetchInto(client, uids, query, into) {
|
|
58487
|
+
const wanted = new Set(uids);
|
|
58488
|
+
for await (const msg of client.fetch(uids.join(","), query, { uid: true })) {
|
|
58489
|
+
if (typeof msg?.uid === "number" && wanted.has(msg.uid)) mergeFetched(into, msg);
|
|
58490
|
+
}
|
|
58491
|
+
}
|
|
58492
|
+
function fetchedHeaders(m) {
|
|
58493
|
+
return m.headers ? parseHeaderBlock(decodeHeaderBytes(asBuffer(m.headers))) : void 0;
|
|
58494
|
+
}
|
|
58495
|
+
function envelopeFromHeaders(m) {
|
|
58496
|
+
const h = fetchedHeaders(m);
|
|
58497
|
+
if (!h || h.headers.length === 0) return void 0;
|
|
58498
|
+
const env = {};
|
|
58499
|
+
if (h.subject) env.subject = h.subject;
|
|
58500
|
+
if (h.from) {
|
|
58501
|
+
const angle = h.from.match(/^\s*"?([^"<]*?)"?\s*<([^>]+)>/);
|
|
58502
|
+
env.from = angle ? [{ ...angle[1].trim() ? { name: angle[1].trim() } : {}, address: angle[2].trim() }] : [{ address: h.from.trim() }];
|
|
58503
|
+
}
|
|
58504
|
+
if (h.messageId) env.messageId = `<${h.messageId}>`;
|
|
58505
|
+
if (h.inReplyTo) env.inReplyTo = `<${h.inReplyTo}>`;
|
|
58506
|
+
if (h.dateHeader) env.date = h.dateHeader;
|
|
58507
|
+
return env;
|
|
58508
|
+
}
|
|
58509
|
+
function contentTypeSuggestsAttachments(m) {
|
|
58510
|
+
const type = fetchedHeaders(m)?.headers.find((h) => h.name.toLowerCase() === "content-type");
|
|
58511
|
+
return /^\s*multipart\/mixed\b/i.test(type?.value ?? "");
|
|
58512
|
+
}
|
|
58454
58513
|
async function fetchRows(client, uids) {
|
|
58455
|
-
if (uids.length === 0) return [];
|
|
58514
|
+
if (uids.length === 0) return { messages: [], omitted: [] };
|
|
58456
58515
|
const byUid = /* @__PURE__ */ new Map();
|
|
58457
|
-
|
|
58458
|
-
|
|
58459
|
-
|
|
58460
|
-
|
|
58461
|
-
|
|
58462
|
-
|
|
58463
|
-
|
|
58464
|
-
|
|
58465
|
-
|
|
58466
|
-
|
|
58467
|
-
|
|
58468
|
-
|
|
58469
|
-
|
|
58470
|
-
|
|
58471
|
-
|
|
58472
|
-
|
|
58473
|
-
|
|
58474
|
-
|
|
58475
|
-
|
|
58476
|
-
|
|
58477
|
-
|
|
58516
|
+
const causes = [];
|
|
58517
|
+
const noteCause = () => {
|
|
58518
|
+
const err = client.takeLastCommandError?.();
|
|
58519
|
+
if (err !== void 0) {
|
|
58520
|
+
const text = describeMailboxFailure(err);
|
|
58521
|
+
if (!causes.includes(text)) causes.push(text);
|
|
58522
|
+
}
|
|
58523
|
+
};
|
|
58524
|
+
const missing = () => uids.filter((uid) => !byUid.get(uid)?.envelope);
|
|
58525
|
+
client.takeLastCommandError?.();
|
|
58526
|
+
await fetchInto(client, uids, ROW_QUERY, byUid);
|
|
58527
|
+
noteCause();
|
|
58528
|
+
let gap = missing();
|
|
58529
|
+
if (gap.length > 0) {
|
|
58530
|
+
await fetchInto(client, gap, ROW_QUERY, byUid);
|
|
58531
|
+
noteCause();
|
|
58532
|
+
gap = missing();
|
|
58533
|
+
}
|
|
58534
|
+
if (gap.length > 0) {
|
|
58535
|
+
const reduced = /* @__PURE__ */ new Map();
|
|
58536
|
+
await fetchInto(client, gap, ROW_QUERY_NO_STRUCTURE, reduced);
|
|
58537
|
+
noteCause();
|
|
58538
|
+
for (const msg of reduced.values()) {
|
|
58539
|
+
if (!msg.envelope) continue;
|
|
58540
|
+
mergeFetched(byUid, { ...msg, degraded: "bodystructure" });
|
|
58541
|
+
}
|
|
58542
|
+
gap = missing();
|
|
58543
|
+
}
|
|
58544
|
+
if (gap.length > 0) {
|
|
58545
|
+
for (const uid of gap) {
|
|
58546
|
+
const bare = /* @__PURE__ */ new Map();
|
|
58547
|
+
await fetchInto(client, [uid], ROW_QUERY_HEADERS_ONLY, bare);
|
|
58548
|
+
noteCause();
|
|
58549
|
+
const msg = bare.get(uid);
|
|
58550
|
+
const envelope = msg ? envelopeFromHeaders(msg) : void 0;
|
|
58551
|
+
if (msg && envelope) {
|
|
58552
|
+
mergeFetched(byUid, { ...msg, envelope, degraded: "envelope" });
|
|
58553
|
+
}
|
|
58554
|
+
}
|
|
58555
|
+
gap = missing();
|
|
58556
|
+
}
|
|
58557
|
+
const omitted = new Set(gap);
|
|
58558
|
+
return {
|
|
58559
|
+
messages: uids.filter((uid) => !omitted.has(uid)).map((uid) => byUid.get(uid)).filter((message) => message !== void 0),
|
|
58560
|
+
omitted: gap,
|
|
58561
|
+
...causes.length > 0 ? { cause: causes.join("; ") } : {}
|
|
58562
|
+
};
|
|
58563
|
+
}
|
|
58564
|
+
function pageRows(fetch) {
|
|
58565
|
+
return {
|
|
58566
|
+
messages: fetch.messages,
|
|
58567
|
+
omitted: fetch.omitted,
|
|
58568
|
+
...fetch.omitted.length > 0 ? { omittedReason: omissionReason(fetch) } : {}
|
|
58569
|
+
};
|
|
58570
|
+
}
|
|
58571
|
+
function omissionReason(fetch) {
|
|
58572
|
+
return "the server's FETCH response for this message could not be read, even without BODYSTRUCTURE or ENVELOPE" + (fetch.cause ? ` (${fetch.cause})` : "");
|
|
58478
58573
|
}
|
|
58479
|
-
async function walkWindows(
|
|
58574
|
+
async function walkWindows(top, page, firstWindow, readWindow, openTop = true) {
|
|
58480
58575
|
const wanted = page.skip + page.take;
|
|
58481
58576
|
const uids = [];
|
|
58482
58577
|
let seen = 0;
|
|
58483
58578
|
let matched = 0;
|
|
58484
|
-
let hi =
|
|
58579
|
+
let hi = top;
|
|
58485
58580
|
let width = Math.max(1, Math.min(firstWindow, MAX_WINDOW));
|
|
58486
58581
|
while (hi >= 1 && seen < wanted) {
|
|
58487
58582
|
const lo = Math.max(1, hi - width + 1);
|
|
58488
|
-
const live = (await readWindow(lo, hi ===
|
|
58583
|
+
const live = (await readWindow(lo, hi === top && openTop ? "*" : hi, hi - lo + 1)).slice().sort((a, b) => b - a);
|
|
58489
58584
|
matched += live.length;
|
|
58490
58585
|
for (const uid of live) {
|
|
58491
58586
|
if (seen >= wanted) break;
|
|
@@ -58497,13 +58592,21 @@ async function walkWindows(exists, page, firstWindow, readWindow) {
|
|
|
58497
58592
|
}
|
|
58498
58593
|
return { uids, matched, exhausted: hi < 1 };
|
|
58499
58594
|
}
|
|
58595
|
+
function sequenceTop(client, statusCount) {
|
|
58596
|
+
const mb = client.mailbox;
|
|
58597
|
+
const exists = mb && typeof mb.exists === "number" && mb.exists >= 0 ? mb.exists : void 0;
|
|
58598
|
+
return exists ?? statusCount;
|
|
58599
|
+
}
|
|
58500
58600
|
async function listLargeMailbox(client, exists, page) {
|
|
58601
|
+
const top = sequenceTop(client, exists) - page.skip;
|
|
58602
|
+
if (top < 1 || page.take === 0) {
|
|
58603
|
+
return { messages: [], omitted: [], total: exists, totalExact: true };
|
|
58604
|
+
}
|
|
58501
58605
|
let deletedSeen = 0;
|
|
58502
58606
|
const walk = await walkWindows(
|
|
58503
|
-
|
|
58504
|
-
page,
|
|
58505
|
-
|
|
58506
|
-
page.skip + page.take + 64,
|
|
58607
|
+
top,
|
|
58608
|
+
{ skip: 0, take: page.take },
|
|
58609
|
+
page.take + DELETED_MARGIN,
|
|
58507
58610
|
async (lo, hi) => {
|
|
58508
58611
|
const live = [];
|
|
58509
58612
|
for await (const msg of client.fetch(`${lo}:${hi}`, { uid: true, flags: true })) {
|
|
@@ -58511,10 +58614,13 @@ async function listLargeMailbox(client, exists, page) {
|
|
|
58511
58614
|
else live.push(msg.uid);
|
|
58512
58615
|
}
|
|
58513
58616
|
return live;
|
|
58514
|
-
}
|
|
58617
|
+
},
|
|
58618
|
+
// From the very top the first window is `lo:*`, as before; below it the
|
|
58619
|
+
// range is exact.
|
|
58620
|
+
page.skip === 0
|
|
58515
58621
|
);
|
|
58516
58622
|
return {
|
|
58517
|
-
|
|
58623
|
+
...pageRows(await fetchRows(client, walk.uids)),
|
|
58518
58624
|
total: Math.max(0, exists - deletedSeen),
|
|
58519
58625
|
totalExact: true
|
|
58520
58626
|
};
|
|
@@ -58530,7 +58636,7 @@ async function searchLargeMailbox(client, path, criteria, exists, page) {
|
|
|
58530
58636
|
return found;
|
|
58531
58637
|
});
|
|
58532
58638
|
return {
|
|
58533
|
-
|
|
58639
|
+
...pageRows(await fetchRows(client, walk.uids)),
|
|
58534
58640
|
total: walk.matched,
|
|
58535
58641
|
totalExact: walk.exhausted
|
|
58536
58642
|
};
|
|
@@ -58539,7 +58645,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
|
|
|
58539
58645
|
const lock = await client.getMailboxLock(path);
|
|
58540
58646
|
try {
|
|
58541
58647
|
const exists = await statusMessageCount(client, path);
|
|
58542
|
-
if (exists === 0) return { messages: [], total: 0, totalExact: true };
|
|
58648
|
+
if (exists === 0) return { messages: [], omitted: [], total: 0, totalExact: true };
|
|
58543
58649
|
if (exists !== void 0 && exists > LARGE_MAILBOX_MESSAGES) {
|
|
58544
58650
|
try {
|
|
58545
58651
|
return isUnfiltered(criteria) ? await listLargeMailbox(client, exists, page) : await searchLargeMailbox(client, path, criteria, exists, page);
|
|
@@ -58552,7 +58658,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
|
|
|
58552
58658
|
}
|
|
58553
58659
|
const uids = await searchUids(client, path, criteria, exists);
|
|
58554
58660
|
if (uids.length === 0 || page.take === 0) {
|
|
58555
|
-
return { messages: [], total: uids.length, totalExact: true };
|
|
58661
|
+
return { messages: [], omitted: [], total: uids.length, totalExact: true };
|
|
58556
58662
|
}
|
|
58557
58663
|
const messages = exists ?? (await client.status(path, { messages: true })).messages ?? void 0;
|
|
58558
58664
|
if (typeof messages === "number" && uids.length > messages) {
|
|
@@ -58561,7 +58667,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
|
|
|
58561
58667
|
);
|
|
58562
58668
|
}
|
|
58563
58669
|
const newest = uids.slice().reverse().slice(page.skip, page.skip + page.take);
|
|
58564
|
-
return {
|
|
58670
|
+
return { ...pageRows(await fetchRows(client, newest)), total: uids.length, totalExact: true };
|
|
58565
58671
|
} finally {
|
|
58566
58672
|
lock.release();
|
|
58567
58673
|
}
|
|
@@ -58589,11 +58695,16 @@ async function run(args, listMode, deps) {
|
|
|
58589
58695
|
}
|
|
58590
58696
|
const limit = args.limit ?? 50;
|
|
58591
58697
|
const offset = args.offset ?? 0;
|
|
58698
|
+
if (unscopedSearch) {
|
|
58699
|
+
const tooDeep = unscopedSearchOffsetError(offset, limit);
|
|
58700
|
+
if (tooDeep) throw new Error(tooDeep);
|
|
58701
|
+
}
|
|
58592
58702
|
const criteria = buildCriteria(args, listMode);
|
|
58593
58703
|
const page = unscopedSearch ? { skip: 0, take: offset + limit } : { skip: offset, take: limit };
|
|
58594
58704
|
const fetched = [];
|
|
58595
58705
|
const failedMailboxes = [];
|
|
58596
58706
|
const failedMailboxReasons = {};
|
|
58707
|
+
const omittedMessages = [];
|
|
58597
58708
|
let totalMatched = 0;
|
|
58598
58709
|
let totalExact = true;
|
|
58599
58710
|
for (const path of paths) {
|
|
@@ -58602,6 +58713,14 @@ async function run(args, listMode, deps) {
|
|
|
58602
58713
|
totalMatched += result.total;
|
|
58603
58714
|
totalExact &&= result.totalExact;
|
|
58604
58715
|
fetched.push(...result.messages.map((message) => ({ message, path })));
|
|
58716
|
+
for (const uid of result.omitted) {
|
|
58717
|
+
omittedMessages.push({
|
|
58718
|
+
id: encodeImapId(cfg.accountLabel, path, uid),
|
|
58719
|
+
mailbox: path,
|
|
58720
|
+
uid,
|
|
58721
|
+
reason: result.omittedReason ?? "the server's FETCH response could not be read"
|
|
58722
|
+
});
|
|
58723
|
+
}
|
|
58605
58724
|
} catch (error) {
|
|
58606
58725
|
failedMailboxes.push(path);
|
|
58607
58726
|
failedMailboxReasons[path] = describeMailboxFailure(error);
|
|
@@ -58630,10 +58749,10 @@ async function run(args, listMode, deps) {
|
|
|
58630
58749
|
const messages = ordered.map(
|
|
58631
58750
|
({ message, path }) => structuredRow(message, cfg.accountLabel, path)
|
|
58632
58751
|
);
|
|
58633
|
-
const partial = failedMailboxes.length > 0;
|
|
58634
|
-
const failureNote =
|
|
58752
|
+
const partial = failedMailboxes.length > 0 || omittedMessages.length > 0;
|
|
58753
|
+
const failureNote = (failedMailboxes.length > 0 ? `
|
|
58635
58754
|
|
|
58636
|
-
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
|
|
58755
|
+
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch);
|
|
58637
58756
|
const verb = listMode ? "listed" : "matched";
|
|
58638
58757
|
const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
|
|
58639
58758
|
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
@@ -58644,7 +58763,8 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
|
|
|
58644
58763
|
count: 0,
|
|
58645
58764
|
partial,
|
|
58646
58765
|
failedMailboxes,
|
|
58647
|
-
failedMailboxReasons
|
|
58766
|
+
failedMailboxReasons,
|
|
58767
|
+
omittedMessages
|
|
58648
58768
|
};
|
|
58649
58769
|
}
|
|
58650
58770
|
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalText} ${verb}):
|
|
@@ -58657,7 +58777,8 @@ Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutatio
|
|
|
58657
58777
|
count: messages.length,
|
|
58658
58778
|
partial,
|
|
58659
58779
|
failedMailboxes,
|
|
58660
|
-
failedMailboxReasons
|
|
58780
|
+
failedMailboxReasons,
|
|
58781
|
+
omittedMessages
|
|
58661
58782
|
};
|
|
58662
58783
|
},
|
|
58663
58784
|
true
|
|
@@ -59617,31 +59738,21 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59617
59738
|
}
|
|
59618
59739
|
if (uidSet.size <= 1) return null;
|
|
59619
59740
|
const uids = [...uidSet].slice(0, limit);
|
|
59620
|
-
const
|
|
59621
|
-
|
|
59622
|
-
|
|
59623
|
-
|
|
59624
|
-
|
|
59625
|
-
|
|
59626
|
-
|
|
59627
|
-
|
|
59628
|
-
// sanity-checked exactly as a row's dateSent is (#234).
|
|
59629
|
-
{
|
|
59630
|
-
envelope: true,
|
|
59631
|
-
flags: true,
|
|
59632
|
-
bodyStructure: true,
|
|
59633
|
-
internalDate: true,
|
|
59634
|
-
headers: ["date"]
|
|
59635
|
-
},
|
|
59636
|
-
{ uid: true }
|
|
59637
|
-
)) {
|
|
59638
|
-
msgs.push(msg);
|
|
59639
|
-
}
|
|
59741
|
+
const rows = await fetchRows(client, uids);
|
|
59742
|
+
const msgs = rows.messages.slice();
|
|
59743
|
+
const omittedMessages = rows.omitted.map((uid) => ({
|
|
59744
|
+
id: encodeImapId(ref.account, ref.path, uid),
|
|
59745
|
+
mailbox: ref.path,
|
|
59746
|
+
uid,
|
|
59747
|
+
reason: omissionReason(rows)
|
|
59748
|
+
}));
|
|
59640
59749
|
msgs.sort((a, b) => dateMs(a) - dateMs(b));
|
|
59641
59750
|
const subject = seed.envelope?.subject || "(no subject)";
|
|
59642
59751
|
const structured = {
|
|
59643
59752
|
subject,
|
|
59644
59753
|
count: msgs.length,
|
|
59754
|
+
partial: omittedMessages.length > 0,
|
|
59755
|
+
omittedMessages,
|
|
59645
59756
|
messages: msgs.map((m) => ({
|
|
59646
59757
|
id: encodeImapId(ref.account, ref.path, m.uid),
|
|
59647
59758
|
subject: m.envelope?.subject || "(no subject)",
|
|
@@ -59656,7 +59767,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59656
59767
|
}))
|
|
59657
59768
|
};
|
|
59658
59769
|
const text = `Thread "${subject}" \u2014 ${msgs.length} message(s) via IMAP (References-linked, oldest first):
|
|
59659
|
-
` + msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n");
|
|
59770
|
+
` + msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n") + omittedNote(omittedMessages, false);
|
|
59660
59771
|
return { count: msgs.length, text, structured };
|
|
59661
59772
|
} finally {
|
|
59662
59773
|
lock.release();
|
|
@@ -59665,7 +59776,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59665
59776
|
true
|
|
59666
59777
|
);
|
|
59667
59778
|
}
|
|
59668
|
-
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, 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;
|
|
59779
|
+
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, MAX_UNSCOPED_SEARCH_DEPTH, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, DELETED_MARGIN, 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;
|
|
59669
59780
|
var init_imapClient = __esm({
|
|
59670
59781
|
"src/services/imapClient.ts"() {
|
|
59671
59782
|
"use strict";
|
|
@@ -59739,6 +59850,26 @@ var init_imapClient = __esm({
|
|
|
59739
59850
|
LARGE_MAILBOX_MESSAGES = 1e4;
|
|
59740
59851
|
FIRST_SEARCH_WINDOW = 5e3;
|
|
59741
59852
|
MAX_WINDOW = 5e4;
|
|
59853
|
+
MAX_UNSCOPED_SEARCH_DEPTH = 5e3;
|
|
59854
|
+
ROW_QUERY = {
|
|
59855
|
+
envelope: true,
|
|
59856
|
+
flags: true,
|
|
59857
|
+
bodyStructure: true,
|
|
59858
|
+
internalDate: true,
|
|
59859
|
+
headers: ["date"]
|
|
59860
|
+
};
|
|
59861
|
+
ROW_QUERY_NO_STRUCTURE = {
|
|
59862
|
+
envelope: true,
|
|
59863
|
+
flags: true,
|
|
59864
|
+
internalDate: true,
|
|
59865
|
+
headers: ["date", "content-type"]
|
|
59866
|
+
};
|
|
59867
|
+
ROW_QUERY_HEADERS_ONLY = {
|
|
59868
|
+
flags: true,
|
|
59869
|
+
internalDate: true,
|
|
59870
|
+
headers: ["date", "from", "subject", "message-id", "in-reply-to", "content-type"]
|
|
59871
|
+
};
|
|
59872
|
+
DELETED_MARGIN = 32;
|
|
59742
59873
|
poolConnect = defaultConnect;
|
|
59743
59874
|
pools = /* @__PURE__ */ new Map();
|
|
59744
59875
|
connecting = /* @__PURE__ */ new Map();
|
package/build/index.js
CHANGED
|
@@ -65378,6 +65378,7 @@ __export(imapClient_exports, {
|
|
|
65378
65378
|
MAX_COMPOSE_SOURCE_BYTES: () => MAX_COMPOSE_SOURCE_BYTES,
|
|
65379
65379
|
MAX_RFC822_FILE_BYTES: () => MAX_RFC822_FILE_BYTES,
|
|
65380
65380
|
MAX_RFC822_INLINE_BYTES: () => MAX_RFC822_INLINE_BYTES,
|
|
65381
|
+
MAX_UNSCOPED_SEARCH_DEPTH: () => MAX_UNSCOPED_SEARCH_DEPTH,
|
|
65381
65382
|
__resetPool: () => __resetPool,
|
|
65382
65383
|
__setPoolConnect: () => __setPoolConnect,
|
|
65383
65384
|
bodyStructureHasAttachments: () => bodyStructureHasAttachments,
|
|
@@ -65421,10 +65422,12 @@ __export(imapClient_exports, {
|
|
|
65421
65422
|
mailFlagColorIndex: () => mailFlagColorIndex,
|
|
65422
65423
|
matchMailbox: () => matchMailbox,
|
|
65423
65424
|
normalizeMessageId: () => normalizeMessageId,
|
|
65425
|
+
omittedNote: () => omittedNote,
|
|
65424
65426
|
resolveImapConfig: () => resolveImapConfig,
|
|
65425
65427
|
resolveImapConfigs: () => resolveImapConfigs,
|
|
65426
65428
|
resolveMailboxPath: () => resolveMailboxPath,
|
|
65427
|
-
shouldUseImap: () => shouldUseImap
|
|
65429
|
+
shouldUseImap: () => shouldUseImap,
|
|
65430
|
+
unscopedSearchOffsetError: () => unscopedSearchOffsetError
|
|
65428
65431
|
});
|
|
65429
65432
|
import { createHash } from "node:crypto";
|
|
65430
65433
|
function encodeImapId(account, path, uid) {
|
|
@@ -65731,7 +65734,12 @@ function structuredRow(m, account, path) {
|
|
|
65731
65734
|
// a caller from "no attachments", so every IMAP-sourced message claimed to
|
|
65732
65735
|
// have none. Falls back to false only when the fetch carried no
|
|
65733
65736
|
// BODYSTRUCTURE at all.
|
|
65734
|
-
|
|
65737
|
+
// A row read without BODYSTRUCTURE (#256 follow-up) judges by its top-level
|
|
65738
|
+
// Content-Type instead, and says so in `metadataIncomplete`.
|
|
65739
|
+
hasAttachments: m.bodyStructure ? bodyStructureHasAttachments(m.bodyStructure) : m.degraded ? contentTypeSuggestsAttachments(m) : false,
|
|
65740
|
+
...m.degraded ? {
|
|
65741
|
+
metadataIncomplete: m.degraded === "bodystructure" ? "BODYSTRUCTURE unreadable; hasAttachments inferred from Content-Type" : "FETCH response unreadable; row rebuilt from raw headers, hasAttachments inferred from Content-Type"
|
|
65742
|
+
} : {},
|
|
65735
65743
|
// Message-ID (when the envelope carries it) is the strongest cross-/intra-
|
|
65736
65744
|
// backend dedup key for the multi-account merge (imapMultiAccount.ts). The
|
|
65737
65745
|
// AppleScript path does not expose it, so cross-backend dedup falls back to
|
|
@@ -65739,6 +65747,13 @@ function structuredRow(m, account, path) {
|
|
|
65739
65747
|
...env.messageId ? { messageId: env.messageId } : {}
|
|
65740
65748
|
};
|
|
65741
65749
|
}
|
|
65750
|
+
function omittedNote(omitted, merged) {
|
|
65751
|
+
if (omitted.length === 0) return "";
|
|
65752
|
+
const list = omitted.map((o) => `UID ${o.uid} in "${o.mailbox}" (${o.id})`).join(", ");
|
|
65753
|
+
return `
|
|
65754
|
+
|
|
65755
|
+
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.`;
|
|
65756
|
+
}
|
|
65742
65757
|
function describeMailboxFailure(error2) {
|
|
65743
65758
|
const raw = errText(error2);
|
|
65744
65759
|
const oneLine = raw.split("\n")[0].trim();
|
|
@@ -65758,6 +65773,10 @@ function messageIdentity(entry) {
|
|
|
65758
65773
|
const messageId = raw.replace(/^<+|>+$/g, "").trim().toLowerCase();
|
|
65759
65774
|
return messageId ? `mid:${messageId}` : `${entry.path}\0${entry.message.uid}`;
|
|
65760
65775
|
}
|
|
65776
|
+
function unscopedSearchOffsetError(offset, limit) {
|
|
65777
|
+
if (offset <= 0 || offset + limit <= MAX_UNSCOPED_SEARCH_DEPTH) return void 0;
|
|
65778
|
+
return `search-messages without a mailbox merges every mailbox's newest offset+limit matches, so it pages at most ${MAX_UNSCOPED_SEARCH_DEPTH.toLocaleString("en-US")} deep (offset ${offset} + limit ${limit} asked). Name a mailbox (e.g. the account's All Mail or Archive) to page deeper, or narrow the search with dateTo.`;
|
|
65779
|
+
}
|
|
65761
65780
|
function isUnfiltered(criteria) {
|
|
65762
65781
|
const keys = Object.keys(criteria);
|
|
65763
65782
|
return keys.length === 1 && criteria.deleted === false;
|
|
@@ -65780,41 +65799,117 @@ async function searchUids(client, path, criteria, size) {
|
|
|
65780
65799
|
`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)")
|
|
65781
65800
|
);
|
|
65782
65801
|
}
|
|
65802
|
+
function mergeFetched(into, msg) {
|
|
65803
|
+
const prev = into.get(msg.uid);
|
|
65804
|
+
if (!prev) {
|
|
65805
|
+
into.set(msg.uid, msg);
|
|
65806
|
+
return;
|
|
65807
|
+
}
|
|
65808
|
+
const merged = { ...prev };
|
|
65809
|
+
for (const [key, value] of Object.entries(msg)) {
|
|
65810
|
+
if (value !== void 0 && value !== null)
|
|
65811
|
+
merged[key] = value;
|
|
65812
|
+
}
|
|
65813
|
+
into.set(msg.uid, merged);
|
|
65814
|
+
}
|
|
65815
|
+
async function fetchInto(client, uids, query, into) {
|
|
65816
|
+
const wanted = new Set(uids);
|
|
65817
|
+
for await (const msg of client.fetch(uids.join(","), query, { uid: true })) {
|
|
65818
|
+
if (typeof msg?.uid === "number" && wanted.has(msg.uid)) mergeFetched(into, msg);
|
|
65819
|
+
}
|
|
65820
|
+
}
|
|
65821
|
+
function fetchedHeaders(m) {
|
|
65822
|
+
return m.headers ? parseHeaderBlock(decodeHeaderBytes(asBuffer(m.headers))) : void 0;
|
|
65823
|
+
}
|
|
65824
|
+
function envelopeFromHeaders(m) {
|
|
65825
|
+
const h = fetchedHeaders(m);
|
|
65826
|
+
if (!h || h.headers.length === 0) return void 0;
|
|
65827
|
+
const env = {};
|
|
65828
|
+
if (h.subject) env.subject = h.subject;
|
|
65829
|
+
if (h.from) {
|
|
65830
|
+
const angle = h.from.match(/^\s*"?([^"<]*?)"?\s*<([^>]+)>/);
|
|
65831
|
+
env.from = angle ? [{ ...angle[1].trim() ? { name: angle[1].trim() } : {}, address: angle[2].trim() }] : [{ address: h.from.trim() }];
|
|
65832
|
+
}
|
|
65833
|
+
if (h.messageId) env.messageId = `<${h.messageId}>`;
|
|
65834
|
+
if (h.inReplyTo) env.inReplyTo = `<${h.inReplyTo}>`;
|
|
65835
|
+
if (h.dateHeader) env.date = h.dateHeader;
|
|
65836
|
+
return env;
|
|
65837
|
+
}
|
|
65838
|
+
function contentTypeSuggestsAttachments(m) {
|
|
65839
|
+
const type = fetchedHeaders(m)?.headers.find((h) => h.name.toLowerCase() === "content-type");
|
|
65840
|
+
return /^\s*multipart\/mixed\b/i.test(type?.value ?? "");
|
|
65841
|
+
}
|
|
65783
65842
|
async function fetchRows(client, uids) {
|
|
65784
|
-
if (uids.length === 0) return [];
|
|
65843
|
+
if (uids.length === 0) return { messages: [], omitted: [] };
|
|
65785
65844
|
const byUid = /* @__PURE__ */ new Map();
|
|
65786
|
-
|
|
65787
|
-
|
|
65788
|
-
|
|
65789
|
-
|
|
65790
|
-
|
|
65791
|
-
|
|
65792
|
-
|
|
65793
|
-
|
|
65794
|
-
|
|
65795
|
-
|
|
65796
|
-
|
|
65797
|
-
|
|
65798
|
-
|
|
65799
|
-
|
|
65800
|
-
|
|
65801
|
-
|
|
65802
|
-
|
|
65803
|
-
|
|
65804
|
-
|
|
65805
|
-
|
|
65806
|
-
|
|
65807
|
-
|
|
65808
|
-
|
|
65845
|
+
const causes = [];
|
|
65846
|
+
const noteCause = () => {
|
|
65847
|
+
const err = client.takeLastCommandError?.();
|
|
65848
|
+
if (err !== void 0) {
|
|
65849
|
+
const text = describeMailboxFailure(err);
|
|
65850
|
+
if (!causes.includes(text)) causes.push(text);
|
|
65851
|
+
}
|
|
65852
|
+
};
|
|
65853
|
+
const missing = () => uids.filter((uid) => !byUid.get(uid)?.envelope);
|
|
65854
|
+
client.takeLastCommandError?.();
|
|
65855
|
+
await fetchInto(client, uids, ROW_QUERY, byUid);
|
|
65856
|
+
noteCause();
|
|
65857
|
+
let gap = missing();
|
|
65858
|
+
if (gap.length > 0) {
|
|
65859
|
+
await fetchInto(client, gap, ROW_QUERY, byUid);
|
|
65860
|
+
noteCause();
|
|
65861
|
+
gap = missing();
|
|
65862
|
+
}
|
|
65863
|
+
if (gap.length > 0) {
|
|
65864
|
+
const reduced = /* @__PURE__ */ new Map();
|
|
65865
|
+
await fetchInto(client, gap, ROW_QUERY_NO_STRUCTURE, reduced);
|
|
65866
|
+
noteCause();
|
|
65867
|
+
for (const msg of reduced.values()) {
|
|
65868
|
+
if (!msg.envelope) continue;
|
|
65869
|
+
mergeFetched(byUid, { ...msg, degraded: "bodystructure" });
|
|
65870
|
+
}
|
|
65871
|
+
gap = missing();
|
|
65872
|
+
}
|
|
65873
|
+
if (gap.length > 0) {
|
|
65874
|
+
for (const uid of gap) {
|
|
65875
|
+
const bare = /* @__PURE__ */ new Map();
|
|
65876
|
+
await fetchInto(client, [uid], ROW_QUERY_HEADERS_ONLY, bare);
|
|
65877
|
+
noteCause();
|
|
65878
|
+
const msg = bare.get(uid);
|
|
65879
|
+
const envelope = msg ? envelopeFromHeaders(msg) : void 0;
|
|
65880
|
+
if (msg && envelope) {
|
|
65881
|
+
mergeFetched(byUid, { ...msg, envelope, degraded: "envelope" });
|
|
65882
|
+
}
|
|
65883
|
+
}
|
|
65884
|
+
gap = missing();
|
|
65885
|
+
}
|
|
65886
|
+
const omitted = new Set(gap);
|
|
65887
|
+
return {
|
|
65888
|
+
messages: uids.filter((uid) => !omitted.has(uid)).map((uid) => byUid.get(uid)).filter((message) => message !== void 0),
|
|
65889
|
+
omitted: gap,
|
|
65890
|
+
...causes.length > 0 ? { cause: causes.join("; ") } : {}
|
|
65891
|
+
};
|
|
65892
|
+
}
|
|
65893
|
+
function pageRows(fetch) {
|
|
65894
|
+
return {
|
|
65895
|
+
messages: fetch.messages,
|
|
65896
|
+
omitted: fetch.omitted,
|
|
65897
|
+
...fetch.omitted.length > 0 ? { omittedReason: omissionReason(fetch) } : {}
|
|
65898
|
+
};
|
|
65899
|
+
}
|
|
65900
|
+
function omissionReason(fetch) {
|
|
65901
|
+
return "the server's FETCH response for this message could not be read, even without BODYSTRUCTURE or ENVELOPE" + (fetch.cause ? ` (${fetch.cause})` : "");
|
|
65902
|
+
}
|
|
65903
|
+
async function walkWindows(top, page, firstWindow, readWindow, openTop = true) {
|
|
65809
65904
|
const wanted = page.skip + page.take;
|
|
65810
65905
|
const uids = [];
|
|
65811
65906
|
let seen = 0;
|
|
65812
65907
|
let matched = 0;
|
|
65813
|
-
let hi =
|
|
65908
|
+
let hi = top;
|
|
65814
65909
|
let width = Math.max(1, Math.min(firstWindow, MAX_WINDOW));
|
|
65815
65910
|
while (hi >= 1 && seen < wanted) {
|
|
65816
65911
|
const lo = Math.max(1, hi - width + 1);
|
|
65817
|
-
const live = (await readWindow(lo, hi ===
|
|
65912
|
+
const live = (await readWindow(lo, hi === top && openTop ? "*" : hi, hi - lo + 1)).slice().sort((a, b) => b - a);
|
|
65818
65913
|
matched += live.length;
|
|
65819
65914
|
for (const uid of live) {
|
|
65820
65915
|
if (seen >= wanted) break;
|
|
@@ -65826,13 +65921,21 @@ async function walkWindows(exists, page, firstWindow, readWindow) {
|
|
|
65826
65921
|
}
|
|
65827
65922
|
return { uids, matched, exhausted: hi < 1 };
|
|
65828
65923
|
}
|
|
65924
|
+
function sequenceTop(client, statusCount) {
|
|
65925
|
+
const mb = client.mailbox;
|
|
65926
|
+
const exists = mb && typeof mb.exists === "number" && mb.exists >= 0 ? mb.exists : void 0;
|
|
65927
|
+
return exists ?? statusCount;
|
|
65928
|
+
}
|
|
65829
65929
|
async function listLargeMailbox(client, exists, page) {
|
|
65930
|
+
const top = sequenceTop(client, exists) - page.skip;
|
|
65931
|
+
if (top < 1 || page.take === 0) {
|
|
65932
|
+
return { messages: [], omitted: [], total: exists, totalExact: true };
|
|
65933
|
+
}
|
|
65830
65934
|
let deletedSeen = 0;
|
|
65831
65935
|
const walk = await walkWindows(
|
|
65832
|
-
|
|
65833
|
-
page,
|
|
65834
|
-
|
|
65835
|
-
page.skip + page.take + 64,
|
|
65936
|
+
top,
|
|
65937
|
+
{ skip: 0, take: page.take },
|
|
65938
|
+
page.take + DELETED_MARGIN,
|
|
65836
65939
|
async (lo, hi) => {
|
|
65837
65940
|
const live = [];
|
|
65838
65941
|
for await (const msg of client.fetch(`${lo}:${hi}`, { uid: true, flags: true })) {
|
|
@@ -65840,10 +65943,13 @@ async function listLargeMailbox(client, exists, page) {
|
|
|
65840
65943
|
else live.push(msg.uid);
|
|
65841
65944
|
}
|
|
65842
65945
|
return live;
|
|
65843
|
-
}
|
|
65946
|
+
},
|
|
65947
|
+
// From the very top the first window is `lo:*`, as before; below it the
|
|
65948
|
+
// range is exact.
|
|
65949
|
+
page.skip === 0
|
|
65844
65950
|
);
|
|
65845
65951
|
return {
|
|
65846
|
-
|
|
65952
|
+
...pageRows(await fetchRows(client, walk.uids)),
|
|
65847
65953
|
total: Math.max(0, exists - deletedSeen),
|
|
65848
65954
|
totalExact: true
|
|
65849
65955
|
};
|
|
@@ -65859,7 +65965,7 @@ async function searchLargeMailbox(client, path, criteria, exists, page) {
|
|
|
65859
65965
|
return found;
|
|
65860
65966
|
});
|
|
65861
65967
|
return {
|
|
65862
|
-
|
|
65968
|
+
...pageRows(await fetchRows(client, walk.uids)),
|
|
65863
65969
|
total: walk.matched,
|
|
65864
65970
|
totalExact: walk.exhausted
|
|
65865
65971
|
};
|
|
@@ -65868,7 +65974,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
|
|
|
65868
65974
|
const lock = await client.getMailboxLock(path);
|
|
65869
65975
|
try {
|
|
65870
65976
|
const exists = await statusMessageCount(client, path);
|
|
65871
|
-
if (exists === 0) return { messages: [], total: 0, totalExact: true };
|
|
65977
|
+
if (exists === 0) return { messages: [], omitted: [], total: 0, totalExact: true };
|
|
65872
65978
|
if (exists !== void 0 && exists > LARGE_MAILBOX_MESSAGES) {
|
|
65873
65979
|
try {
|
|
65874
65980
|
return isUnfiltered(criteria) ? await listLargeMailbox(client, exists, page) : await searchLargeMailbox(client, path, criteria, exists, page);
|
|
@@ -65881,7 +65987,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
|
|
|
65881
65987
|
}
|
|
65882
65988
|
const uids = await searchUids(client, path, criteria, exists);
|
|
65883
65989
|
if (uids.length === 0 || page.take === 0) {
|
|
65884
|
-
return { messages: [], total: uids.length, totalExact: true };
|
|
65990
|
+
return { messages: [], omitted: [], total: uids.length, totalExact: true };
|
|
65885
65991
|
}
|
|
65886
65992
|
const messages = exists ?? (await client.status(path, { messages: true })).messages ?? void 0;
|
|
65887
65993
|
if (typeof messages === "number" && uids.length > messages) {
|
|
@@ -65890,7 +65996,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
|
|
|
65890
65996
|
);
|
|
65891
65997
|
}
|
|
65892
65998
|
const newest = uids.slice().reverse().slice(page.skip, page.skip + page.take);
|
|
65893
|
-
return {
|
|
65999
|
+
return { ...pageRows(await fetchRows(client, newest)), total: uids.length, totalExact: true };
|
|
65894
66000
|
} finally {
|
|
65895
66001
|
lock.release();
|
|
65896
66002
|
}
|
|
@@ -65918,11 +66024,16 @@ async function run(args, listMode, deps) {
|
|
|
65918
66024
|
}
|
|
65919
66025
|
const limit = args.limit ?? 50;
|
|
65920
66026
|
const offset = args.offset ?? 0;
|
|
66027
|
+
if (unscopedSearch) {
|
|
66028
|
+
const tooDeep = unscopedSearchOffsetError(offset, limit);
|
|
66029
|
+
if (tooDeep) throw new Error(tooDeep);
|
|
66030
|
+
}
|
|
65921
66031
|
const criteria = buildCriteria(args, listMode);
|
|
65922
66032
|
const page = unscopedSearch ? { skip: 0, take: offset + limit } : { skip: offset, take: limit };
|
|
65923
66033
|
const fetched = [];
|
|
65924
66034
|
const failedMailboxes = [];
|
|
65925
66035
|
const failedMailboxReasons = {};
|
|
66036
|
+
const omittedMessages = [];
|
|
65926
66037
|
let totalMatched = 0;
|
|
65927
66038
|
let totalExact = true;
|
|
65928
66039
|
for (const path of paths) {
|
|
@@ -65931,6 +66042,14 @@ async function run(args, listMode, deps) {
|
|
|
65931
66042
|
totalMatched += result.total;
|
|
65932
66043
|
totalExact &&= result.totalExact;
|
|
65933
66044
|
fetched.push(...result.messages.map((message) => ({ message, path })));
|
|
66045
|
+
for (const uid of result.omitted) {
|
|
66046
|
+
omittedMessages.push({
|
|
66047
|
+
id: encodeImapId(cfg.accountLabel, path, uid),
|
|
66048
|
+
mailbox: path,
|
|
66049
|
+
uid,
|
|
66050
|
+
reason: result.omittedReason ?? "the server's FETCH response could not be read"
|
|
66051
|
+
});
|
|
66052
|
+
}
|
|
65934
66053
|
} catch (error2) {
|
|
65935
66054
|
failedMailboxes.push(path);
|
|
65936
66055
|
failedMailboxReasons[path] = describeMailboxFailure(error2);
|
|
@@ -65959,10 +66078,10 @@ async function run(args, listMode, deps) {
|
|
|
65959
66078
|
const messages = ordered.map(
|
|
65960
66079
|
({ message, path }) => structuredRow(message, cfg.accountLabel, path)
|
|
65961
66080
|
);
|
|
65962
|
-
const partial2 = failedMailboxes.length > 0;
|
|
65963
|
-
const failureNote =
|
|
66081
|
+
const partial2 = failedMailboxes.length > 0 || omittedMessages.length > 0;
|
|
66082
|
+
const failureNote = (failedMailboxes.length > 0 ? `
|
|
65964
66083
|
|
|
65965
|
-
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
|
|
66084
|
+
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch);
|
|
65966
66085
|
const verb = listMode ? "listed" : "matched";
|
|
65967
66086
|
const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
|
|
65968
66087
|
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
@@ -65973,7 +66092,8 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
|
|
|
65973
66092
|
count: 0,
|
|
65974
66093
|
partial: partial2,
|
|
65975
66094
|
failedMailboxes,
|
|
65976
|
-
failedMailboxReasons
|
|
66095
|
+
failedMailboxReasons,
|
|
66096
|
+
omittedMessages
|
|
65977
66097
|
};
|
|
65978
66098
|
}
|
|
65979
66099
|
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalText} ${verb}):
|
|
@@ -65986,7 +66106,8 @@ Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutatio
|
|
|
65986
66106
|
count: messages.length,
|
|
65987
66107
|
partial: partial2,
|
|
65988
66108
|
failedMailboxes,
|
|
65989
|
-
failedMailboxReasons
|
|
66109
|
+
failedMailboxReasons,
|
|
66110
|
+
omittedMessages
|
|
65990
66111
|
};
|
|
65991
66112
|
},
|
|
65992
66113
|
true
|
|
@@ -66946,31 +67067,21 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66946
67067
|
}
|
|
66947
67068
|
if (uidSet.size <= 1) return null;
|
|
66948
67069
|
const uids = [...uidSet].slice(0, limit);
|
|
66949
|
-
const
|
|
66950
|
-
|
|
66951
|
-
|
|
66952
|
-
|
|
66953
|
-
|
|
66954
|
-
|
|
66955
|
-
|
|
66956
|
-
|
|
66957
|
-
// sanity-checked exactly as a row's dateSent is (#234).
|
|
66958
|
-
{
|
|
66959
|
-
envelope: true,
|
|
66960
|
-
flags: true,
|
|
66961
|
-
bodyStructure: true,
|
|
66962
|
-
internalDate: true,
|
|
66963
|
-
headers: ["date"]
|
|
66964
|
-
},
|
|
66965
|
-
{ uid: true }
|
|
66966
|
-
)) {
|
|
66967
|
-
msgs.push(msg);
|
|
66968
|
-
}
|
|
67070
|
+
const rows = await fetchRows(client, uids);
|
|
67071
|
+
const msgs = rows.messages.slice();
|
|
67072
|
+
const omittedMessages = rows.omitted.map((uid) => ({
|
|
67073
|
+
id: encodeImapId(ref.account, ref.path, uid),
|
|
67074
|
+
mailbox: ref.path,
|
|
67075
|
+
uid,
|
|
67076
|
+
reason: omissionReason(rows)
|
|
67077
|
+
}));
|
|
66969
67078
|
msgs.sort((a, b) => dateMs(a) - dateMs(b));
|
|
66970
67079
|
const subject = seed.envelope?.subject || "(no subject)";
|
|
66971
67080
|
const structured = {
|
|
66972
67081
|
subject,
|
|
66973
67082
|
count: msgs.length,
|
|
67083
|
+
partial: omittedMessages.length > 0,
|
|
67084
|
+
omittedMessages,
|
|
66974
67085
|
messages: msgs.map((m) => ({
|
|
66975
67086
|
id: encodeImapId(ref.account, ref.path, m.uid),
|
|
66976
67087
|
subject: m.envelope?.subject || "(no subject)",
|
|
@@ -66985,7 +67096,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66985
67096
|
}))
|
|
66986
67097
|
};
|
|
66987
67098
|
const text = `Thread "${subject}" \u2014 ${msgs.length} message(s) via IMAP (References-linked, oldest first):
|
|
66988
|
-
` + msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n");
|
|
67099
|
+
` + msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n") + omittedNote(omittedMessages, false);
|
|
66989
67100
|
return { count: msgs.length, text, structured };
|
|
66990
67101
|
} finally {
|
|
66991
67102
|
lock.release();
|
|
@@ -66994,7 +67105,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66994
67105
|
true
|
|
66995
67106
|
);
|
|
66996
67107
|
}
|
|
66997
|
-
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, 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;
|
|
67108
|
+
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, MAX_UNSCOPED_SEARCH_DEPTH, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, DELETED_MARGIN, 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;
|
|
66998
67109
|
var init_imapClient = __esm({
|
|
66999
67110
|
"src/services/imapClient.ts"() {
|
|
67000
67111
|
"use strict";
|
|
@@ -67068,6 +67179,26 @@ var init_imapClient = __esm({
|
|
|
67068
67179
|
LARGE_MAILBOX_MESSAGES = 1e4;
|
|
67069
67180
|
FIRST_SEARCH_WINDOW = 5e3;
|
|
67070
67181
|
MAX_WINDOW = 5e4;
|
|
67182
|
+
MAX_UNSCOPED_SEARCH_DEPTH = 5e3;
|
|
67183
|
+
ROW_QUERY = {
|
|
67184
|
+
envelope: true,
|
|
67185
|
+
flags: true,
|
|
67186
|
+
bodyStructure: true,
|
|
67187
|
+
internalDate: true,
|
|
67188
|
+
headers: ["date"]
|
|
67189
|
+
};
|
|
67190
|
+
ROW_QUERY_NO_STRUCTURE = {
|
|
67191
|
+
envelope: true,
|
|
67192
|
+
flags: true,
|
|
67193
|
+
internalDate: true,
|
|
67194
|
+
headers: ["date", "content-type"]
|
|
67195
|
+
};
|
|
67196
|
+
ROW_QUERY_HEADERS_ONLY = {
|
|
67197
|
+
flags: true,
|
|
67198
|
+
internalDate: true,
|
|
67199
|
+
headers: ["date", "from", "subject", "message-id", "in-reply-to", "content-type"]
|
|
67200
|
+
};
|
|
67201
|
+
DELETED_MARGIN = 32;
|
|
67071
67202
|
poolConnect = defaultConnect;
|
|
67072
67203
|
pools = /* @__PURE__ */ new Map();
|
|
67073
67204
|
connecting = /* @__PURE__ */ new Map();
|
|
@@ -87569,6 +87700,7 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
|
|
|
87569
87700
|
const accountsFailed = [];
|
|
87570
87701
|
const failedMailboxes = [];
|
|
87571
87702
|
const failedMailboxReasons = {};
|
|
87703
|
+
const omittedMessages = [];
|
|
87572
87704
|
for (const config2 of configs) {
|
|
87573
87705
|
const perAccountArgs = { ...args, account: void 0 };
|
|
87574
87706
|
try {
|
|
@@ -87581,12 +87713,25 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
|
|
|
87581
87713
|
for (const [mailbox, reason] of Object.entries(res.failedMailboxReasons)) {
|
|
87582
87714
|
failedMailboxReasons[`${config2.accountLabel} / ${mailbox}`] = reason;
|
|
87583
87715
|
}
|
|
87716
|
+
omittedMessages.push(
|
|
87717
|
+
...(res.omittedMessages ?? []).map((o) => ({
|
|
87718
|
+
...o,
|
|
87719
|
+
mailbox: `${config2.accountLabel} / ${o.mailbox}`
|
|
87720
|
+
}))
|
|
87721
|
+
);
|
|
87584
87722
|
} catch (e) {
|
|
87585
87723
|
accountsFailed.push(config2.accountLabel);
|
|
87586
87724
|
console.error(`IMAP fan-out failed for account "${config2.accountLabel}": ${String(e)}`);
|
|
87587
87725
|
}
|
|
87588
87726
|
}
|
|
87589
|
-
return {
|
|
87727
|
+
return {
|
|
87728
|
+
rows,
|
|
87729
|
+
accountsQueried,
|
|
87730
|
+
accountsFailed,
|
|
87731
|
+
failedMailboxes,
|
|
87732
|
+
failedMailboxReasons,
|
|
87733
|
+
omittedMessages
|
|
87734
|
+
};
|
|
87590
87735
|
}
|
|
87591
87736
|
function configMatchesAccount(config2, account) {
|
|
87592
87737
|
const name = account.name.trim().toLowerCase();
|
|
@@ -88189,7 +88334,10 @@ var LIST_OUTPUT_SCHEMA = {
|
|
|
88189
88334
|
failedMailboxes: external_exports.array(external_exports.string()).optional(),
|
|
88190
88335
|
// Underlying error text per entry in `failedMailboxes`, same keys (#246
|
|
88191
88336
|
// follow-up) — declared so a client can rely on it rather than parse text.
|
|
88192
|
-
failedMailboxReasons: external_exports.record(external_exports.string(), external_exports.string()).optional()
|
|
88337
|
+
failedMailboxReasons: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
88338
|
+
// Messages that belong on the page but could not be read, each with its
|
|
88339
|
+
// imap: id and why (#256 follow-up). Non-empty implies `partial: true`.
|
|
88340
|
+
omittedMessages: external_exports.array(external_exports.object({ id: external_exports.string(), mailbox: external_exports.string(), uid: external_exports.number(), reason: external_exports.string() })).optional()
|
|
88193
88341
|
};
|
|
88194
88342
|
var BATCH_COUNT_OUTPUT_SCHEMA = {
|
|
88195
88343
|
ok: external_exports.boolean().optional(),
|
|
@@ -88268,7 +88416,7 @@ function mergedMessageResponse(fan, apple, limit, verb) {
|
|
|
88268
88416
|
const merged = mergeMessages(fan.rows, apple.rows, limit);
|
|
88269
88417
|
const diagnostics = {
|
|
88270
88418
|
...apple.diagnostics,
|
|
88271
|
-
partial: apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0,
|
|
88419
|
+
partial: apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0 || fan.omittedMessages.length > 0,
|
|
88272
88420
|
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
|
|
88273
88421
|
notSearchedMailboxes: [...apple.diagnostics.notSearchedMailboxes, ...fan.failedMailboxes]
|
|
88274
88422
|
};
|
|
@@ -88280,9 +88428,10 @@ function mergedMessageResponse(fan, apple, limit, verb) {
|
|
|
88280
88428
|
notSearchedMailboxes: diagnostics.notSearchedMailboxes,
|
|
88281
88429
|
timedOutAccounts: diagnostics.timedOutAccounts,
|
|
88282
88430
|
failedMailboxes: fan.failedMailboxes,
|
|
88283
|
-
failedMailboxReasons: fan.failedMailboxReasons
|
|
88431
|
+
failedMailboxReasons: fan.failedMailboxReasons,
|
|
88432
|
+
omittedMessages: fan.omittedMessages
|
|
88284
88433
|
};
|
|
88285
|
-
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
88434
|
+
const coverageBlock = partialCoverageBlock(diagnostics) + omittedNote(fan.omittedMessages, true);
|
|
88286
88435
|
if (merged.length === 0) {
|
|
88287
88436
|
const base = diagnostics.partial ? `No messages found in the portions that were ${verb === "matched" ? "searched" : "listed"}.` : "No messages found";
|
|
88288
88437
|
return successResponse(`${base}${coverageBlock}`, structured);
|
|
@@ -88345,7 +88494,10 @@ registerTool(
|
|
|
88345
88494
|
isFlagged: external_exports.boolean().optional().describe("Filter by flagged status"),
|
|
88346
88495
|
dateFrom: DATE_FILTER_SCHEMA.describe("Start date filter (e.g., 'January 1, 2026')"),
|
|
88347
88496
|
dateTo: DATE_FILTER_SCHEMA.describe("End date filter (e.g., 'March 1, 2026')"),
|
|
88348
|
-
limit: external_exports.number().int().min(1).max(500).optional().describe("Maximum number of results (default: 50, max: 500)")
|
|
88497
|
+
limit: external_exports.number().int().min(1).max(500).optional().describe("Maximum number of results (default: 50, max: 500)"),
|
|
88498
|
+
offset: external_exports.number().int().min(0).optional().describe(
|
|
88499
|
+
"Number of newest matches to skip (for pagination; IMAP accounts only). Without a mailbox, offset + limit may not exceed 5,000 \u2014 name a mailbox to page deeper."
|
|
88500
|
+
)
|
|
88349
88501
|
},
|
|
88350
88502
|
outputSchema: LIST_OUTPUT_SCHEMA
|
|
88351
88503
|
},
|
|
@@ -88356,6 +88508,7 @@ registerTool(
|
|
|
88356
88508
|
mailbox,
|
|
88357
88509
|
account,
|
|
88358
88510
|
limit = 50,
|
|
88511
|
+
offset = 0,
|
|
88359
88512
|
dateFrom,
|
|
88360
88513
|
dateTo,
|
|
88361
88514
|
from,
|
|
@@ -88363,12 +88516,17 @@ registerTool(
|
|
|
88363
88516
|
isRead,
|
|
88364
88517
|
isFlagged
|
|
88365
88518
|
}) => {
|
|
88519
|
+
if (offset > 0 && !mailbox) {
|
|
88520
|
+
const tooDeep = unscopedSearchOffsetError(offset, limit);
|
|
88521
|
+
if (tooDeep) return errorResponse(tooDeep);
|
|
88522
|
+
}
|
|
88366
88523
|
if (shouldUseImap(account)) {
|
|
88367
88524
|
const imapArgs = {
|
|
88368
88525
|
query,
|
|
88369
88526
|
body,
|
|
88370
88527
|
mailbox,
|
|
88371
88528
|
limit,
|
|
88529
|
+
offset,
|
|
88372
88530
|
dateFrom,
|
|
88373
88531
|
dateTo,
|
|
88374
88532
|
from,
|
|
@@ -88383,14 +88541,20 @@ registerTool(
|
|
|
88383
88541
|
count: r.count,
|
|
88384
88542
|
partial: r.partial,
|
|
88385
88543
|
failedMailboxes: r.failedMailboxes,
|
|
88386
|
-
failedMailboxReasons: r.failedMailboxReasons
|
|
88544
|
+
failedMailboxReasons: r.failedMailboxReasons,
|
|
88545
|
+
omittedMessages: r.omittedMessages
|
|
88387
88546
|
});
|
|
88388
88547
|
}
|
|
88389
|
-
const fan = await fanOutImapMessages(imapArgs, "search");
|
|
88390
88548
|
const { appleScriptOnly } = partitionAccountsForCounts(
|
|
88391
88549
|
mailManager.listAccounts(),
|
|
88392
88550
|
resolveImapConfigs()
|
|
88393
88551
|
);
|
|
88552
|
+
if (offset > 0 && appleScriptOnly.length > 0 && !body) {
|
|
88553
|
+
return errorResponse(
|
|
88554
|
+
`offset is supported only on IMAP accounts, and ${appleScriptOnly.map((a) => `"${a.name}"`).join(", ")} ${appleScriptOnly.length === 1 ? "is" : "are"} AppleScript-only. Pass an IMAP account to page, or omit offset.`
|
|
88555
|
+
);
|
|
88556
|
+
}
|
|
88557
|
+
const fan = await fanOutImapMessages(imapArgs, "search");
|
|
88394
88558
|
if (body) {
|
|
88395
88559
|
const apple2 = {
|
|
88396
88560
|
rows: [],
|
|
@@ -88421,6 +88585,11 @@ registerTool(
|
|
|
88421
88585
|
);
|
|
88422
88586
|
return mergedMessageResponse(fan, apple, limit, "matched");
|
|
88423
88587
|
}
|
|
88588
|
+
if (offset > 0) {
|
|
88589
|
+
return errorResponse(
|
|
88590
|
+
`offset is supported only on IMAP accounts; IMAP is not configured for ${account ? `account "${account}"` : "any account"}. Omit offset, or narrow the search with dateFrom/dateTo instead.`
|
|
88591
|
+
);
|
|
88592
|
+
}
|
|
88424
88593
|
if (body) {
|
|
88425
88594
|
return errorResponse(
|
|
88426
88595
|
`Body search requires the IMAP backend, which is not configured for ${account ? `account "${account}"` : "any account"}. Configure IMAP (see the IMAP backend section of the README) or search by query/subject/from instead.`
|
|
@@ -88737,7 +88906,9 @@ registerTool(
|
|
|
88737
88906
|
messages: external_exports.array(MESSAGE_ROW_SCHEMA).optional(),
|
|
88738
88907
|
count: external_exports.number().optional(),
|
|
88739
88908
|
partial: external_exports.boolean().optional(),
|
|
88740
|
-
failedMailboxes: external_exports.array(external_exports.string()).optional()
|
|
88909
|
+
failedMailboxes: external_exports.array(external_exports.string()).optional(),
|
|
88910
|
+
failedMailboxReasons: LIST_OUTPUT_SCHEMA.failedMailboxReasons,
|
|
88911
|
+
omittedMessages: LIST_OUTPUT_SCHEMA.omittedMessages
|
|
88741
88912
|
}
|
|
88742
88913
|
},
|
|
88743
88914
|
withErrorHandling(async ({ id, account, mailbox, limit = 50 }) => {
|
|
@@ -88767,7 +88938,8 @@ ${r.text}`, {
|
|
|
88767
88938
|
count: r.count,
|
|
88768
88939
|
partial: r.partial,
|
|
88769
88940
|
failedMailboxes: r.failedMailboxes,
|
|
88770
|
-
failedMailboxReasons: r.failedMailboxReasons
|
|
88941
|
+
failedMailboxReasons: r.failedMailboxReasons,
|
|
88942
|
+
omittedMessages: r.omittedMessages
|
|
88771
88943
|
});
|
|
88772
88944
|
}
|
|
88773
88945
|
const fan = await fanOutImapMessages({ subject: base, mailbox, limit }, "search");
|
|
@@ -88792,20 +88964,21 @@ ${r.text}`, {
|
|
|
88792
88964
|
const orderedRows = mergedNewestFirst.slice().reverse().sort(
|
|
88793
88965
|
(a, b) => (a.dateReceived ? new Date(a.dateReceived).getTime() : 0) - (b.dateReceived ? new Date(b.dateReceived).getTime() : 0)
|
|
88794
88966
|
);
|
|
88795
|
-
const partial2 = apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0;
|
|
88967
|
+
const partial2 = apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0 || fan.omittedMessages.length > 0;
|
|
88796
88968
|
const coverage = partialCoverageBlock({
|
|
88797
88969
|
...apple.diagnostics,
|
|
88798
88970
|
partial: partial2,
|
|
88799
88971
|
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
|
|
88800
88972
|
notSearchedMailboxes: [...apple.diagnostics.notSearchedMailboxes, ...fan.failedMailboxes]
|
|
88801
|
-
});
|
|
88973
|
+
}) + omittedNote(fan.omittedMessages, true);
|
|
88802
88974
|
const structured2 = {
|
|
88803
88975
|
subject: base,
|
|
88804
88976
|
messages: orderedRows,
|
|
88805
88977
|
count: orderedRows.length,
|
|
88806
88978
|
partial: partial2,
|
|
88807
88979
|
failedMailboxes: fan.failedMailboxes,
|
|
88808
|
-
failedMailboxReasons: fan.failedMailboxReasons
|
|
88980
|
+
failedMailboxReasons: fan.failedMailboxReasons,
|
|
88981
|
+
omittedMessages: fan.omittedMessages
|
|
88809
88982
|
};
|
|
88810
88983
|
if (orderedRows.length === 0) {
|
|
88811
88984
|
return successResponse(`No messages found in thread "${base}".${coverage}`, structured2);
|
|
@@ -88870,7 +89043,8 @@ registerTool(
|
|
|
88870
89043
|
count: r.count,
|
|
88871
89044
|
partial: r.partial,
|
|
88872
89045
|
failedMailboxes: r.failedMailboxes,
|
|
88873
|
-
failedMailboxReasons: r.failedMailboxReasons
|
|
89046
|
+
failedMailboxReasons: r.failedMailboxReasons,
|
|
89047
|
+
omittedMessages: r.omittedMessages
|
|
88874
89048
|
});
|
|
88875
89049
|
}
|
|
88876
89050
|
const fan = await fanOutImapMessages({ mailbox, limit, offset, from, unreadOnly }, "list");
|
package/package.json
CHANGED