apple-mail-mcp 2.19.17 → 2.19.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -280,19 +280,27 @@ human reader; the same information is also returned as structured fields on
280
280
  `search-messages` and `list-messages`, so a caller can tell _"nothing matched"_
281
281
  apart from _"I did not look everywhere"_ without parsing the text:
282
282
 
283
- | Field | Type | Meaning |
284
- | ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
285
- | `partial` | boolean | Coverage was incomplete — **the result is not a confirmed "no such mail"**. True whenever any field below is non-empty. |
286
- | `skippedLargeMailboxes` | string[] | Mailboxes never scanned because their message count exceeded `APPLE_MAIL_MAX_SEARCH_MAILBOX`, formatted `"Account / Mailbox (count)"` — e.g. `"iCloud / Archive (90694)"`. |
287
- | `notSearchedMailboxes` | string[] | Mailboxes that _were_ reached but timed out or errored mid-scan, formatted `"Account / Mailbox"`. Also carries the IMAP path's `failedMailboxes`. |
288
- | `timedOutAccounts` | string[] | Accounts whose whole-account AppleScript was killed by the per-account time budget — nothing from that account was searched. |
289
- | `failedMailboxes` | string[] | IMAP-backend mailboxes that errored. These are merged into `notSearchedMailboxes` as well; read that field unless you need to attribute the failure to the IMAP path specifically. |
283
+ | Field | Type | Meaning |
284
+ | ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
285
+ | `partial` | boolean | Coverage was incomplete — **the result is not a confirmed "no such mail"**. True whenever any field below is non-empty. |
286
+ | `skippedLargeMailboxes` | string[] | Mailboxes never scanned because their message count exceeded `APPLE_MAIL_MAX_SEARCH_MAILBOX`, formatted `"Account / Mailbox (count)"` — e.g. `"iCloud / Archive (90694)"`. |
287
+ | `notSearchedMailboxes` | string[] | Mailboxes that _were_ reached but timed out or errored mid-scan, formatted `"Account / Mailbox"`. Also carries the IMAP path's `failedMailboxes`. |
288
+ | `timedOutAccounts` | string[] | Accounts whose whole-account AppleScript was killed by the per-account time budget — nothing from that account was searched. |
289
+ | `failedMailboxes` | string[] | IMAP-backend mailboxes that errored. These are merged into `notSearchedMailboxes` as well; read that field unless you need to attribute the failure to the IMAP path specifically. |
290
+ | `failedMailboxReasons` | object | The IMAP server's own error text for each entry in `failedMailboxes`, keyed the same way. |
291
+ | `omittedMessages` | object[] | IMAP messages that belong on the page but whose `FETCH` response could not be read even with a reduced item set — `{ id, mailbox, uid, reason }`. The page is short by exactly these; the `id` still works with `get-message` / `get-message-headers`. ([#256](https://github.com/sweetrb/apple-mail-mcp/issues/256)) |
290
292
 
291
293
  Treat a non-empty `skippedLargeMailboxes` as actionable rather than
292
294
  informational: re-run scoped to the named mailbox with a `dateFrom`/`dateTo`
293
295
  window, or configure the [IMAP backend](#imap-backend--opt-in), which searches
294
- those mailboxes server-side. All five fields are optional and are omitted when
295
- coverage was complete.
296
+ those mailboxes server-side. The list fields are optional and are omitted (or
297
+ empty) when coverage was complete.
298
+
299
+ A row the IMAP backend could only read with a reduced item set (its full
300
+ `BODYSTRUCTURE`, or its whole `FETCH` response, was unparseable — e.g. a message
301
+ forwarded as an attachment many levels deep) is still returned, with a
302
+ `metadataIncomplete` note saying what was missing; its `hasAttachments` is then
303
+ inferred from the top-level `Content-Type`.
296
304
 
297
305
  ---
298
306
 
package/build/cli.js CHANGED
@@ -58092,6 +58092,7 @@ __export(imapClient_exports, {
58092
58092
  mailFlagColorIndex: () => mailFlagColorIndex,
58093
58093
  matchMailbox: () => matchMailbox,
58094
58094
  normalizeMessageId: () => normalizeMessageId,
58095
+ omittedNote: () => omittedNote,
58095
58096
  resolveImapConfig: () => resolveImapConfig,
58096
58097
  resolveImapConfigs: () => resolveImapConfigs,
58097
58098
  resolveMailboxPath: () => resolveMailboxPath,
@@ -58402,7 +58403,12 @@ function structuredRow(m, account, path) {
58402
58403
  // a caller from "no attachments", so every IMAP-sourced message claimed to
58403
58404
  // have none. Falls back to false only when the fetch carried no
58404
58405
  // BODYSTRUCTURE at all.
58405
- hasAttachments: bodyStructureHasAttachments(m.bodyStructure),
58406
+ // A row read without BODYSTRUCTURE (#256 follow-up) judges by its top-level
58407
+ // Content-Type instead, and says so in `metadataIncomplete`.
58408
+ hasAttachments: m.bodyStructure ? bodyStructureHasAttachments(m.bodyStructure) : m.degraded ? contentTypeSuggestsAttachments(m) : false,
58409
+ ...m.degraded ? {
58410
+ metadataIncomplete: m.degraded === "bodystructure" ? "BODYSTRUCTURE unreadable; hasAttachments inferred from Content-Type" : "FETCH response unreadable; row rebuilt from raw headers, hasAttachments inferred from Content-Type"
58411
+ } : {},
58406
58412
  // Message-ID (when the envelope carries it) is the strongest cross-/intra-
58407
58413
  // backend dedup key for the multi-account merge (imapMultiAccount.ts). The
58408
58414
  // AppleScript path does not expose it, so cross-backend dedup falls back to
@@ -58410,6 +58416,13 @@ function structuredRow(m, account, path) {
58410
58416
  ...env.messageId ? { messageId: env.messageId } : {}
58411
58417
  };
58412
58418
  }
58419
+ function omittedNote(omitted, merged) {
58420
+ if (omitted.length === 0) return "";
58421
+ const list = omitted.map((o) => `UID ${o.uid} in "${o.mailbox}" (${o.id})`).join(", ");
58422
+ return `
58423
+
58424
+ Partial result. ${omitted.length} message(s) ${merged ? "that may belong" : "that belong"} on this page could not be read and are not listed: ${list}. Reason: ${[...new Set(omitted.map((o) => o.reason))].join("; ")}. get-message or get-message-headers with those ids may still read them.`;
58425
+ }
58413
58426
  function describeMailboxFailure(error) {
58414
58427
  const raw = errText(error);
58415
58428
  const oneLine = raw.split("\n")[0].trim();
@@ -58451,30 +58464,106 @@ async function searchUids(client, path, criteria, size) {
58451
58464
  `IMAP SEARCH on "${path}"${sized} failed: ` + (cause ? errText(cause) : "the server rejected it or the connection dropped before it answered (on a mailbox this size, usually a server-side timeout)")
58452
58465
  );
58453
58466
  }
58467
+ function mergeFetched(into, msg) {
58468
+ const prev = into.get(msg.uid);
58469
+ if (!prev) {
58470
+ into.set(msg.uid, msg);
58471
+ return;
58472
+ }
58473
+ const merged = { ...prev };
58474
+ for (const [key, value] of Object.entries(msg)) {
58475
+ if (value !== void 0 && value !== null)
58476
+ merged[key] = value;
58477
+ }
58478
+ into.set(msg.uid, merged);
58479
+ }
58480
+ async function fetchInto(client, uids, query, into) {
58481
+ const wanted = new Set(uids);
58482
+ for await (const msg of client.fetch(uids.join(","), query, { uid: true })) {
58483
+ if (typeof msg?.uid === "number" && wanted.has(msg.uid)) mergeFetched(into, msg);
58484
+ }
58485
+ }
58486
+ function fetchedHeaders(m) {
58487
+ return m.headers ? parseHeaderBlock(decodeHeaderBytes(asBuffer(m.headers))) : void 0;
58488
+ }
58489
+ function envelopeFromHeaders(m) {
58490
+ const h = fetchedHeaders(m);
58491
+ if (!h || h.headers.length === 0) return void 0;
58492
+ const env = {};
58493
+ if (h.subject) env.subject = h.subject;
58494
+ if (h.from) {
58495
+ const angle = h.from.match(/^\s*"?([^"<]*?)"?\s*<([^>]+)>/);
58496
+ env.from = angle ? [{ ...angle[1].trim() ? { name: angle[1].trim() } : {}, address: angle[2].trim() }] : [{ address: h.from.trim() }];
58497
+ }
58498
+ if (h.messageId) env.messageId = `<${h.messageId}>`;
58499
+ if (h.inReplyTo) env.inReplyTo = `<${h.inReplyTo}>`;
58500
+ if (h.dateHeader) env.date = h.dateHeader;
58501
+ return env;
58502
+ }
58503
+ function contentTypeSuggestsAttachments(m) {
58504
+ const type = fetchedHeaders(m)?.headers.find((h) => h.name.toLowerCase() === "content-type");
58505
+ return /^\s*multipart\/mixed\b/i.test(type?.value ?? "");
58506
+ }
58454
58507
  async function fetchRows(client, uids) {
58455
- if (uids.length === 0) return [];
58508
+ if (uids.length === 0) return { messages: [], omitted: [] };
58456
58509
  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);
58510
+ const causes = [];
58511
+ const noteCause = () => {
58512
+ const err = client.takeLastCommandError?.();
58513
+ if (err !== void 0) {
58514
+ const text = describeMailboxFailure(err);
58515
+ if (!causes.includes(text)) causes.push(text);
58516
+ }
58517
+ };
58518
+ const missing = () => uids.filter((uid) => !byUid.get(uid)?.envelope);
58519
+ client.takeLastCommandError?.();
58520
+ await fetchInto(client, uids, ROW_QUERY, byUid);
58521
+ noteCause();
58522
+ let gap = missing();
58523
+ if (gap.length > 0) {
58524
+ await fetchInto(client, gap, ROW_QUERY, byUid);
58525
+ noteCause();
58526
+ gap = missing();
58527
+ }
58528
+ if (gap.length > 0) {
58529
+ const reduced = /* @__PURE__ */ new Map();
58530
+ await fetchInto(client, gap, ROW_QUERY_NO_STRUCTURE, reduced);
58531
+ noteCause();
58532
+ for (const msg of reduced.values()) {
58533
+ if (!msg.envelope) continue;
58534
+ mergeFetched(byUid, { ...msg, degraded: "bodystructure" });
58535
+ }
58536
+ gap = missing();
58537
+ }
58538
+ if (gap.length > 0) {
58539
+ for (const uid of gap) {
58540
+ const bare = /* @__PURE__ */ new Map();
58541
+ await fetchInto(client, [uid], ROW_QUERY_HEADERS_ONLY, bare);
58542
+ noteCause();
58543
+ const msg = bare.get(uid);
58544
+ const envelope = msg ? envelopeFromHeaders(msg) : void 0;
58545
+ if (msg && envelope) {
58546
+ mergeFetched(byUid, { ...msg, envelope, degraded: "envelope" });
58547
+ }
58548
+ }
58549
+ gap = missing();
58550
+ }
58551
+ const omitted = new Set(gap);
58552
+ return {
58553
+ messages: uids.filter((uid) => !omitted.has(uid)).map((uid) => byUid.get(uid)).filter((message) => message !== void 0),
58554
+ omitted: gap,
58555
+ ...causes.length > 0 ? { cause: causes.join("; ") } : {}
58556
+ };
58557
+ }
58558
+ function pageRows(fetch) {
58559
+ return {
58560
+ messages: fetch.messages,
58561
+ omitted: fetch.omitted,
58562
+ ...fetch.omitted.length > 0 ? { omittedReason: omissionReason(fetch) } : {}
58563
+ };
58564
+ }
58565
+ function omissionReason(fetch) {
58566
+ return "the server's FETCH response for this message could not be read, even without BODYSTRUCTURE or ENVELOPE" + (fetch.cause ? ` (${fetch.cause})` : "");
58478
58567
  }
58479
58568
  async function walkWindows(exists, page, firstWindow, readWindow) {
58480
58569
  const wanted = page.skip + page.take;
@@ -58514,7 +58603,7 @@ async function listLargeMailbox(client, exists, page) {
58514
58603
  }
58515
58604
  );
58516
58605
  return {
58517
- messages: await fetchRows(client, walk.uids),
58606
+ ...pageRows(await fetchRows(client, walk.uids)),
58518
58607
  total: Math.max(0, exists - deletedSeen),
58519
58608
  totalExact: true
58520
58609
  };
@@ -58530,7 +58619,7 @@ async function searchLargeMailbox(client, path, criteria, exists, page) {
58530
58619
  return found;
58531
58620
  });
58532
58621
  return {
58533
- messages: await fetchRows(client, walk.uids),
58622
+ ...pageRows(await fetchRows(client, walk.uids)),
58534
58623
  total: walk.matched,
58535
58624
  totalExact: walk.exhausted
58536
58625
  };
@@ -58539,7 +58628,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
58539
58628
  const lock = await client.getMailboxLock(path);
58540
58629
  try {
58541
58630
  const exists = await statusMessageCount(client, path);
58542
- if (exists === 0) return { messages: [], total: 0, totalExact: true };
58631
+ if (exists === 0) return { messages: [], omitted: [], total: 0, totalExact: true };
58543
58632
  if (exists !== void 0 && exists > LARGE_MAILBOX_MESSAGES) {
58544
58633
  try {
58545
58634
  return isUnfiltered(criteria) ? await listLargeMailbox(client, exists, page) : await searchLargeMailbox(client, path, criteria, exists, page);
@@ -58552,7 +58641,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
58552
58641
  }
58553
58642
  const uids = await searchUids(client, path, criteria, exists);
58554
58643
  if (uids.length === 0 || page.take === 0) {
58555
- return { messages: [], total: uids.length, totalExact: true };
58644
+ return { messages: [], omitted: [], total: uids.length, totalExact: true };
58556
58645
  }
58557
58646
  const messages = exists ?? (await client.status(path, { messages: true })).messages ?? void 0;
58558
58647
  if (typeof messages === "number" && uids.length > messages) {
@@ -58561,7 +58650,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
58561
58650
  );
58562
58651
  }
58563
58652
  const newest = uids.slice().reverse().slice(page.skip, page.skip + page.take);
58564
- return { messages: await fetchRows(client, newest), total: uids.length, totalExact: true };
58653
+ return { ...pageRows(await fetchRows(client, newest)), total: uids.length, totalExact: true };
58565
58654
  } finally {
58566
58655
  lock.release();
58567
58656
  }
@@ -58594,6 +58683,7 @@ async function run(args, listMode, deps) {
58594
58683
  const fetched = [];
58595
58684
  const failedMailboxes = [];
58596
58685
  const failedMailboxReasons = {};
58686
+ const omittedMessages = [];
58597
58687
  let totalMatched = 0;
58598
58688
  let totalExact = true;
58599
58689
  for (const path of paths) {
@@ -58602,6 +58692,14 @@ async function run(args, listMode, deps) {
58602
58692
  totalMatched += result.total;
58603
58693
  totalExact &&= result.totalExact;
58604
58694
  fetched.push(...result.messages.map((message) => ({ message, path })));
58695
+ for (const uid of result.omitted) {
58696
+ omittedMessages.push({
58697
+ id: encodeImapId(cfg.accountLabel, path, uid),
58698
+ mailbox: path,
58699
+ uid,
58700
+ reason: result.omittedReason ?? "the server's FETCH response could not be read"
58701
+ });
58702
+ }
58605
58703
  } catch (error) {
58606
58704
  failedMailboxes.push(path);
58607
58705
  failedMailboxReasons[path] = describeMailboxFailure(error);
@@ -58630,10 +58728,10 @@ async function run(args, listMode, deps) {
58630
58728
  const messages = ordered.map(
58631
58729
  ({ message, path }) => structuredRow(message, cfg.accountLabel, path)
58632
58730
  );
58633
- const partial = failedMailboxes.length > 0;
58634
- const failureNote = partial ? `
58731
+ const partial = failedMailboxes.length > 0 || omittedMessages.length > 0;
58732
+ const failureNote = (failedMailboxes.length > 0 ? `
58635
58733
 
58636
- Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
58734
+ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch);
58637
58735
  const verb = listMode ? "listed" : "matched";
58638
58736
  const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
58639
58737
  const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
@@ -58644,7 +58742,8 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
58644
58742
  count: 0,
58645
58743
  partial,
58646
58744
  failedMailboxes,
58647
- failedMailboxReasons
58745
+ failedMailboxReasons,
58746
+ omittedMessages
58648
58747
  };
58649
58748
  }
58650
58749
  const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalText} ${verb}):
@@ -58657,7 +58756,8 @@ Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutatio
58657
58756
  count: messages.length,
58658
58757
  partial,
58659
58758
  failedMailboxes,
58660
- failedMailboxReasons
58759
+ failedMailboxReasons,
58760
+ omittedMessages
58661
58761
  };
58662
58762
  },
58663
58763
  true
@@ -59617,31 +59717,21 @@ async function imapThread(id, deps = {}, limit = 50) {
59617
59717
  }
59618
59718
  if (uidSet.size <= 1) return null;
59619
59719
  const uids = [...uidSet].slice(0, limit);
59620
- const msgs = [];
59621
- for await (const msg of client.fetch(
59622
- uids.join(","),
59623
- // Same reason as the list/search fetch: get-thread emits structured
59624
- // rows too, so it needs BODYSTRUCTURE or its hasAttachments would
59625
- // silently disagree with the same message seen via search.
59626
- // INTERNALDATE and the Date: header ride along for the same reason as
59627
- // the list/search fetch: the per-message date is recovered and
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
- }
59720
+ const rows = await fetchRows(client, uids);
59721
+ const msgs = rows.messages.slice();
59722
+ const omittedMessages = rows.omitted.map((uid) => ({
59723
+ id: encodeImapId(ref.account, ref.path, uid),
59724
+ mailbox: ref.path,
59725
+ uid,
59726
+ reason: omissionReason(rows)
59727
+ }));
59640
59728
  msgs.sort((a, b) => dateMs(a) - dateMs(b));
59641
59729
  const subject = seed.envelope?.subject || "(no subject)";
59642
59730
  const structured = {
59643
59731
  subject,
59644
59732
  count: msgs.length,
59733
+ partial: omittedMessages.length > 0,
59734
+ omittedMessages,
59645
59735
  messages: msgs.map((m) => ({
59646
59736
  id: encodeImapId(ref.account, ref.path, m.uid),
59647
59737
  subject: m.envelope?.subject || "(no subject)",
@@ -59656,7 +59746,7 @@ async function imapThread(id, deps = {}, limit = 50) {
59656
59746
  }))
59657
59747
  };
59658
59748
  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");
59749
+ ` + msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n") + omittedNote(omittedMessages, false);
59660
59750
  return { count: msgs.length, text, structured };
59661
59751
  } finally {
59662
59752
  lock.release();
@@ -59665,7 +59755,7 @@ async function imapThread(id, deps = {}, limit = 50) {
59665
59755
  true
59666
59756
  );
59667
59757
  }
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;
59758
+ var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
59669
59759
  var init_imapClient = __esm({
59670
59760
  "src/services/imapClient.ts"() {
59671
59761
  "use strict";
@@ -59739,6 +59829,24 @@ var init_imapClient = __esm({
59739
59829
  LARGE_MAILBOX_MESSAGES = 1e4;
59740
59830
  FIRST_SEARCH_WINDOW = 5e3;
59741
59831
  MAX_WINDOW = 5e4;
59832
+ ROW_QUERY = {
59833
+ envelope: true,
59834
+ flags: true,
59835
+ bodyStructure: true,
59836
+ internalDate: true,
59837
+ headers: ["date"]
59838
+ };
59839
+ ROW_QUERY_NO_STRUCTURE = {
59840
+ envelope: true,
59841
+ flags: true,
59842
+ internalDate: true,
59843
+ headers: ["date", "content-type"]
59844
+ };
59845
+ ROW_QUERY_HEADERS_ONLY = {
59846
+ flags: true,
59847
+ internalDate: true,
59848
+ headers: ["date", "from", "subject", "message-id", "in-reply-to", "content-type"]
59849
+ };
59742
59850
  poolConnect = defaultConnect;
59743
59851
  pools = /* @__PURE__ */ new Map();
59744
59852
  connecting = /* @__PURE__ */ new Map();
package/build/index.js CHANGED
@@ -65421,6 +65421,7 @@ __export(imapClient_exports, {
65421
65421
  mailFlagColorIndex: () => mailFlagColorIndex,
65422
65422
  matchMailbox: () => matchMailbox,
65423
65423
  normalizeMessageId: () => normalizeMessageId,
65424
+ omittedNote: () => omittedNote,
65424
65425
  resolveImapConfig: () => resolveImapConfig,
65425
65426
  resolveImapConfigs: () => resolveImapConfigs,
65426
65427
  resolveMailboxPath: () => resolveMailboxPath,
@@ -65731,7 +65732,12 @@ function structuredRow(m, account, path) {
65731
65732
  // a caller from "no attachments", so every IMAP-sourced message claimed to
65732
65733
  // have none. Falls back to false only when the fetch carried no
65733
65734
  // BODYSTRUCTURE at all.
65734
- hasAttachments: bodyStructureHasAttachments(m.bodyStructure),
65735
+ // A row read without BODYSTRUCTURE (#256 follow-up) judges by its top-level
65736
+ // Content-Type instead, and says so in `metadataIncomplete`.
65737
+ hasAttachments: m.bodyStructure ? bodyStructureHasAttachments(m.bodyStructure) : m.degraded ? contentTypeSuggestsAttachments(m) : false,
65738
+ ...m.degraded ? {
65739
+ metadataIncomplete: m.degraded === "bodystructure" ? "BODYSTRUCTURE unreadable; hasAttachments inferred from Content-Type" : "FETCH response unreadable; row rebuilt from raw headers, hasAttachments inferred from Content-Type"
65740
+ } : {},
65735
65741
  // Message-ID (when the envelope carries it) is the strongest cross-/intra-
65736
65742
  // backend dedup key for the multi-account merge (imapMultiAccount.ts). The
65737
65743
  // AppleScript path does not expose it, so cross-backend dedup falls back to
@@ -65739,6 +65745,13 @@ function structuredRow(m, account, path) {
65739
65745
  ...env.messageId ? { messageId: env.messageId } : {}
65740
65746
  };
65741
65747
  }
65748
+ function omittedNote(omitted, merged) {
65749
+ if (omitted.length === 0) return "";
65750
+ const list = omitted.map((o) => `UID ${o.uid} in "${o.mailbox}" (${o.id})`).join(", ");
65751
+ return `
65752
+
65753
+ Partial result. ${omitted.length} message(s) ${merged ? "that may belong" : "that belong"} on this page could not be read and are not listed: ${list}. Reason: ${[...new Set(omitted.map((o) => o.reason))].join("; ")}. get-message or get-message-headers with those ids may still read them.`;
65754
+ }
65742
65755
  function describeMailboxFailure(error2) {
65743
65756
  const raw = errText(error2);
65744
65757
  const oneLine = raw.split("\n")[0].trim();
@@ -65780,30 +65793,106 @@ async function searchUids(client, path, criteria, size) {
65780
65793
  `IMAP SEARCH on "${path}"${sized} failed: ` + (cause ? errText(cause) : "the server rejected it or the connection dropped before it answered (on a mailbox this size, usually a server-side timeout)")
65781
65794
  );
65782
65795
  }
65796
+ function mergeFetched(into, msg) {
65797
+ const prev = into.get(msg.uid);
65798
+ if (!prev) {
65799
+ into.set(msg.uid, msg);
65800
+ return;
65801
+ }
65802
+ const merged = { ...prev };
65803
+ for (const [key, value] of Object.entries(msg)) {
65804
+ if (value !== void 0 && value !== null)
65805
+ merged[key] = value;
65806
+ }
65807
+ into.set(msg.uid, merged);
65808
+ }
65809
+ async function fetchInto(client, uids, query, into) {
65810
+ const wanted = new Set(uids);
65811
+ for await (const msg of client.fetch(uids.join(","), query, { uid: true })) {
65812
+ if (typeof msg?.uid === "number" && wanted.has(msg.uid)) mergeFetched(into, msg);
65813
+ }
65814
+ }
65815
+ function fetchedHeaders(m) {
65816
+ return m.headers ? parseHeaderBlock(decodeHeaderBytes(asBuffer(m.headers))) : void 0;
65817
+ }
65818
+ function envelopeFromHeaders(m) {
65819
+ const h = fetchedHeaders(m);
65820
+ if (!h || h.headers.length === 0) return void 0;
65821
+ const env = {};
65822
+ if (h.subject) env.subject = h.subject;
65823
+ if (h.from) {
65824
+ const angle = h.from.match(/^\s*"?([^"<]*?)"?\s*<([^>]+)>/);
65825
+ env.from = angle ? [{ ...angle[1].trim() ? { name: angle[1].trim() } : {}, address: angle[2].trim() }] : [{ address: h.from.trim() }];
65826
+ }
65827
+ if (h.messageId) env.messageId = `<${h.messageId}>`;
65828
+ if (h.inReplyTo) env.inReplyTo = `<${h.inReplyTo}>`;
65829
+ if (h.dateHeader) env.date = h.dateHeader;
65830
+ return env;
65831
+ }
65832
+ function contentTypeSuggestsAttachments(m) {
65833
+ const type = fetchedHeaders(m)?.headers.find((h) => h.name.toLowerCase() === "content-type");
65834
+ return /^\s*multipart\/mixed\b/i.test(type?.value ?? "");
65835
+ }
65783
65836
  async function fetchRows(client, uids) {
65784
- if (uids.length === 0) return [];
65837
+ if (uids.length === 0) return { messages: [], omitted: [] };
65785
65838
  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);
65839
+ const causes = [];
65840
+ const noteCause = () => {
65841
+ const err = client.takeLastCommandError?.();
65842
+ if (err !== void 0) {
65843
+ const text = describeMailboxFailure(err);
65844
+ if (!causes.includes(text)) causes.push(text);
65845
+ }
65846
+ };
65847
+ const missing = () => uids.filter((uid) => !byUid.get(uid)?.envelope);
65848
+ client.takeLastCommandError?.();
65849
+ await fetchInto(client, uids, ROW_QUERY, byUid);
65850
+ noteCause();
65851
+ let gap = missing();
65852
+ if (gap.length > 0) {
65853
+ await fetchInto(client, gap, ROW_QUERY, byUid);
65854
+ noteCause();
65855
+ gap = missing();
65856
+ }
65857
+ if (gap.length > 0) {
65858
+ const reduced = /* @__PURE__ */ new Map();
65859
+ await fetchInto(client, gap, ROW_QUERY_NO_STRUCTURE, reduced);
65860
+ noteCause();
65861
+ for (const msg of reduced.values()) {
65862
+ if (!msg.envelope) continue;
65863
+ mergeFetched(byUid, { ...msg, degraded: "bodystructure" });
65864
+ }
65865
+ gap = missing();
65866
+ }
65867
+ if (gap.length > 0) {
65868
+ for (const uid of gap) {
65869
+ const bare = /* @__PURE__ */ new Map();
65870
+ await fetchInto(client, [uid], ROW_QUERY_HEADERS_ONLY, bare);
65871
+ noteCause();
65872
+ const msg = bare.get(uid);
65873
+ const envelope = msg ? envelopeFromHeaders(msg) : void 0;
65874
+ if (msg && envelope) {
65875
+ mergeFetched(byUid, { ...msg, envelope, degraded: "envelope" });
65876
+ }
65877
+ }
65878
+ gap = missing();
65879
+ }
65880
+ const omitted = new Set(gap);
65881
+ return {
65882
+ messages: uids.filter((uid) => !omitted.has(uid)).map((uid) => byUid.get(uid)).filter((message) => message !== void 0),
65883
+ omitted: gap,
65884
+ ...causes.length > 0 ? { cause: causes.join("; ") } : {}
65885
+ };
65886
+ }
65887
+ function pageRows(fetch) {
65888
+ return {
65889
+ messages: fetch.messages,
65890
+ omitted: fetch.omitted,
65891
+ ...fetch.omitted.length > 0 ? { omittedReason: omissionReason(fetch) } : {}
65892
+ };
65893
+ }
65894
+ function omissionReason(fetch) {
65895
+ return "the server's FETCH response for this message could not be read, even without BODYSTRUCTURE or ENVELOPE" + (fetch.cause ? ` (${fetch.cause})` : "");
65807
65896
  }
65808
65897
  async function walkWindows(exists, page, firstWindow, readWindow) {
65809
65898
  const wanted = page.skip + page.take;
@@ -65843,7 +65932,7 @@ async function listLargeMailbox(client, exists, page) {
65843
65932
  }
65844
65933
  );
65845
65934
  return {
65846
- messages: await fetchRows(client, walk.uids),
65935
+ ...pageRows(await fetchRows(client, walk.uids)),
65847
65936
  total: Math.max(0, exists - deletedSeen),
65848
65937
  totalExact: true
65849
65938
  };
@@ -65859,7 +65948,7 @@ async function searchLargeMailbox(client, path, criteria, exists, page) {
65859
65948
  return found;
65860
65949
  });
65861
65950
  return {
65862
- messages: await fetchRows(client, walk.uids),
65951
+ ...pageRows(await fetchRows(client, walk.uids)),
65863
65952
  total: walk.matched,
65864
65953
  totalExact: walk.exhausted
65865
65954
  };
@@ -65868,7 +65957,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
65868
65957
  const lock = await client.getMailboxLock(path);
65869
65958
  try {
65870
65959
  const exists = await statusMessageCount(client, path);
65871
- if (exists === 0) return { messages: [], total: 0, totalExact: true };
65960
+ if (exists === 0) return { messages: [], omitted: [], total: 0, totalExact: true };
65872
65961
  if (exists !== void 0 && exists > LARGE_MAILBOX_MESSAGES) {
65873
65962
  try {
65874
65963
  return isUnfiltered(criteria) ? await listLargeMailbox(client, exists, page) : await searchLargeMailbox(client, path, criteria, exists, page);
@@ -65881,7 +65970,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
65881
65970
  }
65882
65971
  const uids = await searchUids(client, path, criteria, exists);
65883
65972
  if (uids.length === 0 || page.take === 0) {
65884
- return { messages: [], total: uids.length, totalExact: true };
65973
+ return { messages: [], omitted: [], total: uids.length, totalExact: true };
65885
65974
  }
65886
65975
  const messages = exists ?? (await client.status(path, { messages: true })).messages ?? void 0;
65887
65976
  if (typeof messages === "number" && uids.length > messages) {
@@ -65890,7 +65979,7 @@ async function fetchMailboxMatches(client, path, criteria, page) {
65890
65979
  );
65891
65980
  }
65892
65981
  const newest = uids.slice().reverse().slice(page.skip, page.skip + page.take);
65893
- return { messages: await fetchRows(client, newest), total: uids.length, totalExact: true };
65982
+ return { ...pageRows(await fetchRows(client, newest)), total: uids.length, totalExact: true };
65894
65983
  } finally {
65895
65984
  lock.release();
65896
65985
  }
@@ -65923,6 +66012,7 @@ async function run(args, listMode, deps) {
65923
66012
  const fetched = [];
65924
66013
  const failedMailboxes = [];
65925
66014
  const failedMailboxReasons = {};
66015
+ const omittedMessages = [];
65926
66016
  let totalMatched = 0;
65927
66017
  let totalExact = true;
65928
66018
  for (const path of paths) {
@@ -65931,6 +66021,14 @@ async function run(args, listMode, deps) {
65931
66021
  totalMatched += result.total;
65932
66022
  totalExact &&= result.totalExact;
65933
66023
  fetched.push(...result.messages.map((message) => ({ message, path })));
66024
+ for (const uid of result.omitted) {
66025
+ omittedMessages.push({
66026
+ id: encodeImapId(cfg.accountLabel, path, uid),
66027
+ mailbox: path,
66028
+ uid,
66029
+ reason: result.omittedReason ?? "the server's FETCH response could not be read"
66030
+ });
66031
+ }
65934
66032
  } catch (error2) {
65935
66033
  failedMailboxes.push(path);
65936
66034
  failedMailboxReasons[path] = describeMailboxFailure(error2);
@@ -65959,10 +66057,10 @@ async function run(args, listMode, deps) {
65959
66057
  const messages = ordered.map(
65960
66058
  ({ message, path }) => structuredRow(message, cfg.accountLabel, path)
65961
66059
  );
65962
- const partial2 = failedMailboxes.length > 0;
65963
- const failureNote = partial2 ? `
66060
+ const partial2 = failedMailboxes.length > 0 || omittedMessages.length > 0;
66061
+ const failureNote = (failedMailboxes.length > 0 ? `
65964
66062
 
65965
- Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "";
66063
+ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"${path}" (${failedMailboxReasons[path]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch);
65966
66064
  const verb = listMode ? "listed" : "matched";
65967
66065
  const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
65968
66066
  const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
@@ -65973,7 +66071,8 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path) => `"
65973
66071
  count: 0,
65974
66072
  partial: partial2,
65975
66073
  failedMailboxes,
65976
- failedMailboxReasons
66074
+ failedMailboxReasons,
66075
+ omittedMessages
65977
66076
  };
65978
66077
  }
65979
66078
  const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalText} ${verb}):
@@ -65986,7 +66085,8 @@ Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutatio
65986
66085
  count: messages.length,
65987
66086
  partial: partial2,
65988
66087
  failedMailboxes,
65989
- failedMailboxReasons
66088
+ failedMailboxReasons,
66089
+ omittedMessages
65990
66090
  };
65991
66091
  },
65992
66092
  true
@@ -66946,31 +67046,21 @@ async function imapThread(id, deps = {}, limit = 50) {
66946
67046
  }
66947
67047
  if (uidSet.size <= 1) return null;
66948
67048
  const uids = [...uidSet].slice(0, limit);
66949
- const msgs = [];
66950
- for await (const msg of client.fetch(
66951
- uids.join(","),
66952
- // Same reason as the list/search fetch: get-thread emits structured
66953
- // rows too, so it needs BODYSTRUCTURE or its hasAttachments would
66954
- // silently disagree with the same message seen via search.
66955
- // INTERNALDATE and the Date: header ride along for the same reason as
66956
- // the list/search fetch: the per-message date is recovered and
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
- }
67049
+ const rows = await fetchRows(client, uids);
67050
+ const msgs = rows.messages.slice();
67051
+ const omittedMessages = rows.omitted.map((uid) => ({
67052
+ id: encodeImapId(ref.account, ref.path, uid),
67053
+ mailbox: ref.path,
67054
+ uid,
67055
+ reason: omissionReason(rows)
67056
+ }));
66969
67057
  msgs.sort((a, b) => dateMs(a) - dateMs(b));
66970
67058
  const subject = seed.envelope?.subject || "(no subject)";
66971
67059
  const structured = {
66972
67060
  subject,
66973
67061
  count: msgs.length,
67062
+ partial: omittedMessages.length > 0,
67063
+ omittedMessages,
66974
67064
  messages: msgs.map((m) => ({
66975
67065
  id: encodeImapId(ref.account, ref.path, m.uid),
66976
67066
  subject: m.envelope?.subject || "(no subject)",
@@ -66985,7 +67075,7 @@ async function imapThread(id, deps = {}, limit = 50) {
66985
67075
  }))
66986
67076
  };
66987
67077
  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");
67078
+ ` + msgs.map((m) => formatRow(m, ref.account, ref.path)).join("\n") + omittedNote(omittedMessages, false);
66989
67079
  return { count: msgs.length, text, structured };
66990
67080
  } finally {
66991
67081
  lock.release();
@@ -66994,7 +67084,7 @@ async function imapThread(id, deps = {}, limit = 50) {
66994
67084
  true
66995
67085
  );
66996
67086
  }
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;
67087
+ var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
66998
67088
  var init_imapClient = __esm({
66999
67089
  "src/services/imapClient.ts"() {
67000
67090
  "use strict";
@@ -67068,6 +67158,24 @@ var init_imapClient = __esm({
67068
67158
  LARGE_MAILBOX_MESSAGES = 1e4;
67069
67159
  FIRST_SEARCH_WINDOW = 5e3;
67070
67160
  MAX_WINDOW = 5e4;
67161
+ ROW_QUERY = {
67162
+ envelope: true,
67163
+ flags: true,
67164
+ bodyStructure: true,
67165
+ internalDate: true,
67166
+ headers: ["date"]
67167
+ };
67168
+ ROW_QUERY_NO_STRUCTURE = {
67169
+ envelope: true,
67170
+ flags: true,
67171
+ internalDate: true,
67172
+ headers: ["date", "content-type"]
67173
+ };
67174
+ ROW_QUERY_HEADERS_ONLY = {
67175
+ flags: true,
67176
+ internalDate: true,
67177
+ headers: ["date", "from", "subject", "message-id", "in-reply-to", "content-type"]
67178
+ };
67071
67179
  poolConnect = defaultConnect;
67072
67180
  pools = /* @__PURE__ */ new Map();
67073
67181
  connecting = /* @__PURE__ */ new Map();
@@ -87569,6 +87677,7 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
87569
87677
  const accountsFailed = [];
87570
87678
  const failedMailboxes = [];
87571
87679
  const failedMailboxReasons = {};
87680
+ const omittedMessages = [];
87572
87681
  for (const config2 of configs) {
87573
87682
  const perAccountArgs = { ...args, account: void 0 };
87574
87683
  try {
@@ -87581,12 +87690,25 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
87581
87690
  for (const [mailbox, reason] of Object.entries(res.failedMailboxReasons)) {
87582
87691
  failedMailboxReasons[`${config2.accountLabel} / ${mailbox}`] = reason;
87583
87692
  }
87693
+ omittedMessages.push(
87694
+ ...(res.omittedMessages ?? []).map((o) => ({
87695
+ ...o,
87696
+ mailbox: `${config2.accountLabel} / ${o.mailbox}`
87697
+ }))
87698
+ );
87584
87699
  } catch (e) {
87585
87700
  accountsFailed.push(config2.accountLabel);
87586
87701
  console.error(`IMAP fan-out failed for account "${config2.accountLabel}": ${String(e)}`);
87587
87702
  }
87588
87703
  }
87589
- return { rows, accountsQueried, accountsFailed, failedMailboxes, failedMailboxReasons };
87704
+ return {
87705
+ rows,
87706
+ accountsQueried,
87707
+ accountsFailed,
87708
+ failedMailboxes,
87709
+ failedMailboxReasons,
87710
+ omittedMessages
87711
+ };
87590
87712
  }
87591
87713
  function configMatchesAccount(config2, account) {
87592
87714
  const name = account.name.trim().toLowerCase();
@@ -88189,7 +88311,10 @@ var LIST_OUTPUT_SCHEMA = {
88189
88311
  failedMailboxes: external_exports.array(external_exports.string()).optional(),
88190
88312
  // Underlying error text per entry in `failedMailboxes`, same keys (#246
88191
88313
  // 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()
88314
+ failedMailboxReasons: external_exports.record(external_exports.string(), external_exports.string()).optional(),
88315
+ // Messages that belong on the page but could not be read, each with its
88316
+ // imap: id and why (#256 follow-up). Non-empty implies `partial: true`.
88317
+ omittedMessages: external_exports.array(external_exports.object({ id: external_exports.string(), mailbox: external_exports.string(), uid: external_exports.number(), reason: external_exports.string() })).optional()
88193
88318
  };
88194
88319
  var BATCH_COUNT_OUTPUT_SCHEMA = {
88195
88320
  ok: external_exports.boolean().optional(),
@@ -88268,7 +88393,7 @@ function mergedMessageResponse(fan, apple, limit, verb) {
88268
88393
  const merged = mergeMessages(fan.rows, apple.rows, limit);
88269
88394
  const diagnostics = {
88270
88395
  ...apple.diagnostics,
88271
- partial: apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0,
88396
+ partial: apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0 || fan.omittedMessages.length > 0,
88272
88397
  timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
88273
88398
  notSearchedMailboxes: [...apple.diagnostics.notSearchedMailboxes, ...fan.failedMailboxes]
88274
88399
  };
@@ -88280,9 +88405,10 @@ function mergedMessageResponse(fan, apple, limit, verb) {
88280
88405
  notSearchedMailboxes: diagnostics.notSearchedMailboxes,
88281
88406
  timedOutAccounts: diagnostics.timedOutAccounts,
88282
88407
  failedMailboxes: fan.failedMailboxes,
88283
- failedMailboxReasons: fan.failedMailboxReasons
88408
+ failedMailboxReasons: fan.failedMailboxReasons,
88409
+ omittedMessages: fan.omittedMessages
88284
88410
  };
88285
- const coverageBlock = partialCoverageBlock(diagnostics);
88411
+ const coverageBlock = partialCoverageBlock(diagnostics) + omittedNote(fan.omittedMessages, true);
88286
88412
  if (merged.length === 0) {
88287
88413
  const base = diagnostics.partial ? `No messages found in the portions that were ${verb === "matched" ? "searched" : "listed"}.` : "No messages found";
88288
88414
  return successResponse(`${base}${coverageBlock}`, structured);
@@ -88383,7 +88509,8 @@ registerTool(
88383
88509
  count: r.count,
88384
88510
  partial: r.partial,
88385
88511
  failedMailboxes: r.failedMailboxes,
88386
- failedMailboxReasons: r.failedMailboxReasons
88512
+ failedMailboxReasons: r.failedMailboxReasons,
88513
+ omittedMessages: r.omittedMessages
88387
88514
  });
88388
88515
  }
88389
88516
  const fan = await fanOutImapMessages(imapArgs, "search");
@@ -88737,7 +88864,9 @@ registerTool(
88737
88864
  messages: external_exports.array(MESSAGE_ROW_SCHEMA).optional(),
88738
88865
  count: external_exports.number().optional(),
88739
88866
  partial: external_exports.boolean().optional(),
88740
- failedMailboxes: external_exports.array(external_exports.string()).optional()
88867
+ failedMailboxes: external_exports.array(external_exports.string()).optional(),
88868
+ failedMailboxReasons: LIST_OUTPUT_SCHEMA.failedMailboxReasons,
88869
+ omittedMessages: LIST_OUTPUT_SCHEMA.omittedMessages
88741
88870
  }
88742
88871
  },
88743
88872
  withErrorHandling(async ({ id, account, mailbox, limit = 50 }) => {
@@ -88767,7 +88896,8 @@ ${r.text}`, {
88767
88896
  count: r.count,
88768
88897
  partial: r.partial,
88769
88898
  failedMailboxes: r.failedMailboxes,
88770
- failedMailboxReasons: r.failedMailboxReasons
88899
+ failedMailboxReasons: r.failedMailboxReasons,
88900
+ omittedMessages: r.omittedMessages
88771
88901
  });
88772
88902
  }
88773
88903
  const fan = await fanOutImapMessages({ subject: base, mailbox, limit }, "search");
@@ -88792,20 +88922,21 @@ ${r.text}`, {
88792
88922
  const orderedRows = mergedNewestFirst.slice().reverse().sort(
88793
88923
  (a, b) => (a.dateReceived ? new Date(a.dateReceived).getTime() : 0) - (b.dateReceived ? new Date(b.dateReceived).getTime() : 0)
88794
88924
  );
88795
- const partial2 = apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0;
88925
+ const partial2 = apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0 || fan.omittedMessages.length > 0;
88796
88926
  const coverage = partialCoverageBlock({
88797
88927
  ...apple.diagnostics,
88798
88928
  partial: partial2,
88799
88929
  timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
88800
88930
  notSearchedMailboxes: [...apple.diagnostics.notSearchedMailboxes, ...fan.failedMailboxes]
88801
- });
88931
+ }) + omittedNote(fan.omittedMessages, true);
88802
88932
  const structured2 = {
88803
88933
  subject: base,
88804
88934
  messages: orderedRows,
88805
88935
  count: orderedRows.length,
88806
88936
  partial: partial2,
88807
88937
  failedMailboxes: fan.failedMailboxes,
88808
- failedMailboxReasons: fan.failedMailboxReasons
88938
+ failedMailboxReasons: fan.failedMailboxReasons,
88939
+ omittedMessages: fan.omittedMessages
88809
88940
  };
88810
88941
  if (orderedRows.length === 0) {
88811
88942
  return successResponse(`No messages found in thread "${base}".${coverage}`, structured2);
@@ -88870,7 +89001,8 @@ registerTool(
88870
89001
  count: r.count,
88871
89002
  partial: r.partial,
88872
89003
  failedMailboxes: r.failedMailboxes,
88873
- failedMailboxReasons: r.failedMailboxReasons
89004
+ failedMailboxReasons: r.failedMailboxReasons,
89005
+ omittedMessages: r.omittedMessages
88874
89006
  });
88875
89007
  }
88876
89008
  const fan = await fanOutImapMessages({ mailbox, limit, offset, from, unreadOnly }, "list");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.19.17",
3
+ "version": "2.19.18",
4
4
  "description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude and other AI assistants",
5
5
  "type": "module",
6
6
  "main": "build/index.js",