apple-tools-mcp 1.2.0 → 2.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/README.md +261 -7
- package/contacts.js +71 -19
- package/index.js +133 -87
- package/indexer.js +33 -23
- package/lib/appleScript.js +292 -0
- package/lib/audit.js +22 -14
- package/lib/calendarWrite.js +594 -0
- package/lib/contactsWrite.js +300 -0
- package/lib/indexGate.js +49 -0
- package/lib/lancedbTables.js +307 -0
- package/lib/mailWrite.js +464 -0
- package/lib/messagesWrite.js +281 -0
- package/lib/writeBridge.js +214 -0
- package/lib/writeGuards.js +250 -0
- package/lib/writeRouting.js +69 -0
- package/lib/writeTools.js +396 -0
- package/package.json +4 -2
- package/scripts/smoke-writes.js +313 -0
- package/search.js +7 -18
package/lib/mailWrite.js
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apple Mail write operations: send, reply, forward, draft, mark read/unread,
|
|
3
|
+
* archive, trash.
|
|
4
|
+
*
|
|
5
|
+
* Messages are addressed by their RFC822 Message-ID (Mail's `message id`
|
|
6
|
+
* property). `file_path` from mail_search / mail_recent is also accepted and
|
|
7
|
+
* is resolved to a Message-ID by reading the .emlx headers, so callers never
|
|
8
|
+
* have to invent an identifier.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { validateEmailPath, unfoldRfc822Headers, safeMatch, stripHtmlTags } from "./validators.js";
|
|
14
|
+
import { runAppleScript, asString, TCC_GUIDANCE, ATTRIBUTION_GUIDANCE } from "./appleScript.js";
|
|
15
|
+
import {
|
|
16
|
+
planWrite,
|
|
17
|
+
validateEmailList,
|
|
18
|
+
validateBody,
|
|
19
|
+
validateSubject,
|
|
20
|
+
validateMessageId,
|
|
21
|
+
writeErrorMessage,
|
|
22
|
+
writeSuccessMessage,
|
|
23
|
+
isFlagTrue,
|
|
24
|
+
truncate
|
|
25
|
+
} from "./writeGuards.js";
|
|
26
|
+
|
|
27
|
+
const MAIL_DIR = path.join(process.env.HOME || "", "Library", "Mail");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the Mail message id from either an explicit id or an .emlx path.
|
|
31
|
+
* @returns {{ messageId: string|null, error: string|null }}
|
|
32
|
+
*/
|
|
33
|
+
export function resolveMailMessageId({ messageId, filePath } = {}) {
|
|
34
|
+
if (messageId) {
|
|
35
|
+
const valid = validateMessageId(messageId);
|
|
36
|
+
if (!valid) return { messageId: null, error: "message_id is not a valid RFC822 Message-ID" };
|
|
37
|
+
return { messageId: valid, error: null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!filePath) {
|
|
41
|
+
return {
|
|
42
|
+
messageId: null,
|
|
43
|
+
error: "message_id is required (or file_path from mail_search / mail_recent). This tool will not guess which message you meant."
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let resolvedPath;
|
|
48
|
+
try {
|
|
49
|
+
resolvedPath = validateEmailPath(filePath, MAIL_DIR);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return { messageId: null, error: `file_path rejected: ${e.message}` };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let raw;
|
|
55
|
+
try {
|
|
56
|
+
raw = fs.readFileSync(resolvedPath, "utf-8");
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return { messageId: null, error: `Could not read the email file (${e.code || "read error"})` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const headerMatch = safeMatch(unfoldRfc822Headers(raw), /^Message-ID:\s*(.+)$/im, 200000);
|
|
62
|
+
const found = headerMatch && headerMatch[1] ? validateMessageId(headerMatch[1].trim()) : null;
|
|
63
|
+
if (!found) {
|
|
64
|
+
return { messageId: null, error: "That email has no usable Message-ID header; pass message_id explicitly." };
|
|
65
|
+
}
|
|
66
|
+
return { messageId: found, error: null };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* AppleScript handler that locates a message by Message-ID. Checks inbox
|
|
71
|
+
* first, then every mailbox of every account.
|
|
72
|
+
*/
|
|
73
|
+
function findMessageHandler() {
|
|
74
|
+
return `on atmFindMessage(msgId)
|
|
75
|
+
tell application "Mail"
|
|
76
|
+
try
|
|
77
|
+
set quickHits to (messages of inbox whose message id is msgId)
|
|
78
|
+
if (count of quickHits) > 0 then return item 1 of quickHits
|
|
79
|
+
end try
|
|
80
|
+
repeat with acct in accounts
|
|
81
|
+
try
|
|
82
|
+
repeat with mb in (every mailbox of acct)
|
|
83
|
+
try
|
|
84
|
+
set hits to (messages of mb whose message id is msgId)
|
|
85
|
+
if (count of hits) > 0 then return item 1 of hits
|
|
86
|
+
end try
|
|
87
|
+
end repeat
|
|
88
|
+
end try
|
|
89
|
+
end repeat
|
|
90
|
+
end tell
|
|
91
|
+
error "MESSAGE_NOT_FOUND"
|
|
92
|
+
end atmFindMessage`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function recipientLines(addresses, kind) {
|
|
96
|
+
return addresses
|
|
97
|
+
.map((address) => ` make new ${kind} at end of ${kind}s with properties {address:${asString(address)}}`)
|
|
98
|
+
.join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build the outgoing-message script shared by send and draft.
|
|
103
|
+
*
|
|
104
|
+
* Mail renders `html content` when it is set; `content` stays populated as
|
|
105
|
+
* the plain-text alternative for clients that do not display HTML.
|
|
106
|
+
*/
|
|
107
|
+
export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
|
|
108
|
+
const recipients = [
|
|
109
|
+
recipientLines(to, "to recipient"),
|
|
110
|
+
recipientLines(cc, "cc recipient"),
|
|
111
|
+
recipientLines(bcc, "bcc recipient")
|
|
112
|
+
].filter((block) => block.length > 0).join("\n");
|
|
113
|
+
|
|
114
|
+
const htmlLine = html
|
|
115
|
+
? ` try
|
|
116
|
+
set html content of newMessage to ${asString(body)}
|
|
117
|
+
end try\n`
|
|
118
|
+
: "";
|
|
119
|
+
|
|
120
|
+
return `tell application "Mail"
|
|
121
|
+
set newMessage to make new outgoing message with properties {subject:${asString(subject)}, content:${asString(html ? stripHtmlTags(body) : body)}, visible:false}
|
|
122
|
+
${htmlLine} tell newMessage
|
|
123
|
+
${recipients}
|
|
124
|
+
end tell
|
|
125
|
+
${send ? "send newMessage" : "save newMessage"}
|
|
126
|
+
end tell
|
|
127
|
+
return "OK"`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function summarizeRecipients(to, cc, bcc) {
|
|
131
|
+
const parts = [];
|
|
132
|
+
if (to.length) parts.push(`to ${to.join(", ")}`);
|
|
133
|
+
if (cc.length) parts.push(`cc ${cc.join(", ")}`);
|
|
134
|
+
if (bcc.length) parts.push(`bcc ${bcc.length} recipient${bcc.length === 1 ? "" : "s"}`);
|
|
135
|
+
return parts.join("; ");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function failure(action, summary, result, secrets) {
|
|
139
|
+
if (result.kind === "tcc") {
|
|
140
|
+
return `${action} failed — attempted to ${summary}. ${TCC_GUIDANCE}`;
|
|
141
|
+
}
|
|
142
|
+
if (result.kind === "not_found") {
|
|
143
|
+
return `${action} failed — attempted to ${summary}. The message could not be found in Mail. Pass a message_id from a current mail_search result.`;
|
|
144
|
+
}
|
|
145
|
+
if (result.kind === "attribution") {
|
|
146
|
+
return `${action} failed — attempted to ${summary}. ${ATTRIBUTION_GUIDANCE}`;
|
|
147
|
+
}
|
|
148
|
+
if (result.kind === "app_unavailable") {
|
|
149
|
+
return `${action} failed — attempted to ${summary}. Mail.app could not be reached on this host.`;
|
|
150
|
+
}
|
|
151
|
+
return writeErrorMessage(action, summary, new Error(result.error || "unknown error"), secrets);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Compose and send (or save as draft) a new email.
|
|
156
|
+
*/
|
|
157
|
+
export function mailCompose(args = {}, { draft = false } = {}) {
|
|
158
|
+
const action = draft ? "mail_draft" : "mail_send";
|
|
159
|
+
|
|
160
|
+
const to = validateEmailList(args.to, "to");
|
|
161
|
+
if (to.error) return { ok: false, message: `${action} refused: ${to.error}` };
|
|
162
|
+
const cc = validateEmailList(args.cc, "cc");
|
|
163
|
+
if (cc.error) return { ok: false, message: `${action} refused: ${cc.error}` };
|
|
164
|
+
const bcc = validateEmailList(args.bcc, "bcc");
|
|
165
|
+
if (bcc.error) return { ok: false, message: `${action} refused: ${bcc.error}` };
|
|
166
|
+
|
|
167
|
+
if (to.addresses.length === 0) {
|
|
168
|
+
return { ok: false, message: `${action} refused: at least one valid address in "to" is required. This tool never invents recipients.` };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const subject = validateSubject(args.subject, { required: !draft });
|
|
172
|
+
if (subject.error) return { ok: false, message: `${action} refused: ${subject.error}` };
|
|
173
|
+
const body = validateBody(args.body, { required: !draft });
|
|
174
|
+
if (body.error) return { ok: false, message: `${action} refused: ${body.error}` };
|
|
175
|
+
|
|
176
|
+
const bodyFormat = args.body_format === undefined ? "plain" : String(args.body_format).toLowerCase();
|
|
177
|
+
if (bodyFormat !== "plain" && bodyFormat !== "html") {
|
|
178
|
+
return { ok: false, message: `${action} refused: body_format must be "plain" or "html"` };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const recipientCount = to.addresses.length + cc.addresses.length + bcc.addresses.length;
|
|
182
|
+
const summary = draft
|
|
183
|
+
? `save a draft ${summarizeRecipients(to.addresses, cc.addresses, bcc.addresses)} with subject "${truncate(subject.text, 120)}"`
|
|
184
|
+
: `send mail ${summarizeRecipients(to.addresses, cc.addresses, bcc.addresses)} with subject "${truncate(subject.text, 120)}"`;
|
|
185
|
+
|
|
186
|
+
// A draft is not delivery, so it does not need multi-recipient confirmation.
|
|
187
|
+
const plan = planWrite({
|
|
188
|
+
action,
|
|
189
|
+
summary,
|
|
190
|
+
recipientCount: draft ? 0 : recipientCount,
|
|
191
|
+
dryRun: isFlagTrue(args.dry_run),
|
|
192
|
+
confirm: isFlagTrue(args.confirm)
|
|
193
|
+
});
|
|
194
|
+
if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
|
|
195
|
+
|
|
196
|
+
const script = buildComposeScript({
|
|
197
|
+
to: to.addresses,
|
|
198
|
+
cc: cc.addresses,
|
|
199
|
+
bcc: bcc.addresses,
|
|
200
|
+
subject: subject.text,
|
|
201
|
+
body: body.text,
|
|
202
|
+
send: !draft,
|
|
203
|
+
html: bodyFormat === "html"
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const result = runAppleScript(script, { timeout: 60000, appName: "Mail" });
|
|
207
|
+
if (!result.ok) {
|
|
208
|
+
return { ok: false, message: failure(action, summary, result, [body.text, subject.text]) };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
ok: true,
|
|
213
|
+
message: writeSuccessMessage(action, draft ? `draft saved to Drafts` : `sent`, {
|
|
214
|
+
to: to.addresses.join(", "),
|
|
215
|
+
cc: cc.addresses.join(", ") || undefined,
|
|
216
|
+
bcc: bcc.addresses.length ? `${bcc.addresses.length} recipient(s)` : undefined,
|
|
217
|
+
subject: truncate(subject.text, 150)
|
|
218
|
+
})
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function buildReplyScript({ messageId, body, replyAll, sendNow }) {
|
|
223
|
+
return `${findMessageHandler()}
|
|
224
|
+
|
|
225
|
+
set theMessage to atmFindMessage(${asString(messageId)})
|
|
226
|
+
tell application "Mail"
|
|
227
|
+
set theReply to missing value
|
|
228
|
+
try
|
|
229
|
+
set theReply to reply theMessage without opening window ${replyAll ? "with reply to all" : "without reply to all"}
|
|
230
|
+
end try
|
|
231
|
+
if theReply is missing value then
|
|
232
|
+
-- Some Mail versions do not return the outgoing message from a reply.
|
|
233
|
+
-- Fall back to a new message addressed to the original sender.
|
|
234
|
+
set origSubject to subject of theMessage
|
|
235
|
+
set origSender to extract address from (sender of theMessage)
|
|
236
|
+
set theReply to make new outgoing message with properties {subject:("Re: " & origSubject), content:${asString(body)}, visible:false}
|
|
237
|
+
tell theReply
|
|
238
|
+
make new to recipient at end of to recipients with properties {address:origSender}
|
|
239
|
+
end tell
|
|
240
|
+
else
|
|
241
|
+
tell theReply
|
|
242
|
+
set content to ${asString(body)} & return & content
|
|
243
|
+
end tell
|
|
244
|
+
end if
|
|
245
|
+
${sendNow ? "send theReply" : "save theReply"}
|
|
246
|
+
end tell
|
|
247
|
+
return "OK"`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function mailReply(args = {}) {
|
|
251
|
+
const action = "mail_reply";
|
|
252
|
+
const resolved = resolveMailMessageId({ messageId: args.message_id, filePath: args.file_path });
|
|
253
|
+
if (resolved.error) return { ok: false, message: `${action} refused: ${resolved.error}` };
|
|
254
|
+
|
|
255
|
+
const body = validateBody(args.body, { required: true });
|
|
256
|
+
if (body.error) return { ok: false, message: `${action} refused: ${body.error}` };
|
|
257
|
+
|
|
258
|
+
const replyAll = isFlagTrue(args.reply_all);
|
|
259
|
+
const sendNow = !isFlagTrue(args.save_as_draft);
|
|
260
|
+
const summary = `${sendNow ? "send" : "draft"} a ${replyAll ? "reply-all" : "reply"} to message ${truncate(resolved.messageId, 120)}`;
|
|
261
|
+
|
|
262
|
+
// reply-all fans out to every original recipient, so treat it like a
|
|
263
|
+
// multi-recipient send and require confirmation.
|
|
264
|
+
const plan = planWrite({
|
|
265
|
+
action,
|
|
266
|
+
summary,
|
|
267
|
+
recipientCount: replyAll && sendNow ? 2 : 0,
|
|
268
|
+
dryRun: isFlagTrue(args.dry_run),
|
|
269
|
+
confirm: isFlagTrue(args.confirm)
|
|
270
|
+
});
|
|
271
|
+
if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
|
|
272
|
+
|
|
273
|
+
const result = runAppleScript(
|
|
274
|
+
buildReplyScript({ messageId: resolved.messageId, body: body.text, replyAll, sendNow }),
|
|
275
|
+
{ timeout: 60000, appName: "Mail" }
|
|
276
|
+
);
|
|
277
|
+
if (!result.ok) return { ok: false, message: failure(action, summary, result, [body.text]) };
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
ok: true,
|
|
281
|
+
message: writeSuccessMessage(action, sendNow ? "reply sent" : "reply saved to Drafts", {
|
|
282
|
+
message_id: resolved.messageId,
|
|
283
|
+
reply_all: String(replyAll)
|
|
284
|
+
})
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function buildForwardScript({ messageId, to, body, sendNow }) {
|
|
289
|
+
return `${findMessageHandler()}
|
|
290
|
+
|
|
291
|
+
set theMessage to atmFindMessage(${asString(messageId)})
|
|
292
|
+
tell application "Mail"
|
|
293
|
+
set theForward to missing value
|
|
294
|
+
try
|
|
295
|
+
set theForward to forward theMessage without opening window
|
|
296
|
+
end try
|
|
297
|
+
if theForward is missing value then error "FORWARD_UNSUPPORTED"
|
|
298
|
+
tell theForward
|
|
299
|
+
set content to ${asString(body)} & return & content
|
|
300
|
+
${to.map((address) => ` make new to recipient at end of to recipients with properties {address:${asString(address)}}`).join("\n")}
|
|
301
|
+
end tell
|
|
302
|
+
${sendNow ? "send theForward" : "save theForward"}
|
|
303
|
+
end tell
|
|
304
|
+
return "OK"`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function mailForward(args = {}) {
|
|
308
|
+
const action = "mail_forward";
|
|
309
|
+
const resolved = resolveMailMessageId({ messageId: args.message_id, filePath: args.file_path });
|
|
310
|
+
if (resolved.error) return { ok: false, message: `${action} refused: ${resolved.error}` };
|
|
311
|
+
|
|
312
|
+
const to = validateEmailList(args.to, "to");
|
|
313
|
+
if (to.error) return { ok: false, message: `${action} refused: ${to.error}` };
|
|
314
|
+
if (to.addresses.length === 0) {
|
|
315
|
+
return { ok: false, message: `${action} refused: at least one valid address in "to" is required.` };
|
|
316
|
+
}
|
|
317
|
+
const body = validateBody(args.body, { required: false });
|
|
318
|
+
if (body.error) return { ok: false, message: `${action} refused: ${body.error}` };
|
|
319
|
+
|
|
320
|
+
const sendNow = !isFlagTrue(args.save_as_draft);
|
|
321
|
+
const summary = `${sendNow ? "forward" : "draft a forward of"} message ${truncate(resolved.messageId, 120)} to ${to.addresses.join(", ")}`;
|
|
322
|
+
|
|
323
|
+
const plan = planWrite({
|
|
324
|
+
action,
|
|
325
|
+
summary,
|
|
326
|
+
recipientCount: sendNow ? to.addresses.length : 0,
|
|
327
|
+
dryRun: isFlagTrue(args.dry_run),
|
|
328
|
+
confirm: isFlagTrue(args.confirm)
|
|
329
|
+
});
|
|
330
|
+
if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
|
|
331
|
+
|
|
332
|
+
const result = runAppleScript(
|
|
333
|
+
buildForwardScript({ messageId: resolved.messageId, to: to.addresses, body: body.text, sendNow }),
|
|
334
|
+
{ timeout: 60000, appName: "Mail" }
|
|
335
|
+
);
|
|
336
|
+
if (!result.ok) return { ok: false, message: failure(action, summary, result, [body.text]) };
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
ok: true,
|
|
340
|
+
message: writeSuccessMessage(action, sendNow ? "forwarded" : "forward saved to Drafts", {
|
|
341
|
+
message_id: resolved.messageId,
|
|
342
|
+
to: to.addresses.join(", ")
|
|
343
|
+
})
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function buildMarkScript({ messageId, read }) {
|
|
348
|
+
return `${findMessageHandler()}
|
|
349
|
+
|
|
350
|
+
set theMessage to atmFindMessage(${asString(messageId)})
|
|
351
|
+
tell application "Mail"
|
|
352
|
+
set read status of theMessage to ${read ? "true" : "false"}
|
|
353
|
+
end tell
|
|
354
|
+
return "OK"`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function mailMark(args = {}) {
|
|
358
|
+
const action = "mail_mark";
|
|
359
|
+
const status = args.status === undefined ? "read" : String(args.status).toLowerCase();
|
|
360
|
+
if (status !== "read" && status !== "unread") {
|
|
361
|
+
return { ok: false, message: `${action} refused: status must be "read" or "unread"` };
|
|
362
|
+
}
|
|
363
|
+
const resolved = resolveMailMessageId({ messageId: args.message_id, filePath: args.file_path });
|
|
364
|
+
if (resolved.error) return { ok: false, message: `${action} refused: ${resolved.error}` };
|
|
365
|
+
|
|
366
|
+
const summary = `mark message ${truncate(resolved.messageId, 120)} as ${status}`;
|
|
367
|
+
const plan = planWrite({
|
|
368
|
+
action,
|
|
369
|
+
summary,
|
|
370
|
+
dryRun: isFlagTrue(args.dry_run),
|
|
371
|
+
confirm: isFlagTrue(args.confirm)
|
|
372
|
+
});
|
|
373
|
+
if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
|
|
374
|
+
|
|
375
|
+
const result = runAppleScript(buildMarkScript({ messageId: resolved.messageId, read: status === "read" }), { appName: "Mail" });
|
|
376
|
+
if (!result.ok) return { ok: false, message: failure(action, summary, result, []) };
|
|
377
|
+
|
|
378
|
+
return { ok: true, message: writeSuccessMessage(action, `marked as ${status}`, { message_id: resolved.messageId }) };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function buildMoveScript({ messageId, mailboxNames, allowDeleteFallback }) {
|
|
382
|
+
const candidates = mailboxNames
|
|
383
|
+
.map((name) => ` if targetBox is missing value then
|
|
384
|
+
try
|
|
385
|
+
set targetBox to mailbox ${asString(name)} of acct
|
|
386
|
+
end try
|
|
387
|
+
end if`)
|
|
388
|
+
.join("\n");
|
|
389
|
+
|
|
390
|
+
return `${findMessageHandler()}
|
|
391
|
+
|
|
392
|
+
set theMessage to atmFindMessage(${asString(messageId)})
|
|
393
|
+
tell application "Mail"
|
|
394
|
+
set acct to account of (mailbox of theMessage)
|
|
395
|
+
set targetBox to missing value
|
|
396
|
+
${candidates}
|
|
397
|
+
if targetBox is missing value then
|
|
398
|
+
${allowDeleteFallback ? "delete theMessage" : 'error "ARCHIVE_MAILBOX_NOT_FOUND"'}
|
|
399
|
+
else
|
|
400
|
+
set mailbox of theMessage to targetBox
|
|
401
|
+
end if
|
|
402
|
+
end tell
|
|
403
|
+
return "OK"`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function mailArchive(args = {}) {
|
|
407
|
+
const action = "mail_archive";
|
|
408
|
+
const resolved = resolveMailMessageId({ messageId: args.message_id, filePath: args.file_path });
|
|
409
|
+
if (resolved.error) return { ok: false, message: `${action} refused: ${resolved.error}` };
|
|
410
|
+
|
|
411
|
+
const summary = `archive message ${truncate(resolved.messageId, 120)}`;
|
|
412
|
+
const plan = planWrite({
|
|
413
|
+
action,
|
|
414
|
+
summary,
|
|
415
|
+
dryRun: isFlagTrue(args.dry_run),
|
|
416
|
+
confirm: isFlagTrue(args.confirm)
|
|
417
|
+
});
|
|
418
|
+
if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
|
|
419
|
+
|
|
420
|
+
const result = runAppleScript(
|
|
421
|
+
buildMoveScript({
|
|
422
|
+
messageId: resolved.messageId,
|
|
423
|
+
mailboxNames: ["Archive", "All Mail", "Archived"],
|
|
424
|
+
allowDeleteFallback: false
|
|
425
|
+
}),
|
|
426
|
+
{ appName: "Mail" }
|
|
427
|
+
);
|
|
428
|
+
if (!result.ok) {
|
|
429
|
+
if (result.kind === "not_found" && String(result.error).includes("ARCHIVE_MAILBOX_NOT_FOUND")) {
|
|
430
|
+
return { ok: false, message: `${action} failed — attempted to ${summary}. That account has no Archive mailbox.` };
|
|
431
|
+
}
|
|
432
|
+
return { ok: false, message: failure(action, summary, result, []) };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return { ok: true, message: writeSuccessMessage(action, "moved to Archive", { message_id: resolved.messageId }) };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function mailTrash(args = {}) {
|
|
439
|
+
const action = "mail_trash";
|
|
440
|
+
const resolved = resolveMailMessageId({ messageId: args.message_id, filePath: args.file_path });
|
|
441
|
+
if (resolved.error) return { ok: false, message: `${action} refused: ${resolved.error}` };
|
|
442
|
+
|
|
443
|
+
const summary = `move message ${truncate(resolved.messageId, 120)} to Trash`;
|
|
444
|
+
const plan = planWrite({
|
|
445
|
+
action,
|
|
446
|
+
summary,
|
|
447
|
+
destructive: true,
|
|
448
|
+
dryRun: isFlagTrue(args.dry_run),
|
|
449
|
+
confirm: isFlagTrue(args.confirm)
|
|
450
|
+
});
|
|
451
|
+
if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
|
|
452
|
+
|
|
453
|
+
const result = runAppleScript(
|
|
454
|
+
buildMoveScript({
|
|
455
|
+
messageId: resolved.messageId,
|
|
456
|
+
mailboxNames: ["Trash", "Deleted Messages", "Bin"],
|
|
457
|
+
allowDeleteFallback: true
|
|
458
|
+
}),
|
|
459
|
+
{ appName: "Mail" }
|
|
460
|
+
);
|
|
461
|
+
if (!result.ok) return { ok: false, message: failure(action, summary, result, []) };
|
|
462
|
+
|
|
463
|
+
return { ok: true, message: writeSuccessMessage(action, "moved to Trash", { message_id: resolved.messageId }) };
|
|
464
|
+
}
|