apple-mail-mcp 2.10.16 → 2.10.21
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 +209 -0
- package/build/index.js +860 -87
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -78057,6 +78057,7 @@ import { tmpdir } from "os";
|
|
|
78057
78057
|
|
|
78058
78058
|
// src/utils/attachmentLimits.ts
|
|
78059
78059
|
var MAX_INLINE_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
78060
|
+
var MAX_IMAP_ATTACHMENT_BYTES = MAX_INLINE_ATTACHMENT_BYTES;
|
|
78060
78061
|
var MAX_INLINE_ATTACHMENT_BASE64_CHARS = Math.ceil(MAX_INLINE_ATTACHMENT_BYTES / 3) * 4;
|
|
78061
78062
|
var MAX_INLINE_ATTACHMENT_BASE64_INPUT_CHARS = MAX_INLINE_ATTACHMENT_BASE64_CHARS * 2;
|
|
78062
78063
|
function isInlineAttachmentBase64WithinLimit(contentBase64) {
|
|
@@ -78217,6 +78218,68 @@ function searchContactsDb(query, opts) {
|
|
|
78217
78218
|
return results;
|
|
78218
78219
|
}
|
|
78219
78220
|
|
|
78221
|
+
// src/services/auditLog.ts
|
|
78222
|
+
import { appendFileSync } from "node:fs";
|
|
78223
|
+
var AUDIT_LOG_ENV = "APPLE_MAIL_MCP_AUDIT_LOG";
|
|
78224
|
+
var AUDIT_SUBJECTS_ENV = "APPLE_MAIL_MCP_AUDIT_SUBJECTS";
|
|
78225
|
+
var AUDIT_SNAPSHOT_MAX_ENV = "APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX";
|
|
78226
|
+
var DEFAULT_SNAPSHOT_MAX = 2e3;
|
|
78227
|
+
function isOn(raw) {
|
|
78228
|
+
return /^(1|true|yes|on)$/i.test((raw ?? "").trim());
|
|
78229
|
+
}
|
|
78230
|
+
function auditLogPath() {
|
|
78231
|
+
const raw = process.env[AUDIT_LOG_ENV]?.trim();
|
|
78232
|
+
return raw ? raw : null;
|
|
78233
|
+
}
|
|
78234
|
+
function isAuditEnabled() {
|
|
78235
|
+
return auditLogPath() !== null;
|
|
78236
|
+
}
|
|
78237
|
+
function auditSubjectsEnabled() {
|
|
78238
|
+
return isAuditEnabled() && isOn(process.env[AUDIT_SUBJECTS_ENV]);
|
|
78239
|
+
}
|
|
78240
|
+
function auditSnapshotMax() {
|
|
78241
|
+
const raw = process.env[AUDIT_SNAPSHOT_MAX_ENV]?.trim();
|
|
78242
|
+
if (raw === void 0 || raw === "") return DEFAULT_SNAPSHOT_MAX;
|
|
78243
|
+
const n = Number(raw);
|
|
78244
|
+
if (!Number.isFinite(n) || n < 0) return DEFAULT_SNAPSHOT_MAX;
|
|
78245
|
+
return Math.floor(n);
|
|
78246
|
+
}
|
|
78247
|
+
function writeAuditRecord(record2) {
|
|
78248
|
+
const path = auditLogPath();
|
|
78249
|
+
if (!path) return;
|
|
78250
|
+
try {
|
|
78251
|
+
appendFileSync(path, `${JSON.stringify(record2)}
|
|
78252
|
+
`, "utf8");
|
|
78253
|
+
} catch (err) {
|
|
78254
|
+
console.error(
|
|
78255
|
+
`[apple-mail-mcp] audit log write failed (${path}): ${err instanceof Error ? err.message : String(err)}`
|
|
78256
|
+
);
|
|
78257
|
+
}
|
|
78258
|
+
}
|
|
78259
|
+
function writeDestructiveAudit(ctx, report) {
|
|
78260
|
+
if (!isAuditEnabled()) return;
|
|
78261
|
+
writeAuditRecord({
|
|
78262
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
78263
|
+
tool: ctx.tool,
|
|
78264
|
+
serverVersion: ctx.serverVersion,
|
|
78265
|
+
args: ctx.args,
|
|
78266
|
+
preImages: report.preImages,
|
|
78267
|
+
outcomes: report.outcomes,
|
|
78268
|
+
countDeltas: report.countDeltas,
|
|
78269
|
+
collateral: report.collateral,
|
|
78270
|
+
subjectsLogged: auditSubjectsEnabled()
|
|
78271
|
+
});
|
|
78272
|
+
}
|
|
78273
|
+
function countDeltaWarning(d) {
|
|
78274
|
+
if (d.status !== "over" || d.expected === null) return null;
|
|
78275
|
+
const extra = (d.observed ?? 0) - d.expected;
|
|
78276
|
+
const where = d.account ? `"${d.mailbox}" in account "${d.account}"` : `"${d.mailbox}"`;
|
|
78277
|
+
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.`;
|
|
78278
|
+
}
|
|
78279
|
+
function reconciliationWarnings(report) {
|
|
78280
|
+
return report.countDeltas.map(countDeltaWarning).filter((w) => w !== null);
|
|
78281
|
+
}
|
|
78282
|
+
|
|
78220
78283
|
// src/services/appleMailManager.ts
|
|
78221
78284
|
function getMailboxScanThreshold() {
|
|
78222
78285
|
const raw = process.env.APPLE_MAIL_MAX_SEARCH_MAILBOX;
|
|
@@ -78228,6 +78291,7 @@ function getMailboxScanThreshold() {
|
|
|
78228
78291
|
}
|
|
78229
78292
|
var SEARCH_ACCOUNT_BUDGET_SECONDS = 30;
|
|
78230
78293
|
var SEARCH_ACCOUNT_TIMEOUT_MS = 45e3;
|
|
78294
|
+
var GROUP_SEP = "";
|
|
78231
78295
|
var FIELD_SEP = "";
|
|
78232
78296
|
var RECORD_SEP = "";
|
|
78233
78297
|
var DIAG_MARKER = "DIAG";
|
|
@@ -78237,6 +78301,17 @@ var CONTENT_MARKER = "CONTENT";
|
|
|
78237
78301
|
var MSGID_MARKER = "MSGID";
|
|
78238
78302
|
var HTML_MARKER = "HTML";
|
|
78239
78303
|
var BATCH_FATAL = "FATAL";
|
|
78304
|
+
var RECON_TAG = "RECON";
|
|
78305
|
+
var SNAP_TAG = "SNAP";
|
|
78306
|
+
var SNAP_PAIR = "P";
|
|
78307
|
+
var SNAP_ITEM = "I";
|
|
78308
|
+
var DELIMITER_REPLACEMENT = "\uFFFD";
|
|
78309
|
+
function stripStreamDelimiters(value) {
|
|
78310
|
+
let out = value;
|
|
78311
|
+
for (const d of [GROUP_SEP, RECORD_SEP, FIELD_SEP])
|
|
78312
|
+
out = out.split(d).join(DELIMITER_REPLACEMENT);
|
|
78313
|
+
return out;
|
|
78314
|
+
}
|
|
78240
78315
|
var AMBIGUOUS_ID_PREFIX = "Message id ";
|
|
78241
78316
|
var AMBIGUOUS_ID_BATCH = "This message id is present in more than one mailbox ";
|
|
78242
78317
|
function normalizeRfcMessageId(mid) {
|
|
@@ -78473,6 +78548,19 @@ function buildAccountScopedScript(account, command) {
|
|
|
78473
78548
|
end tell
|
|
78474
78549
|
`;
|
|
78475
78550
|
}
|
|
78551
|
+
function groupKey(account, mailbox) {
|
|
78552
|
+
return `${account}\0${mailbox}`;
|
|
78553
|
+
}
|
|
78554
|
+
function canonicalNumericId(raw) {
|
|
78555
|
+
const trimmed = (raw ?? "").trim();
|
|
78556
|
+
if (trimmed === "") return "";
|
|
78557
|
+
const n = Number(trimmed);
|
|
78558
|
+
return Number.isFinite(n) ? String(n) : trimmed;
|
|
78559
|
+
}
|
|
78560
|
+
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.";
|
|
78561
|
+
function snapshotKey(entry) {
|
|
78562
|
+
return `${entry.id}\0${entry.messageId}`;
|
|
78563
|
+
}
|
|
78476
78564
|
function buildAppLevelScript(command) {
|
|
78477
78565
|
return `
|
|
78478
78566
|
tell application "Mail"
|
|
@@ -78615,6 +78703,428 @@ var AppleMailManager = class {
|
|
|
78615
78703
|
if (count of _mbM) is 1 then set _tmb to item 1 of _mbM
|
|
78616
78704
|
end if`;
|
|
78617
78705
|
}
|
|
78706
|
+
// ===========================================================================
|
|
78707
|
+
// Destructive-operation forensics (#155)
|
|
78708
|
+
// ===========================================================================
|
|
78709
|
+
/**
|
|
78710
|
+
* What the last destructive operation observed about its own effect.
|
|
78711
|
+
*
|
|
78712
|
+
* Read once, by the tool layer, immediately after the call — every
|
|
78713
|
+
* AppleScript path in this class is synchronous (`spawnSync`), so there is no
|
|
78714
|
+
* await between the mutation and the read and no other operation can land in
|
|
78715
|
+
* between.
|
|
78716
|
+
*
|
|
78717
|
+
* ## Lifetime (one rule, no exceptions)
|
|
78718
|
+
*
|
|
78719
|
+
* The report belongs to the MOST RECENT message mutation, whatever it was.
|
|
78720
|
+
* `beginMutation()` clears it at the start of EVERY message mutation —
|
|
78721
|
+
* destructive or not, instrumented or not — and `consumeLastForensics()`
|
|
78722
|
+
* clears it on read. So the only two answers a caller can get are "the report
|
|
78723
|
+
* for the call I just made" and `undefined`; a mutation that produces no
|
|
78724
|
+
* report can never hand back the previous one's.
|
|
78725
|
+
*
|
|
78726
|
+
* It used to be cleared only by the instrumented paths, which left
|
|
78727
|
+
* `batch-mark-as-read` returning the preceding `batch-delete-messages`'
|
|
78728
|
+
* evidence if nobody had consumed it.
|
|
78729
|
+
*/
|
|
78730
|
+
lastForensics;
|
|
78731
|
+
/**
|
|
78732
|
+
* Start of a message mutation: invalidate whatever the previous one observed.
|
|
78733
|
+
*
|
|
78734
|
+
* Called by every single-message mutation (via `findMessageScript`), by
|
|
78735
|
+
* `moveMessage` (which builds its own script) and by `runBatchOperation`.
|
|
78736
|
+
*/
|
|
78737
|
+
beginMutation() {
|
|
78738
|
+
this.lastForensics = void 0;
|
|
78739
|
+
}
|
|
78740
|
+
/** Take (and clear) the forensic report for the destructive op just run. */
|
|
78741
|
+
consumeLastForensics() {
|
|
78742
|
+
const r = this.lastForensics;
|
|
78743
|
+
this.lastForensics = void 0;
|
|
78744
|
+
return r;
|
|
78745
|
+
}
|
|
78746
|
+
/**
|
|
78747
|
+
* AppleScript that reads a mailbox's message count into `varName`, leaving
|
|
78748
|
+
* `-1` when Mail will not answer. Two Apple Events per mutation group, inside
|
|
78749
|
+
* the script that is already running: no extra `osascript`.
|
|
78750
|
+
*/
|
|
78751
|
+
countFragment(varName, mbVar = "_tmb") {
|
|
78752
|
+
return `
|
|
78753
|
+
set ${varName} to -1
|
|
78754
|
+
try
|
|
78755
|
+
set ${varName} to (count of messages of ${mbVar})
|
|
78756
|
+
end try`;
|
|
78757
|
+
}
|
|
78758
|
+
/**
|
|
78759
|
+
* AppleScript that strips the stream's structural bytes out of `varName`,
|
|
78760
|
+
* in place, before it is appended to the record stream.
|
|
78761
|
+
*
|
|
78762
|
+
* This is the source-side half of the defence described on
|
|
78763
|
+
* `stripStreamDelimiters`: the values that go into a pre-image or a snapshot
|
|
78764
|
+
* (RFC Message-ID, `date received`, subject, mailbox and account names) are
|
|
78765
|
+
* attacker-influenced — a Message-ID is whatever the sender put in the
|
|
78766
|
+
* header — and a crafted one containing a RECORD_SEP plus a forged `RECON`
|
|
78767
|
+
* tag would otherwise inject a reconciliation record, fabricating an `over`
|
|
78768
|
+
* warning on an operation that did exactly the right thing.
|
|
78769
|
+
*
|
|
78770
|
+
* One pass: AppleScript accepts a LIST of text item delimiters when splitting
|
|
78771
|
+
* and uses the first when joining, so all three characters are replaced in a
|
|
78772
|
+
* single `text items` round trip. Verified with `osascript` directly.
|
|
78773
|
+
*
|
|
78774
|
+
* Deliberately distinct variable names (`_zTid`, `_zParts`) — AppleScript
|
|
78775
|
+
* identifiers are case-insensitive, so `_stid` would be the same variable as
|
|
78776
|
+
* the snapshot fragment's `_sTid`.
|
|
78777
|
+
*/
|
|
78778
|
+
sanitizeFragment(varName, indent = " ") {
|
|
78779
|
+
return `
|
|
78780
|
+
${indent}set _zTid to AppleScript's text item delimiters
|
|
78781
|
+
${indent}set AppleScript's text item delimiters to {"${GROUP_SEP}", "${RECORD_SEP}", "${FIELD_SEP}"}
|
|
78782
|
+
${indent}set _zParts to text items of ${varName}
|
|
78783
|
+
${indent}set AppleScript's text item delimiters to "${DELIMITER_REPLACEMENT}"
|
|
78784
|
+
${indent}set ${varName} to _zParts as string
|
|
78785
|
+
${indent}set AppleScript's text item delimiters to _zTid`;
|
|
78786
|
+
}
|
|
78787
|
+
/**
|
|
78788
|
+
* AppleScript emitting one `error:` outcome record into `_out`, with the
|
|
78789
|
+
* runtime error text sanitised first.
|
|
78790
|
+
*
|
|
78791
|
+
* Mail composes that text, and it routinely quotes back a mailbox or message
|
|
78792
|
+
* property, so it is a runtime-read value like any other — the same invariant
|
|
78793
|
+
* that covers the Message-ID and the snapshot covers it. `_zErr` (not `_e`)
|
|
78794
|
+
* because `sanitizeFragment` rewrites its variable in place and the handler's
|
|
78795
|
+
* own binding should be left alone.
|
|
78796
|
+
*/
|
|
78797
|
+
errorEmit(indent) {
|
|
78798
|
+
return `${indent}set _zErr to (_e as string)${this.sanitizeFragment("_zErr", indent)}
|
|
78799
|
+
${indent}set _out to _out & (_idx as string) & "${FIELD_SEP}error:" & _zErr & "${RECORD_SEP}"`;
|
|
78800
|
+
}
|
|
78801
|
+
/** AppleScript emitting one RECON record into `_out`. */
|
|
78802
|
+
reconEmit(acctExpr, mbExpr, beforeVar, afterVar, posExpr = '""') {
|
|
78803
|
+
return `
|
|
78804
|
+
set _out to _out & "${RECON_TAG}${FIELD_SEP}" & ${acctExpr} & "${FIELD_SEP}" & ${mbExpr} & "${FIELD_SEP}" & (${beforeVar} as string) & "${FIELD_SEP}" & (${afterVar} as string) & "${FIELD_SEP}" & ${posExpr} & "${RECORD_SEP}"`;
|
|
78805
|
+
}
|
|
78806
|
+
/**
|
|
78807
|
+
* RECON emission for the unlocated paths, where the account and mailbox names
|
|
78808
|
+
* are read from Mail at runtime (`_uacct`, `mailbox of _msg`) instead of being
|
|
78809
|
+
* interpolated as literals from here — so they get the same delimiter
|
|
78810
|
+
* stripping the literal paths get in TypeScript.
|
|
78811
|
+
*/
|
|
78812
|
+
reconEmitFromMessage(beforeVar, afterVar, posExpr = '""', indent = " ") {
|
|
78813
|
+
return `set _umbName to ""
|
|
78814
|
+
${indent}try
|
|
78815
|
+
${indent} set _umbName to (name of _umb)
|
|
78816
|
+
${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragment("_umbName", indent)}${this.reconEmit("_uacct", "_umbName", beforeVar, afterVar, posExpr)}`;
|
|
78817
|
+
}
|
|
78818
|
+
/**
|
|
78819
|
+
* AppleScript capturing every (numeric id, RFC Message-ID) pair in a mailbox
|
|
78820
|
+
* into a SNAP record — the before/after pair the collateral diff subtracts.
|
|
78821
|
+
*
|
|
78822
|
+
* Empty string when the audit log is off or the snapshot is disabled, so the
|
|
78823
|
+
* whole layer costs literally nothing by default. The two property reads are
|
|
78824
|
+
* BULK (`id of messages of mb`, `message id of messages of mb`) — two Apple
|
|
78825
|
+
* Events for the whole mailbox rather than two per message — and the joining
|
|
78826
|
+
* is pure in-memory AppleScript.
|
|
78827
|
+
*
|
|
78828
|
+
* Above `APPLE_MAIL_MCP_AUDIT_SNAPSHOT_MAX` messages the snapshot is skipped,
|
|
78829
|
+
* and the skip is EMITTED as a record with its reason. A silently skipped
|
|
78830
|
+
* snapshot would read as "nothing collateral happened".
|
|
78831
|
+
*
|
|
78832
|
+
* Caveat recorded in the docs: `(id of msg) as string` renders a Mail id above
|
|
78833
|
+
* AppleScript's 2^29 integer range in scientific notation. The Message-ID is
|
|
78834
|
+
* the authoritative key in this record for exactly that reason; the numeric id
|
|
78835
|
+
* is a convenience — and it is put back into decimal form by
|
|
78836
|
+
* `canonicalNumericId` in `parseSnapshot`, because the raw exponential string
|
|
78837
|
+
* would otherwise fail the `unrequested` membership test and name a REQUESTED
|
|
78838
|
+
* message as collateral.
|
|
78839
|
+
*/
|
|
78840
|
+
snapshotFragment(phase, acctExpr, mbExpr, countVar, mbVar = "_tmb") {
|
|
78841
|
+
const max = auditSnapshotMax();
|
|
78842
|
+
if (!isAuditEnabled() || max <= 0) return "";
|
|
78843
|
+
return `
|
|
78844
|
+
set _sStatus to "ok"
|
|
78845
|
+
set _sPayload to ""
|
|
78846
|
+
set _sIds to {}
|
|
78847
|
+
set _sMids to {}
|
|
78848
|
+
if ${countVar} < 0 then
|
|
78849
|
+
set _sStatus to "unavailable"
|
|
78850
|
+
else if ${countVar} > ${max} then
|
|
78851
|
+
set _sStatus to "skipped"
|
|
78852
|
+
set _sPayload to "mailbox holds " & (${countVar} as string) & " messages, above ${AUDIT_SNAPSHOT_MAX_ENV}=${max}"
|
|
78853
|
+
else
|
|
78854
|
+
try
|
|
78855
|
+
set _sIds to (id of messages of ${mbVar})
|
|
78856
|
+
set _sMids to (message id of messages of ${mbVar})
|
|
78857
|
+
on error
|
|
78858
|
+
set _sStatus to "unavailable"
|
|
78859
|
+
end try
|
|
78860
|
+
if _sStatus is "ok" then
|
|
78861
|
+
if (count of _sIds) is not (count of _sMids) then
|
|
78862
|
+
set _sStatus to "unavailable"
|
|
78863
|
+
else
|
|
78864
|
+
set _sPairs to {}
|
|
78865
|
+
repeat with _q from 1 to (count of _sIds)
|
|
78866
|
+
set _sOne to ""
|
|
78867
|
+
try
|
|
78868
|
+
set _zSnapMid to ((item _q of _sMids) as string)${this.sanitizeFragment("_zSnapMid", " ")}
|
|
78869
|
+
set _sOne to ((item _q of _sIds) as string) & "${SNAP_PAIR}" & _zSnapMid
|
|
78870
|
+
on error
|
|
78871
|
+
set _sOne to ((item _q of _sIds) as string) & "${SNAP_PAIR}"
|
|
78872
|
+
end try
|
|
78873
|
+
set end of _sPairs to _sOne
|
|
78874
|
+
end repeat
|
|
78875
|
+
set _sTid to AppleScript's text item delimiters
|
|
78876
|
+
set AppleScript's text item delimiters to "${SNAP_ITEM}"
|
|
78877
|
+
set _sPayload to _sPairs as string
|
|
78878
|
+
set AppleScript's text item delimiters to _sTid
|
|
78879
|
+
end if
|
|
78880
|
+
end if
|
|
78881
|
+
end if
|
|
78882
|
+
set _out to _out & "${SNAP_TAG}${FIELD_SEP}" & ${acctExpr} & "${FIELD_SEP}" & ${mbExpr} & "${FIELD_SEP}${phase}${FIELD_SEP}" & _sStatus & "${FIELD_SEP}" & _sPayload & "${RECORD_SEP}"`;
|
|
78883
|
+
}
|
|
78884
|
+
/**
|
|
78885
|
+
* AppleScript capturing the message the op is ABOUT to touch into `_pre`,
|
|
78886
|
+
* appended to that id's outcome record.
|
|
78887
|
+
*
|
|
78888
|
+
* Empty when the audit log is off — the pre-image is the only per-message cost
|
|
78889
|
+
* in this feature, and it must not exist by default. Subjects need the second,
|
|
78890
|
+
* separate opt-in; message bodies are never read.
|
|
78891
|
+
*
|
|
78892
|
+
* Every value here is EXTERNALLY CONTROLLED (the Message-ID and the subject
|
|
78893
|
+
* are whatever the sender wrote), so each one is stripped of the stream's
|
|
78894
|
+
* structural bytes before it is appended — see `sanitizeFragment`.
|
|
78895
|
+
*/
|
|
78896
|
+
preImageFragment(msgVar = "_msg") {
|
|
78897
|
+
if (!isAuditEnabled()) return "";
|
|
78898
|
+
const ind = " ";
|
|
78899
|
+
const subject = auditSubjectsEnabled() ? `
|
|
78900
|
+
if _pre is not "" then
|
|
78901
|
+
try
|
|
78902
|
+
set _zSub to ((subject of ${msgVar}) as string)${this.sanitizeFragment("_zSub", ind + " ")}
|
|
78903
|
+
set _pre to _pre & "${FIELD_SEP}" & _zSub
|
|
78904
|
+
end try
|
|
78905
|
+
end if` : "";
|
|
78906
|
+
return `
|
|
78907
|
+
try
|
|
78908
|
+
set _zMid to ((message id of ${msgVar}) as string)${this.sanitizeFragment("_zMid", ind)}
|
|
78909
|
+
set _zDate to ((date received of ${msgVar}) as string)${this.sanitizeFragment("_zDate", ind)}
|
|
78910
|
+
set _pre to "${FIELD_SEP}" & _zMid & "${FIELD_SEP}" & _zDate
|
|
78911
|
+
end try${subject}`;
|
|
78912
|
+
}
|
|
78913
|
+
/**
|
|
78914
|
+
* Parse the delimited stream a destructive AppleScript returns: per-id
|
|
78915
|
+
* outcomes (with their optional pre-image), RECON records and SNAP records.
|
|
78916
|
+
*
|
|
78917
|
+
* `valid` maps 1-based positions back to the id strings the caller passed —
|
|
78918
|
+
* outcomes are reported BY POSITION because a Mail id past 2^29 does not
|
|
78919
|
+
* survive `as string` (see runBatchOperation).
|
|
78920
|
+
*/
|
|
78921
|
+
parseForensicStream(output, valid) {
|
|
78922
|
+
const byId = /* @__PURE__ */ new Map();
|
|
78923
|
+
const okPositions = /* @__PURE__ */ new Set();
|
|
78924
|
+
const outcomes = [];
|
|
78925
|
+
const preImages = /* @__PURE__ */ new Map();
|
|
78926
|
+
const recons = [];
|
|
78927
|
+
const snaps = [];
|
|
78928
|
+
for (const rec of output.split(RECORD_SEP)) {
|
|
78929
|
+
if (!rec) continue;
|
|
78930
|
+
const f = rec.split(FIELD_SEP);
|
|
78931
|
+
if (f.length < 2) continue;
|
|
78932
|
+
if (f[0] === RECON_TAG) {
|
|
78933
|
+
recons.push({
|
|
78934
|
+
account: f[1] ?? "",
|
|
78935
|
+
mailbox: f[2] ?? "",
|
|
78936
|
+
before: Number(f[3]),
|
|
78937
|
+
after: Number(f[4]),
|
|
78938
|
+
pos: f[5] ? Number(f[5]) : null
|
|
78939
|
+
});
|
|
78940
|
+
continue;
|
|
78941
|
+
}
|
|
78942
|
+
if (f[0] === SNAP_TAG) {
|
|
78943
|
+
snaps.push({
|
|
78944
|
+
account: f[1] ?? "",
|
|
78945
|
+
mailbox: f[2] ?? "",
|
|
78946
|
+
phase: f[3] === "after" ? "after" : "before",
|
|
78947
|
+
status: f[4] ?? "",
|
|
78948
|
+
payload: f[5] ?? ""
|
|
78949
|
+
});
|
|
78950
|
+
continue;
|
|
78951
|
+
}
|
|
78952
|
+
const pos = Number(f[0]);
|
|
78953
|
+
const entry = valid[pos - 1];
|
|
78954
|
+
if (!entry) continue;
|
|
78955
|
+
const status = f[1];
|
|
78956
|
+
const id = entry.id;
|
|
78957
|
+
if (status === "ok") {
|
|
78958
|
+
byId.set(id, { id, success: true });
|
|
78959
|
+
okPositions.add(pos);
|
|
78960
|
+
outcomes.push({ id, status: "ok" });
|
|
78961
|
+
if (f.length >= 4) {
|
|
78962
|
+
preImages.set(pos, {
|
|
78963
|
+
messageId: f[2] || null,
|
|
78964
|
+
date: f[3] || null,
|
|
78965
|
+
...f.length >= 5 ? { subject: f[4] } : {}
|
|
78966
|
+
});
|
|
78967
|
+
}
|
|
78968
|
+
} else if (status === "notfound") {
|
|
78969
|
+
byId.set(id, { id, success: false, error: "Message not found" });
|
|
78970
|
+
outcomes.push({ id, status: "notfound" });
|
|
78971
|
+
} else if (status.startsWith("error:")) {
|
|
78972
|
+
const error2 = f.slice(1).join(FIELD_SEP).slice("error:".length);
|
|
78973
|
+
byId.set(id, { id, success: false, error: error2 });
|
|
78974
|
+
outcomes.push({ id, status: "error", error: error2 });
|
|
78975
|
+
} else {
|
|
78976
|
+
const error2 = status || "Unknown error";
|
|
78977
|
+
byId.set(id, { id, success: false, error: error2 });
|
|
78978
|
+
outcomes.push({ id, status: "error", error: error2 });
|
|
78979
|
+
}
|
|
78980
|
+
}
|
|
78981
|
+
return { byId, okPositions, outcomes, preImages, recons, snaps };
|
|
78982
|
+
}
|
|
78983
|
+
/**
|
|
78984
|
+
* Parse one SNAP payload into (numeric id → RFC Message-ID) entries.
|
|
78985
|
+
*
|
|
78986
|
+
* The id is CANONICALISED as it is parsed (`canonicalNumericId`), because
|
|
78987
|
+
* AppleScript renders a Mail id above 2^29 in scientific notation. That is the
|
|
78988
|
+
* only point where the AppleScript representation and the caller's own id
|
|
78989
|
+
* strings meet, so normalising here fixes both the `unrequested` membership
|
|
78990
|
+
* test and the id the report hands back to a human.
|
|
78991
|
+
*/
|
|
78992
|
+
parseSnapshot(payload) {
|
|
78993
|
+
if (!payload) return [];
|
|
78994
|
+
return payload.split(SNAP_ITEM).map((entry) => {
|
|
78995
|
+
const i = entry.indexOf(SNAP_PAIR);
|
|
78996
|
+
return i < 0 ? { id: canonicalNumericId(entry), messageId: "" } : {
|
|
78997
|
+
id: canonicalNumericId(entry.slice(0, i)),
|
|
78998
|
+
messageId: entry.slice(i + SNAP_PAIR.length)
|
|
78999
|
+
};
|
|
79000
|
+
});
|
|
79001
|
+
}
|
|
79002
|
+
/**
|
|
79003
|
+
* Turn the raw RECON/SNAP records into the report the tool layer reports on.
|
|
79004
|
+
*
|
|
79005
|
+
* `expectedFor(account, mailbox, pos)` says how many messages the operation
|
|
79006
|
+
* should have removed from that mailbox — the caller knows this because only
|
|
79007
|
+
* the caller knows which ids succeeded and whether a move's destination IS the
|
|
79008
|
+
* source mailbox.
|
|
79009
|
+
*
|
|
79010
|
+
* It returns **null** for "not predictable", and null propagates: the mailbox
|
|
79011
|
+
* is classified `unknown` and no comparison is made. That is the only honest
|
|
79012
|
+
* answer for a self-move — Mail's behaviour when a message is re-filed into
|
|
79013
|
+
* the mailbox it already occupies is unspecified, so any number here would be
|
|
79014
|
+
* a guess, and a guess is what turns this instrumentation into a false alarm.
|
|
79015
|
+
*
|
|
79016
|
+
* `requestedNumericIds` MUST already be canonical (`canonicalNumericId`): it is
|
|
79017
|
+
* compared against ids that came back through AppleScript, where a value above
|
|
79018
|
+
* 2^29 arrives in scientific notation.
|
|
79019
|
+
*/
|
|
79020
|
+
buildForensicReport(parsed, valid, expectedFor, locationFor, noteFor, requestedNumericIds) {
|
|
79021
|
+
const merged = /* @__PURE__ */ new Map();
|
|
79022
|
+
for (const r of parsed.recons) {
|
|
79023
|
+
const key = groupKey(r.account, r.mailbox);
|
|
79024
|
+
const expected = expectedFor(r.account, r.mailbox, r.pos);
|
|
79025
|
+
const prev = merged.get(key);
|
|
79026
|
+
if (prev) {
|
|
79027
|
+
prev.after = r.after;
|
|
79028
|
+
prev.expected = prev.expected === null || expected === null ? null : prev.expected + expected;
|
|
79029
|
+
} else {
|
|
79030
|
+
merged.set(key, {
|
|
79031
|
+
account: r.account,
|
|
79032
|
+
mailbox: r.mailbox,
|
|
79033
|
+
before: r.before,
|
|
79034
|
+
after: r.after,
|
|
79035
|
+
expected
|
|
79036
|
+
});
|
|
79037
|
+
}
|
|
79038
|
+
}
|
|
79039
|
+
const countDeltas = [...merged.values()].map((m) => {
|
|
79040
|
+
const readable = m.before >= 0 && m.after >= 0;
|
|
79041
|
+
const observed = readable ? m.before - m.after : null;
|
|
79042
|
+
const note = noteFor(m.account, m.mailbox);
|
|
79043
|
+
let status;
|
|
79044
|
+
if (!readable || m.expected === null) status = "unknown";
|
|
79045
|
+
else if (observed === m.expected) status = "match";
|
|
79046
|
+
else if ((observed ?? 0) > m.expected) status = "over";
|
|
79047
|
+
else status = "under";
|
|
79048
|
+
return {
|
|
79049
|
+
account: m.account,
|
|
79050
|
+
mailbox: m.mailbox,
|
|
79051
|
+
before: readable ? m.before : null,
|
|
79052
|
+
after: readable ? m.after : null,
|
|
79053
|
+
expected: m.expected,
|
|
79054
|
+
observed,
|
|
79055
|
+
status,
|
|
79056
|
+
...note ? { note } : {},
|
|
79057
|
+
...status === "unknown" && readable === false ? { note: note ?? "Mail did not report a message count for this mailbox" } : {},
|
|
79058
|
+
...status === "under" && !note ? {
|
|
79059
|
+
note: "Fewer messages left the mailbox than were operated on. This is normal on a store that flags deletions instead of removing them (and when new mail arrives mid-operation), so it is reported but not warned about."
|
|
79060
|
+
} : {}
|
|
79061
|
+
};
|
|
79062
|
+
});
|
|
79063
|
+
const preImages = [];
|
|
79064
|
+
for (const [pos, pre] of parsed.preImages) {
|
|
79065
|
+
const entry = valid[pos - 1];
|
|
79066
|
+
if (!entry) continue;
|
|
79067
|
+
const loc = locationFor(pos);
|
|
79068
|
+
preImages.push({
|
|
79069
|
+
id: entry.id,
|
|
79070
|
+
account: loc.account,
|
|
79071
|
+
mailbox: loc.mailbox,
|
|
79072
|
+
messageId: pre.messageId,
|
|
79073
|
+
date: pre.date,
|
|
79074
|
+
...pre.subject !== void 0 ? { subject: pre.subject } : {}
|
|
79075
|
+
});
|
|
79076
|
+
}
|
|
79077
|
+
const collateral = [];
|
|
79078
|
+
const byMailbox = /* @__PURE__ */ new Map();
|
|
79079
|
+
for (const s of parsed.snaps) {
|
|
79080
|
+
const key = groupKey(s.account, s.mailbox);
|
|
79081
|
+
const g = byMailbox.get(key) ?? { account: s.account, mailbox: s.mailbox };
|
|
79082
|
+
if (s.phase === "before") g.before = s;
|
|
79083
|
+
else g.after = s;
|
|
79084
|
+
byMailbox.set(key, g);
|
|
79085
|
+
}
|
|
79086
|
+
for (const g of byMailbox.values()) {
|
|
79087
|
+
const b = g.before;
|
|
79088
|
+
const a = g.after;
|
|
79089
|
+
if (!b || !a) {
|
|
79090
|
+
collateral.push({
|
|
79091
|
+
account: g.account,
|
|
79092
|
+
mailbox: g.mailbox,
|
|
79093
|
+
snapshot: "unavailable",
|
|
79094
|
+
skipReason: "only one of the before/after snapshots was produced"
|
|
79095
|
+
});
|
|
79096
|
+
continue;
|
|
79097
|
+
}
|
|
79098
|
+
if (b.status !== "ok" || a.status !== "ok") {
|
|
79099
|
+
const bad = b.status !== "ok" ? b : a;
|
|
79100
|
+
collateral.push({
|
|
79101
|
+
account: g.account,
|
|
79102
|
+
mailbox: g.mailbox,
|
|
79103
|
+
snapshot: bad.status === "skipped" ? "skipped" : "unavailable",
|
|
79104
|
+
skipReason: bad.payload || `Mail would not produce the ${bad === b ? "before" : "after"} snapshot for this mailbox`
|
|
79105
|
+
});
|
|
79106
|
+
continue;
|
|
79107
|
+
}
|
|
79108
|
+
const beforeEntries = this.parseSnapshot(b.payload);
|
|
79109
|
+
const afterEntries = this.parseSnapshot(a.payload);
|
|
79110
|
+
const afterKeys = new Set(afterEntries.map((e) => snapshotKey(e)));
|
|
79111
|
+
const beforeKeys = new Set(beforeEntries.map((e) => snapshotKey(e)));
|
|
79112
|
+
const disappeared = beforeEntries.filter((e) => !afterKeys.has(snapshotKey(e)));
|
|
79113
|
+
const appeared = afterEntries.filter((e) => !beforeKeys.has(snapshotKey(e)));
|
|
79114
|
+
const unrequested = disappeared.filter(
|
|
79115
|
+
(e) => !requestedNumericIds.has(canonicalNumericId(e.id))
|
|
79116
|
+
);
|
|
79117
|
+
collateral.push({
|
|
79118
|
+
account: g.account,
|
|
79119
|
+
mailbox: g.mailbox,
|
|
79120
|
+
snapshot: "ok",
|
|
79121
|
+
disappeared,
|
|
79122
|
+
unrequested,
|
|
79123
|
+
appeared
|
|
79124
|
+
});
|
|
79125
|
+
}
|
|
79126
|
+
return { countDeltas, preImages, outcomes: parsed.outcomes, collateral };
|
|
79127
|
+
}
|
|
78618
79128
|
/**
|
|
78619
79129
|
* Returns cached accounts or fetches fresh data if cache is expired/empty.
|
|
78620
79130
|
*/
|
|
@@ -79838,19 +80348,30 @@ var AppleMailManager = class {
|
|
|
79838
80348
|
* deleting the "All Mail" copy are different operations — so scope to the
|
|
79839
80349
|
* mailbox the id actually came from (`idLocationIndex`, populated by every
|
|
79840
80350
|
* list/search) and never guess.
|
|
80351
|
+
*
|
|
80352
|
+
* Every single-message mutation in this class builds its script here and runs
|
|
80353
|
+
* it immediately, so this is also where the previous operation's forensic
|
|
80354
|
+
* report is invalidated — see `beginMutation()`.
|
|
79841
80355
|
*/
|
|
79842
|
-
findMessageScript(id, operation) {
|
|
80356
|
+
findMessageScript(id, operation, instrument = false) {
|
|
80357
|
+
this.beginMutation();
|
|
79843
80358
|
const loc = this.locationFor(id);
|
|
79844
80359
|
if (loc) {
|
|
80360
|
+
const acctLit = `"${escapeForAppleScript(stripStreamDelimiters(loc.account))}"`;
|
|
80361
|
+
const mbLit = `"${escapeForAppleScript(stripStreamDelimiters(loc.mailbox))}"`;
|
|
79845
80362
|
return buildAppLevelScript(`
|
|
79846
80363
|
try
|
|
79847
80364
|
${this.resolveMailboxFragment(loc.account, loc.mailbox)}
|
|
79848
80365
|
if _tmb is missing value then return "error:Message not found"
|
|
79849
80366
|
set matchingMsgs to (messages of _tmb whose id is ${Number(id)})
|
|
79850
80367
|
if (count of matchingMsgs) > 0 then
|
|
79851
|
-
set msg to item 1 of matchingMsgs
|
|
80368
|
+
set msg to item 1 of matchingMsgs${instrument ? `
|
|
80369
|
+
set _out to ""
|
|
80370
|
+
set _pre to ""${this.countFragment("_cb")}${this.snapshotFragment("before", acctLit, mbLit, "_cb")}${this.preImageFragment("msg")}
|
|
80371
|
+
${operation}${this.countFragment("_ca")}${this.snapshotFragment("after", acctLit, mbLit, "_ca")}${this.reconEmit(acctLit, mbLit, "_cb", "_ca")}
|
|
80372
|
+
return "1${FIELD_SEP}ok" & _pre & "${RECORD_SEP}" & _out` : `
|
|
79852
80373
|
${operation}
|
|
79853
|
-
return "ok"
|
|
80374
|
+
return "ok"`}
|
|
79854
80375
|
end if
|
|
79855
80376
|
return "error:Message not found"
|
|
79856
80377
|
on error errMsg
|
|
@@ -79875,14 +80396,61 @@ var AppleMailManager = class {
|
|
|
79875
80396
|
end repeat
|
|
79876
80397
|
if (count of _hits) is 0 then return "error:Message not found"
|
|
79877
80398
|
if (count of _hits) > 1 then return "error:${AMBIGUOUS_ID_PREFIX}${Number(id)} is present in more than one mailbox (" & _names & "); list or search that mailbox first so the operation targets the right copy"
|
|
79878
|
-
set msg to item 1 of _hits
|
|
80399
|
+
set msg to item 1 of _hits${instrument ? `
|
|
80400
|
+
set _out to ""
|
|
80401
|
+
set _pre to ""
|
|
80402
|
+
set _umb to missing value
|
|
80403
|
+
set _uacct to ""
|
|
80404
|
+
try
|
|
80405
|
+
set _umb to (mailbox of msg)
|
|
80406
|
+
set _uacct to (name of (account of _umb))
|
|
80407
|
+
end try
|
|
80408
|
+
set _cb to -1
|
|
80409
|
+
set _ca to -1
|
|
80410
|
+
if _umb is not missing value then
|
|
80411
|
+
try
|
|
80412
|
+
set _cb to (count of messages of _umb)
|
|
80413
|
+
end try
|
|
80414
|
+
end if${this.preImageFragment("msg")}
|
|
79879
80415
|
${operation}
|
|
79880
|
-
|
|
80416
|
+
if _umb is not missing value then
|
|
80417
|
+
try
|
|
80418
|
+
set _ca to (count of messages of _umb)
|
|
80419
|
+
end try
|
|
80420
|
+
${this.reconEmitFromMessage("_cb", "_ca")}
|
|
80421
|
+
end if
|
|
80422
|
+
return "1${FIELD_SEP}ok" & _pre & "${RECORD_SEP}" & _out` : `
|
|
80423
|
+
${operation}
|
|
80424
|
+
return "ok"`}
|
|
79881
80425
|
on error errMsg
|
|
79882
80426
|
return "error:" & errMsg
|
|
79883
80427
|
end try
|
|
79884
80428
|
`);
|
|
79885
80429
|
}
|
|
80430
|
+
/**
|
|
80431
|
+
* Build and stash the forensic report for a SINGLE-message destructive op.
|
|
80432
|
+
*
|
|
80433
|
+
* Same record stream, same parser and same reconciliation rules as the batch
|
|
80434
|
+
* path — a single-message delete is just a one-id batch as far as the evidence
|
|
80435
|
+
* is concerned, so there is exactly one implementation of "what did this
|
|
80436
|
+
* actually do".
|
|
80437
|
+
*/
|
|
80438
|
+
recordSingleForensics(output, id, destination) {
|
|
80439
|
+
const valid = [{ id, num: Number(id) }];
|
|
80440
|
+
const parsed = this.parseForensicStream(output, valid);
|
|
80441
|
+
const succeeded = parsed.okPositions.has(1);
|
|
80442
|
+
const sameMailbox = (account, mailbox) => destination !== void 0 && destination.account === account && this.resolveMailbox(destination.mailbox, destination.account) === this.resolveMailbox(mailbox, account);
|
|
80443
|
+
const home = parsed.recons[0];
|
|
80444
|
+
this.lastForensics = this.buildForensicReport(
|
|
80445
|
+
parsed,
|
|
80446
|
+
valid,
|
|
80447
|
+
// null, not 0, for a self-move — see SELF_MOVE_NOTE.
|
|
80448
|
+
(account, mailbox) => sameMailbox(account, mailbox) ? null : succeeded ? 1 : 0,
|
|
80449
|
+
() => home ? { account: home.account, mailbox: home.mailbox } : this.locationFor(id) ?? { account: "", mailbox: "" },
|
|
80450
|
+
(account, mailbox) => sameMailbox(account, mailbox) ? SELF_MOVE_NOTE : void 0,
|
|
80451
|
+
/* @__PURE__ */ new Set([canonicalNumericId(String(Number(id)))])
|
|
80452
|
+
);
|
|
80453
|
+
}
|
|
79886
80454
|
/**
|
|
79887
80455
|
* Mark a message as read.
|
|
79888
80456
|
*/
|
|
@@ -80003,9 +80571,10 @@ var AppleMailManager = class {
|
|
|
80003
80571
|
* Delete a message.
|
|
80004
80572
|
*/
|
|
80005
80573
|
deleteMessage(id) {
|
|
80006
|
-
const script = this.findMessageScript(id, "delete msg");
|
|
80574
|
+
const script = this.findMessageScript(id, "delete msg", true);
|
|
80007
80575
|
const result = executeAppleScript(script, { timeoutMs: 6e4 });
|
|
80008
80576
|
if (result.success && !result.output.startsWith("error:")) {
|
|
80577
|
+
this.recordSingleForensics(result.output, id);
|
|
80009
80578
|
return { success: true };
|
|
80010
80579
|
}
|
|
80011
80580
|
const raw = result.success ? result.output.replace(/^error:/, "") : result.error || "Unknown error";
|
|
@@ -80063,13 +80632,18 @@ var AppleMailManager = class {
|
|
|
80063
80632
|
const safeMailbox = escapeForAppleScript(targetMailbox);
|
|
80064
80633
|
const safeAccount = escapeForAppleScript(targetAccount);
|
|
80065
80634
|
const loc = this.locationFor(id);
|
|
80635
|
+
const srcAcctLit = loc ? `"${escapeForAppleScript(stripStreamDelimiters(loc.account))}"` : '""';
|
|
80636
|
+
const srcMbLit = loc ? `"${escapeForAppleScript(stripStreamDelimiters(loc.mailbox))}"` : '""';
|
|
80066
80637
|
const findAndMove = loc ? `
|
|
80067
80638
|
${this.resolveMailboxFragment(loc.account, loc.mailbox)}
|
|
80068
80639
|
if _tmb is missing value then return "error:Message not found"
|
|
80069
80640
|
set matchingMsgs to (messages of _tmb whose id is ${Number(id)})
|
|
80070
80641
|
if (count of matchingMsgs) is 0 then return "error:Message not found"
|
|
80071
|
-
|
|
80072
|
-
|
|
80642
|
+
set msg to item 1 of matchingMsgs
|
|
80643
|
+
set _out to ""
|
|
80644
|
+
set _pre to ""${this.countFragment("_cb")}${this.snapshotFragment("before", srcAcctLit, srcMbLit, "_cb")}${this.preImageFragment("msg")}
|
|
80645
|
+
move msg to destMailbox${this.countFragment("_ca")}${this.snapshotFragment("after", srcAcctLit, srcMbLit, "_ca")}${this.reconEmit(srcAcctLit, srcMbLit, "_cb", "_ca")}
|
|
80646
|
+
return "1${FIELD_SEP}ok" & _pre & "${RECORD_SEP}" & _out` : `
|
|
80073
80647
|
set _hits to {}
|
|
80074
80648
|
set _names to ""
|
|
80075
80649
|
repeat with acct in accounts
|
|
@@ -80085,8 +80659,30 @@ var AppleMailManager = class {
|
|
|
80085
80659
|
end repeat
|
|
80086
80660
|
if (count of _hits) is 0 then return "error:Message not found"
|
|
80087
80661
|
if (count of _hits) > 1 then return "error:${AMBIGUOUS_ID_PREFIX}${Number(id)} is present in more than one mailbox (" & _names & "); list or search that mailbox first so the move targets the right copy"
|
|
80088
|
-
|
|
80089
|
-
|
|
80662
|
+
set msg to item 1 of _hits
|
|
80663
|
+
set _out to ""
|
|
80664
|
+
set _pre to ""
|
|
80665
|
+
set _umb to missing value
|
|
80666
|
+
set _uacct to ""
|
|
80667
|
+
try
|
|
80668
|
+
set _umb to (mailbox of msg)
|
|
80669
|
+
set _uacct to (name of (account of _umb))
|
|
80670
|
+
end try
|
|
80671
|
+
set _cb to -1
|
|
80672
|
+
set _ca to -1
|
|
80673
|
+
if _umb is not missing value then
|
|
80674
|
+
try
|
|
80675
|
+
set _cb to (count of messages of _umb)
|
|
80676
|
+
end try
|
|
80677
|
+
end if${this.preImageFragment("msg")}
|
|
80678
|
+
move msg to destMailbox
|
|
80679
|
+
if _umb is not missing value then
|
|
80680
|
+
try
|
|
80681
|
+
set _ca to (count of messages of _umb)
|
|
80682
|
+
end try
|
|
80683
|
+
${this.reconEmitFromMessage("_cb", "_ca")}
|
|
80684
|
+
end if
|
|
80685
|
+
return "1${FIELD_SEP}ok" & _pre & "${RECORD_SEP}" & _out`;
|
|
80090
80686
|
const script = buildAppLevelScript(`
|
|
80091
80687
|
try
|
|
80092
80688
|
-- \`mailboxes of account\` is already flat: it includes nested mailboxes
|
|
@@ -80114,9 +80710,14 @@ var AppleMailManager = class {
|
|
|
80114
80710
|
if (result.output.startsWith("error:")) {
|
|
80115
80711
|
return { success: false, error: result.output.slice("error:".length) };
|
|
80116
80712
|
}
|
|
80713
|
+
this.recordSingleForensics(result.output, id, {
|
|
80714
|
+
account: targetAccount,
|
|
80715
|
+
mailbox: targetMailbox
|
|
80716
|
+
});
|
|
80117
80717
|
return { success: true };
|
|
80118
80718
|
}
|
|
80119
80719
|
moveMessage(id, mailbox, account) {
|
|
80720
|
+
this.beginMutation();
|
|
80120
80721
|
const res = this.moveMessageInternal(id, mailbox, account);
|
|
80121
80722
|
if (res.success) return { success: true };
|
|
80122
80723
|
const error2 = this.classifyMessageMutationError(
|
|
@@ -80198,19 +80799,48 @@ var AppleMailManager = class {
|
|
|
80198
80799
|
*
|
|
80199
80800
|
* `setup` runs once up front (used by move to resolve the destination); it may
|
|
80200
80801
|
* bail the whole batch by returning a `BATCH_FATAL`-prefixed string.
|
|
80802
|
+
*
|
|
80803
|
+
* ## A repeated id names ONE message, and is operated on once
|
|
80804
|
+
*
|
|
80805
|
+
* A batch is a set of messages, not a multiset: two occurrences of id `75811`
|
|
80806
|
+
* are the same message, and Mail can only delete it once. So the id list is
|
|
80807
|
+
* DEDUPED on the numeric value actually sent to AppleScript (`"75811"` and
|
|
80808
|
+
* `" 75811"` are the same target), and the returned array carries one entry
|
|
80809
|
+
* per distinct id, in first-seen order — hence `success` counts distinct
|
|
80810
|
+
* messages rather than list positions.
|
|
80811
|
+
*
|
|
80812
|
+
* This is a correctness requirement for the #155 reconciliation, not a
|
|
80813
|
+
* tidy-up. Counting a repeat as a second operand makes `expected` disagree
|
|
80814
|
+
* with the mailbox — the duplicate can only be reported `notfound` (the
|
|
80815
|
+
* message is already gone) or `ok` twice (on a flag-only store) — and either
|
|
80816
|
+
* way the always-on warning fires on an operation that did exactly the right
|
|
80817
|
+
* thing. A warning users learn to ignore is worse than no warning.
|
|
80201
80818
|
*/
|
|
80202
|
-
runBatchOperation(ids, operation, setup = "", scope) {
|
|
80819
|
+
runBatchOperation(ids, operation, setup = "", scope, forensics) {
|
|
80820
|
+
const instrument = forensics !== void 0;
|
|
80821
|
+
this.beginMutation();
|
|
80203
80822
|
const valid = [];
|
|
80823
|
+
const operands = [];
|
|
80824
|
+
const seenNums = /* @__PURE__ */ new Set();
|
|
80825
|
+
const seenInvalid = /* @__PURE__ */ new Set();
|
|
80204
80826
|
for (const id of ids) {
|
|
80205
80827
|
const num = Number(id);
|
|
80206
|
-
if (Number.isFinite(num))
|
|
80828
|
+
if (Number.isFinite(num)) {
|
|
80829
|
+
if (seenNums.has(num)) continue;
|
|
80830
|
+
seenNums.add(num);
|
|
80831
|
+
valid.push({ id, num });
|
|
80832
|
+
} else {
|
|
80833
|
+
if (seenInvalid.has(id)) continue;
|
|
80834
|
+
seenInvalid.add(id);
|
|
80835
|
+
}
|
|
80836
|
+
operands.push(id);
|
|
80207
80837
|
}
|
|
80208
80838
|
if (valid.length === 0) {
|
|
80209
|
-
return
|
|
80839
|
+
return operands.map((id) => ({ id, success: false, error: "Invalid message ID" }));
|
|
80210
80840
|
}
|
|
80211
80841
|
const resolved = this.resolveBatchScope(scope);
|
|
80212
80842
|
if (resolved.kind === "unresolvable") {
|
|
80213
|
-
return
|
|
80843
|
+
return operands.map((id) => ({ id, success: false, error: resolved.error }));
|
|
80214
80844
|
}
|
|
80215
80845
|
const callerScope = resolved.kind === "scoped" ? resolved : void 0;
|
|
80216
80846
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -80222,39 +80852,45 @@ var AppleMailManager = class {
|
|
|
80222
80852
|
unlocated.push({ num: v.num, pos });
|
|
80223
80853
|
return;
|
|
80224
80854
|
}
|
|
80225
|
-
const key =
|
|
80855
|
+
const key = groupKey(loc.account, loc.mailbox);
|
|
80226
80856
|
const g = groups.get(key) ?? { account: loc.account, mailbox: loc.mailbox, items: [] };
|
|
80227
80857
|
g.items.push({ num: v.num, pos });
|
|
80228
80858
|
groups.set(key, g);
|
|
80229
80859
|
});
|
|
80230
80860
|
const asList = (nums) => `{${nums.join(", ")}}`;
|
|
80231
|
-
const scopedBlocks = [...groups.values()].map(
|
|
80232
|
-
(g)
|
|
80861
|
+
const scopedBlocks = [...groups.values()].map((g) => {
|
|
80862
|
+
const acctLit = `"${escapeForAppleScript(stripStreamDelimiters(g.account))}"`;
|
|
80863
|
+
const mbLit = `"${escapeForAppleScript(stripStreamDelimiters(g.mailbox))}"`;
|
|
80864
|
+
const acctInProse = escapeForAppleScript(stripStreamDelimiters(g.account));
|
|
80865
|
+
const mbInProse = escapeForAppleScript(stripStreamDelimiters(g.mailbox));
|
|
80866
|
+
const pre = instrument ? this.preImageFragment("_msg") : "";
|
|
80867
|
+
return `
|
|
80233
80868
|
${this.resolveMailboxFragment(g.account, g.mailbox)}
|
|
80234
80869
|
set _gids to ${asList(g.items.map((it) => it.num))}
|
|
80235
80870
|
set _gpos to ${asList(g.items.map((it) => it.pos))}
|
|
80236
80871
|
if _tmb is missing value then
|
|
80237
80872
|
repeat with _k from 1 to (count of _gpos)
|
|
80238
|
-
set _out to _out & ((item _k of _gpos) as string) & "${FIELD_SEP}error:source mailbox \\"${
|
|
80873
|
+
set _out to _out & ((item _k of _gpos) as string) & "${FIELD_SEP}error:source mailbox \\"${mbInProse}\\" not found in account \\"${acctInProse}\\"${RECORD_SEP}"
|
|
80239
80874
|
end repeat
|
|
80240
|
-
else
|
|
80875
|
+
else${instrument ? `${this.countFragment("_cb")}${this.snapshotFragment("before", acctLit, mbLit, "_cb")}` : ""}
|
|
80241
80876
|
repeat with _k from 1 to (count of _gids)
|
|
80242
80877
|
set _idx to item _k of _gpos
|
|
80878
|
+
set _pre to ""
|
|
80243
80879
|
try
|
|
80244
80880
|
set _m to (messages of _tmb whose id is (item _k of _gids))
|
|
80245
80881
|
if (count of _m) > 0 then
|
|
80246
|
-
set _msg to item 1 of _m
|
|
80882
|
+
set _msg to item 1 of _m${pre}
|
|
80247
80883
|
${operation}
|
|
80248
|
-
set _out to _out & (_idx as string) & "${FIELD_SEP}ok${RECORD_SEP}"
|
|
80884
|
+
set _out to _out & (_idx as string) & "${FIELD_SEP}ok" & _pre & "${RECORD_SEP}"
|
|
80249
80885
|
else
|
|
80250
80886
|
set _out to _out & (_idx as string) & "${FIELD_SEP}notfound${RECORD_SEP}"
|
|
80251
80887
|
end if
|
|
80252
80888
|
on error _e
|
|
80253
|
-
|
|
80889
|
+
${this.errorEmit(" ")}
|
|
80254
80890
|
end try
|
|
80255
|
-
end repeat
|
|
80256
|
-
end if
|
|
80257
|
-
).join("\n");
|
|
80891
|
+
end repeat${instrument ? `${this.countFragment("_ca")}${this.snapshotFragment("after", acctLit, mbLit, "_ca")}${this.reconEmit(acctLit, mbLit, "_cb", "_ca")}` : ""}
|
|
80892
|
+
end if`;
|
|
80893
|
+
}).join("\n");
|
|
80258
80894
|
const unlocatedBlock = unlocated.length ? `
|
|
80259
80895
|
set _uids to ${asList(unlocated.map((it) => it.num))}
|
|
80260
80896
|
set _upos to ${asList(unlocated.map((it) => it.pos))}
|
|
@@ -80286,14 +80922,35 @@ var AppleMailManager = class {
|
|
|
80286
80922
|
if (item _k of _uhit) is 0 then
|
|
80287
80923
|
set _out to _out & (_idx as string) & "${FIELD_SEP}notfound${RECORD_SEP}"
|
|
80288
80924
|
else if (item _k of _uhit) > 1 then
|
|
80289
|
-
set
|
|
80925
|
+
set _uname to (item _k of _unames)${this.sanitizeFragment("_uname", " ")}
|
|
80926
|
+
set _out to _out & (_idx as string) & "${FIELD_SEP}error:${AMBIGUOUS_ID_BATCH}(" & _uname & "); list or search that mailbox first so the operation targets the right copy${RECORD_SEP}"
|
|
80290
80927
|
else
|
|
80928
|
+
set _pre to ""
|
|
80291
80929
|
try
|
|
80292
|
-
set _msg to item _k of _umsg
|
|
80930
|
+
set _msg to item _k of _umsg${instrument ? `
|
|
80931
|
+
set _umb to missing value
|
|
80932
|
+
set _uacct to ""
|
|
80933
|
+
try
|
|
80934
|
+
set _umb to (mailbox of _msg)
|
|
80935
|
+
set _uacct to (name of (account of _umb))
|
|
80936
|
+
end try
|
|
80937
|
+
set _ucb to -1
|
|
80938
|
+
set _uca to -1
|
|
80939
|
+
if _umb is not missing value then
|
|
80940
|
+
try
|
|
80941
|
+
set _ucb to (count of messages of _umb)
|
|
80942
|
+
end try
|
|
80943
|
+
end if${this.preImageFragment("_msg")}` : ""}
|
|
80293
80944
|
${operation}
|
|
80294
|
-
set _out to _out & (_idx as string) & "${FIELD_SEP}ok${RECORD_SEP}"
|
|
80945
|
+
set _out to _out & (_idx as string) & "${FIELD_SEP}ok" & _pre & "${RECORD_SEP}"${instrument ? `
|
|
80946
|
+
if _umb is not missing value then
|
|
80947
|
+
try
|
|
80948
|
+
set _uca to (count of messages of _umb)
|
|
80949
|
+
end try
|
|
80950
|
+
${this.reconEmitFromMessage("_ucb", "_uca", "(_idx as string)", " ")}
|
|
80951
|
+
end if` : ""}
|
|
80295
80952
|
on error _e
|
|
80296
|
-
|
|
80953
|
+
${this.errorEmit(" ")}
|
|
80297
80954
|
end try
|
|
80298
80955
|
end if
|
|
80299
80956
|
end repeat` : "";
|
|
@@ -80312,33 +80969,46 @@ var AppleMailManager = class {
|
|
|
80312
80969
|
const result = executeAppleScript(script, { timeoutMs });
|
|
80313
80970
|
if (!result.success) {
|
|
80314
80971
|
const err = result.error || "Batch operation failed";
|
|
80315
|
-
return
|
|
80972
|
+
return operands.map((id) => ({ id, success: false, error: err }));
|
|
80316
80973
|
}
|
|
80317
80974
|
if (result.output.startsWith(BATCH_FATAL)) {
|
|
80318
80975
|
const err = result.output.slice(BATCH_FATAL.length);
|
|
80319
|
-
return
|
|
80320
|
-
}
|
|
80321
|
-
const
|
|
80322
|
-
|
|
80323
|
-
|
|
80324
|
-
const
|
|
80325
|
-
|
|
80326
|
-
|
|
80327
|
-
|
|
80328
|
-
|
|
80329
|
-
|
|
80330
|
-
|
|
80331
|
-
|
|
80332
|
-
|
|
80333
|
-
|
|
80334
|
-
|
|
80335
|
-
|
|
80336
|
-
|
|
80337
|
-
|
|
80338
|
-
|
|
80339
|
-
|
|
80976
|
+
return operands.map((id) => ({ id, success: false, error: err }));
|
|
80977
|
+
}
|
|
80978
|
+
const parsed = this.parseForensicStream(result.output, valid);
|
|
80979
|
+
const { byId } = parsed;
|
|
80980
|
+
if (instrument) {
|
|
80981
|
+
const posLocation = /* @__PURE__ */ new Map();
|
|
80982
|
+
for (const g of groups.values()) {
|
|
80983
|
+
for (const it of g.items)
|
|
80984
|
+
posLocation.set(it.pos, { account: g.account, mailbox: g.mailbox });
|
|
80985
|
+
}
|
|
80986
|
+
for (const r of parsed.recons) {
|
|
80987
|
+
if (r.pos !== null) posLocation.set(r.pos, { account: r.account, mailbox: r.mailbox });
|
|
80988
|
+
}
|
|
80989
|
+
const { okPositions } = parsed;
|
|
80990
|
+
const dest = forensics?.destination;
|
|
80991
|
+
const sameMailbox = (account, mailbox) => dest !== void 0 && dest.account === account && this.resolveMailbox(dest.mailbox, dest.account) === this.resolveMailbox(mailbox, account);
|
|
80992
|
+
const expectedFor = (account, mailbox, pos) => {
|
|
80993
|
+
if (sameMailbox(account, mailbox)) return null;
|
|
80994
|
+
if (pos !== null) return okPositions.has(pos) ? 1 : 0;
|
|
80995
|
+
const group = groups.get(groupKey(account, mailbox));
|
|
80996
|
+
if (!group) return 0;
|
|
80997
|
+
return group.items.filter((it) => okPositions.has(it.pos)).length;
|
|
80998
|
+
};
|
|
80999
|
+
const noteFor = (account, mailbox) => sameMailbox(account, mailbox) ? SELF_MOVE_NOTE : void 0;
|
|
81000
|
+
this.lastForensics = this.buildForensicReport(
|
|
81001
|
+
parsed,
|
|
81002
|
+
valid,
|
|
81003
|
+
expectedFor,
|
|
81004
|
+
(pos) => posLocation.get(pos) ?? { account: "", mailbox: "" },
|
|
81005
|
+
noteFor,
|
|
81006
|
+
// Canonicalised, because the ids this is compared against come back
|
|
81007
|
+
// from AppleScript — see canonicalNumericId.
|
|
81008
|
+
new Set(valid.map((v) => canonicalNumericId(String(v.num))))
|
|
81009
|
+
);
|
|
80340
81010
|
}
|
|
80341
|
-
return
|
|
81011
|
+
return operands.map(
|
|
80342
81012
|
(id) => byId.get(id) ?? (Number.isFinite(Number(id)) ? { id, success: false, error: "No result returned" } : { id, success: false, error: "Invalid message ID" })
|
|
80343
81013
|
);
|
|
80344
81014
|
}
|
|
@@ -80346,7 +81016,7 @@ var AppleMailManager = class {
|
|
|
80346
81016
|
* Delete multiple messages at once (single tree walk — see runBatchOperation).
|
|
80347
81017
|
*/
|
|
80348
81018
|
batchDeleteMessages(ids, scope) {
|
|
80349
|
-
return this.runBatchOperation(ids, "delete _msg", "", scope);
|
|
81019
|
+
return this.runBatchOperation(ids, "delete _msg", "", scope, {});
|
|
80350
81020
|
}
|
|
80351
81021
|
/**
|
|
80352
81022
|
* Move multiple messages to a mailbox at once (single tree walk).
|
|
@@ -80369,7 +81039,9 @@ var AppleMailManager = class {
|
|
|
80369
81039
|
if (count of destMatches) is 0 then return "${BATCH_FATAL}Destination mailbox \\"" & destName & "\\" not found in account \\"${safeAccount}\\""
|
|
80370
81040
|
if (count of destMatches) > 1 then return "${BATCH_FATAL}Destination mailbox \\"" & destName & "\\" is ambiguous (" & (count of destMatches) & " matches) in account \\"${safeAccount}\\"; move by full path"
|
|
80371
81041
|
set destMailbox to item 1 of destMatches`;
|
|
80372
|
-
return this.runBatchOperation(ids, "move _msg to destMailbox", setup, scope
|
|
81042
|
+
return this.runBatchOperation(ids, "move _msg to destMailbox", setup, scope, {
|
|
81043
|
+
destination: { account: targetAccount, mailbox: targetMailbox }
|
|
81044
|
+
});
|
|
80373
81045
|
}
|
|
80374
81046
|
/**
|
|
80375
81047
|
* Mark multiple messages as read at once (single tree walk).
|
|
@@ -82783,9 +83455,16 @@ function collectAttachments(node, out = []) {
|
|
|
82783
83455
|
for (const child of node.childNodes ?? []) collectAttachments(child, out);
|
|
82784
83456
|
return out;
|
|
82785
83457
|
}
|
|
82786
|
-
async function streamToBuffer(content) {
|
|
83458
|
+
async function streamToBuffer(content, maxBytes) {
|
|
82787
83459
|
const chunks = [];
|
|
82788
|
-
|
|
83460
|
+
let total = 0;
|
|
83461
|
+
for await (const chunk of content) {
|
|
83462
|
+
total += chunk.byteLength;
|
|
83463
|
+
if (total > maxBytes) {
|
|
83464
|
+
throw new Error(`IMAP attachment exceeds the ${maxBytes / 1024 / 1024} MiB size limit.`);
|
|
83465
|
+
}
|
|
83466
|
+
chunks.push(Buffer.from(chunk));
|
|
83467
|
+
}
|
|
82789
83468
|
return Buffer.concat(chunks);
|
|
82790
83469
|
}
|
|
82791
83470
|
async function imapListAttachments(id, deps = {}) {
|
|
@@ -82822,14 +83501,24 @@ async function imapFetchAttachment(id, attachmentName, deps = {}) {
|
|
|
82822
83501
|
error: `Attachment "${attachmentName}" not found on UID ${ref.uid}. Available: ${names}.`
|
|
82823
83502
|
};
|
|
82824
83503
|
}
|
|
82825
|
-
|
|
82826
|
-
|
|
82827
|
-
|
|
82828
|
-
|
|
82829
|
-
|
|
82830
|
-
|
|
82831
|
-
|
|
82832
|
-
|
|
83504
|
+
if (match.size > MAX_IMAP_ATTACHMENT_BYTES) {
|
|
83505
|
+
return {
|
|
83506
|
+
success: false,
|
|
83507
|
+
error: `IMAP attachment "${attachmentName}" is ${match.size} bytes; the maximum is ${MAX_IMAP_ATTACHMENT_BYTES} bytes (25 MiB).`
|
|
83508
|
+
};
|
|
83509
|
+
}
|
|
83510
|
+
try {
|
|
83511
|
+
const dl = await client.download(String(ref.uid), match.part, { uid: true });
|
|
83512
|
+
const buf = await streamToBuffer(dl.content, MAX_IMAP_ATTACHMENT_BYTES);
|
|
83513
|
+
return {
|
|
83514
|
+
success: true,
|
|
83515
|
+
base64: buf.toString("base64"),
|
|
83516
|
+
bytes: buf.length,
|
|
83517
|
+
mimeType: match.mimeType
|
|
83518
|
+
};
|
|
83519
|
+
} catch (e) {
|
|
83520
|
+
return { success: false, error: `IMAP attachment fetch failed: ${errText(e)}` };
|
|
83521
|
+
}
|
|
82833
83522
|
});
|
|
82834
83523
|
}
|
|
82835
83524
|
async function imapBatch(ids, deps, op) {
|
|
@@ -83060,8 +83749,9 @@ function withErrorHandling(handler, errorPrefix) {
|
|
|
83060
83749
|
|
|
83061
83750
|
// src/tools/batchResults.ts
|
|
83062
83751
|
async function hybridBatchCounts(ids, appleFn, imapFn) {
|
|
83063
|
-
const
|
|
83064
|
-
const
|
|
83752
|
+
const distinctIds = [...new Set(ids)];
|
|
83753
|
+
const imapIds = distinctIds.filter((i) => i.startsWith("imap:"));
|
|
83754
|
+
const numericIds = distinctIds.filter((i) => !i.startsWith("imap:"));
|
|
83065
83755
|
let success = 0;
|
|
83066
83756
|
let fail = 0;
|
|
83067
83757
|
const errors = [];
|
|
@@ -83091,7 +83781,7 @@ function formatBatchErrors(errors, max = 5) {
|
|
|
83091
83781
|
return `: ${shown.join("; ")}${more > 0 ? ` (+${more} more)` : ""}`;
|
|
83092
83782
|
}
|
|
83093
83783
|
var MAX_STRUCTURED_BATCH_ERRORS = 20;
|
|
83094
|
-
function batchResponse(counts, messages, extra = {}) {
|
|
83784
|
+
function batchResponse(counts, messages, extra = {}, warnings = []) {
|
|
83095
83785
|
const { success, fail, errors } = counts;
|
|
83096
83786
|
const distinct = distinctErrors(errors);
|
|
83097
83787
|
const reported = distinct.slice(0, MAX_STRUCTURED_BATCH_ERRORS);
|
|
@@ -83104,9 +83794,13 @@ function batchResponse(counts, messages, extra = {}) {
|
|
|
83104
83794
|
...distinct.length > reported.length ? { errorsTruncated: true } : {}
|
|
83105
83795
|
};
|
|
83106
83796
|
const suffix = formatBatchErrors(distinct);
|
|
83107
|
-
|
|
83108
|
-
|
|
83109
|
-
|
|
83797
|
+
const warn = warnings.length > 0 ? `
|
|
83798
|
+
|
|
83799
|
+
${warnings.join("\n")}` : "";
|
|
83800
|
+
if (fail === 0) return successResponse(`${messages.allSucceeded(success)}${warn}`, structured);
|
|
83801
|
+
if (success === 0)
|
|
83802
|
+
return errorResponse(`${messages.allFailed(fail)}${suffix}${warn}`, structured);
|
|
83803
|
+
return successResponse(`${messages.partial(success, fail)}${suffix}${warn}`, structured);
|
|
83110
83804
|
}
|
|
83111
83805
|
|
|
83112
83806
|
// src/services/imapMultiAccount.ts
|
|
@@ -83769,6 +84463,22 @@ var BATCH_COUNT_OUTPUT_SCHEMA = {
|
|
|
83769
84463
|
// so a short list is never mistaken for the complete one.
|
|
83770
84464
|
errorsTruncated: external_exports.boolean().optional()
|
|
83771
84465
|
};
|
|
84466
|
+
var COUNT_DELTA_OUTPUT_SCHEMA = external_exports.array(
|
|
84467
|
+
external_exports.object({
|
|
84468
|
+
account: external_exports.string().optional(),
|
|
84469
|
+
mailbox: external_exports.string().optional(),
|
|
84470
|
+
before: external_exports.number().nullable().optional(),
|
|
84471
|
+
after: external_exports.number().nullable().optional(),
|
|
84472
|
+
// Nullable for the same reason before/after/observed are: null means no
|
|
84473
|
+
// comparison was possible. For `expected` that is a move whose
|
|
84474
|
+
// destination IS the source mailbox — it always pairs with
|
|
84475
|
+
// `status: "unknown"`, and never with a warning.
|
|
84476
|
+
expected: external_exports.number().nullable().optional(),
|
|
84477
|
+
observed: external_exports.number().nullable().optional(),
|
|
84478
|
+
status: external_exports.enum(["match", "over", "under", "unknown"]).optional(),
|
|
84479
|
+
note: external_exports.string().optional()
|
|
84480
|
+
})
|
|
84481
|
+
).optional();
|
|
83772
84482
|
var CHECK_ITEM_SCHEMA = external_exports.object({}).passthrough();
|
|
83773
84483
|
var require2 = createRequire(import.meta.url);
|
|
83774
84484
|
var { version: version2 } = require2("../package.json");
|
|
@@ -83848,6 +84558,15 @@ function registerTool(name, config2, cb) {
|
|
|
83848
84558
|
}
|
|
83849
84559
|
var mailManager = new AppleMailManager();
|
|
83850
84560
|
registerResourcesAndPrompts(server, mailManager);
|
|
84561
|
+
function collectForensics(tool, args) {
|
|
84562
|
+
const report = mailManager.consumeLastForensics();
|
|
84563
|
+
if (!report) return { warnings: [] };
|
|
84564
|
+
writeDestructiveAudit({ tool, args, serverVersion: version2 }, report);
|
|
84565
|
+
return {
|
|
84566
|
+
...report.countDeltas.length > 0 ? { countDelta: report.countDeltas } : {},
|
|
84567
|
+
warnings: reconciliationWarnings(report)
|
|
84568
|
+
};
|
|
84569
|
+
}
|
|
83851
84570
|
registerTool(
|
|
83852
84571
|
"search-messages",
|
|
83853
84572
|
{
|
|
@@ -84577,14 +85296,28 @@ registerTool(
|
|
|
84577
85296
|
inputSchema: {
|
|
84578
85297
|
id: MESSAGE_ID_SCHEMA
|
|
84579
85298
|
},
|
|
84580
|
-
outputSchema: {
|
|
85299
|
+
outputSchema: {
|
|
85300
|
+
ok: external_exports.boolean().optional(),
|
|
85301
|
+
id: external_exports.string().optional(),
|
|
85302
|
+
countDelta: COUNT_DELTA_OUTPUT_SCHEMA
|
|
85303
|
+
}
|
|
84581
85304
|
},
|
|
84582
85305
|
withErrorHandling(
|
|
84583
85306
|
({ id }) => routeMessage(id, {
|
|
84584
85307
|
imap: () => imapDeleteMessageById(id),
|
|
84585
85308
|
apple: () => {
|
|
84586
85309
|
const { success, error: error2 } = mailManager.deleteMessage(id);
|
|
84587
|
-
|
|
85310
|
+
const { countDelta, warnings } = collectForensics("delete-message", { id });
|
|
85311
|
+
return success ? successResponse(
|
|
85312
|
+
`Message deleted${warnings.length ? `
|
|
85313
|
+
|
|
85314
|
+
${warnings.join("\n")}` : ""}`,
|
|
85315
|
+
{
|
|
85316
|
+
ok: true,
|
|
85317
|
+
id,
|
|
85318
|
+
...countDelta ? { countDelta } : {}
|
|
85319
|
+
}
|
|
85320
|
+
) : errorResponse(error2 || `Failed to delete message "${id}"`);
|
|
84588
85321
|
},
|
|
84589
85322
|
ok: "Message deleted",
|
|
84590
85323
|
fail: `Failed to delete message "${id}"`,
|
|
@@ -84605,7 +85338,8 @@ registerTool(
|
|
|
84605
85338
|
outputSchema: {
|
|
84606
85339
|
ok: external_exports.boolean().optional(),
|
|
84607
85340
|
id: external_exports.string().optional(),
|
|
84608
|
-
mailbox: external_exports.string().optional()
|
|
85341
|
+
mailbox: external_exports.string().optional(),
|
|
85342
|
+
countDelta: COUNT_DELTA_OUTPUT_SCHEMA
|
|
84609
85343
|
}
|
|
84610
85344
|
},
|
|
84611
85345
|
withErrorHandling(
|
|
@@ -84613,7 +85347,17 @@ registerTool(
|
|
|
84613
85347
|
imap: () => imapMoveMessageById(id, mailbox),
|
|
84614
85348
|
apple: () => {
|
|
84615
85349
|
const { success, error: error2 } = mailManager.moveMessage(id, mailbox, account);
|
|
84616
|
-
|
|
85350
|
+
const { countDelta, warnings } = collectForensics("move-message", {
|
|
85351
|
+
id,
|
|
85352
|
+
mailbox,
|
|
85353
|
+
account
|
|
85354
|
+
});
|
|
85355
|
+
return success ? successResponse(
|
|
85356
|
+
`Message moved to "${mailbox}"${warnings.length ? `
|
|
85357
|
+
|
|
85358
|
+
${warnings.join("\n")}` : ""}`,
|
|
85359
|
+
{ ok: true, id, mailbox, ...countDelta ? { countDelta } : {} }
|
|
85360
|
+
) : errorResponse(error2 || `Failed to move message to "${mailbox}"`);
|
|
84617
85361
|
},
|
|
84618
85362
|
ok: `Message moved to "${mailbox}"`,
|
|
84619
85363
|
fail: `Failed to move message to "${mailbox}"`,
|
|
@@ -84631,19 +85375,36 @@ registerTool(
|
|
|
84631
85375
|
sourceMailbox: BATCH_SOURCE_MAILBOX_SCHEMA,
|
|
84632
85376
|
sourceAccount: BATCH_SOURCE_ACCOUNT_SCHEMA
|
|
84633
85377
|
},
|
|
84634
|
-
outputSchema: BATCH_COUNT_OUTPUT_SCHEMA
|
|
85378
|
+
outputSchema: { ...BATCH_COUNT_OUTPUT_SCHEMA, countDelta: COUNT_DELTA_OUTPUT_SCHEMA }
|
|
84635
85379
|
},
|
|
84636
85380
|
withErrorHandling(async ({ ids, sourceMailbox, sourceAccount }) => {
|
|
85381
|
+
let forensics = { warnings: [] };
|
|
84637
85382
|
const counts = await hybridBatchCounts(
|
|
84638
85383
|
ids,
|
|
84639
|
-
(n) =>
|
|
85384
|
+
(n) => {
|
|
85385
|
+
const res = mailManager.batchDeleteMessages(n, {
|
|
85386
|
+
account: sourceAccount,
|
|
85387
|
+
mailbox: sourceMailbox
|
|
85388
|
+
});
|
|
85389
|
+
forensics = collectForensics("batch-delete-messages", {
|
|
85390
|
+
ids,
|
|
85391
|
+
sourceMailbox,
|
|
85392
|
+
sourceAccount
|
|
85393
|
+
});
|
|
85394
|
+
return res;
|
|
85395
|
+
},
|
|
84640
85396
|
(im) => imapBatchDelete(im)
|
|
84641
85397
|
);
|
|
84642
|
-
return batchResponse(
|
|
84643
|
-
|
|
84644
|
-
|
|
84645
|
-
|
|
84646
|
-
|
|
85398
|
+
return batchResponse(
|
|
85399
|
+
counts,
|
|
85400
|
+
{
|
|
85401
|
+
allSucceeded: (n) => `Successfully deleted ${n} message(s)`,
|
|
85402
|
+
allFailed: (n) => `Failed to delete all ${n} message(s)`,
|
|
85403
|
+
partial: (ok, failed) => `Deleted ${ok} message(s), ${failed} failed`
|
|
85404
|
+
},
|
|
85405
|
+
forensics.countDelta ? { countDelta: forensics.countDelta } : {},
|
|
85406
|
+
forensics.warnings
|
|
85407
|
+
);
|
|
84647
85408
|
}, "Error batch deleting messages")
|
|
84648
85409
|
);
|
|
84649
85410
|
registerTool(
|
|
@@ -84657,15 +85418,26 @@ registerTool(
|
|
|
84657
85418
|
sourceMailbox: BATCH_SOURCE_MAILBOX_SCHEMA,
|
|
84658
85419
|
sourceAccount: BATCH_SOURCE_ACCOUNT_SCHEMA
|
|
84659
85420
|
},
|
|
84660
|
-
outputSchema: BATCH_COUNT_OUTPUT_SCHEMA
|
|
85421
|
+
outputSchema: { ...BATCH_COUNT_OUTPUT_SCHEMA, countDelta: COUNT_DELTA_OUTPUT_SCHEMA }
|
|
84661
85422
|
},
|
|
84662
85423
|
withErrorHandling(async ({ ids, mailbox, account, sourceMailbox, sourceAccount }) => {
|
|
85424
|
+
let forensics = { warnings: [] };
|
|
84663
85425
|
const counts = await hybridBatchCounts(
|
|
84664
85426
|
ids,
|
|
84665
|
-
(n) =>
|
|
84666
|
-
account
|
|
84667
|
-
|
|
84668
|
-
|
|
85427
|
+
(n) => {
|
|
85428
|
+
const res = mailManager.batchMoveMessages(n, mailbox, account, {
|
|
85429
|
+
account: sourceAccount,
|
|
85430
|
+
mailbox: sourceMailbox
|
|
85431
|
+
});
|
|
85432
|
+
forensics = collectForensics("batch-move-messages", {
|
|
85433
|
+
ids,
|
|
85434
|
+
mailbox,
|
|
85435
|
+
account,
|
|
85436
|
+
sourceMailbox,
|
|
85437
|
+
sourceAccount
|
|
85438
|
+
});
|
|
85439
|
+
return res;
|
|
85440
|
+
},
|
|
84669
85441
|
(im) => imapBatchMove(im, mailbox, { account })
|
|
84670
85442
|
);
|
|
84671
85443
|
return batchResponse(
|
|
@@ -84675,7 +85447,8 @@ registerTool(
|
|
|
84675
85447
|
allFailed: (n) => `Failed to move all ${n} message(s)`,
|
|
84676
85448
|
partial: (ok, failed) => `Moved ${ok} message(s) to "${mailbox}", ${failed} failed`
|
|
84677
85449
|
},
|
|
84678
|
-
{ mailbox }
|
|
85450
|
+
{ mailbox, ...forensics.countDelta ? { countDelta: forensics.countDelta } : {} },
|
|
85451
|
+
forensics.warnings
|
|
84679
85452
|
);
|
|
84680
85453
|
}, "Error batch moving messages")
|
|
84681
85454
|
);
|