proton-mail-bridge-client 2.1.4 → 2.1.6

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>;
@@ -65,6 +65,9 @@ export declare function redactDraftAttachmentsForListing(draft: DraftRecord): {
65
65
  remoteDraft?: import("./types/index.js").RemoteDraftRef;
66
66
  lastSendResult?: import("./types/index.js").DraftSendResult;
67
67
  };
68
+ export declare function redactQueueRecordAttachments<T extends {
69
+ payload: SendEmailInput;
70
+ }>(record: T): T;
68
71
  export declare function paginateMergedAccountResults<T>(sortedMerged: T[], offset: number, limit: number): {
69
72
  page: T[];
70
73
  hasMore: boolean;
@@ -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,EAAmC,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC1F,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE7D,OAAO,KAAK,EAMV,mBAAmB,EAEnB,WAAW,EAIX,WAAW,EAIX,gBAAgB,EAIjB,MAAM,kBAAkB,CAAC;AAmB1B,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAmrDnF,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;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;AAgWD,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmrIP;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
@@ -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
  },
@@ -2276,6 +2277,32 @@ export function redactDraftAttachmentsForListing(draft) {
2276
2277
  })),
2277
2278
  };
2278
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
+ }
2279
2306
  function draftSource(draft) {
2280
2307
  return {
2281
2308
  uri: buildDraftResourceUri(draft.id),
@@ -2414,10 +2441,24 @@ async function syncDraftToRemote(draftStore, smtpService, imapService, draft) {
2414
2441
  isHtml: draft.isHtml,
2415
2442
  priority: draft.priority,
2416
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,
2417
2449
  inReplyTo: draft.inReplyTo,
2418
2450
  references: draft.references,
2419
2451
  messageId: draft.draftMessageId,
2420
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,
2421
2462
  });
2422
2463
  const remoteDraft = await imapService.upsertRemoteDraft({
2423
2464
  raw,
@@ -3561,9 +3602,14 @@ export function createServer(config, options = {}) {
3561
3602
  // item's id with its owning account's slug — mirrors the read-tool fan-out
3562
3603
  // pattern (get_emails etc.). Single-account guard keeps byte-identical output
3563
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).
3564
3609
  if (accountManager.all().length === 1) {
3565
3610
  const all = await withAudit(auditService, name, args, async () => deliveryQueueService.list());
3566
- const filtered = statusFilter ? all.filter((item) => item.status === statusFilter) : all;
3611
+ const filtered = (statusFilter ? all.filter((item) => item.status === statusFilter) : all)
3612
+ .map(redactQueueRecordAttachments);
3567
3613
  return createTextResult(filtered);
3568
3614
  }
3569
3615
  const perAccount = await withAudit(auditService, name, args, async () => Promise.all(accountManager.all().map(async (bundle) => ({
@@ -3571,7 +3617,8 @@ export function createServer(config, options = {}) {
3571
3617
  items: await bundle.deliveryQueueService.list(),
3572
3618
  }))));
3573
3619
  const tagged = perAccount.flatMap(({ bundle, items }) => items.map((item) => ({ ...item, id: withAccountPrefix(responseSlug(bundle), item.id) })));
3574
- const filtered = statusFilter ? tagged.filter((item) => item.status === statusFilter) : tagged;
3620
+ const filtered = (statusFilter ? tagged.filter((item) => item.status === statusFilter) : tagged)
3621
+ .map(redactQueueRecordAttachments);
3575
3622
  return createTextResult(filtered);
3576
3623
  }
3577
3624
  case "get_unsubscribe_info": {
@@ -3993,7 +4040,10 @@ export function createServer(config, options = {}) {
3993
4040
  : undefined);
3994
4041
  return presentDraft(createDraftBundle, remoteSync ? { ...synced.draft, remoteSync } : synced.draft);
3995
4042
  });
3996
- 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, [
3997
4047
  draftSource(result),
3998
4048
  ...(result.remoteDraft?.emailId
3999
4049
  ? [
@@ -4046,6 +4096,14 @@ export function createServer(config, options = {}) {
4046
4096
  subject: prefixedSubject(detail.subject, "Re:"),
4047
4097
  body: buildReplyText(detail, body),
4048
4098
  isHtml,
4099
+ // Found live (external review): a `from` naming a real alias NOT
4100
+ // itself a separately-configured account (e.g. a genuine alias
4101
+ // reachable only via header override, same as send_email's own
4102
+ // fallback) picked the right draftStore bundle above but never
4103
+ // actually stored the address on the draft — draft.from came back
4104
+ // undefined, so a later send_draft used the account's default
4105
+ // sender instead of the alias the caller asked for.
4106
+ from: optionalString(args, "from"),
4049
4107
  inReplyTo: detail.messageId,
4050
4108
  references: detail.messageId ? [detail.messageId] : undefined,
4051
4109
  attachments,
@@ -4067,7 +4125,8 @@ export function createServer(config, options = {}) {
4067
4125
  : undefined);
4068
4126
  return presentDraft(createReplyDraftBundle, remoteSync ? { ...synced.draft, remoteSync } : synced.draft);
4069
4127
  });
4070
- return createTextResult(result, false, [
4128
+ // Same fix as create_draft/update_draft's identical bloat — see their comments.
4129
+ return createTextResult(redactDraftAttachmentsForListing(result), false, [
4071
4130
  draftSource(result),
4072
4131
  emailSource(detail),
4073
4132
  ...(result.remoteDraft?.emailId
@@ -4120,6 +4179,8 @@ export function createServer(config, options = {}) {
4120
4179
  subject: prefixedSubject(detail.subject, "Fwd:"),
4121
4180
  body: buildForwardText(detail, optionalString(args, "body")),
4122
4181
  isHtml: normalizeBoolean(args.isHtml, false),
4182
+ // Same fix as create_reply_draft — see its comment.
4183
+ from: optionalString(args, "from"),
4123
4184
  attachments,
4124
4185
  sourceEmailId: detail.id,
4125
4186
  sourceMessageId: detail.messageId,
@@ -4139,7 +4200,8 @@ export function createServer(config, options = {}) {
4139
4200
  : undefined);
4140
4201
  return presentDraft(createForwardDraftBundle, remoteSync ? { ...synced.draft, remoteSync } : synced.draft);
4141
4202
  });
4142
- return createTextResult(result, false, [
4203
+ // Same fix as create_draft/update_draft's identical bloat — see their comments.
4204
+ return createTextResult(redactDraftAttachmentsForListing(result), false, [
4143
4205
  draftSource(result),
4144
4206
  emailSource(detail),
4145
4207
  ...(result.remoteDraft?.emailId
@@ -4172,7 +4234,16 @@ export function createServer(config, options = {}) {
4172
4234
  case "get_draft": {
4173
4235
  const { bundle: getDraftBundle, rest: getDraftIdRest } = resolveAccountForDraftId(requireString(args, "draftId"));
4174
4236
  const draft = presentDraft(getDraftBundle, await getDraftBundle.draftStore.getDraft(getDraftIdRest));
4175
- return createTextResult(draft, false, [
4237
+ // Found live (external review): this always echoed every attachment's
4238
+ // full base64 content — ~955k tokens for a single 1 MiB attachment,
4239
+ // serialized twice by createTextResult on top of that. Metadata only by
4240
+ // default now (same redaction as list_drafts/update_draft/create_draft),
4241
+ // with an explicit opt-in for the rare case the caller actually needs
4242
+ // the raw bytes back.
4243
+ const draftResponse = normalizeBoolean(args.includeAttachmentContent, false)
4244
+ ? draft
4245
+ : redactDraftAttachmentsForListing(draft);
4246
+ return createTextResult(draftResponse, false, [
4176
4247
  draftSource(draft),
4177
4248
  ...(draft.remoteDraft?.emailId
4178
4249
  ? [
@@ -4265,7 +4336,8 @@ export function createServer(config, options = {}) {
4265
4336
  const draft = await syncDraftBundle.draftStore.getDraft(syncDraftIdRest);
4266
4337
  const synced = await withAudit(auditService, name, args, async () => syncDraftToRemote(syncDraftBundle.draftStore, syncDraftBundle.smtpService, syncDraftBundle.imapService, draft));
4267
4338
  const presentedSynced = presentDraft(syncDraftBundle, synced.draft);
4268
- return createTextResult({ ...presentedSynced, remoteSync: synced.remoteSync }, false, [
4339
+ // Same fix as create_draft/update_draft's identical bloat — see their comments.
4340
+ return createTextResult({ ...redactDraftAttachmentsForListing(presentedSynced), remoteSync: synced.remoteSync }, false, [
4269
4341
  draftSource(presentedSynced),
4270
4342
  ...(presentedSynced.remoteDraft?.emailId
4271
4343
  ? [
@@ -4592,8 +4664,18 @@ export function createServer(config, options = {}) {
4592
4664
  }));
4593
4665
  const taggedEmails = perAccount.flatMap(({ bundle, result }) => result.emails.map((email) => ({ ...email, id: withAccountPrefix(accountSlugForTag(bundle, primarySlug), email.id) })));
4594
4666
  const mergedAll = sortByDateDesc(taggedEmails, (email) => email.internalDate || email.date);
4595
- const { page: merged, hasMore } = paginateMergedAccountResults(mergedAll, requestedOffset, effectiveLimit);
4667
+ const { page: merged } = paginateMergedAccountResults(mergedAll, requestedOffset, effectiveLimit);
4596
4668
  const total = perAccount.reduce((sum, { result }) => sum + result.total, 0);
4669
+ // Found live (external review): paginateMergedAccountResults' own hasMore
4670
+ // only looks at what was actually FETCHED (mergedAll.length, capped at
4671
+ // perAccountFetchLimit per account) — with a lopsided distribution (e.g.
4672
+ // 300 messages in one account, 0 in another) and a small page near the
4673
+ // start, mergedAll.length can equal requestedOffset+limit exactly even
4674
+ // though the account's real total is far larger, reporting hasMore:false
4675
+ // while 275 more messages exist. `total` above is each account's real,
4676
+ // authoritative IMAP mailbox count (not bounded by what we fetched), so
4677
+ // it's the correct source for this instead.
4678
+ const hasMore = total > requestedOffset + effectiveLimit;
4597
4679
  return createTextResult({
4598
4680
  folder: getEmailsInput.folder?.trim() || "INBOX",
4599
4681
  total,
@@ -6430,6 +6512,8 @@ export function createServer(config, options = {}) {
6430
6512
  subject: prefixedSubject(detail.subject, "Re:"),
6431
6513
  body: buildReplyText(detail, body),
6432
6514
  isHtml,
6515
+ // Same fix as create_reply_draft — see its comment.
6516
+ from: optionalString(args, "from"),
6433
6517
  inReplyTo: detail.messageId,
6434
6518
  references: detail.messageId ? [detail.messageId] : undefined,
6435
6519
  attachments,
@@ -6452,7 +6536,8 @@ export function createServer(config, options = {}) {
6452
6536
  const presented = presentDraft(createThreadReplyDraftBundle, remoteSync ? { ...synced.draft, remoteSync } : synced.draft);
6453
6537
  return { ...presented, threadId: thread.id };
6454
6538
  });
6455
- return createTextResult(result, false, [
6539
+ // Same fix as create_draft/update_draft's identical bloat — see their comments.
6540
+ return createTextResult(redactDraftAttachmentsForListing(result), false, [
6456
6541
  draftSource(result),
6457
6542
  threadSource(thread),
6458
6543
  emailSource(detail),