apple-tools-mcp 2.0.7 → 2.0.9
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 +6 -2
- package/lib/mailWrite.js +249 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -466,11 +466,15 @@ Two arguments are available on **every** write tool:
|
|
|
466
466
|
|
|
467
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 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.
|
|
468
468
|
|
|
469
|
-
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
|
+
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` / `text field` 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”). The pre-paste focus read uses `first UI element whose focused is true` and `value of attribute "AXFocusedUIElement"` — never the compound `focused UI element`, which does not compile on System Events 26.x (**-2741** on MacBook 26.1, **-2740** on Mini 26.6.2).
|
|
470
|
+
|
|
471
|
+
**Compose body focus (2.0.8).** After `make new outgoing message`, To and Subject are AppleScript properties (never paste). Cmd-V runs only after the caret leaves the header: a tall `AXWebArea` (HTML compose body), a tall `text area` (plain compose; short To/Subject fields are skipped), Subject then Tab, or a click in the lower two-thirds of the compose window. If focus is still a header field, the tool raises `BODY_FOCUS_FAILED` and does **not** paste. After paste it checks To/Cc/Bcc counts, the exact subject, and that the native body contains the intended text. A mismatch is `BODY_PASTE_MISDIRECTED`: the outgoing message is deleted and nothing is sent. Quote-prefix Sent prove and Sent/Outbox verify stay gated on this paste-focus contract — 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
|
+
|
|
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. The 2.0.8 fail-closed gates stay; only the focus *query* changed to the forms hosts compiled.
|
|
470
474
|
|
|
471
475
|
**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.
|
|
472
476
|
|
|
473
|
-
**Manual prove (2.0.
|
|
477
|
+
**Manual prove (2.0.9 on the Mac host):** AppleScript for safe-to-paste / focus check **compiles** (no `-2741`/`-2740` on `focused UI element`). Then `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. 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. 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** / **-2740** on body focus). That quote-prefix dual-host Sent prove is a separate bar from the 2.0.7 verify contract.
|
|
474
478
|
|
|
475
479
|
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.
|
|
476
480
|
|
package/lib/mailWrite.js
CHANGED
|
@@ -155,6 +155,40 @@ export function probeMailAutomation() {
|
|
|
155
155
|
|
|
156
156
|
export const ACCESSIBILITY_DENIED_SENTINEL = "ACCESSIBILITY_DENIED";
|
|
157
157
|
export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
|
|
158
|
+
export const BODY_FOCUS_FAILED_SENTINEL = "BODY_FOCUS_FAILED";
|
|
159
|
+
/** Compose body AX height vs To/Subject/Cc fields (those are ~22px). */
|
|
160
|
+
export const MAIL_BODY_MIN_AX_HEIGHT = 50;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* macOS 26.x System Events rejects the compound `focused UI element`
|
|
164
|
+
* (MacBook 26.1 compile -2741; Mini 26.6.2 compile -2740). Hosts compile
|
|
165
|
+
* these instead — same class of dictionary break as bare `web area`.
|
|
166
|
+
*/
|
|
167
|
+
export const ATM_FOCUSED_WHOSE_QUERY = "first UI element whose focused is true";
|
|
168
|
+
export const ATM_FOCUSED_AX_QUERY = 'value of attribute "AXFocusedUIElement"';
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Resolve the focused System Events element without `focused UI element`.
|
|
172
|
+
* AXFocusedUIElement is tried first (nested caret); `whose focused is true`
|
|
173
|
+
* is the compile-proven fallback. A process-level whose match can be the
|
|
174
|
+
* key window — callers must still reject header roles / AXWindow.
|
|
175
|
+
*/
|
|
176
|
+
export function buildAtmFocusedElementHandler() {
|
|
177
|
+
return `on atmFocusedElement()
|
|
178
|
+
tell application "System Events"
|
|
179
|
+
tell process "Mail"
|
|
180
|
+
try
|
|
181
|
+
set fe to ${ATM_FOCUSED_AX_QUERY}
|
|
182
|
+
if fe is not missing value then return fe
|
|
183
|
+
end try
|
|
184
|
+
try
|
|
185
|
+
return ${ATM_FOCUSED_WHOSE_QUERY}
|
|
186
|
+
end try
|
|
187
|
+
end tell
|
|
188
|
+
end tell
|
|
189
|
+
return missing value
|
|
190
|
+
end atmFocusedElement`;
|
|
191
|
+
}
|
|
158
192
|
|
|
159
193
|
/**
|
|
160
194
|
* Body text for native Mail compose (clipboard paste, not AppleScript content).
|
|
@@ -172,51 +206,124 @@ export function composeNativeBody(body, { html = false } = {}) {
|
|
|
172
206
|
}
|
|
173
207
|
|
|
174
208
|
/**
|
|
175
|
-
*
|
|
209
|
+
* Distinctive first line used to prove the native editor received the body.
|
|
210
|
+
* Full multiline `contains` can miss when Mail stores an HTML alternative.
|
|
211
|
+
*/
|
|
212
|
+
export function composeBodyNeedle(body) {
|
|
213
|
+
const text = body === undefined || body === null ? "" : String(body);
|
|
214
|
+
const line = text.split(/\r?\n/).find((candidate) => candidate.trim()) || text;
|
|
215
|
+
const needle = line.trim();
|
|
216
|
+
return needle.length > 120 ? needle.slice(0, 120) : needle;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const BODY_MIN_H = asInteger(MAIL_BODY_MIN_AX_HEIGHT, { min: 1, max: 500, field: "bodyMinHeight" });
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* AppleScript handlers: focus the compose body (not To/Subject) and paste.
|
|
176
223
|
* System Events needs Accessibility; Mail make/send still needs Automation.
|
|
177
224
|
*
|
|
178
225
|
* macOS 26.x System Events has no `web area` class (compile -2741,
|
|
179
|
-
* "Expected class name but found identifier")
|
|
180
|
-
* `
|
|
226
|
+
* "Expected class name but found identifier") and no compound
|
|
227
|
+
* `focused UI element` (compile -2741 / -2740). Hosts compile
|
|
228
|
+
* `text area` / `scroll area` / `text field`, `UI element whose role is
|
|
229
|
+
* "AXWebArea"`, `first UI element whose focused is true`, and
|
|
230
|
+
* `value of attribute "AXFocusedUIElement"`. Never focus the first
|
|
231
|
+
* AXTextArea blindly — on MacBook Mail that is a header field, and
|
|
232
|
+
* Cmd-V becomes BODY_PASTE_MISDIRECTED.
|
|
233
|
+
*
|
|
234
|
+
* Strategies, each fail-closed unless the caret left the header:
|
|
235
|
+
* 1. Click a tall AXWebArea (HTML compose body)
|
|
236
|
+
* 2. Focus a tall `text area` (plain compose body), never a short header
|
|
237
|
+
* 3. Click Subject then Tab into the body (stable Mini/MacBook order)
|
|
238
|
+
* 4. Click the lower two-thirds of the compose window
|
|
181
239
|
*/
|
|
182
240
|
export function buildMailBodyPasteHandler() {
|
|
183
|
-
return
|
|
241
|
+
return `${buildAtmFocusedElementHandler()}
|
|
242
|
+
|
|
243
|
+
on atmSafeToPaste()
|
|
184
244
|
tell application "System Events"
|
|
185
245
|
tell process "Mail"
|
|
186
|
-
set frontmost to true
|
|
187
|
-
if (count of windows) < 1 then error "MAIL_COMPOSE_WINDOW_MISSING"
|
|
188
|
-
set w to first window
|
|
189
|
-
try
|
|
190
|
-
set focused of text area 1 of scroll area 1 of w to true
|
|
191
|
-
return
|
|
192
|
-
end try
|
|
193
246
|
try
|
|
194
|
-
set
|
|
195
|
-
return
|
|
247
|
+
set fe to my atmFocusedElement()
|
|
248
|
+
if fe is missing value then return false
|
|
249
|
+
set r to (role of fe) as string
|
|
250
|
+
if r is "AXWindow" then return false
|
|
251
|
+
if r is "AXTextField" then return false
|
|
252
|
+
if r is "AXComboBox" then return false
|
|
253
|
+
if r is "AXButton" then return false
|
|
254
|
+
if r is "AXMenuButton" then return false
|
|
255
|
+
if r is "AXPopUpButton" then return false
|
|
256
|
+
set eh to 0
|
|
257
|
+
try
|
|
258
|
+
set {ew, eh} to size of fe
|
|
259
|
+
end try
|
|
260
|
+
if r is "AXTextArea" and eh < ${BODY_MIN_H} then return false
|
|
261
|
+
ignoring case
|
|
262
|
+
set bits to ""
|
|
263
|
+
try
|
|
264
|
+
set bits to bits & (description of fe as string) & " "
|
|
265
|
+
end try
|
|
266
|
+
try
|
|
267
|
+
set bits to bits & (name of fe as string) & " "
|
|
268
|
+
end try
|
|
269
|
+
if bits contains "To:" then return false
|
|
270
|
+
if bits contains "Cc:" then return false
|
|
271
|
+
if bits contains "Bcc:" then return false
|
|
272
|
+
if bits contains "Subject" then return false
|
|
273
|
+
end ignoring
|
|
274
|
+
if r is "AXWebArea" then return true
|
|
275
|
+
if r is "AXTextArea" then return true
|
|
276
|
+
if eh >= ${BODY_MIN_H} then return true
|
|
196
277
|
end try
|
|
278
|
+
return false
|
|
279
|
+
end tell
|
|
280
|
+
end tell
|
|
281
|
+
end atmSafeToPaste
|
|
282
|
+
|
|
283
|
+
on atmFocusMailBody(expectedSubject)
|
|
284
|
+
tell application "System Events"
|
|
285
|
+
tell process "Mail"
|
|
286
|
+
set frontmost to true
|
|
287
|
+
if (count of windows) < 1 then error "MAIL_COMPOSE_WINDOW_MISSING"
|
|
288
|
+
set w to window 1
|
|
289
|
+
if expectedSubject is not "" then
|
|
290
|
+
repeat with i from 1 to (count of windows)
|
|
291
|
+
try
|
|
292
|
+
if (name of window i as string) is expectedSubject then
|
|
293
|
+
set w to window i
|
|
294
|
+
exit repeat
|
|
295
|
+
end if
|
|
296
|
+
end try
|
|
297
|
+
end repeat
|
|
298
|
+
end if
|
|
197
299
|
try
|
|
198
|
-
|
|
199
|
-
return
|
|
300
|
+
perform action "AXRaise" of w
|
|
200
301
|
end try
|
|
201
302
|
try
|
|
202
303
|
click (first UI element of w whose role is "AXWebArea")
|
|
203
|
-
|
|
304
|
+
delay 0.08
|
|
305
|
+
if my atmSafeToPaste() then return
|
|
204
306
|
end try
|
|
205
307
|
try
|
|
206
308
|
click (first UI element of scroll area 1 of w whose role is "AXWebArea")
|
|
207
|
-
|
|
309
|
+
delay 0.08
|
|
310
|
+
if my atmSafeToPaste() then return
|
|
208
311
|
end try
|
|
209
312
|
try
|
|
210
313
|
click (first UI element of scroll area 1 of splitter group 1 of w whose role is "AXWebArea")
|
|
211
|
-
|
|
314
|
+
delay 0.08
|
|
315
|
+
if my atmSafeToPaste() then return
|
|
212
316
|
end try
|
|
213
317
|
try
|
|
214
318
|
click (first UI element of scroll area 1 of group 1 of splitter group 1 of w whose role is "AXWebArea")
|
|
215
|
-
|
|
319
|
+
delay 0.08
|
|
320
|
+
if my atmSafeToPaste() then return
|
|
216
321
|
end try
|
|
322
|
+
set atmTallText to missing value
|
|
323
|
+
set atmSubjectElem to missing value
|
|
217
324
|
set atmQueue to UI elements of w
|
|
218
325
|
set atmWalked to 0
|
|
219
|
-
repeat while (count of atmQueue) > 0 and atmWalked <
|
|
326
|
+
repeat while (count of atmQueue) > 0 and atmWalked < 120
|
|
220
327
|
set atmWalked to atmWalked + 1
|
|
221
328
|
set atmElem to item 1 of atmQueue
|
|
222
329
|
if (count of atmQueue) is 1 then
|
|
@@ -226,14 +333,33 @@ export function buildMailBodyPasteHandler() {
|
|
|
226
333
|
end if
|
|
227
334
|
try
|
|
228
335
|
if (role of atmElem as string) is "AXWebArea" then
|
|
229
|
-
|
|
230
|
-
|
|
336
|
+
set {ew, eh} to size of atmElem
|
|
337
|
+
if eh >= ${BODY_MIN_H} then
|
|
338
|
+
click atmElem
|
|
339
|
+
delay 0.08
|
|
340
|
+
if my atmSafeToPaste() then return
|
|
341
|
+
end if
|
|
231
342
|
end if
|
|
232
343
|
end try
|
|
233
344
|
try
|
|
234
345
|
if (role of atmElem as string) is "AXTextArea" then
|
|
235
|
-
set
|
|
236
|
-
|
|
346
|
+
set {tw, th} to size of atmElem
|
|
347
|
+
if th >= ${BODY_MIN_H} and atmTallText is missing value then set atmTallText to atmElem
|
|
348
|
+
end if
|
|
349
|
+
end try
|
|
350
|
+
try
|
|
351
|
+
set r to (role of atmElem as string)
|
|
352
|
+
if r is "AXTextField" or r is "AXTextArea" then
|
|
353
|
+
set bits to ""
|
|
354
|
+
try
|
|
355
|
+
set bits to bits & (description of atmElem as string)
|
|
356
|
+
end try
|
|
357
|
+
try
|
|
358
|
+
set bits to bits & (name of atmElem as string)
|
|
359
|
+
end try
|
|
360
|
+
ignoring case
|
|
361
|
+
if bits contains "Subject" and atmSubjectElem is missing value then set atmSubjectElem to atmElem
|
|
362
|
+
end ignoring
|
|
237
363
|
end if
|
|
238
364
|
end try
|
|
239
365
|
try
|
|
@@ -243,14 +369,52 @@ export function buildMailBodyPasteHandler() {
|
|
|
243
369
|
end repeat
|
|
244
370
|
end try
|
|
245
371
|
end repeat
|
|
372
|
+
try
|
|
373
|
+
set ta to text area 1 of scroll area 1 of w
|
|
374
|
+
set {tw, th} to size of ta
|
|
375
|
+
if th >= ${BODY_MIN_H} then
|
|
376
|
+
set focused of ta to true
|
|
377
|
+
delay 0.08
|
|
378
|
+
if my atmSafeToPaste() then return
|
|
379
|
+
end if
|
|
380
|
+
end try
|
|
381
|
+
if atmTallText is not missing value then
|
|
382
|
+
try
|
|
383
|
+
set focused of atmTallText to true
|
|
384
|
+
end try
|
|
385
|
+
try
|
|
386
|
+
click atmTallText
|
|
387
|
+
end try
|
|
388
|
+
delay 0.08
|
|
389
|
+
if my atmSafeToPaste() then return
|
|
390
|
+
end if
|
|
391
|
+
try
|
|
392
|
+
click (first text field of w whose description contains "Subject")
|
|
393
|
+
delay 0.08
|
|
394
|
+
key code 48
|
|
395
|
+
delay 0.08
|
|
396
|
+
if my atmSafeToPaste() then return
|
|
397
|
+
end try
|
|
398
|
+
if atmSubjectElem is not missing value then
|
|
399
|
+
try
|
|
400
|
+
click atmSubjectElem
|
|
401
|
+
delay 0.08
|
|
402
|
+
key code 48
|
|
403
|
+
delay 0.08
|
|
404
|
+
if my atmSafeToPaste() then return
|
|
405
|
+
end try
|
|
406
|
+
end if
|
|
246
407
|
set {wx, wy} to position of w
|
|
247
408
|
set {ww, wh} to size of w
|
|
248
409
|
click at {(wx + (ww div 2)) as integer, (wy + ((wh * 2) div 3)) as integer}
|
|
410
|
+
delay 0.08
|
|
411
|
+
if my atmSafeToPaste() then return
|
|
412
|
+
error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
249
413
|
end tell
|
|
250
414
|
end tell
|
|
251
415
|
end atmFocusMailBody
|
|
252
416
|
|
|
253
|
-
on atmPasteMailBody(bodyText)
|
|
417
|
+
on atmPasteMailBody(bodyText, expectedSubject)
|
|
254
418
|
set savedClip to ""
|
|
255
419
|
set hadClip to false
|
|
256
420
|
try
|
|
@@ -259,15 +423,16 @@ on atmPasteMailBody(bodyText)
|
|
|
259
423
|
end try
|
|
260
424
|
set the clipboard to bodyText
|
|
261
425
|
tell application "Mail" to activate
|
|
262
|
-
delay 0.
|
|
426
|
+
delay 0.4
|
|
263
427
|
try
|
|
264
|
-
atmFocusMailBody()
|
|
428
|
+
atmFocusMailBody(expectedSubject)
|
|
429
|
+
if atmSafeToPaste() is false then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
265
430
|
tell application "System Events"
|
|
266
431
|
tell process "Mail"
|
|
267
432
|
keystroke "v" using command down
|
|
268
433
|
end tell
|
|
269
434
|
end tell
|
|
270
|
-
delay 0.
|
|
435
|
+
delay 0.2
|
|
271
436
|
on error errMsg number errNum
|
|
272
437
|
if hadClip then set the clipboard to savedClip
|
|
273
438
|
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
@@ -277,8 +442,11 @@ on atmPasteMailBody(bodyText)
|
|
|
277
442
|
end atmPasteMailBody`;
|
|
278
443
|
}
|
|
279
444
|
|
|
280
|
-
function
|
|
445
|
+
function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
|
|
281
446
|
const toCount = asInteger(to.length, { min: 1, max: 99, field: "toCount" });
|
|
447
|
+
const ccCount = asInteger(cc.length, { min: 0, max: 99, field: "ccCount" });
|
|
448
|
+
const bccCount = asInteger(bcc.length, { min: 0, max: 99, field: "bccCount" });
|
|
449
|
+
const needle = composeBodyNeedle(body);
|
|
282
450
|
const stillThere = to
|
|
283
451
|
.map((address, i) => {
|
|
284
452
|
const n = asInteger(i, { min: 0, max: 99, field: "recipientIndex" });
|
|
@@ -290,8 +458,39 @@ function recipientStillPresentChecks(to) {
|
|
|
290
458
|
if atmFound${n} is false then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"`;
|
|
291
459
|
})
|
|
292
460
|
.join("\n");
|
|
461
|
+
const bodyCheck = needle
|
|
462
|
+
? ` set atmGotBody to ""
|
|
463
|
+
try
|
|
464
|
+
set atmGotBody to content of newMessage as string
|
|
465
|
+
end try
|
|
466
|
+
if atmGotBody is "" then
|
|
467
|
+
try
|
|
468
|
+
save newMessage
|
|
469
|
+
end try
|
|
470
|
+
delay 0.2
|
|
471
|
+
try
|
|
472
|
+
set atmGotBody to content of newMessage as string
|
|
473
|
+
end try
|
|
474
|
+
end if
|
|
475
|
+
if atmGotBody does not contain ${asString(needle)} then
|
|
476
|
+
try
|
|
477
|
+
set atmFe to my atmFocusedElement()
|
|
478
|
+
if atmFe is not missing value then
|
|
479
|
+
tell application "System Events"
|
|
480
|
+
set atmAxVal to value of atmFe as string
|
|
481
|
+
if atmAxVal contains ${asString(needle)} then set atmGotBody to atmAxVal
|
|
482
|
+
end tell
|
|
483
|
+
end if
|
|
484
|
+
end try
|
|
485
|
+
end if
|
|
486
|
+
if atmGotBody does not contain ${asString(needle)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"`
|
|
487
|
+
: "";
|
|
293
488
|
return ` if (count of to recipients of newMessage) is not ${toCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
294
|
-
${
|
|
489
|
+
if (count of cc recipients of newMessage) is not ${ccCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
490
|
+
if (count of bcc recipients of newMessage) is not ${bccCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
491
|
+
${stillThere}
|
|
492
|
+
if (subject of newMessage as string) is not ${asString(subject)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
493
|
+
${bodyCheck}`;
|
|
295
494
|
}
|
|
296
495
|
|
|
297
496
|
/**
|
|
@@ -303,9 +502,10 @@ ${stillThere}`;
|
|
|
303
502
|
*
|
|
304
503
|
* Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
|
|
305
504
|
* cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
|
|
306
|
-
* Recipients are AppleScript
|
|
307
|
-
* into the compose window (native editor) via System Events
|
|
308
|
-
*
|
|
505
|
+
* Recipients and subject are AppleScript properties (never paste). The body
|
|
506
|
+
* is pasted into the compose window (native editor) via System Events only
|
|
507
|
+
* after caret focus leaves the header. If paste lands in To/Subject, or the
|
|
508
|
+
* body never receives the intended text, abort + delete (nothing sent).
|
|
309
509
|
*/
|
|
310
510
|
export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
|
|
311
511
|
const recipients = [
|
|
@@ -316,9 +516,9 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
|
|
|
316
516
|
|
|
317
517
|
const plainBody = composeNativeBody(body, { html });
|
|
318
518
|
const pasteAndVerify = plainBody
|
|
319
|
-
? ` atmPasteMailBody(${asString(plainBody)})
|
|
519
|
+
? ` atmPasteMailBody(${asString(plainBody)}, ${asString(subject)})
|
|
320
520
|
tell application "Mail"
|
|
321
|
-
${
|
|
521
|
+
${composeMisdirectChecks({ to, cc, bcc, subject, body: plainBody })}
|
|
322
522
|
end tell`
|
|
323
523
|
: "";
|
|
324
524
|
|
|
@@ -710,8 +910,22 @@ export function isMailAccessibilityDenial(result) {
|
|
|
710
910
|
return text.includes("accessibility_denied") || text.includes("assistive access");
|
|
711
911
|
}
|
|
712
912
|
|
|
913
|
+
export function isMailBodyFocusFailed(result) {
|
|
914
|
+
if (!result) return false;
|
|
915
|
+
return String(result.error || "").toLowerCase().includes("body_focus_failed");
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
export function isMailBodyPasteMisdirected(result) {
|
|
919
|
+
if (!result) return false;
|
|
920
|
+
const text = String(result.error || "").toLowerCase();
|
|
921
|
+
return text.includes("body_paste_misdirected") || text.includes("body_focus_failed");
|
|
922
|
+
}
|
|
923
|
+
|
|
713
924
|
function failure(action, summary, result, secrets) {
|
|
714
925
|
const err = String((result && result.error) || "").toLowerCase();
|
|
926
|
+
if (err.includes("body_focus_failed")) {
|
|
927
|
+
return `${action} failed — attempted to ${summary}. The compose caret could not be moved into the message body; nothing was sent.`;
|
|
928
|
+
}
|
|
715
929
|
if (err.includes("body_paste_misdirected")) {
|
|
716
930
|
return `${action} failed — attempted to ${summary}. The body was pasted into a header field instead of the message body; nothing was sent.`;
|
|
717
931
|
}
|