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.
Files changed (50) hide show
  1. package/README.md +23 -20
  2. package/dist/cli.d.ts +4 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +94 -40
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +104 -28
  8. package/dist/index.js.map +1 -1
  9. package/dist/scripts/install-claude-desktop.d.ts.map +1 -1
  10. package/dist/scripts/install-claude-desktop.js +21 -2
  11. package/dist/scripts/install-claude-desktop.js.map +1 -1
  12. package/dist/services/account-manager.d.ts.map +1 -1
  13. package/dist/services/account-manager.js +1 -0
  14. package/dist/services/account-manager.js.map +1 -1
  15. package/dist/services/background-sync-service.d.ts +1 -0
  16. package/dist/services/background-sync-service.d.ts.map +1 -1
  17. package/dist/services/background-sync-service.js +26 -0
  18. package/dist/services/background-sync-service.js.map +1 -1
  19. package/dist/services/local-index-service.d.ts.map +1 -1
  20. package/dist/services/local-index-service.js +45 -0
  21. package/dist/services/local-index-service.js.map +1 -1
  22. package/dist/services/simple-imap-service.d.ts +12 -1
  23. package/dist/services/simple-imap-service.d.ts.map +1 -1
  24. package/dist/services/simple-imap-service.js +127 -22
  25. package/dist/services/simple-imap-service.js.map +1 -1
  26. package/dist/services/smtp-service.d.ts +3 -0
  27. package/dist/services/smtp-service.d.ts.map +1 -1
  28. package/dist/services/smtp-service.js +32 -5
  29. package/dist/services/smtp-service.js.map +1 -1
  30. package/dist/services/sync-leader.d.ts +19 -0
  31. package/dist/services/sync-leader.d.ts.map +1 -0
  32. package/dist/services/sync-leader.js +128 -0
  33. package/dist/services/sync-leader.js.map +1 -0
  34. package/dist/types/index.d.ts +16 -0
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/utils/helpers.d.ts.map +1 -1
  37. package/dist/utils/helpers.js +37 -7
  38. package/dist/utils/helpers.js.map +1 -1
  39. package/dist/utils/pdf-text-worker.d.ts +2 -0
  40. package/dist/utils/pdf-text-worker.d.ts.map +1 -0
  41. package/dist/utils/pdf-text-worker.js +17 -0
  42. package/dist/utils/pdf-text-worker.js.map +1 -0
  43. package/dist/utils/pdf-text.d.ts +25 -0
  44. package/dist/utils/pdf-text.d.ts.map +1 -0
  45. package/dist/utils/pdf-text.js +148 -0
  46. package/dist/utils/pdf-text.js.map +1 -0
  47. package/dist/utils/runtime-policy.d.ts.map +1 -1
  48. package/dist/utils/runtime-policy.js +2 -1
  49. package/dist/utils/runtime-policy.js.map +1 -1
  50. 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:** 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.
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
- Support multiple Proton Mail addresses configured in Bridge (using Bridge's **Split Addresses** feature). Each address gets its own isolated service stack, local index, and drafts storage.
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
- First, enable **Split Addresses** in Proton Bridge and configure each address as a separate account. Bridge will generate a separate password for each address. Then set `PROTONMAIL_ACCOUNTS_JSON`:
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
- "PROTONMAIL_IMAP_HOST": "127.0.0.1",
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
- **Note:** The password is the **Bridge password for each address**, not your Proton account password. Bridge generates a unique password for each Split Address.
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 first entry in `PROTONMAIL_ACCOUNTS_JSON` (or the main `PROTONMAIL_USERNAME`/`PROTONMAIL_PASSWORD` pair if not set). Email IDs from the primary account have no prefix.
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' # Bridge's local SMTP port requires implicit TLS from the first byte; set false only for a non-Bridge SMTP relay
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 and save to disk |
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 `PROTONMAIL_MAX_INLINE_BYTES`.
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;AAk0CD,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"}
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: Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string")),
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.backgroundSyncService.stop();
655
- await Promise.allSettled([services.imapService.disconnect(), services.smtpService.close()]);
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 ({ config, smtpService, imapService, localIndexService }) => {
742
- const [claudeDesktop, index] = await Promise.all([
743
- getClaudeDesktopInstallStatus(),
744
- localIndexService.getStatus(),
745
- ]);
746
- let imapOk = false;
747
- let smtpOk = false;
748
- let folderCount = 0;
749
- let error;
750
- try {
751
- await imapService.ping();
752
- imapOk = true;
753
- folderCount = (await imapService.getFolders(true)).length;
754
- await smtpService.verifyConnection();
755
- smtpOk = true;
756
- }
757
- catch (err) {
758
- error = err instanceof Error ? err.message : String(err);
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
- account: config.smtp.username,
768
- imapOk,
769
- smtpOk,
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
- error,
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 ({ config, backgroundSyncService, draftStore, localIndexService }) => {
818
- const result = {
819
- account: config.smtp.username,
820
- runtime: sanitizeRuntimeConfig(config.runtime),
821
- backgroundSync: backgroundSyncService.getStatus(),
822
- localIndex: await localIndexService.getStatus(),
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
  });