apple-mail-mcp 2.10.2 → 2.10.4
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 +33 -17
- package/build/index.js +84 -19
- package/docs/IMAP-SETUP.md +27 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -147,6 +147,7 @@ tool, and troubleshooting. Verify any time by running the **`doctor`** tool.
|
|
|
147
147
|
|
|
148
148
|
- **macOS** - Apple Mail and AppleScript are macOS-only
|
|
149
149
|
- **Node.js 20+** - Required for the MCP server
|
|
150
|
+
- **Node.js 22.5+ and Full Disk Access** - Required by `search-contacts` only. It reads the Contacts database directly through Node's built-in `node:sqlite`, which does not exist before 22.5. On an older runtime, or without Full Disk Access for the Node binary, it logs one line to stderr and returns **an empty list rather than an error** — so "no contacts found" can mean "cannot read Contacts". Every other tool works on Node 20+. See [Node runtime & TCC permissions](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md).
|
|
150
151
|
- **Apple Mail** - Must have at least one account configured (iCloud, Gmail, Exchange, etc.)
|
|
151
152
|
|
|
152
153
|
## Features
|
|
@@ -264,6 +265,8 @@ Get the full content of a message.
|
|
|
264
265
|
|-----------|------|----------|-------------|
|
|
265
266
|
| `id` | string | Yes | Message ID |
|
|
266
267
|
| `preferHtml` | boolean | No | Return HTML source instead of plain text |
|
|
268
|
+
| `mailbox` | string | No | Mailbox holding the message (e.g. `"Sent Items"`). With `account`, opens that mailbox directly instead of scanning every mailbox — this is the fix for timeouts on large folders |
|
|
269
|
+
| `account` | string | No | Account holding the message. Pair with `mailbox` to skip the cross-mailbox scan |
|
|
267
270
|
|
|
268
271
|
**Returns:** Subject line and message body (plain text by default, HTML if `preferHtml` is true and HTML content is available).
|
|
269
272
|
|
|
@@ -668,7 +671,7 @@ Return an attachment's bytes as base64 (the read counterpart to inline-base64 se
|
|
|
668
671
|
|
|
669
672
|
| Parameter | Type | Required | Description |
|
|
670
673
|
|-----------|------|----------|-------------|
|
|
671
|
-
| `id` | string | Yes |
|
|
674
|
+
| `id` | string | Yes | Message ID (numeric or `imap:…`) |
|
|
672
675
|
| `attachmentName` | string | Yes | Attachment filename (from `list-attachments`) |
|
|
673
676
|
|
|
674
677
|
**Returns:** The attachment bytes, base64-encoded (also in `structuredContent.contentBase64`).
|
|
@@ -748,6 +751,8 @@ Flag or unflag a message. `flag-message` optionally takes a flag **color**; `unf
|
|
|
748
751
|
|
|
749
752
|
**Flag colors** are an Apple Mail feature — the message's `flag index` (0 red, 1 orange, 2 yellow, 3 green, 4 blue, 5 purple, 6 gray), which is the property a Mail smart mailbox can match on. **The color is applied on both routes** (since 2.10.0): AppleScript sets the flag index directly, and for an **IMAP-routed** id (`imap:…`) the color is written as Mail.app's `$MailFlagBit0/1/2` keywords — a 3-bit field holding the same palette index. `\Flagged` on its own really is colorless, but those keywords ride alongside it in an ordinary `UID STORE`, so a smart mailbox keyed on flag color matches an IMAP-flagged message too. You do **not** need to resolve to a numeric id just to color a flag.
|
|
750
753
|
|
|
754
|
+
To **read** a color, the IMAP read path returns `flagColorIndex` in `structuredContent` — the same 0-6 palette index, omitted when the message carries no color bits. The AppleScript read path does not populate it.
|
|
755
|
+
|
|
751
756
|
---
|
|
752
757
|
|
|
753
758
|
#### `delete-message`
|
|
@@ -853,8 +858,12 @@ Get unread message count.
|
|
|
853
858
|
|
|
854
859
|
| Parameter | Type | Required | Description |
|
|
855
860
|
|-----------|------|----------|-------------|
|
|
856
|
-
| `mailbox` | string | No | Mailbox to check (omit for
|
|
857
|
-
| `account` | string | No | Account to check |
|
|
861
|
+
| `mailbox` | string | No | Mailbox to check (omit for **INBOX**) |
|
|
862
|
+
| `account` | string | No | Account to check (omit to sum each account's INBOX) |
|
|
863
|
+
|
|
864
|
+
**Returns:** The unread count for the requested scope.
|
|
865
|
+
|
|
866
|
+
> Omitting `mailbox` counts **INBOX**, not a cross-mailbox total. This changed in 2.8.15: summing every mailbox was slow and wrong on Gmail, where one message appears in INBOX, All Mail and every label it carries. For account-wide totals use [`get-mail-stats`](#get-mail-stats).
|
|
858
867
|
|
|
859
868
|
---
|
|
860
869
|
|
|
@@ -896,6 +905,8 @@ Rename a mailbox (creates new, moves messages, deletes old).
|
|
|
896
905
|
|
|
897
906
|
### Smart Mailbox Operations (intelligente Postfächer)
|
|
898
907
|
|
|
908
|
+
> **Requires Full Disk Access.** These tools read and write `~/Library/Mail/V*/MailData/SyncedSmartMailboxes.plist`, which is TCC-protected. Without Full Disk Access for the server's Node runtime the read simply finds nothing, and the tools report "no smart mailboxes" or "launch Mail at least once" rather than a permission error — see [Node runtime & TCC permissions](https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md).
|
|
909
|
+
|
|
899
910
|
Smart mailboxes are Apple Mail's **criteria-based virtual views** — not real folders, so no messages are moved. AppleScript's `smart mailbox` / `intelligentes Postfach` terms don't compile reliably on localized (e.g. German) macOS, so these tools read and edit `~/Library/Mail/V*/MailData/SyncedSmartMailboxes.plist` directly.
|
|
900
911
|
|
|
901
912
|
**How writes stay safe:** creating or deleting a smart mailbox first backs the plist up to `SyncedSmartMailboxes.plist.bak`, edits a temp copy with `plutil`/`PlistBuddy`, validates it with `plutil -lint`, and only then atomically renames it into place. Your **existing** smart mailboxes — including any with date/data criteria — are never rewritten, only the single target entry is added or removed. These tools do **not** quit or restart Mail: **quit Mail first** for reliable results, since a running Mail may not show a new smart mailbox until it's relaunched and can overwrite plist edits it didn't make.
|
|
@@ -1030,14 +1041,15 @@ Delete a mail rule by name.
|
|
|
1030
1041
|
|
|
1031
1042
|
#### `search-contacts`
|
|
1032
1043
|
|
|
1033
|
-
Search
|
|
1044
|
+
Search the macOS Contacts database by name, organization, nickname, or email substring.
|
|
1045
|
+
|
|
1046
|
+
Since 2.8.7 this reads the AddressBook SQLite files directly rather than driving Contacts.app over AppleScript, so **Contacts.app need not be running and no Automation grant is involved** — but the Node runtime does need **Full Disk Access**, and **Node 22.5+** (see [Requirements](#requirements)). Without either, the tool returns an empty list rather than an error.
|
|
1034
1047
|
|
|
1035
1048
|
| Parameter | Type | Required | Description |
|
|
1036
1049
|
|-----------|------|----------|-------------|
|
|
1037
|
-
| `query` | string | Yes |
|
|
1038
|
-
| `limit` | number | No | Max results (default: 10) |
|
|
1050
|
+
| `query` | string | Yes | Substring matched against full name, organization, nickname, or any email address |
|
|
1039
1051
|
|
|
1040
|
-
**Returns:** List of contacts with name, email addresses, and phone numbers.
|
|
1052
|
+
**Returns:** List of contacts with name, email addresses, and phone numbers. Results are **not** truncated — a broad query returns every match.
|
|
1041
1053
|
|
|
1042
1054
|
---
|
|
1043
1055
|
|
|
@@ -1130,7 +1142,9 @@ Run a full setup diagnostic: Mail.app automation permission, account state (flag
|
|
|
1130
1142
|
|
|
1131
1143
|
Get mail statistics.
|
|
1132
1144
|
|
|
1133
|
-
|
|
1145
|
+
| Parameter | Type | Required | Description |
|
|
1146
|
+
|-----------|------|----------|-------------|
|
|
1147
|
+
| `account` | string | No | Limit to one account (uses fast IMAP `STATUS` when that account is IMAP-configured). Omit to merge across all accounts. |
|
|
1134
1148
|
|
|
1135
1149
|
**Returns:** Total and per-account message/unread counts, plus recently received stats (24h, 7d, 30d).
|
|
1136
1150
|
|
|
@@ -1271,7 +1285,7 @@ The entrypoint is written as:
|
|
|
1271
1285
|
|
|
1272
1286
|
## Security and Privacy
|
|
1273
1287
|
|
|
1274
|
-
- **
|
|
1288
|
+
- **No third parties** - The server talks only to Mail.app on this Mac (AppleScript) and, when you configure them, directly to **your own** mail provider over TLS (IMAP/SMTP). Nothing is sent to this project or any other service. With the default AppleScript backend everything stays on-device; the opt-in IMAP/SMTP backends necessarily reach your provider, which is what they are for.
|
|
1275
1289
|
- **Permission required** - macOS will prompt for automation permission on first use.
|
|
1276
1290
|
- **No credential storage** - The server doesn't store any passwords or authentication tokens.
|
|
1277
1291
|
- **Email safety** - Use `create-draft` to review emails before sending.
|
|
@@ -1285,7 +1299,7 @@ The entrypoint is written as:
|
|
|
1285
1299
|
| macOS only | Apple Mail and AppleScript are macOS-specific |
|
|
1286
1300
|
| MCP `send-email` is plain-text | The `send-email` tool sends plain text (reading HTML content is supported). To send HTML, use the bundled `apple-mail-send` CLI with `--html-body-file` (sends `multipart/alternative` via SMTP) |
|
|
1287
1301
|
| Attachments require absolute paths | File attachments must use full absolute paths (e.g., `/Users/me/file.pdf`) |
|
|
1288
|
-
|
|
|
1302
|
+
| Smart mailboxes need Mail quit | Smart mailboxes are supported (see [Smart Mailbox Operations](#smart-mailbox-operations-intelligente-postfächer)), but `create-`/`delete-smart-mailbox` edit `SyncedSmartMailboxes.plist` directly — a running Mail may not show a new one until relaunched, and can overwrite plist edits it didn't make. Quit Mail first. Reading them needs Full Disk Access for the Node runtime |
|
|
1289
1303
|
| Very large mailboxes not searchable *via AppleScript* | Apple Mail's AppleScript bridge times out on mailboxes with tens of thousands of messages, so unscoped `search-messages` skips mailboxes above `APPLE_MAIL_MAX_SEARCH_MAILBOX` (default 5000) and reports them as a partial result. Scope with `mailbox` + a date window — or configure the [IMAP backend](#imap-backend--opt-in), which searches these server-side in well under a second. ([#24](https://github.com/sweetrb/apple-mail-mcp/issues/24)) |
|
|
1290
1304
|
| Can't delete/rename server-side mailboxes or mutate drafts *via AppleScript* | Mail.app's AppleScript bridge can only `delete`/`rename` **local "On My Mac"** mailboxes and cannot delete/move drafts — it throws `AppleEvent handler failed` for IMAP/Gmail/Workspace/iCloud/Exchange mailboxes (the GUI can do it). Without IMAP configured, `delete-mailbox`/`rename-mailbox`/`delete-message`/`move-message` return a clear "do it in Mail.app directly" error instead of a generic failure. With the [IMAP backend](#imap-backend--opt-in) configured for the account, these operations run via IMAP and succeed. ([#42](https://github.com/sweetrb/apple-mail-mcp/issues/42)) |
|
|
1291
1305
|
| Message ID format | Message IDs must be numeric (AppleScript ids) or `imap:…` tokens from the IMAP read path (validated by schema) |
|
|
@@ -1385,14 +1399,16 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
|
|
|
1385
1399
|
|
|
1386
1400
|
## Development
|
|
1387
1401
|
|
|
1402
|
+
This repo is **pnpm-only** — `package.json`'s `preinstall` guard hard-fails an `npm install`, because npm resolves off-lockfile and the committed bundle would then mismatch CI.
|
|
1403
|
+
|
|
1388
1404
|
```bash
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1405
|
+
corepack enable && pnpm install --frozen-lockfile # Install dependencies
|
|
1406
|
+
pnpm run build # Typecheck, then bundle src/index.ts + src/cli.ts into build/ (esbuild)
|
|
1407
|
+
pnpm test # Run unit tests
|
|
1408
|
+
pnpm run test:integration # Run integration tests (requires Mail.app)
|
|
1409
|
+
pnpm run test:all # Run all tests (unit + integration)
|
|
1410
|
+
pnpm run lint # Check code style
|
|
1411
|
+
pnpm run format # Format code
|
|
1396
1412
|
```
|
|
1397
1413
|
|
|
1398
1414
|
---
|
package/build/index.js
CHANGED
|
@@ -76079,6 +76079,10 @@ function executeAppleScript(script, options = {}) {
|
|
|
76079
76079
|
return lastError;
|
|
76080
76080
|
}
|
|
76081
76081
|
|
|
76082
|
+
// src/utils/docsUrls.ts
|
|
76083
|
+
var SETUP_GUIDE_URL = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
|
|
76084
|
+
var SETUP_HINT = `Setup guide: ${SETUP_GUIDE_URL} \u2014 run the "doctor" tool to check your setup.`;
|
|
76085
|
+
|
|
76082
76086
|
// src/utils/mimeParse.ts
|
|
76083
76087
|
function extractBoundary(source) {
|
|
76084
76088
|
const match = source.match(/boundary="?([^";\s\r\n]+)"?/i);
|
|
@@ -76838,6 +76842,12 @@ var AppleMailManager = class {
|
|
|
76838
76842
|
};
|
|
76839
76843
|
/** Cache TTL in milliseconds (60 seconds). */
|
|
76840
76844
|
CACHE_TTL_MS = 6e4;
|
|
76845
|
+
/**
|
|
76846
|
+
* Last AppleScript transport error from an account/count read, or null when the
|
|
76847
|
+
* last one succeeded. Lets a tool report "the transport failed" instead of
|
|
76848
|
+
* presenting a fallback zero/empty as a real answer. (#130)
|
|
76849
|
+
*/
|
|
76850
|
+
lastAccountsError = null;
|
|
76841
76851
|
/**
|
|
76842
76852
|
* Remembers where each message id was last seen: id → {account, mailbox}.
|
|
76843
76853
|
*
|
|
@@ -76873,6 +76883,10 @@ var AppleMailManager = class {
|
|
|
76873
76883
|
return this.cache.accounts.data;
|
|
76874
76884
|
}
|
|
76875
76885
|
const accounts = this.fetchAccounts();
|
|
76886
|
+
if (accounts === null) {
|
|
76887
|
+
return this.cache.accounts?.data ?? [];
|
|
76888
|
+
}
|
|
76889
|
+
this.lastAccountsError = null;
|
|
76876
76890
|
this.cache.accounts = { data: accounts, expiry: now + this.CACHE_TTL_MS };
|
|
76877
76891
|
return accounts;
|
|
76878
76892
|
}
|
|
@@ -76974,7 +76988,7 @@ var AppleMailManager = class {
|
|
|
76974
76988
|
serverSideCreateGuard(account, op) {
|
|
76975
76989
|
if (this.isServerSideAccount(account) === true) {
|
|
76976
76990
|
const verb = op === "rename" ? "rename" : "create";
|
|
76977
|
-
return `Account "${account}" stores its mailboxes on the server (IMAP / iCloud / Exchange), and Mail.app cannot ${verb} server-side mailboxes via AppleScript \u2014 a ${verb} would ${op === "rename" ? "leave a half-created orphan" : "orphan a mailbox that can never be removed"}. Configure IMAP for this account (APPLE_MAIL_MCP_IMAP_*) so mailbox create/delete/rename route through the server, or manage the folder in Mail.app directly
|
|
76991
|
+
return `Account "${account}" stores its mailboxes on the server (IMAP / iCloud / Exchange), and Mail.app cannot ${verb} server-side mailboxes via AppleScript \u2014 a ${verb} would ${op === "rename" ? "leave a half-created orphan" : "orphan a mailbox that can never be removed"}. Configure IMAP for this account (APPLE_MAIL_MCP_IMAP_*) so mailbox create/delete/rename route through the server, or manage the folder in Mail.app directly. ${SETUP_HINT}`;
|
|
76978
76992
|
}
|
|
76979
76993
|
return null;
|
|
76980
76994
|
}
|
|
@@ -78679,6 +78693,7 @@ var AppleMailManager = class {
|
|
|
78679
78693
|
const result = executeAppleScript(script, { timeoutMs: 6e4 });
|
|
78680
78694
|
if (!result.success) {
|
|
78681
78695
|
console.error(`Failed to get unread count: ${result.error}`);
|
|
78696
|
+
this.lastAccountsError = result.error ?? "AppleScript transport failed";
|
|
78682
78697
|
return 0;
|
|
78683
78698
|
}
|
|
78684
78699
|
return parseInt(result.output) || 0;
|
|
@@ -79204,9 +79219,38 @@ end tell`;
|
|
|
79204
79219
|
listAccounts() {
|
|
79205
79220
|
return this.getCachedAccounts();
|
|
79206
79221
|
}
|
|
79222
|
+
/**
|
|
79223
|
+
* listAccounts() plus whether the underlying AppleScript read actually worked.
|
|
79224
|
+
*
|
|
79225
|
+
* `failed: true` means the list is a fallback (stale cache or empty) because the
|
|
79226
|
+
* transport errored — NOT that Mail has no accounts. (#130)
|
|
79227
|
+
*/
|
|
79228
|
+
listAccountsChecked() {
|
|
79229
|
+
this.lastAccountsError = null;
|
|
79230
|
+
const accounts = this.getCachedAccounts();
|
|
79231
|
+
const error2 = this.lastAccountsError;
|
|
79232
|
+
return error2 ? { accounts, failed: true, error: error2 } : { accounts, failed: false };
|
|
79233
|
+
}
|
|
79234
|
+
/**
|
|
79235
|
+
* getUnreadCount() plus whether the AppleScript read actually worked.
|
|
79236
|
+
*
|
|
79237
|
+
* On failure the count is `null` rather than 0, so a caller can never mistake a
|
|
79238
|
+
* wedged transport for an empty inbox. (#130)
|
|
79239
|
+
*/
|
|
79240
|
+
getUnreadCountChecked(mailbox, account) {
|
|
79241
|
+
this.lastAccountsError = null;
|
|
79242
|
+
const count = this.getUnreadCount(mailbox, account);
|
|
79243
|
+
const error2 = this.lastAccountsError;
|
|
79244
|
+
return error2 ? { count: null, failed: true, error: error2 } : { count, failed: false };
|
|
79245
|
+
}
|
|
79207
79246
|
/**
|
|
79208
79247
|
* Fetches account list directly from Mail.app via AppleScript.
|
|
79209
79248
|
* Used internally by the cache; prefer getCachedAccounts() or listAccounts().
|
|
79249
|
+
*
|
|
79250
|
+
* Returns `null` when the AppleScript transport itself failed (timeout, wedged
|
|
79251
|
+
* Mail, denied Automation). That is deliberately distinct from `[]`, which means
|
|
79252
|
+
* "Mail answered, and there genuinely are no accounts" — collapsing the two is
|
|
79253
|
+
* what let a wedged transport report a confident "No Mail accounts found". (#130)
|
|
79210
79254
|
*/
|
|
79211
79255
|
fetchAccounts() {
|
|
79212
79256
|
const script = buildAppLevelScript(`
|
|
@@ -79227,7 +79271,8 @@ end tell`;
|
|
|
79227
79271
|
const result = executeAppleScript(script);
|
|
79228
79272
|
if (!result.success) {
|
|
79229
79273
|
console.error(`Failed to list accounts: ${result.error}`);
|
|
79230
|
-
|
|
79274
|
+
this.lastAccountsError = result.error ?? "AppleScript transport failed";
|
|
79275
|
+
return null;
|
|
79231
79276
|
}
|
|
79232
79277
|
if (!result.output.trim()) return [];
|
|
79233
79278
|
const items = result.output.split(RECORD_SEP);
|
|
@@ -79739,12 +79784,6 @@ var import_nodemailer = __toESM(require_nodemailer(), 1);
|
|
|
79739
79784
|
import { execFileSync } from "child_process";
|
|
79740
79785
|
import { isAbsolute as isAbsolute2 } from "path";
|
|
79741
79786
|
import { existsSync as existsSync4 } from "fs";
|
|
79742
|
-
|
|
79743
|
-
// src/utils/docsUrls.ts
|
|
79744
|
-
var SETUP_GUIDE_URL = "https://github.com/sweetrb/apple-mail-mcp/blob/main/docs/IMAP-SETUP.md";
|
|
79745
|
-
var SETUP_HINT = `Setup guide: ${SETUP_GUIDE_URL} \u2014 run the "doctor" tool to check your setup.`;
|
|
79746
|
-
|
|
79747
|
-
// src/services/smtpMailer.ts
|
|
79748
79787
|
var SMTP_ENV = {
|
|
79749
79788
|
host: "APPLE_MAIL_MCP_SMTP_HOST",
|
|
79750
79789
|
port: "APPLE_MAIL_MCP_SMTP_PORT",
|
|
@@ -80144,7 +80183,7 @@ function specToConfig(spec) {
|
|
|
80144
80183
|
}
|
|
80145
80184
|
if (!pass) {
|
|
80146
80185
|
throw new Error(
|
|
80147
|
-
`No IMAP password for account "${spec.accountLabel}". Set a password or a Keychain service/account
|
|
80186
|
+
`No IMAP password for account "${spec.accountLabel}". Set a password or a Keychain service/account. ${SETUP_HINT}`
|
|
80148
80187
|
);
|
|
80149
80188
|
}
|
|
80150
80189
|
return {
|
|
@@ -82826,7 +82865,7 @@ ${mailboxList}`, structured);
|
|
|
82826
82865
|
server.registerTool(
|
|
82827
82866
|
"get-unread-count",
|
|
82828
82867
|
{
|
|
82829
|
-
description: "Use when: you only need the number of unread messages \u2014 INBOX by default, or scoped to one mailbox and/or account \u2014 without listing the messages themselves.\nReturns: the unread count for the requested scope (INBOX when no mailbox is given).\nDo not use when: you need the actual unread messages and their ids (use list-messages with unreadOnly, or search-messages with isRead=false) or broader totals across every mailbox (use get-mail-stats).",
|
|
82868
|
+
description: "Use when: you only need the number of unread messages \u2014 INBOX by default, or scoped to one mailbox and/or account \u2014 without listing the messages themselves.\nReturns: the unread count for the requested scope (INBOX when no mailbox is given). If a source cannot be read the result carries `partial: true` + `failedAccounts`, and a total AppleScript failure returns an ERROR \u2014 a plain count is never a disguised transport failure.\nDo not use when: you need the actual unread messages and their ids (use list-messages with unreadOnly, or search-messages with isRead=false) or broader totals across every mailbox (use get-mail-stats).",
|
|
82830
82869
|
inputSchema: {
|
|
82831
82870
|
mailbox: external_exports.string().optional().describe("Mailbox to check (default: INBOX)"),
|
|
82832
82871
|
account: external_exports.string().optional().describe("Account to check")
|
|
@@ -82834,11 +82873,14 @@ server.registerTool(
|
|
|
82834
82873
|
outputSchema: {
|
|
82835
82874
|
unread: external_exports.number().optional(),
|
|
82836
82875
|
mailbox: external_exports.string().optional(),
|
|
82837
|
-
account: external_exports.string().optional()
|
|
82876
|
+
account: external_exports.string().optional(),
|
|
82877
|
+
partial: external_exports.boolean().optional(),
|
|
82878
|
+
failedAccounts: external_exports.array(external_exports.string()).optional()
|
|
82838
82879
|
}
|
|
82839
82880
|
},
|
|
82840
82881
|
withErrorHandling(async ({ mailbox, account }) => {
|
|
82841
82882
|
let count;
|
|
82883
|
+
const failedAccounts = [];
|
|
82842
82884
|
if (shouldUseImap(account)) {
|
|
82843
82885
|
if (account !== void 0) {
|
|
82844
82886
|
count = await imapUnreadCount(mailbox, { account });
|
|
@@ -82851,17 +82893,32 @@ server.registerTool(
|
|
|
82851
82893
|
total += await imapUnreadCount(mailbox, { config: src.config });
|
|
82852
82894
|
} catch (e) {
|
|
82853
82895
|
console.error(`IMAP unread-count failed for "${src.label}": ${String(e)}`);
|
|
82896
|
+
failedAccounts.push(src.label);
|
|
82854
82897
|
}
|
|
82855
82898
|
} else {
|
|
82856
|
-
|
|
82899
|
+
const r = mailManager.getUnreadCountChecked(mailbox, src.account.name);
|
|
82900
|
+
if (r.failed) failedAccounts.push(src.account.name);
|
|
82901
|
+
else total += r.count ?? 0;
|
|
82857
82902
|
}
|
|
82858
82903
|
}
|
|
82859
82904
|
count = total;
|
|
82860
82905
|
}
|
|
82861
82906
|
} else {
|
|
82862
|
-
|
|
82907
|
+
const r = mailManager.getUnreadCountChecked(mailbox, account);
|
|
82908
|
+
if (r.failed) {
|
|
82909
|
+
return errorResponse(
|
|
82910
|
+
`Could not read the unread count \u2014 the AppleScript transport failed: ${r.error}. This is NOT the same as zero unread. Mail may be busy, wedged, or missing an Automation grant; run the "doctor" tool to check.`
|
|
82911
|
+
);
|
|
82912
|
+
}
|
|
82913
|
+
count = r.count ?? 0;
|
|
82863
82914
|
}
|
|
82864
82915
|
const location = mailbox ? ` in "${mailbox}"` : "";
|
|
82916
|
+
if (failedAccounts.length > 0) {
|
|
82917
|
+
return successResponse(
|
|
82918
|
+
`${count} unread message(s)${location} \u2014 PARTIAL: ${failedAccounts.length} account(s) could not be read (${failedAccounts.join(", ")}), so the real total is higher. Run the "doctor" tool to check.`,
|
|
82919
|
+
{ unread: count, mailbox, account, partial: true, failedAccounts }
|
|
82920
|
+
);
|
|
82921
|
+
}
|
|
82865
82922
|
return successResponse(`${count} unread message(s)${location}`, {
|
|
82866
82923
|
unread: count,
|
|
82867
82924
|
mailbox,
|
|
@@ -83091,16 +83148,23 @@ ${lines || " (none met the threshold)"}`,
|
|
|
83091
83148
|
server.registerTool(
|
|
83092
83149
|
"list-accounts",
|
|
83093
83150
|
{
|
|
83094
|
-
description: "Use when: discovering the configured Mail accounts (e.g. iCloud, Gmail) so you can pass an exact account name to other tools.\nReturns: the account names and a count.\nDo not use when: you want the folders within an account (use list-mailboxes) or messages (use list-messages / search-messages).",
|
|
83151
|
+
description: "Use when: discovering the configured Mail accounts (e.g. iCloud, Gmail) so you can pass an exact account name to other tools.\nReturns: the account names and a count. If the AppleScript transport fails (timeout / wedged Mail / missing Automation grant) this returns an ERROR rather than an empty list \u2014 an empty list always means Mail really has no accounts.\nDo not use when: you want the folders within an account (use list-mailboxes) or messages (use list-messages / search-messages).",
|
|
83095
83152
|
inputSchema: {},
|
|
83096
83153
|
outputSchema: {
|
|
83097
83154
|
accounts: external_exports.array(external_exports.object({}).passthrough()).optional(),
|
|
83098
|
-
count: external_exports.number().optional()
|
|
83155
|
+
count: external_exports.number().optional(),
|
|
83156
|
+
partial: external_exports.boolean().optional(),
|
|
83157
|
+
error: external_exports.string().optional()
|
|
83099
83158
|
}
|
|
83100
83159
|
},
|
|
83101
83160
|
withErrorHandling(() => {
|
|
83102
|
-
const accounts = mailManager.
|
|
83103
|
-
const structured = { accounts, count: accounts.length };
|
|
83161
|
+
const { accounts, failed, error: error2 } = mailManager.listAccountsChecked();
|
|
83162
|
+
const structured = failed ? { accounts, count: accounts.length, partial: true, error: error2 } : { accounts, count: accounts.length };
|
|
83163
|
+
if (failed) {
|
|
83164
|
+
return errorResponse(
|
|
83165
|
+
`Could not read the Mail account list \u2014 the AppleScript transport failed: ${error2}. This is NOT the same as having no accounts. Mail may be busy, wedged, or missing an Automation grant; run the "doctor" tool to check.`
|
|
83166
|
+
);
|
|
83167
|
+
}
|
|
83104
83168
|
if (accounts.length === 0) {
|
|
83105
83169
|
return successResponse("No Mail accounts found", structured);
|
|
83106
83170
|
}
|
|
@@ -83252,7 +83316,7 @@ server.registerTool(
|
|
|
83252
83316
|
withErrorHandling(({ query }) => {
|
|
83253
83317
|
const contacts = mailManager.searchContacts(query);
|
|
83254
83318
|
const structured = {
|
|
83255
|
-
contacts: contacts.map((c) => ({ name: c.name, emails: c.emails })),
|
|
83319
|
+
contacts: contacts.map((c) => ({ name: c.name, emails: c.emails, phones: c.phones })),
|
|
83256
83320
|
count: contacts.length
|
|
83257
83321
|
};
|
|
83258
83322
|
if (contacts.length === 0) {
|
|
@@ -83260,7 +83324,8 @@ server.registerTool(
|
|
|
83260
83324
|
}
|
|
83261
83325
|
const contactList = contacts.map((c) => {
|
|
83262
83326
|
const emails = c.emails.length > 0 ? c.emails.join(", ") : "no email";
|
|
83263
|
-
|
|
83327
|
+
const phones = c.phones.length > 0 ? `; ${c.phones.join(", ")}` : "";
|
|
83328
|
+
return ` - ${c.name} (${emails}${phones})`;
|
|
83264
83329
|
}).join("\n");
|
|
83265
83330
|
return successResponse(`Found ${contacts.length} contact(s):
|
|
83266
83331
|
${contactList}`, structured);
|
package/docs/IMAP-SETUP.md
CHANGED
|
@@ -160,9 +160,22 @@ non-secret config goes here — passwords stay in the Keychain.**
|
|
|
160
160
|
### `APPLE_MAIL_MCP_IMAP_ACCOUNT` and routing
|
|
161
161
|
|
|
162
162
|
`APPLE_MAIL_MCP_IMAP_ACCOUNT` should equal the **Mail.app account name** (what
|
|
163
|
-
`list-accounts` shows)
|
|
164
|
-
argument
|
|
165
|
-
|
|
163
|
+
`list-accounts` shows), because that is how an **explicitly named** `account`
|
|
164
|
+
argument is matched to an IMAP config.
|
|
165
|
+
|
|
166
|
+
**Since v2.6.0 you do not need to pass `account` at all for reads.** When any
|
|
167
|
+
IMAP account is configured, the read tools (`search-messages`, `get-thread`,
|
|
168
|
+
`list-messages`, `list-mailboxes`, `get-unread-count`, `get-mail-stats`) prefer
|
|
169
|
+
IMAP automatically:
|
|
170
|
+
|
|
171
|
+
- explicit IMAP `account` → that account over IMAP;
|
|
172
|
+
- explicit non-IMAP `account` → AppleScript;
|
|
173
|
+
- **no `account` → fan out over every configured IMAP account**, with AppleScript
|
|
174
|
+
used only for the accounts no IMAP config covers.
|
|
175
|
+
|
|
176
|
+
The name still matters for targeting a specific account and for the
|
|
177
|
+
mailbox-write ops (`create`/`delete`/`rename-mailbox`), which route to IMAP only
|
|
178
|
+
for an explicitly named IMAP account.
|
|
166
179
|
|
|
167
180
|
### Multiple accounts
|
|
168
181
|
|
|
@@ -285,9 +298,17 @@ and a server started there can't read passwords either. Options:
|
|
|
285
298
|
restrict the config file's permissions to the service account.
|
|
286
299
|
|
|
287
300
|
**Calls aren't routing to IMAP even though `doctor` shows connected.**
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
301
|
+
For the six **read** tools this should not happen since v2.6.0 — they prefer IMAP
|
|
302
|
+
as soon as any account is configured, with no `account` argument needed. If reads
|
|
303
|
+
still look like AppleScript (slow, or partial-coverage warnings), the usual cause
|
|
304
|
+
is that the server never saw your settings at all: a host that strips the `env`
|
|
305
|
+
block (use the `config.json` method), or a Keychain service/account mismatch so
|
|
306
|
+
the password lookup fails. Run `doctor` — it reports each account separately.
|
|
307
|
+
|
|
308
|
+
For **targeting one specific account**, and for the mailbox-write ops
|
|
309
|
+
(`create`/`delete`/`rename-mailbox`), the `account` argument must match the
|
|
310
|
+
configured account's name/login: set `APPLE_MAIL_MCP_IMAP_ACCOUNT` to the exact
|
|
311
|
+
Mail.app account name from `list-accounts` and pass that same `account`.
|
|
291
312
|
|
|
292
313
|
**A disabled Mail account.** IMAP connects to the server directly, so it works
|
|
293
314
|
even if the account is disabled in Mail.app — useful for reaching an account the
|
package/package.json
CHANGED