apple-mail-mcp 2.14.0 → 2.15.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 +17 -6
- package/build/index.js +135 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1444,12 +1444,23 @@ Three more honesty rules:
|
|
|
1444
1444
|
than list positions — and `expected` stays comparable with the mailbox instead
|
|
1445
1445
|
of double-counting a duplicate into a false `over`.
|
|
1446
1446
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1447
|
+
**`imap:` ids are reconciled too, as of 2.15.0.** `batch-delete-messages` and
|
|
1448
|
+
`batch-move-messages` return the same `countDelta` structure on the IMAP path, so
|
|
1449
|
+
one shape covers both backends and a mixed batch reports an entry per source
|
|
1450
|
+
mailbox from whichever backend handled it. The entries are **concatenated, never
|
|
1451
|
+
summed** — Mail's own count can lag (#155) while the server's `STATUS` cannot, and
|
|
1452
|
+
averaging the two would hide which reading you were looking at.
|
|
1453
|
+
|
|
1454
|
+
Only the operations that actually remove messages from their source reconcile.
|
|
1455
|
+
`batch-mark-as-read` and the flag tools change no count, so emitting
|
|
1456
|
+
`expected: N, observed: 0` for them would manufacture an alarm; they report no
|
|
1457
|
+
`countDelta` at all.
|
|
1458
|
+
|
|
1459
|
+
Note the mis-targeting class `countDelta` was originally built for cannot occur
|
|
1460
|
+
on the IMAP path — a UID names exactly one message in exactly one mailbox — so
|
|
1461
|
+
there the value is effect confirmation rather than target confirmation.
|
|
1462
|
+
|
|
1463
|
+
Single-message tools carry a post-condition check instead. `delete-message` and
|
|
1453
1464
|
`move-message` on an `imap:` id return a **`verification`** object in
|
|
1454
1465
|
`structuredContent`:
|
|
1455
1466
|
|
package/build/index.js
CHANGED
|
@@ -78828,6 +78828,14 @@ function auditSnapshotChunk() {
|
|
|
78828
78828
|
if (!Number.isFinite(n) || n < 1) return DEFAULT_SNAPSHOT_CHUNK;
|
|
78829
78829
|
return Math.floor(n);
|
|
78830
78830
|
}
|
|
78831
|
+
function classifyCountStatus(readable, expected, observed) {
|
|
78832
|
+
if (!readable) return { status: "unknown", unknownReason: "count-unreadable" };
|
|
78833
|
+
if (expected === null) return { status: "unknown", unknownReason: "no-expectation" };
|
|
78834
|
+
if (observed === expected) return { status: "match" };
|
|
78835
|
+
if ((observed ?? 0) > expected) return { status: "over" };
|
|
78836
|
+
if (observed === 0) return { status: "unknown", unknownReason: "count-did-not-move" };
|
|
78837
|
+
return { status: "unknown", unknownReason: "count-partial" };
|
|
78838
|
+
}
|
|
78831
78839
|
function writeAuditRecord(record2) {
|
|
78832
78840
|
const path = auditLogPath();
|
|
78833
78841
|
if (!path) return;
|
|
@@ -79470,16 +79478,61 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79470
79478
|
set _sMiss to ""
|
|
79471
79479
|
set _sPairs to {}
|
|
79472
79480
|
set _sChunk to ${chunk}
|
|
79481
|
+
-- The mailbox's MEASURED length, emitted only when it disagrees with the
|
|
79482
|
+
-- count (#187). -1 = "not measured", which is the normal case: the probe
|
|
79483
|
+
-- only runs a binary search when the count's last position is unreadable.
|
|
79484
|
+
-- Initialised here, not in the else-branch, or the skipped/unavailable
|
|
79485
|
+
-- paths would reference an unbound variable when emitting.
|
|
79486
|
+
set _sTrue to -1
|
|
79473
79487
|
if ${countVar} < 0 then
|
|
79474
79488
|
set _sStatus to "unavailable"
|
|
79475
79489
|
else if ${countVar} > ${max} then
|
|
79476
79490
|
set _sStatus to "skipped"
|
|
79477
79491
|
set _sPayload to "mailbox holds " & (${countVar} as string) & " messages, above ${AUDIT_SNAPSHOT_MAX_ENV}=${max}"
|
|
79478
79492
|
else
|
|
79493
|
+
-- #187: the count can read HIGH, and an out-of-range range RAISES as
|
|
79494
|
+
-- a whole rather than clamping. On a mailbox smaller than one chunk
|
|
79495
|
+
-- there is only ONE slice, so a high count made it fail entirely:
|
|
79496
|
+
-- _sPairs stayed empty, the status collapsed to "unavailable", and
|
|
79497
|
+
-- the record carried no holes and no warning. The collateral
|
|
79498
|
+
-- instrument switched itself off in exactly the stale direction #155
|
|
79499
|
+
-- evidences, silently.
|
|
79500
|
+
--
|
|
79501
|
+
-- So establish a bound that actually EXISTS before slicing. If the
|
|
79502
|
+
-- last position the count claims is readable, the count is not high
|
|
79503
|
+
-- and this costs one probe. Otherwise binary-search the true end,
|
|
79504
|
+
-- which is O(log n) probes and also MEASURES how stale the count is.
|
|
79505
|
+
set _sBound to ${countVar}
|
|
79506
|
+
if _sBound > 0 then
|
|
79507
|
+
set _sEndOk to false
|
|
79508
|
+
try
|
|
79509
|
+
get id of message _sBound of ${mbVar}
|
|
79510
|
+
set _sEndOk to true
|
|
79511
|
+
end try
|
|
79512
|
+
if not _sEndOk then
|
|
79513
|
+
set _sLoB to 0
|
|
79514
|
+
set _sHiB to _sBound
|
|
79515
|
+
repeat while (_sHiB - _sLoB) > 1
|
|
79516
|
+
set _sMid to (_sLoB + _sHiB) div 2
|
|
79517
|
+
set _sMidOk to false
|
|
79518
|
+
try
|
|
79519
|
+
get id of message _sMid of ${mbVar}
|
|
79520
|
+
set _sMidOk to true
|
|
79521
|
+
end try
|
|
79522
|
+
if _sMidOk then
|
|
79523
|
+
set _sLoB to _sMid
|
|
79524
|
+
else
|
|
79525
|
+
set _sHiB to _sMid
|
|
79526
|
+
end if
|
|
79527
|
+
end repeat
|
|
79528
|
+
set _sBound to _sLoB
|
|
79529
|
+
set _sTrue to _sLoB
|
|
79530
|
+
end if
|
|
79531
|
+
end if
|
|
79479
79532
|
set _sLo to 1
|
|
79480
|
-
repeat while _sLo <=
|
|
79533
|
+
repeat while _sLo <= _sBound
|
|
79481
79534
|
set _sHi to _sLo + _sChunk - 1
|
|
79482
|
-
if _sHi >
|
|
79535
|
+
if _sHi > _sBound then set _sHi to _sBound
|
|
79483
79536
|
set _sGot to false
|
|
79484
79537
|
repeat with _sTry from 1 to ${SNAPSHOT_SLICE_ATTEMPTS}
|
|
79485
79538
|
set _sIds to {}
|
|
@@ -79537,7 +79590,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79537
79590
|
-- PARTIAL under the existing rules and withholds the halves a
|
|
79538
79591
|
-- truncation would poison.
|
|
79539
79592
|
try
|
|
79540
|
-
set _sOverId to ((id of message (
|
|
79593
|
+
set _sOverId to ((id of message (_sBound + 1) of ${mbVar}) as string)
|
|
79541
79594
|
-- A specifier that CLAMPS rather than raising hands back the LAST
|
|
79542
79595
|
-- message instead of failing. That is not evidence of a truncation,
|
|
79543
79596
|
-- so only an id this enumeration did not already record counts.
|
|
@@ -79547,7 +79600,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79547
79600
|
end repeat
|
|
79548
79601
|
if not _sSeen then
|
|
79549
79602
|
if _sMiss is not "" then set _sMiss to _sMiss & ","
|
|
79550
|
-
set _sMiss to _sMiss & ((
|
|
79603
|
+
set _sMiss to _sMiss & ((_sBound + 1) as string) & "-end"
|
|
79551
79604
|
end if
|
|
79552
79605
|
end try
|
|
79553
79606
|
if _sMiss is not "" then
|
|
@@ -79562,7 +79615,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79562
79615
|
set _sPayload to _sPairs as string
|
|
79563
79616
|
set AppleScript's text item delimiters to _sTid
|
|
79564
79617
|
end if
|
|
79565
|
-
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}"`;
|
|
79618
|
+
set _out to _out & "${SNAP_TAG}${FIELD_SEP}" & ${acctExpr} & "${FIELD_SEP}" & ${mbExpr} & "${FIELD_SEP}${phase}${FIELD_SEP}" & _sStatus & "${FIELD_SEP}" & _sPayload & "${FIELD_SEP}" & _sMiss & "${FIELD_SEP}" & (_sTrue as string) & "${RECORD_SEP}"`;
|
|
79566
79619
|
}
|
|
79567
79620
|
/**
|
|
79568
79621
|
* AppleScript capturing the message the op is ABOUT to touch into `_pre`,
|
|
@@ -79623,13 +79676,15 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79623
79676
|
continue;
|
|
79624
79677
|
}
|
|
79625
79678
|
if (f[0] === SNAP_TAG) {
|
|
79679
|
+
const measured = f[7] !== void 0 && f[7] !== "" ? Number(f[7]) : -1;
|
|
79626
79680
|
snaps.push({
|
|
79627
79681
|
account: f[1] ?? "",
|
|
79628
79682
|
mailbox: f[2] ?? "",
|
|
79629
79683
|
phase: f[3] === "after" ? "after" : "before",
|
|
79630
79684
|
status: f[4] ?? "",
|
|
79631
79685
|
payload: f[5] ?? "",
|
|
79632
|
-
miss: f[6] ?? ""
|
|
79686
|
+
miss: f[6] ?? "",
|
|
79687
|
+
...Number.isFinite(measured) && measured >= 0 ? { measuredLength: measured } : {}
|
|
79633
79688
|
});
|
|
79634
79689
|
continue;
|
|
79635
79690
|
}
|
|
@@ -79724,25 +79779,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79724
79779
|
const readable = m.before >= 0 && m.after >= 0;
|
|
79725
79780
|
const observed = readable ? m.before - m.after : null;
|
|
79726
79781
|
const note = noteFor(m.account, m.mailbox);
|
|
79727
|
-
|
|
79728
|
-
let unknownReason;
|
|
79729
|
-
if (!readable) {
|
|
79730
|
-
status = "unknown";
|
|
79731
|
-
unknownReason = "count-unreadable";
|
|
79732
|
-
} else if (m.expected === null) {
|
|
79733
|
-
status = "unknown";
|
|
79734
|
-
unknownReason = "no-expectation";
|
|
79735
|
-
} else if (observed === m.expected) {
|
|
79736
|
-
status = "match";
|
|
79737
|
-
} else if ((observed ?? 0) > m.expected) {
|
|
79738
|
-
status = "over";
|
|
79739
|
-
} else if (observed === 0) {
|
|
79740
|
-
status = "unknown";
|
|
79741
|
-
unknownReason = "count-did-not-move";
|
|
79742
|
-
} else {
|
|
79743
|
-
status = "unknown";
|
|
79744
|
-
unknownReason = "count-partial";
|
|
79745
|
-
}
|
|
79782
|
+
const { status, unknownReason } = classifyCountStatus(readable, m.expected, observed);
|
|
79746
79783
|
return {
|
|
79747
79784
|
account: m.account,
|
|
79748
79785
|
mailbox: m.mailbox,
|
|
@@ -79813,6 +79850,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79813
79850
|
const unrequested = disappeared.filter(
|
|
79814
79851
|
(e) => !requestedNumericIds.has(canonicalNumericId(e.id))
|
|
79815
79852
|
);
|
|
79853
|
+
const countStale = [b, a].filter((s) => s.measuredLength !== void 0).map((s) => ({ phase: s.phase, measuredLength: s.measuredLength }));
|
|
79816
79854
|
const holes = [b, a].filter((s) => s.miss !== "").map((s) => ({ phase: s.phase, ranges: s.miss }));
|
|
79817
79855
|
if (holes.length === 0) {
|
|
79818
79856
|
collateral.push({
|
|
@@ -79821,7 +79859,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79821
79859
|
snapshot: "ok",
|
|
79822
79860
|
disappeared,
|
|
79823
79861
|
unrequested,
|
|
79824
|
-
appeared
|
|
79862
|
+
appeared,
|
|
79863
|
+
...countStale.length ? { countStale } : {}
|
|
79825
79864
|
});
|
|
79826
79865
|
continue;
|
|
79827
79866
|
}
|
|
@@ -79833,6 +79872,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79833
79872
|
account: g.account,
|
|
79834
79873
|
mailbox: g.mailbox,
|
|
79835
79874
|
snapshot: "partial",
|
|
79875
|
+
...countStale.length ? { countStale } : {},
|
|
79836
79876
|
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".`,
|
|
79837
79877
|
unobserved: holes,
|
|
79838
79878
|
...a.miss === "" ? { disappeared, unrequested } : {},
|
|
@@ -84425,7 +84465,15 @@ async function imapFetchAttachment(id, attachmentName, deps = {}) {
|
|
|
84425
84465
|
}
|
|
84426
84466
|
});
|
|
84427
84467
|
}
|
|
84428
|
-
async function
|
|
84468
|
+
async function mailboxCount(client, path) {
|
|
84469
|
+
try {
|
|
84470
|
+
const st = await client.status(path, { messages: true });
|
|
84471
|
+
return typeof st.messages === "number" ? st.messages : null;
|
|
84472
|
+
} catch {
|
|
84473
|
+
return null;
|
|
84474
|
+
}
|
|
84475
|
+
}
|
|
84476
|
+
async function imapBatch(ids, deps, op, opts = {}) {
|
|
84429
84477
|
const groups = /* @__PURE__ */ new Map();
|
|
84430
84478
|
const errors = [];
|
|
84431
84479
|
let failed = 0;
|
|
@@ -84442,15 +84490,39 @@ async function imapBatch(ids, deps, op) {
|
|
|
84442
84490
|
groups.set(key, g);
|
|
84443
84491
|
}
|
|
84444
84492
|
let success = 0;
|
|
84493
|
+
const countDelta = [];
|
|
84445
84494
|
for (const g of groups.values()) {
|
|
84446
84495
|
try {
|
|
84447
84496
|
await useClient(depsForAccount(g.account, deps), async (client) => {
|
|
84497
|
+
const before = opts.reconcile ? await mailboxCount(client, g.path) : null;
|
|
84448
84498
|
const lock = await client.getMailboxLock(g.path);
|
|
84449
84499
|
try {
|
|
84450
84500
|
await op(client, g.uids, g.path);
|
|
84451
84501
|
} finally {
|
|
84452
84502
|
lock.release();
|
|
84453
84503
|
}
|
|
84504
|
+
if (!opts.reconcile) return;
|
|
84505
|
+
const after = await mailboxCount(client, g.path);
|
|
84506
|
+
const readable = before !== null && after !== null;
|
|
84507
|
+
const observed = readable ? before - after : null;
|
|
84508
|
+
const { status, unknownReason } = classifyCountStatus(readable, g.uids.length, observed);
|
|
84509
|
+
countDelta.push({
|
|
84510
|
+
account: g.account,
|
|
84511
|
+
mailbox: g.path,
|
|
84512
|
+
before,
|
|
84513
|
+
after,
|
|
84514
|
+
expected: g.uids.length,
|
|
84515
|
+
observed,
|
|
84516
|
+
status,
|
|
84517
|
+
...unknownReason ? { unknownReason } : {},
|
|
84518
|
+
...unknownReason === "count-unreadable" ? { note: "The server did not answer STATUS for this mailbox" } : {},
|
|
84519
|
+
...unknownReason === "count-did-not-move" ? {
|
|
84520
|
+
note: `The mailbox count did not move. On a label store (Gmail) a message can stay visible in an all-mail view after being moved out of a label, so this is not by itself evidence the operation failed \u2014 check the destination.`
|
|
84521
|
+
} : {},
|
|
84522
|
+
...unknownReason === "count-partial" ? {
|
|
84523
|
+
note: `Fewer messages left than were operated on. \`observed\` is a LOWER BOUND on what left, not a count of what left \u2014 a concurrent delivery to this mailbox masks departures one-for-one.`
|
|
84524
|
+
} : {}
|
|
84525
|
+
});
|
|
84454
84526
|
});
|
|
84455
84527
|
success += g.uids.length;
|
|
84456
84528
|
} catch (e) {
|
|
@@ -84458,7 +84530,7 @@ async function imapBatch(ids, deps, op) {
|
|
|
84458
84530
|
errors.push(`${g.path}: ${errText(e)}`);
|
|
84459
84531
|
}
|
|
84460
84532
|
}
|
|
84461
|
-
return { success, failed, errors };
|
|
84533
|
+
return { success, failed, errors, ...countDelta.length ? { countDelta } : {} };
|
|
84462
84534
|
}
|
|
84463
84535
|
var imapBatchMarkRead = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
|
|
84464
84536
|
assertMutated(
|
|
@@ -84493,17 +84565,27 @@ var imapBatchUnflag = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) =
|
|
|
84493
84565
|
`IMAP unflag of ${uids.length} message(s)`
|
|
84494
84566
|
);
|
|
84495
84567
|
});
|
|
84496
|
-
var imapBatchDelete = (ids, deps = {}) => imapBatch(
|
|
84497
|
-
|
|
84498
|
-
|
|
84568
|
+
var imapBatchDelete = (ids, deps = {}) => imapBatch(
|
|
84569
|
+
ids,
|
|
84570
|
+
deps,
|
|
84571
|
+
async (c, uids, path) => {
|
|
84572
|
+
await trashUids(c, uids, path);
|
|
84573
|
+
},
|
|
84574
|
+
{ reconcile: true }
|
|
84575
|
+
);
|
|
84499
84576
|
function imapBatchMove(ids, destMailbox, deps = {}) {
|
|
84500
|
-
return imapBatch(
|
|
84501
|
-
|
|
84502
|
-
|
|
84503
|
-
|
|
84504
|
-
|
|
84505
|
-
|
|
84506
|
-
|
|
84577
|
+
return imapBatch(
|
|
84578
|
+
ids,
|
|
84579
|
+
deps,
|
|
84580
|
+
async (c, uids) => {
|
|
84581
|
+
const dest = await findMailboxPathOrThrow(c, destMailbox) ?? resolveMailboxPath(destMailbox, "list");
|
|
84582
|
+
assertMutated(
|
|
84583
|
+
await c.messageMove(uids, dest, { uid: true }),
|
|
84584
|
+
`IMAP move of ${uids.length} message(s) to "${dest}"`
|
|
84585
|
+
);
|
|
84586
|
+
},
|
|
84587
|
+
{ reconcile: true }
|
|
84588
|
+
);
|
|
84507
84589
|
}
|
|
84508
84590
|
function senderName(from) {
|
|
84509
84591
|
const a = from?.[0];
|
|
@@ -84702,13 +84784,15 @@ async function hybridBatchCounts(ids, appleFn, imapFn) {
|
|
|
84702
84784
|
fail += res.length - s;
|
|
84703
84785
|
errors.push(...res.filter((r) => !r.success && r.error).map((r) => r.error));
|
|
84704
84786
|
}
|
|
84787
|
+
let countDelta;
|
|
84705
84788
|
if (imapIds.length > 0) {
|
|
84706
84789
|
const r = await imapFn(imapIds);
|
|
84707
84790
|
success += r.success;
|
|
84708
84791
|
fail += r.failed;
|
|
84709
84792
|
errors.push(...r.errors);
|
|
84793
|
+
if (r.countDelta?.length) countDelta = r.countDelta;
|
|
84710
84794
|
}
|
|
84711
|
-
return { success, fail, errors };
|
|
84795
|
+
return { success, fail, errors, ...countDelta ? { countDelta } : {} };
|
|
84712
84796
|
}
|
|
84713
84797
|
function distinctErrors(errors) {
|
|
84714
84798
|
return [...new Set(errors.filter(Boolean))];
|
|
@@ -84747,6 +84831,10 @@ ${warnings.join("\n")}` : "";
|
|
|
84747
84831
|
function toManagerScope(args) {
|
|
84748
84832
|
return { account: args.sourceAccount, mailbox: args.sourceMailbox };
|
|
84749
84833
|
}
|
|
84834
|
+
function mergeCountDeltas(apple, imap) {
|
|
84835
|
+
const all = [...apple ?? [], ...imap ?? []];
|
|
84836
|
+
return all.length ? { countDelta: all } : {};
|
|
84837
|
+
}
|
|
84750
84838
|
async function runBatchDelete(deps, args) {
|
|
84751
84839
|
const { ids, sourceMailbox, sourceAccount } = args;
|
|
84752
84840
|
let forensics = { warnings: [] };
|
|
@@ -84770,7 +84858,10 @@ async function runBatchDelete(deps, args) {
|
|
|
84770
84858
|
allFailed: (n) => `Failed to delete all ${n} message(s)`,
|
|
84771
84859
|
partial: (ok, failed) => `Deleted ${ok} message(s), ${failed} failed`
|
|
84772
84860
|
},
|
|
84773
|
-
|
|
84861
|
+
// #181: merge both backends' reconciliation. An AppleScript-only batch is
|
|
84862
|
+
// unchanged; an IMAP-only one now reports a delta where it previously
|
|
84863
|
+
// reported nothing; a mixed batch reports both, per source mailbox.
|
|
84864
|
+
mergeCountDeltas(forensics.countDelta, counts.countDelta),
|
|
84774
84865
|
forensics.warnings
|
|
84775
84866
|
);
|
|
84776
84867
|
}
|
|
@@ -84804,7 +84895,10 @@ async function runBatchMove(deps, args) {
|
|
|
84804
84895
|
allFailed: (n) => `Failed to move all ${n} message(s)`,
|
|
84805
84896
|
partial: (ok, failed) => `Moved ${ok} message(s) to "${mailbox}", ${failed} failed`
|
|
84806
84897
|
},
|
|
84807
|
-
{
|
|
84898
|
+
{
|
|
84899
|
+
mailbox,
|
|
84900
|
+
...mergeCountDeltas(forensics.countDelta, counts.countDelta)
|
|
84901
|
+
},
|
|
84808
84902
|
forensics.warnings
|
|
84809
84903
|
);
|
|
84810
84904
|
}
|
package/package.json
CHANGED