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 +26 -19
- package/build/index.js +49 -21
- package/build/services/appleMailManager.d.ts.map +1 -1
- package/build/services/appleMailManager.js +134 -23
- package/package.json +3 -1
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 (
|
|
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 (
|
|
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
|
|
612
|
-
"You have 12 messages
|
|
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
|
|
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,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
|
|
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:
|
|
93
|
-
dateTo:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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":"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
|
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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 ${
|
|
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.
|
|
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",
|