apple-mail-mcp 2.19.16 → 2.19.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -3
- package/build/cli.js +157 -40
- package/build/index.js +157 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -826,7 +826,7 @@ Reply to an existing message.
|
|
|
826
826
|
| Parameter | Type | Required | Description |
|
|
827
827
|
| ----------- | ------- | -------- | ---------------------------------------------------------------------------------------------- |
|
|
828
828
|
| `id` | string | Yes | Message ID to reply to |
|
|
829
|
-
| `body` | string | Yes | Reply body (plain text; HTML tags such as `<br>` are not rendered)
|
|
829
|
+
| `body` | string | Yes | Reply body (plain text; HTML tags such as `<br>` are not rendered) |
|
|
830
830
|
| `replyAll` | boolean | No | Reply to all recipients (default: false) |
|
|
831
831
|
| `send` | boolean | No | Send immediately (default: true, false = save as draft) |
|
|
832
832
|
| `transport` | string | No | `smtp` or `applescript`; omitted prefers configured SMTP when sending. Drafts use AppleScript. |
|
|
@@ -1081,17 +1081,31 @@ Matching ignores letter case **and Unicode normalization form**: a typed
|
|
|
1081
1081
|
precomposed `México` (NFC) finds a mailbox the server stores decomposed (NFD —
|
|
1082
1082
|
common for folders created on a Mac, and what iCloud keeps), and the server's
|
|
1083
1083
|
own stored spelling is what gets selected. Two mailboxes whose names differ
|
|
1084
|
-
|
|
1084
|
+
_only_ in case or normalization look identical in any listing, so a name that
|
|
1085
1085
|
matches both is refused with an error saying so — rename one of them. For the
|
|
1086
1086
|
same reason `create-mailbox` treats a normalization-equivalent existing name as
|
|
1087
1087
|
already existing, and `rename-mailbox` refuses to create such a twin.
|
|
1088
1088
|
|
|
1089
1089
|
**Messages awaiting expunge are not listed.** On IMAP accounts, search,
|
|
1090
|
-
list, thread and mail-stats queries only match messages
|
|
1090
|
+
list, thread and mail-stats queries only match messages _not_ flagged
|
|
1091
1091
|
`\Deleted` (IMAP `UNDELETED`). iCloud keeps flagged-but-never-expunged
|
|
1092
1092
|
messages out of its message counts yet still returns them from `UID SEARCH`,
|
|
1093
1093
|
which made a filter-less `list-messages` fail on such a mailbox before 2.19.16.
|
|
1094
1094
|
|
|
1095
|
+
**Very large IMAP mailboxes are read newest-first in bounded windows**
|
|
1096
|
+
([#256](https://github.com/sweetrb/apple-mail-mcp/issues/256)). Above 10,000
|
|
1097
|
+
messages, a filter-less `list-messages`/`search-messages` pages by message
|
|
1098
|
+
sequence number from the top of the mailbox — it fetches UIDs and flags for
|
|
1099
|
+
just enough of the newest messages to fill `limit` + `offset` (skipping
|
|
1100
|
+
`\Deleted` ones) and full rows for the page alone, so `limit: 1` on a
|
|
1101
|
+
793,614-message mailbox costs one small `FETCH`, not a whole-mailbox `SEARCH`.
|
|
1102
|
+
A filtered search there runs the same criteria over newest-first sequence
|
|
1103
|
+
windows (5,000 messages, growing to 50,000) and stops once the page is full;
|
|
1104
|
+
the reported total then reads `at least N` unless the walk reached the bottom
|
|
1105
|
+
of the mailbox. A `SEARCH` or `FETCH` that fails names the mailbox, its size and
|
|
1106
|
+
the server's own reason (or a likely timeout) instead of reporting "no
|
|
1107
|
+
messages".
|
|
1108
|
+
|
|
1095
1109
|
**Mail's local "On My Mac" mailboxes** are not children of any account — they
|
|
1096
1110
|
hang off the application — so they are reported under the synthetic account label
|
|
1097
1111
|
**`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,
|
|
@@ -58428,44 +58429,139 @@ function messageIdentity(entry) {
|
|
|
58428
58429
|
const messageId = raw.replace(/^<+|>+$/g, "").trim().toLowerCase();
|
|
58429
58430
|
return messageId ? `mid:${messageId}` : `${entry.path}\0${entry.message.uid}`;
|
|
58430
58431
|
}
|
|
58431
|
-
|
|
58432
|
-
const
|
|
58432
|
+
function isUnfiltered(criteria) {
|
|
58433
|
+
const keys = Object.keys(criteria);
|
|
58434
|
+
return keys.length === 1 && criteria.deleted === false;
|
|
58435
|
+
}
|
|
58436
|
+
async function statusMessageCount(client, path) {
|
|
58433
58437
|
try {
|
|
58434
|
-
const
|
|
58435
|
-
|
|
58436
|
-
|
|
58437
|
-
|
|
58438
|
-
|
|
58438
|
+
const st = await client.status(path, { messages: true });
|
|
58439
|
+
return typeof st.messages === "number" && st.messages >= 0 ? st.messages : void 0;
|
|
58440
|
+
} catch {
|
|
58441
|
+
return void 0;
|
|
58442
|
+
}
|
|
58443
|
+
}
|
|
58444
|
+
async function searchUids(client, path, criteria, size) {
|
|
58445
|
+
client.takeLastCommandError?.();
|
|
58446
|
+
const found = await client.search(criteria, { uid: true });
|
|
58447
|
+
if (Array.isArray(found)) return found;
|
|
58448
|
+
const cause = client.takeLastCommandError?.();
|
|
58449
|
+
const sized = size === void 0 ? "" : ` (${size.toLocaleString("en-US")} messages)`;
|
|
58450
|
+
throw new Error(
|
|
58451
|
+
`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
|
+
);
|
|
58453
|
+
}
|
|
58454
|
+
async function fetchRows(client, uids) {
|
|
58455
|
+
if (uids.length === 0) return [];
|
|
58456
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
58457
|
+
for await (const msg of client.fetch(
|
|
58458
|
+
uids.join(","),
|
|
58459
|
+
// BODYSTRUCTURE rides along so `hasAttachments` is computed rather
|
|
58460
|
+
// than assumed. Measured on 50 real messages: ~390ms -> ~465ms for
|
|
58461
|
+
// the fetch (~17%), same single round trip, no extra request.
|
|
58462
|
+
//
|
|
58463
|
+
// INTERNALDATE rides along for the same reason, and is why `dateReceived`
|
|
58464
|
+
// can finally mean what it says: imapflow's `envelope.date` is built from
|
|
58465
|
+
// the header block, so it IS the `Date:` header, not arrival time.
|
|
58466
|
+
//
|
|
58467
|
+
// The `Date:` header itself (BODY.PEEK[HEADER.FIELDS (DATE)]) rides in the
|
|
58468
|
+
// SAME FETCH command, so a date the server's ENVELOPE parser rejected can
|
|
58469
|
+
// still be recovered (#234). Measured on 50 real messages, 6 alternating
|
|
58470
|
+
// runs: median ~334ms without vs ~315ms with — inside the noise — for ~44
|
|
58471
|
+
// bytes per message. Too cheap to hide behind an opt-in.
|
|
58472
|
+
{ envelope: true, flags: true, bodyStructure: true, internalDate: true, headers: ["date"] },
|
|
58473
|
+
{ uid: true }
|
|
58474
|
+
)) {
|
|
58475
|
+
byUid.set(msg.uid, msg);
|
|
58476
|
+
}
|
|
58477
|
+
return uids.map((uid) => byUid.get(uid)).filter((message) => message !== void 0);
|
|
58478
|
+
}
|
|
58479
|
+
async function walkWindows(exists, page, firstWindow, readWindow) {
|
|
58480
|
+
const wanted = page.skip + page.take;
|
|
58481
|
+
const uids = [];
|
|
58482
|
+
let seen = 0;
|
|
58483
|
+
let matched = 0;
|
|
58484
|
+
let hi = exists;
|
|
58485
|
+
let width = Math.max(1, Math.min(firstWindow, MAX_WINDOW));
|
|
58486
|
+
while (hi >= 1 && seen < wanted) {
|
|
58487
|
+
const lo = Math.max(1, hi - width + 1);
|
|
58488
|
+
const live = (await readWindow(lo, hi === exists ? "*" : hi, hi - lo + 1)).slice().sort((a, b) => b - a);
|
|
58489
|
+
matched += live.length;
|
|
58490
|
+
for (const uid of live) {
|
|
58491
|
+
if (seen >= wanted) break;
|
|
58492
|
+
if (seen >= page.skip) uids.push(uid);
|
|
58493
|
+
seen++;
|
|
58494
|
+
}
|
|
58495
|
+
hi = lo - 1;
|
|
58496
|
+
width = Math.min(width * 4, MAX_WINDOW);
|
|
58497
|
+
}
|
|
58498
|
+
return { uids, matched, exhausted: hi < 1 };
|
|
58499
|
+
}
|
|
58500
|
+
async function listLargeMailbox(client, exists, page) {
|
|
58501
|
+
let deletedSeen = 0;
|
|
58502
|
+
const walk = await walkWindows(
|
|
58503
|
+
exists,
|
|
58504
|
+
page,
|
|
58505
|
+
// Room for a few ghosts on the first read without a second round trip.
|
|
58506
|
+
page.skip + page.take + 64,
|
|
58507
|
+
async (lo, hi) => {
|
|
58508
|
+
const live = [];
|
|
58509
|
+
for await (const msg of client.fetch(`${lo}:${hi}`, { uid: true, flags: true })) {
|
|
58510
|
+
if (msg.flags?.has("\\Deleted")) deletedSeen++;
|
|
58511
|
+
else live.push(msg.uid);
|
|
58512
|
+
}
|
|
58513
|
+
return live;
|
|
58514
|
+
}
|
|
58515
|
+
);
|
|
58516
|
+
return {
|
|
58517
|
+
messages: await fetchRows(client, walk.uids),
|
|
58518
|
+
total: Math.max(0, exists - deletedSeen),
|
|
58519
|
+
totalExact: true
|
|
58520
|
+
};
|
|
58521
|
+
}
|
|
58522
|
+
async function searchLargeMailbox(client, path, criteria, exists, page) {
|
|
58523
|
+
const walk = await walkWindows(exists, page, FIRST_SEARCH_WINDOW, async (lo, hi, width) => {
|
|
58524
|
+
const found = await searchUids(client, path, { ...criteria, seq: `${lo}:${hi}` }, exists);
|
|
58525
|
+
if (found.length > width) {
|
|
58439
58526
|
throw new Error(
|
|
58440
|
-
`IMAP SEARCH on "${path}" reported ${
|
|
58527
|
+
`IMAP SEARCH on "${path}" reported ${found.length} matches in a ${width}-message window \u2014 discarding as corrupted rather than trusting it (see #246).`
|
|
58441
58528
|
);
|
|
58442
58529
|
}
|
|
58443
|
-
|
|
58444
|
-
|
|
58445
|
-
|
|
58446
|
-
|
|
58447
|
-
|
|
58448
|
-
|
|
58449
|
-
|
|
58450
|
-
|
|
58451
|
-
|
|
58452
|
-
|
|
58453
|
-
|
|
58454
|
-
|
|
58455
|
-
|
|
58456
|
-
|
|
58457
|
-
|
|
58458
|
-
|
|
58459
|
-
|
|
58460
|
-
|
|
58461
|
-
|
|
58462
|
-
|
|
58463
|
-
|
|
58530
|
+
return found;
|
|
58531
|
+
});
|
|
58532
|
+
return {
|
|
58533
|
+
messages: await fetchRows(client, walk.uids),
|
|
58534
|
+
total: walk.matched,
|
|
58535
|
+
totalExact: walk.exhausted
|
|
58536
|
+
};
|
|
58537
|
+
}
|
|
58538
|
+
async function fetchMailboxMatches(client, path, criteria, page) {
|
|
58539
|
+
const lock = await client.getMailboxLock(path);
|
|
58540
|
+
try {
|
|
58541
|
+
const exists = await statusMessageCount(client, path);
|
|
58542
|
+
if (exists === 0) return { messages: [], total: 0, totalExact: true };
|
|
58543
|
+
if (exists !== void 0 && exists > LARGE_MAILBOX_MESSAGES) {
|
|
58544
|
+
try {
|
|
58545
|
+
return isUnfiltered(criteria) ? await listLargeMailbox(client, exists, page) : await searchLargeMailbox(client, path, criteria, exists, page);
|
|
58546
|
+
} catch (error) {
|
|
58547
|
+
const detail = errText(error);
|
|
58548
|
+
throw new Error(
|
|
58549
|
+
detail.includes(`"${path}" (`) ? detail : `reading the newest messages of "${path}" (${exists.toLocaleString("en-US")} messages) failed: ${detail}`
|
|
58550
|
+
);
|
|
58551
|
+
}
|
|
58464
58552
|
}
|
|
58465
|
-
|
|
58466
|
-
|
|
58467
|
-
total: uids.length
|
|
58468
|
-
}
|
|
58553
|
+
const uids = await searchUids(client, path, criteria, exists);
|
|
58554
|
+
if (uids.length === 0 || page.take === 0) {
|
|
58555
|
+
return { messages: [], total: uids.length, totalExact: true };
|
|
58556
|
+
}
|
|
58557
|
+
const messages = exists ?? (await client.status(path, { messages: true })).messages ?? void 0;
|
|
58558
|
+
if (typeof messages === "number" && uids.length > messages) {
|
|
58559
|
+
throw new Error(
|
|
58560
|
+
`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).`
|
|
58561
|
+
);
|
|
58562
|
+
}
|
|
58563
|
+
const newest = uids.slice().reverse().slice(page.skip, page.skip + page.take);
|
|
58564
|
+
return { messages: await fetchRows(client, newest), total: uids.length, totalExact: true };
|
|
58469
58565
|
} finally {
|
|
58470
58566
|
lock.release();
|
|
58471
58567
|
}
|
|
@@ -58494,15 +58590,17 @@ async function run(args, listMode, deps) {
|
|
|
58494
58590
|
const limit = args.limit ?? 50;
|
|
58495
58591
|
const offset = args.offset ?? 0;
|
|
58496
58592
|
const criteria = buildCriteria(args, listMode);
|
|
58497
|
-
const
|
|
58593
|
+
const page = unscopedSearch ? { skip: 0, take: offset + limit } : { skip: offset, take: limit };
|
|
58498
58594
|
const fetched = [];
|
|
58499
58595
|
const failedMailboxes = [];
|
|
58500
58596
|
const failedMailboxReasons = {};
|
|
58501
58597
|
let totalMatched = 0;
|
|
58598
|
+
let totalExact = true;
|
|
58502
58599
|
for (const path of paths) {
|
|
58503
58600
|
try {
|
|
58504
|
-
const result = await fetchMailboxMatches(client, path, criteria,
|
|
58601
|
+
const result = await fetchMailboxMatches(client, path, criteria, page);
|
|
58505
58602
|
totalMatched += result.total;
|
|
58603
|
+
totalExact &&= result.totalExact;
|
|
58506
58604
|
fetched.push(...result.messages.map((message) => ({ message, path })));
|
|
58507
58605
|
} catch (error) {
|
|
58508
58606
|
failedMailboxes.push(path);
|
|
@@ -58527,8 +58625,6 @@ async function run(args, listMode, deps) {
|
|
|
58527
58625
|
if (!unique.has(key)) unique.set(key, entry);
|
|
58528
58626
|
}
|
|
58529
58627
|
ordered = [...unique.values()].slice(offset, offset + limit);
|
|
58530
|
-
} else {
|
|
58531
|
-
ordered = fetched.slice(offset, offset + limit);
|
|
58532
58628
|
}
|
|
58533
58629
|
const rows = ordered.map(({ message, path }) => formatRow(message, cfg.accountLabel, path));
|
|
58534
58630
|
const messages = ordered.map(
|
|
@@ -58539,6 +58635,7 @@ async function run(args, listMode, deps) {
|
|
|
58539
58635
|
|
|
58540
58636
|
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
|
|
58541
58637
|
const verb = listMode ? "listed" : "matched";
|
|
58638
|
+
const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
|
|
58542
58639
|
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
58543
58640
|
if (messages.length === 0) {
|
|
58544
58641
|
return {
|
|
@@ -58550,7 +58647,7 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
|
|
|
58550
58647
|
failedMailboxReasons
|
|
58551
58648
|
};
|
|
58552
58649
|
}
|
|
58553
|
-
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${
|
|
58650
|
+
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalText} ${verb}):
|
|
58554
58651
|
` + rows.join("\n") + `
|
|
58555
58652
|
|
|
58556
58653
|
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;
|
|
@@ -59568,7 +59665,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59568
59665
|
true
|
|
59569
59666
|
);
|
|
59570
59667
|
}
|
|
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;
|
|
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;
|
|
59572
59669
|
var init_imapClient = __esm({
|
|
59573
59670
|
"src/services/imapClient.ts"() {
|
|
59574
59671
|
"use strict";
|
|
@@ -59594,7 +59691,24 @@ var init_imapClient = __esm({
|
|
|
59594
59691
|
accounts: "APPLE_MAIL_MCP_IMAP_ACCOUNTS"
|
|
59595
59692
|
};
|
|
59596
59693
|
defaultConnect = async (cfg) => {
|
|
59597
|
-
|
|
59694
|
+
let lastCommandError;
|
|
59695
|
+
const keepErr = (entry) => {
|
|
59696
|
+
if (entry && typeof entry === "object" && "err" in entry) {
|
|
59697
|
+
lastCommandError = entry.err;
|
|
59698
|
+
}
|
|
59699
|
+
};
|
|
59700
|
+
const noop = () => void 0;
|
|
59701
|
+
const client = new import_imapflow.ImapFlow({
|
|
59702
|
+
...buildImapConnectionOptions(cfg),
|
|
59703
|
+
logger: { trace: noop, debug: noop, info: noop, warn: keepErr, error: keepErr, fatal: keepErr }
|
|
59704
|
+
});
|
|
59705
|
+
Object.assign(client, {
|
|
59706
|
+
takeLastCommandError: () => {
|
|
59707
|
+
const err = lastCommandError;
|
|
59708
|
+
lastCommandError = void 0;
|
|
59709
|
+
return err;
|
|
59710
|
+
}
|
|
59711
|
+
});
|
|
59598
59712
|
client.on("error", () => {
|
|
59599
59713
|
});
|
|
59600
59714
|
try {
|
|
@@ -59622,6 +59736,9 @@ var init_imapClient = __esm({
|
|
|
59622
59736
|
starred: "\\flagged"
|
|
59623
59737
|
};
|
|
59624
59738
|
NOT_DELETED = { deleted: false };
|
|
59739
|
+
LARGE_MAILBOX_MESSAGES = 1e4;
|
|
59740
|
+
FIRST_SEARCH_WINDOW = 5e3;
|
|
59741
|
+
MAX_WINDOW = 5e4;
|
|
59625
59742
|
poolConnect = defaultConnect;
|
|
59626
59743
|
pools = /* @__PURE__ */ new Map();
|
|
59627
59744
|
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,
|
|
@@ -65757,44 +65758,139 @@ function messageIdentity(entry) {
|
|
|
65757
65758
|
const messageId = raw.replace(/^<+|>+$/g, "").trim().toLowerCase();
|
|
65758
65759
|
return messageId ? `mid:${messageId}` : `${entry.path}\0${entry.message.uid}`;
|
|
65759
65760
|
}
|
|
65760
|
-
|
|
65761
|
-
const
|
|
65761
|
+
function isUnfiltered(criteria) {
|
|
65762
|
+
const keys = Object.keys(criteria);
|
|
65763
|
+
return keys.length === 1 && criteria.deleted === false;
|
|
65764
|
+
}
|
|
65765
|
+
async function statusMessageCount(client, path) {
|
|
65762
65766
|
try {
|
|
65763
|
-
const
|
|
65764
|
-
|
|
65765
|
-
|
|
65766
|
-
|
|
65767
|
-
|
|
65767
|
+
const st = await client.status(path, { messages: true });
|
|
65768
|
+
return typeof st.messages === "number" && st.messages >= 0 ? st.messages : void 0;
|
|
65769
|
+
} catch {
|
|
65770
|
+
return void 0;
|
|
65771
|
+
}
|
|
65772
|
+
}
|
|
65773
|
+
async function searchUids(client, path, criteria, size) {
|
|
65774
|
+
client.takeLastCommandError?.();
|
|
65775
|
+
const found = await client.search(criteria, { uid: true });
|
|
65776
|
+
if (Array.isArray(found)) return found;
|
|
65777
|
+
const cause = client.takeLastCommandError?.();
|
|
65778
|
+
const sized = size === void 0 ? "" : ` (${size.toLocaleString("en-US")} messages)`;
|
|
65779
|
+
throw new Error(
|
|
65780
|
+
`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
|
+
);
|
|
65782
|
+
}
|
|
65783
|
+
async function fetchRows(client, uids) {
|
|
65784
|
+
if (uids.length === 0) return [];
|
|
65785
|
+
const byUid = /* @__PURE__ */ new Map();
|
|
65786
|
+
for await (const msg of client.fetch(
|
|
65787
|
+
uids.join(","),
|
|
65788
|
+
// BODYSTRUCTURE rides along so `hasAttachments` is computed rather
|
|
65789
|
+
// than assumed. Measured on 50 real messages: ~390ms -> ~465ms for
|
|
65790
|
+
// the fetch (~17%), same single round trip, no extra request.
|
|
65791
|
+
//
|
|
65792
|
+
// INTERNALDATE rides along for the same reason, and is why `dateReceived`
|
|
65793
|
+
// can finally mean what it says: imapflow's `envelope.date` is built from
|
|
65794
|
+
// the header block, so it IS the `Date:` header, not arrival time.
|
|
65795
|
+
//
|
|
65796
|
+
// The `Date:` header itself (BODY.PEEK[HEADER.FIELDS (DATE)]) rides in the
|
|
65797
|
+
// SAME FETCH command, so a date the server's ENVELOPE parser rejected can
|
|
65798
|
+
// still be recovered (#234). Measured on 50 real messages, 6 alternating
|
|
65799
|
+
// runs: median ~334ms without vs ~315ms with — inside the noise — for ~44
|
|
65800
|
+
// bytes per message. Too cheap to hide behind an opt-in.
|
|
65801
|
+
{ envelope: true, flags: true, bodyStructure: true, internalDate: true, headers: ["date"] },
|
|
65802
|
+
{ uid: true }
|
|
65803
|
+
)) {
|
|
65804
|
+
byUid.set(msg.uid, msg);
|
|
65805
|
+
}
|
|
65806
|
+
return uids.map((uid) => byUid.get(uid)).filter((message) => message !== void 0);
|
|
65807
|
+
}
|
|
65808
|
+
async function walkWindows(exists, page, firstWindow, readWindow) {
|
|
65809
|
+
const wanted = page.skip + page.take;
|
|
65810
|
+
const uids = [];
|
|
65811
|
+
let seen = 0;
|
|
65812
|
+
let matched = 0;
|
|
65813
|
+
let hi = exists;
|
|
65814
|
+
let width = Math.max(1, Math.min(firstWindow, MAX_WINDOW));
|
|
65815
|
+
while (hi >= 1 && seen < wanted) {
|
|
65816
|
+
const lo = Math.max(1, hi - width + 1);
|
|
65817
|
+
const live = (await readWindow(lo, hi === exists ? "*" : hi, hi - lo + 1)).slice().sort((a, b) => b - a);
|
|
65818
|
+
matched += live.length;
|
|
65819
|
+
for (const uid of live) {
|
|
65820
|
+
if (seen >= wanted) break;
|
|
65821
|
+
if (seen >= page.skip) uids.push(uid);
|
|
65822
|
+
seen++;
|
|
65823
|
+
}
|
|
65824
|
+
hi = lo - 1;
|
|
65825
|
+
width = Math.min(width * 4, MAX_WINDOW);
|
|
65826
|
+
}
|
|
65827
|
+
return { uids, matched, exhausted: hi < 1 };
|
|
65828
|
+
}
|
|
65829
|
+
async function listLargeMailbox(client, exists, page) {
|
|
65830
|
+
let deletedSeen = 0;
|
|
65831
|
+
const walk = await walkWindows(
|
|
65832
|
+
exists,
|
|
65833
|
+
page,
|
|
65834
|
+
// Room for a few ghosts on the first read without a second round trip.
|
|
65835
|
+
page.skip + page.take + 64,
|
|
65836
|
+
async (lo, hi) => {
|
|
65837
|
+
const live = [];
|
|
65838
|
+
for await (const msg of client.fetch(`${lo}:${hi}`, { uid: true, flags: true })) {
|
|
65839
|
+
if (msg.flags?.has("\\Deleted")) deletedSeen++;
|
|
65840
|
+
else live.push(msg.uid);
|
|
65841
|
+
}
|
|
65842
|
+
return live;
|
|
65843
|
+
}
|
|
65844
|
+
);
|
|
65845
|
+
return {
|
|
65846
|
+
messages: await fetchRows(client, walk.uids),
|
|
65847
|
+
total: Math.max(0, exists - deletedSeen),
|
|
65848
|
+
totalExact: true
|
|
65849
|
+
};
|
|
65850
|
+
}
|
|
65851
|
+
async function searchLargeMailbox(client, path, criteria, exists, page) {
|
|
65852
|
+
const walk = await walkWindows(exists, page, FIRST_SEARCH_WINDOW, async (lo, hi, width) => {
|
|
65853
|
+
const found = await searchUids(client, path, { ...criteria, seq: `${lo}:${hi}` }, exists);
|
|
65854
|
+
if (found.length > width) {
|
|
65768
65855
|
throw new Error(
|
|
65769
|
-
`IMAP SEARCH on "${path}" reported ${
|
|
65856
|
+
`IMAP SEARCH on "${path}" reported ${found.length} matches in a ${width}-message window \u2014 discarding as corrupted rather than trusting it (see #246).`
|
|
65770
65857
|
);
|
|
65771
65858
|
}
|
|
65772
|
-
|
|
65773
|
-
|
|
65774
|
-
|
|
65775
|
-
|
|
65776
|
-
|
|
65777
|
-
|
|
65778
|
-
|
|
65779
|
-
|
|
65780
|
-
|
|
65781
|
-
|
|
65782
|
-
|
|
65783
|
-
|
|
65784
|
-
|
|
65785
|
-
|
|
65786
|
-
|
|
65787
|
-
|
|
65788
|
-
|
|
65789
|
-
|
|
65790
|
-
|
|
65791
|
-
|
|
65792
|
-
|
|
65859
|
+
return found;
|
|
65860
|
+
});
|
|
65861
|
+
return {
|
|
65862
|
+
messages: await fetchRows(client, walk.uids),
|
|
65863
|
+
total: walk.matched,
|
|
65864
|
+
totalExact: walk.exhausted
|
|
65865
|
+
};
|
|
65866
|
+
}
|
|
65867
|
+
async function fetchMailboxMatches(client, path, criteria, page) {
|
|
65868
|
+
const lock = await client.getMailboxLock(path);
|
|
65869
|
+
try {
|
|
65870
|
+
const exists = await statusMessageCount(client, path);
|
|
65871
|
+
if (exists === 0) return { messages: [], total: 0, totalExact: true };
|
|
65872
|
+
if (exists !== void 0 && exists > LARGE_MAILBOX_MESSAGES) {
|
|
65873
|
+
try {
|
|
65874
|
+
return isUnfiltered(criteria) ? await listLargeMailbox(client, exists, page) : await searchLargeMailbox(client, path, criteria, exists, page);
|
|
65875
|
+
} catch (error2) {
|
|
65876
|
+
const detail = errText(error2);
|
|
65877
|
+
throw new Error(
|
|
65878
|
+
detail.includes(`"${path}" (`) ? detail : `reading the newest messages of "${path}" (${exists.toLocaleString("en-US")} messages) failed: ${detail}`
|
|
65879
|
+
);
|
|
65880
|
+
}
|
|
65793
65881
|
}
|
|
65794
|
-
|
|
65795
|
-
|
|
65796
|
-
total: uids.length
|
|
65797
|
-
}
|
|
65882
|
+
const uids = await searchUids(client, path, criteria, exists);
|
|
65883
|
+
if (uids.length === 0 || page.take === 0) {
|
|
65884
|
+
return { messages: [], total: uids.length, totalExact: true };
|
|
65885
|
+
}
|
|
65886
|
+
const messages = exists ?? (await client.status(path, { messages: true })).messages ?? void 0;
|
|
65887
|
+
if (typeof messages === "number" && uids.length > messages) {
|
|
65888
|
+
throw new Error(
|
|
65889
|
+
`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).`
|
|
65890
|
+
);
|
|
65891
|
+
}
|
|
65892
|
+
const newest = uids.slice().reverse().slice(page.skip, page.skip + page.take);
|
|
65893
|
+
return { messages: await fetchRows(client, newest), total: uids.length, totalExact: true };
|
|
65798
65894
|
} finally {
|
|
65799
65895
|
lock.release();
|
|
65800
65896
|
}
|
|
@@ -65823,15 +65919,17 @@ async function run(args, listMode, deps) {
|
|
|
65823
65919
|
const limit = args.limit ?? 50;
|
|
65824
65920
|
const offset = args.offset ?? 0;
|
|
65825
65921
|
const criteria = buildCriteria(args, listMode);
|
|
65826
|
-
const
|
|
65922
|
+
const page = unscopedSearch ? { skip: 0, take: offset + limit } : { skip: offset, take: limit };
|
|
65827
65923
|
const fetched = [];
|
|
65828
65924
|
const failedMailboxes = [];
|
|
65829
65925
|
const failedMailboxReasons = {};
|
|
65830
65926
|
let totalMatched = 0;
|
|
65927
|
+
let totalExact = true;
|
|
65831
65928
|
for (const path of paths) {
|
|
65832
65929
|
try {
|
|
65833
|
-
const result = await fetchMailboxMatches(client, path, criteria,
|
|
65930
|
+
const result = await fetchMailboxMatches(client, path, criteria, page);
|
|
65834
65931
|
totalMatched += result.total;
|
|
65932
|
+
totalExact &&= result.totalExact;
|
|
65835
65933
|
fetched.push(...result.messages.map((message) => ({ message, path })));
|
|
65836
65934
|
} catch (error2) {
|
|
65837
65935
|
failedMailboxes.push(path);
|
|
@@ -65856,8 +65954,6 @@ async function run(args, listMode, deps) {
|
|
|
65856
65954
|
if (!unique.has(key)) unique.set(key, entry);
|
|
65857
65955
|
}
|
|
65858
65956
|
ordered = [...unique.values()].slice(offset, offset + limit);
|
|
65859
|
-
} else {
|
|
65860
|
-
ordered = fetched.slice(offset, offset + limit);
|
|
65861
65957
|
}
|
|
65862
65958
|
const rows = ordered.map(({ message, path }) => formatRow(message, cfg.accountLabel, path));
|
|
65863
65959
|
const messages = ordered.map(
|
|
@@ -65868,6 +65964,7 @@ async function run(args, listMode, deps) {
|
|
|
65868
65964
|
|
|
65869
65965
|
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
|
|
65870
65966
|
const verb = listMode ? "listed" : "matched";
|
|
65967
|
+
const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
|
|
65871
65968
|
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
65872
65969
|
if (messages.length === 0) {
|
|
65873
65970
|
return {
|
|
@@ -65879,7 +65976,7 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
|
|
|
65879
65976
|
failedMailboxReasons
|
|
65880
65977
|
};
|
|
65881
65978
|
}
|
|
65882
|
-
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${
|
|
65979
|
+
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalText} ${verb}):
|
|
65883
65980
|
` + rows.join("\n") + `
|
|
65884
65981
|
|
|
65885
65982
|
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;
|
|
@@ -66897,7 +66994,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66897
66994
|
true
|
|
66898
66995
|
);
|
|
66899
66996
|
}
|
|
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;
|
|
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;
|
|
66901
66998
|
var init_imapClient = __esm({
|
|
66902
66999
|
"src/services/imapClient.ts"() {
|
|
66903
67000
|
"use strict";
|
|
@@ -66923,7 +67020,24 @@ var init_imapClient = __esm({
|
|
|
66923
67020
|
accounts: "APPLE_MAIL_MCP_IMAP_ACCOUNTS"
|
|
66924
67021
|
};
|
|
66925
67022
|
defaultConnect = async (cfg) => {
|
|
66926
|
-
|
|
67023
|
+
let lastCommandError;
|
|
67024
|
+
const keepErr = (entry) => {
|
|
67025
|
+
if (entry && typeof entry === "object" && "err" in entry) {
|
|
67026
|
+
lastCommandError = entry.err;
|
|
67027
|
+
}
|
|
67028
|
+
};
|
|
67029
|
+
const noop = () => void 0;
|
|
67030
|
+
const client = new import_imapflow.ImapFlow({
|
|
67031
|
+
...buildImapConnectionOptions(cfg),
|
|
67032
|
+
logger: { trace: noop, debug: noop, info: noop, warn: keepErr, error: keepErr, fatal: keepErr }
|
|
67033
|
+
});
|
|
67034
|
+
Object.assign(client, {
|
|
67035
|
+
takeLastCommandError: () => {
|
|
67036
|
+
const err = lastCommandError;
|
|
67037
|
+
lastCommandError = void 0;
|
|
67038
|
+
return err;
|
|
67039
|
+
}
|
|
67040
|
+
});
|
|
66927
67041
|
client.on("error", () => {
|
|
66928
67042
|
});
|
|
66929
67043
|
try {
|
|
@@ -66951,6 +67065,9 @@ var init_imapClient = __esm({
|
|
|
66951
67065
|
starred: "\\flagged"
|
|
66952
67066
|
};
|
|
66953
67067
|
NOT_DELETED = { deleted: false };
|
|
67068
|
+
LARGE_MAILBOX_MESSAGES = 1e4;
|
|
67069
|
+
FIRST_SEARCH_WINDOW = 5e3;
|
|
67070
|
+
MAX_WINDOW = 5e4;
|
|
66954
67071
|
poolConnect = defaultConnect;
|
|
66955
67072
|
pools = /* @__PURE__ */ new Map();
|
|
66956
67073
|
connecting = /* @__PURE__ */ new Map();
|
package/package.json
CHANGED