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 +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +174 -29
- package/dist/index.js.map +1 -1
- package/dist/services/simple-imap-service.d.ts.map +1 -1
- package/dist/services/simple-imap-service.js +11 -1
- package/dist/services/simple-imap-service.js.map +1 -1
- package/dist/services/smtp-service.d.ts +2 -1
- package/dist/services/smtp-service.d.ts.map +1 -1
- package/dist/services/smtp-service.js +44 -2
- package/dist/services/smtp-service.js.map +1 -1
- package/package.json +1 -1
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;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAA2H,iBAAiB,EAA+B,MAAM,mCAAmC,CAAC;AAC5N,OAAO,
|
|
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, "&")
|
|
2115
|
+
.replace(/</g, "<")
|
|
2116
|
+
.replace(/>/g, ">")
|
|
2117
|
+
.replace(/"/g, """)
|
|
2118
|
+
.replace(/'/g, "'");
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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")) ??
|
|
3966
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4159
|
+
// Same from-based draftStore routing as create_reply_draft — defaults 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")) ??
|
|
4036
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6477
|
+
// Same from-based draftStore routing as create_reply_draft — defaults 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")) ??
|
|
6341
|
-
:
|
|
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
|
-
|
|
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),
|