proton-mail-bridge-client 1.19.2 → 1.19.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.
Files changed (46) hide show
  1. package/README.md +3 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +88 -11
  4. package/dist/index.js.map +1 -1
  5. package/dist/services/analytics-service.d.ts.map +1 -1
  6. package/dist/services/analytics-service.js +20 -5
  7. package/dist/services/analytics-service.js.map +1 -1
  8. package/dist/services/audit-service.d.ts +1 -0
  9. package/dist/services/audit-service.d.ts.map +1 -1
  10. package/dist/services/audit-service.js +17 -3
  11. package/dist/services/audit-service.js.map +1 -1
  12. package/dist/services/delivery-queue-service.d.ts +1 -0
  13. package/dist/services/delivery-queue-service.d.ts.map +1 -1
  14. package/dist/services/delivery-queue-service.js +59 -6
  15. package/dist/services/delivery-queue-service.js.map +1 -1
  16. package/dist/services/draft-store-service.d.ts +1 -0
  17. package/dist/services/draft-store-service.d.ts.map +1 -1
  18. package/dist/services/draft-store-service.js +27 -2
  19. package/dist/services/draft-store-service.js.map +1 -1
  20. package/dist/services/local-index-service.d.ts +1 -0
  21. package/dist/services/local-index-service.d.ts.map +1 -1
  22. package/dist/services/local-index-service.js +60 -8
  23. package/dist/services/local-index-service.js.map +1 -1
  24. package/dist/services/simple-imap-service.d.ts +5 -0
  25. package/dist/services/simple-imap-service.d.ts.map +1 -1
  26. package/dist/services/simple-imap-service.js +153 -40
  27. package/dist/services/simple-imap-service.js.map +1 -1
  28. package/dist/services/snooze-service.d.ts +1 -0
  29. package/dist/services/snooze-service.d.ts.map +1 -1
  30. package/dist/services/snooze-service.js +61 -4
  31. package/dist/services/snooze-service.js.map +1 -1
  32. package/dist/services/template-service.js +2 -2
  33. package/dist/services/template-service.js.map +1 -1
  34. package/dist/types/index.d.ts +1 -0
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/dist/utils/file-lock.d.ts.map +1 -1
  37. package/dist/utils/file-lock.js +47 -1
  38. package/dist/utils/file-lock.js.map +1 -1
  39. package/dist/utils/helpers.d.ts +1 -0
  40. package/dist/utils/helpers.d.ts.map +1 -1
  41. package/dist/utils/helpers.js +30 -0
  42. package/dist/utils/helpers.js.map +1 -1
  43. package/dist/utils/runtime-policy.d.ts.map +1 -1
  44. package/dist/utils/runtime-policy.js +4 -2
  45. package/dist/utils/runtime-policy.js.map +1 -1
  46. package/package.json +1 -1
package/README.md CHANGED
@@ -325,7 +325,7 @@ All commands support `--json` for machine-readable output, and any MCP tool is d
325
325
  All flags work in both the MCP server and CLI:
326
326
 
327
327
  ```bash
328
- PROTONMAIL_TOOL_TIER=core # expose 20 core tools instead of all 95 — saves context window
328
+ PROTONMAIL_TOOL_TIER=core # expose 19 core tools instead of all 95 — saves context window
329
329
  PROTONMAIL_READ_ONLY=true # disable all write operations
330
330
  PROTONMAIL_ALLOW_SEND=false # disable SMTP sends only (other writes still work)
331
331
  PROTONMAIL_CONFIRM_DESTRUCTIVE=true # require confirmed:true on send, reply, forward, delete
@@ -359,13 +359,13 @@ PROTONMAIL_PASSWORD_COMMAND='pass proton/password'
359
359
  PROTONMAIL_DATA_DIR="$HOME/.proton-mail-bridge-client"
360
360
 
361
361
  # Tools
362
- PROTONMAIL_TOOL_TIER='full' # 'core' exposes 20 essential tools (saves context window); 'full' exposes all 95
362
+ PROTONMAIL_TOOL_TIER='full' # 'core' exposes 19 essential tools (saves context window); 'full' exposes all 95
363
363
 
364
364
  # Safety
365
365
  PROTONMAIL_READ_ONLY='false'
366
366
  PROTONMAIL_ALLOW_SEND='true'
367
367
  PROTONMAIL_ALLOW_REMOTE_DRAFT_SYNC='true'
368
- PROTONMAIL_ALLOWED_ACTIONS='mark_read,mark_unread,star,unstar,archive,trash,restore'
368
+ PROTONMAIL_ALLOWED_ACTIONS='mark_read,mark_unread,star,unstar,archive,trash,restore,move,delete'
369
369
  PROTONMAIL_CONFIRM_DESTRUCTIVE='false'
370
370
  PROTONMAIL_SEND_DELAY_SECONDS='0' # >0: send_email queues instead of sending immediately, cancelable via cancel_send. Only fires while this server stays running.
371
371
  PROTONMAIL_SIGNATURE='' # Plain text, appended to send_email/reply_to_email/reply_all_email/forward_email bodies (text + HTML), after your own text and before any quoted/forwarded content. Opt out per-message with appendSignature: false. Never applied to send_draft/schedule_draft — draft content is already finalized.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAQA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAYnE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAiG,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACrK,OAAO,EAAkB,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE7D,OAAO,KAAK,EAUV,WAAW,EAEX,gBAAgB,EAEjB,MAAM,kBAAkB,CAAC;AAsqD1B,eAAO,MAAM,eAAe,aAyB1B,CAAC;AAgGH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CAwBZ;AAmED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK3G;AAKD,wBAAgB,0BAA0B,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAI1G;AASD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAa9E;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAS7F;AA4KD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,OAAO,GAChB;IAAE,EAAE,EAAE,MAAM,EAAE,CAAC;IAAC,EAAE,EAAE,MAAM,EAAE,CAAA;CAAE,CAyBhC;AA6vBD,wBAAgB,kBAAkB,IAAI,gBAAgB,CAgGrD;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE;IACP,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgzFP;AAED,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAqD1C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AASA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAYnE,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAA2H,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC/L,OAAO,EAAkB,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE7D,OAAO,KAAK,EAUV,WAAW,EAEX,gBAAgB,EAEjB,MAAM,kBAAkB,CAAC;AA0qD1B,eAAO,MAAM,eAAe,aAyB1B,CAAC;AAgGH,wBAAsB,SAAS,CAAC,CAAC,EAC/B,YAAY,EAAE,YAAY,EAC1B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CAwBZ;AAmED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK3G;AAKD,wBAAgB,0BAA0B,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAI1G;AASD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAa9E;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,WAAW,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAS7F;AA4KD,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,OAAO,GAChB;IAAE,EAAE,EAAE,MAAM,EAAE,CAAC;IAAC,EAAE,EAAE,MAAM,EAAE,CAAA;CAAE,CAyBhC;AA4wBD,wBAAgB,kBAAkB,IAAI,gBAAgB,CAkGrD;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE;IACP,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAg2FP;AAED,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAmE1C"}
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { readFileSync } from "node:fs";
4
+ import { chmod, mkdir } from "node:fs/promises";
4
5
  import { createRequire } from "node:module";
5
6
  import { homedir } from "node:os";
6
- import { isAbsolute, join, resolve as pathResolve } from "node:path";
7
+ import { isAbsolute, join, resolve as pathResolve, sep as pathSep } from "node:path";
7
8
  import { fileURLToPath } from "node:url";
8
9
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
10
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -15,7 +16,7 @@ import { BackgroundSyncService } from "./services/background-sync-service.js";
15
16
  import { DeliveryQueueService } from "./services/delivery-queue-service.js";
16
17
  import { DraftStoreService } from "./services/draft-store-service.js";
17
18
  import { LocalIndexService } from "./services/local-index-service.js";
18
- import { BULK_ITEM_TIMEOUT_MS, describeImapError, isLikelyAuthenticationError, isLikelyConnectionError, SimpleIMAPService } from "./services/simple-imap-service.js";
19
+ import { BULK_ITEM_TIMEOUT_MS, describeImapError, isLikelyAuthenticationError, isLikelyConnectionError, isLikelyTlsMismatchError, SimpleIMAPService } from "./services/simple-imap-service.js";
19
20
  import { applySignature, SMTPService } from "./services/smtp-service.js";
20
21
  import { SnoozeService } from "./services/snooze-service.js";
21
22
  import { TemplateService } from "./services/template-service.js";
@@ -497,12 +498,13 @@ const TOOLS = [
497
498
  },
498
499
  {
499
500
  name: "delete_draft",
500
- description: "Permanently delete a locally saved draft from SQLite. Use to discard a draft you no longer need. Does NOT remove a matching draft from the Proton Drafts IMAP folder — that requires a separate mailbox action. Irreversible.",
501
+ description: "Permanently delete a locally saved draft from SQLite. Use to discard a draft you no longer need. Does NOT remove a matching draft from the Proton Drafts IMAP folder — that requires a separate mailbox action. Irreversible; requires confirmed:true when PROTONMAIL_CONFIRM_DESTRUCTIVE is enabled.",
501
502
  annotations: { destructiveHint: true },
502
503
  inputSchema: {
503
504
  type: "object",
504
505
  properties: {
505
506
  draftId: { type: "string", description: "Draft id returned by create_draft, list_drafts, or a create_*_draft call." },
507
+ confirmed: { type: "boolean", description: "Pass true to confirm permanent deletion of the draft. Required when PROTONMAIL_CONFIRM_DESTRUCTIVE is enabled." },
506
508
  },
507
509
  required: ["draftId"],
508
510
  },
@@ -788,11 +790,14 @@ const TOOLS = [
788
790
  },
789
791
  {
790
792
  name: "delete_template",
791
- description: "Delete a saved email template.",
793
+ description: "Delete a saved email template. Irreversible; requires confirmed:true when PROTONMAIL_CONFIRM_DESTRUCTIVE is enabled.",
792
794
  annotations: { destructiveHint: true },
793
795
  inputSchema: {
794
796
  type: "object",
795
- properties: { id: { type: "string", description: "The template id." } },
797
+ properties: {
798
+ id: { type: "string", description: "The template id." },
799
+ confirmed: { type: "boolean", description: "Pass true to confirm permanent deletion of the template. Required when PROTONMAIL_CONFIRM_DESTRUCTIVE is enabled." },
800
+ },
796
801
  required: ["id"],
797
802
  },
798
803
  },
@@ -2640,6 +2645,20 @@ function parseIntegerEnv(name, defaultValue, min = 1, max = 10_000) {
2640
2645
  }
2641
2646
  return Math.min(max, Math.max(min, parsed));
2642
2647
  }
2648
+ // Ports need a hard failure, not the silent clamping parseIntegerEnv applies
2649
+ // for its other (non-port) callers — a typo like PROTONMAIL_IMAP_PORT=99999
2650
+ // should abort startup with a clear message instead of quietly becoming 65535
2651
+ // and failing later with a confusing connection error.
2652
+ function validatePortEnv(name) {
2653
+ const raw = process.env[name]?.trim();
2654
+ if (!raw) {
2655
+ return;
2656
+ }
2657
+ const parsed = Number.parseInt(raw, 10);
2658
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65_535) {
2659
+ throw new Error(`${name} must be an integer between 1 and 65535, got: ${raw}`);
2660
+ }
2661
+ }
2643
2662
  function parseAllowedActionsEnv(name) {
2644
2663
  const configured = parseListValues(process.env[name]);
2645
2664
  if (configured.length === 0) {
@@ -2665,11 +2684,13 @@ export function buildConfigFromEnv() {
2665
2684
  if (!username || !password) {
2666
2685
  throw new Error("Missing required environment variables or secret sources: PROTONMAIL_USERNAME and PROTONMAIL_PASSWORD.");
2667
2686
  }
2687
+ validatePortEnv("PROTONMAIL_SMTP_PORT");
2668
2688
  const smtpPort = parseIntegerEnv("PROTONMAIL_SMTP_PORT", 1025, 1, 65_535);
2669
2689
  // Bridge's local SMTP port requires implicit TLS from the first byte (no
2670
2690
  // plaintext greeting, no STARTTLS) — confirmed against a live Bridge
2671
2691
  // instance. Overridable for non-Bridge SMTP setups.
2672
2692
  const smtpSecure = parseBooleanEnv("PROTONMAIL_SMTP_SECURE", true);
2693
+ validatePortEnv("PROTONMAIL_IMAP_PORT");
2673
2694
  const imapPort = parseIntegerEnv("PROTONMAIL_IMAP_PORT", 1143, 1, 65_535);
2674
2695
  const debug = parseBooleanEnv("DEBUG", false);
2675
2696
  const readOnly = parseBooleanEnv("PROTONMAIL_READ_ONLY", false);
@@ -2759,7 +2780,18 @@ export function createServer(config, options = {}) {
2759
2780
  const templateService = new TemplateService(config, logger);
2760
2781
  if (options.startBackgroundSync) {
2761
2782
  backgroundSyncService.start();
2762
- void deliveryQueueService.start();
2783
+ // start()'s first real work is awaiting recoverInterruptedSends(), which
2784
+ // can throw a lock-acquisition-timeout error (see file-lock.ts). Bare
2785
+ // `void` here would turn that into an unhandled rejection, and the
2786
+ // process-wide unhandledRejection handler below calls process.exit(1) —
2787
+ // crashing the whole server before it serves a single request over
2788
+ // nothing worse than startup lock contention. Log and degrade instead:
2789
+ // the delivery queue just won't be running yet.
2790
+ deliveryQueueService.start().catch((error) => {
2791
+ logger.error("deliveryQueueService.start() failed", "MCPServer", error);
2792
+ });
2793
+ // snoozeService.start() is synchronous (returns void, not a Promise) —
2794
+ // nothing to catch here.
2763
2795
  snoozeService.start();
2764
2796
  }
2765
2797
  const server = new Server({
@@ -3718,6 +3750,17 @@ export function createServer(config, options = {}) {
3718
3750
  if (draft.status === "sent") {
3719
3751
  throw new McpError(ErrorCode.InvalidParams, `Draft ${draft.id} was already sent — scheduling it again would deliver it twice. Use create_draft to compose a new message instead.`);
3720
3752
  }
3753
+ // Mirrors the check above but for the same-ordering case: a draft
3754
+ // has no "scheduled" status (only "draft"/"sent" — see
3755
+ // DraftStoreService), so calling schedule_draft twice on the same
3756
+ // still-"draft" draft (e.g. to change sendAt, or a client retry
3757
+ // after an ambiguous response) sailed past the status check above
3758
+ // and enqueued a second, independent delivery-queue record. Both
3759
+ // fire independently later, delivering the same email twice.
3760
+ const alreadyPendingScheduled = (await deliveryQueueService.list()).find((record) => record.sourceDraftId === draft.id && record.status === "pending");
3761
+ if (alreadyPendingScheduled) {
3762
+ throw new McpError(ErrorCode.InvalidParams, `This draft already has a pending scheduled send (id ${alreadyPendingScheduled.id}, sendAt ${alreadyPendingScheduled.sendAt}). Scheduling it again would deliver it twice. Cancel the existing one with cancel_send first if you want a different sendAt.`);
3763
+ }
3721
3764
  const sendAt = requireString(args, "sendAt");
3722
3765
  const sendAtTime = new Date(sendAt).getTime();
3723
3766
  if (Number.isNaN(sendAtTime)) {
@@ -3766,6 +3809,7 @@ export function createServer(config, options = {}) {
3766
3809
  }
3767
3810
  case "delete_draft": {
3768
3811
  const draft = await draftStore.getDraft(requireString(args, "draftId"));
3812
+ ensureDestructiveConfirmed(config.runtime, normalizeBoolean(args?.confirmed, false), "Permanently delete draft: " + draft.id);
3769
3813
  const deleted = await withAudit(auditService, name, args, async () => {
3770
3814
  const remoteCleanup = resolveRemoteDraftSync(config.runtime, true).enabled
3771
3815
  ? await clearRemoteDraft(draftStore, imapService, draft)
@@ -4313,7 +4357,9 @@ export function createServer(config, options = {}) {
4313
4357
  return createTextResult(result);
4314
4358
  }
4315
4359
  case "delete_template": {
4316
- const result = await withAudit(auditService, name, args, async () => templateService.delete(requireString(args, "id")));
4360
+ const templateId = requireString(args, "id");
4361
+ ensureDestructiveConfirmed(config.runtime, normalizeBoolean(args?.confirmed, false), "Permanently delete template: " + templateId);
4362
+ const result = await withAudit(auditService, name, args, async () => templateService.delete(templateId));
4317
4363
  return createTextResult(result);
4318
4364
  }
4319
4365
  case "render_template": {
@@ -4450,10 +4496,19 @@ export function createServer(config, options = {}) {
4450
4496
  suggestion: "PROTONMAIL_PASSWORD must be the Proton Bridge password (Bridge app -> account -> Mailbox details), not your Proton account password. Confirm you're signed in inside the Bridge app.",
4451
4497
  };
4452
4498
  }
4499
+ // Checked before the generic connection-error case: an EPROTO/
4500
+ // "wrong version number" is unambiguous evidence of a TLS-mode
4501
+ // mismatch, distinct from Bridge simply being unreachable.
4502
+ if (isLikelyTlsMismatchError(reason)) {
4503
+ return {
4504
+ cause: "tls_mismatch",
4505
+ suggestion: "TLS handshake failed against a plaintext/STARTTLS port. PROTONMAIL_IMAP_SECURE/PROTONMAIL_SMTP_SECURE likely doesn't match the security mode of the port in Bridge's Mailbox details (implicit TLS vs STARTTLS) — check both against Bridge's IMAP/SMTP port settings.",
4506
+ };
4507
+ }
4453
4508
  if (isLikelyConnectionError(reason)) {
4454
4509
  return {
4455
4510
  cause: "bridge_unreachable",
4456
- suggestion: "Proton Bridge isn't reachable on the configured host/port. Make sure the Bridge app is running, and that PROTONMAIL_IMAP_HOST/PORT and PROTONMAIL_SMTP_HOST/PORT match Bridge's Mailbox details.",
4511
+ suggestion: "Proton Bridge isn't reachable on the configured host/port, OR (if this is ECONNRESET) a TLS-mode mismatch in the other direction — plaintext attempted against an implicit-TLS port. Make sure the Bridge app is running, that PROTONMAIL_IMAP_HOST/PORT and PROTONMAIL_SMTP_HOST/PORT match Bridge's Mailbox details, and that PROTONMAIL_IMAP_SECURE/PROTONMAIL_SMTP_SECURE match the security mode shown there.",
4457
4512
  };
4458
4513
  }
4459
4514
  return undefined;
@@ -4925,7 +4980,13 @@ export function createServer(config, options = {}) {
4925
4980
  const { writeFile: wf, mkdir: mkd } = await import("node:fs/promises");
4926
4981
  const absDir = pathResolve(downloadDir);
4927
4982
  const absTarget = pathJoin(absDir, saveTo);
4928
- if (!absTarget.startsWith(absDir + "/") && absTarget !== absDir) {
4983
+ // Hardcoded "/" here never matched on win32 (path.resolve/join
4984
+ // produce backslash-separated paths there), so every subdirectory
4985
+ // save failed with a false "escapes the allowed directory" —
4986
+ // fails safe/closed, not a security bypass, but breaks a normal
4987
+ // save on the Windows deployment this codebase explicitly
4988
+ // supports (see install-claude-desktop.ts's win32 branches).
4989
+ if (!absTarget.startsWith(absDir + sep) && absTarget !== absDir) {
4929
4990
  throw new McpError(ErrorCode.InvalidParams, "saveTo path escapes the allowed directory.");
4930
4991
  }
4931
4992
  await mkd(pathResolve(absTarget, ".."), { recursive: true });
@@ -4944,7 +5005,7 @@ export function createServer(config, options = {}) {
4944
5005
  throw error;
4945
5006
  }
4946
5007
  }
4947
- if (!realTarget.startsWith(absDir + "/") && realTarget !== absDir) {
5008
+ if (!realTarget.startsWith(absDir + sep) && realTarget !== absDir) {
4948
5009
  throw new McpError(ErrorCode.InvalidParams, "saveTo path escapes the allowed directory.");
4949
5010
  }
4950
5011
  const buf = Buffer.from(result.base64, "base64");
@@ -4968,7 +5029,9 @@ export function createServer(config, options = {}) {
4968
5029
  throw new McpError(ErrorCode.InvalidParams, "PROTONMAIL_ALLOW_FILE_DOWNLOAD_DIR must be set to use saveTo.");
4969
5030
  const absDir = pathResolve(allowDir);
4970
5031
  const absTarget = pathResolve(join(absDir, saveTo));
4971
- if (!absTarget.startsWith(absDir + "/") && absTarget !== absDir) {
5032
+ // See the identical fix/comment in get_attachment_content above
5033
+ // hardcoded "/" never matched a real subdirectory path on win32.
5034
+ if (!absTarget.startsWith(absDir + pathSep) && absTarget !== absDir) {
4972
5035
  throw new McpError(ErrorCode.InvalidParams, "saveTo path escapes the allowed directory.");
4973
5036
  }
4974
5037
  resolvedPath = absTarget;
@@ -5094,6 +5157,20 @@ export function createServer(config, options = {}) {
5094
5157
  }
5095
5158
  export async function main() {
5096
5159
  const config = buildConfigFromEnv();
5160
+ // Create the data directory explicitly with a restrictive mode as its very
5161
+ // first creation, before any service (draft store, template store, audit
5162
+ // log, local index, ...) does its own mkdir(dirname(...)) as a side effect
5163
+ // and inherits whatever the process umask happens to be. mkdir with
5164
+ // recursive:true is a safe no-op on an already-existing directory — it
5165
+ // will not change that directory's existing mode.
5166
+ await mkdir(config.dataDir, { recursive: true, mode: 0o700 });
5167
+ // mkdir's `mode` only applies when the directory doesn't already exist —
5168
+ // on every real upgrade of an existing install (the common case, not a
5169
+ // fresh one), this directory already existed at 0755 from before this fix,
5170
+ // and mkdir on an existing directory is a silent no-op for its mode.
5171
+ // Explicitly chmod it too so the restriction actually takes effect on
5172
+ // upgrade, not only on a brand-new dataDir.
5173
+ await chmod(config.dataDir, 0o700).catch(() => { });
5097
5174
  const { server, smtpService, imapService, backgroundSyncService, deliveryQueueService, snoozeService } = createServer(config, {
5098
5175
  startBackgroundSync: true,
5099
5176
  });