proton-mail-bridge-client 2.10.0 → 2.12.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 +23 -20
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +94 -40
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +104 -28
- package/dist/index.js.map +1 -1
- package/dist/scripts/install-claude-desktop.d.ts.map +1 -1
- package/dist/scripts/install-claude-desktop.js +21 -2
- package/dist/scripts/install-claude-desktop.js.map +1 -1
- package/dist/services/account-manager.d.ts.map +1 -1
- package/dist/services/account-manager.js +1 -0
- package/dist/services/account-manager.js.map +1 -1
- package/dist/services/background-sync-service.d.ts +1 -0
- package/dist/services/background-sync-service.d.ts.map +1 -1
- package/dist/services/background-sync-service.js +26 -0
- package/dist/services/background-sync-service.js.map +1 -1
- package/dist/services/local-index-service.d.ts.map +1 -1
- package/dist/services/local-index-service.js +45 -0
- package/dist/services/local-index-service.js.map +1 -1
- package/dist/services/simple-imap-service.d.ts +12 -1
- package/dist/services/simple-imap-service.d.ts.map +1 -1
- package/dist/services/simple-imap-service.js +127 -22
- package/dist/services/simple-imap-service.js.map +1 -1
- package/dist/services/smtp-service.d.ts +3 -0
- package/dist/services/smtp-service.d.ts.map +1 -1
- package/dist/services/smtp-service.js +32 -5
- package/dist/services/smtp-service.js.map +1 -1
- package/dist/services/sync-leader.d.ts +19 -0
- package/dist/services/sync-leader.d.ts.map +1 -0
- package/dist/services/sync-leader.js +128 -0
- package/dist/services/sync-leader.js.map +1 -0
- package/dist/types/index.d.ts +16 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/helpers.js +37 -7
- package/dist/utils/helpers.js.map +1 -1
- package/dist/utils/pdf-text-worker.d.ts +2 -0
- package/dist/utils/pdf-text-worker.d.ts.map +1 -0
- package/dist/utils/pdf-text-worker.js +17 -0
- package/dist/utils/pdf-text-worker.js.map +1 -0
- package/dist/utils/pdf-text.d.ts +25 -0
- package/dist/utils/pdf-text.d.ts.map +1 -0
- package/dist/utils/pdf-text.js +148 -0
- package/dist/utils/pdf-text.js.map +1 -0
- package/dist/utils/runtime-policy.d.ts.map +1 -1
- package/dist/utils/runtime-policy.js +2 -1
- package/dist/utils/runtime-policy.js.map +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -35,8 +35,8 @@ Give Claude Desktop (or Cline, or any MCP client) full access to your Proton Mai
|
|
|
35
35
|
> **No terminal?** Follow the [step-by-step guide](docs/install-for-non-developers.md) instead.
|
|
36
36
|
|
|
37
37
|
1. **Install and sign in to [Proton Mail Bridge](https://proton.me/mail/bridge)**, and leave it running. In the Bridge app, open your account and copy the **Bridge password** (it is not your Proton password).
|
|
38
|
-
2. **Connect Claude Desktop**, either way:
|
|
39
|
-
- **Bundle
|
|
38
|
+
2. **Connect Claude Desktop**, either way (quit Claude Desktop completely first, so it does not overwrite the config on exit):
|
|
39
|
+
- **Bundle** (needs Node.js 24 or later on the machine): download the `proton-mail-bridge-client-<your-os>.mcpb` for macOS (`darwin-arm64` Apple silicon, `darwin-x64` Intel), Linux (`linux-x64`, `linux-arm64`) or Windows (`win32-x64`) from the [latest release](https://github.com/googlarz/proton-mail-bridge-client/releases/latest) and open it. Claude Desktop asks for your Proton address and the Bridge password, plus optional settings (Bridge IMAP/SMTP host and port, read-only, allow sending, signature, extra accounts, data and download folders, tool tier); the defaults match a standard Bridge install.
|
|
40
40
|
- **npm:** `npm install -g proton-mail-bridge-client`, then `proton-mail-bridge-client setup-claude-desktop` (it checks your Bridge ports and writes the config). Fully quit and reopen Claude Desktop afterwards.
|
|
41
41
|
3. **Check it works:** run `proton-mail-bridge-client doctor`, or ask Claude "run the Proton Mail doctor".
|
|
42
42
|
4. **Try these prompts:**
|
|
@@ -81,7 +81,7 @@ Download: [proton.me/mail/bridge](https://proton.me/mail/bridge)
|
|
|
81
81
|
|
|
82
82
|
**3. Your Bridge credentials** — from the Bridge app:
|
|
83
83
|
- IMAP host/port (default: `127.0.0.1:1143`)
|
|
84
|
-
- SMTP host/port (default: `127.0.0.1:1025`)
|
|
84
|
+
- SMTP host/port (default: `127.0.0.1:1025`); the IMAP and SMTP password is the same Bridge password
|
|
85
85
|
- Username (your Proton email address)
|
|
86
86
|
- Bridge password (see note above)
|
|
87
87
|
|
|
@@ -290,11 +290,11 @@ Reload the Cline extension after saving. Proton Mail tools will appear in Cline'
|
|
|
290
290
|
|
|
291
291
|
## Multi-account support (v2.1.0+)
|
|
292
292
|
|
|
293
|
-
|
|
293
|
+
Use more than one Proton mailbox from the same server: either several addresses of one account (Bridge's **Split Addresses** feature) or separate Proton accounts signed in to the same Bridge. Each one gets its own isolated service stack, local index, and drafts storage.
|
|
294
294
|
|
|
295
295
|
### Setup
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
In Proton Bridge, make each address or account its own login: with **Split Addresses** on, every address has a login; separate accounts each have their own. Bridge generates a separate password for each login, and the same password works for IMAP and SMTP. The first account is `PROTONMAIL_USERNAME` / `PROTONMAIL_PASSWORD`; **`PROTONMAIL_ACCOUNTS_JSON` lists only the additional ones**, as a JSON array on one line:
|
|
298
298
|
|
|
299
299
|
```json
|
|
300
300
|
{
|
|
@@ -304,27 +304,22 @@ First, enable **Split Addresses** in Proton Bridge and configure each address as
|
|
|
304
304
|
"env": {
|
|
305
305
|
"PROTONMAIL_USERNAME": "primary@proton.me",
|
|
306
306
|
"PROTONMAIL_PASSWORD": "primary-bridge-password",
|
|
307
|
-
"
|
|
308
|
-
"PROTONMAIL_IMAP_PORT": "1143",
|
|
309
|
-
"PROTONMAIL_IMAP_SECURE": "false",
|
|
310
|
-
"PROTONMAIL_SMTP_HOST": "127.0.0.1",
|
|
311
|
-
"PROTONMAIL_SMTP_PORT": "1025",
|
|
312
|
-
"PROTONMAIL_ACCOUNTS_JSON": "[
|
|
313
|
-
{ \"address\": \"primary@proton.me\", \"password\": \"primary-bridge-password\" },
|
|
314
|
-
{ \"address\": \"alias@proton.me\", \"password\": \"alias-bridge-password\" },
|
|
315
|
-
{ \"address\": \"business@custom.com\", \"password\": \"business-bridge-password\" }
|
|
316
|
-
]"
|
|
307
|
+
"PROTONMAIL_ACCOUNTS_JSON": "[{\"address\": \"alias@proton.me\", \"password\": \"alias-bridge-password\"}, {\"address\": \"business@custom.com\", \"password\": \"business-bridge-password\"}]"
|
|
317
308
|
}
|
|
318
309
|
}
|
|
319
310
|
}
|
|
320
311
|
}
|
|
321
312
|
```
|
|
322
313
|
|
|
323
|
-
|
|
314
|
+
Instead of `password` an entry can use `passwordFile` (path to a file with the Bridge password) or `passwordCommand` (a command that prints it), so the password need not sit in the client config; use only one of the three. `autoSyncFolder` sets that account's folders for background sync (default `PROTONMAIL_AUTO_SYNC_FOLDER`). Folders in a list that an account does not have are skipped and reported as `skippedFolders`.
|
|
315
|
+
|
|
316
|
+
Listing the primary address again is refused at startup ("duplicate address"). Host, ports and the TLS mode default to the primary account's; an entry can override them with `imapHost`, `imapPort`, `imapSecure`, `smtpHost`, `smtpPort` and `smtpSecure`.
|
|
317
|
+
|
|
318
|
+
**Note:** The password is the **Bridge password for each login**, not your Proton account password.
|
|
324
319
|
|
|
325
320
|
### How it works
|
|
326
321
|
|
|
327
|
-
- **Primary account:** the
|
|
322
|
+
- **Primary account:** the `PROTONMAIL_USERNAME`/`PROTONMAIL_PASSWORD` pair. Email IDs from the primary account have no prefix.
|
|
328
323
|
- **Additional accounts:** each has an auto-generated slug (e.g., `alias`, `business`). Email IDs carry an account prefix: `alias::message-id`, `business::message-id`.
|
|
329
324
|
- **Sending:** `send_email`, `reply_to_email`, etc. route through the account matching the `from` address. If no account matches, the primary connection is used with a header override (for non-configured aliases).
|
|
330
325
|
- **Search and read:** all read tools (search, threads, analytics, digest) automatically fan out across all configured accounts and merge results, preserving the account prefix in returned IDs.
|
|
@@ -484,7 +479,7 @@ PROTONMAIL_IMAP_PORT='1143'
|
|
|
484
479
|
PROTONMAIL_IMAP_SECURE='false'
|
|
485
480
|
PROTONMAIL_SMTP_HOST='127.0.0.1'
|
|
486
481
|
PROTONMAIL_SMTP_PORT='1025'
|
|
487
|
-
PROTONMAIL_SMTP_SECURE='true' #
|
|
482
|
+
PROTONMAIL_SMTP_SECURE='true' # true = implicit TLS, false = STARTTLS. Unset, the server tries the other mode itself if Bridge's SMTP mode differs
|
|
488
483
|
PROTONMAIL_IMAP_USERNAME='' # optional: log in to IMAP as a different user than PROTONMAIL_USERNAME (default: the same). The send identity, and RESTRICT_OUTBOUND_TO_SELF, still use PROTONMAIL_USERNAME.
|
|
489
484
|
PROTONMAIL_IMAP_PASSWORD='' # optional: a different IMAP password than PROTONMAIL_PASSWORD (default: the same)
|
|
490
485
|
|
|
@@ -528,6 +523,11 @@ PROTONMAIL_SYNC_INTERVAL_MINUTES='5'
|
|
|
528
523
|
PROTONMAIL_IDLE_WATCH='true'
|
|
529
524
|
PROTONMAIL_IDLE_MAX_SECONDS='30'
|
|
530
525
|
|
|
526
|
+
# Attachments
|
|
527
|
+
PROTONMAIL_MAX_INLINE_KB='40' # in KB (older name PROTONMAIL_MAX_INLINE_BYTES still works, also in KB): largest attachment get_attachment_content returns inline as base64
|
|
528
|
+
PROTONMAIL_PDF_MAX_MB='10' # largest PDF get_attachment_text extracts text from (1-100 MB); bigger ones are refused with a message
|
|
529
|
+
PROTONMAIL_PDF_MAX_PAGES='50' # pages get_attachment_text reads from a PDF (1-2000); a longer PDF is cut off with a note saying how many pages are missing
|
|
530
|
+
|
|
531
531
|
# Tuning and diagnostics
|
|
532
532
|
PROTONMAIL_OP_DELAY_MS='0' # minimum gap in ms between IMAP mailbox operations (0-5000); raise it to be gentler on a slow Bridge. 0 = no throttling
|
|
533
533
|
PROTONMAIL_DEBUG='false' # true: get_connection_status and run_doctor include the raw underlying connection error text (otherwise only a classified cause and suggestion)
|
|
@@ -548,7 +548,7 @@ With several accounts or a signature, the server sends from whichever `from` the
|
|
|
548
548
|
| Search and read | Native Claude UX | IMAP + local index |
|
|
549
549
|
| Send email | No | Yes |
|
|
550
550
|
| Draft workflows | Better first-party UX | Full control incl. remote draft sync |
|
|
551
|
-
| Attachment content | Limited | Fetch
|
|
551
|
+
| Attachment content | Limited | Fetch, save to disk, read text from PDFs |
|
|
552
552
|
| Mailbox actions | Limited | Full (star, move, archive, trash, restore, delete, batch) |
|
|
553
553
|
| Folder management | No | Yes (create, rename, delete) |
|
|
554
554
|
| CLI access | No | Full parity with MCP |
|
|
@@ -594,6 +594,8 @@ With several accounts or a signature, the server sends from whichever `from` the
|
|
|
594
594
|
### Import/export & attachments
|
|
595
595
|
`export_email` · `import_email` · `get_attachment_text` · `get_emails_by_ids`
|
|
596
596
|
|
|
597
|
+
`get_attachment_text` reads text-like attachments and PDFs. A PDF comes back page by page, each page headed `--- Page N of M ---`, and the result has a `pdf` field with `totalPages`, `extractedPages`, `omittedPages` and `truncated`. It is limited by `PROTONMAIL_PDF_MAX_MB` (default 10) and `PROTONMAIL_PDF_MAX_PAGES` (default 50); past the page limit the text ends with a line saying how many pages were left out. A password-protected PDF, a damaged file and a scan without a text layer each fail with a short explanation. The PDF is read in memory with [unpdf](https://github.com/unjs/unpdf) (pure JavaScript, no native module): nothing is written to disk, nothing is downloaded, and a document that takes longer than 30 seconds is stopped.
|
|
598
|
+
|
|
597
599
|
---
|
|
598
600
|
|
|
599
601
|
## Using it as a library
|
|
@@ -666,7 +668,7 @@ Measured in October 2026 (v2.7.0) on a copy of the same ~57,000-message index. "
|
|
|
666
668
|
- List and search results are compact by default: attachments are metadata only, and empty or derivable fields are left out. Pass `fields: ["subject","from","date"]` to trim further — 10 results is roughly 800 tokens that way, versus about 11,000 for 50 full results.
|
|
667
669
|
- Defaults are bounded: `search_emails`/`search_indexed_emails` return 50 (pass `limit` for more), `list_drafts` 20, `list_scheduled_sends` 50 newest first. Each reports `hasMore`; page with `offset`.
|
|
668
670
|
- Draft and queued-message bodies are shortened to a preview in list and update responses (`bodyTruncated`, `bodyLength`); use `get_draft` for one full draft. Attachment content is never inlined in a listing.
|
|
669
|
-
- Large attachments: set `PROTONMAIL_ALLOW_FILE_DOWNLOAD_DIR` and pass `saveTo`, rather than raising `
|
|
671
|
+
- Large attachments: set `PROTONMAIL_ALLOW_FILE_DOWNLOAD_DIR` and pass `saveTo`, rather than raising `PROTONMAIL_MAX_INLINE_KB`.
|
|
670
672
|
- **`PROTONMAIL_TOOL_TIER=core`** exposes 25 tools instead of 100: about 7k tokens of tool definitions instead of about 21k (roughly 67% less; the earlier measurement with a reference tokenizer, 7k against 20k for 96 tools, scaled by the 3.7% the four newest tool definitions add), paid on every conversation that loads the server.
|
|
671
673
|
- Repeating an `update_draft` that changes nothing on a draft whose remote copy is already in sync does no store write and no IMAP upload. (A draft whose last sync failed, or that was edited with `syncToRemote:false`, still syncs.)
|
|
672
674
|
|
|
@@ -693,6 +695,7 @@ To check a real Bridge yourself, run the read-only smoke test: `PROTONMAIL_USERN
|
|
|
693
695
|
- `get_emails` and `search_emails` return a composite `emailId` — use it for all subsequent reads and actions.
|
|
694
696
|
- `search_indexed_emails` supports `from:`, `to:`, `subject:`, `label:`, `domain:` shortcuts.
|
|
695
697
|
- The local index lives at `PROTONMAIL_DATA_DIR/mail-index.sqlite`. Background sync and IMAP IDLE keep it warm.
|
|
698
|
+
- **Several clients at once.** Every MCP client starts its own server. Per data directory, only one of them runs the background sync and the IDLE watch (it holds `background-sync.lock`); the others serve tools from the index and take over when it exits. `get_runtime_status` shows `backgroundSync.leader`. A manual `sync_emails` / `run_background_sync` always runs.
|
|
696
699
|
- Filters on the local index ignore case and accents, and match spelling variants: `Müller` finds `Mueller`, `lodz` finds `Łódź`, `strasse` finds `Straße`. Live `search_emails` and `count_messages` go through Bridge: a query with umlauts or other non-ASCII letters also matches the spelled-out form (`Müller` finds `Mueller`), but a plain ASCII query is sent as typed, so `Mueller` does not find `Müller` there. Use `search_indexed_emails` for that.
|
|
697
700
|
- Audit logs live at `PROTONMAIL_DATA_DIR/audit.log`.
|
|
698
701
|
- Draft sync is best-effort — the local draft is always preserved even if remote sync fails.
|
package/dist/cli.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export declare function completionCommands(): Array<{
|
|
|
24
24
|
export declare function completionScript(shell: string): string;
|
|
25
25
|
export declare function commandHelpText(command: string): string | undefined;
|
|
26
26
|
export declare function toolResultExitCode(toolName: string, result: Record<string, unknown>): number;
|
|
27
|
+
export declare function diagnoseConnectionFailure(reason: unknown): {
|
|
28
|
+
cause: string;
|
|
29
|
+
suggestion: string;
|
|
30
|
+
} | undefined;
|
|
27
31
|
interface ToolOnlyCommand {
|
|
28
32
|
command: string;
|
|
29
33
|
tool: string;
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA8BA,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,QAAQ,CAAC;CACjB;AAiBD,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAM3C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAuD1D;AA0ID,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAOD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAoErD,CAAC;AAoBF,eAAO,MAAM,iBAAiB,YAAI,KAAK,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAGlE,wBAAgB,kBAAkB,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAQlG;AAID,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA+DtD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcnE;AAyKD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAY5F;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA8BA,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,QAAQ,CAAC;CACjB;AAiBD,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAM3C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAuD1D;AA0ID,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAOD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAoErD,CAAC;AAoBF,eAAO,MAAM,iBAAiB,YAAI,KAAK,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAGlE,wBAAgB,kBAAkB,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAQlG;AAID,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA+DtD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcnE;AAyKD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAY5F;AAiND,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAiB5G;AAupCD,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IAIb,SAAS,CAAC,EAAE,MAAM,CAAC;IAMnB,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,eAAO,MAAM,kBAAkB,EAAE,eAAe,EAmE/C,CAAC;AA2EF,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAqO1C;AA4ED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGzD"}
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
8
8
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
9
9
|
import { buildConfigFromEnv, createServer, withAudit } from "./index.js";
|
|
10
10
|
import { isMainModule } from "./is-main.js";
|
|
11
|
-
import { SimpleIMAPService } from "./services/simple-imap-service.js";
|
|
11
|
+
import { isLikelyAuthenticationError, isLikelyConnectionError, isLikelyTlsMismatchError, SimpleIMAPService } from "./services/simple-imap-service.js";
|
|
12
12
|
import { ensureDestructiveConfirmed, ensureEmailActionAllowed, ensureMailboxWriteAllowed, sanitizeRuntimeConfig } from "./utils/runtime-policy.js";
|
|
13
13
|
import { isValidEmail, parseEmails } from "./utils/helpers.js";
|
|
14
14
|
import { buildSchedule, defaultSchedulerSystem, findStableAlias, parseTimeOfDay, SCHEDULER_SYSTEMS } from "./utils/schedule-templates.js";
|
|
@@ -235,9 +235,9 @@ export const COMMAND_SPECS = {
|
|
|
235
235
|
"setup-claude-desktop": { usage: "setup-claude-desktop", description: "Run the Claude Desktop setup wizard (works from any install)", flags: [] },
|
|
236
236
|
claude: { usage: "claude <setup|install|check|update|doctor>", description: "Claude Desktop integration: setup wizard, install or update the runtime, check status (update is an alias for install, doctor for check)", flags: [] },
|
|
237
237
|
status: { usage: "status", description: "Show local config, index, runtime, and Claude Desktop status", flags: [] },
|
|
238
|
-
doctor: { usage: "doctor", description: "Verify IMAP, SMTP, and Claude Desktop wiring (exits 1 when the check fails)", flags: [] },
|
|
238
|
+
doctor: { usage: "doctor [--account <slug|address>]", description: "Verify IMAP, SMTP, and Claude Desktop wiring for every account, or one (exits 1 when the check fails)", flags: ["account"] },
|
|
239
239
|
"connection-status": { usage: "connection-status", description: "Show live IMAP/SMTP connectivity state (exits 1 when either is unreachable)", flags: [] },
|
|
240
|
-
"runtime-status": { usage: "runtime-status", description: "Show runtime policy and background sync state", flags: [] },
|
|
240
|
+
"runtime-status": { usage: "runtime-status [--account <slug|address>]", description: "Show runtime policy and background sync state, per account", flags: ["account"] },
|
|
241
241
|
sync: { usage: "sync [--folder <name>] [--limit <n>] [--full]", description: "Refresh the local index from Proton Bridge", flags: ["folder", "limit", "full", "no-attachment-text"] },
|
|
242
242
|
"index-status": { usage: "index-status", description: "Show local index health and freshness", flags: [] },
|
|
243
243
|
folders: { usage: "folders", description: "List available folders from Proton Bridge", flags: [] },
|
|
@@ -577,7 +577,11 @@ async function withMcpClient(run) {
|
|
|
577
577
|
const transport = new StdioClientTransport({
|
|
578
578
|
command: process.execPath,
|
|
579
579
|
args: [cliEntryPath()],
|
|
580
|
-
env:
|
|
580
|
+
env: {
|
|
581
|
+
...Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string")),
|
|
582
|
+
// The CLI reports a failed tool on stderr with its own exit codes; MCP clients get an isError result instead.
|
|
583
|
+
PROTONMAIL_TOOL_ERRORS: "protocol",
|
|
584
|
+
},
|
|
581
585
|
stderr: "ignore",
|
|
582
586
|
});
|
|
583
587
|
const client = new Client({
|
|
@@ -651,10 +655,23 @@ async function withServices(run) {
|
|
|
651
655
|
return await run({ config, ...services });
|
|
652
656
|
}
|
|
653
657
|
finally {
|
|
654
|
-
services.
|
|
655
|
-
|
|
658
|
+
for (const bundle of services.accountManager.all()) {
|
|
659
|
+
bundle.backgroundSyncService.stop();
|
|
660
|
+
await Promise.allSettled([bundle.imapService.disconnect(), bundle.smtpService.close()]);
|
|
661
|
+
}
|
|
656
662
|
}
|
|
657
663
|
}
|
|
664
|
+
// --account picks one account (slug or address); without it, every configured account.
|
|
665
|
+
function selectAccountBundles(accountManager, parsed) {
|
|
666
|
+
const wanted = getStringFlag(parsed.flags, "account");
|
|
667
|
+
if (!wanted)
|
|
668
|
+
return accountManager.all();
|
|
669
|
+
const bundle = accountManager.byAddress(wanted) ?? accountManager.all().find((entry) => entry.account.slug === wanted);
|
|
670
|
+
if (!bundle) {
|
|
671
|
+
throw new CliUsageError(`Unknown account "${wanted}". Configured: ${accountManager.all().map((entry) => entry.account.slug).join(", ")}`);
|
|
672
|
+
}
|
|
673
|
+
return [bundle];
|
|
674
|
+
}
|
|
658
675
|
async function syncIndex(context, input) {
|
|
659
676
|
const snapshot = await context.imapService.collectEmailsForIndex({
|
|
660
677
|
...input,
|
|
@@ -736,41 +753,71 @@ async function runStatus(parsed) {
|
|
|
736
753
|
process.stdout.write(wantJson ? json(result) : renderStatus(result));
|
|
737
754
|
});
|
|
738
755
|
}
|
|
756
|
+
// Same classification as the run_doctor tool, so the CLI explains a failure instead of printing the raw error.
|
|
757
|
+
export function diagnoseConnectionFailure(reason) {
|
|
758
|
+
if (isLikelyAuthenticationError(reason)) {
|
|
759
|
+
return {
|
|
760
|
+
cause: "authentication_failed",
|
|
761
|
+
suggestion: "PROTONMAIL_PASSWORD must be the Bridge password (Bridge app -> account -> Mailbox details), not your Proton account password.",
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
if (isLikelyTlsMismatchError(reason)) {
|
|
765
|
+
return {
|
|
766
|
+
cause: "tls_mismatch",
|
|
767
|
+
suggestion: "TLS handshake failed: PROTONMAIL_IMAP_SECURE / PROTONMAIL_SMTP_SECURE probably does not match the connection mode of that port in Bridge (implicit TLS vs STARTTLS).",
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
if (isLikelyConnectionError(reason)) {
|
|
771
|
+
return { cause: "bridge_unreachable", suggestion: "Bridge is not reachable: check that it is running and that the host and ports match Bridge's Mailbox details." };
|
|
772
|
+
}
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
739
775
|
async function runDoctor(parsed) {
|
|
740
776
|
const wantJson = isTruthyFlag(parsed.flags.json);
|
|
741
|
-
await withServices(async ({
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
777
|
+
await withServices(async ({ accountManager }) => {
|
|
778
|
+
const bundles = selectAccountBundles(accountManager, parsed);
|
|
779
|
+
const checked = await Promise.all(bundles.map(async (bundle) => {
|
|
780
|
+
let imapOk = false;
|
|
781
|
+
let smtpOk = false;
|
|
782
|
+
let folderCount = 0;
|
|
783
|
+
let error;
|
|
784
|
+
let diagnosis;
|
|
785
|
+
try {
|
|
786
|
+
await bundle.imapService.ping();
|
|
787
|
+
imapOk = true;
|
|
788
|
+
folderCount = (await bundle.imapService.getFolders(true)).length;
|
|
789
|
+
await bundle.smtpService.verifyConnection();
|
|
790
|
+
smtpOk = true;
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
error = err instanceof Error ? err.message : String(err);
|
|
794
|
+
diagnosis = diagnoseConnectionFailure(err);
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
slug: bundle.account.slug,
|
|
798
|
+
account: bundle.config.smtp.username,
|
|
799
|
+
ok: imapOk && smtpOk,
|
|
800
|
+
imapOk,
|
|
801
|
+
smtpOk,
|
|
802
|
+
folderCount,
|
|
803
|
+
indexUpdatedAt: (await bundle.localIndexService.getStatus()).updatedAt,
|
|
804
|
+
error,
|
|
805
|
+
...(diagnosis ? { cause: diagnosis.cause, suggestion: diagnosis.suggestion } : {}),
|
|
806
|
+
};
|
|
807
|
+
}));
|
|
808
|
+
const claudeDesktop = await getClaudeDesktopInstallStatus();
|
|
809
|
+
const { slug: _slug, ...firstFields } = checked[0];
|
|
760
810
|
const result = {
|
|
761
|
-
ok: imapOk && smtpOk,
|
|
762
811
|
// A stale/orphaned install elsewhere on disk (wrong path, old
|
|
763
812
|
// version) looks identical to a fresh one on every other field
|
|
764
813
|
// above — this is the field that actually distinguishes them.
|
|
765
814
|
version: await getPkgVersion(),
|
|
766
815
|
entrypoint: cliEntryPath(),
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
folderCount,
|
|
771
|
-
indexUpdatedAt: index.updatedAt,
|
|
816
|
+
...firstFields,
|
|
817
|
+
// Top-level fields describe the first account shown; ok covers all of them.
|
|
818
|
+
ok: checked.every((entry) => entry.ok),
|
|
772
819
|
claudeDesktopInstalled: claudeDesktop.installed,
|
|
773
|
-
|
|
820
|
+
...(checked.length > 1 ? { accounts: checked } : {}),
|
|
774
821
|
};
|
|
775
822
|
process.stdout.write(wantJson ? json(result) : `${result.ok ? "Doctor OK" : "Doctor failed"}\n${json(result)}`);
|
|
776
823
|
if (!result.ok)
|
|
@@ -814,16 +861,23 @@ async function runConnectionStatus(parsed) {
|
|
|
814
861
|
}
|
|
815
862
|
async function runRuntimeStatus(parsed) {
|
|
816
863
|
const wantJson = isTruthyFlag(parsed.flags.json);
|
|
817
|
-
await withServices(async ({
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
864
|
+
await withServices(async ({ accountManager }) => {
|
|
865
|
+
const bundles = selectAccountBundles(accountManager, parsed);
|
|
866
|
+
const perAccount = await Promise.all(bundles.map(async (bundle) => ({
|
|
867
|
+
slug: bundle.account.slug,
|
|
868
|
+
account: bundle.config.smtp.username,
|
|
869
|
+
runtime: sanitizeRuntimeConfig(bundle.config.runtime),
|
|
870
|
+
backgroundSync: bundle.backgroundSyncService.getStatus(),
|
|
871
|
+
localIndex: await bundle.localIndexService.getStatus(),
|
|
823
872
|
drafts: {
|
|
824
|
-
total: (await draftStore.listDrafts(true)).length,
|
|
825
|
-
active: (await draftStore.listDrafts(false)).length,
|
|
873
|
+
total: (await bundle.draftStore.listDrafts(true)).length,
|
|
874
|
+
active: (await bundle.draftStore.listDrafts(false)).length,
|
|
826
875
|
},
|
|
876
|
+
})));
|
|
877
|
+
const { slug: _slug, ...firstFields } = perAccount[0];
|
|
878
|
+
const result = {
|
|
879
|
+
...firstFields,
|
|
880
|
+
...(perAccount.length > 1 ? { accounts: perAccount } : {}),
|
|
827
881
|
};
|
|
828
882
|
process.stdout.write(wantJson ? json(result) : `${JSON.stringify(result, null, 2)}\n`);
|
|
829
883
|
});
|