apple-tools-mcp 2.0.10 → 2.1.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 +11 -9
- package/lib/appleScript.js +15 -2
- package/lib/mailWrite.js +243 -232
- package/lib/shell.js +3 -0
- package/lib/writeRouting.js +2 -2
- package/lib/writeTools.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ This is the first-run flow for Mail, Messages, Contacts, and Calendar **writes**
|
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
That is the ship-gate context. An embedded agent shell, IDE terminal, or MCP host app subprocess is **not** the ship-gate host unless the write bridge is up and the work executes inside launchd-owned `node`.
|
|
125
|
-
3. When prompts appear, click **Allow** for **`node`** to control **Mail**, **Messages**, **Contacts**, and **
|
|
125
|
+
3. When prompts appear, click **Allow** for **`node`** to control **Mail**, **Messages**, **Contacts**, **Calendar**, and **System Events** (needed for `mail_send` / `mail_draft` keystrokes under the LaunchAgent). Watch the host — Allow **`node`** (the LaunchAgent binary), not any other app. Use whatever path the LaunchAgent plist / `which node` reports — `/Users/petercoates/.local/node/bin/node` is a Mini *example* only, not a universal path. Do **not** approve the MCP client / host app that launched a short-lived stdio server.
|
|
126
126
|
4. **Keep Contacts, Mail, and Messages running** on the write host (leave the apps open; do not quit them). Cold `tell application "Contacts"` under launchd often fails with `-600` / “application isn't running” instead of auto-launching — that is **not** an Automation deny. The write path launches Contacts.app before CRUD, but write reliability still requires those three apps to stay running. **Calendar does not need to stay open** (Calendar writes go through EventKit).
|
|
127
127
|
5. **Full Disk Access** on `node` is a separate grant and covers **reads** (Mail / Messages / Calendar / AddressBook databases). Write tools need the Automation / Apple Events grants to Mail.app (`mail_send`, `mail_draft`, `mail_reply`, `mail_forward`, and the other mail writes), Messages.app (`messages_send`), Contacts.app, and Calendar.app.
|
|
128
128
|
6. npm Trusted Publisher / publish tokens are unrelated to TCC. Do not confuse them with this setup.
|
|
@@ -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,
|
|
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 type the body via keystrokes). 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,19 +464,21 @@ 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 type the body into the compose window (System Events keystrokes). 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. On Mini, the LaunchAgent's node also needs **Automation → System Events** if writes run through the write bridge. An Accessibility / System Events deny is not a Mail Automation deny (`-1743` / `-10004`).
|
|
470
470
|
|
|
471
|
-
**Compose body focus (2.0.8).** After `make new outgoing message`, To and Subject are AppleScript properties (never paste). Cmd-V
|
|
471
|
+
**Compose body focus (2.0.8).** After `make new outgoing message`, To and Subject are AppleScript properties (never paste). Cmd-V used to run only after an AX window walker left the header. Quote-prefix Sent prove and Sent/Outbox verify stay gated on fail-closed body/header checks — a MacBook 2.0.7 live `mail_send` with Accessibility allowed still hit `BODY_PASTE_MISDIRECTED` when Cmd-V landed in To/Subject (`ATP-207-SENT-PROVE-20260921-084048`).
|
|
472
472
|
|
|
473
|
-
**macOS 26 focus compile (2.0.9).** Dual-host Sent prove on `@2.0.8` never reached paste: `atmSafeToPaste` used bare `focused UI element`, which System Events 26.x rejects at compile.
|
|
473
|
+
**macOS 26 focus compile (2.0.9).** Dual-host Sent prove on `@2.0.8` never reached paste: `atmSafeToPaste` used bare `focused UI element`, which System Events 26.x rejects at compile.
|
|
474
474
|
|
|
475
|
-
**AppleScript reserved identifiers (2.0.10).** Dual-host Sent prove on `@2.0.9` compiled past the focused-UI fix, then failed in `atmFocusMailBody`: `set {tw, th} to size of atmElem` → **-2741** Expected expression but found `th` (AppleScript reserved ordinal).
|
|
475
|
+
**AppleScript reserved identifiers (2.0.10).** Dual-host Sent prove on `@2.0.9` compiled past the focused-UI fix, then failed in `atmFocusMailBody`: `set {tw, th} to size of atmElem` → **-2741** Expected expression but found `th` (AppleScript reserved ordinal).
|
|
476
476
|
|
|
477
|
-
**
|
|
477
|
+
**Keystroke compose (2.1.0).** macOS 26 System Events reports `windows of process Mail = 0`, so the AX window walker cannot focus a tall `AXWebArea`. Fresh compose now **Tabs into the body** (runtime calibration against Mail `content of` the outgoing message, max Tab×6 as proven on Mini) and **types the body with Return between paragraphs**. Clipboard Cmd-V is only a fallback if the body needle is still missing. Fail-closed: missing needle or changed To/Cc/Bcc/subject is `BODY_FOCUS_FAILED` / `BODY_PASTE_MISDIRECTED`; the outgoing message is deleted and nothing is sent. Apple events are wrapped in `with timeout`; hung `osascript` is **SIGKILL**'d so a wedge fails fast instead of sitting ~60s. If the Mini write-bridge / indexer daemon cannot drive System Events, `mail_send` returns `unsupported: true` so the stdio client falls back **in-process** — it does **not** hang with `MAIL_SEND_TIMEOUT_GUIDANCE` for a message that never sent.
|
|
478
478
|
|
|
479
|
-
**
|
|
479
|
+
**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`.
|
|
480
|
+
|
|
481
|
+
**Manual prove (2.1.0 on the Mac host):** `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 caret must land in the **body** (not To/Subject): no `BODY_FOCUS_FAILED` / `BODY_PASTE_MISDIRECTED`, and the intended text must be in the native body before send, with newlines preserved. 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`). On Mini, prove the production path (write-bridge with node → System Events allowed, **or** the documented in-process fallback). That quote-prefix dual-host Sent prove is a separate bar from the 2.0.7 verify contract.
|
|
480
482
|
|
|
481
483
|
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.
|
|
482
484
|
|
package/lib/appleScript.js
CHANGED
|
@@ -170,6 +170,7 @@ export function needsHostTccAdvice(message) {
|
|
|
170
170
|
const text = String(message || "");
|
|
171
171
|
if (!text) return false;
|
|
172
172
|
if (text.includes(MAIL_SEND_TIMEOUT_GUIDANCE)) return false;
|
|
173
|
+
if (text.includes(MAIL_GUI_SCRIPTING_GUIDANCE)) return false;
|
|
173
174
|
if (text.includes(CONTACTS_APP_NOT_RUNNING_GUIDANCE)) return false;
|
|
174
175
|
if (text.includes(MAIL_APP_NOT_RUNNING_GUIDANCE)) return false;
|
|
175
176
|
if (text.includes(MESSAGES_APP_NOT_RUNNING_GUIDANCE)) return false;
|
|
@@ -339,10 +340,22 @@ export const MAIL_TCC_GUIDANCE =
|
|
|
339
340
|
*/
|
|
340
341
|
export const MAIL_ACCESSIBILITY_GUIDANCE =
|
|
341
342
|
"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
|
+
"mail_send / mail_draft type or 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
344
|
"Allow node in System Settings → Privacy & Security → Accessibility, and keep Automation → Mail allowed. " +
|
|
344
345
|
"This is not a Mail Automation deny (-1743 / -10004).";
|
|
345
346
|
|
|
347
|
+
/**
|
|
348
|
+
* System Events Apple Events / GUI scripting is missing, so the keystroke
|
|
349
|
+
* compose path cannot run. Distinct from a Mail send hang: nothing was typed
|
|
350
|
+
* or sent. Mini write-bridge should return unsupported so stdio falls back
|
|
351
|
+
* in-process rather than wedging ~60s.
|
|
352
|
+
*/
|
|
353
|
+
export const MAIL_GUI_SCRIPTING_GUIDANCE =
|
|
354
|
+
"System Events GUI scripting is unavailable for this process, so the body cannot be typed into Mail. " +
|
|
355
|
+
"mail_send / mail_draft use System Events keystrokes (Tab into the body, typed Returns) so Mail does not cite-wrap the Sent body (FB11734014). " +
|
|
356
|
+
"On a Mini LaunchAgent / write-bridge host, Allow the daemon's node → System Events in System Settings → Privacy & Security → Automation (in addition to node → Mail) and keep Accessibility allowed. " +
|
|
357
|
+
"This is not a Mail send timeout and not a Mail Automation deny (-1743 / -10004).";
|
|
358
|
+
|
|
346
359
|
/**
|
|
347
360
|
* AppleScript hung (ETIMEDOUT / -1712) while finding, opening, replying,
|
|
348
361
|
* or sending. That is not -1743 / -10004, including before send.
|
|
@@ -385,7 +398,7 @@ export function tccGuidanceFor(source) {
|
|
|
385
398
|
export function runAppleScript(script, options = {}) {
|
|
386
399
|
const { timeout = DEFAULT_SCRIPT_TIMEOUT_MS, appName = null, language = null } = options;
|
|
387
400
|
try {
|
|
388
|
-
const output = safeOsascript(script, { timeout, language });
|
|
401
|
+
const output = safeOsascript(script, { timeout, language, killSignal: "SIGKILL" });
|
|
389
402
|
return { ok: true, output: (output || "").trim(), error: null, kind: null };
|
|
390
403
|
} catch (e) {
|
|
391
404
|
const message = e && e.message ? e.message : String(e);
|
package/lib/mailWrite.js
CHANGED
|
@@ -25,10 +25,12 @@ import {
|
|
|
25
25
|
MAIL_TCC_GUIDANCE,
|
|
26
26
|
MAIL_SEND_TIMEOUT_GUIDANCE,
|
|
27
27
|
MAIL_ACCESSIBILITY_GUIDANCE,
|
|
28
|
+
MAIL_GUI_SCRIPTING_GUIDANCE,
|
|
28
29
|
MAIL_APP_NOT_RUNNING_GUIDANCE,
|
|
29
30
|
ATTRIBUTION_GUIDANCE,
|
|
30
31
|
isHardTccDenial
|
|
31
32
|
} from "./appleScript.js";
|
|
33
|
+
import { isIndexerMode } from "./processMode.js";
|
|
32
34
|
import {
|
|
33
35
|
planWrite,
|
|
34
36
|
plannedWriteResult,
|
|
@@ -156,8 +158,21 @@ export function probeMailAutomation() {
|
|
|
156
158
|
export const ACCESSIBILITY_DENIED_SENTINEL = "ACCESSIBILITY_DENIED";
|
|
157
159
|
export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
|
|
158
160
|
export const BODY_FOCUS_FAILED_SENTINEL = "BODY_FOCUS_FAILED";
|
|
159
|
-
|
|
161
|
+
export const GUI_SCRIPTING_UNAVAILABLE_SENTINEL = "GUI_SCRIPTING_UNAVAILABLE";
|
|
162
|
+
export const SYSTEM_EVENTS_PROBE_MARKER = "ATM_SYSTEM_EVENTS_PROBE";
|
|
163
|
+
/** Compose body AX height vs To/Subject/Cc fields (those are ~22px). Kept for 2.0.8–2.0.10 compile tests; 2.1.0 compose does not walk AX. */
|
|
160
164
|
export const MAIL_BODY_MIN_AX_HEIGHT = 50;
|
|
165
|
+
/** Mini-proven Tab count from default compose focus into the body. */
|
|
166
|
+
export const MAIL_BODY_TAB_MAX = 6;
|
|
167
|
+
/** Distinctive character typed then deleted to see whether the caret is in the body. */
|
|
168
|
+
export const MAIL_BODY_CALIBRATE_PROBE = "§";
|
|
169
|
+
/** Cheap System Events Automation probe; SIGKILL if it still hangs. */
|
|
170
|
+
export const MAIL_SE_PROBE_TIMEOUT_MS = 4000;
|
|
171
|
+
/** Compose osascript budget (make + keystroke + send). Far below the ~60s SE wedge. */
|
|
172
|
+
export const MAIL_COMPOSE_TIMEOUT_MS = 25000;
|
|
173
|
+
export const MAIL_SE_APPLEEVENT_TIMEOUT_SEC = 8;
|
|
174
|
+
export const MAIL_APPLEEVENT_TIMEOUT_SEC = 15;
|
|
175
|
+
export const MAIL_SE_PROBE_APPLEEVENT_TIMEOUT_SEC = 3;
|
|
161
176
|
|
|
162
177
|
/**
|
|
163
178
|
* AppleScript ordinal/keyword tokens that fail compile in `set {a, b}`
|
|
@@ -206,10 +221,10 @@ end atmFocusedElement`;
|
|
|
206
221
|
}
|
|
207
222
|
|
|
208
223
|
/**
|
|
209
|
-
* Body text for native Mail compose (
|
|
224
|
+
* Body text for native Mail compose (keystroke / paste, not AppleScript content).
|
|
210
225
|
*
|
|
211
226
|
* HTML is tag-stripped so a plain-looking HTML body (`<p>…</p>`) becomes
|
|
212
|
-
* typed
|
|
227
|
+
* typed text. Never prefixes lines with `>` and never emits
|
|
213
228
|
* `<blockquote>` — AppleScript `content` / `html content` cite-wrap
|
|
214
229
|
* (FB11734014). Mail's `mailto` command is not used: on current Mini/MacBook
|
|
215
230
|
* Mail it does not return an outgoing message (`newMessage` stays undefined,
|
|
@@ -231,213 +246,156 @@ export function composeBodyNeedle(body) {
|
|
|
231
246
|
return needle.length > 120 ? needle.slice(0, 120) : needle;
|
|
232
247
|
}
|
|
233
248
|
|
|
234
|
-
|
|
249
|
+
export function isSystemEventsProbeScript(script) {
|
|
250
|
+
return String(script || "").includes(SYSTEM_EVENTS_PROBE_MARKER);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function buildSystemEventsProbeScript() {
|
|
254
|
+
const secs = asInteger(MAIL_SE_PROBE_APPLEEVENT_TIMEOUT_SEC, {
|
|
255
|
+
min: 1,
|
|
256
|
+
max: 15,
|
|
257
|
+
field: "seProbeTimeout"
|
|
258
|
+
});
|
|
259
|
+
return `-- ${SYSTEM_EVENTS_PROBE_MARKER}
|
|
260
|
+
with timeout of ${secs} seconds
|
|
261
|
+
tell application "System Events"
|
|
262
|
+
set atmName to name
|
|
263
|
+
if atmName is "" then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
|
|
264
|
+
set atmUi to UI elements enabled
|
|
265
|
+
end tell
|
|
266
|
+
end timeout
|
|
267
|
+
if atmUi is false then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
268
|
+
return "OK"`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function probeSystemEventsGui() {
|
|
272
|
+
return runAppleScript(buildSystemEventsProbeScript(), {
|
|
273
|
+
timeout: MAIL_SE_PROBE_TIMEOUT_MS
|
|
274
|
+
});
|
|
275
|
+
}
|
|
235
276
|
|
|
236
277
|
/**
|
|
237
|
-
* AppleScript handlers:
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* "Expected class name but found identifier") and no compound
|
|
242
|
-
* `focused UI element` (compile -2741 / -2740). Hosts compile
|
|
243
|
-
* `text area` / `scroll area` / `text field`, `UI element whose role is
|
|
244
|
-
* "AXWebArea"`, `first UI element whose focused is true`, and
|
|
245
|
-
* `value of attribute "AXFocusedUIElement"`. Never focus the first
|
|
246
|
-
* AXTextArea blindly — on MacBook Mail that is a header field, and
|
|
247
|
-
* Cmd-V becomes BODY_PASTE_MISDIRECTED.
|
|
248
|
-
*
|
|
249
|
-
* AppleScript treats ordinal suffixes as reserved tokens (`th` as in 4th,
|
|
250
|
-
* also `st`/`nd`/`rd`). Dual-host Sent prove on 2.0.9 compiled past the
|
|
251
|
-
* focused-UI fix, then failed in `atmFocusMailBody`:
|
|
252
|
-
* `set {tw, th} to size of atmElem` → -2741 Expected expression but found
|
|
253
|
-
* `th`. Size/position lists use `atmW`/`atmH` (hosts confirmed) and other
|
|
254
|
-
* `atm*` names — never `th`/`tw`/`st`/`nd`/`rd`/`to`/`by`/`id` in
|
|
255
|
-
* destructuring.
|
|
278
|
+
* AppleScript handlers: Tab into the compose body and type it (typed Returns
|
|
279
|
+
* preserve newlines). Does not walk System Events `windows of process Mail`
|
|
280
|
+
* (macOS 26 reports 0). Calibration reads Mail `content of` the outgoing
|
|
281
|
+
* message — never `set content` / `html content` (FB11734014 cite-wrap).
|
|
256
282
|
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* 3. Click Subject then Tab into the body (stable Mini/MacBook order)
|
|
261
|
-
* 4. Click the lower two-thirds of the compose window
|
|
283
|
+
* Tab count is effect-calibrated up to MAIL_BODY_TAB_MAX (Mini-proven 6).
|
|
284
|
+
* Fail-closed if the body needle is missing after type (Cmd-V fallback) and
|
|
285
|
+
* header checks. System Events Apple events are wrapped in `with timeout`.
|
|
262
286
|
*/
|
|
263
287
|
export function buildMailBodyPasteHandler() {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
288
|
+
const tabMax = asInteger(MAIL_BODY_TAB_MAX, { min: 1, max: 12, field: "tabMax" });
|
|
289
|
+
const seSecs = asInteger(MAIL_SE_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 30, field: "seTimeout" });
|
|
290
|
+
const probe = asString(MAIL_BODY_CALIBRATE_PROBE);
|
|
291
|
+
return `on atmSplitParagraphs(bodyText)
|
|
292
|
+
set atmSavedDelim to AppleScript's text item delimiters
|
|
293
|
+
set AppleScript's text item delimiters to return
|
|
294
|
+
set atmCrParts to text items of bodyText
|
|
295
|
+
set AppleScript's text item delimiters to linefeed
|
|
296
|
+
set atmNorm to atmCrParts as string
|
|
297
|
+
set atmLines to text items of atmNorm
|
|
298
|
+
set AppleScript's text item delimiters to atmSavedDelim
|
|
299
|
+
return atmLines
|
|
300
|
+
end atmSplitParagraphs
|
|
301
|
+
|
|
302
|
+
on atmSpacesForTabs(atmLine)
|
|
303
|
+
set atmSavedDelim to AppleScript's text item delimiters
|
|
304
|
+
set AppleScript's text item delimiters to tab
|
|
305
|
+
set atmTabParts to text items of atmLine
|
|
306
|
+
set AppleScript's text item delimiters to " "
|
|
307
|
+
set atmOut to atmTabParts as string
|
|
308
|
+
set AppleScript's text item delimiters to atmSavedDelim
|
|
309
|
+
return atmOut
|
|
310
|
+
end atmSpacesForTabs
|
|
311
|
+
|
|
312
|
+
on atmKeystrokeText(atmText)
|
|
313
|
+
set atmSafe to my atmSpacesForTabs(atmText)
|
|
314
|
+
set atmLen to length of atmSafe
|
|
315
|
+
if atmLen is 0 then return
|
|
316
|
+
set atmPos to 1
|
|
267
317
|
tell application "System Events"
|
|
268
318
|
tell process "Mail"
|
|
269
|
-
|
|
270
|
-
set
|
|
271
|
-
if
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if r is "AXComboBox" then return false
|
|
276
|
-
if r is "AXButton" then return false
|
|
277
|
-
if r is "AXMenuButton" then return false
|
|
278
|
-
if r is "AXPopUpButton" then return false
|
|
279
|
-
set atmH to 0
|
|
280
|
-
try
|
|
281
|
-
set {atmW, atmH} to size of fe
|
|
282
|
-
end try
|
|
283
|
-
if r is "AXTextArea" and atmH < ${BODY_MIN_H} then return false
|
|
284
|
-
ignoring case
|
|
285
|
-
set bits to ""
|
|
286
|
-
try
|
|
287
|
-
set bits to bits & (description of fe as string) & " "
|
|
288
|
-
end try
|
|
289
|
-
try
|
|
290
|
-
set bits to bits & (name of fe as string) & " "
|
|
291
|
-
end try
|
|
292
|
-
if bits contains "To:" then return false
|
|
293
|
-
if bits contains "Cc:" then return false
|
|
294
|
-
if bits contains "Bcc:" then return false
|
|
295
|
-
if bits contains "Subject" then return false
|
|
296
|
-
end ignoring
|
|
297
|
-
if r is "AXWebArea" then return true
|
|
298
|
-
if r is "AXTextArea" then return true
|
|
299
|
-
if atmH >= ${BODY_MIN_H} then return true
|
|
300
|
-
end try
|
|
301
|
-
return false
|
|
319
|
+
repeat while atmPos is less than or equal to atmLen
|
|
320
|
+
set atmEndPos to atmPos + 31
|
|
321
|
+
if atmEndPos > atmLen then set atmEndPos to atmLen
|
|
322
|
+
keystroke (text atmPos thru atmEndPos of atmSafe)
|
|
323
|
+
set atmPos to atmEndPos + 1
|
|
324
|
+
end repeat
|
|
302
325
|
end tell
|
|
303
326
|
end tell
|
|
304
|
-
end
|
|
327
|
+
end atmKeystrokeText
|
|
305
328
|
|
|
306
|
-
on
|
|
307
|
-
tell application "
|
|
308
|
-
|
|
309
|
-
set
|
|
310
|
-
if
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
set atmQueue to UI elements of w
|
|
348
|
-
set atmWalked to 0
|
|
349
|
-
repeat while (count of atmQueue) > 0 and atmWalked < 120
|
|
350
|
-
set atmWalked to atmWalked + 1
|
|
351
|
-
set atmElem to item 1 of atmQueue
|
|
352
|
-
if (count of atmQueue) is 1 then
|
|
353
|
-
set atmQueue to {}
|
|
354
|
-
else
|
|
355
|
-
set atmQueue to items 2 thru -1 of atmQueue
|
|
356
|
-
end if
|
|
357
|
-
try
|
|
358
|
-
if (role of atmElem as string) is "AXWebArea" then
|
|
359
|
-
set {atmW, atmH} to size of atmElem
|
|
360
|
-
if atmH >= ${BODY_MIN_H} then
|
|
361
|
-
click atmElem
|
|
362
|
-
delay 0.08
|
|
363
|
-
if my atmSafeToPaste() then return
|
|
364
|
-
end if
|
|
365
|
-
end if
|
|
366
|
-
end try
|
|
367
|
-
try
|
|
368
|
-
if (role of atmElem as string) is "AXTextArea" then
|
|
369
|
-
set {atmW, atmH} to size of atmElem
|
|
370
|
-
if atmH >= ${BODY_MIN_H} and atmTallText is missing value then set atmTallText to atmElem
|
|
371
|
-
end if
|
|
372
|
-
end try
|
|
373
|
-
try
|
|
374
|
-
set r to (role of atmElem as string)
|
|
375
|
-
if r is "AXTextField" or r is "AXTextArea" then
|
|
376
|
-
set bits to ""
|
|
377
|
-
try
|
|
378
|
-
set bits to bits & (description of atmElem as string)
|
|
379
|
-
end try
|
|
380
|
-
try
|
|
381
|
-
set bits to bits & (name of atmElem as string)
|
|
382
|
-
end try
|
|
383
|
-
ignoring case
|
|
384
|
-
if bits contains "Subject" and atmSubjectElem is missing value then set atmSubjectElem to atmElem
|
|
385
|
-
end ignoring
|
|
386
|
-
end if
|
|
387
|
-
end try
|
|
388
|
-
try
|
|
389
|
-
set atmKids to UI elements of atmElem
|
|
390
|
-
repeat with i from 1 to (count of atmKids)
|
|
391
|
-
set end of atmQueue to item i of atmKids
|
|
392
|
-
end repeat
|
|
393
|
-
end try
|
|
394
|
-
end repeat
|
|
395
|
-
try
|
|
396
|
-
set ta to text area 1 of scroll area 1 of w
|
|
397
|
-
set {atmW, atmH} to size of ta
|
|
398
|
-
if atmH >= ${BODY_MIN_H} then
|
|
399
|
-
set focused of ta to true
|
|
400
|
-
delay 0.08
|
|
401
|
-
if my atmSafeToPaste() then return
|
|
402
|
-
end if
|
|
403
|
-
end try
|
|
404
|
-
if atmTallText is not missing value then
|
|
405
|
-
try
|
|
406
|
-
set focused of atmTallText to true
|
|
407
|
-
end try
|
|
408
|
-
try
|
|
409
|
-
click atmTallText
|
|
410
|
-
end try
|
|
411
|
-
delay 0.08
|
|
412
|
-
if my atmSafeToPaste() then return
|
|
413
|
-
end if
|
|
414
|
-
try
|
|
415
|
-
click (first text field of w whose description contains "Subject")
|
|
416
|
-
delay 0.08
|
|
417
|
-
key code 48
|
|
418
|
-
delay 0.08
|
|
419
|
-
if my atmSafeToPaste() then return
|
|
420
|
-
end try
|
|
421
|
-
if atmSubjectElem is not missing value then
|
|
422
|
-
try
|
|
423
|
-
click atmSubjectElem
|
|
424
|
-
delay 0.08
|
|
329
|
+
on atmMailBodyContains(msg, needle)
|
|
330
|
+
tell application "Mail"
|
|
331
|
+
try
|
|
332
|
+
set atmGot to content of msg as string
|
|
333
|
+
if atmGot contains needle then return true
|
|
334
|
+
end try
|
|
335
|
+
end tell
|
|
336
|
+
return false
|
|
337
|
+
end atmMailBodyContains
|
|
338
|
+
|
|
339
|
+
on atmTabIntoMailBody(msg)
|
|
340
|
+
tell application "Mail" to activate
|
|
341
|
+
delay 0.25
|
|
342
|
+
with timeout of ${seSecs} seconds
|
|
343
|
+
tell application "System Events"
|
|
344
|
+
tell process "Mail" to set frontmost to true
|
|
345
|
+
end tell
|
|
346
|
+
end timeout
|
|
347
|
+
delay 0.08
|
|
348
|
+
repeat with atmN from 0 to ${tabMax}
|
|
349
|
+
with timeout of ${seSecs} seconds
|
|
350
|
+
tell application "System Events"
|
|
351
|
+
tell process "Mail"
|
|
352
|
+
keystroke ${probe}
|
|
353
|
+
end tell
|
|
354
|
+
end tell
|
|
355
|
+
end timeout
|
|
356
|
+
delay 0.08
|
|
357
|
+
set atmHit to my atmMailBodyContains(msg, ${probe})
|
|
358
|
+
with timeout of ${seSecs} seconds
|
|
359
|
+
tell application "System Events"
|
|
360
|
+
tell process "Mail"
|
|
361
|
+
key code 51
|
|
362
|
+
end tell
|
|
363
|
+
end tell
|
|
364
|
+
end timeout
|
|
365
|
+
if atmHit then return
|
|
366
|
+
if atmN is ${tabMax} then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
367
|
+
with timeout of ${seSecs} seconds
|
|
368
|
+
tell application "System Events"
|
|
369
|
+
tell process "Mail"
|
|
425
370
|
key code 48
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
371
|
+
end tell
|
|
372
|
+
end tell
|
|
373
|
+
end timeout
|
|
374
|
+
delay 0.08
|
|
375
|
+
end repeat
|
|
376
|
+
error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
377
|
+
end atmTabIntoMailBody
|
|
378
|
+
|
|
379
|
+
on atmTypeMailBody(bodyText)
|
|
380
|
+
set atmLines to my atmSplitParagraphs(bodyText)
|
|
381
|
+
set atmLast to count of atmLines
|
|
382
|
+
if atmLast is 0 then return
|
|
383
|
+
with timeout of ${seSecs} seconds
|
|
384
|
+
repeat with atmI from 1 to atmLast
|
|
385
|
+
set atmLine to item atmI of atmLines as string
|
|
386
|
+
my atmKeystrokeText(atmLine)
|
|
387
|
+
if atmI < atmLast then
|
|
388
|
+
tell application "System Events"
|
|
389
|
+
tell process "Mail"
|
|
390
|
+
key code 36
|
|
391
|
+
end tell
|
|
392
|
+
end tell
|
|
429
393
|
end if
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
delay 0.08
|
|
434
|
-
if my atmSafeToPaste() then return
|
|
435
|
-
error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
436
|
-
end tell
|
|
437
|
-
end tell
|
|
438
|
-
end atmFocusMailBody
|
|
394
|
+
end repeat
|
|
395
|
+
end timeout
|
|
396
|
+
end atmTypeMailBody
|
|
439
397
|
|
|
440
|
-
on
|
|
398
|
+
on atmPasteMailBodyFallback(bodyText)
|
|
441
399
|
set savedClip to ""
|
|
442
400
|
set hadClip to false
|
|
443
401
|
try
|
|
@@ -445,26 +403,43 @@ on atmPasteMailBody(bodyText, expectedSubject)
|
|
|
445
403
|
set hadClip to true
|
|
446
404
|
end try
|
|
447
405
|
set the clipboard to bodyText
|
|
448
|
-
tell application "Mail" to activate
|
|
449
|
-
delay 0.4
|
|
450
406
|
try
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
407
|
+
with timeout of ${seSecs} seconds
|
|
408
|
+
tell application "System Events"
|
|
409
|
+
tell process "Mail"
|
|
410
|
+
keystroke "v" using command down
|
|
411
|
+
end tell
|
|
456
412
|
end tell
|
|
457
|
-
end
|
|
458
|
-
delay 0.
|
|
413
|
+
end timeout
|
|
414
|
+
delay 0.15
|
|
459
415
|
on error errMsg number errNum
|
|
460
416
|
if hadClip then set the clipboard to savedClip
|
|
461
417
|
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
462
418
|
error errMsg number errNum
|
|
463
419
|
end try
|
|
464
420
|
if hadClip then set the clipboard to savedClip
|
|
465
|
-
end
|
|
421
|
+
end atmPasteMailBodyFallback
|
|
422
|
+
|
|
423
|
+
on atmFillMailBody(bodyText, needle, msg)
|
|
424
|
+
try
|
|
425
|
+
atmTabIntoMailBody(msg)
|
|
426
|
+
atmTypeMailBody(bodyText)
|
|
427
|
+
delay 0.12
|
|
428
|
+
if needle is not "" then
|
|
429
|
+
if my atmMailBodyContains(msg, needle) is false then
|
|
430
|
+
atmPasteMailBodyFallback(bodyText)
|
|
431
|
+
end if
|
|
432
|
+
end if
|
|
433
|
+
on error errMsg number errNum
|
|
434
|
+
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
435
|
+
if errMsg contains "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}" then error "${GUI_SCRIPTING_UNAVAILABLE_SENTINEL}"
|
|
436
|
+
error errMsg number errNum
|
|
437
|
+
end try
|
|
438
|
+
end atmFillMailBody`;
|
|
466
439
|
}
|
|
467
440
|
|
|
441
|
+
export const buildMailBodyFillHandler = buildMailBodyPasteHandler;
|
|
442
|
+
|
|
468
443
|
function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
|
|
469
444
|
const toCount = asInteger(to.length, { min: 1, max: 99, field: "toCount" });
|
|
470
445
|
const ccCount = asInteger(cc.length, { min: 0, max: 99, field: "ccCount" });
|
|
@@ -495,17 +470,6 @@ function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
|
|
|
495
470
|
set atmGotBody to content of newMessage as string
|
|
496
471
|
end try
|
|
497
472
|
end if
|
|
498
|
-
if atmGotBody does not contain ${asString(needle)} then
|
|
499
|
-
try
|
|
500
|
-
set atmFe to my atmFocusedElement()
|
|
501
|
-
if atmFe is not missing value then
|
|
502
|
-
tell application "System Events"
|
|
503
|
-
set atmAxVal to value of atmFe as string
|
|
504
|
-
if atmAxVal contains ${asString(needle)} then set atmGotBody to atmAxVal
|
|
505
|
-
end tell
|
|
506
|
-
end if
|
|
507
|
-
end try
|
|
508
|
-
end if
|
|
509
473
|
if atmGotBody does not contain ${asString(needle)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"`
|
|
510
474
|
: "";
|
|
511
475
|
return ` if (count of to recipients of newMessage) is not ${toCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
@@ -526,9 +490,10 @@ ${bodyCheck}`;
|
|
|
526
490
|
* Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
|
|
527
491
|
* cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
|
|
528
492
|
* Recipients and subject are AppleScript properties (never paste). The body
|
|
529
|
-
* is
|
|
530
|
-
*
|
|
531
|
-
*
|
|
493
|
+
* is typed into the native compose editor via System Events (Tab into body,
|
|
494
|
+
* typed Returns for newlines, Cmd-V only if the body needle is still
|
|
495
|
+
* missing). Do not walk `windows of process Mail`. If the body never
|
|
496
|
+
* receives the intended text, or headers change, abort + delete (nothing sent).
|
|
532
497
|
*/
|
|
533
498
|
export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
|
|
534
499
|
const recipients = [
|
|
@@ -537,9 +502,11 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
|
|
|
537
502
|
recipientLines(bcc, "bcc recipient")
|
|
538
503
|
].filter((block) => block.length > 0).join("\n");
|
|
539
504
|
|
|
505
|
+
const mailSecs = asInteger(MAIL_APPLEEVENT_TIMEOUT_SEC, { min: 1, max: 60, field: "mailTimeout" });
|
|
540
506
|
const plainBody = composeNativeBody(body, { html });
|
|
507
|
+
const needle = composeBodyNeedle(plainBody);
|
|
541
508
|
const pasteAndVerify = plainBody
|
|
542
|
-
? `
|
|
509
|
+
? ` atmFillMailBody(${asString(plainBody)}, ${asString(needle)}, newMessage)
|
|
543
510
|
tell application "Mail"
|
|
544
511
|
${composeMisdirectChecks({ to, cc, bcc, subject, body: plainBody })}
|
|
545
512
|
end tell`
|
|
@@ -548,11 +515,13 @@ ${composeMisdirectChecks({ to, cc, bcc, subject, body: plainBody })}
|
|
|
548
515
|
return `${buildMailBodyPasteHandler()}
|
|
549
516
|
|
|
550
517
|
tell application "Mail"
|
|
551
|
-
|
|
552
|
-
|
|
518
|
+
with timeout of ${mailSecs} seconds
|
|
519
|
+
set newMessage to make new outgoing message with properties {subject:${asString(subject)}, visible:true}
|
|
520
|
+
tell newMessage
|
|
553
521
|
${recipients}
|
|
554
|
-
|
|
555
|
-
|
|
522
|
+
end tell
|
|
523
|
+
activate
|
|
524
|
+
end timeout
|
|
556
525
|
end tell
|
|
557
526
|
try
|
|
558
527
|
${pasteAndVerify}
|
|
@@ -564,10 +533,12 @@ on error errMsg number errNum
|
|
|
564
533
|
end try
|
|
565
534
|
set atmOutgoingId to ""
|
|
566
535
|
tell application "Mail"
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
536
|
+
with timeout of ${mailSecs} seconds
|
|
537
|
+
try
|
|
538
|
+
set atmOutgoingId to message id of newMessage as string
|
|
539
|
+
end try
|
|
540
|
+
${send ? "send newMessage" : "save newMessage"}
|
|
541
|
+
end timeout
|
|
571
542
|
end tell
|
|
572
543
|
if atmOutgoingId is not "" then return atmOutgoingId
|
|
573
544
|
return "OK"`;
|
|
@@ -927,6 +898,14 @@ export function verifyQueuedMessage(match = {}, { attempts = SENT_VERIFY_ATTEMPT
|
|
|
927
898
|
return last;
|
|
928
899
|
}
|
|
929
900
|
|
|
901
|
+
export function isMailGuiScriptingUnavailable(result) {
|
|
902
|
+
if (!result) return false;
|
|
903
|
+
const text = String(result.error || "").toLowerCase();
|
|
904
|
+
if (text.includes("gui_scripting_unavailable")) return true;
|
|
905
|
+
if (result.kind === "timeout" && text.includes("system events")) return true;
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
|
|
930
909
|
export function isMailAccessibilityDenial(result) {
|
|
931
910
|
if (!result) return false;
|
|
932
911
|
const text = String(result.error || "").toLowerCase();
|
|
@@ -946,6 +925,9 @@ export function isMailBodyPasteMisdirected(result) {
|
|
|
946
925
|
|
|
947
926
|
function failure(action, summary, result, secrets) {
|
|
948
927
|
const err = String((result && result.error) || "").toLowerCase();
|
|
928
|
+
if (err.includes("gui_scripting_unavailable")) {
|
|
929
|
+
return `${action} failed — attempted to ${summary}. ${MAIL_GUI_SCRIPTING_GUIDANCE}`;
|
|
930
|
+
}
|
|
949
931
|
if (err.includes("body_focus_failed")) {
|
|
950
932
|
return `${action} failed — attempted to ${summary}. The compose caret could not be moved into the message body; nothing was sent.`;
|
|
951
933
|
}
|
|
@@ -1051,10 +1033,24 @@ function finalizeMailWrite({
|
|
|
1051
1033
|
return { ok: false, delivered: false, mailbox: null, message: failure(action, summary, result, secrets) };
|
|
1052
1034
|
}
|
|
1053
1035
|
|
|
1036
|
+
function guiScriptingUnavailableResult(action, summary, result, { indexerMode } = {}) {
|
|
1037
|
+
const unsupported = Boolean(indexerMode);
|
|
1038
|
+
const message = isMailAccessibilityDenial(result)
|
|
1039
|
+
? `${action} failed — attempted to ${summary}. ${MAIL_ACCESSIBILITY_GUIDANCE}`
|
|
1040
|
+
: `${action} failed — attempted to ${summary}. ${MAIL_GUI_SCRIPTING_GUIDANCE}`;
|
|
1041
|
+
return {
|
|
1042
|
+
ok: false,
|
|
1043
|
+
delivered: false,
|
|
1044
|
+
mailbox: null,
|
|
1045
|
+
...(unsupported ? { unsupported: true } : {}),
|
|
1046
|
+
message
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1054
1050
|
/**
|
|
1055
1051
|
* Compose and send (or save as draft) a new email.
|
|
1056
1052
|
*/
|
|
1057
|
-
export function mailCompose(args = {}, { draft = false } = {}) {
|
|
1053
|
+
export function mailCompose(args = {}, { draft = false, indexerMode = isIndexerMode() } = {}) {
|
|
1058
1054
|
const action = draft ? "mail_draft" : "mail_send";
|
|
1059
1055
|
|
|
1060
1056
|
const to = validateEmailList(args.to, "to");
|
|
@@ -1093,6 +1089,14 @@ export function mailCompose(args = {}, { draft = false } = {}) {
|
|
|
1093
1089
|
});
|
|
1094
1090
|
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1095
1091
|
|
|
1092
|
+
const plainBody = composeNativeBody(body.text, { html: bodyFormat === "html" });
|
|
1093
|
+
if (plainBody) {
|
|
1094
|
+
const se = probeSystemEventsGui();
|
|
1095
|
+
if (!se.ok) {
|
|
1096
|
+
return guiScriptingUnavailableResult(action, summary, se, { indexerMode });
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1096
1100
|
const script = buildComposeScript({
|
|
1097
1101
|
to: to.addresses,
|
|
1098
1102
|
cc: cc.addresses,
|
|
@@ -1103,7 +1107,14 @@ export function mailCompose(args = {}, { draft = false } = {}) {
|
|
|
1103
1107
|
html: bodyFormat === "html"
|
|
1104
1108
|
});
|
|
1105
1109
|
|
|
1106
|
-
const result = runAppleScript(script, { timeout:
|
|
1110
|
+
const result = runAppleScript(script, { timeout: MAIL_COMPOSE_TIMEOUT_MS, appName: "Mail" });
|
|
1111
|
+
if (
|
|
1112
|
+
!result.ok &&
|
|
1113
|
+
indexerMode &&
|
|
1114
|
+
(isMailGuiScriptingUnavailable(result) || isMailAccessibilityDenial(result))
|
|
1115
|
+
) {
|
|
1116
|
+
return guiScriptingUnavailableResult(action, summary, result, { indexerMode });
|
|
1117
|
+
}
|
|
1107
1118
|
return finalizeMailWrite({
|
|
1108
1119
|
action,
|
|
1109
1120
|
summary,
|
package/lib/shell.js
CHANGED
|
@@ -131,9 +131,12 @@ export function safeOsascript(script, options = {}) {
|
|
|
131
131
|
args.push("-e", script);
|
|
132
132
|
|
|
133
133
|
// Use -e with the script as an argument (never a shell string).
|
|
134
|
+
// Hung Apple Events ignore SIGTERM (Mini write-bridge ~60s wedge).
|
|
135
|
+
// Timeout must SIGKILL osascript so compose fails fast instead of hanging.
|
|
134
136
|
const result = spawnSync('osascript', args, {
|
|
135
137
|
encoding: 'utf-8',
|
|
136
138
|
timeout,
|
|
139
|
+
killSignal: 'SIGKILL',
|
|
137
140
|
shell: false
|
|
138
141
|
});
|
|
139
142
|
|
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,
|
|
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 type the body via keystrokes). " +
|
|
106
106
|
"Do not add node via + in the Contacts or Calendars privacy lists."
|
|
107
107
|
);
|
|
108
108
|
}
|
|
@@ -127,7 +127,7 @@ export function tccFallbackAdvice({
|
|
|
127
127
|
if (mini && bridgeAvailable) {
|
|
128
128
|
return (
|
|
129
129
|
"The indexer daemon was reachable but the write was still denied; grant the daemon's node binary Full Disk Access (reads) and " +
|
|
130
|
-
"Allow node in System Settings > Privacy & Security > Automation for Mail.app, Messages.app, Contacts.app,
|
|
130
|
+
"Allow node in System Settings > Privacy & Security > Automation for Mail.app, Messages.app, Contacts.app, Calendar.app, and System Events. " +
|
|
131
131
|
"A hang or timeout on Mail compose is TCC / Automation denied, not Mail.app missing. " +
|
|
132
132
|
"Do not add node via + in the Contacts or Calendars privacy lists — those panes often have no Add button."
|
|
133
133
|
);
|
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; the body is typed into Mail's compose window (System Events keystrokes). 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; the body is typed into Mail's compose window (System Events keystrokes). AppleScript content/html content is not set because it quote-wraps the Sent body" },
|
|
78
78
|
...CONFIRM_PROPS
|
|
79
79
|
},
|
|
80
80
|
required: ["to"]
|