apple-mail-mcp 1.0.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/LICENSE +21 -0
- package/README.md +522 -0
- package/build/index.d.ts +23 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +401 -0
- package/build/services/appleMailManager.d.ts +232 -0
- package/build/services/appleMailManager.d.ts.map +1 -0
- package/build/services/appleMailManager.js +1208 -0
- package/build/types.d.ts +306 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +13 -0
- package/build/utils/applescript.d.ts +45 -0
- package/build/utils/applescript.d.ts.map +1 -0
- package/build/utils/applescript.js +372 -0
- package/package.json +86 -0
package/build/index.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Apple Mail MCP Server
|
|
4
|
+
*
|
|
5
|
+
* A Model Context Protocol (MCP) server that provides AI assistants
|
|
6
|
+
* with the ability to interact with Apple Mail on macOS.
|
|
7
|
+
*
|
|
8
|
+
* This server exposes tools for:
|
|
9
|
+
* - Reading and searching emails
|
|
10
|
+
* - Sending emails
|
|
11
|
+
* - Managing mailboxes
|
|
12
|
+
* - Managing multiple accounts (iCloud, Gmail, Exchange, etc.)
|
|
13
|
+
*
|
|
14
|
+
* Architecture:
|
|
15
|
+
* - Tool definitions are declarative (schema + handler)
|
|
16
|
+
* - The AppleMailManager class handles all AppleScript operations
|
|
17
|
+
* - Error handling is consistent across all tools
|
|
18
|
+
*
|
|
19
|
+
* @module apple-mail-mcp
|
|
20
|
+
* @see https://modelcontextprotocol.io
|
|
21
|
+
*/
|
|
22
|
+
import { createRequire } from "module";
|
|
23
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
24
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { AppleMailManager } from "./services/appleMailManager.js";
|
|
27
|
+
// Read version from package.json to keep it in sync
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const { version } = require("../package.json");
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Server Initialization
|
|
32
|
+
// =============================================================================
|
|
33
|
+
/**
|
|
34
|
+
* MCP server instance configured for Apple Mail operations.
|
|
35
|
+
*/
|
|
36
|
+
const server = new McpServer({
|
|
37
|
+
name: "apple-mail",
|
|
38
|
+
version,
|
|
39
|
+
description: "MCP server for managing Apple Mail - read, search, send, and organize emails",
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* Singleton instance of the Apple Mail manager.
|
|
43
|
+
* Handles all AppleScript execution and mail operations.
|
|
44
|
+
*/
|
|
45
|
+
const mailManager = new AppleMailManager();
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// Response Helpers
|
|
48
|
+
// =============================================================================
|
|
49
|
+
/**
|
|
50
|
+
* Creates a successful MCP tool response.
|
|
51
|
+
*/
|
|
52
|
+
function successResponse(message) {
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: "text", text: message }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Creates an error MCP tool response.
|
|
59
|
+
*/
|
|
60
|
+
function errorResponse(message) {
|
|
61
|
+
return {
|
|
62
|
+
content: [{ type: "text", text: message }],
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Wraps a tool handler with consistent error handling.
|
|
68
|
+
*/
|
|
69
|
+
function withErrorHandling(handler, errorPrefix) {
|
|
70
|
+
return async (params) => {
|
|
71
|
+
try {
|
|
72
|
+
return handler(params);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
76
|
+
return errorResponse(`${errorPrefix}: ${message}`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// Message Tools
|
|
82
|
+
// =============================================================================
|
|
83
|
+
// --- search-messages ---
|
|
84
|
+
server.tool("search-messages", {
|
|
85
|
+
query: z.string().optional().describe("Text to search for in subject, sender, or content"),
|
|
86
|
+
from: z.string().optional().describe("Filter by sender email address"),
|
|
87
|
+
subject: z.string().optional().describe("Filter by subject line"),
|
|
88
|
+
mailbox: z.string().optional().describe("Mailbox to search in (e.g., 'INBOX')"),
|
|
89
|
+
account: z.string().optional().describe("Account to search in"),
|
|
90
|
+
isRead: z.boolean().optional().describe("Filter by read status"),
|
|
91
|
+
isFlagged: z.boolean().optional().describe("Filter by flagged status"),
|
|
92
|
+
limit: z.number().optional().describe("Maximum number of results (default: 50)"),
|
|
93
|
+
}, withErrorHandling(({ query, mailbox, account, limit = 50 }) => {
|
|
94
|
+
const messages = mailManager.searchMessages(query, mailbox, account, limit);
|
|
95
|
+
if (messages.length === 0) {
|
|
96
|
+
return successResponse("No messages found matching criteria");
|
|
97
|
+
}
|
|
98
|
+
const messageList = messages
|
|
99
|
+
.map((m) => ` - ${m.subject} (from: ${m.sender}) [${m.isRead ? "read" : "unread"}]`)
|
|
100
|
+
.join("\n");
|
|
101
|
+
return successResponse(`Found ${messages.length} message(s):\n${messageList}`);
|
|
102
|
+
}, "Error searching messages"));
|
|
103
|
+
// --- get-message ---
|
|
104
|
+
server.tool("get-message", {
|
|
105
|
+
id: z.string().min(1, "Message ID is required"),
|
|
106
|
+
}, withErrorHandling(({ id }) => {
|
|
107
|
+
const content = mailManager.getMessageContent(id);
|
|
108
|
+
if (!content) {
|
|
109
|
+
return errorResponse(`Message with ID "${id}" not found`);
|
|
110
|
+
}
|
|
111
|
+
return successResponse(`Subject: ${content.subject}\n\n${content.plainText}`);
|
|
112
|
+
}, "Error retrieving message"));
|
|
113
|
+
// --- list-messages ---
|
|
114
|
+
server.tool("list-messages", {
|
|
115
|
+
mailbox: z.string().optional().describe("Mailbox to list messages from (default: INBOX)"),
|
|
116
|
+
account: z.string().optional().describe("Account to list messages from"),
|
|
117
|
+
limit: z.number().optional().describe("Maximum number of messages (default: 50)"),
|
|
118
|
+
unreadOnly: z.boolean().optional().describe("Only show unread messages"),
|
|
119
|
+
}, withErrorHandling(({ mailbox, account, limit = 50 }) => {
|
|
120
|
+
const messages = mailManager.listMessages(mailbox, account, limit);
|
|
121
|
+
if (messages.length === 0) {
|
|
122
|
+
return successResponse("No messages found");
|
|
123
|
+
}
|
|
124
|
+
const messageList = messages.map((m) => ` - ${m.subject} (from: ${m.sender})`).join("\n");
|
|
125
|
+
return successResponse(`Found ${messages.length} message(s):\n${messageList}`);
|
|
126
|
+
}, "Error listing messages"));
|
|
127
|
+
// --- send-email ---
|
|
128
|
+
server.tool("send-email", {
|
|
129
|
+
to: z.array(z.string()).min(1, "At least one recipient is required"),
|
|
130
|
+
subject: z.string().min(1, "Subject is required"),
|
|
131
|
+
body: z.string().min(1, "Body is required"),
|
|
132
|
+
cc: z.array(z.string()).optional().describe("CC recipients"),
|
|
133
|
+
bcc: z.array(z.string()).optional().describe("BCC recipients"),
|
|
134
|
+
account: z.string().optional().describe("Account to send from"),
|
|
135
|
+
}, withErrorHandling(({ to, subject, body, cc, bcc, account }) => {
|
|
136
|
+
const success = mailManager.sendEmail(to, subject, body, cc, bcc, account);
|
|
137
|
+
if (!success) {
|
|
138
|
+
return errorResponse("Failed to send email. Check Mail.app configuration.");
|
|
139
|
+
}
|
|
140
|
+
return successResponse(`Email sent to ${to.join(", ")}`);
|
|
141
|
+
}, "Error sending email"));
|
|
142
|
+
// --- create-draft ---
|
|
143
|
+
server.tool("create-draft", {
|
|
144
|
+
to: z.array(z.string()).min(1, "At least one recipient is required"),
|
|
145
|
+
subject: z.string().min(1, "Subject is required"),
|
|
146
|
+
body: z.string().min(1, "Body is required"),
|
|
147
|
+
cc: z.array(z.string()).optional().describe("CC recipients"),
|
|
148
|
+
bcc: z.array(z.string()).optional().describe("BCC recipients"),
|
|
149
|
+
account: z.string().optional().describe("Account to create draft in"),
|
|
150
|
+
}, withErrorHandling(({ to, subject, body, cc, bcc, account }) => {
|
|
151
|
+
const success = mailManager.createDraft(to, subject, body, cc, bcc, account);
|
|
152
|
+
if (!success) {
|
|
153
|
+
return errorResponse("Failed to create draft. Check Mail.app configuration.");
|
|
154
|
+
}
|
|
155
|
+
return successResponse(`Draft created for ${to.join(", ")}`);
|
|
156
|
+
}, "Error creating draft"));
|
|
157
|
+
// --- reply-to-message ---
|
|
158
|
+
server.tool("reply-to-message", {
|
|
159
|
+
id: z.string().min(1, "Message ID is required"),
|
|
160
|
+
body: z.string().min(1, "Reply body is required"),
|
|
161
|
+
replyAll: z.boolean().optional().default(false).describe("Reply to all recipients"),
|
|
162
|
+
send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
|
|
163
|
+
}, withErrorHandling(({ id, body, replyAll, send }) => {
|
|
164
|
+
const success = mailManager.replyToMessage(id, body, replyAll, send);
|
|
165
|
+
if (!success) {
|
|
166
|
+
return errorResponse(`Failed to reply to message "${id}"`);
|
|
167
|
+
}
|
|
168
|
+
return successResponse(send ? "Reply sent" : "Reply saved as draft");
|
|
169
|
+
}, "Error replying to message"));
|
|
170
|
+
// --- forward-message ---
|
|
171
|
+
server.tool("forward-message", {
|
|
172
|
+
id: z.string().min(1, "Message ID is required"),
|
|
173
|
+
to: z.array(z.string()).min(1, "At least one recipient is required"),
|
|
174
|
+
body: z.string().optional().describe("Optional message to prepend"),
|
|
175
|
+
send: z.boolean().optional().default(true).describe("Send immediately (false = save as draft)"),
|
|
176
|
+
}, withErrorHandling(({ id, to, body, send }) => {
|
|
177
|
+
const success = mailManager.forwardMessage(id, to, body, send);
|
|
178
|
+
if (!success) {
|
|
179
|
+
return errorResponse(`Failed to forward message "${id}"`);
|
|
180
|
+
}
|
|
181
|
+
return successResponse(send ? `Message forwarded to ${to.join(", ")}` : "Forward saved as draft");
|
|
182
|
+
}, "Error forwarding message"));
|
|
183
|
+
// --- mark-as-read ---
|
|
184
|
+
server.tool("mark-as-read", {
|
|
185
|
+
id: z.string().min(1, "Message ID is required"),
|
|
186
|
+
}, withErrorHandling(({ id }) => {
|
|
187
|
+
const success = mailManager.markAsRead(id);
|
|
188
|
+
if (!success) {
|
|
189
|
+
return errorResponse(`Failed to mark message "${id}" as read`);
|
|
190
|
+
}
|
|
191
|
+
return successResponse("Message marked as read");
|
|
192
|
+
}, "Error marking message as read"));
|
|
193
|
+
// --- mark-as-unread ---
|
|
194
|
+
server.tool("mark-as-unread", {
|
|
195
|
+
id: z.string().min(1, "Message ID is required"),
|
|
196
|
+
}, withErrorHandling(({ id }) => {
|
|
197
|
+
const success = mailManager.markAsUnread(id);
|
|
198
|
+
if (!success) {
|
|
199
|
+
return errorResponse(`Failed to mark message "${id}" as unread`);
|
|
200
|
+
}
|
|
201
|
+
return successResponse("Message marked as unread");
|
|
202
|
+
}, "Error marking message as unread"));
|
|
203
|
+
// --- flag-message ---
|
|
204
|
+
server.tool("flag-message", {
|
|
205
|
+
id: z.string().min(1, "Message ID is required"),
|
|
206
|
+
}, withErrorHandling(({ id }) => {
|
|
207
|
+
const success = mailManager.flagMessage(id);
|
|
208
|
+
if (!success) {
|
|
209
|
+
return errorResponse(`Failed to flag message "${id}"`);
|
|
210
|
+
}
|
|
211
|
+
return successResponse("Message flagged");
|
|
212
|
+
}, "Error flagging message"));
|
|
213
|
+
// --- delete-message ---
|
|
214
|
+
server.tool("delete-message", {
|
|
215
|
+
id: z.string().min(1, "Message ID is required"),
|
|
216
|
+
}, withErrorHandling(({ id }) => {
|
|
217
|
+
const success = mailManager.deleteMessage(id);
|
|
218
|
+
if (!success) {
|
|
219
|
+
return errorResponse(`Failed to delete message "${id}"`);
|
|
220
|
+
}
|
|
221
|
+
return successResponse("Message deleted");
|
|
222
|
+
}, "Error deleting message"));
|
|
223
|
+
// --- move-message ---
|
|
224
|
+
server.tool("move-message", {
|
|
225
|
+
id: z.string().min(1, "Message ID is required"),
|
|
226
|
+
mailbox: z.string().min(1, "Destination mailbox is required"),
|
|
227
|
+
account: z.string().optional().describe("Account containing the destination mailbox"),
|
|
228
|
+
}, withErrorHandling(({ id, mailbox, account }) => {
|
|
229
|
+
const success = mailManager.moveMessage(id, mailbox, account);
|
|
230
|
+
if (!success) {
|
|
231
|
+
return errorResponse(`Failed to move message to "${mailbox}"`);
|
|
232
|
+
}
|
|
233
|
+
return successResponse(`Message moved to "${mailbox}"`);
|
|
234
|
+
}, "Error moving message"));
|
|
235
|
+
// --- batch-delete-messages ---
|
|
236
|
+
server.tool("batch-delete-messages", {
|
|
237
|
+
ids: z.array(z.string()).min(1, "At least one message ID is required"),
|
|
238
|
+
}, withErrorHandling(({ ids }) => {
|
|
239
|
+
const results = mailManager.batchDeleteMessages(ids);
|
|
240
|
+
const successCount = results.filter((r) => r.success).length;
|
|
241
|
+
const failCount = results.length - successCount;
|
|
242
|
+
if (failCount === 0) {
|
|
243
|
+
return successResponse(`Successfully deleted ${successCount} message(s)`);
|
|
244
|
+
}
|
|
245
|
+
else if (successCount === 0) {
|
|
246
|
+
return errorResponse(`Failed to delete all ${failCount} message(s)`);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
return successResponse(`Deleted ${successCount} message(s), ${failCount} failed`);
|
|
250
|
+
}
|
|
251
|
+
}, "Error batch deleting messages"));
|
|
252
|
+
// --- batch-move-messages ---
|
|
253
|
+
server.tool("batch-move-messages", {
|
|
254
|
+
ids: z.array(z.string()).min(1, "At least one message ID is required"),
|
|
255
|
+
mailbox: z.string().min(1, "Destination mailbox is required"),
|
|
256
|
+
account: z.string().optional().describe("Account containing the destination mailbox"),
|
|
257
|
+
}, withErrorHandling(({ ids, mailbox, account }) => {
|
|
258
|
+
const results = mailManager.batchMoveMessages(ids, mailbox, account);
|
|
259
|
+
const successCount = results.filter((r) => r.success).length;
|
|
260
|
+
const failCount = results.length - successCount;
|
|
261
|
+
if (failCount === 0) {
|
|
262
|
+
return successResponse(`Successfully moved ${successCount} message(s) to "${mailbox}"`);
|
|
263
|
+
}
|
|
264
|
+
else if (successCount === 0) {
|
|
265
|
+
return errorResponse(`Failed to move all ${failCount} message(s)`);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
return successResponse(`Moved ${successCount} message(s) to "${mailbox}", ${failCount} failed`);
|
|
269
|
+
}
|
|
270
|
+
}, "Error batch moving messages"));
|
|
271
|
+
// --- batch-mark-as-read ---
|
|
272
|
+
server.tool("batch-mark-as-read", {
|
|
273
|
+
ids: z.array(z.string()).min(1, "At least one message ID is required"),
|
|
274
|
+
}, withErrorHandling(({ ids }) => {
|
|
275
|
+
const results = mailManager.batchMarkAsRead(ids);
|
|
276
|
+
const successCount = results.filter((r) => r.success).length;
|
|
277
|
+
const failCount = results.length - successCount;
|
|
278
|
+
if (failCount === 0) {
|
|
279
|
+
return successResponse(`Successfully marked ${successCount} message(s) as read`);
|
|
280
|
+
}
|
|
281
|
+
else if (successCount === 0) {
|
|
282
|
+
return errorResponse(`Failed to mark all ${failCount} message(s) as read`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
return successResponse(`Marked ${successCount} message(s) as read, ${failCount} failed`);
|
|
286
|
+
}
|
|
287
|
+
}, "Error batch marking messages as read"));
|
|
288
|
+
// --- list-attachments ---
|
|
289
|
+
server.tool("list-attachments", {
|
|
290
|
+
id: z.string().min(1, "Message ID is required"),
|
|
291
|
+
}, withErrorHandling(({ id }) => {
|
|
292
|
+
const attachments = mailManager.listAttachments(id);
|
|
293
|
+
if (attachments.length === 0) {
|
|
294
|
+
return successResponse("No attachments found");
|
|
295
|
+
}
|
|
296
|
+
const attachmentList = attachments
|
|
297
|
+
.map((a) => {
|
|
298
|
+
const sizeKb = Math.round(a.size / 1024);
|
|
299
|
+
return ` - ${a.name} (${a.mimeType}, ${sizeKb} KB)`;
|
|
300
|
+
})
|
|
301
|
+
.join("\n");
|
|
302
|
+
return successResponse(`Found ${attachments.length} attachment(s):\n${attachmentList}`);
|
|
303
|
+
}, "Error listing attachments"));
|
|
304
|
+
// =============================================================================
|
|
305
|
+
// Mailbox Tools
|
|
306
|
+
// =============================================================================
|
|
307
|
+
// --- list-mailboxes ---
|
|
308
|
+
server.tool("list-mailboxes", {
|
|
309
|
+
account: z.string().optional().describe("Account to list mailboxes from"),
|
|
310
|
+
}, withErrorHandling(({ account }) => {
|
|
311
|
+
const mailboxes = mailManager.listMailboxes(account);
|
|
312
|
+
if (mailboxes.length === 0) {
|
|
313
|
+
return successResponse("No mailboxes found");
|
|
314
|
+
}
|
|
315
|
+
const mailboxList = mailboxes.map((m) => ` - ${m.name} (${m.unreadCount} unread)`).join("\n");
|
|
316
|
+
return successResponse(`Found ${mailboxes.length} mailbox(es):\n${mailboxList}`);
|
|
317
|
+
}, "Error listing mailboxes"));
|
|
318
|
+
// --- get-unread-count ---
|
|
319
|
+
server.tool("get-unread-count", {
|
|
320
|
+
mailbox: z.string().optional().describe("Mailbox to check (default: all)"),
|
|
321
|
+
account: z.string().optional().describe("Account to check"),
|
|
322
|
+
}, withErrorHandling(({ mailbox, account }) => {
|
|
323
|
+
const count = mailManager.getUnreadCount(mailbox, account);
|
|
324
|
+
const location = mailbox ? ` in "${mailbox}"` : "";
|
|
325
|
+
return successResponse(`${count} unread message(s)${location}`);
|
|
326
|
+
}, "Error getting unread count"));
|
|
327
|
+
// =============================================================================
|
|
328
|
+
// Account Tools
|
|
329
|
+
// =============================================================================
|
|
330
|
+
// --- list-accounts ---
|
|
331
|
+
server.tool("list-accounts", {}, withErrorHandling(() => {
|
|
332
|
+
const accounts = mailManager.listAccounts();
|
|
333
|
+
if (accounts.length === 0) {
|
|
334
|
+
return successResponse("No Mail accounts found");
|
|
335
|
+
}
|
|
336
|
+
const accountList = accounts.map((a) => ` - ${a.name}`).join("\n");
|
|
337
|
+
return successResponse(`Found ${accounts.length} account(s):\n${accountList}`);
|
|
338
|
+
}, "Error listing accounts"));
|
|
339
|
+
// =============================================================================
|
|
340
|
+
// Diagnostics Tools
|
|
341
|
+
// =============================================================================
|
|
342
|
+
// --- health-check ---
|
|
343
|
+
server.tool("health-check", {}, withErrorHandling(() => {
|
|
344
|
+
const result = mailManager.healthCheck();
|
|
345
|
+
const statusIcon = result.healthy ? "✓" : "✗";
|
|
346
|
+
const statusText = result.healthy ? "All checks passed" : "Issues detected";
|
|
347
|
+
const checkLines = result.checks
|
|
348
|
+
.map((c) => {
|
|
349
|
+
const icon = c.passed ? "✓" : "✗";
|
|
350
|
+
return ` ${icon} ${c.name}: ${c.message}`;
|
|
351
|
+
})
|
|
352
|
+
.join("\n");
|
|
353
|
+
return successResponse(`${statusIcon} ${statusText}\n\n${checkLines}`);
|
|
354
|
+
}, "Error running health check"));
|
|
355
|
+
// --- get-mail-stats ---
|
|
356
|
+
server.tool("get-mail-stats", {}, withErrorHandling(() => {
|
|
357
|
+
const stats = mailManager.getMailStats();
|
|
358
|
+
const lines = [];
|
|
359
|
+
lines.push(`📊 Mail Statistics`);
|
|
360
|
+
lines.push(`══════════════════`);
|
|
361
|
+
lines.push(`Total messages: ${stats.totalMessages}`);
|
|
362
|
+
lines.push(`Unread messages: ${stats.totalUnread}`);
|
|
363
|
+
lines.push(``);
|
|
364
|
+
if (stats.recentlyReceived) {
|
|
365
|
+
lines.push(`📥 Recently Received:`);
|
|
366
|
+
lines.push(` Last 24 hours: ${stats.recentlyReceived.last24h}`);
|
|
367
|
+
lines.push(` Last 7 days: ${stats.recentlyReceived.last7d}`);
|
|
368
|
+
lines.push(` Last 30 days: ${stats.recentlyReceived.last30d}`);
|
|
369
|
+
lines.push(``);
|
|
370
|
+
}
|
|
371
|
+
if (stats.accounts.length > 0) {
|
|
372
|
+
lines.push(`📁 By Account:`);
|
|
373
|
+
for (const account of stats.accounts) {
|
|
374
|
+
lines.push(` ${account.name}: ${account.totalMessages} messages (${account.unreadMessages} unread)`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return successResponse(lines.join("\n"));
|
|
378
|
+
}, "Error getting mail statistics"));
|
|
379
|
+
// --- get-sync-status ---
|
|
380
|
+
server.tool("get-sync-status", {}, withErrorHandling(() => {
|
|
381
|
+
const status = mailManager.getSyncStatus();
|
|
382
|
+
const lines = [];
|
|
383
|
+
lines.push(`🔄 Mail Sync Status`);
|
|
384
|
+
lines.push(`═══════════════════`);
|
|
385
|
+
if (status.error) {
|
|
386
|
+
lines.push(`Status: ⚠️ ${status.error}`);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
lines.push(`Mail.app: ${status.recentActivity ? "Running" : "Not running"}`);
|
|
390
|
+
lines.push(`Sync active: ${status.syncDetected ? "Yes" : "No"}`);
|
|
391
|
+
}
|
|
392
|
+
return successResponse(lines.join("\n"));
|
|
393
|
+
}, "Error getting sync status"));
|
|
394
|
+
// =============================================================================
|
|
395
|
+
// Server Startup
|
|
396
|
+
// =============================================================================
|
|
397
|
+
/**
|
|
398
|
+
* Initialize and start the MCP server.
|
|
399
|
+
*/
|
|
400
|
+
const transport = new StdioServerTransport();
|
|
401
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple Mail Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles all interactions with Apple Mail via AppleScript.
|
|
5
|
+
* This is the core service layer for the MCP server.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Text escaping is handled by dedicated helper functions
|
|
9
|
+
* - AppleScript generation uses template builders for consistency
|
|
10
|
+
* - All public methods return typed results (no raw strings)
|
|
11
|
+
* - Error handling is consistent across all operations
|
|
12
|
+
*
|
|
13
|
+
* @module services/appleMailManager
|
|
14
|
+
*/
|
|
15
|
+
import type { Message, MessageContent, Mailbox, Account, Attachment, HealthCheckResult, MailStats, BatchOperationResult, SyncStatus, RecentlyReceivedStats } from "../types.js";
|
|
16
|
+
/**
|
|
17
|
+
* Manager class for Apple Mail operations.
|
|
18
|
+
*
|
|
19
|
+
* Provides methods for:
|
|
20
|
+
* - Reading and searching messages
|
|
21
|
+
* - Sending emails
|
|
22
|
+
* - Managing mailboxes
|
|
23
|
+
* - Listing accounts
|
|
24
|
+
*
|
|
25
|
+
* All operations are synchronous since they rely on AppleScript
|
|
26
|
+
* execution via osascript. Error handling is consistent: methods
|
|
27
|
+
* return null/false/empty-array on failure rather than throwing.
|
|
28
|
+
*/
|
|
29
|
+
export declare class AppleMailManager {
|
|
30
|
+
/**
|
|
31
|
+
* Default account used when no account is specified.
|
|
32
|
+
*/
|
|
33
|
+
private defaultAccount;
|
|
34
|
+
/**
|
|
35
|
+
* Resolves the account to use for an operation.
|
|
36
|
+
* Falls back to first available account if not specified.
|
|
37
|
+
*/
|
|
38
|
+
private resolveAccount;
|
|
39
|
+
/**
|
|
40
|
+
* Resolves a mailbox name to its actual name in the account.
|
|
41
|
+
*
|
|
42
|
+
* Different account types (IMAP, Exchange, iCloud) use different
|
|
43
|
+
* mailbox naming conventions:
|
|
44
|
+
* - IMAP/Gmail: "INBOX", "Sent", "Drafts"
|
|
45
|
+
* - Exchange: "Inbox", "Sent Items", "Deleted Items"
|
|
46
|
+
* - iCloud: "INBOX", "Sent", "Trash"
|
|
47
|
+
*
|
|
48
|
+
* This method tries to find a matching mailbox by:
|
|
49
|
+
* 1. Exact match
|
|
50
|
+
* 2. Case-insensitive match
|
|
51
|
+
* 3. Known aliases (e.g., "Sent" -> "Sent Items")
|
|
52
|
+
*
|
|
53
|
+
* @param mailbox - Requested mailbox name
|
|
54
|
+
* @param account - Account to search in
|
|
55
|
+
* @returns Actual mailbox name, or original if not found
|
|
56
|
+
*/
|
|
57
|
+
private resolveMailbox;
|
|
58
|
+
/**
|
|
59
|
+
* Search for messages matching criteria.
|
|
60
|
+
*
|
|
61
|
+
* @param query - Text to search for in subject or sender
|
|
62
|
+
* @param mailbox - Mailbox to search in (e.g., "INBOX")
|
|
63
|
+
* @param account - Account to search in
|
|
64
|
+
* @param limit - Maximum number of results
|
|
65
|
+
* @returns Array of matching messages
|
|
66
|
+
*/
|
|
67
|
+
searchMessages(query?: string, mailbox?: string, account?: string, limit?: number): Message[];
|
|
68
|
+
/**
|
|
69
|
+
* Get a message by ID.
|
|
70
|
+
*
|
|
71
|
+
* Note: Mail.app message IDs are unique per mailbox. This method searches
|
|
72
|
+
* all mailboxes in all accounts to find the message.
|
|
73
|
+
*/
|
|
74
|
+
getMessageById(id: string): Message | null;
|
|
75
|
+
/**
|
|
76
|
+
* Get the content of a message.
|
|
77
|
+
*/
|
|
78
|
+
getMessageContent(id: string): MessageContent | null;
|
|
79
|
+
/**
|
|
80
|
+
* List messages in a mailbox.
|
|
81
|
+
*
|
|
82
|
+
* @param mailbox - Mailbox to list from (default: INBOX)
|
|
83
|
+
* @param account - Account to list from
|
|
84
|
+
* @param limit - Maximum number of messages
|
|
85
|
+
* @returns Array of messages
|
|
86
|
+
*/
|
|
87
|
+
listMessages(mailbox?: string, account?: string, limit?: number): Message[];
|
|
88
|
+
/**
|
|
89
|
+
* Parse message list output from AppleScript.
|
|
90
|
+
*/
|
|
91
|
+
private parseMessageList;
|
|
92
|
+
/**
|
|
93
|
+
* Send an email.
|
|
94
|
+
*
|
|
95
|
+
* @param to - Recipient email addresses
|
|
96
|
+
* @param subject - Email subject
|
|
97
|
+
* @param body - Email body (plain text)
|
|
98
|
+
* @param cc - CC recipients
|
|
99
|
+
* @param bcc - BCC recipients
|
|
100
|
+
* @param account - Account to send from
|
|
101
|
+
* @returns true if sent successfully
|
|
102
|
+
*/
|
|
103
|
+
sendEmail(to: string[], subject: string, body: string, cc?: string[], bcc?: string[], account?: string): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Create a draft email (saved to Drafts folder, not sent).
|
|
106
|
+
*
|
|
107
|
+
* @param to - Recipient email addresses
|
|
108
|
+
* @param subject - Email subject
|
|
109
|
+
* @param body - Email body (plain text)
|
|
110
|
+
* @param cc - CC recipients
|
|
111
|
+
* @param bcc - BCC recipients
|
|
112
|
+
* @param account - Account to create draft in
|
|
113
|
+
* @returns true if draft created successfully
|
|
114
|
+
*/
|
|
115
|
+
createDraft(to: string[], subject: string, body: string, cc?: string[], bcc?: string[], account?: string): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Reply to a message.
|
|
118
|
+
*
|
|
119
|
+
* @param id - Message ID to reply to
|
|
120
|
+
* @param body - Reply body
|
|
121
|
+
* @param replyAll - If true, reply to all recipients
|
|
122
|
+
* @param send - If true, send immediately; if false, save as draft
|
|
123
|
+
* @returns true if reply created/sent successfully
|
|
124
|
+
*/
|
|
125
|
+
replyToMessage(id: string, body: string, replyAll?: boolean, send?: boolean): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Forward a message.
|
|
128
|
+
*
|
|
129
|
+
* @param id - Message ID to forward
|
|
130
|
+
* @param to - Recipients to forward to
|
|
131
|
+
* @param body - Optional body to prepend
|
|
132
|
+
* @param send - If true, send immediately; if false, save as draft
|
|
133
|
+
* @returns true if forward created/sent successfully
|
|
134
|
+
*/
|
|
135
|
+
forwardMessage(id: string, to: string[], body?: string, send?: boolean): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Helper to find and operate on a message by ID.
|
|
138
|
+
*/
|
|
139
|
+
private findMessageScript;
|
|
140
|
+
/**
|
|
141
|
+
* Mark a message as read.
|
|
142
|
+
*/
|
|
143
|
+
markAsRead(id: string): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Mark a message as unread.
|
|
146
|
+
*/
|
|
147
|
+
markAsUnread(id: string): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Flag a message.
|
|
150
|
+
*/
|
|
151
|
+
flagMessage(id: string): boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Unflag a message.
|
|
154
|
+
*/
|
|
155
|
+
unflagMessage(id: string): boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Delete a message.
|
|
158
|
+
*/
|
|
159
|
+
deleteMessage(id: string): boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Move a message to a different mailbox.
|
|
162
|
+
*/
|
|
163
|
+
moveMessage(id: string, mailbox: string, account?: string): boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Delete multiple messages at once.
|
|
166
|
+
*
|
|
167
|
+
* @param ids - Array of message IDs to delete
|
|
168
|
+
* @returns Array of results for each message
|
|
169
|
+
*/
|
|
170
|
+
batchDeleteMessages(ids: string[]): BatchOperationResult[];
|
|
171
|
+
/**
|
|
172
|
+
* Move multiple messages to a mailbox at once.
|
|
173
|
+
*
|
|
174
|
+
* @param ids - Array of message IDs to move
|
|
175
|
+
* @param mailbox - Destination mailbox name
|
|
176
|
+
* @param account - Account containing the destination mailbox
|
|
177
|
+
* @returns Array of results for each message
|
|
178
|
+
*/
|
|
179
|
+
batchMoveMessages(ids: string[], mailbox: string, account?: string): BatchOperationResult[];
|
|
180
|
+
/**
|
|
181
|
+
* Mark multiple messages as read at once.
|
|
182
|
+
*
|
|
183
|
+
* @param ids - Array of message IDs to mark as read
|
|
184
|
+
* @returns Array of results for each message
|
|
185
|
+
*/
|
|
186
|
+
batchMarkAsRead(ids: string[]): BatchOperationResult[];
|
|
187
|
+
/**
|
|
188
|
+
* List attachments for a message.
|
|
189
|
+
*/
|
|
190
|
+
listAttachments(id: string): Attachment[];
|
|
191
|
+
/**
|
|
192
|
+
* List all mailboxes for an account.
|
|
193
|
+
*/
|
|
194
|
+
listMailboxes(account?: string): Mailbox[];
|
|
195
|
+
/**
|
|
196
|
+
* Get unread count for a mailbox.
|
|
197
|
+
*/
|
|
198
|
+
getUnreadCount(mailbox?: string, account?: string): number;
|
|
199
|
+
/**
|
|
200
|
+
* List all mail accounts.
|
|
201
|
+
*/
|
|
202
|
+
listAccounts(): Account[];
|
|
203
|
+
/**
|
|
204
|
+
* Run health check on Mail.app connectivity.
|
|
205
|
+
*/
|
|
206
|
+
healthCheck(): HealthCheckResult;
|
|
207
|
+
/**
|
|
208
|
+
* Get mail statistics.
|
|
209
|
+
*/
|
|
210
|
+
getMailStats(): MailStats;
|
|
211
|
+
/**
|
|
212
|
+
* Get counts of recently received messages.
|
|
213
|
+
*
|
|
214
|
+
* Only counts messages in INBOX for performance (scanning all mailboxes
|
|
215
|
+
* is too slow for large accounts).
|
|
216
|
+
*
|
|
217
|
+
* @returns Counts of messages received in last 24h, 7d, and 30d
|
|
218
|
+
*/
|
|
219
|
+
getRecentlyReceivedStats(): RecentlyReceivedStats;
|
|
220
|
+
/**
|
|
221
|
+
* Get sync status for Mail.app.
|
|
222
|
+
*
|
|
223
|
+
* Checks for sync activity indicators like:
|
|
224
|
+
* - Activity monitor status
|
|
225
|
+
* - Network activity status
|
|
226
|
+
* - Background refresh indicators
|
|
227
|
+
*
|
|
228
|
+
* @returns Sync status information
|
|
229
|
+
*/
|
|
230
|
+
getSyncStatus(): SyncStatus;
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=appleMailManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"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"}
|