apple-mail-mcp 1.3.0 → 1.5.0

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
@@ -724,6 +724,16 @@ If installed from source, use this configuration:
724
724
  | Attachment save path restrictions | `save-attachment` only allows saving to home directory, `/tmp`, `/private/tmp`, and `/Volumes`; path traversal is blocked |
725
725
  | Attachment count limit | `send-email` and `create-draft` accept a maximum of 20 file attachments |
726
726
 
727
+ ### Reply / Forward from Background Processes (Fixed in v1.4.0)
728
+
729
+ Prior to v1.4.0, `reply-to-message` and `forward-message` would send messages with **empty body text** when the MCP server ran as a background process (e.g., spawned via `execSync` from Node.js, which is how Claude Code invokes it).
730
+
731
+ **Root cause:** The AppleScript `reply msg with opening window` command creates a GUI compose window asynchronously. When `set content` runs immediately after, the window may not be ready, and the content assignment is silently ignored. Delays (`delay 1`, `delay 2`) were unreliable — the compose window's readiness depends on system load, Mail.app state, and whether the process has GUI access.
732
+
733
+ **Fix:** Replaced `with opening window` with `without opening window` for both `reply` and `forward` commands. With this approach, `set content` works immediately and reliably from background processes. `In-Reply-To` and `References` headers are still set correctly by Mail.app, and no GUI compose window is opened.
734
+
735
+ See [#7](https://github.com/sweetrb/apple-mail-mcp/issues/7) for full details and the list of approaches that were tested.
736
+
727
737
  ### Backslash Escaping (Important for AI Agents)
728
738
 
729
739
  When sending content containing backslashes (`\`) to this MCP server, **you must escape them as `\\`** in the JSON parameters.
@@ -100,6 +100,16 @@ export declare class AppleMailManager {
100
100
  * Get the content of a message.
101
101
  */
102
102
  getMessageContent(id: string): MessageContent | null;
103
+ /**
104
+ * Get the raw MIME source of a message.
105
+ * Used as fallback for attachment extraction when AppleScript
106
+ * mail attachments returns empty.
107
+ *
108
+ * Timeout is 2x the default (120s) because `source of msg` returns
109
+ * the entire raw message including base64-encoded attachments —
110
+ * a 20MB attachment can take several seconds over Exchange/IMAP.
111
+ */
112
+ getRawSource(id: string): string | null;
103
113
  /**
104
114
  * List messages in a mailbox.
105
115
  *
@@ -111,6 +121,14 @@ export declare class AppleMailManager {
111
121
  listMessages(mailbox?: string, account?: string, limit?: number, from?: string, offset?: number): Message[];
112
122
  /**
113
123
  * Parse message list output from AppleScript.
124
+ *
125
+ * Two emission schemas, disambiguated by length:
126
+ * 7 fields: single-mailbox — ...|hasAtt (mailbox from caller)
127
+ * 8 fields: all-mailboxes — ...|mailbox|hasAtt
128
+ *
129
+ * `hasAttachments` here is the fast-path AppleScript count only; it will
130
+ * false-negative for MIME-embedded attachments (a known AppleScript
131
+ * limitation). Use getMessage or list-attachments for authoritative info.
114
132
  */
115
133
  private parseMessageList;
116
134
  /**
@@ -233,10 +251,14 @@ export declare class AppleMailManager {
233
251
  batchUnflagMessages(ids: string[]): BatchOperationResult[];
234
252
  /**
235
253
  * List attachments for a message.
254
+ * Tries AppleScript first, falls back to MIME source parsing
255
+ * when AppleScript returns empty (known issue across all account types).
236
256
  */
237
257
  listAttachments(id: string): Attachment[];
238
258
  /**
239
259
  * Save an attachment from a message to disk.
260
+ * Tries AppleScript first, falls back to MIME source extraction
261
+ * when AppleScript can't find the attachment.
240
262
  */
241
263
  saveAttachment(id: string, attachmentName: string, savePath: string): boolean;
242
264
  /**
@@ -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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"}
@@ -13,10 +13,11 @@
13
13
  * @module services/appleMailManager
14
14
  */
15
15
  import { spawnSync } from "child_process";
16
- import { existsSync } from "fs";
16
+ 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
21
  // =============================================================================
21
22
  // Text Processing Utilities
22
23
  // =============================================================================
@@ -423,7 +424,24 @@ export class AppleMailManager {
423
424
  set msgDeleted to deleted status of msg as string
424
425
  set msgMailbox to name of mb
425
426
  set msgAccount to name of acct
426
- return msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged & "|||" & msgJunk & "|||" & msgDeleted & "|||" & msgMailbox & "|||" & msgAccount
427
+ set hasAtt to "false"
428
+ try
429
+ set attCount to count of mail attachments of msg
430
+ if attCount > 0 then set hasAtt to "true"
431
+ end try
432
+ -- MIME-embedded attachments are invisible to AppleScript's
433
+ -- attachment object. Fall back to scanning the raw source.
434
+ -- This reads the full message source (can be MB-sized for
435
+ -- messages with large bodies), so it's the slowest part of
436
+ -- get-message for attachmentless messages. Accepted as the
437
+ -- cost of correct hasAttachments in the detail view.
438
+ if hasAtt is "false" then
439
+ try
440
+ set rawSrc to source of msg
441
+ if rawSrc contains "Content-Disposition: attachment" then set hasAtt to "true"
442
+ end try
443
+ end if
444
+ return msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged & "|||" & msgJunk & "|||" & msgDeleted & "|||" & msgMailbox & "|||" & msgAccount & "|||" & hasAtt
427
445
  end if
428
446
  end try
429
447
  end repeat
@@ -453,7 +471,7 @@ export class AppleMailManager {
453
471
  isDeleted: parts[6] === "true",
454
472
  mailbox: parts[7],
455
473
  account: parts[8],
456
- hasAttachments: false,
474
+ hasAttachments: parts.length > 9 ? parts[9] === "true" : false,
457
475
  };
458
476
  }
459
477
  /**
@@ -502,6 +520,40 @@ export class AppleMailManager {
502
520
  htmlContent: htmlContent || undefined,
503
521
  };
504
522
  }
523
+ /**
524
+ * Get the raw MIME source of a message.
525
+ * Used as fallback for attachment extraction when AppleScript
526
+ * mail attachments returns empty.
527
+ *
528
+ * Timeout is 2x the default (120s) because `source of msg` returns
529
+ * the entire raw message including base64-encoded attachments —
530
+ * a 20MB attachment can take several seconds over Exchange/IMAP.
531
+ */
532
+ getRawSource(id) {
533
+ const script = buildAppLevelScript(`
534
+ try
535
+ repeat with acct in accounts
536
+ repeat with mb in mailboxes of acct
537
+ try
538
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
539
+ if (count of matchingMsgs) > 0 then
540
+ set msg to item 1 of matchingMsgs
541
+ return source of msg
542
+ end if
543
+ end try
544
+ end repeat
545
+ end repeat
546
+ return ""
547
+ on error errMsg
548
+ return ""
549
+ end try
550
+ `);
551
+ const result = executeAppleScript(script, { timeoutMs: 120000 });
552
+ if (!result.success || !result.output.trim()) {
553
+ return null;
554
+ }
555
+ return result.output;
556
+ }
505
557
  /**
506
558
  * List messages in a mailbox.
507
559
  *
@@ -549,8 +601,12 @@ export class AppleMailManager {
549
601
  set msgDate to ${AS_DATE_TO_STRING}
550
602
  set msgRead to read status of msg as string
551
603
  set msgFlagged to flagged status of msg as string
604
+ set msgHasAtt to "false"
605
+ try
606
+ if (count of mail attachments of msg) > 0 then set msgHasAtt to "true"
607
+ end try
552
608
  if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
553
- set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged
609
+ set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged & "|||" & msgHasAtt
554
610
  set msgCount to msgCount + 1
555
611
  end if
556
612
  end try
@@ -583,8 +639,12 @@ export class AppleMailManager {
583
639
  set msgDate to ${AS_DATE_TO_STRING}
584
640
  set msgRead to read status of msg as string
585
641
  set msgFlagged to flagged status of msg as string
642
+ set msgHasAtt to "false"
643
+ try
644
+ if (count of mail attachments of msg) > 0 then set msgHasAtt to "true"
645
+ end try
586
646
  if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
587
- set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged & "|||" & name of mb
647
+ set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged & "|||" & name of mb & "|||" & msgHasAtt
588
648
  set msgCount to msgCount + 1
589
649
  end if
590
650
  end if
@@ -607,6 +667,14 @@ export class AppleMailManager {
607
667
  }
608
668
  /**
609
669
  * Parse message list output from AppleScript.
670
+ *
671
+ * Two emission schemas, disambiguated by length:
672
+ * 7 fields: single-mailbox — ...|hasAtt (mailbox from caller)
673
+ * 8 fields: all-mailboxes — ...|mailbox|hasAtt
674
+ *
675
+ * `hasAttachments` here is the fast-path AppleScript count only; it will
676
+ * false-negative for MIME-embedded attachments (a known AppleScript
677
+ * limitation). Use getMessage or list-attachments for authoritative info.
610
678
  */
611
679
  parseMessageList(output, mailbox, account) {
612
680
  const items = output.split("|||ITEM|||");
@@ -615,6 +683,15 @@ export class AppleMailManager {
615
683
  const parts = item.split("|||");
616
684
  if (parts.length < 6)
617
685
  continue;
686
+ let msgMailbox = mailbox;
687
+ let hasAttachments = false;
688
+ if (parts.length >= 8) {
689
+ msgMailbox = parts[6];
690
+ hasAttachments = parts[7] === "true";
691
+ }
692
+ else if (parts.length === 7) {
693
+ hasAttachments = parts[6] === "true";
694
+ }
618
695
  messages.push({
619
696
  id: parts[0].trim(),
620
697
  subject: parts[1],
@@ -625,9 +702,9 @@ export class AppleMailManager {
625
702
  isFlagged: parts[5] === "true",
626
703
  isJunk: false,
627
704
  isDeleted: false,
628
- mailbox: parts.length >= 7 ? parts[6] : mailbox,
705
+ mailbox: msgMailbox,
629
706
  account,
630
- hasAttachments: false,
707
+ hasAttachments,
631
708
  });
632
709
  }
633
710
  return messages;
@@ -826,8 +903,8 @@ export class AppleMailManager {
826
903
  set matchingMsgs to (messages of mb whose id is ${Number(id)})
827
904
  if (count of matchingMsgs) > 0 then
828
905
  set msg to item 1 of matchingMsgs
829
- set theReply to reply msg with opening window${replyAllClause}
830
- set content of theReply to "${safeBody}" & return & return & content of theReply
906
+ set theReply to reply msg without opening window${replyAllClause}
907
+ set content of theReply to "${safeBody}"
831
908
  ${sendAction}
832
909
  return "ok"
833
910
  end if
@@ -871,9 +948,9 @@ export class AppleMailManager {
871
948
  set matchingMsgs to (messages of mb whose id is ${Number(id)})
872
949
  if (count of matchingMsgs) > 0 then
873
950
  set msg to item 1 of matchingMsgs
874
- set theForward to forward msg with opening window
951
+ set theForward to forward msg without opening window
875
952
  ${recipientCommands}
876
- ${safeBody ? `set content of theForward to "${safeBody}" & return & return & content of theForward` : ""}
953
+ ${safeBody ? `set content of theForward to "${safeBody}"` : ""}
877
954
  ${sendAction}
878
955
  return "ok"
879
956
  end if
@@ -1102,8 +1179,11 @@ export class AppleMailManager {
1102
1179
  }
1103
1180
  /**
1104
1181
  * List attachments for a message.
1182
+ * Tries AppleScript first, falls back to MIME source parsing
1183
+ * when AppleScript returns empty (known issue across all account types).
1105
1184
  */
1106
1185
  listAttachments(id) {
1186
+ // Attempt 1: AppleScript mail attachments
1107
1187
  const script = buildAppLevelScript(`
1108
1188
  try
1109
1189
  repeat with acct in accounts
@@ -1133,26 +1213,39 @@ export class AppleMailManager {
1133
1213
  end try
1134
1214
  `);
1135
1215
  const result = executeAppleScript(script, { timeoutMs: 60000 });
1136
- if (!result.success || !result.output.trim()) {
1137
- return [];
1138
- }
1139
- const items = result.output.split("|||ITEM|||");
1140
- const attachments = [];
1141
- for (const item of items) {
1142
- const parts = item.split("|||");
1143
- if (parts.length < 3)
1144
- continue;
1145
- attachments.push({
1146
- id: `${id}-${parts[0]}`,
1147
- name: parts[0],
1148
- mimeType: parts[1],
1149
- size: parseInt(parts[2]) || 0,
1150
- });
1216
+ if (result.success && result.output.trim()) {
1217
+ const items = result.output.split("|||ITEM|||");
1218
+ const attachments = [];
1219
+ for (const item of items) {
1220
+ const parts = item.split("|||");
1221
+ if (parts.length < 3)
1222
+ continue;
1223
+ attachments.push({
1224
+ id: `${id}-${parts[0]}`,
1225
+ name: parts[0],
1226
+ mimeType: parts[1],
1227
+ size: parseInt(parts[2]) || 0,
1228
+ });
1229
+ }
1230
+ if (attachments.length > 0)
1231
+ return attachments;
1151
1232
  }
1152
- return attachments;
1233
+ // Attempt 2: MIME source fallback
1234
+ const rawSource = this.getRawSource(id);
1235
+ if (!rawSource)
1236
+ return [];
1237
+ const mimeAttachments = parseMimeAttachments(rawSource);
1238
+ return mimeAttachments.map((att) => ({
1239
+ id: `${id}-${att.name}`,
1240
+ name: att.name,
1241
+ mimeType: att.mimeType,
1242
+ size: att.size,
1243
+ }));
1153
1244
  }
1154
1245
  /**
1155
1246
  * Save an attachment from a message to disk.
1247
+ * Tries AppleScript first, falls back to MIME source extraction
1248
+ * when AppleScript can't find the attachment.
1156
1249
  */
1157
1250
  saveAttachment(id, attachmentName, savePath) {
1158
1251
  // Validate attachment name: block path separators, traversal, null bytes, and backslashes
@@ -1170,9 +1263,8 @@ export class AppleMailManager {
1170
1263
  }
1171
1264
  const safeName = escapeForAppleScript(attachmentName);
1172
1265
  const safePath = escapeForAppleScript(resolvedPath);
1173
- // Use Number(id) as defense-in-depth — the Zod schema already enforces numeric IDs,
1174
- // but this ensures raw interpolation into AppleScript is safe even if validation changes.
1175
1266
  const numericId = Number(id);
1267
+ // Attempt 1: AppleScript save
1176
1268
  const script = buildAppLevelScript(`
1177
1269
  try
1178
1270
  repeat with acct in accounts
@@ -1199,11 +1291,35 @@ export class AppleMailManager {
1199
1291
  end try
1200
1292
  `);
1201
1293
  const result = executeAppleScript(script, { timeoutMs: 60000 });
1202
- if (!result.success || result.output.startsWith("error:")) {
1203
- console.error(`Failed to save attachment: ${result.error || result.output}`);
1294
+ if (result.success && result.output === "ok") {
1295
+ return true;
1296
+ }
1297
+ // Attempt 2: MIME source fallback
1298
+ const rawSource = this.getRawSource(id);
1299
+ if (!rawSource) {
1300
+ console.error(`Failed to save attachment: could not retrieve message source`);
1301
+ return false;
1302
+ }
1303
+ const attachment = extractMimeAttachment(rawSource, attachmentName);
1304
+ if (!attachment) {
1305
+ console.error(`Failed to save attachment: "${attachmentName}" not found in MIME source`);
1306
+ return false;
1307
+ }
1308
+ try {
1309
+ const outPath = resolve(resolvedPath, attachmentName);
1310
+ // Verify the resolved output path is still within allowed directories
1311
+ const isOutAllowed = allowedPrefixes.some((prefix) => outPath.startsWith(prefix));
1312
+ if (!isOutAllowed) {
1313
+ console.error(`Output path "${outPath}" is outside allowed directories`);
1314
+ return false;
1315
+ }
1316
+ writeFileSync(outPath, attachment.data);
1317
+ return true;
1318
+ }
1319
+ catch (err) {
1320
+ console.error(`Failed to write attachment to disk: ${err}`);
1204
1321
  return false;
1205
1322
  }
1206
- return true;
1207
1323
  }
1208
1324
  // ===========================================================================
1209
1325
  // Mailbox Operations
@@ -0,0 +1,41 @@
1
+ /**
2
+ * MIME Source Parser for Attachment Extraction
3
+ *
4
+ * Parses raw email MIME source to extract attachment metadata and content.
5
+ * Used as a fallback when AppleScript's `mail attachments` returns empty
6
+ * (which happens across all account types: iCloud, Google, Exchange).
7
+ *
8
+ * @module utils/mimeParse
9
+ */
10
+ export interface MimeAttachmentInfo {
11
+ /** Filename from Content-Disposition or Content-Type name parameter */
12
+ name: string;
13
+ /** MIME type from Content-Type header */
14
+ mimeType: string;
15
+ /** Size in bytes from Content-Disposition size parameter, or estimated from body */
16
+ size: number;
17
+ }
18
+ export interface MimeAttachmentData extends MimeAttachmentInfo {
19
+ /** Decoded binary content */
20
+ data: Buffer;
21
+ }
22
+ /**
23
+ * Parse MIME source and return metadata for all file attachments.
24
+ * Skips inline dispositions (signature images, etc.). Descends into
25
+ * nested multipart/* containers.
26
+ *
27
+ * @param source - Raw MIME source of the email
28
+ * @returns Array of attachment metadata (name, mimeType, size)
29
+ */
30
+ export declare function parseMimeAttachments(source: string): MimeAttachmentInfo[];
31
+ /**
32
+ * Extract and decode a specific attachment from MIME source by filename.
33
+ * Supports base64, quoted-printable, and 7bit/8bit/binary transfer encodings.
34
+ * Descends into nested multipart/* containers.
35
+ *
36
+ * @param source - Raw MIME source of the email
37
+ * @param attachmentName - Filename to extract
38
+ * @returns Decoded attachment data, or null if not found
39
+ */
40
+ export declare function extractMimeAttachment(source: string, attachmentName: string): MimeAttachmentData | null;
41
+ //# sourceMappingURL=mimeParse.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,238 @@
1
+ /**
2
+ * MIME Source Parser for Attachment Extraction
3
+ *
4
+ * Parses raw email MIME source to extract attachment metadata and content.
5
+ * Used as a fallback when AppleScript's `mail attachments` returns empty
6
+ * (which happens across all account types: iCloud, Google, Exchange).
7
+ *
8
+ * @module utils/mimeParse
9
+ */
10
+ /**
11
+ * Extract the boundary string from a Content-Type header value
12
+ * (or from any string containing a boundary= parameter).
13
+ */
14
+ function extractBoundary(source) {
15
+ const match = source.match(/boundary="?([^";\s\r\n]+)"?/i);
16
+ return match ? match[1] : null;
17
+ }
18
+ /**
19
+ * Extract a header value from a MIME part header block.
20
+ * Handles folded headers (continuation lines starting with whitespace).
21
+ */
22
+ function getHeader(headers, name) {
23
+ const regex = new RegExp(`^${name}:\\s*(.+(?:\\r?\\n[ \\t]+.+)*)`, "im");
24
+ const match = headers.match(regex);
25
+ if (!match)
26
+ return null;
27
+ // Unfold: replace newline+whitespace with single space
28
+ return match[1].replace(/\r?\n[ \t]+/g, " ").trim();
29
+ }
30
+ /**
31
+ * Extract filename from Content-Disposition or Content-Type headers.
32
+ */
33
+ function extractFilename(headers) {
34
+ // Try Content-Disposition filename first
35
+ const dispHeader = getHeader(headers, "Content-Disposition");
36
+ if (dispHeader) {
37
+ const fnMatch = dispHeader.match(/filename="?([^";\r\n]+)"?/i);
38
+ if (fnMatch)
39
+ return fnMatch[1].trim();
40
+ }
41
+ // Fall back to Content-Type name parameter
42
+ const ctHeader = getHeader(headers, "Content-Type");
43
+ if (ctHeader) {
44
+ const nameMatch = ctHeader.match(/name="?([^";\r\n]+)"?/i);
45
+ if (nameMatch)
46
+ return nameMatch[1].trim();
47
+ }
48
+ return null;
49
+ }
50
+ /**
51
+ * Check if a MIME part has inline disposition (not a real attachment).
52
+ */
53
+ function isInlineDisposition(headers) {
54
+ const dispHeader = getHeader(headers, "Content-Disposition");
55
+ if (!dispHeader)
56
+ return false;
57
+ return dispHeader.toLowerCase().startsWith("inline");
58
+ }
59
+ /**
60
+ * Extract size from Content-Disposition size parameter.
61
+ */
62
+ function extractSize(headers) {
63
+ const dispHeader = getHeader(headers, "Content-Disposition");
64
+ if (dispHeader) {
65
+ const sizeMatch = dispHeader.match(/size=(\d+)/i);
66
+ if (sizeMatch)
67
+ return parseInt(sizeMatch[1], 10);
68
+ }
69
+ return 0;
70
+ }
71
+ /**
72
+ * Extract MIME type from Content-Type header.
73
+ */
74
+ function extractMimeType(headers) {
75
+ const ctHeader = getHeader(headers, "Content-Type");
76
+ if (!ctHeader)
77
+ return "application/octet-stream";
78
+ const typeMatch = ctHeader.match(/^([^;\s]+)/);
79
+ return typeMatch ? typeMatch[1].toLowerCase() : "application/octet-stream";
80
+ }
81
+ /**
82
+ * Estimate decoded size from base64 content length.
83
+ */
84
+ function estimateBase64Size(base64Body) {
85
+ const cleaned = base64Body.replace(/[\s\r\n]/g, "");
86
+ return Math.floor((cleaned.length * 3) / 4);
87
+ }
88
+ /**
89
+ * Split a MIME block into parts using the given boundary.
90
+ * Does not recurse — call walkLeafParts for recursive traversal.
91
+ */
92
+ function splitMimeParts(source, boundary) {
93
+ const parts = [];
94
+ const boundaryDelim = `--${boundary}`;
95
+ const sections = source.split(boundaryDelim);
96
+ for (const section of sections) {
97
+ const trimmed = section.trim();
98
+ if (!trimmed || trimmed.startsWith("--"))
99
+ continue;
100
+ // Split headers from body at first blank line
101
+ const blankLineIdx = trimmed.search(/\r?\n\r?\n/);
102
+ if (blankLineIdx === -1)
103
+ continue;
104
+ const headers = trimmed.substring(0, blankLineIdx);
105
+ const body = trimmed.substring(blankLineIdx).replace(/^\r?\n\r?\n/, "");
106
+ parts.push({ headers, body });
107
+ }
108
+ return parts;
109
+ }
110
+ /**
111
+ * Walk a multipart MIME block and return all non-multipart leaf parts,
112
+ * descending into nested multipart/* containers (alternative, related, mixed).
113
+ */
114
+ function walkLeafParts(source, boundary) {
115
+ const result = [];
116
+ const parts = splitMimeParts(source, boundary);
117
+ for (const part of parts) {
118
+ const ct = getHeader(part.headers, "Content-Type");
119
+ if (ct && /^multipart\//i.test(ct)) {
120
+ const nestedBoundary = extractBoundary(ct);
121
+ if (nestedBoundary) {
122
+ result.push(...walkLeafParts(part.body, nestedBoundary));
123
+ continue;
124
+ }
125
+ }
126
+ result.push(part);
127
+ }
128
+ return result;
129
+ }
130
+ /**
131
+ * Decode a MIME part body to bytes based on its transfer encoding.
132
+ * Supports base64, quoted-printable, and 7bit/8bit/binary (raw).
133
+ */
134
+ function decodeBody(body, encoding) {
135
+ const enc = (encoding || "").toLowerCase().trim();
136
+ if (enc === "base64") {
137
+ return Buffer.from(body.replace(/[\s\r\n]/g, ""), "base64");
138
+ }
139
+ if (enc === "quoted-printable") {
140
+ return decodeQuotedPrintable(body);
141
+ }
142
+ // 7bit, 8bit, binary, or unspecified — treat as raw bytes
143
+ return Buffer.from(body, "binary");
144
+ }
145
+ /**
146
+ * Decode quoted-printable-encoded body to bytes.
147
+ * Handles soft line breaks (=<CRLF>) and =XX hex escapes per RFC 2045 §6.7.
148
+ */
149
+ function decodeQuotedPrintable(body) {
150
+ // Remove soft line breaks: `=` immediately followed by CRLF or LF
151
+ const noSoft = body.replace(/=\r?\n/g, "");
152
+ const bytes = [];
153
+ for (let i = 0; i < noSoft.length; i++) {
154
+ const c = noSoft[i];
155
+ if (c === "=" && i + 2 < noSoft.length) {
156
+ const hex = noSoft.substring(i + 1, i + 3);
157
+ if (/^[0-9A-Fa-f]{2}$/.test(hex)) {
158
+ bytes.push(parseInt(hex, 16));
159
+ i += 2;
160
+ continue;
161
+ }
162
+ }
163
+ bytes.push(c.charCodeAt(0) & 0xff);
164
+ }
165
+ return Buffer.from(bytes);
166
+ }
167
+ /**
168
+ * Estimate body size for metadata when Content-Disposition size is absent.
169
+ */
170
+ function estimateSize(body, encoding) {
171
+ const enc = (encoding || "").toLowerCase().trim();
172
+ if (enc === "base64")
173
+ return estimateBase64Size(body);
174
+ // For other encodings the body length is a reasonable proxy
175
+ return body.length;
176
+ }
177
+ /**
178
+ * Parse MIME source and return metadata for all file attachments.
179
+ * Skips inline dispositions (signature images, etc.). Descends into
180
+ * nested multipart/* containers.
181
+ *
182
+ * @param source - Raw MIME source of the email
183
+ * @returns Array of attachment metadata (name, mimeType, size)
184
+ */
185
+ export function parseMimeAttachments(source) {
186
+ if (!source || !source.trim())
187
+ return [];
188
+ const boundary = extractBoundary(source);
189
+ if (!boundary)
190
+ return [];
191
+ const parts = walkLeafParts(source, boundary);
192
+ const attachments = [];
193
+ for (const part of parts) {
194
+ const filename = extractFilename(part.headers);
195
+ if (!filename)
196
+ continue;
197
+ if (isInlineDisposition(part.headers))
198
+ continue;
199
+ const encoding = getHeader(part.headers, "Content-Transfer-Encoding");
200
+ attachments.push({
201
+ name: filename,
202
+ mimeType: extractMimeType(part.headers),
203
+ size: extractSize(part.headers) || estimateSize(part.body, encoding),
204
+ });
205
+ }
206
+ return attachments;
207
+ }
208
+ /**
209
+ * Extract and decode a specific attachment from MIME source by filename.
210
+ * Supports base64, quoted-printable, and 7bit/8bit/binary transfer encodings.
211
+ * Descends into nested multipart/* containers.
212
+ *
213
+ * @param source - Raw MIME source of the email
214
+ * @param attachmentName - Filename to extract
215
+ * @returns Decoded attachment data, or null if not found
216
+ */
217
+ export function extractMimeAttachment(source, attachmentName) {
218
+ if (!source || !source.trim())
219
+ return null;
220
+ const boundary = extractBoundary(source);
221
+ if (!boundary)
222
+ return null;
223
+ const parts = walkLeafParts(source, boundary);
224
+ for (const part of parts) {
225
+ const filename = extractFilename(part.headers);
226
+ if (filename !== attachmentName)
227
+ continue;
228
+ const encoding = getHeader(part.headers, "Content-Transfer-Encoding");
229
+ const data = decodeBody(part.body, encoding);
230
+ return {
231
+ name: filename,
232
+ mimeType: extractMimeType(part.headers),
233
+ size: extractSize(part.headers) || data.length,
234
+ data,
235
+ };
236
+ }
237
+ return null;
238
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
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",
@@ -27,6 +27,7 @@
27
27
  "format": "prettier --write src",
28
28
  "format:check": "prettier --check src",
29
29
  "typecheck": "tsc --noEmit",
30
+ "version": "node -e \"const p=require('./package.json'); const f='.claude-plugin/plugin.json'; const c=JSON.parse(require('fs').readFileSync(f,'utf8')); c.version=p.version; require('fs').writeFileSync(f,JSON.stringify(c,null,2)+'\\n')\" && git add .claude-plugin/plugin.json",
30
31
  "prepublishOnly": "npm run lint && npm run test && npm run build",
31
32
  "prepare": "husky && npm run build"
32
33
  },