proton-mail-bridge-client 2.0.2 → 2.0.3
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.map +1 -1
- package/dist/index.js +158 -43
- package/dist/index.js.map +1 -1
- package/dist/services/audit-service.d.ts +2 -0
- package/dist/services/audit-service.d.ts.map +1 -1
- package/dist/services/audit-service.js +16 -0
- package/dist/services/audit-service.js.map +1 -1
- package/dist/services/delivery-queue-service.d.ts.map +1 -1
- package/dist/services/delivery-queue-service.js +61 -28
- package/dist/services/delivery-queue-service.js.map +1 -1
- package/dist/services/local-index-service.d.ts +1 -0
- package/dist/services/local-index-service.d.ts.map +1 -1
- package/dist/services/local-index-service.js +38 -7
- package/dist/services/local-index-service.js.map +1 -1
- package/dist/services/simple-imap-service.d.ts +7 -5
- package/dist/services/simple-imap-service.d.ts.map +1 -1
- package/dist/services/simple-imap-service.js +242 -57
- package/dist/services/simple-imap-service.js.map +1 -1
- package/dist/utils/account-identity.d.ts.map +1 -1
- package/dist/utils/account-identity.js +49 -22
- package/dist/utils/account-identity.js.map +1 -1
- package/dist/utils/helpers.d.ts +2 -1
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/helpers.js +65 -9
- package/dist/utils/helpers.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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,EAAkB,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE7D,OAAO,KAAK,EAUV,WAAW,EAEX,gBAAgB,EAEjB,MAAM,kBAAkB,CAAC;AA2qD1B,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;AA4KD,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;AAwzBD,wBAAgB,kBAAkB,IAAI,gBAAgB,CAkGrD;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,gBAAgB,EACxB,OAAO,GAAE;IACP,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC1B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmhGP;AAED,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAmE1C"}
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { BackgroundSyncService } from "./services/background-sync-service.js";
|
|
|
16
16
|
import { DeliveryQueueService } from "./services/delivery-queue-service.js";
|
|
17
17
|
import { DraftStoreService } from "./services/draft-store-service.js";
|
|
18
18
|
import { LocalIndexService } from "./services/local-index-service.js";
|
|
19
|
-
import { BULK_ITEM_TIMEOUT_MS, describeImapError, isLikelyAuthenticationError, isLikelyConnectionError, isLikelyTlsMismatchError, SimpleIMAPService } from "./services/simple-imap-service.js";
|
|
19
|
+
import { BULK_ITEM_TIMEOUT_MS, describeImapError, isLikelyAuthenticationError, isLikelyConnectionError, isLikelyTlsMismatchError, SimpleIMAPService, UID_VALIDITY_MISMATCH_ERROR } from "./services/simple-imap-service.js";
|
|
20
20
|
import { applySignature, SMTPService } from "./services/smtp-service.js";
|
|
21
21
|
import { SnoozeService } from "./services/snooze-service.js";
|
|
22
22
|
import { TemplateService } from "./services/template-service.js";
|
|
@@ -2370,27 +2370,32 @@ async function getAnalyticsSampleFromIndex(imapService, localIndexService, days,
|
|
|
2370
2370
|
return result.emails;
|
|
2371
2371
|
}
|
|
2372
2372
|
async function runEmailAction(imapService, emailId, action, targetFolder) {
|
|
2373
|
+
// undefined for an old-format id (no embedded UIDVALIDITY) — every method
|
|
2374
|
+
// below's own assertMailboxUidValidity guard already treats that as
|
|
2375
|
+
// "unverifiable, don't block on it", same as every direct single-message
|
|
2376
|
+
// tool handler.
|
|
2377
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
2373
2378
|
switch (action) {
|
|
2374
2379
|
case "mark_read":
|
|
2375
|
-
return imapService.markEmailRead(emailId, true);
|
|
2380
|
+
return imapService.markEmailRead(emailId, true, uidValidity);
|
|
2376
2381
|
case "mark_unread":
|
|
2377
|
-
return imapService.markEmailRead(emailId, false);
|
|
2382
|
+
return imapService.markEmailRead(emailId, false, uidValidity);
|
|
2378
2383
|
case "star":
|
|
2379
|
-
return imapService.starEmail(emailId, true);
|
|
2384
|
+
return imapService.starEmail(emailId, true, uidValidity);
|
|
2380
2385
|
case "unstar":
|
|
2381
|
-
return imapService.starEmail(emailId, false);
|
|
2386
|
+
return imapService.starEmail(emailId, false, uidValidity);
|
|
2382
2387
|
case "archive":
|
|
2383
|
-
return imapService.archiveEmail(emailId);
|
|
2388
|
+
return imapService.archiveEmail(emailId, uidValidity);
|
|
2384
2389
|
case "trash":
|
|
2385
|
-
return imapService.trashEmail(emailId);
|
|
2390
|
+
return imapService.trashEmail(emailId, uidValidity);
|
|
2386
2391
|
case "restore":
|
|
2387
|
-
return imapService.restoreEmail(emailId, targetFolder);
|
|
2392
|
+
return imapService.restoreEmail(emailId, targetFolder, uidValidity);
|
|
2388
2393
|
case "move":
|
|
2389
2394
|
if (!targetFolder)
|
|
2390
2395
|
throw new McpError(ErrorCode.InvalidParams, "targetFolder is required for action 'move'.");
|
|
2391
|
-
return imapService.moveEmail(emailId, targetFolder);
|
|
2396
|
+
return imapService.moveEmail(emailId, targetFolder, uidValidity);
|
|
2392
2397
|
case "delete":
|
|
2393
|
-
return imapService.deleteEmail(emailId);
|
|
2398
|
+
return imapService.deleteEmail(emailId, uidValidity);
|
|
2394
2399
|
}
|
|
2395
2400
|
}
|
|
2396
2401
|
function emailSourceFromActionResult(result) {
|
|
@@ -2501,11 +2506,19 @@ function ensureBulkBatchSize(uidsLength, max) {
|
|
|
2501
2506
|
throw new McpError(ErrorCode.InvalidParams, "Batch size " + uidsLength + " exceeds limit " + max + ". Use match criteria to narrow the scope, or increase maxBatchSize.");
|
|
2502
2507
|
}
|
|
2503
2508
|
}
|
|
2504
|
-
|
|
2509
|
+
// `currentUidValidity` is optional and, when omitted, preserves the exact
|
|
2510
|
+
// original behavior (folder/format check only) — every caller that already
|
|
2511
|
+
// knows the folder's current UIDVALIDITY (the bulk_delete/bulk_update_flags/
|
|
2512
|
+
// bulk_update_labels handlers below, via a single shared getMailboxUidValidity
|
|
2513
|
+
// call reused for both this and resolveUidsForBulkOp) should pass it, so a
|
|
2514
|
+
// stale-generation id gets its own clear error instead of being lumped in
|
|
2515
|
+
// with a genuinely malformed one.
|
|
2516
|
+
function getBulkNotFoundEmailIds(emailIds, folder, currentUidValidity) {
|
|
2505
2517
|
if (!emailIds) {
|
|
2506
2518
|
return [];
|
|
2507
2519
|
}
|
|
2508
|
-
|
|
2520
|
+
const excluded = [];
|
|
2521
|
+
for (const emailId of emailIds) {
|
|
2509
2522
|
// Found live: this compared the emailId's folder segment against
|
|
2510
2523
|
// `folder` as raw, still-percent-encoded text — createEmailId encodes
|
|
2511
2524
|
// it (Folders/MCP-Snoozed -> Folders%2FMCP-Snoozed) but `folder` here
|
|
@@ -2516,30 +2529,50 @@ function getBulkNotFoundEmailIds(emailIds, folder) {
|
|
|
2516
2529
|
// reported genuinely valid ids as notFound. parseEmailId already does
|
|
2517
2530
|
// the matching decodeURIComponent correctly — reuse it instead of
|
|
2518
2531
|
// hand-rolling the same parse a second, inconsistent way.
|
|
2532
|
+
let parsed;
|
|
2519
2533
|
try {
|
|
2520
|
-
|
|
2521
|
-
return parsed.folder !== folder;
|
|
2534
|
+
parsed = parseEmailId(emailId);
|
|
2522
2535
|
}
|
|
2523
2536
|
catch {
|
|
2524
|
-
|
|
2537
|
+
excluded.push({ emailId, error: "Email ID could not be resolved to a UID." });
|
|
2538
|
+
continue;
|
|
2525
2539
|
}
|
|
2526
|
-
|
|
2540
|
+
if (parsed.folder !== folder) {
|
|
2541
|
+
excluded.push({ emailId, error: "Email ID could not be resolved to a UID." });
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
// An id with no embedded uidValidity (pre-this-fix format) is
|
|
2545
|
+
// unverifiable, not stale — same "can't check it, so don't block on
|
|
2546
|
+
// it" stance assertMailboxUidValidity already takes for the
|
|
2547
|
+
// single-message mutation path. Only a *checkable* mismatch is
|
|
2548
|
+
// treated as a stale reference. resolveUidsForBulkOp applies this
|
|
2549
|
+
// exact same exclusion (see its own comment) against the exact same
|
|
2550
|
+
// currentUidValidity value, so the uid this method silently drops from
|
|
2551
|
+
// execution and the id this method reports as excluded here always
|
|
2552
|
+
// agree.
|
|
2553
|
+
if (currentUidValidity !== undefined &&
|
|
2554
|
+
parsed.uidValidity !== undefined &&
|
|
2555
|
+
parsed.uidValidity !== currentUidValidity) {
|
|
2556
|
+
excluded.push({ emailId, error: UID_VALIDITY_MISMATCH_ERROR });
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
return excluded;
|
|
2527
2560
|
}
|
|
2528
|
-
function withBulkNotFound(result,
|
|
2529
|
-
if (
|
|
2561
|
+
function withBulkNotFound(result, excludedEmailIds) {
|
|
2562
|
+
if (excludedEmailIds.length === 0) {
|
|
2530
2563
|
return result;
|
|
2531
2564
|
}
|
|
2532
2565
|
return {
|
|
2533
2566
|
...result,
|
|
2534
|
-
total: result.total +
|
|
2535
|
-
notFound: result.notFound +
|
|
2567
|
+
total: result.total + excludedEmailIds.length,
|
|
2568
|
+
notFound: result.notFound + excludedEmailIds.length,
|
|
2536
2569
|
results: [
|
|
2537
2570
|
...result.results,
|
|
2538
|
-
...
|
|
2571
|
+
...excludedEmailIds.map(({ emailId, error }) => ({
|
|
2539
2572
|
uid: 0,
|
|
2540
2573
|
emailId,
|
|
2541
2574
|
ok: false,
|
|
2542
|
-
error
|
|
2575
|
+
error,
|
|
2543
2576
|
})),
|
|
2544
2577
|
],
|
|
2545
2578
|
};
|
|
@@ -3689,9 +3722,20 @@ export function createServer(config, options = {}) {
|
|
|
3689
3722
|
// lock and flips "draft" -> "sending" in that same critical
|
|
3690
3723
|
// section — only the call that wins the claim proceeds to SMTP.
|
|
3691
3724
|
await draftStore.claimForSending(draft.id);
|
|
3725
|
+
// Found live: withAudit wrapped the SMTP call, so an audit-log
|
|
3726
|
+
// write failure AFTER a successful send (e.g. disk full) was
|
|
3727
|
+
// indistinguishable from a send failure — it threw out of
|
|
3728
|
+
// withAudit, and the catch below reverted an already-sent draft
|
|
3729
|
+
// back to "draft", making it resendable and enabling a genuine
|
|
3730
|
+
// duplicate delivery. SMTP is called directly here so its outcome
|
|
3731
|
+
// (and the revert-on-failure below) can't be corrupted by a
|
|
3732
|
+
// separate audit-write failure — the audit record for the success
|
|
3733
|
+
// case is written afterward, on its own, with a failure there only
|
|
3734
|
+
// logged, never reverting a send that already happened.
|
|
3735
|
+
const sendStartedAt = Date.now();
|
|
3692
3736
|
let result;
|
|
3693
3737
|
try {
|
|
3694
|
-
result = await
|
|
3738
|
+
result = await smtpService.sendEmail({
|
|
3695
3739
|
to: draft.to,
|
|
3696
3740
|
cc: draft.cc,
|
|
3697
3741
|
bcc: draft.bcc,
|
|
@@ -3708,15 +3752,41 @@ export function createServer(config, options = {}) {
|
|
|
3708
3752
|
// saw and approved. If a signature is wanted, it belongs in the
|
|
3709
3753
|
// draft body itself.
|
|
3710
3754
|
appendSignature: false,
|
|
3711
|
-
})
|
|
3755
|
+
});
|
|
3712
3756
|
}
|
|
3713
3757
|
catch (error) {
|
|
3714
3758
|
// Mirrors SnoozeService.wake()'s catch handler: revert the claim
|
|
3715
3759
|
// so the draft is retryable instead of stuck in "sending"
|
|
3716
3760
|
// forever because SMTP failed.
|
|
3717
3761
|
await draftStore.revertSending(draft.id);
|
|
3762
|
+
await auditService
|
|
3763
|
+
.record({
|
|
3764
|
+
timestamp: new Date().toISOString(),
|
|
3765
|
+
tool: name,
|
|
3766
|
+
status: "error",
|
|
3767
|
+
durationMs: Date.now() - sendStartedAt,
|
|
3768
|
+
input: sanitizeAuditValue(args),
|
|
3769
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3770
|
+
})
|
|
3771
|
+
.catch((auditError) => logger.error("Failed to record audit log for a failed send_draft", "MCPServer", {
|
|
3772
|
+
draftId: draft.id,
|
|
3773
|
+
error: auditError,
|
|
3774
|
+
}));
|
|
3718
3775
|
throw error;
|
|
3719
3776
|
}
|
|
3777
|
+
try {
|
|
3778
|
+
await auditService.record({
|
|
3779
|
+
timestamp: new Date().toISOString(),
|
|
3780
|
+
tool: name,
|
|
3781
|
+
status: "success",
|
|
3782
|
+
durationMs: Date.now() - sendStartedAt,
|
|
3783
|
+
input: sanitizeAuditValue(args),
|
|
3784
|
+
result: sanitizeAuditValue(result),
|
|
3785
|
+
});
|
|
3786
|
+
}
|
|
3787
|
+
catch (auditError) {
|
|
3788
|
+
logger.error("Failed to record audit log for a successful send_draft — the email was already sent, so the draft is not being reverted", "MCPServer", { draftId: draft.id, error: auditError });
|
|
3789
|
+
}
|
|
3720
3790
|
let sentDraft = await draftStore.markSent(draft.id, {
|
|
3721
3791
|
messageId: result.messageId,
|
|
3722
3792
|
accepted: result.accepted,
|
|
@@ -3961,7 +4031,12 @@ export function createServer(config, options = {}) {
|
|
|
3961
4031
|
if (labelsToAdd.length === 0 && labelsToRemove.length === 0) {
|
|
3962
4032
|
throw new McpError(ErrorCode.InvalidParams, "Provide at least one label in labelsToAdd or labelsToRemove.");
|
|
3963
4033
|
}
|
|
3964
|
-
|
|
4034
|
+
// undefined for an old-format id (no embedded UIDVALIDITY) —
|
|
4035
|
+
// updateMessageLabels's own assertMailboxUidValidity guard already
|
|
4036
|
+
// treats that as "unverifiable, don't block on it", same as every
|
|
4037
|
+
// other mutation handler below.
|
|
4038
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4039
|
+
const result = await withAudit(auditService, name, args, async () => imapService.updateMessageLabels(emailId, labelsToAdd, labelsToRemove, uidValidity));
|
|
3965
4040
|
return createTextResult(result, false, [emailSource({ id: emailId })]);
|
|
3966
4041
|
}
|
|
3967
4042
|
case "update_message_flags": {
|
|
@@ -3976,7 +4051,8 @@ export function createServer(config, options = {}) {
|
|
|
3976
4051
|
if (flagsToAdd.length === 0 && flagsToRemove.length === 0) {
|
|
3977
4052
|
throw new McpError(ErrorCode.InvalidParams, "Provide at least one flag in flagsToAdd or flagsToRemove.");
|
|
3978
4053
|
}
|
|
3979
|
-
const
|
|
4054
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4055
|
+
const result = await withAudit(auditService, name, args, async () => imapService.updateMessageFlags(emailId, flagsToAdd, flagsToRemove, uidValidity));
|
|
3980
4056
|
return createTextResult(result, false, [emailSource({ id: emailId })]);
|
|
3981
4057
|
}
|
|
3982
4058
|
case "count_messages": {
|
|
@@ -4039,7 +4115,12 @@ export function createServer(config, options = {}) {
|
|
|
4039
4115
|
const folder = optionalString(args, "folder") ?? "INBOX";
|
|
4040
4116
|
const targetFolder = requireString(args, "targetFolder");
|
|
4041
4117
|
const max = getBulkMaxBatchSize(args);
|
|
4042
|
-
|
|
4118
|
+
// Only fetched (an extra STATUS round trip) when there are ids to
|
|
4119
|
+
// check against it — a match-only bulk_move has no ids that could
|
|
4120
|
+
// be stale, since match resolves directly against the live
|
|
4121
|
+
// mailbox each time.
|
|
4122
|
+
const currentUidValidity = emailIds ? await imapService.getMailboxUidValidity(folder) : undefined;
|
|
4123
|
+
const notFoundEmailIds = getBulkNotFoundEmailIds(emailIds, folder, currentUidValidity);
|
|
4043
4124
|
const preview = await imapService.bulkMove({
|
|
4044
4125
|
emailIds,
|
|
4045
4126
|
match,
|
|
@@ -4077,12 +4158,22 @@ export function createServer(config, options = {}) {
|
|
|
4077
4158
|
throw new McpError(ErrorCode.InvalidParams, "Provide emailIds OR match, not both.");
|
|
4078
4159
|
const folder = optionalString(args, "folder") ?? "INBOX";
|
|
4079
4160
|
const max = getBulkMaxBatchSize(args);
|
|
4080
|
-
|
|
4161
|
+
// Fetched once, up front, and reused for both the notFound
|
|
4162
|
+
// reporting below and resolveUidsForBulkOp's own filtering — a
|
|
4163
|
+
// single shared point-in-time value, same "resolve once" spirit
|
|
4164
|
+
// as the uids resolution itself, so the ids this call reports as
|
|
4165
|
+
// excluded and the uids it actually acts on can never disagree
|
|
4166
|
+
// about which generation was current. Only fetched when there are
|
|
4167
|
+
// ids to check against it (a match-only call has none).
|
|
4168
|
+
const currentUidValidity = emailIds ? await imapService.getMailboxUidValidity(folder) : undefined;
|
|
4169
|
+
const notFoundEmailIds = getBulkNotFoundEmailIds(emailIds, folder, currentUidValidity);
|
|
4081
4170
|
// Resolve the match/emailIds set exactly once and reuse it for both
|
|
4082
4171
|
// the preview and the real run — see resolveUidsForBulkOp's
|
|
4083
4172
|
// comment for why re-resolving `match` a second time (the old
|
|
4084
4173
|
// dry-run-then-real-run pattern) could silently exceed maxBatchSize.
|
|
4085
|
-
|
|
4174
|
+
// Also excludes any id whose embedded UIDVALIDITY is stale — see
|
|
4175
|
+
// resolveUidsForBulkOp's comment.
|
|
4176
|
+
const uids = await imapService.resolveUidsForBulkOp(folder, emailIds, match, currentUidValidity);
|
|
4086
4177
|
ensureBulkBatchSize(uids.length, max);
|
|
4087
4178
|
if (normalizeBoolean(args.dryRun, false)) {
|
|
4088
4179
|
const preview = await imapService.bulkDelete({
|
|
@@ -4123,12 +4214,16 @@ export function createServer(config, options = {}) {
|
|
|
4123
4214
|
throw new McpError(ErrorCode.InvalidParams, "Provide flagsToAdd or flagsToRemove.");
|
|
4124
4215
|
const folder = optionalString(args, "folder") ?? "INBOX";
|
|
4125
4216
|
const max = getBulkMaxBatchSize(args);
|
|
4126
|
-
|
|
4217
|
+
// See the bulk_delete case above for why this is fetched once and
|
|
4218
|
+
// shared between notFound reporting and uid resolution.
|
|
4219
|
+
const currentUidValidity = emailIds ? await imapService.getMailboxUidValidity(folder) : undefined;
|
|
4220
|
+
const notFoundEmailIds = getBulkNotFoundEmailIds(emailIds, folder, currentUidValidity);
|
|
4127
4221
|
// Resolve the match/emailIds set exactly once and reuse it for both
|
|
4128
4222
|
// the preview and the real run — see resolveUidsForBulkOp's
|
|
4129
4223
|
// comment for why re-resolving `match` a second time (the old
|
|
4130
4224
|
// dry-run-then-real-run pattern) could silently exceed maxBatchSize.
|
|
4131
|
-
|
|
4225
|
+
// Also excludes any id whose embedded UIDVALIDITY is stale.
|
|
4226
|
+
const uids = await imapService.resolveUidsForBulkOp(folder, emailIds, match, currentUidValidity);
|
|
4132
4227
|
ensureBulkBatchSize(uids.length, max);
|
|
4133
4228
|
if (normalizeBoolean(args.dryRun, false)) {
|
|
4134
4229
|
const preview = await imapService.bulkUpdateFlags({ emailIds, match, folder, flagsToAdd, flagsToRemove, resolvedUids: uids, dryRun: true });
|
|
@@ -4153,12 +4248,16 @@ export function createServer(config, options = {}) {
|
|
|
4153
4248
|
const labelsToRemove = Array.isArray(args.labelsToRemove) ? args.labelsToRemove.map(String) : [];
|
|
4154
4249
|
const folder = optionalString(args, "folder") ?? "INBOX";
|
|
4155
4250
|
const max = getBulkMaxBatchSize(args);
|
|
4156
|
-
|
|
4251
|
+
// See the bulk_delete case above for why this is fetched once and
|
|
4252
|
+
// shared between notFound reporting and uid resolution.
|
|
4253
|
+
const currentUidValidity = emailIds ? await imapService.getMailboxUidValidity(folder) : undefined;
|
|
4254
|
+
const notFoundEmailIds = getBulkNotFoundEmailIds(emailIds, folder, currentUidValidity);
|
|
4157
4255
|
// Resolve the match/emailIds set exactly once and reuse it for both
|
|
4158
4256
|
// the preview and the real run — see resolveUidsForBulkOp's
|
|
4159
4257
|
// comment for why re-resolving `match` a second time (the old
|
|
4160
4258
|
// dry-run-then-real-run pattern) could silently exceed maxBatchSize.
|
|
4161
|
-
|
|
4259
|
+
// Also excludes any id whose embedded UIDVALIDITY is stale.
|
|
4260
|
+
const uids = await imapService.resolveUidsForBulkOp(folder, emailIds, match, currentUidValidity);
|
|
4162
4261
|
ensureBulkBatchSize(uids.length, max);
|
|
4163
4262
|
if (normalizeBoolean(args.dryRun, false)) {
|
|
4164
4263
|
const preview = await imapService.bulkUpdateLabels({ emailIds, match, folder, labelsToAdd, labelsToRemove, resolvedUids: uids, dryRun: true });
|
|
@@ -4262,20 +4361,27 @@ export function createServer(config, options = {}) {
|
|
|
4262
4361
|
ensureMailboxWriteAllowed(config.runtime);
|
|
4263
4362
|
ensureDestructiveConfirmed(config.runtime, normalizeBoolean(args?.confirmed, false), "Permanently delete folder and all messages in it: " + requireString(args, "path"));
|
|
4264
4363
|
return createTextResult(await withAudit(auditService, name, args, async () => imapService.deleteFolder(requireString(args, "path"))));
|
|
4265
|
-
case "mark_email_read":
|
|
4364
|
+
case "mark_email_read": {
|
|
4266
4365
|
ensureEmailActionAllowed(config.runtime, normalizeBoolean(args.isRead, true) ? "mark_read" : "mark_unread");
|
|
4267
|
-
|
|
4268
|
-
|
|
4366
|
+
const emailId = requireString(args, "emailId");
|
|
4367
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4368
|
+
return createTextResult(await withAudit(auditService, name, args, async () => imapService.markEmailRead(emailId, normalizeBoolean(args.isRead, true), uidValidity)));
|
|
4369
|
+
}
|
|
4370
|
+
case "star_email": {
|
|
4269
4371
|
ensureEmailActionAllowed(config.runtime, normalizeBoolean(args.isStarred, true) ? "star" : "unstar");
|
|
4270
|
-
|
|
4372
|
+
const emailId = requireString(args, "emailId");
|
|
4373
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4374
|
+
return createTextResult(await withAudit(auditService, name, args, async () => imapService.starEmail(emailId, normalizeBoolean(args.isStarred, true), uidValidity)));
|
|
4375
|
+
}
|
|
4271
4376
|
case "move_email":
|
|
4272
4377
|
{
|
|
4273
4378
|
ensureEmailActionAllowed(config.runtime, "move");
|
|
4274
4379
|
const emailId = requireString(args, "emailId");
|
|
4275
4380
|
const targetFolder = requireString(args, "targetFolder");
|
|
4381
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4276
4382
|
const result = await withAudit(auditService, name, args, async () => {
|
|
4277
4383
|
try {
|
|
4278
|
-
return await imapService.moveEmail(emailId, targetFolder);
|
|
4384
|
+
return await imapService.moveEmail(emailId, targetFolder, uidValidity);
|
|
4279
4385
|
}
|
|
4280
4386
|
catch (error) {
|
|
4281
4387
|
if (isMissingTargetFolderError(error)) {
|
|
@@ -4300,7 +4406,9 @@ export function createServer(config, options = {}) {
|
|
|
4300
4406
|
case "archive_email":
|
|
4301
4407
|
{
|
|
4302
4408
|
ensureEmailActionAllowed(config.runtime, "archive");
|
|
4303
|
-
const
|
|
4409
|
+
const emailId = requireString(args, "emailId");
|
|
4410
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4411
|
+
const result = await withAudit(auditService, name, args, async () => imapService.archiveEmail(emailId, uidValidity));
|
|
4304
4412
|
const sources = result.targetEmailId
|
|
4305
4413
|
? [
|
|
4306
4414
|
{
|
|
@@ -4317,7 +4425,9 @@ export function createServer(config, options = {}) {
|
|
|
4317
4425
|
case "trash_email":
|
|
4318
4426
|
{
|
|
4319
4427
|
ensureEmailActionAllowed(config.runtime, "trash");
|
|
4320
|
-
const
|
|
4428
|
+
const emailId = requireString(args, "emailId");
|
|
4429
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4430
|
+
const result = await withAudit(auditService, name, args, async () => imapService.trashEmail(emailId, uidValidity));
|
|
4321
4431
|
const sources = result.targetEmailId
|
|
4322
4432
|
? [
|
|
4323
4433
|
{
|
|
@@ -4334,7 +4444,9 @@ export function createServer(config, options = {}) {
|
|
|
4334
4444
|
case "restore_email":
|
|
4335
4445
|
{
|
|
4336
4446
|
ensureEmailActionAllowed(config.runtime, "restore");
|
|
4337
|
-
const
|
|
4447
|
+
const emailId = requireString(args, "emailId");
|
|
4448
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4449
|
+
const result = await withAudit(auditService, name, args, async () => imapService.restoreEmail(emailId, optionalString(args, "targetFolder"), uidValidity));
|
|
4338
4450
|
const sources = result.targetEmailId
|
|
4339
4451
|
? [
|
|
4340
4452
|
{
|
|
@@ -4407,10 +4519,13 @@ export function createServer(config, options = {}) {
|
|
|
4407
4519
|
const result = await withAudit(auditService, name, args, async () => templateService.render(requireString(args, "id"), variables));
|
|
4408
4520
|
return createTextResult(result);
|
|
4409
4521
|
}
|
|
4410
|
-
case "delete_email":
|
|
4522
|
+
case "delete_email": {
|
|
4411
4523
|
ensureDestructiveConfirmed(config.runtime, normalizeBoolean(args.confirmed, false), `Permanently delete ${String(args.emailId ?? "?")} (cannot be recovered)`);
|
|
4412
4524
|
ensureMailboxWriteAllowed(config.runtime);
|
|
4413
|
-
|
|
4525
|
+
const emailId = requireString(args, "emailId");
|
|
4526
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4527
|
+
return createTextResult(await withAudit(auditService, name, args, async () => imapService.deleteEmail(emailId, uidValidity)));
|
|
4528
|
+
}
|
|
4414
4529
|
case "batch_email_action":
|
|
4415
4530
|
{
|
|
4416
4531
|
const emailIds = [...new Set(parseStringListArg(args, "emailIds"))];
|