@remit/mailbox-service 0.0.110 → 0.0.112
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/package.json +21 -6
- package/src/body-part-mapper.ts +51 -1
- package/src/body-sync-queue.test.ts +39 -99
- package/src/body-sync-terminal.test.ts +115 -277
- package/src/body-sync-terminal.ts +10 -12
- package/src/body-sync.test.ts +925 -0
- package/src/body-sync.ts +226 -200
- package/src/classification-backfill.test.ts +197 -527
- package/src/classify-message.test.ts +11 -74
- package/src/external-move.test.ts +102 -209
- package/src/external-move.ts +13 -0
- package/src/filter-operations.test.ts +319 -0
- package/src/filter-operations.ts +377 -0
- package/src/filter-update.test.ts +313 -0
- package/src/filters/filter-config.test.ts +55 -197
- package/src/filters/filter-config.ts +4 -39
- package/src/filters/match.test.ts +1 -38
- package/src/filters/match.ts +3 -52
- package/src/filters/pipeline.test.ts +158 -289
- package/src/filters/pipeline.ts +56 -170
- package/src/flag-push-terminal.ts +9 -11
- package/src/flag-push.test.ts +76 -372
- package/src/flag-queue.test.ts +197 -0
- package/src/folder-operations.test.ts +273 -0
- package/src/folder-operations.ts +217 -0
- package/src/forwarded-message.test.ts +122 -0
- package/src/imapflow-connect-retry.test.ts +76 -113
- package/src/imapflow-connection.test.ts +34 -0
- package/src/imapflow-connection.ts +19 -15
- package/src/index.ts +59 -12
- package/src/list-id-backfill.test.ts +133 -499
- package/src/mailbox-cursor.test.ts +81 -189
- package/src/mailbox-delete-intent.test.ts +78 -205
- package/src/mailbox-intent.ts +5 -3
- package/src/mailbox-management.test.ts +107 -220
- package/src/mailbox-management.ts +1 -2
- package/src/mailbox-presence.test.ts +36 -34
- package/src/mailbox-queue.ts +2 -20
- package/src/mailbox-rename-intent.test.ts +156 -325
- package/src/mailbox-rename-subtree.test.ts +205 -582
- package/src/mailbox-sync.test.ts +182 -744
- package/src/message-move.test.ts +726 -0
- package/src/message-move.ts +227 -57
- package/src/message-read.ts +87 -0
- package/src/message-save-retry.test.ts +90 -0
- package/src/message-sync-flags.test.ts +10 -1
- package/src/message-sync.test.ts +762 -0
- package/src/message-sync.ts +409 -239
- package/src/mime-walker.ts +16 -15
- package/src/organize-job.ts +93 -0
- package/src/outbox-attachment.test.ts +97 -209
- package/src/outbox-failed-recovery.test.ts +198 -269
- package/src/outbox-queue.test.ts +24 -0
- package/src/outbox-queue.ts +25 -8
- package/src/ownership.ts +140 -0
- package/src/placement-move-terminal.ts +9 -12
- package/src/placement-move.ts +33 -6
- package/src/placement-settled.ts +28 -4
- package/src/placement.test.ts +404 -0
- package/src/quarantine.test.ts +90 -148
- package/src/quarantine.ts +3 -2
- package/src/refusal.ts +29 -0
- package/src/restore-source-placement.ts +0 -4
- package/src/spam-report.test.ts +238 -710
- package/src/sync-warning.test.ts +213 -0
- package/src/sync-warning.ts +56 -0
- package/src/test-helpers/fake-imap.ts +690 -0
- package/src/test-helpers/test-queue.ts +56 -0
- package/src/test-helpers/test-store.ts +299 -0
- package/src/thread-root.test.ts +29 -0
- package/src/thread-root.ts +51 -0
- package/src/types/mailparser-augment.d.ts +8 -0
- package/src/types.ts +1 -1
- package/src/adversarial-mime.e2e.test.ts +0 -307
- 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-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-resync.test.ts +0 -605
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- 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-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-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/outbox-queue-settled-row.test.ts +0 -140
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-transition-race.test.ts +0 -482
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it
|
|
3
|
-
import type {
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
+
import type { AccountItem, MailboxItem } from "@remit/data-ports";
|
|
4
4
|
import { MailboxCursorState } from "@remit/domain-enums";
|
|
5
5
|
import {
|
|
6
6
|
guardConnectionCursor,
|
|
@@ -8,229 +8,121 @@ import {
|
|
|
8
8
|
isCursorRebuildNeeded,
|
|
9
9
|
MailboxCursorPausedError,
|
|
10
10
|
} from "./mailbox-cursor.js";
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
update,
|
|
32
|
-
} as unknown as Pick<IMailboxRepository, "update">;
|
|
33
|
-
|
|
34
|
-
const result = await guardMailboxCursor(
|
|
35
|
-
{ mailboxService },
|
|
36
|
-
accountId,
|
|
37
|
-
baseMailbox,
|
|
38
|
-
100,
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
assert.deepEqual(result, { ok: true });
|
|
42
|
-
assert.equal(update.mock.calls.length, 0);
|
|
11
|
+
import { eml, FakeImapServer } from "./test-helpers/fake-imap.js";
|
|
12
|
+
import {
|
|
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
|
+
let inbox: MailboxItem;
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
store = await openStore();
|
|
26
|
+
imap = new FakeImapServer();
|
|
27
|
+
imap.addFolder("INBOX", { uidValidity: 100 });
|
|
28
|
+
account = await seedAccount(store);
|
|
29
|
+
inbox = await seedMailbox(store, account, "INBOX", {
|
|
30
|
+
overrides: { uidValidity: 100 },
|
|
43
31
|
});
|
|
32
|
+
});
|
|
44
33
|
|
|
45
|
-
|
|
46
|
-
const update = mock.fn(async () => ({}) as MailboxItem);
|
|
47
|
-
const mailboxService = { update } as unknown as Pick<
|
|
48
|
-
IMailboxRepository,
|
|
49
|
-
"update"
|
|
50
|
-
>;
|
|
51
|
-
|
|
52
|
-
const result = await guardMailboxCursor(
|
|
53
|
-
{ mailboxService },
|
|
54
|
-
accountId,
|
|
55
|
-
{ ...baseMailbox, cursorState: undefined },
|
|
56
|
-
100,
|
|
57
|
-
);
|
|
34
|
+
afterEach(() => store.close());
|
|
58
35
|
|
|
59
|
-
|
|
60
|
-
});
|
|
36
|
+
const stored = () => store.mailbox.get(account.accountId, inbox.mailboxId);
|
|
61
37
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const mailboxService = { update } as unknown as Pick<
|
|
65
|
-
IMailboxRepository,
|
|
66
|
-
"update"
|
|
67
|
-
>;
|
|
68
|
-
|
|
69
|
-
const result = await guardMailboxCursor(
|
|
70
|
-
{ mailboxService },
|
|
71
|
-
accountId,
|
|
72
|
-
{ ...baseMailbox, uidValidity: 100 },
|
|
73
|
-
200,
|
|
74
|
-
);
|
|
38
|
+
const pause = (cursorState: MailboxItem["cursorState"]) =>
|
|
39
|
+
store.mailbox.update(account.accountId, inbox.mailboxId, { cursorState });
|
|
75
40
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
});
|
|
41
|
+
const guard = async (served: number) =>
|
|
42
|
+
guardMailboxCursor(
|
|
43
|
+
{ mailboxService: store.mailbox },
|
|
44
|
+
account.accountId,
|
|
45
|
+
await stored(),
|
|
46
|
+
served,
|
|
47
|
+
);
|
|
84
48
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
>;
|
|
91
|
-
|
|
92
|
-
const result = await guardMailboxCursor(
|
|
93
|
-
{ mailboxService },
|
|
94
|
-
accountId,
|
|
95
|
-
{ ...baseMailbox, cursorState: MailboxCursorState.cursor_invalid },
|
|
96
|
-
100,
|
|
97
|
-
);
|
|
49
|
+
describe("guardMailboxCursor", () => {
|
|
50
|
+
it("passes a mailbox whose served UIDVALIDITY matches", async () => {
|
|
51
|
+
assert.deepEqual(await guard(100), { ok: true });
|
|
52
|
+
assert.equal((await stored()).cursorState, MailboxCursorState.normal);
|
|
53
|
+
});
|
|
98
54
|
|
|
99
|
-
|
|
55
|
+
it("trips and persists cursor_invalid when the served UIDVALIDITY disagrees", async () => {
|
|
56
|
+
assert.deepEqual(await guard(200), { ok: false, state: "cursor_invalid" });
|
|
100
57
|
assert.equal(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"paused mailboxes must not be re-tripped on every call (frugal — invariant 6)",
|
|
58
|
+
(await stored()).cursorState,
|
|
59
|
+
MailboxCursorState.cursor_invalid,
|
|
104
60
|
);
|
|
105
61
|
});
|
|
106
62
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
999,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
assert.deepEqual(result, { ok: false, state: "rebuilding" });
|
|
122
|
-
assert.equal(update.mock.calls.length, 0);
|
|
123
|
-
});
|
|
63
|
+
for (const state of [
|
|
64
|
+
MailboxCursorState.cursor_invalid,
|
|
65
|
+
MailboxCursorState.rebuilding,
|
|
66
|
+
]) {
|
|
67
|
+
it(`stays paused while ${state}, whatever the server serves`, async () => {
|
|
68
|
+
await pause(state);
|
|
69
|
+
|
|
70
|
+
assert.deepEqual(await guard(100), { ok: false, state });
|
|
71
|
+
assert.equal((await stored()).cursorState, state);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
124
74
|
});
|
|
125
75
|
|
|
126
76
|
describe("guardConnectionCursor", () => {
|
|
127
|
-
const
|
|
128
|
-
(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const openBox = mock.fn(async () => fakeBoxStatus(100));
|
|
134
|
-
const connection = { openBox } as unknown as IImapConnection;
|
|
135
|
-
const update = mock.fn(async () => ({}) as MailboxItem);
|
|
136
|
-
const mailboxService = { update } as unknown as Pick<
|
|
137
|
-
IMailboxRepository,
|
|
138
|
-
"update"
|
|
139
|
-
>;
|
|
140
|
-
|
|
141
|
-
const guarded = guardConnectionCursor(
|
|
142
|
-
connection,
|
|
143
|
-
{ mailboxService },
|
|
144
|
-
accountId,
|
|
145
|
-
{ ...baseMailbox, uidValidity: 100 },
|
|
77
|
+
const guarded = async () =>
|
|
78
|
+
guardConnectionCursor(
|
|
79
|
+
imap.connection(),
|
|
80
|
+
{ mailboxService: store.mailbox },
|
|
81
|
+
account.accountId,
|
|
82
|
+
await stored(),
|
|
146
83
|
);
|
|
147
84
|
|
|
148
|
-
|
|
85
|
+
it("opens the box and passes other calls through when the cursor holds", async () => {
|
|
86
|
+
imap.deliver("INBOX", eml({}));
|
|
87
|
+
const connection = await guarded();
|
|
149
88
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
assert.equal(update.mock.calls.length, 0);
|
|
153
|
-
});
|
|
89
|
+
const box = await connection.openBox("INBOX");
|
|
90
|
+
const rows = await connection.fetchMessages([1]);
|
|
154
91
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
update: mock.fn(async () => ({}) as MailboxItem),
|
|
160
|
-
} as unknown as Pick<IMailboxRepository, "update">;
|
|
161
|
-
|
|
162
|
-
const guarded = guardConnectionCursor(
|
|
163
|
-
connection,
|
|
164
|
-
{ mailboxService },
|
|
165
|
-
accountId,
|
|
166
|
-
{ ...baseMailbox, cursorState: MailboxCursorState.cursor_invalid },
|
|
92
|
+
assert.equal(box.uidvalidity, 100);
|
|
93
|
+
assert.deepEqual(
|
|
94
|
+
rows.map((row) => row.uid),
|
|
95
|
+
[1],
|
|
167
96
|
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("refuses to open a mailbox already paused", async () => {
|
|
100
|
+
await pause(MailboxCursorState.cursor_invalid);
|
|
101
|
+
const connection = await guarded();
|
|
168
102
|
|
|
169
103
|
await assert.rejects(
|
|
170
|
-
|
|
104
|
+
connection.openBox("INBOX"),
|
|
171
105
|
(err: unknown) =>
|
|
172
106
|
err instanceof MailboxCursorPausedError &&
|
|
173
107
|
err.state === "cursor_invalid",
|
|
174
108
|
);
|
|
175
|
-
assert.equal(
|
|
176
|
-
openBox.mock.calls.length,
|
|
177
|
-
0,
|
|
178
|
-
"a mailbox already known paused must not touch the network (frugal — invariant 6)",
|
|
179
|
-
);
|
|
180
109
|
});
|
|
181
110
|
|
|
182
|
-
it("trips and
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
const update = mock.fn(async () => ({}) as MailboxItem);
|
|
186
|
-
const mailboxService = { update } as unknown as Pick<
|
|
187
|
-
IMailboxRepository,
|
|
188
|
-
"update"
|
|
189
|
-
>;
|
|
190
|
-
|
|
191
|
-
const guarded = guardConnectionCursor(
|
|
192
|
-
connection,
|
|
193
|
-
{ mailboxService },
|
|
194
|
-
accountId,
|
|
195
|
-
{ ...baseMailbox, uidValidity: 100 },
|
|
196
|
-
);
|
|
111
|
+
it("trips the mailbox and refuses when the server renumbered it", async () => {
|
|
112
|
+
const connection = await guarded();
|
|
113
|
+
imap.renumber("INBOX", 200);
|
|
197
114
|
|
|
198
115
|
await assert.rejects(
|
|
199
|
-
|
|
116
|
+
connection.openBox("INBOX"),
|
|
200
117
|
(err: unknown) =>
|
|
201
118
|
err instanceof MailboxCursorPausedError &&
|
|
202
119
|
err.state === "cursor_invalid",
|
|
203
120
|
);
|
|
204
121
|
assert.equal(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"the real openBox must run once to learn the served UIDVALIDITY",
|
|
208
|
-
);
|
|
209
|
-
assert.equal(
|
|
210
|
-
update.mock.calls.length,
|
|
211
|
-
1,
|
|
212
|
-
"the mismatch must trip the mailbox",
|
|
122
|
+
(await stored()).cursorState,
|
|
123
|
+
MailboxCursorState.cursor_invalid,
|
|
213
124
|
);
|
|
214
125
|
});
|
|
215
|
-
|
|
216
|
-
it("passes other methods through untouched (e.g. fetchMessages)", async () => {
|
|
217
|
-
const fetchMessages = mock.fn(async () => []);
|
|
218
|
-
const connection = {
|
|
219
|
-
openBox: async () => fakeBoxStatus(100),
|
|
220
|
-
fetchMessages,
|
|
221
|
-
} as unknown as IImapConnection;
|
|
222
|
-
|
|
223
|
-
const guarded = guardConnectionCursor(
|
|
224
|
-
connection,
|
|
225
|
-
{ mailboxService: { update: async () => ({}) as MailboxItem } },
|
|
226
|
-
accountId,
|
|
227
|
-
{ ...baseMailbox, uidValidity: 100 },
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
await guarded.fetchMessages([1, 2, 3]);
|
|
231
|
-
assert.equal(fetchMessages.mock.calls.length, 1);
|
|
232
|
-
assert.deepEqual(fetchMessages.mock.calls[0].arguments, [[1, 2, 3]]);
|
|
233
|
-
});
|
|
234
126
|
});
|
|
235
127
|
|
|
236
128
|
describe("isCursorRebuildNeeded", () => {
|
|
@@ -1,245 +1,118 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type {
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
+
import type { AccountItem, MailboxItem } from "@remit/data-ports";
|
|
4
4
|
import { MailboxSyncStatus } from "@remit/domain-enums";
|
|
5
5
|
import { MailboxManagementService } from "./mailbox-management.js";
|
|
6
6
|
import { MailboxQueueService } from "./mailbox-queue.js";
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const next = { ...current, syncStatus: intent.to } as MailboxItem;
|
|
42
|
-
byId.set(mailboxId, next);
|
|
43
|
-
return next;
|
|
44
|
-
},
|
|
45
|
-
deleteMailboxWithMail: async (_accountId, mailboxId) => {
|
|
46
|
-
removed.push(mailboxId);
|
|
47
|
-
byId.delete(mailboxId);
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
return { repo: repo as IMailboxRepository, byId, removed };
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const queueOver = (repo: IMailboxRepository) => {
|
|
55
|
-
const sent: string[] = [];
|
|
56
|
-
const service = new MailboxQueueService({
|
|
57
|
-
mailboxService: repo,
|
|
58
|
-
sqsQueueUrl: "http://localhost:4566/000000000000/mailbox",
|
|
7
|
+
import {
|
|
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
|
+
let receipts: MailboxItem;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
capture = captureQueue();
|
|
25
|
+
queue = capture.sent;
|
|
26
|
+
store = await openStore();
|
|
27
|
+
account = await seedAccount(store);
|
|
28
|
+
receipts = await seedMailbox(store, account, "Receipts");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
capture.restore();
|
|
33
|
+
await store.close();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const markAs = (syncStatus: MailboxItem["syncStatus"], pendingPath?: string) =>
|
|
37
|
+
store.mailbox.transition(account.accountId, receipts.mailboxId, {
|
|
38
|
+
from: [MailboxSyncStatus.synced],
|
|
39
|
+
to: syncStatus,
|
|
40
|
+
set: pendingPath ? { pendingPath } : {},
|
|
59
41
|
});
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
await service.deleteMailbox("mbx-1", "acc-1");
|
|
80
|
-
|
|
81
|
-
assert.equal(byId.get("mbx-1")?.syncStatus, MailboxSyncStatus.deleting);
|
|
82
|
-
assert.equal(sent.length, 1);
|
|
83
|
-
assert.equal(
|
|
84
|
-
(JSON.parse(sent[0]) as { path: string }).path,
|
|
85
|
-
"Receipts",
|
|
86
|
-
"the event names the path the server holds",
|
|
42
|
+
|
|
43
|
+
const requestDelete = () =>
|
|
44
|
+
new MailboxQueueService({
|
|
45
|
+
mailboxService: store.mailbox,
|
|
46
|
+
sqsQueueUrl: "http://localhost:4566/000000000000/mailbox",
|
|
47
|
+
}).deleteMailbox(receipts.mailboxId, account.accountId);
|
|
48
|
+
|
|
49
|
+
const current = () => store.mailbox.get(account.accountId, receipts.mailboxId);
|
|
50
|
+
|
|
51
|
+
const management = () => new MailboxManagementService(store.mailbox);
|
|
52
|
+
|
|
53
|
+
describe("MailboxQueueService.deleteMailbox records the intent", () => {
|
|
54
|
+
it("records deleting and enqueues one DELETE naming the server's path", async () => {
|
|
55
|
+
await requestDelete();
|
|
56
|
+
|
|
57
|
+
assert.equal((await current()).syncStatus, MailboxSyncStatus.deleting);
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
queue.map((event) => event.body.path),
|
|
60
|
+
["Receipts"],
|
|
87
61
|
);
|
|
88
62
|
});
|
|
89
63
|
|
|
90
64
|
it("accepts a retry from a failed delete", async () => {
|
|
91
|
-
|
|
92
|
-
row({ syncStatus: MailboxSyncStatus.failed }),
|
|
93
|
-
]);
|
|
94
|
-
const { service, sent } = queueOver(repo);
|
|
65
|
+
await markAs(MailboxSyncStatus.failed);
|
|
95
66
|
|
|
96
|
-
await
|
|
67
|
+
await requestDelete();
|
|
97
68
|
|
|
98
|
-
assert.equal(
|
|
99
|
-
assert.equal(
|
|
69
|
+
assert.equal((await current()).syncStatus, MailboxSyncStatus.deleting);
|
|
70
|
+
assert.equal(queue.length, 1);
|
|
100
71
|
});
|
|
101
72
|
|
|
102
|
-
it("
|
|
103
|
-
|
|
104
|
-
const { service, sent } = queueOver(repo);
|
|
73
|
+
it("answers a second delete with a 409 and enqueues nothing", async () => {
|
|
74
|
+
await requestDelete();
|
|
105
75
|
|
|
106
76
|
await assert.rejects(
|
|
107
|
-
|
|
77
|
+
requestDelete(),
|
|
108
78
|
(error: unknown) =>
|
|
109
79
|
(error as { statusCode?: number }).statusCode === 409 &&
|
|
110
80
|
(error as Error).message ===
|
|
111
81
|
'A delete is already in progress for "Receipts".',
|
|
112
82
|
);
|
|
113
|
-
assert.equal(
|
|
83
|
+
assert.equal(queue.length, 1);
|
|
114
84
|
});
|
|
115
85
|
|
|
116
|
-
it("
|
|
117
|
-
|
|
118
|
-
row({
|
|
119
|
-
syncStatus: MailboxSyncStatus.pending,
|
|
120
|
-
pendingPath: "Bonnen",
|
|
121
|
-
}),
|
|
122
|
-
]);
|
|
123
|
-
const { service } = queueOver(repo);
|
|
86
|
+
it("answers a delete during a rename with a 409 naming the rename", async () => {
|
|
87
|
+
await markAs(MailboxSyncStatus.pending, "Bonnen");
|
|
124
88
|
|
|
125
89
|
await assert.rejects(
|
|
126
|
-
|
|
90
|
+
requestDelete(),
|
|
127
91
|
(error: unknown) =>
|
|
128
92
|
(error as Error).message === 'A rename is in progress for "Receipts".',
|
|
129
93
|
);
|
|
130
94
|
});
|
|
131
95
|
});
|
|
132
96
|
|
|
133
|
-
describe("MailboxManagementService.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const connection = (calls: string[]): IImapConnection =>
|
|
138
|
-
({
|
|
139
|
-
deleteMailbox: async (path: string) => {
|
|
140
|
-
calls.push(path);
|
|
141
|
-
return { path };
|
|
142
|
-
},
|
|
143
|
-
}) as unknown as IImapConnection;
|
|
144
|
-
|
|
145
|
-
it("takes the folder's mail with it, not the row alone", async () => {
|
|
146
|
-
// Removing the row alone leaves every message and thread_message row keyed
|
|
147
|
-
// to a dead mailboxId — out of every reader, and still in the search index
|
|
148
|
-
// because nothing writes the `message.removed` outbox event (D8).
|
|
149
|
-
const { repo, removed, byId } = deleting();
|
|
150
|
-
const service = new MailboxManagementService(repo);
|
|
151
|
-
const calls: string[] = [];
|
|
152
|
-
|
|
153
|
-
const result = await service.syncDelete(
|
|
154
|
-
"acc-1",
|
|
155
|
-
"mbx-1",
|
|
156
|
-
"Receipts",
|
|
157
|
-
async () => connection(calls),
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
assert.equal(result.success, true);
|
|
161
|
-
assert.deepEqual(calls, ["Receipts"]);
|
|
162
|
-
assert.deepEqual(removed, ["mbx-1"]);
|
|
163
|
-
assert.equal(byId.has("mbx-1"), false);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("issues no DELETE when the row is not deleting", async () => {
|
|
167
|
-
const { repo, removed } = store([row()]);
|
|
168
|
-
const service = new MailboxManagementService(repo);
|
|
169
|
-
const calls: string[] = [];
|
|
170
|
-
|
|
171
|
-
const result = await service.syncDelete(
|
|
172
|
-
"acc-1",
|
|
173
|
-
"mbx-1",
|
|
174
|
-
"Receipts",
|
|
175
|
-
async () => connection(calls),
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
assert.equal(result.success, true);
|
|
179
|
-
assert.deepEqual(calls, [], "the connection is never asked");
|
|
180
|
-
assert.deepEqual(removed, []);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("resolves when the row is already gone", async () => {
|
|
184
|
-
const { repo } = store([]);
|
|
185
|
-
const service = new MailboxManagementService(repo);
|
|
186
|
-
const calls: string[] = [];
|
|
187
|
-
|
|
188
|
-
const result = await service.syncDelete(
|
|
189
|
-
"acc-1",
|
|
190
|
-
"mbx-1",
|
|
191
|
-
"Receipts",
|
|
192
|
-
async () => connection(calls),
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
assert.equal(result.success, true);
|
|
196
|
-
assert.deepEqual(calls, []);
|
|
197
|
-
});
|
|
97
|
+
describe("MailboxManagementService.failDelete", () => {
|
|
98
|
+
it("marks the row failed and keeps its path, with no rename target", async () => {
|
|
99
|
+
await requestDelete();
|
|
198
100
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const service = new MailboxManagementService(repo);
|
|
204
|
-
const calls: string[] = [];
|
|
205
|
-
|
|
206
|
-
await service.settleDelete("acc-1", "mbx-1");
|
|
207
|
-
const result = await service.syncDelete(
|
|
208
|
-
"acc-1",
|
|
209
|
-
"mbx-1",
|
|
210
|
-
"Receipts",
|
|
211
|
-
async () => connection(calls),
|
|
101
|
+
await management().failDelete(
|
|
102
|
+
account.accountId,
|
|
103
|
+
receipts.mailboxId,
|
|
104
|
+
"Permission denied",
|
|
212
105
|
);
|
|
213
106
|
|
|
214
|
-
|
|
215
|
-
assert.
|
|
216
|
-
assert.
|
|
217
|
-
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
describe("MailboxManagementService.failDelete", () => {
|
|
221
|
-
it("marks the row failed, unwinding nothing", async () => {
|
|
222
|
-
// DELETE is all-or-nothing, so fullPath is still what the server holds,
|
|
223
|
-
// and no rename target is written — which is how the client tells a failed
|
|
224
|
-
// delete from a failed rename (T9).
|
|
225
|
-
const { repo, byId } = store([
|
|
226
|
-
row({ syncStatus: MailboxSyncStatus.deleting }),
|
|
227
|
-
]);
|
|
228
|
-
const service = new MailboxManagementService(repo);
|
|
229
|
-
|
|
230
|
-
await service.failDelete("acc-1", "mbx-1", "Permission denied");
|
|
231
|
-
|
|
232
|
-
assert.equal(byId.get("mbx-1")?.syncStatus, MailboxSyncStatus.failed);
|
|
233
|
-
assert.equal(byId.get("mbx-1")?.fullPath, "Receipts");
|
|
234
|
-
assert.equal(byId.get("mbx-1")?.pendingPath, undefined);
|
|
107
|
+
const failed = await current();
|
|
108
|
+
assert.equal(failed.syncStatus, MailboxSyncStatus.failed);
|
|
109
|
+
assert.equal(failed.fullPath, "Receipts");
|
|
110
|
+
assert.equal(failed.pendingPath, undefined);
|
|
235
111
|
});
|
|
236
112
|
|
|
237
113
|
it("raises a NotFoundError when the row is gone", async () => {
|
|
238
|
-
const { repo } = store([]);
|
|
239
|
-
const service = new MailboxManagementService(repo);
|
|
240
|
-
|
|
241
114
|
await assert.rejects(
|
|
242
|
-
|
|
115
|
+
management().failDelete(account.accountId, "mbx-gone", "denied"),
|
|
243
116
|
(error: unknown) => (error as Error).name === "NotFoundError",
|
|
244
117
|
);
|
|
245
118
|
});
|