proton-mail-bridge-client 2.0.2 → 2.0.4
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 +164 -47
- 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 +88 -26
- 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 +84 -10
- package/dist/services/local-index-service.js.map +1 -1
- package/dist/services/simple-imap-service.d.ts +12 -5
- package/dist/services/simple-imap-service.d.ts.map +1 -1
- package/dist/services/simple-imap-service.js +369 -76
- 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqhGP;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({
|
|
@@ -4091,6 +4182,7 @@ export function createServer(config, options = {}) {
|
|
|
4091
4182
|
folder,
|
|
4092
4183
|
permanent,
|
|
4093
4184
|
resolvedUids: uids,
|
|
4185
|
+
uidValidity: currentUidValidity,
|
|
4094
4186
|
dryRun: true,
|
|
4095
4187
|
});
|
|
4096
4188
|
return createTextResult(withBulkNotFound(preview, notFoundEmailIds));
|
|
@@ -4101,6 +4193,7 @@ export function createServer(config, options = {}) {
|
|
|
4101
4193
|
folder,
|
|
4102
4194
|
permanent,
|
|
4103
4195
|
resolvedUids: uids,
|
|
4196
|
+
uidValidity: currentUidValidity,
|
|
4104
4197
|
dryRun: false,
|
|
4105
4198
|
}));
|
|
4106
4199
|
return createTextResult(withBulkNotFound(result, notFoundEmailIds));
|
|
@@ -4123,18 +4216,22 @@ export function createServer(config, options = {}) {
|
|
|
4123
4216
|
throw new McpError(ErrorCode.InvalidParams, "Provide flagsToAdd or flagsToRemove.");
|
|
4124
4217
|
const folder = optionalString(args, "folder") ?? "INBOX";
|
|
4125
4218
|
const max = getBulkMaxBatchSize(args);
|
|
4126
|
-
|
|
4219
|
+
// See the bulk_delete case above for why this is fetched once and
|
|
4220
|
+
// shared between notFound reporting and uid resolution.
|
|
4221
|
+
const currentUidValidity = emailIds ? await imapService.getMailboxUidValidity(folder) : undefined;
|
|
4222
|
+
const notFoundEmailIds = getBulkNotFoundEmailIds(emailIds, folder, currentUidValidity);
|
|
4127
4223
|
// Resolve the match/emailIds set exactly once and reuse it for both
|
|
4128
4224
|
// the preview and the real run — see resolveUidsForBulkOp's
|
|
4129
4225
|
// comment for why re-resolving `match` a second time (the old
|
|
4130
4226
|
// dry-run-then-real-run pattern) could silently exceed maxBatchSize.
|
|
4131
|
-
|
|
4227
|
+
// Also excludes any id whose embedded UIDVALIDITY is stale.
|
|
4228
|
+
const uids = await imapService.resolveUidsForBulkOp(folder, emailIds, match, currentUidValidity);
|
|
4132
4229
|
ensureBulkBatchSize(uids.length, max);
|
|
4133
4230
|
if (normalizeBoolean(args.dryRun, false)) {
|
|
4134
|
-
const preview = await imapService.bulkUpdateFlags({ emailIds, match, folder, flagsToAdd, flagsToRemove, resolvedUids: uids, dryRun: true });
|
|
4231
|
+
const preview = await imapService.bulkUpdateFlags({ emailIds, match, folder, flagsToAdd, flagsToRemove, resolvedUids: uids, uidValidity: currentUidValidity, dryRun: true });
|
|
4135
4232
|
return createTextResult(withBulkNotFound(preview, notFoundEmailIds));
|
|
4136
4233
|
}
|
|
4137
|
-
const result = await withAudit(auditService, name, args, () => imapService.bulkUpdateFlags({ emailIds, match, folder, flagsToAdd, flagsToRemove, resolvedUids: uids, dryRun: false }));
|
|
4234
|
+
const result = await withAudit(auditService, name, args, () => imapService.bulkUpdateFlags({ emailIds, match, folder, flagsToAdd, flagsToRemove, resolvedUids: uids, uidValidity: currentUidValidity, dryRun: false }));
|
|
4138
4235
|
return createTextResult(withBulkNotFound(result, notFoundEmailIds));
|
|
4139
4236
|
}
|
|
4140
4237
|
case "bulk_update_labels": {
|
|
@@ -4153,18 +4250,22 @@ export function createServer(config, options = {}) {
|
|
|
4153
4250
|
const labelsToRemove = Array.isArray(args.labelsToRemove) ? args.labelsToRemove.map(String) : [];
|
|
4154
4251
|
const folder = optionalString(args, "folder") ?? "INBOX";
|
|
4155
4252
|
const max = getBulkMaxBatchSize(args);
|
|
4156
|
-
|
|
4253
|
+
// See the bulk_delete case above for why this is fetched once and
|
|
4254
|
+
// shared between notFound reporting and uid resolution.
|
|
4255
|
+
const currentUidValidity = emailIds ? await imapService.getMailboxUidValidity(folder) : undefined;
|
|
4256
|
+
const notFoundEmailIds = getBulkNotFoundEmailIds(emailIds, folder, currentUidValidity);
|
|
4157
4257
|
// Resolve the match/emailIds set exactly once and reuse it for both
|
|
4158
4258
|
// the preview and the real run — see resolveUidsForBulkOp's
|
|
4159
4259
|
// comment for why re-resolving `match` a second time (the old
|
|
4160
4260
|
// dry-run-then-real-run pattern) could silently exceed maxBatchSize.
|
|
4161
|
-
|
|
4261
|
+
// Also excludes any id whose embedded UIDVALIDITY is stale.
|
|
4262
|
+
const uids = await imapService.resolveUidsForBulkOp(folder, emailIds, match, currentUidValidity);
|
|
4162
4263
|
ensureBulkBatchSize(uids.length, max);
|
|
4163
4264
|
if (normalizeBoolean(args.dryRun, false)) {
|
|
4164
|
-
const preview = await imapService.bulkUpdateLabels({ emailIds, match, folder, labelsToAdd, labelsToRemove, resolvedUids: uids, dryRun: true });
|
|
4265
|
+
const preview = await imapService.bulkUpdateLabels({ emailIds, match, folder, labelsToAdd, labelsToRemove, resolvedUids: uids, uidValidity: currentUidValidity, dryRun: true });
|
|
4165
4266
|
return createTextResult(withBulkNotFound(preview, notFoundEmailIds));
|
|
4166
4267
|
}
|
|
4167
|
-
const result = await withAudit(auditService, name, args, () => imapService.bulkUpdateLabels({ emailIds, match, folder, labelsToAdd, labelsToRemove, resolvedUids: uids, dryRun: false }));
|
|
4268
|
+
const result = await withAudit(auditService, name, args, () => imapService.bulkUpdateLabels({ emailIds, match, folder, labelsToAdd, labelsToRemove, resolvedUids: uids, uidValidity: currentUidValidity, dryRun: false }));
|
|
4168
4269
|
return createTextResult(withBulkNotFound(result, notFoundEmailIds));
|
|
4169
4270
|
}
|
|
4170
4271
|
case "top_senders": {
|
|
@@ -4262,20 +4363,27 @@ export function createServer(config, options = {}) {
|
|
|
4262
4363
|
ensureMailboxWriteAllowed(config.runtime);
|
|
4263
4364
|
ensureDestructiveConfirmed(config.runtime, normalizeBoolean(args?.confirmed, false), "Permanently delete folder and all messages in it: " + requireString(args, "path"));
|
|
4264
4365
|
return createTextResult(await withAudit(auditService, name, args, async () => imapService.deleteFolder(requireString(args, "path"))));
|
|
4265
|
-
case "mark_email_read":
|
|
4366
|
+
case "mark_email_read": {
|
|
4266
4367
|
ensureEmailActionAllowed(config.runtime, normalizeBoolean(args.isRead, true) ? "mark_read" : "mark_unread");
|
|
4267
|
-
|
|
4268
|
-
|
|
4368
|
+
const emailId = requireString(args, "emailId");
|
|
4369
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4370
|
+
return createTextResult(await withAudit(auditService, name, args, async () => imapService.markEmailRead(emailId, normalizeBoolean(args.isRead, true), uidValidity)));
|
|
4371
|
+
}
|
|
4372
|
+
case "star_email": {
|
|
4269
4373
|
ensureEmailActionAllowed(config.runtime, normalizeBoolean(args.isStarred, true) ? "star" : "unstar");
|
|
4270
|
-
|
|
4374
|
+
const emailId = requireString(args, "emailId");
|
|
4375
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4376
|
+
return createTextResult(await withAudit(auditService, name, args, async () => imapService.starEmail(emailId, normalizeBoolean(args.isStarred, true), uidValidity)));
|
|
4377
|
+
}
|
|
4271
4378
|
case "move_email":
|
|
4272
4379
|
{
|
|
4273
4380
|
ensureEmailActionAllowed(config.runtime, "move");
|
|
4274
4381
|
const emailId = requireString(args, "emailId");
|
|
4275
4382
|
const targetFolder = requireString(args, "targetFolder");
|
|
4383
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4276
4384
|
const result = await withAudit(auditService, name, args, async () => {
|
|
4277
4385
|
try {
|
|
4278
|
-
return await imapService.moveEmail(emailId, targetFolder);
|
|
4386
|
+
return await imapService.moveEmail(emailId, targetFolder, uidValidity);
|
|
4279
4387
|
}
|
|
4280
4388
|
catch (error) {
|
|
4281
4389
|
if (isMissingTargetFolderError(error)) {
|
|
@@ -4300,7 +4408,9 @@ export function createServer(config, options = {}) {
|
|
|
4300
4408
|
case "archive_email":
|
|
4301
4409
|
{
|
|
4302
4410
|
ensureEmailActionAllowed(config.runtime, "archive");
|
|
4303
|
-
const
|
|
4411
|
+
const emailId = requireString(args, "emailId");
|
|
4412
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4413
|
+
const result = await withAudit(auditService, name, args, async () => imapService.archiveEmail(emailId, uidValidity));
|
|
4304
4414
|
const sources = result.targetEmailId
|
|
4305
4415
|
? [
|
|
4306
4416
|
{
|
|
@@ -4317,7 +4427,9 @@ export function createServer(config, options = {}) {
|
|
|
4317
4427
|
case "trash_email":
|
|
4318
4428
|
{
|
|
4319
4429
|
ensureEmailActionAllowed(config.runtime, "trash");
|
|
4320
|
-
const
|
|
4430
|
+
const emailId = requireString(args, "emailId");
|
|
4431
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4432
|
+
const result = await withAudit(auditService, name, args, async () => imapService.trashEmail(emailId, uidValidity));
|
|
4321
4433
|
const sources = result.targetEmailId
|
|
4322
4434
|
? [
|
|
4323
4435
|
{
|
|
@@ -4334,7 +4446,9 @@ export function createServer(config, options = {}) {
|
|
|
4334
4446
|
case "restore_email":
|
|
4335
4447
|
{
|
|
4336
4448
|
ensureEmailActionAllowed(config.runtime, "restore");
|
|
4337
|
-
const
|
|
4449
|
+
const emailId = requireString(args, "emailId");
|
|
4450
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4451
|
+
const result = await withAudit(auditService, name, args, async () => imapService.restoreEmail(emailId, optionalString(args, "targetFolder"), uidValidity));
|
|
4338
4452
|
const sources = result.targetEmailId
|
|
4339
4453
|
? [
|
|
4340
4454
|
{
|
|
@@ -4407,10 +4521,13 @@ export function createServer(config, options = {}) {
|
|
|
4407
4521
|
const result = await withAudit(auditService, name, args, async () => templateService.render(requireString(args, "id"), variables));
|
|
4408
4522
|
return createTextResult(result);
|
|
4409
4523
|
}
|
|
4410
|
-
case "delete_email":
|
|
4524
|
+
case "delete_email": {
|
|
4411
4525
|
ensureDestructiveConfirmed(config.runtime, normalizeBoolean(args.confirmed, false), `Permanently delete ${String(args.emailId ?? "?")} (cannot be recovered)`);
|
|
4412
4526
|
ensureMailboxWriteAllowed(config.runtime);
|
|
4413
|
-
|
|
4527
|
+
const emailId = requireString(args, "emailId");
|
|
4528
|
+
const uidValidity = parseEmailId(emailId).uidValidity;
|
|
4529
|
+
return createTextResult(await withAudit(auditService, name, args, async () => imapService.deleteEmail(emailId, uidValidity)));
|
|
4530
|
+
}
|
|
4414
4531
|
case "batch_email_action":
|
|
4415
4532
|
{
|
|
4416
4533
|
const emailIds = [...new Set(parseStringListArg(args, "emailIds"))];
|