apple-mail-mcp 2.20.6 → 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 +430 -125
- 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,31 +82846,69 @@ 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."
|
|
82762
82890
|
);
|
|
82763
82891
|
return parts.join(" ");
|
|
82764
82892
|
}
|
|
82893
|
+
var STALLED_PROBE_SUFFIX = ` (size unknown (count probe exceeded ${BYID_PROBE_TIMEOUT_SECONDS}s))`;
|
|
82894
|
+
function extractFreshlyStalledMailboxes(raw) {
|
|
82895
|
+
const covIdx = raw.indexOf(BYID_COVERAGE_MARKER);
|
|
82896
|
+
if (covIdx === -1) return [];
|
|
82897
|
+
const [skippedRaw = ""] = raw.slice(covIdx + BYID_COVERAGE_MARKER.length).split(DIAG_FIELD_SEP);
|
|
82898
|
+
const out = [];
|
|
82899
|
+
for (const entry of skippedRaw.split(DIAG_ITEM_SEP)) {
|
|
82900
|
+
const trimmed = entry.trim();
|
|
82901
|
+
if (!trimmed.endsWith(STALLED_PROBE_SUFFIX)) continue;
|
|
82902
|
+
const withoutSuffix = trimmed.slice(0, -STALLED_PROBE_SUFFIX.length);
|
|
82903
|
+
const sepIdx = withoutSuffix.indexOf(" / ");
|
|
82904
|
+
if (sepIdx === -1) continue;
|
|
82905
|
+
const account = withoutSuffix.slice(0, sepIdx);
|
|
82906
|
+
const path3 = withoutSuffix.slice(sepIdx + 3);
|
|
82907
|
+
const leaf = path3.split("/").pop();
|
|
82908
|
+
if (account && leaf) out.push(`${account}${STALLED_KEY_SEP}${leaf}`);
|
|
82909
|
+
}
|
|
82910
|
+
return out;
|
|
82911
|
+
}
|
|
82765
82912
|
var GROUP_SEP = "";
|
|
82766
82913
|
var FIELD_SEP = "";
|
|
82767
82914
|
var RECORD_SEP = "";
|
|
@@ -82826,7 +82973,7 @@ function splitSearchDiagnostics(output, account) {
|
|
|
82826
82973
|
}
|
|
82827
82974
|
return { payload, diagnostics };
|
|
82828
82975
|
}
|
|
82829
|
-
var ALLOWED_SAVE_ROOTS = [
|
|
82976
|
+
var ALLOWED_SAVE_ROOTS = [homedir5(), "/tmp", "/private/tmp", "/Volumes"];
|
|
82830
82977
|
function isPathWithinAllowedRoots(resolvedPath) {
|
|
82831
82978
|
return ALLOWED_SAVE_ROOTS.some((root) => {
|
|
82832
82979
|
const base2 = root.endsWith(sep2) ? root.slice(0, -1) : root;
|
|
@@ -83120,6 +83267,24 @@ function buildAppLevelScript(command) {
|
|
|
83120
83267
|
end tell
|
|
83121
83268
|
`;
|
|
83122
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
|
+
}
|
|
83123
83288
|
function mailboxPathFragment(mailboxVar, outputVar) {
|
|
83124
83289
|
return `
|
|
83125
83290
|
set ${outputVar} to name of ${mailboxVar}
|
|
@@ -83272,6 +83437,23 @@ var AppleMailManager = class {
|
|
|
83272
83437
|
* cross-mailbox scan — see `resolveScopedById` (#270).
|
|
83273
83438
|
*/
|
|
83274
83439
|
idLocationIndex = /* @__PURE__ */ new Map();
|
|
83440
|
+
/**
|
|
83441
|
+
* "Account\x1fLeafMailboxName" pairs whose `count of messages` size-guard
|
|
83442
|
+
* probe has previously exceeded BYID_PROBE_TIMEOUT_SECONDS (#270 follow-up,
|
|
83443
|
+
* @j5pu). The 3s `with timeout of` wrapper only stops the SERVER's script
|
|
83444
|
+
* from waiting — Mail.app handles Apple events one at a time and keeps
|
|
83445
|
+
* running the abandoned count to completion regardless, so re-probing the
|
|
83446
|
+
* same mailbox on the next call doesn't just waste its own 3s, it queues
|
|
83447
|
+
* behind whatever is still draining inside Mail.app. Once a mailbox has
|
|
83448
|
+
* demonstrated this, the unscoped scan skips the probe for it entirely —
|
|
83449
|
+
* no `count of messages` call at all — straight to "size unknown,
|
|
83450
|
+
* previously timed out". Keyed on the leaf mailbox name (not the
|
|
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).
|
|
83455
|
+
*/
|
|
83456
|
+
stalledMailboxes = new StalledMailboxStore();
|
|
83275
83457
|
/** Error from the most recent numeric message read, if it was refused. */
|
|
83276
83458
|
lastMessageLookupError;
|
|
83277
83459
|
/** Cap on the id→location index so a long-lived process can't grow unbounded. */
|
|
@@ -83934,6 +84116,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
83934
84116
|
return cached2.data;
|
|
83935
84117
|
}
|
|
83936
84118
|
const names = this.fetchMailboxNames(account);
|
|
84119
|
+
if (names === null) return [];
|
|
83937
84120
|
this.cache.mailboxNames.set(account, { data: names, expiry: now + this.CACHE_TTL_MS });
|
|
83938
84121
|
return names;
|
|
83939
84122
|
}
|
|
@@ -84458,15 +84641,24 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84458
84641
|
if (!result.success || !result.output.trim()) {
|
|
84459
84642
|
if (!result.success) {
|
|
84460
84643
|
if (result.timedOut) this.lastMessageLookupError = timedOutScanningForId(id);
|
|
84644
|
+
else if (looksLikeMailNotRunning(result.error))
|
|
84645
|
+
this.lastMessageLookupError = mailNotRunningForId(id);
|
|
84461
84646
|
console.error(`Failed to resolve message ${id}: ${result.error}`);
|
|
84462
84647
|
}
|
|
84463
84648
|
return null;
|
|
84464
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
|
+
}
|
|
84465
84658
|
if (result.output.startsWith(LOOKUP_ERROR_MARKER)) {
|
|
84466
|
-
|
|
84467
|
-
|
|
84468
|
-
|
|
84469
|
-
);
|
|
84659
|
+
const rawTail = result.output.slice(LOOKUP_ERROR_MARKER.length);
|
|
84660
|
+
this.stalledMailboxes.record(extractFreshlyStalledMailboxes(rawTail));
|
|
84661
|
+
this.lastMessageLookupError = describeUnscopedMiss(id, rawTail);
|
|
84470
84662
|
return null;
|
|
84471
84663
|
}
|
|
84472
84664
|
return result.output;
|
|
@@ -84483,6 +84675,10 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84483
84675
|
{ timeoutMs: READ_ONLY_BYID_TIMEOUT_MS }
|
|
84484
84676
|
);
|
|
84485
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
|
+
}
|
|
84486
84682
|
if (!result.success) {
|
|
84487
84683
|
console.error(`Failed to read message ${id} from ${where}: ${result.error}`);
|
|
84488
84684
|
if (result.timedOut) {
|
|
@@ -84534,69 +84730,171 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84534
84730
|
* instead of a false flat "not found". A copy that sits only in a skipped
|
|
84535
84731
|
* mailbox therefore no longer counts toward the ambiguity check — harmless
|
|
84536
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.
|
|
84537
84754
|
*/
|
|
84538
84755
|
unscopedByIdScript(id, innerAction, opts = {}) {
|
|
84539
84756
|
const threshold = getByIdScanThreshold();
|
|
84540
|
-
const
|
|
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) => {
|
|
84764
|
+
if (threshold <= 0) return "";
|
|
84765
|
+
const skipEntry = (sizeLabelExpr) => `
|
|
84766
|
+
set _skipPath to ""
|
|
84767
|
+
try
|
|
84768
|
+
${mailboxPathFragment(mbVar, "_skipPath")}
|
|
84769
|
+
end try
|
|
84770
|
+
set _skipped to _skipped & ${acctNameExpr} & " / " & _skipPath & " (" & ${sizeLabelExpr} & ")${DIAG_ITEM_SEP}"
|
|
84771
|
+
set _probe to false`;
|
|
84772
|
+
const probeBlock = `set _mbCount to -1
|
|
84773
|
+
set _probeErr to 0
|
|
84541
84774
|
try
|
|
84542
84775
|
with timeout of ${BYID_PROBE_TIMEOUT_SECONDS} seconds
|
|
84543
84776
|
set _mbCount to count of messages of ${mbVar}
|
|
84544
84777
|
end timeout
|
|
84778
|
+
on error number _probeErrNum
|
|
84779
|
+
set _probeErr to _probeErrNum
|
|
84545
84780
|
end try
|
|
84546
|
-
if
|
|
84547
|
-
set
|
|
84548
|
-
try
|
|
84549
|
-
${mailboxPathFragment(mbVar, "_skipPath")}
|
|
84550
|
-
end try
|
|
84551
|
-
set _sizeLabel to "size unknown (count probe exceeded ${BYID_PROBE_TIMEOUT_SECONDS}s)"
|
|
84552
|
-
if _mbCount > -1 then set _sizeLabel to (_mbCount as string)
|
|
84553
|
-
set _skipped to _skipped & ${acctNameExpr} & " / " & _skipPath & " (" & _sizeLabel & ")${DIAG_ITEM_SEP}"
|
|
84781
|
+
if _probeErr is -1712 then
|
|
84782
|
+
set _skipped to _skipped & ${acctNameExpr} & " / " & ${mbNameExpr} & " (${stalledLabel})${DIAG_ITEM_SEP}"
|
|
84554
84783
|
set _probe to false
|
|
84555
|
-
|
|
84556
|
-
|
|
84784
|
+
set _probeStalled to true
|
|
84785
|
+
else if _mbCount < 0 or _mbCount > ${threshold} then
|
|
84786
|
+
set _sizeLabel to "size unknown (count failed)"
|
|
84787
|
+
if _mbCount > -1 then set _sizeLabel to (_mbCount as string)
|
|
84788
|
+
${skipEntry("_sizeLabel")}
|
|
84789
|
+
end if`;
|
|
84790
|
+
if (stalledKeys.length === 0) return probeBlock;
|
|
84791
|
+
return `if _stalledKeys contains {(${acctNameExpr} & "${FIELD_SEP}" & ${mbNameExpr})} then
|
|
84792
|
+
${skipEntry('"size unknown (previously timed out)"')}
|
|
84793
|
+
else
|
|
84794
|
+
${probeBlock}
|
|
84795
|
+
end if`;
|
|
84796
|
+
};
|
|
84797
|
+
const budgetCheck = (stopAtExpr) => `if ((current date) - _startedAt) > ${BYID_SCAN_BUDGET_SECONDS} then
|
|
84557
84798
|
set _budgetHit to true
|
|
84799
|
+
set _stopAt to ${stopAtExpr}
|
|
84558
84800
|
exit repeat
|
|
84559
84801
|
end if`;
|
|
84802
|
+
const hitFound = opts.firstHit ? "(count of _hits) > 0" : "false";
|
|
84560
84803
|
const stopOnHit = opts.firstHit ? `if (count of _hits) > 0 then exit repeat` : "";
|
|
84561
|
-
return
|
|
84804
|
+
return buildAppLevelScriptIfRunning(`
|
|
84562
84805
|
try
|
|
84563
84806
|
set _hits to {}
|
|
84564
84807
|
set _hitMbs to {}
|
|
84565
84808
|
set _hitAccts to {}
|
|
84566
84809
|
set _names to ""
|
|
84567
84810
|
set _skipped to ""
|
|
84811
|
+
set _unreached to ""
|
|
84568
84812
|
set _budgetHit to false
|
|
84813
|
+
set _probeStalled to false
|
|
84814
|
+
set _stopAt to 0
|
|
84815
|
+
set _stalledKeys to ${stalledList}
|
|
84569
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
|
|
84570
84826
|
repeat with acct in accounts
|
|
84571
|
-
|
|
84572
|
-
${
|
|
84573
|
-
|
|
84574
|
-
|
|
84575
|
-
|
|
84576
|
-
|
|
84577
|
-
|
|
84578
|
-
|
|
84579
|
-
|
|
84580
|
-
|
|
84581
|
-
|
|
84582
|
-
|
|
84583
|
-
|
|
84584
|
-
|
|
84585
|
-
|
|
84586
|
-
|
|
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
|
|
84587
84874
|
end if
|
|
84588
|
-
|
|
84589
|
-
end repeat
|
|
84875
|
+
end if
|
|
84590
84876
|
end repeat
|
|
84591
84877
|
-- #183: local mailboxes belong to no account, so the walk above cannot
|
|
84592
84878
|
-- reach them. Collect into the SAME _hits/_names, which means an id
|
|
84593
84879
|
-- present both in an account and locally is now correctly reported as
|
|
84594
84880
|
-- ambiguous rather than silently resolving to the account copy.
|
|
84595
|
-
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
|
|
84596
84885
|
repeat with mb in _mbs
|
|
84597
|
-
|
|
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
|
|
84598
84892
|
set _probe to true
|
|
84599
|
-
${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
|
|
84600
84898
|
if _probe then
|
|
84601
84899
|
try
|
|
84602
84900
|
set matchingMsgs to (messages of mb whose id is ${Number(id)})
|
|
@@ -84604,14 +84902,17 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84604
84902
|
set end of _hits to item 1 of matchingMsgs
|
|
84605
84903
|
set end of _hitMbs to (contents of mb)
|
|
84606
84904
|
set end of _hitAccts to "${LOCAL_STORE_LABEL}"
|
|
84607
|
-
set _names to _names & "${LOCAL_STORE_LABEL}/" &
|
|
84905
|
+
set _names to _names & "${LOCAL_STORE_LABEL}/" & _mbName & ", "
|
|
84608
84906
|
end if
|
|
84609
84907
|
end try
|
|
84610
84908
|
end if
|
|
84611
84909
|
${stopOnHit}
|
|
84612
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
|
|
84613
84914
|
end if
|
|
84614
|
-
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
|
|
84615
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"`}
|
|
84616
84917
|
if (count of _hits) ${opts.firstHit ? "> 0" : "is 1"} then
|
|
84617
84918
|
set msg to item 1 of _hits
|
|
@@ -84671,7 +84972,7 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
84671
84972
|
end repeat
|
|
84672
84973
|
end ignoring`;
|
|
84673
84974
|
const miss = `${LOOKUP_ERROR_MARKER}${SCOPED_MISS_MARKER}`;
|
|
84674
|
-
return
|
|
84975
|
+
return buildAppLevelScriptIfRunning(`
|
|
84675
84976
|
try
|
|
84676
84977
|
${bind}
|
|
84677
84978
|
if targetMb is missing value then return "${miss}nomailbox"
|
|
@@ -86174,12 +86475,12 @@ ${this.errorEmit(" ")}
|
|
|
86174
86475
|
const safeName = escapeForAppleScript(attachmentName);
|
|
86175
86476
|
let temporaryDirectory;
|
|
86176
86477
|
try {
|
|
86177
|
-
temporaryDirectory = mkdtempSync2(
|
|
86478
|
+
temporaryDirectory = mkdtempSync2(join6(target.saveDirectory, ".apple-mail-mcp-"));
|
|
86178
86479
|
} catch (error3) {
|
|
86179
86480
|
console.error(`Failed to create attachment staging directory: ${error3}`);
|
|
86180
86481
|
return false;
|
|
86181
86482
|
}
|
|
86182
|
-
const temporaryPath =
|
|
86483
|
+
const temporaryPath = join6(temporaryDirectory, "attachment");
|
|
86183
86484
|
const safeTemporaryPath = escapeForAppleScript(temporaryPath);
|
|
86184
86485
|
const cleanupTemporaryDirectory = () => {
|
|
86185
86486
|
try {
|
|
@@ -86239,9 +86540,9 @@ ${this.errorEmit(" ")}
|
|
|
86239
86540
|
}
|
|
86240
86541
|
let mimeTemporaryDirectory;
|
|
86241
86542
|
try {
|
|
86242
|
-
mimeTemporaryDirectory = mkdtempSync2(
|
|
86243
|
-
const mimeTemporaryPath =
|
|
86244
|
-
|
|
86543
|
+
mimeTemporaryDirectory = mkdtempSync2(join6(target.saveDirectory, ".apple-mail-mcp-"));
|
|
86544
|
+
const mimeTemporaryPath = join6(mimeTemporaryDirectory, "attachment");
|
|
86545
|
+
writeFileSync4(mimeTemporaryPath, attachment.data, { flag: "wx", mode: 384 });
|
|
86245
86546
|
copyFileSync(mimeTemporaryPath, target.savedPath, fsConstants.COPYFILE_EXCL);
|
|
86246
86547
|
chmodSync(target.savedPath, 384);
|
|
86247
86548
|
return true;
|
|
@@ -86266,7 +86567,7 @@ ${this.errorEmit(" ")}
|
|
|
86266
86567
|
let dir = null;
|
|
86267
86568
|
try {
|
|
86268
86569
|
dir = mkdtempSync2("/private/tmp/amcp-fetch-");
|
|
86269
|
-
const dest =
|
|
86570
|
+
const dest = join6(dir, attachmentName.replace(/[/\\]/g, "_"));
|
|
86270
86571
|
const ok = this.saveAttachment(id, attachmentName, dir);
|
|
86271
86572
|
if (!ok) {
|
|
86272
86573
|
return {
|
|
@@ -86274,7 +86575,7 @@ ${this.errorEmit(" ")}
|
|
|
86274
86575
|
error: `Attachment "${attachmentName}" not found on message ${id}`
|
|
86275
86576
|
};
|
|
86276
86577
|
}
|
|
86277
|
-
const buf =
|
|
86578
|
+
const buf = readFileSync3(dest);
|
|
86278
86579
|
return { success: true, base64: buf.toString("base64"), bytes: buf.length };
|
|
86279
86580
|
} catch (e) {
|
|
86280
86581
|
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
|
@@ -86518,12 +86819,12 @@ ${this.errorEmit(" ")}
|
|
|
86518
86819
|
// gives full control over criteria without UI/GUI scripting.
|
|
86519
86820
|
// ===========================================================================
|
|
86520
86821
|
findSyncedSmartPlist() {
|
|
86521
|
-
const base2 =
|
|
86822
|
+
const base2 = join6(homedir5(), "Library", "Mail");
|
|
86522
86823
|
try {
|
|
86523
86824
|
const versions = readdirSync2(base2).filter((d) => d.startsWith("V"));
|
|
86524
86825
|
versions.sort().reverse();
|
|
86525
86826
|
for (const v of versions) {
|
|
86526
|
-
const p =
|
|
86827
|
+
const p = join6(base2, v, "MailData", "SyncedSmartMailboxes.plist");
|
|
86527
86828
|
if (existsSync3(p)) return p;
|
|
86528
86829
|
}
|
|
86529
86830
|
} catch {
|
|
@@ -86611,12 +86912,12 @@ ${this.errorEmit(" ")}
|
|
|
86611
86912
|
const lint = spawnSync2("plutil", ["-lint", temp], { encoding: "utf8" });
|
|
86612
86913
|
if (lint.status !== 0) {
|
|
86613
86914
|
try {
|
|
86614
|
-
|
|
86915
|
+
unlinkSync2(temp);
|
|
86615
86916
|
} catch {
|
|
86616
86917
|
}
|
|
86617
86918
|
return false;
|
|
86618
86919
|
}
|
|
86619
|
-
|
|
86920
|
+
renameSync2(temp, plistPath);
|
|
86620
86921
|
return true;
|
|
86621
86922
|
}
|
|
86622
86923
|
buildSmartMailboxEntry(name2, fromContains = "", subjectContains = "", bodyContains = "") {
|
|
@@ -86709,19 +87010,19 @@ ${this.errorEmit(" ")}
|
|
|
86709
87010
|
const cleanupEntryFiles = () => {
|
|
86710
87011
|
for (const f of [entryJson, entryPlist]) {
|
|
86711
87012
|
try {
|
|
86712
|
-
|
|
87013
|
+
unlinkSync2(f);
|
|
86713
87014
|
} catch {
|
|
86714
87015
|
}
|
|
86715
87016
|
}
|
|
86716
87017
|
};
|
|
86717
|
-
|
|
87018
|
+
writeFileSync4(entryJson, JSON.stringify([entry]), "utf8");
|
|
86718
87019
|
const conv = spawnSync2("plutil", ["-convert", "xml1", "-o", entryPlist, entryJson], {
|
|
86719
87020
|
encoding: "utf8"
|
|
86720
87021
|
});
|
|
86721
87022
|
if (conv.status !== 0) {
|
|
86722
87023
|
cleanupEntryFiles();
|
|
86723
87024
|
try {
|
|
86724
|
-
|
|
87025
|
+
unlinkSync2(temp);
|
|
86725
87026
|
} catch {
|
|
86726
87027
|
}
|
|
86727
87028
|
return {
|
|
@@ -86736,7 +87037,7 @@ ${this.errorEmit(" ")}
|
|
|
86736
87037
|
cleanupEntryFiles();
|
|
86737
87038
|
if (ins.status !== 0) {
|
|
86738
87039
|
try {
|
|
86739
|
-
|
|
87040
|
+
unlinkSync2(temp);
|
|
86740
87041
|
} catch {
|
|
86741
87042
|
}
|
|
86742
87043
|
return {
|
|
@@ -86779,7 +87080,7 @@ ${this.errorEmit(" ")}
|
|
|
86779
87080
|
});
|
|
86780
87081
|
if (del.status !== 0) {
|
|
86781
87082
|
try {
|
|
86782
|
-
|
|
87083
|
+
unlinkSync2(temp);
|
|
86783
87084
|
} catch {
|
|
86784
87085
|
}
|
|
86785
87086
|
return { deleted: false, error: (del.stderr || "PlistBuddy delete failed").trim() };
|
|
@@ -87002,7 +87303,10 @@ end tell`;
|
|
|
87002
87303
|
}
|
|
87003
87304
|
/**
|
|
87004
87305
|
* Fetches canonical mailbox paths for an account directly from Mail.app.
|
|
87005
|
-
* 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.
|
|
87006
87310
|
*/
|
|
87007
87311
|
fetchMailboxNames(account) {
|
|
87008
87312
|
const body = `
|
|
@@ -87015,7 +87319,8 @@ end tell`;
|
|
|
87015
87319
|
return mbNames
|
|
87016
87320
|
`;
|
|
87017
87321
|
const script = isLocalStoreLabel(account) ? buildAppLevelScript(`${localMailboxBindingFragment()}${body}`) : buildAccountScopedScript(account, body);
|
|
87018
|
-
const result = executeAppleScript(script);
|
|
87322
|
+
const result = executeAppleScript(guardMailRunning(script));
|
|
87323
|
+
if (result.success && result.output.startsWith(MAIL_NOT_RUNNING_MARKER)) return null;
|
|
87019
87324
|
if (!result.success || !result.output) {
|
|
87020
87325
|
return [];
|
|
87021
87326
|
}
|
|
@@ -87501,7 +87806,7 @@ ${actionStmts.join("\n")}
|
|
|
87501
87806
|
|
|
87502
87807
|
// src/index.ts
|
|
87503
87808
|
init_smtpMailer();
|
|
87504
|
-
import { writeFileSync as
|
|
87809
|
+
import { writeFileSync as writeFileSync5 } from "fs";
|
|
87505
87810
|
import { join as joinPath } from "path";
|
|
87506
87811
|
|
|
87507
87812
|
// src/tools/compose.ts
|
|
@@ -88656,19 +88961,19 @@ var ImapIdleWatcher = class {
|
|
|
88656
88961
|
};
|
|
88657
88962
|
|
|
88658
88963
|
// src/services/fileConfig.ts
|
|
88659
|
-
import { existsSync as existsSync4, readFileSync as
|
|
88660
|
-
import { join as
|
|
88661
|
-
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";
|
|
88662
88967
|
function fileConfigPath(env = process.env) {
|
|
88663
88968
|
const override = env.APPLE_MAIL_MCP_CONFIG_FILE;
|
|
88664
88969
|
if (override && override.trim()) return override.trim();
|
|
88665
|
-
return
|
|
88970
|
+
return join7(homedir6(), "Library", "Application Support", "apple-mail-mcp", "config.json");
|
|
88666
88971
|
}
|
|
88667
88972
|
function loadFileConfig(env = process.env, path3 = fileConfigPath(env)) {
|
|
88668
88973
|
const applied = [];
|
|
88669
88974
|
try {
|
|
88670
88975
|
if (!existsSync4(path3)) return applied;
|
|
88671
|
-
const parsed = JSON.parse(
|
|
88976
|
+
const parsed = JSON.parse(readFileSync4(path3, "utf8"));
|
|
88672
88977
|
if (!parsed || typeof parsed !== "object") return applied;
|
|
88673
88978
|
for (const [k, v] of Object.entries(parsed)) {
|
|
88674
88979
|
if (typeof v !== "string") continue;
|
|
@@ -89414,7 +89719,7 @@ registerTool(
|
|
|
89414
89719
|
} catch (error3) {
|
|
89415
89720
|
return errorResponse(error3 instanceof Error ? error3.message : String(error3));
|
|
89416
89721
|
}
|
|
89417
|
-
|
|
89722
|
+
writeFileSync5(target.savedPath, a.bytes, { flag: "wx", mode: 384 });
|
|
89418
89723
|
return successResponse(`${summary}. Written to ${target.savedPath}.`, {
|
|
89419
89724
|
...record2,
|
|
89420
89725
|
savedPath: target.savedPath
|
|
@@ -90343,7 +90648,7 @@ registerTool(
|
|
|
90343
90648
|
if (!r.success || !r.base64) {
|
|
90344
90649
|
return errorResponse(r.error || `Failed to fetch attachment "${attachmentName}"`);
|
|
90345
90650
|
}
|
|
90346
|
-
|
|
90651
|
+
writeFileSync5(target.savedPath, Buffer.from(r.base64, "base64"), { flag: "wx", mode: 384 });
|
|
90347
90652
|
return successResponse(`Attachment "${attachmentName}" saved to ${savePath}`, {
|
|
90348
90653
|
ok: true,
|
|
90349
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