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
|
@@ -0,0 +1,1208 @@
|
|
|
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 { executeAppleScript } from "../utils/applescript.js";
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Text Processing Utilities
|
|
18
|
+
// =============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Escapes text for safe embedding in AppleScript string literals.
|
|
21
|
+
*
|
|
22
|
+
* AppleScript strings use double quotes, so we need to escape:
|
|
23
|
+
* 1. Backslashes (\) - escaped as \\
|
|
24
|
+
* 2. Double quotes (") - escaped as \"
|
|
25
|
+
*
|
|
26
|
+
* @param text - Raw text to escape
|
|
27
|
+
* @returns Text safe for AppleScript string embedding
|
|
28
|
+
*/
|
|
29
|
+
function escapeForAppleScript(text) {
|
|
30
|
+
if (!text)
|
|
31
|
+
return "";
|
|
32
|
+
return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parses AppleScript date representation to JavaScript Date.
|
|
36
|
+
*
|
|
37
|
+
* AppleScript returns dates in a verbose format like:
|
|
38
|
+
* "date Saturday, December 27, 2025 at 3:44:02 PM"
|
|
39
|
+
*
|
|
40
|
+
* @param appleScriptDate - Date string from AppleScript
|
|
41
|
+
* @returns Parsed Date, or current date if parsing fails
|
|
42
|
+
*/
|
|
43
|
+
function parseAppleScriptDate(appleScriptDate) {
|
|
44
|
+
const withoutPrefix = appleScriptDate.replace(/^date\s+/, "");
|
|
45
|
+
const normalized = withoutPrefix.replace(" at ", " ");
|
|
46
|
+
const parsed = new Date(normalized);
|
|
47
|
+
return isNaN(parsed.getTime()) ? new Date() : parsed;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Builds an AppleScript command scoped to a specific account.
|
|
51
|
+
*/
|
|
52
|
+
function buildAccountScopedScript(account, command) {
|
|
53
|
+
return `
|
|
54
|
+
tell application "Mail"
|
|
55
|
+
tell account "${escapeForAppleScript(account)}"
|
|
56
|
+
${command}
|
|
57
|
+
end tell
|
|
58
|
+
end tell
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Builds an AppleScript command at the application level.
|
|
63
|
+
*/
|
|
64
|
+
function buildAppLevelScript(command) {
|
|
65
|
+
return `
|
|
66
|
+
tell application "Mail"
|
|
67
|
+
${command}
|
|
68
|
+
end tell
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Common mailbox name variations across different account types.
|
|
73
|
+
* Maps normalized (lowercase) names to possible actual names.
|
|
74
|
+
*/
|
|
75
|
+
const MAILBOX_ALIASES = {
|
|
76
|
+
inbox: ["INBOX", "Inbox", "inbox"],
|
|
77
|
+
sent: ["Sent", "Sent Items", "Sent Messages", "SENT", "sent"],
|
|
78
|
+
drafts: ["Drafts", "DRAFTS", "drafts", "Draft"],
|
|
79
|
+
trash: ["Trash", "Deleted Items", "Deleted Messages", "TRASH", "trash"],
|
|
80
|
+
junk: ["Junk", "Junk Email", "Spam", "JUNK", "junk"],
|
|
81
|
+
archive: ["Archive", "ARCHIVE", "archive", "All Mail"],
|
|
82
|
+
};
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Apple Mail Manager Class
|
|
85
|
+
// =============================================================================
|
|
86
|
+
/**
|
|
87
|
+
* Manager class for Apple Mail operations.
|
|
88
|
+
*
|
|
89
|
+
* Provides methods for:
|
|
90
|
+
* - Reading and searching messages
|
|
91
|
+
* - Sending emails
|
|
92
|
+
* - Managing mailboxes
|
|
93
|
+
* - Listing accounts
|
|
94
|
+
*
|
|
95
|
+
* All operations are synchronous since they rely on AppleScript
|
|
96
|
+
* execution via osascript. Error handling is consistent: methods
|
|
97
|
+
* return null/false/empty-array on failure rather than throwing.
|
|
98
|
+
*/
|
|
99
|
+
export class AppleMailManager {
|
|
100
|
+
/**
|
|
101
|
+
* Default account used when no account is specified.
|
|
102
|
+
*/
|
|
103
|
+
defaultAccount = null;
|
|
104
|
+
/**
|
|
105
|
+
* Resolves the account to use for an operation.
|
|
106
|
+
* Falls back to first available account if not specified.
|
|
107
|
+
*/
|
|
108
|
+
resolveAccount(account) {
|
|
109
|
+
if (account)
|
|
110
|
+
return account;
|
|
111
|
+
if (this.defaultAccount)
|
|
112
|
+
return this.defaultAccount;
|
|
113
|
+
// Get first account as default
|
|
114
|
+
const accounts = this.listAccounts();
|
|
115
|
+
if (accounts.length > 0) {
|
|
116
|
+
this.defaultAccount = accounts[0].name;
|
|
117
|
+
return this.defaultAccount;
|
|
118
|
+
}
|
|
119
|
+
return "iCloud"; // Last resort fallback
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolves a mailbox name to its actual name in the account.
|
|
123
|
+
*
|
|
124
|
+
* Different account types (IMAP, Exchange, iCloud) use different
|
|
125
|
+
* mailbox naming conventions:
|
|
126
|
+
* - IMAP/Gmail: "INBOX", "Sent", "Drafts"
|
|
127
|
+
* - Exchange: "Inbox", "Sent Items", "Deleted Items"
|
|
128
|
+
* - iCloud: "INBOX", "Sent", "Trash"
|
|
129
|
+
*
|
|
130
|
+
* This method tries to find a matching mailbox by:
|
|
131
|
+
* 1. Exact match
|
|
132
|
+
* 2. Case-insensitive match
|
|
133
|
+
* 3. Known aliases (e.g., "Sent" -> "Sent Items")
|
|
134
|
+
*
|
|
135
|
+
* @param mailbox - Requested mailbox name
|
|
136
|
+
* @param account - Account to search in
|
|
137
|
+
* @returns Actual mailbox name, or original if not found
|
|
138
|
+
*/
|
|
139
|
+
resolveMailbox(mailbox, account) {
|
|
140
|
+
// Get actual mailbox names from the account
|
|
141
|
+
const script = buildAccountScopedScript(account, `
|
|
142
|
+
set mbNames to {}
|
|
143
|
+
repeat with mb in mailboxes
|
|
144
|
+
set end of mbNames to name of mb
|
|
145
|
+
end repeat
|
|
146
|
+
return mbNames
|
|
147
|
+
`);
|
|
148
|
+
const result = executeAppleScript(script);
|
|
149
|
+
if (!result.success || !result.output) {
|
|
150
|
+
return mailbox; // Fall back to original
|
|
151
|
+
}
|
|
152
|
+
// Parse the mailbox names (AppleScript returns comma-separated list)
|
|
153
|
+
const actualMailboxes = result.output.split(", ").map((s) => s.trim());
|
|
154
|
+
// 1. Try exact match
|
|
155
|
+
if (actualMailboxes.includes(mailbox)) {
|
|
156
|
+
return mailbox;
|
|
157
|
+
}
|
|
158
|
+
// 2. Try case-insensitive match
|
|
159
|
+
const lowerMailbox = mailbox.toLowerCase();
|
|
160
|
+
const caseMatch = actualMailboxes.find((mb) => mb.toLowerCase() === lowerMailbox);
|
|
161
|
+
if (caseMatch) {
|
|
162
|
+
return caseMatch;
|
|
163
|
+
}
|
|
164
|
+
// 3. Try known aliases
|
|
165
|
+
const aliases = MAILBOX_ALIASES[lowerMailbox];
|
|
166
|
+
if (aliases) {
|
|
167
|
+
for (const alias of aliases) {
|
|
168
|
+
if (actualMailboxes.includes(alias)) {
|
|
169
|
+
return alias;
|
|
170
|
+
}
|
|
171
|
+
// Also try case-insensitive alias match
|
|
172
|
+
const aliasMatch = actualMailboxes.find((mb) => mb.toLowerCase() === alias.toLowerCase());
|
|
173
|
+
if (aliasMatch) {
|
|
174
|
+
return aliasMatch;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// No match found, return original and let AppleScript handle the error
|
|
179
|
+
return mailbox;
|
|
180
|
+
}
|
|
181
|
+
// ===========================================================================
|
|
182
|
+
// Message Operations
|
|
183
|
+
// ===========================================================================
|
|
184
|
+
/**
|
|
185
|
+
* Search for messages matching criteria.
|
|
186
|
+
*
|
|
187
|
+
* @param query - Text to search for in subject or sender
|
|
188
|
+
* @param mailbox - Mailbox to search in (e.g., "INBOX")
|
|
189
|
+
* @param account - Account to search in
|
|
190
|
+
* @param limit - Maximum number of results
|
|
191
|
+
* @returns Array of matching messages
|
|
192
|
+
*/
|
|
193
|
+
searchMessages(query, mailbox, account, limit = 50) {
|
|
194
|
+
const targetAccount = this.resolveAccount(account);
|
|
195
|
+
const requestedMailbox = mailbox || "INBOX";
|
|
196
|
+
const targetMailbox = this.resolveMailbox(requestedMailbox, targetAccount);
|
|
197
|
+
// Build the search condition
|
|
198
|
+
let searchCondition = "";
|
|
199
|
+
if (query) {
|
|
200
|
+
const safeQuery = escapeForAppleScript(query);
|
|
201
|
+
searchCondition = `whose subject contains "${safeQuery}" or sender contains "${safeQuery}"`;
|
|
202
|
+
}
|
|
203
|
+
const searchCommand = `
|
|
204
|
+
set outputText to ""
|
|
205
|
+
set theMailbox to mailbox "${escapeForAppleScript(targetMailbox)}"
|
|
206
|
+
set allMessages to messages of theMailbox ${searchCondition}
|
|
207
|
+
set msgCount to 0
|
|
208
|
+
repeat with msg in allMessages
|
|
209
|
+
if msgCount >= ${limit} then exit repeat
|
|
210
|
+
try
|
|
211
|
+
set msgId to id of msg as string
|
|
212
|
+
set msgSubject to subject of msg
|
|
213
|
+
set msgSender to sender of msg
|
|
214
|
+
set msgDate to date received of msg as string
|
|
215
|
+
set msgRead to read status of msg as string
|
|
216
|
+
set msgFlagged to flagged status of msg as string
|
|
217
|
+
if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
|
|
218
|
+
set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged
|
|
219
|
+
set msgCount to msgCount + 1
|
|
220
|
+
end try
|
|
221
|
+
end repeat
|
|
222
|
+
return outputText
|
|
223
|
+
`;
|
|
224
|
+
const script = buildAccountScopedScript(targetAccount, searchCommand);
|
|
225
|
+
const result = executeAppleScript(script);
|
|
226
|
+
if (!result.success) {
|
|
227
|
+
console.error(`Failed to search messages: ${result.error}`);
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
if (!result.output.trim())
|
|
231
|
+
return [];
|
|
232
|
+
return this.parseMessageList(result.output, targetMailbox, targetAccount);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Get a message by ID.
|
|
236
|
+
*
|
|
237
|
+
* Note: Mail.app message IDs are unique per mailbox. This method searches
|
|
238
|
+
* all mailboxes in all accounts to find the message.
|
|
239
|
+
*/
|
|
240
|
+
getMessageById(id) {
|
|
241
|
+
const script = buildAppLevelScript(`
|
|
242
|
+
try
|
|
243
|
+
repeat with acct in accounts
|
|
244
|
+
repeat with mb in mailboxes of acct
|
|
245
|
+
try
|
|
246
|
+
set matchingMsgs to (messages of mb whose id is ${id})
|
|
247
|
+
if (count of matchingMsgs) > 0 then
|
|
248
|
+
set msg to item 1 of matchingMsgs
|
|
249
|
+
set msgSubject to subject of msg
|
|
250
|
+
set msgSender to sender of msg
|
|
251
|
+
set msgDate to date received of msg as string
|
|
252
|
+
set msgRead to read status of msg as string
|
|
253
|
+
set msgFlagged to flagged status of msg as string
|
|
254
|
+
set msgJunk to junk mail status of msg as string
|
|
255
|
+
set msgDeleted to deleted status of msg as string
|
|
256
|
+
set msgMailbox to name of mb
|
|
257
|
+
set msgAccount to name of acct
|
|
258
|
+
return msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged & "|||" & msgJunk & "|||" & msgDeleted & "|||" & msgMailbox & "|||" & msgAccount
|
|
259
|
+
end if
|
|
260
|
+
end try
|
|
261
|
+
end repeat
|
|
262
|
+
end repeat
|
|
263
|
+
return ""
|
|
264
|
+
on error errMsg
|
|
265
|
+
return ""
|
|
266
|
+
end try
|
|
267
|
+
`);
|
|
268
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 }); // Longer timeout for search
|
|
269
|
+
if (!result.success || !result.output.trim()) {
|
|
270
|
+
console.error(`Failed to get message ${id}: ${result.error}`);
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const parts = result.output.split("|||");
|
|
274
|
+
if (parts.length < 9)
|
|
275
|
+
return null;
|
|
276
|
+
return {
|
|
277
|
+
id: id.toString(),
|
|
278
|
+
subject: parts[0],
|
|
279
|
+
sender: parts[1],
|
|
280
|
+
recipients: [],
|
|
281
|
+
dateReceived: parseAppleScriptDate(parts[2]),
|
|
282
|
+
isRead: parts[3] === "true",
|
|
283
|
+
isFlagged: parts[4] === "true",
|
|
284
|
+
isJunk: parts[5] === "true",
|
|
285
|
+
isDeleted: parts[6] === "true",
|
|
286
|
+
mailbox: parts[7],
|
|
287
|
+
account: parts[8],
|
|
288
|
+
hasAttachments: false,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Get the content of a message.
|
|
293
|
+
*/
|
|
294
|
+
getMessageContent(id) {
|
|
295
|
+
const script = buildAppLevelScript(`
|
|
296
|
+
try
|
|
297
|
+
repeat with acct in accounts
|
|
298
|
+
repeat with mb in mailboxes of acct
|
|
299
|
+
try
|
|
300
|
+
set matchingMsgs to (messages of mb whose id is ${id})
|
|
301
|
+
if (count of matchingMsgs) > 0 then
|
|
302
|
+
set msg to item 1 of matchingMsgs
|
|
303
|
+
set msgSubject to subject of msg
|
|
304
|
+
set msgContent to content of msg
|
|
305
|
+
return msgSubject & "|||CONTENT|||" & msgContent
|
|
306
|
+
end if
|
|
307
|
+
end try
|
|
308
|
+
end repeat
|
|
309
|
+
end repeat
|
|
310
|
+
return ""
|
|
311
|
+
on error errMsg
|
|
312
|
+
return ""
|
|
313
|
+
end try
|
|
314
|
+
`);
|
|
315
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
316
|
+
if (!result.success || !result.output.trim()) {
|
|
317
|
+
console.error(`Failed to get message content: ${result.error}`);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const parts = result.output.split("|||CONTENT|||");
|
|
321
|
+
if (parts.length < 2)
|
|
322
|
+
return null;
|
|
323
|
+
return {
|
|
324
|
+
id: id.toString(),
|
|
325
|
+
subject: parts[0],
|
|
326
|
+
plainText: parts[1],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* List messages in a mailbox.
|
|
331
|
+
*
|
|
332
|
+
* @param mailbox - Mailbox to list from (default: INBOX)
|
|
333
|
+
* @param account - Account to list from
|
|
334
|
+
* @param limit - Maximum number of messages
|
|
335
|
+
* @returns Array of messages
|
|
336
|
+
*/
|
|
337
|
+
listMessages(mailbox, account, limit = 50) {
|
|
338
|
+
const targetAccount = this.resolveAccount(account);
|
|
339
|
+
const requestedMailbox = mailbox || "INBOX";
|
|
340
|
+
const targetMailbox = this.resolveMailbox(requestedMailbox, targetAccount);
|
|
341
|
+
const listCommand = `
|
|
342
|
+
set outputText to ""
|
|
343
|
+
set theMailbox to mailbox "${escapeForAppleScript(targetMailbox)}"
|
|
344
|
+
set msgCount to 0
|
|
345
|
+
repeat with msg in messages of theMailbox
|
|
346
|
+
if msgCount >= ${limit} then exit repeat
|
|
347
|
+
try
|
|
348
|
+
set msgId to id of msg as string
|
|
349
|
+
set msgSubject to subject of msg
|
|
350
|
+
set msgSender to sender of msg
|
|
351
|
+
set msgDate to date received of msg as string
|
|
352
|
+
set msgRead to read status of msg as string
|
|
353
|
+
set msgFlagged to flagged status of msg as string
|
|
354
|
+
if msgCount > 0 then set outputText to outputText & "|||ITEM|||"
|
|
355
|
+
set outputText to outputText & msgId & "|||" & msgSubject & "|||" & msgSender & "|||" & msgDate & "|||" & msgRead & "|||" & msgFlagged
|
|
356
|
+
set msgCount to msgCount + 1
|
|
357
|
+
end try
|
|
358
|
+
end repeat
|
|
359
|
+
return outputText
|
|
360
|
+
`;
|
|
361
|
+
const script = buildAccountScopedScript(targetAccount, listCommand);
|
|
362
|
+
const result = executeAppleScript(script);
|
|
363
|
+
if (!result.success) {
|
|
364
|
+
console.error(`Failed to list messages: ${result.error}`);
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
if (!result.output.trim())
|
|
368
|
+
return [];
|
|
369
|
+
return this.parseMessageList(result.output, targetMailbox, targetAccount);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Parse message list output from AppleScript.
|
|
373
|
+
*/
|
|
374
|
+
parseMessageList(output, mailbox, account) {
|
|
375
|
+
const items = output.split("|||ITEM|||");
|
|
376
|
+
const messages = [];
|
|
377
|
+
for (const item of items) {
|
|
378
|
+
const parts = item.split("|||");
|
|
379
|
+
if (parts.length < 6)
|
|
380
|
+
continue;
|
|
381
|
+
messages.push({
|
|
382
|
+
id: parts[0].trim(),
|
|
383
|
+
subject: parts[1],
|
|
384
|
+
sender: parts[2],
|
|
385
|
+
recipients: [],
|
|
386
|
+
dateReceived: parseAppleScriptDate(parts[3]),
|
|
387
|
+
isRead: parts[4] === "true",
|
|
388
|
+
isFlagged: parts[5] === "true",
|
|
389
|
+
isJunk: false,
|
|
390
|
+
isDeleted: false,
|
|
391
|
+
mailbox,
|
|
392
|
+
account,
|
|
393
|
+
hasAttachments: false,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return messages;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Send an email.
|
|
400
|
+
*
|
|
401
|
+
* @param to - Recipient email addresses
|
|
402
|
+
* @param subject - Email subject
|
|
403
|
+
* @param body - Email body (plain text)
|
|
404
|
+
* @param cc - CC recipients
|
|
405
|
+
* @param bcc - BCC recipients
|
|
406
|
+
* @param account - Account to send from
|
|
407
|
+
* @returns true if sent successfully
|
|
408
|
+
*/
|
|
409
|
+
sendEmail(to, subject, body, cc, bcc, account) {
|
|
410
|
+
const safeSubject = escapeForAppleScript(subject);
|
|
411
|
+
const safeBody = escapeForAppleScript(body);
|
|
412
|
+
// Build recipient additions
|
|
413
|
+
let recipientCommands = "";
|
|
414
|
+
for (const addr of to) {
|
|
415
|
+
recipientCommands += `make new to recipient at end of to recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
|
|
416
|
+
}
|
|
417
|
+
if (cc) {
|
|
418
|
+
for (const addr of cc) {
|
|
419
|
+
recipientCommands += `make new cc recipient at end of cc recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (bcc) {
|
|
423
|
+
for (const addr of bcc) {
|
|
424
|
+
recipientCommands += `make new bcc recipient at end of bcc recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
let sendCommand;
|
|
428
|
+
if (account) {
|
|
429
|
+
const safeAccount = escapeForAppleScript(account);
|
|
430
|
+
sendCommand = `
|
|
431
|
+
set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:true}
|
|
432
|
+
tell newMessage
|
|
433
|
+
${recipientCommands}
|
|
434
|
+
set sender to "${safeAccount}"
|
|
435
|
+
end tell
|
|
436
|
+
send newMessage
|
|
437
|
+
return "sent"
|
|
438
|
+
`;
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
sendCommand = `
|
|
442
|
+
set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:true}
|
|
443
|
+
tell newMessage
|
|
444
|
+
${recipientCommands}
|
|
445
|
+
end tell
|
|
446
|
+
send newMessage
|
|
447
|
+
return "sent"
|
|
448
|
+
`;
|
|
449
|
+
}
|
|
450
|
+
const script = buildAppLevelScript(sendCommand);
|
|
451
|
+
const result = executeAppleScript(script);
|
|
452
|
+
if (!result.success) {
|
|
453
|
+
console.error(`Failed to send email: ${result.error}`);
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
return result.output.includes("sent");
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Create a draft email (saved to Drafts folder, not sent).
|
|
460
|
+
*
|
|
461
|
+
* @param to - Recipient email addresses
|
|
462
|
+
* @param subject - Email subject
|
|
463
|
+
* @param body - Email body (plain text)
|
|
464
|
+
* @param cc - CC recipients
|
|
465
|
+
* @param bcc - BCC recipients
|
|
466
|
+
* @param account - Account to create draft in
|
|
467
|
+
* @returns true if draft created successfully
|
|
468
|
+
*/
|
|
469
|
+
createDraft(to, subject, body, cc, bcc, account) {
|
|
470
|
+
const safeSubject = escapeForAppleScript(subject);
|
|
471
|
+
const safeBody = escapeForAppleScript(body);
|
|
472
|
+
// Build recipient additions
|
|
473
|
+
let recipientCommands = "";
|
|
474
|
+
for (const addr of to) {
|
|
475
|
+
recipientCommands += `make new to recipient at end of to recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
|
|
476
|
+
}
|
|
477
|
+
if (cc) {
|
|
478
|
+
for (const addr of cc) {
|
|
479
|
+
recipientCommands += `make new cc recipient at end of cc recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (bcc) {
|
|
483
|
+
for (const addr of bcc) {
|
|
484
|
+
recipientCommands += `make new bcc recipient at end of bcc recipients with properties {address:"${escapeForAppleScript(addr)}"}\n`;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
let draftCommand;
|
|
488
|
+
if (account) {
|
|
489
|
+
const safeAccount = escapeForAppleScript(account);
|
|
490
|
+
draftCommand = `
|
|
491
|
+
set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:false}
|
|
492
|
+
tell newMessage
|
|
493
|
+
${recipientCommands}
|
|
494
|
+
set sender to "${safeAccount}"
|
|
495
|
+
end tell
|
|
496
|
+
return "draft created"
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
draftCommand = `
|
|
501
|
+
set newMessage to make new outgoing message with properties {subject:"${safeSubject}", content:"${safeBody}", visible:false}
|
|
502
|
+
tell newMessage
|
|
503
|
+
${recipientCommands}
|
|
504
|
+
end tell
|
|
505
|
+
return "draft created"
|
|
506
|
+
`;
|
|
507
|
+
}
|
|
508
|
+
const script = buildAppLevelScript(draftCommand);
|
|
509
|
+
const result = executeAppleScript(script);
|
|
510
|
+
if (!result.success) {
|
|
511
|
+
console.error(`Failed to create draft: ${result.error}`);
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
return result.output.includes("draft created");
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Reply to a message.
|
|
518
|
+
*
|
|
519
|
+
* @param id - Message ID to reply to
|
|
520
|
+
* @param body - Reply body
|
|
521
|
+
* @param replyAll - If true, reply to all recipients
|
|
522
|
+
* @param send - If true, send immediately; if false, save as draft
|
|
523
|
+
* @returns true if reply created/sent successfully
|
|
524
|
+
*/
|
|
525
|
+
replyToMessage(id, body, replyAll = false, send = true) {
|
|
526
|
+
const safeBody = escapeForAppleScript(body);
|
|
527
|
+
const replyAllClause = replyAll ? " with reply to all" : "";
|
|
528
|
+
const sendAction = send ? "send theReply" : "";
|
|
529
|
+
const script = buildAppLevelScript(`
|
|
530
|
+
try
|
|
531
|
+
repeat with acct in accounts
|
|
532
|
+
repeat with mb in mailboxes of acct
|
|
533
|
+
try
|
|
534
|
+
set matchingMsgs to (messages of mb whose id is ${id})
|
|
535
|
+
if (count of matchingMsgs) > 0 then
|
|
536
|
+
set msg to item 1 of matchingMsgs
|
|
537
|
+
set theReply to reply msg with opening window${replyAllClause}
|
|
538
|
+
set content of theReply to "${safeBody}" & return & return & content of theReply
|
|
539
|
+
${sendAction}
|
|
540
|
+
return "ok"
|
|
541
|
+
end if
|
|
542
|
+
end try
|
|
543
|
+
end repeat
|
|
544
|
+
end repeat
|
|
545
|
+
return "error:Message not found"
|
|
546
|
+
on error errMsg
|
|
547
|
+
return "error:" & errMsg
|
|
548
|
+
end try
|
|
549
|
+
`);
|
|
550
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
551
|
+
if (!result.success || result.output.startsWith("error:")) {
|
|
552
|
+
console.error(`Failed to reply to message: ${result.error || result.output}`);
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
return true;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Forward a message.
|
|
559
|
+
*
|
|
560
|
+
* @param id - Message ID to forward
|
|
561
|
+
* @param to - Recipients to forward to
|
|
562
|
+
* @param body - Optional body to prepend
|
|
563
|
+
* @param send - If true, send immediately; if false, save as draft
|
|
564
|
+
* @returns true if forward created/sent successfully
|
|
565
|
+
*/
|
|
566
|
+
forwardMessage(id, to, body, send = true) {
|
|
567
|
+
const safeBody = body ? escapeForAppleScript(body) : "";
|
|
568
|
+
const sendAction = send ? "send theForward" : "";
|
|
569
|
+
// Build recipient additions
|
|
570
|
+
let recipientCommands = "";
|
|
571
|
+
for (const addr of to) {
|
|
572
|
+
recipientCommands += `make new to recipient at end of to recipients of theForward with properties {address:"${escapeForAppleScript(addr)}"}\n`;
|
|
573
|
+
}
|
|
574
|
+
const script = buildAppLevelScript(`
|
|
575
|
+
try
|
|
576
|
+
repeat with acct in accounts
|
|
577
|
+
repeat with mb in mailboxes of acct
|
|
578
|
+
try
|
|
579
|
+
set matchingMsgs to (messages of mb whose id is ${id})
|
|
580
|
+
if (count of matchingMsgs) > 0 then
|
|
581
|
+
set msg to item 1 of matchingMsgs
|
|
582
|
+
set theForward to forward msg with opening window
|
|
583
|
+
${recipientCommands}
|
|
584
|
+
${safeBody ? `set content of theForward to "${safeBody}" & return & return & content of theForward` : ""}
|
|
585
|
+
${sendAction}
|
|
586
|
+
return "ok"
|
|
587
|
+
end if
|
|
588
|
+
end try
|
|
589
|
+
end repeat
|
|
590
|
+
end repeat
|
|
591
|
+
return "error:Message not found"
|
|
592
|
+
on error errMsg
|
|
593
|
+
return "error:" & errMsg
|
|
594
|
+
end try
|
|
595
|
+
`);
|
|
596
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
597
|
+
if (!result.success || result.output.startsWith("error:")) {
|
|
598
|
+
console.error(`Failed to forward message: ${result.error || result.output}`);
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Helper to find and operate on a message by ID.
|
|
605
|
+
*/
|
|
606
|
+
findMessageScript(id, operation) {
|
|
607
|
+
return buildAppLevelScript(`
|
|
608
|
+
try
|
|
609
|
+
repeat with acct in accounts
|
|
610
|
+
repeat with mb in mailboxes of acct
|
|
611
|
+
try
|
|
612
|
+
set matchingMsgs to (messages of mb whose id is ${id})
|
|
613
|
+
if (count of matchingMsgs) > 0 then
|
|
614
|
+
set msg to item 1 of matchingMsgs
|
|
615
|
+
${operation}
|
|
616
|
+
return "ok"
|
|
617
|
+
end if
|
|
618
|
+
end try
|
|
619
|
+
end repeat
|
|
620
|
+
end repeat
|
|
621
|
+
return "error:Message not found"
|
|
622
|
+
on error errMsg
|
|
623
|
+
return "error:" & errMsg
|
|
624
|
+
end try
|
|
625
|
+
`);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Mark a message as read.
|
|
629
|
+
*/
|
|
630
|
+
markAsRead(id) {
|
|
631
|
+
const script = this.findMessageScript(id, "set read status of msg to true");
|
|
632
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
633
|
+
if (!result.success || result.output.startsWith("error:")) {
|
|
634
|
+
console.error(`Failed to mark message as read: ${result.error || result.output}`);
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Mark a message as unread.
|
|
641
|
+
*/
|
|
642
|
+
markAsUnread(id) {
|
|
643
|
+
const script = this.findMessageScript(id, "set read status of msg to false");
|
|
644
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
645
|
+
if (!result.success || result.output.startsWith("error:")) {
|
|
646
|
+
console.error(`Failed to mark message as unread: ${result.error || result.output}`);
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Flag a message.
|
|
653
|
+
*/
|
|
654
|
+
flagMessage(id) {
|
|
655
|
+
const script = this.findMessageScript(id, "set flagged status of msg to true");
|
|
656
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
657
|
+
if (!result.success || result.output.startsWith("error:")) {
|
|
658
|
+
console.error(`Failed to flag message: ${result.error || result.output}`);
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Unflag a message.
|
|
665
|
+
*/
|
|
666
|
+
unflagMessage(id) {
|
|
667
|
+
const script = this.findMessageScript(id, "set flagged status of msg to false");
|
|
668
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
669
|
+
if (!result.success || result.output.startsWith("error:")) {
|
|
670
|
+
console.error(`Failed to unflag message: ${result.error || result.output}`);
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Delete a message.
|
|
677
|
+
*/
|
|
678
|
+
deleteMessage(id) {
|
|
679
|
+
const script = this.findMessageScript(id, "delete msg");
|
|
680
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
681
|
+
if (!result.success || result.output.startsWith("error:")) {
|
|
682
|
+
console.error(`Failed to delete message: ${result.error || result.output}`);
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Move a message to a different mailbox.
|
|
689
|
+
*/
|
|
690
|
+
moveMessage(id, mailbox, account) {
|
|
691
|
+
const targetAccount = this.resolveAccount(account);
|
|
692
|
+
const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
|
|
693
|
+
const safeMailbox = escapeForAppleScript(targetMailbox);
|
|
694
|
+
const safeAccount = escapeForAppleScript(targetAccount);
|
|
695
|
+
const script = buildAppLevelScript(`
|
|
696
|
+
try
|
|
697
|
+
repeat with acct in accounts
|
|
698
|
+
repeat with mb in mailboxes of acct
|
|
699
|
+
try
|
|
700
|
+
set matchingMsgs to (messages of mb whose id is ${id})
|
|
701
|
+
if (count of matchingMsgs) > 0 then
|
|
702
|
+
set msg to item 1 of matchingMsgs
|
|
703
|
+
set destMailbox to mailbox "${safeMailbox}" of account "${safeAccount}"
|
|
704
|
+
move msg to destMailbox
|
|
705
|
+
return "ok"
|
|
706
|
+
end if
|
|
707
|
+
end try
|
|
708
|
+
end repeat
|
|
709
|
+
end repeat
|
|
710
|
+
return "error:Message not found"
|
|
711
|
+
on error errMsg
|
|
712
|
+
return "error:" & errMsg
|
|
713
|
+
end try
|
|
714
|
+
`);
|
|
715
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
716
|
+
if (!result.success || result.output.startsWith("error:")) {
|
|
717
|
+
console.error(`Failed to move message: ${result.error || result.output}`);
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
// ===========================================================================
|
|
723
|
+
// Batch Operations
|
|
724
|
+
// ===========================================================================
|
|
725
|
+
/**
|
|
726
|
+
* Delete multiple messages at once.
|
|
727
|
+
*
|
|
728
|
+
* @param ids - Array of message IDs to delete
|
|
729
|
+
* @returns Array of results for each message
|
|
730
|
+
*/
|
|
731
|
+
batchDeleteMessages(ids) {
|
|
732
|
+
const results = [];
|
|
733
|
+
for (const id of ids) {
|
|
734
|
+
const success = this.deleteMessage(id);
|
|
735
|
+
results.push({
|
|
736
|
+
id,
|
|
737
|
+
success,
|
|
738
|
+
error: success ? undefined : "Failed to delete message",
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
return results;
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Move multiple messages to a mailbox at once.
|
|
745
|
+
*
|
|
746
|
+
* @param ids - Array of message IDs to move
|
|
747
|
+
* @param mailbox - Destination mailbox name
|
|
748
|
+
* @param account - Account containing the destination mailbox
|
|
749
|
+
* @returns Array of results for each message
|
|
750
|
+
*/
|
|
751
|
+
batchMoveMessages(ids, mailbox, account) {
|
|
752
|
+
const results = [];
|
|
753
|
+
for (const id of ids) {
|
|
754
|
+
const success = this.moveMessage(id, mailbox, account);
|
|
755
|
+
results.push({
|
|
756
|
+
id,
|
|
757
|
+
success,
|
|
758
|
+
error: success ? undefined : "Failed to move message",
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
return results;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Mark multiple messages as read at once.
|
|
765
|
+
*
|
|
766
|
+
* @param ids - Array of message IDs to mark as read
|
|
767
|
+
* @returns Array of results for each message
|
|
768
|
+
*/
|
|
769
|
+
batchMarkAsRead(ids) {
|
|
770
|
+
const results = [];
|
|
771
|
+
for (const id of ids) {
|
|
772
|
+
const success = this.markAsRead(id);
|
|
773
|
+
results.push({
|
|
774
|
+
id,
|
|
775
|
+
success,
|
|
776
|
+
error: success ? undefined : "Failed to mark message as read",
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return results;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* List attachments for a message.
|
|
783
|
+
*/
|
|
784
|
+
listAttachments(id) {
|
|
785
|
+
const script = buildAppLevelScript(`
|
|
786
|
+
try
|
|
787
|
+
repeat with acct in accounts
|
|
788
|
+
repeat with mb in mailboxes of acct
|
|
789
|
+
try
|
|
790
|
+
set matchingMsgs to (messages of mb whose id is ${id})
|
|
791
|
+
if (count of matchingMsgs) > 0 then
|
|
792
|
+
set msg to item 1 of matchingMsgs
|
|
793
|
+
set outputText to ""
|
|
794
|
+
set attCount to 0
|
|
795
|
+
repeat with att in mail attachments of msg
|
|
796
|
+
set attName to name of att
|
|
797
|
+
set attType to MIME type of att
|
|
798
|
+
set attSize to file size of att as string
|
|
799
|
+
if attCount > 0 then set outputText to outputText & "|||ITEM|||"
|
|
800
|
+
set outputText to outputText & attName & "|||" & attType & "|||" & attSize
|
|
801
|
+
set attCount to attCount + 1
|
|
802
|
+
end repeat
|
|
803
|
+
return outputText
|
|
804
|
+
end if
|
|
805
|
+
end try
|
|
806
|
+
end repeat
|
|
807
|
+
end repeat
|
|
808
|
+
return ""
|
|
809
|
+
on error errMsg
|
|
810
|
+
return ""
|
|
811
|
+
end try
|
|
812
|
+
`);
|
|
813
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
814
|
+
if (!result.success || !result.output.trim()) {
|
|
815
|
+
return [];
|
|
816
|
+
}
|
|
817
|
+
const items = result.output.split("|||ITEM|||");
|
|
818
|
+
const attachments = [];
|
|
819
|
+
for (const item of items) {
|
|
820
|
+
const parts = item.split("|||");
|
|
821
|
+
if (parts.length < 3)
|
|
822
|
+
continue;
|
|
823
|
+
attachments.push({
|
|
824
|
+
id: `${id}-${parts[0]}`,
|
|
825
|
+
name: parts[0],
|
|
826
|
+
mimeType: parts[1],
|
|
827
|
+
size: parseInt(parts[2]) || 0,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
return attachments;
|
|
831
|
+
}
|
|
832
|
+
// ===========================================================================
|
|
833
|
+
// Mailbox Operations
|
|
834
|
+
// ===========================================================================
|
|
835
|
+
/**
|
|
836
|
+
* List all mailboxes for an account.
|
|
837
|
+
*/
|
|
838
|
+
listMailboxes(account) {
|
|
839
|
+
const targetAccount = this.resolveAccount(account);
|
|
840
|
+
const listCommand = `
|
|
841
|
+
set mailboxList to {}
|
|
842
|
+
repeat with mb in mailboxes
|
|
843
|
+
set mbName to name of mb
|
|
844
|
+
set mbUnread to unread count of mb
|
|
845
|
+
set mbCount to count of messages of mb
|
|
846
|
+
set end of mailboxList to mbName & "|||" & mbUnread & "|||" & mbCount
|
|
847
|
+
end repeat
|
|
848
|
+
set AppleScript's text item delimiters to "|||ITEM|||"
|
|
849
|
+
return mailboxList as text
|
|
850
|
+
`;
|
|
851
|
+
const script = buildAccountScopedScript(targetAccount, listCommand);
|
|
852
|
+
const result = executeAppleScript(script);
|
|
853
|
+
if (!result.success) {
|
|
854
|
+
console.error(`Failed to list mailboxes: ${result.error}`);
|
|
855
|
+
return [];
|
|
856
|
+
}
|
|
857
|
+
if (!result.output.trim())
|
|
858
|
+
return [];
|
|
859
|
+
const items = result.output.split("|||ITEM|||");
|
|
860
|
+
const mailboxes = [];
|
|
861
|
+
for (const item of items) {
|
|
862
|
+
const parts = item.split("|||");
|
|
863
|
+
if (parts.length < 3)
|
|
864
|
+
continue;
|
|
865
|
+
mailboxes.push({
|
|
866
|
+
name: parts[0],
|
|
867
|
+
account: targetAccount,
|
|
868
|
+
unreadCount: parseInt(parts[1]) || 0,
|
|
869
|
+
messageCount: parseInt(parts[2]) || 0,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
return mailboxes;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Get unread count for a mailbox.
|
|
876
|
+
*/
|
|
877
|
+
getUnreadCount(mailbox, account) {
|
|
878
|
+
const targetAccount = this.resolveAccount(account);
|
|
879
|
+
let command;
|
|
880
|
+
if (mailbox) {
|
|
881
|
+
const targetMailbox = this.resolveMailbox(mailbox, targetAccount);
|
|
882
|
+
const safeMailbox = escapeForAppleScript(targetMailbox);
|
|
883
|
+
command = `return unread count of mailbox "${safeMailbox}"`;
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
// Get total unread across all mailboxes
|
|
887
|
+
command = `
|
|
888
|
+
set total to 0
|
|
889
|
+
repeat with mb in mailboxes
|
|
890
|
+
set total to total + (unread count of mb)
|
|
891
|
+
end repeat
|
|
892
|
+
return total
|
|
893
|
+
`;
|
|
894
|
+
}
|
|
895
|
+
const script = buildAccountScopedScript(targetAccount, command);
|
|
896
|
+
const result = executeAppleScript(script);
|
|
897
|
+
if (!result.success) {
|
|
898
|
+
console.error(`Failed to get unread count: ${result.error}`);
|
|
899
|
+
return 0;
|
|
900
|
+
}
|
|
901
|
+
return parseInt(result.output) || 0;
|
|
902
|
+
}
|
|
903
|
+
// ===========================================================================
|
|
904
|
+
// Account Operations
|
|
905
|
+
// ===========================================================================
|
|
906
|
+
/**
|
|
907
|
+
* List all mail accounts.
|
|
908
|
+
*/
|
|
909
|
+
listAccounts() {
|
|
910
|
+
const script = buildAppLevelScript(`
|
|
911
|
+
set accountList to {}
|
|
912
|
+
repeat with acct in accounts
|
|
913
|
+
set acctName to name of acct
|
|
914
|
+
set acctEmail to email addresses of acct
|
|
915
|
+
set acctEnabled to enabled of acct
|
|
916
|
+
set emailStr to ""
|
|
917
|
+
if (count of acctEmail) > 0 then
|
|
918
|
+
set emailStr to item 1 of acctEmail
|
|
919
|
+
end if
|
|
920
|
+
set end of accountList to acctName & "|||" & emailStr & "|||" & acctEnabled
|
|
921
|
+
end repeat
|
|
922
|
+
set AppleScript's text item delimiters to "|||ITEM|||"
|
|
923
|
+
return accountList as text
|
|
924
|
+
`);
|
|
925
|
+
const result = executeAppleScript(script);
|
|
926
|
+
if (!result.success) {
|
|
927
|
+
console.error(`Failed to list accounts: ${result.error}`);
|
|
928
|
+
return [];
|
|
929
|
+
}
|
|
930
|
+
if (!result.output.trim())
|
|
931
|
+
return [];
|
|
932
|
+
const items = result.output.split("|||ITEM|||");
|
|
933
|
+
const accounts = [];
|
|
934
|
+
for (const item of items) {
|
|
935
|
+
const parts = item.split("|||");
|
|
936
|
+
if (parts.length < 3)
|
|
937
|
+
continue;
|
|
938
|
+
accounts.push({
|
|
939
|
+
name: parts[0],
|
|
940
|
+
email: parts[1],
|
|
941
|
+
enabled: parts[2] === "true",
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return accounts;
|
|
945
|
+
}
|
|
946
|
+
// ===========================================================================
|
|
947
|
+
// Diagnostics
|
|
948
|
+
// ===========================================================================
|
|
949
|
+
/**
|
|
950
|
+
* Run health check on Mail.app connectivity.
|
|
951
|
+
*/
|
|
952
|
+
healthCheck() {
|
|
953
|
+
const checks = [];
|
|
954
|
+
// Check 1: Mail.app is accessible
|
|
955
|
+
const mailCheck = executeAppleScript('tell application "Mail" to return "ok"');
|
|
956
|
+
if (mailCheck.success && mailCheck.output === "ok") {
|
|
957
|
+
checks.push({
|
|
958
|
+
name: "mail_app",
|
|
959
|
+
passed: true,
|
|
960
|
+
message: "Mail.app is accessible",
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
const errorHint = mailCheck.error?.includes("not authorized")
|
|
965
|
+
? " (check Automation permissions in System Preferences)"
|
|
966
|
+
: "";
|
|
967
|
+
checks.push({
|
|
968
|
+
name: "mail_app",
|
|
969
|
+
passed: false,
|
|
970
|
+
message: `Mail.app is not accessible${errorHint}`,
|
|
971
|
+
});
|
|
972
|
+
return { healthy: false, checks };
|
|
973
|
+
}
|
|
974
|
+
// Check 2: AppleScript permissions
|
|
975
|
+
const permCheck = executeAppleScript('tell application "Mail" to get name of account 1');
|
|
976
|
+
if (permCheck.success) {
|
|
977
|
+
checks.push({
|
|
978
|
+
name: "permissions",
|
|
979
|
+
passed: true,
|
|
980
|
+
message: "AppleScript automation permissions granted",
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
const isPermError = permCheck.error?.includes("not authorized") || permCheck.error?.includes("not permitted");
|
|
985
|
+
checks.push({
|
|
986
|
+
name: "permissions",
|
|
987
|
+
passed: !isPermError,
|
|
988
|
+
message: isPermError
|
|
989
|
+
? "AppleScript permissions denied. Grant access in System Preferences > Privacy & Security > Automation"
|
|
990
|
+
: `Permission check returned: ${permCheck.error}`,
|
|
991
|
+
});
|
|
992
|
+
if (isPermError) {
|
|
993
|
+
return { healthy: false, checks };
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
// Check 3: At least one account accessible
|
|
997
|
+
const accounts = this.listAccounts();
|
|
998
|
+
if (accounts.length > 0) {
|
|
999
|
+
const accountNames = accounts.map((a) => a.name).join(", ");
|
|
1000
|
+
checks.push({
|
|
1001
|
+
name: "accounts",
|
|
1002
|
+
passed: true,
|
|
1003
|
+
message: `Found ${accounts.length} account(s): ${accountNames}`,
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
checks.push({
|
|
1008
|
+
name: "accounts",
|
|
1009
|
+
passed: false,
|
|
1010
|
+
message: "No Mail accounts found. Set up an account in Mail.app first.",
|
|
1011
|
+
});
|
|
1012
|
+
return { healthy: false, checks };
|
|
1013
|
+
}
|
|
1014
|
+
// Check 4: Basic operations work
|
|
1015
|
+
const mailboxes = this.listMailboxes(accounts[0].name);
|
|
1016
|
+
checks.push({
|
|
1017
|
+
name: "operations",
|
|
1018
|
+
passed: true,
|
|
1019
|
+
message: `Basic operations working (${mailboxes.length} mailbox(es) in ${accounts[0].name})`,
|
|
1020
|
+
});
|
|
1021
|
+
return {
|
|
1022
|
+
healthy: checks.every((c) => c.passed),
|
|
1023
|
+
checks,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Get mail statistics.
|
|
1028
|
+
*/
|
|
1029
|
+
getMailStats() {
|
|
1030
|
+
const accounts = this.listAccounts();
|
|
1031
|
+
const accountStats = [];
|
|
1032
|
+
let totalMessages = 0;
|
|
1033
|
+
let totalUnread = 0;
|
|
1034
|
+
for (const account of accounts) {
|
|
1035
|
+
const mailboxes = this.listMailboxes(account.name);
|
|
1036
|
+
let accountMessages = 0;
|
|
1037
|
+
let accountUnread = 0;
|
|
1038
|
+
const mailboxStats = mailboxes.map((mb) => {
|
|
1039
|
+
accountMessages += mb.messageCount;
|
|
1040
|
+
accountUnread += mb.unreadCount;
|
|
1041
|
+
return {
|
|
1042
|
+
name: mb.name,
|
|
1043
|
+
messageCount: mb.messageCount,
|
|
1044
|
+
unreadCount: mb.unreadCount,
|
|
1045
|
+
};
|
|
1046
|
+
});
|
|
1047
|
+
totalMessages += accountMessages;
|
|
1048
|
+
totalUnread += accountUnread;
|
|
1049
|
+
accountStats.push({
|
|
1050
|
+
name: account.name,
|
|
1051
|
+
totalMessages: accountMessages,
|
|
1052
|
+
unreadMessages: accountUnread,
|
|
1053
|
+
mailboxCount: mailboxes.length,
|
|
1054
|
+
mailboxes: mailboxStats,
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
// Get recently received stats
|
|
1058
|
+
const recentlyReceived = this.getRecentlyReceivedStats();
|
|
1059
|
+
return {
|
|
1060
|
+
totalMessages,
|
|
1061
|
+
totalUnread,
|
|
1062
|
+
accounts: accountStats,
|
|
1063
|
+
recentlyReceived,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Get counts of recently received messages.
|
|
1068
|
+
*
|
|
1069
|
+
* Only counts messages in INBOX for performance (scanning all mailboxes
|
|
1070
|
+
* is too slow for large accounts).
|
|
1071
|
+
*
|
|
1072
|
+
* @returns Counts of messages received in last 24h, 7d, and 30d
|
|
1073
|
+
*/
|
|
1074
|
+
getRecentlyReceivedStats() {
|
|
1075
|
+
// Get message counts for different time periods
|
|
1076
|
+
const now = new Date();
|
|
1077
|
+
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
1078
|
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
1079
|
+
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
1080
|
+
// Format dates for AppleScript comparison
|
|
1081
|
+
const formatDate = (d) => {
|
|
1082
|
+
const months = [
|
|
1083
|
+
"January",
|
|
1084
|
+
"February",
|
|
1085
|
+
"March",
|
|
1086
|
+
"April",
|
|
1087
|
+
"May",
|
|
1088
|
+
"June",
|
|
1089
|
+
"July",
|
|
1090
|
+
"August",
|
|
1091
|
+
"September",
|
|
1092
|
+
"October",
|
|
1093
|
+
"November",
|
|
1094
|
+
"December",
|
|
1095
|
+
];
|
|
1096
|
+
return `date "${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}"`;
|
|
1097
|
+
};
|
|
1098
|
+
// Only scan INBOX for performance - scanning all mailboxes is too slow
|
|
1099
|
+
const script = buildAppLevelScript(`
|
|
1100
|
+
set last24h to 0
|
|
1101
|
+
set last7d to 0
|
|
1102
|
+
set last30d to 0
|
|
1103
|
+
set oneDayAgo to ${formatDate(oneDayAgo)}
|
|
1104
|
+
set sevenDaysAgo to ${formatDate(sevenDaysAgo)}
|
|
1105
|
+
set thirtyDaysAgo to ${formatDate(thirtyDaysAgo)}
|
|
1106
|
+
|
|
1107
|
+
repeat with acct in accounts
|
|
1108
|
+
try
|
|
1109
|
+
-- Try common inbox names
|
|
1110
|
+
set inboxNames to {"INBOX", "Inbox", "inbox"}
|
|
1111
|
+
repeat with inboxName in inboxNames
|
|
1112
|
+
try
|
|
1113
|
+
set theInbox to mailbox inboxName of acct
|
|
1114
|
+
set last24h to last24h + (count of (messages of theInbox whose date received >= oneDayAgo))
|
|
1115
|
+
set last7d to last7d + (count of (messages of theInbox whose date received >= sevenDaysAgo))
|
|
1116
|
+
set last30d to last30d + (count of (messages of theInbox whose date received >= thirtyDaysAgo))
|
|
1117
|
+
exit repeat
|
|
1118
|
+
end try
|
|
1119
|
+
end repeat
|
|
1120
|
+
end try
|
|
1121
|
+
end repeat
|
|
1122
|
+
|
|
1123
|
+
return (last24h as string) & "|||" & (last7d as string) & "|||" & (last30d as string)
|
|
1124
|
+
`);
|
|
1125
|
+
const result = executeAppleScript(script, { timeoutMs: 60000 });
|
|
1126
|
+
if (!result.success || !result.output.trim()) {
|
|
1127
|
+
console.error(`Failed to get recently received stats: ${result.error}`);
|
|
1128
|
+
return { last24h: 0, last7d: 0, last30d: 0 };
|
|
1129
|
+
}
|
|
1130
|
+
const parts = result.output.split("|||");
|
|
1131
|
+
if (parts.length < 3) {
|
|
1132
|
+
return { last24h: 0, last7d: 0, last30d: 0 };
|
|
1133
|
+
}
|
|
1134
|
+
return {
|
|
1135
|
+
last24h: parseInt(parts[0]) || 0,
|
|
1136
|
+
last7d: parseInt(parts[1]) || 0,
|
|
1137
|
+
last30d: parseInt(parts[2]) || 0,
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Get sync status for Mail.app.
|
|
1142
|
+
*
|
|
1143
|
+
* Checks for sync activity indicators like:
|
|
1144
|
+
* - Activity monitor status
|
|
1145
|
+
* - Network activity status
|
|
1146
|
+
* - Background refresh indicators
|
|
1147
|
+
*
|
|
1148
|
+
* @returns Sync status information
|
|
1149
|
+
*/
|
|
1150
|
+
getSyncStatus() {
|
|
1151
|
+
// Check for Mail.app background activity and sync status
|
|
1152
|
+
// Mail.app doesn't expose sync status directly through AppleScript,
|
|
1153
|
+
// so we check for recent changes and activity indicators
|
|
1154
|
+
const script = buildAppLevelScript(`
|
|
1155
|
+
set syncInfo to ""
|
|
1156
|
+
|
|
1157
|
+
-- Check if Mail.app is running
|
|
1158
|
+
tell application "System Events"
|
|
1159
|
+
set mailRunning to (name of processes) contains "Mail"
|
|
1160
|
+
end tell
|
|
1161
|
+
|
|
1162
|
+
if not mailRunning then
|
|
1163
|
+
return "not_running"
|
|
1164
|
+
end if
|
|
1165
|
+
|
|
1166
|
+
-- Check for background activity by looking at message counts changing
|
|
1167
|
+
-- This is a proxy for sync activity since Mail doesn't expose sync status
|
|
1168
|
+
set accountCount to count of accounts
|
|
1169
|
+
set totalMailboxes to 0
|
|
1170
|
+
repeat with acct in accounts
|
|
1171
|
+
set totalMailboxes to totalMailboxes + (count of mailboxes of acct)
|
|
1172
|
+
end repeat
|
|
1173
|
+
|
|
1174
|
+
return "running|||" & accountCount & "|||" & totalMailboxes
|
|
1175
|
+
`);
|
|
1176
|
+
const result = executeAppleScript(script);
|
|
1177
|
+
if (!result.success) {
|
|
1178
|
+
return {
|
|
1179
|
+
syncDetected: false,
|
|
1180
|
+
pendingUpload: 0,
|
|
1181
|
+
recentActivity: false,
|
|
1182
|
+
secondsSinceLastChange: -1,
|
|
1183
|
+
error: result.error,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
if (result.output === "not_running") {
|
|
1187
|
+
return {
|
|
1188
|
+
syncDetected: false,
|
|
1189
|
+
pendingUpload: 0,
|
|
1190
|
+
recentActivity: false,
|
|
1191
|
+
secondsSinceLastChange: -1,
|
|
1192
|
+
error: "Mail.app is not running",
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
// Parse the response
|
|
1196
|
+
const parts = result.output.split("|||");
|
|
1197
|
+
const isRunning = parts[0] === "running";
|
|
1198
|
+
const accountCount = parseInt(parts[1]) || 0;
|
|
1199
|
+
// Mail.app is running with accounts configured - assume sync is active
|
|
1200
|
+
// (Mail.app syncs automatically when running)
|
|
1201
|
+
return {
|
|
1202
|
+
syncDetected: isRunning && accountCount > 0,
|
|
1203
|
+
pendingUpload: 0, // Not exposed by Mail.app
|
|
1204
|
+
recentActivity: isRunning,
|
|
1205
|
+
secondsSinceLastChange: 0,
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
}
|