apple-mail-mcp 2.19.18 → 2.19.20

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
@@ -240,6 +240,7 @@ Search for messages matching criteria. Searches all accounts by default.
240
240
  | `dateFrom` | string | No | Start date filter (e.g., "January 1, 2026") |
241
241
  | `dateTo` | string | No | End date filter (e.g., "March 1, 2026") |
242
242
  | `limit` | number | No | Max results, 1–500 (default: 50) |
243
+ | `offset` | number | No | Newest matches to skip, ≥ 0 (for pagination). IMAP accounts only; without a `mailbox`, `offset` + `limit` may not exceed 5,000 |
243
244
 
244
245
  **Returns:** List of matching messages with ID, date, subject, sender, and read state.
245
246
 
@@ -1103,14 +1104,23 @@ which made a filter-less `list-messages` fail on such a mailbox before 2.19.16.
1103
1104
  **Very large IMAP mailboxes are read newest-first in bounded windows**
1104
1105
  ([#256](https://github.com/sweetrb/apple-mail-mcp/issues/256)). Above 10,000
1105
1106
  messages, a filter-less `list-messages`/`search-messages` pages by message
1106
- sequence number from the top of the mailbox — it fetches UIDs and flags for
1107
- just enough of the newest messages to fill `limit` + `offset` (skipping
1108
- `\Deleted` ones) and full rows for the page alone, so `limit: 1` on a
1109
- 793,614-message mailbox costs one small `FETCH`, not a whole-mailbox `SEARCH`.
1107
+ sequence number: page `offset` of `limit` is sequence numbers
1108
+ `N-offset-limit+1 .. N-offset`, so it fetches UIDs and flags for that range
1109
+ alone (plus a small margin; `\Deleted` ones are skipped and the page topped up
1110
+ from below) and full rows for the page. The cost is the page, not the offset:
1111
+ `limit: 1` at offset 790,000 of a 793,614-message mailbox is one ~33-message
1112
+ `FETCH`, not a whole-mailbox `SEARCH` or a walk over every skipped message.
1113
+ Offsets count sequence positions. iCloud keeps messages awaiting expunge out of
1114
+ the sequence space, so there they are exact; on a server that keeps them in it,
1115
+ a page shifts by the number of such messages above it (they are never
1116
+ returned).
1110
1117
  A filtered search there runs the same criteria over newest-first sequence
1111
1118
  windows (5,000 messages, growing to 50,000) and stops once the page is full;
1112
1119
  the reported total then reads `at least N` unless the walk reached the bottom
1113
- of the mailbox. A `SEARCH` or `FETCH` that fails names the mailbox, its size and
1120
+ of the mailbox. A filtered search's deep `offset` has no such shortcut — which
1121
+ messages match is only known by searching the skipped windows — so its cost
1122
+ still grows with the offset (in 50,000-message `SEARCH` windows); narrow it
1123
+ with `dateTo` instead. A `SEARCH` or `FETCH` that fails names the mailbox, its size and
1114
1124
  the server's own reason (or a likely timeout) instead of reporting "no
1115
1125
  messages".
1116
1126
 
package/build/cli.js CHANGED
@@ -58049,6 +58049,7 @@ __export(imapClient_exports, {
58049
58049
  MAX_COMPOSE_SOURCE_BYTES: () => MAX_COMPOSE_SOURCE_BYTES,
58050
58050
  MAX_RFC822_FILE_BYTES: () => MAX_RFC822_FILE_BYTES,
58051
58051
  MAX_RFC822_INLINE_BYTES: () => MAX_RFC822_INLINE_BYTES,
58052
+ MAX_UNSCOPED_SEARCH_DEPTH: () => MAX_UNSCOPED_SEARCH_DEPTH,
58052
58053
  __resetPool: () => __resetPool,
58053
58054
  __setPoolConnect: () => __setPoolConnect,
58054
58055
  bodyStructureHasAttachments: () => bodyStructureHasAttachments,
@@ -58096,7 +58097,8 @@ __export(imapClient_exports, {
58096
58097
  resolveImapConfig: () => resolveImapConfig,
58097
58098
  resolveImapConfigs: () => resolveImapConfigs,
58098
58099
  resolveMailboxPath: () => resolveMailboxPath,
58099
- shouldUseImap: () => shouldUseImap
58100
+ shouldUseImap: () => shouldUseImap,
58101
+ unscopedSearchOffsetError: () => unscopedSearchOffsetError
58100
58102
  });
58101
58103
  import { createHash } from "node:crypto";
58102
58104
  function encodeImapId(account, path, uid) {
@@ -58442,6 +58444,10 @@ function messageIdentity(entry) {
58442
58444
  const messageId = raw.replace(/^<+|>+$/g, "").trim().toLowerCase();
58443
58445
  return messageId ? `mid:${messageId}` : `${entry.path}\0${entry.message.uid}`;
58444
58446
  }
58447
+ function unscopedSearchOffsetError(offset, limit) {
58448
+ if (offset <= 0 || offset + limit <= MAX_UNSCOPED_SEARCH_DEPTH) return void 0;
58449
+ return `search-messages without a mailbox merges every mailbox's newest offset+limit matches, so it pages at most ${MAX_UNSCOPED_SEARCH_DEPTH.toLocaleString("en-US")} deep (offset ${offset} + limit ${limit} asked). Name a mailbox (e.g. the account's All Mail or Archive) to page deeper, or narrow the search with dateTo.`;
58450
+ }
58445
58451
  function isUnfiltered(criteria) {
58446
58452
  const keys = Object.keys(criteria);
58447
58453
  return keys.length === 1 && criteria.deleted === false;
@@ -58565,16 +58571,16 @@ function pageRows(fetch) {
58565
58571
  function omissionReason(fetch) {
58566
58572
  return "the server's FETCH response for this message could not be read, even without BODYSTRUCTURE or ENVELOPE" + (fetch.cause ? ` (${fetch.cause})` : "");
58567
58573
  }
58568
- async function walkWindows(exists, page, firstWindow, readWindow) {
58574
+ async function walkWindows(top, page, firstWindow, readWindow, openTop = true) {
58569
58575
  const wanted = page.skip + page.take;
58570
58576
  const uids = [];
58571
58577
  let seen = 0;
58572
58578
  let matched = 0;
58573
- let hi = exists;
58579
+ let hi = top;
58574
58580
  let width = Math.max(1, Math.min(firstWindow, MAX_WINDOW));
58575
58581
  while (hi >= 1 && seen < wanted) {
58576
58582
  const lo = Math.max(1, hi - width + 1);
58577
- const live = (await readWindow(lo, hi === exists ? "*" : hi, hi - lo + 1)).slice().sort((a, b) => b - a);
58583
+ const live = (await readWindow(lo, hi === top && openTop ? "*" : hi, hi - lo + 1)).slice().sort((a, b) => b - a);
58578
58584
  matched += live.length;
58579
58585
  for (const uid of live) {
58580
58586
  if (seen >= wanted) break;
@@ -58586,13 +58592,21 @@ async function walkWindows(exists, page, firstWindow, readWindow) {
58586
58592
  }
58587
58593
  return { uids, matched, exhausted: hi < 1 };
58588
58594
  }
58595
+ function sequenceTop(client, statusCount) {
58596
+ const mb = client.mailbox;
58597
+ const exists = mb && typeof mb.exists === "number" && mb.exists >= 0 ? mb.exists : void 0;
58598
+ return exists ?? statusCount;
58599
+ }
58589
58600
  async function listLargeMailbox(client, exists, page) {
58601
+ const top = sequenceTop(client, exists) - page.skip;
58602
+ if (top < 1 || page.take === 0) {
58603
+ return { messages: [], omitted: [], total: exists, totalExact: true };
58604
+ }
58590
58605
  let deletedSeen = 0;
58591
58606
  const walk = await walkWindows(
58592
- exists,
58593
- page,
58594
- // Room for a few ghosts on the first read without a second round trip.
58595
- page.skip + page.take + 64,
58607
+ top,
58608
+ { skip: 0, take: page.take },
58609
+ page.take + DELETED_MARGIN,
58596
58610
  async (lo, hi) => {
58597
58611
  const live = [];
58598
58612
  for await (const msg of client.fetch(`${lo}:${hi}`, { uid: true, flags: true })) {
@@ -58600,7 +58614,10 @@ async function listLargeMailbox(client, exists, page) {
58600
58614
  else live.push(msg.uid);
58601
58615
  }
58602
58616
  return live;
58603
- }
58617
+ },
58618
+ // From the very top the first window is `lo:*`, as before; below it the
58619
+ // range is exact.
58620
+ page.skip === 0
58604
58621
  );
58605
58622
  return {
58606
58623
  ...pageRows(await fetchRows(client, walk.uids)),
@@ -58678,6 +58695,10 @@ async function run(args, listMode, deps) {
58678
58695
  }
58679
58696
  const limit = args.limit ?? 50;
58680
58697
  const offset = args.offset ?? 0;
58698
+ if (unscopedSearch) {
58699
+ const tooDeep = unscopedSearchOffsetError(offset, limit);
58700
+ if (tooDeep) throw new Error(tooDeep);
58701
+ }
58681
58702
  const criteria = buildCriteria(args, listMode);
58682
58703
  const page = unscopedSearch ? { skip: 0, take: offset + limit } : { skip: offset, take: limit };
58683
58704
  const fetched = [];
@@ -59755,7 +59776,7 @@ async function imapThread(id, deps = {}, limit = 50) {
59755
59776
  true
59756
59777
  );
59757
59778
  }
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;
59779
+ var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, MAX_UNSCOPED_SEARCH_DEPTH, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, DELETED_MARGIN, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
59759
59780
  var init_imapClient = __esm({
59760
59781
  "src/services/imapClient.ts"() {
59761
59782
  "use strict";
@@ -59829,6 +59850,7 @@ var init_imapClient = __esm({
59829
59850
  LARGE_MAILBOX_MESSAGES = 1e4;
59830
59851
  FIRST_SEARCH_WINDOW = 5e3;
59831
59852
  MAX_WINDOW = 5e4;
59853
+ MAX_UNSCOPED_SEARCH_DEPTH = 5e3;
59832
59854
  ROW_QUERY = {
59833
59855
  envelope: true,
59834
59856
  flags: true,
@@ -59847,6 +59869,7 @@ var init_imapClient = __esm({
59847
59869
  internalDate: true,
59848
59870
  headers: ["date", "from", "subject", "message-id", "in-reply-to", "content-type"]
59849
59871
  };
59872
+ DELETED_MARGIN = 32;
59850
59873
  poolConnect = defaultConnect;
59851
59874
  pools = /* @__PURE__ */ new Map();
59852
59875
  connecting = /* @__PURE__ */ new Map();
package/build/index.js CHANGED
@@ -3118,12 +3118,13 @@ var require_data = __commonJS({
3118
3118
  }
3119
3119
  });
3120
3120
 
3121
- // node_modules/.pnpm/fast-uri@3.1.6/node_modules/fast-uri/lib/utils.js
3121
+ // node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/lib/utils.js
3122
3122
  var require_utils = __commonJS({
3123
- "node_modules/.pnpm/fast-uri@3.1.6/node_modules/fast-uri/lib/utils.js"(exports, module) {
3123
+ "node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/lib/utils.js"(exports, module) {
3124
3124
  "use strict";
3125
3125
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3126
3126
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
3127
+ var isPort = RegExp.prototype.test.bind(/^\d*$/u);
3127
3128
  var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
3128
3129
  var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
3129
3130
  var isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u);
@@ -3589,8 +3590,12 @@ var require_utils = __commonJS({
3589
3590
  uriTokens.push(host);
3590
3591
  }
3591
3592
  if (typeof component.port === "number" || typeof component.port === "string") {
3593
+ const port = String(component.port);
3594
+ if (!isPort(port)) {
3595
+ throw new TypeError("URI port is malformed.");
3596
+ }
3592
3597
  uriTokens.push(":");
3593
- uriTokens.push(String(component.port));
3598
+ uriTokens.push(port);
3594
3599
  }
3595
3600
  return uriTokens.length ? uriTokens.join("") : void 0;
3596
3601
  }
@@ -3615,9 +3620,9 @@ var require_utils = __commonJS({
3615
3620
  }
3616
3621
  });
3617
3622
 
3618
- // node_modules/.pnpm/fast-uri@3.1.6/node_modules/fast-uri/lib/schemes.js
3623
+ // node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/lib/schemes.js
3619
3624
  var require_schemes = __commonJS({
3620
- "node_modules/.pnpm/fast-uri@3.1.6/node_modules/fast-uri/lib/schemes.js"(exports, module) {
3625
+ "node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/lib/schemes.js"(exports, module) {
3621
3626
  "use strict";
3622
3627
  var { isUUID } = require_utils();
3623
3628
  var URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu;
@@ -3826,9 +3831,9 @@ var require_schemes = __commonJS({
3826
3831
  }
3827
3832
  });
3828
3833
 
3829
- // node_modules/.pnpm/fast-uri@3.1.6/node_modules/fast-uri/index.js
3834
+ // node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/index.js
3830
3835
  var require_fast_uri = __commonJS({
3831
- "node_modules/.pnpm/fast-uri@3.1.6/node_modules/fast-uri/index.js"(exports, module) {
3836
+ "node_modules/.pnpm/fast-uri@3.1.8/node_modules/fast-uri/index.js"(exports, module) {
3832
3837
  "use strict";
3833
3838
  var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3834
3839
  var { SCHEMES, getSchemeHandler } = require_schemes();
@@ -4033,12 +4038,15 @@ var require_fast_uri = __commonJS({
4033
4038
  }
4034
4039
  return false;
4035
4040
  }
4041
+ function isIPLiteral(host) {
4042
+ return host[0] === "[" && host[host.length - 1] === "]";
4043
+ }
4036
4044
  function hasMalformedComponentPercentEncoding(matches) {
4037
4045
  const host = matches[4];
4038
- return hasMalformedPercentEncoding(matches[3]) || host !== void 0 && !(host[0] === "[" && host[host.length - 1] === "]") && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches[6]) || hasMalformedPercentEncoding(matches[7]) || hasMalformedPercentEncoding(matches[8]);
4046
+ return hasMalformedPercentEncoding(matches[3]) || host !== void 0 && !isIPLiteral(host) && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches[6]) || hasMalformedPercentEncoding(matches[7]) || hasMalformedPercentEncoding(matches[8]);
4039
4047
  }
4040
4048
  function canonicalizeHost(parsed, options, schemeHandler, isIP) {
4041
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && parsed.host[0] !== "[" && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
4049
+ if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
4042
4050
  try {
4043
4051
  parsed.host = new URL("http://" + parsed.host).hostname;
4044
4052
  } catch (e) {
@@ -4125,10 +4133,11 @@ var require_fast_uri = __commonJS({
4125
4133
  if (parsed.host) {
4126
4134
  const ipv4result = isIPv4(parsed.host);
4127
4135
  if (ipv4result === false) {
4128
- const bracketedIPLiteral = parsed.host[0] === "[" && parsed.host[parsed.host.length - 1] === "]";
4136
+ const bracketedIPLiteral = isIPLiteral(parsed.host);
4137
+ const hasIPLiteralBracket = parsed.host.indexOf("[") !== -1 || parsed.host.indexOf("]") !== -1;
4129
4138
  const ipv6result = normalizeIPv6(parsed.host);
4130
4139
  isIP = ipv6result.isIPV6 || ipv6result.isIPVFuture === true;
4131
- malformedIPLiteral = bracketedIPLiteral && ipv6result.error === true;
4140
+ malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
4132
4141
  parsed.host = isIP ? ipv6result.host : ipv6result.host.toLowerCase();
4133
4142
  if (malformedIPLiteral) {
4134
4143
  parsed.error = parsed.error || "URI host is malformed.";
@@ -4151,14 +4160,17 @@ var require_fast_uri = __commonJS({
4151
4160
  parsed.error = parsed.error || "URI is not a " + options.reference + " reference.";
4152
4161
  }
4153
4162
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
4154
- malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
4155
- if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4156
- if (uri.indexOf("%") !== -1) {
4157
- if (parsed.host !== void 0 && !malformedIPLiteral) {
4158
- const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4159
- parsed.host = reescapeHostDelimiters(host, isIP);
4160
- }
4163
+ if (!malformedIPLiteral) {
4164
+ malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
4165
+ }
4166
+ if (uri.indexOf("%") !== -1 && parsed.host !== void 0 && !malformedIPLiteral) {
4167
+ let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4168
+ if (!isIP) {
4169
+ host = normalizePercentEncoding(host.toLowerCase());
4161
4170
  }
4171
+ parsed.host = reescapeHostDelimiters(host, isIP);
4172
+ }
4173
+ if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4162
4174
  if (parsed.path) {
4163
4175
  parsed.path = normalizePathEncoding(parsed.path);
4164
4176
  }
@@ -65378,6 +65390,7 @@ __export(imapClient_exports, {
65378
65390
  MAX_COMPOSE_SOURCE_BYTES: () => MAX_COMPOSE_SOURCE_BYTES,
65379
65391
  MAX_RFC822_FILE_BYTES: () => MAX_RFC822_FILE_BYTES,
65380
65392
  MAX_RFC822_INLINE_BYTES: () => MAX_RFC822_INLINE_BYTES,
65393
+ MAX_UNSCOPED_SEARCH_DEPTH: () => MAX_UNSCOPED_SEARCH_DEPTH,
65381
65394
  __resetPool: () => __resetPool,
65382
65395
  __setPoolConnect: () => __setPoolConnect,
65383
65396
  bodyStructureHasAttachments: () => bodyStructureHasAttachments,
@@ -65425,7 +65438,8 @@ __export(imapClient_exports, {
65425
65438
  resolveImapConfig: () => resolveImapConfig,
65426
65439
  resolveImapConfigs: () => resolveImapConfigs,
65427
65440
  resolveMailboxPath: () => resolveMailboxPath,
65428
- shouldUseImap: () => shouldUseImap
65441
+ shouldUseImap: () => shouldUseImap,
65442
+ unscopedSearchOffsetError: () => unscopedSearchOffsetError
65429
65443
  });
65430
65444
  import { createHash } from "node:crypto";
65431
65445
  function encodeImapId(account, path, uid) {
@@ -65771,6 +65785,10 @@ function messageIdentity(entry) {
65771
65785
  const messageId = raw.replace(/^<+|>+$/g, "").trim().toLowerCase();
65772
65786
  return messageId ? `mid:${messageId}` : `${entry.path}\0${entry.message.uid}`;
65773
65787
  }
65788
+ function unscopedSearchOffsetError(offset, limit) {
65789
+ if (offset <= 0 || offset + limit <= MAX_UNSCOPED_SEARCH_DEPTH) return void 0;
65790
+ return `search-messages without a mailbox merges every mailbox's newest offset+limit matches, so it pages at most ${MAX_UNSCOPED_SEARCH_DEPTH.toLocaleString("en-US")} deep (offset ${offset} + limit ${limit} asked). Name a mailbox (e.g. the account's All Mail or Archive) to page deeper, or narrow the search with dateTo.`;
65791
+ }
65774
65792
  function isUnfiltered(criteria) {
65775
65793
  const keys = Object.keys(criteria);
65776
65794
  return keys.length === 1 && criteria.deleted === false;
@@ -65894,16 +65912,16 @@ function pageRows(fetch) {
65894
65912
  function omissionReason(fetch) {
65895
65913
  return "the server's FETCH response for this message could not be read, even without BODYSTRUCTURE or ENVELOPE" + (fetch.cause ? ` (${fetch.cause})` : "");
65896
65914
  }
65897
- async function walkWindows(exists, page, firstWindow, readWindow) {
65915
+ async function walkWindows(top, page, firstWindow, readWindow, openTop = true) {
65898
65916
  const wanted = page.skip + page.take;
65899
65917
  const uids = [];
65900
65918
  let seen = 0;
65901
65919
  let matched = 0;
65902
- let hi = exists;
65920
+ let hi = top;
65903
65921
  let width = Math.max(1, Math.min(firstWindow, MAX_WINDOW));
65904
65922
  while (hi >= 1 && seen < wanted) {
65905
65923
  const lo = Math.max(1, hi - width + 1);
65906
- const live = (await readWindow(lo, hi === exists ? "*" : hi, hi - lo + 1)).slice().sort((a, b) => b - a);
65924
+ const live = (await readWindow(lo, hi === top && openTop ? "*" : hi, hi - lo + 1)).slice().sort((a, b) => b - a);
65907
65925
  matched += live.length;
65908
65926
  for (const uid of live) {
65909
65927
  if (seen >= wanted) break;
@@ -65915,13 +65933,21 @@ async function walkWindows(exists, page, firstWindow, readWindow) {
65915
65933
  }
65916
65934
  return { uids, matched, exhausted: hi < 1 };
65917
65935
  }
65936
+ function sequenceTop(client, statusCount) {
65937
+ const mb = client.mailbox;
65938
+ const exists = mb && typeof mb.exists === "number" && mb.exists >= 0 ? mb.exists : void 0;
65939
+ return exists ?? statusCount;
65940
+ }
65918
65941
  async function listLargeMailbox(client, exists, page) {
65942
+ const top = sequenceTop(client, exists) - page.skip;
65943
+ if (top < 1 || page.take === 0) {
65944
+ return { messages: [], omitted: [], total: exists, totalExact: true };
65945
+ }
65919
65946
  let deletedSeen = 0;
65920
65947
  const walk = await walkWindows(
65921
- exists,
65922
- page,
65923
- // Room for a few ghosts on the first read without a second round trip.
65924
- page.skip + page.take + 64,
65948
+ top,
65949
+ { skip: 0, take: page.take },
65950
+ page.take + DELETED_MARGIN,
65925
65951
  async (lo, hi) => {
65926
65952
  const live = [];
65927
65953
  for await (const msg of client.fetch(`${lo}:${hi}`, { uid: true, flags: true })) {
@@ -65929,7 +65955,10 @@ async function listLargeMailbox(client, exists, page) {
65929
65955
  else live.push(msg.uid);
65930
65956
  }
65931
65957
  return live;
65932
- }
65958
+ },
65959
+ // From the very top the first window is `lo:*`, as before; below it the
65960
+ // range is exact.
65961
+ page.skip === 0
65933
65962
  );
65934
65963
  return {
65935
65964
  ...pageRows(await fetchRows(client, walk.uids)),
@@ -66007,6 +66036,10 @@ async function run(args, listMode, deps) {
66007
66036
  }
66008
66037
  const limit = args.limit ?? 50;
66009
66038
  const offset = args.offset ?? 0;
66039
+ if (unscopedSearch) {
66040
+ const tooDeep = unscopedSearchOffsetError(offset, limit);
66041
+ if (tooDeep) throw new Error(tooDeep);
66042
+ }
66010
66043
  const criteria = buildCriteria(args, listMode);
66011
66044
  const page = unscopedSearch ? { skip: 0, take: offset + limit } : { skip: offset, take: limit };
66012
66045
  const fetched = [];
@@ -67084,7 +67117,7 @@ async function imapThread(id, deps = {}, limit = 50) {
67084
67117
  true
67085
67118
  );
67086
67119
  }
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;
67120
+ var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED, LARGE_MAILBOX_MESSAGES, FIRST_SEARCH_WINDOW, MAX_WINDOW, MAX_UNSCOPED_SEARCH_DEPTH, ROW_QUERY, ROW_QUERY_NO_STRUCTURE, ROW_QUERY_HEADERS_ONLY, DELETED_MARGIN, poolConnect, pools, connecting, MAX_COMPOSE_SOURCE_BYTES, MAX_RFC822_INLINE_BYTES, MAX_RFC822_FILE_BYTES, HEADER_WINDOW_BYTES, MAIL_FLAG_BITS, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
67088
67121
  var init_imapClient = __esm({
67089
67122
  "src/services/imapClient.ts"() {
67090
67123
  "use strict";
@@ -67158,6 +67191,7 @@ var init_imapClient = __esm({
67158
67191
  LARGE_MAILBOX_MESSAGES = 1e4;
67159
67192
  FIRST_SEARCH_WINDOW = 5e3;
67160
67193
  MAX_WINDOW = 5e4;
67194
+ MAX_UNSCOPED_SEARCH_DEPTH = 5e3;
67161
67195
  ROW_QUERY = {
67162
67196
  envelope: true,
67163
67197
  flags: true,
@@ -67176,6 +67210,7 @@ var init_imapClient = __esm({
67176
67210
  internalDate: true,
67177
67211
  headers: ["date", "from", "subject", "message-id", "in-reply-to", "content-type"]
67178
67212
  };
67213
+ DELETED_MARGIN = 32;
67179
67214
  poolConnect = defaultConnect;
67180
67215
  pools = /* @__PURE__ */ new Map();
67181
67216
  connecting = /* @__PURE__ */ new Map();
@@ -88471,7 +88506,10 @@ registerTool(
88471
88506
  isFlagged: external_exports.boolean().optional().describe("Filter by flagged status"),
88472
88507
  dateFrom: DATE_FILTER_SCHEMA.describe("Start date filter (e.g., 'January 1, 2026')"),
88473
88508
  dateTo: DATE_FILTER_SCHEMA.describe("End date filter (e.g., 'March 1, 2026')"),
88474
- limit: external_exports.number().int().min(1).max(500).optional().describe("Maximum number of results (default: 50, max: 500)")
88509
+ limit: external_exports.number().int().min(1).max(500).optional().describe("Maximum number of results (default: 50, max: 500)"),
88510
+ offset: external_exports.number().int().min(0).optional().describe(
88511
+ "Number of newest matches to skip (for pagination; IMAP accounts only). Without a mailbox, offset + limit may not exceed 5,000 \u2014 name a mailbox to page deeper."
88512
+ )
88475
88513
  },
88476
88514
  outputSchema: LIST_OUTPUT_SCHEMA
88477
88515
  },
@@ -88482,6 +88520,7 @@ registerTool(
88482
88520
  mailbox,
88483
88521
  account,
88484
88522
  limit = 50,
88523
+ offset = 0,
88485
88524
  dateFrom,
88486
88525
  dateTo,
88487
88526
  from,
@@ -88489,12 +88528,17 @@ registerTool(
88489
88528
  isRead,
88490
88529
  isFlagged
88491
88530
  }) => {
88531
+ if (offset > 0 && !mailbox) {
88532
+ const tooDeep = unscopedSearchOffsetError(offset, limit);
88533
+ if (tooDeep) return errorResponse(tooDeep);
88534
+ }
88492
88535
  if (shouldUseImap(account)) {
88493
88536
  const imapArgs = {
88494
88537
  query,
88495
88538
  body,
88496
88539
  mailbox,
88497
88540
  limit,
88541
+ offset,
88498
88542
  dateFrom,
88499
88543
  dateTo,
88500
88544
  from,
@@ -88513,11 +88557,16 @@ registerTool(
88513
88557
  omittedMessages: r.omittedMessages
88514
88558
  });
88515
88559
  }
88516
- const fan = await fanOutImapMessages(imapArgs, "search");
88517
88560
  const { appleScriptOnly } = partitionAccountsForCounts(
88518
88561
  mailManager.listAccounts(),
88519
88562
  resolveImapConfigs()
88520
88563
  );
88564
+ if (offset > 0 && appleScriptOnly.length > 0 && !body) {
88565
+ return errorResponse(
88566
+ `offset is supported only on IMAP accounts, and ${appleScriptOnly.map((a) => `"${a.name}"`).join(", ")} ${appleScriptOnly.length === 1 ? "is" : "are"} AppleScript-only. Pass an IMAP account to page, or omit offset.`
88567
+ );
88568
+ }
88569
+ const fan = await fanOutImapMessages(imapArgs, "search");
88521
88570
  if (body) {
88522
88571
  const apple2 = {
88523
88572
  rows: [],
@@ -88548,6 +88597,11 @@ registerTool(
88548
88597
  );
88549
88598
  return mergedMessageResponse(fan, apple, limit, "matched");
88550
88599
  }
88600
+ if (offset > 0) {
88601
+ return errorResponse(
88602
+ `offset is supported only on IMAP accounts; IMAP is not configured for ${account ? `account "${account}"` : "any account"}. Omit offset, or narrow the search with dateFrom/dateTo instead.`
88603
+ );
88604
+ }
88551
88605
  if (body) {
88552
88606
  return errorResponse(
88553
88607
  `Body search requires the IMAP backend, which is not configured for ${account ? `account "${account}"` : "any account"}. Configure IMAP (see the IMAP backend section of the README) or search by query/subject/from instead.`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.19.18",
3
+ "version": "2.19.20",
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",