apple-mail-mcp 2.20.0 → 2.20.1

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
@@ -813,7 +813,10 @@ Save an email to Drafts without sending.
813
813
  so a draft with `inReplyTo`/`references` cannot go through AppleScript. Instead the
814
814
  server composes it with the same MIME builder as the SMTP send (attachments included) and
815
815
  files it over IMAP into the account's Drafts mailbox (SPECIAL-USE `\Drafts`), flagged
816
- `\Draft`. The account is `account` when given (it must be a configured IMAP account);
816
+ `\Draft`. The account is `account` when given (it must be a configured IMAP account —
817
+ pass the exact `accountLabel` or login from `list-accounts`/your `APPLE_MAIL_MCP_IMAP_*`
818
+ config, e.g. `"user@icloud.com"`; a generic provider name like `"iCloud"` is not a
819
+ recognized alias and is refused);
817
820
  otherwise the IMAP account whose login is the SMTP identity, else the only configured
818
821
  IMAP account. When none qualifies — IMAP unconfigured, an unknown `account`, or several
819
822
  accounts and no way to choose — the call **fails and files nothing**; it never falls back
@@ -1024,7 +1027,18 @@ List attachments on a message.
1024
1027
  | --------- | ------ | -------- | ----------- |
1025
1028
  | `id` | string | Yes | Message ID |
1026
1029
 
1027
- **Returns:** List of attachments with name, MIME type, and size.
1030
+ **Returns:** List of attachments with name, MIME type, and `size` — the **decoded** byte count,
1031
+ i.e. what `fetch-attachment` returns.
1032
+
1033
+ **Sizes over IMAP.** `BODYSTRUCTURE` reports a part's size in its transfer encoding, so a
1034
+ base64 attachment's raw figure is ~4/3 of the file. `list-attachments` converts it: a
1035
+ `Content-Disposition` `size=` parameter wins when present; 7bit/8bit/binary parts are exact
1036
+ as reported; base64 parts are measured exactly from a short tail of each part (padding and
1037
+ line wrap), fetched in one batched `FETCH` for the whole message; quoted-printable parts up
1038
+ to 64 KiB are fetched and counted exactly. If a part can't be measured (the probe fails, the
1039
+ wrap is irregular, or a quoted-printable part is larger), the size is an estimate within a
1040
+ few bytes for base64 — an upper bound for quoted-printable — and the entry carries
1041
+ `sizeApproximate: true`. `fetch-attachment` always reports the exact `bytes`.
1028
1042
 
1029
1043
  ---
1030
1044
 
package/build/cli.js CHANGED
@@ -57841,9 +57841,12 @@ __export(imapClient_exports, {
57841
57841
  MAX_UNSCOPED_SEARCH_DEPTH: () => MAX_UNSCOPED_SEARCH_DEPTH,
57842
57842
  __resetPool: () => __resetPool,
57843
57843
  __setPoolConnect: () => __setPoolConnect,
57844
+ base64DecodedLength: () => base64DecodedLength,
57845
+ base64DecodedSizeFromTail: () => base64DecodedSizeFromTail,
57844
57846
  bodyStructureHasAttachments: () => bodyStructureHasAttachments,
57845
57847
  buildImapConnectionOptions: () => buildImapConnectionOptions,
57846
57848
  decodeImapId: () => decodeImapId,
57849
+ decodedSizeWithoutBody: () => decodedSizeWithoutBody,
57847
57850
  dropAllPools: () => dropAllPools,
57848
57851
  encodeImapId: () => encodeImapId,
57849
57852
  imapAppendDraft: () => imapAppendDraft,
@@ -57884,6 +57887,7 @@ __export(imapClient_exports, {
57884
57887
  matchMailbox: () => matchMailbox,
57885
57888
  normalizeMessageId: () => normalizeMessageId,
57886
57889
  omittedNote: () => omittedNote,
57890
+ quotedPrintableDecodedLength: () => quotedPrintableDecodedLength,
57887
57891
  resolveDraftImapAccount: () => resolveDraftImapAccount,
57888
57892
  resolveImapConfig: () => resolveImapConfig,
57889
57893
  resolveImapConfigs: () => resolveImapConfigs,
@@ -59344,6 +59348,56 @@ async function imapDeleteMessageById(id, deps = {}) {
59344
59348
  }
59345
59349
  });
59346
59350
  }
59351
+ function dispositionSize(params) {
59352
+ const raw = params?.size?.trim();
59353
+ return raw && /^\d+$/.test(raw) ? Number(raw) : void 0;
59354
+ }
59355
+ function base64DecodedLength(body) {
59356
+ const s = typeof body === "string" ? body : body.toString("latin1");
59357
+ const n = s.replace(/[^A-Za-z0-9+/]/g, "").length;
59358
+ return Math.floor(n * 3 / 4);
59359
+ }
59360
+ function base64DecodedSizeFromTail(total, tail) {
59361
+ if (tail.length >= total) return base64DecodedLength(tail);
59362
+ const s = tail.toString("latin1");
59363
+ const term = s.includes("\r\n") ? "\r\n" : "\n";
59364
+ const segs = s.split(term);
59365
+ if (segs.length < 3) return void 0;
59366
+ let trailing = 0;
59367
+ while (segs.length > 1 && segs[segs.length - 1] === "") {
59368
+ segs.pop();
59369
+ trailing++;
59370
+ }
59371
+ const last = segs[segs.length - 1];
59372
+ const full = segs.slice(1, -1);
59373
+ if (full.length === 0 || !last) return void 0;
59374
+ const L = full[0].length;
59375
+ const alphabet = /^[A-Za-z0-9+/]+$/;
59376
+ if (L === 0 || L % 4 !== 0 || !full.every((l) => l.length === L && alphabet.test(l))) {
59377
+ return void 0;
59378
+ }
59379
+ const m = /^([A-Za-z0-9+/]*)(=*)$/.exec(last);
59380
+ if (!m || m[2].length > 2 || last.length > L) return void 0;
59381
+ const before = total - trailing * term.length - last.length;
59382
+ if (before < 0 || before % (L + term.length) !== 0) return void 0;
59383
+ const chars = before / (L + term.length) * L + last.length;
59384
+ if (chars % 4 !== 0) return void 0;
59385
+ return chars / 4 * 3 - m[2].length;
59386
+ }
59387
+ function quotedPrintableDecodedLength(body) {
59388
+ const s = (typeof body === "string" ? body : body.toString("latin1")).replace(/=\r?\n/g, "");
59389
+ const escapes = s.match(/=[0-9A-Fa-f]{2}/g)?.length ?? 0;
59390
+ return s.length - escapes * 2;
59391
+ }
59392
+ function decodedSizeWithoutBody(a) {
59393
+ if (a.declaredSize !== void 0) return { size: a.declaredSize, exact: true };
59394
+ if (a.encoding === "base64") {
59395
+ const chars = a.encodedSize - 2 * Math.floor(a.encodedSize / 78);
59396
+ return { size: Math.floor(chars * 3 / 4), exact: false };
59397
+ }
59398
+ if (a.encoding === "quoted-printable") return { size: a.encodedSize, exact: false };
59399
+ return { size: a.encodedSize, exact: true };
59400
+ }
59347
59401
  function collectAttachments(node, out = []) {
59348
59402
  if (!node) return out;
59349
59403
  const filename = node.dispositionParameters?.filename || node.parameters?.name;
@@ -59355,7 +59409,9 @@ function collectAttachments(node, out = []) {
59355
59409
  part: node.part,
59356
59410
  filename: filename || `part-${node.part}`,
59357
59411
  mimeType: node.type || "application/octet-stream",
59358
- size: node.size ?? 0
59412
+ encodedSize: node.size ?? 0,
59413
+ encoding: (node.encoding || "").toLowerCase().trim(),
59414
+ declaredSize: dispositionSize(node.dispositionParameters)
59359
59415
  });
59360
59416
  }
59361
59417
  for (const child of node.childNodes ?? []) collectAttachments(child, out);
@@ -59376,6 +59432,52 @@ async function streamToBuffer(content, maxBytes) {
59376
59432
  }
59377
59433
  return Buffer.concat(chunks);
59378
59434
  }
59435
+ async function resolveDecodedSizes(client, uid, atts) {
59436
+ const out = atts.map(decodedSizeWithoutBody);
59437
+ const probes = [];
59438
+ atts.forEach((a, i) => {
59439
+ if (a.declaredSize !== void 0 || a.encodedSize <= 0) return;
59440
+ if (a.encoding === "base64") {
59441
+ probes.push({ i, start: Math.max(0, a.encodedSize - BASE64_TAIL_WINDOW), kind: "b64" });
59442
+ } else if (a.encoding === "quoted-printable" && a.encodedSize <= QP_EXACT_MAX_BYTES) {
59443
+ probes.push({ i, start: 0, kind: "qp" });
59444
+ }
59445
+ });
59446
+ if (probes.length === 0) return out;
59447
+ let parts;
59448
+ try {
59449
+ const msg = await client.fetchOne(
59450
+ String(uid),
59451
+ {
59452
+ uid: true,
59453
+ bodyParts: probes.map((p) => ({
59454
+ key: atts[p.i].part,
59455
+ start: p.start,
59456
+ maxLength: atts[p.i].encodedSize - p.start
59457
+ }))
59458
+ },
59459
+ { uid: true }
59460
+ );
59461
+ parts = msg ? msg.bodyParts : void 0;
59462
+ } catch {
59463
+ return out;
59464
+ }
59465
+ for (const p of probes) {
59466
+ const a = atts[p.i];
59467
+ const buf = parts?.get(a.part.toLowerCase());
59468
+ if (!buf) continue;
59469
+ let exact;
59470
+ if (p.kind === "qp") {
59471
+ exact = quotedPrintableDecodedLength(buf);
59472
+ } else {
59473
+ const requested = a.encodedSize - p.start;
59474
+ const total = buf.length < requested ? p.start + buf.length : a.encodedSize;
59475
+ exact = base64DecodedSizeFromTail(total, buf);
59476
+ }
59477
+ if (exact !== void 0) out[p.i] = { size: exact, exact: true };
59478
+ }
59479
+ return out;
59480
+ }
59379
59481
  async function imapListAttachments(id, deps = {}) {
59380
59482
  const ref = decodeImapId(id);
59381
59483
  if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
@@ -59384,12 +59486,18 @@ async function imapListAttachments(id, deps = {}) {
59384
59486
  if (!msg || !msg.bodyStructure) {
59385
59487
  return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
59386
59488
  }
59387
- const attachments = collectAttachments(msg.bodyStructure).map((a) => ({
59388
- id: `${id}#${a.part}`,
59389
- name: a.filename,
59390
- mimeType: a.mimeType,
59391
- size: a.size
59392
- }));
59489
+ const atts = collectAttachments(msg.bodyStructure);
59490
+ const sizes = await resolveDecodedSizes(client, ref.uid, atts);
59491
+ const attachments = atts.map((a, i) => {
59492
+ const info = {
59493
+ id: `${id}#${a.part}`,
59494
+ name: a.filename,
59495
+ mimeType: a.mimeType,
59496
+ size: sizes[i].size
59497
+ };
59498
+ if (!sizes[i].exact) info.sizeApproximate = true;
59499
+ return info;
59500
+ });
59393
59501
  return { success: true, attachments };
59394
59502
  });
59395
59503
  }
@@ -59410,10 +59518,11 @@ async function imapFetchAttachment(id, attachmentName, deps = {}) {
59410
59518
  error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`
59411
59519
  };
59412
59520
  }
59413
- if (match.size > MAX_IMAP_ATTACHMENT_BYTES) {
59521
+ const expected = decodedSizeWithoutBody(match).size;
59522
+ if (expected > MAX_IMAP_ATTACHMENT_BYTES) {
59414
59523
  return {
59415
59524
  success: false,
59416
- error: `IMAP attachment "${attachmentName}" is ${match.size} bytes; the maximum is ${MAX_IMAP_ATTACHMENT_BYTES} bytes (25 MiB).`
59525
+ error: `IMAP attachment "${attachmentName}" is ${expected} bytes; the maximum is ${MAX_IMAP_ATTACHMENT_BYTES} bytes (25 MiB).`
59417
59526
  };
59418
59527
  }
59419
59528
  try {
@@ -59601,7 +59710,7 @@ async function imapThread(id, deps = {}, limit = 50) {
59601
59710
  true
59602
59711
  );
59603
59712
  }
59604
- 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;
59713
+ 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;
59605
59714
  var init_imapClient = __esm({
59606
59715
  "src/services/imapClient.ts"() {
59607
59716
  "use strict";
@@ -59706,6 +59815,8 @@ var init_imapClient = __esm({
59706
59815
  imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
59707
59816
  imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
59708
59817
  FALLBACK_TRASH_PATH = "Trash";
59818
+ BASE64_TAIL_WINDOW = 1024;
59819
+ QP_EXACT_MAX_BYTES = 64 * 1024;
59709
59820
  imapBatchMarkRead = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
59710
59821
  assertMutated(
59711
59822
  await c.messageFlagsAdd(uids, ["\\Seen"], { uid: true }),
package/build/index.js CHANGED
@@ -7256,7 +7256,7 @@ function isInlineDisposition(headers) {
7256
7256
  function extractSize(headers) {
7257
7257
  const dispHeader = getHeader(headers, "Content-Disposition");
7258
7258
  if (dispHeader) {
7259
- const sizeMatch = dispHeader.match(/size=(\d+)/i);
7259
+ const sizeMatch = dispHeader.match(/(?:^|;)\s*size\s*=\s*"?(\d+)/i);
7260
7260
  if (sizeMatch) return parseInt(sizeMatch[1], 10);
7261
7261
  }
7262
7262
  return 0;
@@ -7285,8 +7285,8 @@ function decodePartText(bytes, charset) {
7285
7285
  return new TextDecoder("windows-1252").decode(bytes);
7286
7286
  }
7287
7287
  }
7288
- function estimateBase64Size(base64Body) {
7289
- const cleaned = base64Body.replace(/[\s\r\n]/g, "");
7288
+ function base64DecodedSize(base64Body) {
7289
+ const cleaned = base64Body.replace(/[^A-Za-z0-9+/]/g, "");
7290
7290
  return Math.floor(cleaned.length * 3 / 4);
7291
7291
  }
7292
7292
  function splitMimeParts(source, boundary) {
@@ -7349,7 +7349,8 @@ function decodeQuotedPrintable(body) {
7349
7349
  }
7350
7350
  function estimateSize(body, encoding) {
7351
7351
  const enc = (encoding || "").toLowerCase().trim();
7352
- if (enc === "base64") return estimateBase64Size(body);
7352
+ if (enc === "base64") return base64DecodedSize(body);
7353
+ if (enc === "quoted-printable") return decodeQuotedPrintable(body).length;
7353
7354
  return body.length;
7354
7355
  }
7355
7356
  function parseMimeAttachments(source) {
@@ -65182,9 +65183,12 @@ __export(imapClient_exports, {
65182
65183
  MAX_UNSCOPED_SEARCH_DEPTH: () => MAX_UNSCOPED_SEARCH_DEPTH,
65183
65184
  __resetPool: () => __resetPool,
65184
65185
  __setPoolConnect: () => __setPoolConnect,
65186
+ base64DecodedLength: () => base64DecodedLength,
65187
+ base64DecodedSizeFromTail: () => base64DecodedSizeFromTail,
65185
65188
  bodyStructureHasAttachments: () => bodyStructureHasAttachments,
65186
65189
  buildImapConnectionOptions: () => buildImapConnectionOptions,
65187
65190
  decodeImapId: () => decodeImapId,
65191
+ decodedSizeWithoutBody: () => decodedSizeWithoutBody,
65188
65192
  dropAllPools: () => dropAllPools,
65189
65193
  encodeImapId: () => encodeImapId,
65190
65194
  imapAppendDraft: () => imapAppendDraft,
@@ -65225,6 +65229,7 @@ __export(imapClient_exports, {
65225
65229
  matchMailbox: () => matchMailbox,
65226
65230
  normalizeMessageId: () => normalizeMessageId,
65227
65231
  omittedNote: () => omittedNote,
65232
+ quotedPrintableDecodedLength: () => quotedPrintableDecodedLength,
65228
65233
  resolveDraftImapAccount: () => resolveDraftImapAccount,
65229
65234
  resolveImapConfig: () => resolveImapConfig,
65230
65235
  resolveImapConfigs: () => resolveImapConfigs,
@@ -66685,6 +66690,56 @@ async function imapDeleteMessageById(id, deps = {}) {
66685
66690
  }
66686
66691
  });
66687
66692
  }
66693
+ function dispositionSize(params) {
66694
+ const raw = params?.size?.trim();
66695
+ return raw && /^\d+$/.test(raw) ? Number(raw) : void 0;
66696
+ }
66697
+ function base64DecodedLength(body) {
66698
+ const s = typeof body === "string" ? body : body.toString("latin1");
66699
+ const n = s.replace(/[^A-Za-z0-9+/]/g, "").length;
66700
+ return Math.floor(n * 3 / 4);
66701
+ }
66702
+ function base64DecodedSizeFromTail(total, tail) {
66703
+ if (tail.length >= total) return base64DecodedLength(tail);
66704
+ const s = tail.toString("latin1");
66705
+ const term = s.includes("\r\n") ? "\r\n" : "\n";
66706
+ const segs = s.split(term);
66707
+ if (segs.length < 3) return void 0;
66708
+ let trailing = 0;
66709
+ while (segs.length > 1 && segs[segs.length - 1] === "") {
66710
+ segs.pop();
66711
+ trailing++;
66712
+ }
66713
+ const last = segs[segs.length - 1];
66714
+ const full = segs.slice(1, -1);
66715
+ if (full.length === 0 || !last) return void 0;
66716
+ const L = full[0].length;
66717
+ const alphabet = /^[A-Za-z0-9+/]+$/;
66718
+ if (L === 0 || L % 4 !== 0 || !full.every((l) => l.length === L && alphabet.test(l))) {
66719
+ return void 0;
66720
+ }
66721
+ const m = /^([A-Za-z0-9+/]*)(=*)$/.exec(last);
66722
+ if (!m || m[2].length > 2 || last.length > L) return void 0;
66723
+ const before = total - trailing * term.length - last.length;
66724
+ if (before < 0 || before % (L + term.length) !== 0) return void 0;
66725
+ const chars = before / (L + term.length) * L + last.length;
66726
+ if (chars % 4 !== 0) return void 0;
66727
+ return chars / 4 * 3 - m[2].length;
66728
+ }
66729
+ function quotedPrintableDecodedLength(body) {
66730
+ const s = (typeof body === "string" ? body : body.toString("latin1")).replace(/=\r?\n/g, "");
66731
+ const escapes = s.match(/=[0-9A-Fa-f]{2}/g)?.length ?? 0;
66732
+ return s.length - escapes * 2;
66733
+ }
66734
+ function decodedSizeWithoutBody(a) {
66735
+ if (a.declaredSize !== void 0) return { size: a.declaredSize, exact: true };
66736
+ if (a.encoding === "base64") {
66737
+ const chars = a.encodedSize - 2 * Math.floor(a.encodedSize / 78);
66738
+ return { size: Math.floor(chars * 3 / 4), exact: false };
66739
+ }
66740
+ if (a.encoding === "quoted-printable") return { size: a.encodedSize, exact: false };
66741
+ return { size: a.encodedSize, exact: true };
66742
+ }
66688
66743
  function collectAttachments(node, out = []) {
66689
66744
  if (!node) return out;
66690
66745
  const filename = node.dispositionParameters?.filename || node.parameters?.name;
@@ -66696,7 +66751,9 @@ function collectAttachments(node, out = []) {
66696
66751
  part: node.part,
66697
66752
  filename: filename || `part-${node.part}`,
66698
66753
  mimeType: node.type || "application/octet-stream",
66699
- size: node.size ?? 0
66754
+ encodedSize: node.size ?? 0,
66755
+ encoding: (node.encoding || "").toLowerCase().trim(),
66756
+ declaredSize: dispositionSize(node.dispositionParameters)
66700
66757
  });
66701
66758
  }
66702
66759
  for (const child of node.childNodes ?? []) collectAttachments(child, out);
@@ -66717,6 +66774,52 @@ async function streamToBuffer(content, maxBytes) {
66717
66774
  }
66718
66775
  return Buffer.concat(chunks);
66719
66776
  }
66777
+ async function resolveDecodedSizes(client, uid, atts) {
66778
+ const out = atts.map(decodedSizeWithoutBody);
66779
+ const probes = [];
66780
+ atts.forEach((a, i) => {
66781
+ if (a.declaredSize !== void 0 || a.encodedSize <= 0) return;
66782
+ if (a.encoding === "base64") {
66783
+ probes.push({ i, start: Math.max(0, a.encodedSize - BASE64_TAIL_WINDOW), kind: "b64" });
66784
+ } else if (a.encoding === "quoted-printable" && a.encodedSize <= QP_EXACT_MAX_BYTES) {
66785
+ probes.push({ i, start: 0, kind: "qp" });
66786
+ }
66787
+ });
66788
+ if (probes.length === 0) return out;
66789
+ let parts;
66790
+ try {
66791
+ const msg = await client.fetchOne(
66792
+ String(uid),
66793
+ {
66794
+ uid: true,
66795
+ bodyParts: probes.map((p) => ({
66796
+ key: atts[p.i].part,
66797
+ start: p.start,
66798
+ maxLength: atts[p.i].encodedSize - p.start
66799
+ }))
66800
+ },
66801
+ { uid: true }
66802
+ );
66803
+ parts = msg ? msg.bodyParts : void 0;
66804
+ } catch {
66805
+ return out;
66806
+ }
66807
+ for (const p of probes) {
66808
+ const a = atts[p.i];
66809
+ const buf = parts?.get(a.part.toLowerCase());
66810
+ if (!buf) continue;
66811
+ let exact;
66812
+ if (p.kind === "qp") {
66813
+ exact = quotedPrintableDecodedLength(buf);
66814
+ } else {
66815
+ const requested = a.encodedSize - p.start;
66816
+ const total = buf.length < requested ? p.start + buf.length : a.encodedSize;
66817
+ exact = base64DecodedSizeFromTail(total, buf);
66818
+ }
66819
+ if (exact !== void 0) out[p.i] = { size: exact, exact: true };
66820
+ }
66821
+ return out;
66822
+ }
66720
66823
  async function imapListAttachments(id, deps = {}) {
66721
66824
  const ref = decodeImapId(id);
66722
66825
  if (!ref) return { success: false, error: `Not an IMAP message id: "${id}".` };
@@ -66725,12 +66828,18 @@ async function imapListAttachments(id, deps = {}) {
66725
66828
  if (!msg || !msg.bodyStructure) {
66726
66829
  return { success: false, error: `IMAP message UID ${ref.uid} not found in "${ref.path}".` };
66727
66830
  }
66728
- const attachments = collectAttachments(msg.bodyStructure).map((a) => ({
66729
- id: `${id}#${a.part}`,
66730
- name: a.filename,
66731
- mimeType: a.mimeType,
66732
- size: a.size
66733
- }));
66831
+ const atts = collectAttachments(msg.bodyStructure);
66832
+ const sizes = await resolveDecodedSizes(client, ref.uid, atts);
66833
+ const attachments = atts.map((a, i) => {
66834
+ const info = {
66835
+ id: `${id}#${a.part}`,
66836
+ name: a.filename,
66837
+ mimeType: a.mimeType,
66838
+ size: sizes[i].size
66839
+ };
66840
+ if (!sizes[i].exact) info.sizeApproximate = true;
66841
+ return info;
66842
+ });
66734
66843
  return { success: true, attachments };
66735
66844
  });
66736
66845
  }
@@ -66751,10 +66860,11 @@ async function imapFetchAttachment(id, attachmentName, deps = {}) {
66751
66860
  error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`
66752
66861
  };
66753
66862
  }
66754
- if (match.size > MAX_IMAP_ATTACHMENT_BYTES) {
66863
+ const expected = decodedSizeWithoutBody(match).size;
66864
+ if (expected > MAX_IMAP_ATTACHMENT_BYTES) {
66755
66865
  return {
66756
66866
  success: false,
66757
- error: `IMAP attachment "${attachmentName}" is ${match.size} bytes; the maximum is ${MAX_IMAP_ATTACHMENT_BYTES} bytes (25 MiB).`
66867
+ error: `IMAP attachment "${attachmentName}" is ${expected} bytes; the maximum is ${MAX_IMAP_ATTACHMENT_BYTES} bytes (25 MiB).`
66758
66868
  };
66759
66869
  }
66760
66870
  try {
@@ -66942,7 +67052,7 @@ async function imapThread(id, deps = {}, limit = 50) {
66942
67052
  true
66943
67053
  );
66944
67054
  }
66945
- 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;
67055
+ 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;
66946
67056
  var init_imapClient = __esm({
66947
67057
  "src/services/imapClient.ts"() {
66948
67058
  "use strict";
@@ -67047,6 +67157,8 @@ var init_imapClient = __esm({
67047
67157
  imapMarkRead = (id, deps = {}) => flagOp(id, "\\Seen", true, deps);
67048
67158
  imapMarkUnread = (id, deps = {}) => flagOp(id, "\\Seen", false, deps);
67049
67159
  FALLBACK_TRASH_PATH = "Trash";
67160
+ BASE64_TAIL_WINDOW = 1024;
67161
+ QP_EXACT_MAX_BYTES = 64 * 1024;
67050
67162
  imapBatchMarkRead = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
67051
67163
  assertMutated(
67052
67164
  await c.messageFlagsAdd(uids, ["\\Seen"], { uid: true }),
@@ -89721,7 +89833,7 @@ registerTool(
89721
89833
  registerTool(
89722
89834
  "list-attachments",
89723
89835
  {
89724
- description: "Use when: enumerating a message's attachments (by id) to discover their names, MIME types, and sizes \u2014 typically before saving or fetching one.\nReturns: each attachment's name, MIME type, and size, plus a count.\nDo not use when: you want the bytes (use fetch-attachment for inline base64, or save-attachment to write to disk). Get the message id from search-messages or list-messages first.",
89836
+ description: "Use when: enumerating a message's attachments (by id) to discover their names, MIME types, and sizes \u2014 typically before saving or fetching one.\nReturns: each attachment's name, MIME type, and decoded size in bytes (what fetch-attachment returns), plus a count; sizeApproximate: true marks the rare IMAP size that is an estimate.\nDo not use when: you want the bytes (use fetch-attachment for inline base64, or save-attachment to write to disk). Get the message id from search-messages or list-messages first.",
89725
89837
  inputSchema: {
89726
89838
  id: MESSAGE_ID_SCHEMA
89727
89839
  },
@@ -89741,8 +89853,9 @@ registerTool(
89741
89853
  return successResponse("No attachments found", structured);
89742
89854
  }
89743
89855
  const attachmentList = attachments.map((a) => {
89744
- const sizeKb = Math.round(a.size / 1024);
89745
- return ` - ${a.name} (${a.mimeType}, ${sizeKb} KB)`;
89856
+ const approx = "sizeApproximate" in a && a.sizeApproximate ? "~" : "";
89857
+ const size = a.size < 1024 ? `${a.size} bytes` : `${Math.round(a.size / 1024)} KB`;
89858
+ return ` - ${a.name} (${a.mimeType}, ${approx}${size})`;
89746
89859
  }).join("\n");
89747
89860
  return successResponse(
89748
89861
  `Found ${attachments.length} attachment(s):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "2.20.0",
3
+ "version": "2.20.1",
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",