apple-mail-mcp 2.5.0 → 2.6.0
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 +35 -4
- package/build/index.js +332 -48
- package/build/services/imapClient.d.ts +14 -0
- package/build/services/imapClient.d.ts.map +1 -1
- package/build/services/imapClient.js +21 -0
- package/build/services/imapMultiAccount.d.ts +124 -0
- package/build/services/imapMultiAccount.d.ts.map +1 -0
- package/build/services/imapMultiAccount.js +253 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -373,7 +373,7 @@ What routes to IMAP when an account is IMAP-configured:
|
|
|
373
373
|
- **Folder ops:** `create-mailbox`, `rename-mailbox`, `delete-mailbox` — IMAP's `CREATE`/`RENAME`/`DELETE` succeed on the iCloud/Gmail/Workspace/Exchange mailboxes Mail.app's AppleScript bridge can't touch (#42).
|
|
374
374
|
- **Message mutations:** `mark-as-read`/`unread`, `flag-message`/`unflag-message`, `move-message`, `delete-message`.
|
|
375
375
|
- **Batch mutations (2.1):** `batch-mark-as-read`/`unread`, `batch-flag`/`unflag-messages`, `batch-move-messages`, `batch-delete-messages` — `imap:` ids are grouped by mailbox and applied as a single `UID STORE`/`UID MOVE`; numeric ids in the same batch still use AppleScript.
|
|
376
|
-
- **Counts & stats (2.1):** `get-unread-count` and `list-mailboxes` use `STATUS`; `get-mail-stats`
|
|
376
|
+
- **Counts & stats (2.1):** `get-unread-count` and `list-mailboxes` use `STATUS`; `get-mail-stats` uses `STATUS` + `SEARCH SINCE` — authoritative and fast even on huge mailboxes. As of v2.6.0 these prefer IMAP whenever it's configured (see *Read routing* below), merging across accounts when no `account` is given.
|
|
377
377
|
- **Attachments (2.1):** `list-attachments`, `save-attachment`, `fetch-attachment` use `BODYSTRUCTURE` + `FETCH BODY[part]` for `imap:` ids — faster and able to see MIME-embedded attachments AppleScript misses.
|
|
378
378
|
- **Threading (2.1):** `get-thread` links a conversation via `References`/`Message-ID` (`HEADER SEARCH`) for an `imap:` seed, falling back to subject grouping otherwise.
|
|
379
379
|
|
|
@@ -384,9 +384,34 @@ attachment/thread tools and it routes to IMAP automatically; bare numeric ids
|
|
|
384
384
|
continue to use AppleScript. So an agent never has to know which backend a
|
|
385
385
|
message came from — the id carries it.
|
|
386
386
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
387
|
+
**Read routing (v2.6.0): reads PREFER direct IMAP whenever IMAP is configured.**
|
|
388
|
+
The read tools — `search-messages`, `get-thread`, `list-messages`,
|
|
389
|
+
`list-mailboxes`, `get-unread-count`, `get-mail-stats` — now go to IMAP whenever
|
|
390
|
+
any `APPLE_MAIL_MCP_IMAP_*` account is configured, not just when an explicit
|
|
391
|
+
matching `account` is passed. There are three cases:
|
|
392
|
+
|
|
393
|
+
- **Explicit IMAP account** — single-account IMAP (fast server-side path).
|
|
394
|
+
- **Explicit non-IMAP account** — AppleScript (that account isn't on IMAP).
|
|
395
|
+
- **No `account` given** — **merge across all accounts**: the query fans out over
|
|
396
|
+
*every* configured IMAP account, **and** AppleScript runs **only for the
|
|
397
|
+
accounts no IMAP config covers** (the account list is partitioned — accounts
|
|
398
|
+
already served by IMAP are *not* re-scanned via AppleScript). If every Mail
|
|
399
|
+
account is IMAP-configured, AppleScript is skipped entirely. The results are
|
|
400
|
+
merged so no account is dropped. Message lists still de-duplicate as a safety
|
|
401
|
+
net (preferring the IMAP copy, which carries the round-trippable `imap:` id) and
|
|
402
|
+
sort newest-first; count tools (`get-unread-count`, `get-mail-stats`) count each
|
|
403
|
+
account via exactly one backend so a coverage mismatch can never double- (or
|
|
404
|
+
under-) count.
|
|
405
|
+
- **Default mailbox is resolved per account.** When you don't pin a `mailbox`, a
|
|
406
|
+
fan-out search scopes each account to its own default — Gmail/Workspace to
|
|
407
|
+
`[Gmail]/All Mail`, every other IMAP host (iCloud, etc.) to `INBOX` (since
|
|
408
|
+
`[Gmail]/All Mail` is Gmail-only and selecting it elsewhere would silently drop
|
|
409
|
+
that account). Pin a `mailbox` to search a wider scope on non-Gmail accounts.
|
|
410
|
+
|
|
411
|
+
If IMAP is **not** configured at all, every read behaves exactly as before
|
|
412
|
+
(pure AppleScript). The three mailbox-**write** ops (`create-mailbox`,
|
|
413
|
+
`delete-mailbox`, `rename-mailbox`) remain conservative — they route to IMAP only
|
|
414
|
+
for an explicitly-named IMAP account, never on an omitted account.
|
|
390
415
|
|
|
391
416
|
| Variable | Required | Default | Description |
|
|
392
417
|
|----------|----------|---------|-------------|
|
|
@@ -596,6 +621,8 @@ Reply to an existing message.
|
|
|
596
621
|
}
|
|
597
622
|
```
|
|
598
623
|
|
|
624
|
+
> **Transport (v2.5.0):** when SMTP is configured, `reply-to-message` sends via **clean SMTP**, threading the reply with proper RFC 5322 `In-Reply-To`/`References` headers (built from the original message) so it lands in the same conversation. When SMTP is not configured (or the original lacks the headers needed to thread), it falls back to Mail.app's AppleScript `reply … without opening window` — same reliable-from-background-process path as before. See [SMTP transport](#smtp-transport).
|
|
625
|
+
|
|
599
626
|
**⚠️ Safety:** With the default `send: true`, sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling (or pass `send: false` to save a draft for review).
|
|
600
627
|
|
|
601
628
|
---
|
|
@@ -611,6 +638,8 @@ Forward a message to new recipients.
|
|
|
611
638
|
| `body` | string | No | Message to prepend |
|
|
612
639
|
| `send` | boolean | No | Send immediately (default: true, false = save as draft) |
|
|
613
640
|
|
|
641
|
+
> **Transport (v2.5.0):** when SMTP is configured, `forward-message` sends via **clean SMTP** (a fresh message with the original quoted, no threading headers — a forward starts a new conversation). When SMTP is not configured it falls back to Mail.app's AppleScript `forward … without opening window`. See [SMTP transport](#smtp-transport).
|
|
642
|
+
|
|
614
643
|
**⚠️ Safety:** With the default `send: true`, sends real mail immediately and cannot be unsent. Confirm the recipients, subject, and body with the user before calling (or pass `send: false` to save a draft for review).
|
|
615
644
|
|
|
616
645
|
---
|
|
@@ -1128,6 +1157,8 @@ Prior to v1.4.0, `reply-to-message` and `forward-message` would send messages wi
|
|
|
1128
1157
|
|
|
1129
1158
|
**Fix:** Replaced `with opening window` with `without opening window` for both `reply` and `forward` commands. With this approach, `set content` works immediately and reliably from background processes. `In-Reply-To` and `References` headers are still set correctly by Mail.app, and no GUI compose window is opened.
|
|
1130
1159
|
|
|
1160
|
+
**Update (v2.5.0):** when SMTP is configured, `reply-to-message` and `forward-message` now prefer **clean direct SMTP** instead of AppleScript — the same prefer-direct model as `send-email`. Replies are threaded with RFC 5322 `In-Reply-To`/`References` headers built from the original message; forwards start a new conversation. The AppleScript `without opening window` path above remains the fallback when SMTP is not configured (or, for replies, when the original message lacks the headers needed to thread).
|
|
1161
|
+
|
|
1131
1162
|
See [#7](https://github.com/sweetrb/apple-mail-mcp/issues/7) for full details and the list of approaches that were tested.
|
|
1132
1163
|
|
|
1133
1164
|
### Backslash Escaping (Important for AI Agents)
|
package/build/index.js
CHANGED
|
@@ -28,8 +28,9 @@ import { writeFileSync } from "fs";
|
|
|
28
28
|
import { resolve as resolvePath, join as joinPath } from "path";
|
|
29
29
|
import { sendViaSmtp, sendSerialViaSmtp, shouldUseSmtp, isSmtpConfigured, resolveSmtpConfig, } from "./services/smtpMailer.js";
|
|
30
30
|
import { buildReplyOptions, buildForwardOptions, parseOriginalHeaders, } from "./services/replyForward.js";
|
|
31
|
-
import { isImapAccount, resolveImapConfigs, dropAllPools, imapSearchMessages, imapListMessages, imapUnreadCount, imapListMailboxes, imapMailStats, imapListAttachments, imapFetchAttachment, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete, imapBatchMove, imapThread, imapCreateMailbox, imapDeleteMailbox, imapRenameMailbox, imapGetMessage, imapMarkRead, imapMarkUnread, imapFlagMessage, imapUnflagMessage, imapMoveMessageById, imapDeleteMessageById, } from "./services/imapClient.js";
|
|
31
|
+
import { isImapAccount, shouldUseImap, resolveImapConfigs, dropAllPools, imapSearchMessages, imapListMessages, imapUnreadCount, imapListMailboxes, imapMailStats, imapListAttachments, imapFetchAttachment, imapBatchMarkRead, imapBatchMarkUnread, imapBatchFlag, imapBatchUnflag, imapBatchDelete, imapBatchMove, imapThread, imapCreateMailbox, imapDeleteMailbox, imapRenameMailbox, imapGetMessage, imapMarkRead, imapMarkUnread, imapFlagMessage, imapUnflagMessage, imapMoveMessageById, imapDeleteMessageById, } from "./services/imapClient.js";
|
|
32
32
|
import { successResponse, errorResponse, partialCoverageBlock, withErrorHandling, messageSummary, } from "./tools/respond.js";
|
|
33
|
+
import { fanOutImapMessages, mergeMessages, formatMergedRows, partitionAccountsForCounts, planCountSources, } from "./services/imapMultiAccount.js";
|
|
33
34
|
import { routeMessage } from "./services/messageRouter.js";
|
|
34
35
|
import { runDoctor, formatDoctorReport } from "./tools/doctor.js";
|
|
35
36
|
import { registerResourcesAndPrompts } from "./tools/resourcesAndPrompts.js";
|
|
@@ -116,6 +117,83 @@ const CHECK_ITEM_SCHEMA = z.object({}).passthrough();
|
|
|
116
117
|
// Read version from package.json to keep it in sync
|
|
117
118
|
const require = createRequire(import.meta.url);
|
|
118
119
|
const { version } = require("../package.json");
|
|
120
|
+
/** An empty SearchDiagnostics (complete coverage). */
|
|
121
|
+
function emptyDiagnostics() {
|
|
122
|
+
return {
|
|
123
|
+
partial: false,
|
|
124
|
+
timedOutAccounts: [],
|
|
125
|
+
skippedLargeMailboxes: [],
|
|
126
|
+
notSearchedMailboxes: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** Merge two diagnostics objects (union the lists, OR the partial flag). */
|
|
130
|
+
function mergeDiagnostics(a, b) {
|
|
131
|
+
return {
|
|
132
|
+
partial: a.partial || b.partial,
|
|
133
|
+
timedOutAccounts: [...a.timedOutAccounts, ...b.timedOutAccounts],
|
|
134
|
+
skippedLargeMailboxes: [...a.skippedLargeMailboxes, ...b.skippedLargeMailboxes],
|
|
135
|
+
notSearchedMailboxes: [...a.notSearchedMailboxes, ...b.notSearchedMailboxes],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Run a per-account AppleScript scan over ONLY the given accounts (the ones no
|
|
140
|
+
* IMAP config covers), concatenating their rows and merging diagnostics. The IMAP
|
|
141
|
+
* fan-out already covers the IMAP accounts, so this avoids scanning them on the
|
|
142
|
+
* AppleScript side — which, for an all-IMAP user, means ZERO AppleScript work and
|
|
143
|
+
* therefore no reliance on the fragile composite dedup. `scan` runs one account.
|
|
144
|
+
*/
|
|
145
|
+
function appleScanForAccounts(accounts, scan) {
|
|
146
|
+
const rows = [];
|
|
147
|
+
let diagnostics = emptyDiagnostics();
|
|
148
|
+
for (const acct of accounts) {
|
|
149
|
+
const res = scan(acct.name);
|
|
150
|
+
rows.push(...res.messages.map(messageSummary));
|
|
151
|
+
diagnostics = mergeDiagnostics(diagnostics, res.diagnostics);
|
|
152
|
+
}
|
|
153
|
+
return { rows, diagnostics };
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Build the success response for a no-account, prefer-IMAP MERGED message list
|
|
157
|
+
* (search-messages / list-messages — v2.6.0). Concatenates the IMAP fan-out rows
|
|
158
|
+
* with the (already partitioned) AppleScript rows, de-dups (IMAP copy wins — a
|
|
159
|
+
* safety net for heuristic misses + IMAP-vs-IMAP dupes, no longer load-bearing
|
|
160
|
+
* for matched accounts), sorts newest-first, applies `limit`, and preserves the
|
|
161
|
+
* partial-coverage diagnostics (AppleScript scan + any failed IMAP fan-out).
|
|
162
|
+
*
|
|
163
|
+
* @param verb "matched" (search) or "listed" (list) — only affects empty-state text.
|
|
164
|
+
*/
|
|
165
|
+
function mergedMessageResponse(fan, apple, limit, verb) {
|
|
166
|
+
const merged = mergeMessages(fan.rows, apple.rows, limit);
|
|
167
|
+
// Surface IMAP fan-out failures alongside the AppleScript diagnostics so a
|
|
168
|
+
// partial merge is never mistaken for a confirmed "no such mail".
|
|
169
|
+
const diagnostics = {
|
|
170
|
+
...apple.diagnostics,
|
|
171
|
+
partial: apple.diagnostics.partial || fan.accountsFailed.length > 0,
|
|
172
|
+
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
|
|
173
|
+
};
|
|
174
|
+
const structured = {
|
|
175
|
+
messages: merged,
|
|
176
|
+
count: merged.length,
|
|
177
|
+
partial: diagnostics.partial,
|
|
178
|
+
skippedLargeMailboxes: diagnostics.skippedLargeMailboxes,
|
|
179
|
+
notSearchedMailboxes: diagnostics.notSearchedMailboxes,
|
|
180
|
+
timedOutAccounts: diagnostics.timedOutAccounts,
|
|
181
|
+
};
|
|
182
|
+
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
183
|
+
if (merged.length === 0) {
|
|
184
|
+
const base = diagnostics.partial
|
|
185
|
+
? `No messages found in the portions that were ${verb === "matched" ? "searched" : "listed"}.`
|
|
186
|
+
: "No messages found";
|
|
187
|
+
return successResponse(`${base}${coverageBlock}`, structured);
|
|
188
|
+
}
|
|
189
|
+
const parts = [];
|
|
190
|
+
if (fan.accountsQueried.length > 0)
|
|
191
|
+
parts.push(`IMAP account(s): ${fan.accountsQueried.join(", ")}`);
|
|
192
|
+
if (apple.rows.length > 0)
|
|
193
|
+
parts.push("AppleScript");
|
|
194
|
+
const accountsNote = parts.length > 0 ? ` (merged across ${parts.join(" + ")})` : "";
|
|
195
|
+
return successResponse(`Found ${merged.length} message(s)${accountsNote}:\n${formatMergedRows(merged)}${coverageBlock}`, structured);
|
|
196
|
+
}
|
|
119
197
|
// =============================================================================
|
|
120
198
|
// Server Initialization
|
|
121
199
|
// =============================================================================
|
|
@@ -190,13 +268,17 @@ server.registerTool("search-messages", {
|
|
|
190
268
|
},
|
|
191
269
|
outputSchema: LIST_OUTPUT_SCHEMA,
|
|
192
270
|
}, withErrorHandling(async ({ query, mailbox, account, limit = 50, dateFrom, dateTo, from, subject, isRead, isFlagged, }) => {
|
|
193
|
-
// IMAP backend
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
271
|
+
// IMAP backend: prefer direct IMAP whenever IMAP is configured (v2.6.0).
|
|
272
|
+
// - explicit IMAP account → single-account IMAP (fast path);
|
|
273
|
+
// - no account + IMAP configured → MERGE: IMAP fans out over every
|
|
274
|
+
// configured account; AppleScript scans ONLY the accounts no IMAP
|
|
275
|
+
// config covers (partitioned — so an all-IMAP user runs ZERO
|
|
276
|
+
// AppleScript and never relies on the composite dedup);
|
|
277
|
+
// - explicit non-IMAP account (or IMAP unconfigured) → AppleScript below.
|
|
278
|
+
if (shouldUseImap(account)) {
|
|
279
|
+
const imapArgs = {
|
|
197
280
|
query,
|
|
198
281
|
mailbox,
|
|
199
|
-
account,
|
|
200
282
|
limit,
|
|
201
283
|
dateFrom,
|
|
202
284
|
dateTo,
|
|
@@ -204,12 +286,19 @@ server.registerTool("search-messages", {
|
|
|
204
286
|
subject,
|
|
205
287
|
isRead,
|
|
206
288
|
isFlagged,
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
289
|
+
};
|
|
290
|
+
if (account !== undefined) {
|
|
291
|
+
const r = await imapSearchMessages({ ...imapArgs, account });
|
|
292
|
+
return successResponse(r.text, {
|
|
293
|
+
messages: r.messages,
|
|
294
|
+
count: r.count,
|
|
295
|
+
partial: r.partial,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const fan = await fanOutImapMessages(imapArgs, "search");
|
|
299
|
+
const { appleScriptOnly } = partitionAccountsForCounts(mailManager.listAccounts(), resolveImapConfigs());
|
|
300
|
+
const apple = appleScanForAccounts(appleScriptOnly, (acctName) => mailManager.searchMessagesWithDiagnostics(query, mailbox, acctName, limit, dateFrom, dateTo, from, subject, isRead, isFlagged));
|
|
301
|
+
return mergedMessageResponse(fan, apple, limit, "matched");
|
|
213
302
|
}
|
|
214
303
|
const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(query, mailbox, account, limit, dateFrom, dateTo, from, subject, isRead, isFlagged);
|
|
215
304
|
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
@@ -323,15 +412,48 @@ server.registerTool("get-thread", {
|
|
|
323
412
|
if (!seedSubject)
|
|
324
413
|
return errorResponse(`Could not determine the subject of message "${id}"`);
|
|
325
414
|
const base = normalizeSubject(seedSubject);
|
|
326
|
-
// IMAP backend: server-side subject search.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
415
|
+
// IMAP backend: server-side subject search (prefer-IMAP, v2.6.0).
|
|
416
|
+
// - explicit IMAP account → single-account IMAP subject search;
|
|
417
|
+
// - no account + IMAP configured → MERGE: IMAP fan-out subject search over
|
|
418
|
+
// every configured account + AppleScript subject search over ONLY the
|
|
419
|
+
// accounts no IMAP config covers (partitioned), then re-order oldest-
|
|
420
|
+
// first for natural thread reading.
|
|
421
|
+
if (shouldUseImap(account)) {
|
|
422
|
+
if (account !== undefined) {
|
|
423
|
+
const r = await imapSearchMessages({ subject: base, mailbox, account, limit });
|
|
424
|
+
return successResponse(`Thread "${base}":\n${r.text}`, {
|
|
425
|
+
subject: base,
|
|
426
|
+
messages: r.messages,
|
|
427
|
+
count: r.count,
|
|
428
|
+
partial: r.partial,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const fan = await fanOutImapMessages({ subject: base, mailbox, limit }, "search");
|
|
432
|
+
const { appleScriptOnly } = partitionAccountsForCounts(mailManager.listAccounts(), resolveImapConfigs());
|
|
433
|
+
const apple = appleScanForAccounts(appleScriptOnly, (acctName) => mailManager.searchMessagesWithDiagnostics(undefined, mailbox, acctName, limit, undefined, undefined, undefined, base));
|
|
434
|
+
// Merge + de-dup (IMAP wins), then sort OLDEST-first for thread order.
|
|
435
|
+
const mergedNewestFirst = mergeMessages(fan.rows, apple.rows, limit);
|
|
436
|
+
const orderedRows = mergedNewestFirst
|
|
437
|
+
.slice()
|
|
438
|
+
.reverse() // mergeMessages returns newest-first; threads read oldest-first
|
|
439
|
+
.sort((a, b) => (a.dateReceived ? new Date(a.dateReceived).getTime() : 0) -
|
|
440
|
+
(b.dateReceived ? new Date(b.dateReceived).getTime() : 0));
|
|
441
|
+
const partial = apple.diagnostics.partial || fan.accountsFailed.length > 0;
|
|
442
|
+
const coverage = partialCoverageBlock({
|
|
443
|
+
...apple.diagnostics,
|
|
444
|
+
partial,
|
|
445
|
+
timedOutAccounts: [...apple.diagnostics.timedOutAccounts, ...fan.accountsFailed],
|
|
334
446
|
});
|
|
447
|
+
const structured = {
|
|
448
|
+
subject: base,
|
|
449
|
+
messages: orderedRows,
|
|
450
|
+
count: orderedRows.length,
|
|
451
|
+
partial,
|
|
452
|
+
};
|
|
453
|
+
if (orderedRows.length === 0) {
|
|
454
|
+
return successResponse(`No messages found in thread "${base}".${coverage}`, structured);
|
|
455
|
+
}
|
|
456
|
+
return successResponse(`Thread "${base}" — ${orderedRows.length} message(s), oldest first:\n${formatMergedRows(orderedRows)}${coverage}`, structured);
|
|
335
457
|
}
|
|
336
458
|
const { messages, diagnostics } = mailManager.searchMessagesWithDiagnostics(undefined, mailbox, account, limit, undefined, undefined, undefined, base);
|
|
337
459
|
// Oldest-first is the natural reading order for a conversation.
|
|
@@ -369,15 +491,27 @@ server.registerTool("list-messages", {
|
|
|
369
491
|
},
|
|
370
492
|
outputSchema: LIST_OUTPUT_SCHEMA,
|
|
371
493
|
}, withErrorHandling(async ({ mailbox, account, limit = 50, offset = 0, from, unreadOnly }) => {
|
|
372
|
-
// IMAP backend
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
494
|
+
// IMAP backend: prefer direct IMAP whenever IMAP is configured (v2.6.0).
|
|
495
|
+
// - explicit IMAP account → single-account IMAP listing (fast path);
|
|
496
|
+
// - no account + IMAP configured → MERGE: IMAP fans out over every
|
|
497
|
+
// configured account; AppleScript lists ONLY the accounts no IMAP config
|
|
498
|
+
// covers (partitioned — all-IMAP user runs ZERO AppleScript). NOTE:
|
|
499
|
+
// pagination via `offset` is applied PER-BACKEND before the merge, so
|
|
500
|
+
// deep offsets in a merged multi-account list are approximate — recommend
|
|
501
|
+
// scoping with an `account` for exact pagination.
|
|
502
|
+
if (shouldUseImap(account)) {
|
|
503
|
+
if (account !== undefined) {
|
|
504
|
+
const r = await imapListMessages({ mailbox, account, limit, offset, from, unreadOnly });
|
|
505
|
+
return successResponse(r.text, {
|
|
506
|
+
messages: r.messages,
|
|
507
|
+
count: r.count,
|
|
508
|
+
partial: r.partial,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
const fan = await fanOutImapMessages({ mailbox, limit, offset, from, unreadOnly }, "list");
|
|
512
|
+
const { appleScriptOnly } = partitionAccountsForCounts(mailManager.listAccounts(), resolveImapConfigs());
|
|
513
|
+
const apple = appleScanForAccounts(appleScriptOnly, (acctName) => mailManager.listMessagesWithDiagnostics(mailbox, acctName, limit, from, offset));
|
|
514
|
+
return mergedMessageResponse(fan, apple, limit, "listed");
|
|
381
515
|
}
|
|
382
516
|
const { messages, diagnostics } = mailManager.listMessagesWithDiagnostics(mailbox, account, limit, from, offset);
|
|
383
517
|
const coverageBlock = partialCoverageBlock(diagnostics);
|
|
@@ -1058,21 +1192,65 @@ server.registerTool("list-mailboxes", {
|
|
|
1058
1192
|
},
|
|
1059
1193
|
}, withErrorHandling(async ({ account }) => {
|
|
1060
1194
|
// IMAP (I6): LIST + per-mailbox STATUS — sees the true server hierarchy and
|
|
1061
|
-
// authoritative counts
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
})
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1195
|
+
// authoritative counts. Prefer-IMAP (v2.6.0):
|
|
1196
|
+
// - explicit IMAP account → that account's IMAP mailboxes;
|
|
1197
|
+
// - no account + IMAP configured → concatenate every configured IMAP
|
|
1198
|
+
// account's mailboxes (each name prefixed with its account label to
|
|
1199
|
+
// disambiguate identical mailbox names across accounts) PLUS the
|
|
1200
|
+
// AppleScript mailboxes of every account NOT covered by IMAP.
|
|
1201
|
+
if (shouldUseImap(account)) {
|
|
1202
|
+
if (account !== undefined) {
|
|
1203
|
+
const boxes = await imapListMailboxes({ account });
|
|
1204
|
+
const structured = {
|
|
1205
|
+
mailboxes: boxes.map((b) => ({
|
|
1206
|
+
name: b.path,
|
|
1207
|
+
unreadCount: b.unseen,
|
|
1208
|
+
messageCount: b.messages,
|
|
1209
|
+
})),
|
|
1210
|
+
count: boxes.length,
|
|
1211
|
+
};
|
|
1212
|
+
if (boxes.length === 0)
|
|
1213
|
+
return successResponse("No mailboxes found", structured);
|
|
1214
|
+
const list = boxes.map((b) => ` - ${b.path} (${b.unseen} unread)`).join("\n");
|
|
1215
|
+
return successResponse(`Found ${boxes.length} mailbox(es):\n${list}`, structured);
|
|
1216
|
+
}
|
|
1217
|
+
const configs = resolveImapConfigs();
|
|
1218
|
+
const rows = [];
|
|
1219
|
+
for (const config of configs) {
|
|
1220
|
+
try {
|
|
1221
|
+
const boxes = await imapListMailboxes({ config });
|
|
1222
|
+
for (const b of boxes) {
|
|
1223
|
+
// Prefix with the account label so "INBOX" from two accounts is
|
|
1224
|
+
// distinguishable; keep the raw path available via the structured row.
|
|
1225
|
+
rows.push({
|
|
1226
|
+
name: `${config.accountLabel}/${b.path}`,
|
|
1227
|
+
account: config.accountLabel,
|
|
1228
|
+
unreadCount: b.unseen,
|
|
1229
|
+
messageCount: b.messages,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
catch (e) {
|
|
1234
|
+
console.error(`IMAP list-mailboxes failed for "${config.accountLabel}": ${String(e)}`);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
// AppleScript for the accounts IMAP doesn't cover (no double-listing).
|
|
1238
|
+
const { appleScriptOnly } = partitionAccountsForCounts(mailManager.listAccounts(), configs);
|
|
1239
|
+
for (const acct of appleScriptOnly) {
|
|
1240
|
+
for (const mb of mailManager.listMailboxes(acct.name)) {
|
|
1241
|
+
rows.push({
|
|
1242
|
+
name: `${acct.name}/${mb.name}`,
|
|
1243
|
+
account: acct.name,
|
|
1244
|
+
unreadCount: mb.unreadCount,
|
|
1245
|
+
messageCount: mb.messageCount,
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
const structured = { mailboxes: rows, count: rows.length };
|
|
1250
|
+
if (rows.length === 0)
|
|
1073
1251
|
return successResponse("No mailboxes found", structured);
|
|
1074
|
-
const list =
|
|
1075
|
-
return successResponse(`Found ${
|
|
1252
|
+
const list = rows.map((b) => ` - ${b.name} (${b.unreadCount} unread)`).join("\n");
|
|
1253
|
+
return successResponse(`Found ${rows.length} mailbox(es):\n${list}`, structured);
|
|
1076
1254
|
}
|
|
1077
1255
|
const mailboxes = mailManager.listMailboxes(account);
|
|
1078
1256
|
const structured = { mailboxes, count: mailboxes.length };
|
|
@@ -1096,10 +1274,40 @@ server.registerTool("get-unread-count", {
|
|
|
1096
1274
|
},
|
|
1097
1275
|
}, withErrorHandling(async ({ mailbox, account }) => {
|
|
1098
1276
|
// IMAP (I4): STATUS (UNSEEN) is authoritative and fast even on huge
|
|
1099
|
-
// mailboxes
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1277
|
+
// mailboxes. Prefer-IMAP (v2.6.0). Counts are ACCOUNT-CENTRIC so each account
|
|
1278
|
+
// is counted exactly once even if the coverage heuristic mis-matches:
|
|
1279
|
+
// - explicit IMAP account → IMAP UNSEEN for that account;
|
|
1280
|
+
// - no account + IMAP configured → planCountSources assigns each account
|
|
1281
|
+
// ONE source (its matching IMAP config, else AppleScript) and counts any
|
|
1282
|
+
// config that matched no account once via IMAP — no double-counting;
|
|
1283
|
+
// - explicit non-IMAP account (or IMAP unconfigured) → AppleScript.
|
|
1284
|
+
let count;
|
|
1285
|
+
if (shouldUseImap(account)) {
|
|
1286
|
+
if (account !== undefined) {
|
|
1287
|
+
count = await imapUnreadCount(mailbox, { account });
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
const sources = planCountSources(mailManager.listAccounts(), resolveImapConfigs());
|
|
1291
|
+
let total = 0;
|
|
1292
|
+
for (const src of sources) {
|
|
1293
|
+
if (src.kind === "imap") {
|
|
1294
|
+
try {
|
|
1295
|
+
total += await imapUnreadCount(mailbox, { config: src.config });
|
|
1296
|
+
}
|
|
1297
|
+
catch (e) {
|
|
1298
|
+
console.error(`IMAP unread-count failed for "${src.label}": ${String(e)}`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
total += mailManager.getUnreadCount(mailbox, src.account.name);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
count = total;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
count = mailManager.getUnreadCount(mailbox, account);
|
|
1310
|
+
}
|
|
1103
1311
|
const location = mailbox ? ` in "${mailbox}"` : "";
|
|
1104
1312
|
return successResponse(`${count} unread message(s)${location}`, {
|
|
1105
1313
|
unread: count,
|
|
@@ -1538,7 +1746,12 @@ server.registerTool("get-mail-stats", {
|
|
|
1538
1746
|
}, withErrorHandling(async ({ account }) => {
|
|
1539
1747
|
// IMAP (I3): for a named IMAP account, STATUS gives authoritative counts and
|
|
1540
1748
|
// SEARCH SINCE gives recent activity — fast even on huge mailboxes.
|
|
1541
|
-
|
|
1749
|
+
// - explicit IMAP account → IMAP STATUS for that account (today's path);
|
|
1750
|
+
// - explicit non-IMAP account → AppleScript all-accounts stats below;
|
|
1751
|
+
// - no account + IMAP configured → MERGE: sum IMAP STATUS over every
|
|
1752
|
+
// configured account + AppleScript per-account stats for the accounts
|
|
1753
|
+
// IMAP does NOT cover (partitioned so no account is double-counted).
|
|
1754
|
+
if (account !== undefined && isImapAccount(account)) {
|
|
1542
1755
|
const s = await imapMailStats({ account });
|
|
1543
1756
|
const lines = [
|
|
1544
1757
|
`📊 Mail Statistics — ${account} (IMAP)`,
|
|
@@ -1553,6 +1766,77 @@ server.registerTool("get-mail-stats", {
|
|
|
1553
1766
|
];
|
|
1554
1767
|
return successResponse(lines.join("\n"), { account, ...s });
|
|
1555
1768
|
}
|
|
1769
|
+
if (account === undefined && shouldUseImap(account)) {
|
|
1770
|
+
let totalMessages = 0;
|
|
1771
|
+
let totalUnread = 0;
|
|
1772
|
+
const recent = { last24h: 0, last7d: 0, last30d: 0 };
|
|
1773
|
+
const perAccount = [];
|
|
1774
|
+
// ACCOUNT-CENTRIC: each account counted via exactly ONE source so a
|
|
1775
|
+
// heuristic mis-match can't double-count. IMAP sources use STATUS; the
|
|
1776
|
+
// AppleScript sources are built from listMailboxes (same source
|
|
1777
|
+
// getMailStats uses) — never getMailStats(), which is all-accounts and
|
|
1778
|
+
// would re-count the IMAP-covered ones. Recently-received from AppleScript
|
|
1779
|
+
// is INBOX-wide (not per-account), so it's omitted for AppleScript sources;
|
|
1780
|
+
// IMAP's per-account recent IS included.
|
|
1781
|
+
const sources = planCountSources(mailManager.listAccounts(), resolveImapConfigs());
|
|
1782
|
+
for (const src of sources) {
|
|
1783
|
+
if (src.kind === "imap") {
|
|
1784
|
+
try {
|
|
1785
|
+
const s = await imapMailStats({ config: src.config });
|
|
1786
|
+
totalMessages += s.totalMessages;
|
|
1787
|
+
totalUnread += s.totalUnread;
|
|
1788
|
+
recent.last24h += s.recent.last24h;
|
|
1789
|
+
recent.last7d += s.recent.last7d;
|
|
1790
|
+
recent.last30d += s.recent.last30d;
|
|
1791
|
+
perAccount.push({
|
|
1792
|
+
name: src.label,
|
|
1793
|
+
totalMessages: s.totalMessages,
|
|
1794
|
+
unreadMessages: s.totalUnread,
|
|
1795
|
+
backend: "imap",
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
catch (e) {
|
|
1799
|
+
console.error(`IMAP mail-stats failed for "${src.label}": ${String(e)}`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
else {
|
|
1803
|
+
let m = 0;
|
|
1804
|
+
let u = 0;
|
|
1805
|
+
for (const mb of mailManager.listMailboxes(src.account.name)) {
|
|
1806
|
+
m += mb.messageCount;
|
|
1807
|
+
u += mb.unreadCount;
|
|
1808
|
+
}
|
|
1809
|
+
totalMessages += m;
|
|
1810
|
+
totalUnread += u;
|
|
1811
|
+
perAccount.push({
|
|
1812
|
+
name: src.label,
|
|
1813
|
+
totalMessages: m,
|
|
1814
|
+
unreadMessages: u,
|
|
1815
|
+
backend: "applescript",
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const lines = [
|
|
1820
|
+
`📊 Mail Statistics (merged: IMAP + AppleScript)`,
|
|
1821
|
+
`══════════════════`,
|
|
1822
|
+
`Total messages: ${totalMessages}`,
|
|
1823
|
+
`Unread messages: ${totalUnread}`,
|
|
1824
|
+
``,
|
|
1825
|
+
`📥 Recently Received (IMAP INBOXes):`,
|
|
1826
|
+
` Last 24 hours: ${recent.last24h}`,
|
|
1827
|
+
` Last 7 days: ${recent.last7d}`,
|
|
1828
|
+
` Last 30 days: ${recent.last30d}`,
|
|
1829
|
+
``,
|
|
1830
|
+
`📁 By Account:`,
|
|
1831
|
+
...perAccount.map((a) => ` ${a.name}: ${a.totalMessages} messages (${a.unreadMessages} unread) [${a.backend}]`),
|
|
1832
|
+
];
|
|
1833
|
+
return successResponse(lines.join("\n"), {
|
|
1834
|
+
totalMessages,
|
|
1835
|
+
totalUnread,
|
|
1836
|
+
accounts: perAccount,
|
|
1837
|
+
recent,
|
|
1838
|
+
});
|
|
1839
|
+
}
|
|
1556
1840
|
const stats = mailManager.getMailStats();
|
|
1557
1841
|
const lines = [];
|
|
1558
1842
|
lines.push(`📊 Mail Statistics`);
|
|
@@ -139,6 +139,20 @@ export interface ImapDeps {
|
|
|
139
139
|
}
|
|
140
140
|
/** True when `account` matches any configured IMAP account (label or user). */
|
|
141
141
|
export declare function isImapAccount(account: string | undefined, env?: NodeJS.ProcessEnv): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Read-side routing gate (v2.6.0 — prefer-IMAP reads). Returns true when a read
|
|
144
|
+
* tool should go to IMAP rather than AppleScript:
|
|
145
|
+
* - IMAP is configured at all, AND
|
|
146
|
+
* - either the caller named no account (→ merge across all accounts), or the
|
|
147
|
+
* named account is itself a configured IMAP account.
|
|
148
|
+
* An explicitly-named NON-IMAP account returns false → AppleScript. When IMAP is
|
|
149
|
+
* not configured at all this is always false, so behavior is unchanged.
|
|
150
|
+
*
|
|
151
|
+
* NOTE: the 3 mailbox-WRITE ops (create/delete/rename-mailbox) deliberately keep
|
|
152
|
+
* using `isImapAccount` — they only route to IMAP for an explicitly-named IMAP
|
|
153
|
+
* account, never on an omitted account.
|
|
154
|
+
*/
|
|
155
|
+
export declare function shouldUseImap(account: string | undefined, env?: NodeJS.ProcessEnv): boolean;
|
|
142
156
|
/** Account labels of every configured IMAP account (C2), for diagnostics. */
|
|
143
157
|
export declare function listImapAccountLabels(env?: NodeJS.ProcessEnv): string[];
|
|
144
158
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"imapClient.d.ts","sourceRoot":"","sources":["../../src/services/imapClient.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"imapClient.d.ts","sourceRoot":"","sources":["../../src/services/imapClient.ts"],"names":[],"mappings":"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"}
|
|
@@ -142,6 +142,22 @@ export function isImapAccount(account, env = process.env) {
|
|
|
142
142
|
return false;
|
|
143
143
|
return listImapAccountSpecs(env).some((s) => s.accountLabel === account || s.user === account);
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Read-side routing gate (v2.6.0 — prefer-IMAP reads). Returns true when a read
|
|
147
|
+
* tool should go to IMAP rather than AppleScript:
|
|
148
|
+
* - IMAP is configured at all, AND
|
|
149
|
+
* - either the caller named no account (→ merge across all accounts), or the
|
|
150
|
+
* named account is itself a configured IMAP account.
|
|
151
|
+
* An explicitly-named NON-IMAP account returns false → AppleScript. When IMAP is
|
|
152
|
+
* not configured at all this is always false, so behavior is unchanged.
|
|
153
|
+
*
|
|
154
|
+
* NOTE: the 3 mailbox-WRITE ops (create/delete/rename-mailbox) deliberately keep
|
|
155
|
+
* using `isImapAccount` — they only route to IMAP for an explicitly-named IMAP
|
|
156
|
+
* account, never on an omitted account.
|
|
157
|
+
*/
|
|
158
|
+
export function shouldUseImap(account, env = process.env) {
|
|
159
|
+
return (listImapAccountSpecs(env).length > 0 && (account === undefined || isImapAccount(account, env)));
|
|
160
|
+
}
|
|
145
161
|
/** Account labels of every configured IMAP account (C2), for diagnostics. */
|
|
146
162
|
export function listImapAccountLabels(env = process.env) {
|
|
147
163
|
return listImapAccountSpecs(env).map((s) => s.accountLabel);
|
|
@@ -278,6 +294,11 @@ function structuredRow(m, account, path) {
|
|
|
278
294
|
mailbox: path,
|
|
279
295
|
account,
|
|
280
296
|
hasAttachments: false,
|
|
297
|
+
// Message-ID (when the envelope carries it) is the strongest cross-/intra-
|
|
298
|
+
// backend dedup key for the multi-account merge (imapMultiAccount.ts). The
|
|
299
|
+
// AppleScript path does not expose it, so cross-backend dedup falls back to
|
|
300
|
+
// the subject|sender|date composite key.
|
|
301
|
+
...(env.messageId ? { messageId: env.messageId } : {}),
|
|
281
302
|
};
|
|
282
303
|
}
|
|
283
304
|
async function run(args, listMode, deps) {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-account read merge (v2.6.0 — prefer-IMAP reads).
|
|
3
|
+
*
|
|
4
|
+
* v2.6.0 flips the read tools to PREFER direct IMAP whenever IMAP is configured
|
|
5
|
+
* (see `shouldUseImap` in imapClient.ts). When a read is issued with NO explicit
|
|
6
|
+
* account, results must still cover EVERY account the user has — so we:
|
|
7
|
+
*
|
|
8
|
+
* 1. fan the IMAP query out over every configured IMAP account
|
|
9
|
+
* (`resolveImapConfigs()`), and
|
|
10
|
+
* 2. ALSO run the existing AppleScript all-accounts path,
|
|
11
|
+
*
|
|
12
|
+
* then merge the two, de-duplicating any message that appears in both backends
|
|
13
|
+
* (e.g. a Gmail account that is both IMAP-configured AND visible to Mail.app's
|
|
14
|
+
* AppleScript scan) and preferring the IMAP copy (it carries the round-trippable
|
|
15
|
+
* `imap:` id that the mutation tools need).
|
|
16
|
+
*
|
|
17
|
+
* This module holds the backend-agnostic merge/dedup/sort + the per-account IMAP
|
|
18
|
+
* fan-out so the three message-list sites (search-messages, get-thread,
|
|
19
|
+
* list-messages) don't each re-implement it.
|
|
20
|
+
*
|
|
21
|
+
* @module services/imapMultiAccount
|
|
22
|
+
*/
|
|
23
|
+
import { type ImapSearchArgs, type ImapConfig, type ImapDeps } from "../services/imapClient.js";
|
|
24
|
+
import type { Account } from "../types.js";
|
|
25
|
+
/** A structured message row as emitted by either backend (permissive on keys). */
|
|
26
|
+
export type MessageRow = Record<string, unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* Cross-backend dedup key for a message row.
|
|
29
|
+
*
|
|
30
|
+
* - PREFER the normalized Message-ID when present (`mid:<id>`). It's the only
|
|
31
|
+
* globally-unique identity, so two IMAP accounts that both hold the very
|
|
32
|
+
* same message (rare, but possible with multi-delivery) collapse to one.
|
|
33
|
+
* - FALL BACK to `normalizedSubject|sender|dateReceivedEpoch` when no
|
|
34
|
+
* Message-ID is available. The AppleScript backend never exposes a
|
|
35
|
+
* Message-ID, so this composite is what actually dedups the common case: a
|
|
36
|
+
* Gmail account surfaced by BOTH the IMAP fan-out and the AppleScript
|
|
37
|
+
* all-accounts scan. The IMAP and AppleScript copies of one message share
|
|
38
|
+
* the same subject, sender, and received timestamp, so they collide here.
|
|
39
|
+
*
|
|
40
|
+
* Limitation (note for live testing): the composite key assumes the two backends
|
|
41
|
+
* report the SAME received timestamp to the second. If Mail.app and the IMAP
|
|
42
|
+
* server disagree on `dateReceived` (timezone/rounding), a message could escape
|
|
43
|
+
* dedup and appear twice. Message-ID dedup (IMAP-vs-IMAP) is exact; the
|
|
44
|
+
* composite is best-effort.
|
|
45
|
+
*/
|
|
46
|
+
export declare function dedupKey(row: MessageRow): string;
|
|
47
|
+
/**
|
|
48
|
+
* Merge IMAP rows with AppleScript rows: concatenate, de-dup by {@link dedupKey}
|
|
49
|
+
* preferring the IMAP copy (IMAP rows are passed first and win on collision),
|
|
50
|
+
* sort newest-first by dateReceived, then apply `limit`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function mergeMessages(imapRows: MessageRow[], appleRows: MessageRow[], limit: number): MessageRow[];
|
|
53
|
+
/** Gmail/Workspace IMAP host? Its `[Gmail]/All Mail` virtual mailbox is Gmail-only. */
|
|
54
|
+
export declare function isGmailHost(host: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Fan an IMAP message query out over EVERY configured IMAP account and return
|
|
57
|
+
* the concatenated structured rows (not yet merged with AppleScript, not yet
|
|
58
|
+
* limited — the caller merges + limits). `kind` picks the underlying query so
|
|
59
|
+
* the mailbox-default and unreadOnly semantics match the single-account path.
|
|
60
|
+
*
|
|
61
|
+
* Per-account failures are swallowed (logged) so one unreachable account doesn't
|
|
62
|
+
* sink the whole read; the returned `accountsQueried`/`accountsFailed` let the
|
|
63
|
+
* caller surface partial-coverage diagnostics.
|
|
64
|
+
*/
|
|
65
|
+
export declare function fanOutImapMessages(args: ImapSearchArgs, kind: "search" | "list", deps?: Omit<ImapDeps, "config" | "account">, configs?: ImapConfig[]): Promise<{
|
|
66
|
+
rows: MessageRow[];
|
|
67
|
+
accountsQueried: string[];
|
|
68
|
+
accountsFailed: string[];
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Per-pair matcher: does this IMAP config correspond to this Mail.app account?
|
|
72
|
+
* Compared case-insensitively, the config's `accountLabel` AND `user` against the
|
|
73
|
+
* Mail account's `name` AND `email`. An empty email can't match (avoids
|
|
74
|
+
* `"" === ""` false positives). This is the single source of truth for coverage;
|
|
75
|
+
* everything else delegates here.
|
|
76
|
+
*/
|
|
77
|
+
export declare function configMatchesAccount(config: ImapConfig, account: Account): boolean;
|
|
78
|
+
/** True when ANY config matches this Mail account (delegates to the per-pair matcher). */
|
|
79
|
+
export declare function isAccountCoveredByImap(account: Account, configs: ImapConfig[]): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Split Mail.app accounts into those covered by IMAP (counted via IMAP) and the
|
|
82
|
+
* rest (counted via AppleScript), so reads/counts skip the AppleScript scan for
|
|
83
|
+
* IMAP-covered accounts.
|
|
84
|
+
*/
|
|
85
|
+
export declare function partitionAccountsForCounts(accounts: Account[], configs: ImapConfig[]): {
|
|
86
|
+
imapCovered: Account[];
|
|
87
|
+
appleScriptOnly: Account[];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* One unit to count for the count tools (get-unread-count / get-mail-stats),
|
|
91
|
+
* carrying exactly ONE source so every account is counted once and only once:
|
|
92
|
+
* - `{ kind: "imap", config }` — count this account via IMAP STATUS;
|
|
93
|
+
* - `{ kind: "applescript", account }`— count this Mail account via AppleScript.
|
|
94
|
+
*/
|
|
95
|
+
export type CountSource = {
|
|
96
|
+
kind: "imap";
|
|
97
|
+
config: ImapConfig;
|
|
98
|
+
label: string;
|
|
99
|
+
} | {
|
|
100
|
+
kind: "applescript";
|
|
101
|
+
account: Account;
|
|
102
|
+
label: string;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Plan the count sources so NO account is double-counted even when the coverage
|
|
106
|
+
* heuristic mis-matches (the failure mode the naive `Σimap(all configs) +
|
|
107
|
+
* Σapple(uncovered)` had: a config that fails to match its Mail account lands in
|
|
108
|
+
* BOTH sums).
|
|
109
|
+
*
|
|
110
|
+
* Account-centric: walk the Mail.app accounts; each is counted via its FIRST
|
|
111
|
+
* matching config (IMAP) or, if none matches, via AppleScript. Then any IMAP
|
|
112
|
+
* config that matched NO Mail account (configured-but-not-present-in-Mail.app) is
|
|
113
|
+
* added once as an IMAP source. A config is consumed by at most one Mail account,
|
|
114
|
+
* so two Mail accounts can't both claim the same config and inflate the total.
|
|
115
|
+
*/
|
|
116
|
+
export declare function planCountSources(accounts: Account[], configs: ImapConfig[]): CountSource[];
|
|
117
|
+
/**
|
|
118
|
+
* Render the structured message rows into the human text block the read tools
|
|
119
|
+
* emit (` - ID: … | date | subject (from: sender) [read|unread]`). Shared so the
|
|
120
|
+
* merged path matches the single-backend formatting. `showReadState` mirrors
|
|
121
|
+
* search/list (which show [read]) vs. plain list rows.
|
|
122
|
+
*/
|
|
123
|
+
export declare function formatMergedRows(rows: MessageRow[], showReadState?: boolean): string;
|
|
124
|
+
//# sourceMappingURL=imapMultiAccount.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imapMultiAccount.d.ts","sourceRoot":"","sources":["../../src/services/imapMultiAccount.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,kFAAkF;AAClF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAuCjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAMhD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,UAAU,EAAE,EACtB,SAAS,EAAE,UAAU,EAAE,EACvB,KAAK,EAAE,MAAM,GACZ,UAAU,EAAE,CAcd;AAED,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,QAAQ,GAAG,MAAM,EACvB,IAAI,GAAE,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,SAAS,CAAM,EAC/C,OAAO,GAAE,UAAU,EAAyB,GAC3C,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAAC,eAAe,EAAE,MAAM,EAAE,CAAC;IAAC,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA2BtF;AAmBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAQlF;AAED,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,OAAO,CAEvF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,UAAU,EAAE,GACpB;IAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAAC,eAAe,EAAE,OAAO,EAAE,CAAA;CAAE,CAQxD;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACnD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,WAAW,EAAE,CAmB1F;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE,aAAa,UAAO,GAAG,MAAM,CAajF"}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-account read merge (v2.6.0 — prefer-IMAP reads).
|
|
3
|
+
*
|
|
4
|
+
* v2.6.0 flips the read tools to PREFER direct IMAP whenever IMAP is configured
|
|
5
|
+
* (see `shouldUseImap` in imapClient.ts). When a read is issued with NO explicit
|
|
6
|
+
* account, results must still cover EVERY account the user has — so we:
|
|
7
|
+
*
|
|
8
|
+
* 1. fan the IMAP query out over every configured IMAP account
|
|
9
|
+
* (`resolveImapConfigs()`), and
|
|
10
|
+
* 2. ALSO run the existing AppleScript all-accounts path,
|
|
11
|
+
*
|
|
12
|
+
* then merge the two, de-duplicating any message that appears in both backends
|
|
13
|
+
* (e.g. a Gmail account that is both IMAP-configured AND visible to Mail.app's
|
|
14
|
+
* AppleScript scan) and preferring the IMAP copy (it carries the round-trippable
|
|
15
|
+
* `imap:` id that the mutation tools need).
|
|
16
|
+
*
|
|
17
|
+
* This module holds the backend-agnostic merge/dedup/sort + the per-account IMAP
|
|
18
|
+
* fan-out so the three message-list sites (search-messages, get-thread,
|
|
19
|
+
* list-messages) don't each re-implement it.
|
|
20
|
+
*
|
|
21
|
+
* @module services/imapMultiAccount
|
|
22
|
+
*/
|
|
23
|
+
import { imapSearchMessages, imapListMessages, resolveImapConfigs, } from "../services/imapClient.js";
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a Message-ID for comparison: strip surrounding angle brackets and
|
|
26
|
+
* whitespace, lowercase. Returns undefined when the row has no usable id.
|
|
27
|
+
*/
|
|
28
|
+
function normalizeMessageId(row) {
|
|
29
|
+
const raw = typeof row.messageId === "string" ? row.messageId.trim() : "";
|
|
30
|
+
if (!raw)
|
|
31
|
+
return undefined;
|
|
32
|
+
const inner = raw.replace(/^<+|>+$/g, "").trim();
|
|
33
|
+
return inner ? inner.toLowerCase() : undefined;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Normalize a subject the same way the thread tool does for grouping: drop
|
|
37
|
+
* leading Re:/Fwd: prefixes, collapse whitespace, lowercase. Kept local (and
|
|
38
|
+
* deliberately simple) so the merge has no dependency cycle with the thread
|
|
39
|
+
* module; exactness isn't required — this only feeds the fallback dedup key.
|
|
40
|
+
*/
|
|
41
|
+
function normalizeSubjectForKey(subject) {
|
|
42
|
+
const s = typeof subject === "string" ? subject : "";
|
|
43
|
+
return s
|
|
44
|
+
.replace(/^(\s*(re|fwd|fw|aw|sv|antw)\s*:\s*)+/i, "")
|
|
45
|
+
.replace(/\s+/g, " ")
|
|
46
|
+
.trim()
|
|
47
|
+
.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
/** Epoch ms of the row's dateReceived (ISO string or Date), or 0 when absent. */
|
|
50
|
+
function dateEpoch(row) {
|
|
51
|
+
const d = row.dateReceived;
|
|
52
|
+
if (d instanceof Date)
|
|
53
|
+
return d.getTime();
|
|
54
|
+
if (typeof d === "string" && d) {
|
|
55
|
+
const t = new Date(d).getTime();
|
|
56
|
+
return Number.isNaN(t) ? 0 : t;
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Cross-backend dedup key for a message row.
|
|
62
|
+
*
|
|
63
|
+
* - PREFER the normalized Message-ID when present (`mid:<id>`). It's the only
|
|
64
|
+
* globally-unique identity, so two IMAP accounts that both hold the very
|
|
65
|
+
* same message (rare, but possible with multi-delivery) collapse to one.
|
|
66
|
+
* - FALL BACK to `normalizedSubject|sender|dateReceivedEpoch` when no
|
|
67
|
+
* Message-ID is available. The AppleScript backend never exposes a
|
|
68
|
+
* Message-ID, so this composite is what actually dedups the common case: a
|
|
69
|
+
* Gmail account surfaced by BOTH the IMAP fan-out and the AppleScript
|
|
70
|
+
* all-accounts scan. The IMAP and AppleScript copies of one message share
|
|
71
|
+
* the same subject, sender, and received timestamp, so they collide here.
|
|
72
|
+
*
|
|
73
|
+
* Limitation (note for live testing): the composite key assumes the two backends
|
|
74
|
+
* report the SAME received timestamp to the second. If Mail.app and the IMAP
|
|
75
|
+
* server disagree on `dateReceived` (timezone/rounding), a message could escape
|
|
76
|
+
* dedup and appear twice. Message-ID dedup (IMAP-vs-IMAP) is exact; the
|
|
77
|
+
* composite is best-effort.
|
|
78
|
+
*/
|
|
79
|
+
export function dedupKey(row) {
|
|
80
|
+
const mid = normalizeMessageId(row);
|
|
81
|
+
if (mid)
|
|
82
|
+
return `mid:${mid}`;
|
|
83
|
+
const subject = normalizeSubjectForKey(row.subject);
|
|
84
|
+
const sender = (typeof row.sender === "string" ? row.sender : "").trim().toLowerCase();
|
|
85
|
+
return `k:${subject}|${sender}|${dateEpoch(row)}`;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Merge IMAP rows with AppleScript rows: concatenate, de-dup by {@link dedupKey}
|
|
89
|
+
* preferring the IMAP copy (IMAP rows are passed first and win on collision),
|
|
90
|
+
* sort newest-first by dateReceived, then apply `limit`.
|
|
91
|
+
*/
|
|
92
|
+
export function mergeMessages(imapRows, appleRows, limit) {
|
|
93
|
+
const byKey = new Map();
|
|
94
|
+
// IMAP first so its copy wins; AppleScript only fills keys IMAP didn't supply.
|
|
95
|
+
for (const r of imapRows) {
|
|
96
|
+
const k = dedupKey(r);
|
|
97
|
+
if (!byKey.has(k))
|
|
98
|
+
byKey.set(k, r);
|
|
99
|
+
}
|
|
100
|
+
for (const r of appleRows) {
|
|
101
|
+
const k = dedupKey(r);
|
|
102
|
+
if (!byKey.has(k))
|
|
103
|
+
byKey.set(k, r);
|
|
104
|
+
}
|
|
105
|
+
const merged = [...byKey.values()];
|
|
106
|
+
merged.sort((a, b) => dateEpoch(b) - dateEpoch(a)); // newest first
|
|
107
|
+
return limit >= 0 ? merged.slice(0, limit) : merged;
|
|
108
|
+
}
|
|
109
|
+
/** Gmail/Workspace IMAP host? Its `[Gmail]/All Mail` virtual mailbox is Gmail-only. */
|
|
110
|
+
export function isGmailHost(host) {
|
|
111
|
+
return /(^|\.)gmail\.com$/i.test(host.trim());
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Fan an IMAP message query out over EVERY configured IMAP account and return
|
|
115
|
+
* the concatenated structured rows (not yet merged with AppleScript, not yet
|
|
116
|
+
* limited — the caller merges + limits). `kind` picks the underlying query so
|
|
117
|
+
* the mailbox-default and unreadOnly semantics match the single-account path.
|
|
118
|
+
*
|
|
119
|
+
* Per-account failures are swallowed (logged) so one unreachable account doesn't
|
|
120
|
+
* sink the whole read; the returned `accountsQueried`/`accountsFailed` let the
|
|
121
|
+
* caller surface partial-coverage diagnostics.
|
|
122
|
+
*/
|
|
123
|
+
export async function fanOutImapMessages(args, kind, deps = {}, configs = resolveImapConfigs()) {
|
|
124
|
+
const rows = [];
|
|
125
|
+
const accountsQueried = [];
|
|
126
|
+
const accountsFailed = [];
|
|
127
|
+
for (const config of configs) {
|
|
128
|
+
// Default-mailbox resolution is PER-ACCOUNT: the single-account search default
|
|
129
|
+
// ("[Gmail]/All Mail") is Gmail-only, so fanning it out to a non-Gmail account
|
|
130
|
+
// (e.g. iCloud) makes that SELECT fail and the account silently drops from the
|
|
131
|
+
// merged results. When the caller pinned no mailbox, give Gmail hosts their
|
|
132
|
+
// All-Mail default (undefined → resolved downstream) and every other host a
|
|
133
|
+
// universal "INBOX". (limit is applied AFTER the cross-account merge so the
|
|
134
|
+
// global newest-N is correct even when one account dominates.)
|
|
135
|
+
const mailbox = args.mailbox ?? (isGmailHost(config.host) ? undefined : "INBOX");
|
|
136
|
+
const perAccountArgs = { ...args, account: undefined, mailbox };
|
|
137
|
+
try {
|
|
138
|
+
const res = kind === "search"
|
|
139
|
+
? await imapSearchMessages(perAccountArgs, { ...deps, config })
|
|
140
|
+
: await imapListMessages(perAccountArgs, { ...deps, config });
|
|
141
|
+
rows.push(...res.messages);
|
|
142
|
+
accountsQueried.push(config.accountLabel);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
accountsFailed.push(config.accountLabel);
|
|
146
|
+
console.error(`IMAP fan-out failed for account "${config.accountLabel}": ${String(e)}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { rows, accountsQueried, accountsFailed };
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Account coverage + count partitioning (reads, get-unread-count, get-mail-stats)
|
|
153
|
+
//
|
|
154
|
+
// Coverage is decided per (config, Mail-account) PAIR by configMatchesAccount.
|
|
155
|
+
// - Message-list reads use partitionAccountsForCounts to run AppleScript ONLY
|
|
156
|
+
// for accounts no IMAP config covers (IMAP fans out over the configs), so an
|
|
157
|
+
// all-IMAP user runs zero AppleScript and never depends on composite dedup.
|
|
158
|
+
// - Count tools use planCountSources, which assigns each account EXACTLY ONE
|
|
159
|
+
// source (its matching config, else AppleScript) and adds any config that
|
|
160
|
+
// matched no account once — so even a heuristic MISS can't double-count.
|
|
161
|
+
//
|
|
162
|
+
// Matching is case-insensitive on the config's accountLabel/user vs. the Mail
|
|
163
|
+
// account's name/email. accountLabel defaults to the login address and users
|
|
164
|
+
// typically set it to the Mail.app account NAME, so checking both against both
|
|
165
|
+
// catches the common setups (label=accountName, label=email, login=email).
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
/**
|
|
168
|
+
* Per-pair matcher: does this IMAP config correspond to this Mail.app account?
|
|
169
|
+
* Compared case-insensitively, the config's `accountLabel` AND `user` against the
|
|
170
|
+
* Mail account's `name` AND `email`. An empty email can't match (avoids
|
|
171
|
+
* `"" === ""` false positives). This is the single source of truth for coverage;
|
|
172
|
+
* everything else delegates here.
|
|
173
|
+
*/
|
|
174
|
+
export function configMatchesAccount(config, account) {
|
|
175
|
+
const name = account.name.trim().toLowerCase();
|
|
176
|
+
const email = (account.email ?? "").trim().toLowerCase();
|
|
177
|
+
const label = config.accountLabel.trim().toLowerCase();
|
|
178
|
+
const user = config.user.trim().toLowerCase();
|
|
179
|
+
return (label === name || (!!email && label === email) || user === name || (!!email && user === email));
|
|
180
|
+
}
|
|
181
|
+
/** True when ANY config matches this Mail account (delegates to the per-pair matcher). */
|
|
182
|
+
export function isAccountCoveredByImap(account, configs) {
|
|
183
|
+
return configs.some((c) => configMatchesAccount(c, account));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Split Mail.app accounts into those covered by IMAP (counted via IMAP) and the
|
|
187
|
+
* rest (counted via AppleScript), so reads/counts skip the AppleScript scan for
|
|
188
|
+
* IMAP-covered accounts.
|
|
189
|
+
*/
|
|
190
|
+
export function partitionAccountsForCounts(accounts, configs) {
|
|
191
|
+
const imapCovered = [];
|
|
192
|
+
const appleScriptOnly = [];
|
|
193
|
+
for (const a of accounts) {
|
|
194
|
+
if (isAccountCoveredByImap(a, configs))
|
|
195
|
+
imapCovered.push(a);
|
|
196
|
+
else
|
|
197
|
+
appleScriptOnly.push(a);
|
|
198
|
+
}
|
|
199
|
+
return { imapCovered, appleScriptOnly };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Plan the count sources so NO account is double-counted even when the coverage
|
|
203
|
+
* heuristic mis-matches (the failure mode the naive `Σimap(all configs) +
|
|
204
|
+
* Σapple(uncovered)` had: a config that fails to match its Mail account lands in
|
|
205
|
+
* BOTH sums).
|
|
206
|
+
*
|
|
207
|
+
* Account-centric: walk the Mail.app accounts; each is counted via its FIRST
|
|
208
|
+
* matching config (IMAP) or, if none matches, via AppleScript. Then any IMAP
|
|
209
|
+
* config that matched NO Mail account (configured-but-not-present-in-Mail.app) is
|
|
210
|
+
* added once as an IMAP source. A config is consumed by at most one Mail account,
|
|
211
|
+
* so two Mail accounts can't both claim the same config and inflate the total.
|
|
212
|
+
*/
|
|
213
|
+
export function planCountSources(accounts, configs) {
|
|
214
|
+
const sources = [];
|
|
215
|
+
const usedConfigs = new Set();
|
|
216
|
+
for (const account of accounts) {
|
|
217
|
+
const match = configs.find((c) => !usedConfigs.has(c) && configMatchesAccount(c, account));
|
|
218
|
+
if (match) {
|
|
219
|
+
usedConfigs.add(match);
|
|
220
|
+
sources.push({ kind: "imap", config: match, label: account.name });
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
sources.push({ kind: "applescript", account, label: account.name });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// IMAP accounts that exist in config but not in Mail.app — count them once.
|
|
227
|
+
for (const config of configs) {
|
|
228
|
+
if (!usedConfigs.has(config)) {
|
|
229
|
+
sources.push({ kind: "imap", config, label: config.accountLabel });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return sources;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Render the structured message rows into the human text block the read tools
|
|
236
|
+
* emit (` - ID: … | date | subject (from: sender) [read|unread]`). Shared so the
|
|
237
|
+
* merged path matches the single-backend formatting. `showReadState` mirrors
|
|
238
|
+
* search/list (which show [read]) vs. plain list rows.
|
|
239
|
+
*/
|
|
240
|
+
export function formatMergedRows(rows, showReadState = true) {
|
|
241
|
+
return rows
|
|
242
|
+
.map((m) => {
|
|
243
|
+
const date = (() => {
|
|
244
|
+
const e = dateEpoch(m);
|
|
245
|
+
return e ? new Date(e).toLocaleDateString() : "";
|
|
246
|
+
})();
|
|
247
|
+
const subject = typeof m.subject === "string" ? m.subject : "(no subject)";
|
|
248
|
+
const sender = typeof m.sender === "string" ? m.sender : "(unknown)";
|
|
249
|
+
const state = showReadState ? ` [${m.isRead ? "read" : "unread"}]` : "";
|
|
250
|
+
return ` - ID: ${String(m.id)} | ${date} | ${subject} (from: ${sender})${state}`;
|
|
251
|
+
})
|
|
252
|
+
.join("\n");
|
|
253
|
+
}
|
package/package.json
CHANGED