apple-tools-mcp 2.0.6 → 2.0.8
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 +8 -3
- package/index.js +3 -5
- package/lib/appleScript.js +1 -1
- package/lib/calendarWrite.js +5 -4
- package/lib/contactsWrite.js +4 -3
- package/lib/mailWrite.js +466 -99
- package/lib/messagesWrite.js +3 -2
- package/lib/writeGuards.js +18 -0
- package/lib/writeTools.js +20 -3
- package/package.json +1 -1
- package/scripts/smoke-writes.js +6 -5
package/README.md
CHANGED
|
@@ -446,6 +446,7 @@ Two arguments are available on **every** write tool:
|
|
|
446
446
|
- **Deletes always need `confirm: true`** — `mail_trash`, `calendar_remove`, `contacts_remove`, and `contacts_edit` when it clears every email or phone. Without it the call returns a `CONFIRMATION REQUIRED` preview and changes nothing. There is no bulk delete tool and no silent mass delete: one id per call.
|
|
447
447
|
- **Multi-recipient sends always need `confirm: true`** — `mail_send` / `mail_forward` when `to` + `cc` + `bcc` total more than one address, `mail_reply` with `reply_all: true`, and `messages_send` to multiple handles or to a group chat. A single-recipient send runs on the first call.
|
|
448
448
|
- **`mail_draft` is exempt from the recipient rule** because a draft is never delivered.
|
|
449
|
+
- **`dry_run` and confirm-blocked calls are not deliveries.** Planned writes return `ok: false`, `planned: true`, `delivered: false`, and MCP sets `isError: true`, so a client that only checks `isError` cannot treat a preview as sent. The text is `DRY RUN` / `CONFIRMATION REQUIRED` and never `verified in Sent`. Re-run with `dry_run=false` (and `confirm=true` when required) to actually send.
|
|
449
450
|
- Responses name what happened (tool, ids, recipients, titles) and never echo message bodies — including on the error path.
|
|
450
451
|
- Writes never invent data. A missing or malformed `message_id`, `event_id`, `contact_id`, `chat_id`, or recipient is refused with a message saying so. Calendar times must be explicit local datetimes (`YYYY-MM-DD HH:MM`); natural language such as "next Tuesday" is rejected for writes.
|
|
451
452
|
|
|
@@ -465,13 +466,17 @@ Two arguments are available on **every** write tool:
|
|
|
465
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 paste the body into the compose window (System Events). Mail's `mailto` command was shipped in 2.0.4 and **fails on Mini/MacBook Mail**: it does not return an outgoing message (`newMessage` undefined, AppleScript **-2753**). On current Mail (Ventura+, FB11734014) `content` / `html content` store the body as a citation: every plain-text line prefixed with `>`, plus a `multipart/alternative` HTML part wrapped in `<blockquote type="cite">`. Desktop Mail often hides the bar with inline styles; iOS Mail paints the whole body purple with a left quote bar — even when the subject is not `Re:`/`Fwd:` and there is no `In-Reply-To`. Reply and forward still quote the original, which is expected.
|
|
467
468
|
|
|
468
|
-
Paste uses System Events, so **node needs Accessibility** (Privacy & Security → Accessibility) in addition to Automation → Mail. That Accessibility deny is not a Mail Automation deny (`-1743` / `-10004`). Body focus uses `text area` / `scroll area` and `UI element whose role is "AXWebArea"` — never the System Events class `web area`, which does not compile on macOS 26.x (**-2741**, “Expected class name but found identifier”).
|
|
469
|
+
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”).
|
|
469
470
|
|
|
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
|
+
**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.
|
|
474
|
+
|
|
475
|
+
**Manual prove (2.0.8 on the Mac host):** `mail_send` a short plain message and a short `body_format: "html"` message whose body looks like ordinary paragraphs (for example `<p>Quick note</p>`), neither with a `Re:`/`Fwd:` subject. 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** on body focus). That quote-prefix dual-host Sent prove is a separate bar from the 2.0.7 verify contract.
|
|
471
476
|
|
|
472
477
|
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.
|
|
473
478
|
|
|
474
|
-
**Send timeout vs TCC — check Sent before retrying.** `mail_send` / `mail_reply` / `mail_forward` can hang after Mail has already put the message in Sent. That hang is a **timeout** (`ETIMEDOUT` / `-1712` / AppleEvent timed out), not a TCC deny. The same hang-vs-TCC split applies to **find / reply / open before send** — an Allowed `ETIMEDOUT` there is never `MAIL_TCC_GUIDANCE`. Real Mail Automation denials report **`-1743`**, **`-10004`**, or “not authorized to send Apple events”. After a send hang the tool looks in Sent (and Outbox) and returns **success** if the message is there — never label a delivered send as TCC fail or timeout. Replies match `In-Reply-To` or an exact recent `Re:` + original subject (the fallback compose does not set reply headers). Forwards match the intended recipient plus an exact `Fwd:` subject or the original Message-ID in a forwarded body — not `In-Reply-To`, and not an unrelated `Fwd:` that only shares a To. Neither scan treats the original itself as this send. If Sent-verify misses, the error tells you it was a hang and to check Sent. **Clients must Sent-check before retrying a timed-out send**; retrying a message that already landed sends a second copy. This is not a silent TCC grant.
|
|
479
|
+
**Send timeout vs TCC — check Sent before retrying.** `mail_send` / `mail_reply` / `mail_forward` can hang after Mail has already put the message in Sent. That hang is a **timeout** (`ETIMEDOUT` / `-1712` / AppleEvent timed out), not a TCC deny. The same hang-vs-TCC split applies to **find / reply / open before send** — an Allowed `ETIMEDOUT` there is never `MAIL_TCC_GUIDANCE`. Real Mail Automation denials report **`-1743`**, **`-10004`**, or “not authorized to send Apple events”. After a send hang the tool looks in Sent (and Outbox) and returns **success** if the message is there — never label a delivered send as TCC fail or timeout. Replies match `In-Reply-To` or an exact recent `Re:` + original subject (the fallback compose does not set reply headers). Forwards match the intended recipient plus an exact `Fwd:` subject or the original Message-ID in a forwarded body — not `In-Reply-To`, and not an unrelated `Fwd:` that only shares a To. Compose (`mail_send`) matches the intended **To plus exact subject** (and Message-ID when available), never subject alone. Neither scan treats the original itself as this send. A real send that returns without throw is also verified the same way before success. If Sent-verify misses, the error tells you it was a hang (or a verify-miss after send returned) and to check Sent. **Clients must Sent-check before retrying a timed-out send**; retrying a message that already landed sends a second copy. This is not a silent TCC grant.
|
|
475
480
|
|
|
476
481
|
### Messages write tool
|
|
477
482
|
|
package/index.js
CHANGED
|
@@ -27,7 +27,8 @@ import {
|
|
|
27
27
|
WRITE_TOOL_DEFINITIONS,
|
|
28
28
|
isWriteTool,
|
|
29
29
|
dispatchWriteTool,
|
|
30
|
-
executeWriteToolLocally
|
|
30
|
+
executeWriteToolLocally,
|
|
31
|
+
mcpWriteResult
|
|
31
32
|
} from "./lib/writeTools.js";
|
|
32
33
|
import { startWriteBridgeServer, defaultSocketPath } from "./lib/writeBridge.js";
|
|
33
34
|
import { closeEventKitSession, ensureEventKitSession } from "./lib/eventKitSession.js";
|
|
@@ -1373,10 +1374,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1373
1374
|
socketPath: WRITE_SOCKET_PATH,
|
|
1374
1375
|
log: (msg) => console.error(msg)
|
|
1375
1376
|
});
|
|
1376
|
-
return
|
|
1377
|
-
content: [{ type: "text", text: writeResult.message }],
|
|
1378
|
-
...(writeResult.ok === false ? { isError: true } : {})
|
|
1379
|
-
};
|
|
1377
|
+
return mcpWriteResult(writeResult);
|
|
1380
1378
|
}
|
|
1381
1379
|
|
|
1382
1380
|
switch (name) {
|
package/lib/appleScript.js
CHANGED
|
@@ -353,7 +353,7 @@ export const MAIL_SEND_TIMEOUT_GUIDANCE =
|
|
|
353
353
|
"Mail AppleScript timed out (ETIMEDOUT / -1712 / AppleEvent timed out) — this is a find/reply/send hang (including find/reply/open before send), not a TCC / Automation deny. " +
|
|
354
354
|
"Real Mail Automation denials report -1743, -10004, or \"not authorized to send Apple events\". " +
|
|
355
355
|
"A timeout can happen after Mail already delivered the message. Check Sent (and Outbox) for this send before retrying; a retry after a successful delivery sends a second copy. " +
|
|
356
|
-
"This tool verifies Sent when a send hangs; if nothing is there yet, wait and look again rather than immediately resending.";
|
|
356
|
+
"This tool verifies Sent/Outbox when a send hangs (To + subject, Message-ID when available — never subject alone); if nothing is there yet, wait and look again rather than immediately resending.";
|
|
357
357
|
|
|
358
358
|
/**
|
|
359
359
|
* Messages writes are a separate Automation target from Mail / Contacts / Calendar.
|
package/lib/calendarWrite.js
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "./appleScript.js";
|
|
23
23
|
import {
|
|
24
24
|
planWrite,
|
|
25
|
+
plannedWriteResult,
|
|
25
26
|
validateEventId,
|
|
26
27
|
validateEventKitId,
|
|
27
28
|
validateCalendarName,
|
|
@@ -772,7 +773,7 @@ export function calendarAdd(args = {}) {
|
|
|
772
773
|
`${alerts.minutes.length ? ` with alerts ${alerts.minutes.join(", ")} min before` : ""}`;
|
|
773
774
|
|
|
774
775
|
const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
|
|
775
|
-
if (!plan.proceed) return
|
|
776
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
776
777
|
|
|
777
778
|
// Non-recurring creates MUST go through EventKit. Mini 8be6cd5: EventKit
|
|
778
779
|
// add failed (likely writeOnly cannot list calendars) and we silently
|
|
@@ -1037,7 +1038,7 @@ export function calendarEdit(args = {}) {
|
|
|
1037
1038
|
|
|
1038
1039
|
const summary = `update event ${eventId}: ${changed.join(", ")}`;
|
|
1039
1040
|
const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
|
|
1040
|
-
if (!plan.proceed) return
|
|
1041
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1041
1042
|
|
|
1042
1043
|
const preferEventKit = Boolean(eventKitId) || eventId.includes(":");
|
|
1043
1044
|
if (preferEventKit && !recurrence.rule) {
|
|
@@ -1219,7 +1220,7 @@ export function calendarRemove(args = {}) {
|
|
|
1219
1220
|
dryRun: isFlagTrue(args.dry_run),
|
|
1220
1221
|
confirm: isFlagTrue(args.confirm)
|
|
1221
1222
|
});
|
|
1222
|
-
if (!plan.proceed) return
|
|
1223
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1223
1224
|
|
|
1224
1225
|
// writeOnly cannot re-query; prefer the EventKit session that cached
|
|
1225
1226
|
// the EKEvent from create. One-shot lookup is a full-access fallback.
|
|
@@ -1339,7 +1340,7 @@ export function calendarRsvp(args = {}) {
|
|
|
1339
1340
|
|
|
1340
1341
|
const summary = `RSVP ${responseRaw} to event ${eventId}`;
|
|
1341
1342
|
const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
|
|
1342
|
-
if (!plan.proceed) return
|
|
1343
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
1343
1344
|
|
|
1344
1345
|
const result = runAppleScript(
|
|
1345
1346
|
buildRsvpScript({ eventId, status: RSVP_STATUS[responseRaw], attendeeEmail }),
|
package/lib/contactsWrite.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { safeOpenApp } from "./shell.js";
|
|
21
21
|
import {
|
|
22
22
|
planWrite,
|
|
23
|
+
plannedWriteResult,
|
|
23
24
|
normalizeList,
|
|
24
25
|
isEmailAddress,
|
|
25
26
|
isPhoneNumber,
|
|
@@ -254,7 +255,7 @@ export function contactsAdd(args = {}, deps = {}) {
|
|
|
254
255
|
`${phones.phones.length ? ` and ${phones.phones.length} phone number(s)` : ""}`;
|
|
255
256
|
|
|
256
257
|
const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
|
|
257
|
-
if (!plan.proceed) return
|
|
258
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
258
259
|
|
|
259
260
|
const properties = {};
|
|
260
261
|
if (firstName.text) properties["first name"] = firstName.text;
|
|
@@ -357,7 +358,7 @@ export function contactsEdit(args = {}, deps = {}) {
|
|
|
357
358
|
dryRun: isFlagTrue(args.dry_run),
|
|
358
359
|
confirm: isFlagTrue(args.confirm)
|
|
359
360
|
});
|
|
360
|
-
if (!plan.proceed) return
|
|
361
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
361
362
|
|
|
362
363
|
const ready = ensureContactsAppReady(deps);
|
|
363
364
|
if (!ready.ok) return { ok: false, message: failure(action, summary, ready) };
|
|
@@ -413,7 +414,7 @@ export function contactsRemove(args = {}, deps = {}) {
|
|
|
413
414
|
dryRun: isFlagTrue(args.dry_run),
|
|
414
415
|
confirm: isFlagTrue(args.confirm)
|
|
415
416
|
});
|
|
416
|
-
if (!plan.proceed) return
|
|
417
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
417
418
|
|
|
418
419
|
const ready = ensureContactsAppReady(deps);
|
|
419
420
|
if (!ready.ok) return { ok: false, message: failure(action, summary, ready) };
|
package/lib/mailWrite.js
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
* is resolved to a Message-ID by reading the .emlx headers, so callers never
|
|
8
8
|
* have to invent an identifier.
|
|
9
9
|
*
|
|
10
|
-
* After a send/reply/forward hang, Sent
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* After a real send (and after a send/reply/forward hang), Sent and Outbox
|
|
11
|
+
* are checked before the tool reports success. AppleScript `send` returning
|
|
12
|
+
* without throw is not enough. A message already in Sent or still in Outbox
|
|
13
|
+
* is success — never a TCC fail. Hang recover matches To + subject (and
|
|
14
|
+
* Message-ID when available), never subject alone. ETIMEDOUT / -1712 is a
|
|
15
|
+
* timeout (find/reply/open before send too); -1743 / -10004 is a hard deny.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import fs from "fs";
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
} from "./appleScript.js";
|
|
30
32
|
import {
|
|
31
33
|
planWrite,
|
|
34
|
+
plannedWriteResult,
|
|
32
35
|
validateEmailList,
|
|
33
36
|
validateBody,
|
|
34
37
|
validateSubject,
|
|
@@ -152,6 +155,9 @@ export function probeMailAutomation() {
|
|
|
152
155
|
|
|
153
156
|
export const ACCESSIBILITY_DENIED_SENTINEL = "ACCESSIBILITY_DENIED";
|
|
154
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;
|
|
155
161
|
|
|
156
162
|
/**
|
|
157
163
|
* Body text for native Mail compose (clipboard paste, not AppleScript content).
|
|
@@ -169,51 +175,117 @@ export function composeNativeBody(body, { html = false } = {}) {
|
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
/**
|
|
172
|
-
*
|
|
178
|
+
* Distinctive first line used to prove the native editor received the body.
|
|
179
|
+
* Full multiline `contains` can miss when Mail stores an HTML alternative.
|
|
180
|
+
*/
|
|
181
|
+
export function composeBodyNeedle(body) {
|
|
182
|
+
const text = body === undefined || body === null ? "" : String(body);
|
|
183
|
+
const line = text.split(/\r?\n/).find((candidate) => candidate.trim()) || text;
|
|
184
|
+
const needle = line.trim();
|
|
185
|
+
return needle.length > 120 ? needle.slice(0, 120) : needle;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const BODY_MIN_H = asInteger(MAIL_BODY_MIN_AX_HEIGHT, { min: 1, max: 500, field: "bodyMinHeight" });
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* AppleScript handlers: focus the compose body (not To/Subject) and paste.
|
|
173
192
|
* System Events needs Accessibility; Mail make/send still needs Automation.
|
|
174
193
|
*
|
|
175
194
|
* macOS 26.x System Events has no `web area` class (compile -2741,
|
|
176
195
|
* "Expected class name but found identifier"). Hosts compile
|
|
177
|
-
* `text area` / `scroll area` and `UI element whose role is
|
|
196
|
+
* `text area` / `scroll area` / `text field` and `UI element whose role is
|
|
197
|
+
* "AXWebArea"`. Never focus the first AXTextArea blindly — on MacBook Mail
|
|
198
|
+
* that is a header field, and Cmd-V becomes BODY_PASTE_MISDIRECTED.
|
|
199
|
+
*
|
|
200
|
+
* Strategies, each fail-closed unless the caret left the header:
|
|
201
|
+
* 1. Click a tall AXWebArea (HTML compose body)
|
|
202
|
+
* 2. Focus a tall `text area` (plain compose body), never a short header
|
|
203
|
+
* 3. Click Subject then Tab into the body (stable Mini/MacBook order)
|
|
204
|
+
* 4. Click the lower two-thirds of the compose window
|
|
178
205
|
*/
|
|
179
206
|
export function buildMailBodyPasteHandler() {
|
|
180
|
-
return `on
|
|
207
|
+
return `on atmSafeToPaste()
|
|
181
208
|
tell application "System Events"
|
|
182
209
|
tell process "Mail"
|
|
183
|
-
set frontmost to true
|
|
184
|
-
if (count of windows) < 1 then error "MAIL_COMPOSE_WINDOW_MISSING"
|
|
185
|
-
set w to first window
|
|
186
|
-
try
|
|
187
|
-
set focused of text area 1 of scroll area 1 of w to true
|
|
188
|
-
return
|
|
189
|
-
end try
|
|
190
210
|
try
|
|
191
|
-
set
|
|
192
|
-
|
|
211
|
+
set fe to focused UI element
|
|
212
|
+
set r to (role of fe) as string
|
|
213
|
+
if r is "AXTextField" then return false
|
|
214
|
+
if r is "AXComboBox" then return false
|
|
215
|
+
if r is "AXButton" then return false
|
|
216
|
+
if r is "AXMenuButton" then return false
|
|
217
|
+
if r is "AXPopUpButton" then return false
|
|
218
|
+
set eh to 0
|
|
219
|
+
try
|
|
220
|
+
set {ew, eh} to size of fe
|
|
221
|
+
end try
|
|
222
|
+
if r is "AXTextArea" and eh < ${BODY_MIN_H} then return false
|
|
223
|
+
ignoring case
|
|
224
|
+
set bits to ""
|
|
225
|
+
try
|
|
226
|
+
set bits to bits & (description of fe as string) & " "
|
|
227
|
+
end try
|
|
228
|
+
try
|
|
229
|
+
set bits to bits & (name of fe as string) & " "
|
|
230
|
+
end try
|
|
231
|
+
if bits contains "To:" then return false
|
|
232
|
+
if bits contains "Cc:" then return false
|
|
233
|
+
if bits contains "Bcc:" then return false
|
|
234
|
+
if bits contains "Subject" then return false
|
|
235
|
+
end ignoring
|
|
236
|
+
if r is "AXWebArea" then return true
|
|
237
|
+
if r is "AXTextArea" then return true
|
|
238
|
+
if eh >= ${BODY_MIN_H} then return true
|
|
193
239
|
end try
|
|
240
|
+
return false
|
|
241
|
+
end tell
|
|
242
|
+
end tell
|
|
243
|
+
end atmSafeToPaste
|
|
244
|
+
|
|
245
|
+
on atmFocusMailBody(expectedSubject)
|
|
246
|
+
tell application "System Events"
|
|
247
|
+
tell process "Mail"
|
|
248
|
+
set frontmost to true
|
|
249
|
+
if (count of windows) < 1 then error "MAIL_COMPOSE_WINDOW_MISSING"
|
|
250
|
+
set w to window 1
|
|
251
|
+
if expectedSubject is not "" then
|
|
252
|
+
repeat with i from 1 to (count of windows)
|
|
253
|
+
try
|
|
254
|
+
if (name of window i as string) is expectedSubject then
|
|
255
|
+
set w to window i
|
|
256
|
+
exit repeat
|
|
257
|
+
end if
|
|
258
|
+
end try
|
|
259
|
+
end repeat
|
|
260
|
+
end if
|
|
194
261
|
try
|
|
195
|
-
|
|
196
|
-
return
|
|
262
|
+
perform action "AXRaise" of w
|
|
197
263
|
end try
|
|
198
264
|
try
|
|
199
265
|
click (first UI element of w whose role is "AXWebArea")
|
|
200
|
-
|
|
266
|
+
delay 0.08
|
|
267
|
+
if my atmSafeToPaste() then return
|
|
201
268
|
end try
|
|
202
269
|
try
|
|
203
270
|
click (first UI element of scroll area 1 of w whose role is "AXWebArea")
|
|
204
|
-
|
|
271
|
+
delay 0.08
|
|
272
|
+
if my atmSafeToPaste() then return
|
|
205
273
|
end try
|
|
206
274
|
try
|
|
207
275
|
click (first UI element of scroll area 1 of splitter group 1 of w whose role is "AXWebArea")
|
|
208
|
-
|
|
276
|
+
delay 0.08
|
|
277
|
+
if my atmSafeToPaste() then return
|
|
209
278
|
end try
|
|
210
279
|
try
|
|
211
280
|
click (first UI element of scroll area 1 of group 1 of splitter group 1 of w whose role is "AXWebArea")
|
|
212
|
-
|
|
281
|
+
delay 0.08
|
|
282
|
+
if my atmSafeToPaste() then return
|
|
213
283
|
end try
|
|
284
|
+
set atmTallText to missing value
|
|
285
|
+
set atmSubjectElem to missing value
|
|
214
286
|
set atmQueue to UI elements of w
|
|
215
287
|
set atmWalked to 0
|
|
216
|
-
repeat while (count of atmQueue) > 0 and atmWalked <
|
|
288
|
+
repeat while (count of atmQueue) > 0 and atmWalked < 120
|
|
217
289
|
set atmWalked to atmWalked + 1
|
|
218
290
|
set atmElem to item 1 of atmQueue
|
|
219
291
|
if (count of atmQueue) is 1 then
|
|
@@ -223,14 +295,33 @@ export function buildMailBodyPasteHandler() {
|
|
|
223
295
|
end if
|
|
224
296
|
try
|
|
225
297
|
if (role of atmElem as string) is "AXWebArea" then
|
|
226
|
-
|
|
227
|
-
|
|
298
|
+
set {ew, eh} to size of atmElem
|
|
299
|
+
if eh >= ${BODY_MIN_H} then
|
|
300
|
+
click atmElem
|
|
301
|
+
delay 0.08
|
|
302
|
+
if my atmSafeToPaste() then return
|
|
303
|
+
end if
|
|
228
304
|
end if
|
|
229
305
|
end try
|
|
230
306
|
try
|
|
231
307
|
if (role of atmElem as string) is "AXTextArea" then
|
|
232
|
-
set
|
|
233
|
-
|
|
308
|
+
set {tw, th} to size of atmElem
|
|
309
|
+
if th >= ${BODY_MIN_H} and atmTallText is missing value then set atmTallText to atmElem
|
|
310
|
+
end if
|
|
311
|
+
end try
|
|
312
|
+
try
|
|
313
|
+
set r to (role of atmElem as string)
|
|
314
|
+
if r is "AXTextField" or r is "AXTextArea" then
|
|
315
|
+
set bits to ""
|
|
316
|
+
try
|
|
317
|
+
set bits to bits & (description of atmElem as string)
|
|
318
|
+
end try
|
|
319
|
+
try
|
|
320
|
+
set bits to bits & (name of atmElem as string)
|
|
321
|
+
end try
|
|
322
|
+
ignoring case
|
|
323
|
+
if bits contains "Subject" and atmSubjectElem is missing value then set atmSubjectElem to atmElem
|
|
324
|
+
end ignoring
|
|
234
325
|
end if
|
|
235
326
|
end try
|
|
236
327
|
try
|
|
@@ -240,14 +331,52 @@ export function buildMailBodyPasteHandler() {
|
|
|
240
331
|
end repeat
|
|
241
332
|
end try
|
|
242
333
|
end repeat
|
|
334
|
+
try
|
|
335
|
+
set ta to text area 1 of scroll area 1 of w
|
|
336
|
+
set {tw, th} to size of ta
|
|
337
|
+
if th >= ${BODY_MIN_H} then
|
|
338
|
+
set focused of ta to true
|
|
339
|
+
delay 0.08
|
|
340
|
+
if my atmSafeToPaste() then return
|
|
341
|
+
end if
|
|
342
|
+
end try
|
|
343
|
+
if atmTallText is not missing value then
|
|
344
|
+
try
|
|
345
|
+
set focused of atmTallText to true
|
|
346
|
+
end try
|
|
347
|
+
try
|
|
348
|
+
click atmTallText
|
|
349
|
+
end try
|
|
350
|
+
delay 0.08
|
|
351
|
+
if my atmSafeToPaste() then return
|
|
352
|
+
end if
|
|
353
|
+
try
|
|
354
|
+
click (first text field of w whose description contains "Subject")
|
|
355
|
+
delay 0.08
|
|
356
|
+
key code 48
|
|
357
|
+
delay 0.08
|
|
358
|
+
if my atmSafeToPaste() then return
|
|
359
|
+
end try
|
|
360
|
+
if atmSubjectElem is not missing value then
|
|
361
|
+
try
|
|
362
|
+
click atmSubjectElem
|
|
363
|
+
delay 0.08
|
|
364
|
+
key code 48
|
|
365
|
+
delay 0.08
|
|
366
|
+
if my atmSafeToPaste() then return
|
|
367
|
+
end try
|
|
368
|
+
end if
|
|
243
369
|
set {wx, wy} to position of w
|
|
244
370
|
set {ww, wh} to size of w
|
|
245
371
|
click at {(wx + (ww div 2)) as integer, (wy + ((wh * 2) div 3)) as integer}
|
|
372
|
+
delay 0.08
|
|
373
|
+
if my atmSafeToPaste() then return
|
|
374
|
+
error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
246
375
|
end tell
|
|
247
376
|
end tell
|
|
248
377
|
end atmFocusMailBody
|
|
249
378
|
|
|
250
|
-
on atmPasteMailBody(bodyText)
|
|
379
|
+
on atmPasteMailBody(bodyText, expectedSubject)
|
|
251
380
|
set savedClip to ""
|
|
252
381
|
set hadClip to false
|
|
253
382
|
try
|
|
@@ -256,15 +385,16 @@ on atmPasteMailBody(bodyText)
|
|
|
256
385
|
end try
|
|
257
386
|
set the clipboard to bodyText
|
|
258
387
|
tell application "Mail" to activate
|
|
259
|
-
delay 0.
|
|
388
|
+
delay 0.4
|
|
260
389
|
try
|
|
261
|
-
atmFocusMailBody()
|
|
390
|
+
atmFocusMailBody(expectedSubject)
|
|
391
|
+
if atmSafeToPaste() is false then error "${BODY_FOCUS_FAILED_SENTINEL}"
|
|
262
392
|
tell application "System Events"
|
|
263
393
|
tell process "Mail"
|
|
264
394
|
keystroke "v" using command down
|
|
265
395
|
end tell
|
|
266
396
|
end tell
|
|
267
|
-
delay 0.
|
|
397
|
+
delay 0.2
|
|
268
398
|
on error errMsg number errNum
|
|
269
399
|
if hadClip then set the clipboard to savedClip
|
|
270
400
|
if errMsg contains "assistive access" or errNum is -25211 then error "${ACCESSIBILITY_DENIED_SENTINEL}"
|
|
@@ -274,8 +404,11 @@ on atmPasteMailBody(bodyText)
|
|
|
274
404
|
end atmPasteMailBody`;
|
|
275
405
|
}
|
|
276
406
|
|
|
277
|
-
function
|
|
407
|
+
function composeMisdirectChecks({ to, cc = [], bcc = [], subject, body }) {
|
|
278
408
|
const toCount = asInteger(to.length, { min: 1, max: 99, field: "toCount" });
|
|
409
|
+
const ccCount = asInteger(cc.length, { min: 0, max: 99, field: "ccCount" });
|
|
410
|
+
const bccCount = asInteger(bcc.length, { min: 0, max: 99, field: "bccCount" });
|
|
411
|
+
const needle = composeBodyNeedle(body);
|
|
279
412
|
const stillThere = to
|
|
280
413
|
.map((address, i) => {
|
|
281
414
|
const n = asInteger(i, { min: 0, max: 99, field: "recipientIndex" });
|
|
@@ -287,8 +420,38 @@ function recipientStillPresentChecks(to) {
|
|
|
287
420
|
if atmFound${n} is false then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"`;
|
|
288
421
|
})
|
|
289
422
|
.join("\n");
|
|
423
|
+
const bodyCheck = needle
|
|
424
|
+
? ` set atmGotBody to ""
|
|
425
|
+
try
|
|
426
|
+
set atmGotBody to content of newMessage as string
|
|
427
|
+
end try
|
|
428
|
+
if atmGotBody is "" then
|
|
429
|
+
try
|
|
430
|
+
save newMessage
|
|
431
|
+
end try
|
|
432
|
+
delay 0.2
|
|
433
|
+
try
|
|
434
|
+
set atmGotBody to content of newMessage as string
|
|
435
|
+
end try
|
|
436
|
+
end if
|
|
437
|
+
if atmGotBody does not contain ${asString(needle)} then
|
|
438
|
+
try
|
|
439
|
+
tell application "System Events"
|
|
440
|
+
tell process "Mail"
|
|
441
|
+
set atmAxVal to value of focused UI element as string
|
|
442
|
+
if atmAxVal contains ${asString(needle)} then set atmGotBody to atmAxVal
|
|
443
|
+
end tell
|
|
444
|
+
end tell
|
|
445
|
+
end try
|
|
446
|
+
end if
|
|
447
|
+
if atmGotBody does not contain ${asString(needle)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"`
|
|
448
|
+
: "";
|
|
290
449
|
return ` if (count of to recipients of newMessage) is not ${toCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
291
|
-
${
|
|
450
|
+
if (count of cc recipients of newMessage) is not ${ccCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
451
|
+
if (count of bcc recipients of newMessage) is not ${bccCount} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
452
|
+
${stillThere}
|
|
453
|
+
if (subject of newMessage as string) is not ${asString(subject)} then error "${BODY_PASTE_MISDIRECTED_SENTINEL}"
|
|
454
|
+
${bodyCheck}`;
|
|
292
455
|
}
|
|
293
456
|
|
|
294
457
|
/**
|
|
@@ -300,9 +463,10 @@ ${stillThere}`;
|
|
|
300
463
|
*
|
|
301
464
|
* Do not set AppleScript `content` or `html content`: Ventura+ FB11734014
|
|
302
465
|
* cite-wraps those setters (`>` prefixes + `<blockquote type="cite">`).
|
|
303
|
-
* Recipients are AppleScript
|
|
304
|
-
* into the compose window (native editor) via System Events
|
|
305
|
-
*
|
|
466
|
+
* Recipients and subject are AppleScript properties (never paste). The body
|
|
467
|
+
* is pasted into the compose window (native editor) via System Events only
|
|
468
|
+
* after caret focus leaves the header. If paste lands in To/Subject, or the
|
|
469
|
+
* body never receives the intended text, abort + delete (nothing sent).
|
|
306
470
|
*/
|
|
307
471
|
export function buildComposeScript({ to, cc, bcc, subject, body, send, html = false }) {
|
|
308
472
|
const recipients = [
|
|
@@ -313,9 +477,9 @@ export function buildComposeScript({ to, cc, bcc, subject, body, send, html = fa
|
|
|
313
477
|
|
|
314
478
|
const plainBody = composeNativeBody(body, { html });
|
|
315
479
|
const pasteAndVerify = plainBody
|
|
316
|
-
? ` atmPasteMailBody(${asString(plainBody)})
|
|
480
|
+
? ` atmPasteMailBody(${asString(plainBody)}, ${asString(subject)})
|
|
317
481
|
tell application "Mail"
|
|
318
|
-
${
|
|
482
|
+
${composeMisdirectChecks({ to, cc, bcc, subject, body: plainBody })}
|
|
319
483
|
end tell`
|
|
320
484
|
: "";
|
|
321
485
|
|
|
@@ -336,9 +500,14 @@ on error errMsg number errNum
|
|
|
336
500
|
end try
|
|
337
501
|
error errMsg number errNum
|
|
338
502
|
end try
|
|
503
|
+
set atmOutgoingId to ""
|
|
339
504
|
tell application "Mail"
|
|
505
|
+
try
|
|
506
|
+
set atmOutgoingId to message id of newMessage as string
|
|
507
|
+
end try
|
|
340
508
|
${send ? "send newMessage" : "save newMessage"}
|
|
341
509
|
end tell
|
|
510
|
+
if atmOutgoingId is not "" then return atmOutgoingId
|
|
342
511
|
return "OK"`;
|
|
343
512
|
}
|
|
344
513
|
|
|
@@ -350,8 +519,31 @@ function summarizeRecipients(to, cc, bcc) {
|
|
|
350
519
|
return parts.join("; ");
|
|
351
520
|
}
|
|
352
521
|
|
|
522
|
+
export const SENT_VERIFY_SENT = "SENT";
|
|
523
|
+
export const SENT_VERIFY_OUTBOX = "OUTBOX";
|
|
524
|
+
export const SENT_VERIFY_NOT_FOUND = "NOT_FOUND";
|
|
525
|
+
/** Older hang-recovery scripts returned FOUND to mean Sent. Still accepted. */
|
|
353
526
|
export const SENT_VERIFY_FOUND = "FOUND";
|
|
354
527
|
export const SENT_VERIFY_TIMEOUT_MS = 15000;
|
|
528
|
+
export const SENT_VERIFY_ATTEMPTS = 3;
|
|
529
|
+
export const SENT_VERIFY_RETRY_MS = 400;
|
|
530
|
+
|
|
531
|
+
export const MAIL_VERIFY_MISS_GUIDANCE =
|
|
532
|
+
"Mail reported the send call succeeded, but the message was not found in Sent or Outbox. Do not assume it was delivered.";
|
|
533
|
+
|
|
534
|
+
function defaultSentVerifySleep(ms) {
|
|
535
|
+
if (!ms || ms <= 0) return;
|
|
536
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Test hook: skip the retry delay without changing attempt count. */
|
|
540
|
+
export const sentVerifyClock = {
|
|
541
|
+
sleep: defaultSentVerifySleep
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
export function resetSentVerifyClock() {
|
|
545
|
+
sentVerifyClock.sleep = defaultSentVerifySleep;
|
|
546
|
+
}
|
|
355
547
|
|
|
356
548
|
/**
|
|
357
549
|
* Hard Mail Automation deny: -1743 / -10004 / "not authorized…", or
|
|
@@ -376,10 +568,10 @@ function mailBoxesPreamble() {
|
|
|
376
568
|
return ` set cutoff to (current date) - (10 * minutes)
|
|
377
569
|
set boxes to {}
|
|
378
570
|
try
|
|
379
|
-
set end of boxes to
|
|
571
|
+
set end of boxes to outgoing mailbox
|
|
380
572
|
end try
|
|
381
573
|
try
|
|
382
|
-
set end of boxes to
|
|
574
|
+
set end of boxes to sent mailbox
|
|
383
575
|
end try
|
|
384
576
|
repeat with acct in accounts
|
|
385
577
|
try
|
|
@@ -392,6 +584,48 @@ function appleScriptToList(addresses = []) {
|
|
|
392
584
|
return `{${addresses.map((address) => asString(address)).join(", ")}}`;
|
|
393
585
|
}
|
|
394
586
|
|
|
587
|
+
function loadMailboxMessagesSnippet() {
|
|
588
|
+
return ` set recentMsgs to {}
|
|
589
|
+
set scanAll to false
|
|
590
|
+
try
|
|
591
|
+
if boxRef is outgoing mailbox then set scanAll to true
|
|
592
|
+
end try
|
|
593
|
+
try
|
|
594
|
+
set nm to name of boxRef as string
|
|
595
|
+
if nm contains "Outbox" then set scanAll to true
|
|
596
|
+
if nm contains "Outgoing" then set scanAll to true
|
|
597
|
+
end try
|
|
598
|
+
if scanAll then
|
|
599
|
+
set recentMsgs to messages of boxRef
|
|
600
|
+
else
|
|
601
|
+
set recentMsgs to (messages of boxRef whose date sent > cutoff)
|
|
602
|
+
end if`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function verifyHitReturn() {
|
|
606
|
+
return ` try
|
|
607
|
+
if boxRef is outgoing mailbox then return "${SENT_VERIFY_OUTBOX}"
|
|
608
|
+
end try
|
|
609
|
+
try
|
|
610
|
+
set nm to name of boxRef as string
|
|
611
|
+
if nm contains "Outbox" then return "${SENT_VERIFY_OUTBOX}"
|
|
612
|
+
if nm contains "Outgoing" then return "${SENT_VERIFY_OUTBOX}"
|
|
613
|
+
end try
|
|
614
|
+
return "${SENT_VERIFY_SENT}"`;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function recipientHitSnippet() {
|
|
618
|
+
return ` set hitTo to false
|
|
619
|
+
try
|
|
620
|
+
repeat with recip in (to recipients of msg)
|
|
621
|
+
set recipAddr to address of recip as string
|
|
622
|
+
repeat with wanted in wantedTos
|
|
623
|
+
if recipAddr is (wanted as string) then set hitTo to true
|
|
624
|
+
end repeat
|
|
625
|
+
end repeat
|
|
626
|
+
end try`;
|
|
627
|
+
}
|
|
628
|
+
|
|
395
629
|
/**
|
|
396
630
|
* Look in Sent / Outbox for a reply of `messageId`.
|
|
397
631
|
* Proper Mail replies set In-Reply-To. The fallback `make new outgoing
|
|
@@ -414,7 +648,7 @@ tell application "Mail"
|
|
|
414
648
|
${mailBoxesPreamble()}
|
|
415
649
|
repeat with boxRef in boxes
|
|
416
650
|
try
|
|
417
|
-
|
|
651
|
+
${loadMailboxMessagesSnippet()}
|
|
418
652
|
repeat with msg in recentMsgs
|
|
419
653
|
try
|
|
420
654
|
set candId to message id of msg
|
|
@@ -423,12 +657,18 @@ ${mailBoxesPreamble()}
|
|
|
423
657
|
else
|
|
424
658
|
try
|
|
425
659
|
set src to source of msg
|
|
426
|
-
if src contains ("In-Reply-To: " & ${needleAngle}) then
|
|
427
|
-
|
|
660
|
+
if src contains ("In-Reply-To: " & ${needleAngle}) then
|
|
661
|
+
${verifyHitReturn()}
|
|
662
|
+
end if
|
|
663
|
+
if src contains ("In-Reply-To: " & ${needleBare}) then
|
|
664
|
+
${verifyHitReturn()}
|
|
665
|
+
end if
|
|
428
666
|
end try
|
|
429
667
|
if origSubject is not "" then
|
|
430
668
|
set subj to subject of msg
|
|
431
|
-
if subj is ("Re: " & origSubject) then
|
|
669
|
+
if subj is ("Re: " & origSubject) then
|
|
670
|
+
${verifyHitReturn()}
|
|
671
|
+
end if
|
|
432
672
|
end if
|
|
433
673
|
end if
|
|
434
674
|
end try
|
|
@@ -436,7 +676,7 @@ ${mailBoxesPreamble()}
|
|
|
436
676
|
end try
|
|
437
677
|
end repeat
|
|
438
678
|
end tell
|
|
439
|
-
return "
|
|
679
|
+
return "${SENT_VERIFY_NOT_FOUND}"`;
|
|
440
680
|
}
|
|
441
681
|
|
|
442
682
|
/**
|
|
@@ -463,7 +703,7 @@ ${mailBoxesPreamble()}
|
|
|
463
703
|
set wantedTos to ${toList}
|
|
464
704
|
repeat with boxRef in boxes
|
|
465
705
|
try
|
|
466
|
-
|
|
706
|
+
${loadMailboxMessagesSnippet()}
|
|
467
707
|
repeat with msg in recentMsgs
|
|
468
708
|
try
|
|
469
709
|
set candId to message id of msg
|
|
@@ -474,15 +714,7 @@ ${mailBoxesPreamble()}
|
|
|
474
714
|
if src contains ("In-Reply-To: " & ${needleAngle}) or src contains ("In-Reply-To: " & ${needleBare}) then
|
|
475
715
|
-- A reply to the original is not this forward.
|
|
476
716
|
else
|
|
477
|
-
|
|
478
|
-
try
|
|
479
|
-
repeat with recip in (to recipients of msg)
|
|
480
|
-
set recipAddr to address of recip as string
|
|
481
|
-
repeat with wanted in wantedTos
|
|
482
|
-
if recipAddr is (wanted as string) then set hitTo to true
|
|
483
|
-
end repeat
|
|
484
|
-
end repeat
|
|
485
|
-
end try
|
|
717
|
+
${recipientHitSnippet()}
|
|
486
718
|
if hitTo then
|
|
487
719
|
set subj to subject of msg
|
|
488
720
|
set exactFwd to false
|
|
@@ -498,8 +730,12 @@ ${mailBoxesPreamble()}
|
|
|
498
730
|
set mentionsOrigId to false
|
|
499
731
|
if src contains ${needleAngle} then set mentionsOrigId to true
|
|
500
732
|
if src contains ${needleBare} then set mentionsOrigId to true
|
|
501
|
-
if exactFwd then
|
|
502
|
-
|
|
733
|
+
if exactFwd then
|
|
734
|
+
${verifyHitReturn()}
|
|
735
|
+
end if
|
|
736
|
+
if looksForward and mentionsOrigId then
|
|
737
|
+
${verifyHitReturn()}
|
|
738
|
+
end if
|
|
503
739
|
end if
|
|
504
740
|
end if
|
|
505
741
|
end if
|
|
@@ -508,45 +744,125 @@ ${mailBoxesPreamble()}
|
|
|
508
744
|
end try
|
|
509
745
|
end repeat
|
|
510
746
|
end tell
|
|
511
|
-
return "
|
|
747
|
+
return "${SENT_VERIFY_NOT_FOUND}"`;
|
|
512
748
|
}
|
|
513
749
|
|
|
514
750
|
/**
|
|
515
|
-
* Look in Sent / Outbox for a compose
|
|
516
|
-
*
|
|
751
|
+
* Look in Sent / Outbox for a compose matching To + subject.
|
|
752
|
+
* Never matches subject alone — short subjects like "test" are unsafe.
|
|
753
|
+
* When `messageId` is present, prefer that id together with To.
|
|
517
754
|
*/
|
|
518
|
-
export function
|
|
519
|
-
const
|
|
755
|
+
export function buildFindSentByRecipientAndSubjectScript(subject, toAddresses = [], messageId = null) {
|
|
756
|
+
const toList = appleScriptToList(toAddresses);
|
|
757
|
+
const subj = asString(subject || "");
|
|
758
|
+
const idLit = messageId ? asString(messageId) : null;
|
|
759
|
+
const matchBody = idLit
|
|
760
|
+
? `${recipientHitSnippet()}
|
|
761
|
+
set candId to message id of msg
|
|
762
|
+
set hitId to false
|
|
763
|
+
try
|
|
764
|
+
if candId is ${idLit} then set hitId to true
|
|
765
|
+
end try
|
|
766
|
+
if hitId and hitTo then
|
|
767
|
+
${verifyHitReturn()}
|
|
768
|
+
end if
|
|
769
|
+
if hitTo then
|
|
770
|
+
set subj to subject of msg
|
|
771
|
+
if subj is ${subj} then
|
|
772
|
+
${verifyHitReturn()}
|
|
773
|
+
end if
|
|
774
|
+
end if`
|
|
775
|
+
: `${recipientHitSnippet()}
|
|
776
|
+
if hitTo then
|
|
777
|
+
set subj to subject of msg
|
|
778
|
+
if subj is ${subj} then
|
|
779
|
+
${verifyHitReturn()}
|
|
780
|
+
end if
|
|
781
|
+
end if`;
|
|
782
|
+
|
|
520
783
|
return `tell application "Mail"
|
|
521
784
|
${mailBoxesPreamble()}
|
|
785
|
+
set wantedTos to ${toList}
|
|
522
786
|
repeat with boxRef in boxes
|
|
523
787
|
try
|
|
524
|
-
|
|
525
|
-
|
|
788
|
+
${loadMailboxMessagesSnippet()}
|
|
789
|
+
repeat with msg in recentMsgs
|
|
790
|
+
try
|
|
791
|
+
${matchBody}
|
|
792
|
+
end try
|
|
793
|
+
end repeat
|
|
526
794
|
end try
|
|
527
795
|
end repeat
|
|
528
796
|
end tell
|
|
529
|
-
return "
|
|
797
|
+
return "${SENT_VERIFY_NOT_FOUND}"`;
|
|
530
798
|
}
|
|
531
799
|
|
|
532
800
|
/**
|
|
533
|
-
*
|
|
534
|
-
*
|
|
801
|
+
* @deprecated Subject-only matching is unsafe. Requires To as the second
|
|
802
|
+
* argument; without recipients the scan cannot hit.
|
|
535
803
|
*/
|
|
536
|
-
export function
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
804
|
+
export function buildFindSentBySubjectScript(subject, toAddresses = []) {
|
|
805
|
+
return buildFindSentByRecipientAndSubjectScript(subject, toAddresses);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export function parseSentVerifyOutput(output) {
|
|
809
|
+
const out = String(output || "").trim();
|
|
810
|
+
if (out === SENT_VERIFY_SENT || out === SENT_VERIFY_FOUND) {
|
|
811
|
+
return { found: true, mailbox: "sent" };
|
|
812
|
+
}
|
|
813
|
+
if (out === SENT_VERIFY_OUTBOX) {
|
|
814
|
+
return { found: true, mailbox: "outbox" };
|
|
815
|
+
}
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export function parseOutgoingMessageId(output) {
|
|
820
|
+
const raw = String(output || "").trim();
|
|
821
|
+
if (!raw || raw === "OK") return null;
|
|
822
|
+
return validateMessageId(raw);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function pickSentVerifyScript({
|
|
826
|
+
inReplyTo = null,
|
|
827
|
+
subject = null,
|
|
828
|
+
forwardTo = null,
|
|
829
|
+
to = null,
|
|
830
|
+
messageId = null
|
|
831
|
+
} = {}) {
|
|
832
|
+
if (forwardTo && inReplyTo) return buildFindSentForwardScript(inReplyTo, forwardTo);
|
|
833
|
+
if (inReplyTo) return buildFindSentByInReplyToScript(inReplyTo);
|
|
834
|
+
const toList = Array.isArray(to) ? to : [];
|
|
835
|
+
if (toList.length > 0 && (subject || messageId)) {
|
|
836
|
+
return buildFindSentByRecipientAndSubjectScript(subject || "", toList, messageId);
|
|
837
|
+
}
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* After a send hang, ask Mail whether the message is already in Sent/Outbox.
|
|
843
|
+
* Compose matches require To + subject (Message-ID when available) — never
|
|
844
|
+
* subject alone. Returns a hit object or null. Verify failure is not success.
|
|
845
|
+
*/
|
|
846
|
+
export function recoverIfInSent(match = {}) {
|
|
847
|
+
const script = pickSentVerifyScript(match);
|
|
544
848
|
if (!script) return null;
|
|
545
849
|
const result = runAppleScript(script, { timeout: SENT_VERIFY_TIMEOUT_MS, appName: "Mail" });
|
|
546
850
|
if (!result.ok) return null;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
851
|
+
return parseSentVerifyOutput(result.output);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Poll Sent/Outbox after a send that returned without throw.
|
|
856
|
+
*/
|
|
857
|
+
export function verifyQueuedMessage(match = {}, { attempts = SENT_VERIFY_ATTEMPTS, retryMs = SENT_VERIFY_RETRY_MS } = {}) {
|
|
858
|
+
let last = null;
|
|
859
|
+
const n = Math.max(1, attempts);
|
|
860
|
+
for (let i = 0; i < n; i++) {
|
|
861
|
+
last = recoverIfInSent(match);
|
|
862
|
+
if (last) return last;
|
|
863
|
+
if (i < n - 1) sentVerifyClock.sleep(retryMs);
|
|
864
|
+
}
|
|
865
|
+
return last;
|
|
550
866
|
}
|
|
551
867
|
|
|
552
868
|
export function isMailAccessibilityDenial(result) {
|
|
@@ -555,8 +871,22 @@ export function isMailAccessibilityDenial(result) {
|
|
|
555
871
|
return text.includes("accessibility_denied") || text.includes("assistive access");
|
|
556
872
|
}
|
|
557
873
|
|
|
874
|
+
export function isMailBodyFocusFailed(result) {
|
|
875
|
+
if (!result) return false;
|
|
876
|
+
return String(result.error || "").toLowerCase().includes("body_focus_failed");
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
export function isMailBodyPasteMisdirected(result) {
|
|
880
|
+
if (!result) return false;
|
|
881
|
+
const text = String(result.error || "").toLowerCase();
|
|
882
|
+
return text.includes("body_paste_misdirected") || text.includes("body_focus_failed");
|
|
883
|
+
}
|
|
884
|
+
|
|
558
885
|
function failure(action, summary, result, secrets) {
|
|
559
886
|
const err = String((result && result.error) || "").toLowerCase();
|
|
887
|
+
if (err.includes("body_focus_failed")) {
|
|
888
|
+
return `${action} failed — attempted to ${summary}. The compose caret could not be moved into the message body; nothing was sent.`;
|
|
889
|
+
}
|
|
560
890
|
if (err.includes("body_paste_misdirected")) {
|
|
561
891
|
return `${action} failed — attempted to ${summary}. The body was pasted into a header field instead of the message body; nothing was sent.`;
|
|
562
892
|
}
|
|
@@ -586,9 +916,41 @@ function failure(action, summary, result, secrets) {
|
|
|
586
916
|
}
|
|
587
917
|
|
|
588
918
|
/**
|
|
589
|
-
* Finish a compose/reply/forward. A send
|
|
590
|
-
*
|
|
919
|
+
* Finish a compose/reply/forward. A real send is not success until Sent or
|
|
920
|
+
* Outbox verify hits. A send hang is not labeled TCC; if Mail already
|
|
921
|
+
* delivered, return success. Hard -1743/-10004 stays a deny.
|
|
591
922
|
*/
|
|
923
|
+
function deliverySummary(successSummary, mailbox, recovered) {
|
|
924
|
+
if (mailbox === "outbox") {
|
|
925
|
+
return recovered
|
|
926
|
+
? "queued in Outbox (still sending; verified after AppleScript hang)"
|
|
927
|
+
: "queued in Outbox (still sending)";
|
|
928
|
+
}
|
|
929
|
+
return recovered
|
|
930
|
+
? `${successSummary} (verified in Sent after AppleScript hang)`
|
|
931
|
+
: `${successSummary} (verified in Sent)`;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function sendVerifiedSuccess({ action, successSummary, details, verified, recovered }) {
|
|
935
|
+
const mailbox = verified.mailbox;
|
|
936
|
+
const delivered = mailbox === "sent";
|
|
937
|
+
return {
|
|
938
|
+
ok: true,
|
|
939
|
+
delivered,
|
|
940
|
+
mailbox,
|
|
941
|
+
recovered: recovered || undefined,
|
|
942
|
+
message: writeSuccessMessage(action, deliverySummary(successSummary, mailbox, recovered), {
|
|
943
|
+
...details,
|
|
944
|
+
mailbox,
|
|
945
|
+
delivery: delivered ? "sent" : "outbox"
|
|
946
|
+
})
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function verifyMissMessage(action, summary) {
|
|
951
|
+
return `${action} failed — attempted to ${summary}. ${MAIL_VERIFY_MISS_GUIDANCE}`;
|
|
952
|
+
}
|
|
953
|
+
|
|
592
954
|
function finalizeMailWrite({
|
|
593
955
|
action,
|
|
594
956
|
summary,
|
|
@@ -598,30 +960,33 @@ function finalizeMailWrite({
|
|
|
598
960
|
inReplyTo = null,
|
|
599
961
|
subject = null,
|
|
600
962
|
forwardTo = null,
|
|
963
|
+
to = null,
|
|
964
|
+
messageId = null,
|
|
601
965
|
successSummary,
|
|
602
966
|
details
|
|
603
967
|
}) {
|
|
968
|
+
const match = { inReplyTo, subject, forwardTo, to, messageId };
|
|
969
|
+
|
|
604
970
|
if (result.ok) {
|
|
605
|
-
|
|
971
|
+
if (!sendNow) {
|
|
972
|
+
return { ok: true, delivered: false, mailbox: null, message: writeSuccessMessage(action, successSummary, details) };
|
|
973
|
+
}
|
|
974
|
+
const verified = verifyQueuedMessage(match);
|
|
975
|
+
if (!verified) {
|
|
976
|
+
return { ok: false, delivered: false, mailbox: null, message: verifyMissMessage(action, summary) };
|
|
977
|
+
}
|
|
978
|
+
return sendVerifiedSuccess({ action, successSummary, details, verified, recovered: false });
|
|
606
979
|
}
|
|
607
980
|
|
|
608
981
|
if (sendNow && isMailSendTimeout(result)) {
|
|
609
|
-
const recovered = recoverIfInSent(
|
|
982
|
+
const recovered = recoverIfInSent(match);
|
|
610
983
|
if (recovered) {
|
|
611
|
-
return {
|
|
612
|
-
ok: true,
|
|
613
|
-
recovered: true,
|
|
614
|
-
message: writeSuccessMessage(
|
|
615
|
-
action,
|
|
616
|
-
`${successSummary} (verified in Sent after AppleScript hang)`,
|
|
617
|
-
details
|
|
618
|
-
)
|
|
619
|
-
};
|
|
984
|
+
return sendVerifiedSuccess({ action, successSummary, details, verified: recovered, recovered: true });
|
|
620
985
|
}
|
|
621
|
-
return { ok: false, message: failure(action, summary, result, secrets) };
|
|
986
|
+
return { ok: false, delivered: false, mailbox: null, message: failure(action, summary, result, secrets) };
|
|
622
987
|
}
|
|
623
988
|
|
|
624
|
-
return { ok: false, message: failure(action, summary, result, secrets) };
|
|
989
|
+
return { ok: false, delivered: false, mailbox: null, message: failure(action, summary, result, secrets) };
|
|
625
990
|
}
|
|
626
991
|
|
|
627
992
|
/**
|
|
@@ -664,7 +1029,7 @@ export function mailCompose(args = {}, { draft = false } = {}) {
|
|
|
664
1029
|
dryRun: isFlagTrue(args.dry_run),
|
|
665
1030
|
confirm: isFlagTrue(args.confirm)
|
|
666
1031
|
});
|
|
667
|
-
if (!plan.proceed) return
|
|
1032
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
668
1033
|
|
|
669
1034
|
const script = buildComposeScript({
|
|
670
1035
|
to: to.addresses,
|
|
@@ -684,6 +1049,8 @@ export function mailCompose(args = {}, { draft = false } = {}) {
|
|
|
684
1049
|
secrets: [body.text, subject.text],
|
|
685
1050
|
sendNow: !draft,
|
|
686
1051
|
subject: draft ? null : subject.text,
|
|
1052
|
+
to: draft ? null : to.addresses,
|
|
1053
|
+
messageId: draft ? null : parseOutgoingMessageId(result.output),
|
|
687
1054
|
successSummary: draft ? "draft saved to Drafts" : "sent",
|
|
688
1055
|
details: {
|
|
689
1056
|
to: to.addresses.join(", "),
|
|
@@ -743,7 +1110,7 @@ export function mailReply(args = {}) {
|
|
|
743
1110
|
dryRun: isFlagTrue(args.dry_run),
|
|
744
1111
|
confirm: isFlagTrue(args.confirm)
|
|
745
1112
|
});
|
|
746
|
-
if (!plan.proceed) return
|
|
1113
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
747
1114
|
|
|
748
1115
|
const result = runAppleScript(
|
|
749
1116
|
buildReplyScript({ messageId: resolved.messageId, body: body.text, replyAll, sendNow }),
|
|
@@ -806,7 +1173,7 @@ export function mailForward(args = {}) {
|
|
|
806
1173
|
dryRun: isFlagTrue(args.dry_run),
|
|
807
1174
|
confirm: isFlagTrue(args.confirm)
|
|
808
1175
|
});
|
|
809
|
-
if (!plan.proceed) return
|
|
1176
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
810
1177
|
|
|
811
1178
|
const result = runAppleScript(
|
|
812
1179
|
buildForwardScript({ messageId: resolved.messageId, to: to.addresses, body: body.text, sendNow }),
|
|
@@ -854,7 +1221,7 @@ export function mailMark(args = {}) {
|
|
|
854
1221
|
dryRun: isFlagTrue(args.dry_run),
|
|
855
1222
|
confirm: isFlagTrue(args.confirm)
|
|
856
1223
|
});
|
|
857
|
-
if (!plan.proceed) return
|
|
1224
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
858
1225
|
|
|
859
1226
|
const result = runAppleScript(buildMarkScript({ messageId: resolved.messageId, read: status === "read" }), { appName: "Mail" });
|
|
860
1227
|
if (!result.ok) return { ok: false, message: failure(action, summary, result, []) };
|
|
@@ -899,7 +1266,7 @@ export function mailArchive(args = {}) {
|
|
|
899
1266
|
dryRun: isFlagTrue(args.dry_run),
|
|
900
1267
|
confirm: isFlagTrue(args.confirm)
|
|
901
1268
|
});
|
|
902
|
-
if (!plan.proceed) return
|
|
1269
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
903
1270
|
|
|
904
1271
|
const result = runAppleScript(
|
|
905
1272
|
buildMoveScript({
|
|
@@ -932,7 +1299,7 @@ export function mailTrash(args = {}) {
|
|
|
932
1299
|
dryRun: isFlagTrue(args.dry_run),
|
|
933
1300
|
confirm: isFlagTrue(args.confirm)
|
|
934
1301
|
});
|
|
935
|
-
if (!plan.proceed) return
|
|
1302
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
936
1303
|
|
|
937
1304
|
const result = runAppleScript(
|
|
938
1305
|
buildMoveScript({
|
package/lib/messagesWrite.js
CHANGED
|
@@ -17,6 +17,7 @@ import { escapeSQL } from "./validators.js";
|
|
|
17
17
|
import { runAppleScript, asString, MESSAGES_TCC_GUIDANCE, MESSAGES_APP_NOT_RUNNING_GUIDANCE, ATTRIBUTION_GUIDANCE } from "./appleScript.js";
|
|
18
18
|
import {
|
|
19
19
|
planWrite,
|
|
20
|
+
plannedWriteResult,
|
|
20
21
|
normalizeList,
|
|
21
22
|
isMessagesHandle,
|
|
22
23
|
validateChatGuid,
|
|
@@ -261,7 +262,7 @@ export function messagesSend(args = {}, deps = {}) {
|
|
|
261
262
|
dryRun,
|
|
262
263
|
confirm
|
|
263
264
|
});
|
|
264
|
-
if (!plan.proceed) return
|
|
265
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
265
266
|
|
|
266
267
|
const result = runAppleScript(
|
|
267
268
|
buildSendToChatScript({ chatGuid: guid, text: body.text, attachmentPath: attachment.filePath }),
|
|
@@ -293,7 +294,7 @@ export function messagesSend(args = {}, deps = {}) {
|
|
|
293
294
|
|
|
294
295
|
const summary = `send a message to ${handles.join(", ")} over ${serviceRaw}${attachment.filePath ? " with an attachment" : ""}`;
|
|
295
296
|
const plan = planWrite({ action, summary, recipientCount: handles.length, dryRun, confirm });
|
|
296
|
-
if (!plan.proceed) return
|
|
297
|
+
if (!plan.proceed) return plannedWriteResult(plan);
|
|
297
298
|
|
|
298
299
|
const result = runAppleScript(
|
|
299
300
|
buildSendToHandlesScript({
|
package/lib/writeGuards.js
CHANGED
|
@@ -215,6 +215,24 @@ export function planWrite({
|
|
|
215
215
|
return { decision: "execute", proceed: true, requiresConfirm, message: null };
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Planned / dry_run / confirm-blocked results are not deliveries.
|
|
220
|
+
*
|
|
221
|
+
* MCP CallTool only surfaces `message` plus optional `isError`. Clients that
|
|
222
|
+
* treat a missing `isError` as success would read `ok: true` planned results
|
|
223
|
+
* as delivered. Planned writes therefore return `ok: false` with
|
|
224
|
+
* `planned: true` and `delivered: false`.
|
|
225
|
+
*/
|
|
226
|
+
export function plannedWriteResult(plan) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
planned: true,
|
|
230
|
+
delivered: false,
|
|
231
|
+
mailbox: null,
|
|
232
|
+
message: plan && plan.message ? plan.message : "Nothing was changed."
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
218
236
|
export function truncate(text, max = 200) {
|
|
219
237
|
if (typeof text !== "string") return "";
|
|
220
238
|
const oneLine = text.replace(/\s+/g, " ").trim();
|
package/lib/writeTools.js
CHANGED
|
@@ -48,7 +48,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
48
48
|
// ============ MAIL WRITES ============
|
|
49
49
|
{
|
|
50
50
|
name: "mail_send",
|
|
51
|
-
description: "Send an email through Mail.app. Sending to more than one recipient in total (to + cc + bcc) requires confirm=true.
|
|
51
|
+
description: "Send an email through Mail.app. Success means the message was verified in Sent or is still in Outbox (still sending) — AppleScript send returning is not enough. Sending to more than one recipient in total (to + cc + bcc) requires confirm=true. dry_run and unconfirmed multi-recipient calls are not deliveries (MCP isError). A send hang is a timeout, not TCC, unless Mail reports -1743/-10004; hang recover matches To+subject (Message-ID when available), never subject alone. Clients must Sent-check before retrying a timed-out send.",
|
|
52
52
|
inputSchema: {
|
|
53
53
|
type: "object",
|
|
54
54
|
properties: {
|
|
@@ -82,7 +82,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
name: "mail_reply",
|
|
85
|
-
description: "Reply to an existing email. reply_all=true fans out to every original recipient and requires confirm=true. A send hang is a timeout, not TCC, unless Mail reports -1743/-10004;
|
|
85
|
+
description: "Reply to an existing email. reply_all=true fans out to every original recipient and requires confirm=true. A real send is verified in Sent/Outbox before success. A send hang is a timeout, not TCC, unless Mail reports -1743/-10004; hang recover uses In-Reply-To. Clients must Sent-check before retrying a timed-out send.",
|
|
86
86
|
inputSchema: {
|
|
87
87
|
type: "object",
|
|
88
88
|
properties: {
|
|
@@ -97,7 +97,7 @@ export const WRITE_TOOL_DEFINITIONS = [
|
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
name: "mail_forward",
|
|
100
|
-
description: "Forward an existing email to new recipients. More than one recipient requires confirm=true. A send hang is a timeout, not TCC, unless Mail reports -1743/-10004;
|
|
100
|
+
description: "Forward an existing email to new recipients. More than one recipient requires confirm=true. A real send is verified in Sent/Outbox before success. A send hang is a timeout, not TCC, unless Mail reports -1743/-10004; hang recover matches intended To plus Fwd: subject or original Message-ID. Clients must Sent-check before retrying a timed-out send.",
|
|
101
101
|
inputSchema: {
|
|
102
102
|
type: "object",
|
|
103
103
|
properties: {
|
|
@@ -343,6 +343,23 @@ export function isWriteTool(name) {
|
|
|
343
343
|
return Object.prototype.hasOwnProperty.call(WRITE_TOOL_HANDLERS, name);
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
/**
|
|
347
|
+
* MCP CallTool payload for a write. Planned / dry_run / confirm-blocked
|
|
348
|
+
* results set isError so clients that only check isError cannot treat them
|
|
349
|
+
* as delivered. Verified sends (ok: true) do not set isError.
|
|
350
|
+
*
|
|
351
|
+
* @param {{ ok?: boolean, planned?: boolean, message?: string }} writeResult
|
|
352
|
+
*/
|
|
353
|
+
export function mcpWriteResult(writeResult) {
|
|
354
|
+
const message = writeResult && writeResult.message ? writeResult.message : "write failed";
|
|
355
|
+
const planned = Boolean(writeResult && writeResult.planned);
|
|
356
|
+
const failed = !writeResult || writeResult.ok === false;
|
|
357
|
+
return {
|
|
358
|
+
content: [{ type: "text", text: message }],
|
|
359
|
+
...((failed || planned) ? { isError: true } : {})
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
346
363
|
/**
|
|
347
364
|
* Run a write in this process.
|
|
348
365
|
* @returns {{ ok: boolean, message: string }}
|
package/package.json
CHANGED
package/scripts/smoke-writes.js
CHANGED
|
@@ -233,8 +233,9 @@ function line(label, value) {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
function step(name, result, severity = "error") {
|
|
236
|
-
const
|
|
237
|
-
const
|
|
236
|
+
const planned = result && result.planned === true;
|
|
237
|
+
const failed = result && result.ok === false && !planned;
|
|
238
|
+
const status = planned ? "PLANNED" : failed ? (severity === "warning" ? "WARN" : "FAIL") : "OK";
|
|
238
239
|
console.log(`\n[${status}] ${name}`);
|
|
239
240
|
console.log(` ${result.message}`);
|
|
240
241
|
return result;
|
|
@@ -348,7 +349,7 @@ async function main() {
|
|
|
348
349
|
}));
|
|
349
350
|
results.push(created);
|
|
350
351
|
|
|
351
|
-
if (created.ok !== false) {
|
|
352
|
+
if (created.ok !== false || created.planned) {
|
|
352
353
|
const contactId = apply ? extractContactId(created.message) : "ABCD1234-SMOKE:ABPerson";
|
|
353
354
|
if (apply && !contactId) {
|
|
354
355
|
console.log(" contacts_add reported success but returned no contact id; skipping edit/delete.");
|
|
@@ -405,7 +406,7 @@ async function main() {
|
|
|
405
406
|
}));
|
|
406
407
|
results.push(eventCreated);
|
|
407
408
|
|
|
408
|
-
if (eventCreated.ok !== false) {
|
|
409
|
+
if (eventCreated.ok !== false || eventCreated.planned) {
|
|
409
410
|
const eventId = apply ? extractEventId(eventCreated.message) : "ATM-SMOKE-EVENT-UID";
|
|
410
411
|
const extractedKitId = apply ? extractEventKitId(eventCreated.message) : "EK-SMOKE";
|
|
411
412
|
const eventKitId = extractedKitId || (eventId && eventId.includes(":") ? eventId : null);
|
|
@@ -451,7 +452,7 @@ async function main() {
|
|
|
451
452
|
}
|
|
452
453
|
}
|
|
453
454
|
|
|
454
|
-
const failed = results.some((r) => r && r.ok === false);
|
|
455
|
+
const failed = results.some((r) => r && r.ok === false && !r.planned);
|
|
455
456
|
console.log("\n" + "=".repeat(60));
|
|
456
457
|
if (failed) {
|
|
457
458
|
console.log("Result: FAILED - see the messages above.");
|