proton-mail-bridge-client 2.1.6 → 2.1.7

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
@@ -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 truncateDraftBodyForResponse<T extends {
69
+ body: string;
70
+ }>(draft: T): T;
68
71
  export declare function redactQueueRecordAttachments<T extends {
69
72
  payload: SendEmailInput;
70
73
  }>(record: T): T;
@@ -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
@@ -385,12 +385,14 @@ const TOOLS = [
385
385
  },
386
386
  {
387
387
  name: "list_drafts",
388
- description: "List all locally saved drafts with their status, subject, and timestamps. Attachment content is omitted here (filename/type/size only) to keep this unbounded listing from blowing up on large attachments — use get_draft for the full attachment content of one draft. Use to review in-progress or unsent messages. Does NOT list drafts stored only on the Proton server — use list_remote_drafts for those. Prefer get_draft when you already have a draftId and need the full content.",
388
+ description: "List all locally saved drafts with their status, subject, and timestamps. Attachment content is omitted here (filename/type/size only) — use get_draft for the full attachment content of one draft. Each draft's body is also truncated to a short preview (bodyTruncated:true, bodyLength gives the real size) — use get_draft for the full body of one draft. Use to review in-progress or unsent messages. Does NOT list drafts stored only on the Proton server — use list_remote_drafts for those. Prefer get_draft when you already have a draftId and need the full content.",
389
389
  annotations: { readOnlyHint: true },
390
390
  inputSchema: {
391
391
  type: "object",
392
392
  properties: {
393
393
  includeSent: { type: "boolean", description: "Include drafts already sent.", default: false },
394
+ limit: { type: "number", description: "Maximum drafts to return. Omit to return every draft (still with truncated bodies/attachments) — set this when you only need a bounded page." },
395
+ offset: { type: "number", description: "Number of drafts to skip, newest-updated first.", default: 0 },
394
396
  },
395
397
  },
396
398
  },
@@ -2277,6 +2279,34 @@ export function redactDraftAttachmentsForListing(draft) {
2277
2279
  })),
2278
2280
  };
2279
2281
  }
2282
+ // How much of a draft's body survives into a mutation/listing response before
2283
+ // it's truncated — long enough to recognize the draft at a glance, short
2284
+ // enough that a large body (a long thread quoted into a reply, a big pasted
2285
+ // table) doesn't dominate the response the way redactDraftAttachmentsForListing
2286
+ // already stops a base64 attachment from doing.
2287
+ const DRAFT_BODY_PREVIEW_LENGTH = 500;
2288
+ // Found live (external review, repeated across three rounds): create_draft,
2289
+ // update_draft, create_reply_draft, create_forward_draft,
2290
+ // create_thread_reply_draft, sync_draft_to_remote, and list_drafts all echoed
2291
+ // the draft's COMPLETE body back — confirmed live at ~40k tokens for a single
2292
+ // update_draft call that only changed the subject of a ~202 KB draft, and
2293
+ // ~261k tokens for list_drafts with 107 drafts. The caller supplied the body
2294
+ // itself (create/update) or already has it from a prior call (list) — same
2295
+ // "caller already knows what it sent" reasoning as attachment redaction, just
2296
+ // for the text body instead of base64. get_draft deliberately keeps the full
2297
+ // body — it's the "let me look at this one draft" call, not a repeated-edit
2298
+ // or bulk-listing one.
2299
+ export function truncateDraftBodyForResponse(draft) {
2300
+ if (draft.body.length <= DRAFT_BODY_PREVIEW_LENGTH) {
2301
+ return draft;
2302
+ }
2303
+ return {
2304
+ ...draft,
2305
+ body: `${draft.body.slice(0, DRAFT_BODY_PREVIEW_LENGTH)}…`,
2306
+ bodyLength: draft.body.length,
2307
+ bodyTruncated: true,
2308
+ };
2309
+ }
2280
2310
  // Same fix as redactDraftAttachmentsForListing, for list_scheduled_sends — a
2281
2311
  // queued send's payload carries the same full base64 EmailAttachmentInput
2282
2312
  // content (found live: ~955k tokens for a single 1 MiB attachment on ONE
@@ -4041,9 +4071,9 @@ export function createServer(config, options = {}) {
4041
4071
  return presentDraft(createDraftBundle, remoteSync ? { ...synced.draft, remoteSync } : synced.draft);
4042
4072
  });
4043
4073
  // 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, [
4074
+ // caller already knows what it attached (and what body it just sent);
4075
+ // get_draft is the "let me see the full content" call, not this one.
4076
+ return createTextResult(truncateDraftBodyForResponse(redactDraftAttachmentsForListing(result)), false, [
4047
4077
  draftSource(result),
4048
4078
  ...(result.remoteDraft?.emailId
4049
4079
  ? [
@@ -4126,7 +4156,7 @@ export function createServer(config, options = {}) {
4126
4156
  return presentDraft(createReplyDraftBundle, remoteSync ? { ...synced.draft, remoteSync } : synced.draft);
4127
4157
  });
4128
4158
  // Same fix as create_draft/update_draft's identical bloat — see their comments.
4129
- return createTextResult(redactDraftAttachmentsForListing(result), false, [
4159
+ return createTextResult(truncateDraftBodyForResponse(redactDraftAttachmentsForListing(result)), false, [
4130
4160
  draftSource(result),
4131
4161
  emailSource(detail),
4132
4162
  ...(result.remoteDraft?.emailId
@@ -4201,7 +4231,7 @@ export function createServer(config, options = {}) {
4201
4231
  return presentDraft(createForwardDraftBundle, remoteSync ? { ...synced.draft, remoteSync } : synced.draft);
4202
4232
  });
4203
4233
  // Same fix as create_draft/update_draft's identical bloat — see their comments.
4204
- return createTextResult(redactDraftAttachmentsForListing(result), false, [
4234
+ return createTextResult(truncateDraftBodyForResponse(redactDraftAttachmentsForListing(result)), false, [
4205
4235
  draftSource(result),
4206
4236
  emailSource(detail),
4207
4237
  ...(result.remoteDraft?.emailId
@@ -4220,10 +4250,23 @@ export function createServer(config, options = {}) {
4220
4250
  // drafts only — a non-primary account's drafts aren't listed here yet. Use
4221
4251
  // get_draft with that account's prefixed draftId if you already have one.
4222
4252
  const drafts = await draftStore.listDrafts(normalizeBoolean(args.includeSent, false));
4253
+ // Found live (external review): every draft's full body was returned
4254
+ // alongside the already-redacted attachments — confirmed live at ~261k
4255
+ // tokens for 107 drafts. Bodies are now truncated the same way a
4256
+ // mutation response's body is (see truncateDraftBodyForResponse). limit/
4257
+ // offset are optional and additive — omitting them still returns every
4258
+ // draft (just with truncated bodies/attachments), so this doesn't change
4259
+ // behavior for a caller not using the new params.
4260
+ const offset = normalizeLimit(args.offset, 0, 0, 10_000);
4261
+ const limit = typeof args.limit === "number" ? normalizeLimit(args.limit, drafts.length, 1, 10_000) : undefined;
4262
+ const page = limit !== undefined ? drafts.slice(offset, offset + limit) : drafts.slice(offset);
4223
4263
  return createTextResult({
4224
4264
  total: drafts.length,
4225
- drafts: drafts.map(redactDraftAttachmentsForListing),
4226
- }, false, drafts.map(draftSource));
4265
+ offset,
4266
+ returned: page.length,
4267
+ hasMore: offset + page.length < drafts.length,
4268
+ drafts: page.map((draft) => truncateDraftBodyForResponse(redactDraftAttachmentsForListing(draft))),
4269
+ }, false, page.map(draftSource));
4227
4270
  }
4228
4271
  case "list_remote_drafts": {
4229
4272
  // Scope limitation (follow-up round): stays scoped to the primary account's
@@ -4314,10 +4357,13 @@ export function createServer(config, options = {}) {
4314
4357
  // subject/body — the attachments usually aren't touched by this call at all, so
4315
4358
  // re-echoing their full base64 `content` (serialized twice by createTextResult,
4316
4359
  // same issue redactDraftAttachmentsForListing already fixed for list_drafts)
4317
- // burns tokens on every single edit for a draft that has any attachment. The
4318
- // caller already knows what it attached; get_draft remains full-content for the
4319
- // "let me look at this draft" case, which is a single call rather than a loop.
4320
- return createTextResult(redactDraftAttachmentsForListing(result), false, [
4360
+ // burns tokens on every single edit for a draft that has any attachment. Same
4361
+ // reasoning for the body itself confirmed live at ~40k tokens for a single
4362
+ // subject-only edit of a ~202 KB draft (see truncateDraftBodyForResponse). The
4363
+ // caller already knows what it attached and what body it just sent; get_draft
4364
+ // remains full-content for the "let me look at this draft" case, which is a
4365
+ // single call rather than a loop.
4366
+ return createTextResult(truncateDraftBodyForResponse(redactDraftAttachmentsForListing(result)), false, [
4321
4367
  draftSource(result),
4322
4368
  ...(result.remoteDraft?.emailId
4323
4369
  ? [
@@ -4337,7 +4383,7 @@ export function createServer(config, options = {}) {
4337
4383
  const synced = await withAudit(auditService, name, args, async () => syncDraftToRemote(syncDraftBundle.draftStore, syncDraftBundle.smtpService, syncDraftBundle.imapService, draft));
4338
4384
  const presentedSynced = presentDraft(syncDraftBundle, synced.draft);
4339
4385
  // Same fix as create_draft/update_draft's identical bloat — see their comments.
4340
- return createTextResult({ ...redactDraftAttachmentsForListing(presentedSynced), remoteSync: synced.remoteSync }, false, [
4386
+ return createTextResult({ ...truncateDraftBodyForResponse(redactDraftAttachmentsForListing(presentedSynced)), remoteSync: synced.remoteSync }, false, [
4341
4387
  draftSource(presentedSynced),
4342
4388
  ...(presentedSynced.remoteDraft?.emailId
4343
4389
  ? [
@@ -4657,25 +4703,34 @@ export function createServer(config, options = {}) {
4657
4703
  // requested offset/limit to that merged superset instead of per account.
4658
4704
  const primarySlug = accountManager.primary().account.slug;
4659
4705
  const requestedOffset = getEmailsInput.offset ?? 0;
4660
- const perAccountFetchLimit = requestedOffset + effectiveLimit;
4706
+ // +1 beyond what's strictly needed to fill the page: whether that extra
4707
+ // item actually comes back is what tells us hasMore, independent of any
4708
+ // filter (beforeUid etc.) or of how the raw mailbox total compares to the
4709
+ // page window. Two things were tried and each broke a different case:
4710
+ // (1) using mergedAll.length > offset+limit as hasMore, with NO extra
4711
+ // overfetch — a lopsided distribution (e.g. 300 messages in one account,
4712
+ // 0 in another) fetches exactly offset+limit from the big account, so
4713
+ // mergedAll.length can never exceed that window even though hundreds
4714
+ // more exist, reporting hasMore:false wrongly.
4715
+ // (2) using the raw summed mailbox `total` for hasMore instead — correct
4716
+ // for that case, but wrong under a restrictive filter like beforeUid:
4717
+ // `total` is the UNFILTERED mailbox message count, not the count of
4718
+ // messages actually matching the filter, so a beforeUid narrowing 300
4719
+ // messages down to 2 matches still compared against total=300 and
4720
+ // reported hasMore:true with nothing left to page to.
4721
+ // Fetching one extra item fixes both: it directly observes whether a
4722
+ // NEXT matching item exists, under whatever filter is active, rather
4723
+ // than inferring it from a count that can be either capped-by-fetch or
4724
+ // filter-blind depending on which one you pick.
4725
+ const perAccountFetchLimit = requestedOffset + effectiveLimit + 1;
4661
4726
  const perAccount = await Promise.all(accountManager.all().map(async (bundle) => {
4662
4727
  const result = await bundle.imapService.getEmails({ ...getEmailsInput, offset: 0, limit: perAccountFetchLimit });
4663
4728
  return { bundle, result };
4664
4729
  }));
4665
4730
  const taggedEmails = perAccount.flatMap(({ bundle, result }) => result.emails.map((email) => ({ ...email, id: withAccountPrefix(accountSlugForTag(bundle, primarySlug), email.id) })));
4666
4731
  const mergedAll = sortByDateDesc(taggedEmails, (email) => email.internalDate || email.date);
4667
- const { page: merged } = paginateMergedAccountResults(mergedAll, requestedOffset, effectiveLimit);
4732
+ const { page: merged, hasMore } = paginateMergedAccountResults(mergedAll, requestedOffset, effectiveLimit);
4668
4733
  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;
4679
4734
  return createTextResult({
4680
4735
  folder: getEmailsInput.folder?.trim() || "INBOX",
4681
4736
  total,
@@ -6537,7 +6592,7 @@ export function createServer(config, options = {}) {
6537
6592
  return { ...presented, threadId: thread.id };
6538
6593
  });
6539
6594
  // Same fix as create_draft/update_draft's identical bloat — see their comments.
6540
- return createTextResult(redactDraftAttachmentsForListing(result), false, [
6595
+ return createTextResult(truncateDraftBodyForResponse(redactDraftAttachmentsForListing(result)), false, [
6541
6596
  draftSource(result),
6542
6597
  threadSource(thread),
6543
6598
  emailSource(detail),