apple-mail-mcp 2.14.1 → 2.15.1
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 +107 -37
- 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;
|
|
@@ -79162,6 +79170,26 @@ var COUNT_PARTIAL_NOTE = `Mail's count moved by less than this operation account
|
|
|
79162
79170
|
function snapshotKey(entry) {
|
|
79163
79171
|
return `${entry.id}\0${entry.messageId}`;
|
|
79164
79172
|
}
|
|
79173
|
+
function crossCheckRenumbered(disappeared, appeared) {
|
|
79174
|
+
const index = (entries) => {
|
|
79175
|
+
const m = /* @__PURE__ */ new Map();
|
|
79176
|
+
for (const e of entries) {
|
|
79177
|
+
if (!e.messageId) continue;
|
|
79178
|
+
m.set(e.messageId, m.has(e.messageId) ? null : e);
|
|
79179
|
+
}
|
|
79180
|
+
return m;
|
|
79181
|
+
};
|
|
79182
|
+
const gone = index(disappeared);
|
|
79183
|
+
const came = index(appeared);
|
|
79184
|
+
const out = [];
|
|
79185
|
+
for (const [mid, before] of gone) {
|
|
79186
|
+
const after = came.get(mid);
|
|
79187
|
+
if (!before || !after) continue;
|
|
79188
|
+
if (before.id === after.id) continue;
|
|
79189
|
+
out.push({ messageId: mid, before: before.id, after: after.id });
|
|
79190
|
+
}
|
|
79191
|
+
return out;
|
|
79192
|
+
}
|
|
79165
79193
|
function buildAppLevelScript(command) {
|
|
79166
79194
|
return `
|
|
79167
79195
|
tell application "Mail"
|
|
@@ -79771,25 +79799,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79771
79799
|
const readable = m.before >= 0 && m.after >= 0;
|
|
79772
79800
|
const observed = readable ? m.before - m.after : null;
|
|
79773
79801
|
const note = noteFor(m.account, m.mailbox);
|
|
79774
|
-
|
|
79775
|
-
let unknownReason;
|
|
79776
|
-
if (!readable) {
|
|
79777
|
-
status = "unknown";
|
|
79778
|
-
unknownReason = "count-unreadable";
|
|
79779
|
-
} else if (m.expected === null) {
|
|
79780
|
-
status = "unknown";
|
|
79781
|
-
unknownReason = "no-expectation";
|
|
79782
|
-
} else if (observed === m.expected) {
|
|
79783
|
-
status = "match";
|
|
79784
|
-
} else if ((observed ?? 0) > m.expected) {
|
|
79785
|
-
status = "over";
|
|
79786
|
-
} else if (observed === 0) {
|
|
79787
|
-
status = "unknown";
|
|
79788
|
-
unknownReason = "count-did-not-move";
|
|
79789
|
-
} else {
|
|
79790
|
-
status = "unknown";
|
|
79791
|
-
unknownReason = "count-partial";
|
|
79792
|
-
}
|
|
79802
|
+
const { status, unknownReason } = classifyCountStatus(readable, m.expected, observed);
|
|
79793
79803
|
return {
|
|
79794
79804
|
account: m.account,
|
|
79795
79805
|
mailbox: m.mailbox,
|
|
@@ -79855,8 +79865,12 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79855
79865
|
const afterEntries = this.parseSnapshot(a.payload);
|
|
79856
79866
|
const afterKeys = new Set(afterEntries.map((e) => snapshotKey(e)));
|
|
79857
79867
|
const beforeKeys = new Set(beforeEntries.map((e) => snapshotKey(e)));
|
|
79858
|
-
const
|
|
79859
|
-
const
|
|
79868
|
+
const rawDisappeared = beforeEntries.filter((e) => !afterKeys.has(snapshotKey(e)));
|
|
79869
|
+
const rawAppeared = afterEntries.filter((e) => !beforeKeys.has(snapshotKey(e)));
|
|
79870
|
+
const renumbered = crossCheckRenumbered(rawDisappeared, rawAppeared);
|
|
79871
|
+
const renumberedMids = new Set(renumbered.map((r) => r.messageId));
|
|
79872
|
+
const disappeared = rawDisappeared.filter((e) => !renumberedMids.has(e.messageId));
|
|
79873
|
+
const appeared = rawAppeared.filter((e) => !renumberedMids.has(e.messageId));
|
|
79860
79874
|
const unrequested = disappeared.filter(
|
|
79861
79875
|
(e) => !requestedNumericIds.has(canonicalNumericId(e.id))
|
|
79862
79876
|
);
|
|
@@ -79870,7 +79884,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79870
79884
|
disappeared,
|
|
79871
79885
|
unrequested,
|
|
79872
79886
|
appeared,
|
|
79873
|
-
...countStale.length ? { countStale } : {}
|
|
79887
|
+
...countStale.length ? { countStale } : {},
|
|
79888
|
+
...renumbered.length ? { renumbered } : {}
|
|
79874
79889
|
});
|
|
79875
79890
|
continue;
|
|
79876
79891
|
}
|
|
@@ -79883,6 +79898,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79883
79898
|
mailbox: g.mailbox,
|
|
79884
79899
|
snapshot: "partial",
|
|
79885
79900
|
...countStale.length ? { countStale } : {},
|
|
79901
|
+
...renumbered.length ? { renumbered } : {},
|
|
79886
79902
|
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".`,
|
|
79887
79903
|
unobserved: holes,
|
|
79888
79904
|
...a.miss === "" ? { disappeared, unrequested } : {},
|
|
@@ -84475,7 +84491,15 @@ async function imapFetchAttachment(id, attachmentName, deps = {}) {
|
|
|
84475
84491
|
}
|
|
84476
84492
|
});
|
|
84477
84493
|
}
|
|
84478
|
-
async function
|
|
84494
|
+
async function mailboxCount(client, path) {
|
|
84495
|
+
try {
|
|
84496
|
+
const st = await client.status(path, { messages: true });
|
|
84497
|
+
return typeof st.messages === "number" ? st.messages : null;
|
|
84498
|
+
} catch {
|
|
84499
|
+
return null;
|
|
84500
|
+
}
|
|
84501
|
+
}
|
|
84502
|
+
async function imapBatch(ids, deps, op, opts = {}) {
|
|
84479
84503
|
const groups = /* @__PURE__ */ new Map();
|
|
84480
84504
|
const errors = [];
|
|
84481
84505
|
let failed = 0;
|
|
@@ -84492,15 +84516,39 @@ async function imapBatch(ids, deps, op) {
|
|
|
84492
84516
|
groups.set(key, g);
|
|
84493
84517
|
}
|
|
84494
84518
|
let success = 0;
|
|
84519
|
+
const countDelta = [];
|
|
84495
84520
|
for (const g of groups.values()) {
|
|
84496
84521
|
try {
|
|
84497
84522
|
await useClient(depsForAccount(g.account, deps), async (client) => {
|
|
84523
|
+
const before = opts.reconcile ? await mailboxCount(client, g.path) : null;
|
|
84498
84524
|
const lock = await client.getMailboxLock(g.path);
|
|
84499
84525
|
try {
|
|
84500
84526
|
await op(client, g.uids, g.path);
|
|
84501
84527
|
} finally {
|
|
84502
84528
|
lock.release();
|
|
84503
84529
|
}
|
|
84530
|
+
if (!opts.reconcile) return;
|
|
84531
|
+
const after = await mailboxCount(client, g.path);
|
|
84532
|
+
const readable = before !== null && after !== null;
|
|
84533
|
+
const observed = readable ? before - after : null;
|
|
84534
|
+
const { status, unknownReason } = classifyCountStatus(readable, g.uids.length, observed);
|
|
84535
|
+
countDelta.push({
|
|
84536
|
+
account: g.account,
|
|
84537
|
+
mailbox: g.path,
|
|
84538
|
+
before,
|
|
84539
|
+
after,
|
|
84540
|
+
expected: g.uids.length,
|
|
84541
|
+
observed,
|
|
84542
|
+
status,
|
|
84543
|
+
...unknownReason ? { unknownReason } : {},
|
|
84544
|
+
...unknownReason === "count-unreadable" ? { note: "The server did not answer STATUS for this mailbox" } : {},
|
|
84545
|
+
...unknownReason === "count-did-not-move" ? {
|
|
84546
|
+
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.`
|
|
84547
|
+
} : {},
|
|
84548
|
+
...unknownReason === "count-partial" ? {
|
|
84549
|
+
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.`
|
|
84550
|
+
} : {}
|
|
84551
|
+
});
|
|
84504
84552
|
});
|
|
84505
84553
|
success += g.uids.length;
|
|
84506
84554
|
} catch (e) {
|
|
@@ -84508,7 +84556,7 @@ async function imapBatch(ids, deps, op) {
|
|
|
84508
84556
|
errors.push(`${g.path}: ${errText(e)}`);
|
|
84509
84557
|
}
|
|
84510
84558
|
}
|
|
84511
|
-
return { success, failed, errors };
|
|
84559
|
+
return { success, failed, errors, ...countDelta.length ? { countDelta } : {} };
|
|
84512
84560
|
}
|
|
84513
84561
|
var imapBatchMarkRead = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) => {
|
|
84514
84562
|
assertMutated(
|
|
@@ -84543,17 +84591,27 @@ var imapBatchUnflag = (ids, deps = {}) => imapBatch(ids, deps, async (c, uids) =
|
|
|
84543
84591
|
`IMAP unflag of ${uids.length} message(s)`
|
|
84544
84592
|
);
|
|
84545
84593
|
});
|
|
84546
|
-
var imapBatchDelete = (ids, deps = {}) => imapBatch(
|
|
84547
|
-
|
|
84548
|
-
|
|
84594
|
+
var imapBatchDelete = (ids, deps = {}) => imapBatch(
|
|
84595
|
+
ids,
|
|
84596
|
+
deps,
|
|
84597
|
+
async (c, uids, path) => {
|
|
84598
|
+
await trashUids(c, uids, path);
|
|
84599
|
+
},
|
|
84600
|
+
{ reconcile: true }
|
|
84601
|
+
);
|
|
84549
84602
|
function imapBatchMove(ids, destMailbox, deps = {}) {
|
|
84550
|
-
return imapBatch(
|
|
84551
|
-
|
|
84552
|
-
|
|
84553
|
-
|
|
84554
|
-
|
|
84555
|
-
|
|
84556
|
-
|
|
84603
|
+
return imapBatch(
|
|
84604
|
+
ids,
|
|
84605
|
+
deps,
|
|
84606
|
+
async (c, uids) => {
|
|
84607
|
+
const dest = await findMailboxPathOrThrow(c, destMailbox) ?? resolveMailboxPath(destMailbox, "list");
|
|
84608
|
+
assertMutated(
|
|
84609
|
+
await c.messageMove(uids, dest, { uid: true }),
|
|
84610
|
+
`IMAP move of ${uids.length} message(s) to "${dest}"`
|
|
84611
|
+
);
|
|
84612
|
+
},
|
|
84613
|
+
{ reconcile: true }
|
|
84614
|
+
);
|
|
84557
84615
|
}
|
|
84558
84616
|
function senderName(from) {
|
|
84559
84617
|
const a = from?.[0];
|
|
@@ -84752,13 +84810,15 @@ async function hybridBatchCounts(ids, appleFn, imapFn) {
|
|
|
84752
84810
|
fail += res.length - s;
|
|
84753
84811
|
errors.push(...res.filter((r) => !r.success && r.error).map((r) => r.error));
|
|
84754
84812
|
}
|
|
84813
|
+
let countDelta;
|
|
84755
84814
|
if (imapIds.length > 0) {
|
|
84756
84815
|
const r = await imapFn(imapIds);
|
|
84757
84816
|
success += r.success;
|
|
84758
84817
|
fail += r.failed;
|
|
84759
84818
|
errors.push(...r.errors);
|
|
84819
|
+
if (r.countDelta?.length) countDelta = r.countDelta;
|
|
84760
84820
|
}
|
|
84761
|
-
return { success, fail, errors };
|
|
84821
|
+
return { success, fail, errors, ...countDelta ? { countDelta } : {} };
|
|
84762
84822
|
}
|
|
84763
84823
|
function distinctErrors(errors) {
|
|
84764
84824
|
return [...new Set(errors.filter(Boolean))];
|
|
@@ -84797,6 +84857,10 @@ ${warnings.join("\n")}` : "";
|
|
|
84797
84857
|
function toManagerScope(args) {
|
|
84798
84858
|
return { account: args.sourceAccount, mailbox: args.sourceMailbox };
|
|
84799
84859
|
}
|
|
84860
|
+
function mergeCountDeltas(apple, imap) {
|
|
84861
|
+
const all = [...apple ?? [], ...imap ?? []];
|
|
84862
|
+
return all.length ? { countDelta: all } : {};
|
|
84863
|
+
}
|
|
84800
84864
|
async function runBatchDelete(deps, args) {
|
|
84801
84865
|
const { ids, sourceMailbox, sourceAccount } = args;
|
|
84802
84866
|
let forensics = { warnings: [] };
|
|
@@ -84820,7 +84884,10 @@ async function runBatchDelete(deps, args) {
|
|
|
84820
84884
|
allFailed: (n) => `Failed to delete all ${n} message(s)`,
|
|
84821
84885
|
partial: (ok, failed) => `Deleted ${ok} message(s), ${failed} failed`
|
|
84822
84886
|
},
|
|
84823
|
-
|
|
84887
|
+
// #181: merge both backends' reconciliation. An AppleScript-only batch is
|
|
84888
|
+
// unchanged; an IMAP-only one now reports a delta where it previously
|
|
84889
|
+
// reported nothing; a mixed batch reports both, per source mailbox.
|
|
84890
|
+
mergeCountDeltas(forensics.countDelta, counts.countDelta),
|
|
84824
84891
|
forensics.warnings
|
|
84825
84892
|
);
|
|
84826
84893
|
}
|
|
@@ -84854,7 +84921,10 @@ async function runBatchMove(deps, args) {
|
|
|
84854
84921
|
allFailed: (n) => `Failed to move all ${n} message(s)`,
|
|
84855
84922
|
partial: (ok, failed) => `Moved ${ok} message(s) to "${mailbox}", ${failed} failed`
|
|
84856
84923
|
},
|
|
84857
|
-
{
|
|
84924
|
+
{
|
|
84925
|
+
mailbox,
|
|
84926
|
+
...mergeCountDeltas(forensics.countDelta, counts.countDelta)
|
|
84927
|
+
},
|
|
84858
84928
|
forensics.warnings
|
|
84859
84929
|
);
|
|
84860
84930
|
}
|
package/package.json
CHANGED