apple-mail-mcp 2.19.15 → 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 +22 -2
- package/build/cli.js +178 -46
- package/build/index.js +178 -46
- 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,11 +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
|
+
**Messages awaiting expunge are not listed.** On IMAP accounts, search,
|
|
1090
|
+
list, thread and mail-stats queries only match messages _not_ flagged
|
|
1091
|
+
`\Deleted` (IMAP `UNDELETED`). iCloud keeps flagged-but-never-expunged
|
|
1092
|
+
messages out of its message counts yet still returns them from `UID SEARCH`,
|
|
1093
|
+
which made a filter-less `list-messages` fail on such a mailbox before 2.19.16.
|
|
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
|
+
|
|
1089
1109
|
**Mail's local "On My Mac" mailboxes** are not children of any account — they
|
|
1090
1110
|
hang off the application — so they are reported under the synthetic account label
|
|
1091
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,
|
|
@@ -58323,7 +58324,7 @@ async function resolveMailboxPath(client, mailbox, _mode) {
|
|
|
58323
58324
|
return staticMailboxAlias(mailbox);
|
|
58324
58325
|
}
|
|
58325
58326
|
function buildCriteria(a, listMode) {
|
|
58326
|
-
const c = {};
|
|
58327
|
+
const c = { ...NOT_DELETED };
|
|
58327
58328
|
if (a.query) c.or = [{ subject: a.query }, { from: a.query }];
|
|
58328
58329
|
if (a.body) c.body = a.body;
|
|
58329
58330
|
if (a.from) c.from = a.from;
|
|
@@ -58335,7 +58336,6 @@ function buildCriteria(a, listMode) {
|
|
|
58335
58336
|
if (a.isFlagged === false) c.unflagged = true;
|
|
58336
58337
|
if (a.dateFrom) c.since = new Date(a.dateFrom);
|
|
58337
58338
|
if (a.dateTo) c.before = new Date(a.dateTo);
|
|
58338
|
-
if (Object.keys(c).length === 0) c.all = true;
|
|
58339
58339
|
return c;
|
|
58340
58340
|
}
|
|
58341
58341
|
function validDate(d) {
|
|
@@ -58429,44 +58429,139 @@ function messageIdentity(entry) {
|
|
|
58429
58429
|
const messageId = raw.replace(/^<+|>+$/g, "").trim().toLowerCase();
|
|
58430
58430
|
return messageId ? `mid:${messageId}` : `${entry.path}\0${entry.message.uid}`;
|
|
58431
58431
|
}
|
|
58432
|
-
|
|
58433
|
-
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) {
|
|
58434
58437
|
try {
|
|
58435
|
-
const
|
|
58436
|
-
|
|
58437
|
-
|
|
58438
|
-
|
|
58439
|
-
|
|
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) {
|
|
58440
58526
|
throw new Error(
|
|
58441
|
-
`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).`
|
|
58442
58528
|
);
|
|
58443
58529
|
}
|
|
58444
|
-
|
|
58445
|
-
|
|
58446
|
-
|
|
58447
|
-
|
|
58448
|
-
|
|
58449
|
-
|
|
58450
|
-
|
|
58451
|
-
|
|
58452
|
-
|
|
58453
|
-
|
|
58454
|
-
|
|
58455
|
-
|
|
58456
|
-
|
|
58457
|
-
|
|
58458
|
-
|
|
58459
|
-
|
|
58460
|
-
|
|
58461
|
-
|
|
58462
|
-
|
|
58463
|
-
|
|
58464
|
-
|
|
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
|
+
}
|
|
58465
58552
|
}
|
|
58466
|
-
|
|
58467
|
-
|
|
58468
|
-
total: uids.length
|
|
58469
|
-
}
|
|
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 };
|
|
58470
58565
|
} finally {
|
|
58471
58566
|
lock.release();
|
|
58472
58567
|
}
|
|
@@ -58495,15 +58590,17 @@ async function run(args, listMode, deps) {
|
|
|
58495
58590
|
const limit = args.limit ?? 50;
|
|
58496
58591
|
const offset = args.offset ?? 0;
|
|
58497
58592
|
const criteria = buildCriteria(args, listMode);
|
|
58498
|
-
const
|
|
58593
|
+
const page = unscopedSearch ? { skip: 0, take: offset + limit } : { skip: offset, take: limit };
|
|
58499
58594
|
const fetched = [];
|
|
58500
58595
|
const failedMailboxes = [];
|
|
58501
58596
|
const failedMailboxReasons = {};
|
|
58502
58597
|
let totalMatched = 0;
|
|
58598
|
+
let totalExact = true;
|
|
58503
58599
|
for (const path of paths) {
|
|
58504
58600
|
try {
|
|
58505
|
-
const result = await fetchMailboxMatches(client, path, criteria,
|
|
58601
|
+
const result = await fetchMailboxMatches(client, path, criteria, page);
|
|
58506
58602
|
totalMatched += result.total;
|
|
58603
|
+
totalExact &&= result.totalExact;
|
|
58507
58604
|
fetched.push(...result.messages.map((message) => ({ message, path })));
|
|
58508
58605
|
} catch (error) {
|
|
58509
58606
|
failedMailboxes.push(path);
|
|
@@ -58528,8 +58625,6 @@ async function run(args, listMode, deps) {
|
|
|
58528
58625
|
if (!unique.has(key)) unique.set(key, entry);
|
|
58529
58626
|
}
|
|
58530
58627
|
ordered = [...unique.values()].slice(offset, offset + limit);
|
|
58531
|
-
} else {
|
|
58532
|
-
ordered = fetched.slice(offset, offset + limit);
|
|
58533
58628
|
}
|
|
58534
58629
|
const rows = ordered.map(({ message, path }) => formatRow(message, cfg.accountLabel, path));
|
|
58535
58630
|
const messages = ordered.map(
|
|
@@ -58540,6 +58635,7 @@ async function run(args, listMode, deps) {
|
|
|
58540
58635
|
|
|
58541
58636
|
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
|
|
58542
58637
|
const verb = listMode ? "listed" : "matched";
|
|
58638
|
+
const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
|
|
58543
58639
|
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
58544
58640
|
if (messages.length === 0) {
|
|
58545
58641
|
return {
|
|
@@ -58551,7 +58647,7 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
|
|
|
58551
58647
|
failedMailboxReasons
|
|
58552
58648
|
};
|
|
58553
58649
|
}
|
|
58554
|
-
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}):
|
|
58555
58651
|
` + rows.join("\n") + `
|
|
58556
58652
|
|
|
58557
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;
|
|
@@ -58629,7 +58725,10 @@ function imapMailStats(deps = {}) {
|
|
|
58629
58725
|
try {
|
|
58630
58726
|
const lock = await client.getMailboxLock("INBOX");
|
|
58631
58727
|
try {
|
|
58632
|
-
const found = await client.search(
|
|
58728
|
+
const found = await client.search(
|
|
58729
|
+
{ since: since(days), ...NOT_DELETED },
|
|
58730
|
+
{ uid: true }
|
|
58731
|
+
);
|
|
58633
58732
|
return Array.isArray(found) ? found.length : 0;
|
|
58634
58733
|
} finally {
|
|
58635
58734
|
lock.release();
|
|
@@ -59498,11 +59597,23 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59498
59597
|
if (Array.isArray(found)) found.forEach((u) => uidSet.add(u));
|
|
59499
59598
|
};
|
|
59500
59599
|
if (seedMsgId) {
|
|
59501
|
-
addFound(
|
|
59502
|
-
|
|
59600
|
+
addFound(
|
|
59601
|
+
await client.search(
|
|
59602
|
+
{ header: { references: seedMsgId }, ...NOT_DELETED },
|
|
59603
|
+
{ uid: true }
|
|
59604
|
+
)
|
|
59605
|
+
);
|
|
59606
|
+
addFound(
|
|
59607
|
+
await client.search(
|
|
59608
|
+
{ header: { "in-reply-to": seedMsgId }, ...NOT_DELETED },
|
|
59609
|
+
{ uid: true }
|
|
59610
|
+
)
|
|
59611
|
+
);
|
|
59503
59612
|
}
|
|
59504
59613
|
for (const mid of [...refIds].slice(0, 20)) {
|
|
59505
|
-
addFound(
|
|
59614
|
+
addFound(
|
|
59615
|
+
await client.search({ header: { "message-id": mid }, ...NOT_DELETED }, { uid: true })
|
|
59616
|
+
);
|
|
59506
59617
|
}
|
|
59507
59618
|
if (uidSet.size <= 1) return null;
|
|
59508
59619
|
const uids = [...uidSet].slice(0, limit);
|
|
@@ -59554,7 +59665,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
59554
59665
|
true
|
|
59555
59666
|
);
|
|
59556
59667
|
}
|
|
59557
|
-
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, 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;
|
|
59558
59669
|
var init_imapClient = __esm({
|
|
59559
59670
|
"src/services/imapClient.ts"() {
|
|
59560
59671
|
"use strict";
|
|
@@ -59580,7 +59691,24 @@ var init_imapClient = __esm({
|
|
|
59580
59691
|
accounts: "APPLE_MAIL_MCP_IMAP_ACCOUNTS"
|
|
59581
59692
|
};
|
|
59582
59693
|
defaultConnect = async (cfg) => {
|
|
59583
|
-
|
|
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
|
+
});
|
|
59584
59712
|
client.on("error", () => {
|
|
59585
59713
|
});
|
|
59586
59714
|
try {
|
|
@@ -59607,6 +59735,10 @@ var init_imapClient = __esm({
|
|
|
59607
59735
|
junk: "\\junk",
|
|
59608
59736
|
starred: "\\flagged"
|
|
59609
59737
|
};
|
|
59738
|
+
NOT_DELETED = { deleted: false };
|
|
59739
|
+
LARGE_MAILBOX_MESSAGES = 1e4;
|
|
59740
|
+
FIRST_SEARCH_WINDOW = 5e3;
|
|
59741
|
+
MAX_WINDOW = 5e4;
|
|
59610
59742
|
poolConnect = defaultConnect;
|
|
59611
59743
|
pools = /* @__PURE__ */ new Map();
|
|
59612
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,
|
|
@@ -65652,7 +65653,7 @@ async function resolveMailboxPath(client, mailbox, _mode) {
|
|
|
65652
65653
|
return staticMailboxAlias(mailbox);
|
|
65653
65654
|
}
|
|
65654
65655
|
function buildCriteria(a, listMode) {
|
|
65655
|
-
const c = {};
|
|
65656
|
+
const c = { ...NOT_DELETED };
|
|
65656
65657
|
if (a.query) c.or = [{ subject: a.query }, { from: a.query }];
|
|
65657
65658
|
if (a.body) c.body = a.body;
|
|
65658
65659
|
if (a.from) c.from = a.from;
|
|
@@ -65664,7 +65665,6 @@ function buildCriteria(a, listMode) {
|
|
|
65664
65665
|
if (a.isFlagged === false) c.unflagged = true;
|
|
65665
65666
|
if (a.dateFrom) c.since = new Date(a.dateFrom);
|
|
65666
65667
|
if (a.dateTo) c.before = new Date(a.dateTo);
|
|
65667
|
-
if (Object.keys(c).length === 0) c.all = true;
|
|
65668
65668
|
return c;
|
|
65669
65669
|
}
|
|
65670
65670
|
function validDate(d) {
|
|
@@ -65758,44 +65758,139 @@ function messageIdentity(entry) {
|
|
|
65758
65758
|
const messageId = raw.replace(/^<+|>+$/g, "").trim().toLowerCase();
|
|
65759
65759
|
return messageId ? `mid:${messageId}` : `${entry.path}\0${entry.message.uid}`;
|
|
65760
65760
|
}
|
|
65761
|
-
|
|
65762
|
-
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) {
|
|
65763
65766
|
try {
|
|
65764
|
-
const
|
|
65765
|
-
|
|
65766
|
-
|
|
65767
|
-
|
|
65768
|
-
|
|
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) {
|
|
65769
65855
|
throw new Error(
|
|
65770
|
-
`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).`
|
|
65771
65857
|
);
|
|
65772
65858
|
}
|
|
65773
|
-
|
|
65774
|
-
|
|
65775
|
-
|
|
65776
|
-
|
|
65777
|
-
|
|
65778
|
-
|
|
65779
|
-
|
|
65780
|
-
|
|
65781
|
-
|
|
65782
|
-
|
|
65783
|
-
|
|
65784
|
-
|
|
65785
|
-
|
|
65786
|
-
|
|
65787
|
-
|
|
65788
|
-
|
|
65789
|
-
|
|
65790
|
-
|
|
65791
|
-
|
|
65792
|
-
|
|
65793
|
-
|
|
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
|
+
}
|
|
65794
65881
|
}
|
|
65795
|
-
|
|
65796
|
-
|
|
65797
|
-
total: uids.length
|
|
65798
|
-
}
|
|
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 };
|
|
65799
65894
|
} finally {
|
|
65800
65895
|
lock.release();
|
|
65801
65896
|
}
|
|
@@ -65824,15 +65919,17 @@ async function run(args, listMode, deps) {
|
|
|
65824
65919
|
const limit = args.limit ?? 50;
|
|
65825
65920
|
const offset = args.offset ?? 0;
|
|
65826
65921
|
const criteria = buildCriteria(args, listMode);
|
|
65827
|
-
const
|
|
65922
|
+
const page = unscopedSearch ? { skip: 0, take: offset + limit } : { skip: offset, take: limit };
|
|
65828
65923
|
const fetched = [];
|
|
65829
65924
|
const failedMailboxes = [];
|
|
65830
65925
|
const failedMailboxReasons = {};
|
|
65831
65926
|
let totalMatched = 0;
|
|
65927
|
+
let totalExact = true;
|
|
65832
65928
|
for (const path of paths) {
|
|
65833
65929
|
try {
|
|
65834
|
-
const result = await fetchMailboxMatches(client, path, criteria,
|
|
65930
|
+
const result = await fetchMailboxMatches(client, path, criteria, page);
|
|
65835
65931
|
totalMatched += result.total;
|
|
65932
|
+
totalExact &&= result.totalExact;
|
|
65836
65933
|
fetched.push(...result.messages.map((message) => ({ message, path })));
|
|
65837
65934
|
} catch (error2) {
|
|
65838
65935
|
failedMailboxes.push(path);
|
|
@@ -65857,8 +65954,6 @@ async function run(args, listMode, deps) {
|
|
|
65857
65954
|
if (!unique.has(key)) unique.set(key, entry);
|
|
65858
65955
|
}
|
|
65859
65956
|
ordered = [...unique.values()].slice(offset, offset + limit);
|
|
65860
|
-
} else {
|
|
65861
|
-
ordered = fetched.slice(offset, offset + limit);
|
|
65862
65957
|
}
|
|
65863
65958
|
const rows = ordered.map(({ message, path }) => formatRow(message, cfg.accountLabel, path));
|
|
65864
65959
|
const messages = ordered.map(
|
|
@@ -65869,6 +65964,7 @@ async function run(args, listMode, deps) {
|
|
|
65869
65964
|
|
|
65870
65965
|
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
|
|
65871
65966
|
const verb = listMode ? "listed" : "matched";
|
|
65967
|
+
const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
|
|
65872
65968
|
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
65873
65969
|
if (messages.length === 0) {
|
|
65874
65970
|
return {
|
|
@@ -65880,7 +65976,7 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
|
|
|
65880
65976
|
failedMailboxReasons
|
|
65881
65977
|
};
|
|
65882
65978
|
}
|
|
65883
|
-
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}):
|
|
65884
65980
|
` + rows.join("\n") + `
|
|
65885
65981
|
|
|
65886
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;
|
|
@@ -65958,7 +66054,10 @@ function imapMailStats(deps = {}) {
|
|
|
65958
66054
|
try {
|
|
65959
66055
|
const lock = await client.getMailboxLock("INBOX");
|
|
65960
66056
|
try {
|
|
65961
|
-
const found = await client.search(
|
|
66057
|
+
const found = await client.search(
|
|
66058
|
+
{ since: since(days), ...NOT_DELETED },
|
|
66059
|
+
{ uid: true }
|
|
66060
|
+
);
|
|
65962
66061
|
return Array.isArray(found) ? found.length : 0;
|
|
65963
66062
|
} finally {
|
|
65964
66063
|
lock.release();
|
|
@@ -66827,11 +66926,23 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66827
66926
|
if (Array.isArray(found)) found.forEach((u) => uidSet.add(u));
|
|
66828
66927
|
};
|
|
66829
66928
|
if (seedMsgId) {
|
|
66830
|
-
addFound(
|
|
66831
|
-
|
|
66929
|
+
addFound(
|
|
66930
|
+
await client.search(
|
|
66931
|
+
{ header: { references: seedMsgId }, ...NOT_DELETED },
|
|
66932
|
+
{ uid: true }
|
|
66933
|
+
)
|
|
66934
|
+
);
|
|
66935
|
+
addFound(
|
|
66936
|
+
await client.search(
|
|
66937
|
+
{ header: { "in-reply-to": seedMsgId }, ...NOT_DELETED },
|
|
66938
|
+
{ uid: true }
|
|
66939
|
+
)
|
|
66940
|
+
);
|
|
66832
66941
|
}
|
|
66833
66942
|
for (const mid of [...refIds].slice(0, 20)) {
|
|
66834
|
-
addFound(
|
|
66943
|
+
addFound(
|
|
66944
|
+
await client.search({ header: { "message-id": mid }, ...NOT_DELETED }, { uid: true })
|
|
66945
|
+
);
|
|
66835
66946
|
}
|
|
66836
66947
|
if (uidSet.size <= 1) return null;
|
|
66837
66948
|
const uids = [...uidSet].slice(0, limit);
|
|
@@ -66883,7 +66994,7 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
66883
66994
|
true
|
|
66884
66995
|
);
|
|
66885
66996
|
}
|
|
66886
|
-
var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, 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;
|
|
66887
66998
|
var init_imapClient = __esm({
|
|
66888
66999
|
"src/services/imapClient.ts"() {
|
|
66889
67000
|
"use strict";
|
|
@@ -66909,7 +67020,24 @@ var init_imapClient = __esm({
|
|
|
66909
67020
|
accounts: "APPLE_MAIL_MCP_IMAP_ACCOUNTS"
|
|
66910
67021
|
};
|
|
66911
67022
|
defaultConnect = async (cfg) => {
|
|
66912
|
-
|
|
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
|
+
});
|
|
66913
67041
|
client.on("error", () => {
|
|
66914
67042
|
});
|
|
66915
67043
|
try {
|
|
@@ -66936,6 +67064,10 @@ var init_imapClient = __esm({
|
|
|
66936
67064
|
junk: "\\junk",
|
|
66937
67065
|
starred: "\\flagged"
|
|
66938
67066
|
};
|
|
67067
|
+
NOT_DELETED = { deleted: false };
|
|
67068
|
+
LARGE_MAILBOX_MESSAGES = 1e4;
|
|
67069
|
+
FIRST_SEARCH_WINDOW = 5e3;
|
|
67070
|
+
MAX_WINDOW = 5e4;
|
|
66939
67071
|
poolConnect = defaultConnect;
|
|
66940
67072
|
pools = /* @__PURE__ */ new Map();
|
|
66941
67073
|
connecting = /* @__PURE__ */ new Map();
|
package/package.json
CHANGED