apple-mail-mcp 2.10.33 → 2.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -12
- package/build/index.js +58 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1319,14 +1319,26 @@ returns the comparison in `structuredContent`:
|
|
|
1319
1319
|
}
|
|
1320
1320
|
```
|
|
1321
1321
|
|
|
1322
|
-
`status` is deliberately
|
|
1322
|
+
`status` is deliberately not a pass/fail flag:
|
|
1323
1323
|
|
|
1324
1324
|
| `status` | Meaning | Warns? |
|
|
1325
1325
|
|----------|---------|--------|
|
|
1326
1326
|
| `match` | Exactly as many messages left the mailbox as the operation acted on. | No |
|
|
1327
1327
|
| `over` | **More** left than were operated on. Messages are unaccounted for. | **Yes** |
|
|
1328
|
-
| `
|
|
1329
|
-
|
|
1328
|
+
| `unknown` | No comparison this server is willing to assert. `unknownReason` says which of four situations produced it. | No |
|
|
1329
|
+
|
|
1330
|
+
`unknownReason` distinguishes four cases that are *not* interchangeable:
|
|
1331
|
+
|
|
1332
|
+
| `unknownReason` | Meaning |
|
|
1333
|
+
|-----------------|---------|
|
|
1334
|
+
| `count-unreadable` | Mail would not report a count at all (`before`/`after` null). |
|
|
1335
|
+
| `no-expectation` | No expectation is predictable, so no comparison exists — a move whose destination **is** the source mailbox. |
|
|
1336
|
+
| `count-did-not-move` | The count did not move. On a store that flags deletions instead of removing them this is the **ordinary, correct** reading for an operation that fully succeeded. |
|
|
1337
|
+
| `count-partial` | The count moved, but by less than the operation accounted for. A flag-only store cannot produce this, which is why it is worth telling apart. |
|
|
1338
|
+
|
|
1339
|
+
> **`under` was removed in 2.11.0.** It used to mean "fewer left than expected"
|
|
1340
|
+
> and was documented as routine. Field evidence retired it — see
|
|
1341
|
+
> [Why `observed` is a lower bound](#why-observed-is-a-lower-bound-155).
|
|
1330
1342
|
|
|
1331
1343
|
#### What an `over` warning does and does not tell you
|
|
1332
1344
|
|
|
@@ -1346,7 +1358,7 @@ trusted:
|
|
|
1346
1358
|
**Concurrent departure is the benign cause to rule out first**, and the warning
|
|
1347
1359
|
text says so. What the asymmetry argument actually buys is the other half:
|
|
1348
1360
|
concurrent *arrivals* cannot produce `over`, because a message arriving
|
|
1349
|
-
mid-operation *raises* the after-count and biases the reading
|
|
1361
|
+
mid-operation *raises* the after-count and biases the reading short.
|
|
1350
1362
|
That is why `over` is the interesting direction — a strong signal, not a proof.
|
|
1351
1363
|
|
|
1352
1364
|
Setting `APPLE_MAIL_MCP_AUDIT_LOG` is what settles which one you have: the
|
|
@@ -1354,12 +1366,42 @@ collateral diff below **names** the messages that disappeared, and "the
|
|
|
1354
1366
|
newsletter my rule files every morning" is a very different report from a message
|
|
1355
1367
|
nothing should have touched.
|
|
1356
1368
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1369
|
+
#### Why `observed` is a lower bound (#155)
|
|
1370
|
+
|
|
1371
|
+
**`observed` is the movement of Mail's count. It is a lower bound on how many
|
|
1372
|
+
messages left, not a count of how many left.**
|
|
1373
|
+
|
|
1374
|
+
On iCloud, @scottstern0325 ran the check that settled this: for two batches
|
|
1375
|
+
reporting `observed: 0`, the messages were located **in Trash**, matched by
|
|
1376
|
+
`date received` + sender against the audit log's pre-image. The deletes had
|
|
1377
|
+
happened. Mail's count had not caught up. Across four readings — 0 of 4, 0 of 1
|
|
1378
|
+
(a *single-id* delete), 15 of 16, and 14 of 15 — the shortfall bore no relation
|
|
1379
|
+
to batch size, which is what a lagging count looks like and not what a
|
|
1380
|
+
store-behaviour rule looks like.
|
|
1381
|
+
|
|
1382
|
+
So a short reading is not evidence about your operation. **The per-id outcomes
|
|
1383
|
+
are what report success; this number is not. Do not retry on the strength of
|
|
1384
|
+
it** — that is how a message gets deleted twice.
|
|
1385
|
+
|
|
1386
|
+
Two things follow:
|
|
1387
|
+
|
|
1388
|
+
- A count that does not move at all is still the ordinary reading on a store that
|
|
1389
|
+
flags deletions instead of removing them (Gmail label mailboxes, IMAP accounts
|
|
1390
|
+
with "move deleted messages to Trash" off). It reports
|
|
1391
|
+
`unknownReason: "count-did-not-move"` and says so, and it is never warned about
|
|
1392
|
+
— a warning that fires on every ordinary Gmail delete would be ignored exactly
|
|
1393
|
+
when it matters.
|
|
1394
|
+
- **To confirm where messages went, match them at the destination by `date
|
|
1395
|
+
received` plus sender — not by the numeric ids you passed.** Ids are renumbered
|
|
1396
|
+
by the move and do not survive it.
|
|
1397
|
+
|
|
1398
|
+
**Removed in 2.11.0: the "reported success with no observed effect" warning.**
|
|
1399
|
+
Shipped in 2.10.30, it fired when the count was flat, the snapshot read cleanly
|
|
1400
|
+
and nothing had disappeared. Its premise was that the snapshot corroborated the
|
|
1401
|
+
count — but both are read back-to-back in the same script, and the record that
|
|
1402
|
+
prompted it turns out to have had *both* instruments stale at once. It therefore
|
|
1403
|
+
fired on stores that had done exactly what they were asked. It is gone rather
|
|
1404
|
+
than narrowed; `over` is the only surviving assertion.
|
|
1363
1405
|
|
|
1364
1406
|
Three more honesty rules:
|
|
1365
1407
|
|
|
@@ -1489,8 +1531,15 @@ a hole, because a wrong name here is worse than a missing one:
|
|
|
1489
1531
|
| both | withheld | withheld |
|
|
1490
1532
|
|
|
1491
1533
|
**An absent field means "not computable", never "empty".** Check
|
|
1492
|
-
`"snapshot": "ok"` before reading `disappeared` as a clean bill of health
|
|
1493
|
-
|
|
1534
|
+
`"snapshot": "ok"` before reading `disappeared` as a clean bill of health.
|
|
1535
|
+
|
|
1536
|
+
> ⚠️ **`"ok"` is necessary, not sufficient.** The enumeration's range is bounded
|
|
1537
|
+
> by Mail's own message count, and #155 established that count can lag the
|
|
1538
|
+
> mutation. A count reading **low** truncates the enumeration silently — the
|
|
1539
|
+
> unread tail is never requested, so it never registers as a failed slice and the
|
|
1540
|
+
> status still says `"ok"` — and messages past that bound would then look like
|
|
1541
|
+
> they disappeared. Until that is fixed, treat a `disappeared` entry as a lead to
|
|
1542
|
+
> check, not a proof.
|
|
1494
1543
|
|
|
1495
1544
|
### Privacy, and what the file costs you
|
|
1496
1545
|
|
package/build/index.js
CHANGED
|
@@ -78071,11 +78071,17 @@ function sleep(ms) {
|
|
|
78071
78071
|
}
|
|
78072
78072
|
}
|
|
78073
78073
|
}
|
|
78074
|
+
var PERMISSION_DENIED_PATTERN = /not authorized|not permitted|access.*denied/i;
|
|
78075
|
+
var PERMISSION_DENIED_MESSAGE = "Permission denied. Grant automation access in System Settings > Privacy & Security > Automation.";
|
|
78076
|
+
function isPermissionDenied(error2) {
|
|
78077
|
+
if (!error2) return false;
|
|
78078
|
+
return PERMISSION_DENIED_PATTERN.test(error2) || error2.includes(PERMISSION_DENIED_MESSAGE);
|
|
78079
|
+
}
|
|
78074
78080
|
var ERROR_MAPPINGS = [
|
|
78075
78081
|
// Permission errors
|
|
78076
78082
|
{
|
|
78077
|
-
pattern:
|
|
78078
|
-
message:
|
|
78083
|
+
pattern: PERMISSION_DENIED_PATTERN,
|
|
78084
|
+
message: PERMISSION_DENIED_MESSAGE
|
|
78079
78085
|
},
|
|
78080
78086
|
// Application not running
|
|
78081
78087
|
{
|
|
@@ -78854,17 +78860,8 @@ function countDeltaWarning(d) {
|
|
|
78854
78860
|
const where = d.account ? `"${d.mailbox}" in account "${d.account}"` : `"${d.mailbox}"`;
|
|
78855
78861
|
return `\u26A0\uFE0F Effect mismatch in ${where}: ${d.observed} message(s) left the mailbox but only ${d.expected} were operated on (count ${d.before} \u2192 ${d.after}). ${extra} message(s) are unaccounted for. Anything else removing mail from this mailbox at the same moment \u2014 a Mail rule, a server-side filter, another client, an IMAP expunge \u2014 reads the same way, so rule that out first. If nothing else was touching it, this is the signature of https://github.com/sweetrb/apple-mail-mcp/issues/155 \u2014 please report it there, and set ${AUDIT_LOG_ENV}=/path/to/audit.ndjson to capture which messages disappeared.`;
|
|
78856
78862
|
}
|
|
78857
|
-
function falseOkWarning(d, collateral) {
|
|
78858
|
-
if (d.status !== "under" || d.expected === null) return null;
|
|
78859
|
-
if (d.observed !== 0 || d.expected <= 0) return null;
|
|
78860
|
-
if (!collateral || collateral.snapshot !== "ok") return null;
|
|
78861
|
-
if ((collateral.disappeared?.length ?? 0) !== 0) return null;
|
|
78862
|
-
const where = d.account ? `"${d.mailbox}" in account "${d.account}"` : `"${d.mailbox}"`;
|
|
78863
|
-
return `\u26A0\uFE0F Reported success with no observed effect in ${where}: ${d.expected} message(s) were operated on and reported ok, but the mailbox count did not move (${d.before} \u2192 ${d.after}) and the collateral snapshot \u2014 which read this mailbox successfully \u2014 shows no message left it. Either the operation silently did nothing, or Mail's count and listing are both stale and the messages did move. Check the destination (Trash for a delete) before assuming either: if they are there, this is a measurement-timing bug; if they are still in the source mailbox minutes later, the operation failed while reporting success. Please report at https://github.com/sweetrb/apple-mail-mcp/issues/155 with ${AUDIT_LOG_ENV}=/path/to/audit.ndjson set.`;
|
|
78864
|
-
}
|
|
78865
78863
|
function reconciliationWarnings(report) {
|
|
78866
|
-
|
|
78867
|
-
return report.countDeltas.flatMap((d) => [countDeltaWarning(d), falseOkWarning(d, collateralFor(d))]).filter((w) => w !== null);
|
|
78864
|
+
return report.countDeltas.map((d) => countDeltaWarning(d)).filter((w) => w !== null);
|
|
78868
78865
|
}
|
|
78869
78866
|
|
|
78870
78867
|
// src/services/appleMailManager.ts
|
|
@@ -79142,6 +79139,8 @@ function canonicalNumericId(raw) {
|
|
|
79142
79139
|
return Number.isFinite(n) ? String(n) : trimmed;
|
|
79143
79140
|
}
|
|
79144
79141
|
var SELF_MOVE_NOTE = "Destination is the source mailbox, so no message should leave it. What Mail does to the count when a message is re-filed into the mailbox it already occupies is unspecified, so there is no expected delta to compare against: this mailbox is reported without a comparison and is never warned about.";
|
|
79142
|
+
var COUNT_UNMOVED_NOTE = `Mail's count did not move. This is the ordinary reading on a store that flags deletions instead of removing them (Gmail label mailboxes and IMAP accounts with "move deleted messages to Trash" off), where the message stays put and the operation still fully succeeded. It can also mean Mail's count simply had not caught up yet. The per-id outcomes are what report success; this number is not, so do not retry on the strength of it.`;
|
|
79143
|
+
var COUNT_PARTIAL_NOTE = `Mail's count moved by less than this operation accounted for. That is a LOWER BOUND on what left, not a count of what left: Mail has been observed reporting a stale count for a delete it had already performed (issue #155), and new mail arriving mid-operation reads the same way. No claim is made either way. To confirm where the messages went, match them at the destination by "date received" plus sender \u2014 NOT by the numeric ids you passed, which are renumbered by the move and do not survive it.`;
|
|
79145
79144
|
function snapshotKey(entry) {
|
|
79146
79145
|
return `${entry.id}\0${entry.messageId}`;
|
|
79147
79146
|
}
|
|
@@ -79679,10 +79678,24 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79679
79678
|
const observed = readable ? m.before - m.after : null;
|
|
79680
79679
|
const note = noteFor(m.account, m.mailbox);
|
|
79681
79680
|
let status;
|
|
79682
|
-
|
|
79683
|
-
|
|
79684
|
-
|
|
79685
|
-
|
|
79681
|
+
let unknownReason;
|
|
79682
|
+
if (!readable) {
|
|
79683
|
+
status = "unknown";
|
|
79684
|
+
unknownReason = "count-unreadable";
|
|
79685
|
+
} else if (m.expected === null) {
|
|
79686
|
+
status = "unknown";
|
|
79687
|
+
unknownReason = "no-expectation";
|
|
79688
|
+
} else if (observed === m.expected) {
|
|
79689
|
+
status = "match";
|
|
79690
|
+
} else if ((observed ?? 0) > m.expected) {
|
|
79691
|
+
status = "over";
|
|
79692
|
+
} else if (observed === 0) {
|
|
79693
|
+
status = "unknown";
|
|
79694
|
+
unknownReason = "count-did-not-move";
|
|
79695
|
+
} else {
|
|
79696
|
+
status = "unknown";
|
|
79697
|
+
unknownReason = "count-partial";
|
|
79698
|
+
}
|
|
79686
79699
|
return {
|
|
79687
79700
|
account: m.account,
|
|
79688
79701
|
mailbox: m.mailbox,
|
|
@@ -79691,11 +79704,11 @@ ${indent}end try${this.sanitizeFragment("_uacct", indent)}${this.sanitizeFragmen
|
|
|
79691
79704
|
expected: m.expected,
|
|
79692
79705
|
observed,
|
|
79693
79706
|
status,
|
|
79707
|
+
...unknownReason ? { unknownReason } : {},
|
|
79694
79708
|
...note ? { note } : {},
|
|
79695
|
-
...
|
|
79696
|
-
...
|
|
79697
|
-
|
|
79698
|
-
} : {}
|
|
79709
|
+
...unknownReason === "count-unreadable" ? { note: note ?? "Mail did not report a message count for this mailbox" } : {},
|
|
79710
|
+
...unknownReason === "count-did-not-move" && !note ? { note: COUNT_UNMOVED_NOTE } : {},
|
|
79711
|
+
...unknownReason === "count-partial" && !note ? { note: COUNT_PARTIAL_NOTE } : {}
|
|
79699
79712
|
};
|
|
79700
79713
|
});
|
|
79701
79714
|
const preImages = [];
|
|
@@ -82834,7 +82847,7 @@ ${actionStmts.join("\n")}
|
|
|
82834
82847
|
message: "Mail.app is accessible"
|
|
82835
82848
|
});
|
|
82836
82849
|
} else {
|
|
82837
|
-
const errorHint = mailCheck.error
|
|
82850
|
+
const errorHint = isPermissionDenied(mailCheck.error) ? " (check System Settings > Privacy & Security > Automation)" : "";
|
|
82838
82851
|
checks.push({
|
|
82839
82852
|
name: "mail_app",
|
|
82840
82853
|
passed: false,
|
|
@@ -82850,7 +82863,7 @@ ${actionStmts.join("\n")}
|
|
|
82850
82863
|
message: "AppleScript automation permissions granted"
|
|
82851
82864
|
});
|
|
82852
82865
|
} else {
|
|
82853
|
-
const isPermError =
|
|
82866
|
+
const isPermError = isPermissionDenied(permCheck.error);
|
|
82854
82867
|
checks.push({
|
|
82855
82868
|
name: "permissions",
|
|
82856
82869
|
passed: !isPermError,
|
|
@@ -83674,7 +83687,12 @@ function structuredRow(m, account, path) {
|
|
|
83674
83687
|
flagColorIndex: mailFlagColorIndex(m.flags),
|
|
83675
83688
|
mailbox: path,
|
|
83676
83689
|
account,
|
|
83677
|
-
|
|
83690
|
+
// Derived from BODYSTRUCTURE, which the list/search fetch now requests.
|
|
83691
|
+
// This was hardcoded `false` from 2.2.0 until 2.11.1 — indistinguishable to
|
|
83692
|
+
// a caller from "no attachments", so every IMAP-sourced message claimed to
|
|
83693
|
+
// have none. Falls back to false only when the fetch carried no
|
|
83694
|
+
// BODYSTRUCTURE at all.
|
|
83695
|
+
hasAttachments: bodyStructureHasAttachments(m.bodyStructure),
|
|
83678
83696
|
// Message-ID (when the envelope carries it) is the strongest cross-/intra-
|
|
83679
83697
|
// backend dedup key for the multi-account merge (imapMultiAccount.ts). The
|
|
83680
83698
|
// AppleScript path does not expose it, so cross-backend dedup falls back to
|
|
@@ -83705,7 +83723,10 @@ async function run(args, listMode, deps) {
|
|
|
83705
83723
|
const byUid = /* @__PURE__ */ new Map();
|
|
83706
83724
|
for await (const msg of client.fetch(
|
|
83707
83725
|
newest.join(","),
|
|
83708
|
-
|
|
83726
|
+
// BODYSTRUCTURE rides along so `hasAttachments` is computed rather
|
|
83727
|
+
// than assumed. Measured on 50 real messages: ~390ms -> ~465ms for
|
|
83728
|
+
// the fetch (~17%), same single round trip, no extra request.
|
|
83729
|
+
{ envelope: true, flags: true, bodyStructure: true },
|
|
83709
83730
|
{ uid: true }
|
|
83710
83731
|
)) {
|
|
83711
83732
|
byUid.set(msg.uid, msg);
|
|
@@ -84199,7 +84220,8 @@ function collectAttachments(node, out = []) {
|
|
|
84199
84220
|
if (!node) return out;
|
|
84200
84221
|
const filename = node.dispositionParameters?.filename || node.parameters?.name;
|
|
84201
84222
|
const disposition = node.disposition?.toLowerCase();
|
|
84202
|
-
const
|
|
84223
|
+
const isEmbeddedByReference = disposition === "inline" && !!node.id;
|
|
84224
|
+
const isAttachment = !!node.part && (disposition === "attachment" || !!filename && !isEmbeddedByReference);
|
|
84203
84225
|
if (isAttachment) {
|
|
84204
84226
|
out.push({
|
|
84205
84227
|
part: node.part,
|
|
@@ -84211,6 +84233,9 @@ function collectAttachments(node, out = []) {
|
|
|
84211
84233
|
for (const child of node.childNodes ?? []) collectAttachments(child, out);
|
|
84212
84234
|
return out;
|
|
84213
84235
|
}
|
|
84236
|
+
function bodyStructureHasAttachments(node) {
|
|
84237
|
+
return !!node && collectAttachments(node).length > 0;
|
|
84238
|
+
}
|
|
84214
84239
|
async function streamToBuffer(content, maxBytes) {
|
|
84215
84240
|
const chunks = [];
|
|
84216
84241
|
let total = 0;
|
|
@@ -84382,7 +84407,10 @@ async function imapThread(id, deps = {}, limit = 50) {
|
|
|
84382
84407
|
const msgs = [];
|
|
84383
84408
|
for await (const msg of client.fetch(
|
|
84384
84409
|
uids.join(","),
|
|
84385
|
-
|
|
84410
|
+
// Same reason as the list/search fetch: get-thread emits structured
|
|
84411
|
+
// rows too, so it needs BODYSTRUCTURE or its hasAttachments would
|
|
84412
|
+
// silently disagree with the same message seen via search.
|
|
84413
|
+
{ envelope: true, flags: true, bodyStructure: true },
|
|
84386
84414
|
{ uid: true }
|
|
84387
84415
|
)) {
|
|
84388
84416
|
msgs.push(msg);
|
|
@@ -85303,7 +85331,10 @@ var COUNT_DELTA_OUTPUT_SCHEMA = external_exports.array(
|
|
|
85303
85331
|
// `status: "unknown"`, and never with a warning.
|
|
85304
85332
|
expected: external_exports.number().nullable().optional(),
|
|
85305
85333
|
observed: external_exports.number().nullable().optional(),
|
|
85306
|
-
status: external_exports.enum(["match", "over", "
|
|
85334
|
+
status: external_exports.enum(["match", "over", "unknown"]).optional(),
|
|
85335
|
+
// Declared explicitly: the SDK stamps additionalProperties:false on a bare
|
|
85336
|
+
// zod shape, so an undeclared key makes the CLIENT reject the result.
|
|
85337
|
+
unknownReason: external_exports.enum(["count-unreadable", "no-expectation", "count-did-not-move", "count-partial"]).optional(),
|
|
85307
85338
|
note: external_exports.string().optional()
|
|
85308
85339
|
})
|
|
85309
85340
|
).optional();
|
package/package.json
CHANGED