apple-mail-mcp 2.20.9 → 2.20.11

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.
Files changed (4) hide show
  1. package/README.md +27 -3
  2. package/build/cli.js +57 -12
  3. package/build/index.js +20465 -20389
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -237,12 +237,29 @@ Search for messages matching criteria. Searches all accounts by default.
237
237
  | `account` | string | No | Account to search in (omit to search all accounts) |
238
238
  | `isRead` | boolean | No | Filter by read status |
239
239
  | `isFlagged` | boolean | No | Filter by flagged status |
240
+ | `flagColor` | string | No | Filter to messages flagged with exactly this color — see [`flag-message`](#flag-message--unflag-message) for the palette. Implies `isFlagged: true`; pairing this with `isFlagged: false` is rejected. See the flag-color note below. |
240
241
  | `dateFrom` | string | No | Start date filter (e.g., "January 1, 2026") |
241
242
  | `dateTo` | string | No | End date filter (e.g., "March 1, 2026") |
242
243
  | `limit` | number | No | Max results, 1–500 (default: 50) |
243
244
  | `offset` | number | No | Newest matches to skip, ≥ 0 (for pagination). IMAP accounts only; without a `mailbox`, `offset` + `limit` may not exceed 5,000 |
244
245
 
245
- **Returns:** List of matching messages with ID, date, subject, sender, and read state.
246
+ **Returns:** List of matching messages with ID, date, subject, sender, read state,
247
+ and `flagColor` when flagged with an explicit color.
248
+
249
+ **Flag color as a label (#286).** Every row from both backends carries
250
+ `flagColor` (the palette name — `red`, `orange`, `yellow`, `green`, `blue`,
251
+ `purple`, `gray`) whenever the message is flagged with an explicit color;
252
+ omitted for an unflagged message or one flagged with Mail's plain default (no
253
+ color chosen). Combined with the `flagColor` filter above, a colored flag
254
+ works as a label across mailboxes: flag a set of messages with
255
+ `flag-message`/`batch-flag-messages`, then find exactly that set later with
256
+ `search-messages`, without moving anything into a dedicated folder. **Known
257
+ limitation, inherent to Apple's `$MailFlagBit0/1/2` IMAP wire encoding, not
258
+ specific to this server:** `red` (palette index 0) sets no keyword bits, so
259
+ over IMAP it is indistinguishable from a plain colorless flag, both when
260
+ reading `flagColor` back and when filtering on it. AppleScript-backed
261
+ accounts are unaffected (Mail's own `flag index` property is `0` for red, `-1`
262
+ for none).
246
263
 
247
264
  ⚠️ **Row dates.** Since 2.19.7 every row — on both the IMAP and AppleScript
248
265
  backends — carries both `dateSent` (the message's `Date:` header) and
@@ -392,7 +409,9 @@ List messages in a mailbox.
392
409
  | `from` | string | No | Filter by sender email address or name |
393
410
  | `unreadOnly` | boolean | No | Only show unread messages |
394
411
 
395
- **Returns:** List of messages with ID, date, subject, and sender.
412
+ **Returns:** List of messages with ID, date, subject, sender, and `flagColor`
413
+ when flagged with an explicit color (see the flag-color note under
414
+ [`search-messages`](#search-messages), which also supports filtering by it).
396
415
 
397
416
  ⚠️ **Row dates.** Since 2.19.7 every row — on both the **IMAP** and
398
417
  **AppleScript** backends — carries both `dateSent` (the message's `Date:`
@@ -995,7 +1014,12 @@ Flag or unflag a message. `flag-message` optionally takes a flag **color**; `unf
995
1014
 
996
1015
  **Flag colors** are an Apple Mail feature — the message's `flag index` (0 red, 1 orange, 2 yellow, 3 green, 4 blue, 5 purple, 6 gray), which is the property a Mail smart mailbox can match on. **The color is applied on both routes** (since 2.10.0): AppleScript sets the flag index directly, and for an **IMAP-routed** id (`imap:…`) the color is written as Mail.app's `$MailFlagBit0/1/2` keywords — a 3-bit field holding the same palette index. `\Flagged` on its own really is colorless, but those keywords ride alongside it in an ordinary `UID STORE`, so a smart mailbox keyed on flag color matches an IMAP-flagged message too. You do **not** need to resolve to a numeric id just to color a flag.
997
1016
 
998
- To **read** a color, the IMAP read path returns `flagColorIndex` in `structuredContent` — the same 0-6 palette index, omitted when the message carries no color bits. The AppleScript read path does not populate it.
1017
+ To **read** a color back, or **find** messages by color, use
1018
+ [`search-messages`](#search-messages) (or [`list-messages`](#list-messages) for
1019
+ read-back only) — both routes now return `flagColor` (the palette name) on
1020
+ every flagged-with-a-color row, and `search-messages` takes a `flagColor`
1021
+ filter using this same palette (#286). The IMAP route additionally still
1022
+ carries the raw 0-6 `flagColorIndex` for back-compat.
999
1023
 
1000
1024
  ---
1001
1025
 
package/build/cli.js CHANGED
@@ -58021,6 +58021,7 @@ var init_auditLog = __esm({
58021
58021
  // src/services/imapClient.ts
58022
58022
  var imapClient_exports = {};
58023
58023
  __export(imapClient_exports, {
58024
+ FLAG_COLOR_NAMES: () => FLAG_COLOR_NAMES,
58024
58025
  HEADER_WINDOW_BYTES: () => HEADER_WINDOW_BYTES,
58025
58026
  IMAP_ENV: () => IMAP_ENV,
58026
58027
  LARGE_MAILBOX_MESSAGES: () => LARGE_MAILBOX_MESSAGES,
@@ -58038,6 +58039,7 @@ __export(imapClient_exports, {
58038
58039
  decodedSizeWithoutBody: () => decodedSizeWithoutBody,
58039
58040
  dropAllPools: () => dropAllPools,
58040
58041
  encodeImapId: () => encodeImapId,
58042
+ flagColorName: () => flagColorName,
58041
58043
  imapAppendDraft: () => imapAppendDraft,
58042
58044
  imapAppendSentCopy: () => imapAppendSentCopy,
58043
58045
  imapBatchDelete: () => imapBatchDelete,
@@ -58324,6 +58326,9 @@ function buildCriteria(a, listMode) {
58324
58326
  if (a.isFlagged === false) c.unflagged = true;
58325
58327
  if (a.dateFrom) c.since = new Date(a.dateFrom);
58326
58328
  if (a.dateTo) c.before = new Date(a.dateTo);
58329
+ if (a.flagColorIndex !== void 0) {
58330
+ c.flagged = true;
58331
+ }
58327
58332
  return c;
58328
58333
  }
58329
58334
  function validDate(d) {
@@ -58366,6 +58371,7 @@ function isoOrEmpty(d) {
58366
58371
  }
58367
58372
  function structuredRow(m, account, path3) {
58368
58373
  const env = m.envelope ?? {};
58374
+ const flagColorIndex = mailFlagColorIndex(m.flags);
58369
58375
  return {
58370
58376
  id: encodeImapId(account, path3, m.uid),
58371
58377
  subject: env.subject || "(no subject)",
@@ -58382,7 +58388,19 @@ function structuredRow(m, account, path3) {
58382
58388
  dateReceived: isoOrEmpty(validDate(m.internalDate) ?? headerDate(m)),
58383
58389
  isRead: m.flags?.has("\\Seen") ?? false,
58384
58390
  isFlagged: m.flags?.has("\\Flagged") ?? false,
58385
- flagColorIndex: mailFlagColorIndex(m.flags),
58391
+ // flagColorIndex has been computed here since 2.19.x but never reached a
58392
+ // caller in friendly form (#286, @j5pu) — flagColor is the palette name
58393
+ // (e.g. "orange"), omitted whenever the index is (unflagged, or flagged
58394
+ // with Mail's plain default and no explicit color). flagColorIndex stays
58395
+ // for back-compat.
58396
+ // ⚠️ "red" (index 0) sets no $MailFlagBit keyword — it is wire-identical to
58397
+ // a plain colorless flag over IMAP, so a red-flagged message reads back
58398
+ // here with no flagColor (and flagColor: "red" in search-messages also
58399
+ // matches a plain flag). Inherent to Apple's bitfield encoding, not
58400
+ // fixable from the client side; the AppleScript route is unaffected (Mail's
58401
+ // own `flag index` is 0 for red, -1 for none).
58402
+ flagColorIndex,
58403
+ flagColor: flagColorName(flagColorIndex),
58386
58404
  mailbox: path3,
58387
58405
  account,
58388
58406
  // Derived from BODYSTRUCTURE, which the list/search fetch now requests.
@@ -58461,6 +58479,16 @@ async function searchUids(client, path3, criteria, size) {
58461
58479
  `IMAP SEARCH on "${path3}"${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)")
58462
58480
  );
58463
58481
  }
58482
+ async function filterUidsByColor(client, uids, colorIndex) {
58483
+ if (uids.length === 0) return uids;
58484
+ const keep = /* @__PURE__ */ new Set();
58485
+ for await (const msg of client.fetch(uids.join(","), { uid: true, flags: true }, { uid: true })) {
58486
+ if (typeof msg?.uid !== "number") continue;
58487
+ const idx = mailFlagColorIndex(msg.flags);
58488
+ if (colorIndex === 0 ? idx === void 0 : idx === colorIndex) keep.add(msg.uid);
58489
+ }
58490
+ return uids.filter((uid) => keep.has(uid));
58491
+ }
58464
58492
  function mergeFetched(into, msg) {
58465
58493
  const prev = into.get(msg.uid);
58466
58494
  if (!prev) {
@@ -58616,14 +58644,15 @@ async function listLargeMailbox(client, exists, page) {
58616
58644
  totalExact: true
58617
58645
  };
58618
58646
  }
58619
- async function searchLargeMailbox(client, path3, criteria, exists, page) {
58647
+ async function searchLargeMailbox(client, path3, criteria, exists, page, colorIndex) {
58620
58648
  const walk = await walkWindows(exists, page, FIRST_SEARCH_WINDOW, async (lo, hi, width) => {
58621
- const found = await searchUids(client, path3, { ...criteria, seq: `${lo}:${hi}` }, exists);
58649
+ let found = await searchUids(client, path3, { ...criteria, seq: `${lo}:${hi}` }, exists);
58622
58650
  if (found.length > width) {
58623
58651
  throw new Error(
58624
58652
  `IMAP SEARCH on "${path3}" reported ${found.length} matches in a ${width}-message window \u2014 discarding as corrupted rather than trusting it (see #246).`
58625
58653
  );
58626
58654
  }
58655
+ if (colorIndex !== void 0) found = await filterUidsByColor(client, found, colorIndex);
58627
58656
  return found;
58628
58657
  });
58629
58658
  return {
@@ -58632,14 +58661,14 @@ async function searchLargeMailbox(client, path3, criteria, exists, page) {
58632
58661
  totalExact: walk.exhausted
58633
58662
  };
58634
58663
  }
58635
- async function fetchMailboxMatches(client, path3, criteria, page) {
58664
+ async function fetchMailboxMatches(client, path3, criteria, page, colorIndex) {
58636
58665
  const lock = await client.getMailboxLock(path3);
58637
58666
  try {
58638
58667
  const exists = await statusMessageCount(client, path3);
58639
58668
  if (exists === 0) return { messages: [], omitted: [], total: 0, totalExact: true };
58640
58669
  if (exists !== void 0 && exists > LARGE_MAILBOX_MESSAGES) {
58641
58670
  try {
58642
- return isUnfiltered(criteria) ? await listLargeMailbox(client, exists, page) : await searchLargeMailbox(client, path3, criteria, exists, page);
58671
+ return isUnfiltered(criteria) ? await listLargeMailbox(client, exists, page) : await searchLargeMailbox(client, path3, criteria, exists, page, colorIndex);
58643
58672
  } catch (error2) {
58644
58673
  const detail = errText(error2);
58645
58674
  throw new Error(
@@ -58647,16 +58676,20 @@ async function fetchMailboxMatches(client, path3, criteria, page) {
58647
58676
  );
58648
58677
  }
58649
58678
  }
58650
- const uids = await searchUids(client, path3, criteria, exists);
58651
- if (uids.length === 0 || page.take === 0) {
58652
- return { messages: [], omitted: [], total: uids.length, totalExact: true };
58679
+ const rawUids = await searchUids(client, path3, criteria, exists);
58680
+ if (rawUids.length === 0) {
58681
+ return { messages: [], omitted: [], total: 0, totalExact: true };
58653
58682
  }
58654
58683
  const messages = exists ?? (await client.status(path3, { messages: true })).messages ?? void 0;
58655
- if (typeof messages === "number" && uids.length > messages) {
58684
+ if (typeof messages === "number" && rawUids.length > messages) {
58656
58685
  throw new Error(
58657
- `IMAP SEARCH on "${path3}" reported ${uids.length} matches, more than the mailbox's own ${messages} messages \u2014 discarding as corrupted rather than trusting it (see #246).`
58686
+ `IMAP SEARCH on "${path3}" reported ${rawUids.length} matches, more than the mailbox's own ${messages} messages \u2014 discarding as corrupted rather than trusting it (see #246).`
58658
58687
  );
58659
58688
  }
58689
+ const uids = colorIndex !== void 0 ? await filterUidsByColor(client, rawUids, colorIndex) : rawUids;
58690
+ if (uids.length === 0 || page.take === 0) {
58691
+ return { messages: [], omitted: [], total: uids.length, totalExact: true };
58692
+ }
58660
58693
  const newest = uids.slice().reverse().slice(page.skip, page.skip + page.take);
58661
58694
  return { ...pageRows(await fetchRows(client, newest)), total: uids.length, totalExact: true };
58662
58695
  } finally {
@@ -58706,7 +58739,7 @@ async function run(args, listMode, deps) {
58706
58739
  }
58707
58740
  try {
58708
58741
  const outcome = await raceStop(
58709
- fetchMailboxMatches(client, path3, criteria, page),
58742
+ fetchMailboxMatches(client, path3, criteria, page, args.flagColorIndex),
58710
58743
  deps.signal
58711
58744
  );
58712
58745
  if (outcome === STOPPED) {
@@ -59427,6 +59460,9 @@ function mailFlagColorIndex(flags) {
59427
59460
  }
59428
59461
  return any ? idx : void 0;
59429
59462
  }
59463
+ function flagColorName(index) {
59464
+ return index === void 0 ? void 0 : FLAG_COLOR_NAMES[index];
59465
+ }
59430
59466
  function flagOp(id, flag, add, deps) {
59431
59467
  const ref = decodeImapId(id);
59432
59468
  if (!ref) return Promise.resolve({ success: false, error: `Not an IMAP message id: "${id}".` });
@@ -59940,7 +59976,7 @@ async function imapThread(id, deps = {}, limit = 50) {
59940
59976
  true
59941
59977
  );
59942
59978
  }
59943
- 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, BASE64_TAIL_WINDOW, QP_EXACT_MAX_BYTES, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
59979
+ 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, FLAG_COLOR_NAMES, imapMarkRead, imapMarkUnread, FALLBACK_TRASH_PATH, BASE64_TAIL_WINDOW, QP_EXACT_MAX_BYTES, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete;
59944
59980
  var init_imapClient = __esm({
59945
59981
  "src/services/imapClient.ts"() {
59946
59982
  "use strict";
@@ -60043,6 +60079,15 @@ var init_imapClient = __esm({
60043
60079
  MAX_RFC822_FILE_BYTES = MAX_COMPOSE_SOURCE_BYTES;
60044
60080
  HEADER_WINDOW_BYTES = 64 * 1024;
60045
60081
  MAIL_FLAG_BITS = ["$MailFlagBit0", "$MailFlagBit1", "$MailFlagBit2"];
60082
+ FLAG_COLOR_NAMES = [
60083
+ "red",
60084
+ "orange",
60085
+ "yellow",
60086
+ "green",
60087
+ "blue",
60088
+ "purple",
60089
+ "gray"
60090
+ ];
60046
60091
  imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
60047
60092
  imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
60048
60093
  FALLBACK_TRASH_PATH = "Trash";