apple-mail-mcp 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
 
@@ -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 accept alphanumeric characters and safe punctuation only |
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,23 @@ 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
+ .optional();
27
44
  // Read version from package.json to keep it in sync
28
45
  const require = createRequire(import.meta.url);
29
46
  const { version } = require("../package.json");
@@ -89,8 +106,8 @@ server.tool("search-messages", {
89
106
  account: z.string().optional().describe("Account to search in (omit to search all accounts)"),
90
107
  isRead: z.boolean().optional().describe("Filter by read status"),
91
108
  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')"),
109
+ dateFrom: DATE_FILTER_SCHEMA.describe("Start date filter (e.g., 'January 1, 2026')"),
110
+ dateTo: DATE_FILTER_SCHEMA.describe("End date filter (e.g., 'March 1, 2026')"),
94
111
  limit: z.number().optional().describe("Maximum number of results (default: 50)"),
95
112
  }, withErrorHandling(({ query, mailbox, account, limit = 50, dateFrom, dateTo }) => {
96
113
  const messages = mailManager.searchMessages(query, mailbox, account, limit, dateFrom, dateTo);
@@ -104,7 +121,7 @@ server.tool("search-messages", {
104
121
  }, "Error searching messages"));
105
122
  // --- get-message ---
106
123
  server.tool("get-message", {
107
- id: z.string().min(1, "Message ID is required"),
124
+ id: MESSAGE_ID_SCHEMA,
108
125
  preferHtml: z.boolean().optional().describe("Return HTML source instead of plain text"),
109
126
  }, withErrorHandling(({ id, preferHtml }) => {
110
127
  const content = mailManager.getMessageContent(id);
@@ -144,6 +161,7 @@ server.tool("send-email", {
144
161
  account: z.string().optional().describe("Account to send from"),
145
162
  attachments: z
146
163
  .array(z.string())
164
+ .max(20, "Cannot attach more than 20 files")
147
165
  .optional()
148
166
  .describe("Absolute file paths to attach (e.g., ['/Users/me/report.pdf'])"),
149
167
  }, withErrorHandling(({ to, subject, body, cc, bcc, account, attachments }) => {
@@ -208,6 +226,7 @@ server.tool("create-draft", {
208
226
  account: z.string().optional().describe("Account to create draft in"),
209
227
  attachments: z
210
228
  .array(z.string())
229
+ .max(20, "Cannot attach more than 20 files")
211
230
  .optional()
212
231
  .describe("Absolute file paths to attach (e.g., ['/Users/me/report.pdf'])"),
213
232
  }, withErrorHandling(({ to, subject, body, cc, bcc, account, attachments }) => {
@@ -220,7 +239,7 @@ server.tool("create-draft", {
220
239
  }, "Error creating draft"));
221
240
  // --- reply-to-message ---
222
241
  server.tool("reply-to-message", {
223
- id: z.string().min(1, "Message ID is required"),
242
+ id: MESSAGE_ID_SCHEMA,
224
243
  body: z.string().min(1, "Reply body is required"),
225
244
  replyAll: z.boolean().optional().default(false).describe("Reply to all recipients"),
226
245
  send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
@@ -233,7 +252,7 @@ server.tool("reply-to-message", {
233
252
  }, "Error replying to message"));
234
253
  // --- forward-message ---
235
254
  server.tool("forward-message", {
236
- id: z.string().min(1, "Message ID is required"),
255
+ id: MESSAGE_ID_SCHEMA,
237
256
  to: z.array(z.string()).min(1, "At least one recipient is required"),
238
257
  body: z.string().optional().describe("Optional message to prepend"),
239
258
  send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
@@ -246,7 +265,7 @@ server.tool("forward-message", {
246
265
  }, "Error forwarding message"));
247
266
  // --- mark-as-read ---
248
267
  server.tool("mark-as-read", {
249
- id: z.string().min(1, "Message ID is required"),
268
+ id: MESSAGE_ID_SCHEMA,
250
269
  }, withErrorHandling(({ id }) => {
251
270
  const success = mailManager.markAsRead(id);
252
271
  if (!success) {
@@ -256,7 +275,7 @@ server.tool("mark-as-read", {
256
275
  }, "Error marking message as read"));
257
276
  // --- mark-as-unread ---
258
277
  server.tool("mark-as-unread", {
259
- id: z.string().min(1, "Message ID is required"),
278
+ id: MESSAGE_ID_SCHEMA,
260
279
  }, withErrorHandling(({ id }) => {
261
280
  const success = mailManager.markAsUnread(id);
262
281
  if (!success) {
@@ -266,7 +285,7 @@ server.tool("mark-as-unread", {
266
285
  }, "Error marking message as unread"));
267
286
  // --- flag-message ---
268
287
  server.tool("flag-message", {
269
- id: z.string().min(1, "Message ID is required"),
288
+ id: MESSAGE_ID_SCHEMA,
270
289
  }, withErrorHandling(({ id }) => {
271
290
  const success = mailManager.flagMessage(id);
272
291
  if (!success) {
@@ -276,7 +295,7 @@ server.tool("flag-message", {
276
295
  }, "Error flagging message"));
277
296
  // --- unflag-message ---
278
297
  server.tool("unflag-message", {
279
- id: z.string().min(1, "Message ID is required"),
298
+ id: MESSAGE_ID_SCHEMA,
280
299
  }, withErrorHandling(({ id }) => {
281
300
  const success = mailManager.unflagMessage(id);
282
301
  if (!success) {
@@ -286,7 +305,7 @@ server.tool("unflag-message", {
286
305
  }, "Error unflagging message"));
287
306
  // --- delete-message ---
288
307
  server.tool("delete-message", {
289
- id: z.string().min(1, "Message ID is required"),
308
+ id: MESSAGE_ID_SCHEMA,
290
309
  }, withErrorHandling(({ id }) => {
291
310
  const success = mailManager.deleteMessage(id);
292
311
  if (!success) {
@@ -296,7 +315,7 @@ server.tool("delete-message", {
296
315
  }, "Error deleting message"));
297
316
  // --- move-message ---
298
317
  server.tool("move-message", {
299
- id: z.string().min(1, "Message ID is required"),
318
+ id: MESSAGE_ID_SCHEMA,
300
319
  mailbox: z.string().min(1, "Destination mailbox is required"),
301
320
  account: z.string().optional().describe("Account containing the destination mailbox"),
302
321
  }, withErrorHandling(({ id, mailbox, account }) => {
@@ -308,7 +327,7 @@ server.tool("move-message", {
308
327
  }, "Error moving message"));
309
328
  // --- batch-delete-messages ---
310
329
  server.tool("batch-delete-messages", {
311
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
330
+ ids: BATCH_IDS_SCHEMA,
312
331
  }, withErrorHandling(({ ids }) => {
313
332
  const results = mailManager.batchDeleteMessages(ids);
314
333
  const successCount = results.filter((r) => r.success).length;
@@ -325,7 +344,7 @@ server.tool("batch-delete-messages", {
325
344
  }, "Error batch deleting messages"));
326
345
  // --- batch-move-messages ---
327
346
  server.tool("batch-move-messages", {
328
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
347
+ ids: BATCH_IDS_SCHEMA,
329
348
  mailbox: z.string().min(1, "Destination mailbox is required"),
330
349
  account: z.string().optional().describe("Account containing the destination mailbox"),
331
350
  }, withErrorHandling(({ ids, mailbox, account }) => {
@@ -344,7 +363,7 @@ server.tool("batch-move-messages", {
344
363
  }, "Error batch moving messages"));
345
364
  // --- batch-mark-as-read ---
346
365
  server.tool("batch-mark-as-read", {
347
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
366
+ ids: BATCH_IDS_SCHEMA,
348
367
  }, withErrorHandling(({ ids }) => {
349
368
  const results = mailManager.batchMarkAsRead(ids);
350
369
  const successCount = results.filter((r) => r.success).length;
@@ -361,7 +380,7 @@ server.tool("batch-mark-as-read", {
361
380
  }, "Error batch marking messages as read"));
362
381
  // --- batch-mark-as-unread ---
363
382
  server.tool("batch-mark-as-unread", {
364
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
383
+ ids: BATCH_IDS_SCHEMA,
365
384
  }, withErrorHandling(({ ids }) => {
366
385
  const results = mailManager.batchMarkAsUnread(ids);
367
386
  const successCount = results.filter((r) => r.success).length;
@@ -378,7 +397,7 @@ server.tool("batch-mark-as-unread", {
378
397
  }, "Error batch marking messages as unread"));
379
398
  // --- batch-flag-messages ---
380
399
  server.tool("batch-flag-messages", {
381
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
400
+ ids: BATCH_IDS_SCHEMA,
382
401
  }, withErrorHandling(({ ids }) => {
383
402
  const results = mailManager.batchFlagMessages(ids);
384
403
  const successCount = results.filter((r) => r.success).length;
@@ -395,7 +414,7 @@ server.tool("batch-flag-messages", {
395
414
  }, "Error batch flagging messages"));
396
415
  // --- batch-unflag-messages ---
397
416
  server.tool("batch-unflag-messages", {
398
- ids: z.array(z.string()).min(1, "At least one message ID is required"),
417
+ ids: BATCH_IDS_SCHEMA,
399
418
  }, withErrorHandling(({ ids }) => {
400
419
  const results = mailManager.batchUnflagMessages(ids);
401
420
  const successCount = results.filter((r) => r.success).length;
@@ -412,7 +431,7 @@ server.tool("batch-unflag-messages", {
412
431
  }, "Error batch unflagging messages"));
413
432
  // --- list-attachments ---
414
433
  server.tool("list-attachments", {
415
- id: z.string().min(1, "Message ID is required"),
434
+ id: MESSAGE_ID_SCHEMA,
416
435
  }, withErrorHandling(({ id }) => {
417
436
  const attachments = mailManager.listAttachments(id);
418
437
  if (attachments.length === 0) {
@@ -428,7 +447,7 @@ server.tool("list-attachments", {
428
447
  }, "Error listing attachments"));
429
448
  // --- save-attachment ---
430
449
  server.tool("save-attachment", {
431
- id: z.string().min(1, "Message ID is required"),
450
+ id: MESSAGE_ID_SCHEMA,
432
451
  attachmentName: z.string().min(1, "Attachment name is required"),
433
452
  savePath: z.string().min(1, "Save directory path is required"),
434
453
  }, 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
@@ -310,15 +311,18 @@ export class AppleMailManager {
310
311
  const safeQuery = escapeForAppleScript(query);
311
312
  searchCondition = `whose subject contains "${safeQuery}" or sender contains "${safeQuery}"`;
312
313
  }
313
- // Build date filter AppleScript
314
+ // Build date filter AppleScript.
315
+ // Note: dateFrom/dateTo are already validated by DATE_FILTER_SCHEMA (alphanumeric + safe
316
+ // punctuation only), so escapeForAppleScript() below is belt-and-suspenders — it won't
317
+ // alter valid date strings but guards against future schema changes.
314
318
  let dateFilter = "";
315
319
  if (dateFrom || dateTo) {
316
320
  const dateChecks = [];
317
321
  if (dateFrom) {
318
- dateChecks.push(`date received of msg >= date "${dateFrom}"`);
322
+ dateChecks.push(`date received of msg >= date "${escapeForAppleScript(dateFrom)}"`);
319
323
  }
320
324
  if (dateTo) {
321
- dateChecks.push(`date received of msg <= date "${dateTo}"`);
325
+ dateChecks.push(`date received of msg <= date "${escapeForAppleScript(dateTo)}"`);
322
326
  }
323
327
  dateFilter = dateChecks.join(" and ");
324
328
  }
@@ -368,7 +372,7 @@ export class AppleMailManager {
368
372
  repeat with acct in accounts
369
373
  repeat with mb in mailboxes of acct
370
374
  try
371
- set matchingMsgs to (messages of mb whose id is ${id})
375
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
372
376
  if (count of matchingMsgs) > 0 then
373
377
  set msg to item 1 of matchingMsgs
374
378
  set msgSubject to subject of msg
@@ -423,7 +427,7 @@ export class AppleMailManager {
423
427
  repeat with acct in accounts
424
428
  repeat with mb in mailboxes of acct
425
429
  try
426
- set matchingMsgs to (messages of mb whose id is ${id})
430
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
427
431
  if (count of matchingMsgs) > 0 then
428
432
  set msg to item 1 of matchingMsgs
429
433
  set msgSubject to subject of msg
@@ -728,7 +732,7 @@ export class AppleMailManager {
728
732
  repeat with acct in accounts
729
733
  repeat with mb in mailboxes of acct
730
734
  try
731
- set matchingMsgs to (messages of mb whose id is ${id})
735
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
732
736
  if (count of matchingMsgs) > 0 then
733
737
  set msg to item 1 of matchingMsgs
734
738
  set theReply to reply msg with opening window${replyAllClause}
@@ -773,7 +777,7 @@ export class AppleMailManager {
773
777
  repeat with acct in accounts
774
778
  repeat with mb in mailboxes of acct
775
779
  try
776
- set matchingMsgs to (messages of mb whose id is ${id})
780
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
777
781
  if (count of matchingMsgs) > 0 then
778
782
  set msg to item 1 of matchingMsgs
779
783
  set theForward to forward msg with opening window
@@ -806,7 +810,7 @@ export class AppleMailManager {
806
810
  repeat with acct in accounts
807
811
  repeat with mb in mailboxes of acct
808
812
  try
809
- set matchingMsgs to (messages of mb whose id is ${id})
813
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
810
814
  if (count of matchingMsgs) > 0 then
811
815
  set msg to item 1 of matchingMsgs
812
816
  ${operation}
@@ -894,7 +898,7 @@ export class AppleMailManager {
894
898
  repeat with acct in accounts
895
899
  repeat with mb in mailboxes of acct
896
900
  try
897
- set matchingMsgs to (messages of mb whose id is ${id})
901
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
898
902
  if (count of matchingMsgs) > 0 then
899
903
  set msg to item 1 of matchingMsgs
900
904
  set destMailbox to mailbox "${safeMailbox}" of account "${safeAccount}"
@@ -1014,7 +1018,7 @@ export class AppleMailManager {
1014
1018
  repeat with acct in accounts
1015
1019
  repeat with mb in mailboxes of acct
1016
1020
  try
1017
- set matchingMsgs to (messages of mb whose id is ${id})
1021
+ set matchingMsgs to (messages of mb whose id is ${Number(id)})
1018
1022
  if (count of matchingMsgs) > 0 then
1019
1023
  set msg to item 1 of matchingMsgs
1020
1024
  set outputText to ""
@@ -1060,14 +1064,30 @@ export class AppleMailManager {
1060
1064
  * Save an attachment from a message to disk.
1061
1065
  */
1062
1066
  saveAttachment(id, attachmentName, savePath) {
1067
+ // Validate attachment name: block path separators, traversal, null bytes, and backslashes
1068
+ if (/[/\\\0]/.test(attachmentName) || attachmentName.includes("..")) {
1069
+ console.error(`Invalid attachment name: "${attachmentName}"`);
1070
+ return false;
1071
+ }
1072
+ // Resolve the save path to prevent symlink / ".." traversal bypass
1073
+ const resolvedPath = resolve(savePath);
1074
+ const allowedPrefixes = [homedir(), "/tmp", "/private/tmp", "/Volumes"];
1075
+ const isAllowed = allowedPrefixes.some((prefix) => resolvedPath.startsWith(prefix));
1076
+ if (!isAllowed) {
1077
+ console.error(`Save path "${savePath}" is outside allowed directories`);
1078
+ return false;
1079
+ }
1063
1080
  const safeName = escapeForAppleScript(attachmentName);
1064
- const safePath = escapeForAppleScript(savePath);
1081
+ const safePath = escapeForAppleScript(resolvedPath);
1082
+ // Use Number(id) as defense-in-depth — the Zod schema already enforces numeric IDs,
1083
+ // but this ensures raw interpolation into AppleScript is safe even if validation changes.
1084
+ const numericId = Number(id);
1065
1085
  const script = buildAppLevelScript(`
1066
1086
  try
1067
1087
  repeat with acct in accounts
1068
1088
  repeat with mb in mailboxes of acct
1069
1089
  try
1070
- set matchingMsgs to (messages of mb whose id is ${id})
1090
+ set matchingMsgs to (messages of mb whose id is ${numericId})
1071
1091
  if (count of matchingMsgs) > 0 then
1072
1092
  set msg to item 1 of matchingMsgs
1073
1093
  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.2.1",
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",