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 +21 -14
- package/build/index.js +38 -19
- package/build/services/appleMailManager.d.ts.map +1 -1
- package/build/services/appleMailManager.js +33 -13
- package/package.json +3 -1
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
|
|
780
|
-
npm run build
|
|
781
|
-
npm test
|
|
782
|
-
npm run
|
|
783
|
-
npm run
|
|
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:
|
|
93
|
-
dateTo:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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":"AAAA;;;;;;;;;;;;;GAaG;
|
|
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(
|
|
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 ${
|
|
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.
|
|
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",
|