@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,287 +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 { MailboxManagementService } from "./mailbox-management.js";
|
|
6
|
-
import { FolderGoneUpstreamError } from "./mailbox-upstream.js";
|
|
7
|
-
import {
|
|
8
|
-
type FakeImapConnection,
|
|
9
|
-
FakeImapServer,
|
|
10
|
-
} from "./test-helpers/fake-imap.js";
|
|
11
|
-
import {
|
|
12
|
-
beforeNext,
|
|
13
|
-
openStore,
|
|
14
|
-
type Store,
|
|
15
|
-
seedAccount,
|
|
16
|
-
seedMailbox,
|
|
17
|
-
} from "./test-helpers/test-store.js";
|
|
18
|
-
|
|
19
|
-
let store: Store;
|
|
20
|
-
let imap: FakeImapServer;
|
|
21
|
-
let account: AccountItem;
|
|
22
|
-
|
|
23
|
-
beforeEach(async () => {
|
|
24
|
-
store = await openStore();
|
|
25
|
-
imap = new FakeImapServer();
|
|
26
|
-
account = await seedAccount(store);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
afterEach(() => store.close());
|
|
30
|
-
|
|
31
|
-
const folder = async (
|
|
32
|
-
fullPath: string,
|
|
33
|
-
pendingPath?: string,
|
|
34
|
-
overrides: Partial<MailboxItem> = {},
|
|
35
|
-
): Promise<MailboxItem> => {
|
|
36
|
-
imap.addFolder(fullPath);
|
|
37
|
-
const mailbox = await seedMailbox(store, account, fullPath, { overrides });
|
|
38
|
-
if (!pendingPath) return mailbox;
|
|
39
|
-
const recorded = await store.mailbox.transition(
|
|
40
|
-
account.accountId,
|
|
41
|
-
mailbox.mailboxId,
|
|
42
|
-
{
|
|
43
|
-
from: [MailboxSyncStatus.synced],
|
|
44
|
-
to: MailboxSyncStatus.pending,
|
|
45
|
-
set: { pendingPath },
|
|
46
|
-
},
|
|
47
|
-
);
|
|
48
|
-
if (!recorded) throw new Error(`could not record a rename on ${fullPath}`);
|
|
49
|
-
return recorded;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const current = (mailbox: MailboxItem) =>
|
|
53
|
-
store.mailbox.get(account.accountId, mailbox.mailboxId);
|
|
54
|
-
|
|
55
|
-
const management = () => new MailboxManagementService(store.mailbox);
|
|
56
|
-
|
|
57
|
-
const renameWork = (connection: FakeImapConnection = imap.connection()) =>
|
|
58
|
-
management().syncRename(
|
|
59
|
-
account.accountId,
|
|
60
|
-
workId as string,
|
|
61
|
-
"Work",
|
|
62
|
-
"Projects",
|
|
63
|
-
async () => connection,
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
let workId: string | undefined;
|
|
67
|
-
|
|
68
|
-
const work = async (
|
|
69
|
-
pendingPath: string | null = "Projects",
|
|
70
|
-
overrides: Partial<MailboxItem> = {},
|
|
71
|
-
) => {
|
|
72
|
-
const mailbox = await folder("Work", pendingPath ?? undefined, overrides);
|
|
73
|
-
workId = mailbox.mailboxId;
|
|
74
|
-
return mailbox;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const assertAt = async (
|
|
78
|
-
mailbox: MailboxItem,
|
|
79
|
-
fullPath: string,
|
|
80
|
-
syncStatus: MailboxItem["syncStatus"],
|
|
81
|
-
pendingPath?: string,
|
|
82
|
-
) => {
|
|
83
|
-
const row = await current(mailbox);
|
|
84
|
-
assert.equal(row.fullPath, fullPath);
|
|
85
|
-
assert.equal(row.syncStatus, syncStatus);
|
|
86
|
-
assert.equal(row.pendingPath, pendingPath);
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
describe("MailboxManagementService.syncRename settles one intent", () => {
|
|
90
|
-
it("adopts the path the server resolved over the one requested", async () => {
|
|
91
|
-
const parent = await work();
|
|
92
|
-
const child = await folder("Work/2026", "Projects/2026");
|
|
93
|
-
imap.namespacePrefix = "INBOX/";
|
|
94
|
-
|
|
95
|
-
await renameWork();
|
|
96
|
-
|
|
97
|
-
await assertAt(parent, "INBOX/Projects", MailboxSyncStatus.synced);
|
|
98
|
-
await assertAt(child, "INBOX/Projects/2026", MailboxSyncStatus.synced);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("leaves rows it must not claim alone", async () => {
|
|
102
|
-
const parent = await work();
|
|
103
|
-
const interloper = await folder("Projects/Fresh");
|
|
104
|
-
const elsewhere = await folder("Admin", "Beheer");
|
|
105
|
-
const sameBranch = await folder("Other", "Projects/Old");
|
|
106
|
-
const offRebase = await folder("Work/2026", "Projects/Archief");
|
|
107
|
-
|
|
108
|
-
await renameWork();
|
|
109
|
-
|
|
110
|
-
await assertAt(parent, "Projects", MailboxSyncStatus.synced);
|
|
111
|
-
await assertAt(interloper, "Projects/Fresh", MailboxSyncStatus.synced);
|
|
112
|
-
await assertAt(elsewhere, "Admin", MailboxSyncStatus.pending, "Beheer");
|
|
113
|
-
await assertAt(
|
|
114
|
-
sameBranch,
|
|
115
|
-
"Other",
|
|
116
|
-
MailboxSyncStatus.pending,
|
|
117
|
-
"Projects/Old",
|
|
118
|
-
);
|
|
119
|
-
await assertAt(
|
|
120
|
-
offRebase,
|
|
121
|
-
"Work/2026",
|
|
122
|
-
MailboxSyncStatus.pending,
|
|
123
|
-
"Projects/Archief",
|
|
124
|
-
);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("settles the rest when a descendant is deleted mid-settle", async () => {
|
|
128
|
-
const parent = await work();
|
|
129
|
-
const gone = await folder("Work/Gone", "Projects/Gone");
|
|
130
|
-
const child = await folder("Work/2026", "Projects/2026");
|
|
131
|
-
beforeNext(store.mailbox, "transition", () =>
|
|
132
|
-
store.mailbox.delete(account.accountId, gone.mailboxId),
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const result = await renameWork();
|
|
136
|
-
|
|
137
|
-
assert.equal(result.success, true);
|
|
138
|
-
await assertAt(parent, "Projects", MailboxSyncStatus.synced);
|
|
139
|
-
await assertAt(child, "Projects/2026", MailboxSyncStatus.synced);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
for (const [label, pendingPath] of [
|
|
143
|
-
["already settled", null],
|
|
144
|
-
["pending for another target", "Beheer"],
|
|
145
|
-
] as const) {
|
|
146
|
-
it(`issues no RENAME when the row is ${label}`, async () => {
|
|
147
|
-
await work(pendingPath);
|
|
148
|
-
|
|
149
|
-
const result = await renameWork();
|
|
150
|
-
|
|
151
|
-
assert.equal(result.success, true);
|
|
152
|
-
assert.ok(imap.hasFolder("Work"));
|
|
153
|
-
assert.equal(imap.hasFolder("Projects"), false);
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe("MailboxManagementService.syncRename reads a NONEXISTENT answer", () => {
|
|
159
|
-
it("settles when the source has moved because the rename already landed", async () => {
|
|
160
|
-
const parent = await work();
|
|
161
|
-
await imap.connection().renameMailbox("Work", "Projects");
|
|
162
|
-
|
|
163
|
-
const result = await renameWork();
|
|
164
|
-
|
|
165
|
-
assert.equal(result.success, true);
|
|
166
|
-
await assertAt(parent, "Projects", MailboxSyncStatus.synced);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("reads the target through the account's namespace prefix", async () => {
|
|
170
|
-
const parent = await work("Projects", {
|
|
171
|
-
hierarchyDelimiter: ".",
|
|
172
|
-
namespacePrefix: "INBOX.",
|
|
173
|
-
});
|
|
174
|
-
imap.removeFolder("Work");
|
|
175
|
-
imap.addFolder("INBOX.Projects");
|
|
176
|
-
|
|
177
|
-
const result = await renameWork();
|
|
178
|
-
|
|
179
|
-
assert.equal(result.success, true);
|
|
180
|
-
assert.equal((await current(parent)).syncStatus, MailboxSyncStatus.synced);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("reports the folder gone, carrying the server's answer, when neither path is listed", async () => {
|
|
184
|
-
await work();
|
|
185
|
-
imap.removeFolder("Work");
|
|
186
|
-
|
|
187
|
-
await assert.rejects(
|
|
188
|
-
renameWork(),
|
|
189
|
-
(error: unknown) =>
|
|
190
|
-
error instanceof FolderGoneUpstreamError &&
|
|
191
|
-
(error.cause as { serverResponseCode?: string })?.serverResponseCode ===
|
|
192
|
-
"NONEXISTENT",
|
|
193
|
-
);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("refuses rather than confirming a delete when the source is still listed", async () => {
|
|
197
|
-
await work();
|
|
198
|
-
const connection = imap.connection();
|
|
199
|
-
connection.renameMailbox = async () => {
|
|
200
|
-
throw Object.assign(new Error("Command failed"), {
|
|
201
|
-
serverResponseCode: "NONEXISTENT",
|
|
202
|
-
});
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
await assert.rejects(
|
|
206
|
-
renameWork(connection),
|
|
207
|
-
(error: unknown) =>
|
|
208
|
-
!(error instanceof FolderGoneUpstreamError) &&
|
|
209
|
-
(error as { serverResponseCode?: string }).serverResponseCode ===
|
|
210
|
-
"NONEXISTENT",
|
|
211
|
-
);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("refuses rather than confirming a delete when the listing cannot be read", async () => {
|
|
215
|
-
await work();
|
|
216
|
-
imap.removeFolder("Work");
|
|
217
|
-
const connection = imap.connection();
|
|
218
|
-
connection.listMailboxes = async () => {
|
|
219
|
-
throw new Error("IMAP connection lost");
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
await assert.rejects(
|
|
223
|
-
renameWork(connection),
|
|
224
|
-
(error: unknown) => !(error instanceof FolderGoneUpstreamError),
|
|
225
|
-
);
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
describe("MailboxManagementService.failRename", () => {
|
|
230
|
-
const fail = () =>
|
|
231
|
-
management().failRename(
|
|
232
|
-
account.accountId,
|
|
233
|
-
workId as string,
|
|
234
|
-
"Work",
|
|
235
|
-
"Projects",
|
|
236
|
-
"Permission denied",
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
it("marks every row this rename recorded failed, keeping path and target", async () => {
|
|
240
|
-
const parent = await work();
|
|
241
|
-
const child = await folder("Work/2026", "Projects/2026");
|
|
242
|
-
const other = await folder("Other", "Projects/Old");
|
|
243
|
-
|
|
244
|
-
await fail();
|
|
245
|
-
|
|
246
|
-
await assertAt(parent, "Work", MailboxSyncStatus.failed, "Projects");
|
|
247
|
-
await assertAt(
|
|
248
|
-
child,
|
|
249
|
-
"Work/2026",
|
|
250
|
-
MailboxSyncStatus.failed,
|
|
251
|
-
"Projects/2026",
|
|
252
|
-
);
|
|
253
|
-
await assertAt(other, "Other", MailboxSyncStatus.pending, "Projects/Old");
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it("raises a NotFoundError when the folder row is gone", async () => {
|
|
257
|
-
workId = "mbx-gone";
|
|
258
|
-
|
|
259
|
-
await assert.rejects(
|
|
260
|
-
fail(),
|
|
261
|
-
(error: unknown) => (error as Error).name === "NotFoundError",
|
|
262
|
-
);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("records nothing, quietly, for a row that carries no intent", async () => {
|
|
266
|
-
const parent = await work(null);
|
|
267
|
-
|
|
268
|
-
await fail();
|
|
269
|
-
|
|
270
|
-
await assertAt(parent, "Work", MailboxSyncStatus.synced);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("fails a rename the connection never issued as a refusal", async () => {
|
|
274
|
-
const parent = await work();
|
|
275
|
-
const connection = imap.connection();
|
|
276
|
-
connection.renameMailbox = async () => {
|
|
277
|
-
throw new Error(
|
|
278
|
-
'Rename of "Work" was not issued: the IMAP connection is not ready',
|
|
279
|
-
);
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
await assert.rejects(renameWork(connection), /not ready/);
|
|
283
|
-
await fail();
|
|
284
|
-
|
|
285
|
-
await assertAt(parent, "Work", MailboxSyncStatus.failed, "Projects");
|
|
286
|
-
});
|
|
287
|
-
});
|
package/src/mailbox-sync.test.ts
DELETED
|
@@ -1,286 +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 {
|
|
5
|
-
MailboxCursorState,
|
|
6
|
-
MailboxSpecialUse,
|
|
7
|
-
MailboxSyncStatus,
|
|
8
|
-
} from "@remit/domain-enums";
|
|
9
|
-
import { parseImapAttributes } from "./attribute-mapper.js";
|
|
10
|
-
import { MailboxSyncService } from "./mailbox-sync.js";
|
|
11
|
-
import {
|
|
12
|
-
eml,
|
|
13
|
-
type FakeImapConnection,
|
|
14
|
-
FakeImapServer,
|
|
15
|
-
} from "./test-helpers/fake-imap.js";
|
|
16
|
-
import {
|
|
17
|
-
openStore,
|
|
18
|
-
type Store,
|
|
19
|
-
seedAccount,
|
|
20
|
-
seedMailbox,
|
|
21
|
-
silentLog,
|
|
22
|
-
} from "./test-helpers/test-store.js";
|
|
23
|
-
|
|
24
|
-
describe("parseImapAttributes – locale invariance (#194)", () => {
|
|
25
|
-
it("recognizes \\Sent regardless of folder name", () => {
|
|
26
|
-
const dutch = parseImapAttributes(["\\HasNoChildren", "\\Sent"]);
|
|
27
|
-
assert.deepEqual(dutch.specialUse, [MailboxSpecialUse.Sent]);
|
|
28
|
-
|
|
29
|
-
const english = parseImapAttributes(["\\Sent"]);
|
|
30
|
-
assert.deepEqual(english.specialUse, [MailboxSpecialUse.Sent]);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("recognizes \\Drafts on a localized 'Concepten' folder", () => {
|
|
34
|
-
const parsed = parseImapAttributes(["\\Drafts"]);
|
|
35
|
-
assert.deepEqual(parsed.specialUse, [MailboxSpecialUse.Drafts]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("returns an empty list when no flag is present", () => {
|
|
39
|
-
const parsed = parseImapAttributes(["\\HasNoChildren"]);
|
|
40
|
-
assert.deepEqual(parsed.specialUse, []);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("normalizes case: \\sent is treated the same as \\Sent", () => {
|
|
44
|
-
const parsed = parseImapAttributes(["\\sent"]);
|
|
45
|
-
assert.deepEqual(parsed.specialUse, [MailboxSpecialUse.Sent]);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
let store: Store;
|
|
50
|
-
let imap: FakeImapServer;
|
|
51
|
-
let account: AccountItem;
|
|
52
|
-
|
|
53
|
-
beforeEach(async () => {
|
|
54
|
-
store = await openStore();
|
|
55
|
-
imap = new FakeImapServer();
|
|
56
|
-
account = await seedAccount(store);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
afterEach(() => store.close());
|
|
60
|
-
|
|
61
|
-
const sweep = (connection: FakeImapConnection = imap.connection()) =>
|
|
62
|
-
new MailboxSyncService(
|
|
63
|
-
store.mailbox,
|
|
64
|
-
store.mailboxSpecialUse,
|
|
65
|
-
silentLog,
|
|
66
|
-
).syncMailboxes({ accountId: account.accountId }, connection);
|
|
67
|
-
|
|
68
|
-
const rows = async () =>
|
|
69
|
-
(await store.mailbox.listAllByAccount(account.accountId)).sort((a, b) =>
|
|
70
|
-
a.fullPath.localeCompare(b.fullPath),
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
const paths = async () => (await rows()).map((mailbox) => mailbox.fullPath);
|
|
74
|
-
|
|
75
|
-
const row = (mailbox: MailboxItem) =>
|
|
76
|
-
store.mailbox.get(account.accountId, mailbox.mailboxId);
|
|
77
|
-
|
|
78
|
-
const byPath = async (fullPath: string) => {
|
|
79
|
-
const found = await store.mailbox.findByPath(account.accountId, fullPath);
|
|
80
|
-
if (!found) throw new Error(`no row at ${fullPath}`);
|
|
81
|
-
return found;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const inState = async (
|
|
85
|
-
fullPath: string,
|
|
86
|
-
syncStatus: MailboxItem["syncStatus"],
|
|
87
|
-
pendingPath?: string,
|
|
88
|
-
) => {
|
|
89
|
-
const mailbox = await seedMailbox(store, account, fullPath, {
|
|
90
|
-
overrides: { syncStatus },
|
|
91
|
-
});
|
|
92
|
-
if (!pendingPath) return mailbox;
|
|
93
|
-
const recorded = await store.mailbox.transition(
|
|
94
|
-
account.accountId,
|
|
95
|
-
mailbox.mailboxId,
|
|
96
|
-
{ from: [syncStatus], to: syncStatus, set: { pendingPath } },
|
|
97
|
-
);
|
|
98
|
-
if (!recorded) throw new Error(`could not record ${pendingPath}`);
|
|
99
|
-
return recorded;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
describe("the STATUS sweep and the mailbox cursor", () => {
|
|
103
|
-
const swept = async () => {
|
|
104
|
-
imap.addFolder("INBOX", { uidValidity: 1 });
|
|
105
|
-
imap.deliver("INBOX", eml({}));
|
|
106
|
-
await sweep();
|
|
107
|
-
return byPath("INBOX");
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
it("trips cursor_invalid without touching the message-sync cursor", async () => {
|
|
111
|
-
const inbox = await swept();
|
|
112
|
-
imap.renumber("INBOX", 2);
|
|
113
|
-
|
|
114
|
-
await sweep();
|
|
115
|
-
|
|
116
|
-
const after = await row(inbox);
|
|
117
|
-
assert.equal(after.cursorState, MailboxCursorState.cursor_invalid);
|
|
118
|
-
assert.equal(after.highestModseq, inbox.highestModseq);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("writes nothing when only the server mod-sequence moved", async () => {
|
|
122
|
-
const inbox = await swept();
|
|
123
|
-
imap.store("INBOX", [1], ["\\Flagged"]);
|
|
124
|
-
|
|
125
|
-
await sweep();
|
|
126
|
-
|
|
127
|
-
assert.deepEqual(await row(inbox), inbox);
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
describe("reconciling the folder list", () => {
|
|
132
|
-
for (const syncStatus of [
|
|
133
|
-
MailboxSyncStatus.pending,
|
|
134
|
-
MailboxSyncStatus.deleting,
|
|
135
|
-
]) {
|
|
136
|
-
it(`keeps a ${syncStatus} folder the server omits or lists as \\Noselect`, async () => {
|
|
137
|
-
imap.addFolder("INBOX");
|
|
138
|
-
imap.addFolder("Half", { attributes: ["\\Noselect"] });
|
|
139
|
-
await inState("New Folder", syncStatus);
|
|
140
|
-
await inState("Half", syncStatus);
|
|
141
|
-
|
|
142
|
-
const result = await sweep();
|
|
143
|
-
|
|
144
|
-
assert.equal(result.deleted, 0);
|
|
145
|
-
assert.deepEqual(await paths(), ["Half", "INBOX", "New Folder"]);
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
it("deletes a synced folder that has left the server", async () => {
|
|
150
|
-
imap.addFolder("INBOX");
|
|
151
|
-
await seedMailbox(store, account, "Old Folder");
|
|
152
|
-
|
|
153
|
-
const result = await sweep();
|
|
154
|
-
|
|
155
|
-
assert.equal(result.deleted, 1);
|
|
156
|
-
assert.deepEqual(await paths(), ["INBOX"]);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("discovers a new folder as synced", async () => {
|
|
160
|
-
imap.addFolder("INBOX");
|
|
161
|
-
|
|
162
|
-
await sweep();
|
|
163
|
-
|
|
164
|
-
assert.equal((await byPath("INBOX")).syncStatus, MailboxSyncStatus.synced);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("inserts no second row for the path a pending rename is on its way to", async () => {
|
|
168
|
-
imap.addFolder("INBOX");
|
|
169
|
-
imap.addFolder("Records/2025");
|
|
170
|
-
await inState("Archive/2025", MailboxSyncStatus.pending, "Records/2025");
|
|
171
|
-
|
|
172
|
-
const result = await sweep();
|
|
173
|
-
|
|
174
|
-
assert.equal(result.created, 1);
|
|
175
|
-
assert.deepEqual(await paths(), ["Archive/2025", "INBOX"]);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("discovers a failed rename's target as a folder of its own", async () => {
|
|
179
|
-
imap.addFolder("INBOX");
|
|
180
|
-
imap.addFolder("Archive/2025");
|
|
181
|
-
imap.addFolder("Records/2025");
|
|
182
|
-
await inState("Archive/2025", MailboxSyncStatus.failed, "Records/2025");
|
|
183
|
-
|
|
184
|
-
await sweep();
|
|
185
|
-
|
|
186
|
-
assert.deepEqual(await paths(), ["Archive/2025", "INBOX", "Records/2025"]);
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe("a lookalike of a flagged folder", () => {
|
|
191
|
-
it("keeps folders called Deleted and Bin beside the flagged Trash", async () => {
|
|
192
|
-
imap.addFolder("INBOX");
|
|
193
|
-
imap.addFolder("Trash", { attributes: ["\\Trash"] });
|
|
194
|
-
imap.addFolder("Deleted");
|
|
195
|
-
imap.addFolder("Bin");
|
|
196
|
-
|
|
197
|
-
await sweep();
|
|
198
|
-
|
|
199
|
-
assert.deepEqual(await paths(), ["Bin", "Deleted", "INBOX", "Trash"]);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("hides the unflagged twin of a folder the server flagged", async () => {
|
|
203
|
-
imap.addFolder("INBOX");
|
|
204
|
-
imap.addFolder("[Gmail]/Trash", { attributes: ["\\Trash"] });
|
|
205
|
-
imap.addFolder("Trash");
|
|
206
|
-
|
|
207
|
-
await sweep();
|
|
208
|
-
|
|
209
|
-
assert.deepEqual(await paths(), ["[Gmail]/Trash", "INBOX"]);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it("keeps a folder that holds a role of its own", async () => {
|
|
213
|
-
imap.addFolder("INBOX");
|
|
214
|
-
imap.addFolder("Archive", { attributes: ["\\Archive"] });
|
|
215
|
-
imap.addFolder("INBOX/All Mail");
|
|
216
|
-
|
|
217
|
-
await sweep();
|
|
218
|
-
|
|
219
|
-
assert.deepEqual(await paths(), ["Archive", "INBOX", "INBOX/All Mail"]);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
describe("a folder leaving mid-sweep", () => {
|
|
224
|
-
const statusFailingFor = (
|
|
225
|
-
path: string,
|
|
226
|
-
before: () => Promise<unknown>,
|
|
227
|
-
message: string,
|
|
228
|
-
) => {
|
|
229
|
-
const connection = imap.connection();
|
|
230
|
-
const status = connection.getMailboxStatus;
|
|
231
|
-
connection.getMailboxStatus = async (asked) => {
|
|
232
|
-
if (asked !== path) return status(asked);
|
|
233
|
-
await before();
|
|
234
|
-
throw new Error(message);
|
|
235
|
-
};
|
|
236
|
-
return connection;
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
it("asks no STATUS of a folder mid-mutation and leaves its row alone", async () => {
|
|
240
|
-
imap.addFolder("INBOX");
|
|
241
|
-
imap.addFolder("Doomed");
|
|
242
|
-
const doomed = await inState("Doomed", MailboxSyncStatus.deleting);
|
|
243
|
-
const connection = imap.connection();
|
|
244
|
-
const asked: string[] = [];
|
|
245
|
-
const status = connection.getMailboxStatus;
|
|
246
|
-
connection.getMailboxStatus = (path) => {
|
|
247
|
-
asked.push(path);
|
|
248
|
-
return status(path);
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
await sweep(connection);
|
|
252
|
-
|
|
253
|
-
assert.deepEqual(asked, ["INBOX"]);
|
|
254
|
-
assert.deepEqual(await row(doomed), doomed);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("finishes the sweep when the folder whose STATUS failed has been deleted", async () => {
|
|
258
|
-
imap.addFolder("INBOX");
|
|
259
|
-
imap.addFolder("Doomed");
|
|
260
|
-
await sweep();
|
|
261
|
-
const doomed = await byPath("Doomed");
|
|
262
|
-
|
|
263
|
-
await assert.doesNotReject(
|
|
264
|
-
sweep(
|
|
265
|
-
statusFailingFor(
|
|
266
|
-
"Doomed",
|
|
267
|
-
() => store.mailbox.delete(account.accountId, doomed.mailboxId),
|
|
268
|
-
"Mailbox doesn't exist: Doomed",
|
|
269
|
-
),
|
|
270
|
-
),
|
|
271
|
-
);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it("fails the sweep when the folder whose STATUS failed is still live", async () => {
|
|
275
|
-
imap.addFolder("INBOX");
|
|
276
|
-
imap.addFolder("Doomed");
|
|
277
|
-
await sweep();
|
|
278
|
-
|
|
279
|
-
await assert.rejects(
|
|
280
|
-
sweep(
|
|
281
|
-
statusFailingFor("Doomed", async () => {}, "connection reset by peer"),
|
|
282
|
-
),
|
|
283
|
-
/connection reset by peer/,
|
|
284
|
-
);
|
|
285
|
-
});
|
|
286
|
-
});
|