proton-mail-bridge-client 2.1.7 → 2.1.8
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 +2 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -22
- package/dist/index.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 +37 -2
- package/dist/services/smtp-service.js.map +1 -1
- package/package.json +1 -1
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
|
-
- `
|
|
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
|
@@ -12,6 +12,7 @@ import type { BulkOperationResult, DraftRecord, EmailDetail, ProtonMailConfig, S
|
|
|
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>;
|
|
15
|
+
export declare function optionalClearableString(args: Record<string, unknown>, key: string): string | undefined;
|
|
15
16
|
export declare function headerString(headers: Record<string, unknown> | undefined, name: string): string | undefined;
|
|
16
17
|
export declare function parseAuthenticationResults(value?: string): {
|
|
17
18
|
dkim?: string;
|
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,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,EAChB,cAAc,EAGf,MAAM,kBAAkB,CAAC;AAmB1B,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAsrDnF,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;
|
|
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,EAChB,cAAc,EAGf,MAAM,kBAAkB,CAAC;AAmB1B,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAsrDnF,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;AAiCD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKtG;AA+CD,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;AAoBD,wBAAgB,4BAA4B,CAAC,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,CAAC,KAUhF;AAQD,wBAAgB,4BAA4B,CAAC,CAAC,SAAS;IAAE,OAAO,EAAE,cAAc,CAAA;CAAE,EAAE,MAAM,EAAE,CAAC,KAmB5F;AA8WD,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0zIP;AAED,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAmE1C"}
|
package/dist/index.js
CHANGED
|
@@ -1841,6 +1841,23 @@ function optionalString(args, key) {
|
|
|
1841
1841
|
const value = args[key];
|
|
1842
1842
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1843
1843
|
}
|
|
1844
|
+
// Found live (external review): update_draft's body:'' and replyTo:'' both
|
|
1845
|
+
// returned success but left the old values in place — optionalString collapses
|
|
1846
|
+
// an explicitly-empty string to undefined the same as an omitted field
|
|
1847
|
+
// entirely, and DraftStoreService.updateDraft's patch treats undefined as
|
|
1848
|
+
// "don't touch this field" (`patch.body ?? existing.body`). So there was no
|
|
1849
|
+
// way to actually clear either field once set — only used for update_draft's
|
|
1850
|
+
// body/replyTo, which are the two fields the review confirmed this breaks
|
|
1851
|
+
// (cc/bcc/attachments already have their own clear path via
|
|
1852
|
+
// `args.x === undefined ? undefined : ...`, checked separately). Not applied
|
|
1853
|
+
// to optionalString itself, since most of its many other callers legitimately
|
|
1854
|
+
// want "" treated the same as "not provided".
|
|
1855
|
+
export function optionalClearableString(args, key) {
|
|
1856
|
+
if (!(key in args) || args[key] === undefined) {
|
|
1857
|
+
return undefined;
|
|
1858
|
+
}
|
|
1859
|
+
return typeof args[key] === "string" ? args[key].trim() : undefined;
|
|
1860
|
+
}
|
|
1844
1861
|
// Shared by get_email_by_id and get_emails_by_ids: applies body preference/
|
|
1845
1862
|
// truncation and strips the full raw header map by default (needless token
|
|
1846
1863
|
// bloat — get_email_by_id re-adds it when showHeaders is explicitly true).
|
|
@@ -2489,7 +2506,7 @@ async function syncDraftToRemote(draftStore, smtpService, imapService, draft) {
|
|
|
2489
2506
|
// signature into the draft body themselves, got it appended a second time
|
|
2490
2507
|
// on every remote sync.
|
|
2491
2508
|
appendSignature: false,
|
|
2492
|
-
});
|
|
2509
|
+
}, /* preserveBcc */ true);
|
|
2493
2510
|
const remoteDraft = await imapService.upsertRemoteDraft({
|
|
2494
2511
|
raw,
|
|
2495
2512
|
messageId: draft.draftMessageId,
|
|
@@ -4246,10 +4263,24 @@ export function createServer(config, options = {}) {
|
|
|
4246
4263
|
]);
|
|
4247
4264
|
}
|
|
4248
4265
|
case "list_drafts": {
|
|
4249
|
-
//
|
|
4250
|
-
// drafts only — a
|
|
4251
|
-
//
|
|
4252
|
-
|
|
4266
|
+
// Found live (external review): this stayed scoped to the primary
|
|
4267
|
+
// account's own drafts only — a draft created via create_draft(from:
|
|
4268
|
+
// secondary@example.com) is real and gettable by its prefixed draftId,
|
|
4269
|
+
// but never showed up here, so day-to-day draft discovery silently
|
|
4270
|
+
// missed every non-primary account's drafts. Now fans out across every
|
|
4271
|
+
// configured account, same pattern as list_scheduled_sends/
|
|
4272
|
+
// list_snoozed, and re-sorts the merged set by updatedAt (each
|
|
4273
|
+
// account's own list already arrives that way). Single-account guard
|
|
4274
|
+
// keeps byte-identical output for the common case.
|
|
4275
|
+
const includeSent = normalizeBoolean(args.includeSent, false);
|
|
4276
|
+
const allDrafts = accountManager.all().length === 1
|
|
4277
|
+
? (await draftStore.listDrafts(includeSent)).map((draft) => ({ bundle: primaryBundle, draft }))
|
|
4278
|
+
: (await Promise.all(accountManager.all().map(async (bundle) => {
|
|
4279
|
+
const drafts = await bundle.draftStore.listDrafts(includeSent);
|
|
4280
|
+
return drafts.map((draft) => ({ bundle, draft: presentDraft(bundle, draft) }));
|
|
4281
|
+
})))
|
|
4282
|
+
.flat()
|
|
4283
|
+
.sort((left, right) => right.draft.updatedAt.localeCompare(left.draft.updatedAt));
|
|
4253
4284
|
// Found live (external review): every draft's full body was returned
|
|
4254
4285
|
// alongside the already-redacted attachments — confirmed live at ~261k
|
|
4255
4286
|
// tokens for 107 drafts. Bodies are now truncated the same way a
|
|
@@ -4258,15 +4289,15 @@ export function createServer(config, options = {}) {
|
|
|
4258
4289
|
// draft (just with truncated bodies/attachments), so this doesn't change
|
|
4259
4290
|
// behavior for a caller not using the new params.
|
|
4260
4291
|
const offset = normalizeLimit(args.offset, 0, 0, 10_000);
|
|
4261
|
-
const limit = typeof args.limit === "number" ? normalizeLimit(args.limit,
|
|
4262
|
-
const page = limit !== undefined ?
|
|
4292
|
+
const limit = typeof args.limit === "number" ? normalizeLimit(args.limit, allDrafts.length, 1, 10_000) : undefined;
|
|
4293
|
+
const page = limit !== undefined ? allDrafts.slice(offset, offset + limit) : allDrafts.slice(offset);
|
|
4263
4294
|
return createTextResult({
|
|
4264
|
-
total:
|
|
4295
|
+
total: allDrafts.length,
|
|
4265
4296
|
offset,
|
|
4266
4297
|
returned: page.length,
|
|
4267
|
-
hasMore: offset + page.length <
|
|
4268
|
-
drafts: page.map((draft) => truncateDraftBodyForResponse(redactDraftAttachmentsForListing(draft))),
|
|
4269
|
-
}, false, page.map(draftSource));
|
|
4298
|
+
hasMore: offset + page.length < allDrafts.length,
|
|
4299
|
+
drafts: page.map(({ draft }) => truncateDraftBodyForResponse(redactDraftAttachmentsForListing(draft))),
|
|
4300
|
+
}, false, page.map(({ draft }) => draftSource(draft)));
|
|
4270
4301
|
}
|
|
4271
4302
|
case "list_remote_drafts": {
|
|
4272
4303
|
// Scope limitation (follow-up round): stays scoped to the primary account's
|
|
@@ -4304,7 +4335,11 @@ export function createServer(config, options = {}) {
|
|
|
4304
4335
|
const to = args.to === undefined ? undefined : parseEmails(optionalString(args, "to"));
|
|
4305
4336
|
const cc = args.cc === undefined ? undefined : parseEmails(optionalString(args, "cc"));
|
|
4306
4337
|
const bcc = args.bcc === undefined ? undefined : parseEmails(optionalString(args, "bcc"));
|
|
4307
|
-
|
|
4338
|
+
// body/replyTo use optionalClearableString, not optionalString — see its
|
|
4339
|
+
// comment — so an explicit body:'' or replyTo:'' actually clears the
|
|
4340
|
+
// field instead of being silently treated as "field not provided".
|
|
4341
|
+
const body = optionalClearableString(args, "body");
|
|
4342
|
+
const replyTo = optionalClearableString(args, "replyTo");
|
|
4308
4343
|
const from = optionalString(args, "from");
|
|
4309
4344
|
const priority = optionalString(args, "priority");
|
|
4310
4345
|
const attachments = args.attachments === undefined ? undefined : optionalAttachmentList(args.attachments);
|
|
@@ -4329,7 +4364,7 @@ export function createServer(config, options = {}) {
|
|
|
4329
4364
|
cc,
|
|
4330
4365
|
bcc,
|
|
4331
4366
|
subject: optionalString(args, "subject"),
|
|
4332
|
-
body
|
|
4367
|
+
body,
|
|
4333
4368
|
isHtml: typeof args.isHtml === "boolean" ? args.isHtml : undefined,
|
|
4334
4369
|
priority: priority === "high" || priority === "low" || priority === "normal"
|
|
4335
4370
|
? priority
|
|
@@ -4401,6 +4436,22 @@ export function createServer(config, options = {}) {
|
|
|
4401
4436
|
ensureSendAllowed(config.runtime);
|
|
4402
4437
|
const { bundle: sendDraftBundle, rest: sendDraftIdRest } = resolveAccountForDraftId(requireString(args, "draftId"));
|
|
4403
4438
|
const draft = await sendDraftBundle.draftStore.getDraft(sendDraftIdRest);
|
|
4439
|
+
// Found live (external review): sendDraftBundle above is resolved from
|
|
4440
|
+
// the draftId's own account-slug prefix — i.e. where the draft is
|
|
4441
|
+
// STORED — which never changes just because update_draft later set a
|
|
4442
|
+
// different draft.from. Repro: create_draft on primary, then
|
|
4443
|
+
// update_draft(from: secondary@example.com), then send_draft — the
|
|
4444
|
+
// payload correctly carries "secondary@example.com", but it went out
|
|
4445
|
+
// through PRIMARY's live SMTP connection with that as a header
|
|
4446
|
+
// override, which a Split-Addresses config (separate login per
|
|
4447
|
+
// address) can reject outright instead of accepting as a plain alias.
|
|
4448
|
+
// sendBundleForDraft is resolved separately, same distinction
|
|
4449
|
+
// send_email/reply_to_email already draw between "where to read/store"
|
|
4450
|
+
// and "which account to actually send through" — draft storage,
|
|
4451
|
+
// claimForSending, markSent, and remote-draft cleanup all still use
|
|
4452
|
+
// sendDraftBundle (the draft doesn't move), only the SMTP call itself
|
|
4453
|
+
// uses the resolved account.
|
|
4454
|
+
const sendBundleForDraft = draft.from ? accountManager.byAddress(draft.from) ?? sendDraftBundle : sendDraftBundle;
|
|
4404
4455
|
// Found live: calling send_draft twice on the same draft sent it
|
|
4405
4456
|
// twice — nothing here checked whether it was already marked
|
|
4406
4457
|
// "sent" before sending again. Refuse; create_draft for a new
|
|
@@ -4468,7 +4519,7 @@ export function createServer(config, options = {}) {
|
|
|
4468
4519
|
const sendStartedAt = Date.now();
|
|
4469
4520
|
let result;
|
|
4470
4521
|
try {
|
|
4471
|
-
result = await
|
|
4522
|
+
result = await sendBundleForDraft.smtpService.sendEmail({
|
|
4472
4523
|
to: draft.to,
|
|
4473
4524
|
cc: draft.cc,
|
|
4474
4525
|
bcc: draft.bcc,
|
|
@@ -4540,15 +4591,27 @@ export function createServer(config, options = {}) {
|
|
|
4540
4591
|
: undefined,
|
|
4541
4592
|
};
|
|
4542
4593
|
sentDraft = presentDraft(sendDraftBundle, remoteCleanup.draft);
|
|
4594
|
+
// Found live (external review): the send and markSent() above already
|
|
4595
|
+
// succeeded — this citation lookup is purely for the response's source
|
|
4596
|
+
// list, and used to throw uncaught if the original message had been
|
|
4597
|
+
// moved/deleted since the draft was created, reporting a false failure
|
|
4598
|
+
// for an email that had actually already been sent (and the draft
|
|
4599
|
+
// already marked "sent", so a caller who saw the error and retried
|
|
4600
|
+
// would correctly hit the already-sent guard above — but with no way to
|
|
4601
|
+
// know the first attempt had, in fact, worked). Best-effort, same as
|
|
4602
|
+
// verifySentCopy just below.
|
|
4543
4603
|
const sources = [draftSource(sentDraft)];
|
|
4544
4604
|
if (sentDraft.sourceEmailId) {
|
|
4545
|
-
|
|
4605
|
+
try {
|
|
4606
|
+
sources.push(emailSource(await sendDraftBundle.imapService.getEmailById(sentDraft.sourceEmailId)));
|
|
4607
|
+
}
|
|
4608
|
+
catch (_) { }
|
|
4546
4609
|
}
|
|
4547
4610
|
let sentCopyTokenDraft = "[sent-copy:unverified]";
|
|
4548
4611
|
try {
|
|
4549
4612
|
const verifyMsgId = result.messageId;
|
|
4550
4613
|
if (verifyMsgId) {
|
|
4551
|
-
const scv = await verifySentCopy(
|
|
4614
|
+
const scv = await verifySentCopy(sendBundleForDraft.imapService, verifyMsgId);
|
|
4552
4615
|
if (scv.found)
|
|
4553
4616
|
sentCopyTokenDraft = "[sent-copy:verified]";
|
|
4554
4617
|
}
|
|
@@ -4570,6 +4633,13 @@ export function createServer(config, options = {}) {
|
|
|
4570
4633
|
ensureSendAllowed(config.runtime);
|
|
4571
4634
|
const { bundle: scheduleDraftBundle, rest: scheduleDraftIdRest } = resolveAccountForDraftId(requireString(args, "draftId"));
|
|
4572
4635
|
const draft = await scheduleDraftBundle.draftStore.getDraft(scheduleDraftIdRest);
|
|
4636
|
+
// Same fix as send_draft's identical sendBundleForDraft — see its
|
|
4637
|
+
// comment. The delivery queue fires via whichever account's own
|
|
4638
|
+
// deliveryQueueService it was enqueued into, so this must be the
|
|
4639
|
+
// account draft.from actually names, not always the draft's storage
|
|
4640
|
+
// account — used consistently below for both the duplicate-schedule
|
|
4641
|
+
// check and the enqueue call itself, so both look at the same queue.
|
|
4642
|
+
const sendBundleForScheduleDraft = draft.from ? accountManager.byAddress(draft.from) ?? scheduleDraftBundle : scheduleDraftBundle;
|
|
4573
4643
|
// Mirrors send_draft's own pendingScheduled guard, for the reverse
|
|
4574
4644
|
// ordering: send_draft already delivered this draft once, and
|
|
4575
4645
|
// scheduling it again here would deliver it a second time at
|
|
@@ -4594,9 +4664,9 @@ export function createServer(config, options = {}) {
|
|
|
4594
4664
|
// duplicate. This external check is kept only as an optional
|
|
4595
4665
|
// early-exit optimization to avoid a wasted round-trip to the lock
|
|
4596
4666
|
// in the common non-racing case.
|
|
4597
|
-
const alreadyPendingScheduled = (await
|
|
4667
|
+
const alreadyPendingScheduled = (await sendBundleForScheduleDraft.deliveryQueueService.list()).find((record) => record.sourceDraftId === draft.id && record.status === "pending");
|
|
4598
4668
|
if (alreadyPendingScheduled) {
|
|
4599
|
-
throw new McpError(ErrorCode.InvalidParams, `This draft already has a pending scheduled send (id ${withAccountPrefix(responseSlug(
|
|
4669
|
+
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
4670
|
}
|
|
4601
4671
|
const sendAt = requireString(args, "sendAt");
|
|
4602
4672
|
const sendAtTime = new Date(sendAt).getTime();
|
|
@@ -4620,7 +4690,7 @@ export function createServer(config, options = {}) {
|
|
|
4620
4690
|
if (external.length > 0)
|
|
4621
4691
|
throw new McpError(ErrorCode.InvalidParams, "PROTONMAIL_RESTRICT_OUTBOUND_TO_SELF is enabled. All recipients must be the authenticated user.");
|
|
4622
4692
|
}
|
|
4623
|
-
const queued = await withAudit(auditService, name, args, async () =>
|
|
4693
|
+
const queued = await withAudit(auditService, name, args, async () => sendBundleForScheduleDraft.deliveryQueueService.enqueue({
|
|
4624
4694
|
to: draft.to,
|
|
4625
4695
|
cc: draft.cc,
|
|
4626
4696
|
bcc: draft.bcc,
|
|
@@ -4639,7 +4709,7 @@ export function createServer(config, options = {}) {
|
|
|
4639
4709
|
}, new Date(sendAtTime).toISOString(), "scheduled_send", draft.id));
|
|
4640
4710
|
return createTextResult({
|
|
4641
4711
|
queued: true,
|
|
4642
|
-
id: withAccountPrefix(responseSlug(
|
|
4712
|
+
id: withAccountPrefix(responseSlug(sendBundleForScheduleDraft), queued.id),
|
|
4643
4713
|
draftId: withAccountPrefix(responseSlug(scheduleDraftBundle), draft.id),
|
|
4644
4714
|
sendAt: queued.sendAt,
|
|
4645
4715
|
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 +4840,20 @@ export function createServer(config, options = {}) {
|
|
|
4770
4840
|
}
|
|
4771
4841
|
const preferHtml = normalizeBoolean(args.preferHtml, false);
|
|
4772
4842
|
const maxBodyLength = normalizeLimit(args.maxBodyLength, undefined, 1, 500_000);
|
|
4843
|
+
// Found live (external review): every id went straight to the primary
|
|
4844
|
+
// account's imapService regardless of its own "<slug>::" prefix, so a
|
|
4845
|
+
// non-primary account's id in the list either resolved the wrong
|
|
4846
|
+
// message on primary or (more often) just failed to be found — the
|
|
4847
|
+
// list [primaryId, secondary::id] reported succeeded:1 instead of 2.
|
|
4848
|
+
// Each id is now resolved to its own account first, same as every
|
|
4849
|
+
// other emailId-taking tool, and the returned id keeps its prefix.
|
|
4773
4850
|
const results = await Promise.all(emailIds.map(async (emailId) => {
|
|
4774
4851
|
try {
|
|
4775
|
-
const
|
|
4776
|
-
|
|
4852
|
+
const { bundle, rest } = resolveAccountForEmailId(emailId);
|
|
4853
|
+
const detail = await bundle.imapService.getEmailById(rest);
|
|
4854
|
+
const output = formatEmailDetailOutput(detail, preferHtml, maxBodyLength);
|
|
4855
|
+
output.id = prefixedIdFor(bundle, detail.id);
|
|
4856
|
+
return { emailId, ok: true, email: output };
|
|
4777
4857
|
}
|
|
4778
4858
|
catch (error) {
|
|
4779
4859
|
return { emailId, ok: false, error: error instanceof Error ? error.message : String(error) };
|