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 +2 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +122 -30
- 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
|
@@ -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;
|
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,
|
|
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,EACnB,cAAc,EACd,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;AAuCH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,GAAG,CAAC,CAEtE;AAmED,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
|
@@ -1740,14 +1740,26 @@ function normalizeStructuredContent(value) {
|
|
|
1740
1740
|
}
|
|
1741
1741
|
return normalized;
|
|
1742
1742
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
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
|
-
//
|
|
4250
|
-
// drafts only — a
|
|
4251
|
-
//
|
|
4252
|
-
|
|
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,
|
|
4262
|
-
const page = limit !== undefined ?
|
|
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:
|
|
4307
|
+
total: allDrafts.length,
|
|
4265
4308
|
offset,
|
|
4266
4309
|
returned: page.length,
|
|
4267
|
-
hasMore: offset + page.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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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(
|
|
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 () =>
|
|
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(
|
|
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
|
|
4776
|
-
|
|
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) };
|