apple-mail-mcp 1.5.1 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -823,4 +823,6 @@ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for gui
|
|
|
823
823
|
|
|
824
824
|
## Related Projects
|
|
825
825
|
|
|
826
|
-
- [apple-notes-mcp](https://github.com/sweetrb/apple-notes-mcp)
|
|
826
|
+
- [apple-notes-mcp](https://github.com/sweetrb/apple-notes-mcp) — MCP server for Apple Notes
|
|
827
|
+
- [apple-numbers-mcp](https://github.com/sweetrb/apple-numbers-mcp) — MCP server for Apple Numbers spreadsheets
|
|
828
|
+
- [apple-photos-mcp](https://github.com/sweetrb/apple-photos-mcp) — MCP server for Apple Photos
|
package/build/index.js
CHANGED
|
@@ -103,8 +103,11 @@ function withErrorHandling(handler, errorPrefix) {
|
|
|
103
103
|
// --- search-messages ---
|
|
104
104
|
server.tool("search-messages", {
|
|
105
105
|
query: z.string().optional().describe("Text to search for in subject, sender, or content"),
|
|
106
|
-
from: z
|
|
107
|
-
|
|
106
|
+
from: z
|
|
107
|
+
.string()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe("Filter by sender (substring match against the full sender string, i.e. display name + address — not an exact address match)"),
|
|
110
|
+
subject: z.string().optional().describe("Filter by subject line (substring match)"),
|
|
108
111
|
mailbox: z
|
|
109
112
|
.string()
|
|
110
113
|
.optional()
|
|
@@ -115,8 +118,8 @@ server.tool("search-messages", {
|
|
|
115
118
|
dateFrom: DATE_FILTER_SCHEMA.describe("Start date filter (e.g., 'January 1, 2026')"),
|
|
116
119
|
dateTo: DATE_FILTER_SCHEMA.describe("End date filter (e.g., 'March 1, 2026')"),
|
|
117
120
|
limit: z.number().optional().describe("Maximum number of results (default: 50)"),
|
|
118
|
-
}, withErrorHandling(({ query, mailbox, account, limit = 50, dateFrom, dateTo }) => {
|
|
119
|
-
const messages = mailManager.searchMessages(query, mailbox, account, limit, dateFrom, dateTo);
|
|
121
|
+
}, withErrorHandling(({ query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged, }) => {
|
|
122
|
+
const messages = mailManager.searchMessages(query, mailbox, account, limit, dateFrom, dateTo, from, subject, isRead, isFlagged);
|
|
120
123
|
if (messages.length === 0) {
|
|
121
124
|
return successResponse("No messages found matching criteria");
|
|
122
125
|
}
|
|
@@ -26,6 +26,27 @@ import type { Message, MessageContent, Mailbox, Account, Attachment, HealthCheck
|
|
|
26
26
|
* execution via osascript. Error handling is consistent: methods
|
|
27
27
|
* return null/false/empty-array on failure rather than throwing.
|
|
28
28
|
*/
|
|
29
|
+
export interface SearchConditionFilters {
|
|
30
|
+
query?: string;
|
|
31
|
+
from?: string;
|
|
32
|
+
subject?: string;
|
|
33
|
+
isRead?: boolean;
|
|
34
|
+
isFlagged?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the AppleScript `whose` clause for searchMessages from a filter set.
|
|
38
|
+
*
|
|
39
|
+
* - `query` is a subject-OR-sender substring match, parenthesized so it groups
|
|
40
|
+
* correctly when ANDed with other filters.
|
|
41
|
+
* - `from` and `subject` are substring matches (`sender`/`subject` contains).
|
|
42
|
+
* - `isRead` / `isFlagged` are boolean status checks.
|
|
43
|
+
* - Returns "" when no filters are set. Every interpolated value is escaped.
|
|
44
|
+
*
|
|
45
|
+
* Exported for unit testing: the bug this addresses (filters declared in the
|
|
46
|
+
* tool schema but silently dropped) lived in this logic, so it gets direct
|
|
47
|
+
* coverage independent of Mail.app.
|
|
48
|
+
*/
|
|
49
|
+
export declare function buildSearchCondition(filters: SearchConditionFilters): string;
|
|
29
50
|
export declare class AppleMailManager {
|
|
30
51
|
/**
|
|
31
52
|
* Default account used when no account is specified.
|
|
@@ -88,7 +109,7 @@ export declare class AppleMailManager {
|
|
|
88
109
|
* @param limit - Maximum number of results
|
|
89
110
|
* @returns Array of matching messages
|
|
90
111
|
*/
|
|
91
|
-
searchMessages(query?: string, mailbox?: string, account?: string, limit?: number, dateFrom?: string, dateTo?: string): Message[];
|
|
112
|
+
searchMessages(query?: string, mailbox?: string, account?: string, limit?: number, dateFrom?: string, dateTo?: string, from?: string, subject?: string, isRead?: boolean, isFlagged?: boolean): Message[];
|
|
92
113
|
/**
|
|
93
114
|
* Get a message by ID.
|
|
94
115
|
*
|
|
@@ -216,6 +237,25 @@ export declare class AppleMailManager {
|
|
|
216
237
|
/**
|
|
217
238
|
* Move a message to a different mailbox.
|
|
218
239
|
*/
|
|
240
|
+
/**
|
|
241
|
+
* Move a message to a destination mailbox, with full nested-mailbox support.
|
|
242
|
+
*
|
|
243
|
+
* Resolving the destination as `mailbox "X" of account "Y"` only finds
|
|
244
|
+
* top-level mailboxes, so nested destinations (e.g. a "Moore" subfolder)
|
|
245
|
+
* silently failed. Instead we walk the target account's full mailbox tree and
|
|
246
|
+
* match by name. Resolution is:
|
|
247
|
+
* - account-scoped (won't move to a same-named mailbox in another account)
|
|
248
|
+
* - ambiguity-aware: if the name matches more than one mailbox in the
|
|
249
|
+
* account we refuse to guess and return an error — silently moving mail to
|
|
250
|
+
* the wrong folder is worse than failing.
|
|
251
|
+
* The source message is located by walking every account's tree breadth-first
|
|
252
|
+
* (top-level mailboxes like Inbox are checked first), so messages in nested
|
|
253
|
+
* mailboxes are found too.
|
|
254
|
+
*
|
|
255
|
+
* Returns a result object so batch callers can surface the specific failure
|
|
256
|
+
* (destination not found / ambiguous / message not found).
|
|
257
|
+
*/
|
|
258
|
+
private moveMessageInternal;
|
|
219
259
|
moveMessage(id: string, mailbox: string, account?: string): boolean;
|
|
220
260
|
/**
|
|
221
261
|
* Delete multiple messages at once.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,OAAO,EACP,OAAO,EACP,UAAU,EACV,iBAAiB,EACjB,SAAS,EAET,oBAAoB,EACpB,UAAU,EACV,qBAAqB,EACrB,QAAQ,EACR,OAAO,EACP,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AA6HpB;;;;;;;;;;;;GAYG;AACH,qBAAa,gBAAgB;IAC3B;;OAEG;IACH,OAAO,CAAC,cAAc,CAAuB;IAE7C;;;;OAIG;IACH,OAAO,CAAC,KAAK,CAGX;IAEF,8CAA8C;IAC9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IAEvC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAUzB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAW7B;;;OAGG;IACH,OAAO,CAAC,eAAe;IAKvB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAwCtB;;;;;;;;;;;;;;;;;OAiBG;IACH,OAAO,CAAC,cAAc;IAyCtB;;;;;;;;OAQG;IACH,cAAc,CACZ,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,KAAK,SAAK,EACV,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"appleMailManager.d.ts","sourceRoot":"","sources":["../../src/services/appleMailManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -123,22 +123,40 @@ const MAILBOX_ALIASES = {
|
|
|
123
123
|
junk: ["Junk", "Junk Email", "Spam", "JUNK", "junk"],
|
|
124
124
|
archive: ["Archive", "ARCHIVE", "archive", "All Mail"],
|
|
125
125
|
};
|
|
126
|
-
// =============================================================================
|
|
127
|
-
// Apple Mail Manager Class
|
|
128
|
-
// =============================================================================
|
|
129
126
|
/**
|
|
130
|
-
*
|
|
127
|
+
* Build the AppleScript `whose` clause for searchMessages from a filter set.
|
|
131
128
|
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
* -
|
|
135
|
-
* -
|
|
136
|
-
* -
|
|
129
|
+
* - `query` is a subject-OR-sender substring match, parenthesized so it groups
|
|
130
|
+
* correctly when ANDed with other filters.
|
|
131
|
+
* - `from` and `subject` are substring matches (`sender`/`subject` contains).
|
|
132
|
+
* - `isRead` / `isFlagged` are boolean status checks.
|
|
133
|
+
* - Returns "" when no filters are set. Every interpolated value is escaped.
|
|
137
134
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
135
|
+
* Exported for unit testing: the bug this addresses (filters declared in the
|
|
136
|
+
* tool schema but silently dropped) lived in this logic, so it gets direct
|
|
137
|
+
* coverage independent of Mail.app.
|
|
141
138
|
*/
|
|
139
|
+
export function buildSearchCondition(filters) {
|
|
140
|
+
const { query, from, subject, isRead, isFlagged } = filters;
|
|
141
|
+
const conditions = [];
|
|
142
|
+
if (query) {
|
|
143
|
+
const safeQuery = escapeForAppleScript(query);
|
|
144
|
+
conditions.push(`(subject contains "${safeQuery}" or sender contains "${safeQuery}")`);
|
|
145
|
+
}
|
|
146
|
+
if (from) {
|
|
147
|
+
conditions.push(`sender contains "${escapeForAppleScript(from)}"`);
|
|
148
|
+
}
|
|
149
|
+
if (subject) {
|
|
150
|
+
conditions.push(`subject contains "${escapeForAppleScript(subject)}"`);
|
|
151
|
+
}
|
|
152
|
+
if (typeof isRead === "boolean") {
|
|
153
|
+
conditions.push(`read status is ${isRead ? "true" : "false"}`);
|
|
154
|
+
}
|
|
155
|
+
if (typeof isFlagged === "boolean") {
|
|
156
|
+
conditions.push(`flagged status is ${isFlagged ? "true" : "false"}`);
|
|
157
|
+
}
|
|
158
|
+
return conditions.length > 0 ? `whose ${conditions.join(" and ")}` : "";
|
|
159
|
+
}
|
|
142
160
|
export class AppleMailManager {
|
|
143
161
|
/**
|
|
144
162
|
* Default account used when no account is specified.
|
|
@@ -289,7 +307,7 @@ export class AppleMailManager {
|
|
|
289
307
|
* @param limit - Maximum number of results
|
|
290
308
|
* @returns Array of matching messages
|
|
291
309
|
*/
|
|
292
|
-
searchMessages(query, mailbox, account, limit = 50, dateFrom, dateTo) {
|
|
310
|
+
searchMessages(query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged) {
|
|
293
311
|
// If no account specified, search across all accounts
|
|
294
312
|
if (!account) {
|
|
295
313
|
const accounts = this.listAccounts();
|
|
@@ -298,18 +316,16 @@ export class AppleMailManager {
|
|
|
298
316
|
if (allMessages.length >= limit)
|
|
299
317
|
break;
|
|
300
318
|
const remaining = limit - allMessages.length;
|
|
301
|
-
const msgs = this.searchMessages(query, mailbox, acct.name, remaining, dateFrom, dateTo);
|
|
319
|
+
const msgs = this.searchMessages(query, mailbox, acct.name, remaining, dateFrom, dateTo, from, subject, isRead, isFlagged);
|
|
302
320
|
allMessages.push(...msgs);
|
|
303
321
|
}
|
|
304
322
|
return allMessages.slice(0, limit);
|
|
305
323
|
}
|
|
306
324
|
const targetAccount = this.resolveAccount(account);
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
searchCondition = `whose subject contains "${safeQuery}" or sender contains "${safeQuery}"`;
|
|
312
|
-
}
|
|
325
|
+
// `query` is a subject-OR-sender substring match; from/subject/isRead/isFlagged
|
|
326
|
+
// are additional AND filters. Date filtering stays post-fetch below — `whose`
|
|
327
|
+
// date comparisons are unreliable in Mail.app AppleScript. See buildSearchCondition.
|
|
328
|
+
const searchCondition = buildSearchCondition({ query, from, subject, isRead, isFlagged });
|
|
313
329
|
// Build date filter AppleScript.
|
|
314
330
|
// Note: dateFrom/dateTo are already validated by DATE_FILTER_SCHEMA (alphanumeric + safe
|
|
315
331
|
// punctuation only), so escapeForAppleScript() below is belt-and-suspenders — it won't
|
|
@@ -720,6 +736,30 @@ export class AppleMailManager {
|
|
|
720
736
|
* @param account - Account to send from
|
|
721
737
|
* @returns true if sent successfully
|
|
722
738
|
*/
|
|
739
|
+
// ───────────────────────────────────────────────────────────────────
|
|
740
|
+
// KNOWN BUG: outgoing emails sent via AppleScript on macOS 15+ get wrapped
|
|
741
|
+
// in <blockquote type="cite"> under the Apple-Mail-URLShareWrapperClass
|
|
742
|
+
// template, so they render to recipients as quoted/forwarded content.
|
|
743
|
+
// Plain-text alternative gets `>` prefixes on every line.
|
|
744
|
+
//
|
|
745
|
+
// Reproduces with EVERY AppleScript message-creation pattern I tried:
|
|
746
|
+
// • make new outgoing message with properties {content: ..., ...}
|
|
747
|
+
// • make new outgoing message (no content) + `set content of newMessage`
|
|
748
|
+
// • setting `default message format` to plain format first
|
|
749
|
+
//
|
|
750
|
+
// Apple radar FB11734014 (open since Ventura, no movement).
|
|
751
|
+
// Discussion: https://forums.macrumors.com/threads/applescript-creating-a-
|
|
752
|
+
// new-message-in-mail-app-is-causing-weird-formatting-issues.2385052/
|
|
753
|
+
//
|
|
754
|
+
// Workaround for callers who need clean emails today: use SMTP directly
|
|
755
|
+
// (Python smtplib). See e.g. sweetrb/nhl-bracket-tracker's send_email.py.
|
|
756
|
+
//
|
|
757
|
+
// Proper fix is probably to abandon `make new outgoing message` and either:
|
|
758
|
+
// 1. Build the .emlx file ourselves and drop it into a Drafts mailbox.
|
|
759
|
+
// 2. Switch to smtplib-style direct send with Keychain-stored creds.
|
|
760
|
+
// 3. Use Mail.app's NSSharingService rather than AppleScript.
|
|
761
|
+
// Tracking issue: https://github.com/sweetrb/apple-mail-mcp/issues/12
|
|
762
|
+
// ───────────────────────────────────────────────────────────────────
|
|
723
763
|
sendEmail(to, subject, body, cc, bcc, account, attachments) {
|
|
724
764
|
const safeSubject = escapeForAppleScript(subject);
|
|
725
765
|
const safeBody = escapeForAppleScript(body);
|
|
@@ -1056,21 +1096,53 @@ export class AppleMailManager {
|
|
|
1056
1096
|
/**
|
|
1057
1097
|
* Move a message to a different mailbox.
|
|
1058
1098
|
*/
|
|
1059
|
-
|
|
1099
|
+
/**
|
|
1100
|
+
* Move a message to a destination mailbox, with full nested-mailbox support.
|
|
1101
|
+
*
|
|
1102
|
+
* Resolving the destination as `mailbox "X" of account "Y"` only finds
|
|
1103
|
+
* top-level mailboxes, so nested destinations (e.g. a "Moore" subfolder)
|
|
1104
|
+
* silently failed. Instead we walk the target account's full mailbox tree and
|
|
1105
|
+
* match by name. Resolution is:
|
|
1106
|
+
* - account-scoped (won't move to a same-named mailbox in another account)
|
|
1107
|
+
* - ambiguity-aware: if the name matches more than one mailbox in the
|
|
1108
|
+
* account we refuse to guess and return an error — silently moving mail to
|
|
1109
|
+
* the wrong folder is worse than failing.
|
|
1110
|
+
* The source message is located by walking every account's tree breadth-first
|
|
1111
|
+
* (top-level mailboxes like Inbox are checked first), so messages in nested
|
|
1112
|
+
* mailboxes are found too.
|
|
1113
|
+
*
|
|
1114
|
+
* Returns a result object so batch callers can surface the specific failure
|
|
1115
|
+
* (destination not found / ambiguous / message not found).
|
|
1116
|
+
*/
|
|
1117
|
+
moveMessageInternal(id, mailbox, account) {
|
|
1060
1118
|
const targetAccount = this.resolveAccount(account);
|
|
1061
1119
|
const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
|
|
1062
1120
|
const safeMailbox = escapeForAppleScript(targetMailbox);
|
|
1063
1121
|
const safeAccount = escapeForAppleScript(targetAccount);
|
|
1064
1122
|
const script = buildAppLevelScript(`
|
|
1065
1123
|
try
|
|
1124
|
+
-- \`mailboxes of account\` is already flat: it includes nested mailboxes
|
|
1125
|
+
-- (named by path, e.g. "Processed/Vendors"). Descending via \`mailboxes of mb\`
|
|
1126
|
+
-- is unreliable (it double-prepends the parent path), so we DON'T recurse —
|
|
1127
|
+
-- we match against this flat list by exact name and use the reference directly
|
|
1128
|
+
-- (addressing \`mailbox "X" of account "Y"\` only finds some top-level mailboxes).
|
|
1129
|
+
set destName to "${safeMailbox}"
|
|
1130
|
+
set destMatches to {}
|
|
1131
|
+
repeat with mb in (mailboxes of account "${safeAccount}")
|
|
1132
|
+
if (name of mb) is destName then set end of destMatches to mb
|
|
1133
|
+
end repeat
|
|
1134
|
+
if (count of destMatches) is 0 then return "error:Destination mailbox \\"" & destName & "\\" not found in account \\"${safeAccount}\\""
|
|
1135
|
+
if (count of destMatches) > 1 then return "error:Destination mailbox \\"" & destName & "\\" is ambiguous (" & (count of destMatches) & " matches) in account \\"${safeAccount}\\"; disambiguate or move by full path"
|
|
1136
|
+
set destMailbox to item 1 of destMatches
|
|
1137
|
+
|
|
1138
|
+
-- Find the message by id. The flat mailbox list already covers nested
|
|
1139
|
+
-- mailboxes, so this reaches messages in subfolders without recursing.
|
|
1066
1140
|
repeat with acct in accounts
|
|
1067
|
-
repeat with mb in mailboxes of acct
|
|
1141
|
+
repeat with mb in (mailboxes of acct)
|
|
1068
1142
|
try
|
|
1069
1143
|
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
1070
1144
|
if (count of matchingMsgs) > 0 then
|
|
1071
|
-
|
|
1072
|
-
set destMailbox to mailbox "${safeMailbox}" of account "${safeAccount}"
|
|
1073
|
-
move msg to destMailbox
|
|
1145
|
+
move (item 1 of matchingMsgs) to destMailbox
|
|
1074
1146
|
return "ok"
|
|
1075
1147
|
end if
|
|
1076
1148
|
end try
|
|
@@ -1081,12 +1153,21 @@ export class AppleMailManager {
|
|
|
1081
1153
|
return "error:" & errMsg
|
|
1082
1154
|
end try
|
|
1083
1155
|
`);
|
|
1084
|
-
const result = executeAppleScript(script, { timeoutMs:
|
|
1085
|
-
if (!result.success
|
|
1086
|
-
|
|
1087
|
-
return false;
|
|
1156
|
+
const result = executeAppleScript(script, { timeoutMs: 90000 });
|
|
1157
|
+
if (!result.success) {
|
|
1158
|
+
return { success: false, error: result.error || "AppleScript execution failed" };
|
|
1088
1159
|
}
|
|
1089
|
-
|
|
1160
|
+
if (result.output.startsWith("error:")) {
|
|
1161
|
+
return { success: false, error: result.output.slice("error:".length) };
|
|
1162
|
+
}
|
|
1163
|
+
return { success: true };
|
|
1164
|
+
}
|
|
1165
|
+
moveMessage(id, mailbox, account) {
|
|
1166
|
+
const { success, error } = this.moveMessageInternal(id, mailbox, account);
|
|
1167
|
+
if (!success) {
|
|
1168
|
+
console.error(`Failed to move message: ${error}`);
|
|
1169
|
+
}
|
|
1170
|
+
return success;
|
|
1090
1171
|
}
|
|
1091
1172
|
// ===========================================================================
|
|
1092
1173
|
// Batch Operations
|
|
@@ -1120,11 +1201,11 @@ export class AppleMailManager {
|
|
|
1120
1201
|
batchMoveMessages(ids, mailbox, account) {
|
|
1121
1202
|
const results = [];
|
|
1122
1203
|
for (const id of ids) {
|
|
1123
|
-
const success = this.
|
|
1204
|
+
const { success, error } = this.moveMessageInternal(id, mailbox, account);
|
|
1124
1205
|
results.push({
|
|
1125
1206
|
id,
|
|
1126
1207
|
success,
|
|
1127
|
-
error: success ? undefined : "Failed to move message",
|
|
1208
|
+
error: success ? undefined : error || "Failed to move message",
|
|
1128
1209
|
});
|
|
1129
1210
|
}
|
|
1130
1211
|
return results;
|