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.
- package/README.md +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -11
- package/dist/index.js.map +1 -1
- package/dist/services/analytics-service.d.ts.map +1 -1
- package/dist/services/analytics-service.js +20 -5
- package/dist/services/analytics-service.js.map +1 -1
- package/dist/services/audit-service.d.ts +1 -0
- package/dist/services/audit-service.d.ts.map +1 -1
- package/dist/services/audit-service.js +17 -3
- package/dist/services/audit-service.js.map +1 -1
- package/dist/services/delivery-queue-service.d.ts +1 -0
- package/dist/services/delivery-queue-service.d.ts.map +1 -1
- package/dist/services/delivery-queue-service.js +59 -6
- package/dist/services/delivery-queue-service.js.map +1 -1
- package/dist/services/draft-store-service.d.ts +1 -0
- package/dist/services/draft-store-service.d.ts.map +1 -1
- package/dist/services/draft-store-service.js +27 -2
- package/dist/services/draft-store-service.js.map +1 -1
- package/dist/services/local-index-service.d.ts +1 -0
- package/dist/services/local-index-service.d.ts.map +1 -1
- package/dist/services/local-index-service.js +60 -8
- package/dist/services/local-index-service.js.map +1 -1
- package/dist/services/simple-imap-service.d.ts +5 -0
- package/dist/services/simple-imap-service.d.ts.map +1 -1
- package/dist/services/simple-imap-service.js +153 -40
- package/dist/services/simple-imap-service.js.map +1 -1
- package/dist/services/snooze-service.d.ts +1 -0
- package/dist/services/snooze-service.d.ts.map +1 -1
- package/dist/services/snooze-service.js +61 -4
- package/dist/services/snooze-service.js.map +1 -1
- package/dist/services/template-service.js +2 -2
- package/dist/services/template-service.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/file-lock.d.ts.map +1 -1
- package/dist/utils/file-lock.js +47 -1
- package/dist/utils/file-lock.js.map +1 -1
- package/dist/utils/helpers.d.ts +1 -0
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/helpers.js +30 -0
- package/dist/utils/helpers.js.map +1 -1
- package/dist/utils/runtime-policy.d.ts.map +1 -1
- package/dist/utils/runtime-policy.js +4 -2
- package/dist/utils/runtime-policy.js.map +1 -1
- 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
|
|
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
|
|
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.
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
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: {
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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 +
|
|
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
|
-
|
|
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
|
});
|