apple-mail-mcp 2.10.32 → 2.10.33
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 +40 -6
- package/build/index.js +112 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1399,6 +1399,7 @@ batch of only `imap:` ids returns no `countDelta` rather than a fabricated one.
|
|
|
1399
1399
|
| `APPLE_MAIL_MCP_AUDIT_LOG` | *(off)* | Absolute path to an NDJSON file. Setting it enables the audit log **and** the collateral diff below |
|
|
1400
1400
|
| `APPLE_MAIL_MCP_AUDIT_SUBJECTS` | `0` | Set `1` to also record message **subjects**. Separate, deliberate second opt-in — see Privacy |
|
|
1401
1401
|
| `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX` | `2000` | Skip the collateral snapshot for mailboxes larger than this many messages. `0` disables the snapshot entirely |
|
|
1402
|
+
| `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
1403
|
|
|
1403
1404
|
When set, each destructive operation appends **one JSON object per line**
|
|
1404
1405
|
containing: timestamp, tool name, server version, the arguments it was called
|
|
@@ -1451,12 +1452,45 @@ exceed AppleScript's 2^29 integer range (where Mail hands them back as
|
|
|
1451
1452
|
compared in the raw form, a message you explicitly asked to delete would be
|
|
1452
1453
|
reported here as collateral.
|
|
1453
1454
|
|
|
1454
|
-
This
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1455
|
+
This is O(mailbox size), so it is bounded by `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX`.
|
|
1456
|
+
When the bound bites, the record says so explicitly (`"snapshot": "skipped"` with
|
|
1457
|
+
a reason) rather than omitting the field — a silently skipped snapshot would read
|
|
1458
|
+
as "nothing collateral happened", which is worse than no snapshot at all.
|
|
1459
|
+
`countDelta` is unaffected by the skip and still reconciles the counts.
|
|
1460
|
+
|
|
1461
|
+
#### Partial snapshots on large mailboxes
|
|
1462
|
+
|
|
1463
|
+
The mailbox is read in `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_CHUNK`-sized slices, each
|
|
1464
|
+
retried once on its own. It used to be a single whole-mailbox read, which meant
|
|
1465
|
+
Mail declining that one request lost the **entire** diff — and the bigger the
|
|
1466
|
+
mailbox, the more likely that was. The mechanism that attributes collateral
|
|
1467
|
+
damage was therefore least reliable exactly when the blast radius was largest.
|
|
1468
|
+
|
|
1469
|
+
When a slice still will not read, the snapshot is reported as `partial` and it
|
|
1470
|
+
**names its own gap**:
|
|
1471
|
+
|
|
1472
|
+
```json
|
|
1473
|
+
{
|
|
1474
|
+
"snapshot": "partial",
|
|
1475
|
+
"unobserved": [{ "phase": "after", "ranges": "251-500" }],
|
|
1476
|
+
"appeared": [],
|
|
1477
|
+
"skipReason": "Mail would not read 251-500 (after) of this mailbox, so the snapshot has a hole in it. …"
|
|
1478
|
+
}
|
|
1479
|
+
```
|
|
1480
|
+
|
|
1481
|
+
Each half of the diff is withheld when the snapshot that could **refute** it has
|
|
1482
|
+
a hole, because a wrong name here is worse than a missing one:
|
|
1483
|
+
|
|
1484
|
+
| Hole in | `disappeared` / `unrequested` | `appeared` |
|
|
1485
|
+
|---------|-------------------------------|------------|
|
|
1486
|
+
| neither (`"ok"`) | reported | reported |
|
|
1487
|
+
| `before` | reported (an undercount — a message never read before cannot be missed after) | withheld |
|
|
1488
|
+
| `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 |
|
|
1489
|
+
| both | withheld | withheld |
|
|
1490
|
+
|
|
1491
|
+
**An absent field means "not computable", never "empty".** Check
|
|
1492
|
+
`"snapshot": "ok"` before reading `disappeared` as a clean bill of health — the
|
|
1493
|
+
false-ok warning does exactly that, so a `partial` snapshot never triggers it.
|
|
1460
1494
|
|
|
1461
1495
|
### Privacy, and what the file costs you
|
|
1462
1496
|
|
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;
|
|
@@ -79402,10 +79412,24 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79402
79412
|
* into a SNAP record — the before/after pair the collateral diff subtracts.
|
|
79403
79413
|
*
|
|
79404
79414
|
* 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
|
-
*
|
|
79415
|
+
* whole layer costs literally nothing by default. The property reads are BULK
|
|
79416
|
+
* (`id of messages i thru j of mb`) — two Apple Events per SLICE rather than
|
|
79417
|
+
* two per message — and the joining is pure in-memory AppleScript.
|
|
79418
|
+
*
|
|
79419
|
+
* ## Why it is sliced rather than one whole-mailbox read (#176)
|
|
79420
|
+
*
|
|
79421
|
+
* This used to be a single `id of messages of mb` pair. When Mail declined
|
|
79422
|
+
* that request the entire snapshot came back `unavailable`, and the cost of
|
|
79423
|
+
* the request grows with the mailbox — so the one mechanism that can attribute
|
|
79424
|
+
* an unrequested departure was least reliable exactly when the batch and the
|
|
79425
|
+
* mailbox, and therefore the blast radius, were largest. That correlation was
|
|
79426
|
+
* the defect, not any individual failure.
|
|
79427
|
+
*
|
|
79428
|
+
* Now the mailbox is read in `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_CHUNK`-sized
|
|
79429
|
+
* slices; each slice is retried once on its own; and a slice that still will
|
|
79430
|
+
* not read costs only its own range. The unreadable ranges are emitted in
|
|
79431
|
+
* their own field, so the diff can report a PARTIAL snapshot that names its
|
|
79432
|
+
* own gap instead of an all-or-nothing `unavailable`.
|
|
79409
79433
|
*
|
|
79410
79434
|
* Above `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX` messages the snapshot is skipped,
|
|
79411
79435
|
* and the skip is EMITTED as a record with its reason. A silently skipped
|
|
@@ -79422,46 +79446,77 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79422
79446
|
snapshotFragment(phase, acctExpr, mbExpr, countVar, mbVar = "_tmb") {
|
|
79423
79447
|
const max = auditSnapshotMax();
|
|
79424
79448
|
if (!isAuditEnabled() || max <= 0) return "";
|
|
79449
|
+
const chunk = auditSnapshotChunk();
|
|
79425
79450
|
return `
|
|
79426
79451
|
set _sStatus to "ok"
|
|
79427
79452
|
set _sPayload to ""
|
|
79428
|
-
set
|
|
79429
|
-
set
|
|
79453
|
+
set _sMiss to ""
|
|
79454
|
+
set _sPairs to {}
|
|
79455
|
+
set _sChunk to ${chunk}
|
|
79430
79456
|
if ${countVar} < 0 then
|
|
79431
79457
|
set _sStatus to "unavailable"
|
|
79432
79458
|
else if ${countVar} > ${max} then
|
|
79433
79459
|
set _sStatus to "skipped"
|
|
79434
79460
|
set _sPayload to "mailbox holds " & (${countVar} as string) & " messages, above ${AUDIT_SNAPSHOT_MAX_ENV}=${max}"
|
|
79435
79461
|
else
|
|
79436
|
-
|
|
79437
|
-
|
|
79438
|
-
set
|
|
79439
|
-
|
|
79440
|
-
set
|
|
79441
|
-
|
|
79442
|
-
|
|
79443
|
-
|
|
79462
|
+
set _sLo to 1
|
|
79463
|
+
repeat while _sLo <= ${countVar}
|
|
79464
|
+
set _sHi to _sLo + _sChunk - 1
|
|
79465
|
+
if _sHi > ${countVar} then set _sHi to ${countVar}
|
|
79466
|
+
set _sGot to false
|
|
79467
|
+
repeat with _sTry from 1 to ${SNAPSHOT_SLICE_ATTEMPTS}
|
|
79468
|
+
set _sIds to {}
|
|
79469
|
+
set _sMids to {}
|
|
79470
|
+
-- A slice is staged into _sBuf and merged only once it has been
|
|
79471
|
+
-- read IN FULL. Appending as we go would leave a slice that threw
|
|
79472
|
+
-- halfway both partially recorded AND marked unread, and the
|
|
79473
|
+
-- retry would then record its messages a second time.
|
|
79474
|
+
set _sBuf to {}
|
|
79475
|
+
try
|
|
79476
|
+
set _sIds to (id of messages _sLo thru _sHi of ${mbVar})
|
|
79477
|
+
set _sMids to (message id of messages _sLo thru _sHi of ${mbVar})
|
|
79478
|
+
if (class of _sIds) is not list then set _sIds to {_sIds}
|
|
79479
|
+
if (class of _sMids) is not list then set _sMids to {_sMids}
|
|
79480
|
+
if (count of _sIds) is (count of _sMids) then
|
|
79481
|
+
repeat with _q from 1 to (count of _sIds)
|
|
79482
|
+
set _sOne to ""
|
|
79483
|
+
try
|
|
79484
|
+
set _zSnapMid to ((item _q of _sMids) as string)${this.sanitizeFragment("_zSnapMid", " ")}
|
|
79485
|
+
set _sOne to ((item _q of _sIds) as string) & "${SNAP_PAIR}" & _zSnapMid
|
|
79486
|
+
on error
|
|
79487
|
+
set _sOne to ((item _q of _sIds) as string) & "${SNAP_PAIR}"
|
|
79488
|
+
end try
|
|
79489
|
+
set end of _sBuf to _sOne
|
|
79490
|
+
end repeat
|
|
79491
|
+
set _sGot to true
|
|
79492
|
+
end if
|
|
79493
|
+
end try
|
|
79494
|
+
if _sGot then
|
|
79495
|
+
repeat with _sB in _sBuf
|
|
79496
|
+
set end of _sPairs to (contents of _sB)
|
|
79497
|
+
end repeat
|
|
79498
|
+
exit repeat
|
|
79499
|
+
end if
|
|
79500
|
+
end repeat
|
|
79501
|
+
if not _sGot then
|
|
79502
|
+
if _sMiss is not "" then set _sMiss to _sMiss & ","
|
|
79503
|
+
set _sMiss to _sMiss & (_sLo as string) & "-" & (_sHi as string)
|
|
79504
|
+
end if
|
|
79505
|
+
set _sLo to _sHi + 1
|
|
79506
|
+
end repeat
|
|
79507
|
+
if _sMiss is not "" then
|
|
79508
|
+
if (count of _sPairs) is 0 then
|
|
79444
79509
|
set _sStatus to "unavailable"
|
|
79445
79510
|
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
|
|
79511
|
+
set _sStatus to "partial"
|
|
79461
79512
|
end if
|
|
79462
79513
|
end if
|
|
79514
|
+
set _sTid to AppleScript's text item delimiters
|
|
79515
|
+
set AppleScript's text item delimiters to "${SNAP_ITEM}"
|
|
79516
|
+
set _sPayload to _sPairs as string
|
|
79517
|
+
set AppleScript's text item delimiters to _sTid
|
|
79463
79518
|
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}"`;
|
|
79519
|
+
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
79520
|
}
|
|
79466
79521
|
/**
|
|
79467
79522
|
* AppleScript capturing the message the op is ABOUT to touch into `_pre`,
|
|
@@ -79527,7 +79582,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79527
79582
|
mailbox: f[2] ?? "",
|
|
79528
79583
|
phase: f[3] === "after" ? "after" : "before",
|
|
79529
79584
|
status: f[4] ?? "",
|
|
79530
|
-
payload: f[5] ?? ""
|
|
79585
|
+
payload: f[5] ?? "",
|
|
79586
|
+
miss: f[6] ?? ""
|
|
79531
79587
|
});
|
|
79532
79588
|
continue;
|
|
79533
79589
|
}
|
|
@@ -79677,8 +79733,9 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79677
79733
|
});
|
|
79678
79734
|
continue;
|
|
79679
79735
|
}
|
|
79680
|
-
|
|
79681
|
-
|
|
79736
|
+
const dead = (s) => s.status !== "ok" && s.status !== "partial";
|
|
79737
|
+
if (dead(b) || dead(a)) {
|
|
79738
|
+
const bad = dead(b) ? b : a;
|
|
79682
79739
|
collateral.push({
|
|
79683
79740
|
account: g.account,
|
|
79684
79741
|
mailbox: g.mailbox,
|
|
@@ -79696,13 +79753,30 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79696
79753
|
const unrequested = disappeared.filter(
|
|
79697
79754
|
(e) => !requestedNumericIds.has(canonicalNumericId(e.id))
|
|
79698
79755
|
);
|
|
79756
|
+
const holes = [b, a].filter((s) => s.miss !== "").map((s) => ({ phase: s.phase, ranges: s.miss }));
|
|
79757
|
+
if (holes.length === 0) {
|
|
79758
|
+
collateral.push({
|
|
79759
|
+
account: g.account,
|
|
79760
|
+
mailbox: g.mailbox,
|
|
79761
|
+
snapshot: "ok",
|
|
79762
|
+
disappeared,
|
|
79763
|
+
unrequested,
|
|
79764
|
+
appeared
|
|
79765
|
+
});
|
|
79766
|
+
continue;
|
|
79767
|
+
}
|
|
79768
|
+
const derivable = [
|
|
79769
|
+
a.miss === "" ? `what left the ${beforeEntries.length} message(s) read before it` : null,
|
|
79770
|
+
b.miss === "" ? "what arrived during it" : null
|
|
79771
|
+
].filter((s) => s !== null);
|
|
79699
79772
|
collateral.push({
|
|
79700
79773
|
account: g.account,
|
|
79701
79774
|
mailbox: g.mailbox,
|
|
79702
|
-
snapshot: "
|
|
79703
|
-
|
|
79704
|
-
|
|
79705
|
-
|
|
79775
|
+
snapshot: "partial",
|
|
79776
|
+
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".`,
|
|
79777
|
+
unobserved: holes,
|
|
79778
|
+
...a.miss === "" ? { disappeared, unrequested } : {},
|
|
79779
|
+
...b.miss === "" ? { appeared } : {}
|
|
79706
79780
|
});
|
|
79707
79781
|
}
|
|
79708
79782
|
return { countDeltas, preImages, outcomes: parsed.outcomes, collateral };
|
|
@@ -86387,7 +86461,7 @@ registerTool(
|
|
|
86387
86461
|
registerTool(
|
|
86388
86462
|
"fetch-attachment",
|
|
86389
86463
|
{
|
|
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
|
|
86464
|
+
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
86465
|
inputSchema: {
|
|
86392
86466
|
id: MESSAGE_ID_SCHEMA,
|
|
86393
86467
|
attachmentName: external_exports.string().min(1, "Attachment name is required")
|
package/package.json
CHANGED