proton-mail-bridge-client 2.11.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 (37) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.d.ts +4 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +89 -39
  5. package/dist/cli.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +36 -13
  8. package/dist/index.js.map +1 -1
  9. package/dist/services/background-sync-service.d.ts +1 -0
  10. package/dist/services/background-sync-service.d.ts.map +1 -1
  11. package/dist/services/background-sync-service.js +26 -0
  12. package/dist/services/background-sync-service.js.map +1 -1
  13. package/dist/services/local-index-service.d.ts.map +1 -1
  14. package/dist/services/local-index-service.js +42 -0
  15. package/dist/services/local-index-service.js.map +1 -1
  16. package/dist/services/simple-imap-service.d.ts +3 -0
  17. package/dist/services/simple-imap-service.d.ts.map +1 -1
  18. package/dist/services/simple-imap-service.js +58 -3
  19. package/dist/services/simple-imap-service.js.map +1 -1
  20. package/dist/services/sync-leader.d.ts +19 -0
  21. package/dist/services/sync-leader.d.ts.map +1 -0
  22. package/dist/services/sync-leader.js +128 -0
  23. package/dist/services/sync-leader.js.map +1 -0
  24. package/dist/types/index.d.ts +6 -0
  25. package/dist/types/index.d.ts.map +1 -1
  26. package/dist/utils/helpers.d.ts.map +1 -1
  27. package/dist/utils/helpers.js +14 -6
  28. package/dist/utils/helpers.js.map +1 -1
  29. package/dist/utils/pdf-text-worker.d.ts +2 -0
  30. package/dist/utils/pdf-text-worker.d.ts.map +1 -0
  31. package/dist/utils/pdf-text-worker.js +17 -0
  32. package/dist/utils/pdf-text-worker.js.map +1 -0
  33. package/dist/utils/pdf-text.d.ts +2 -0
  34. package/dist/utils/pdf-text.d.ts.map +1 -1
  35. package/dist/utils/pdf-text.js +28 -2
  36. package/dist/utils/pdf-text.js.map +1 -1
  37. package/package.json +1 -1
package/README.md CHANGED
@@ -695,6 +695,7 @@ To check a real Bridge yourself, run the read-only smoke test: `PROTONMAIL_USERN
695
695
  - `get_emails` and `search_emails` return a composite `emailId` — use it for all subsequent reads and actions.
696
696
  - `search_indexed_emails` supports `from:`, `to:`, `subject:`, `label:`, `domain:` shortcuts.
697
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.
698
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.
699
700
  - Audit logs live at `PROTONMAIL_DATA_DIR/audit.log`.
700
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;AAo0CD,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: [] },
@@ -655,9 +655,22 @@ async function withServices(run) {
655
655
  return await run({ config, ...services });
656
656
  }
657
657
  finally {
658
- services.backgroundSyncService.stop();
659
- 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
+ }
662
+ }
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(", ")}`);
660
672
  }
673
+ return [bundle];
661
674
  }
662
675
  async function syncIndex(context, input) {
663
676
  const snapshot = await context.imapService.collectEmailsForIndex({
@@ -740,41 +753,71 @@ async function runStatus(parsed) {
740
753
  process.stdout.write(wantJson ? json(result) : renderStatus(result));
741
754
  });
742
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
+ }
743
775
  async function runDoctor(parsed) {
744
776
  const wantJson = isTruthyFlag(parsed.flags.json);
745
- await withServices(async ({ config, smtpService, imapService, localIndexService }) => {
746
- const [claudeDesktop, index] = await Promise.all([
747
- getClaudeDesktopInstallStatus(),
748
- localIndexService.getStatus(),
749
- ]);
750
- let imapOk = false;
751
- let smtpOk = false;
752
- let folderCount = 0;
753
- let error;
754
- try {
755
- await imapService.ping();
756
- imapOk = true;
757
- folderCount = (await imapService.getFolders(true)).length;
758
- await smtpService.verifyConnection();
759
- smtpOk = true;
760
- }
761
- catch (err) {
762
- error = err instanceof Error ? err.message : String(err);
763
- }
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];
764
810
  const result = {
765
- ok: imapOk && smtpOk,
766
811
  // A stale/orphaned install elsewhere on disk (wrong path, old
767
812
  // version) looks identical to a fresh one on every other field
768
813
  // above — this is the field that actually distinguishes them.
769
814
  version: await getPkgVersion(),
770
815
  entrypoint: cliEntryPath(),
771
- account: config.smtp.username,
772
- imapOk,
773
- smtpOk,
774
- folderCount,
775
- 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),
776
819
  claudeDesktopInstalled: claudeDesktop.installed,
777
- error,
820
+ ...(checked.length > 1 ? { accounts: checked } : {}),
778
821
  };
779
822
  process.stdout.write(wantJson ? json(result) : `${result.ok ? "Doctor OK" : "Doctor failed"}\n${json(result)}`);
780
823
  if (!result.ok)
@@ -818,16 +861,23 @@ async function runConnectionStatus(parsed) {
818
861
  }
819
862
  async function runRuntimeStatus(parsed) {
820
863
  const wantJson = isTruthyFlag(parsed.flags.json);
821
- await withServices(async ({ config, backgroundSyncService, draftStore, localIndexService }) => {
822
- const result = {
823
- account: config.smtp.username,
824
- runtime: sanitizeRuntimeConfig(config.runtime),
825
- backgroundSync: backgroundSyncService.getStatus(),
826
- 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(),
827
872
  drafts: {
828
- total: (await draftStore.listDrafts(true)).length,
829
- active: (await draftStore.listDrafts(false)).length,
873
+ total: (await bundle.draftStore.listDrafts(true)).length,
874
+ active: (await bundle.draftStore.listDrafts(false)).length,
830
875
  },
876
+ })));
877
+ const { slug: _slug, ...firstFields } = perAccount[0];
878
+ const result = {
879
+ ...firstFields,
880
+ ...(perAccount.length > 1 ? { accounts: perAccount } : {}),
831
881
  };
832
882
  process.stdout.write(wantJson ? json(result) : `${JSON.stringify(result, null, 2)}\n`);
833
883
  });