@remit/mailbox-service 0.0.111 → 0.0.113
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/account-credentials.d.ts +83 -0
- package/dist/account-credentials.js +104 -0
- package/dist/attribute-mapper.d.ts +36 -0
- package/dist/attribute-mapper.js +119 -0
- package/dist/body-parse.d.ts +28 -0
- package/{src/body-parse.ts → dist/body-parse.js} +18 -26
- package/dist/body-part-mapper.d.ts +92 -0
- package/dist/body-part-mapper.js +301 -0
- package/dist/body-sync-queue.d.ts +59 -0
- package/dist/body-sync-queue.js +56 -0
- package/dist/body-sync-terminal.d.ts +58 -0
- package/dist/body-sync-terminal.js +95 -0
- package/dist/body-sync.d.ts +351 -0
- package/dist/body-sync.js +1204 -0
- package/dist/calendar-parts.d.ts +23 -0
- package/dist/calendar-parts.js +18 -0
- package/dist/classification-backfill.d.ts +91 -0
- package/dist/classification-backfill.js +180 -0
- package/dist/classify-message.d.ts +31 -0
- package/dist/classify-message.js +89 -0
- package/dist/connection-factory.d.ts +54 -0
- package/dist/connection-factory.js +54 -0
- package/dist/connection-test.d.ts +41 -0
- package/dist/connection-test.js +152 -0
- package/dist/external-move.d.ts +94 -0
- package/dist/external-move.js +173 -0
- package/dist/filter-operations.d.ts +42 -0
- package/dist/filter-operations.js +211 -0
- package/dist/filters/filter-config.d.ts +14 -0
- package/dist/filters/filter-config.js +10 -0
- package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
- package/dist/filters/list-id.js +26 -0
- package/dist/filters/match.d.ts +69 -0
- package/dist/filters/match.js +165 -0
- package/dist/filters/pipeline.d.ts +64 -0
- package/dist/filters/pipeline.js +100 -0
- package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +19 -77
- package/dist/flag-push-terminal.js +63 -0
- package/dist/flag-push.d.ts +69 -0
- package/dist/flag-push.js +111 -0
- package/dist/flag-queue.d.ts +175 -0
- package/dist/flag-queue.js +291 -0
- package/dist/folder-operations.d.ts +39 -0
- package/dist/folder-operations.js +112 -0
- package/dist/folder-placement.d.ts +13 -0
- package/dist/folder-placement.js +23 -0
- package/dist/heuristics/authenticationResults.d.ts +10 -0
- package/dist/heuristics/authenticationResults.js +187 -0
- package/dist/heuristics/classifyByHeaders.d.ts +82 -0
- package/dist/heuristics/classifyByHeaders.js +365 -0
- package/dist/heuristics/classifyPlacement.d.ts +67 -0
- package/dist/heuristics/classifyPlacement.js +153 -0
- package/dist/heuristics/machineSenders.d.ts +21 -0
- package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
- package/dist/heuristics/senderMismatch.d.ts +81 -0
- package/dist/heuristics/senderMismatch.js +192 -0
- package/dist/heuristics/socialDomains.d.ts +14 -0
- package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
- package/dist/heuristics/transactionalDomains.d.ts +18 -0
- package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
- package/dist/imapflow-connection.d.ts +377 -0
- package/dist/imapflow-connection.js +1147 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +65 -0
- package/dist/list-id-backfill.d.ts +83 -0
- package/dist/list-id-backfill.js +132 -0
- package/dist/mailbox-cursor-rebuild.d.ts +74 -0
- package/dist/mailbox-cursor-rebuild.js +71 -0
- package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
- package/dist/mailbox-cursor.js +102 -0
- package/dist/mailbox-intent.d.ts +41 -0
- package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +39 -63
- package/dist/mailbox-management.d.ts +240 -0
- package/dist/mailbox-management.js +576 -0
- package/dist/mailbox-presence.d.ts +52 -0
- package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
- package/dist/mailbox-queue.d.ts +126 -0
- package/dist/mailbox-queue.js +214 -0
- package/dist/mailbox-sync.d.ts +93 -0
- package/dist/mailbox-sync.js +490 -0
- package/dist/mailbox-upstream.d.ts +38 -0
- package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
- package/dist/message-category.d.ts +51 -0
- package/{src/message-category.ts → dist/message-category.js} +28 -71
- package/dist/message-move.d.ts +285 -0
- package/dist/message-move.js +858 -0
- package/dist/message-parser.d.ts +14 -0
- package/dist/message-parser.js +17 -0
- package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
- package/dist/message-presence.js +29 -0
- package/dist/message-read.d.ts +51 -0
- package/dist/message-read.js +31 -0
- package/dist/message-sync.d.ts +345 -0
- package/dist/message-sync.js +1358 -0
- package/dist/mime-walker.d.ts +102 -0
- package/dist/mime-walker.js +198 -0
- package/dist/organize-job.d.ts +20 -0
- package/dist/organize-job.js +37 -0
- package/dist/outbox-attachment-content.d.ts +37 -0
- package/dist/outbox-attachment-content.js +50 -0
- package/dist/outbox-attachment-filename.d.ts +19 -0
- package/dist/outbox-attachment-filename.js +64 -0
- package/dist/outbox-attachment.d.ts +148 -0
- package/dist/outbox-attachment.js +273 -0
- package/dist/outbox-queue.d.ts +73 -0
- package/dist/outbox-queue.js +284 -0
- package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
- package/dist/outbox-status.js +18 -0
- package/dist/ownership.d.ts +24 -0
- package/dist/ownership.js +83 -0
- package/dist/placement-move-terminal.d.ts +67 -0
- package/dist/placement-move-terminal.js +88 -0
- package/dist/placement-move.d.ts +83 -0
- package/dist/placement-move.js +216 -0
- package/{src/placement-settled.ts → dist/placement-settled.d.ts} +19 -50
- package/dist/placement-settled.js +75 -0
- package/dist/quarantine.d.ts +95 -0
- package/dist/quarantine.js +131 -0
- package/dist/refusal.d.ts +31 -0
- package/dist/refusal.js +3 -0
- package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -101
- package/dist/restore-source-placement.js +79 -0
- package/dist/search-text-backfill.d.ts +15 -0
- package/dist/search-text-backfill.js +33 -0
- package/dist/snippet.d.ts +61 -0
- package/dist/snippet.js +129 -0
- package/dist/spam-report.d.ts +86 -0
- package/dist/spam-report.js +241 -0
- package/dist/sync-warning.d.ts +21 -0
- package/dist/sync-warning.js +26 -0
- package/dist/sync-watermarks.d.ts +138 -0
- package/dist/sync-watermarks.js +205 -0
- package/dist/test-helpers/fake-imap.d.ts +151 -0
- package/dist/test-helpers/fake-imap.js +508 -0
- package/dist/test-helpers/folder-roles.d.ts +34 -0
- package/dist/test-helpers/folder-roles.js +47 -0
- package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
- package/dist/test-helpers/mime-tree-generator.js +329 -0
- package/dist/test-helpers/test-store.d.ts +69 -0
- package/dist/test-helpers/test-store.js +193 -0
- package/dist/text/normalizer.d.ts +20 -0
- package/dist/text/normalizer.js +102 -0
- package/dist/thread-message-rows.d.ts +69 -0
- package/dist/thread-message-rows.js +40 -0
- package/dist/thread-root.d.ts +7 -0
- package/dist/thread-root.js +25 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +19 -0
- package/package.json +86 -73
- package/src/account-credentials.ts +0 -210
- package/src/adversarial-mime.e2e.test.ts +0 -307
- package/src/attribute-mapper.test.ts +0 -104
- package/src/attribute-mapper.ts +0 -148
- package/src/body-parse.test.ts +0 -55
- package/src/body-part-mapper.property.test.ts +0 -354
- package/src/body-part-mapper.ts +0 -360
- package/src/body-sync-auto-read-writeonce.test.ts +0 -219
- package/src/body-sync-calendar-suggestion.test.ts +0 -433
- package/src/body-sync-category-denormalize.test.ts +0 -352
- package/src/body-sync-category-override.test.ts +0 -501
- package/src/body-sync-category-writeonce.test.ts +0 -234
- package/src/body-sync-filter.test.ts +0 -237
- package/src/body-sync-message-gone.test.ts +0 -139
- package/src/body-sync-placement-flags.test.ts +0 -347
- package/src/body-sync-placement-writeonce.test.ts +0 -337
- package/src/body-sync-quarantine.test.ts +0 -276
- package/src/body-sync-queue.test.ts +0 -124
- package/src/body-sync-queue.ts +0 -131
- package/src/body-sync-resync.test.ts +0 -605
- package/src/body-sync-terminal.test.ts +0 -310
- package/src/body-sync-terminal.ts +0 -176
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/body-sync.ts +0 -1970
- package/src/calendar-parts.ts +0 -41
- package/src/classification-backfill.test.ts +0 -615
- package/src/classification-backfill.ts +0 -363
- package/src/classify-message.test.ts +0 -205
- package/src/classify-message.ts +0 -116
- package/src/connection-factory.ts +0 -102
- package/src/connection-test.ts +0 -208
- package/src/credentials.test.ts +0 -181
- package/src/external-move.test.ts +0 -369
- package/src/external-move.ts +0 -272
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/filter-config.test.ts +0 -229
- package/src/filters/filter-config.ts +0 -69
- package/src/filters/list-id.test.ts +0 -50
- package/src/filters/match.test.ts +0 -429
- package/src/filters/match.ts +0 -274
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/filters/pipeline.test.ts +0 -330
- package/src/filters/pipeline.ts +0 -284
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-push.test.ts +0 -408
- package/src/flag-push.ts +0 -198
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/flag-queue.ts +0 -509
- package/src/folder-placement.ts +0 -56
- package/src/heuristics/authenticationResults.test.ts +0 -189
- package/src/heuristics/authenticationResults.ts +0 -212
- package/src/heuristics/classifyByHeaders.test.ts +0 -889
- package/src/heuristics/classifyByHeaders.ts +0 -467
- package/src/heuristics/classifyPlacement.test.ts +0 -366
- package/src/heuristics/classifyPlacement.ts +0 -211
- package/src/heuristics/classifyRealMail.test.ts +0 -242
- package/src/heuristics/machineSenders.test.ts +0 -101
- package/src/heuristics/senderMismatch.test.ts +0 -346
- package/src/heuristics/senderMismatch.ts +0 -259
- package/src/imapflow-connect-retry.test.ts +0 -134
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- package/src/imapflow-connection.test.ts +0 -469
- package/src/imapflow-connection.ts +0 -1475
- package/src/index.ts +0 -350
- package/src/list-id-backfill.test.ts +0 -573
- package/src/list-id-backfill.ts +0 -274
- package/src/mailbox-cursor-rebuild.test.ts +0 -171
- package/src/mailbox-cursor-rebuild.ts +0 -146
- package/src/mailbox-cursor.test.ts +0 -249
- package/src/mailbox-delete-intent.test.ts +0 -246
- package/src/mailbox-management.test.ts +0 -356
- package/src/mailbox-management.ts +0 -839
- package/src/mailbox-presence.test.ts +0 -72
- package/src/mailbox-queue.ts +0 -415
- package/src/mailbox-rename-intent.test.ts +0 -389
- package/src/mailbox-rename-subtree.test.ts +0 -664
- package/src/mailbox-sync.test.ts +0 -848
- package/src/mailbox-sync.ts +0 -724
- package/src/message-delete-junk-reconcile.test.ts +0 -117
- package/src/message-delete-no-trash.test.ts +0 -245
- package/src/message-delete-unsettled-placement.test.ts +0 -396
- package/src/message-move-copy.test.ts +0 -562
- package/src/message-move-empty-trash.test.ts +0 -326
- package/src/message-move-same-mailbox.test.ts +0 -139
- package/src/message-move-unsettled-placement.test.ts +0 -378
- package/src/message-move.ts +0 -1348
- package/src/message-parser.ts +0 -26
- package/src/message-presence.test.ts +0 -50
- package/src/message-sync-address-spoof.test.ts +0 -293
- package/src/message-sync-category.test.ts +0 -142
- package/src/message-sync-changedsince.test.ts +0 -840
- package/src/message-sync-flags.test.ts +0 -158
- package/src/message-sync-junk-harvest.test.ts +0 -244
- package/src/message-sync-quarantine.test.ts +0 -416
- package/src/message-sync-repoint.test.ts +0 -419
- package/src/message-sync.ts +0 -1963
- package/src/mime-walker.test.ts +0 -311
- package/src/mime-walker.ts +0 -305
- package/src/outbox-attachment-content.test.ts +0 -153
- package/src/outbox-attachment-content.ts +0 -104
- package/src/outbox-attachment-filename.test.ts +0 -132
- package/src/outbox-attachment-filename.ts +0 -76
- package/src/outbox-attachment.test.ts +0 -738
- package/src/outbox-attachment.ts +0 -536
- package/src/outbox-failed-recovery.test.ts +0 -370
- package/src/outbox-queue-settled-row.test.ts +0 -140
- package/src/outbox-queue.test.ts +0 -291
- package/src/outbox-queue.ts +0 -508
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-terminal.ts +0 -148
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-move.ts +0 -336
- package/src/placement-settled.test.ts +0 -147
- package/src/placement-transition-race.test.ts +0 -482
- package/src/quarantine.test.ts +0 -191
- package/src/quarantine.ts +0 -228
- package/src/snippet.test.ts +0 -213
- package/src/snippet.ts +0 -152
- package/src/spam-report.test.ts +0 -801
- package/src/spam-report.ts +0 -357
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- package/src/sync-watermarks.test.ts +0 -409
- package/src/sync-watermarks.ts +0 -319
- package/src/system-flag-wire-format.test.ts +0 -77
- package/src/test-helpers/folder-roles.ts +0 -66
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- package/src/test-helpers/mime-tree-generator.ts +0 -461
- package/src/text/normalizer.test.ts +0 -223
- package/src/text/normalizer.ts +0 -140
- package/src/thread-message-rows.ts +0 -77
- package/src/types/mailparser-augment.d.ts +0 -15
- package/src/types/natural-porter-stemmer.d.ts +0 -50
- package/src/types/stopword.d.ts +0 -69
- package/src/types.ts +0 -371
- package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
- package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
- package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
- package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
- package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
- package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
- package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
- package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
- package/tsconfig.json +0 -8
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { MailboxCursorState } from "@remit/domain-enums";
|
|
2
|
+
const isNormal = (state) => state === undefined || state === MailboxCursorState.normal;
|
|
3
|
+
/**
|
|
4
|
+
* Compare a mailbox's stored UIDVALIDITY against what the server just served
|
|
5
|
+
* (from an `openBox`/`STATUS` response) and act:
|
|
6
|
+
*
|
|
7
|
+
* - Already `cursor_invalid` or `rebuilding` — outbound IMAP is already
|
|
8
|
+
* paused for this mailbox; short-circuit without a write (frugal — epic
|
|
9
|
+
* #1281 invariant 6) and report why.
|
|
10
|
+
* - `normal` but the served value disagrees with the stored one — trip the
|
|
11
|
+
* mailbox to `cursor_invalid` (the only write this function performs) and
|
|
12
|
+
* report the pause. This is the detection edge: whichever worker path
|
|
13
|
+
* notices the mismatch first is the one that flips the switch.
|
|
14
|
+
* - `normal` and the values agree — proceed, nothing to do.
|
|
15
|
+
*
|
|
16
|
+
* Callers must treat a `{ ok: false }` result as an expected, routine pause
|
|
17
|
+
* (epic #1281 invariant 3), not a fault: log/metric and skip the outbound
|
|
18
|
+
* operation for this round rather than throwing.
|
|
19
|
+
*/
|
|
20
|
+
export const guardMailboxCursor = async (deps, accountId, mailbox, servedUidValidity) => {
|
|
21
|
+
if (!isNormal(mailbox.cursorState)) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
state: mailbox.cursorState === MailboxCursorState.rebuilding
|
|
25
|
+
? "rebuilding"
|
|
26
|
+
: "cursor_invalid",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (mailbox.uidValidity !== servedUidValidity) {
|
|
30
|
+
await deps.mailboxService.update(accountId, mailbox.mailboxId, {
|
|
31
|
+
cursorState: MailboxCursorState.cursor_invalid,
|
|
32
|
+
});
|
|
33
|
+
return { ok: false, state: "cursor_invalid" };
|
|
34
|
+
}
|
|
35
|
+
return { ok: true };
|
|
36
|
+
};
|
|
37
|
+
/** True when a mailbox row is in either non-`normal` cursor state. */
|
|
38
|
+
export const isCursorRebuildNeeded = (cursorState) => cursorState === MailboxCursorState.cursor_invalid ||
|
|
39
|
+
cursorState === MailboxCursorState.rebuilding;
|
|
40
|
+
/**
|
|
41
|
+
* Thrown by the `openBox` override on a {@link guardConnectionCursor}-wrapped
|
|
42
|
+
* connection when the mailbox cursor is (or just became) non-`normal`. Every
|
|
43
|
+
* caller must treat this as the routine, expected pause described on {@link
|
|
44
|
+
* guardMailboxCursor} — catch it around the outbound operation and skip
|
|
45
|
+
* (ack/log/return), never let it surface as an infrastructure fault.
|
|
46
|
+
*/
|
|
47
|
+
export class MailboxCursorPausedError extends Error {
|
|
48
|
+
state;
|
|
49
|
+
constructor(state) {
|
|
50
|
+
super(`Mailbox cursor is ${state}`);
|
|
51
|
+
this.state = state;
|
|
52
|
+
this.name = "MailboxCursorPausedError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Wrap a live `IImapConnection` so its `openBox` is the single, structural
|
|
57
|
+
* choke point for UIDVALIDITY cursor guarding (epic #1281 invariants 3 & 5).
|
|
58
|
+
*
|
|
59
|
+
* Every outbound IMAP operation that touches a stored UID requires an
|
|
60
|
+
* `openBox`/SELECT first — that is an IMAP protocol requirement, not a
|
|
61
|
+
* convention — so gating there means a caller cannot reach `fetchMessages`,
|
|
62
|
+
* `addFlags`, `moveMessages`, `fetchMessageBody`, etc. against a paused
|
|
63
|
+
* mailbox's stale axis without going through the same wrapped `openBox` this
|
|
64
|
+
* function defines. A handler that forgets to call `guardMailboxCursor`
|
|
65
|
+
* manually can no longer skip the check by omission; wiring a connection
|
|
66
|
+
* through here makes that structural instead of per-callsite.
|
|
67
|
+
*
|
|
68
|
+
* - Already `cursor_invalid`/`rebuilding` — throws {@link
|
|
69
|
+
* MailboxCursorPausedError} immediately, without calling the real
|
|
70
|
+
* `openBox` (frugal — invariant 6, no network round-trip for a mailbox
|
|
71
|
+
* already known paused).
|
|
72
|
+
* - `normal` but the served UIDVALIDITY disagrees — trips the mailbox (via
|
|
73
|
+
* {@link guardMailboxCursor}) and throws the same error.
|
|
74
|
+
* - `normal` and it matches — delegates to the real `openBox` and returns
|
|
75
|
+
* normally.
|
|
76
|
+
*
|
|
77
|
+
* `mailbox` is a snapshot taken once by the caller (they already need it for
|
|
78
|
+
* `mailboxPath`) — not re-fetched per call, so this adds no extra read.
|
|
79
|
+
*/
|
|
80
|
+
export const guardConnectionCursor = (connection, deps, accountId, mailbox) => {
|
|
81
|
+
const guardedOpenBox = async (mailboxPath, readOnly) => {
|
|
82
|
+
if (!isNormal(mailbox.cursorState)) {
|
|
83
|
+
throw new MailboxCursorPausedError(mailbox.cursorState === MailboxCursorState.rebuilding
|
|
84
|
+
? "rebuilding"
|
|
85
|
+
: "cursor_invalid");
|
|
86
|
+
}
|
|
87
|
+
const boxStatus = await connection.openBox(mailboxPath, readOnly);
|
|
88
|
+
const check = await guardMailboxCursor(deps, accountId, mailbox, boxStatus.uidvalidity);
|
|
89
|
+
if (!check.ok) {
|
|
90
|
+
throw new MailboxCursorPausedError(check.state);
|
|
91
|
+
}
|
|
92
|
+
return boxStatus;
|
|
93
|
+
};
|
|
94
|
+
return new Proxy(connection, {
|
|
95
|
+
get(target, prop, receiver) {
|
|
96
|
+
if (prop === "openBox")
|
|
97
|
+
return guardedOpenBox;
|
|
98
|
+
const value = Reflect.get(target, prop, receiver);
|
|
99
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { IMailboxRepository, MailboxItem } from "@remit/data-ports";
|
|
2
|
+
/**
|
|
3
|
+
* The states a rename or a delete intent may be recorded from (T4, T7). Both
|
|
4
|
+
* failed variants are legal starting points — a retry re-records the same
|
|
5
|
+
* intent on the same row, so no duplicate folder can be created — and every
|
|
6
|
+
* other state means a mutation is already in flight on the folder.
|
|
7
|
+
*/
|
|
8
|
+
export declare const INTENT_RECORDABLE_FROM: readonly ["synced", "failed"];
|
|
9
|
+
export declare const carriesIntent: (row: Pick<MailboxItem, "syncStatus">) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether a row was written by one particular rename intent
|
|
12
|
+
* (docs/architecture/folder-rename-and-delete.md D15).
|
|
13
|
+
*
|
|
14
|
+
* A recorded target that merely sits at or under the rename's target is not
|
|
15
|
+
* enough to identify one. Two folders can hold targets under the same path —
|
|
16
|
+
* `Work → Projects` in flight while `Other → Projects/Old` is recorded — and a
|
|
17
|
+
* settle that claimed both would stamp a row this rename never touched onto a
|
|
18
|
+
* path it never asked for, leaving that folder's own rename unrepresented and
|
|
19
|
+
* two rows claiming one path for the sweep to reap and re-insert.
|
|
20
|
+
*
|
|
21
|
+
* The identity is the pair: the row sits at or under the path the rename is
|
|
22
|
+
* leaving, **and** the target it recorded is exactly what this rename's own
|
|
23
|
+
* rebase would have written for it. Nothing but this intent writes that pair,
|
|
24
|
+
* because the intent is recorded over one subtree in one batch (D6) and
|
|
25
|
+
* the transition is the only writer of either field (D3).
|
|
26
|
+
*/
|
|
27
|
+
export declare const recordedByRename: (row: Pick<MailboxItem, "fullPath" | "pendingPath">, oldPath: string, newPath: string, delimiter: string) => boolean;
|
|
28
|
+
export declare const folderInFlight: (row: MailboxItem) => string;
|
|
29
|
+
/**
|
|
30
|
+
* Say why a folder intent lost its compare-and-set (D3).
|
|
31
|
+
*
|
|
32
|
+
* Existence was established by the read the caller took before recording, so a
|
|
33
|
+
* lost predicate is a conflict rather than a 404 — but the row can have gone in
|
|
34
|
+
* between, and one primary-key read tells the two apart. For a subtree intent
|
|
35
|
+
* the blocker may be a descendant, and the message says so: a subtree cannot be
|
|
36
|
+
* half-renamed (D6), so a descendant mid-mutation refuses the whole rename and
|
|
37
|
+
* a user who was not thinking about it has to be told which folder it is.
|
|
38
|
+
*
|
|
39
|
+
* Always rejects.
|
|
40
|
+
*/
|
|
41
|
+
export declare const refuseContestedIntent: (mailboxService: Pick<IMailboxRepository, "get" | "findByPathPrefix">, accountId: string, mailboxId: string, scope: "folder" | "subtree") => Promise<never>;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type { IMailboxRepository, MailboxItem } from "@remit/data-ports";
|
|
2
1
|
import { ConflictError, NotFoundError } from "@remit/data-ports/errors";
|
|
3
2
|
import { rebaseMailboxPath } from "@remit/data-ports/mailbox-name";
|
|
4
3
|
import { MailboxSyncStatus } from "@remit/domain-enums";
|
|
5
4
|
import { isNotFoundError } from "./mailbox-presence.js";
|
|
6
|
-
|
|
7
5
|
/**
|
|
8
6
|
* The states a rename or a delete intent may be recorded from (T4, T7). Both
|
|
9
7
|
* failed variants are legal starting points — a retry re-records the same
|
|
@@ -11,13 +9,10 @@ import { isNotFoundError } from "./mailbox-presence.js";
|
|
|
11
9
|
* other state means a mutation is already in flight on the folder.
|
|
12
10
|
*/
|
|
13
11
|
export const INTENT_RECORDABLE_FROM = [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
]
|
|
17
|
-
|
|
18
|
-
const carriesIntent = (row: Pick<MailboxItem, "syncStatus">): boolean =>
|
|
19
|
-
(INTENT_RECORDABLE_FROM as readonly string[]).includes(row.syncStatus);
|
|
20
|
-
|
|
12
|
+
MailboxSyncStatus.synced,
|
|
13
|
+
MailboxSyncStatus.failed,
|
|
14
|
+
];
|
|
15
|
+
export const carriesIntent = (row) => INTENT_RECORDABLE_FROM.includes(row.syncStatus);
|
|
21
16
|
/**
|
|
22
17
|
* Whether a row was written by one particular rename intent
|
|
23
18
|
* (docs/architecture/folder-rename-and-delete.md D15).
|
|
@@ -32,34 +27,27 @@ const carriesIntent = (row: Pick<MailboxItem, "syncStatus">): boolean =>
|
|
|
32
27
|
* The identity is the pair: the row sits at or under the path the rename is
|
|
33
28
|
* leaving, **and** the target it recorded is exactly what this rename's own
|
|
34
29
|
* rebase would have written for it. Nothing but this intent writes that pair,
|
|
35
|
-
* because the intent is recorded over one subtree in one
|
|
30
|
+
* because the intent is recorded over one subtree in one batch (D6) and
|
|
36
31
|
* the transition is the only writer of either field (D3).
|
|
37
32
|
*/
|
|
38
|
-
export const recordedByRename = (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
newPath: string,
|
|
42
|
-
delimiter: string,
|
|
43
|
-
): boolean =>
|
|
44
|
-
row.pendingPath !== undefined &&
|
|
45
|
-
rebaseMailboxPath(row.fullPath, oldPath, newPath, delimiter) ===
|
|
46
|
-
row.pendingPath;
|
|
47
|
-
|
|
33
|
+
export const recordedByRename = (row, oldPath, newPath, delimiter) => row.pendingPath !== undefined &&
|
|
34
|
+
rebaseMailboxPath(row.fullPath, oldPath, newPath, delimiter) ===
|
|
35
|
+
row.pendingPath;
|
|
48
36
|
/**
|
|
49
37
|
* What is already happening to a folder, as a clause naming the folder. The
|
|
50
38
|
* six states, not the four enum values: `pending` covers a create in flight and
|
|
51
39
|
* a rename in flight, and only `pendingPath` tells them apart.
|
|
52
40
|
*/
|
|
53
|
-
const inFlight = (row
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
41
|
+
const inFlight = (row) => {
|
|
42
|
+
if (row.syncStatus === MailboxSyncStatus.deleting) {
|
|
43
|
+
return `A delete is already in progress for "${row.fullPath}"`;
|
|
44
|
+
}
|
|
45
|
+
if (row.pendingPath !== undefined) {
|
|
46
|
+
return `A rename is in progress for "${row.fullPath}"`;
|
|
47
|
+
}
|
|
48
|
+
return `"${row.fullPath}" is still being created`;
|
|
61
49
|
};
|
|
62
|
-
|
|
50
|
+
export const folderInFlight = (row) => `${inFlight(row)}.`;
|
|
63
51
|
/**
|
|
64
52
|
* Say why a folder intent lost its compare-and-set (D3).
|
|
65
53
|
*
|
|
@@ -72,38 +60,26 @@ const inFlight = (row: MailboxItem): string => {
|
|
|
72
60
|
*
|
|
73
61
|
* Always rejects.
|
|
74
62
|
*/
|
|
75
|
-
export const refuseContestedIntent = async (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
98
|
-
: [];
|
|
99
|
-
const blocking = descendants.find((row) => !carriesIntent(row));
|
|
100
|
-
if (blocking) {
|
|
101
|
-
throw new ConflictError(`${inFlight(blocking)}, inside this folder.`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Every row reads recordable now, so the row that blocked has already moved
|
|
105
|
-
// on. Nothing to name, and the same request succeeds on a retry.
|
|
106
|
-
throw new ConflictError(
|
|
107
|
-
`"${root.fullPath}" changed while this request was being recorded. Try again.`,
|
|
108
|
-
);
|
|
63
|
+
export const refuseContestedIntent = async (mailboxService, accountId, mailboxId, scope) => {
|
|
64
|
+
const root = await mailboxService
|
|
65
|
+
.get(accountId, mailboxId)
|
|
66
|
+
.catch((error) => {
|
|
67
|
+
if (isNotFoundError(error))
|
|
68
|
+
return undefined;
|
|
69
|
+
throw error;
|
|
70
|
+
});
|
|
71
|
+
if (!root)
|
|
72
|
+
throw new NotFoundError(`Mailbox not found: ${mailboxId}`);
|
|
73
|
+
if (!carriesIntent(root))
|
|
74
|
+
throw new ConflictError(folderInFlight(root));
|
|
75
|
+
const descendants = scope === "subtree"
|
|
76
|
+
? await mailboxService.findByPathPrefix(accountId, root.fullPath, root.hierarchyDelimiter)
|
|
77
|
+
: [];
|
|
78
|
+
const blocking = descendants.find((row) => !carriesIntent(row));
|
|
79
|
+
if (blocking) {
|
|
80
|
+
throw new ConflictError(`${inFlight(blocking)}, inside this folder.`);
|
|
81
|
+
}
|
|
82
|
+
// Every row reads recordable now, so the row that blocked has already moved
|
|
83
|
+
// on. Nothing to name, and the same request succeeds on a retry.
|
|
84
|
+
throw new ConflictError(`"${root.fullPath}" changed while this request was being recorded. Try again.`);
|
|
109
85
|
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import type { IMailboxRepository } from "@remit/data-ports";
|
|
2
|
+
import type { IImapConnection } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Input for creating a mailbox
|
|
5
|
+
*/
|
|
6
|
+
export interface CreateMailboxInput {
|
|
7
|
+
accountId: string;
|
|
8
|
+
path: string;
|
|
9
|
+
subscribe?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Input for renaming a mailbox
|
|
13
|
+
*/
|
|
14
|
+
export interface RenameMailboxInput {
|
|
15
|
+
mailboxId: string;
|
|
16
|
+
newPath: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Input for deleting a mailbox
|
|
20
|
+
*/
|
|
21
|
+
export interface DeleteMailboxInput {
|
|
22
|
+
mailboxId: string;
|
|
23
|
+
force?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Result of syncing mailbox operation to IMAP
|
|
27
|
+
*/
|
|
28
|
+
export interface MailboxManagementSyncResult {
|
|
29
|
+
success: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
/**
|
|
32
|
+
* Where the folder ended up, on a rename the server confirmed. Absent when
|
|
33
|
+
* the recorded intent had already moved on, so no RENAME was issued —
|
|
34
|
+
* which is how a caller tells the two apart without a second read.
|
|
35
|
+
*/
|
|
36
|
+
renamed?: {
|
|
37
|
+
oldPath: string;
|
|
38
|
+
newPath: string;
|
|
39
|
+
delimiter: string;
|
|
40
|
+
};
|
|
41
|
+
existing?: {
|
|
42
|
+
mailboxId: string;
|
|
43
|
+
fullPath: string;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Logger interface for MailboxManagementService
|
|
48
|
+
*/
|
|
49
|
+
export interface MailboxManagementLogger {
|
|
50
|
+
info(obj: Record<string, unknown>, msg: string): void;
|
|
51
|
+
error(obj: Record<string, unknown>, msg: string): void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse a mailbox path to extract name and parent path
|
|
55
|
+
*/
|
|
56
|
+
export declare const parseMailboxPath: (path: string, delimiter?: string) => {
|
|
57
|
+
name: string;
|
|
58
|
+
parent: string | null;
|
|
59
|
+
depth: number;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Validate mailbox path for invalid characters and operations
|
|
63
|
+
*/
|
|
64
|
+
export declare const validateMailboxPath: (path: string) => void;
|
|
65
|
+
/**
|
|
66
|
+
* Validate that an operation can be performed on a mailbox path
|
|
67
|
+
*/
|
|
68
|
+
export declare const validateMailboxOperation: (operation: "delete" | "rename", path: string) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Service for managing mailbox operations (create, rename, delete).
|
|
71
|
+
*
|
|
72
|
+
* Implements an optimistic local-first pattern:
|
|
73
|
+
* 1. Updates are applied locally first
|
|
74
|
+
* 2. Changes are queued for IMAP sync via SQS
|
|
75
|
+
* 3. Worker processes queue and syncs to IMAP server
|
|
76
|
+
*/
|
|
77
|
+
export declare class MailboxManagementService {
|
|
78
|
+
private mailboxService;
|
|
79
|
+
private log;
|
|
80
|
+
constructor(mailboxService: IMailboxRepository, logger?: MailboxManagementLogger);
|
|
81
|
+
/**
|
|
82
|
+
* Settle a create: `pending` with **no** recorded rename target, which is the
|
|
83
|
+
* state a create in flight is (D3, D10).
|
|
84
|
+
*
|
|
85
|
+
* The absence check is what the whole `wherePendingPath` predicate exists
|
|
86
|
+
* for. A create whose acknowledgement was lost, redelivered against a row a
|
|
87
|
+
* rename has since claimed, passes a `pending`-only guard; the CREATE
|
|
88
|
+
* collides, ImapFlow reports the collision as `{created: false}` rather than
|
|
89
|
+
* an error, and this write then strands the row `synced` with a rename
|
|
90
|
+
* target on it — the seventh combination — leaving the rename's own settle
|
|
91
|
+
* with nothing to match and the RENAME never running.
|
|
92
|
+
*
|
|
93
|
+
* A null means the row is gone or the intent moved on. The handlers'
|
|
94
|
+
* #289-class terminal guards classify the NotFoundError as the user having
|
|
95
|
+
* deleted the folder mid-sync.
|
|
96
|
+
*/
|
|
97
|
+
private settleCreate;
|
|
98
|
+
/**
|
|
99
|
+
* The rows one rename intent recorded, each with the path it settles to.
|
|
100
|
+
*
|
|
101
|
+
* The settle deliberately does not re-resolve the subtree (D15): membership
|
|
102
|
+
* changes between the intent and the settle — the sweep inserts a row,
|
|
103
|
+
* another client deletes a descendant — and an all-or-nothing settle then
|
|
104
|
+
* either throws, poisoning the account's FIFO group (#339), or resolves
|
|
105
|
+
* leaving every row `pending` forever with no route out, because `failed` is
|
|
106
|
+
* only reachable from a settle that ran.
|
|
107
|
+
*
|
|
108
|
+
* So the rows identify themselves, by the pair `recordedByRename` tests: a
|
|
109
|
+
* target at or under the rename's target is not on its own enough, because a
|
|
110
|
+
* second folder renamed under the same branch would be claimed by a rename
|
|
111
|
+
* that never touched it. A row that appeared afterwards carries no such pair
|
|
112
|
+
* and is not touched.
|
|
113
|
+
*
|
|
114
|
+
* `confirmedPath` re-prefixes each recorded target off the path the rename
|
|
115
|
+
* actually resolved to, so a normalization the requested string did not carry
|
|
116
|
+
* moves the whole branch rather than the folder alone.
|
|
117
|
+
*
|
|
118
|
+
* `delimiter` is the renamed folder's own, read from the row the caller
|
|
119
|
+
* already holds — deriving one from whatever the listing happens to contain
|
|
120
|
+
* strands every descendant when that row has gone.
|
|
121
|
+
*/
|
|
122
|
+
private intentCarryingRows;
|
|
123
|
+
/**
|
|
124
|
+
* Sync a CREATE operation to IMAP.
|
|
125
|
+
* Called by worker after dequeuing MAILBOX_CREATE event.
|
|
126
|
+
*
|
|
127
|
+
* @param accountId - Account that owns the mailbox (tenant scope)
|
|
128
|
+
* @param mailboxId - ID of the mailbox to create
|
|
129
|
+
* @param path - Path of the mailbox to create
|
|
130
|
+
* @param getConnection - Factory to get IMAP connection
|
|
131
|
+
* @param subscribe - Whether to subscribe after creation
|
|
132
|
+
*/
|
|
133
|
+
syncCreate: (accountId: string, mailboxId: string, path: string, getConnection: () => Promise<IImapConnection>, subscribe?: boolean) => Promise<MailboxManagementSyncResult>;
|
|
134
|
+
/**
|
|
135
|
+
* Sync a RENAME operation to IMAP and settle the rows that recorded it (T5).
|
|
136
|
+
* Called by worker after dequeuing MAILBOX_RENAME event.
|
|
137
|
+
*
|
|
138
|
+
* Guarded on the intent, not on the event (D10): the row must still be
|
|
139
|
+
* `pending` with this event's target recorded, or the rename resolves with no
|
|
140
|
+
* IMAP call at all. That is what stops a redelivered MAILBOX_RENAME re-issuing
|
|
141
|
+
* a RENAME from a path that has already moved.
|
|
142
|
+
*
|
|
143
|
+
* @param accountId - Account that owns the mailbox (tenant scope)
|
|
144
|
+
* @param mailboxId - ID of the mailbox to rename
|
|
145
|
+
* @param oldPath - Path the intent was recorded against
|
|
146
|
+
* @param newPath - Target the intent recorded
|
|
147
|
+
* @param getConnection - Factory to get IMAP connection
|
|
148
|
+
*/
|
|
149
|
+
syncRename: (accountId: string, mailboxId: string, oldPath: string, newPath: string, getConnection: () => Promise<IImapConnection>) => Promise<MailboxManagementSyncResult>;
|
|
150
|
+
/**
|
|
151
|
+
* Issue the RENAME and read back the path it resolved to.
|
|
152
|
+
*
|
|
153
|
+
* A `NONEXISTENT` here has three readings, and only one of them may destroy
|
|
154
|
+
* anything. Either another client deleted the folder; or this rename already
|
|
155
|
+
* landed and only its settle was lost, in which case the folder is alive at
|
|
156
|
+
* the target; or the server is saying something this code cannot classify.
|
|
157
|
+
* The listing separates the first two, and everything it cannot answer is a
|
|
158
|
+
* refused rename (T6) — a rename that failed costs the user a retry, while
|
|
159
|
+
* reading an unclear answer as a delete costs them the folder's mail.
|
|
160
|
+
*
|
|
161
|
+
* So the caller is told which of the two it is by the *kind* of error, never
|
|
162
|
+
* by re-reading the server's code: only `FolderGoneUpstreamError` removes a
|
|
163
|
+
* folder, and it is raised exactly where the listing held neither path.
|
|
164
|
+
*/
|
|
165
|
+
private issueRename;
|
|
166
|
+
/**
|
|
167
|
+
* Whether the rename this event was enqueued for is still the folder's live
|
|
168
|
+
* intent. `pending` alone cannot answer it — that state covers a create in
|
|
169
|
+
* flight too — so the recorded target is half the predicate, and it is the
|
|
170
|
+
* same predicate the settling write carries, so the guard and the write
|
|
171
|
+
* cannot disagree between the read and the write.
|
|
172
|
+
*/
|
|
173
|
+
private renameIntentStanding;
|
|
174
|
+
/**
|
|
175
|
+
* T5, per row (D15): each intent-carrying row adopts its own confirmed path,
|
|
176
|
+
* drops the recorded target and settles `synced`, on its own conditional
|
|
177
|
+
* write. A row another client moved in between fails its predicate and is
|
|
178
|
+
* skipped rather than overwritten; a row that is gone is skipped for free.
|
|
179
|
+
*
|
|
180
|
+
* **The named folder is written last, and that ordering is load-bearing.**
|
|
181
|
+
* The redelivery's guard reads that row and nothing else, so it is the
|
|
182
|
+
* witness for the whole settle: while it still carries the intent, a
|
|
183
|
+
* redelivery re-enters and finishes whatever the last attempt left. Settling
|
|
184
|
+
* it first would ack a run that died part-way through its descendants, and
|
|
185
|
+
* those rows would stay `pending` forever — invisible to message sync, and
|
|
186
|
+
* with no route out, because an intent may only be recorded from `synced` or
|
|
187
|
+
* `failed`, so neither a retry nor a dismissal can reach them.
|
|
188
|
+
*/
|
|
189
|
+
private settleRenameIntent;
|
|
190
|
+
/**
|
|
191
|
+
* T6: the rename did not land, so every row that recorded it goes to
|
|
192
|
+
* `failed`, keeping its target so the client can name what the rename was
|
|
193
|
+
* aiming at and offer a retry. Nothing is restored — `fullPath` was never
|
|
194
|
+
* written, which is the whole point of recording the target instead (D2).
|
|
195
|
+
*/
|
|
196
|
+
failRename: (accountId: string, mailboxId: string, oldPath: string, newPath: string, reason: string) => Promise<void>;
|
|
197
|
+
/**
|
|
198
|
+
* The folder the rename was to move was found gone from the server, and
|
|
199
|
+
* the target was not there either, so it was deleted by another client
|
|
200
|
+
* mid-rename (D8). Every row that recorded this rename's intent — the
|
|
201
|
+
* root and each descendant — goes with its mail, the same walk `failRename`
|
|
202
|
+
* uses: deleting the root alone strands every descendant `pending` with
|
|
203
|
+
* nothing left to revisit it, which is the orphaning bug this closes.
|
|
204
|
+
*/
|
|
205
|
+
abandonRenameSubtree: (accountId: string, mailboxId: string, oldPath: string, newPath: string) => Promise<void>;
|
|
206
|
+
/**
|
|
207
|
+
* Sync a DELETE operation to IMAP.
|
|
208
|
+
* Called by worker after dequeuing MAILBOX_DELETE event.
|
|
209
|
+
*
|
|
210
|
+
* @param accountId - Account that owns the mailbox (tenant scope)
|
|
211
|
+
* @param mailboxId - ID of the mailbox to delete
|
|
212
|
+
* @param path - Path of the mailbox to delete
|
|
213
|
+
* @param getConnection - Factory to get IMAP connection
|
|
214
|
+
*/
|
|
215
|
+
syncDelete: (accountId: string, mailboxId: string, path: string, getConnection: () => Promise<IImapConnection>) => Promise<MailboxManagementSyncResult>;
|
|
216
|
+
/**
|
|
217
|
+
* Whether the delete this event was enqueued for is still the folder's live
|
|
218
|
+
* intent (D10). Anything else resolves the job with no IMAP call — never a
|
|
219
|
+
* throw, which would hold back every later job in the account's FIFO group
|
|
220
|
+
* (#339).
|
|
221
|
+
*/
|
|
222
|
+
private deleteIntentStanding;
|
|
223
|
+
/**
|
|
224
|
+
* T8: the confirmed delete takes the folder's local mail with it (D8).
|
|
225
|
+
* Removing the row alone leaves every `message` and `thread_message` row
|
|
226
|
+
* keyed to a dead mailboxId — out of every reader.
|
|
227
|
+
*
|
|
228
|
+
* The removal is batched and the mailbox row goes last, so the row stays
|
|
229
|
+
* `deleting` throughout and an SQS redelivery re-enters the handler, passes
|
|
230
|
+
* the guard above, and resumes where it stopped.
|
|
231
|
+
*/
|
|
232
|
+
settleDelete: (accountId: string, mailboxId: string) => Promise<void>;
|
|
233
|
+
/**
|
|
234
|
+
* T9: the server refused the delete. Nothing is unwound — DELETE is
|
|
235
|
+
* all-or-nothing, so `fullPath` is still what the server holds, and no rename
|
|
236
|
+
* target is written, which is how the client tells a failed delete from a
|
|
237
|
+
* failed rename.
|
|
238
|
+
*/
|
|
239
|
+
failDelete: (accountId: string, mailboxId: string, reason: string) => Promise<void>;
|
|
240
|
+
}
|