proton-mail-bridge-client 2.1.3 → 2.1.5

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/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ import { LocalIndexService } from "./services/local-index-service.js";
8
8
  import { SimpleIMAPService } from "./services/simple-imap-service.js";
9
9
  import { SMTPService } from "./services/smtp-service.js";
10
10
  import { SnoozeService } from "./services/snooze-service.js";
11
- import type { BulkOperationResult, DraftRecord, EmailDetail, ProtonMailConfig } from "./types/index.js";
11
+ import type { BulkOperationResult, DraftRecord, EmailDetail, ProtonMailConfig, SendEmailInput } from "./types/index.js";
12
12
  import { AccountManager, type AccountBundle } from "./services/account-manager.js";
13
13
  export declare const CORE_TOOL_NAMES: Set<string>;
14
14
  export declare function withAudit<T>(auditService: AuditService, tool: string, input: Record<string, unknown>, operation: () => Promise<T>): Promise<T>;
@@ -23,6 +23,8 @@ export declare function extractUnsubscribeInfo(detail: EmailDetail): {
23
23
  mailto?: string;
24
24
  url?: string;
25
25
  };
26
+ export declare function buildReplyHtml(detail: EmailDetail, htmlBody: string): string;
27
+ export declare function buildForwardHtml(detail: EmailDetail, htmlBody?: string): string;
26
28
  export declare function getReplyRecipients(detail: EmailDetail, ownerEmail: string, replyAll: boolean): {
27
29
  to: string[];
28
30
  cc: string[];
@@ -63,6 +65,9 @@ export declare function redactDraftAttachmentsForListing(draft: DraftRecord): {
63
65
  remoteDraft?: import("./types/index.js").RemoteDraftRef;
64
66
  lastSendResult?: import("./types/index.js").DraftSendResult;
65
67
  };
68
+ export declare function redactQueueRecordAttachments<T extends {
69
+ payload: SendEmailInput;
70
+ }>(record: T): T;
66
71
  export declare function paginateMergedAccountResults<T>(sortedMerged: T[], offset: number, limit: number): {
67
72
  page: T[];
68
73
  hasMore: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, ListResources
12
12
  import { isMainModule } from "./is-main.js";
13
13
  import { AnalyticsService } from "./services/analytics-service.js";
14
14
  import { BULK_ITEM_TIMEOUT_MS, describeImapError, isLikelyAuthenticationError, isLikelyConnectionError, isLikelyTlsMismatchError, UID_VALIDITY_MISMATCH_ERROR } from "./services/simple-imap-service.js";
15
- import { applySignature } from "./services/smtp-service.js";
15
+ import { applySignature, plainTextToHtml } from "./services/smtp-service.js";
16
16
  import { ensureValidEmails, foldQuotedHistory, isTextLikeMimeType, isValidEmail, lowerCaseAddress, normalizeBoolean, normalizeLimit, normalizeJsonValue, parseEmailId, parseEmails, projectFields, renderMarkdown, slugifyAccountAddress, splitAccountPrefix, stringifyForJson, withAccountPrefix, } from "./utils/helpers.js";
17
17
  import { AccountManager } from "./services/account-manager.js";
18
18
  import { logger } from "./utils/logger.js";
@@ -408,12 +408,13 @@ const TOOLS = [
408
408
  },
409
409
  {
410
410
  name: "get_draft",
411
- description: "Fetch the full content of a single locally saved draft by its draftId. Use to read or verify a draft before sending or updating. Prefer list_drafts to discover draftIds first. Does NOT fetch drafts from the Proton server — use list_remote_drafts for those.",
411
+ description: "Fetch the full content of a single locally saved draft by its draftId. Use to read or verify a draft before sending or updating. Prefer list_drafts to discover draftIds first. Does NOT fetch drafts from the Proton server — use list_remote_drafts for those. Attachments are metadata only (filename/contentType/size, no base64 content) by default — set includeAttachmentContent:true if you actually need the raw bytes back (e.g. to re-attach them elsewhere); a large attachment's base64 content can be hundreds of thousands of tokens.",
412
412
  annotations: { readOnlyHint: true },
413
413
  inputSchema: {
414
414
  type: "object",
415
415
  properties: {
416
416
  draftId: { type: "string", description: "Draft id returned by create_draft, list_drafts, or a create_*_draft call." },
417
+ includeAttachmentContent: { type: "boolean", description: "Return each attachment's full base64 content instead of just its metadata. Off by default — see the tool description.", default: false },
417
418
  },
418
419
  required: ["draftId"],
419
420
  },
@@ -2069,6 +2070,53 @@ function buildForwardText(detail, body) {
2069
2070
  .filter((line, index, array) => line !== "" || (index > 0 && array[index - 1] !== ""))
2070
2071
  .join("\n");
2071
2072
  }
2073
+ // HTML counterparts of buildReplyText/buildForwardText — needed because a
2074
+ // markdown-authored reply/forward (markdownBody) renders its OWN new text to a
2075
+ // real, separate htmlBody, but buildReplyText/buildForwardText only ever wrapped
2076
+ // the PLAIN TEXT `body` with the quoted/forwarded original. That original never
2077
+ // made it into htmlBody at all — an HTML-viewing recipient of a markdown reply
2078
+ // saw only the new text and signature, with the entire original message missing
2079
+ // (confirmed on generated MIME messages), while the text/plain part had it via
2080
+ // buildReplyText. Uses the original message's own detail.html when present
2081
+ // (real markup, not reconstructed); falls back to escaping detail.text/preview
2082
+ // through plainTextToHtml otherwise, same as a plain-text send's own html
2083
+ // alternative (see smtp-service.ts's plainTextToHtml comment).
2084
+ export function buildReplyHtml(detail, htmlBody) {
2085
+ const originalHtml = typeof detail.html === "string" && detail.html
2086
+ ? detail.html
2087
+ : plainTextToHtml(detail.text || detail.preview || "");
2088
+ const fromText = formatAddressList(detail.from);
2089
+ const dateText = detail.date || detail.internalDate || "an unknown date";
2090
+ return [
2091
+ htmlBody,
2092
+ `<p>On ${escapeHtmlForQuote(dateText)}, ${escapeHtmlForQuote(fromText || "the sender")} wrote:</p>`,
2093
+ `<blockquote>${originalHtml}</blockquote>`,
2094
+ ].join("\n");
2095
+ }
2096
+ export function buildForwardHtml(detail, htmlBody) {
2097
+ const originalHtml = typeof detail.html === "string" && detail.html
2098
+ ? detail.html
2099
+ : plainTextToHtml(detail.text || detail.preview || "");
2100
+ return [
2101
+ htmlBody?.trim() || "",
2102
+ "<p>---------- Forwarded message ---------</p>",
2103
+ `<p>From: ${escapeHtmlForQuote(formatAddressList(detail.from))}<br>`,
2104
+ `Date: ${escapeHtmlForQuote(detail.date || detail.internalDate || "")}<br>`,
2105
+ `Subject: ${escapeHtmlForQuote(detail.subject)}<br>`,
2106
+ `To: ${escapeHtmlForQuote(formatAddressList(detail.to))}${detail.cc.length > 0 ? `<br>Cc: ${escapeHtmlForQuote(formatAddressList(detail.cc))}` : ""}</p>`,
2107
+ originalHtml,
2108
+ ]
2109
+ .filter((part) => part !== "")
2110
+ .join("\n");
2111
+ }
2112
+ function escapeHtmlForQuote(value) {
2113
+ return value
2114
+ .replace(/&/g, "&amp;")
2115
+ .replace(/</g, "&lt;")
2116
+ .replace(/>/g, "&gt;")
2117
+ .replace(/"/g, "&quot;")
2118
+ .replace(/'/g, "&#39;");
2119
+ }
2072
2120
  export function getReplyRecipients(detail, ownerEmail, replyAll) {
2073
2121
  const owner = lowerCaseAddress(ownerEmail);
2074
2122
  const primary = addressValues(detail.replyTo).length > 0 ? detail.replyTo : detail.from;
@@ -2229,6 +2277,32 @@ export function redactDraftAttachmentsForListing(draft) {
2229
2277
  })),
2230
2278
  };
2231
2279
  }
2280
+ // Same fix as redactDraftAttachmentsForListing, for list_scheduled_sends — a
2281
+ // queued send's payload carries the same full base64 EmailAttachmentInput
2282
+ // content (found live: ~955k tokens for a single 1 MiB attachment on ONE
2283
+ // record, canceled records included, and list_scheduled_sends has no filter
2284
+ // or cap any more than list_drafts did). No explicit return-type annotation
2285
+ // for the same reason as the function above.
2286
+ export function redactQueueRecordAttachments(record) {
2287
+ const attachments = record.payload.attachments;
2288
+ if (!attachments || attachments.length === 0) {
2289
+ return record;
2290
+ }
2291
+ return {
2292
+ ...record,
2293
+ payload: {
2294
+ ...record.payload,
2295
+ attachments: attachments.map((attachment) => ({
2296
+ filename: attachment.filename,
2297
+ contentType: attachment.contentType,
2298
+ cid: attachment.cid,
2299
+ contentDisposition: attachment.contentDisposition,
2300
+ content: "",
2301
+ size: Buffer.byteLength(attachment.content, "base64"),
2302
+ })),
2303
+ },
2304
+ };
2305
+ }
2232
2306
  function draftSource(draft) {
2233
2307
  return {
2234
2308
  uri: buildDraftResourceUri(draft.id),
@@ -2367,10 +2441,24 @@ async function syncDraftToRemote(draftStore, smtpService, imapService, draft) {
2367
2441
  isHtml: draft.isHtml,
2368
2442
  priority: draft.priority,
2369
2443
  replyTo: draft.replyTo,
2444
+ // Found live (external review): this never passed draft.from, so a draft
2445
+ // saved under an alias (e.g. alias@example.com) previewed in the remote
2446
+ // Drafts mailbox under the primary address instead — the remote preview
2447
+ // and the eventual send_draft/schedule_draft sender didn't match.
2448
+ from: draft.from,
2370
2449
  inReplyTo: draft.inReplyTo,
2371
2450
  references: draft.references,
2372
2451
  messageId: draft.draftMessageId,
2373
2452
  attachments: draft.attachments,
2453
+ // Same reasoning as send_draft's identical appendSignature: false — draft
2454
+ // content is already finalized and reviewed by the time it's synced here.
2455
+ // Found live (external review): this used to omit appendSignature, so the
2456
+ // remote preview auto-added PROTONMAIL_SIGNATURE while send_draft/
2457
+ // schedule_draft never did — the actually-sent message either lacked the
2458
+ // signature the remote preview showed, or, if the caller had also typed a
2459
+ // signature into the draft body themselves, got it appended a second time
2460
+ // on every remote sync.
2461
+ appendSignature: false,
2374
2462
  });
2375
2463
  const remoteDraft = await imapService.upsertRemoteDraft({
2376
2464
  raw,
@@ -3514,9 +3602,14 @@ export function createServer(config, options = {}) {
3514
3602
  // item's id with its owning account's slug — mirrors the read-tool fan-out
3515
3603
  // pattern (get_emails etc.). Single-account guard keeps byte-identical output
3516
3604
  // for the common case.
3605
+ // Each record's payload carries the same full base64 attachment content as
3606
+ // a draft — see redactQueueRecordAttachments' comment. Redacted for every
3607
+ // record regardless of status (including canceled ones, which otherwise
3608
+ // stayed just as bloated once queued).
3517
3609
  if (accountManager.all().length === 1) {
3518
3610
  const all = await withAudit(auditService, name, args, async () => deliveryQueueService.list());
3519
- const filtered = statusFilter ? all.filter((item) => item.status === statusFilter) : all;
3611
+ const filtered = (statusFilter ? all.filter((item) => item.status === statusFilter) : all)
3612
+ .map(redactQueueRecordAttachments);
3520
3613
  return createTextResult(filtered);
3521
3614
  }
3522
3615
  const perAccount = await withAudit(auditService, name, args, async () => Promise.all(accountManager.all().map(async (bundle) => ({
@@ -3524,7 +3617,8 @@ export function createServer(config, options = {}) {
3524
3617
  items: await bundle.deliveryQueueService.list(),
3525
3618
  }))));
3526
3619
  const tagged = perAccount.flatMap(({ bundle, items }) => items.map((item) => ({ ...item, id: withAccountPrefix(responseSlug(bundle), item.id) })));
3527
- const filtered = statusFilter ? tagged.filter((item) => item.status === statusFilter) : tagged;
3620
+ const filtered = (statusFilter ? tagged.filter((item) => item.status === statusFilter) : tagged)
3621
+ .map(redactQueueRecordAttachments);
3528
3622
  return createTextResult(filtered);
3529
3623
  }
3530
3624
  case "get_unsubscribe_info": {
@@ -3634,8 +3728,14 @@ export function createServer(config, options = {}) {
3634
3728
  const includeQuoteReply = normalizeBoolean(args.includeQuote, true);
3635
3729
  // Signature goes right after the user's own reply text, before the
3636
3730
  // quoted original — not at the very end, after the quote.
3637
- const signedReply = applySignature(body, htmlBody, normalizeBoolean(args.appendSignature, true));
3731
+ const signedReply = applySignature(body, htmlBody, normalizeBoolean(args.appendSignature, true), isHtml);
3638
3732
  const replyBody = includeQuoteReply ? buildReplyText(detail, signedReply.body) : signedReply.body;
3733
+ // A separate htmlBody (the markdown-rendered case) needs the SAME quoted
3734
+ // original merged in — see buildReplyHtml's comment for why this was
3735
+ // previously missing entirely from the html part.
3736
+ const replyHtmlBody = includeQuoteReply && signedReply.htmlBody !== undefined
3737
+ ? buildReplyHtml(detail, signedReply.htmlBody)
3738
+ : signedReply.htmlBody;
3639
3739
  if (config.runtime.restrictOutboundToSelf) {
3640
3740
  const selfAddr = config.smtp.username.toLowerCase();
3641
3741
  const allR = [...to, ...cc, ...extraBcc];
@@ -3654,7 +3754,7 @@ export function createServer(config, options = {}) {
3654
3754
  subject: prefixedSubject(detail.subject, "Re:"),
3655
3755
  body: replyBody,
3656
3756
  isHtml,
3657
- htmlBody: signedReply.htmlBody,
3757
+ htmlBody: replyHtmlBody,
3658
3758
  fromName: fromNameReply,
3659
3759
  from: fromReply,
3660
3760
  sanitizeHtml: sanitizeHtmlReply,
@@ -3730,8 +3830,12 @@ export function createServer(config, options = {}) {
3730
3830
  const includeQuoteRa = normalizeBoolean(args.includeQuote, true);
3731
3831
  // Signature goes right after the user's own reply text, before the
3732
3832
  // quoted original — not at the very end, after the quote.
3733
- const signedReplyRa = applySignature(bodyRa, htmlBodyRa, normalizeBoolean(args.appendSignature, true));
3833
+ const signedReplyRa = applySignature(bodyRa, htmlBodyRa, normalizeBoolean(args.appendSignature, true), isHtmlRa);
3734
3834
  const replyBodyRa = includeQuoteRa ? buildReplyText(detailRa, signedReplyRa.body) : signedReplyRa.body;
3835
+ // Same fix as reply_to_email — see buildReplyHtml's comment.
3836
+ const replyHtmlBodyRa = includeQuoteRa && signedReplyRa.htmlBody !== undefined
3837
+ ? buildReplyHtml(detailRa, signedReplyRa.htmlBody)
3838
+ : signedReplyRa.htmlBody;
3735
3839
  const resultRa = await withAudit(auditService, name, args, async () => sendBundleForRa.smtpService.sendEmail({
3736
3840
  to: toRa,
3737
3841
  cc: ccRa,
@@ -3739,7 +3843,7 @@ export function createServer(config, options = {}) {
3739
3843
  subject: prefixedSubject(detailRa.subject, "Re:"),
3740
3844
  body: replyBodyRa,
3741
3845
  isHtml: isHtmlRa,
3742
- htmlBody: signedReplyRa.htmlBody,
3846
+ htmlBody: replyHtmlBodyRa,
3743
3847
  fromName: fromNameRa,
3744
3848
  from: fromRa,
3745
3849
  sanitizeHtml: sanitizeHtmlRa,
@@ -3838,7 +3942,13 @@ export function createServer(config, options = {}) {
3838
3942
  // Signature goes right after the user's own note, before the
3839
3943
  // "---------- Forwarded message ---------" block — not at the very
3840
3944
  // end, after the forwarded content.
3841
- const signedFwd = applySignature(body ?? "", htmlBody, normalizeBoolean(args.appendSignature, true));
3945
+ const signedFwd = applySignature(body ?? "", htmlBody, normalizeBoolean(args.appendSignature, true), isHtml);
3946
+ // Same fix as reply_to_email/reply_all_email — see buildReplyHtml's
3947
+ // comment (applies equally to a markdown-authored forward's separate
3948
+ // htmlBody, which buildForwardText never touches).
3949
+ const forwardHtmlBody = signedFwd.htmlBody !== undefined
3950
+ ? buildForwardHtml(detail, signedFwd.htmlBody)
3951
+ : signedFwd.htmlBody;
3842
3952
  const result = await withAudit(auditService, name, args, async () => sendBundleForFwd.smtpService.sendEmail({
3843
3953
  to,
3844
3954
  cc,
@@ -3846,7 +3956,7 @@ export function createServer(config, options = {}) {
3846
3956
  subject: prefixedSubject(detail.subject, "Fwd:"),
3847
3957
  body: buildForwardText(detail, signedFwd.body),
3848
3958
  isHtml,
3849
- htmlBody: signedFwd.htmlBody,
3959
+ htmlBody: forwardHtmlBody,
3850
3960
  fromName: fromNameFwd,
3851
3961
  from: fromFwd,
3852
3962
  sanitizeHtml: sanitizeHtmlFwd,
@@ -3930,7 +4040,10 @@ export function createServer(config, options = {}) {
3930
4040
  : undefined);
3931
4041
  return presentDraft(createDraftBundle, remoteSync ? { ...synced.draft, remoteSync } : synced.draft);
3932
4042
  });
3933
- return createTextResult(result, false, [
4043
+ // Same fix as update_draft's identical bloat — see its comment. The
4044
+ // caller already knows what it attached; get_draft is the "let me see
4045
+ // the full attachment content" call, not this one.
4046
+ return createTextResult(redactDraftAttachmentsForListing(result), false, [
3934
4047
  draftSource(result),
3935
4048
  ...(result.remoteDraft?.emailId
3936
4049
  ? [
@@ -3944,14 +4057,19 @@ export function createServer(config, options = {}) {
3944
4057
  ]);
3945
4058
  }
3946
4059
  case "create_reply_draft": {
3947
- const detail = await imapService.getEmailById(requireString(args, "emailId"));
4060
+ // emailId carries the same "<slug>::" account prefix as everywhere else —
4061
+ // this previously always read via the PRIMARY account's imapService
4062
+ // regardless of that prefix, so replying to a non-primary account's email
4063
+ // either resolved the wrong message or threw not-found.
4064
+ const { bundle: createReplyReadBundle, rest: createReplyEmailIdRest } = resolveAccountForEmailId(requireString(args, "emailId"));
4065
+ const detail = await createReplyReadBundle.imapService.getEmailById(createReplyEmailIdRest);
3948
4066
  const body = requireString(args, "body");
3949
4067
  const isHtml = normalizeBoolean(args.isHtml, false);
3950
4068
  const replyAll = normalizeBoolean(args.replyAll, false);
3951
4069
  const attachments = optionalAttachmentList(args.attachments);
3952
4070
  const extraCc = parseEmails(optionalString(args, "cc"));
3953
4071
  const extraBcc = parseEmails(optionalString(args, "bcc"));
3954
- const recipients = getReplyRecipients(detail, config.smtp.username, replyAll);
4072
+ const recipients = getReplyRecipients(detail, createReplyReadBundle.config.smtp.username, replyAll);
3955
4073
  const cc = uniqueAddresses([...recipients.cc, ...extraCc]);
3956
4074
  const to = uniqueAddresses(recipients.to);
3957
4075
  ensureValidEmails(to, "to");
@@ -3960,10 +4078,15 @@ export function createServer(config, options = {}) {
3960
4078
  if (to.length === 0) {
3961
4079
  throw new McpError(ErrorCode.InvalidParams, "Unable to infer reply recipient.");
3962
4080
  }
3963
- // Same from-based draftStore routing as create_draft — see its comment.
4081
+ // Same from-based draftStore routing as create_draft — see its comment
4082
+ // except the no-`from` default is the ACCOUNT THE ORIGINAL MESSAGE BELONGS
4083
+ // TO, not always the primary: a reply drafted against a non-primary
4084
+ // account's email with no explicit `from` should default to replying as
4085
+ // that same account, not silently switch to primary. `from` still wins
4086
+ // when it names a different configured account.
3964
4087
  const createReplyDraftBundle = optionalString(args, "from") !== undefined
3965
- ? accountManager.byAddress(optionalString(args, "from")) ?? primaryBundle
3966
- : primaryBundle;
4088
+ ? accountManager.byAddress(optionalString(args, "from")) ?? createReplyReadBundle
4089
+ : createReplyReadBundle;
3967
4090
  const result = await withAudit(auditService, name, args, async () => {
3968
4091
  const draft = await createReplyDraftBundle.draftStore.createDraft({
3969
4092
  mode: "reply",
@@ -3994,7 +4117,8 @@ export function createServer(config, options = {}) {
3994
4117
  : undefined);
3995
4118
  return presentDraft(createReplyDraftBundle, remoteSync ? { ...synced.draft, remoteSync } : synced.draft);
3996
4119
  });
3997
- return createTextResult(result, false, [
4120
+ // Same fix as create_draft/update_draft's identical bloat — see their comments.
4121
+ return createTextResult(redactDraftAttachmentsForListing(result), false, [
3998
4122
  draftSource(result),
3999
4123
  emailSource(detail),
4000
4124
  ...(result.remoteDraft?.emailId
@@ -4009,7 +4133,9 @@ export function createServer(config, options = {}) {
4009
4133
  ]);
4010
4134
  }
4011
4135
  case "create_forward_draft": {
4012
- const detail = await imapService.getEmailById(requireString(args, "emailId"));
4136
+ // Same emailId account resolution as create_reply_draft — see its comment.
4137
+ const { bundle: createForwardReadBundle, rest: createForwardEmailIdRest } = resolveAccountForEmailId(requireString(args, "emailId"));
4138
+ const detail = await createForwardReadBundle.imapService.getEmailById(createForwardEmailIdRest);
4013
4139
  const to = parseEmails(requireString(args, "to"));
4014
4140
  const cc = parseEmails(optionalString(args, "cc"));
4015
4141
  const bcc = parseEmails(optionalString(args, "bcc"));
@@ -4027,13 +4153,15 @@ export function createServer(config, options = {}) {
4027
4153
  const originalAttachments = includeAttachments
4028
4154
  ? await Promise.all(detail.attachments
4029
4155
  .filter((a) => a.id)
4030
- .map((a) => imapService.getAttachmentForForward(detail.id, a.id)))
4156
+ .map((a) => createForwardReadBundle.imapService.getAttachmentForForward(detail.id, a.id)))
4031
4157
  : [];
4032
4158
  const attachments = [...originalAttachments, ...callerAttachments];
4033
- // Same from-based draftStore routing as create_draftsee its comment.
4159
+ // Same from-based draftStore routing as create_reply_draftdefaults to
4160
+ // the ORIGINAL MESSAGE's own account, not always primary, when `from`
4161
+ // isn't given.
4034
4162
  const createForwardDraftBundle = optionalString(args, "from") !== undefined
4035
- ? accountManager.byAddress(optionalString(args, "from")) ?? primaryBundle
4036
- : primaryBundle;
4163
+ ? accountManager.byAddress(optionalString(args, "from")) ?? createForwardReadBundle
4164
+ : createForwardReadBundle;
4037
4165
  const result = await withAudit(auditService, name, args, async () => {
4038
4166
  const draft = await createForwardDraftBundle.draftStore.createDraft({
4039
4167
  mode: "forward",
@@ -4062,7 +4190,8 @@ export function createServer(config, options = {}) {
4062
4190
  : undefined);
4063
4191
  return presentDraft(createForwardDraftBundle, remoteSync ? { ...synced.draft, remoteSync } : synced.draft);
4064
4192
  });
4065
- return createTextResult(result, false, [
4193
+ // Same fix as create_draft/update_draft's identical bloat — see their comments.
4194
+ return createTextResult(redactDraftAttachmentsForListing(result), false, [
4066
4195
  draftSource(result),
4067
4196
  emailSource(detail),
4068
4197
  ...(result.remoteDraft?.emailId
@@ -4095,7 +4224,16 @@ export function createServer(config, options = {}) {
4095
4224
  case "get_draft": {
4096
4225
  const { bundle: getDraftBundle, rest: getDraftIdRest } = resolveAccountForDraftId(requireString(args, "draftId"));
4097
4226
  const draft = presentDraft(getDraftBundle, await getDraftBundle.draftStore.getDraft(getDraftIdRest));
4098
- return createTextResult(draft, false, [
4227
+ // Found live (external review): this always echoed every attachment's
4228
+ // full base64 content — ~955k tokens for a single 1 MiB attachment,
4229
+ // serialized twice by createTextResult on top of that. Metadata only by
4230
+ // default now (same redaction as list_drafts/update_draft/create_draft),
4231
+ // with an explicit opt-in for the rare case the caller actually needs
4232
+ // the raw bytes back.
4233
+ const draftResponse = normalizeBoolean(args.includeAttachmentContent, false)
4234
+ ? draft
4235
+ : redactDraftAttachmentsForListing(draft);
4236
+ return createTextResult(draftResponse, false, [
4099
4237
  draftSource(draft),
4100
4238
  ...(draft.remoteDraft?.emailId
4101
4239
  ? [
@@ -4188,7 +4326,8 @@ export function createServer(config, options = {}) {
4188
4326
  const draft = await syncDraftBundle.draftStore.getDraft(syncDraftIdRest);
4189
4327
  const synced = await withAudit(auditService, name, args, async () => syncDraftToRemote(syncDraftBundle.draftStore, syncDraftBundle.smtpService, syncDraftBundle.imapService, draft));
4190
4328
  const presentedSynced = presentDraft(syncDraftBundle, synced.draft);
4191
- return createTextResult({ ...presentedSynced, remoteSync: synced.remoteSync }, false, [
4329
+ // Same fix as create_draft/update_draft's identical bloat — see their comments.
4330
+ return createTextResult({ ...redactDraftAttachmentsForListing(presentedSynced), remoteSync: synced.remoteSync }, false, [
4192
4331
  draftSource(presentedSynced),
4193
4332
  ...(presentedSynced.remoteDraft?.emailId
4194
4333
  ? [
@@ -6335,10 +6474,15 @@ export function createServer(config, options = {}) {
6335
6474
  if (to.length === 0) {
6336
6475
  throw new McpError(ErrorCode.InvalidParams, "Unable to infer reply recipient.");
6337
6476
  }
6338
- // Same from-based draftStore routing as create_draftsee its comment.
6477
+ // Same from-based draftStore routing as create_reply_draftdefaults to
6478
+ // the THREAD's own account (threadReplyBundle, already resolved above from
6479
+ // the threadId's own prefix), not always primary, when `from` isn't given.
6480
+ // Previously always defaulted to primary regardless of which account the
6481
+ // thread actually belonged to — a reply drafted with no explicit `from`
6482
+ // against a non-primary account's thread silently switched accounts.
6339
6483
  const createThreadReplyDraftBundle = optionalString(args, "from") !== undefined
6340
- ? accountManager.byAddress(optionalString(args, "from")) ?? primaryBundle
6341
- : primaryBundle;
6484
+ ? accountManager.byAddress(optionalString(args, "from")) ?? threadReplyBundle
6485
+ : threadReplyBundle;
6342
6486
  const result = await withAudit(auditService, name, args, async () => {
6343
6487
  const draft = await createThreadReplyDraftBundle.draftStore.createDraft({
6344
6488
  mode: "reply",
@@ -6370,7 +6514,8 @@ export function createServer(config, options = {}) {
6370
6514
  const presented = presentDraft(createThreadReplyDraftBundle, remoteSync ? { ...synced.draft, remoteSync } : synced.draft);
6371
6515
  return { ...presented, threadId: thread.id };
6372
6516
  });
6373
- return createTextResult(result, false, [
6517
+ // Same fix as create_draft/update_draft's identical bloat — see their comments.
6518
+ return createTextResult(redactDraftAttachmentsForListing(result), false, [
6374
6519
  draftSource(result),
6375
6520
  threadSource(thread),
6376
6521
  emailSource(detail),