apple-mail-mcp 2.20.7 → 2.20.8
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 +8 -1
- package/build/index.js +382 -134
- package/docs/IMAP-SETUP.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -331,7 +331,7 @@ Get the full content of a message.
|
|
|
331
331
|
|
|
332
332
|
**Returns:** Subject line and message body (plain text by default, HTML if `preferHtml` is true and HTML content is available). `structuredContent` also carries `rfcMessageId` and, since 2.19.0, two dates: `dateSent` (the message's `Date:` header — Mail's `date sent`) and `dateReceived` (arrival in the mailbox — Mail's `date received` / IMAP `INTERNALDATE`). They differ legitimately by transit time; when they differ by **years**, the mailbox was migrated or re-imported and the arrival timestamp was reset — trust `dateSent` for chronology ([#224](https://github.com/sweetrb/apple-mail-mcp/issues/224)). Since 2.19.6 `dateSent` is **omitted** when it is more than 7 days later than `dateReceived`: a message cannot be sent after it arrived, and Mail.app substitutes a timestamp of its own for a `Date:` header it cannot parse ([#234](https://github.com/sweetrb/apple-mail-mcp/issues/234)). `isHtml` reports what was actually returned — a message with no `text/plain` part returns its HTML part with `isHtml: true`. Bodies are decoded by each part's declared `charset`, falling back to windows-1252 for bytes that are not valid UTF-8.
|
|
333
333
|
|
|
334
|
-
**Numeric ids — how the message is found ([#270](https://github.com/sweetrb/apple-mail-mcp/issues/270)):** with `account` + `mailbox` the read opens that one mailbox and stops there. Otherwise it opens the mailbox the id was last listed or searched from (a remembered location that turns out stale — the message was moved, e.g. by another client — is forgotten), and only then scans every mailbox. That cross-mailbox scan skips any mailbox holding more than `APPLE_MAIL_MAX_BYID_SCAN_MAILBOX` messages (default `50000`; `0` disables the guard) and stops after ~9s, because Mail's `whose id is` probe costs seconds per hundred thousand messages; a miss then names the mailboxes it
|
|
334
|
+
**Numeric ids — how the message is found ([#270](https://github.com/sweetrb/apple-mail-mcp/issues/270)):** with `account` + `mailbox` the read opens that one mailbox and stops there. Otherwise it opens the mailbox the id was last listed or searched from (a remembered location that turns out stale — the message was moved, e.g. by another client — is forgotten), and only then scans every mailbox. That cross-mailbox scan skips any mailbox holding more than `APPLE_MAIL_MAX_BYID_SCAN_MAILBOX` messages (default `50000`; `0` disables the guard) and stops after ~9s, because Mail's `whose id is` probe costs seconds per hundred thousand messages; a miss then names the mailboxes it skipped and, if it stopped early, the ones it never reached. A mailbox whose size count does not answer within 3s is remembered — on disk, for 7 days, across restarts (`APPLE_MAIL_MCP_STALLED_MAILBOXES_FILE`, default `~/Library/Application Support/apple-mail-mcp/stalled-mailboxes.json`; delete it to forget) — and never counted again, since Mail.app keeps working on an abandoned count and everything after it would queue; the scan stops the moment such a count times out rather than queue behind it. If Mail.app is not running these reads say so instead of launching it. Pass `account` + `mailbox` to read from one of those, or use the message's `imap:` id, which never scans. The same resolution backs `get-message-headers`, `get-thread`'s seed lookup, and the original-message read of `reply-to-message` / `forward-message` and the attachment tools.
|
|
335
335
|
|
|
336
336
|
> **Large messages / attachments:** reading a full message routes through
|
|
337
337
|
> `osascript`, whose captured output buffer defaults to **64 MB**. Override it
|
|
@@ -2147,6 +2147,13 @@ In a JSON string literal, `\\` — two characters — denotes **one** literal ba
|
|
|
2147
2147
|
skipped those because they hold more than `APPLE_MAIL_MAX_BYID_SCAN_MAILBOX` messages (default
|
|
2148
2148
|
`50000`). The message may be in one of them: pass `account` + `mailbox` to read it from there, or
|
|
2149
2149
|
use its `imap:` id ([#270](https://github.com/sweetrb/apple-mail-mcp/issues/270)).
|
|
2150
|
+
- "**stopped … before reaching: …**" lists the mailboxes the scan never got to (its ~9s budget ran
|
|
2151
|
+
out, or a mailbox's size count timed out and Mail.app was left busy finishing it). A timed-out
|
|
2152
|
+
mailbox is remembered for 7 days and skipped from then on, so retrying shortly usually covers the
|
|
2153
|
+
rest. "**Mail.app did not answer within 3s**" means Mail.app was still busy with such a count when
|
|
2154
|
+
the lookup started — retry in a little while.
|
|
2155
|
+
- "**Mail.app is not running**" — numeric-id reads need Mail.app open; the server never launches it
|
|
2156
|
+
for them. `imap:` ids read over IMAP without Mail.app.
|
|
2150
2157
|
|
|
2151
2158
|
### "... is present in more than one mailbox"
|
|
2152
2159
|
|
package/build/index.js
CHANGED
|
@@ -7739,8 +7739,8 @@ var init_attachmentLimits = __esm({
|
|
|
7739
7739
|
|
|
7740
7740
|
// src/utils/attachmentReadPolicy.ts
|
|
7741
7741
|
import { realpathSync, statSync } from "fs";
|
|
7742
|
-
import { homedir as
|
|
7743
|
-
import { delimiter, isAbsolute, join as
|
|
7742
|
+
import { homedir as homedir3, tmpdir } from "os";
|
|
7743
|
+
import { delimiter, isAbsolute, join as join3, resolve, sep } from "path";
|
|
7744
7744
|
function canonicalize(path3) {
|
|
7745
7745
|
return realpathSync.native(path3);
|
|
7746
7746
|
}
|
|
@@ -7764,9 +7764,9 @@ function isProtectedPath(candidate) {
|
|
|
7764
7764
|
if (sensitiveRoots().some((root) => isWithinRoot(candidate, root))) return true;
|
|
7765
7765
|
let home;
|
|
7766
7766
|
try {
|
|
7767
|
-
home = canonicalize(
|
|
7767
|
+
home = canonicalize(homedir3());
|
|
7768
7768
|
} catch {
|
|
7769
|
-
home = resolve(
|
|
7769
|
+
home = resolve(homedir3());
|
|
7770
7770
|
}
|
|
7771
7771
|
if (!isWithinRoot(candidate, home)) return false;
|
|
7772
7772
|
const relative = candidate.slice(home.length).split(sep).filter(Boolean);
|
|
@@ -7826,12 +7826,12 @@ var init_attachmentReadPolicy = __esm({
|
|
|
7826
7826
|
"src/utils/attachmentReadPolicy.ts"() {
|
|
7827
7827
|
"use strict";
|
|
7828
7828
|
ATTACHMENT_READ_ROOTS_ENV = "APPLE_MAIL_MCP_ATTACHMENT_READ_ROOTS";
|
|
7829
|
-
DEFAULT_ATTACHMENT_READ_ROOTS = [
|
|
7829
|
+
DEFAULT_ATTACHMENT_READ_ROOTS = [homedir3(), "/Volumes", tmpdir(), "/tmp", "/private/tmp"];
|
|
7830
7830
|
SENSITIVE_HOME_ROOTS = [
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7831
|
+
join3(homedir3(), ".ssh"),
|
|
7832
|
+
join3(homedir3(), ".aws"),
|
|
7833
|
+
join3(homedir3(), ".config", "gh"),
|
|
7834
|
+
join3(homedir3(), "Library", "Keychains")
|
|
7835
7835
|
];
|
|
7836
7836
|
}
|
|
7837
7837
|
});
|
|
@@ -22061,7 +22061,7 @@ var require_thread_stream = __commonJS({
|
|
|
22061
22061
|
var { version: version4 } = require_package();
|
|
22062
22062
|
var { EventEmitter: EventEmitter7 } = __require("events");
|
|
22063
22063
|
var { Worker } = __require("worker_threads");
|
|
22064
|
-
var { join:
|
|
22064
|
+
var { join: join8 } = __require("path");
|
|
22065
22065
|
var { pathToFileURL } = __require("url");
|
|
22066
22066
|
var { wait } = require_wait();
|
|
22067
22067
|
var {
|
|
@@ -22112,7 +22112,7 @@ var require_thread_stream = __commonJS({
|
|
|
22112
22112
|
function createWorker(stream, opts) {
|
|
22113
22113
|
const { filename, workerData } = opts;
|
|
22114
22114
|
const bundlerOverrides = "__bundlerPathsOverrides" in globalThis ? globalThis.__bundlerPathsOverrides : {};
|
|
22115
|
-
const toExecute = bundlerOverrides["thread-stream-worker"] ||
|
|
22115
|
+
const toExecute = bundlerOverrides["thread-stream-worker"] || join8(__dirname, "lib", "worker.js");
|
|
22116
22116
|
const worker = new Worker(toExecute, {
|
|
22117
22117
|
...opts.workerOpts,
|
|
22118
22118
|
name: opts.workerOpts?.name || "thread-stream",
|
|
@@ -22580,7 +22580,7 @@ var require_transport = __commonJS({
|
|
|
22580
22580
|
var { createRequire: createRequire2 } = __require("module");
|
|
22581
22581
|
var { existsSync: existsSync5 } = __require("node:fs");
|
|
22582
22582
|
var getCallers = require_caller();
|
|
22583
|
-
var { join:
|
|
22583
|
+
var { join: join8, isAbsolute: isAbsolute2, sep: sep3 } = __require("node:path");
|
|
22584
22584
|
var { fileURLToPath } = __require("node:url");
|
|
22585
22585
|
var sleep2 = require_atomic_sleep();
|
|
22586
22586
|
var onExit = require_on_exit_leak_free();
|
|
@@ -22733,7 +22733,7 @@ var require_transport = __commonJS({
|
|
|
22733
22733
|
throw new Error("only one of target or targets can be specified");
|
|
22734
22734
|
}
|
|
22735
22735
|
if (targets) {
|
|
22736
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
22736
|
+
target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
|
|
22737
22737
|
options.targets = targets.filter((dest) => dest.target).map((dest) => {
|
|
22738
22738
|
return {
|
|
22739
22739
|
...dest,
|
|
@@ -22751,7 +22751,7 @@ var require_transport = __commonJS({
|
|
|
22751
22751
|
});
|
|
22752
22752
|
});
|
|
22753
22753
|
} else if (pipeline) {
|
|
22754
|
-
target = bundlerOverrides["pino-worker"] ||
|
|
22754
|
+
target = bundlerOverrides["pino-worker"] || join8(__dirname, "worker.js");
|
|
22755
22755
|
options.pipelines = [pipeline.map((dest) => {
|
|
22756
22756
|
return {
|
|
22757
22757
|
...dest,
|
|
@@ -22774,7 +22774,7 @@ var require_transport = __commonJS({
|
|
|
22774
22774
|
return origin;
|
|
22775
22775
|
}
|
|
22776
22776
|
if (origin === "pino/file") {
|
|
22777
|
-
return
|
|
22777
|
+
return join8(__dirname, "..", "file.js");
|
|
22778
22778
|
}
|
|
22779
22779
|
let fixTarget2;
|
|
22780
22780
|
for (const filePath of callers) {
|
|
@@ -23754,7 +23754,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
23754
23754
|
return circularValue;
|
|
23755
23755
|
}
|
|
23756
23756
|
let res = "";
|
|
23757
|
-
let
|
|
23757
|
+
let join8 = ",";
|
|
23758
23758
|
const originalIndentation = indentation;
|
|
23759
23759
|
if (Array.isArray(value)) {
|
|
23760
23760
|
if (value.length === 0) {
|
|
@@ -23768,7 +23768,7 @@ var require_safe_stable_stringify = __commonJS({
|
|
|
23768
23768
|
indentation += spacer;
|
|
23769
23769
|
res += `
|
|
23770
23770
|
${indentation}`;
|
|
23771
|
-
|
|
23771
|
+
join8 = `,
|
|
23772
23772
|
${indentation}`;
|
|
23773
23773
|
}
|
|
23774
23774
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -23776,13 +23776,13 @@ ${indentation}`;
|
|
|
23776
23776
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
23777
23777
|
const tmp2 = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
23778
23778
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
23779
|
-
res +=
|
|
23779
|
+
res += join8;
|
|
23780
23780
|
}
|
|
23781
23781
|
const tmp = stringifyFnReplacer(String(i), value, stack, replacer, spacer, indentation);
|
|
23782
23782
|
res += tmp !== void 0 ? tmp : "null";
|
|
23783
23783
|
if (value.length - 1 > maximumBreadth) {
|
|
23784
23784
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
23785
|
-
res += `${
|
|
23785
|
+
res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
23786
23786
|
}
|
|
23787
23787
|
if (spacer !== "") {
|
|
23788
23788
|
res += `
|
|
@@ -23803,7 +23803,7 @@ ${originalIndentation}`;
|
|
|
23803
23803
|
let separator = "";
|
|
23804
23804
|
if (spacer !== "") {
|
|
23805
23805
|
indentation += spacer;
|
|
23806
|
-
|
|
23806
|
+
join8 = `,
|
|
23807
23807
|
${indentation}`;
|
|
23808
23808
|
whitespace = " ";
|
|
23809
23809
|
}
|
|
@@ -23817,13 +23817,13 @@ ${indentation}`;
|
|
|
23817
23817
|
const tmp = stringifyFnReplacer(key2, value, stack, replacer, spacer, indentation);
|
|
23818
23818
|
if (tmp !== void 0) {
|
|
23819
23819
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
23820
|
-
separator =
|
|
23820
|
+
separator = join8;
|
|
23821
23821
|
}
|
|
23822
23822
|
}
|
|
23823
23823
|
if (keyLength > maximumBreadth) {
|
|
23824
23824
|
const removedKeys = keyLength - maximumBreadth;
|
|
23825
23825
|
res += `${separator}"...":${whitespace}"${getItemCount(removedKeys)} not stringified"`;
|
|
23826
|
-
separator =
|
|
23826
|
+
separator = join8;
|
|
23827
23827
|
}
|
|
23828
23828
|
if (spacer !== "" && separator.length > 1) {
|
|
23829
23829
|
res = `
|
|
@@ -23864,7 +23864,7 @@ ${originalIndentation}`;
|
|
|
23864
23864
|
}
|
|
23865
23865
|
const originalIndentation = indentation;
|
|
23866
23866
|
let res = "";
|
|
23867
|
-
let
|
|
23867
|
+
let join8 = ",";
|
|
23868
23868
|
if (Array.isArray(value)) {
|
|
23869
23869
|
if (value.length === 0) {
|
|
23870
23870
|
return "[]";
|
|
@@ -23877,7 +23877,7 @@ ${originalIndentation}`;
|
|
|
23877
23877
|
indentation += spacer;
|
|
23878
23878
|
res += `
|
|
23879
23879
|
${indentation}`;
|
|
23880
|
-
|
|
23880
|
+
join8 = `,
|
|
23881
23881
|
${indentation}`;
|
|
23882
23882
|
}
|
|
23883
23883
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
@@ -23885,13 +23885,13 @@ ${indentation}`;
|
|
|
23885
23885
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
23886
23886
|
const tmp2 = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
23887
23887
|
res += tmp2 !== void 0 ? tmp2 : "null";
|
|
23888
|
-
res +=
|
|
23888
|
+
res += join8;
|
|
23889
23889
|
}
|
|
23890
23890
|
const tmp = stringifyArrayReplacer(String(i), value[i], stack, replacer, spacer, indentation);
|
|
23891
23891
|
res += tmp !== void 0 ? tmp : "null";
|
|
23892
23892
|
if (value.length - 1 > maximumBreadth) {
|
|
23893
23893
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
23894
|
-
res += `${
|
|
23894
|
+
res += `${join8}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
23895
23895
|
}
|
|
23896
23896
|
if (spacer !== "") {
|
|
23897
23897
|
res += `
|
|
@@ -23904,7 +23904,7 @@ ${originalIndentation}`;
|
|
|
23904
23904
|
let whitespace = "";
|
|
23905
23905
|
if (spacer !== "") {
|
|
23906
23906
|
indentation += spacer;
|
|
23907
|
-
|
|
23907
|
+
join8 = `,
|
|
23908
23908
|
${indentation}`;
|
|
23909
23909
|
whitespace = " ";
|
|
23910
23910
|
}
|
|
@@ -23913,7 +23913,7 @@ ${indentation}`;
|
|
|
23913
23913
|
const tmp = stringifyArrayReplacer(key2, value[key2], stack, replacer, spacer, indentation);
|
|
23914
23914
|
if (tmp !== void 0) {
|
|
23915
23915
|
res += `${separator}${strEscape(key2)}:${whitespace}${tmp}`;
|
|
23916
|
-
separator =
|
|
23916
|
+
separator = join8;
|
|
23917
23917
|
}
|
|
23918
23918
|
}
|
|
23919
23919
|
if (spacer !== "" && separator.length > 1) {
|
|
@@ -23971,20 +23971,20 @@ ${originalIndentation}`;
|
|
|
23971
23971
|
indentation += spacer;
|
|
23972
23972
|
let res2 = `
|
|
23973
23973
|
${indentation}`;
|
|
23974
|
-
const
|
|
23974
|
+
const join9 = `,
|
|
23975
23975
|
${indentation}`;
|
|
23976
23976
|
const maximumValuesToStringify = Math.min(value.length, maximumBreadth);
|
|
23977
23977
|
let i = 0;
|
|
23978
23978
|
for (; i < maximumValuesToStringify - 1; i++) {
|
|
23979
23979
|
const tmp2 = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
23980
23980
|
res2 += tmp2 !== void 0 ? tmp2 : "null";
|
|
23981
|
-
res2 +=
|
|
23981
|
+
res2 += join9;
|
|
23982
23982
|
}
|
|
23983
23983
|
const tmp = stringifyIndent(String(i), value[i], stack, spacer, indentation);
|
|
23984
23984
|
res2 += tmp !== void 0 ? tmp : "null";
|
|
23985
23985
|
if (value.length - 1 > maximumBreadth) {
|
|
23986
23986
|
const removedKeys = value.length - maximumBreadth - 1;
|
|
23987
|
-
res2 += `${
|
|
23987
|
+
res2 += `${join9}"... ${getItemCount(removedKeys)} not stringified"`;
|
|
23988
23988
|
}
|
|
23989
23989
|
res2 += `
|
|
23990
23990
|
${originalIndentation}`;
|
|
@@ -24000,16 +24000,16 @@ ${originalIndentation}`;
|
|
|
24000
24000
|
return '"[Object]"';
|
|
24001
24001
|
}
|
|
24002
24002
|
indentation += spacer;
|
|
24003
|
-
const
|
|
24003
|
+
const join8 = `,
|
|
24004
24004
|
${indentation}`;
|
|
24005
24005
|
let res = "";
|
|
24006
24006
|
let separator = "";
|
|
24007
24007
|
let maximumPropertiesToStringify = Math.min(keyLength, maximumBreadth);
|
|
24008
24008
|
if (isTypedArrayWithEntries(value)) {
|
|
24009
|
-
res += stringifyTypedArray(value,
|
|
24009
|
+
res += stringifyTypedArray(value, join8, maximumBreadth);
|
|
24010
24010
|
keys = keys.slice(value.length);
|
|
24011
24011
|
maximumPropertiesToStringify -= value.length;
|
|
24012
|
-
separator =
|
|
24012
|
+
separator = join8;
|
|
24013
24013
|
}
|
|
24014
24014
|
if (deterministic) {
|
|
24015
24015
|
keys = sort(keys, comparator);
|
|
@@ -24020,13 +24020,13 @@ ${indentation}`;
|
|
|
24020
24020
|
const tmp = stringifyIndent(key2, value[key2], stack, spacer, indentation);
|
|
24021
24021
|
if (tmp !== void 0) {
|
|
24022
24022
|
res += `${separator}${strEscape(key2)}: ${tmp}`;
|
|
24023
|
-
separator =
|
|
24023
|
+
separator = join8;
|
|
24024
24024
|
}
|
|
24025
24025
|
}
|
|
24026
24026
|
if (keyLength > maximumBreadth) {
|
|
24027
24027
|
const removedKeys = keyLength - maximumBreadth;
|
|
24028
24028
|
res += `${separator}"...": "${getItemCount(removedKeys)} not stringified"`;
|
|
24029
|
-
separator =
|
|
24029
|
+
separator = join8;
|
|
24030
24030
|
}
|
|
24031
24031
|
if (separator !== "") {
|
|
24032
24032
|
res = `
|
|
@@ -82202,19 +82202,19 @@ import {
|
|
|
82202
82202
|
constants as fsConstants,
|
|
82203
82203
|
chmodSync,
|
|
82204
82204
|
existsSync as existsSync3,
|
|
82205
|
-
writeFileSync as
|
|
82206
|
-
readFileSync as
|
|
82205
|
+
writeFileSync as writeFileSync4,
|
|
82206
|
+
readFileSync as readFileSync3,
|
|
82207
82207
|
readdirSync as readdirSync2,
|
|
82208
|
-
unlinkSync,
|
|
82208
|
+
unlinkSync as unlinkSync2,
|
|
82209
82209
|
copyFileSync,
|
|
82210
|
-
renameSync,
|
|
82210
|
+
renameSync as renameSync2,
|
|
82211
82211
|
mkdtempSync as mkdtempSync2,
|
|
82212
82212
|
rmSync as rmSync2,
|
|
82213
82213
|
realpathSync as realpathSync2,
|
|
82214
82214
|
lstatSync
|
|
82215
82215
|
} from "fs";
|
|
82216
|
-
import { resolve as resolve2, sep as sep2, join as
|
|
82217
|
-
import { homedir as
|
|
82216
|
+
import { resolve as resolve2, sep as sep2, join as join6 } from "path";
|
|
82217
|
+
import { homedir as homedir5 } from "os";
|
|
82218
82218
|
import { randomUUID } from "crypto";
|
|
82219
82219
|
|
|
82220
82220
|
// src/utils/applescript.ts
|
|
@@ -82548,11 +82548,120 @@ var TemplateStore = class {
|
|
|
82548
82548
|
}
|
|
82549
82549
|
};
|
|
82550
82550
|
|
|
82551
|
+
// src/services/stalledMailboxStore.ts
|
|
82552
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
82553
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
82554
|
+
import { homedir as homedir2 } from "os";
|
|
82555
|
+
var STALLED_MAILBOX_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
82556
|
+
var STALLED_KEY_SEP = "";
|
|
82557
|
+
function defaultStalledMailboxesFile() {
|
|
82558
|
+
const env = process.env.APPLE_MAIL_MCP_STALLED_MAILBOXES_FILE;
|
|
82559
|
+
if (env && env.trim()) return env.trim();
|
|
82560
|
+
return join2(
|
|
82561
|
+
homedir2(),
|
|
82562
|
+
"Library",
|
|
82563
|
+
"Application Support",
|
|
82564
|
+
"apple-mail-mcp",
|
|
82565
|
+
"stalled-mailboxes.json"
|
|
82566
|
+
);
|
|
82567
|
+
}
|
|
82568
|
+
var StalledMailboxStore = class {
|
|
82569
|
+
fileOverride;
|
|
82570
|
+
ttlMs;
|
|
82571
|
+
now;
|
|
82572
|
+
/** key → recordedAt. Holds everything learned, even when the disk write failed. */
|
|
82573
|
+
memory = /* @__PURE__ */ new Map();
|
|
82574
|
+
constructor(opts = {}) {
|
|
82575
|
+
this.fileOverride = opts.file;
|
|
82576
|
+
this.ttlMs = opts.ttlMs ?? STALLED_MAILBOX_TTL_MS;
|
|
82577
|
+
this.now = opts.now ?? Date.now;
|
|
82578
|
+
}
|
|
82579
|
+
/** Resolved per call so an env override set after construction (tests) is honoured. */
|
|
82580
|
+
get file() {
|
|
82581
|
+
return this.fileOverride ?? defaultStalledMailboxesFile();
|
|
82582
|
+
}
|
|
82583
|
+
/**
|
|
82584
|
+
* Every live (unexpired) key, merging what is on disk — possibly written by
|
|
82585
|
+
* another server process — with what this process learned. Re-read on each
|
|
82586
|
+
* call: the file is a handful of entries and is only consulted once per
|
|
82587
|
+
* unscoped scan, which itself costs seconds.
|
|
82588
|
+
*/
|
|
82589
|
+
keys() {
|
|
82590
|
+
const merged = this.merged();
|
|
82591
|
+
return Array.from(merged.keys());
|
|
82592
|
+
}
|
|
82593
|
+
/** Record keys whose probe just timed out, and persist them. */
|
|
82594
|
+
record(keys) {
|
|
82595
|
+
const fresh = keys.filter((k) => k.includes(STALLED_KEY_SEP));
|
|
82596
|
+
if (fresh.length === 0) return;
|
|
82597
|
+
const at = this.now();
|
|
82598
|
+
for (const k of fresh) this.memory.set(k, at);
|
|
82599
|
+
this.persist(this.merged());
|
|
82600
|
+
}
|
|
82601
|
+
/** Disk ∪ memory, newest timestamp per key, expired entries dropped. */
|
|
82602
|
+
merged() {
|
|
82603
|
+
const cutoff = this.now() - this.ttlMs;
|
|
82604
|
+
const out = /* @__PURE__ */ new Map();
|
|
82605
|
+
const put = (k, at) => {
|
|
82606
|
+
if (at < cutoff) return;
|
|
82607
|
+
const prev = out.get(k);
|
|
82608
|
+
if (prev === void 0 || at > prev) out.set(k, at);
|
|
82609
|
+
};
|
|
82610
|
+
for (const [k, at] of this.readDisk()) put(k, at);
|
|
82611
|
+
for (const [k, at] of this.memory) put(k, at);
|
|
82612
|
+
for (const [k, at] of this.memory) if (at < cutoff) this.memory.delete(k);
|
|
82613
|
+
return out;
|
|
82614
|
+
}
|
|
82615
|
+
readDisk() {
|
|
82616
|
+
const out = /* @__PURE__ */ new Map();
|
|
82617
|
+
let text;
|
|
82618
|
+
try {
|
|
82619
|
+
text = readFileSync2(this.file, "utf8");
|
|
82620
|
+
} catch {
|
|
82621
|
+
return out;
|
|
82622
|
+
}
|
|
82623
|
+
try {
|
|
82624
|
+
const parsed = JSON.parse(text);
|
|
82625
|
+
const entries = parsed && Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
82626
|
+
for (const e of entries) {
|
|
82627
|
+
if (e && typeof e.account === "string" && typeof e.mailbox === "string" && e.account && e.mailbox && typeof e.recordedAt === "number" && Number.isFinite(e.recordedAt)) {
|
|
82628
|
+
out.set(`${e.account}${STALLED_KEY_SEP}${e.mailbox}`, e.recordedAt);
|
|
82629
|
+
}
|
|
82630
|
+
}
|
|
82631
|
+
} catch (err) {
|
|
82632
|
+
console.error(`Ignoring unreadable stalled-mailbox list ${this.file}: ${String(err)}`);
|
|
82633
|
+
}
|
|
82634
|
+
return out;
|
|
82635
|
+
}
|
|
82636
|
+
persist(entries) {
|
|
82637
|
+
const data = {
|
|
82638
|
+
version: 1,
|
|
82639
|
+
entries: Array.from(entries, ([k, recordedAt]) => {
|
|
82640
|
+
const i = k.indexOf(STALLED_KEY_SEP);
|
|
82641
|
+
return { account: k.slice(0, i), mailbox: k.slice(i + 1), recordedAt };
|
|
82642
|
+
})
|
|
82643
|
+
};
|
|
82644
|
+
const file = this.file;
|
|
82645
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
82646
|
+
try {
|
|
82647
|
+
mkdirSync2(dirname2(file), { recursive: true });
|
|
82648
|
+
writeFileSync2(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
82649
|
+
renameSync(tmp, file);
|
|
82650
|
+
} catch (err) {
|
|
82651
|
+
try {
|
|
82652
|
+
unlinkSync(tmp);
|
|
82653
|
+
} catch {
|
|
82654
|
+
}
|
|
82655
|
+
console.error(`Failed to persist stalled-mailbox list to ${file}: ${String(err)}`);
|
|
82656
|
+
}
|
|
82657
|
+
}
|
|
82658
|
+
};
|
|
82659
|
+
|
|
82551
82660
|
// src/utils/attachmentMaterialize.ts
|
|
82552
82661
|
init_attachmentLimits();
|
|
82553
82662
|
init_attachmentReadPolicy();
|
|
82554
|
-
import { writeFileSync as
|
|
82555
|
-
import { join as
|
|
82663
|
+
import { writeFileSync as writeFileSync3, rmSync, mkdtempSync } from "fs";
|
|
82664
|
+
import { join as join4 } from "path";
|
|
82556
82665
|
import { tmpdir as tmpdir2 } from "os";
|
|
82557
82666
|
function materializeAttachments(attachments) {
|
|
82558
82667
|
if (!attachments || attachments.length === 0) {
|
|
@@ -82566,10 +82675,10 @@ function materializeAttachments(attachments) {
|
|
|
82566
82675
|
if (!a.filename || !a.contentBase64) {
|
|
82567
82676
|
throw new Error("Inline attachment requires both filename and contentBase64.");
|
|
82568
82677
|
}
|
|
82569
|
-
if (!dir) dir = mkdtempSync(
|
|
82678
|
+
if (!dir) dir = mkdtempSync(join4(tmpdir2(), "amcp-att-"));
|
|
82570
82679
|
const safeName = a.filename.replace(/[/\\]/g, "_");
|
|
82571
|
-
const p =
|
|
82572
|
-
|
|
82680
|
+
const p = join4(dir, safeName);
|
|
82681
|
+
writeFileSync3(p, decodeInlineAttachment(a.contentBase64), { mode: 384 });
|
|
82573
82682
|
return p;
|
|
82574
82683
|
});
|
|
82575
82684
|
} catch (error3) {
|
|
@@ -82601,8 +82710,8 @@ init_attachmentReadPolicy();
|
|
|
82601
82710
|
|
|
82602
82711
|
// src/utils/contactsDb.ts
|
|
82603
82712
|
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
82604
|
-
import { join as
|
|
82605
|
-
import { homedir as
|
|
82713
|
+
import { join as join5 } from "path";
|
|
82714
|
+
import { homedir as homedir4 } from "os";
|
|
82606
82715
|
function loadSqlite() {
|
|
82607
82716
|
try {
|
|
82608
82717
|
const mod = __require("node:sqlite");
|
|
@@ -82621,11 +82730,11 @@ function loadSqlite() {
|
|
|
82621
82730
|
}
|
|
82622
82731
|
}
|
|
82623
82732
|
function resolveContactsDbPaths(baseDir) {
|
|
82624
|
-
const root = baseDir ??
|
|
82733
|
+
const root = baseDir ?? join5(homedir4(), "Library", "Application Support", "AddressBook");
|
|
82625
82734
|
const paths = [];
|
|
82626
|
-
const topLevel =
|
|
82735
|
+
const topLevel = join5(root, "AddressBook-v22.abcddb");
|
|
82627
82736
|
if (existsSync2(topLevel)) paths.push(topLevel);
|
|
82628
|
-
const sourcesDir =
|
|
82737
|
+
const sourcesDir = join5(root, "Sources");
|
|
82629
82738
|
if (existsSync2(sourcesDir)) {
|
|
82630
82739
|
let entries = [];
|
|
82631
82740
|
try {
|
|
@@ -82634,7 +82743,7 @@ function resolveContactsDbPaths(baseDir) {
|
|
|
82634
82743
|
entries = [];
|
|
82635
82744
|
}
|
|
82636
82745
|
for (const entry of entries) {
|
|
82637
|
-
const candidate =
|
|
82746
|
+
const candidate = join5(sourcesDir, entry, "AddressBook-v22.abcddb");
|
|
82638
82747
|
if (existsSync2(candidate)) paths.push(candidate);
|
|
82639
82748
|
}
|
|
82640
82749
|
}
|
|
@@ -82737,25 +82846,44 @@ function getByIdScanThreshold() {
|
|
|
82737
82846
|
var BYID_SCAN_BUDGET_SECONDS = 9;
|
|
82738
82847
|
var BYID_PROBE_TIMEOUT_SECONDS = 3;
|
|
82739
82848
|
var BYID_COVERAGE_MARKER = "COV";
|
|
82849
|
+
var BYID_BUSY_MARKER = "BUSY";
|
|
82850
|
+
var MAIL_NOT_RUNNING_MARKER = "NOTRUNNING";
|
|
82851
|
+
var UNREACHED_LIST_MAX = 15;
|
|
82740
82852
|
var SCOPED_MISS_MARKER = "SCOPED";
|
|
82853
|
+
function mailNotRunningForId(id) {
|
|
82854
|
+
return `Mail.app is not running, so message ${Number(id)} could not be looked up by its numeric Mail.app id (this server does not launch Mail.app by itself). Open Mail.app and retry, or use the message's imap: id from list-messages/search-messages, which reads over IMAP without Mail.app.`;
|
|
82855
|
+
}
|
|
82856
|
+
function mailBusyForId(id) {
|
|
82857
|
+
return `Mail.app did not answer within ${BYID_PROBE_TIMEOUT_SECONDS}s, so message ${Number(id)} was not looked up \u2014 it is still busy with an earlier request (most often a size count on a very large mailbox that a previous lookup gave up on; Mail.app finishes it regardless). Retry in a little while, or use the message's imap: id from list-messages/search-messages.`;
|
|
82858
|
+
}
|
|
82859
|
+
function summariseList(items, max) {
|
|
82860
|
+
if (items.length <= max) return items.join(", ");
|
|
82861
|
+
return `${items.slice(0, max).join(", ")} and ${items.length - max} more`;
|
|
82862
|
+
}
|
|
82741
82863
|
function describeUnscopedMiss(id, raw) {
|
|
82742
82864
|
const covIdx = raw.indexOf(BYID_COVERAGE_MARKER);
|
|
82743
82865
|
if (covIdx === -1) return raw.trim();
|
|
82744
82866
|
const base2 = raw.slice(0, covIdx).trim();
|
|
82745
|
-
const [skippedRaw = "", budgetRaw = ""] = raw.slice(covIdx + BYID_COVERAGE_MARKER.length).split(DIAG_FIELD_SEP);
|
|
82746
|
-
const
|
|
82867
|
+
const [skippedRaw = "", budgetRaw = "", stalledRaw = "", unreachedRaw = ""] = raw.slice(covIdx + BYID_COVERAGE_MARKER.length).split(DIAG_FIELD_SEP);
|
|
82868
|
+
const list = (s) => s.split(DIAG_ITEM_SEP).map((x) => x.trim()).filter(Boolean);
|
|
82869
|
+
const skipped = list(skippedRaw);
|
|
82870
|
+
const unreached = list(unreachedRaw);
|
|
82747
82871
|
const budgetHit = budgetRaw.trim() === "true";
|
|
82748
|
-
|
|
82872
|
+
const probeStalled = stalledRaw.trim() === "true";
|
|
82873
|
+
if (skipped.length === 0 && !budgetHit && !probeStalled) return base2;
|
|
82749
82874
|
const parts = [`Message ${Number(id)} not found in the mailboxes scanned.`];
|
|
82750
82875
|
if (skipped.length > 0) {
|
|
82751
82876
|
parts.push(
|
|
82752
82877
|
`Not scanned \u2014 too large for a cross-mailbox id scan (over ${getByIdScanThreshold()} messages; APPLE_MAIL_MAX_BYID_SCAN_MAILBOX): ${skipped.join(", ")}.`
|
|
82753
82878
|
);
|
|
82754
82879
|
}
|
|
82755
|
-
|
|
82880
|
+
const reach = unreached.length > 0 ? `before reaching: ${summariseList(unreached, UNREACHED_LIST_MAX)}.` : "before reaching every mailbox.";
|
|
82881
|
+
if (probeStalled) {
|
|
82756
82882
|
parts.push(
|
|
82757
|
-
`The scan
|
|
82883
|
+
`The scan stopped early, ${reach} A mailbox's size count did not answer within ${BYID_PROBE_TIMEOUT_SECONDS}s, and Mail.app keeps working on an abandoned count, so anything asked after it would only queue behind it. That mailbox is now remembered and skipped for ${Math.round(STALLED_MAILBOX_TTL_MS / 864e5)} days \u2014 retry in a little while to scan the rest.`
|
|
82758
82884
|
);
|
|
82885
|
+
} else if (budgetHit) {
|
|
82886
|
+
parts.push(`The scan also stopped after its ${BYID_SCAN_BUDGET_SECONDS}s budget ${reach}`);
|
|
82759
82887
|
}
|
|
82760
82888
|
parts.push(
|
|
82761
82889
|
"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."
|
|
@@ -82777,7 +82905,7 @@ function extractFreshlyStalledMailboxes(raw) {
|
|
|
82777
82905
|
const account = withoutSuffix.slice(0, sepIdx);
|
|
82778
82906
|
const path3 = withoutSuffix.slice(sepIdx + 3);
|
|
82779
82907
|
const leaf = path3.split("/").pop();
|
|
82780
|
-
if (account && leaf) out.push(`${account}
|
|
82908
|
+
if (account && leaf) out.push(`${account}${STALLED_KEY_SEP}${leaf}`);
|
|
82781
82909
|
}
|
|
82782
82910
|
return out;
|
|
82783
82911
|
}
|
|
@@ -82845,7 +82973,7 @@ function splitSearchDiagnostics(output, account) {
|
|
|
82845
82973
|
}
|
|
82846
82974
|
return { payload, diagnostics };
|
|
82847
82975
|
}
|
|
82848
|
-
var ALLOWED_SAVE_ROOTS = [
|
|
82976
|
+
var ALLOWED_SAVE_ROOTS = [homedir5(), "/tmp", "/private/tmp", "/Volumes"];
|
|
82849
82977
|
function isPathWithinAllowedRoots(resolvedPath) {
|
|
82850
82978
|
return ALLOWED_SAVE_ROOTS.some((root) => {
|
|
82851
82979
|
const base2 = root.endsWith(sep2) ? root.slice(0, -1) : root;
|
|
@@ -83139,6 +83267,24 @@ function buildAppLevelScript(command) {
|
|
|
83139
83267
|
end tell
|
|
83140
83268
|
`;
|
|
83141
83269
|
}
|
|
83270
|
+
function buildAppLevelScriptIfRunning(command) {
|
|
83271
|
+
return guardMailRunning(buildAppLevelScript(command));
|
|
83272
|
+
}
|
|
83273
|
+
function guardMailRunning(script) {
|
|
83274
|
+
return `
|
|
83275
|
+
if application "Mail" is running then
|
|
83276
|
+
${script}
|
|
83277
|
+
else
|
|
83278
|
+
return "${MAIL_NOT_RUNNING_MARKER}"
|
|
83279
|
+
end if
|
|
83280
|
+
`;
|
|
83281
|
+
}
|
|
83282
|
+
function looksLikeMailNotRunning(error3) {
|
|
83283
|
+
if (!error3) return false;
|
|
83284
|
+
return /application isn't running|\(-600\)|Mail\.app is not responding\. Try opening Mail\.app/i.test(
|
|
83285
|
+
error3
|
|
83286
|
+
);
|
|
83287
|
+
}
|
|
83142
83288
|
function mailboxPathFragment(mailboxVar, outputVar) {
|
|
83143
83289
|
return `
|
|
83144
83290
|
set ${outputVar} to name of ${mailboxVar}
|
|
@@ -83302,13 +83448,12 @@ var AppleMailManager = class {
|
|
|
83302
83448
|
* demonstrated this, the unscoped scan skips the probe for it entirely —
|
|
83303
83449
|
* no `count of messages` call at all — straight to "size unknown,
|
|
83304
83450
|
* previously timed out". Keyed on the leaf mailbox name (not the
|
|
83305
|
-
* container-walked path `mailboxPathFragment` builds for display)
|
|
83306
|
-
*
|
|
83307
|
-
*
|
|
83308
|
-
*
|
|
83309
|
-
* stalled once on a store this large has no reason to get faster.
|
|
83451
|
+
* container-walked path `mailboxPathFragment` builds for display), matched
|
|
83452
|
+
* against names the scan prefetches once per account, so checking it costs
|
|
83453
|
+
* no Apple event per mailbox visited. Persisted across restarts and shared
|
|
83454
|
+
* between server processes, with a 7-day TTL (see StalledMailboxStore).
|
|
83310
83455
|
*/
|
|
83311
|
-
|
|
83456
|
+
stalledMailboxes = new StalledMailboxStore();
|
|
83312
83457
|
/** Error from the most recent numeric message read, if it was refused. */
|
|
83313
83458
|
lastMessageLookupError;
|
|
83314
83459
|
/** Cap on the id→location index so a long-lived process can't grow unbounded. */
|
|
@@ -83971,6 +84116,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
83971
84116
|
return cached2.data;
|
|
83972
84117
|
}
|
|
83973
84118
|
const names = this.fetchMailboxNames(account);
|
|
84119
|
+
if (names === null) return [];
|
|
83974
84120
|
this.cache.mailboxNames.set(account, { data: names, expiry: now + this.CACHE_TTL_MS });
|
|
83975
84121
|
return names;
|
|
83976
84122
|
}
|
|
@@ -84495,14 +84641,23 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84495
84641
|
if (!result.success || !result.output.trim()) {
|
|
84496
84642
|
if (!result.success) {
|
|
84497
84643
|
if (result.timedOut) this.lastMessageLookupError = timedOutScanningForId(id);
|
|
84644
|
+
else if (looksLikeMailNotRunning(result.error))
|
|
84645
|
+
this.lastMessageLookupError = mailNotRunningForId(id);
|
|
84498
84646
|
console.error(`Failed to resolve message ${id}: ${result.error}`);
|
|
84499
84647
|
}
|
|
84500
84648
|
return null;
|
|
84501
84649
|
}
|
|
84650
|
+
if (result.output.startsWith(MAIL_NOT_RUNNING_MARKER)) {
|
|
84651
|
+
this.lastMessageLookupError = mailNotRunningForId(id);
|
|
84652
|
+
return null;
|
|
84653
|
+
}
|
|
84654
|
+
if (result.output.startsWith(LOOKUP_ERROR_MARKER + BYID_BUSY_MARKER)) {
|
|
84655
|
+
this.lastMessageLookupError = mailBusyForId(id);
|
|
84656
|
+
return null;
|
|
84657
|
+
}
|
|
84502
84658
|
if (result.output.startsWith(LOOKUP_ERROR_MARKER)) {
|
|
84503
84659
|
const rawTail = result.output.slice(LOOKUP_ERROR_MARKER.length);
|
|
84504
|
-
|
|
84505
|
-
this.knownStalledMailboxes.add(key);
|
|
84660
|
+
this.stalledMailboxes.record(extractFreshlyStalledMailboxes(rawTail));
|
|
84506
84661
|
this.lastMessageLookupError = describeUnscopedMiss(id, rawTail);
|
|
84507
84662
|
return null;
|
|
84508
84663
|
}
|
|
@@ -84520,6 +84675,10 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84520
84675
|
{ timeoutMs: READ_ONLY_BYID_TIMEOUT_MS }
|
|
84521
84676
|
);
|
|
84522
84677
|
const where = `${loc.account} / ${loc.mailbox}`;
|
|
84678
|
+
if (result.success && result.output.startsWith(MAIL_NOT_RUNNING_MARKER) || !result.success && !result.timedOut && looksLikeMailNotRunning(result.error)) {
|
|
84679
|
+
this.lastMessageLookupError = mailNotRunningForId(id);
|
|
84680
|
+
return { kind: "done" };
|
|
84681
|
+
}
|
|
84523
84682
|
if (!result.success) {
|
|
84524
84683
|
console.error(`Failed to read message ${id} from ${where}: ${result.error}`);
|
|
84525
84684
|
if (result.timedOut) {
|
|
@@ -84571,20 +84730,38 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84571
84730
|
* instead of a false flat "not found". A copy that sits only in a skipped
|
|
84572
84731
|
* mailbox therefore no longer counts toward the ambiguity check — harmless
|
|
84573
84732
|
* for these read-only lookups, whose copies are the same message.
|
|
84733
|
+
*
|
|
84734
|
+
* #270 follow-up (@j5pu, 2.20.7 retest) — three refinements:
|
|
84735
|
+
*
|
|
84736
|
+
* - Names are prefetched: account names once (`name of every account`)
|
|
84737
|
+
* and each account's mailbox leaf names once (`name of mailboxes of
|
|
84738
|
+
* acct`), so the remembered-stalled check is a local string comparison
|
|
84739
|
+
* instead of two Apple events per remembered entry per mailbox visited,
|
|
84740
|
+
* and an early stop can name every mailbox it never reached without
|
|
84741
|
+
* asking Mail.app anything more.
|
|
84742
|
+
* - The account-name prefetch is itself capped at BYID_PROBE_TIMEOUT_SECONDS.
|
|
84743
|
+
* It is trivially cheap, so if it does not answer Mail.app is still busy
|
|
84744
|
+
* (usually draining a count an earlier call abandoned): return
|
|
84745
|
+
* BYID_BUSY_MARKER rather than walk on and let small mailboxes' probes
|
|
84746
|
+
* time out behind it — which would wrongly remember THEM as stalled.
|
|
84747
|
+
* - A probe that times out (-1712) STOPS the walk on the spot. Mail.app
|
|
84748
|
+
* keeps running the abandoned count, so every later event — the next
|
|
84749
|
+
* probe, a `whose` scan, even the container walk that used to build the
|
|
84750
|
+
* skip entry's display path — would queue behind it until the 15s
|
|
84751
|
+
* SIGKILL, losing the very result that records the stall. The skip entry
|
|
84752
|
+
* is built from the prefetched leaf name (no Apple event) and the script
|
|
84753
|
+
* returns at once, so the caller always learns and persists it.
|
|
84574
84754
|
*/
|
|
84575
84755
|
unscopedByIdScript(id, innerAction, opts = {}) {
|
|
84576
84756
|
const threshold = getByIdScanThreshold();
|
|
84577
|
-
const
|
|
84578
|
-
|
|
84579
|
-
const
|
|
84580
|
-
|
|
84581
|
-
|
|
84582
|
-
|
|
84583
|
-
|
|
84584
|
-
};
|
|
84585
|
-
const sizeGuard = (mbVar, acctNameExpr) => {
|
|
84757
|
+
const stalledKeys = threshold > 0 ? this.stalledMailboxes.keys() : [];
|
|
84758
|
+
const stalledList = `{${stalledKeys.map((key) => {
|
|
84759
|
+
const i = key.indexOf(STALLED_KEY_SEP);
|
|
84760
|
+
return `"${escapeForAppleScript(key.slice(0, i))}${FIELD_SEP}${escapeForAppleScript(key.slice(i + 1))}"`;
|
|
84761
|
+
}).join(", ")}}`;
|
|
84762
|
+
const stalledLabel = `size unknown (count probe exceeded ${BYID_PROBE_TIMEOUT_SECONDS}s)`;
|
|
84763
|
+
const sizeGuard = (mbVar, acctNameExpr, mbNameExpr) => {
|
|
84586
84764
|
if (threshold <= 0) return "";
|
|
84587
|
-
const stalled = stalledCheck(mbVar, acctNameExpr);
|
|
84588
84765
|
const skipEntry = (sizeLabelExpr) => `
|
|
84589
84766
|
set _skipPath to ""
|
|
84590
84767
|
try
|
|
@@ -84593,67 +84770,131 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84593
84770
|
set _skipped to _skipped & ${acctNameExpr} & " / " & _skipPath & " (" & ${sizeLabelExpr} & ")${DIAG_ITEM_SEP}"
|
|
84594
84771
|
set _probe to false`;
|
|
84595
84772
|
const probeBlock = `set _mbCount to -1
|
|
84773
|
+
set _probeErr to 0
|
|
84596
84774
|
try
|
|
84597
84775
|
with timeout of ${BYID_PROBE_TIMEOUT_SECONDS} seconds
|
|
84598
84776
|
set _mbCount to count of messages of ${mbVar}
|
|
84599
84777
|
end timeout
|
|
84778
|
+
on error number _probeErrNum
|
|
84779
|
+
set _probeErr to _probeErrNum
|
|
84600
84780
|
end try
|
|
84601
|
-
if
|
|
84602
|
-
set
|
|
84781
|
+
if _probeErr is -1712 then
|
|
84782
|
+
set _skipped to _skipped & ${acctNameExpr} & " / " & ${mbNameExpr} & " (${stalledLabel})${DIAG_ITEM_SEP}"
|
|
84783
|
+
set _probe to false
|
|
84784
|
+
set _probeStalled to true
|
|
84785
|
+
else if _mbCount < 0 or _mbCount > ${threshold} then
|
|
84786
|
+
set _sizeLabel to "size unknown (count failed)"
|
|
84603
84787
|
if _mbCount > -1 then set _sizeLabel to (_mbCount as string)
|
|
84604
84788
|
${skipEntry("_sizeLabel")}
|
|
84605
84789
|
end if`;
|
|
84606
|
-
if (
|
|
84607
|
-
return `if ${
|
|
84790
|
+
if (stalledKeys.length === 0) return probeBlock;
|
|
84791
|
+
return `if _stalledKeys contains {(${acctNameExpr} & "${FIELD_SEP}" & ${mbNameExpr})} then
|
|
84608
84792
|
${skipEntry('"size unknown (previously timed out)"')}
|
|
84609
84793
|
else
|
|
84610
84794
|
${probeBlock}
|
|
84611
84795
|
end if`;
|
|
84612
84796
|
};
|
|
84613
|
-
const budgetCheck = `if ((current date) - _startedAt) > ${BYID_SCAN_BUDGET_SECONDS} then
|
|
84797
|
+
const budgetCheck = (stopAtExpr) => `if ((current date) - _startedAt) > ${BYID_SCAN_BUDGET_SECONDS} then
|
|
84614
84798
|
set _budgetHit to true
|
|
84799
|
+
set _stopAt to ${stopAtExpr}
|
|
84615
84800
|
exit repeat
|
|
84616
84801
|
end if`;
|
|
84802
|
+
const hitFound = opts.firstHit ? "(count of _hits) > 0" : "false";
|
|
84617
84803
|
const stopOnHit = opts.firstHit ? `if (count of _hits) > 0 then exit repeat` : "";
|
|
84618
|
-
return
|
|
84804
|
+
return buildAppLevelScriptIfRunning(`
|
|
84619
84805
|
try
|
|
84620
84806
|
set _hits to {}
|
|
84621
84807
|
set _hitMbs to {}
|
|
84622
84808
|
set _hitAccts to {}
|
|
84623
84809
|
set _names to ""
|
|
84624
84810
|
set _skipped to ""
|
|
84811
|
+
set _unreached to ""
|
|
84625
84812
|
set _budgetHit to false
|
|
84813
|
+
set _probeStalled to false
|
|
84814
|
+
set _stopAt to 0
|
|
84815
|
+
set _stalledKeys to ${stalledList}
|
|
84626
84816
|
set _startedAt to current date
|
|
84817
|
+
set _acctNames to {}
|
|
84818
|
+
try
|
|
84819
|
+
with timeout of ${BYID_PROBE_TIMEOUT_SECONDS} seconds
|
|
84820
|
+
set _acctNames to name of every account
|
|
84821
|
+
end timeout
|
|
84822
|
+
on error number _busyErrNum
|
|
84823
|
+
if _busyErrNum is -1712 then return "${LOOKUP_ERROR_MARKER}${BYID_BUSY_MARKER}"
|
|
84824
|
+
end try
|
|
84825
|
+
set _ai to 0
|
|
84627
84826
|
repeat with acct in accounts
|
|
84628
|
-
|
|
84629
|
-
${
|
|
84630
|
-
|
|
84631
|
-
|
|
84632
|
-
|
|
84633
|
-
|
|
84634
|
-
|
|
84635
|
-
|
|
84636
|
-
|
|
84637
|
-
|
|
84638
|
-
|
|
84639
|
-
|
|
84640
|
-
|
|
84641
|
-
|
|
84642
|
-
|
|
84643
|
-
|
|
84827
|
+
set _ai to _ai + 1
|
|
84828
|
+
if ${hitFound} then exit repeat
|
|
84829
|
+
set _acctName to ""
|
|
84830
|
+
if _ai <= (length of _acctNames) then set _acctName to item _ai of _acctNames
|
|
84831
|
+
if _acctName is "" then set _acctName to (name of acct)
|
|
84832
|
+
if _budgetHit or _probeStalled then
|
|
84833
|
+
-- Stopped early: name what is left without asking Mail.app.
|
|
84834
|
+
set _unreached to _unreached & _acctName & " (every mailbox)${DIAG_ITEM_SEP}"
|
|
84835
|
+
else
|
|
84836
|
+
set _mbNames to {}
|
|
84837
|
+
try
|
|
84838
|
+
set _mbNames to name of mailboxes of acct
|
|
84839
|
+
end try
|
|
84840
|
+
set _mi to 0
|
|
84841
|
+
repeat with mb in mailboxes of acct
|
|
84842
|
+
set _mi to _mi + 1
|
|
84843
|
+
${budgetCheck("_mi")}
|
|
84844
|
+
set _mbName to ""
|
|
84845
|
+
if _mi <= (length of _mbNames) then set _mbName to item _mi of _mbNames
|
|
84846
|
+
if _mbName is "" then set _mbName to (name of mb)
|
|
84847
|
+
set _probe to true
|
|
84848
|
+
${sizeGuard("mb", "_acctName", "_mbName")}
|
|
84849
|
+
if _probeStalled then
|
|
84850
|
+
set _stopAt to _mi + 1
|
|
84851
|
+
exit repeat
|
|
84852
|
+
end if
|
|
84853
|
+
if _probe then
|
|
84854
|
+
try
|
|
84855
|
+
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
84856
|
+
if (count of matchingMsgs) > 0 then
|
|
84857
|
+
set end of _hits to item 1 of matchingMsgs
|
|
84858
|
+
set end of _hitMbs to (contents of mb)
|
|
84859
|
+
set end of _hitAccts to _acctName
|
|
84860
|
+
set _names to _names & _acctName & "/" & _mbName & ", "
|
|
84861
|
+
end if
|
|
84862
|
+
end try
|
|
84863
|
+
end if
|
|
84864
|
+
${stopOnHit}
|
|
84865
|
+
end repeat
|
|
84866
|
+
if _budgetHit or _probeStalled then
|
|
84867
|
+
if (length of _mbNames) > 0 then
|
|
84868
|
+
repeat with _ri from _stopAt to (length of _mbNames)
|
|
84869
|
+
set _unreached to _unreached & _acctName & " / " & (item _ri of _mbNames) & "${DIAG_ITEM_SEP}"
|
|
84870
|
+
end repeat
|
|
84871
|
+
else
|
|
84872
|
+
set _unreached to _unreached & _acctName & " (remaining mailboxes)${DIAG_ITEM_SEP}"
|
|
84873
|
+
end if
|
|
84644
84874
|
end if
|
|
84645
|
-
|
|
84646
|
-
end repeat
|
|
84875
|
+
end if
|
|
84647
84876
|
end repeat
|
|
84648
84877
|
-- #183: local mailboxes belong to no account, so the walk above cannot
|
|
84649
84878
|
-- reach them. Collect into the SAME _hits/_names, which means an id
|
|
84650
84879
|
-- present both in an account and locally is now correctly reported as
|
|
84651
84880
|
-- ambiguous rather than silently resolving to the account copy.
|
|
84652
|
-
if
|
|
84881
|
+
if _budgetHit or _probeStalled then
|
|
84882
|
+
set _unreached to _unreached & "${LOCAL_STORE_LABEL} (any local mailboxes)${DIAG_ITEM_SEP}"
|
|
84883
|
+
else if not (${hitFound}) then${localMailboxBindingFragment()}
|
|
84884
|
+
set _mi to 0
|
|
84653
84885
|
repeat with mb in _mbs
|
|
84654
|
-
|
|
84886
|
+
set _mi to _mi + 1
|
|
84887
|
+
${budgetCheck("_mi")}
|
|
84888
|
+
set _mbName to ""
|
|
84889
|
+
try
|
|
84890
|
+
set _mbName to (name of mb)
|
|
84891
|
+
end try
|
|
84655
84892
|
set _probe to true
|
|
84656
|
-
${sizeGuard("mb", `"${LOCAL_STORE_LABEL}"
|
|
84893
|
+
${sizeGuard("mb", `"${LOCAL_STORE_LABEL}"`, "_mbName")}
|
|
84894
|
+
if _probeStalled then
|
|
84895
|
+
set _stopAt to _mi + 1
|
|
84896
|
+
exit repeat
|
|
84897
|
+
end if
|
|
84657
84898
|
if _probe then
|
|
84658
84899
|
try
|
|
84659
84900
|
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
@@ -84661,14 +84902,17 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84661
84902
|
set end of _hits to item 1 of matchingMsgs
|
|
84662
84903
|
set end of _hitMbs to (contents of mb)
|
|
84663
84904
|
set end of _hitAccts to "${LOCAL_STORE_LABEL}"
|
|
84664
|
-
set _names to _names & "${LOCAL_STORE_LABEL}/" &
|
|
84905
|
+
set _names to _names & "${LOCAL_STORE_LABEL}/" & _mbName & ", "
|
|
84665
84906
|
end if
|
|
84666
84907
|
end try
|
|
84667
84908
|
end if
|
|
84668
84909
|
${stopOnHit}
|
|
84669
84910
|
end repeat
|
|
84911
|
+
if (_budgetHit or _probeStalled) and _stopAt <= (length of _mbs) then
|
|
84912
|
+
set _unreached to _unreached & "${LOCAL_STORE_LABEL} (" & ((length of _mbs) - _stopAt + 1) & " more local mailboxes)${DIAG_ITEM_SEP}"
|
|
84913
|
+
end if
|
|
84670
84914
|
end if
|
|
84671
|
-
if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found${BYID_COVERAGE_MARKER}" & _skipped & "${DIAG_FIELD_SEP}" & (_budgetHit as string)
|
|
84915
|
+
if (count of _hits) is 0 then return "${LOOKUP_ERROR_MARKER}Message not found${BYID_COVERAGE_MARKER}" & _skipped & "${DIAG_FIELD_SEP}" & (_budgetHit as string) & "${DIAG_FIELD_SEP}" & (_probeStalled as string) & "${DIAG_FIELD_SEP}" & _unreached
|
|
84672
84916
|
${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"`}
|
|
84673
84917
|
if (count of _hits) ${opts.firstHit ? "> 0" : "is 1"} then
|
|
84674
84918
|
set msg to item 1 of _hits
|
|
@@ -84728,7 +84972,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84728
84972
|
end repeat
|
|
84729
84973
|
end ignoring`;
|
|
84730
84974
|
const miss = `${LOOKUP_ERROR_MARKER}${SCOPED_MISS_MARKER}`;
|
|
84731
|
-
return
|
|
84975
|
+
return buildAppLevelScriptIfRunning(`
|
|
84732
84976
|
try
|
|
84733
84977
|
${bind}
|
|
84734
84978
|
if targetMb is missing value then return "${miss}nomailbox"
|
|
@@ -86231,12 +86475,12 @@ ${this.errorEmit(" ")}
|
|
|
86231
86475
|
const safeName = escapeForAppleScript(attachmentName);
|
|
86232
86476
|
let temporaryDirectory;
|
|
86233
86477
|
try {
|
|
86234
|
-
temporaryDirectory = mkdtempSync2(
|
|
86478
|
+
temporaryDirectory = mkdtempSync2(join6(target.saveDirectory, ".apple-mail-mcp-"));
|
|
86235
86479
|
} catch (error3) {
|
|
86236
86480
|
console.error(`Failed to create attachment staging directory: ${error3}`);
|
|
86237
86481
|
return false;
|
|
86238
86482
|
}
|
|
86239
|
-
const temporaryPath =
|
|
86483
|
+
const temporaryPath = join6(temporaryDirectory, "attachment");
|
|
86240
86484
|
const safeTemporaryPath = escapeForAppleScript(temporaryPath);
|
|
86241
86485
|
const cleanupTemporaryDirectory = () => {
|
|
86242
86486
|
try {
|
|
@@ -86296,9 +86540,9 @@ ${this.errorEmit(" ")}
|
|
|
86296
86540
|
}
|
|
86297
86541
|
let mimeTemporaryDirectory;
|
|
86298
86542
|
try {
|
|
86299
|
-
mimeTemporaryDirectory = mkdtempSync2(
|
|
86300
|
-
const mimeTemporaryPath =
|
|
86301
|
-
|
|
86543
|
+
mimeTemporaryDirectory = mkdtempSync2(join6(target.saveDirectory, ".apple-mail-mcp-"));
|
|
86544
|
+
const mimeTemporaryPath = join6(mimeTemporaryDirectory, "attachment");
|
|
86545
|
+
writeFileSync4(mimeTemporaryPath, attachment.data, { flag: "wx", mode: 384 });
|
|
86302
86546
|
copyFileSync(mimeTemporaryPath, target.savedPath, fsConstants.COPYFILE_EXCL);
|
|
86303
86547
|
chmodSync(target.savedPath, 384);
|
|
86304
86548
|
return true;
|
|
@@ -86323,7 +86567,7 @@ ${this.errorEmit(" ")}
|
|
|
86323
86567
|
let dir = null;
|
|
86324
86568
|
try {
|
|
86325
86569
|
dir = mkdtempSync2("/private/tmp/amcp-fetch-");
|
|
86326
|
-
const dest =
|
|
86570
|
+
const dest = join6(dir, attachmentName.replace(/[/\\]/g, "_"));
|
|
86327
86571
|
const ok = this.saveAttachment(id, attachmentName, dir);
|
|
86328
86572
|
if (!ok) {
|
|
86329
86573
|
return {
|
|
@@ -86331,7 +86575,7 @@ ${this.errorEmit(" ")}
|
|
|
86331
86575
|
error: `Attachment "${attachmentName}" not found on message ${id}`
|
|
86332
86576
|
};
|
|
86333
86577
|
}
|
|
86334
|
-
const buf =
|
|
86578
|
+
const buf = readFileSync3(dest);
|
|
86335
86579
|
return { success: true, base64: buf.toString("base64"), bytes: buf.length };
|
|
86336
86580
|
} catch (e) {
|
|
86337
86581
|
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
@@ -86575,12 +86819,12 @@ ${this.errorEmit(" ")}
|
|
|
86575
86819
|
// gives full control over criteria without UI/GUI scripting.
|
|
86576
86820
|
// ===========================================================================
|
|
86577
86821
|
findSyncedSmartPlist() {
|
|
86578
|
-
const base2 =
|
|
86822
|
+
const base2 = join6(homedir5(), "Library", "Mail");
|
|
86579
86823
|
try {
|
|
86580
86824
|
const versions = readdirSync2(base2).filter((d) => d.startsWith("V"));
|
|
86581
86825
|
versions.sort().reverse();
|
|
86582
86826
|
for (const v of versions) {
|
|
86583
|
-
const p =
|
|
86827
|
+
const p = join6(base2, v, "MailData", "SyncedSmartMailboxes.plist");
|
|
86584
86828
|
if (existsSync3(p)) return p;
|
|
86585
86829
|
}
|
|
86586
86830
|
} catch {
|
|
@@ -86668,12 +86912,12 @@ ${this.errorEmit(" ")}
|
|
|
86668
86912
|
const lint = spawnSync2("plutil", ["-lint", temp], { encoding: "utf8" });
|
|
86669
86913
|
if (lint.status !== 0) {
|
|
86670
86914
|
try {
|
|
86671
|
-
|
|
86915
|
+
unlinkSync2(temp);
|
|
86672
86916
|
} catch {
|
|
86673
86917
|
}
|
|
86674
86918
|
return false;
|
|
86675
86919
|
}
|
|
86676
|
-
|
|
86920
|
+
renameSync2(temp, plistPath);
|
|
86677
86921
|
return true;
|
|
86678
86922
|
}
|
|
86679
86923
|
buildSmartMailboxEntry(name2, fromContains = "", subjectContains = "", bodyContains = "") {
|
|
@@ -86766,19 +87010,19 @@ ${this.errorEmit(" ")}
|
|
|
86766
87010
|
const cleanupEntryFiles = () => {
|
|
86767
87011
|
for (const f of [entryJson, entryPlist]) {
|
|
86768
87012
|
try {
|
|
86769
|
-
|
|
87013
|
+
unlinkSync2(f);
|
|
86770
87014
|
} catch {
|
|
86771
87015
|
}
|
|
86772
87016
|
}
|
|
86773
87017
|
};
|
|
86774
|
-
|
|
87018
|
+
writeFileSync4(entryJson, JSON.stringify([entry]), "utf8");
|
|
86775
87019
|
const conv = spawnSync2("plutil", ["-convert", "xml1", "-o", entryPlist, entryJson], {
|
|
86776
87020
|
encoding: "utf8"
|
|
86777
87021
|
});
|
|
86778
87022
|
if (conv.status !== 0) {
|
|
86779
87023
|
cleanupEntryFiles();
|
|
86780
87024
|
try {
|
|
86781
|
-
|
|
87025
|
+
unlinkSync2(temp);
|
|
86782
87026
|
} catch {
|
|
86783
87027
|
}
|
|
86784
87028
|
return {
|
|
@@ -86793,7 +87037,7 @@ ${this.errorEmit(" ")}
|
|
|
86793
87037
|
cleanupEntryFiles();
|
|
86794
87038
|
if (ins.status !== 0) {
|
|
86795
87039
|
try {
|
|
86796
|
-
|
|
87040
|
+
unlinkSync2(temp);
|
|
86797
87041
|
} catch {
|
|
86798
87042
|
}
|
|
86799
87043
|
return {
|
|
@@ -86836,7 +87080,7 @@ ${this.errorEmit(" ")}
|
|
|
86836
87080
|
});
|
|
86837
87081
|
if (del.status !== 0) {
|
|
86838
87082
|
try {
|
|
86839
|
-
|
|
87083
|
+
unlinkSync2(temp);
|
|
86840
87084
|
} catch {
|
|
86841
87085
|
}
|
|
86842
87086
|
return { deleted: false, error: (del.stderr || "PlistBuddy delete failed").trim() };
|
|
@@ -87059,7 +87303,10 @@ end tell`;
|
|
|
87059
87303
|
}
|
|
87060
87304
|
/**
|
|
87061
87305
|
* Fetches canonical mailbox paths for an account directly from Mail.app.
|
|
87062
|
-
* Used internally by the cache; prefer getCachedMailboxNames().
|
|
87306
|
+
* Used internally by the cache; prefer getCachedMailboxNames(). Returns null
|
|
87307
|
+
* when Mail.app is not running — checked without launching it, so resolving
|
|
87308
|
+
* a mailbox name (e.g. for a scoped by-id read, #270 follow-up) never starts
|
|
87309
|
+
* Mail.app as a side effect.
|
|
87063
87310
|
*/
|
|
87064
87311
|
fetchMailboxNames(account) {
|
|
87065
87312
|
const body = `
|
|
@@ -87072,7 +87319,8 @@ end tell`;
|
|
|
87072
87319
|
return mbNames
|
|
87073
87320
|
`;
|
|
87074
87321
|
const script = isLocalStoreLabel(account) ? buildAppLevelScript(`${localMailboxBindingFragment()}${body}`) : buildAccountScopedScript(account, body);
|
|
87075
|
-
const result = executeAppleScript(script);
|
|
87322
|
+
const result = executeAppleScript(guardMailRunning(script));
|
|
87323
|
+
if (result.success && result.output.startsWith(MAIL_NOT_RUNNING_MARKER)) return null;
|
|
87076
87324
|
if (!result.success || !result.output) {
|
|
87077
87325
|
return [];
|
|
87078
87326
|
}
|
|
@@ -87558,7 +87806,7 @@ ${actionStmts.join("\n")}
|
|
|
87558
87806
|
|
|
87559
87807
|
// src/index.ts
|
|
87560
87808
|
init_smtpMailer();
|
|
87561
|
-
import { writeFileSync as
|
|
87809
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
87562
87810
|
import { join as joinPath } from "path";
|
|
87563
87811
|
|
|
87564
87812
|
// src/tools/compose.ts
|
|
@@ -88713,19 +88961,19 @@ var ImapIdleWatcher = class {
|
|
|
88713
88961
|
};
|
|
88714
88962
|
|
|
88715
88963
|
// src/services/fileConfig.ts
|
|
88716
|
-
import { existsSync as existsSync4, readFileSync as
|
|
88717
|
-
import { join as
|
|
88718
|
-
import { homedir as
|
|
88964
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
88965
|
+
import { join as join7 } from "path";
|
|
88966
|
+
import { homedir as homedir6 } from "os";
|
|
88719
88967
|
function fileConfigPath(env = process.env) {
|
|
88720
88968
|
const override = env.APPLE_MAIL_MCP_CONFIG_FILE;
|
|
88721
88969
|
if (override && override.trim()) return override.trim();
|
|
88722
|
-
return
|
|
88970
|
+
return join7(homedir6(), "Library", "Application Support", "apple-mail-mcp", "config.json");
|
|
88723
88971
|
}
|
|
88724
88972
|
function loadFileConfig(env = process.env, path3 = fileConfigPath(env)) {
|
|
88725
88973
|
const applied = [];
|
|
88726
88974
|
try {
|
|
88727
88975
|
if (!existsSync4(path3)) return applied;
|
|
88728
|
-
const parsed = JSON.parse(
|
|
88976
|
+
const parsed = JSON.parse(readFileSync4(path3, "utf8"));
|
|
88729
88977
|
if (!parsed || typeof parsed !== "object") return applied;
|
|
88730
88978
|
for (const [k, v] of Object.entries(parsed)) {
|
|
88731
88979
|
if (typeof v !== "string") continue;
|
|
@@ -89471,7 +89719,7 @@ registerTool(
|
|
|
89471
89719
|
} catch (error3) {
|
|
89472
89720
|
return errorResponse(error3 instanceof Error ? error3.message : String(error3));
|
|
89473
89721
|
}
|
|
89474
|
-
|
|
89722
|
+
writeFileSync5(target.savedPath, a.bytes, { flag: "wx", mode: 384 });
|
|
89475
89723
|
return successResponse(`${summary}. Written to ${target.savedPath}.`, {
|
|
89476
89724
|
...record2,
|
|
89477
89725
|
savedPath: target.savedPath
|
|
@@ -90400,7 +90648,7 @@ registerTool(
|
|
|
90400
90648
|
if (!r.success || !r.base64) {
|
|
90401
90649
|
return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
|
|
90402
90650
|
}
|
|
90403
|
-
|
|
90651
|
+
writeFileSync5(target.savedPath, Buffer.from(r.base64, "base64"), { flag: "wx", mode: 384 });
|
|
90404
90652
|
return successResponse(`Attachment "${attachmentName}" saved to ${savePath}`, {
|
|
90405
90653
|
ok: true,
|
|
90406
90654
|
attachmentName,
|
package/docs/IMAP-SETUP.md
CHANGED
|
@@ -370,6 +370,7 @@ GUI is ignoring.
|
|
|
370
370
|
| `APPLE_MAIL_MCP_SMTP_KEYCHAIN_SERVICE` / `_KEYCHAIN_ACCOUNT` | SMTP Keychain reference. |
|
|
371
371
|
| `APPLE_MAIL_MCP_CONFIG_FILE` | Path to the config JSON (default app-support dir). |
|
|
372
372
|
| `APPLE_MAIL_MCP_TEMPLATES_FILE` | Email-templates store path (default `~/Library/Application Support/apple-mail-mcp/templates.json`). |
|
|
373
|
+
| `APPLE_MAIL_MCP_STALLED_MAILBOXES_FILE` | Where the numeric-id scan remembers mailboxes whose size count timed out, so they are skipped for 7 days across restarts (default `~/Library/Application Support/apple-mail-mcp/stalled-mailboxes.json`; delete it to forget them). |
|
|
373
374
|
| `APPLE_MAIL_MCP_MAX_BUFFER` | Max AppleScript (`osascript`) output buffer in bytes (default 64 MiB). |
|
|
374
375
|
| `APPLE_MAIL_MAX_SEARCH_MAILBOX` | Per-mailbox message-count guard for unscoped AppleScript search (default `5000`; `0` disables). Note: no `_MCP` in the name. |
|
|
375
376
|
| `APPLE_MAIL_MAX_BYID_SCAN_MAILBOX` | Per-mailbox message-count guard for the cross-mailbox scan a numeric message id falls back to (default `50000`; `0` disables). Skipped mailboxes are named in the not-found error. Note: no `_MCP` in the name. |
|
package/package.json
CHANGED