apple-tools-mcp 2.1.1 → 2.1.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 +7 -5
- package/lib/appleScript.js +10 -6
- package/lib/mailWrite.js +305 -104
- package/lib/permissions.js +9 -2
- package/lib/writeRouting.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ If the invoked CLI path and `process.execPath` differ, the command prints a **WA
|
|
|
104
104
|
|
|
105
105
|
On the host UI (Mini Screen Sharing or MacBook local), open **System Settings → Privacy & Security → Automation**, then run the command. Click **Allow** for **`node`** → Contacts, Calendar, Mail, Messages, and System Events. npm `postinstall` only **prints a reminder** — it does not run the probes unattended.
|
|
106
106
|
|
|
107
|
-
This is a real Apple Events pass: Mail uses `make new outgoing message` (compose then discard; nothing is sent). Messages enumerates accounts (nothing is sent). `dry_run` of `mail_send` / `messages_send` never talks to those apps and **does not count**. Contacts creates and deletes a throwaway person in-script. Calendar lists calendars only (no leftover events). System Events is
|
|
107
|
+
This is a real Apple Events pass: Mail uses `make new outgoing message` (compose then discard; nothing is sent). Messages enumerates accounts (nothing is sent). `dry_run` of `mail_send` / `messages_send` never talks to those apps and **does not count**. Contacts creates and deletes a throwaway person in-script. Calendar lists calendars only (no leftover events). System Events is probed with **process / GUI scripting** (`tell process` + `unix id` of System Events; nothing is typed) so the keystroke compose path can pop **Allow** and verify a real Automation grant. A soft `tell application "System Events" to get name` is **not** enough — that can succeed while TCC still has node → System Events **denied**. If a prior **Don't Allow** left a sticky Deny, `permissions` reports System Events **missing** and exits non-zero: turn **System Events** on for that node in System Settings → Privacy & Security → **Automation**, then **re-run `apple-tools-mcp permissions` after Allow**.
|
|
108
108
|
|
|
109
109
|
**One-pass first-run:** Allow **`node`** to control **Mail**, **Messages**, **Contacts**, **Calendar**, and **System Events** in the same Automation pass. Contacts or Calendar being allowed does **not** grant Mail, Messages, or System Events — they are separate Apple Events targets. A denied Mail grant hangs `mail_send` / `mail_draft` / `mail_reply` / `mail_forward` until the client disconnects; `dry_run` never talks to Mail, so that deny is invisible until a real compose.
|
|
110
110
|
|
|
@@ -308,7 +308,7 @@ Missing `config.json` is fine — env then the 5-minute default apply.
|
|
|
308
308
|
|
|
309
309
|
On Mini, run the **indexer daemon**, not a sleep-pipe wrapper around `apple-tools-mcp`. Claude Desktop and other clients still attach via short-lived stdio MCP (`npx -y apple-tools-mcp` or the global `apple-tools-mcp` bin).
|
|
310
310
|
|
|
311
|
-
The daemon does two jobs: it refreshes the vector index, and it serves the **write bridge** at `~/.apple-tools-mcp/writer.sock` so stdio clients can perform Mail / Messages / Contacts / Calendar writes that their host app cannot be granted (see [step 2b](#2b-grant-automation-for-write-tools-first-run--ship-gate)). When macOS prompts, Allow **`node`** (the LaunchAgent binary) to control Mail.app, Messages.app, Contacts.app, Calendar.app, **and System Events** (mail_send / mail_draft
|
|
311
|
+
The daemon does two jobs: it refreshes the vector index, and it serves the **write bridge** at `~/.apple-tools-mcp/writer.sock` so stdio clients can perform Mail / Messages / Contacts / Calendar writes that their host app cannot be granted (see [step 2b](#2b-grant-automation-for-write-tools-first-run--ship-gate)). When macOS prompts, Allow **`node`** (the LaunchAgent binary) to control Mail.app, Messages.app, Contacts.app, Calendar.app, **and System Events** (mail_send / mail_draft fill the body via AX hit-test). Do not approve the MCP client / host app that launched a short-lived stdio server, and do not try to add `node` via **+** in the Contacts or Calendars privacy lists.
|
|
312
312
|
|
|
313
313
|
**Keep Contacts.app, Mail.app, and Messages.app running all the time** on the Mini (and on any MacBook that does local writes). Quitting them makes Apple Events to those apps unreliable (`-600` / “application isn't running”), which is a cold-launch miss — not a missing Automation grant. The Contacts write path launches Contacts.app before add/edit/remove, but do not rely on that instead of leaving the apps open. **Calendar.app does not need to stay open**; Calendar writes use EventKit.
|
|
314
314
|
|
|
@@ -464,7 +464,7 @@ Two arguments are available on **every** write tool:
|
|
|
464
464
|
|
|
465
465
|
`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.
|
|
466
466
|
|
|
467
|
-
**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
|
|
467
|
+
**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 fill the body into the compose window (subject title-marker + coordinate AX hit-test, then AXValue). 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 (those paths still prepend via `content`; fixing reply/forward cite-wrap is out of scope for 2.1.0).
|
|
468
468
|
|
|
469
469
|
Keystrokes use System Events, so **node needs Accessibility** (Privacy & Security → Accessibility) **and** Automation → Mail **and** Automation → System Events. Run `apple-tools-mcp permissions` after first install or upgrade so **node → System Events** pops **Allow** in the same sitting as Mail / Messages / Contacts / Calendar. An Accessibility / System Events deny is not a Mail Automation deny (`-1743` / `-10004`).
|
|
470
470
|
|
|
@@ -478,9 +478,11 @@ Keystrokes use System Events, so **node needs Accessibility** (Privacy & Securit
|
|
|
478
478
|
|
|
479
479
|
**AX body-focus oracle (2.1.1).** Dual-host Mail 16 / macOS 26 showed that Mail AppleScript `content of` the outgoing message is **not** a live oracle for UI-typed/pasted text: `set content` reads back, but after Tab + keystroke/paste into the UI body `content of` stays empty (`typedSeen=false`, bodyLen 0) even when typing still produces a plain Sent `.emlx`. `@2.1.0` calibrated Tab against that property and fail-closed with `BODY_FOCUS_FAILED`. 2.1.1 Tabs until the focused AX role is a body field (`AXWebArea` / non-header `AXTextArea` via `AXFocusedUIElement` / `first UI element whose focused is true` — never `focused UI element`, never `windows of process Mail`). Cmd-V runs only when that AX **value** is readable and still missing the needle. An empty AX value is not a miss (Mail 16 WebArea); header checks still fail-closed. Ship prove that the body landed remains the Sent `.emlx` after send (2.0.7 To+subject verify). Do not “fix Tab count.”
|
|
480
480
|
|
|
481
|
+
**Compose body hit-test (2.1.3).** Mini host: `AXFocusedUIElement` role coerce fails (**-1700**); `windows of process Mail` is empty; a deep `UI elements` tree walk (the `app_ax_find` class of walk) **truncates after ~23 header/toolbar nodes** and never reaches compose `AXWebArea`. Subject-anchored clipboard read-back is refuted on Mini. The proven path: identify the compose window by the **subject title** among **top-level** System Events UI elements, then **coordinate AX hit-test** (`AXUIElementCopyElementAtPosition`) into the body region. The hit must be `AXWebArea` **"message body"**; a short `AXTextField` is fail-closed (`BODY_FOCUS_FAILED`). Focus + `AXValue` produced a **plain** Sent `.emlx` (cite=0) — no AppleScript `content` setter, no physical mouse click. Typed Returns / Cmd-V run only if a readable AX value is still missing the needle. System Events permissions probe is unchanged from 2.1.2.
|
|
482
|
+
|
|
481
483
|
**mail_send success is Sent/Outbox verify (2.0.7).** AppleScript `send` returning without throw is not enough. After a real send the tool looks in **Sent** and **Outbox** and reports success only if the message is there. The success text names the delivery state (`mailbox: sent` + `delivery: sent`, or `mailbox: outbox` + `delivery: outbox` while still sending). If verify misses, the tool returns a failure (`isError`) — not success. Hang/timeout recover matches **To + subject** (and Message-ID when compose captured one). It never matches subject alone (short subjects like `test` are unsafe). `from` / account selection is out of scope; Mail's default From is used. `dry_run` / confirm-blocked writes stay `ok: false`, `planned: true`, `delivered: false`.
|
|
482
484
|
|
|
483
|
-
**Manual prove (2.1.
|
|
485
|
+
**Manual prove (2.1.3 on the Mini host):** After `apple-tools-mcp permissions` reports System Events granted, `mail_send` a short **multiline** 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. The body must land via title-marker + hit-test (not Tab / focused-role): no `BODY_FOCUS_FAILED` / `BODY_PASTE_MISDIRECTED`. Inspect each Sent `.emlx`: the text/plain part must contain the intended text (newlines preserved) and 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`). Prove the production path (write-bridge with node → System Events allowed, **or** the documented in-process fallback). That quote-prefix Mini Sent prove is a separate bar from the 2.0.7 verify contract.
|
|
484
486
|
|
|
485
487
|
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.
|
|
486
488
|
|
|
@@ -603,7 +605,7 @@ The message names which process macOS was actually asking about. Work through it
|
|
|
603
605
|
|
|
604
606
|
1. **MacBook / Terminal (`permissions` CLI):** there is **no** always-on indexer. Re-run `apple-tools-mcp permissions` from **Terminal.app**, Allow the **printed `process.execPath`**, and check System Settings → Privacy & Security → **Automation** for that node → Contacts, Calendar, Mail, Messages, and System Events. Do **not** start `apple-tools-indexer`.
|
|
605
607
|
2. **Mini only:** `pgrep -fl apple-tools-indexer` / `~/.apple-tools-mcp/writer.sock`. If the LaunchAgent is the product host, start it so writes run under launchd-owned node (see [Mini ship-gate](#mini-ship-gate-host-setup)).
|
|
606
|
-
3. **Did you approve the Automation prompts for `node`?** Re-run `apple-tools-mcp permissions` with the product `node` (it prints `process.execPath`). Open System Settings → Privacy & Security → **Automation** and confirm **that printed node** is allowed to control **Mail**, **Messages**, Contacts, Calendar, and **System Events**. On Mini the LaunchAgent binary is whatever the plist / `which node` reports (`/Users/petercoates/.local/node/bin/node` is a Mini *example* only). MacBook Claude nvm is `/Users/petercoates/.nvm/versions/node/v22.21.1/bin/node`, not Homebrew. Watch the host — Allow **`node`**, not any other app. Contacts or Calendar being allowed does **not** grant Mail. A `mail_send` hang is **Automation denied**, not “Mail.app could not be reached”; `dry_run` never talks to Mail so it cannot detect this. Do **not** approve the MCP client / host app that launched a short-lived stdio server, and do **not** try to add `node` via **+** in the Contacts or Calendars privacy lists — those panes often have no Add button. `tccutil reset AppleEvents` re-arms the Automation prompt so you can Allow **`node`** again. Full Disk Access is a separate read grant; npm Trusted Publisher / tokens are unrelated.
|
|
608
|
+
3. **Did you approve the Automation prompts for `node`?** Re-run `apple-tools-mcp permissions` with the product `node` (it prints `process.execPath`). Open System Settings → Privacy & Security → **Automation** and confirm **that printed node** is allowed to control **Mail**, **Messages**, Contacts, Calendar, and **System Events**. A prior **Don't Allow** for System Events is a sticky Deny — the Allow dialog will not appear again; turn **System Events** on for that node, then re-run `permissions` (it must not report granted from a soft name check). On Mini the LaunchAgent binary is whatever the plist / `which node` reports (`/Users/petercoates/.local/node/bin/node` is a Mini *example* only). MacBook Claude nvm is `/Users/petercoates/.nvm/versions/node/v22.21.1/bin/node`, not Homebrew. Watch the host — Allow **`node`**, not any other app. Contacts or Calendar being allowed does **not** grant Mail. A `mail_send` hang is **Automation denied**, not “Mail.app could not be reached”; `dry_run` never talks to Mail so it cannot detect this. Do **not** approve the MCP client / host app that launched a short-lived stdio server, and do **not** try to add `node` via **+** in the Contacts or Calendars privacy lists — those panes often have no Add button. `tccutil reset AppleEvents` re-arms the Automation prompt so you can Allow **`node`** again. Full Disk Access is a separate read grant; npm Trusted Publisher / tokens are unrelated.
|
|
607
609
|
4. **Mini: is the daemon's node binary the one with Full Disk Access?** LaunchAgents do not inherit your shell `PATH`; confirm the plist points at the same path `which node` reports.
|
|
608
610
|
5. **Is it actually the write path?** Contacts or Calendar *reads* failing with `EPERM` is a Full Disk Access / attribution problem, not the entitlement gap — fix FDA for the responsible process. Contacts or Calendar *CRUD* failing with no prompt under Claude Desktop is the [documented host limitation](#reads-and-writes-are-different-mechanisms): Claude.app carries neither the addressbook nor the calendars entitlement. On Mini, start the daemon so the write goes through the bridge. On MacBook, use Terminal.app + the printed node.
|
|
609
611
|
6. **"Contacts.app / Calendar.app could not be reached"** with the app clearly installed is an **Automation / responsible-process** failure, not a missing app — macOS reports a refused Apple event as `-1728` / "can't get application". On MacBook / Terminal, Allow the printed `process.execPath`. On Mini, the write bridge / LaunchAgent may apply. A Contacts `-600` / “application isn't running” is a **cold launch**, not Automation — keep Contacts, Mail, and Messages running; Calendar does not need to stay open.
|
package/lib/appleScript.js
CHANGED
|
@@ -340,20 +340,24 @@ export const MAIL_TCC_GUIDANCE =
|
|
|
340
340
|
*/
|
|
341
341
|
export const MAIL_ACCESSIBILITY_GUIDANCE =
|
|
342
342
|
"macOS denied Accessibility (System Events could not drive Mail's compose window). " +
|
|
343
|
-
"mail_send / mail_draft
|
|
343
|
+
"mail_send / mail_draft fill the body via AX hit-test so Mail does not cite-wrap it (FB11734014); Mail's mailto command does not return an outgoing message on current Mini/MacBook Mail (-2753). " +
|
|
344
344
|
"Allow node in System Settings → Privacy & Security → Accessibility, and keep Automation → Mail allowed. " +
|
|
345
345
|
"This is not a Mail Automation deny (-1743 / -10004).";
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
* or sent. Mini write-bridge should return unsupported so stdio falls back
|
|
351
|
-
* in-process rather than wedging ~60s.
|
|
348
|
+
* Sticky Don't Allow for node → System Events: the Allow dialog will not
|
|
349
|
+
* reappear. Operators must flip the Automation row and re-run permissions.
|
|
352
350
|
*/
|
|
351
|
+
export const SYSTEM_EVENTS_STICKY_DENY_GUIDANCE =
|
|
352
|
+
"A prior Don't Allow for node → System Events is a sticky Deny: the Allow dialog will not appear again. " +
|
|
353
|
+
"Turn System Events on for this node in System Settings → Privacy & Security → Automation, then re-run apple-tools-mcp permissions after Allow.";
|
|
354
|
+
|
|
355
|
+
/** System Events GUI scripting missing — not a Mail send hang. */
|
|
353
356
|
export const MAIL_GUI_SCRIPTING_GUIDANCE =
|
|
354
357
|
"System Events GUI scripting is unavailable for this process, so the body cannot be typed into Mail. " +
|
|
355
|
-
"mail_send / mail_draft
|
|
358
|
+
"mail_send / mail_draft locate the compose body via System Events (title-marker + AX hit-test) so Mail does not cite-wrap the Sent body (FB11734014). " +
|
|
356
359
|
"Allow node → System Events in System Settings → Privacy & Security → Automation (run apple-tools-mcp permissions after install/upgrade) in addition to node → Mail, and keep Accessibility allowed. " +
|
|
360
|
+
SYSTEM_EVENTS_STICKY_DENY_GUIDANCE + " " +
|
|
357
361
|
"This is not a Mail send timeout and not a Mail Automation deny (-1743 / -10004).";
|
|
358
362
|
|
|
359
363
|
/**
|
package/lib/mailWrite.js
CHANGED
|
@@ -160,11 +160,19 @@ export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
|
|
|
160
160
|
export const BODY_FOCUS_FAILED_SENTINEL = "BODY_FOCUS_FAILED";
|
|
161
161
|
export const GUI_SCRIPTING_UNAVAILABLE_SENTINEL = "GUI_SCRIPTING_UNAVAILABLE";
|
|
162
162
|
export const SYSTEM_EVENTS_PROBE_MARKER = "ATM_SYSTEM_EVENTS_PROBE";
|
|
163
|
-
/**
|
|
163
|
+
/** Permissions-only marker: process / GUI scripting, not application `name`. */
|
|
164
|
+
export const SYSTEM_EVENTS_AUTOMATION_PROBE_MARKER = "ATM_SYSTEM_EVENTS_AUTOMATION_PROBE";
|
|
165
|
+
/** Compose body AX height vs To/Subject/Cc fields (those are ~22px). Used to reject short header AXTextAreas; does not walk windows. */
|
|
164
166
|
export const MAIL_BODY_MIN_AX_HEIGHT = 50;
|
|
165
|
-
/** Mini
|
|
167
|
+
/** Historical Mini Tab count. 2.1.3 finds the body via title-marker + AX hit-test, not Tab. */
|
|
166
168
|
export const MAIL_BODY_TAB_MAX = 6;
|
|
167
|
-
/**
|
|
169
|
+
/** Mail window titles may truncate; match this many leading subject characters. */
|
|
170
|
+
export const MAIL_COMPOSE_TITLE_MARKER_MAX = 60;
|
|
171
|
+
/** Top-level `UI elements` of process Mail only — never a deep tree walk. */
|
|
172
|
+
export const MAIL_COMPOSE_TOP_UI_MAX = 48;
|
|
173
|
+
/** Marker inside the JXA hit-test so tests can pin the proven Mini path. */
|
|
174
|
+
export const ATM_AX_HIT_TEST_MARKER = "ATM_AX_HIT_TEST";
|
|
175
|
+
/** Cheap pre-compose System Events check; SIGKILL if it still hangs. Not the permissions grant. */
|
|
168
176
|
export const MAIL_SE_PROBE_TIMEOUT_MS = 4000;
|
|
169
177
|
/** First-run / upgrade permissions probe: long enough for the Allow click. */
|
|
170
178
|
export const SYSTEM_EVENTS_AUTOMATION_PROBE_TIMEOUT_MS = 30000;
|
|
@@ -246,10 +254,110 @@ export function composeBodyNeedle(body) {
|
|
|
246
254
|
return needle.length > 120 ? needle.slice(0, 120) : needle;
|
|
247
255
|
}
|
|
248
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Subject fragment used to identify the compose window among top-level
|
|
259
|
+
* System Events UI elements. Mail's window title is the subject after
|
|
260
|
+
* `make new outgoing message`; empty drafts show "New Message".
|
|
261
|
+
*/
|
|
262
|
+
export function composeTitleMarker(subject) {
|
|
263
|
+
const text = subject === undefined || subject === null ? "" : String(subject).trim();
|
|
264
|
+
if (!text) return "New Message";
|
|
265
|
+
return text.length > MAIL_COMPOSE_TITLE_MARKER_MAX
|
|
266
|
+
? text.slice(0, MAIL_COMPOSE_TITLE_MARKER_MAX)
|
|
267
|
+
: text;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* JXA AX hit-test: `AXUIElementCopyElementAtPosition` at compose-body
|
|
272
|
+
* coordinates, then AXFocused + AXValue on the `AXWebArea` "message body".
|
|
273
|
+
* Mini-proven (computer-use): deep UI-element walks truncate before the
|
|
274
|
+
* WebArea; a coordinate hit-test does not. No mouse click.
|
|
275
|
+
*/
|
|
276
|
+
export function buildAtmAxHitTestJxa() {
|
|
277
|
+
return `ObjC.import("ApplicationServices");
|
|
278
|
+
ObjC.import("Foundation");
|
|
279
|
+
function axStr(el, attr) {
|
|
280
|
+
var out = Ref();
|
|
281
|
+
if ($.AXUIElementCopyAttributeValue(el, attr, out) !== 0) return "";
|
|
282
|
+
if (out[0] == null) return "";
|
|
283
|
+
try { return ObjC.unwrap(out[0]).toString(); } catch (e) { return ""; }
|
|
284
|
+
}
|
|
285
|
+
function axHeight(el) {
|
|
286
|
+
var out = Ref();
|
|
287
|
+
if ($.AXUIElementCopyAttributeValue(el, "AXSize", out) !== 0 || out[0] == null) return 0;
|
|
288
|
+
try {
|
|
289
|
+
var u = ObjC.unwrap(out[0]);
|
|
290
|
+
if (u && typeof u.height === "number") return u.height;
|
|
291
|
+
if (u && u.Height != null) return Number(u.Height);
|
|
292
|
+
if (u && u[1] != null) return Number(u[1]);
|
|
293
|
+
} catch (e) {}
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
function axBits(el) {
|
|
297
|
+
return [axStr(el, "AXDescription"), axStr(el, "AXTitle"), axStr(el, "AXRoleDescription")].join(" ");
|
|
298
|
+
}
|
|
299
|
+
function isHeaderHit(role, bits, height, minH) {
|
|
300
|
+
if (role === "AXTextField" || role === "AXComboBox" || role === "AXButton" || role === "AXMenuButton" || role === "AXPopUpButton" || role === "AXWindow") return true;
|
|
301
|
+
if (role === "AXTextArea" && height > 0 && height < minH) return true;
|
|
302
|
+
var lower = bits.toLowerCase();
|
|
303
|
+
if (lower.indexOf("to:") !== -1) return true;
|
|
304
|
+
if (lower.indexOf("cc:") !== -1) return true;
|
|
305
|
+
if (lower.indexOf("bcc:") !== -1) return true;
|
|
306
|
+
if (lower.indexOf("subject") !== -1 && lower.indexOf("message body") === -1) return true;
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
function isMailBodyHit(role, bits) {
|
|
310
|
+
if (role !== "AXWebArea") return false;
|
|
311
|
+
return bits.toLowerCase().indexOf("message body") !== -1;
|
|
312
|
+
}
|
|
313
|
+
function run(argv) {
|
|
314
|
+
// ${ATM_AX_HIT_TEST_MARKER}
|
|
315
|
+
var pid = parseInt(argv[0], 10);
|
|
316
|
+
var body = String(argv[1] || "");
|
|
317
|
+
var needle = String(argv[2] || "");
|
|
318
|
+
var pointStr = String(argv[3] || "");
|
|
319
|
+
var minH = parseInt(argv[4], 10);
|
|
320
|
+
if (!minH) minH = 50;
|
|
321
|
+
if (!pid) return "BODY_FOCUS_FAILED";
|
|
322
|
+
var app = $.AXUIElementCreateApplication(pid);
|
|
323
|
+
var points = pointStr.split(";");
|
|
324
|
+
var chosen = null;
|
|
325
|
+
for (var i = 0; i < points.length; i++) {
|
|
326
|
+
var xy = points[i].split(",");
|
|
327
|
+
var x = parseFloat(xy[0]);
|
|
328
|
+
var y = parseFloat(xy[1]);
|
|
329
|
+
if (!isFinite(x) || !isFinite(y)) continue;
|
|
330
|
+
var hit = Ref();
|
|
331
|
+
var err = $.AXUIElementCopyElementAtPosition(app, x, y, hit);
|
|
332
|
+
if (err !== 0 || hit[0] == null) continue;
|
|
333
|
+
var el = hit[0];
|
|
334
|
+
var role = axStr(el, "AXRole");
|
|
335
|
+
var bits = axBits(el);
|
|
336
|
+
var height = axHeight(el);
|
|
337
|
+
if (isHeaderHit(role, bits, height, minH)) continue;
|
|
338
|
+
if (isMailBodyHit(role, bits)) {
|
|
339
|
+
chosen = el;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (!chosen) return "BODY_FOCUS_FAILED";
|
|
344
|
+
try { $.AXUIElementSetAttributeValue(chosen, "AXFocused", true); } catch (e) {}
|
|
345
|
+
try { $.AXUIElementSetAttributeValue(chosen, "AXValue", body); } catch (e) {}
|
|
346
|
+
var val = axStr(chosen, "AXValue");
|
|
347
|
+
if (val !== "" && needle !== "" && val.indexOf(needle) === -1) return "VALUE_MISS";
|
|
348
|
+
return "OK";
|
|
349
|
+
}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
249
352
|
export function isSystemEventsProbeScript(script) {
|
|
250
353
|
return String(script || "").includes(SYSTEM_EVENTS_PROBE_MARKER);
|
|
251
354
|
}
|
|
252
355
|
|
|
356
|
+
export function isSystemEventsAutomationProbeScript(script) {
|
|
357
|
+
return String(script || "").includes(SYSTEM_EVENTS_AUTOMATION_PROBE_MARKER);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Cheap pre-compose check. Do not use for `permissions` — `get name` can soft-pass. */
|
|
253
361
|
export function buildSystemEventsProbeScript() {
|
|
254
362
|
const secs = asInteger(MAIL_SE_PROBE_APPLEEVENT_TIMEOUT_SEC, {
|
|
255
363
|
min: 1,
|
|
@@ -275,15 +383,41 @@ export function probeSystemEventsGui({ timeout = MAIL_SE_PROBE_TIMEOUT_MS } = {}
|
|
|
275
383
|
}
|
|
276
384
|
|
|
277
385
|
/**
|
|
278
|
-
* First-run / upgrade probe
|
|
279
|
-
*
|
|
280
|
-
*
|
|
386
|
+
* First-run / upgrade probe. Soft `tell application "System Events" to get
|
|
387
|
+
* name` is not enough — Mini @2.1.1 reported granted while TCC auth_value=0
|
|
388
|
+
* for node → com.apple.systemevents. Process / GUI scripting (`tell process`)
|
|
389
|
+
* is the grant mail_send actually needs. Does not type into the focused UI.
|
|
390
|
+
* Timeout is long enough for the operator to click Allow. No short inner
|
|
391
|
+
* AppleEvent timeout (same as the Mail compose-and-discard probe).
|
|
392
|
+
*/
|
|
393
|
+
export function buildSystemEventsAutomationProbeScript() {
|
|
394
|
+
return `-- ${SYSTEM_EVENTS_AUTOMATION_PROBE_MARKER}
|
|
395
|
+
tell application "System Events"
|
|
396
|
+
tell process "System Events"
|
|
397
|
+
set atmPid to unix id
|
|
398
|
+
set atmShown to name
|
|
399
|
+
if atmShown is "" then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
|
|
400
|
+
if atmPid is missing value then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
|
|
401
|
+
if (atmPid as integer) is less than or equal to 0 then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
|
|
402
|
+
count of UI elements
|
|
403
|
+
end tell
|
|
404
|
+
set atmUi to UI elements enabled
|
|
405
|
+
end tell
|
|
406
|
+
if atmUi is false then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
407
|
+
return "OK"`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* First-run / upgrade probe: a live System Events GUI-scripting Apple Event
|
|
412
|
+
* that creates or verifies Automation → System Events. Does not type.
|
|
281
413
|
* @returns {{ ok: boolean, message: string, kind: string|null }}
|
|
282
414
|
*/
|
|
283
415
|
export function probeSystemEventsAutomation() {
|
|
284
416
|
const action = "system_events_automation_probe";
|
|
285
|
-
const summary = "
|
|
286
|
-
const result =
|
|
417
|
+
const summary = "verify System Events process / GUI scripting (Automation / Apple Events grant; nothing is typed)";
|
|
418
|
+
const result = runAppleScript(buildSystemEventsAutomationProbeScript(), {
|
|
419
|
+
timeout: SYSTEM_EVENTS_AUTOMATION_PROBE_TIMEOUT_MS
|
|
420
|
+
});
|
|
287
421
|
if (!result.ok) {
|
|
288
422
|
const kind = systemEventsGrantKind(result);
|
|
289
423
|
const guidance = isMailAccessibilityDenial(result)
|
|
@@ -300,7 +434,7 @@ export function probeSystemEventsAutomation() {
|
|
|
300
434
|
kind: null,
|
|
301
435
|
message: writeSuccessMessage(
|
|
302
436
|
action,
|
|
303
|
-
"System Events Automation allowed (process
|
|
437
|
+
"System Events Automation allowed (process GUI scripting verified; nothing was typed)"
|
|
304
438
|
)
|
|
305
439
|
};
|
|
306
440
|
}
|
|
@@ -317,66 +451,28 @@ function systemEventsGrantKind(result) {
|
|
|
317
451
|
}
|
|
318
452
|
|
|
319
453
|
/**
|
|
320
|
-
* AppleScript handlers:
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* `content of` (
|
|
327
|
-
*
|
|
454
|
+
* AppleScript handlers: find the compose window by subject title among
|
|
455
|
+
* top-level System Events UI elements (macOS 26 `windows of process Mail`
|
|
456
|
+
* is 0), then coordinate AX hit-test (`AXUIElementCopyElementAtPosition`)
|
|
457
|
+
* to the `AXWebArea` "message body". Mini computer-use: deep UI-element
|
|
458
|
+
* walks truncate after ~23 header/toolbar nodes and never reach the body;
|
|
459
|
+
* AXFocusedUIElement role coerce fails (-1700). No mouse click. No
|
|
460
|
+
* `entire contents`. Never live Mail `content of` (Mail 16 stays empty
|
|
461
|
+
* after UI fill). Never `set content` / `html content` (FB11734014).
|
|
328
462
|
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
* events are wrapped in
|
|
463
|
+
* Hit must be AXWebArea "message body"; a short AXTextField is fail-closed
|
|
464
|
+
* (BODY_FOCUS_FAILED). Focus + AXValue (Mini: plain Sent .emlx, cite=0).
|
|
465
|
+
* Typed Returns / Cmd-V only if a readable AX value is still missing the
|
|
466
|
+
* needle. Header checks stay. System Events Apple events are wrapped in
|
|
467
|
+
* `with timeout`.
|
|
333
468
|
*/
|
|
334
469
|
export function buildMailBodyPasteHandler() {
|
|
335
|
-
const tabMax = asInteger(MAIL_BODY_TAB_MAX, { min: 1, max: 12, field: "tabMax" });
|
|
336
470
|
const seSecs = asInteger(MAIL_SE_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 30, field: "seTimeout" });
|
|
337
471
|
const bodyMinH = asInteger(MAIL_BODY_MIN_AX_HEIGHT, { min: 1, max: 500, field: "bodyMinHeight" });
|
|
472
|
+
const topMax = asInteger(MAIL_COMPOSE_TOP_UI_MAX, { min: 1, max: 64, field: "topUiMax" });
|
|
473
|
+
const jxaLit = asString(buildAtmAxHitTestJxa());
|
|
338
474
|
return `${buildAtmFocusedElementHandler()}
|
|
339
475
|
|
|
340
|
-
on atmSafeToPaste()
|
|
341
|
-
tell application "System Events"
|
|
342
|
-
tell process "Mail"
|
|
343
|
-
try
|
|
344
|
-
set fe to my atmFocusedElement()
|
|
345
|
-
if fe is missing value then return false
|
|
346
|
-
set r to (role of fe) as string
|
|
347
|
-
if r is "AXWindow" then return false
|
|
348
|
-
if r is "AXTextField" then return false
|
|
349
|
-
if r is "AXComboBox" then return false
|
|
350
|
-
if r is "AXButton" then return false
|
|
351
|
-
if r is "AXMenuButton" then return false
|
|
352
|
-
if r is "AXPopUpButton" then return false
|
|
353
|
-
set atmH to 0
|
|
354
|
-
try
|
|
355
|
-
set {atmW, atmH} to size of fe
|
|
356
|
-
end try
|
|
357
|
-
if r is "AXTextArea" and atmH > 0 and atmH < ${bodyMinH} then return false
|
|
358
|
-
ignoring case
|
|
359
|
-
set bits to ""
|
|
360
|
-
try
|
|
361
|
-
set bits to bits & (description of fe as string) & " "
|
|
362
|
-
end try
|
|
363
|
-
try
|
|
364
|
-
set bits to bits & (name of fe as string) & " "
|
|
365
|
-
end try
|
|
366
|
-
if bits contains "To:" then return false
|
|
367
|
-
if bits contains "Cc:" then return false
|
|
368
|
-
if bits contains "Bcc:" then return false
|
|
369
|
-
if bits contains "Subject" then return false
|
|
370
|
-
end ignoring
|
|
371
|
-
if r is "AXWebArea" then return true
|
|
372
|
-
if r is "AXTextArea" then return true
|
|
373
|
-
if atmH >= ${bodyMinH} then return true
|
|
374
|
-
end try
|
|
375
|
-
return false
|
|
376
|
-
end tell
|
|
377
|
-
end tell
|
|
378
|
-
end atmSafeToPaste
|
|
379
|
-
|
|
380
476
|
on atmAxFocusedValue()
|
|
381
477
|
tell application "System Events"
|
|
382
478
|
try
|
|
@@ -388,6 +484,101 @@ on atmAxFocusedValue()
|
|
|
388
484
|
return ""
|
|
389
485
|
end atmAxFocusedValue
|
|
390
486
|
|
|
487
|
+
on atmTopElemBits(atmEl)
|
|
488
|
+
set atmBits to ""
|
|
489
|
+
try
|
|
490
|
+
set atmBits to atmBits & (name of atmEl as string) & " "
|
|
491
|
+
end try
|
|
492
|
+
try
|
|
493
|
+
set atmBits to atmBits & (title of atmEl as string) & " "
|
|
494
|
+
end try
|
|
495
|
+
try
|
|
496
|
+
set atmBits to atmBits & (description of atmEl as string) & " "
|
|
497
|
+
end try
|
|
498
|
+
try
|
|
499
|
+
set atmBits to atmBits & (value of attribute "AXTitle" of atmEl as string) & " "
|
|
500
|
+
end try
|
|
501
|
+
return atmBits
|
|
502
|
+
end atmTopElemBits
|
|
503
|
+
|
|
504
|
+
on atmTopElemExactTitle(atmEl, atmMarker)
|
|
505
|
+
try
|
|
506
|
+
if (name of atmEl as string) is atmMarker then return true
|
|
507
|
+
end try
|
|
508
|
+
try
|
|
509
|
+
if (title of atmEl as string) is atmMarker then return true
|
|
510
|
+
end try
|
|
511
|
+
try
|
|
512
|
+
if (value of attribute "AXTitle" of atmEl as string) is atmMarker then return true
|
|
513
|
+
end try
|
|
514
|
+
return false
|
|
515
|
+
end atmTopElemExactTitle
|
|
516
|
+
|
|
517
|
+
on atmComposeFrameByTitle(atmMarker)
|
|
518
|
+
with timeout of ${seSecs} seconds
|
|
519
|
+
tell application "System Events"
|
|
520
|
+
tell process "Mail"
|
|
521
|
+
set atmElems to UI elements
|
|
522
|
+
set atmCount to count of atmElems
|
|
523
|
+
if atmCount > ${topMax} then set atmCount to ${topMax}
|
|
524
|
+
repeat with atmIdx from 1 to atmCount
|
|
525
|
+
set atmEl to item atmIdx of atmElems
|
|
526
|
+
set atmRole to ""
|
|
527
|
+
try
|
|
528
|
+
set atmRole to (role of atmEl as string)
|
|
529
|
+
end try
|
|
530
|
+
if atmRole is not "AXMenuBar" then
|
|
531
|
+
if my atmTopElemExactTitle(atmEl, atmMarker) then
|
|
532
|
+
set atmPos to position of atmEl
|
|
533
|
+
set atmSz to size of atmEl
|
|
534
|
+
return {item 1 of atmPos, item 2 of atmPos, item 1 of atmSz, item 2 of atmSz}
|
|
535
|
+
end if
|
|
536
|
+
end if
|
|
537
|
+
end repeat
|
|
538
|
+
repeat with atmIdx from 1 to atmCount
|
|
539
|
+
set atmEl to item atmIdx of atmElems
|
|
540
|
+
set atmRole to ""
|
|
541
|
+
try
|
|
542
|
+
set atmRole to (role of atmEl as string)
|
|
543
|
+
end try
|
|
544
|
+
if atmRole is not "AXMenuBar" then
|
|
545
|
+
set atmBits to my atmTopElemBits(atmEl)
|
|
546
|
+
ignoring case
|
|
547
|
+
if atmBits contains atmMarker then
|
|
548
|
+
set atmPos to position of atmEl
|
|
549
|
+
set atmSz to size of atmEl
|
|
550
|
+
return {item 1 of atmPos, item 2 of atmPos, item 1 of atmSz, item 2 of atmSz}
|
|
551
|
+
end if
|
|
552
|
+
end ignoring
|
|
553
|
+
end if
|
|
554
|
+
end repeat
|
|
555
|
+
end tell
|
|
556
|
+
end tell
|
|
557
|
+
end timeout
|
|
558
|
+
error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
559
|
+
end atmComposeFrameByTitle
|
|
560
|
+
|
|
561
|
+
on atmBodyHitPointList(atmFrameX, atmFrameY, atmFrameW, atmFrameH)
|
|
562
|
+
set atmMidX to (atmFrameX + (atmFrameW / 2)) as integer
|
|
563
|
+
set atmHitY1 to (atmFrameY + ((atmFrameH * 2) / 3)) as integer
|
|
564
|
+
set atmHitY2 to (atmFrameY + ((atmFrameH * 3) / 4)) as integer
|
|
565
|
+
set atmHitX2 to (atmFrameX + ((atmFrameW * 2) / 5)) as integer
|
|
566
|
+
set atmHitY3 to (atmFrameY + ((atmFrameH * 7) / 10)) as integer
|
|
567
|
+
return (atmMidX as string) & "," & (atmHitY1 as string) & ";" & (atmMidX as string) & "," & (atmHitY2 as string) & ";" & (atmHitX2 as string) & "," & (atmHitY3 as string)
|
|
568
|
+
end atmBodyHitPointList
|
|
569
|
+
|
|
570
|
+
on atmAxHitFill(atmPid, atmBody, atmNeedle, atmPoints)
|
|
571
|
+
set atmJxa to ${jxaLit}
|
|
572
|
+
set atmOut to ""
|
|
573
|
+
try
|
|
574
|
+
set atmOut to (run script atmJxa in "JavaScript" with parameters {atmPid, atmBody, atmNeedle, atmPoints, ${bodyMinH}}) as string
|
|
575
|
+
on error errMsg number errNum
|
|
576
|
+
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
577
|
+
error errMsg number errNum
|
|
578
|
+
end try
|
|
579
|
+
return atmOut
|
|
580
|
+
end atmAxHitFill
|
|
581
|
+
|
|
391
582
|
on atmSplitParagraphs(bodyText)
|
|
392
583
|
set atmSavedDelim to AppleScript's text item delimiters
|
|
393
584
|
set AppleScript's text item delimiters to return
|
|
@@ -426,30 +617,6 @@ on atmKeystrokeText(atmText)
|
|
|
426
617
|
end tell
|
|
427
618
|
end atmKeystrokeText
|
|
428
619
|
|
|
429
|
-
on atmTabIntoMailBody(msg)
|
|
430
|
-
tell application "Mail" to activate
|
|
431
|
-
delay 0.25
|
|
432
|
-
with timeout of ${seSecs} seconds
|
|
433
|
-
tell application "System Events"
|
|
434
|
-
tell process "Mail" to set frontmost to true
|
|
435
|
-
end tell
|
|
436
|
-
end timeout
|
|
437
|
-
delay 0.08
|
|
438
|
-
repeat with atmN from 0 to ${tabMax}
|
|
439
|
-
if my atmSafeToPaste() then return
|
|
440
|
-
if atmN is ${tabMax} then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
441
|
-
with timeout of ${seSecs} seconds
|
|
442
|
-
tell application "System Events"
|
|
443
|
-
tell process "Mail"
|
|
444
|
-
key code 48
|
|
445
|
-
end tell
|
|
446
|
-
end tell
|
|
447
|
-
end timeout
|
|
448
|
-
delay 0.08
|
|
449
|
-
end repeat
|
|
450
|
-
error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
451
|
-
end atmTabIntoMailBody
|
|
452
|
-
|
|
453
620
|
on atmTypeMailBody(bodyText)
|
|
454
621
|
set atmLines to my atmSplitParagraphs(bodyText)
|
|
455
622
|
set atmLast to count of atmLines
|
|
@@ -494,17 +661,50 @@ on atmPasteMailBodyFallback(bodyText)
|
|
|
494
661
|
if hadClip then set the clipboard to savedClip
|
|
495
662
|
end atmPasteMailBodyFallback
|
|
496
663
|
|
|
497
|
-
on atmFillMailBody(bodyText, needle, msg)
|
|
664
|
+
on atmFillMailBody(bodyText, needle, titleMarker, msg)
|
|
498
665
|
try
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
666
|
+
tell application "Mail" to activate
|
|
667
|
+
delay 0.25
|
|
668
|
+
with timeout of ${seSecs} seconds
|
|
669
|
+
tell application "System Events"
|
|
670
|
+
tell process "Mail" to set frontmost to true
|
|
671
|
+
end tell
|
|
672
|
+
end timeout
|
|
673
|
+
delay 0.08
|
|
674
|
+
set atmPid to 0
|
|
675
|
+
with timeout of ${seSecs} seconds
|
|
676
|
+
tell application "System Events"
|
|
677
|
+
tell process "Mail"
|
|
678
|
+
set atmPid to unix id
|
|
679
|
+
end tell
|
|
680
|
+
end tell
|
|
681
|
+
end timeout
|
|
682
|
+
if atmPid is 0 then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
683
|
+
set atmFrame to my atmComposeFrameByTitle(titleMarker)
|
|
684
|
+
set atmFrameX to item 1 of atmFrame
|
|
685
|
+
set atmFrameY to item 2 of atmFrame
|
|
686
|
+
set atmFrameW to item 3 of atmFrame
|
|
687
|
+
set atmFrameH to item 4 of atmFrame
|
|
688
|
+
if atmFrameW < 40 or atmFrameH < ${bodyMinH} then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
689
|
+
set atmPoints to my atmBodyHitPointList(atmFrameX, atmFrameY, atmFrameW, atmFrameH)
|
|
690
|
+
set atmHit to my atmAxHitFill(atmPid, bodyText, needle, atmPoints)
|
|
691
|
+
if atmHit is "${BODY_FOCUS_FAILED_SENTINEL}" then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
692
|
+
if atmHit is "VALUE_MISS" then
|
|
693
|
+
with timeout of ${seSecs} seconds
|
|
694
|
+
tell application "System Events"
|
|
695
|
+
tell process "Mail"
|
|
696
|
+
keystroke "a" using command down
|
|
697
|
+
end tell
|
|
698
|
+
end tell
|
|
699
|
+
end timeout
|
|
700
|
+
atmTypeMailBody(bodyText)
|
|
701
|
+
delay 0.12
|
|
702
|
+
if needle is not "" then
|
|
703
|
+
set atmAxVal to my atmAxFocusedValue()
|
|
704
|
+
if atmAxVal is not "" then
|
|
705
|
+
if atmAxVal does not contain needle then
|
|
706
|
+
atmPasteMailBodyFallback(bodyText)
|
|
707
|
+
end if
|
|
508
708
|
end if
|
|
509
709
|
end if
|
|
510
710
|
end if
|
|
@@ -558,13 +758,13 @@ ${bodyCheck}`;
|
|
|
558
758
|
* Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
|
|
559
759
|
* cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
|
|
560
760
|
* Recipients and subject are AppleScript properties (never paste). The body
|
|
561
|
-
* is
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
761
|
+
* is filled via title-marker compose window + coordinate AX hit-test to the
|
|
762
|
+
* AXWebArea "message body" (AXFocused + AXValue; Mini-proven plain Sent).
|
|
763
|
+
* Do not walk `windows of process Mail`. Do not deep-walk UI elements.
|
|
764
|
+
* Do not use Mail `content of` as the oracle that text landed (Mail 16 /
|
|
765
|
+
* macOS 26: that property stays empty). If the body element is not found or
|
|
766
|
+
* is a short AXTextField, or headers change, abort + delete (nothing sent).
|
|
767
|
+
* Ship prove that the body landed is Sent `.emlx` after send.
|
|
568
768
|
*/
|
|
569
769
|
export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
|
|
570
770
|
const recipients = [
|
|
@@ -576,8 +776,9 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
|
|
|
576
776
|
const mailSecs = asInteger(MAIL_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 60, field: "mailTimeout" });
|
|
577
777
|
const plainBody = composeNativeBody(body, { html });
|
|
578
778
|
const needle = composeBodyNeedle(plainBody);
|
|
779
|
+
const titleMarker = composeTitleMarker(subject);
|
|
579
780
|
const pasteAndVerify = plainBody
|
|
580
|
-
? ` atmFillMailBody(${asString(plainBody)}, ${asString(needle)}, newMessage)
|
|
781
|
+
? ` atmFillMailBody(${asString(plainBody)}, ${asString(needle)}, ${asString(titleMarker)}, newMessage)
|
|
581
782
|
tell application "Mail"
|
|
582
783
|
${composeMisdirectChecks({ to, cc, bcc, subject, body: plainBody })}
|
|
583
784
|
end tell`
|
package/lib/permissions.js
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
* points at a different node, print a WARN — Allows attach to execPath.
|
|
11
11
|
* Mail and Messages use the existing live Apple Events helpers — dry_run of
|
|
12
12
|
* mail_send / messages_send never talks to those apps and does not count.
|
|
13
|
+
* System Events uses process / GUI scripting (`tell process`), not a soft
|
|
14
|
+
* application `get name` — that can succeed without AppleEvents auth.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import fs from "fs";
|
|
16
18
|
import path from "path";
|
|
17
|
-
import { classifyAppleScriptError } from "./appleScript.js";
|
|
19
|
+
import { classifyAppleScriptError, SYSTEM_EVENTS_STICKY_DENY_GUIDANCE } from "./appleScript.js";
|
|
18
20
|
import { probeMailAutomation, probeSystemEventsAutomation } from "./mailWrite.js";
|
|
19
21
|
import { probeMessagesAutomation } from "./messagesWrite.js";
|
|
20
22
|
import { probeContactsAutomation } from "./contactsWrite.js";
|
|
@@ -272,7 +274,9 @@ function bannerLines(binary, version) {
|
|
|
272
274
|
"Nothing is sent to third parties. Mail composes and discards a temporary outgoing message.",
|
|
273
275
|
"Messages only enumerates accounts. Contacts creates and deletes a throwaway person in-script.",
|
|
274
276
|
"Calendar lists calendars only — no events are created.",
|
|
275
|
-
"System Events is
|
|
277
|
+
"System Events is probed with process / GUI scripting (tell process + unix id; nothing is typed).",
|
|
278
|
+
"A soft tell System Events to get name is not enough — that can succeed without AppleEvents auth.",
|
|
279
|
+
"A prior Don't Allow is a sticky Deny: turn System Events on for this node in Automation, then re-run this command."
|
|
276
280
|
];
|
|
277
281
|
}
|
|
278
282
|
|
|
@@ -359,6 +363,9 @@ export async function runPermissionsCommand({
|
|
|
359
363
|
} else {
|
|
360
364
|
log("Result: INCOMPLETE — one or more required grants are missing or errored.");
|
|
361
365
|
log(hostAdvice);
|
|
366
|
+
if (grants["System Events"] !== GRANT_GRANTED) {
|
|
367
|
+
log(SYSTEM_EVENTS_STICKY_DENY_GUIDANCE);
|
|
368
|
+
}
|
|
362
369
|
log("Click Allow for the missing surfaces and re-run this command. Exit is non-zero (fail closed).");
|
|
363
370
|
}
|
|
364
371
|
return code;
|
package/lib/writeRouting.js
CHANGED
|
@@ -102,7 +102,7 @@ export function terminalAutomationAdvice(execPath = process.execPath) {
|
|
|
102
102
|
export function miniLaunchAgentAdvice() {
|
|
103
103
|
return (
|
|
104
104
|
"This Mac has a Mini write-bridge / LaunchAgent. Grant the daemon's node Full Disk Access (reads) and " +
|
|
105
|
-
"Allow that node in System Settings → Privacy & Security → Automation for Mail.app, Messages.app, Contacts.app, Calendar.app, and System Events (mail_send / mail_draft
|
|
105
|
+
"Allow that node in System Settings → Privacy & Security → Automation for Mail.app, Messages.app, Contacts.app, Calendar.app, and System Events (mail_send / mail_draft fill the body via AX hit-test). " +
|
|
106
106
|
"Do not add node via + in the Contacts or Calendars privacy lists."
|
|
107
107
|
);
|
|
108
108
|
}
|