@remit/imap-worker 0.0.86 → 0.0.88
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/README.md +3 -1
- package/package.json +10 -2
- package/src/account-check.ts +1 -1
- package/src/body-sync-gate.test.ts +1 -1
- package/src/body-sync-gate.ts +1 -1
- package/src/e2e-processor-shim.ts +6 -21
- package/src/emit.ts +1 -1
- package/src/failure-kind.ts +1 -1
- package/src/handlers/append-sent-message.sqlite.test.ts +176 -0
- package/src/handlers/append-sent-message.ts +3 -24
- package/src/handlers/delete-account-objects.test.ts +1 -1
- package/src/handlers/delete-account-objects.ts +1 -1
- package/src/handlers/empty-trash.sqlite.test.ts +209 -0
- package/src/handlers/empty-trash.ts +3 -24
- package/src/handlers/flag-push.ts +11 -18
- package/src/handlers/mailbox-management.sqlite.test.ts +265 -0
- package/src/handlers/mailbox-management.ts +12 -48
- package/src/handlers/message-copy.sqlite.test.ts +298 -0
- package/src/handlers/message-copy.ts +31 -49
- package/src/handlers/message-delete-terminal.ts +12 -10
- package/src/handlers/message-delete.sqlite.test.ts +291 -0
- package/src/handlers/message-delete.ts +26 -32
- package/src/handlers/message-move-terminal.ts +12 -10
- package/src/handlers/message-move.sqlite.test.ts +271 -0
- package/src/handlers/message-move.test.ts +76 -863
- package/src/handlers/message-move.ts +49 -39
- package/src/handlers/placement-move-push.test.ts +96 -329
- package/src/handlers/placement-move-push.ts +75 -29
- package/src/handlers/placement-push.sqlite.test.ts +298 -0
- package/src/handlers/probe-message-id.sqlite.test.ts +194 -0
- package/src/handlers/probe-message-id.ts +15 -0
- package/src/handlers/sync-mailboxes.test.ts +0 -1
- package/src/handlers/sync-mailboxes.ts +1 -1
- package/src/handlers/sync-message-body.sqlite.test.ts +314 -0
- package/src/handlers/sync-message-body.test.ts +3 -299
- package/src/handlers/sync-message-body.ts +21 -11
- package/src/handlers/sync-messages.sqlite.test.ts +183 -0
- package/src/handlers/sync-messages.ts +27 -33
- package/src/index.ts +5 -3
- package/src/mail-sync-selection.sqlite.test.ts +21 -21
- package/src/poller.ts +5 -4
- package/src/processor.test.ts +1 -1
- package/src/processor.ts +1 -1
- package/src/scheduler/attachment-sweep.sqlite.test.ts +74 -0
- package/src/scheduler/attachment-sweep.ts +1 -1
- package/src/scheduler/{calendar-subscriptions.test.ts → calendar-subscriptions.sqlite.test.ts} +43 -55
- package/src/scheduler/calendar-subscriptions.ts +6 -6
- package/src/scheduler/half-deleted-accounts.sqlite.test.ts +203 -0
- package/src/scheduler/handler.ts +12 -1
- package/src/scheduler/loop.test.ts +2 -0
- package/src/scheduler/run-tick.sqlite.test.ts +199 -0
- package/src/scheduler/run-tick.ts +39 -1
- package/src/scheduler/runner.ts +21 -1
- package/src/scheduler/stranded-placement-redrive.ts +265 -0
- package/src/stranded-placement.sqlite.test.ts +795 -0
- package/src/test-helpers/test-world.ts +398 -0
- package/src/thread-join.sqlite.test.ts +106 -0
- package/src/with-oauth-lifecycle-deps.sqlite.test.ts +146 -15
- package/src/with-oauth-lifecycle.ts +1 -1
- package/src/handlers/append-sent-message.test.ts +0 -529
- package/src/handlers/empty-trash.test.ts +0 -728
- package/src/handlers/flag-push-placement-race.test.ts +0 -385
- package/src/handlers/flag-push.test.ts +0 -325
- package/src/handlers/mailbox-management.test.ts +0 -850
- package/src/handlers/message-copy.test.ts +0 -654
- package/src/handlers/message-delete.test.ts +0 -1869
- package/src/handlers/message-move-terminal.test.ts +0 -412
- package/src/handlers/sync-messages.test.ts +0 -569
- package/src/scheduler/attachment-sweep.test.ts +0 -169
- package/src/scheduler/run-tick.test.ts +0 -478
- package/src/with-oauth-lifecycle.test.ts +0 -318
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A move whose event dies must not leave its row `moving`, and a row already
|
|
3
|
+
* left there must be re-driven to whatever the server says (imap-mutations R1).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { after, before, describe, it } from "node:test";
|
|
9
|
+
import type { MailboxItem, MessageItem } from "@remit/data-ports";
|
|
10
|
+
import {
|
|
11
|
+
MailboxCursorState,
|
|
12
|
+
MailboxSpecialUse,
|
|
13
|
+
MessageMutation,
|
|
14
|
+
MessageStatus,
|
|
15
|
+
MessageSyncStatus,
|
|
16
|
+
} from "@remit/domain-enums";
|
|
17
|
+
import { PlacementMoveService } from "@remit/mailbox-service";
|
|
18
|
+
import type {
|
|
19
|
+
MessageCopyEvent,
|
|
20
|
+
MessageMoveEvent,
|
|
21
|
+
PlacementMovePushEvent,
|
|
22
|
+
} from "./events.js";
|
|
23
|
+
import { handleMessageCopy } from "./handlers/message-copy.js";
|
|
24
|
+
import { handleMessageMove } from "./handlers/message-move.js";
|
|
25
|
+
import { handlePlacementMovePush } from "./handlers/placement-move-push.js";
|
|
26
|
+
import {
|
|
27
|
+
redriveStrandedPlacements,
|
|
28
|
+
STRANDED_PLACEMENT_AFTER_MILLIS,
|
|
29
|
+
type StrandedPlacementEvent,
|
|
30
|
+
} from "./scheduler/stranded-placement-redrive.js";
|
|
31
|
+
import {
|
|
32
|
+
mail,
|
|
33
|
+
openStore,
|
|
34
|
+
type SeededMessage,
|
|
35
|
+
type World,
|
|
36
|
+
} from "./test-helpers/test-world.js";
|
|
37
|
+
|
|
38
|
+
let store: Awaited<ReturnType<typeof openStore>>;
|
|
39
|
+
|
|
40
|
+
before(async () => {
|
|
41
|
+
store = await openStore();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
after(() => store.close());
|
|
45
|
+
|
|
46
|
+
interface Arranged {
|
|
47
|
+
w: World;
|
|
48
|
+
inbox: MailboxItem;
|
|
49
|
+
notifications: MailboxItem;
|
|
50
|
+
trash: MailboxItem;
|
|
51
|
+
siblingTravel: MailboxItem;
|
|
52
|
+
message: SeededMessage;
|
|
53
|
+
seededSyncStatus: MessageItem["syncStatus"] | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const arrange = async (
|
|
57
|
+
seedIn: "inbox" | "trash" = "inbox",
|
|
58
|
+
): Promise<Arranged> => {
|
|
59
|
+
const w = await store.world();
|
|
60
|
+
const inbox = await w.folder("INBOX");
|
|
61
|
+
const notifications = await w.folder("INBOX/Notifications");
|
|
62
|
+
const trash = await w.folder("Trash", {
|
|
63
|
+
specialUse: MailboxSpecialUse.Trash,
|
|
64
|
+
});
|
|
65
|
+
const siblingTravel = await w.siblingFolder("INBOX/Travel");
|
|
66
|
+
const message = await w.message(seedIn === "inbox" ? inbox : trash);
|
|
67
|
+
const seeded = await w.row(message.messageId);
|
|
68
|
+
return {
|
|
69
|
+
w,
|
|
70
|
+
inbox,
|
|
71
|
+
notifications,
|
|
72
|
+
trash,
|
|
73
|
+
siblingTravel,
|
|
74
|
+
message,
|
|
75
|
+
seededSyncStatus: seeded?.syncStatus,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const placementMover = (w: World): PlacementMoveService =>
|
|
80
|
+
new PlacementMoveService({
|
|
81
|
+
messageService: w.client.message,
|
|
82
|
+
threadMessageService: w.client.threadMessage,
|
|
83
|
+
markerService: w.client.placementMove,
|
|
84
|
+
addressService: w.client.address,
|
|
85
|
+
mailboxSpecialUseService: w.client.mailboxSpecialUse,
|
|
86
|
+
mailboxService: w.client.mailbox,
|
|
87
|
+
sqsQueueUrl: process.env.SQS_QUEUE_URL_MESSAGE_MGMT ?? "",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const placementOf = async (a: Arranged) => {
|
|
91
|
+
const row = await a.w.row(a.message.messageId);
|
|
92
|
+
const [listing] = await a.w.threadRows(a.message.messageId);
|
|
93
|
+
return {
|
|
94
|
+
status: row?.status,
|
|
95
|
+
syncStatus: row?.syncStatus,
|
|
96
|
+
mailboxId: row?.mailboxId,
|
|
97
|
+
uid: row?.uid,
|
|
98
|
+
listedIn: listing?.mailboxId,
|
|
99
|
+
listedDeleted: listing?.isDeleted,
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const backIn = (a: Arranged, mailbox: MailboxItem) => ({
|
|
104
|
+
status: MessageStatus.active,
|
|
105
|
+
syncStatus: MessageSyncStatus.synced,
|
|
106
|
+
mailboxId: mailbox.mailboxId,
|
|
107
|
+
uid: a.message.uid,
|
|
108
|
+
listedIn: mailbox.mailboxId,
|
|
109
|
+
listedDeleted: false,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/** As the sync left it, before any mutation touched it. */
|
|
113
|
+
const untouched = (a: Arranged, mailbox: MailboxItem) => ({
|
|
114
|
+
...backIn(a, mailbox),
|
|
115
|
+
syncStatus: a.seededSyncStatus,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const gone = {
|
|
119
|
+
status: undefined,
|
|
120
|
+
syncStatus: undefined,
|
|
121
|
+
mailboxId: undefined,
|
|
122
|
+
uid: undefined,
|
|
123
|
+
listedIn: undefined,
|
|
124
|
+
listedDeleted: undefined,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const queueRefuses = (w: World): void => {
|
|
128
|
+
w.queue.answer(async () => {
|
|
129
|
+
throw new Error("queue unreachable");
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const queueTakes = (w: World): void => {
|
|
134
|
+
w.queue.answer(async () => ({ Failed: [] }));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const redriveLater = async (
|
|
138
|
+
a: Arranged,
|
|
139
|
+
byMillis = STRANDED_PLACEMENT_AFTER_MILLIS + 60_000,
|
|
140
|
+
): Promise<StrandedPlacementEvent[]> => {
|
|
141
|
+
const emitted: StrandedPlacementEvent[] = [];
|
|
142
|
+
await redriveStrandedPlacements(
|
|
143
|
+
{
|
|
144
|
+
client: a.w.client,
|
|
145
|
+
emit: async (event) => {
|
|
146
|
+
emitted.push(event);
|
|
147
|
+
},
|
|
148
|
+
log: a.w.log,
|
|
149
|
+
},
|
|
150
|
+
a.w.account,
|
|
151
|
+
Date.now() + byMillis,
|
|
152
|
+
);
|
|
153
|
+
return emitted;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const stamped = <T>(event: StrandedPlacementEvent | undefined): T => {
|
|
157
|
+
if (!event) throw new Error("nothing was re-driven");
|
|
158
|
+
return { ...event, eventId: randomUUID(), timestamp: Date.now() } as T;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The state the producer wrote before it checked whose folder it was filing
|
|
163
|
+
* into, and which the production rows still carry.
|
|
164
|
+
*/
|
|
165
|
+
const strandForeignFiling = async (
|
|
166
|
+
a: Arranged,
|
|
167
|
+
marker: "with-marker" | "marker-dropped",
|
|
168
|
+
): Promise<void> => {
|
|
169
|
+
const { w, message, inbox, siblingTravel } = a;
|
|
170
|
+
if (marker === "with-marker") {
|
|
171
|
+
await w.client.placementMove.put({
|
|
172
|
+
messageId: message.messageId,
|
|
173
|
+
accountId: w.account.accountId,
|
|
174
|
+
accountConfigId: w.account.accountConfigId,
|
|
175
|
+
sourceMailboxId: inbox.mailboxId,
|
|
176
|
+
destinationMailboxId: siblingTravel.mailboxId,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
await w.client.message.transitionPlacement(
|
|
180
|
+
message.messageId,
|
|
181
|
+
{
|
|
182
|
+
status: MessageStatus.active,
|
|
183
|
+
mailboxId: inbox.mailboxId,
|
|
184
|
+
uid: message.uid,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
mailboxId: siblingTravel.mailboxId,
|
|
188
|
+
status: MessageStatus.moving,
|
|
189
|
+
syncStatus: MessageSyncStatus.pending,
|
|
190
|
+
abandonedMutation: MessageMutation.none,
|
|
191
|
+
originalMailboxId: inbox.mailboxId,
|
|
192
|
+
originalUid: message.uid,
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
const listing = await w.client.threadMessage.getByMessageId(
|
|
196
|
+
w.account.accountConfigId,
|
|
197
|
+
message.messageId,
|
|
198
|
+
);
|
|
199
|
+
await w.client.threadMessage.update(
|
|
200
|
+
listing.accountConfigId,
|
|
201
|
+
listing.threadMessageId,
|
|
202
|
+
{ mailboxId: siblingTravel.mailboxId },
|
|
203
|
+
{
|
|
204
|
+
composites: {
|
|
205
|
+
sentDate: listing.sentDate,
|
|
206
|
+
mailboxId: listing.mailboxId,
|
|
207
|
+
isRead: listing.isRead,
|
|
208
|
+
isDeleted: listing.isDeleted,
|
|
209
|
+
hasStars: listing.hasStars,
|
|
210
|
+
hasAttachment: listing.hasAttachment,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
describe("a move whose event dies leaves no row moving", () => {
|
|
217
|
+
it("refuses a filing into another account's folder before any local write", async () => {
|
|
218
|
+
const a = await arrange();
|
|
219
|
+
|
|
220
|
+
const outcome = await placementMover(a.w).moveMessage(
|
|
221
|
+
a.w.account.accountConfigId,
|
|
222
|
+
a.message.messageId,
|
|
223
|
+
a.siblingTravel.mailboxId,
|
|
224
|
+
a.w.account.accountId,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
assert.equal(outcome, "refused");
|
|
228
|
+
assert.deepEqual(await placementOf(a), untouched(a, a.inbox));
|
|
229
|
+
assert.equal(
|
|
230
|
+
await a.w.client.placementMove.find(a.message.messageId),
|
|
231
|
+
null,
|
|
232
|
+
);
|
|
233
|
+
assert.deepEqual(
|
|
234
|
+
a.w.queued().filter((event) => event.type === "PLACEMENT_MOVE_PUSH"),
|
|
235
|
+
[],
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("hands back a user move whose event the queue never took", async () => {
|
|
240
|
+
const a = await arrange();
|
|
241
|
+
queueRefuses(a.w);
|
|
242
|
+
|
|
243
|
+
await assert.rejects(
|
|
244
|
+
a.w.client.messageMove.moveMessage(
|
|
245
|
+
a.w.account.accountConfigId,
|
|
246
|
+
a.message.messageId,
|
|
247
|
+
a.notifications.mailboxId,
|
|
248
|
+
a.w.account.accountId,
|
|
249
|
+
),
|
|
250
|
+
/queue unreachable/,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
assert.deepEqual(await placementOf(a), backIn(a, a.inbox));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("hands back a move to Trash whose batch entry the queue refused", async () => {
|
|
257
|
+
const a = await arrange();
|
|
258
|
+
a.w.queue.answer(async () => ({
|
|
259
|
+
Failed: [{ Id: "0", Code: "InvalidParameterValue", SenderFault: true }],
|
|
260
|
+
}));
|
|
261
|
+
|
|
262
|
+
await assert.rejects(
|
|
263
|
+
a.w.client.messageMove.deleteMessages(
|
|
264
|
+
a.w.account.accountConfigId,
|
|
265
|
+
[a.message.messageId],
|
|
266
|
+
a.w.account.accountId,
|
|
267
|
+
),
|
|
268
|
+
/refused 1 of 1/,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
assert.deepEqual(await placementOf(a), backIn(a, a.inbox));
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("finishes the local removal of a permanent delete whose batch send failed", async () => {
|
|
275
|
+
const a = await arrange();
|
|
276
|
+
queueRefuses(a.w);
|
|
277
|
+
|
|
278
|
+
await assert.rejects(
|
|
279
|
+
a.w.client.messageMove.deleteMessages(
|
|
280
|
+
a.w.account.accountConfigId,
|
|
281
|
+
[a.message.messageId],
|
|
282
|
+
a.w.account.accountId,
|
|
283
|
+
{ permanent: true },
|
|
284
|
+
),
|
|
285
|
+
/queue unreachable/,
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
assert.deepEqual(await placementOf(a), gone);
|
|
289
|
+
assert.deepEqual(await redriveLater(a), [], "nothing is left deleting");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("puts Trash back when the queue never took its Empty Trash", async () => {
|
|
293
|
+
const a = await arrange("trash");
|
|
294
|
+
queueRefuses(a.w);
|
|
295
|
+
|
|
296
|
+
await assert.rejects(
|
|
297
|
+
a.w.client.messageMove.emptyTrash(
|
|
298
|
+
a.w.account.accountConfigId,
|
|
299
|
+
a.w.account.accountId,
|
|
300
|
+
),
|
|
301
|
+
/queue unreachable/,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
assert.deepEqual(await placementOf(a), untouched(a, a.trash));
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("leaves a row already being deleted as it was when Empty Trash never left", async () => {
|
|
308
|
+
const a = await arrange("trash");
|
|
309
|
+
await a.w.client.message.transitionPlacement(
|
|
310
|
+
a.message.messageId,
|
|
311
|
+
{ status: MessageStatus.active, mailboxId: a.trash.mailboxId },
|
|
312
|
+
{ status: MessageStatus.deleting, syncStatus: MessageSyncStatus.failed },
|
|
313
|
+
);
|
|
314
|
+
const listing = await a.w.client.threadMessage.getByMessageId(
|
|
315
|
+
a.w.account.accountConfigId,
|
|
316
|
+
a.message.messageId,
|
|
317
|
+
);
|
|
318
|
+
await a.w.client.threadMessage.update(
|
|
319
|
+
listing.accountConfigId,
|
|
320
|
+
listing.threadMessageId,
|
|
321
|
+
{ isDeleted: true },
|
|
322
|
+
{
|
|
323
|
+
composites: {
|
|
324
|
+
sentDate: listing.sentDate,
|
|
325
|
+
mailboxId: listing.mailboxId,
|
|
326
|
+
isRead: listing.isRead,
|
|
327
|
+
isDeleted: listing.isDeleted,
|
|
328
|
+
hasStars: listing.hasStars,
|
|
329
|
+
hasAttachment: listing.hasAttachment,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
queueRefuses(a.w);
|
|
334
|
+
|
|
335
|
+
await assert.rejects(
|
|
336
|
+
a.w.client.messageMove.emptyTrash(
|
|
337
|
+
a.w.account.accountConfigId,
|
|
338
|
+
a.w.account.accountId,
|
|
339
|
+
),
|
|
340
|
+
/queue unreachable/,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
assert.deepEqual(await placementOf(a), {
|
|
344
|
+
status: MessageStatus.deleting,
|
|
345
|
+
syncStatus: MessageSyncStatus.failed,
|
|
346
|
+
mailboxId: a.trash.mailboxId,
|
|
347
|
+
uid: a.message.uid,
|
|
348
|
+
listedIn: a.trash.mailboxId,
|
|
349
|
+
listedDeleted: true,
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("drops the copy row when the queue never took its copy", async () => {
|
|
354
|
+
const a = await arrange();
|
|
355
|
+
queueRefuses(a.w);
|
|
356
|
+
|
|
357
|
+
await assert.rejects(
|
|
358
|
+
a.w.client.messageMove.copyMessage(
|
|
359
|
+
a.w.account.accountConfigId,
|
|
360
|
+
a.message.messageId,
|
|
361
|
+
a.notifications.mailboxId,
|
|
362
|
+
a.w.account.accountId,
|
|
363
|
+
),
|
|
364
|
+
/queue unreachable/,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
assert.deepEqual(
|
|
368
|
+
await a.w.client.message.listAllByMailbox(a.notifications.mailboxId),
|
|
369
|
+
[],
|
|
370
|
+
);
|
|
371
|
+
assert.deepEqual(await placementOf(a), untouched(a, a.inbox));
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("reports both the queue's error and a hand-back that failed", async () => {
|
|
375
|
+
const a = await arrange();
|
|
376
|
+
queueRefuses(a.w);
|
|
377
|
+
const repo = a.w.client.message;
|
|
378
|
+
const original = repo.transitionPlacement;
|
|
379
|
+
let calls = 0;
|
|
380
|
+
repo.transitionPlacement = async (...args) => {
|
|
381
|
+
calls += 1;
|
|
382
|
+
if (calls > 1) throw new Error("SQLITE_BUSY");
|
|
383
|
+
return original.apply(repo, args);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
await assert
|
|
387
|
+
.rejects(
|
|
388
|
+
a.w.client.messageMove.moveMessage(
|
|
389
|
+
a.w.account.accountConfigId,
|
|
390
|
+
a.message.messageId,
|
|
391
|
+
a.notifications.mailboxId,
|
|
392
|
+
a.w.account.accountId,
|
|
393
|
+
),
|
|
394
|
+
(error: unknown) => {
|
|
395
|
+
assert.ok(error instanceof AggregateError);
|
|
396
|
+
assert.deepEqual(
|
|
397
|
+
error.errors.map((inner: Error) => inner.message),
|
|
398
|
+
["queue unreachable", "SQLITE_BUSY"],
|
|
399
|
+
);
|
|
400
|
+
return true;
|
|
401
|
+
},
|
|
402
|
+
)
|
|
403
|
+
.finally(() => {
|
|
404
|
+
repo.transitionPlacement = original;
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe("the reconcile re-drives a move or delete already stranded", () => {
|
|
410
|
+
it("re-sends the push for a filing whose push was never queued", async () => {
|
|
411
|
+
const a = await arrange();
|
|
412
|
+
queueRefuses(a.w);
|
|
413
|
+
await assert.rejects(
|
|
414
|
+
placementMover(a.w).moveMessage(
|
|
415
|
+
a.w.account.accountConfigId,
|
|
416
|
+
a.message.messageId,
|
|
417
|
+
a.notifications.mailboxId,
|
|
418
|
+
a.w.account.accountId,
|
|
419
|
+
),
|
|
420
|
+
);
|
|
421
|
+
assert.equal(
|
|
422
|
+
(await a.w.client.placementMove.find(a.message.messageId))?.state,
|
|
423
|
+
"pending",
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
assert.deepEqual(await redriveLater(a), [
|
|
427
|
+
{
|
|
428
|
+
type: "PLACEMENT_MOVE_PUSH",
|
|
429
|
+
accountId: a.w.account.accountId,
|
|
430
|
+
accountConfigId: a.w.account.accountConfigId,
|
|
431
|
+
messageId: a.message.messageId,
|
|
432
|
+
},
|
|
433
|
+
]);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("files a stranded move on the server through the move it re-sends", async () => {
|
|
437
|
+
const a = await arrange();
|
|
438
|
+
queueTakes(a.w);
|
|
439
|
+
await a.w.client.messageMove.moveMessage(
|
|
440
|
+
a.w.account.accountConfigId,
|
|
441
|
+
a.message.messageId,
|
|
442
|
+
a.notifications.mailboxId,
|
|
443
|
+
a.w.account.accountId,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const [move] = await redriveLater(a);
|
|
447
|
+
assert.equal(move?.type, "MESSAGE_MOVE");
|
|
448
|
+
await handleMessageMove(
|
|
449
|
+
stamped<MessageMoveEvent>(move),
|
|
450
|
+
a.w.log,
|
|
451
|
+
1,
|
|
452
|
+
a.w.scope,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
const settled = await a.w.row(a.message.messageId);
|
|
456
|
+
assert.equal(settled?.status, MessageStatus.active);
|
|
457
|
+
assert.equal(settled?.mailboxId, a.notifications.mailboxId);
|
|
458
|
+
assert.equal(
|
|
459
|
+
a.w.imap.uids(a.notifications.fullPath).includes(settled?.uid ?? -1),
|
|
460
|
+
true,
|
|
461
|
+
);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it("hands back a foreign filing through the push it re-sends", async () => {
|
|
465
|
+
const a = await arrange();
|
|
466
|
+
await strandForeignFiling(a, "with-marker");
|
|
467
|
+
|
|
468
|
+
const [push] = await redriveLater(a);
|
|
469
|
+
assert.equal(push?.type, "PLACEMENT_MOVE_PUSH");
|
|
470
|
+
await handlePlacementMovePush(
|
|
471
|
+
stamped<PlacementMovePushEvent>(push),
|
|
472
|
+
a.w.log,
|
|
473
|
+
1,
|
|
474
|
+
a.w.scope,
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
assert.deepEqual(await placementOf(a), backIn(a, a.inbox));
|
|
478
|
+
assert.equal(
|
|
479
|
+
await a.w.client.placementMove.find(a.message.messageId),
|
|
480
|
+
null,
|
|
481
|
+
);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("re-sends a move for a foreign filing whose marker is gone, and the move hands it back", async () => {
|
|
485
|
+
const a = await arrange();
|
|
486
|
+
await strandForeignFiling(a, "marker-dropped");
|
|
487
|
+
|
|
488
|
+
const emitted = await redriveLater(a);
|
|
489
|
+
assert.deepEqual(emitted, [
|
|
490
|
+
{
|
|
491
|
+
type: "MESSAGE_MOVE",
|
|
492
|
+
schemaVersion: 3,
|
|
493
|
+
accountId: a.w.account.accountId,
|
|
494
|
+
messageId: a.message.messageId,
|
|
495
|
+
sourceMailboxId: a.inbox.mailboxId,
|
|
496
|
+
destinationMailboxId: a.siblingTravel.mailboxId,
|
|
497
|
+
uid: a.message.uid,
|
|
498
|
+
},
|
|
499
|
+
]);
|
|
500
|
+
|
|
501
|
+
await handleMessageMove(
|
|
502
|
+
stamped<MessageMoveEvent>(emitted[0]),
|
|
503
|
+
a.w.log,
|
|
504
|
+
1,
|
|
505
|
+
a.w.scope,
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
assert.deepEqual(await placementOf(a), backIn(a, a.inbox));
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it("reconciles a filing away when its source folder is gone", async () => {
|
|
512
|
+
const a = await arrange();
|
|
513
|
+
await strandForeignFiling(a, "with-marker");
|
|
514
|
+
await a.w.client.mailbox.delete(a.w.account.accountId, a.inbox.mailboxId);
|
|
515
|
+
|
|
516
|
+
await handlePlacementMovePush(
|
|
517
|
+
stamped<PlacementMovePushEvent>({
|
|
518
|
+
type: "PLACEMENT_MOVE_PUSH",
|
|
519
|
+
accountId: a.w.account.accountId,
|
|
520
|
+
accountConfigId: a.w.account.accountConfigId,
|
|
521
|
+
messageId: a.message.messageId,
|
|
522
|
+
}),
|
|
523
|
+
a.w.log,
|
|
524
|
+
1,
|
|
525
|
+
a.w.scope,
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
assert.deepEqual(await placementOf(a), gone);
|
|
529
|
+
assert.equal(
|
|
530
|
+
await a.w.client.placementMove.find(a.message.messageId),
|
|
531
|
+
null,
|
|
532
|
+
);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("re-sends the delete for a move to Trash whose event was lost", async () => {
|
|
536
|
+
const a = await arrange();
|
|
537
|
+
queueTakes(a.w);
|
|
538
|
+
await a.w.client.messageMove.deleteMessages(
|
|
539
|
+
a.w.account.accountConfigId,
|
|
540
|
+
[a.message.messageId],
|
|
541
|
+
a.w.account.accountId,
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
assert.deepEqual(await redriveLater(a), [
|
|
545
|
+
{
|
|
546
|
+
type: "MESSAGE_DELETE",
|
|
547
|
+
schemaVersion: 3,
|
|
548
|
+
accountId: a.w.account.accountId,
|
|
549
|
+
messageId: a.message.messageId,
|
|
550
|
+
mailboxId: a.inbox.mailboxId,
|
|
551
|
+
uid: a.message.uid,
|
|
552
|
+
operation: "move_to_trash",
|
|
553
|
+
destinationMailboxId: a.trash.mailboxId,
|
|
554
|
+
},
|
|
555
|
+
]);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("re-sends the expunge for a permanent delete whose event was lost", async () => {
|
|
559
|
+
const a = await arrange();
|
|
560
|
+
queueTakes(a.w);
|
|
561
|
+
await a.w.client.messageMove.deleteMessages(
|
|
562
|
+
a.w.account.accountConfigId,
|
|
563
|
+
[a.message.messageId],
|
|
564
|
+
a.w.account.accountId,
|
|
565
|
+
{ permanent: true },
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
assert.deepEqual(await redriveLater(a), [
|
|
569
|
+
{
|
|
570
|
+
type: "MESSAGE_DELETE",
|
|
571
|
+
schemaVersion: 3,
|
|
572
|
+
accountId: a.w.account.accountId,
|
|
573
|
+
messageId: a.message.messageId,
|
|
574
|
+
mailboxId: a.inbox.mailboxId,
|
|
575
|
+
uid: a.message.uid,
|
|
576
|
+
operation: "permanent_delete",
|
|
577
|
+
},
|
|
578
|
+
]);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("passes over a move while a folder it touches awaits a cursor rebuild", async () => {
|
|
582
|
+
const a = await arrange();
|
|
583
|
+
queueTakes(a.w);
|
|
584
|
+
await a.w.client.messageMove.moveMessage(
|
|
585
|
+
a.w.account.accountConfigId,
|
|
586
|
+
a.message.messageId,
|
|
587
|
+
a.notifications.mailboxId,
|
|
588
|
+
a.w.account.accountId,
|
|
589
|
+
);
|
|
590
|
+
await a.w.client.mailbox.update(a.w.account.accountId, a.inbox.mailboxId, {
|
|
591
|
+
cursorState: MailboxCursorState.rebuilding,
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
assert.deepEqual(await redriveLater(a), []);
|
|
595
|
+
|
|
596
|
+
await a.w.client.mailbox.update(a.w.account.accountId, a.inbox.mailboxId, {
|
|
597
|
+
cursorState: MailboxCursorState.normal,
|
|
598
|
+
});
|
|
599
|
+
assert.deepEqual(
|
|
600
|
+
(await redriveLater(a)).map((event) => event.type),
|
|
601
|
+
["MESSAGE_MOVE"],
|
|
602
|
+
);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it("leaves a move inside its budget and a just re-driven move alone", async () => {
|
|
606
|
+
const a = await arrange();
|
|
607
|
+
queueTakes(a.w);
|
|
608
|
+
await a.w.client.messageMove.moveMessage(
|
|
609
|
+
a.w.account.accountConfigId,
|
|
610
|
+
a.message.messageId,
|
|
611
|
+
a.notifications.mailboxId,
|
|
612
|
+
a.w.account.accountId,
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
assert.deepEqual(await redriveLater(a, 0), [], "inside the budget");
|
|
616
|
+
assert.deepEqual(
|
|
617
|
+
(await redriveLater(a)).map((event) => event.type),
|
|
618
|
+
["MESSAGE_MOVE"],
|
|
619
|
+
);
|
|
620
|
+
assert.deepEqual(
|
|
621
|
+
await redriveLater(a, STRANDED_PLACEMENT_AFTER_MILLIS / 2),
|
|
622
|
+
[],
|
|
623
|
+
"a re-driven row waits out the budget again",
|
|
624
|
+
);
|
|
625
|
+
assert.equal(
|
|
626
|
+
(await a.w.row(a.message.messageId))?.status,
|
|
627
|
+
MessageStatus.moving,
|
|
628
|
+
);
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
describe("a uid-0 copy whose event is gone", () => {
|
|
633
|
+
const strandCopy = async (a: Arranged): Promise<string> => {
|
|
634
|
+
queueTakes(a.w);
|
|
635
|
+
return a.w.client.messageMove.copyMessage(
|
|
636
|
+
a.w.account.accountConfigId,
|
|
637
|
+
a.message.messageId,
|
|
638
|
+
a.notifications.mailboxId,
|
|
639
|
+
a.w.account.accountId,
|
|
640
|
+
);
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
const redriveCopy = async (a: Arranged): Promise<void> => {
|
|
644
|
+
const [copy] = await redriveLater(a);
|
|
645
|
+
assert.equal(copy?.type, "MESSAGE_COPY");
|
|
646
|
+
await handleMessageCopy(
|
|
647
|
+
stamped<MessageCopyEvent>(copy),
|
|
648
|
+
a.w.log,
|
|
649
|
+
1,
|
|
650
|
+
a.w.scope,
|
|
651
|
+
);
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
it("binds a copy that did land to the destination's uid, as its one row", async () => {
|
|
655
|
+
const a = await arrange();
|
|
656
|
+
const copyId = await strandCopy(a);
|
|
657
|
+
const landedUid = a.w.imap.deliver(
|
|
658
|
+
a.notifications.fullPath,
|
|
659
|
+
mail(a.message.messageIdHeader),
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
await redriveCopy(a);
|
|
663
|
+
|
|
664
|
+
const copy = await a.w.row(copyId);
|
|
665
|
+
assert.equal(copy?.status, MessageStatus.active);
|
|
666
|
+
assert.equal(copy?.uid, landedUid);
|
|
667
|
+
assert.deepEqual(a.w.imap.commands("COPY"), []);
|
|
668
|
+
assert.deepEqual(a.w.imap.uids(a.notifications.fullPath), [landedUid]);
|
|
669
|
+
assert.deepEqual(
|
|
670
|
+
(
|
|
671
|
+
await a.w.client.message.listAllByMailbox(a.notifications.mailboxId)
|
|
672
|
+
).map((row) => row.messageId),
|
|
673
|
+
[copyId],
|
|
674
|
+
);
|
|
675
|
+
assert.deepEqual(await a.w.threadRows(copyId), [
|
|
676
|
+
{
|
|
677
|
+
mailboxId: a.notifications.mailboxId,
|
|
678
|
+
uid: landedUid,
|
|
679
|
+
isDeleted: false,
|
|
680
|
+
},
|
|
681
|
+
]);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it("finds the source of a copy in a thread longer than a page", async () => {
|
|
685
|
+
const a = await arrange();
|
|
686
|
+
const source = await a.w.client.threadMessage.getByMessageId(
|
|
687
|
+
a.w.account.accountConfigId,
|
|
688
|
+
a.message.messageId,
|
|
689
|
+
);
|
|
690
|
+
for (let n = 0; n < 101; n += 1) {
|
|
691
|
+
await a.w.client.threadMessage.create({
|
|
692
|
+
accountConfigId: a.w.account.accountConfigId,
|
|
693
|
+
threadId: source.threadId,
|
|
694
|
+
messageId: `reply-${n}`,
|
|
695
|
+
mailboxId: a.inbox.mailboxId,
|
|
696
|
+
uid: 1000 + n,
|
|
697
|
+
referenceOrder: n + 1,
|
|
698
|
+
internalDate: source.internalDate + n + 1,
|
|
699
|
+
sentDate: source.sentDate + n + 1,
|
|
700
|
+
isRead: false,
|
|
701
|
+
isDeleted: false,
|
|
702
|
+
hasAttachment: false,
|
|
703
|
+
hasStars: false,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
const copyId = await strandCopy(a);
|
|
707
|
+
const landedUid = a.w.imap.deliver(
|
|
708
|
+
a.notifications.fullPath,
|
|
709
|
+
mail(a.message.messageIdHeader),
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
await redriveCopy(a);
|
|
713
|
+
|
|
714
|
+
assert.equal((await a.w.row(copyId))?.uid, landedUid);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it("removes a copy whose source was deleted", async () => {
|
|
718
|
+
const a = await arrange();
|
|
719
|
+
const copyId = await strandCopy(a);
|
|
720
|
+
await a.w.client.message.transitionPlacement(
|
|
721
|
+
a.message.messageId,
|
|
722
|
+
{ status: MessageStatus.active },
|
|
723
|
+
{ status: MessageStatus.deleted },
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
assert.deepEqual(await redriveLater(a), []);
|
|
727
|
+
|
|
728
|
+
assert.equal(await a.w.row(copyId), undefined);
|
|
729
|
+
assert.ok(
|
|
730
|
+
a.w.logs.some(
|
|
731
|
+
(line) =>
|
|
732
|
+
line.fields.alert === "stranded_copy_unresolvable" &&
|
|
733
|
+
line.fields.reason === "source row deleted",
|
|
734
|
+
),
|
|
735
|
+
);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("copies once a copy that never landed", async () => {
|
|
739
|
+
const a = await arrange();
|
|
740
|
+
const copyId = await strandCopy(a);
|
|
741
|
+
|
|
742
|
+
await redriveCopy(a);
|
|
743
|
+
|
|
744
|
+
const copy = await a.w.row(copyId);
|
|
745
|
+
assert.equal(copy?.status, MessageStatus.active);
|
|
746
|
+
assert.equal(a.w.imap.commands("COPY").length, 1);
|
|
747
|
+
assert.deepEqual(a.w.imap.uids(a.notifications.fullPath), [copy?.uid]);
|
|
748
|
+
assert.deepEqual(await placementOf(a), {
|
|
749
|
+
...backIn(a, a.inbox),
|
|
750
|
+
syncStatus: a.seededSyncStatus,
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
it("removes a copy whose source message is gone everywhere", async () => {
|
|
755
|
+
const a = await arrange();
|
|
756
|
+
const copyId = await strandCopy(a);
|
|
757
|
+
await a.w.client.threadMessage.deleteMany(
|
|
758
|
+
(
|
|
759
|
+
await a.w.client.threadMessage.findAllByMessageId(
|
|
760
|
+
a.w.account.accountConfigId,
|
|
761
|
+
a.message.messageId,
|
|
762
|
+
)
|
|
763
|
+
).map((row) => ({
|
|
764
|
+
accountConfigId: row.accountConfigId,
|
|
765
|
+
threadMessageId: row.threadMessageId,
|
|
766
|
+
})),
|
|
767
|
+
);
|
|
768
|
+
await a.w.client.message.delete(a.message.messageId);
|
|
769
|
+
|
|
770
|
+
assert.deepEqual(await redriveLater(a), []);
|
|
771
|
+
|
|
772
|
+
assert.equal(await a.w.row(copyId), undefined);
|
|
773
|
+
assert.deepEqual(await a.w.threadRows(copyId), []);
|
|
774
|
+
assert.ok(
|
|
775
|
+
a.w.logs.some(
|
|
776
|
+
(line) =>
|
|
777
|
+
line.level === "error" &&
|
|
778
|
+
line.fields.alert === "stranded_copy_unresolvable" &&
|
|
779
|
+
line.fields.reason === "source row missing" &&
|
|
780
|
+
line.fields.messageId === copyId,
|
|
781
|
+
),
|
|
782
|
+
);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
it("is left alone inside the budget", async () => {
|
|
786
|
+
const a = await arrange();
|
|
787
|
+
const copyId = await strandCopy(a);
|
|
788
|
+
|
|
789
|
+
assert.deepEqual(await redriveLater(a, 0), []);
|
|
790
|
+
|
|
791
|
+
const copy = await a.w.row(copyId);
|
|
792
|
+
assert.equal(copy?.status, MessageStatus.moving);
|
|
793
|
+
assert.equal(copy?.uid, 0);
|
|
794
|
+
});
|
|
795
|
+
});
|