apple-mail-mcp 2.20.3 → 2.20.5
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 +50 -30
- package/build/cli.js +93 -6
- package/build/index.js +497 -224
- package/docs/IMAP-SETUP.md +27 -25
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -19994,6 +19994,84 @@ var init_nodemailer = __esm({
|
|
|
19994
19994
|
}
|
|
19995
19995
|
});
|
|
19996
19996
|
|
|
19997
|
+
// src/utils/callStop.ts
|
|
19998
|
+
function searchDeadlineMs(env = process.env) {
|
|
19999
|
+
const raw = env[SEARCH_DEADLINE_ENV];
|
|
20000
|
+
if (raw === void 0 || raw.trim() === "") return DEFAULT_SEARCH_DEADLINE_MS;
|
|
20001
|
+
const n = Number(raw);
|
|
20002
|
+
if (!Number.isFinite(n)) return DEFAULT_SEARCH_DEADLINE_MS;
|
|
20003
|
+
return Math.max(MIN_SEARCH_DEADLINE_MS, Math.floor(n));
|
|
20004
|
+
}
|
|
20005
|
+
function createCallStop(timing, deadlineMs, envVar = SEARCH_DEADLINE_ENV) {
|
|
20006
|
+
const controller = new AbortController();
|
|
20007
|
+
const upstream = timing?.signal;
|
|
20008
|
+
const onCancel = () => {
|
|
20009
|
+
if (!controller.signal.aborted) controller.abort({ kind: "cancelled" });
|
|
20010
|
+
};
|
|
20011
|
+
if (upstream?.aborted) onCancel();
|
|
20012
|
+
else upstream?.addEventListener("abort", onCancel, { once: true });
|
|
20013
|
+
const startedAt = timing?.arrivedAt ?? Date.now();
|
|
20014
|
+
const remaining = Math.max(0, deadlineMs - (Date.now() - startedAt));
|
|
20015
|
+
const timer = setTimeout(() => {
|
|
20016
|
+
if (!controller.signal.aborted) {
|
|
20017
|
+
controller.abort({ kind: "deadline", deadlineMs, envVar });
|
|
20018
|
+
}
|
|
20019
|
+
}, remaining);
|
|
20020
|
+
timer.unref?.();
|
|
20021
|
+
return {
|
|
20022
|
+
signal: controller.signal,
|
|
20023
|
+
dispose() {
|
|
20024
|
+
clearTimeout(timer);
|
|
20025
|
+
upstream?.removeEventListener("abort", onCancel);
|
|
20026
|
+
}
|
|
20027
|
+
};
|
|
20028
|
+
}
|
|
20029
|
+
function describeStop(signal) {
|
|
20030
|
+
const reason = signal?.reason;
|
|
20031
|
+
if (reason && typeof reason === "object" && reason.kind === "deadline") {
|
|
20032
|
+
return `the ${Math.round(reason.deadlineMs / 1e3)}s search deadline (${reason.envVar})`;
|
|
20033
|
+
}
|
|
20034
|
+
return "the request being cancelled";
|
|
20035
|
+
}
|
|
20036
|
+
function stoppedByDeadline(signal) {
|
|
20037
|
+
const reason = signal?.reason;
|
|
20038
|
+
return !!reason && typeof reason === "object" && reason.kind === "deadline";
|
|
20039
|
+
}
|
|
20040
|
+
function raceStop(work, signal) {
|
|
20041
|
+
if (!signal) return work;
|
|
20042
|
+
if (signal.aborted) {
|
|
20043
|
+
work.catch(() => void 0);
|
|
20044
|
+
return Promise.resolve(STOPPED);
|
|
20045
|
+
}
|
|
20046
|
+
return new Promise((resolve5, reject) => {
|
|
20047
|
+
const onAbort = () => {
|
|
20048
|
+
work.catch(() => void 0);
|
|
20049
|
+
resolve5(STOPPED);
|
|
20050
|
+
};
|
|
20051
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
20052
|
+
work.then(
|
|
20053
|
+
(value) => {
|
|
20054
|
+
signal.removeEventListener("abort", onAbort);
|
|
20055
|
+
resolve5(value);
|
|
20056
|
+
},
|
|
20057
|
+
(error3) => {
|
|
20058
|
+
signal.removeEventListener("abort", onAbort);
|
|
20059
|
+
reject(error3);
|
|
20060
|
+
}
|
|
20061
|
+
);
|
|
20062
|
+
});
|
|
20063
|
+
}
|
|
20064
|
+
var DEFAULT_SEARCH_DEADLINE_MS, MIN_SEARCH_DEADLINE_MS, SEARCH_DEADLINE_ENV, STOPPED;
|
|
20065
|
+
var init_callStop = __esm({
|
|
20066
|
+
"src/utils/callStop.ts"() {
|
|
20067
|
+
"use strict";
|
|
20068
|
+
DEFAULT_SEARCH_DEADLINE_MS = 45e3;
|
|
20069
|
+
MIN_SEARCH_DEADLINE_MS = 2e3;
|
|
20070
|
+
SEARCH_DEADLINE_ENV = "APPLE_MAIL_MCP_SEARCH_DEADLINE_MS";
|
|
20071
|
+
STOPPED = /* @__PURE__ */ Symbol("stopped");
|
|
20072
|
+
}
|
|
20073
|
+
});
|
|
20074
|
+
|
|
19997
20075
|
// node_modules/.pnpm/pino-std-serializers@7.1.0/node_modules/pino-std-serializers/lib/err-helpers.js
|
|
19998
20076
|
var require_err_helpers = __commonJS({
|
|
19999
20077
|
"node_modules/.pnpm/pino-std-serializers@7.1.0/node_modules/pino-std-serializers/lib/err-helpers.js"(exports, module) {
|
|
@@ -65379,6 +65457,7 @@ __export(imapClient_exports, {
|
|
|
65379
65457
|
resolveImapConfigs: () => resolveImapConfigs,
|
|
65380
65458
|
resolveMailboxPath: () => resolveMailboxPath,
|
|
65381
65459
|
shouldUseImap: () => shouldUseImap,
|
|
65460
|
+
stoppedNote: () => stoppedNote,
|
|
65382
65461
|
unscopedSearchOffsetError: () => unscopedSearchOffsetError
|
|
65383
65462
|
});
|
|
65384
65463
|
import { createHash } from "node:crypto";
|
|
@@ -65699,6 +65778,12 @@ function structuredRow(m, account, path3) {
|
|
|
65699
65778
|
...env.messageId ? { messageId: env.messageId } : {}
|
|
65700
65779
|
};
|
|
65701
65780
|
}
|
|
65781
|
+
function stoppedNote(timedOut, signal) {
|
|
65782
|
+
if (timedOut.length === 0) return "";
|
|
65783
|
+
return `
|
|
65784
|
+
|
|
65785
|
+
Partial result. Stopped by ${describeStop(signal)} before finishing ${timedOut.length} mailbox(es): ${timedOut.map((m) => `"${m}"`).join(", ")}. This is NOT a confirmed "no such mail" \u2014 narrow the search with \`mailbox\` and/or \`dateFrom\`, or raise ${SEARCH_DEADLINE_ENV}.`;
|
|
65786
|
+
}
|
|
65702
65787
|
function omittedNote(omitted, merged) {
|
|
65703
65788
|
if (omitted.length === 0) return "";
|
|
65704
65789
|
const list = omitted.map((o) => `UID ${o.uid} in "${o.mailbox}" (${o.id})`).join(", ");
|
|
@@ -65988,9 +66073,23 @@ async function run(args, listMode, deps) {
|
|
|
65988
66073
|
const omittedMessages = [];
|
|
65989
66074
|
let totalMatched = 0;
|
|
65990
66075
|
let totalExact = true;
|
|
65991
|
-
|
|
66076
|
+
const timedOutMailboxes = [];
|
|
66077
|
+
for (const [index, path3] of paths.entries()) {
|
|
66078
|
+
if (deps.signal?.aborted) {
|
|
66079
|
+
timedOutMailboxes.push(...paths.slice(index));
|
|
66080
|
+
break;
|
|
66081
|
+
}
|
|
65992
66082
|
try {
|
|
65993
|
-
const
|
|
66083
|
+
const outcome = await raceStop(
|
|
66084
|
+
fetchMailboxMatches(client, path3, criteria, page),
|
|
66085
|
+
deps.signal
|
|
66086
|
+
);
|
|
66087
|
+
if (outcome === STOPPED) {
|
|
66088
|
+
abandonClient(client, cfg, deps);
|
|
66089
|
+
timedOutMailboxes.push(...paths.slice(index));
|
|
66090
|
+
break;
|
|
66091
|
+
}
|
|
66092
|
+
const result = outcome;
|
|
65994
66093
|
totalMatched += result.total;
|
|
65995
66094
|
totalExact &&= result.totalExact;
|
|
65996
66095
|
fetched.push(...result.messages.map((message) => ({ message, path: path3 })));
|
|
@@ -66030,10 +66129,14 @@ async function run(args, listMode, deps) {
|
|
|
66030
66129
|
const messages = ordered.map(
|
|
66031
66130
|
({ message, path: path3 }) => structuredRow(message, cfg.accountLabel, path3)
|
|
66032
66131
|
);
|
|
66033
|
-
const partial2 = failedMailboxes.length > 0 || omittedMessages.length > 0;
|
|
66132
|
+
const partial2 = failedMailboxes.length > 0 || omittedMessages.length > 0 || timedOutMailboxes.length > 0;
|
|
66133
|
+
const stopped = timedOutMailboxes.length > 0 ? {
|
|
66134
|
+
timedOutMailboxes,
|
|
66135
|
+
stoppedBy: stoppedByDeadline(deps.signal) ? "deadline" : "cancelled"
|
|
66136
|
+
} : {};
|
|
66034
66137
|
const failureNote = (failedMailboxes.length > 0 ? `
|
|
66035
66138
|
|
|
66036
|
-
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path3) => `"${path3}" (${failedMailboxReasons[path3]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch);
|
|
66139
|
+
Partial result. Could not search mailbox(es): ${failedMailboxes.map((path3) => `"${path3}" (${failedMailboxReasons[path3]})`).join(", ")}.` : "") + omittedNote(omittedMessages, unscopedSearch) + stoppedNote(timedOutMailboxes, deps.signal);
|
|
66037
66140
|
const verb = listMode ? "listed" : "matched";
|
|
66038
66141
|
const totalText = totalExact ? `${totalMatched} total` : `at least ${totalMatched} total`;
|
|
66039
66142
|
const scope = unscopedSearch ? allMailboxCount === 1 ? `mailbox "${paths[0]}"` : `${allMailboxCount} selectable mailboxes` : `mailbox "${paths[0]}"`;
|
|
@@ -66045,7 +66148,8 @@ Partial result. Could not search mailbox(es): ${failedMailboxes.map((path3) => `
|
|
|
66045
66148
|
partial: partial2,
|
|
66046
66149
|
failedMailboxes,
|
|
66047
66150
|
failedMailboxReasons,
|
|
66048
|
-
omittedMessages
|
|
66151
|
+
omittedMessages,
|
|
66152
|
+
...stopped
|
|
66049
66153
|
};
|
|
66050
66154
|
}
|
|
66051
66155
|
const text = `Found ${rows.length} message(s) via IMAP (server-side, account ${cfg.accountLabel}, ${scope}; ${totalText} ${verb}):
|
|
@@ -66059,7 +66163,8 @@ Note: these IMAP IDs (imap:\u2026) work with get-message and the message mutatio
|
|
|
66059
66163
|
partial: partial2,
|
|
66060
66164
|
failedMailboxes,
|
|
66061
66165
|
failedMailboxReasons,
|
|
66062
|
-
omittedMessages
|
|
66166
|
+
omittedMessages,
|
|
66167
|
+
...stopped
|
|
66063
66168
|
};
|
|
66064
66169
|
},
|
|
66065
66170
|
true
|
|
@@ -66202,6 +66307,20 @@ async function dropPool(key) {
|
|
|
66202
66307
|
await e.client.logout().catch(() => void 0);
|
|
66203
66308
|
e.client.close?.();
|
|
66204
66309
|
}
|
|
66310
|
+
function abandonClient(client, cfg, deps) {
|
|
66311
|
+
if (!deps.connect) {
|
|
66312
|
+
const key = poolKey(cfg);
|
|
66313
|
+
const entry = pools.get(key);
|
|
66314
|
+
if (entry && entry.client === client) {
|
|
66315
|
+
if (entry.idle) clearTimeout(entry.idle);
|
|
66316
|
+
pools.delete(key);
|
|
66317
|
+
}
|
|
66318
|
+
}
|
|
66319
|
+
try {
|
|
66320
|
+
client.close?.();
|
|
66321
|
+
} catch {
|
|
66322
|
+
}
|
|
66323
|
+
}
|
|
66205
66324
|
async function dropAllPools() {
|
|
66206
66325
|
await Promise.all([...pools.keys()].map((k) => dropPool(k)));
|
|
66207
66326
|
}
|
|
@@ -67200,6 +67319,7 @@ var import_imapflow, IMAP_ENV, defaultConnect, SPECIAL_USE_ALIASES, NOT_DELETED,
|
|
|
67200
67319
|
var init_imapClient = __esm({
|
|
67201
67320
|
"src/services/imapClient.ts"() {
|
|
67202
67321
|
"use strict";
|
|
67322
|
+
init_callStop();
|
|
67203
67323
|
import_imapflow = __toESM(require_imap_flow(), 1);
|
|
67204
67324
|
init_smtpMailer();
|
|
67205
67325
|
init_docsUrls();
|
|
@@ -82603,6 +82723,44 @@ var READ_ONLY_BYID_TIMEOUT_MS = 15e3;
|
|
|
82603
82723
|
function timedOutResolvingId(id) {
|
|
82604
82724
|
return `AppleScript timed out resolving id ${Number(id)} \u2014 Mail.app's scripting bridge may be wedged; try health-check.`;
|
|
82605
82725
|
}
|
|
82726
|
+
function timedOutScanningForId(id) {
|
|
82727
|
+
return `AppleScript timed out scanning every mailbox for id ${Number(id)} \u2014 the cross-mailbox scan did not finish within ${READ_ONLY_BYID_TIMEOUT_MS / 1e3}s (very large mailboxes; if health-check also fails, Mail.app's scripting bridge may be wedged). Pass account + mailbox to read it from one mailbox, or use the message's imap: id from list-messages/search-messages.`;
|
|
82728
|
+
}
|
|
82729
|
+
function getByIdScanThreshold() {
|
|
82730
|
+
const raw = process.env.APPLE_MAIL_MAX_BYID_SCAN_MAILBOX;
|
|
82731
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
82732
|
+
const n = Number(raw);
|
|
82733
|
+
if (Number.isFinite(n) && n >= 0) return Math.floor(n);
|
|
82734
|
+
}
|
|
82735
|
+
return 5e4;
|
|
82736
|
+
}
|
|
82737
|
+
var BYID_SCAN_BUDGET_SECONDS = 9;
|
|
82738
|
+
var BYID_COVERAGE_MARKER = "COV";
|
|
82739
|
+
var SCOPED_MISS_MARKER = "SCOPED";
|
|
82740
|
+
function describeUnscopedMiss(id, raw) {
|
|
82741
|
+
const covIdx = raw.indexOf(BYID_COVERAGE_MARKER);
|
|
82742
|
+
if (covIdx === -1) return raw.trim();
|
|
82743
|
+
const base2 = raw.slice(0, covIdx).trim();
|
|
82744
|
+
const [skippedRaw = "", budgetRaw = ""] = raw.slice(covIdx + BYID_COVERAGE_MARKER.length).split(DIAG_FIELD_SEP);
|
|
82745
|
+
const skipped = skippedRaw.split(DIAG_ITEM_SEP).map((s) => s.trim()).filter(Boolean);
|
|
82746
|
+
const budgetHit = budgetRaw.trim() === "true";
|
|
82747
|
+
if (skipped.length === 0 && !budgetHit) return base2;
|
|
82748
|
+
const parts = [`Message ${Number(id)} not found in the mailboxes scanned.`];
|
|
82749
|
+
if (skipped.length > 0) {
|
|
82750
|
+
parts.push(
|
|
82751
|
+
`Not scanned \u2014 too large for a cross-mailbox id scan (over ${getByIdScanThreshold()} messages; APPLE_MAIL_MAX_BYID_SCAN_MAILBOX): ${skipped.join(", ")}.`
|
|
82752
|
+
);
|
|
82753
|
+
}
|
|
82754
|
+
if (budgetHit) {
|
|
82755
|
+
parts.push(
|
|
82756
|
+
`The scan also stopped after its ${BYID_SCAN_BUDGET_SECONDS}s budget before reaching every mailbox.`
|
|
82757
|
+
);
|
|
82758
|
+
}
|
|
82759
|
+
parts.push(
|
|
82760
|
+
"If the message is in a mailbox that was not scanned, pass account + mailbox to read it from that mailbox directly, or use its imap: id from list-messages/search-messages."
|
|
82761
|
+
);
|
|
82762
|
+
return parts.join(" ");
|
|
82763
|
+
}
|
|
82606
82764
|
var GROUP_SEP = "";
|
|
82607
82765
|
var FIELD_SEP = "";
|
|
82608
82766
|
var RECORD_SEP = "";
|
|
@@ -83108,8 +83266,9 @@ var AppleMailManager = class {
|
|
|
83108
83266
|
* Items") isn't reached before the AppleScript timeout fires, so the fetch
|
|
83109
83267
|
* returns a false "not found" (only INBOX ids, reached early, worked). Every
|
|
83110
83268
|
* search/list/by-id result records its id→location here so a subsequent fetch
|
|
83111
|
-
* opens the one right mailbox directly. A stale entry (message moved
|
|
83112
|
-
* misses and falls back to the
|
|
83269
|
+
* opens the one right mailbox directly. A stale entry (message moved by
|
|
83270
|
+
* another client) misses, is EVICTED, and the read falls back to the bounded
|
|
83271
|
+
* cross-mailbox scan — see `resolveScopedById` (#270).
|
|
83113
83272
|
*/
|
|
83114
83273
|
idLocationIndex = /* @__PURE__ */ new Map();
|
|
83115
83274
|
/** Error from the most recent numeric message read, if it was refused. */
|
|
@@ -84209,8 +84368,12 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84209
84368
|
/**
|
|
84210
84369
|
* Get a message by ID.
|
|
84211
84370
|
*
|
|
84212
|
-
* Note: Mail.app message IDs are unique per mailbox.
|
|
84213
|
-
*
|
|
84371
|
+
* Note: Mail.app message IDs are unique per mailbox. Resolution goes through
|
|
84372
|
+
* `resolveById` — the remembered location first, then the bounded
|
|
84373
|
+
* cross-mailbox scan — taking the FIRST copy found rather than refusing an id
|
|
84374
|
+
* a label store repeats across INBOX / "All Mail": every caller of this reads
|
|
84375
|
+
* per-message metadata (get-thread's seed subject, a mutation error's mailbox
|
|
84376
|
+
* hint) that is identical across those copies.
|
|
84214
84377
|
*/
|
|
84215
84378
|
getMessageById(id, deepAttachmentCheck = false) {
|
|
84216
84379
|
this.lastMessageLookupError = void 0;
|
|
@@ -84220,14 +84383,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84220
84383
|
if rawSrc contains "Content-Disposition: attachment" then set hasAtt to "true"
|
|
84221
84384
|
end try
|
|
84222
84385
|
end if` : "";
|
|
84223
|
-
const
|
|
84224
|
-
try
|
|
84225
|
-
repeat with acct in accounts
|
|
84226
|
-
repeat with mb in mailboxes of acct
|
|
84227
|
-
try
|
|
84228
|
-
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
84229
|
-
if (count of matchingMsgs) > 0 then
|
|
84230
|
-
set msg to item 1 of matchingMsgs
|
|
84386
|
+
const innerFetch = `
|
|
84231
84387
|
set msgSubject to subject of msg
|
|
84232
84388
|
set msgSender to sender of msg
|
|
84233
84389
|
set d to date received of msg
|
|
@@ -84237,31 +84393,20 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84237
84393
|
set msgJunk to junk mail status of msg as string
|
|
84238
84394
|
set msgDeleted to deleted status of msg as string
|
|
84239
84395
|
set msgMailbox to ""
|
|
84240
|
-
|
|
84241
|
-
|
|
84396
|
+
try
|
|
84397
|
+
${mailboxPathFragment("_hitMb", "msgMailbox")}
|
|
84398
|
+
end try
|
|
84399
|
+
set msgAccount to _hitAcct
|
|
84242
84400
|
set hasAtt to "false"
|
|
84243
84401
|
try
|
|
84244
84402
|
set attCount to count of mail attachments of msg
|
|
84245
84403
|
if attCount > 0 then set hasAtt to "true"
|
|
84246
84404
|
end try
|
|
84247
84405
|
${deepScan}
|
|
84248
|
-
return msgSubject & "${FIELD_SEP}" & msgSender & "${FIELD_SEP}" & msgDate & "${FIELD_SEP}" & msgRead & "${FIELD_SEP}" & msgFlagged & "${FIELD_SEP}" & msgJunk & "${FIELD_SEP}" & msgDeleted & "${FIELD_SEP}" & msgMailbox & "${FIELD_SEP}" & msgAccount & "${FIELD_SEP}" & hasAtt
|
|
84249
|
-
|
|
84250
|
-
|
|
84251
|
-
|
|
84252
|
-
end repeat
|
|
84253
|
-
return ""
|
|
84254
|
-
on error errMsg
|
|
84255
|
-
return ""
|
|
84256
|
-
end try
|
|
84257
|
-
`);
|
|
84258
|
-
const result = executeAppleScript(script, { timeoutMs: READ_ONLY_BYID_TIMEOUT_MS });
|
|
84259
|
-
if (!result.success || !result.output.trim()) {
|
|
84260
|
-
if (result.timedOut) this.lastMessageLookupError = timedOutResolvingId(id);
|
|
84261
|
-
console.error(`Failed to get message ${id}: ${result.error}`);
|
|
84262
|
-
return null;
|
|
84263
|
-
}
|
|
84264
|
-
const parts = result.output.split(FIELD_SEP);
|
|
84406
|
+
return msgSubject & "${FIELD_SEP}" & msgSender & "${FIELD_SEP}" & msgDate & "${FIELD_SEP}" & msgRead & "${FIELD_SEP}" & msgFlagged & "${FIELD_SEP}" & msgJunk & "${FIELD_SEP}" & msgDeleted & "${FIELD_SEP}" & msgMailbox & "${FIELD_SEP}" & msgAccount & "${FIELD_SEP}" & hasAtt`;
|
|
84407
|
+
const output = this.resolveById(id, innerFetch, void 0, { firstHit: true });
|
|
84408
|
+
if (output === null) return null;
|
|
84409
|
+
const parts = output.split(FIELD_SEP);
|
|
84265
84410
|
if (parts.length < 9) return null;
|
|
84266
84411
|
this.rememberLocation(id.toString(), parts[8], parts[7]);
|
|
84267
84412
|
return {
|
|
@@ -84279,47 +84424,194 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84279
84424
|
hasAttachments: parts.length > 9 ? parts[9] === "true" : false
|
|
84280
84425
|
};
|
|
84281
84426
|
}
|
|
84427
|
+
/**
|
|
84428
|
+
* Resolve numeric `id` and run `innerAction` against it — the ONE resolution
|
|
84429
|
+
* path behind every read-only by-id lookup (getMessageById,
|
|
84430
|
+
* getMessageContent, getMessageHeaders, getRawSource), so the four cannot
|
|
84431
|
+
* drift apart again. Returns the action's output, or null with
|
|
84432
|
+
* `lastMessageLookupError` set when there is something to say (#270):
|
|
84433
|
+
*
|
|
84434
|
+
* 1. Scoped, when a location is known — the caller's explicit `account` +
|
|
84435
|
+
* `mailbox`, else the `idLocationIndex` entry (see `resolveScopedById`):
|
|
84436
|
+
* - explicit hint, miss → a clean "not found in <account> / <mailbox>".
|
|
84437
|
+
* NO fallback: the caller said where to look, and the cross-mailbox
|
|
84438
|
+
* scan it would fall back to is the 20-50s operation @j5pu timed.
|
|
84439
|
+
* - index entry, miss → the entry is stale (the message was moved, by
|
|
84440
|
+
* another client or in Mail itself): evict it, then fall back to (2).
|
|
84441
|
+
* - timeout on either → report it; no second 15s scan on top.
|
|
84442
|
+
* 2. Unscoped: every mailbox of every account plus the local store, but
|
|
84443
|
+
* mailboxes above `getByIdScanThreshold()` are skipped and the walk stops
|
|
84444
|
+
* at `BYID_SCAN_BUDGET_SECONDS`; a miss names what was not covered.
|
|
84445
|
+
*/
|
|
84446
|
+
resolveById(id, innerAction, hint, opts = {}) {
|
|
84447
|
+
const remembered = this.idLocationIndex.get(String(id));
|
|
84448
|
+
const loc = hint?.account && hint?.mailbox ? { account: hint.account, mailbox: hint.mailbox, explicit: true } : remembered ? { ...remembered, explicit: false } : void 0;
|
|
84449
|
+
if (loc) {
|
|
84450
|
+
const scoped = this.resolveScopedById(id, loc, innerAction);
|
|
84451
|
+
if (scoped.kind === "hit") return scoped.output;
|
|
84452
|
+
if (scoped.kind === "done") return null;
|
|
84453
|
+
}
|
|
84454
|
+
const result = executeAppleScript(this.unscopedByIdScript(id, innerAction, opts), {
|
|
84455
|
+
timeoutMs: READ_ONLY_BYID_TIMEOUT_MS
|
|
84456
|
+
});
|
|
84457
|
+
if (!result.success || !result.output.trim()) {
|
|
84458
|
+
if (!result.success) {
|
|
84459
|
+
if (result.timedOut) this.lastMessageLookupError = timedOutScanningForId(id);
|
|
84460
|
+
console.error(`Failed to resolve message ${id}: ${result.error}`);
|
|
84461
|
+
}
|
|
84462
|
+
return null;
|
|
84463
|
+
}
|
|
84464
|
+
if (result.output.startsWith(LOOKUP_ERROR_MARKER)) {
|
|
84465
|
+
this.lastMessageLookupError = describeUnscopedMiss(
|
|
84466
|
+
id,
|
|
84467
|
+
result.output.slice(LOOKUP_ERROR_MARKER.length)
|
|
84468
|
+
);
|
|
84469
|
+
return null;
|
|
84470
|
+
}
|
|
84471
|
+
return result.output;
|
|
84472
|
+
}
|
|
84473
|
+
/**
|
|
84474
|
+
* The scoped half of `resolveById`: open exactly `loc`'s mailbox and look for
|
|
84475
|
+
* the id there. `hit` carries the action's output; `done` means stop with
|
|
84476
|
+
* `lastMessageLookupError` set; `fallback` means the remembered location was
|
|
84477
|
+
* stale and has been evicted, so the caller should try the bounded scan.
|
|
84478
|
+
*/
|
|
84479
|
+
resolveScopedById(id, loc, innerAction) {
|
|
84480
|
+
const result = executeAppleScript(
|
|
84481
|
+
this.scopedByIdScript(loc.account, loc.mailbox, id, innerAction),
|
|
84482
|
+
{ timeoutMs: READ_ONLY_BYID_TIMEOUT_MS }
|
|
84483
|
+
);
|
|
84484
|
+
const where = `${loc.account} / ${loc.mailbox}`;
|
|
84485
|
+
if (!result.success) {
|
|
84486
|
+
console.error(`Failed to read message ${id} from ${where}: ${result.error}`);
|
|
84487
|
+
if (result.timedOut) {
|
|
84488
|
+
this.lastMessageLookupError = timedOutResolvingId(id);
|
|
84489
|
+
return { kind: "done" };
|
|
84490
|
+
}
|
|
84491
|
+
if (loc.explicit) {
|
|
84492
|
+
this.lastMessageLookupError = `Could not read message ${Number(id)} from ${where}: ${result.error ?? "AppleScript error"}`;
|
|
84493
|
+
return { kind: "done" };
|
|
84494
|
+
}
|
|
84495
|
+
this.forgetLocation(id);
|
|
84496
|
+
return { kind: "fallback" };
|
|
84497
|
+
}
|
|
84498
|
+
const scopedPrefix = LOOKUP_ERROR_MARKER + SCOPED_MISS_MARKER;
|
|
84499
|
+
if (result.output.trim() && !result.output.startsWith(scopedPrefix)) {
|
|
84500
|
+
return { kind: "hit", output: result.output };
|
|
84501
|
+
}
|
|
84502
|
+
if (!loc.explicit) {
|
|
84503
|
+
this.forgetLocation(id);
|
|
84504
|
+
return { kind: "fallback" };
|
|
84505
|
+
}
|
|
84506
|
+
const reason = result.output.startsWith(scopedPrefix) ? result.output.slice(scopedPrefix.length) : "nomessage";
|
|
84507
|
+
if (reason === "nomailbox") {
|
|
84508
|
+
this.lastMessageLookupError = `Mailbox "${loc.mailbox}" not found in account "${loc.account}" \u2014 message ${Number(id)} was not looked up. Check the names with list-mailboxes, or omit account/mailbox to scan every mailbox.`;
|
|
84509
|
+
} else if (reason.startsWith("error:")) {
|
|
84510
|
+
this.lastMessageLookupError = `Could not read message ${Number(id)} from ${where}: ${reason.slice("error:".length)}`;
|
|
84511
|
+
} else {
|
|
84512
|
+
this.lastMessageLookupError = `Message ${Number(id)} not found in ${where} \u2014 it may have been moved or deleted. Re-list or search to get its current id, or omit account/mailbox to scan every mailbox.`;
|
|
84513
|
+
}
|
|
84514
|
+
return { kind: "done" };
|
|
84515
|
+
}
|
|
84516
|
+
/** Drop a stale id→location entry (#270) so the next read doesn't retry it. */
|
|
84517
|
+
forgetLocation(id) {
|
|
84518
|
+
this.idLocationIndex.delete(String(id));
|
|
84519
|
+
}
|
|
84282
84520
|
/**
|
|
84283
84521
|
* The unscoped by-id resolution: walk every mailbox of every account, then the
|
|
84284
84522
|
* local store (#183), and run `innerAction` with `msg` bound when EXACTLY one
|
|
84285
|
-
* mailbox holds the id
|
|
84286
|
-
*
|
|
84287
|
-
*
|
|
84288
|
-
*
|
|
84523
|
+
* mailbox holds the id (or on the first hit, with `firstHit`) —
|
|
84524
|
+
* `{LOOKUP_ERROR_MARKER}` prefixes both the not-found and the ambiguous
|
|
84525
|
+
* outcome. `_hitMb` / `_hitAcct` are bound to the hit's mailbox and account
|
|
84526
|
+
* name alongside `msg`.
|
|
84527
|
+
*
|
|
84528
|
+
* Bounded (#270): a mailbox above `getByIdScanThreshold()` messages is
|
|
84529
|
+
* skipped rather than probed, and the walk stops once it has run for
|
|
84530
|
+
* `BYID_SCAN_BUDGET_SECONDS`. Either way the not-found outcome carries a
|
|
84531
|
+
* `BYID_COVERAGE_MARKER` trailer naming what was not covered, so the caller
|
|
84532
|
+
* can say "not found in the mailboxes scanned; these large ones were not"
|
|
84533
|
+
* instead of a false flat "not found". A copy that sits only in a skipped
|
|
84534
|
+
* mailbox therefore no longer counts toward the ambiguity check — harmless
|
|
84535
|
+
* for these read-only lookups, whose copies are the same message.
|
|
84289
84536
|
*/
|
|
84290
|
-
unscopedByIdScript(id, innerAction) {
|
|
84537
|
+
unscopedByIdScript(id, innerAction, opts = {}) {
|
|
84538
|
+
const threshold = getByIdScanThreshold();
|
|
84539
|
+
const sizeGuard = (mbVar, acctNameExpr) => threshold > 0 ? `set _mbCount to 0
|
|
84540
|
+
try
|
|
84541
|
+
set _mbCount to count of messages of ${mbVar}
|
|
84542
|
+
end try
|
|
84543
|
+
if _mbCount > ${threshold} then
|
|
84544
|
+
set _skipPath to ""
|
|
84545
|
+
try
|
|
84546
|
+
${mailboxPathFragment(mbVar, "_skipPath")}
|
|
84547
|
+
end try
|
|
84548
|
+
set _skipped to _skipped & ${acctNameExpr} & " / " & _skipPath & " (" & (_mbCount as string) & ")${DIAG_ITEM_SEP}"
|
|
84549
|
+
set _probe to false
|
|
84550
|
+
end if` : "";
|
|
84551
|
+
const budgetCheck = `if ((current date) - _startedAt) > ${BYID_SCAN_BUDGET_SECONDS} then
|
|
84552
|
+
set _budgetHit to true
|
|
84553
|
+
exit repeat
|
|
84554
|
+
end if`;
|
|
84555
|
+
const stopOnHit = opts.firstHit ? `if (count of _hits) > 0 then exit repeat` : "";
|
|
84291
84556
|
return buildAppLevelScript(`
|
|
84292
84557
|
try
|
|
84293
84558
|
set _hits to {}
|
|
84559
|
+
set _hitMbs to {}
|
|
84560
|
+
set _hitAccts to {}
|
|
84294
84561
|
set _names to ""
|
|
84562
|
+
set _skipped to ""
|
|
84563
|
+
set _budgetHit to false
|
|
84564
|
+
set _startedAt to current date
|
|
84295
84565
|
repeat with acct in accounts
|
|
84566
|
+
if _budgetHit then exit repeat
|
|
84567
|
+
${stopOnHit}
|
|
84296
84568
|
repeat with mb in mailboxes of acct
|
|
84297
|
-
|
|
84298
|
-
|
|
84299
|
-
|
|
84300
|
-
|
|
84301
|
-
|
|
84302
|
-
|
|
84303
|
-
|
|
84569
|
+
${budgetCheck}
|
|
84570
|
+
set _probe to true
|
|
84571
|
+
${sizeGuard("mb", "(name of acct)")}
|
|
84572
|
+
if _probe then
|
|
84573
|
+
try
|
|
84574
|
+
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
84575
|
+
if (count of matchingMsgs) > 0 then
|
|
84576
|
+
set end of _hits to item 1 of matchingMsgs
|
|
84577
|
+
set end of _hitMbs to (contents of mb)
|
|
84578
|
+
set end of _hitAccts to (name of acct)
|
|
84579
|
+
set _names to _names & (name of acct) & "/" & (name of mb) & ", "
|
|
84580
|
+
end if
|
|
84581
|
+
end try
|
|
84582
|
+
end if
|
|
84583
|
+
${stopOnHit}
|
|
84304
84584
|
end repeat
|
|
84305
84585
|
end repeat
|
|
84306
84586
|
-- #183: local mailboxes belong to no account, so the walk above cannot
|
|
84307
84587
|
-- reach them. Collect into the SAME _hits/_names, which means an id
|
|
84308
84588
|
-- present both in an account and locally is now correctly reported as
|
|
84309
|
-
-- ambiguous rather than silently resolving to the account copy
|
|
84310
|
-
|
|
84311
|
-
|
|
84312
|
-
|
|
84313
|
-
|
|
84314
|
-
|
|
84315
|
-
|
|
84589
|
+
-- ambiguous rather than silently resolving to the account copy.
|
|
84590
|
+
if (not _budgetHit) and ${opts.firstHit ? "(count of _hits) is 0" : "true"} then${localMailboxBindingFragment()}
|
|
84591
|
+
repeat with mb in _mbs
|
|
84592
|
+
${budgetCheck}
|
|
84593
|
+
set _probe to true
|
|
84594
|
+
${sizeGuard("mb", `"${LOCAL_STORE_LABEL}"`)}
|
|
84595
|
+
if _probe then
|
|
84596
|
+
try
|
|
84597
|
+
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
84598
|
+
if (count of matchingMsgs) > 0 then
|
|
84599
|
+
set end of _hits to item 1 of matchingMsgs
|
|
84600
|
+
set end of _hitMbs to (contents of mb)
|
|
84601
|
+
set end of _hitAccts to "${LOCAL_STORE_LABEL}"
|
|
84602
|
+
set _names to _names & "${LOCAL_STORE_LABEL}/" & (name of mb) & ", "
|
|
84603
|
+
end if
|
|
84604
|
+
end try
|
|
84316
84605
|
end if
|
|
84317
|
-
|
|
84318
|
-
|
|
84319
|
-
if
|
|
84320
|
-
if (count of _hits)
|
|
84321
|
-
if (count of _hits)
|
|
84606
|
+
${stopOnHit}
|
|
84607
|
+
end repeat
|
|
84608
|
+
end if
|
|
84609
|
+
if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found${BYID_COVERAGE_MARKER}" & _skipped & "${DIAG_FIELD_SEP}" & (_budgetHit as string)
|
|
84610
|
+
${opts.firstHit ? "" : `if (count of _hits) > 1 then return "${LOOKUP_ERROR_MARKER}${AMBIGUOUS_ID_PREFIX}${Number(id)} is present in more than one mailbox (" & _names & "); list or search that mailbox first so the read targets the right copy"`}
|
|
84611
|
+
if (count of _hits) ${opts.firstHit ? "> 0" : "is 1"} then
|
|
84322
84612
|
set msg to item 1 of _hits
|
|
84613
|
+
set _hitMb to item 1 of _hitMbs
|
|
84614
|
+
set _hitAcct to item 1 of _hitAccts
|
|
84323
84615
|
${innerAction}
|
|
84324
84616
|
end if
|
|
84325
84617
|
return ""
|
|
@@ -84331,20 +84623,25 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84331
84623
|
/**
|
|
84332
84624
|
* Build an app-level AppleScript that opens exactly one account+mailbox, finds
|
|
84333
84625
|
* the message with numeric `id` in it, and runs `innerAction` (which may assume
|
|
84334
|
-
* `msg`
|
|
84335
|
-
* so a message in a late-iterated large folder resolves
|
|
84336
|
-
* the timeout-prone full-mailbox scan.
|
|
84626
|
+
* `msg`, `_hitMb` and `_hitAcct` are bound). Used by the by-id fast path
|
|
84627
|
+
* (`resolveScopedById`) so a message in a late-iterated large folder resolves
|
|
84628
|
+
* directly instead of via the timeout-prone full-mailbox scan.
|
|
84337
84629
|
*
|
|
84338
84630
|
* The mailbox name is resolved through `resolveMailbox` (so an alias like
|
|
84339
84631
|
* "Sent"→"Sent Items" or a casing mismatch like "INBOX"→"Inbox" still opens the
|
|
84340
84632
|
* right folder), and matched case-insensitively by iterating the account's
|
|
84341
84633
|
* mailboxes — `mailbox "INBOX" of account …` throws on accounts whose inbox is
|
|
84342
84634
|
* actually named "Inbox", which would silently drop us back to the slow scan.
|
|
84343
|
-
*
|
|
84635
|
+
*
|
|
84636
|
+
* A miss is reported, not swallowed (#270): `{LOOKUP_ERROR_MARKER}
|
|
84637
|
+
* {SCOPED_MISS_MARKER}` followed by `nomailbox`, `nomessage` or
|
|
84638
|
+
* `error:<Mail's text>`, so the caller can return a clean "not found in
|
|
84639
|
+
* <mailbox>" for an explicit hint instead of guessing.
|
|
84344
84640
|
*/
|
|
84345
84641
|
scopedByIdScript(account, mailbox, id, innerAction) {
|
|
84346
84642
|
const resolved = this.resolveMailbox(mailbox, account);
|
|
84347
84643
|
const bind = isLocalStoreLabel(account) ? `${localMailboxBindingFragment()}
|
|
84644
|
+
set _hitAcct to "${LOCAL_STORE_LABEL}"
|
|
84348
84645
|
set targetMb to missing value
|
|
84349
84646
|
ignoring case
|
|
84350
84647
|
repeat with mb in _mbs
|
|
@@ -84356,6 +84653,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84356
84653
|
end if
|
|
84357
84654
|
end repeat
|
|
84358
84655
|
end ignoring` : `set acct to (first account whose name is "${escapeForAppleScript(account)}")
|
|
84656
|
+
set _hitAcct to (name of acct)
|
|
84359
84657
|
set targetMb to missing value
|
|
84360
84658
|
ignoring case
|
|
84361
84659
|
repeat with mb in mailboxes of acct
|
|
@@ -84367,19 +84665,19 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84367
84665
|
end if
|
|
84368
84666
|
end repeat
|
|
84369
84667
|
end ignoring`;
|
|
84668
|
+
const miss = `${LOOKUP_ERROR_MARKER}${SCOPED_MISS_MARKER}`;
|
|
84370
84669
|
return buildAppLevelScript(`
|
|
84371
84670
|
try
|
|
84372
84671
|
${bind}
|
|
84373
|
-
if targetMb is
|
|
84374
|
-
|
|
84375
|
-
|
|
84376
|
-
|
|
84377
|
-
|
|
84378
|
-
|
|
84379
|
-
end if
|
|
84672
|
+
if targetMb is missing value then return "${miss}nomailbox"
|
|
84673
|
+
set _hitMb to (contents of targetMb)
|
|
84674
|
+
set matchingMsgs to (messages of targetMb whose id is ${Number(id)})
|
|
84675
|
+
if (count of matchingMsgs) is 0 then return "${miss}nomessage"
|
|
84676
|
+
set msg to item 1 of matchingMsgs
|
|
84677
|
+
${innerAction}
|
|
84380
84678
|
return ""
|
|
84381
84679
|
on error errMsg
|
|
84382
|
-
return ""
|
|
84680
|
+
return "${miss}error:" & errMsg
|
|
84383
84681
|
end try
|
|
84384
84682
|
`);
|
|
84385
84683
|
}
|
|
@@ -84392,6 +84690,9 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84392
84690
|
* source can be MB-sized (it includes base64 attachments) and the plain-text
|
|
84393
84691
|
* path doesn't need it; fetching it unconditionally was both slow and, worse,
|
|
84394
84692
|
* returned the entire raw MIME blob mislabeled as HTML (#32).
|
|
84693
|
+
* @param hint - Explicit account + mailbox (both required to take effect):
|
|
84694
|
+
* the read opens that one mailbox and, on a miss, reports "not found in
|
|
84695
|
+
* <account> / <mailbox>" without scanning anywhere else (#270).
|
|
84395
84696
|
*/
|
|
84396
84697
|
getMessageContent(id, includeHtml = false, hint) {
|
|
84397
84698
|
this.lastMessageLookupError = void 0;
|
|
@@ -84409,41 +84710,15 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84409
84710
|
${sourceFetch}
|
|
84410
84711
|
${AS_MESSAGE_DATES_FRAGMENT}
|
|
84411
84712
|
return msgSubject & "${MSGID_MARKER}" & msgRfcId & "${DATES_MARKER}" & msgDates & "${CONTENT_MARKER}" & msgContent & "${HTML_MARKER}" & htmlSource`;
|
|
84412
|
-
const
|
|
84413
|
-
|
|
84414
|
-
const scopedScript = this.scopedByIdScript(loc.account, loc.mailbox, id, innerFetch);
|
|
84415
|
-
const scoped = this.parseMessageContent(
|
|
84416
|
-
id,
|
|
84417
|
-
executeAppleScript(scopedScript, { timeoutMs: READ_ONLY_BYID_TIMEOUT_MS }),
|
|
84418
|
-
includeHtml
|
|
84419
|
-
);
|
|
84420
|
-
if (scoped) return scoped;
|
|
84421
|
-
}
|
|
84422
|
-
const script = this.unscopedByIdScript(id, innerFetch);
|
|
84423
|
-
return this.parseMessageContent(
|
|
84424
|
-
id,
|
|
84425
|
-
executeAppleScript(script, { timeoutMs: READ_ONLY_BYID_TIMEOUT_MS }),
|
|
84426
|
-
includeHtml
|
|
84427
|
-
);
|
|
84713
|
+
const output = this.resolveById(id, innerFetch, hint);
|
|
84714
|
+
return output === null ? null : this.parseMessageContent(id, output, includeHtml);
|
|
84428
84715
|
}
|
|
84429
84716
|
/**
|
|
84430
84717
|
* Parse the marker-delimited output of a getMessageContent AppleScript into a
|
|
84431
|
-
* MessageContent, or null when
|
|
84432
|
-
* the scoped fast path and the full-mailbox-scan fallback.
|
|
84718
|
+
* MessageContent, or null when the payload is malformed.
|
|
84433
84719
|
*/
|
|
84434
|
-
parseMessageContent(id,
|
|
84435
|
-
|
|
84436
|
-
if (!result.success) {
|
|
84437
|
-
if (result.timedOut) this.lastMessageLookupError = timedOutResolvingId(id);
|
|
84438
|
-
console.error(`Failed to get message content: ${result.error}`);
|
|
84439
|
-
}
|
|
84440
|
-
return null;
|
|
84441
|
-
}
|
|
84442
|
-
if (result.output.startsWith(LOOKUP_ERROR_MARKER)) {
|
|
84443
|
-
this.lastMessageLookupError = result.output.slice(LOOKUP_ERROR_MARKER.length).trim();
|
|
84444
|
-
return null;
|
|
84445
|
-
}
|
|
84446
|
-
const htmlSplit = result.output.split(HTML_MARKER);
|
|
84720
|
+
parseMessageContent(id, output, includeHtml) {
|
|
84721
|
+
const htmlSplit = output.split(HTML_MARKER);
|
|
84447
84722
|
const contentPart = htmlSplit[0];
|
|
84448
84723
|
const rawSource = htmlSplit.length > 1 ? htmlSplit[1] : "";
|
|
84449
84724
|
const parts = contentPart.split(CONTENT_MARKER);
|
|
@@ -84468,8 +84743,8 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84468
84743
|
/**
|
|
84469
84744
|
* Fetch ONLY the raw RFC 5322 header block of a message (#224) — Mail's
|
|
84470
84745
|
* `all headers` property — plus its `date received`, so the caller can show the
|
|
84471
|
-
* arrival timestamp beside the author's `Date:` header. Same
|
|
84472
|
-
*
|
|
84746
|
+
* arrival timestamp beside the author's `Date:` header. Same resolution as
|
|
84747
|
+
* getMessageContent (resolveById); never reads the body or source.
|
|
84473
84748
|
* Returns null (with `lastMessageLookupError` set when Mail said why) on a miss.
|
|
84474
84749
|
*/
|
|
84475
84750
|
getMessageHeaders(id, hint) {
|
|
@@ -84481,39 +84756,14 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84481
84756
|
end try
|
|
84482
84757
|
${AS_MESSAGE_DATES_FRAGMENT}
|
|
84483
84758
|
return msgDates & "${DATES_MARKER}" & msgHeaders`;
|
|
84484
|
-
const
|
|
84485
|
-
|
|
84486
|
-
|
|
84487
|
-
|
|
84488
|
-
|
|
84489
|
-
|
|
84490
|
-
|
|
84491
|
-
|
|
84492
|
-
if (result.output.startsWith(LOOKUP_ERROR_MARKER)) {
|
|
84493
|
-
this.lastMessageLookupError = result.output.slice(LOOKUP_ERROR_MARKER.length).trim();
|
|
84494
|
-
return null;
|
|
84495
|
-
}
|
|
84496
|
-
const idx = result.output.indexOf(DATES_MARKER);
|
|
84497
|
-
if (idx === -1) return null;
|
|
84498
|
-
const { dateReceived } = parseMessageDates(result.output.slice(0, idx));
|
|
84499
|
-
const raw = result.output.slice(idx + DATES_MARKER.length);
|
|
84500
|
-
if (!raw.trim()) return null;
|
|
84501
|
-
return { raw, ...dateReceived ? { dateReceived } : {} };
|
|
84502
|
-
};
|
|
84503
|
-
const loc = hint?.account && hint?.mailbox ? { account: hint.account, mailbox: hint.mailbox } : this.idLocationIndex.get(id.toString());
|
|
84504
|
-
if (loc) {
|
|
84505
|
-
const scoped = parse4(
|
|
84506
|
-
executeAppleScript(this.scopedByIdScript(loc.account, loc.mailbox, id, innerFetch), {
|
|
84507
|
-
timeoutMs: READ_ONLY_BYID_TIMEOUT_MS
|
|
84508
|
-
})
|
|
84509
|
-
);
|
|
84510
|
-
if (scoped) return scoped;
|
|
84511
|
-
}
|
|
84512
|
-
return parse4(
|
|
84513
|
-
executeAppleScript(this.unscopedByIdScript(id, innerFetch), {
|
|
84514
|
-
timeoutMs: READ_ONLY_BYID_TIMEOUT_MS
|
|
84515
|
-
})
|
|
84516
|
-
);
|
|
84759
|
+
const output = this.resolveById(id, innerFetch, hint);
|
|
84760
|
+
if (output === null) return null;
|
|
84761
|
+
const idx = output.indexOf(DATES_MARKER);
|
|
84762
|
+
if (idx === -1) return null;
|
|
84763
|
+
const { dateReceived } = parseMessageDates(output.slice(0, idx));
|
|
84764
|
+
const raw = output.slice(idx + DATES_MARKER.length);
|
|
84765
|
+
if (!raw.trim()) return null;
|
|
84766
|
+
return { raw, ...dateReceived ? { dateReceived } : {} };
|
|
84517
84767
|
}
|
|
84518
84768
|
/**
|
|
84519
84769
|
* Get the raw MIME source of a message.
|
|
@@ -84527,38 +84777,11 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84527
84777
|
* that budget; a call still running past it is a wedged scripting bridge,
|
|
84528
84778
|
* not legitimate transfer time, so there is no case where the old 120s
|
|
84529
84779
|
* ceiling was buying a real result a shorter one would have missed.
|
|
84780
|
+
* Same resolution as getMessageContent (resolveById).
|
|
84530
84781
|
*/
|
|
84531
84782
|
getRawSource(id, hint) {
|
|
84532
84783
|
this.lastMessageLookupError = void 0;
|
|
84533
|
-
|
|
84534
|
-
if (loc) {
|
|
84535
|
-
const scopedScript = this.scopedByIdScript(
|
|
84536
|
-
loc.account,
|
|
84537
|
-
loc.mailbox,
|
|
84538
|
-
id,
|
|
84539
|
-
"return source of msg"
|
|
84540
|
-
);
|
|
84541
|
-
const scoped = executeAppleScript(scopedScript, { timeoutMs: READ_ONLY_BYID_TIMEOUT_MS });
|
|
84542
|
-
if (scoped.success && scoped.output.trim() && !scoped.output.startsWith(LOOKUP_ERROR_MARKER)) {
|
|
84543
|
-
return scoped.output;
|
|
84544
|
-
}
|
|
84545
|
-
if (scoped.success && scoped.output.startsWith(LOOKUP_ERROR_MARKER)) {
|
|
84546
|
-
this.lastMessageLookupError = scoped.output.slice(LOOKUP_ERROR_MARKER.length).trim();
|
|
84547
|
-
} else if (!scoped.success && scoped.timedOut) {
|
|
84548
|
-
this.lastMessageLookupError = timedOutResolvingId(id);
|
|
84549
|
-
}
|
|
84550
|
-
}
|
|
84551
|
-
const script = this.unscopedByIdScript(id, "return source of msg");
|
|
84552
|
-
const result = executeAppleScript(script, { timeoutMs: READ_ONLY_BYID_TIMEOUT_MS });
|
|
84553
|
-
if (!result.success || !result.output.trim()) {
|
|
84554
|
-
if (result.timedOut) this.lastMessageLookupError = timedOutResolvingId(id);
|
|
84555
|
-
return null;
|
|
84556
|
-
}
|
|
84557
|
-
if (result.output.startsWith(LOOKUP_ERROR_MARKER)) {
|
|
84558
|
-
this.lastMessageLookupError = result.output.slice(LOOKUP_ERROR_MARKER.length).trim();
|
|
84559
|
-
return null;
|
|
84560
|
-
}
|
|
84561
|
-
return result.output;
|
|
84784
|
+
return this.resolveById(id, "return source of msg", hint);
|
|
84562
84785
|
}
|
|
84563
84786
|
/**
|
|
84564
84787
|
* List messages in a mailbox.
|
|
@@ -87497,10 +87720,14 @@ function currentCallTiming() {
|
|
|
87497
87720
|
return callTiming.getStore();
|
|
87498
87721
|
}
|
|
87499
87722
|
function withErrorHandling(handler, errorPrefix) {
|
|
87500
|
-
return async (params) => {
|
|
87723
|
+
return async (params, extra) => {
|
|
87501
87724
|
const arrivedAt = Date.now();
|
|
87725
|
+
const signal = extra?.signal;
|
|
87502
87726
|
return serializeAppleScript(async () => {
|
|
87503
|
-
|
|
87727
|
+
if (signal?.aborted) {
|
|
87728
|
+
return errorResponse(`${errorPrefix}: request was cancelled before it started`);
|
|
87729
|
+
}
|
|
87730
|
+
const timing = { arrivedAt, queueWaitMs: Date.now() - arrivedAt, signal };
|
|
87504
87731
|
return callTiming.run(timing, async () => {
|
|
87505
87732
|
try {
|
|
87506
87733
|
return await handler(params);
|
|
@@ -87530,10 +87757,12 @@ async function readOriginal(deps, id, cfg) {
|
|
|
87530
87757
|
return { original, plainText: extractTextBody(source.raw) };
|
|
87531
87758
|
}
|
|
87532
87759
|
const raw = deps.mail.getRawSource(id);
|
|
87533
|
-
if (!raw)
|
|
87760
|
+
if (!raw) {
|
|
87761
|
+
const why = deps.mail.consumeLastMessageLookupError?.();
|
|
87534
87762
|
throw new Error(
|
|
87535
|
-
|
|
87763
|
+
`Cannot read the original message source${why ? ` (${why})` : ""}. Re-list the intended mailbox and retry with its message id.`
|
|
87536
87764
|
);
|
|
87765
|
+
}
|
|
87537
87766
|
const content = deps.mail.getMessageContent(id);
|
|
87538
87767
|
return { original: parseOriginalHeaders(raw), plainText: content?.plainText ?? null };
|
|
87539
87768
|
}
|
|
@@ -87759,6 +87988,9 @@ function unlistableStoreError(account, error3, knownAccounts) {
|
|
|
87759
87988
|
return parts.join("\n\n");
|
|
87760
87989
|
}
|
|
87761
87990
|
|
|
87991
|
+
// src/index.ts
|
|
87992
|
+
init_callStop();
|
|
87993
|
+
|
|
87762
87994
|
// src/tools/batchResults.ts
|
|
87763
87995
|
async function hybridBatchCounts(ids, appleFn, imapFn) {
|
|
87764
87996
|
const distinctIds = [...new Set(ids)];
|
|
@@ -87937,12 +88169,17 @@ function mergeMessages(imapRows, appleRows, limit) {
|
|
|
87937
88169
|
}
|
|
87938
88170
|
async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapConfigs()) {
|
|
87939
88171
|
const rows = [];
|
|
88172
|
+
const timedOutMailboxes = [];
|
|
87940
88173
|
const accountsQueried = [];
|
|
87941
88174
|
const accountsFailed = [];
|
|
87942
88175
|
const failedMailboxes = [];
|
|
87943
88176
|
const failedMailboxReasons = {};
|
|
87944
88177
|
const omittedMessages = [];
|
|
87945
88178
|
for (const config2 of configs) {
|
|
88179
|
+
if (deps.signal?.aborted) {
|
|
88180
|
+
timedOutMailboxes.push(`${config2.accountLabel} / *`);
|
|
88181
|
+
continue;
|
|
88182
|
+
}
|
|
87946
88183
|
const perAccountArgs = { ...args, account: void 0 };
|
|
87947
88184
|
try {
|
|
87948
88185
|
const res = kind === "search" ? await imapSearchMessages(perAccountArgs, { ...deps, config: config2 }) : await imapListMessages(perAccountArgs, { ...deps, config: config2 });
|
|
@@ -87954,6 +88191,9 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
|
|
|
87954
88191
|
for (const [mailbox, reason] of Object.entries(res.failedMailboxReasons)) {
|
|
87955
88192
|
failedMailboxReasons[`${config2.accountLabel} / ${mailbox}`] = reason;
|
|
87956
88193
|
}
|
|
88194
|
+
timedOutMailboxes.push(
|
|
88195
|
+
...(res.timedOutMailboxes ?? []).map((mailbox) => `${config2.accountLabel} / ${mailbox}`)
|
|
88196
|
+
);
|
|
87957
88197
|
omittedMessages.push(
|
|
87958
88198
|
...(res.omittedMessages ?? []).map((o) => ({
|
|
87959
88199
|
...o,
|
|
@@ -87971,7 +88211,8 @@ async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapCo
|
|
|
87971
88211
|
accountsFailed,
|
|
87972
88212
|
failedMailboxes,
|
|
87973
88213
|
failedMailboxReasons,
|
|
87974
|
-
omittedMessages
|
|
88214
|
+
omittedMessages,
|
|
88215
|
+
timedOutMailboxes
|
|
87975
88216
|
};
|
|
87976
88217
|
}
|
|
87977
88218
|
function configMatchesAccount(config2, account) {
|
|
@@ -88589,7 +88830,11 @@ var LIST_OUTPUT_SCHEMA = {
|
|
|
88589
88830
|
failedMailboxReasons: external_exports.record(external_exports.string(), external_exports.string()).optional(),
|
|
88590
88831
|
// Messages that belong on the page but could not be read, each with its
|
|
88591
88832
|
// imap: id and why (#256 follow-up). Non-empty implies `partial: true`.
|
|
88592
|
-
omittedMessages: external_exports.array(external_exports.object({ id: external_exports.string(), mailbox: external_exports.string(), uid: external_exports.number(), reason: external_exports.string() })).optional()
|
|
88833
|
+
omittedMessages: external_exports.array(external_exports.object({ id: external_exports.string(), mailbox: external_exports.string(), uid: external_exports.number(), reason: external_exports.string() })).optional(),
|
|
88834
|
+
// Mailboxes a cancelled or out-of-time search-messages never finished (#276),
|
|
88835
|
+
// and why. Non-empty implies `partial: true`.
|
|
88836
|
+
timedOutMailboxes: external_exports.array(external_exports.string()).optional(),
|
|
88837
|
+
stoppedBy: external_exports.enum(["deadline", "cancelled"]).optional()
|
|
88593
88838
|
};
|
|
88594
88839
|
var BATCH_COUNT_OUTPUT_SCHEMA = {
|
|
88595
88840
|
ok: external_exports.boolean().optional(),
|
|
@@ -88664,11 +88909,12 @@ function appleScanForAccounts(accounts, scan) {
|
|
|
88664
88909
|
}
|
|
88665
88910
|
return { rows, diagnostics };
|
|
88666
88911
|
}
|
|
88667
|
-
function mergedMessageResponse(fan, apple, limit, verb) {
|
|
88912
|
+
function mergedMessageResponse(fan, apple, limit, verb, stopSignal) {
|
|
88668
88913
|
const merged = mergeMessages(fan.rows, apple.rows, limit);
|
|
88914
|
+
const timedOutMailboxes = fan.timedOutMailboxes ?? [];
|
|
88669
88915
|
const diagnostics = {
|
|
88670
88916
|
...apple.diagnostics,
|
|
88671
|
-
partial: apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0 || fan.omittedMessages.length > 0,
|
|
88917
|
+
partial: apple.diagnostics.partial || fan.accountsFailed.length > 0 || fan.failedMailboxes.length > 0 || fan.omittedMessages.length > 0 || timedOutMailboxes.length > 0,
|
|
88672
88918
|
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
|
|
88673
88919
|
notSearchedMailboxes: [...apple.diagnostics.notSearchedMailboxes, ...fan.failedMailboxes]
|
|
88674
88920
|
};
|
|
@@ -88681,9 +88927,13 @@ function mergedMessageResponse(fan, apple, limit, verb) {
|
|
|
88681
88927
|
timedOutAccounts: diagnostics.timedOutAccounts,
|
|
88682
88928
|
failedMailboxes: fan.failedMailboxes,
|
|
88683
88929
|
failedMailboxReasons: fan.failedMailboxReasons,
|
|
88684
|
-
omittedMessages: fan.omittedMessages
|
|
88930
|
+
omittedMessages: fan.omittedMessages,
|
|
88931
|
+
...timedOutMailboxes.length > 0 ? {
|
|
88932
|
+
timedOutMailboxes,
|
|
88933
|
+
stoppedBy: stoppedByDeadline(stopSignal) ? "deadline" : "cancelled"
|
|
88934
|
+
} : {}
|
|
88685
88935
|
};
|
|
88686
|
-
const coverageBlock = partialCoverageBlock(diagnostics) + omittedNote(fan.omittedMessages, true);
|
|
88936
|
+
const coverageBlock = partialCoverageBlock(diagnostics) + omittedNote(fan.omittedMessages, true) + stoppedNote(timedOutMailboxes, stopSignal);
|
|
88687
88937
|
if (merged.length === 0) {
|
|
88688
88938
|
const base2 = diagnostics.partial ? `No messages found in the portions that were ${verb === "matched" ? "searched" : "listed"}.` : "No messages found";
|
|
88689
88939
|
return successResponse(`${base2}${coverageBlock}`, structured);
|
|
@@ -88730,7 +88980,7 @@ function collectForensics(tool, args) {
|
|
|
88730
88980
|
registerTool(
|
|
88731
88981
|
"search-messages",
|
|
88732
88982
|
{
|
|
88733
|
-
description: "Use when: finding messages by query/sender/subject/date/read/flag filters and you need their ids for follow-up operations.\nReturns: matching messages with id, date, subject, sender, and read state (plus partial-coverage diagnostics when some mailboxes were skipped).\nDo not use when: you want a plain mailbox listing without filters (use list-messages), already have an id and want the body (use get-message), or want a whole conversation (use get-thread).\nPrefer this first to obtain the message ids that get-message/mark-as-read/delete-message/move-message and the batch tools require.",
|
|
88983
|
+
description: "Use when: finding messages by query/sender/subject/date/read/flag filters and you need their ids for follow-up operations.\nReturns: matching messages with id, date, subject, sender, and read state (plus partial-coverage diagnostics when some mailboxes were skipped). An IMAP search stops at its deadline (APPLE_MAIL_MCP_SEARCH_DEADLINE_MS, default 45s) or when the request is cancelled, returning the matches found so far with partial: true and timedOutMailboxes \u2014 narrow with mailbox/dateFrom rather than repeating it.\nDo not use when: you want a plain mailbox listing without filters (use list-messages), already have an id and want the body (use get-message), or want a whole conversation (use get-thread).\nPrefer this first to obtain the message ids that get-message/mark-as-read/delete-message/move-message and the batch tools require.",
|
|
88734
88984
|
inputSchema: {
|
|
88735
88985
|
query: external_exports.string().optional().describe("Text to search for in subject or sender"),
|
|
88736
88986
|
body: external_exports.string().optional().describe(
|
|
@@ -88773,6 +89023,15 @@ registerTool(
|
|
|
88773
89023
|
if (tooDeep) return errorResponse(tooDeep);
|
|
88774
89024
|
}
|
|
88775
89025
|
if (shouldUseImap(account)) {
|
|
89026
|
+
const stop = createCallStop(currentCallTiming(), searchDeadlineMs());
|
|
89027
|
+
try {
|
|
89028
|
+
return await searchViaImap(stop.signal);
|
|
89029
|
+
} finally {
|
|
89030
|
+
stop.dispose();
|
|
89031
|
+
}
|
|
89032
|
+
}
|
|
89033
|
+
return searchViaAppleScript();
|
|
89034
|
+
async function searchViaImap(signal) {
|
|
88776
89035
|
const imapArgs = {
|
|
88777
89036
|
query,
|
|
88778
89037
|
body,
|
|
@@ -88787,14 +89046,15 @@ registerTool(
|
|
|
88787
89046
|
isFlagged
|
|
88788
89047
|
};
|
|
88789
89048
|
if (account !== void 0) {
|
|
88790
|
-
const r = await imapSearchMessages({ ...imapArgs, account });
|
|
89049
|
+
const r = await imapSearchMessages({ ...imapArgs, account }, { signal });
|
|
88791
89050
|
return successResponse(r.text, {
|
|
88792
89051
|
messages: r.messages,
|
|
88793
89052
|
count: r.count,
|
|
88794
89053
|
partial: r.partial,
|
|
88795
89054
|
failedMailboxes: r.failedMailboxes,
|
|
88796
89055
|
failedMailboxReasons: r.failedMailboxReasons,
|
|
88797
|
-
omittedMessages: r.omittedMessages
|
|
89056
|
+
omittedMessages: r.omittedMessages,
|
|
89057
|
+
...r.timedOutMailboxes?.length ? { timedOutMailboxes: r.timedOutMailboxes, stoppedBy: r.stoppedBy } : {}
|
|
88798
89058
|
});
|
|
88799
89059
|
}
|
|
88800
89060
|
const { appleScriptOnly } = partitionAccountsForCounts(
|
|
@@ -88806,7 +89066,7 @@ registerTool(
|
|
|
88806
89066
|
`offset is supported only on IMAP accounts, and ${appleScriptOnly.map((a) => `"${a.name}"`).join(", ")} ${appleScriptOnly.length === 1 ? "is" : "are"} AppleScript-only. Pass an IMAP account to page, or omit offset.`
|
|
88807
89067
|
);
|
|
88808
89068
|
}
|
|
88809
|
-
const fan = await fanOutImapMessages(imapArgs, "search");
|
|
89069
|
+
const fan = await fanOutImapMessages(imapArgs, "search", { signal });
|
|
88810
89070
|
if (body) {
|
|
88811
89071
|
const apple2 = {
|
|
88812
89072
|
rows: [],
|
|
@@ -88818,7 +89078,18 @@ registerTool(
|
|
|
88818
89078
|
)
|
|
88819
89079
|
}
|
|
88820
89080
|
};
|
|
88821
|
-
return mergedMessageResponse(fan, apple2, limit, "matched");
|
|
89081
|
+
return mergedMessageResponse(fan, apple2, limit, "matched", signal);
|
|
89082
|
+
}
|
|
89083
|
+
if (signal.aborted && appleScriptOnly.length > 0) {
|
|
89084
|
+
const apple2 = {
|
|
89085
|
+
rows: [],
|
|
89086
|
+
diagnostics: {
|
|
89087
|
+
...emptyDiagnostics(),
|
|
89088
|
+
partial: true,
|
|
89089
|
+
timedOutAccounts: appleScriptOnly.map((a) => a.name)
|
|
89090
|
+
}
|
|
89091
|
+
};
|
|
89092
|
+
return mergedMessageResponse(fan, apple2, limit, "matched", signal);
|
|
88822
89093
|
}
|
|
88823
89094
|
const apple = appleScanForAccounts(
|
|
88824
89095
|
appleScriptOnly,
|
|
@@ -88835,51 +89106,53 @@ registerTool(
|
|
|
88835
89106
|
isFlagged
|
|
88836
89107
|
)
|
|
88837
89108
|
);
|
|
88838
|
-
return mergedMessageResponse(fan, apple, limit, "matched");
|
|
89109
|
+
return mergedMessageResponse(fan, apple, limit, "matched", signal);
|
|
88839
89110
|
}
|
|
88840
|
-
|
|
88841
|
-
|
|
88842
|
-
|
|
89111
|
+
function searchViaAppleScript() {
|
|
89112
|
+
if (offset > 0) {
|
|
89113
|
+
return errorResponse(
|
|
89114
|
+
`offset is supported only on IMAP accounts; IMAP is not configured for ${account ? `account "${account}"` : "any account"}. Omit offset, or narrow the search with dateFrom/dateTo instead.`
|
|
89115
|
+
);
|
|
89116
|
+
}
|
|
89117
|
+
if (body) {
|
|
89118
|
+
return errorResponse(
|
|
89119
|
+
`Body search requires the IMAP backend, which is not configured for ${account ? `account "${account}"` : "any account"}. Configure IMAP (see the IMAP backend section of the README) or search by query/subject/from instead.`
|
|
89120
|
+
);
|
|
89121
|
+
}
|
|
89122
|
+
const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(
|
|
89123
|
+
query,
|
|
89124
|
+
mailbox,
|
|
89125
|
+
account,
|
|
89126
|
+
limit,
|
|
89127
|
+
dateFrom,
|
|
89128
|
+
dateTo,
|
|
89129
|
+
from,
|
|
89130
|
+
subject,
|
|
89131
|
+
isRead,
|
|
89132
|
+
isFlagged
|
|
88843
89133
|
);
|
|
88844
|
-
|
|
88845
|
-
|
|
88846
|
-
|
|
88847
|
-
|
|
89134
|
+
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
89135
|
+
const structured = {
|
|
89136
|
+
messages: messages.map(messageSummary),
|
|
89137
|
+
count: messages.length,
|
|
89138
|
+
partial: diagnostics.partial,
|
|
89139
|
+
skippedLargeMailboxes: diagnostics.skippedLargeMailboxes,
|
|
89140
|
+
notSearchedMailboxes: diagnostics.notSearchedMailboxes,
|
|
89141
|
+
timedOutAccounts: diagnostics.timedOutAccounts
|
|
89142
|
+
};
|
|
89143
|
+
if (messages.length === 0) {
|
|
89144
|
+
const base2 = diagnostics.partial ? "No messages found in the portions that were searched." : "No messages found matching criteria";
|
|
89145
|
+
return successResponse(`${base2}${coverageBlock}`, structured);
|
|
89146
|
+
}
|
|
89147
|
+
const messageList = messages.map(
|
|
89148
|
+
(m) => ` - ID: ${m.id} | ${m.dateReceived.toLocaleDateString()} | ${m.subject} (from: ${m.sender}) [${m.isRead ? "read" : "unread"}]`
|
|
89149
|
+
).join("\n");
|
|
89150
|
+
return successResponse(
|
|
89151
|
+
`Found ${messages.length} message(s):
|
|
89152
|
+
${messageList}${coverageBlock}`,
|
|
89153
|
+
structured
|
|
88848
89154
|
);
|
|
88849
89155
|
}
|
|
88850
|
-
const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(
|
|
88851
|
-
query,
|
|
88852
|
-
mailbox,
|
|
88853
|
-
account,
|
|
88854
|
-
limit,
|
|
88855
|
-
dateFrom,
|
|
88856
|
-
dateTo,
|
|
88857
|
-
from,
|
|
88858
|
-
subject,
|
|
88859
|
-
isRead,
|
|
88860
|
-
isFlagged
|
|
88861
|
-
);
|
|
88862
|
-
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
88863
|
-
const structured = {
|
|
88864
|
-
messages: messages.map(messageSummary),
|
|
88865
|
-
count: messages.length,
|
|
88866
|
-
partial: diagnostics.partial,
|
|
88867
|
-
skippedLargeMailboxes: diagnostics.skippedLargeMailboxes,
|
|
88868
|
-
notSearchedMailboxes: diagnostics.notSearchedMailboxes,
|
|
88869
|
-
timedOutAccounts: diagnostics.timedOutAccounts
|
|
88870
|
-
};
|
|
88871
|
-
if (messages.length === 0) {
|
|
88872
|
-
const base2 = diagnostics.partial ? "No messages found in the portions that were searched." : "No messages found matching criteria";
|
|
88873
|
-
return successResponse(`${base2}${coverageBlock}`, structured);
|
|
88874
|
-
}
|
|
88875
|
-
const messageList = messages.map(
|
|
88876
|
-
(m) => ` - ID: ${m.id} | ${m.dateReceived.toLocaleDateString()} | ${m.subject} (from: ${m.sender}) [${m.isRead ? "read" : "unread"}]`
|
|
88877
|
-
).join("\n");
|
|
88878
|
-
return successResponse(
|
|
88879
|
-
`Found ${messages.length} message(s):
|
|
88880
|
-
${messageList}${coverageBlock}`,
|
|
88881
|
-
structured
|
|
88882
|
-
);
|
|
88883
89156
|
},
|
|
88884
89157
|
"Error searching messages"
|
|
88885
89158
|
)
|