proton-mail-bridge-client 2.1.19 → 2.1.21

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.
@@ -1 +1 @@
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,EAAwB,MAAM,mCAAmC,CAAC;AAC5F,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAA2H,iBAAiB,EAA+B,MAAM,mCAAmC,CAAC;AAC5N,OAAO,EAAmC,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE7D,OAAO,KAAK,EAMV,mBAAmB,EACnB,cAAc,EACd,WAAW,EAIX,WAAW,EAIX,gBAAgB,EAChB,cAAc,EAGf,MAAM,kBAAkB,CAAC;AAoB1B,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAwrDnF,eAAO,MAAM,eAAe,aA2C1B,CAAC;AAqCH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,GAAG,CAAC,CAEtE;AAmED,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;AAiCD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKtG;AA+CD,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;AAuLD,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAW5E;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAe/E;AAWD,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;AA6ED,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CA4CtE;AAcD,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYlE;AAsBD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAQ/F;AAED,wBAAgB,4BAA4B,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,CAAC,KAUhF;AAQD,wBAAgB,4BAA4B,CAAC,CAAC,SAAS;IAAE,OAAO,EAAE,cAAc,CAAA;CAAE,EAAE,MAAM,EAAE,CAAC,KA8B5F;AA8WD,wBAAgB,4BAA4B,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAKjI;AA8PD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEzE;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAOzE;AAED,UAAU,mBAAmB;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AASD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,EACd,kBAAkB,CAAC,EAAE,MAAM,GAC1B,mBAAmB,EAAE,CA8CvB;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,mBAAmB,EAC3B,gBAAgB,EAAE,mBAAmB,EAAE,GACtC,mBAAmB,CAmBrB;AAWD,wBAAgB,sBAAsB,CACpC,cAAc,EAAE,cAAc,EAC9B,QAAQ,EAAE,MAAM,EAAE,GACjB,GAAG,CAAC,MAAM,GAAG,SAAS,EAAE;IAAE,MAAM,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAYvE;AAOD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,mBAAmB,CAQ3G;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,mBAAmB,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,mBAAmB,EAAE,CAK/G;AAID,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,EAAE,GAAG,mBAAmB,CASpF;AAsQD,wBAAgB,kBAAkB,IAAI,gBAAgB,CAkGrD;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE;IACP,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA82IP;AAED,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAmE1C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -154,6 +154,7 @@ const TOOLS = [
154
154
  type: "object",
155
155
  properties: {
156
156
  emailId: { type: "string", description: "Original email id." },
157
+ undoWindowSeconds: { type: "number", description: "Override PROTONMAIL_SEND_DELAY_SECONDS for this one send: queue it for this many seconds (cancelable via cancel_send) instead of the server's configured default. 0 sends immediately even if the server has a default window configured. Same caveat as the server default: only fires while this server process stays running." },
157
158
  body: { type: "string", description: "Reply body to prepend (plain text). Required unless markdownBody is provided." },
158
159
  markdownBody: { type: "string", description: "Reply body in Markdown. Rendered to HTML with body as plain-text fallback; takes precedence over body+isHtml." },
159
160
  replyAll: { type: "boolean", description: "Reply to all original recipients.", default: false },
@@ -194,6 +195,7 @@ const TOOLS = [
194
195
  type: "object",
195
196
  properties: {
196
197
  emailId: { type: "string", description: "Original email id." },
198
+ undoWindowSeconds: { type: "number", description: "Override PROTONMAIL_SEND_DELAY_SECONDS for this one send: queue it for this many seconds (cancelable via cancel_send) instead of the server's configured default. 0 sends immediately even if the server has a default window configured. Same caveat as the server default: only fires while this server process stays running." },
197
199
  body: { type: "string", description: "Reply body to prepend (plain text). Required unless markdownBody is provided." },
198
200
  markdownBody: { type: "string", description: "Reply body in Markdown. Rendered to HTML with body as plain-text fallback; takes precedence over body+isHtml." },
199
201
  isHtml: { type: "boolean", description: "Send body as HTML (ignored when markdownBody is provided).", default: false },
@@ -233,6 +235,7 @@ const TOOLS = [
233
235
  type: "object",
234
236
  properties: {
235
237
  emailId: { type: "string", description: "Original email id." },
238
+ undoWindowSeconds: { type: "number", description: "Override PROTONMAIL_SEND_DELAY_SECONDS for this one send: queue it for this many seconds (cancelable via cancel_send) instead of the server's configured default. 0 sends immediately even if the server has a default window configured. Same caveat as the server default: only fires while this server process stays running." },
236
239
  to: { type: "string", description: "Forward recipient list, comma-separated." },
237
240
  body: { type: "string", description: "Optional message before the forwarded content (plain text)." },
238
241
  markdownBody: { type: "string", description: "Optional introductory note in Markdown. Rendered to HTML with body as plain-text fallback; takes precedence over body+isHtml." },
@@ -2530,7 +2533,61 @@ function parseResourceUri(uri) {
2530
2533
  }
2531
2534
  throw new Error(`Unsupported resource URI: ${uri}`);
2532
2535
  }
2536
+ // Writes attachment bytes to <downloadDir>/<saveTo>, refusing any path that escapes the
2537
+ // download dir (lexically or through a symlink). The containment check canonicalizes
2538
+ // BOTH sides — comparing a canonical target with a merely-resolved allowed dir rejected
2539
+ // every save when the allowed dir is or sits under a symlink (macOS /var/folders is
2540
+ // really /private/var/folders). Attachments are private mail content, so directories
2541
+ // created here are owner-only (0700) and the file is 0600, existing files included.
2542
+ export async function writeAttachmentToDownloadDir(downloadDir, saveTo, data) {
2543
+ const { resolve: pathResolve, join: pathJoin, dirname, basename, sep } = await import("node:path");
2544
+ const { realpathSync } = await import("node:fs");
2545
+ const { writeFile: wf, mkdir: mkd, chmod: chm } = await import("node:fs/promises");
2546
+ const absDir = pathResolve(downloadDir);
2547
+ const absTarget = pathJoin(absDir, saveTo);
2548
+ // A hardcoded "/" never matched on win32 (path.resolve/join produce
2549
+ // backslash-separated paths there), hence `sep`.
2550
+ if (!absTarget.startsWith(absDir + sep) && absTarget !== absDir) {
2551
+ throw new McpError(ErrorCode.InvalidParams, "saveTo path escapes the allowed directory.");
2552
+ }
2553
+ await mkd(pathResolve(absTarget, ".."), { recursive: true, mode: 0o700 });
2554
+ const realDir = realpathSync(absDir);
2555
+ let realTarget;
2556
+ try {
2557
+ realTarget = realpathSync(absTarget);
2558
+ }
2559
+ catch (error) {
2560
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
2561
+ realTarget = realpathSync(dirname(absTarget)) + sep + basename(absTarget);
2562
+ }
2563
+ else {
2564
+ throw error;
2565
+ }
2566
+ }
2567
+ if (!realTarget.startsWith(realDir + sep) && realTarget !== realDir) {
2568
+ throw new McpError(ErrorCode.InvalidParams, "saveTo path escapes the allowed directory.");
2569
+ }
2570
+ await wf(absTarget, data, { mode: 0o600 });
2571
+ // writeFile's mode only applies to a newly created file; tighten an existing one too.
2572
+ await chm(absTarget, 0o600);
2573
+ return absTarget;
2574
+ }
2575
+ // Syncs run one at a time per draft, and re-read the draft once they hold the lock: a
2576
+ // sync that queued behind another sees the remoteDraft the first one recorded (so it
2577
+ // updates that copy instead of APPENDing a second) and uploads the latest content.
2533
2578
  async function syncDraftToRemote(draftStore, smtpService, imapService, draft) {
2579
+ try {
2580
+ return await draftStore.withDraftSyncLock(draft.id, async () => syncDraftToRemoteLocked(draftStore, smtpService, imapService, await draftStore.getDraft(draft.id).catch(() => draft)));
2581
+ }
2582
+ catch (error) {
2583
+ // Only reachable when the sync lock itself could not be taken — the upload's own
2584
+ // failures are handled (and recorded) inside syncDraftToRemoteLocked.
2585
+ const message = error instanceof Error ? error.message : String(error);
2586
+ logger.warn("Draft remote sync could not start", "MCPServer", { draftId: draft.id, error });
2587
+ return { draft: await draftStore.markRemoteSyncError(draft.id, message), remoteSync: { ok: false, message } };
2588
+ }
2589
+ }
2590
+ async function syncDraftToRemoteLocked(draftStore, smtpService, imapService, draft) {
2534
2591
  try {
2535
2592
  const raw = await smtpService.buildRawMessage({
2536
2593
  to: draft.to,
@@ -3387,6 +3444,33 @@ export function createServer(config, options = {}) {
3387
3444
  function responseSlug(bundle) {
3388
3445
  return bundle.account.slug === accountManager.primary().account.slug ? undefined : bundle.account.slug;
3389
3446
  }
3447
+ // Undo-send window for one send: the server default (PROTONMAIL_SEND_DELAY_SECONDS),
3448
+ // overridable per call with undoWindowSeconds (0 = send immediately). Shared by
3449
+ // send_email, reply_to_email, reply_all_email and forward_email.
3450
+ function resolveUndoWindowSeconds(args) {
3451
+ if (args.undoWindowSeconds === undefined) {
3452
+ return config.runtime.sendDelaySeconds ?? 0;
3453
+ }
3454
+ const requested = Number(args.undoWindowSeconds);
3455
+ if (!Number.isInteger(requested) || requested < 0 || requested > 300) {
3456
+ throw new McpError(ErrorCode.InvalidParams, "undoWindowSeconds must be an integer between 0 and 300.");
3457
+ }
3458
+ return requested;
3459
+ }
3460
+ // Queues a send on the SENDING account's own delivery queue (so it fires through that
3461
+ // account's SMTP) and returns the tool result. The id carries that account's slug so
3462
+ // cancel_send/list_scheduled_sends can find it again.
3463
+ async function queueUndoSend(bundle, payload, undoWindowSeconds, toolName, toolArgs, extra = {}) {
3464
+ const sendAt = new Date(Date.now() + undoWindowSeconds * 1000).toISOString();
3465
+ const queued = await withAudit(auditService, toolName, toolArgs, async () => bundle.deliveryQueueService.enqueue(payload, sendAt, "undo_send"));
3466
+ return createTextResult({
3467
+ queued: true,
3468
+ id: withAccountPrefix(responseSlug(bundle), queued.id),
3469
+ sendAt: queued.sendAt,
3470
+ ...extra,
3471
+ note: `Not sent yet — will send in ~${undoWindowSeconds}s unless canceled with cancel_send. This server must stay running for the send to fire; if it's restarted before sendAt, the send fires on next startup instead.`,
3472
+ });
3473
+ }
3390
3474
  // Alias for responseSlug — tagAccountIds (added alongside the digest/follow-up
3391
3475
  // multi-account fan-out) was written against this name; kept as a thin alias rather
3392
3476
  // than rewriting every call site to the other name.
@@ -3652,14 +3736,7 @@ export function createServer(config, options = {}) {
3652
3736
  // immediately, cancelable via cancel_send until the window elapses.
3653
3737
  // undoWindowSeconds overrides the server default for this one send —
3654
3738
  // including forcing 0 (send immediately) when a default is configured.
3655
- let undoWindowSeconds = config.runtime.sendDelaySeconds;
3656
- if (args.undoWindowSeconds !== undefined) {
3657
- const requested = Number(args.undoWindowSeconds);
3658
- if (!Number.isInteger(requested) || requested < 0 || requested > 300) {
3659
- throw new McpError(ErrorCode.InvalidParams, "undoWindowSeconds must be an integer between 0 and 300.");
3660
- }
3661
- undoWindowSeconds = requested;
3662
- }
3739
+ const undoWindowSeconds = resolveUndoWindowSeconds(args);
3663
3740
  // A `from` matching a configured additional account routes through that
3664
3741
  // account's own SMTP connection — required under Bridge's Split Addresses
3665
3742
  // feature, where each address is its own separate IMAP/SMTP login, not just
@@ -3874,7 +3951,7 @@ export function createServer(config, options = {}) {
3874
3951
  if (dryRunReply) {
3875
3952
  return createTextResult({ dryRun: true, wouldSendTo: { to, cc, bcc: extraBcc }, subject: prefixedSubject(detail.subject, "Re:"), note: "No email was sent." }, false, [emailSource({ ...detail, id: prefixedIdFor(readBundleReply, detail.id) })]);
3876
3953
  }
3877
- const result = await withAudit(auditService, name, args, async () => sendBundleForReply.smtpService.sendEmail({
3954
+ const replyPayload = {
3878
3955
  to,
3879
3956
  cc,
3880
3957
  bcc: extraBcc,
@@ -3890,7 +3967,12 @@ export function createServer(config, options = {}) {
3890
3967
  attachments,
3891
3968
  // Already applied above, before quote-wrapping.
3892
3969
  appendSignature: false,
3893
- }));
3970
+ };
3971
+ const undoWindowreply = resolveUndoWindowSeconds(args);
3972
+ if (undoWindowreply > 0) {
3973
+ return queueUndoSend(sendBundleForReply, replyPayload, undoWindowreply, name, args, { repliedTo: prefixedIdFor(readBundleReply, detail.id), to, cc });
3974
+ }
3975
+ const result = await withAudit(auditService, name, args, async () => sendBundleForReply.smtpService.sendEmail(replyPayload));
3894
3976
  let sentCopyTokenReply = "[sent-copy:unverified]";
3895
3977
  try {
3896
3978
  const verifyMsgId = result.messageId;
@@ -3963,7 +4045,7 @@ export function createServer(config, options = {}) {
3963
4045
  const replyHtmlBodyRa = includeQuoteRa && signedReplyRa.htmlBody !== undefined
3964
4046
  ? buildReplyHtml(detailRa, signedReplyRa.htmlBody)
3965
4047
  : signedReplyRa.htmlBody;
3966
- const resultRa = await withAudit(auditService, name, args, async () => sendBundleForRa.smtpService.sendEmail({
4048
+ const replyAllPayload = {
3967
4049
  to: toRa,
3968
4050
  cc: ccRa,
3969
4051
  bcc: extraBccRa,
@@ -3979,7 +4061,12 @@ export function createServer(config, options = {}) {
3979
4061
  attachments: attachmentsRa,
3980
4062
  // Already applied above, before quote-wrapping.
3981
4063
  appendSignature: false,
3982
- }));
4064
+ };
4065
+ const undoWindowreplyAll = resolveUndoWindowSeconds(args);
4066
+ if (undoWindowreplyAll > 0) {
4067
+ return queueUndoSend(sendBundleForRa, replyAllPayload, undoWindowreplyAll, name, args, { repliedTo: prefixedIdFor(readBundleRa, detailRa.id), to: toRa, cc: ccRa });
4068
+ }
4069
+ const resultRa = await withAudit(auditService, name, args, async () => sendBundleForRa.smtpService.sendEmail(replyAllPayload));
3983
4070
  let sentCopyTokenRa = "[sent-copy:unverified]";
3984
4071
  try {
3985
4072
  const verifyMsgId = resultRa.messageId;
@@ -4076,7 +4163,7 @@ export function createServer(config, options = {}) {
4076
4163
  const forwardHtmlBody = signedFwd.htmlBody !== undefined
4077
4164
  ? buildForwardHtml(detail, signedFwd.htmlBody)
4078
4165
  : signedFwd.htmlBody;
4079
- const result = await withAudit(auditService, name, args, async () => sendBundleForFwd.smtpService.sendEmail({
4166
+ const forwardPayload = {
4080
4167
  to,
4081
4168
  cc,
4082
4169
  bcc,
@@ -4090,7 +4177,12 @@ export function createServer(config, options = {}) {
4090
4177
  attachments: fwdAttachments,
4091
4178
  // Already applied above, before quote-wrapping.
4092
4179
  appendSignature: false,
4093
- }));
4180
+ };
4181
+ const undoWindowforward = resolveUndoWindowSeconds(args);
4182
+ if (undoWindowforward > 0) {
4183
+ return queueUndoSend(sendBundleForFwd, forwardPayload, undoWindowforward, name, args, { forwardedMessage: prefixedIdFor(readBundleFwd, detail.id), to, cc });
4184
+ }
4185
+ const result = await withAudit(auditService, name, args, async () => sendBundleForFwd.smtpService.sendEmail(forwardPayload));
4094
4186
  let sentCopyTokenFwd = "[sent-copy:unverified]";
4095
4187
  try {
4096
4188
  const verifyMsgId = result.messageId;
@@ -4801,7 +4893,10 @@ export function createServer(config, options = {}) {
4801
4893
  // Draft content is already finalized and reviewed — see the
4802
4894
  // identical note on send_draft.
4803
4895
  appendSignature: false,
4804
- }, new Date(sendAtTime).toISOString(), "scheduled_send", draft.id));
4896
+ }, new Date(sendAtTime).toISOString(), "scheduled_send", draft.id,
4897
+ // The draft stays in its own account's store even when `from` routes the
4898
+ // send through another account's queue/SMTP.
4899
+ scheduleDraftBundle.account.slug));
4805
4900
  return createTextResult({
4806
4901
  queued: true,
4807
4902
  id: withAccountPrefix(responseSlug(sendBundleForScheduleDraft), queued.id),
@@ -6887,42 +6982,9 @@ export function createServer(config, options = {}) {
6887
6982
  if (!downloadDir) {
6888
6983
  throw new McpError(ErrorCode.InvalidParams, "PROTONMAIL_ALLOW_FILE_DOWNLOAD_DIR env var is not set.");
6889
6984
  }
6890
- const { resolve: pathResolve, join: pathJoin, dirname, basename, sep } = await import("node:path");
6891
- const { realpathSync } = await import("node:fs");
6892
- const { writeFile: wf, mkdir: mkd } = await import("node:fs/promises");
6893
- const absDir = pathResolve(downloadDir);
6894
- const absTarget = pathJoin(absDir, saveTo);
6895
- // Hardcoded "/" here never matched on win32 (path.resolve/join
6896
- // produce backslash-separated paths there), so every subdirectory
6897
- // save failed with a false "escapes the allowed directory" —
6898
- // fails safe/closed, not a security bypass, but breaks a normal
6899
- // save on the Windows deployment this codebase explicitly
6900
- // supports (see install-claude-desktop.ts's win32 branches).
6901
- if (!absTarget.startsWith(absDir + sep) && absTarget !== absDir) {
6902
- throw new McpError(ErrorCode.InvalidParams, "saveTo path escapes the allowed directory.");
6903
- }
6904
- await mkd(pathResolve(absTarget, ".."), { recursive: true });
6905
- let realTarget;
6906
- try {
6907
- realTarget = realpathSync(absTarget);
6908
- }
6909
- catch (error) {
6910
- if (error &&
6911
- typeof error === "object" &&
6912
- "code" in error &&
6913
- error.code === "ENOENT") {
6914
- realTarget = realpathSync(dirname(absTarget)) + sep + basename(absTarget);
6915
- }
6916
- else {
6917
- throw error;
6918
- }
6919
- }
6920
- if (!realTarget.startsWith(absDir + sep) && realTarget !== absDir) {
6921
- throw new McpError(ErrorCode.InvalidParams, "saveTo path escapes the allowed directory.");
6922
- }
6923
6985
  const buf = Buffer.from(result.base64, "base64");
6924
- await wf(absTarget, buf);
6925
- return createTextResult({ saved: true, path: absTarget, bytes: buf.length, filename: result.attachment?.filename });
6986
+ const savedPath = await writeAttachmentToDownloadDir(downloadDir, saveTo, buf);
6987
+ return createTextResult({ saved: true, path: savedPath, bytes: buf.length, filename: result.attachment?.filename });
6926
6988
  }
6927
6989
  return createTextResult(result, false, [attachmentSource(result.emailId, result.attachment)]);
6928
6990
  }