apple-mail-mcp 1.6.5 → 1.6.7

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/build/index.js CHANGED
@@ -174,9 +174,13 @@ server.tool("search-messages", {
174
174
  // --- get-message ---
175
175
  server.tool("get-message", {
176
176
  id: MESSAGE_ID_SCHEMA,
177
- preferHtml: z.boolean().optional().describe("Return HTML source instead of plain text"),
177
+ preferHtml: z
178
+ .boolean()
179
+ .optional()
180
+ .describe("Return the HTML body (extracted from the message source) instead of plain text"),
178
181
  }, withErrorHandling(({ id, preferHtml }) => {
179
- const content = mailManager.getMessageContent(id);
182
+ // Only fetch/parse the raw source when HTML is actually requested (#32).
183
+ const content = mailManager.getMessageContent(id, preferHtml === true);
180
184
  if (!content) {
181
185
  return errorResponse(`Message with ID "${id}" not found`);
182
186
  }
@@ -180,11 +180,18 @@ export declare class AppleMailManager {
180
180
  * Note: Mail.app message IDs are unique per mailbox. This method searches
181
181
  * all mailboxes in all accounts to find the message.
182
182
  */
183
- getMessageById(id: string): Message | null;
183
+ getMessageById(id: string, deepAttachmentCheck?: boolean): Message | null;
184
184
  /**
185
185
  * Get the content of a message.
186
+ *
187
+ * @param id - Message ID
188
+ * @param includeHtml - When true, also fetch the raw MIME source and extract
189
+ * the `text/html` body part into `htmlContent`. This is opt-in because the
190
+ * source can be MB-sized (it includes base64 attachments) and the plain-text
191
+ * path doesn't need it; fetching it unconditionally was both slow and, worse,
192
+ * returned the entire raw MIME blob mislabeled as HTML (#32).
186
193
  */
187
- getMessageContent(id: string): MessageContent | null;
194
+ getMessageContent(id: string, includeHtml?: boolean): MessageContent | null;
188
195
  /**
189
196
  * Get the raw MIME source of a message.
190
197
  * Used as fallback for attachment extraction when AppleScript
@@ -335,35 +342,48 @@ export declare class AppleMailManager {
335
342
  private moveMessageInternal;
336
343
  moveMessage(id: string, mailbox: string, account?: string): boolean;
337
344
  /**
338
- * Delete multiple messages at once.
345
+ * Run one operation over many message IDs in a SINGLE osascript invocation.
346
+ *
347
+ * Previously each batch method looped and called the per-id method, so a
348
+ * 100-id batch spawned 100 osascript processes — each one re-resolving
349
+ * accounts and walking the whole account→mailbox tree — all serialized
350
+ * through the gate (issue #31). This walks the tree exactly once: for each
351
+ * mailbox it probes the still-pending IDs with `whose id is` (indexed, so
352
+ * effectively free) and applies `operation` to any match, tracking found IDs
353
+ * so it can stop early once all are accounted for. Per-id outcomes come back
354
+ * as control-char-delimited `id<FS>status` records (status: `ok`,
355
+ * `notfound`, or `error:<msg>`), and results are returned in input order.
339
356
  *
340
- * @param ids - Array of message IDs to delete
341
- * @returns Array of results for each message
357
+ * `setup` runs once before the walk (used by move to resolve the destination);
358
+ * it may bail the whole batch by returning a `BATCH_FATAL`-prefixed string.
359
+ */
360
+ private runBatchOperation;
361
+ /**
362
+ * Delete multiple messages at once (single tree walk — see runBatchOperation).
342
363
  */
343
364
  batchDeleteMessages(ids: string[]): BatchOperationResult[];
344
365
  /**
345
- * Move multiple messages to a mailbox at once.
366
+ * Move multiple messages to a mailbox at once (single tree walk).
346
367
  *
347
- * @param ids - Array of message IDs to move
348
- * @param mailbox - Destination mailbox name
349
- * @param account - Account containing the destination mailbox
350
- * @returns Array of results for each message
368
+ * The destination is resolved once (account-scoped, ambiguity-aware a name
369
+ * matching more than one mailbox fails the whole batch rather than guessing),
370
+ * then every matched message is moved in the same walk.
351
371
  */
352
372
  batchMoveMessages(ids: string[], mailbox: string, account?: string): BatchOperationResult[];
353
373
  /**
354
- * Mark multiple messages as read at once.
374
+ * Mark multiple messages as read at once (single tree walk).
355
375
  */
356
376
  batchMarkAsRead(ids: string[]): BatchOperationResult[];
357
377
  /**
358
- * Mark multiple messages as unread at once.
378
+ * Mark multiple messages as unread at once (single tree walk).
359
379
  */
360
380
  batchMarkAsUnread(ids: string[]): BatchOperationResult[];
361
381
  /**
362
- * Flag multiple messages at once.
382
+ * Flag multiple messages at once (single tree walk).
363
383
  */
364
384
  batchFlagMessages(ids: string[]): BatchOperationResult[];
365
385
  /**
366
- * Unflag multiple messages at once.
386
+ * Unflag multiple messages at once (single tree walk).
367
387
  */
368
388
  batchUnflagMessages(ids: string[]): BatchOperationResult[];
369
389
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"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"}
@@ -17,7 +17,7 @@ import { existsSync, writeFileSync } from "fs";
17
17
  import { isAbsolute, resolve } from "path";
18
18
  import { homedir } from "os";
19
19
  import { executeAppleScript } from "../utils/applescript.js";
20
- import { parseMimeAttachments, extractMimeAttachment } from "../utils/mimeParse.js";
20
+ import { parseMimeAttachments, extractMimeAttachment, extractHtmlBody } from "../utils/mimeParse.js";
21
21
  // =============================================================================
22
22
  // Search Tuning (issue #24)
23
23
  // =============================================================================
@@ -70,6 +70,7 @@ const DIAG_FIELD_SEP = "\x1dF\x1d"; // between diagnostics fields
70
70
  const DIAG_ITEM_SEP = "\x1dM\x1d"; // between diagnostics list items
71
71
  const CONTENT_MARKER = "\x1dCONTENT\x1d"; // subject/plain-text boundary
72
72
  const HTML_MARKER = "\x1dHTML\x1d"; // plain-text/source boundary
73
+ const BATCH_FATAL = "\x1dFATAL\x1d"; // prefix for a whole-batch failure (e.g. bad destination)
73
74
  /**
74
75
  * Merge a per-account SearchDiagnostics into an aggregate (all-accounts) one.
75
76
  *
@@ -651,7 +652,21 @@ export class AppleMailManager {
651
652
  * Note: Mail.app message IDs are unique per mailbox. This method searches
652
653
  * all mailboxes in all accounts to find the message.
653
654
  */
654
- getMessageById(id) {
655
+ getMessageById(id, deepAttachmentCheck = false) {
656
+ // MIME-embedded attachments are invisible to AppleScript's `mail attachments`
657
+ // object, so the only way to detect them is to scan the raw `source of msg`.
658
+ // That reads the entire message (can be MB-sized), so it's the slowest part
659
+ // of this path — now opt-in via `deepAttachmentCheck` rather than run on
660
+ // every attachmentless message (#32). Default off: hasAttachments reflects
661
+ // the fast attachment count only.
662
+ const deepScan = deepAttachmentCheck
663
+ ? `if hasAtt is "false" then
664
+ try
665
+ set rawSrc to source of msg
666
+ if rawSrc contains "Content-Disposition: attachment" then set hasAtt to "true"
667
+ end try
668
+ end if`
669
+ : "";
655
670
  const script = buildAppLevelScript(`
656
671
  try
657
672
  repeat with acct in accounts
@@ -675,18 +690,7 @@ export class AppleMailManager {
675
690
  set attCount to count of mail attachments of msg
676
691
  if attCount > 0 then set hasAtt to "true"
677
692
  end try
678
- -- MIME-embedded attachments are invisible to AppleScript's
679
- -- attachment object. Fall back to scanning the raw source.
680
- -- This reads the full message source (can be MB-sized for
681
- -- messages with large bodies), so it's the slowest part of
682
- -- get-message for attachmentless messages. Accepted as the
683
- -- cost of correct hasAttachments in the detail view.
684
- if hasAtt is "false" then
685
- try
686
- set rawSrc to source of msg
687
- if rawSrc contains "Content-Disposition: attachment" then set hasAtt to "true"
688
- end try
689
- end if
693
+ ${deepScan}
690
694
  return msgSubject & "${FIELD_SEP}" & msgSender & "${FIELD_SEP}" & msgDate & "${FIELD_SEP}" & msgRead & "${FIELD_SEP}" & msgFlagged & "${FIELD_SEP}" & msgJunk & "${FIELD_SEP}" & msgDeleted & "${FIELD_SEP}" & msgMailbox & "${FIELD_SEP}" & msgAccount & "${FIELD_SEP}" & hasAtt
691
695
  end if
692
696
  end try
@@ -722,8 +726,20 @@ export class AppleMailManager {
722
726
  }
723
727
  /**
724
728
  * Get the content of a message.
729
+ *
730
+ * @param id - Message ID
731
+ * @param includeHtml - When true, also fetch the raw MIME source and extract
732
+ * the `text/html` body part into `htmlContent`. This is opt-in because the
733
+ * source can be MB-sized (it includes base64 attachments) and the plain-text
734
+ * path doesn't need it; fetching it unconditionally was both slow and, worse,
735
+ * returned the entire raw MIME blob mislabeled as HTML (#32).
725
736
  */
726
- getMessageContent(id) {
737
+ getMessageContent(id, includeHtml = false) {
738
+ // Only `source of msg` is fetched when HTML is requested. `content of msg`
739
+ // is the plain-text body and is always cheap.
740
+ const sourceFetch = includeHtml
741
+ ? `set htmlSource to ""\n try\n set htmlSource to source of msg\n end try`
742
+ : `set htmlSource to ""`;
727
743
  const script = buildAppLevelScript(`
728
744
  try
729
745
  repeat with acct in accounts
@@ -734,11 +750,8 @@ export class AppleMailManager {
734
750
  set msg to item 1 of matchingMsgs
735
751
  set msgSubject to subject of msg
736
752
  set msgContent to content of msg
737
- set htmlContent to ""
738
- try
739
- set htmlContent to source of msg
740
- end try
741
- return msgSubject & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlContent
753
+ ${sourceFetch}
754
+ return msgSubject & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlSource
742
755
  end if
743
756
  end try
744
757
  end repeat
@@ -755,15 +768,19 @@ export class AppleMailManager {
755
768
  }
756
769
  const htmlSplit = result.output.split(HTML_MARKER);
757
770
  const contentPart = htmlSplit[0];
758
- const htmlContent = htmlSplit.length > 1 ? htmlSplit[1] : undefined;
771
+ const rawSource = htmlSplit.length > 1 ? htmlSplit[1] : "";
759
772
  const parts = contentPart.split(CONTENT_MARKER);
760
773
  if (parts.length < 2)
761
774
  return null;
775
+ // Extract the actual text/html body from the raw MIME source rather than
776
+ // returning the whole source. Falls back to undefined when the message has
777
+ // no HTML part (e.g. a plain-text-only email).
778
+ const htmlContent = includeHtml && rawSource ? extractHtmlBody(rawSource) || undefined : undefined;
762
779
  return {
763
780
  id: id.toString(),
764
781
  subject: parts[0],
765
782
  plainText: parts[1],
766
- htmlContent: htmlContent || undefined,
783
+ htmlContent,
767
784
  };
768
785
  }
769
786
  /**
@@ -1466,90 +1483,174 @@ export class AppleMailManager {
1466
1483
  // Batch Operations
1467
1484
  // ===========================================================================
1468
1485
  /**
1469
- * Delete multiple messages at once.
1486
+ * Run one operation over many message IDs in a SINGLE osascript invocation.
1487
+ *
1488
+ * Previously each batch method looped and called the per-id method, so a
1489
+ * 100-id batch spawned 100 osascript processes — each one re-resolving
1490
+ * accounts and walking the whole account→mailbox tree — all serialized
1491
+ * through the gate (issue #31). This walks the tree exactly once: for each
1492
+ * mailbox it probes the still-pending IDs with `whose id is` (indexed, so
1493
+ * effectively free) and applies `operation` to any match, tracking found IDs
1494
+ * so it can stop early once all are accounted for. Per-id outcomes come back
1495
+ * as control-char-delimited `id<FS>status` records (status: `ok`,
1496
+ * `notfound`, or `error:<msg>`), and results are returned in input order.
1470
1497
  *
1471
- * @param ids - Array of message IDs to delete
1472
- * @returns Array of results for each message
1498
+ * `setup` runs once before the walk (used by move to resolve the destination);
1499
+ * it may bail the whole batch by returning a `BATCH_FATAL`-prefixed string.
1473
1500
  */
1474
- batchDeleteMessages(ids) {
1475
- const results = [];
1501
+ runBatchOperation(ids, operation, setup = "") {
1502
+ // Keep the numeric IDs paired with their original string form and 1-based
1503
+ // position. The AppleScript reports outcomes by POSITION, not by id: a Mail
1504
+ // id large enough to exceed AppleScript's 2^29 integer range coerces to
1505
+ // scientific notation under `as string` (999999999 -> "9.99999999E+8"), so
1506
+ // echoing the id back can't be matched to the input. Positions are always
1507
+ // small integers, so they round-trip cleanly.
1508
+ const valid = [];
1476
1509
  for (const id of ids) {
1477
- const success = this.deleteMessage(id);
1478
- results.push({
1479
- id,
1480
- success,
1481
- error: success ? undefined : "Failed to delete message",
1482
- });
1510
+ const num = Number(id);
1511
+ if (Number.isFinite(num))
1512
+ valid.push({ id, num });
1483
1513
  }
1484
- return results;
1514
+ if (valid.length === 0) {
1515
+ return ids.map((id) => ({ id, success: false, error: "Invalid message ID" }));
1516
+ }
1517
+ const script = buildAppLevelScript(`
1518
+ try
1519
+ ${setup}
1520
+ set _out to ""
1521
+ set _done to {}
1522
+ set _ids to {${valid.map((v) => v.num).join(", ")}}
1523
+ set _total to count of _ids
1524
+ repeat with acct in accounts
1525
+ if (count of _done) is _total then exit repeat
1526
+ repeat with mb in (mailboxes of acct)
1527
+ if (count of _done) is _total then exit repeat
1528
+ repeat with _idx from 1 to _total
1529
+ if _idx is not in _done then
1530
+ set _theId to item _idx of _ids
1531
+ try
1532
+ set _m to (messages of mb whose id is _theId)
1533
+ if (count of _m) > 0 then
1534
+ set _msg to item 1 of _m
1535
+ ${operation}
1536
+ set end of _done to _idx
1537
+ set _out to _out & (_idx as string) & "${FIELD_SEP}ok${RECORD_SEP}"
1538
+ end if
1539
+ on error _e
1540
+ set end of _done to _idx
1541
+ set _out to _out & (_idx as string) & "${FIELD_SEP}error:" & _e & "${RECORD_SEP}"
1542
+ end try
1543
+ end if
1544
+ end repeat
1545
+ end repeat
1546
+ end repeat
1547
+ repeat with _idx from 1 to _total
1548
+ if _idx is not in _done then set _out to _out & (_idx as string) & "${FIELD_SEP}notfound${RECORD_SEP}"
1549
+ end repeat
1550
+ return _out
1551
+ on error errMsg
1552
+ return "${BATCH_FATAL}" & errMsg
1553
+ end try
1554
+ `);
1555
+ // Generous timeout: one walk over the tree with indexed id probes. Scale a
1556
+ // little with batch size, capped.
1557
+ const timeoutMs = Math.min(180000, 60000 + valid.length * 500);
1558
+ const result = executeAppleScript(script, { timeoutMs });
1559
+ if (!result.success) {
1560
+ const err = result.error || "Batch operation failed";
1561
+ return ids.map((id) => ({ id, success: false, error: err }));
1562
+ }
1563
+ if (result.output.startsWith(BATCH_FATAL)) {
1564
+ const err = result.output.slice(BATCH_FATAL.length);
1565
+ return ids.map((id) => ({ id, success: false, error: err }));
1566
+ }
1567
+ // Map by-position outcomes back to the original id strings.
1568
+ const byId = new Map();
1569
+ for (const rec of result.output.split(RECORD_SEP)) {
1570
+ if (!rec)
1571
+ continue;
1572
+ const sep = rec.indexOf(FIELD_SEP);
1573
+ if (sep < 0)
1574
+ continue;
1575
+ const pos = Number(rec.slice(0, sep));
1576
+ const status = rec.slice(sep + FIELD_SEP.length);
1577
+ const entry = valid[pos - 1];
1578
+ if (!entry)
1579
+ continue;
1580
+ const id = entry.id;
1581
+ if (status === "ok") {
1582
+ byId.set(id, { id, success: true });
1583
+ }
1584
+ else if (status === "notfound") {
1585
+ byId.set(id, { id, success: false, error: "Message not found" });
1586
+ }
1587
+ else if (status.startsWith("error:")) {
1588
+ byId.set(id, { id, success: false, error: status.slice("error:".length) });
1589
+ }
1590
+ else {
1591
+ byId.set(id, { id, success: false, error: status || "Unknown error" });
1592
+ }
1593
+ }
1594
+ return ids.map((id) => byId.get(id) ??
1595
+ (Number.isFinite(Number(id))
1596
+ ? { id, success: false, error: "No result returned" }
1597
+ : { id, success: false, error: "Invalid message ID" }));
1485
1598
  }
1486
1599
  /**
1487
- * Move multiple messages to a mailbox at once.
1600
+ * Delete multiple messages at once (single tree walk — see runBatchOperation).
1601
+ */
1602
+ batchDeleteMessages(ids) {
1603
+ return this.runBatchOperation(ids, "delete _msg");
1604
+ }
1605
+ /**
1606
+ * Move multiple messages to a mailbox at once (single tree walk).
1488
1607
  *
1489
- * @param ids - Array of message IDs to move
1490
- * @param mailbox - Destination mailbox name
1491
- * @param account - Account containing the destination mailbox
1492
- * @returns Array of results for each message
1608
+ * The destination is resolved once (account-scoped, ambiguity-aware a name
1609
+ * matching more than one mailbox fails the whole batch rather than guessing),
1610
+ * then every matched message is moved in the same walk.
1493
1611
  */
1494
1612
  batchMoveMessages(ids, mailbox, account) {
1495
- const results = [];
1496
- for (const id of ids) {
1497
- const { success, error } = this.moveMessageInternal(id, mailbox, account);
1498
- results.push({
1499
- id,
1500
- success,
1501
- error: success ? undefined : error || "Failed to move message",
1502
- });
1503
- }
1504
- return results;
1613
+ const targetAccount = this.resolveAccount(account);
1614
+ const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
1615
+ const safeMailbox = escapeForAppleScript(targetMailbox);
1616
+ const safeAccount = escapeForAppleScript(targetAccount);
1617
+ // Resolved once, before the walk. `mailboxes of account` is already flat
1618
+ // (includes nested mailboxes by path), so we match by exact name and use the
1619
+ // reference directly. A bad/ambiguous destination fails the whole batch.
1620
+ const setup = `
1621
+ set destName to "${safeMailbox}"
1622
+ set destMatches to {}
1623
+ repeat with _dmb in (mailboxes of account "${safeAccount}")
1624
+ if (name of _dmb) is destName then set end of destMatches to _dmb
1625
+ end repeat
1626
+ if (count of destMatches) is 0 then return "${BATCH_FATAL}Destination mailbox \\"" & destName & "\\" not found in account \\"${safeAccount}\\""
1627
+ if (count of destMatches) > 1 then return "${BATCH_FATAL}Destination mailbox \\"" & destName & "\\" is ambiguous (" & (count of destMatches) & " matches) in account \\"${safeAccount}\\"; move by full path"
1628
+ set destMailbox to item 1 of destMatches`;
1629
+ return this.runBatchOperation(ids, "move _msg to destMailbox", setup);
1505
1630
  }
1506
1631
  /**
1507
- * Mark multiple messages as read at once.
1632
+ * Mark multiple messages as read at once (single tree walk).
1508
1633
  */
1509
1634
  batchMarkAsRead(ids) {
1510
- const results = [];
1511
- for (const id of ids) {
1512
- const success = this.markAsRead(id);
1513
- results.push({ id, success, error: success ? undefined : "Failed to mark message as read" });
1514
- }
1515
- return results;
1635
+ return this.runBatchOperation(ids, "set read status of _msg to true");
1516
1636
  }
1517
1637
  /**
1518
- * Mark multiple messages as unread at once.
1638
+ * Mark multiple messages as unread at once (single tree walk).
1519
1639
  */
1520
1640
  batchMarkAsUnread(ids) {
1521
- const results = [];
1522
- for (const id of ids) {
1523
- const success = this.markAsUnread(id);
1524
- results.push({
1525
- id,
1526
- success,
1527
- error: success ? undefined : "Failed to mark message as unread",
1528
- });
1529
- }
1530
- return results;
1641
+ return this.runBatchOperation(ids, "set read status of _msg to false");
1531
1642
  }
1532
1643
  /**
1533
- * Flag multiple messages at once.
1644
+ * Flag multiple messages at once (single tree walk).
1534
1645
  */
1535
1646
  batchFlagMessages(ids) {
1536
- const results = [];
1537
- for (const id of ids) {
1538
- const success = this.flagMessage(id);
1539
- results.push({ id, success, error: success ? undefined : "Failed to flag message" });
1540
- }
1541
- return results;
1647
+ return this.runBatchOperation(ids, "set flagged status of _msg to true");
1542
1648
  }
1543
1649
  /**
1544
- * Unflag multiple messages at once.
1650
+ * Unflag multiple messages at once (single tree walk).
1545
1651
  */
1546
1652
  batchUnflagMessages(ids) {
1547
- const results = [];
1548
- for (const id of ids) {
1549
- const success = this.unflagMessage(id);
1550
- results.push({ id, success, error: success ? undefined : "Failed to unflag message" });
1551
- }
1552
- return results;
1653
+ return this.runBatchOperation(ids, "set flagged status of _msg to false");
1553
1654
  }
1554
1655
  /**
1555
1656
  * List attachments for a message.
@@ -28,6 +28,21 @@ export interface MimeAttachmentData extends MimeAttachmentInfo {
28
28
  * @returns Array of attachment metadata (name, mimeType, size)
29
29
  */
30
30
  export declare function parseMimeAttachments(source: string): MimeAttachmentInfo[];
31
+ /**
32
+ * Extract the decoded `text/html` body from raw MIME source.
33
+ *
34
+ * Used by get-message's `preferHtml` path so it returns the actual HTML body
35
+ * rather than the entire raw MIME blob (headers + base64 attachments), which is
36
+ * both wrong and enormous (#32). Handles both multipart messages (walks leaf
37
+ * parts, descending into nested multipart/* containers) and a non-multipart
38
+ * message whose top-level Content-Type is text/html. Bodies are decoded per
39
+ * Content-Transfer-Encoding (base64 / quoted-printable / raw) and returned as
40
+ * UTF-8 text.
41
+ *
42
+ * @param source - Raw MIME source of the email
43
+ * @returns The decoded HTML body, or null if the message has no text/html part
44
+ */
45
+ export declare function extractHtmlBody(source: string): string | null;
31
46
  /**
32
47
  * Extract and decode a specific attachment from MIME source by filename.
33
48
  * Supports base64, quoted-printable, and 7bit/8bit/binary transfer encodings.
@@ -1 +1 @@
1
- {"version":3,"file":"mimeParse.d.ts","sourceRoot":"","sources":["../../src/utils/mimeParse.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAmB,SAAQ,kBAAkB;IAC5D,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;CACd;AAyLD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAyBzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,kBAAkB,GAAG,IAAI,CAwB3B"}
1
+ {"version":3,"file":"mimeParse.d.ts","sourceRoot":"","sources":["../../src/utils/mimeParse.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,kBAAmB,SAAQ,kBAAkB;IAC5D,6BAA6B;IAC7B,IAAI,EAAE,MAAM,CAAC;CACd;AAyLD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAyBzE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAuB7D;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GACrB,kBAAkB,GAAG,IAAI,CAwB3B"}
@@ -205,6 +205,44 @@ export function parseMimeAttachments(source) {
205
205
  }
206
206
  return attachments;
207
207
  }
208
+ /**
209
+ * Extract the decoded `text/html` body from raw MIME source.
210
+ *
211
+ * Used by get-message's `preferHtml` path so it returns the actual HTML body
212
+ * rather than the entire raw MIME blob (headers + base64 attachments), which is
213
+ * both wrong and enormous (#32). Handles both multipart messages (walks leaf
214
+ * parts, descending into nested multipart/* containers) and a non-multipart
215
+ * message whose top-level Content-Type is text/html. Bodies are decoded per
216
+ * Content-Transfer-Encoding (base64 / quoted-printable / raw) and returned as
217
+ * UTF-8 text.
218
+ *
219
+ * @param source - Raw MIME source of the email
220
+ * @returns The decoded HTML body, or null if the message has no text/html part
221
+ */
222
+ export function extractHtmlBody(source) {
223
+ if (!source || !source.trim())
224
+ return null;
225
+ const boundary = extractBoundary(source);
226
+ if (boundary) {
227
+ for (const part of walkLeafParts(source, boundary)) {
228
+ if (extractMimeType(part.headers) === "text/html") {
229
+ const encoding = getHeader(part.headers, "Content-Transfer-Encoding");
230
+ return decodeBody(part.body, encoding).toString("utf8");
231
+ }
232
+ }
233
+ return null;
234
+ }
235
+ // Non-multipart: split top headers from body and check the top Content-Type.
236
+ const blankLineIdx = source.search(/\r?\n\r?\n/);
237
+ if (blankLineIdx === -1)
238
+ return null;
239
+ const headers = source.substring(0, blankLineIdx);
240
+ if (extractMimeType(headers) !== "text/html")
241
+ return null;
242
+ const body = source.substring(blankLineIdx).replace(/^\r?\n\r?\n/, "");
243
+ const encoding = getHeader(headers, "Content-Transfer-Encoding");
244
+ return decodeBody(body, encoding).toString("utf8");
245
+ }
208
246
  /**
209
247
  * Extract and decode a specific attachment from MIME source by filename.
210
248
  * Supports base64, quoted-printable, and 7bit/8bit/binary transfer encodings.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "1.6.5",
3
+ "version": "1.6.7",
4
4
  "description": "MCP server for Apple Mail - read, search, send, and manage emails via Claude",
5
5
  "type": "module",
6
6
  "main": "build/index.js",