proton-mail-bridge-client 1.19.3 → 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 (39) hide show
  1. package/README.md +3 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +77 -11
  4. package/dist/index.js.map +1 -1
  5. package/dist/services/audit-service.d.ts +1 -0
  6. package/dist/services/audit-service.d.ts.map +1 -1
  7. package/dist/services/audit-service.js +17 -3
  8. package/dist/services/audit-service.js.map +1 -1
  9. package/dist/services/delivery-queue-service.d.ts +1 -0
  10. package/dist/services/delivery-queue-service.d.ts.map +1 -1
  11. package/dist/services/delivery-queue-service.js +47 -5
  12. package/dist/services/delivery-queue-service.js.map +1 -1
  13. package/dist/services/draft-store-service.d.ts +1 -0
  14. package/dist/services/draft-store-service.d.ts.map +1 -1
  15. package/dist/services/draft-store-service.js +27 -2
  16. package/dist/services/draft-store-service.js.map +1 -1
  17. package/dist/services/local-index-service.d.ts +1 -0
  18. package/dist/services/local-index-service.d.ts.map +1 -1
  19. package/dist/services/local-index-service.js +60 -8
  20. package/dist/services/local-index-service.js.map +1 -1
  21. package/dist/services/simple-imap-service.d.ts +5 -0
  22. package/dist/services/simple-imap-service.d.ts.map +1 -1
  23. package/dist/services/simple-imap-service.js +145 -37
  24. package/dist/services/simple-imap-service.js.map +1 -1
  25. package/dist/services/snooze-service.d.ts +1 -0
  26. package/dist/services/snooze-service.d.ts.map +1 -1
  27. package/dist/services/snooze-service.js +61 -4
  28. package/dist/services/snooze-service.js.map +1 -1
  29. package/dist/services/template-service.js +2 -2
  30. package/dist/services/template-service.js.map +1 -1
  31. package/dist/types/index.d.ts +1 -0
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/dist/utils/file-lock.d.ts.map +1 -1
  34. package/dist/utils/file-lock.js +47 -1
  35. package/dist/utils/file-lock.js.map +1 -1
  36. package/dist/utils/runtime-policy.d.ts.map +1 -1
  37. package/dist/utils/runtime-policy.js +4 -2
  38. package/dist/utils/runtime-policy.js.map +1 -1
  39. 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAg0FP;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({
@@ -3777,6 +3809,7 @@ export function createServer(config, options = {}) {
3777
3809
  }
3778
3810
  case "delete_draft": {
3779
3811
  const draft = await draftStore.getDraft(requireString(args, "draftId"));
3812
+ ensureDestructiveConfirmed(config.runtime, normalizeBoolean(args?.confirmed, false), "Permanently delete draft: " + draft.id);
3780
3813
  const deleted = await withAudit(auditService, name, args, async () => {
3781
3814
  const remoteCleanup = resolveRemoteDraftSync(config.runtime, true).enabled
3782
3815
  ? await clearRemoteDraft(draftStore, imapService, draft)
@@ -4324,7 +4357,9 @@ export function createServer(config, options = {}) {
4324
4357
  return createTextResult(result);
4325
4358
  }
4326
4359
  case "delete_template": {
4327
- 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));
4328
4363
  return createTextResult(result);
4329
4364
  }
4330
4365
  case "render_template": {
@@ -4461,10 +4496,19 @@ export function createServer(config, options = {}) {
4461
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.",
4462
4497
  };
4463
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
+ }
4464
4508
  if (isLikelyConnectionError(reason)) {
4465
4509
  return {
4466
4510
  cause: "bridge_unreachable",
4467
- 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.",
4468
4512
  };
4469
4513
  }
4470
4514
  return undefined;
@@ -4936,7 +4980,13 @@ export function createServer(config, options = {}) {
4936
4980
  const { writeFile: wf, mkdir: mkd } = await import("node:fs/promises");
4937
4981
  const absDir = pathResolve(downloadDir);
4938
4982
  const absTarget = pathJoin(absDir, saveTo);
4939
- 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) {
4940
4990
  throw new McpError(ErrorCode.InvalidParams, "saveTo path escapes the allowed directory.");
4941
4991
  }
4942
4992
  await mkd(pathResolve(absTarget, ".."), { recursive: true });
@@ -4955,7 +5005,7 @@ export function createServer(config, options = {}) {
4955
5005
  throw error;
4956
5006
  }
4957
5007
  }
4958
- if (!realTarget.startsWith(absDir + "/") && realTarget !== absDir) {
5008
+ if (!realTarget.startsWith(absDir + sep) && realTarget !== absDir) {
4959
5009
  throw new McpError(ErrorCode.InvalidParams, "saveTo path escapes the allowed directory.");
4960
5010
  }
4961
5011
  const buf = Buffer.from(result.base64, "base64");
@@ -4979,7 +5029,9 @@ export function createServer(config, options = {}) {
4979
5029
  throw new McpError(ErrorCode.InvalidParams, "PROTONMAIL_ALLOW_FILE_DOWNLOAD_DIR must be set to use saveTo.");
4980
5030
  const absDir = pathResolve(allowDir);
4981
5031
  const absTarget = pathResolve(join(absDir, saveTo));
4982
- 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) {
4983
5035
  throw new McpError(ErrorCode.InvalidParams, "saveTo path escapes the allowed directory.");
4984
5036
  }
4985
5037
  resolvedPath = absTarget;
@@ -5105,6 +5157,20 @@ export function createServer(config, options = {}) {
5105
5157
  }
5106
5158
  export async function main() {
5107
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(() => { });
5108
5174
  const { server, smtpService, imapService, backgroundSyncService, deliveryQueueService, snoozeService } = createServer(config, {
5109
5175
  startBackgroundSync: true,
5110
5176
  });