apple-mail-mcp 1.2.0 → 1.3.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
@@ -139,7 +139,7 @@ Search for messages matching criteria. Searches all accounts by default.
139
139
  | `query` | string | No | Text to search in subject/sender |
140
140
  | `from` | string | No | Filter by sender email address |
141
141
  | `subject` | string | No | Filter by subject line |
142
- | `mailbox` | string | No | Mailbox to search in (default: INBOX) |
142
+ | `mailbox` | string | No | Mailbox to search in (omit to search all mailboxes) |
143
143
  | `account` | string | No | Account to search in (omit to search all accounts) |
144
144
  | `isRead` | boolean | No | Filter by read status |
145
145
  | `isFlagged` | boolean | No | Filter by flagged status |
@@ -168,7 +168,7 @@ List messages in a mailbox.
168
168
 
169
169
  | Parameter | Type | Required | Description |
170
170
  |-----------|------|----------|-------------|
171
- | `mailbox` | string | No | Mailbox name (default: INBOX) |
171
+ | `mailbox` | string | No | Mailbox name (omit to list from all mailboxes) |
172
172
  | `account` | string | No | Account name |
173
173
  | `limit` | number | No | Max messages (default: 50) |
174
174
  | `offset` | number | No | Number of messages to skip (for pagination) |
@@ -191,7 +191,7 @@ Send a new email immediately.
191
191
  | `cc` | string[] | No | CC recipients |
192
192
  | `bcc` | string[] | No | BCC recipients |
193
193
  | `account` | string | No | Send from specific account |
194
- | `attachments` | string[] | No | Absolute file paths to attach (e.g., `["/Users/me/report.pdf"]`) |
194
+ | `attachments` | string[] | No | Absolute file paths to attach, max 20 files (e.g., `["/Users/me/report.pdf"]`) |
195
195
 
196
196
  **Example:**
197
197
  ```json
@@ -212,11 +212,11 @@ Send individual personalized emails to a list of recipients (mail merge). Each r
212
212
 
213
213
  | Parameter | Type | Required | Description |
214
214
  |-----------|------|----------|-------------|
215
- | `recipients` | object[] | Yes | List of recipients (see below) |
215
+ | `recipients` | object[] | Yes | List of recipients, max 100 (see below) |
216
216
  | `subject` | string | Yes | Email subject — use `{{Key}}` for placeholders |
217
217
  | `body` | string | Yes | Email body — use `{{Key}}` for placeholders |
218
218
  | `account` | string | No | Send from specific account |
219
- | `delayMs` | number | No | Delay between sends in ms (default: 500) |
219
+ | `delayMs` | number | No | Delay between sends in ms (default: 500, max 10000) |
220
220
 
221
221
  Each recipient object:
222
222
 
@@ -253,7 +253,7 @@ Save an email to Drafts without sending.
253
253
  | `cc` | string[] | No | CC recipients |
254
254
  | `bcc` | string[] | No | BCC recipients |
255
255
  | `account` | string | No | Account for draft |
256
- | `attachments` | string[] | No | Absolute file paths to attach |
256
+ | `attachments` | string[] | No | Absolute file paths to attach, max 20 files |
257
257
 
258
258
  **Returns:** Confirmation that draft was created.
259
259
 
@@ -371,19 +371,19 @@ Save a message attachment to disk.
371
371
 
372
372
  ### Batch Operations
373
373
 
374
- All batch operations accept an array of message IDs and return per-item success/failure results.
374
+ All batch operations accept an array of message IDs (max 100 per batch) and return per-item success/failure results.
375
375
 
376
376
  #### `batch-delete-messages`
377
377
 
378
378
  | Parameter | Type | Required | Description |
379
379
  |-----------|------|----------|-------------|
380
- | `ids` | string[] | Yes | Message IDs to delete |
380
+ | `ids` | string[] | Yes | Message IDs to delete (max 100) |
381
381
 
382
382
  #### `batch-move-messages`
383
383
 
384
384
  | Parameter | Type | Required | Description |
385
385
  |-----------|------|----------|-------------|
386
- | `ids` | string[] | Yes | Message IDs to move |
386
+ | `ids` | string[] | Yes | Message IDs to move (max 100) |
387
387
  | `mailbox` | string | Yes | Destination mailbox |
388
388
  | `account` | string | No | Account containing mailbox |
389
389
 
@@ -391,13 +391,13 @@ All batch operations accept an array of message IDs and return per-item success/
391
391
 
392
392
  | Parameter | Type | Required | Description |
393
393
  |-----------|------|----------|-------------|
394
- | `ids` | string[] | Yes | Message IDs |
394
+ | `ids` | string[] | Yes | Message IDs (max 100) |
395
395
 
396
396
  #### `batch-flag-messages` / `batch-unflag-messages`
397
397
 
398
398
  | Parameter | Type | Required | Description |
399
399
  |-----------|------|----------|-------------|
400
- | `ids` | string[] | Yes | Message IDs |
400
+ | `ids` | string[] | Yes | Message IDs (max 100) |
401
401
 
402
402
  ---
403
403
 
@@ -608,12 +608,12 @@ Check Mail.app sync activity.
608
608
 
609
609
  ```
610
610
  User: "Check my inbox for new emails"
611
- AI: [calls list-messages with mailbox="INBOX"]
612
- "You have 12 messages in your inbox. Here are the most recent..."
611
+ AI: [calls list-messages]
612
+ "You have 12 messages. Here are the most recent..."
613
613
 
614
614
  User: "Show me emails from Sarah"
615
615
  AI: [calls search-messages with query="Sarah"]
616
- "Found 3 emails from Sarah..."
616
+ "Found 3 emails from Sarah across all mailboxes..."
617
617
 
618
618
  User: "Read the first one"
619
619
  AI: [calls get-message with id="..."]
@@ -718,6 +718,11 @@ If installed from source, use this configuration:
718
718
  | Attachments require absolute paths | File attachments must use full absolute paths (e.g., `/Users/me/file.pdf`) |
719
719
  | No smart mailboxes | Cannot access Smart Mailboxes via AppleScript |
720
720
  | In-memory templates | Email templates are not persisted across server restarts |
721
+ | Numeric-only message IDs | Message IDs must contain only digits (validated by schema) |
722
+ | Batch size cap | Batch operations are limited to 100 messages per request |
723
+ | Date filter format | Date filters must be valid parseable dates (e.g., "January 1, 2026" or "2026-03-15"); bare numbers or non-date strings are rejected |
724
+ | Attachment save path restrictions | `save-attachment` only allows saving to home directory, `/tmp`, `/private/tmp`, and `/Volumes`; path traversal is blocked |
725
+ | Attachment count limit | `send-email` and `create-draft` accept a maximum of 20 file attachments |
721
726
 
722
727
  ### Backslash Escaping (Important for AI Agents)
723
728
 
@@ -776,11 +781,13 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
776
781
  ## Development
777
782
 
778
783
  ```bash
779
- npm install # Install dependencies
780
- npm run build # Compile TypeScript
781
- npm test # Run test suite (28 tests)
782
- npm run lint # Check code style
783
- npm run format # Format code
784
+ npm install # Install dependencies
785
+ npm run build # Compile TypeScript
786
+ npm test # Run unit tests
787
+ npm run test:integration # Run integration tests (requires Mail.app)
788
+ npm run test:all # Run all tests (unit + integration)
789
+ npm run lint # Check code style
790
+ npm run format # Format code
784
791
  ```
785
792
 
786
793
  ---
package/build/index.js CHANGED
@@ -24,6 +24,26 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
24
24
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
25
25
  import { z } from "zod";
26
26
  import { AppleMailManager } from "./services/appleMailManager.js";
27
+ // =============================================================================
28
+ // Shared Validation Schemas
29
+ // =============================================================================
30
+ /** Message IDs in Apple Mail are always numeric. Enforce this at the schema level
31
+ * to prevent AppleScript injection via the `whose id is ${id}` interpolation. */
32
+ const MESSAGE_ID_SCHEMA = z.string().regex(/^\d+$/, "Message ID must be numeric");
33
+ /** Batch operations are capped to prevent unbounded loops / DoS. */
34
+ const BATCH_IDS_SCHEMA = z
35
+ .array(MESSAGE_ID_SCHEMA)
36
+ .min(1, "At least one message ID is required")
37
+ .max(100, "Cannot process more than 100 messages in a single batch");
38
+ /** Date filter strings must look like natural-language dates (e.g. "March 1, 2026").
39
+ * Block characters that could escape an AppleScript `date "..."` literal. */
40
+ const DATE_FILTER_SCHEMA = z
41
+ .string()
42
+ .regex(/^[a-zA-Z0-9 ,/\-:]+$/, "Date must contain only alphanumeric characters, spaces, commas, slashes, hyphens, and colons")
43
+ .refine((val) => !isNaN(new Date(val).getTime()), {
44
+ message: "Date string must be a valid date (e.g., 'January 1, 2026' or '2026-03-15')",
45
+ })
46
+ .optional();
27
47
  // Read version from package.json to keep it in sync
28
48
  const require = createRequire(import.meta.url);
29
49
  const { version } = require("../package.json");
@@ -85,12 +105,15 @@ server.tool("search-messages", {
85
105
  query: z.string().optional().describe("Text to search for in subject, sender, or content"),
86
106
  from: z.string().optional().describe("Filter by sender email address"),
87
107
  subject: z.string().optional().describe("Filter by subject line"),
88
- mailbox: z.string().optional().describe("Mailbox to search in (e.g., 'INBOX')"),
108
+ mailbox: z
109
+ .string()
110
+ .optional()
111
+ .describe("Mailbox to search in (e.g., 'INBOX'). Omit to search all mailboxes."),
89
112
  account: z.string().optional().describe("Account to search in (omit to search all accounts)"),
90
113
  isRead: z.boolean().optional().describe("Filter by read status"),
91
114
  isFlagged: z.boolean().optional().describe("Filter by flagged status"),
92
- dateFrom: z.string().optional().describe("Start date filter (e.g., 'January 1, 2026')"),
93
- dateTo: z.string().optional().describe("End date filter (e.g., 'March 1, 2026')"),
115
+ dateFrom: DATE_FILTER_SCHEMA.describe("Start date filter (e.g., 'January 1, 2026')"),
116
+ dateTo: DATE_FILTER_SCHEMA.describe("End date filter (e.g., 'March 1, 2026')"),
94
117
  limit: z.number().optional().describe("Maximum number of results (default: 50)"),
95
118
  }, withErrorHandling(({ query, mailbox, account, limit = 50, dateFrom, dateTo }) => {
96
119
  const messages = mailManager.searchMessages(query, mailbox, account, limit, dateFrom, dateTo);
@@ -104,7 +127,7 @@ server.tool("search-messages", {
104
127
  }, "Error searching messages"));
105
128
  // --- get-message ---
106
129
  server.tool("get-message", {
107
- id: z.string().min(1, "Message ID is required"),
130
+ id: MESSAGE_ID_SCHEMA,
108
131
  preferHtml: z.boolean().optional().describe("Return HTML source instead of plain text"),
109
132
  }, withErrorHandling(({ id, preferHtml }) => {
110
133
  const content = mailManager.getMessageContent(id);
@@ -118,7 +141,10 @@ server.tool("get-message", {
118
141
  }, "Error retrieving message"));
119
142
  // --- list-messages ---
120
143
  server.tool("list-messages", {
121
- mailbox: z.string().optional().describe("Mailbox to list messages from (default: INBOX)"),
144
+ mailbox: z
145
+ .string()
146
+ .optional()
147
+ .describe("Mailbox to list messages from. Omit to list from all mailboxes."),
122
148
  account: z.string().optional().describe("Account to list messages from"),
123
149
  limit: z.number().optional().describe("Maximum number of messages (default: 50)"),
124
150
  offset: z.number().optional().describe("Number of messages to skip (for pagination)"),
@@ -144,6 +170,7 @@ server.tool("send-email", {
144
170
  account: z.string().optional().describe("Account to send from"),
145
171
  attachments: z
146
172
  .array(z.string())
173
+ .max(20, "Cannot attach more than 20 files")
147
174
  .optional()
148
175
  .describe("Absolute file paths to attach (e.g., ['/Users/me/report.pdf'])"),
149
176
  }, withErrorHandling(({ to, subject, body, cc, bcc, account, attachments }) => {
@@ -208,6 +235,7 @@ server.tool("create-draft", {
208
235
  account: z.string().optional().describe("Account to create draft in"),
209
236
  attachments: z
210
237
  .array(z.string())
238
+ .max(20, "Cannot attach more than 20 files")
211
239
  .optional()
212
240
  .describe("Absolute file paths to attach (e.g., ['/Users/me/report.pdf'])"),
213
241
  }, withErrorHandling(({ to, subject, body, cc, bcc, account, attachments }) => {
@@ -220,7 +248,7 @@ server.tool("create-draft", {
220
248
  }, "Error creating draft"));
221
249
  // --- reply-to-message ---
222
250
  server.tool("reply-to-message", {
223
- id: z.string().min(1, "Message ID is required"),
251
+ id: MESSAGE_ID_SCHEMA,
224
252
  body: z.string().min(1, "Reply body is required"),
225
253
  replyAll: z.boolean().optional().default(false).describe("Reply to all recipients"),
226
254
  send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
@@ -233,7 +261,7 @@ server.tool("reply-to-message", {
233
261
  }, "Error replying to message"));
234
262
  // --- forward-message ---
235
263
  server.tool("forward-message", {
236
- id: z.string().min(1, "Message ID is required"),
264
+ id: MESSAGE_ID_SCHEMA,
237
265
  to: z.array(z.string()).min(1, "At least one recipient is required"),
238
266
  body: z.string().optional().describe("Optional message to prepend"),
239
267
  send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
@@ -246,7 +274,7 @@ server.tool("forward-message", {
246
274
  }, "Error forwarding message"));
247
275
  // --- mark-as-read ---
248
276
  server.tool("mark-as-read", {
249
- id: z.string().min(1, "Message ID is required"),
277
+ id: MESSAGE_ID_SCHEMA,
250
278
  }, withErrorHandling(({ id }) => {
251
279
  const success = mailManager.markAsRead(id);
252
280
  if (!success) {
@@ -256,7 +284,7 @@ server.tool("mark-as-read", {
256
284
  }, "Error marking message as read"));
257
285
  // --- mark-as-unread ---
258
286
  server.tool("mark-as-unread", {
259
- id: z.string().min(1, "Message ID is required"),
287
+ id: MESSAGE_ID_SCHEMA,
260
288
  }, withErrorHandling(({ id }) => {
261
289
  const success = mailManager.markAsUnread(id);
262
290
  if (!success) {
@@ -266,7 +294,7 @@ server.tool("mark-as-unread", {
266
294
  }, "Error marking message as unread"));
267
295
  // --- flag-message ---
268
296
  server.tool("flag-message", {
269
- id: z.string().min(1, "Message ID is required"),
297
+ id: MESSAGE_ID_SCHEMA,
270
298
  }, withErrorHandling(({ id }) => {
271
299
  const success = mailManager.flagMessage(id);
272
300
  if (!success) {
@@ -276,7 +304,7 @@ server.tool("flag-message", {
276
304
  }, "Error flagging message"));
277
305
  // --- unflag-message ---
278
306
  server.tool("unflag-message", {
279
- id: z.string().min(1, "Message ID is required"),
307
+ id: MESSAGE_ID_SCHEMA,
280
308
  }, withErrorHandling(({ id }) => {
281
309
  const success = mailManager.unflagMessage(id);
282
310
  if (!success) {
@@ -286,7 +314,7 @@ server.tool("unflag-message", {
286
314
  }, "Error unflagging message"));
287
315
  // --- delete-message ---
288
316
  server.tool("delete-message", {
289
- id: z.string().min(1, "Message ID is required"),
317
+ id: MESSAGE_ID_SCHEMA,
290
318
  }, withErrorHandling(({ id }) => {
291
319
  const success = mailManager.deleteMessage(id);
292
320
  if (!success) {
@@ -296,7 +324,7 @@ server.tool("delete-message", {
296
324
  }, "Error deleting message"));
297
325
  // --- move-message ---
298
326
  server.tool("move-message", {
299
- id: z.string().min(1, "Message ID is required"),
327
+ id: MESSAGE_ID_SCHEMA,
300
328
  mailbox: z.string().min(1, "Destination mailbox is required"),
301
329
  account: z.string().optional().describe("Account containing the destination mailbox"),
302
330
  }, withErrorHandling(({ id, mailbox, account }) => {
@@ -308,7 +336,7 @@ server.tool("move-message", {
308
336
  }, "Error moving message"));
309
337
  // --- batch-delete-messages ---
310
338
  server.tool("batch-delete-messages", {
311
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
339
+ ids: BATCH_IDS_SCHEMA,
312
340
  }, withErrorHandling(({ ids }) => {
313
341
  const results = mailManager.batchDeleteMessages(ids);
314
342
  const successCount = results.filter((r) => r.success).length;
@@ -325,7 +353,7 @@ server.tool("batch-delete-messages", {
325
353
  }, "Error batch deleting messages"));
326
354
  // --- batch-move-messages ---
327
355
  server.tool("batch-move-messages", {
328
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
356
+ ids: BATCH_IDS_SCHEMA,
329
357
  mailbox: z.string().min(1, "Destination mailbox is required"),
330
358
  account: z.string().optional().describe("Account containing the destination mailbox"),
331
359
  }, withErrorHandling(({ ids, mailbox, account }) => {
@@ -344,7 +372,7 @@ server.tool("batch-move-messages", {
344
372
  }, "Error batch moving messages"));
345
373
  // --- batch-mark-as-read ---
346
374
  server.tool("batch-mark-as-read", {
347
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
375
+ ids: BATCH_IDS_SCHEMA,
348
376
  }, withErrorHandling(({ ids }) => {
349
377
  const results = mailManager.batchMarkAsRead(ids);
350
378
  const successCount = results.filter((r) => r.success).length;
@@ -361,7 +389,7 @@ server.tool("batch-mark-as-read", {
361
389
  }, "Error batch marking messages as read"));
362
390
  // --- batch-mark-as-unread ---
363
391
  server.tool("batch-mark-as-unread", {
364
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
392
+ ids: BATCH_IDS_SCHEMA,
365
393
  }, withErrorHandling(({ ids }) => {
366
394
  const results = mailManager.batchMarkAsUnread(ids);
367
395
  const successCount = results.filter((r) => r.success).length;
@@ -378,7 +406,7 @@ server.tool("batch-mark-as-unread", {
378
406
  }, "Error batch marking messages as unread"));
379
407
  // --- batch-flag-messages ---
380
408
  server.tool("batch-flag-messages", {
381
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
409
+ ids: BATCH_IDS_SCHEMA,
382
410
  }, withErrorHandling(({ ids }) => {
383
411
  const results = mailManager.batchFlagMessages(ids);
384
412
  const successCount = results.filter((r) => r.success).length;
@@ -395,7 +423,7 @@ server.tool("batch-flag-messages", {
395
423
  }, "Error batch flagging messages"));
396
424
  // --- batch-unflag-messages ---
397
425
  server.tool("batch-unflag-messages", {
398
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
426
+ ids: BATCH_IDS_SCHEMA,
399
427
  }, withErrorHandling(({ ids }) => {
400
428
  const results = mailManager.batchUnflagMessages(ids);
401
429
  const successCount = results.filter((r) => r.success).length;
@@ -412,7 +440,7 @@ server.tool("batch-unflag-messages", {
412
440
  }, "Error batch unflagging messages"));
413
441
  // --- list-attachments ---
414
442
  server.tool("list-attachments", {
415
- id: z.string().min(1, "Message ID is required"),
443
+ id: MESSAGE_ID_SCHEMA,
416
444
  }, withErrorHandling(({ id }) => {
417
445
  const attachments = mailManager.listAttachments(id);
418
446
  if (attachments.length === 0) {
@@ -428,7 +456,7 @@ server.tool("list-attachments", {
428
456
  }, "Error listing attachments"));
429
457
  // --- save-attachment ---
430
458
  server.tool("save-attachment", {
431
- id: z.string().min(1, "Message ID is required"),
459
+ id: MESSAGE_ID_SCHEMA,
432
460
  attachmentName: z.string().min(1, "Attachment name is required"),
433
461
  savePath: z.string().min(1, "Save directory path is required"),
434
462
  }, withErrorHandling(({ id, attachmentName, savePath }) => {
@@ -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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"}
@@ -14,7 +14,8 @@
14
14
  */
15
15
  import { spawnSync } from "child_process";
16
16
  import { existsSync } from "fs";
17
- import { isAbsolute } from "path";
17
+ import { isAbsolute, resolve } from "path";
18
+ import { homedir } from "os";
18
19
  import { executeAppleScript } from "../utils/applescript.js";
19
20
  // =============================================================================
20
21
  // Text Processing Utilities
@@ -302,27 +303,32 @@ export class AppleMailManager {
302
303
  return allMessages.slice(0, limit);
303
304
  }
304
305
  const targetAccount = this.resolveAccount(account);
305
- const requestedMailbox = mailbox || "INBOX";
306
- const targetMailbox = this.resolveMailbox(requestedMailbox, targetAccount);
307
306
  // Build the search condition
308
307
  let searchCondition = "";
309
308
  if (query) {
310
309
  const safeQuery = escapeForAppleScript(query);
311
310
  searchCondition = `whose subject contains "${safeQuery}" or sender contains "${safeQuery}"`;
312
311
  }
313
- // Build date filter AppleScript
312
+ // Build date filter AppleScript.
313
+ // Note: dateFrom/dateTo are already validated by DATE_FILTER_SCHEMA (alphanumeric + safe
314
+ // punctuation only), so escapeForAppleScript() below is belt-and-suspenders — it won't
315
+ // alter valid date strings but guards against future schema changes.
314
316
  let dateFilter = "";
315
317
  if (dateFrom || dateTo) {
316
318
  const dateChecks = [];
317
319
  if (dateFrom) {
318
- dateChecks.push(`date received of msg >= date "${dateFrom}"`);
320
+ dateChecks.push(`date received of msg >= date "${escapeForAppleScript(dateFrom)}"`);
319
321
  }
320
322
  if (dateTo) {
321
- dateChecks.push(`date received of msg <= date "${dateTo}"`);
323
+ dateChecks.push(`date received of msg <= date "${escapeForAppleScript(dateTo)}"`);
322
324
  }
323
325
  dateFilter = dateChecks.join(" and ");
324
326
  }
325
- const searchCommand = `
327
+ let searchCommand;
328
+ if (mailbox) {
329
+ // Search a specific mailbox
330
+ const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
331
+ searchCommand = `
326
332
  set outputText to ""
327
333
  set theMailbox to mailbox "${escapeForAppleScript(targetMailbox)}"
328
334
  set allMessages to messages of theMailbox ${searchCondition}
@@ -346,6 +352,42 @@ export class AppleMailManager {
346
352
  end repeat
347
353
  return outputText
348
354
  `;
355
+ }
356
+ else {
357
+ // Search ALL mailboxes — iterate every mailbox in the account, dedup by message ID
358
+ searchCommand = `
359
+ set outputText to ""
360
+ set msgCount to 0
361
+ set seenIds to {}
362
+ repeat with mb in mailboxes
363
+ if msgCount >= ${limit} then exit repeat
364
+ try
365
+ set allMessages to messages of mb ${searchCondition}
366
+ repeat with msg in allMessages
367
+ if msgCount >= ${limit} then exit repeat
368
+ try
369
+ set msgId to id of msg as string
370
+ if seenIds does not contain msgId then
371
+ set end of seenIds to msgId
372
+ ${dateFilter ? `set msgDate to date received of msg\n if not (${dateFilter}) then\n -- skip message outside date range\n else` : ""}
373
+ set msgSubject to subject of msg
374
+ set msgSender to sender of msg
375
+ set d to date received of msg
376
+ set msgDateStr to ${AS_DATE_TO_STRING}
377
+ set msgRead to read status of msg as string
378
+ set msgFlagged to flagged status of msg as string
379
+ if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
380
+ set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDateStr & "|||" & msgRead & "|||" & msgFlagged & "|||" & name of mb
381
+ set msgCount to msgCount + 1
382
+ ${dateFilter ? "end if" : ""}
383
+ end if
384
+ end try
385
+ end repeat
386
+ end try
387
+ end repeat
388
+ return outputText
389
+ `;
390
+ }
349
391
  const script = buildAccountScopedScript(targetAccount, searchCommand);
350
392
  const result = executeAppleScript(script, { timeoutMs: 60000 });
351
393
  if (!result.success) {
@@ -354,7 +396,7 @@ export class AppleMailManager {
354
396
  }
355
397
  if (!result.output.trim())
356
398
  return [];
357
- return this.parseMessageList(result.output, targetMailbox, targetAccount);
399
+ return this.parseMessageList(result.output, mailbox || "INBOX", targetAccount);
358
400
  }
359
401
  /**
360
402
  * Get a message by ID.
@@ -368,7 +410,7 @@ export class AppleMailManager {
368
410
  repeat with acct in accounts
369
411
  repeat with mb in mailboxes of acct
370
412
  try
371
- set matchingMsgs to (messages of mb whose id is ${id})
413
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
372
414
  if (count of matchingMsgs) > 0 then
373
415
  set msg to item 1 of matchingMsgs
374
416
  set msgSubject to subject of msg
@@ -423,7 +465,7 @@ export class AppleMailManager {
423
465
  repeat with acct in accounts
424
466
  repeat with mb in mailboxes of acct
425
467
  try
426
- set matchingMsgs to (messages of mb whose id is ${id})
468
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
427
469
  if (count of matchingMsgs) > 0 then
428
470
  set msg to item 1 of matchingMsgs
429
471
  set msgSubject to subject of msg
@@ -469,12 +511,27 @@ export class AppleMailManager {
469
511
  * @returns Array of messages
470
512
  */
471
513
  listMessages(mailbox, account, limit = 50, from, offset = 0) {
514
+ // If no account specified, list across all accounts
515
+ if (!account) {
516
+ const accounts = this.listAccounts();
517
+ const allMessages = [];
518
+ for (const acct of accounts) {
519
+ if (allMessages.length >= limit)
520
+ break;
521
+ const remaining = limit - allMessages.length;
522
+ const msgs = this.listMessages(mailbox, acct.name, remaining, from, offset);
523
+ allMessages.push(...msgs);
524
+ }
525
+ return allMessages.slice(0, limit);
526
+ }
472
527
  const targetAccount = this.resolveAccount(account);
473
- const requestedMailbox = mailbox || "INBOX";
474
- const targetMailbox = this.resolveMailbox(requestedMailbox, targetAccount);
475
528
  const safeFrom = from ? escapeForAppleScript(from) : "";
476
529
  const fromFilter = from ? `whose sender contains "${safeFrom}"` : "";
477
- const listCommand = `
530
+ let listCommand;
531
+ if (mailbox) {
532
+ // List from a specific mailbox
533
+ const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
534
+ listCommand = `
478
535
  set outputText to ""
479
536
  set theMailbox to mailbox "${escapeForAppleScript(targetMailbox)}"
480
537
  set msgCount to 0
@@ -500,15 +557,53 @@ export class AppleMailManager {
500
557
  end repeat
501
558
  return outputText
502
559
  `;
560
+ }
561
+ else {
562
+ // List from ALL mailboxes — iterate every mailbox in the account, dedup by message ID
563
+ listCommand = `
564
+ set outputText to ""
565
+ set msgCount to 0
566
+ set skipped to 0
567
+ set seenIds to {}
568
+ repeat with mb in mailboxes
569
+ if msgCount >= ${limit} then exit repeat
570
+ try
571
+ repeat with msg in messages of mb ${fromFilter}
572
+ if msgCount >= ${limit} then exit repeat
573
+ try
574
+ set msgId to id of msg as string
575
+ if seenIds does not contain msgId then
576
+ set end of seenIds to msgId
577
+ if skipped < ${offset} then
578
+ set skipped to skipped + 1
579
+ else
580
+ set msgSubject to subject of msg
581
+ set msgSender to sender of msg
582
+ set d to date received of msg
583
+ set msgDate to ${AS_DATE_TO_STRING}
584
+ set msgRead to read status of msg as string
585
+ set msgFlagged to flagged status of msg as string
586
+ if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
587
+ set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged & "|||" & name of mb
588
+ set msgCount to msgCount + 1
589
+ end if
590
+ end if
591
+ end try
592
+ end repeat
593
+ end try
594
+ end repeat
595
+ return outputText
596
+ `;
597
+ }
503
598
  const script = buildAccountScopedScript(targetAccount, listCommand);
504
- const result = executeAppleScript(script);
599
+ const result = executeAppleScript(script, { timeoutMs: 60000 });
505
600
  if (!result.success) {
506
601
  console.error(`Failed to list messages: ${result.error}`);
507
602
  return [];
508
603
  }
509
604
  if (!result.output.trim())
510
605
  return [];
511
- return this.parseMessageList(result.output, targetMailbox, targetAccount);
606
+ return this.parseMessageList(result.output, mailbox || "INBOX", targetAccount);
512
607
  }
513
608
  /**
514
609
  * Parse message list output from AppleScript.
@@ -530,7 +625,7 @@ export class AppleMailManager {
530
625
  isFlagged: parts[5] === "true",
531
626
  isJunk: false,
532
627
  isDeleted: false,
533
- mailbox,
628
+ mailbox: parts.length >= 7 ? parts[6] : mailbox,
534
629
  account,
535
630
  hasAttachments: false,
536
631
  });
@@ -728,7 +823,7 @@ export class AppleMailManager {
728
823
  repeat with acct in accounts
729
824
  repeat with mb in mailboxes of acct
730
825
  try
731
- set matchingMsgs to (messages of mb whose id is ${id})
826
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
732
827
  if (count of matchingMsgs) > 0 then
733
828
  set msg to item 1 of matchingMsgs
734
829
  set theReply to reply msg with opening window${replyAllClause}
@@ -773,7 +868,7 @@ export class AppleMailManager {
773
868
  repeat with acct in accounts
774
869
  repeat with mb in mailboxes of acct
775
870
  try
776
- set matchingMsgs to (messages of mb whose id is ${id})
871
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
777
872
  if (count of matchingMsgs) > 0 then
778
873
  set msg to item 1 of matchingMsgs
779
874
  set theForward to forward msg with opening window
@@ -806,7 +901,7 @@ export class AppleMailManager {
806
901
  repeat with acct in accounts
807
902
  repeat with mb in mailboxes of acct
808
903
  try
809
- set matchingMsgs to (messages of mb whose id is ${id})
904
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
810
905
  if (count of matchingMsgs) > 0 then
811
906
  set msg to item 1 of matchingMsgs
812
907
  ${operation}
@@ -894,7 +989,7 @@ export class AppleMailManager {
894
989
  repeat with acct in accounts
895
990
  repeat with mb in mailboxes of acct
896
991
  try
897
- set matchingMsgs to (messages of mb whose id is ${id})
992
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
898
993
  if (count of matchingMsgs) > 0 then
899
994
  set msg to item 1 of matchingMsgs
900
995
  set destMailbox to mailbox "${safeMailbox}" of account "${safeAccount}"
@@ -1014,7 +1109,7 @@ export class AppleMailManager {
1014
1109
  repeat with acct in accounts
1015
1110
  repeat with mb in mailboxes of acct
1016
1111
  try
1017
- set matchingMsgs to (messages of mb whose id is ${id})
1112
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
1018
1113
  if (count of matchingMsgs) > 0 then
1019
1114
  set msg to item 1 of matchingMsgs
1020
1115
  set outputText to ""
@@ -1060,14 +1155,30 @@ export class AppleMailManager {
1060
1155
  * Save an attachment from a message to disk.
1061
1156
  */
1062
1157
  saveAttachment(id, attachmentName, savePath) {
1158
+ // Validate attachment name: block path separators, traversal, null bytes, and backslashes
1159
+ if (/[/\\\0]/.test(attachmentName) || attachmentName.includes("..")) {
1160
+ console.error(`Invalid attachment name: "${attachmentName}"`);
1161
+ return false;
1162
+ }
1163
+ // Resolve the save path to prevent symlink / ".." traversal bypass
1164
+ const resolvedPath = resolve(savePath);
1165
+ const allowedPrefixes = [homedir(), "/tmp", "/private/tmp", "/Volumes"];
1166
+ const isAllowed = allowedPrefixes.some((prefix) => resolvedPath.startsWith(prefix));
1167
+ if (!isAllowed) {
1168
+ console.error(`Save path "${savePath}" is outside allowed directories`);
1169
+ return false;
1170
+ }
1063
1171
  const safeName = escapeForAppleScript(attachmentName);
1064
- const safePath = escapeForAppleScript(savePath);
1172
+ 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
+ const numericId = Number(id);
1065
1176
  const script = buildAppLevelScript(`
1066
1177
  try
1067
1178
  repeat with acct in accounts
1068
1179
  repeat with mb in mailboxes of acct
1069
1180
  try
1070
- set matchingMsgs to (messages of mb whose id is ${id})
1181
+ set matchingMsgs to (messages of mb whose id is ${numericId})
1071
1182
  if (count of matchingMsgs) > 0 then
1072
1183
  set msg to item 1 of matchingMsgs
1073
1184
  repeat with att in mail attachments of msg
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-mail-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.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",
@@ -18,6 +18,8 @@
18
18
  "start": "node build/index.js",
19
19
  "dev": "tsc --watch",
20
20
  "test": "vitest run",
21
+ "test:integration": "vitest run --config vitest.integration.config.ts",
22
+ "test:all": "vitest run && vitest run --config vitest.integration.config.ts",
21
23
  "test:watch": "vitest",
22
24
  "test:coverage": "vitest run --coverage",
23
25
  "lint": "eslint src",