apple-tools-mcp 2.0.4 → 2.0.6
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 +5 -3
- package/lib/appleScript.js +11 -0
- package/lib/mailWrite.js +173 -48
- package/lib/writeTools.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -461,11 +461,13 @@ Two arguments are available on **every** write tool:
|
|
|
461
461
|
| `mail_archive` | `message_id` or `file_path` | none |
|
|
462
462
|
| `mail_trash` | `message_id` or `file_path` | **`confirm` required** |
|
|
463
463
|
|
|
464
|
-
`body_format: "html"` uses the same
|
|
464
|
+
`body_format: "html"` uses the same compose path with a tag-stripped body pasted into Mail's native editor. It does **not** set Mail's `content` or `html content` — those setters cite-wrap the whole message (`>` prefixes and `<blockquote type="cite">`, same iOS purple bar). Mail may still generate its own HTML alternative from the native compose. The default is plain text.
|
|
465
465
|
|
|
466
|
-
**Compose is not quoted.** `mail_send` / `mail_draft` (plain and html)
|
|
466
|
+
**Compose is not quoted.** `mail_send` / `mail_draft` (plain and html) call `make new outgoing message` **without** AppleScript `content:` so `newMessage` is a real Mail object, then paste the body into the compose window (System Events). Mail's `mailto` command was shipped in 2.0.4 and **fails on Mini/MacBook Mail**: it does not return an outgoing message (`newMessage` undefined, AppleScript **-2753**). On current Mail (Ventura+, FB11734014) `content` / `html content` store the body as a citation: every plain-text line prefixed with `>`, plus a `multipart/alternative` HTML part wrapped in `<blockquote type="cite">`. Desktop Mail often hides the bar with inline styles; iOS Mail paints the whole body purple with a left quote bar — even when the subject is not `Re:`/`Fwd:` and there is no `In-Reply-To`. Reply and forward still quote the original, which is expected.
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
Paste uses System Events, so **node needs Accessibility** (Privacy & Security → Accessibility) in addition to Automation → Mail. That Accessibility deny is not a Mail Automation deny (`-1743` / `-10004`). Body focus uses `text area` / `scroll area` and `UI element whose role is "AXWebArea"` — never the System Events class `web area`, which does not compile on macOS 26.x (**-2741**, “Expected class name but found identifier”).
|
|
469
|
+
|
|
470
|
+
**Manual prove (2.0.6 on the Mac host):** `mail_send` a short plain message and a short `body_format: "html"` message whose body looks like ordinary paragraphs (for example `<p>Quick note</p>`), neither with a `Re:`/`Fwd:` subject. Inspect each Sent `.emlx`: the text/plain part must not prefix every body line with `>`, and any HTML alternative must not wrap the whole body in `<blockquote type="cite">`. Compose must succeed (no `-2753` / undefined `newMessage`, no **-2741** on body focus).
|
|
469
471
|
|
|
470
472
|
Emails are addressed by their RFC822 **Message-ID**. Pass `message_id`, or pass the `file_path` from `mail_search` / `mail_recent` and the server reads the Message-ID out of the `.emlx` headers for you. `mail_archive` moves the message to its account's Archive (or All Mail) mailbox; `mail_trash` moves it to that account's Trash.
|
|
471
473
|
|
package/lib/appleScript.js
CHANGED
|
@@ -179,6 +179,7 @@ export function needsHostTccAdvice(message) {
|
|
|
179
179
|
text.includes(CALENDAR_TCC_GUIDANCE) ||
|
|
180
180
|
text.includes(MESSAGES_TCC_GUIDANCE) ||
|
|
181
181
|
text.includes(MAIL_TCC_GUIDANCE) ||
|
|
182
|
+
text.includes(MAIL_ACCESSIBILITY_GUIDANCE) ||
|
|
182
183
|
text.includes(ATTRIBUTION_GUIDANCE) ||
|
|
183
184
|
text.includes(TCC_GUIDANCE)
|
|
184
185
|
) {
|
|
@@ -332,6 +333,16 @@ export const MAIL_TCC_GUIDANCE =
|
|
|
332
333
|
"Allow node in System Settings → Privacy & Security → Automation for Mail (same Allow-via-prompt as Contacts and Calendar — do not add node via + in a privacy list). " +
|
|
333
334
|
"A Contacts or Calendar grant does not include Mail.";
|
|
334
335
|
|
|
336
|
+
/**
|
|
337
|
+
* Body paste for mail_send / mail_draft uses System Events. That is
|
|
338
|
+
* Accessibility, not Mail Automation (-1743).
|
|
339
|
+
*/
|
|
340
|
+
export const MAIL_ACCESSIBILITY_GUIDANCE =
|
|
341
|
+
"macOS denied Accessibility (System Events could not drive Mail's compose window). " +
|
|
342
|
+
"mail_send / mail_draft paste the body so Mail does not cite-wrap it (FB11734014); Mail's mailto command does not return an outgoing message on current Mini/MacBook Mail (-2753). " +
|
|
343
|
+
"Allow node in System Settings → Privacy & Security → Accessibility, and keep Automation → Mail allowed. " +
|
|
344
|
+
"This is not a Mail Automation deny (-1743 / -10004).";
|
|
345
|
+
|
|
335
346
|
/**
|
|
336
347
|
* AppleScript hung (ETIMEDOUT / -1712) while finding, opening, replying,
|
|
337
348
|
* or sending. That is not -1743 / -10004, including before send.
|
package/lib/mailWrite.js
CHANGED
|
@@ -19,8 +19,10 @@ import { validateEmailPath, unfoldRfc822Headers, safeMatch, stripHtmlTags } from
|
|
|
19
19
|
import {
|
|
20
20
|
runAppleScript,
|
|
21
21
|
asString,
|
|
22
|
+
asInteger,
|
|
22
23
|
MAIL_TCC_GUIDANCE,
|
|
23
24
|
MAIL_SEND_TIMEOUT_GUIDANCE,
|
|
25
|
+
MAIL_ACCESSIBILITY_GUIDANCE,
|
|
24
26
|
MAIL_APP_NOT_RUNNING_GUIDANCE,
|
|
25
27
|
ATTRIBUTION_GUIDANCE,
|
|
26
28
|
isHardTccDenial
|
|
@@ -148,62 +150,159 @@ export function probeMailAutomation() {
|
|
|
148
150
|
};
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
*
|
|
154
|
-
* Newlines become %0D%0A so Mail keeps paragraphs. Characters that
|
|
155
|
-
* encodeURIComponent leaves unescaped (`!'()*`) are encoded too, because
|
|
156
|
-
* they can break the URL or the AppleScript string around it.
|
|
157
|
-
*
|
|
158
|
-
* Does not prefix lines with `>` — Mail's native mailto compose path
|
|
159
|
-
* already treats the decoded body as original text, not a citation.
|
|
160
|
-
*/
|
|
161
|
-
export function encodeMailtoQueryValue(value) {
|
|
162
|
-
const text = value === undefined || value === null ? "" : String(value);
|
|
163
|
-
const normalized = text.replace(/\r\n|\r|\n/g, "\r\n");
|
|
164
|
-
return encodeURIComponent(normalized).replace(/[!'()*]/g, (ch) => {
|
|
165
|
-
return `%${ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`;
|
|
166
|
-
});
|
|
167
|
-
}
|
|
153
|
+
export const ACCESSIBILITY_DENIED_SENTINEL = "ACCESSIBILITY_DENIED";
|
|
154
|
+
export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
|
|
168
155
|
|
|
169
156
|
/**
|
|
170
|
-
* Body text for Mail
|
|
157
|
+
* Body text for native Mail compose (clipboard paste, not AppleScript content).
|
|
171
158
|
*
|
|
172
159
|
* HTML is tag-stripped so a plain-looking HTML body (`<p>…</p>`) becomes
|
|
173
|
-
*
|
|
174
|
-
* `<blockquote>` — AppleScript `
|
|
175
|
-
*
|
|
160
|
+
* typed/pasted text. Never prefixes lines with `>` and never emits
|
|
161
|
+
* `<blockquote>` — AppleScript `content` / `html content` cite-wrap
|
|
162
|
+
* (FB11734014). Mail's `mailto` command is not used: on current Mini/MacBook
|
|
163
|
+
* Mail it does not return an outgoing message (`newMessage` stays undefined,
|
|
164
|
+
* AppleScript -2753).
|
|
176
165
|
*/
|
|
177
|
-
export function
|
|
166
|
+
export function composeNativeBody(body, { html = false } = {}) {
|
|
178
167
|
if (html) return stripHtmlTags(body);
|
|
179
168
|
return body === undefined || body === null ? "" : String(body);
|
|
180
169
|
}
|
|
181
170
|
|
|
182
171
|
/**
|
|
183
|
-
*
|
|
172
|
+
* AppleScript handlers: focus the compose body (not To) and paste.
|
|
173
|
+
* System Events needs Accessibility; Mail make/send still needs Automation.
|
|
184
174
|
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* (FB11734014: setting AppleScript `content` wraps the whole body in a
|
|
189
|
-
* cite-blockquote — `>` prefixes on text/plain, `<blockquote type="cite">`
|
|
190
|
-
* on text/html — which iOS Mail paints as purple text with a left bar).
|
|
175
|
+
* macOS 26.x System Events has no `web area` class (compile -2741,
|
|
176
|
+
* "Expected class name but found identifier"). Hosts compile
|
|
177
|
+
* `text area` / `scroll area` and `UI element whose role is "AXWebArea"`.
|
|
191
178
|
*/
|
|
192
|
-
export function
|
|
193
|
-
return `
|
|
179
|
+
export function buildMailBodyPasteHandler() {
|
|
180
|
+
return `on atmFocusMailBody()
|
|
181
|
+
tell application "System Events"
|
|
182
|
+
tell process "Mail"
|
|
183
|
+
set frontmost to true
|
|
184
|
+
if (count of windows) < 1 then error "MAIL_COMPOSE_WINDOW_MISSING"
|
|
185
|
+
set w to first window
|
|
186
|
+
try
|
|
187
|
+
set focused of text area 1 of scroll area 1 of w to true
|
|
188
|
+
return
|
|
189
|
+
end try
|
|
190
|
+
try
|
|
191
|
+
set focused of text area 1 of w to true
|
|
192
|
+
return
|
|
193
|
+
end try
|
|
194
|
+
try
|
|
195
|
+
set focused of (first UI element of w whose role is "AXTextArea") to true
|
|
196
|
+
return
|
|
197
|
+
end try
|
|
198
|
+
try
|
|
199
|
+
click (first UI element of w whose role is "AXWebArea")
|
|
200
|
+
return
|
|
201
|
+
end try
|
|
202
|
+
try
|
|
203
|
+
click (first UI element of scroll area 1 of w whose role is "AXWebArea")
|
|
204
|
+
return
|
|
205
|
+
end try
|
|
206
|
+
try
|
|
207
|
+
click (first UI element of scroll area 1 of splitter group 1 of w whose role is "AXWebArea")
|
|
208
|
+
return
|
|
209
|
+
end try
|
|
210
|
+
try
|
|
211
|
+
click (first UI element of scroll area 1 of group 1 of splitter group 1 of w whose role is "AXWebArea")
|
|
212
|
+
return
|
|
213
|
+
end try
|
|
214
|
+
set atmQueue to UI elements of w
|
|
215
|
+
set atmWalked to 0
|
|
216
|
+
repeat while (count of atmQueue) > 0 and atmWalked < 80
|
|
217
|
+
set atmWalked to atmWalked + 1
|
|
218
|
+
set atmElem to item 1 of atmQueue
|
|
219
|
+
if (count of atmQueue) is 1 then
|
|
220
|
+
set atmQueue to {}
|
|
221
|
+
else
|
|
222
|
+
set atmQueue to items 2 thru -1 of atmQueue
|
|
223
|
+
end if
|
|
224
|
+
try
|
|
225
|
+
if (role of atmElem as string) is "AXWebArea" then
|
|
226
|
+
click atmElem
|
|
227
|
+
return
|
|
228
|
+
end if
|
|
229
|
+
end try
|
|
230
|
+
try
|
|
231
|
+
if (role of atmElem as string) is "AXTextArea" then
|
|
232
|
+
set focused of atmElem to true
|
|
233
|
+
return
|
|
234
|
+
end if
|
|
235
|
+
end try
|
|
236
|
+
try
|
|
237
|
+
set atmKids to UI elements of atmElem
|
|
238
|
+
repeat with i from 1 to (count of atmKids)
|
|
239
|
+
set end of atmQueue to item i of atmKids
|
|
240
|
+
end repeat
|
|
241
|
+
end try
|
|
242
|
+
end repeat
|
|
243
|
+
set {wx, wy} to position of w
|
|
244
|
+
set {ww, wh} to size of w
|
|
245
|
+
click at {(wx + (ww div 2)) as integer, (wy + ((wh * 2) div 3)) as integer}
|
|
246
|
+
end tell
|
|
247
|
+
end tell
|
|
248
|
+
end atmFocusMailBody
|
|
249
|
+
|
|
250
|
+
on atmPasteMailBody(bodyText)
|
|
251
|
+
set savedClip to ""
|
|
252
|
+
set hadClip to false
|
|
253
|
+
try
|
|
254
|
+
set savedClip to the clipboard as text
|
|
255
|
+
set hadClip to true
|
|
256
|
+
end try
|
|
257
|
+
set the clipboard to bodyText
|
|
258
|
+
tell application "Mail" to activate
|
|
259
|
+
delay 0.25
|
|
260
|
+
try
|
|
261
|
+
atmFocusMailBody()
|
|
262
|
+
tell application "System Events"
|
|
263
|
+
tell process "Mail"
|
|
264
|
+
keystroke "v" using command down
|
|
265
|
+
end tell
|
|
266
|
+
end tell
|
|
267
|
+
delay 0.15
|
|
268
|
+
on error errMsg number errNum
|
|
269
|
+
if hadClip then set the clipboard to savedClip
|
|
270
|
+
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
271
|
+
error errMsg number errNum
|
|
272
|
+
end try
|
|
273
|
+
if hadClip then set the clipboard to savedClip
|
|
274
|
+
end atmPasteMailBody`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function recipientStillPresentChecks(to) {
|
|
278
|
+
const toCount = asInteger(to.length, { min: 1, max: 99, field: "toCount" });
|
|
279
|
+
const stillThere = to
|
|
280
|
+
.map((address, i) => {
|
|
281
|
+
const n = asInteger(i, { min: 0, max: 99, field: "recipientIndex" });
|
|
282
|
+
return ` set atmWanted${n} to ${asString(address)}
|
|
283
|
+
set atmFound${n} to false
|
|
284
|
+
repeat with r in to recipients of newMessage
|
|
285
|
+
if (address of r as string) is atmWanted${n} then set atmFound${n} to true
|
|
286
|
+
end repeat
|
|
287
|
+
if atmFound${n} is false then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"`;
|
|
288
|
+
})
|
|
289
|
+
.join("\n");
|
|
290
|
+
return ` if (count of to recipients of newMessage) is not ${toCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
291
|
+
${stillThere}`;
|
|
194
292
|
}
|
|
195
293
|
|
|
196
294
|
/**
|
|
197
295
|
* Build the outgoing-message script shared by send and draft.
|
|
198
296
|
*
|
|
199
|
-
* `
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
297
|
+
* Always `make new outgoing message` (no `content:`) so `newMessage` is a
|
|
298
|
+
* real Mail object — Mail's `mailto` command is a no-op / non-returning
|
|
299
|
+
* on Mini+MacBook and left `newMessage` undefined (-2753).
|
|
300
|
+
*
|
|
301
|
+
* Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
|
|
302
|
+
* cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
|
|
303
|
+
* Recipients are AppleScript `make new … recipient`. The body is pasted
|
|
304
|
+
* into the compose window (native editor) via System Events. If paste
|
|
305
|
+
* lands in To, recipient count changes and we abort + delete (nothing sent).
|
|
207
306
|
*/
|
|
208
307
|
export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
|
|
209
308
|
const recipients = [
|
|
@@ -212,19 +311,32 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
|
|
|
212
311
|
recipientLines(bcc, "bcc recipient")
|
|
213
312
|
].filter((block) => block.length > 0).join("\n");
|
|
214
313
|
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
314
|
+
const plainBody = composeNativeBody(body, { html });
|
|
315
|
+
const pasteAndVerify = plainBody
|
|
316
|
+
? ` atmPasteMailBody(${asString(plainBody)})
|
|
317
|
+
tell application "Mail"
|
|
318
|
+
${recipientStillPresentChecks(to)}
|
|
319
|
+
end tell`
|
|
320
|
+
: "";
|
|
219
321
|
|
|
220
|
-
return
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
end try
|
|
322
|
+
return `${buildMailBodyPasteHandler()}
|
|
323
|
+
|
|
324
|
+
tell application "Mail"
|
|
325
|
+
set newMessage to make new outgoing message with properties {subject:${asString(subject)}, visible:true}
|
|
225
326
|
tell newMessage
|
|
226
327
|
${recipients}
|
|
227
328
|
end tell
|
|
329
|
+
activate
|
|
330
|
+
end tell
|
|
331
|
+
try
|
|
332
|
+
${pasteAndVerify}
|
|
333
|
+
on error errMsg number errNum
|
|
334
|
+
try
|
|
335
|
+
tell application "Mail" to delete newMessage
|
|
336
|
+
end try
|
|
337
|
+
error errMsg number errNum
|
|
338
|
+
end try
|
|
339
|
+
tell application "Mail"
|
|
228
340
|
${send ? "send newMessage" : "save newMessage"}
|
|
229
341
|
end tell
|
|
230
342
|
return "OK"`;
|
|
@@ -437,7 +549,20 @@ export function recoverIfInSent({ inReplyTo = null, subject = null, forwardTo =
|
|
|
437
549
|
return null;
|
|
438
550
|
}
|
|
439
551
|
|
|
552
|
+
export function isMailAccessibilityDenial(result) {
|
|
553
|
+
if (!result) return false;
|
|
554
|
+
const text = String(result.error || "").toLowerCase();
|
|
555
|
+
return text.includes("accessibility_denied") || text.includes("assistive access");
|
|
556
|
+
}
|
|
557
|
+
|
|
440
558
|
function failure(action, summary, result, secrets) {
|
|
559
|
+
const err = String((result && result.error) || "").toLowerCase();
|
|
560
|
+
if (err.includes("body_paste_misdirected")) {
|
|
561
|
+
return `${action} failed — attempted to ${summary}. The body was pasted into a header field instead of the message body; nothing was sent.`;
|
|
562
|
+
}
|
|
563
|
+
if (isMailAccessibilityDenial(result)) {
|
|
564
|
+
return `${action} failed — attempted to ${summary}. ${MAIL_ACCESSIBILITY_GUIDANCE}`;
|
|
565
|
+
}
|
|
441
566
|
if (result.kind === "tcc" || isHardTccDenial(result && result.error)) {
|
|
442
567
|
return `${action} failed — attempted to ${summary}. ${MAIL_TCC_GUIDANCE}`;
|
|
443
568
|
}
|
package/lib/writeTools.js
CHANGED
|
@@ -57,7 +57,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
57
57
|
bcc: { type: "array", items: { type: "string" }, description: "BCC email addresses" },
|
|
58
58
|
subject: { type: "string", description: "Subject line (required)" },
|
|
59
59
|
body: { type: "string", description: "Message body (required)" },
|
|
60
|
-
body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped
|
|
60
|
+
body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped and pasted into Mail's compose window; AppleScript content/html content is not set because it quote-wraps the Sent body" },
|
|
61
61
|
...CONFIRM_PROPS
|
|
62
62
|
},
|
|
63
63
|
required: ["to", "subject", "body"]
|
|
@@ -74,7 +74,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
74
74
|
bcc: { type: "array", items: { type: "string" }, description: "BCC email addresses" },
|
|
75
75
|
subject: { type: "string", description: "Subject line" },
|
|
76
76
|
body: { type: "string", description: "Message body" },
|
|
77
|
-
body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped
|
|
77
|
+
body_format: { type: "string", enum: ["plain", "html"], description: "Plain text (default) or HTML. HTML is tag-stripped and pasted into Mail's compose window; AppleScript content/html content is not set because it quote-wraps the Sent body" },
|
|
78
78
|
...CONFIRM_PROPS
|
|
79
79
|
},
|
|
80
80
|
required: ["to"]
|