@remit/mailbox-service 0.0.112 → 0.0.114
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} +16 -72
- 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} +38 -64
- 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 +129 -0
- package/dist/mailbox-queue.js +216 -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 +280 -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} +13 -68
- 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 -97
- 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 -88
- package/src/account-credentials.ts +0 -210
- 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 -410
- package/src/body-sync-queue.test.ts +0 -64
- package/src/body-sync-queue.ts +0 -131
- package/src/body-sync-terminal.test.ts +0 -148
- package/src/body-sync-terminal.ts +0 -174
- package/src/body-sync.test.ts +0 -925
- package/src/body-sync.ts +0 -1891
- package/src/calendar-parts.ts +0 -41
- package/src/classification-backfill.test.ts +0 -285
- package/src/classification-backfill.ts +0 -363
- package/src/classify-message.test.ts +0 -142
- 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 -262
- package/src/external-move.ts +0 -285
- package/src/filter-operations.test.ts +0 -319
- package/src/filter-operations.ts +0 -377
- package/src/filter-update.test.ts +0 -313
- package/src/filters/filter-config.test.ts +0 -87
- package/src/filters/filter-config.ts +0 -34
- package/src/filters/list-id.test.ts +0 -50
- package/src/filters/match.test.ts +0 -392
- package/src/filters/match.ts +0 -225
- package/src/filters/pipeline.test.ts +0 -199
- package/src/filters/pipeline.ts +0 -170
- package/src/flag-push.test.ts +0 -112
- package/src/flag-push.ts +0 -198
- package/src/flag-queue.test.ts +0 -197
- package/src/flag-queue.ts +0 -509
- package/src/folder-operations.test.ts +0 -273
- package/src/folder-operations.ts +0 -217
- package/src/folder-placement.ts +0 -56
- package/src/forwarded-message.test.ts +0 -122
- 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 -97
- package/src/imapflow-connection.test.ts +0 -503
- package/src/imapflow-connection.ts +0 -1476
- package/src/index.ts +0 -397
- package/src/list-id-backfill.test.ts +0 -207
- 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 -141
- package/src/mailbox-delete-intent.test.ts +0 -119
- package/src/mailbox-management.test.ts +0 -243
- package/src/mailbox-management.ts +0 -838
- package/src/mailbox-presence.test.ts +0 -74
- package/src/mailbox-queue.ts +0 -397
- package/src/mailbox-rename-intent.test.ts +0 -220
- package/src/mailbox-rename-subtree.test.ts +0 -287
- package/src/mailbox-sync.test.ts +0 -286
- package/src/mailbox-sync.ts +0 -724
- package/src/message-move.test.ts +0 -726
- package/src/message-move.ts +0 -1518
- package/src/message-parser.ts +0 -26
- package/src/message-presence.test.ts +0 -50
- package/src/message-read.ts +0 -87
- package/src/message-save-retry.test.ts +0 -90
- package/src/message-sync-flags.test.ts +0 -167
- package/src/message-sync.test.ts +0 -762
- package/src/message-sync.ts +0 -2133
- package/src/mime-walker.test.ts +0 -311
- package/src/mime-walker.ts +0 -306
- package/src/organize-job.ts +0 -93
- 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 -626
- package/src/outbox-attachment.ts +0 -536
- package/src/outbox-failed-recovery.test.ts +0 -299
- package/src/outbox-queue.test.ts +0 -315
- package/src/outbox-queue.ts +0 -525
- package/src/ownership.ts +0 -140
- package/src/placement-move-terminal.ts +0 -145
- package/src/placement-move.ts +0 -363
- package/src/placement-settled.test.ts +0 -147
- package/src/placement.test.ts +0 -404
- package/src/quarantine.test.ts +0 -133
- package/src/quarantine.ts +0 -229
- package/src/refusal.ts +0 -29
- package/src/snippet.test.ts +0 -213
- package/src/snippet.ts +0 -152
- package/src/spam-report.test.ts +0 -329
- package/src/spam-report.ts +0 -357
- package/src/sync-warning.test.ts +0 -213
- package/src/sync-warning.ts +0 -56
- 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/fake-imap.ts +0 -690
- package/src/test-helpers/folder-roles.ts +0 -66
- package/src/test-helpers/mime-tree-generator.ts +0 -461
- package/src/test-helpers/test-queue.ts +0 -56
- package/src/test-helpers/test-store.ts +0 -299
- 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/thread-root.test.ts +0 -29
- package/src/thread-root.ts +0 -51
- package/src/types/mailparser-augment.d.ts +0 -23
- 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/tsconfig.json +0 -8
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { MailboxSyncStatus } from "@remit/domain-enums";
|
|
4
|
-
import {
|
|
5
|
-
isFolderMutationInFlight,
|
|
6
|
-
isMailboxMutationInFlight,
|
|
7
|
-
} from "./mailbox-presence.js";
|
|
8
|
-
import {
|
|
9
|
-
openStore,
|
|
10
|
-
seedAccount,
|
|
11
|
-
seedMailbox,
|
|
12
|
-
} from "./test-helpers/test-store.js";
|
|
13
|
-
|
|
14
|
-
describe("isFolderMutationInFlight", () => {
|
|
15
|
-
for (const [syncStatus, inFlight] of [
|
|
16
|
-
[MailboxSyncStatus.synced, false],
|
|
17
|
-
[MailboxSyncStatus.pending, true],
|
|
18
|
-
[MailboxSyncStatus.deleting, true],
|
|
19
|
-
[MailboxSyncStatus.failed, false],
|
|
20
|
-
] as const) {
|
|
21
|
-
it(`${syncStatus} is ${inFlight ? "" : "not "}a mutation in flight`, () => {
|
|
22
|
-
assert.equal(isFolderMutationInFlight({ syncStatus }), inFlight);
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
it("does not read failed as a folder to leave alone", () => {
|
|
27
|
-
// `failed` means the folder exists at `fullPath` and the last rename or
|
|
28
|
-
// delete intent did not land — so it holds the user's mail and syncing it
|
|
29
|
-
// is right. D7 upholds that by leaving no row at all for a create the
|
|
30
|
-
// server refused. Reading it as in-flight would stop syncing a live folder
|
|
31
|
-
// permanently and silently.
|
|
32
|
-
assert.equal(
|
|
33
|
-
isFolderMutationInFlight({ syncStatus: MailboxSyncStatus.failed }),
|
|
34
|
-
false,
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("isMailboxMutationInFlight", () => {
|
|
40
|
-
it("reads the stored row, and treats an absent one as in flight", async () => {
|
|
41
|
-
const store = await openStore();
|
|
42
|
-
const account = await seedAccount(store);
|
|
43
|
-
const deleting = await seedMailbox(store, account, "Receipts", {
|
|
44
|
-
overrides: { syncStatus: MailboxSyncStatus.deleting },
|
|
45
|
-
});
|
|
46
|
-
const settled = await seedMailbox(store, account, "Work");
|
|
47
|
-
|
|
48
|
-
assert.equal(
|
|
49
|
-
await isMailboxMutationInFlight(
|
|
50
|
-
store.mailbox,
|
|
51
|
-
account.accountId,
|
|
52
|
-
deleting.mailboxId,
|
|
53
|
-
),
|
|
54
|
-
true,
|
|
55
|
-
);
|
|
56
|
-
assert.equal(
|
|
57
|
-
await isMailboxMutationInFlight(
|
|
58
|
-
store.mailbox,
|
|
59
|
-
account.accountId,
|
|
60
|
-
settled.mailboxId,
|
|
61
|
-
),
|
|
62
|
-
false,
|
|
63
|
-
);
|
|
64
|
-
assert.equal(
|
|
65
|
-
await isMailboxMutationInFlight(
|
|
66
|
-
store.mailbox,
|
|
67
|
-
account.accountId,
|
|
68
|
-
"mbx-gone",
|
|
69
|
-
),
|
|
70
|
-
true,
|
|
71
|
-
);
|
|
72
|
-
await store.close();
|
|
73
|
-
});
|
|
74
|
-
});
|
package/src/mailbox-queue.ts
DELETED
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { SendMessageCommand, type SQSClient } from "@aws-sdk/client-sqs";
|
|
3
|
-
import type {
|
|
4
|
-
CreateMailboxInput,
|
|
5
|
-
CreateMailboxResult,
|
|
6
|
-
IMailboxRepository,
|
|
7
|
-
MailboxItem,
|
|
8
|
-
} from "@remit/data-ports";
|
|
9
|
-
import { NotFoundError } from "@remit/data-ports/errors";
|
|
10
|
-
import { MailboxSyncStatus } from "@remit/domain-enums";
|
|
11
|
-
import { createQueueProducer } from "@remit/sqs-client/producer";
|
|
12
|
-
import {
|
|
13
|
-
INTENT_RECORDABLE_FROM,
|
|
14
|
-
recordedByRename,
|
|
15
|
-
refuseContestedIntent,
|
|
16
|
-
} from "./mailbox-intent.js";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* MAILBOX_CREATE event structure (matches remit-imap-worker/events.ts)
|
|
20
|
-
*/
|
|
21
|
-
interface MailboxCreateEvent {
|
|
22
|
-
type: "MAILBOX_CREATE";
|
|
23
|
-
eventId: string;
|
|
24
|
-
timestamp: number;
|
|
25
|
-
accountId: string;
|
|
26
|
-
mailboxId: string;
|
|
27
|
-
path: string;
|
|
28
|
-
subscribe?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* MAILBOX_RENAME event structure (matches remit-imap-worker/events.ts)
|
|
33
|
-
*/
|
|
34
|
-
interface MailboxRenameEvent {
|
|
35
|
-
type: "MAILBOX_RENAME";
|
|
36
|
-
eventId: string;
|
|
37
|
-
timestamp: number;
|
|
38
|
-
accountId: string;
|
|
39
|
-
mailboxId: string;
|
|
40
|
-
oldPath: string;
|
|
41
|
-
newPath: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* MAILBOX_DELETE event structure (matches remit-imap-worker/events.ts)
|
|
46
|
-
*/
|
|
47
|
-
interface MailboxDeleteEvent {
|
|
48
|
-
type: "MAILBOX_DELETE";
|
|
49
|
-
eventId: string;
|
|
50
|
-
timestamp: number;
|
|
51
|
-
accountId: string;
|
|
52
|
-
mailboxId: string;
|
|
53
|
-
path: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
type MailboxManagementEvent =
|
|
57
|
-
| MailboxCreateEvent
|
|
58
|
-
| MailboxRenameEvent
|
|
59
|
-
| MailboxDeleteEvent;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Logger interface
|
|
63
|
-
*/
|
|
64
|
-
export interface MailboxQueueLogger {
|
|
65
|
-
info(obj: Record<string, unknown>, msg: string): void;
|
|
66
|
-
error(obj: Record<string, unknown>, msg: string): void;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const noopLogger: MailboxQueueLogger = {
|
|
70
|
-
info: () => {},
|
|
71
|
-
error: () => {},
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Input for creating a mailbox via the queue service
|
|
76
|
-
*/
|
|
77
|
-
export type CreateMailboxQueueInput = Omit<CreateMailboxInput, "syncStatus">;
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Configuration for MailboxQueueService
|
|
81
|
-
*/
|
|
82
|
-
export interface MailboxQueueConfig {
|
|
83
|
-
mailboxService: IMailboxRepository;
|
|
84
|
-
sqsQueueUrl: string;
|
|
85
|
-
sqsEndpoint?: string;
|
|
86
|
-
logger?: MailboxQueueLogger;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Service for mailbox management with automatic IMAP sync queueing.
|
|
91
|
-
*
|
|
92
|
-
* Implements optimistic local-first pattern:
|
|
93
|
-
* 1. Updates local state immediately
|
|
94
|
-
* 2. Enqueues mailbox management event to SQS for worker to sync to IMAP
|
|
95
|
-
*
|
|
96
|
-
* This follows the same pattern as FlagQueueService (RFC 014).
|
|
97
|
-
*/
|
|
98
|
-
export class MailboxQueueService {
|
|
99
|
-
private mailboxService: IMailboxRepository;
|
|
100
|
-
private sqs: SQSClient;
|
|
101
|
-
private queueUrl: string;
|
|
102
|
-
private log: MailboxQueueLogger;
|
|
103
|
-
|
|
104
|
-
constructor(config: MailboxQueueConfig) {
|
|
105
|
-
const { mailboxService, sqsQueueUrl, sqsEndpoint } = config;
|
|
106
|
-
this.mailboxService = mailboxService;
|
|
107
|
-
this.queueUrl = sqsQueueUrl;
|
|
108
|
-
this.log = config.logger ?? noopLogger;
|
|
109
|
-
|
|
110
|
-
this.sqs = createQueueProducer({
|
|
111
|
-
queueUrl: sqsQueueUrl,
|
|
112
|
-
endpoint: sqsEndpoint,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Create a new mailbox.
|
|
118
|
-
* Updates local state (with syncStatus=pending) and enqueues IMAP CREATE.
|
|
119
|
-
*
|
|
120
|
-
* @param input - The mailbox creation input (without syncStatus)
|
|
121
|
-
* @param accountId - The account ID for the IMAP sync event
|
|
122
|
-
* @param subscribe - Whether to subscribe to the mailbox after creation
|
|
123
|
-
* @returns The created mailbox, or `PathTaken` when the account already has a folder at that path
|
|
124
|
-
*/
|
|
125
|
-
createMailbox = async (
|
|
126
|
-
input: CreateMailboxQueueInput,
|
|
127
|
-
accountId: string,
|
|
128
|
-
subscribe?: boolean,
|
|
129
|
-
): Promise<CreateMailboxResult> => {
|
|
130
|
-
const created = await this.mailboxService.create({
|
|
131
|
-
...input,
|
|
132
|
-
syncStatus: MailboxSyncStatus.pending,
|
|
133
|
-
});
|
|
134
|
-
if (created.outcome === "PathTaken") return created;
|
|
135
|
-
const { mailbox } = created;
|
|
136
|
-
|
|
137
|
-
this.log.info(
|
|
138
|
-
{ mailboxId: mailbox.mailboxId, path: mailbox.fullPath },
|
|
139
|
-
"Created mailbox (local)",
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
// Enqueue IMAP sync
|
|
143
|
-
await this.enqueueEvent({
|
|
144
|
-
type: "MAILBOX_CREATE",
|
|
145
|
-
eventId: randomUUID(),
|
|
146
|
-
timestamp: Date.now(),
|
|
147
|
-
accountId,
|
|
148
|
-
mailboxId: mailbox.mailboxId,
|
|
149
|
-
path: mailbox.fullPath,
|
|
150
|
-
subscribe,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
return created;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Record a rename intent over the folder and every descendant, then enqueue
|
|
158
|
-
* IMAP RENAME.
|
|
159
|
-
*
|
|
160
|
-
* The target is recorded, not the path (D2): every row keeps the `fullPath`
|
|
161
|
-
* the server holds and takes its own rewritten target in `pendingPath`, so a
|
|
162
|
-
* rename that never lands needs no unwinding — dropping `pendingPath` is the
|
|
163
|
-
* whole revert — and nothing that resolves a folder path from a row can
|
|
164
|
-
* resolve one the server does not have.
|
|
165
|
-
*
|
|
166
|
-
* @param mailboxId - The mailbox to rename
|
|
167
|
-
* @param newPath - The path the rename is aiming at
|
|
168
|
-
* @param accountId - The account ID for the IMAP sync event
|
|
169
|
-
* @returns The folder that was named, carrying its recorded target
|
|
170
|
-
*/
|
|
171
|
-
renameMailbox = async (
|
|
172
|
-
mailboxId: string,
|
|
173
|
-
newPath: string,
|
|
174
|
-
accountId: string,
|
|
175
|
-
): Promise<MailboxItem> => {
|
|
176
|
-
// Get current mailbox to capture old path
|
|
177
|
-
const mailbox = await this.mailboxService.get(accountId, mailboxId);
|
|
178
|
-
const oldPath = mailbox.fullPath;
|
|
179
|
-
|
|
180
|
-
// One intent over the folder and every descendant, in one batch:
|
|
181
|
-
// IMAP RENAME moves the subtree in one command, so a partially recorded
|
|
182
|
-
// rename is the state that produced today's phantom rows (D6). A
|
|
183
|
-
// descendant already mid-mutation refuses the whole rename.
|
|
184
|
-
const written = await this.mailboxService.transitionSubtree(
|
|
185
|
-
accountId,
|
|
186
|
-
mailboxId,
|
|
187
|
-
{
|
|
188
|
-
from: INTENT_RECORDABLE_FROM,
|
|
189
|
-
to: MailboxSyncStatus.pending,
|
|
190
|
-
newPath,
|
|
191
|
-
},
|
|
192
|
-
);
|
|
193
|
-
if (!written) {
|
|
194
|
-
return refuseContestedIntent(
|
|
195
|
-
this.mailboxService,
|
|
196
|
-
accountId,
|
|
197
|
-
mailboxId,
|
|
198
|
-
"subtree",
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
const updated = written.find((row) => row.mailboxId === mailboxId);
|
|
202
|
-
if (!updated) throw new NotFoundError(`Mailbox not found: ${mailboxId}`);
|
|
203
|
-
|
|
204
|
-
this.log.info(
|
|
205
|
-
{
|
|
206
|
-
accountId,
|
|
207
|
-
mailboxId,
|
|
208
|
-
intent: "rename",
|
|
209
|
-
from: mailbox.syncStatus,
|
|
210
|
-
to: MailboxSyncStatus.pending,
|
|
211
|
-
oldPath,
|
|
212
|
-
newPath,
|
|
213
|
-
subtreeSize: written.length,
|
|
214
|
-
},
|
|
215
|
-
"Recorded rename intent",
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
// Enqueue IMAP sync
|
|
219
|
-
await this.enqueueEvent({
|
|
220
|
-
type: "MAILBOX_RENAME",
|
|
221
|
-
eventId: randomUUID(),
|
|
222
|
-
timestamp: Date.now(),
|
|
223
|
-
accountId,
|
|
224
|
-
mailboxId,
|
|
225
|
-
oldPath,
|
|
226
|
-
newPath,
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
return updated;
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* The route out of a failed rename or a failed delete that keeps the folder
|
|
234
|
-
* as it is (T10, T11): the recorded target is dropped and the row settles
|
|
235
|
-
* back to `synced`. Nothing is enqueued — `fullPath` was never written, so
|
|
236
|
-
* the server has nothing to undo.
|
|
237
|
-
*
|
|
238
|
-
* Reached without new API surface, from a PATCH whose `fullPath` equals the
|
|
239
|
-
* row's confirmed one. From `synced` it is a no-op; from a state with a
|
|
240
|
-
* mutation in flight it is a 409, because dismissing an intent that is still
|
|
241
|
-
* running would leave the settle with no row to write.
|
|
242
|
-
*/
|
|
243
|
-
dismissMailboxIntent = async (
|
|
244
|
-
mailboxId: string,
|
|
245
|
-
accountId: string,
|
|
246
|
-
): Promise<MailboxItem> => {
|
|
247
|
-
const current = await this.mailboxService.get(accountId, mailboxId);
|
|
248
|
-
if (current.syncStatus === MailboxSyncStatus.synced) return current;
|
|
249
|
-
if (current.syncStatus !== MailboxSyncStatus.failed) {
|
|
250
|
-
return refuseContestedIntent(
|
|
251
|
-
this.mailboxService,
|
|
252
|
-
accountId,
|
|
253
|
-
mailboxId,
|
|
254
|
-
"folder",
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// A failed rename was recorded over a subtree, so it is dismissed over the
|
|
259
|
-
// same one. A failed delete carries no target and was recorded on the
|
|
260
|
-
// folder alone, so there is nothing else to clear.
|
|
261
|
-
const target = current.pendingPath;
|
|
262
|
-
const alsoRecorded =
|
|
263
|
-
target === undefined
|
|
264
|
-
? []
|
|
265
|
-
: (
|
|
266
|
-
await this.mailboxService.findBySyncStatus(
|
|
267
|
-
accountId,
|
|
268
|
-
MailboxSyncStatus.failed,
|
|
269
|
-
)
|
|
270
|
-
).filter(
|
|
271
|
-
(row) =>
|
|
272
|
-
row.mailboxId !== mailboxId &&
|
|
273
|
-
recordedByRename(
|
|
274
|
-
row,
|
|
275
|
-
current.fullPath,
|
|
276
|
-
target,
|
|
277
|
-
current.hierarchyDelimiter,
|
|
278
|
-
),
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
let dismissed: MailboxItem | undefined;
|
|
282
|
-
// The named folder first, so a caller always gets back the row it asked
|
|
283
|
-
// about. Each write carries its own predicate, so a row somebody else
|
|
284
|
-
// moved between the read and the write is skipped, not overwritten (D3).
|
|
285
|
-
for (const row of [current, ...alsoRecorded]) {
|
|
286
|
-
const written = await this.mailboxService.transition(
|
|
287
|
-
accountId,
|
|
288
|
-
row.mailboxId,
|
|
289
|
-
{
|
|
290
|
-
from: [MailboxSyncStatus.failed],
|
|
291
|
-
wherePendingPath: row.pendingPath ?? null,
|
|
292
|
-
to: MailboxSyncStatus.synced,
|
|
293
|
-
},
|
|
294
|
-
);
|
|
295
|
-
if (row.mailboxId === mailboxId) dismissed = written ?? undefined;
|
|
296
|
-
this.log.info(
|
|
297
|
-
{
|
|
298
|
-
accountId,
|
|
299
|
-
mailboxId: row.mailboxId,
|
|
300
|
-
intent: "dismiss",
|
|
301
|
-
from: MailboxSyncStatus.failed,
|
|
302
|
-
to: MailboxSyncStatus.synced,
|
|
303
|
-
outcome: written ? "settled" : "superseded",
|
|
304
|
-
},
|
|
305
|
-
"Dismissed folder intent",
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (dismissed) return dismissed;
|
|
310
|
-
// The row moved between the read and its write. Re-read rather than
|
|
311
|
-
// reporting a dismissal that did not happen.
|
|
312
|
-
return this.mailboxService.get(accountId, mailboxId);
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Record a delete intent (T7) and enqueue IMAP DELETE.
|
|
317
|
-
*
|
|
318
|
-
* The row stays at `deleting` and stays visible until the worker confirms the
|
|
319
|
-
* server delete and removes it with the folder's mail (D8, D11). The enqueue
|
|
320
|
-
* happens only after the transition wins, and an enqueue failure stays thrown
|
|
321
|
-
* — a 500 to the caller, loud — rather than being swallowed or rolled back.
|
|
322
|
-
*
|
|
323
|
-
* @param mailboxId - The mailbox to delete
|
|
324
|
-
* @param accountId - The account ID for the IMAP sync event
|
|
325
|
-
*/
|
|
326
|
-
deleteMailbox = async (
|
|
327
|
-
mailboxId: string,
|
|
328
|
-
accountId: string,
|
|
329
|
-
): Promise<void> => {
|
|
330
|
-
// Get current mailbox to capture path
|
|
331
|
-
const mailbox = await this.mailboxService.get(accountId, mailboxId);
|
|
332
|
-
|
|
333
|
-
const recorded = await this.mailboxService.transition(
|
|
334
|
-
accountId,
|
|
335
|
-
mailboxId,
|
|
336
|
-
{ from: INTENT_RECORDABLE_FROM, to: MailboxSyncStatus.deleting },
|
|
337
|
-
);
|
|
338
|
-
if (!recorded) {
|
|
339
|
-
await refuseContestedIntent(
|
|
340
|
-
this.mailboxService,
|
|
341
|
-
accountId,
|
|
342
|
-
mailboxId,
|
|
343
|
-
"folder",
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
this.log.info(
|
|
348
|
-
{
|
|
349
|
-
accountId,
|
|
350
|
-
mailboxId,
|
|
351
|
-
intent: "delete",
|
|
352
|
-
from: mailbox.syncStatus,
|
|
353
|
-
to: MailboxSyncStatus.deleting,
|
|
354
|
-
path: mailbox.fullPath,
|
|
355
|
-
},
|
|
356
|
-
"Recorded delete intent",
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
// Enqueue IMAP sync
|
|
360
|
-
await this.enqueueEvent({
|
|
361
|
-
type: "MAILBOX_DELETE",
|
|
362
|
-
eventId: randomUUID(),
|
|
363
|
-
timestamp: Date.now(),
|
|
364
|
-
accountId,
|
|
365
|
-
mailboxId,
|
|
366
|
-
path: mailbox.fullPath,
|
|
367
|
-
});
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Enqueue a mailbox management event to SQS.
|
|
372
|
-
*
|
|
373
|
-
* FIFO queues require MessageGroupId; standard queues reject it. We detect
|
|
374
|
-
* FIFO queues by the `.fifo` suffix on the queue URL and scope ordering to
|
|
375
|
-
* the account so events for different accounts can be processed in parallel.
|
|
376
|
-
*/
|
|
377
|
-
private enqueueEvent = async (
|
|
378
|
-
event: MailboxManagementEvent,
|
|
379
|
-
): Promise<void> => {
|
|
380
|
-
const useFifo = this.queueUrl.endsWith(".fifo");
|
|
381
|
-
await this.sqs.send(
|
|
382
|
-
new SendMessageCommand({
|
|
383
|
-
QueueUrl: this.queueUrl,
|
|
384
|
-
MessageBody: JSON.stringify(event),
|
|
385
|
-
...(useFifo && {
|
|
386
|
-
MessageGroupId: event.accountId,
|
|
387
|
-
MessageDeduplicationId: `${event.type}:${event.mailboxId}:${event.eventId}`,
|
|
388
|
-
}),
|
|
389
|
-
}),
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
this.log.info(
|
|
393
|
-
{ eventId: event.eventId, type: event.type, mailboxId: event.mailboxId },
|
|
394
|
-
`Enqueued ${event.type} event`,
|
|
395
|
-
);
|
|
396
|
-
};
|
|
397
|
-
}
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
-
import type { AccountItem, MailboxItem } from "@remit/data-ports";
|
|
4
|
-
import { MailboxSyncStatus } from "@remit/domain-enums";
|
|
5
|
-
import { MailboxQueueService } from "./mailbox-queue.js";
|
|
6
|
-
import {
|
|
7
|
-
beforeNext,
|
|
8
|
-
captureQueue,
|
|
9
|
-
openStore,
|
|
10
|
-
type QueueCapture,
|
|
11
|
-
type QueuedEvent,
|
|
12
|
-
type Store,
|
|
13
|
-
seedAccount,
|
|
14
|
-
seedMailbox,
|
|
15
|
-
} from "./test-helpers/test-store.js";
|
|
16
|
-
|
|
17
|
-
let store: Store;
|
|
18
|
-
let capture: QueueCapture;
|
|
19
|
-
let queue: QueuedEvent[];
|
|
20
|
-
let account: AccountItem;
|
|
21
|
-
|
|
22
|
-
beforeEach(async () => {
|
|
23
|
-
capture = captureQueue();
|
|
24
|
-
queue = capture.sent;
|
|
25
|
-
store = await openStore();
|
|
26
|
-
account = await seedAccount(store);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
afterEach(async () => {
|
|
30
|
-
capture.restore();
|
|
31
|
-
await store.close();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const folder = async (
|
|
35
|
-
fullPath: string,
|
|
36
|
-
state?: { syncStatus: MailboxItem["syncStatus"]; pendingPath?: string },
|
|
37
|
-
): Promise<MailboxItem> => {
|
|
38
|
-
const mailbox = await seedMailbox(store, account, fullPath);
|
|
39
|
-
if (!state) return mailbox;
|
|
40
|
-
const moved = await store.mailbox.transition(
|
|
41
|
-
account.accountId,
|
|
42
|
-
mailbox.mailboxId,
|
|
43
|
-
{
|
|
44
|
-
from: [MailboxSyncStatus.synced],
|
|
45
|
-
to: state.syncStatus,
|
|
46
|
-
set: state.pendingPath ? { pendingPath: state.pendingPath } : {},
|
|
47
|
-
},
|
|
48
|
-
);
|
|
49
|
-
if (!moved) throw new Error(`could not put ${fullPath} in its state`);
|
|
50
|
-
return moved;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const queueService = () =>
|
|
54
|
-
new MailboxQueueService({
|
|
55
|
-
mailboxService: store.mailbox,
|
|
56
|
-
sqsQueueUrl: "http://localhost:4566/000000000000/mailbox",
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const rename = (mailbox: MailboxItem, newPath: string) =>
|
|
60
|
-
queueService().renameMailbox(mailbox.mailboxId, newPath, account.accountId);
|
|
61
|
-
|
|
62
|
-
const dismiss = (mailbox: MailboxItem) =>
|
|
63
|
-
queueService().dismissMailboxIntent(mailbox.mailboxId, account.accountId);
|
|
64
|
-
|
|
65
|
-
const current = (mailbox: MailboxItem) =>
|
|
66
|
-
store.mailbox.get(account.accountId, mailbox.mailboxId);
|
|
67
|
-
|
|
68
|
-
const rejectsWith409 = (message: string) => (error: unknown) =>
|
|
69
|
-
(error as { statusCode?: number }).statusCode === 409 &&
|
|
70
|
-
(error as Error).message === message;
|
|
71
|
-
|
|
72
|
-
describe("MailboxQueueService.renameMailbox records the intent", () => {
|
|
73
|
-
it("records the target over the subtree and leaves every confirmed path alone", async () => {
|
|
74
|
-
const parent = await folder("Work");
|
|
75
|
-
const child = await folder("Work/2026");
|
|
76
|
-
const lookalike = await folder("Workshop");
|
|
77
|
-
|
|
78
|
-
const renamed = await rename(parent, "Projects");
|
|
79
|
-
|
|
80
|
-
assert.equal(renamed.mailboxId, parent.mailboxId);
|
|
81
|
-
assert.equal((await current(parent)).fullPath, "Work");
|
|
82
|
-
assert.equal((await current(parent)).pendingPath, "Projects");
|
|
83
|
-
const movedChild = await current(child);
|
|
84
|
-
assert.equal(movedChild.fullPath, "Work/2026");
|
|
85
|
-
assert.equal(movedChild.pendingPath, "Projects/2026");
|
|
86
|
-
assert.equal(movedChild.syncStatus, MailboxSyncStatus.pending);
|
|
87
|
-
assert.equal(
|
|
88
|
-
(await current(lookalike)).syncStatus,
|
|
89
|
-
MailboxSyncStatus.synced,
|
|
90
|
-
);
|
|
91
|
-
assert.equal((await current(lookalike)).pendingPath, undefined);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("records the path the RENAME carries, `$` patterns and all", async () => {
|
|
95
|
-
const parent = await folder("Work");
|
|
96
|
-
const child = await folder("Work/2026");
|
|
97
|
-
|
|
98
|
-
await rename(parent, "Q1$&Q2");
|
|
99
|
-
|
|
100
|
-
assert.equal((await current(child)).pendingPath, "Q1$&Q2/2026");
|
|
101
|
-
assert.deepEqual(
|
|
102
|
-
queue.map((event) => event.body.newPath),
|
|
103
|
-
["Q1$&Q2"],
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("records a retry from a failed rename onto the same row", async () => {
|
|
108
|
-
const parent = await folder("Work", {
|
|
109
|
-
syncStatus: MailboxSyncStatus.failed,
|
|
110
|
-
pendingPath: "Projects",
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
await rename(parent, "Projects");
|
|
114
|
-
|
|
115
|
-
const retried = await current(parent);
|
|
116
|
-
assert.equal(retried.syncStatus, MailboxSyncStatus.pending);
|
|
117
|
-
assert.equal(retried.pendingPath, "Projects");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("refuses the whole subtree with a 409 naming the descendant that blocks it", async () => {
|
|
121
|
-
const parent = await folder("Archive");
|
|
122
|
-
await folder("Archive/2025/Receipts", {
|
|
123
|
-
syncStatus: MailboxSyncStatus.pending,
|
|
124
|
-
pendingPath: "Archive/2025/Bonnen",
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
await assert.rejects(
|
|
128
|
-
rename(parent, "Archief"),
|
|
129
|
-
rejectsWith409(
|
|
130
|
-
'A rename is in progress for "Archive/2025/Receipts", inside this folder.',
|
|
131
|
-
),
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
assert.equal((await current(parent)).syncStatus, MailboxSyncStatus.synced);
|
|
135
|
-
assert.equal(queue.length, 0);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("refuses with a 409 naming the folder's own delete", async () => {
|
|
139
|
-
const parent = await folder("Archive", {
|
|
140
|
-
syncStatus: MailboxSyncStatus.deleting,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
await assert.rejects(
|
|
144
|
-
rename(parent, "Archief"),
|
|
145
|
-
rejectsWith409('A delete is already in progress for "Archive".'),
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("answers 404 when the folder went between the read and the intent", async () => {
|
|
150
|
-
const parent = await folder("Work");
|
|
151
|
-
beforeNext(store.mailbox, "transitionSubtree", () =>
|
|
152
|
-
store.mailbox.delete(account.accountId, parent.mailboxId),
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
await assert.rejects(
|
|
156
|
-
rename(parent, "Projects"),
|
|
157
|
-
(error: unknown) => (error as Error).name === "NotFoundError",
|
|
158
|
-
);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe("MailboxQueueService.dismissMailboxIntent", () => {
|
|
163
|
-
it("clears a failed rename over its whole subtree and enqueues nothing", async () => {
|
|
164
|
-
const parent = await folder("Work", {
|
|
165
|
-
syncStatus: MailboxSyncStatus.failed,
|
|
166
|
-
pendingPath: "Projects",
|
|
167
|
-
});
|
|
168
|
-
const child = await folder("Work/2026", {
|
|
169
|
-
syncStatus: MailboxSyncStatus.failed,
|
|
170
|
-
pendingPath: "Projects/2026",
|
|
171
|
-
});
|
|
172
|
-
const other = await folder("Other", {
|
|
173
|
-
syncStatus: MailboxSyncStatus.failed,
|
|
174
|
-
pendingPath: "Projects/Old",
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const dismissed = await dismiss(parent);
|
|
178
|
-
|
|
179
|
-
assert.equal(dismissed.syncStatus, MailboxSyncStatus.synced);
|
|
180
|
-
for (const mailbox of [parent, child]) {
|
|
181
|
-
const row = await current(mailbox);
|
|
182
|
-
assert.equal(row.syncStatus, MailboxSyncStatus.synced);
|
|
183
|
-
assert.equal(row.pendingPath, undefined);
|
|
184
|
-
}
|
|
185
|
-
assert.equal((await current(other)).pendingPath, "Projects/Old");
|
|
186
|
-
assert.equal(queue.length, 0);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("dismisses a failed delete on the named folder alone", async () => {
|
|
190
|
-
const parent = await folder("Work", {
|
|
191
|
-
syncStatus: MailboxSyncStatus.failed,
|
|
192
|
-
});
|
|
193
|
-
const child = await folder("Work/2026", {
|
|
194
|
-
syncStatus: MailboxSyncStatus.failed,
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
await dismiss(parent);
|
|
198
|
-
|
|
199
|
-
assert.equal((await current(parent)).syncStatus, MailboxSyncStatus.synced);
|
|
200
|
-
assert.equal((await current(child)).syncStatus, MailboxSyncStatus.failed);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("leaves a folder with nothing to dismiss as it is", async () => {
|
|
204
|
-
const work = await folder("Work");
|
|
205
|
-
|
|
206
|
-
assert.equal((await dismiss(work)).syncStatus, MailboxSyncStatus.synced);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("refuses with a 409 while a mutation is still in flight", async () => {
|
|
210
|
-
const work = await folder("Work", {
|
|
211
|
-
syncStatus: MailboxSyncStatus.pending,
|
|
212
|
-
pendingPath: "Projects",
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
await assert.rejects(
|
|
216
|
-
dismiss(work),
|
|
217
|
-
rejectsWith409('A rename is in progress for "Work".'),
|
|
218
|
-
);
|
|
219
|
-
});
|
|
220
|
-
});
|