apple-mail-mcp 2.10.32 → 2.11.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 +99 -16
- package/build/index.js +141 -57
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1319,14 +1319,26 @@ returns the comparison in `structuredContent`:
|
|
|
1319
1319
|
}
|
|
1320
1320
|
```
|
|
1321
1321
|
|
|
1322
|
-
`status` is deliberately
|
|
1322
|
+
`status` is deliberately not a pass/fail flag:
|
|
1323
1323
|
|
|
1324
1324
|
| `status` | Meaning | Warns? |
|
|
1325
1325
|
|----------|---------|--------|
|
|
1326
1326
|
| `match` | Exactly as many messages left the mailbox as the operation acted on. | No |
|
|
1327
1327
|
| `over` | **More** left than were operated on. Messages are unaccounted for. | **Yes** |
|
|
1328
|
-
| `
|
|
1329
|
-
|
|
1328
|
+
| `unknown` | No comparison this server is willing to assert. `unknownReason` says which of four situations produced it. | No |
|
|
1329
|
+
|
|
1330
|
+
`unknownReason` distinguishes four cases that are *not* interchangeable:
|
|
1331
|
+
|
|
1332
|
+
| `unknownReason` | Meaning |
|
|
1333
|
+
|-----------------|---------|
|
|
1334
|
+
| `count-unreadable` | Mail would not report a count at all (`before`/`after` null). |
|
|
1335
|
+
| `no-expectation` | No expectation is predictable, so no comparison exists — a move whose destination **is** the source mailbox. |
|
|
1336
|
+
| `count-did-not-move` | The count did not move. On a store that flags deletions instead of removing them this is the **ordinary, correct** reading for an operation that fully succeeded. |
|
|
1337
|
+
| `count-partial` | The count moved, but by less than the operation accounted for. A flag-only store cannot produce this, which is why it is worth telling apart. |
|
|
1338
|
+
|
|
1339
|
+
> **`under` was removed in 2.11.0.** It used to mean "fewer left than expected"
|
|
1340
|
+
> and was documented as routine. Field evidence retired it — see
|
|
1341
|
+
> [Why `observed` is a lower bound](#why-observed-is-a-lower-bound-155).
|
|
1330
1342
|
|
|
1331
1343
|
#### What an `over` warning does and does not tell you
|
|
1332
1344
|
|
|
@@ -1346,7 +1358,7 @@ trusted:
|
|
|
1346
1358
|
**Concurrent departure is the benign cause to rule out first**, and the warning
|
|
1347
1359
|
text says so. What the asymmetry argument actually buys is the other half:
|
|
1348
1360
|
concurrent *arrivals* cannot produce `over`, because a message arriving
|
|
1349
|
-
mid-operation *raises* the after-count and biases the reading
|
|
1361
|
+
mid-operation *raises* the after-count and biases the reading short.
|
|
1350
1362
|
That is why `over` is the interesting direction — a strong signal, not a proof.
|
|
1351
1363
|
|
|
1352
1364
|
Setting `APPLE_MAIL_MCP_AUDIT_LOG` is what settles which one you have: the
|
|
@@ -1354,12 +1366,42 @@ collateral diff below **names** the messages that disappeared, and "the
|
|
|
1354
1366
|
newsletter my rule files every morning" is a very different report from a message
|
|
1355
1367
|
nothing should have touched.
|
|
1356
1368
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1369
|
+
#### Why `observed` is a lower bound (#155)
|
|
1370
|
+
|
|
1371
|
+
**`observed` is the movement of Mail's count. It is a lower bound on how many
|
|
1372
|
+
messages left, not a count of how many left.**
|
|
1373
|
+
|
|
1374
|
+
On iCloud, @scottstern0325 ran the check that settled this: for two batches
|
|
1375
|
+
reporting `observed: 0`, the messages were located **in Trash**, matched by
|
|
1376
|
+
`date received` + sender against the audit log's pre-image. The deletes had
|
|
1377
|
+
happened. Mail's count had not caught up. Across four readings — 0 of 4, 0 of 1
|
|
1378
|
+
(a *single-id* delete), 15 of 16, and 14 of 15 — the shortfall bore no relation
|
|
1379
|
+
to batch size, which is what a lagging count looks like and not what a
|
|
1380
|
+
store-behaviour rule looks like.
|
|
1381
|
+
|
|
1382
|
+
So a short reading is not evidence about your operation. **The per-id outcomes
|
|
1383
|
+
are what report success; this number is not. Do not retry on the strength of
|
|
1384
|
+
it** — that is how a message gets deleted twice.
|
|
1385
|
+
|
|
1386
|
+
Two things follow:
|
|
1387
|
+
|
|
1388
|
+
- A count that does not move at all is still the ordinary reading on a store that
|
|
1389
|
+
flags deletions instead of removing them (Gmail label mailboxes, IMAP accounts
|
|
1390
|
+
with "move deleted messages to Trash" off). It reports
|
|
1391
|
+
`unknownReason: "count-did-not-move"` and says so, and it is never warned about
|
|
1392
|
+
— a warning that fires on every ordinary Gmail delete would be ignored exactly
|
|
1393
|
+
when it matters.
|
|
1394
|
+
- **To confirm where messages went, match them at the destination by `date
|
|
1395
|
+
received` plus sender — not by the numeric ids you passed.** Ids are renumbered
|
|
1396
|
+
by the move and do not survive it.
|
|
1397
|
+
|
|
1398
|
+
**Removed in 2.11.0: the "reported success with no observed effect" warning.**
|
|
1399
|
+
Shipped in 2.10.30, it fired when the count was flat, the snapshot read cleanly
|
|
1400
|
+
and nothing had disappeared. Its premise was that the snapshot corroborated the
|
|
1401
|
+
count — but both are read back-to-back in the same script, and the record that
|
|
1402
|
+
prompted it turns out to have had *both* instruments stale at once. It therefore
|
|
1403
|
+
fired on stores that had done exactly what they were asked. It is gone rather
|
|
1404
|
+
than narrowed; `over` is the only surviving assertion.
|
|
1363
1405
|
|
|
1364
1406
|
Three more honesty rules:
|
|
1365
1407
|
|
|
@@ -1399,6 +1441,7 @@ batch of only `imap:` ids returns no `countDelta` rather than a fabricated one.
|
|
|
1399
1441
|
| `APPLE_MAIL_MCP_AUDIT_LOG` | *(off)* | Absolute path to an NDJSON file. Setting it enables the audit log **and** the collateral diff below |
|
|
1400
1442
|
| `APPLE_MAIL_MCP_AUDIT_SUBJECTS` | `0` | Set `1` to also record message **subjects**. Separate, deliberate second opt-in — see Privacy |
|
|
1401
1443
|
| `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX` | `2000` | Skip the collateral snapshot for mailboxes larger than this many messages. `0` disables the snapshot entirely |
|
|
1444
|
+
| `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_CHUNK` | `250` | How many messages the collateral snapshot reads from Mail per request. Lower it if Mail declines slices on a very large mailbox |
|
|
1402
1445
|
|
|
1403
1446
|
When set, each destructive operation appends **one JSON object per line**
|
|
1404
1447
|
containing: timestamp, tool name, server version, the arguments it was called
|
|
@@ -1451,12 +1494,52 @@ exceed AppleScript's 2^29 integer range (where Mail hands them back as
|
|
|
1451
1494
|
compared in the raw form, a message you explicitly asked to delete would be
|
|
1452
1495
|
reported here as collateral.
|
|
1453
1496
|
|
|
1454
|
-
This
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1497
|
+
This is O(mailbox size), so it is bounded by `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX`.
|
|
1498
|
+
When the bound bites, the record says so explicitly (`"snapshot": "skipped"` with
|
|
1499
|
+
a reason) rather than omitting the field — a silently skipped snapshot would read
|
|
1500
|
+
as "nothing collateral happened", which is worse than no snapshot at all.
|
|
1501
|
+
`countDelta` is unaffected by the skip and still reconciles the counts.
|
|
1502
|
+
|
|
1503
|
+
#### Partial snapshots on large mailboxes
|
|
1504
|
+
|
|
1505
|
+
The mailbox is read in `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_CHUNK`-sized slices, each
|
|
1506
|
+
retried once on its own. It used to be a single whole-mailbox read, which meant
|
|
1507
|
+
Mail declining that one request lost the **entire** diff — and the bigger the
|
|
1508
|
+
mailbox, the more likely that was. The mechanism that attributes collateral
|
|
1509
|
+
damage was therefore least reliable exactly when the blast radius was largest.
|
|
1510
|
+
|
|
1511
|
+
When a slice still will not read, the snapshot is reported as `partial` and it
|
|
1512
|
+
**names its own gap**:
|
|
1513
|
+
|
|
1514
|
+
```json
|
|
1515
|
+
{
|
|
1516
|
+
"snapshot": "partial",
|
|
1517
|
+
"unobserved": [{ "phase": "after", "ranges": "251-500" }],
|
|
1518
|
+
"appeared": [],
|
|
1519
|
+
"skipReason": "Mail would not read 251-500 (after) of this mailbox, so the snapshot has a hole in it. …"
|
|
1520
|
+
}
|
|
1521
|
+
```
|
|
1522
|
+
|
|
1523
|
+
Each half of the diff is withheld when the snapshot that could **refute** it has
|
|
1524
|
+
a hole, because a wrong name here is worse than a missing one:
|
|
1525
|
+
|
|
1526
|
+
| Hole in | `disappeared` / `unrequested` | `appeared` |
|
|
1527
|
+
|---------|-------------------------------|------------|
|
|
1528
|
+
| neither (`"ok"`) | reported | reported |
|
|
1529
|
+
| `before` | reported (an undercount — a message never read before cannot be missed after) | withheld |
|
|
1530
|
+
| `after` | **withheld** — a message absent from a partial `after` may merely be unread, and naming it would present an innocent message as evidence of data loss | reported |
|
|
1531
|
+
| both | withheld | withheld |
|
|
1532
|
+
|
|
1533
|
+
**An absent field means "not computable", never "empty".** Check
|
|
1534
|
+
`"snapshot": "ok"` before reading `disappeared` as a clean bill of health.
|
|
1535
|
+
|
|
1536
|
+
> ⚠️ **`"ok"` is necessary, not sufficient.** The enumeration's range is bounded
|
|
1537
|
+
> by Mail's own message count, and #155 established that count can lag the
|
|
1538
|
+
> mutation. A count reading **low** truncates the enumeration silently — the
|
|
1539
|
+
> unread tail is never requested, so it never registers as a failed slice and the
|
|
1540
|
+
> status still says `"ok"` — and messages past that bound would then look like
|
|
1541
|
+
> they disappeared. Until that is fixed, treat a `disappeared` entry as a lead to
|
|
1542
|
+
> check, not a proof.
|
|
1460
1543
|
|
|
1461
1544
|
### Privacy, and what the file costs you
|
|
1462
1545
|
|
package/build/index.js
CHANGED
|
@@ -78791,7 +78791,10 @@ import { appendFileSync } from "node:fs";
|
|
|
78791
78791
|
var AUDIT_LOG_ENV = "APPLE_MAIL_MCP_AUDIT_LOG";
|
|
78792
78792
|
var AUDIT_SUBJECTS_ENV = "APPLE_MAIL_MCP_AUDIT_SUBJECTS";
|
|
78793
78793
|
var AUDIT_SNAPSHOT_MAX_ENV = "APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX";
|
|
78794
|
+
var AUDIT_SNAPSHOT_CHUNK_ENV = "APPLE_MAIL_MCP_AUDIT_SNAPSHOT_CHUNK";
|
|
78794
78795
|
var DEFAULT_SNAPSHOT_MAX = 2e3;
|
|
78796
|
+
var DEFAULT_SNAPSHOT_CHUNK = 250;
|
|
78797
|
+
var SNAPSHOT_SLICE_ATTEMPTS = 2;
|
|
78795
78798
|
function isOn(raw) {
|
|
78796
78799
|
return /^(1|true|yes|on)$/i.test((raw ?? "").trim());
|
|
78797
78800
|
}
|
|
@@ -78812,6 +78815,13 @@ function auditSnapshotMax() {
|
|
|
78812
78815
|
if (!Number.isFinite(n) || n < 0) return DEFAULT_SNAPSHOT_MAX;
|
|
78813
78816
|
return Math.floor(n);
|
|
78814
78817
|
}
|
|
78818
|
+
function auditSnapshotChunk() {
|
|
78819
|
+
const raw = process.env[AUDIT_SNAPSHOT_CHUNK_ENV]?.trim();
|
|
78820
|
+
if (raw === void 0 || raw === "") return DEFAULT_SNAPSHOT_CHUNK;
|
|
78821
|
+
const n = Number(raw);
|
|
78822
|
+
if (!Number.isFinite(n) || n < 1) return DEFAULT_SNAPSHOT_CHUNK;
|
|
78823
|
+
return Math.floor(n);
|
|
78824
|
+
}
|
|
78815
78825
|
function writeAuditRecord(record2) {
|
|
78816
78826
|
const path = auditLogPath();
|
|
78817
78827
|
if (!path) return;
|
|
@@ -78844,17 +78854,8 @@ function countDeltaWarning(d) {
|
|
|
78844
78854
|
const where = d.account ? `"${d.mailbox}" in account "${d.account}"` : `"${d.mailbox}"`;
|
|
78845
78855
|
return `\u26A0\uFE0F Effect mismatch in ${where}: ${d.observed} message(s) left the mailbox but only ${d.expected} were operated on (count ${d.before} \u2192 ${d.after}). ${extra} message(s) are unaccounted for. Anything else removing mail from this mailbox at the same moment \u2014 a Mail rule, a server-side filter, another client, an IMAP expunge \u2014 reads the same way, so rule that out first. If nothing else was touching it, this is the signature of https://github.com/sweetrb/apple-mail-mcp/issues/155 \u2014 please report it there, and set ${AUDIT_LOG_ENV}=/path/to/audit.ndjson to capture which messages disappeared.`;
|
|
78846
78856
|
}
|
|
78847
|
-
function falseOkWarning(d, collateral) {
|
|
78848
|
-
if (d.status !== "under" || d.expected === null) return null;
|
|
78849
|
-
if (d.observed !== 0 || d.expected <= 0) return null;
|
|
78850
|
-
if (!collateral || collateral.snapshot !== "ok") return null;
|
|
78851
|
-
if ((collateral.disappeared?.length ?? 0) !== 0) return null;
|
|
78852
|
-
const where = d.account ? `"${d.mailbox}" in account "${d.account}"` : `"${d.mailbox}"`;
|
|
78853
|
-
return `\u26A0\uFE0F Reported success with no observed effect in ${where}: ${d.expected} message(s) were operated on and reported ok, but the mailbox count did not move (${d.before} \u2192 ${d.after}) and the collateral snapshot \u2014 which read this mailbox successfully \u2014 shows no message left it. Either the operation silently did nothing, or Mail's count and listing are both stale and the messages did move. Check the destination (Trash for a delete) before assuming either: if they are there, this is a measurement-timing bug; if they are still in the source mailbox minutes later, the operation failed while reporting success. Please report at https://github.com/sweetrb/apple-mail-mcp/issues/155 with ${AUDIT_LOG_ENV}=/path/to/audit.ndjson set.`;
|
|
78854
|
-
}
|
|
78855
78857
|
function reconciliationWarnings(report) {
|
|
78856
|
-
|
|
78857
|
-
return report.countDeltas.flatMap((d) => [countDeltaWarning(d), falseOkWarning(d, collateralFor(d))]).filter((w) => w !== null);
|
|
78858
|
+
return report.countDeltas.map((d) => countDeltaWarning(d)).filter((w) => w !== null);
|
|
78858
78859
|
}
|
|
78859
78860
|
|
|
78860
78861
|
// src/services/appleMailManager.ts
|
|
@@ -79132,6 +79133,8 @@ function canonicalNumericId(raw) {
|
|
|
79132
79133
|
return Number.isFinite(n) ? String(n) : trimmed;
|
|
79133
79134
|
}
|
|
79134
79135
|
var SELF_MOVE_NOTE = "Destination is the source mailbox, so no message should leave it. What Mail does to the count when a message is re-filed into the mailbox it already occupies is unspecified, so there is no expected delta to compare against: this mailbox is reported without a comparison and is never warned about.";
|
|
79136
|
+
var COUNT_UNMOVED_NOTE = `Mail's count did not move. This is the ordinary reading on a store that flags deletions instead of removing them (Gmail label mailboxes and IMAP accounts with "move deleted messages to Trash" off), where the message stays put and the operation still fully succeeded. It can also mean Mail's count simply had not caught up yet. The per-id outcomes are what report success; this number is not, so do not retry on the strength of it.`;
|
|
79137
|
+
var COUNT_PARTIAL_NOTE = `Mail's count moved by less than this operation accounted for. That is a LOWER BOUND on what left, not a count of what left: Mail has been observed reporting a stale count for a delete it had already performed (issue #155), and new mail arriving mid-operation reads the same way. No claim is made either way. To confirm where the messages went, match them at the destination by "date received" plus sender \u2014 NOT by the numeric ids you passed, which are renumbered by the move and do not survive it.`;
|
|
79135
79138
|
function snapshotKey(entry) {
|
|
79136
79139
|
return `${entry.id}\0${entry.messageId}`;
|
|
79137
79140
|
}
|
|
@@ -79402,10 +79405,24 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79402
79405
|
* into a SNAP record — the before/after pair the collateral diff subtracts.
|
|
79403
79406
|
*
|
|
79404
79407
|
* Empty string when the audit log is off or the snapshot is disabled, so the
|
|
79405
|
-
* whole layer costs literally nothing by default. The
|
|
79406
|
-
*
|
|
79407
|
-
*
|
|
79408
|
-
*
|
|
79408
|
+
* whole layer costs literally nothing by default. The property reads are BULK
|
|
79409
|
+
* (`id of messages i thru j of mb`) — two Apple Events per SLICE rather than
|
|
79410
|
+
* two per message — and the joining is pure in-memory AppleScript.
|
|
79411
|
+
*
|
|
79412
|
+
* ## Why it is sliced rather than one whole-mailbox read (#176)
|
|
79413
|
+
*
|
|
79414
|
+
* This used to be a single `id of messages of mb` pair. When Mail declined
|
|
79415
|
+
* that request the entire snapshot came back `unavailable`, and the cost of
|
|
79416
|
+
* the request grows with the mailbox — so the one mechanism that can attribute
|
|
79417
|
+
* an unrequested departure was least reliable exactly when the batch and the
|
|
79418
|
+
* mailbox, and therefore the blast radius, were largest. That correlation was
|
|
79419
|
+
* the defect, not any individual failure.
|
|
79420
|
+
*
|
|
79421
|
+
* Now the mailbox is read in `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_CHUNK`-sized
|
|
79422
|
+
* slices; each slice is retried once on its own; and a slice that still will
|
|
79423
|
+
* not read costs only its own range. The unreadable ranges are emitted in
|
|
79424
|
+
* their own field, so the diff can report a PARTIAL snapshot that names its
|
|
79425
|
+
* own gap instead of an all-or-nothing `unavailable`.
|
|
79409
79426
|
*
|
|
79410
79427
|
* Above `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX` messages the snapshot is skipped,
|
|
79411
79428
|
* and the skip is EMITTED as a record with its reason. A silently skipped
|
|
@@ -79422,46 +79439,77 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79422
79439
|
snapshotFragment(phase, acctExpr, mbExpr, countVar, mbVar = "_tmb") {
|
|
79423
79440
|
const max = auditSnapshotMax();
|
|
79424
79441
|
if (!isAuditEnabled() || max <= 0) return "";
|
|
79442
|
+
const chunk = auditSnapshotChunk();
|
|
79425
79443
|
return `
|
|
79426
79444
|
set _sStatus to "ok"
|
|
79427
79445
|
set _sPayload to ""
|
|
79428
|
-
set
|
|
79429
|
-
set
|
|
79446
|
+
set _sMiss to ""
|
|
79447
|
+
set _sPairs to {}
|
|
79448
|
+
set _sChunk to ${chunk}
|
|
79430
79449
|
if ${countVar} < 0 then
|
|
79431
79450
|
set _sStatus to "unavailable"
|
|
79432
79451
|
else if ${countVar} > ${max} then
|
|
79433
79452
|
set _sStatus to "skipped"
|
|
79434
79453
|
set _sPayload to "mailbox holds " & (${countVar} as string) & " messages, above ${AUDIT_SNAPSHOT_MAX_ENV}=${max}"
|
|
79435
79454
|
else
|
|
79436
|
-
|
|
79437
|
-
|
|
79438
|
-
set
|
|
79439
|
-
|
|
79440
|
-
set
|
|
79441
|
-
|
|
79442
|
-
|
|
79443
|
-
|
|
79455
|
+
set _sLo to 1
|
|
79456
|
+
repeat while _sLo <= ${countVar}
|
|
79457
|
+
set _sHi to _sLo + _sChunk - 1
|
|
79458
|
+
if _sHi > ${countVar} then set _sHi to ${countVar}
|
|
79459
|
+
set _sGot to false
|
|
79460
|
+
repeat with _sTry from 1 to ${SNAPSHOT_SLICE_ATTEMPTS}
|
|
79461
|
+
set _sIds to {}
|
|
79462
|
+
set _sMids to {}
|
|
79463
|
+
-- A slice is staged into _sBuf and merged only once it has been
|
|
79464
|
+
-- read IN FULL. Appending as we go would leave a slice that threw
|
|
79465
|
+
-- halfway both partially recorded AND marked unread, and the
|
|
79466
|
+
-- retry would then record its messages a second time.
|
|
79467
|
+
set _sBuf to {}
|
|
79468
|
+
try
|
|
79469
|
+
set _sIds to (id of messages _sLo thru _sHi of ${mbVar})
|
|
79470
|
+
set _sMids to (message id of messages _sLo thru _sHi of ${mbVar})
|
|
79471
|
+
if (class of _sIds) is not list then set _sIds to {_sIds}
|
|
79472
|
+
if (class of _sMids) is not list then set _sMids to {_sMids}
|
|
79473
|
+
if (count of _sIds) is (count of _sMids) then
|
|
79474
|
+
repeat with _q from 1 to (count of _sIds)
|
|
79475
|
+
set _sOne to ""
|
|
79476
|
+
try
|
|
79477
|
+
set _zSnapMid to ((item _q of _sMids) as string)${this.sanitizeFragment("_zSnapMid", " ")}
|
|
79478
|
+
set _sOne to ((item _q of _sIds) as string) & "${SNAP_PAIR}" & _zSnapMid
|
|
79479
|
+
on error
|
|
79480
|
+
set _sOne to ((item _q of _sIds) as string) & "${SNAP_PAIR}"
|
|
79481
|
+
end try
|
|
79482
|
+
set end of _sBuf to _sOne
|
|
79483
|
+
end repeat
|
|
79484
|
+
set _sGot to true
|
|
79485
|
+
end if
|
|
79486
|
+
end try
|
|
79487
|
+
if _sGot then
|
|
79488
|
+
repeat with _sB in _sBuf
|
|
79489
|
+
set end of _sPairs to (contents of _sB)
|
|
79490
|
+
end repeat
|
|
79491
|
+
exit repeat
|
|
79492
|
+
end if
|
|
79493
|
+
end repeat
|
|
79494
|
+
if not _sGot then
|
|
79495
|
+
if _sMiss is not "" then set _sMiss to _sMiss & ","
|
|
79496
|
+
set _sMiss to _sMiss & (_sLo as string) & "-" & (_sHi as string)
|
|
79497
|
+
end if
|
|
79498
|
+
set _sLo to _sHi + 1
|
|
79499
|
+
end repeat
|
|
79500
|
+
if _sMiss is not "" then
|
|
79501
|
+
if (count of _sPairs) is 0 then
|
|
79444
79502
|
set _sStatus to "unavailable"
|
|
79445
79503
|
else
|
|
79446
|
-
set
|
|
79447
|
-
repeat with _q from 1 to (count of _sIds)
|
|
79448
|
-
set _sOne to ""
|
|
79449
|
-
try
|
|
79450
|
-
set _zSnapMid to ((item _q of _sMids) as string)${this.sanitizeFragment("_zSnapMid", " ")}
|
|
79451
|
-
set _sOne to ((item _q of _sIds) as string) & "${SNAP_PAIR}" & _zSnapMid
|
|
79452
|
-
on error
|
|
79453
|
-
set _sOne to ((item _q of _sIds) as string) & "${SNAP_PAIR}"
|
|
79454
|
-
end try
|
|
79455
|
-
set end of _sPairs to _sOne
|
|
79456
|
-
end repeat
|
|
79457
|
-
set _sTid to AppleScript's text item delimiters
|
|
79458
|
-
set AppleScript's text item delimiters to "${SNAP_ITEM}"
|
|
79459
|
-
set _sPayload to _sPairs as string
|
|
79460
|
-
set AppleScript's text item delimiters to _sTid
|
|
79504
|
+
set _sStatus to "partial"
|
|
79461
79505
|
end if
|
|
79462
79506
|
end if
|
|
79507
|
+
set _sTid to AppleScript's text item delimiters
|
|
79508
|
+
set AppleScript's text item delimiters to "${SNAP_ITEM}"
|
|
79509
|
+
set _sPayload to _sPairs as string
|
|
79510
|
+
set AppleScript's text item delimiters to _sTid
|
|
79463
79511
|
end if
|
|
79464
|
-
set _out to _out & "${SNAP_TAG}${FIELD_SEP}" & ${acctExpr} & "${FIELD_SEP}" & ${mbExpr} & "${FIELD_SEP}${phase}${FIELD_SEP}" & _sStatus & "${FIELD_SEP}" & _sPayload & "${RECORD_SEP}"`;
|
|
79512
|
+
set _out to _out & "${SNAP_TAG}${FIELD_SEP}" & ${acctExpr} & "${FIELD_SEP}" & ${mbExpr} & "${FIELD_SEP}${phase}${FIELD_SEP}" & _sStatus & "${FIELD_SEP}" & _sPayload & "${FIELD_SEP}" & _sMiss & "${RECORD_SEP}"`;
|
|
79465
79513
|
}
|
|
79466
79514
|
/**
|
|
79467
79515
|
* AppleScript capturing the message the op is ABOUT to touch into `_pre`,
|
|
@@ -79527,7 +79575,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79527
79575
|
mailbox: f[2] ?? "",
|
|
79528
79576
|
phase: f[3] === "after" ? "after" : "before",
|
|
79529
79577
|
status: f[4] ?? "",
|
|
79530
|
-
payload: f[5] ?? ""
|
|
79578
|
+
payload: f[5] ?? "",
|
|
79579
|
+
miss: f[6] ?? ""
|
|
79531
79580
|
});
|
|
79532
79581
|
continue;
|
|
79533
79582
|
}
|
|
@@ -79623,10 +79672,24 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79623
79672
|
const observed = readable ? m.before - m.after : null;
|
|
79624
79673
|
const note = noteFor(m.account, m.mailbox);
|
|
79625
79674
|
let status;
|
|
79626
|
-
|
|
79627
|
-
|
|
79628
|
-
|
|
79629
|
-
|
|
79675
|
+
let unknownReason;
|
|
79676
|
+
if (!readable) {
|
|
79677
|
+
status = "unknown";
|
|
79678
|
+
unknownReason = "count-unreadable";
|
|
79679
|
+
} else if (m.expected === null) {
|
|
79680
|
+
status = "unknown";
|
|
79681
|
+
unknownReason = "no-expectation";
|
|
79682
|
+
} else if (observed === m.expected) {
|
|
79683
|
+
status = "match";
|
|
79684
|
+
} else if ((observed ?? 0) > m.expected) {
|
|
79685
|
+
status = "over";
|
|
79686
|
+
} else if (observed === 0) {
|
|
79687
|
+
status = "unknown";
|
|
79688
|
+
unknownReason = "count-did-not-move";
|
|
79689
|
+
} else {
|
|
79690
|
+
status = "unknown";
|
|
79691
|
+
unknownReason = "count-partial";
|
|
79692
|
+
}
|
|
79630
79693
|
return {
|
|
79631
79694
|
account: m.account,
|
|
79632
79695
|
mailbox: m.mailbox,
|
|
@@ -79635,11 +79698,11 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79635
79698
|
expected: m.expected,
|
|
79636
79699
|
observed,
|
|
79637
79700
|
status,
|
|
79701
|
+
...unknownReason ? { unknownReason } : {},
|
|
79638
79702
|
...note ? { note } : {},
|
|
79639
|
-
...
|
|
79640
|
-
...
|
|
79641
|
-
|
|
79642
|
-
} : {}
|
|
79703
|
+
...unknownReason === "count-unreadable" ? { note: note ?? "Mail did not report a message count for this mailbox" } : {},
|
|
79704
|
+
...unknownReason === "count-did-not-move" && !note ? { note: COUNT_UNMOVED_NOTE } : {},
|
|
79705
|
+
...unknownReason === "count-partial" && !note ? { note: COUNT_PARTIAL_NOTE } : {}
|
|
79643
79706
|
};
|
|
79644
79707
|
});
|
|
79645
79708
|
const preImages = [];
|
|
@@ -79677,8 +79740,9 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79677
79740
|
});
|
|
79678
79741
|
continue;
|
|
79679
79742
|
}
|
|
79680
|
-
|
|
79681
|
-
|
|
79743
|
+
const dead = (s) => s.status !== "ok" && s.status !== "partial";
|
|
79744
|
+
if (dead(b) || dead(a)) {
|
|
79745
|
+
const bad = dead(b) ? b : a;
|
|
79682
79746
|
collateral.push({
|
|
79683
79747
|
account: g.account,
|
|
79684
79748
|
mailbox: g.mailbox,
|
|
@@ -79696,13 +79760,30 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79696
79760
|
const unrequested = disappeared.filter(
|
|
79697
79761
|
(e) => !requestedNumericIds.has(canonicalNumericId(e.id))
|
|
79698
79762
|
);
|
|
79763
|
+
const holes = [b, a].filter((s) => s.miss !== "").map((s) => ({ phase: s.phase, ranges: s.miss }));
|
|
79764
|
+
if (holes.length === 0) {
|
|
79765
|
+
collateral.push({
|
|
79766
|
+
account: g.account,
|
|
79767
|
+
mailbox: g.mailbox,
|
|
79768
|
+
snapshot: "ok",
|
|
79769
|
+
disappeared,
|
|
79770
|
+
unrequested,
|
|
79771
|
+
appeared
|
|
79772
|
+
});
|
|
79773
|
+
continue;
|
|
79774
|
+
}
|
|
79775
|
+
const derivable = [
|
|
79776
|
+
a.miss === "" ? `what left the ${beforeEntries.length} message(s) read before it` : null,
|
|
79777
|
+
b.miss === "" ? "what arrived during it" : null
|
|
79778
|
+
].filter((s) => s !== null);
|
|
79699
79779
|
collateral.push({
|
|
79700
79780
|
account: g.account,
|
|
79701
79781
|
mailbox: g.mailbox,
|
|
79702
|
-
snapshot: "
|
|
79703
|
-
|
|
79704
|
-
|
|
79705
|
-
|
|
79782
|
+
snapshot: "partial",
|
|
79783
|
+
skipReason: `Mail would not read ${holes.map((h) => `${h.ranges} (${h.phase})`).join(", ")} of this mailbox, so the snapshot has a hole in it. ` + (derivable.length > 0 ? `Still derivable and reported: ${derivable.join(" and ")}. ` : `Neither half of the diff is derivable from it. `) + `Anything the unread range could refute is omitted rather than guessed \u2014 an absent field here means "not computable", not "empty".`,
|
|
79784
|
+
unobserved: holes,
|
|
79785
|
+
...a.miss === "" ? { disappeared, unrequested } : {},
|
|
79786
|
+
...b.miss === "" ? { appeared } : {}
|
|
79706
79787
|
});
|
|
79707
79788
|
}
|
|
79708
79789
|
return { countDeltas, preImages, outcomes: parsed.outcomes, collateral };
|
|
@@ -85229,7 +85310,10 @@ var COUNT_DELTA_OUTPUT_SCHEMA = external_exports.array(
|
|
|
85229
85310
|
// `status: "unknown"`, and never with a warning.
|
|
85230
85311
|
expected: external_exports.number().nullable().optional(),
|
|
85231
85312
|
observed: external_exports.number().nullable().optional(),
|
|
85232
|
-
status: external_exports.enum(["match", "over", "
|
|
85313
|
+
status: external_exports.enum(["match", "over", "unknown"]).optional(),
|
|
85314
|
+
// Declared explicitly: the SDK stamps additionalProperties:false on a bare
|
|
85315
|
+
// zod shape, so an undeclared key makes the CLIENT reject the result.
|
|
85316
|
+
unknownReason: external_exports.enum(["count-unreadable", "no-expectation", "count-did-not-move", "count-partial"]).optional(),
|
|
85233
85317
|
note: external_exports.string().optional()
|
|
85234
85318
|
})
|
|
85235
85319
|
).optional();
|
|
@@ -86387,7 +86471,7 @@ registerTool(
|
|
|
86387
86471
|
registerTool(
|
|
86388
86472
|
"fetch-attachment",
|
|
86389
86473
|
{
|
|
86390
|
-
description: "Use when: retrieving an attachment's raw bytes inline as base64 (by message id and attachmentName), e.g. to process its contents without
|
|
86474
|
+
description: "Use when: retrieving an attachment's raw bytes inline as base64 (by message id and attachmentName), e.g. to process its contents without keeping a file.\nReturns: the attachment's bytes base64-encoded, with its size and (for IMAP) MIME type.\nDo not use when: you don't know the attachment name (use list-attachments first) or you just want it saved to disk (use save-attachment).\nSafety: leaves no file behind, but the AppleScript path is not disk-free \u2014 Mail writes the attachment into a private temp directory, which is read back and then deleted. Needs no Full Disk Access either way: Mail performs that write under the Automation grant, and this server never reads the mail store itself.",
|
|
86391
86475
|
inputSchema: {
|
|
86392
86476
|
id: MESSAGE_ID_SCHEMA,
|
|
86393
86477
|
attachmentName: external_exports.string().min(1, "Attachment name is required")
|
package/package.json
CHANGED