proton-mail-bridge-client 2.1.7 → 2.1.9

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/README.md CHANGED
@@ -311,6 +311,7 @@ First, enable **Split Addresses** in Proton Bridge and configure each address as
311
311
  - Search & read: `search_indexed_emails`, `get_emails`, `get_threads`, `get_thread_by_id`, `count_messages`, `get_labels`, `folder_stats`, `top_senders`, `get_contacts`, `get_volume_trends`, `get_email_analytics`, `get_email_stats`, `get_folders`
312
312
  - Triage: `get_inbox_digest`, `get_follow_up_candidates`, `get_actionable_threads`, `find_document_threads`, `prepare_meeting_context`
313
313
  - Diagnostics: `get_connection_status`, `get_runtime_status`, `run_doctor` (include an `accounts` array showing per-account status)
314
+ - Drafts: `list_drafts` — lists every configured account's drafts, merged and re-sorted by last update; supports optional `limit`/`offset`
314
315
 
315
316
  **Account-specific (prefix to target):**
316
317
  - Actions: `mark_email_read`, `star_email`, `move_email`, `delete_email`, etc. — prefix the `emailId` with account slug if targeting a non-primary account
@@ -318,7 +319,7 @@ First, enable **Split Addresses** in Proton Bridge and configure each address as
318
319
  - Send: `send_email`, `reply_to_email`, `forward_email` — match the `from` address to route through the correct account's SMTP
319
320
 
320
321
  **Primary-only (for now):**
321
- - `list_drafts`, `list_remote_drafts` — show primary account drafts only
322
+ - `list_remote_drafts` — shows the primary account's remote Proton Drafts folder only
322
323
  - Thread actions (`move_thread`, `delete_thread`, `flag_thread`) — operate on the account the thread ID's prefix names
323
324
 
324
325
  ### Verify setup
package/dist/index.d.ts CHANGED
@@ -8,10 +8,12 @@ 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, SendEmailInput } from "./types/index.js";
11
+ import type { BulkOperationResult, CitationSource, 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
+ export declare function withSources<T>(value: T, _sources: CitationSource[]): T;
14
15
  export declare function withAudit<T>(auditService: AuditService, tool: string, input: Record<string, unknown>, operation: () => Promise<T>): Promise<T>;
16
+ export declare function optionalClearableString(args: Record<string, unknown>, key: string): string | undefined;
15
17
  export declare function headerString(headers: Record<string, unknown> | undefined, name: string): string | undefined;
16
18
  export declare function parseAuthenticationResults(value?: string): {
17
19
  dkim?: string;
@@ -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
@@ -1740,14 +1740,26 @@ function normalizeStructuredContent(value) {
1740
1740
  }
1741
1741
  return normalized;
1742
1742
  }
1743
- function withSources(value, sources) {
1744
- if (sources.length === 0 || !value || typeof value !== "object" || Array.isArray(value)) {
1745
- return value;
1746
- }
1747
- return {
1748
- ...value,
1749
- sources,
1750
- };
1743
+ // Found live (self-review, token-efficiency pass): this used to merge the full
1744
+ // CitationSource[] (uri, name, title, description, mimeType, provider, AND a
1745
+ // locator object repeating id/folder/from/subject/date) into the returned
1746
+ // payload — which then got serialized into content[0].text AND, separately,
1747
+ // into structuredContent (createTextResult always builds both). For a
1748
+ // search/list-style tool, almost every field in each source is ALREADY present
1749
+ // on the corresponding item in the tool's own result array (e.g.
1750
+ // search_indexed_emails' sources[].snippet duplicates emails[].preview
1751
+ // character-for-character, sources[].locator.subject duplicates
1752
+ // emails[].subject, etc.) — confirmed live at roughly doubling a 10-result
1753
+ // search's response size for zero new information. The same citation data is
1754
+ // already emitted, far more compactly, as its own resource_link content block
1755
+ // per source (see citationToResourceLink below) — no code in this repo (CLI,
1756
+ // tests, or index.ts itself) ever reads a result's `.sources` field back, so
1757
+ // nothing depends on it being embedded in the payload too. Kept as a
1758
+ // pass-through function (not deleted) so createTextResult's call sites don't
1759
+ // all need touching if a real reason to re-add a *trimmed* sources field ever
1760
+ // comes up.
1761
+ export function withSources(value, _sources) {
1762
+ return value;
1751
1763
  }
1752
1764
  function createTextResult(value, isError = false, sources = []) {
1753
1765
  const payload = withSources(value, sources);
@@ -1841,6 +1853,23 @@ function optionalString(args, key) {
1841
1853
  const value = args[key];
1842
1854
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
1843
1855
  }
1856
+ // Found live (external review): update_draft's body:'' and replyTo:'' both
1857
+ // returned success but left the old values in place — optionalString collapses
1858
+ // an explicitly-empty string to undefined the same as an omitted field
1859
+ // entirely, and DraftStoreService.updateDraft's patch treats undefined as
1860
+ // "don't touch this field" (`patch.body ?? existing.body`). So there was no
1861
+ // way to actually clear either field once set — only used for update_draft's
1862
+ // body/replyTo, which are the two fields the review confirmed this breaks
1863
+ // (cc/bcc/attachments already have their own clear path via
1864
+ // `args.x === undefined ? undefined : ...`, checked separately). Not applied
1865
+ // to optionalString itself, since most of its many other callers legitimately
1866
+ // want "" treated the same as "not provided".
1867
+ export function optionalClearableString(args, key) {
1868
+ if (!(key in args) || args[key] === undefined) {
1869
+ return undefined;
1870
+ }
1871
+ return typeof args[key] === "string" ? args[key].trim() : undefined;
1872
+ }
1844
1873
  // Shared by get_email_by_id and get_emails_by_ids: applies body preference/
1845
1874
  // truncation and strips the full raw header map by default (needless token
1846
1875
  // bloat — get_email_by_id re-adds it when showHeaders is explicitly true).
@@ -2489,7 +2518,7 @@ async function syncDraftToRemote(draftStore, smtpService, imapService, draft) {
2489
2518
  // signature into the draft body themselves, got it appended a second time
2490
2519
  // on every remote sync.
2491
2520
  appendSignature: false,
2492
- });
2521
+ }, /* preserveBcc */ true);
2493
2522
  const remoteDraft = await imapService.upsertRemoteDraft({
2494
2523
  raw,
2495
2524
  messageId: draft.draftMessageId,
@@ -4246,10 +4275,24 @@ export function createServer(config, options = {}) {
4246
4275
  ]);
4247
4276
  }
4248
4277
  case "list_drafts": {
4249
- // Scope limitation (follow-up round): stays scoped to the primary account's
4250
- // drafts only — a non-primary account's drafts aren't listed here yet. Use
4251
- // get_draft with that account's prefixed draftId if you already have one.
4252
- const drafts = await draftStore.listDrafts(normalizeBoolean(args.includeSent, false));
4278
+ // Found live (external review): this stayed scoped to the primary
4279
+ // account's own drafts only — a draft created via create_draft(from:
4280
+ // secondary@example.com) is real and gettable by its prefixed draftId,
4281
+ // but never showed up here, so day-to-day draft discovery silently
4282
+ // missed every non-primary account's drafts. Now fans out across every
4283
+ // configured account, same pattern as list_scheduled_sends/
4284
+ // list_snoozed, and re-sorts the merged set by updatedAt (each
4285
+ // account's own list already arrives that way). Single-account guard
4286
+ // keeps byte-identical output for the common case.
4287
+ const includeSent = normalizeBoolean(args.includeSent, false);
4288
+ const allDrafts = accountManager.all().length === 1
4289
+ ? (await draftStore.listDrafts(includeSent)).map((draft) => ({ bundle: primaryBundle, draft }))
4290
+ : (await Promise.all(accountManager.all().map(async (bundle) => {
4291
+ const drafts = await bundle.draftStore.listDrafts(includeSent);
4292
+ return drafts.map((draft) => ({ bundle, draft: presentDraft(bundle, draft) }));
4293
+ })))
4294
+ .flat()
4295
+ .sort((left, right) => right.draft.updatedAt.localeCompare(left.draft.updatedAt));
4253
4296
  // Found live (external review): every draft's full body was returned
4254
4297
  // alongside the already-redacted attachments — confirmed live at ~261k
4255
4298
  // tokens for 107 drafts. Bodies are now truncated the same way a
@@ -4258,15 +4301,15 @@ export function createServer(config, options = {}) {
4258
4301
  // draft (just with truncated bodies/attachments), so this doesn't change
4259
4302
  // behavior for a caller not using the new params.
4260
4303
  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);
4304
+ const limit = typeof args.limit === "number" ? normalizeLimit(args.limit, allDrafts.length, 1, 10_000) : undefined;
4305
+ const page = limit !== undefined ? allDrafts.slice(offset, offset + limit) : allDrafts.slice(offset);
4263
4306
  return createTextResult({
4264
- total: drafts.length,
4307
+ total: allDrafts.length,
4265
4308
  offset,
4266
4309
  returned: page.length,
4267
- hasMore: offset + page.length < drafts.length,
4268
- drafts: page.map((draft) => truncateDraftBodyForResponse(redactDraftAttachmentsForListing(draft))),
4269
- }, false, page.map(draftSource));
4310
+ hasMore: offset + page.length < allDrafts.length,
4311
+ drafts: page.map(({ draft }) => truncateDraftBodyForResponse(redactDraftAttachmentsForListing(draft))),
4312
+ }, false, page.map(({ draft }) => draftSource(draft)));
4270
4313
  }
4271
4314
  case "list_remote_drafts": {
4272
4315
  // Scope limitation (follow-up round): stays scoped to the primary account's
@@ -4304,7 +4347,11 @@ export function createServer(config, options = {}) {
4304
4347
  const to = args.to === undefined ? undefined : parseEmails(optionalString(args, "to"));
4305
4348
  const cc = args.cc === undefined ? undefined : parseEmails(optionalString(args, "cc"));
4306
4349
  const bcc = args.bcc === undefined ? undefined : parseEmails(optionalString(args, "bcc"));
4307
- const replyTo = optionalString(args, "replyTo");
4350
+ // body/replyTo use optionalClearableString, not optionalString — see its
4351
+ // comment — so an explicit body:'' or replyTo:'' actually clears the
4352
+ // field instead of being silently treated as "field not provided".
4353
+ const body = optionalClearableString(args, "body");
4354
+ const replyTo = optionalClearableString(args, "replyTo");
4308
4355
  const from = optionalString(args, "from");
4309
4356
  const priority = optionalString(args, "priority");
4310
4357
  const attachments = args.attachments === undefined ? undefined : optionalAttachmentList(args.attachments);
@@ -4329,7 +4376,7 @@ export function createServer(config, options = {}) {
4329
4376
  cc,
4330
4377
  bcc,
4331
4378
  subject: optionalString(args, "subject"),
4332
- body: optionalString(args, "body"),
4379
+ body,
4333
4380
  isHtml: typeof args.isHtml === "boolean" ? args.isHtml : undefined,
4334
4381
  priority: priority === "high" || priority === "low" || priority === "normal"
4335
4382
  ? priority
@@ -4401,6 +4448,22 @@ export function createServer(config, options = {}) {
4401
4448
  ensureSendAllowed(config.runtime);
4402
4449
  const { bundle: sendDraftBundle, rest: sendDraftIdRest } = resolveAccountForDraftId(requireString(args, "draftId"));
4403
4450
  const draft = await sendDraftBundle.draftStore.getDraft(sendDraftIdRest);
4451
+ // Found live (external review): sendDraftBundle above is resolved from
4452
+ // the draftId's own account-slug prefix — i.e. where the draft is
4453
+ // STORED — which never changes just because update_draft later set a
4454
+ // different draft.from. Repro: create_draft on primary, then
4455
+ // update_draft(from: secondary@example.com), then send_draft — the
4456
+ // payload correctly carries "secondary@example.com", but it went out
4457
+ // through PRIMARY's live SMTP connection with that as a header
4458
+ // override, which a Split-Addresses config (separate login per
4459
+ // address) can reject outright instead of accepting as a plain alias.
4460
+ // sendBundleForDraft is resolved separately, same distinction
4461
+ // send_email/reply_to_email already draw between "where to read/store"
4462
+ // and "which account to actually send through" — draft storage,
4463
+ // claimForSending, markSent, and remote-draft cleanup all still use
4464
+ // sendDraftBundle (the draft doesn't move), only the SMTP call itself
4465
+ // uses the resolved account.
4466
+ const sendBundleForDraft = draft.from ? accountManager.byAddress(draft.from) ?? sendDraftBundle : sendDraftBundle;
4404
4467
  // Found live: calling send_draft twice on the same draft sent it
4405
4468
  // twice — nothing here checked whether it was already marked
4406
4469
  // "sent" before sending again. Refuse; create_draft for a new
@@ -4468,7 +4531,7 @@ export function createServer(config, options = {}) {
4468
4531
  const sendStartedAt = Date.now();
4469
4532
  let result;
4470
4533
  try {
4471
- result = await sendDraftBundle.smtpService.sendEmail({
4534
+ result = await sendBundleForDraft.smtpService.sendEmail({
4472
4535
  to: draft.to,
4473
4536
  cc: draft.cc,
4474
4537
  bcc: draft.bcc,
@@ -4540,15 +4603,27 @@ export function createServer(config, options = {}) {
4540
4603
  : undefined,
4541
4604
  };
4542
4605
  sentDraft = presentDraft(sendDraftBundle, remoteCleanup.draft);
4606
+ // Found live (external review): the send and markSent() above already
4607
+ // succeeded — this citation lookup is purely for the response's source
4608
+ // list, and used to throw uncaught if the original message had been
4609
+ // moved/deleted since the draft was created, reporting a false failure
4610
+ // for an email that had actually already been sent (and the draft
4611
+ // already marked "sent", so a caller who saw the error and retried
4612
+ // would correctly hit the already-sent guard above — but with no way to
4613
+ // know the first attempt had, in fact, worked). Best-effort, same as
4614
+ // verifySentCopy just below.
4543
4615
  const sources = [draftSource(sentDraft)];
4544
4616
  if (sentDraft.sourceEmailId) {
4545
- sources.push(emailSource(await sendDraftBundle.imapService.getEmailById(sentDraft.sourceEmailId)));
4617
+ try {
4618
+ sources.push(emailSource(await sendDraftBundle.imapService.getEmailById(sentDraft.sourceEmailId)));
4619
+ }
4620
+ catch (_) { }
4546
4621
  }
4547
4622
  let sentCopyTokenDraft = "[sent-copy:unverified]";
4548
4623
  try {
4549
4624
  const verifyMsgId = result.messageId;
4550
4625
  if (verifyMsgId) {
4551
- const scv = await verifySentCopy(sendDraftBundle.imapService, verifyMsgId);
4626
+ const scv = await verifySentCopy(sendBundleForDraft.imapService, verifyMsgId);
4552
4627
  if (scv.found)
4553
4628
  sentCopyTokenDraft = "[sent-copy:verified]";
4554
4629
  }
@@ -4570,6 +4645,13 @@ export function createServer(config, options = {}) {
4570
4645
  ensureSendAllowed(config.runtime);
4571
4646
  const { bundle: scheduleDraftBundle, rest: scheduleDraftIdRest } = resolveAccountForDraftId(requireString(args, "draftId"));
4572
4647
  const draft = await scheduleDraftBundle.draftStore.getDraft(scheduleDraftIdRest);
4648
+ // Same fix as send_draft's identical sendBundleForDraft — see its
4649
+ // comment. The delivery queue fires via whichever account's own
4650
+ // deliveryQueueService it was enqueued into, so this must be the
4651
+ // account draft.from actually names, not always the draft's storage
4652
+ // account — used consistently below for both the duplicate-schedule
4653
+ // check and the enqueue call itself, so both look at the same queue.
4654
+ const sendBundleForScheduleDraft = draft.from ? accountManager.byAddress(draft.from) ?? scheduleDraftBundle : scheduleDraftBundle;
4573
4655
  // Mirrors send_draft's own pendingScheduled guard, for the reverse
4574
4656
  // ordering: send_draft already delivered this draft once, and
4575
4657
  // scheduling it again here would deliver it a second time at
@@ -4594,9 +4676,9 @@ export function createServer(config, options = {}) {
4594
4676
  // duplicate. This external check is kept only as an optional
4595
4677
  // early-exit optimization to avoid a wasted round-trip to the lock
4596
4678
  // in the common non-racing case.
4597
- const alreadyPendingScheduled = (await scheduleDraftBundle.deliveryQueueService.list()).find((record) => record.sourceDraftId === draft.id && record.status === "pending");
4679
+ const alreadyPendingScheduled = (await sendBundleForScheduleDraft.deliveryQueueService.list()).find((record) => record.sourceDraftId === draft.id && record.status === "pending");
4598
4680
  if (alreadyPendingScheduled) {
4599
- throw new McpError(ErrorCode.InvalidParams, `This draft already has a pending scheduled send (id ${withAccountPrefix(responseSlug(scheduleDraftBundle), alreadyPendingScheduled.id)}, sendAt ${alreadyPendingScheduled.sendAt}). Scheduling it again would deliver it twice. Cancel the existing one with cancel_send first if you want a different sendAt.`);
4681
+ throw new McpError(ErrorCode.InvalidParams, `This draft already has a pending scheduled send (id ${withAccountPrefix(responseSlug(sendBundleForScheduleDraft), alreadyPendingScheduled.id)}, sendAt ${alreadyPendingScheduled.sendAt}). Scheduling it again would deliver it twice. Cancel the existing one with cancel_send first if you want a different sendAt.`);
4600
4682
  }
4601
4683
  const sendAt = requireString(args, "sendAt");
4602
4684
  const sendAtTime = new Date(sendAt).getTime();
@@ -4620,7 +4702,7 @@ export function createServer(config, options = {}) {
4620
4702
  if (external.length > 0)
4621
4703
  throw new McpError(ErrorCode.InvalidParams, "PROTONMAIL_RESTRICT_OUTBOUND_TO_SELF is enabled. All recipients must be the authenticated user.");
4622
4704
  }
4623
- const queued = await withAudit(auditService, name, args, async () => scheduleDraftBundle.deliveryQueueService.enqueue({
4705
+ const queued = await withAudit(auditService, name, args, async () => sendBundleForScheduleDraft.deliveryQueueService.enqueue({
4624
4706
  to: draft.to,
4625
4707
  cc: draft.cc,
4626
4708
  bcc: draft.bcc,
@@ -4639,7 +4721,7 @@ export function createServer(config, options = {}) {
4639
4721
  }, new Date(sendAtTime).toISOString(), "scheduled_send", draft.id));
4640
4722
  return createTextResult({
4641
4723
  queued: true,
4642
- id: withAccountPrefix(responseSlug(scheduleDraftBundle), queued.id),
4724
+ id: withAccountPrefix(responseSlug(sendBundleForScheduleDraft), queued.id),
4643
4725
  draftId: withAccountPrefix(responseSlug(scheduleDraftBundle), draft.id),
4644
4726
  sendAt: queued.sendAt,
4645
4727
  note: "The draft's content was snapshotted now and queued. This server must stay running for the send to fire at sendAt — if restarted first, it fires on next startup instead. The draft record itself is not automatically marked sent; use cancel_send with this id to abort before it fires.",
@@ -4770,10 +4852,20 @@ export function createServer(config, options = {}) {
4770
4852
  }
4771
4853
  const preferHtml = normalizeBoolean(args.preferHtml, false);
4772
4854
  const maxBodyLength = normalizeLimit(args.maxBodyLength, undefined, 1, 500_000);
4855
+ // Found live (external review): every id went straight to the primary
4856
+ // account's imapService regardless of its own "<slug>::" prefix, so a
4857
+ // non-primary account's id in the list either resolved the wrong
4858
+ // message on primary or (more often) just failed to be found — the
4859
+ // list [primaryId, secondary::id] reported succeeded:1 instead of 2.
4860
+ // Each id is now resolved to its own account first, same as every
4861
+ // other emailId-taking tool, and the returned id keeps its prefix.
4773
4862
  const results = await Promise.all(emailIds.map(async (emailId) => {
4774
4863
  try {
4775
- const detail = await imapService.getEmailById(emailId);
4776
- return { emailId, ok: true, email: formatEmailDetailOutput(detail, preferHtml, maxBodyLength) };
4864
+ const { bundle, rest } = resolveAccountForEmailId(emailId);
4865
+ const detail = await bundle.imapService.getEmailById(rest);
4866
+ const output = formatEmailDetailOutput(detail, preferHtml, maxBodyLength);
4867
+ output.id = prefixedIdFor(bundle, detail.id);
4868
+ return { emailId, ok: true, email: output };
4777
4869
  }
4778
4870
  catch (error) {
4779
4871
  return { emailId, ok: false, error: error instanceof Error ? error.message : String(error) };