@sempervirens-labs/apple-mail-mcp 1.4.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
@@ -181,6 +181,57 @@ Get recent emails from a mailbox.
181
181
  }
182
182
  ```
183
183
 
184
+ ### `mail_get_emails_by_ids`
185
+
186
+ Get specific emails by their IDs. Useful for retrieving full details of specific emails after browsing with `mail_get_emails` (with `includeContent: false`).
187
+
188
+ | Parameter | Type | Required | Default | Description |
189
+ |-----------|------|----------|---------|-------------|
190
+ | `ids` | number[] | **Yes** | - | Array of email IDs to retrieve |
191
+ | `account` | string | No | - | Account name (helps optimize search) |
192
+ | `mailbox` | string | No | "INBOX" | Mailbox name (helps optimize search) |
193
+ | `includeContent` | boolean | No | true | Include email body |
194
+
195
+ **Example usage:**
196
+ ```json
197
+ {
198
+ "ids": [12345, 67890],
199
+ "includeContent": true
200
+ }
201
+ ```
202
+
203
+ **Example response:**
204
+ ```json
205
+ {
206
+ "emails": [
207
+ {
208
+ "id": 12345,
209
+ "subject": "Meeting tomorrow",
210
+ "sender": "John Doe <john@example.com>",
211
+ "to": ["you@example.com"],
212
+ "cc": [],
213
+ "bcc": [],
214
+ "dateSent": "Monday, 10. January 2025 at 09:30:00",
215
+ "isRead": false,
216
+ "content": "Let's meet at 2pm to discuss the project..."
217
+ },
218
+ {
219
+ "id": 67890,
220
+ "subject": "Project update",
221
+ "sender": "Jane Smith <jane@example.com>",
222
+ "to": ["you@example.com"],
223
+ "cc": [],
224
+ "bcc": [],
225
+ "dateSent": "Tuesday, 11. January 2025 at 14:15:00",
226
+ "isRead": true,
227
+ "content": "The project is progressing well..."
228
+ }
229
+ ],
230
+ "count": 2,
231
+ "requestedIds": [12345, 67890]
232
+ }
233
+ ```
234
+
184
235
  ### `mail_search`
185
236
 
186
237
  Search emails by subject, sender, or content.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sempervirens-labs/apple-mail-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "MCP server for Apple Mail - list accounts, mailboxes, search emails, and send messages",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -3,7 +3,7 @@ import { execSync } from "child_process";
3
3
  /**
4
4
  * Execute an AppleScript and return the result
5
5
  */
6
- export function runAppleScript(script: string): string {
6
+ export function runAppleScript(script: string, timeoutMs: number = 30000): string {
7
7
  try {
8
8
  // Use osascript with heredoc to handle complex scripts
9
9
  const result = execSync(`osascript <<'APPLESCRIPT'
@@ -11,6 +11,7 @@ ${script}
11
11
  APPLESCRIPT`, {
12
12
  encoding: "utf-8",
13
13
  maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large results
14
+ timeout: timeoutMs,
14
15
  });
15
16
  return result.trim();
16
17
  } catch (error: any) {
@@ -206,6 +207,108 @@ end tell
206
207
  return emails;
207
208
  }
208
209
 
210
+ /**
211
+ * Get specific emails by their IDs
212
+ */
213
+ export function getEmailsByIds(options: {
214
+ ids: number[];
215
+ account?: string;
216
+ mailbox?: string;
217
+ includeContent?: boolean;
218
+ }): Email[] {
219
+ const { ids, account, mailbox = "INBOX", includeContent = true } = options;
220
+
221
+ if (!ids || ids.length === 0) {
222
+ return [];
223
+ }
224
+
225
+ const contentPart = includeContent
226
+ ? `set msgContent to content of msg`
227
+ : `set msgContent to ""`;
228
+
229
+ const accountPart = account
230
+ ? `mailbox "${mailbox}" of account "${account}"`
231
+ : `mailbox "${mailbox}"`;
232
+
233
+ // Build the ID list for AppleScript
234
+ const idList = ids.join(", ");
235
+
236
+ const script = `
237
+ tell application "Mail"
238
+ set results to ""
239
+ set targetIds to {${idList}}
240
+ try
241
+ set theMailbox to ${accountPart}
242
+ set allMessages to messages of theMailbox
243
+
244
+ repeat with targetId in targetIds
245
+ repeat with msg in allMessages
246
+ if id of msg is targetId then
247
+ set msgId to id of msg
248
+ set msgSubject to subject of msg
249
+ set msgSender to sender of msg
250
+ set msgDate to date sent of msg
251
+ set msgRead to read status of msg
252
+ ${contentPart}
253
+
254
+ -- Get recipients
255
+ set toList to ""
256
+ repeat with r in to recipients of msg
257
+ set toList to toList & address of r & ","
258
+ end repeat
259
+ if toList is not "" then set toList to text 1 thru -2 of toList
260
+
261
+ set ccList to ""
262
+ repeat with r in cc recipients of msg
263
+ set ccList to ccList & address of r & ","
264
+ end repeat
265
+ if ccList is not "" then set ccList to text 1 thru -2 of ccList
266
+
267
+ set bccList to ""
268
+ repeat with r in bcc recipients of msg
269
+ set bccList to bccList & address of r & ","
270
+ end repeat
271
+ if bccList is not "" then set bccList to text 1 thru -2 of bccList
272
+
273
+ set results to results & msgId & "<<>>" & msgSubject & "<<>>" & msgSender & "<<>>" & toList & "<<>>" & ccList & "<<>>" & bccList & "<<>>" & (msgDate as string) & "<<>>" & msgRead & "<<>>" & msgContent & "|||"
274
+ exit repeat -- Found the message, move to next ID
275
+ end if
276
+ end repeat
277
+ end repeat
278
+ on error errMsg
279
+ return "ERROR:" & errMsg
280
+ end try
281
+ return results
282
+ end tell
283
+ `;
284
+
285
+ const result = runAppleScript(script);
286
+
287
+ if (result.startsWith("ERROR:")) {
288
+ throw new Error(result.substring(6));
289
+ }
290
+
291
+ const emails: Email[] = [];
292
+ const parts = result.split("|||").filter(Boolean);
293
+
294
+ for (const part of parts) {
295
+ const [id, subject, sender, to, cc, bcc, dateSent, isRead, content] = part.split("<<>>");
296
+ emails.push({
297
+ id: parseInt(id) || 0,
298
+ subject: subject || "(No Subject)",
299
+ sender: sender || "(Unknown)",
300
+ to: to ? to.split(",").map(s => s.trim()).filter(Boolean) : [],
301
+ cc: cc ? cc.split(",").map(s => s.trim()).filter(Boolean) : [],
302
+ bcc: bcc ? bcc.split(",").map(s => s.trim()).filter(Boolean) : [],
303
+ dateSent: dateSent || "",
304
+ isRead: isRead === "true",
305
+ content: content || undefined,
306
+ });
307
+ }
308
+
309
+ return emails;
310
+ }
311
+
209
312
  /**
210
313
  * Search emails by query
211
314
  */
@@ -655,6 +758,9 @@ export function createDraftReply(options: {
655
758
 
656
759
  const replyCommand = replyAll ? "reply theMessage with opening window and reply to all" : "reply theMessage with opening window";
657
760
 
761
+ // Escape the body for AppleScript - handle quotes and newlines
762
+ const escapedBody = body.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '" & return & "');
763
+
658
764
  const script = `
659
765
  tell application "Mail"
660
766
  try
@@ -663,9 +769,8 @@ tell application "Mail"
663
769
 
664
770
  set replyMessage to ${replyCommand}
665
771
 
666
- -- Prepend the new body to the reply
667
- set currentContent to content of replyMessage
668
- set content of replyMessage to "${body.replace(/"/g, '\\"').replace(/\n/g, "\\n")}" & return & return & currentContent
772
+ -- Set the reply body content (the quoted original will appear below in the compose window)
773
+ set content of replyMessage to "${escapedBody}"
669
774
 
670
775
  return "Draft reply created successfully"
671
776
  on error errMsg
@@ -675,7 +780,8 @@ end tell
675
780
  `;
676
781
 
677
782
  try {
678
- const result = runAppleScript(script);
783
+ // Use longer timeout for reply operations as they can be slow
784
+ const result = runAppleScript(script, 60000);
679
785
  if (result.startsWith("ERROR:")) {
680
786
  return { success: false, message: result.substring(6) };
681
787
  }
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  listAccounts,
13
13
  listMailboxes,
14
14
  getEmails,
15
+ getEmailsByIds,
15
16
  searchEmails,
16
17
  getUnreadCount,
17
18
  sendEmail,
@@ -90,6 +91,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
90
91
  };
91
92
  }
92
93
 
94
+ case "mail_get_emails_by_ids": {
95
+ const ids = args?.ids as number[];
96
+ if (!ids || !Array.isArray(ids) || ids.length === 0) {
97
+ throw new Error("Required field: ids (must be a non-empty array of email IDs)");
98
+ }
99
+ const emails = getEmailsByIds({
100
+ ids,
101
+ account: args?.account as string | undefined,
102
+ mailbox: (args?.mailbox as string) || "INBOX",
103
+ includeContent: (args?.includeContent as boolean) ?? true,
104
+ });
105
+ return {
106
+ content: [
107
+ {
108
+ type: "text",
109
+ text: JSON.stringify({ emails, count: emails.length, requestedIds: ids }, null, 2),
110
+ },
111
+ ],
112
+ };
113
+ }
114
+
93
115
  case "mail_search": {
94
116
  const query = args?.query as string;
95
117
  if (!query) {
package/src/tools.ts CHANGED
@@ -60,6 +60,38 @@ export const MAIL_GET_EMAILS: Tool = {
60
60
  },
61
61
  };
62
62
 
63
+ export const MAIL_GET_EMAILS_BY_IDS: Tool = {
64
+ name: "mail_get_emails_by_ids",
65
+ description: "Get specific emails by their IDs in Apple Mail. Use this to retrieve full details of specific emails after browsing with mail_get_emails (includeContent: false).",
66
+ inputSchema: {
67
+ type: "object",
68
+ properties: {
69
+ ids: {
70
+ type: "array",
71
+ items: {
72
+ type: "number",
73
+ },
74
+ description: "Array of email IDs to retrieve (obtained from mail_get_emails or mail_search)",
75
+ },
76
+ account: {
77
+ type: "string",
78
+ description: "The name of the email account (helps optimize search)",
79
+ },
80
+ mailbox: {
81
+ type: "string",
82
+ description: "The name of the mailbox/folder where the emails are located (default: INBOX, helps optimize search)",
83
+ default: "INBOX",
84
+ },
85
+ includeContent: {
86
+ type: "boolean",
87
+ description: "Whether to include the email body content (default: true)",
88
+ default: true,
89
+ },
90
+ },
91
+ required: ["ids"],
92
+ },
93
+ };
94
+
63
95
  export const MAIL_SEARCH: Tool = {
64
96
  name: "mail_search",
65
97
  description: "Search emails in Apple Mail by subject, sender, or content",
@@ -332,6 +364,7 @@ export const tools: Tool[] = [
332
364
  MAIL_LIST_ACCOUNTS,
333
365
  MAIL_LIST_MAILBOXES,
334
366
  MAIL_GET_EMAILS,
367
+ MAIL_GET_EMAILS_BY_IDS,
335
368
  MAIL_SEARCH,
336
369
  MAIL_GET_UNREAD_COUNT,
337
370
  MAIL_SEND,