@remit/imap-worker 0.0.69 → 0.0.71
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 +1 -1
- package/src/handlers/append-sent-message.test.ts +20 -30
- package/src/handlers/delete-account-objects.test.ts +6 -16
- package/src/handlers/empty-trash.test.ts +159 -28
- package/src/handlers/empty-trash.ts +31 -9
- package/src/handlers/mailbox-management.test.ts +25 -35
- package/src/handlers/message-copy.test.ts +25 -35
- package/src/handlers/message-delete.test.ts +40 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { beforeEach, describe, it } from "node:test";
|
|
3
|
-
import
|
|
3
|
+
import { noopLogger } from "@remit/logger-lambda/noop-logger";
|
|
4
4
|
import type { AppendSentMessageEvent } from "../events.js";
|
|
5
5
|
import {
|
|
6
6
|
APPEND_SENT_MAX_ATTEMPTS,
|
|
@@ -8,16 +8,6 @@ import {
|
|
|
8
8
|
handleAppendSentMessage,
|
|
9
9
|
} from "./append-sent-message.js";
|
|
10
10
|
|
|
11
|
-
const noopLog = {
|
|
12
|
-
info: () => {},
|
|
13
|
-
warn: () => {},
|
|
14
|
-
error: () => {},
|
|
15
|
-
debug: () => {},
|
|
16
|
-
fatal: () => {},
|
|
17
|
-
trace: () => {},
|
|
18
|
-
child: () => noopLog,
|
|
19
|
-
} as unknown as Logger;
|
|
20
|
-
|
|
21
11
|
interface Call {
|
|
22
12
|
method: string;
|
|
23
13
|
args: unknown[];
|
|
@@ -175,7 +165,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
175
165
|
});
|
|
176
166
|
|
|
177
167
|
it("appends a seen RFC822 copy to the Sent folder and drops the outbox row", async () => {
|
|
178
|
-
await handleAppendSentMessage(event,
|
|
168
|
+
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
179
169
|
|
|
180
170
|
const append = called("connection.append")[0];
|
|
181
171
|
assert.equal(append?.args[0], "INBOX/Sent");
|
|
@@ -188,7 +178,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
188
178
|
});
|
|
189
179
|
|
|
190
180
|
it("takes the draft's stored attachments with the row (#679)", async () => {
|
|
191
|
-
await handleAppendSentMessage(event,
|
|
181
|
+
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
192
182
|
|
|
193
183
|
// The row is the only reference to those objects. Sweeping has to happen
|
|
194
184
|
// here too, not only on a discard, or a sent message leaves its files
|
|
@@ -201,7 +191,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
201
191
|
});
|
|
202
192
|
|
|
203
193
|
it("builds the message from the outbox row's own headers", async () => {
|
|
204
|
-
await handleAppendSentMessage(event,
|
|
194
|
+
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
205
195
|
|
|
206
196
|
const raw = String(called("connection.append")[0]?.args[1] as Buffer);
|
|
207
197
|
assert.match(raw, /^From: Alice <alice@example\.com>$/m);
|
|
@@ -216,7 +206,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
216
206
|
it("uses a bare address when the outbox row carries no display name", async () => {
|
|
217
207
|
h.outbox = { ...h.outbox, fromName: undefined };
|
|
218
208
|
|
|
219
|
-
await handleAppendSentMessage(event,
|
|
209
|
+
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
220
210
|
|
|
221
211
|
const raw = String(called("connection.append")[0]?.args[1] as Buffer);
|
|
222
212
|
assert.match(raw, /^From: alice@example\.com$/m);
|
|
@@ -225,7 +215,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
225
215
|
it("settles the row as unfiled when the account has no Sent folder at all", async () => {
|
|
226
216
|
h.sentMailbox = null;
|
|
227
217
|
|
|
228
|
-
await handleAppendSentMessage(event,
|
|
218
|
+
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
229
219
|
|
|
230
220
|
assert.equal(called("connection.append").length, 0);
|
|
231
221
|
assert.equal(called("outboxMessage.delete").length, 0);
|
|
@@ -243,7 +233,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
243
233
|
it("skips the append while the outbox row is not yet sent", async () => {
|
|
244
234
|
h.outbox = { ...h.outbox, status: "pending" };
|
|
245
235
|
|
|
246
|
-
await handleAppendSentMessage(event,
|
|
236
|
+
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
247
237
|
|
|
248
238
|
assert.equal(called("connection.append").length, 0);
|
|
249
239
|
assert.equal(called("outboxMessage.delete").length, 0);
|
|
@@ -252,7 +242,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
252
242
|
it("returns early without touching the outbox when the account is soft-deleted", async () => {
|
|
253
243
|
h.account = { ...h.account, deletedAt: Date.now() };
|
|
254
244
|
|
|
255
|
-
await handleAppendSentMessage(event,
|
|
245
|
+
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
256
246
|
|
|
257
247
|
assert.equal(called("connection.append").length, 0);
|
|
258
248
|
assert.equal(called("outboxMessage.delete").length, 0);
|
|
@@ -264,7 +254,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
264
254
|
};
|
|
265
255
|
|
|
266
256
|
await assert.rejects(
|
|
267
|
-
handleAppendSentMessage(event,
|
|
257
|
+
handleAppendSentMessage(event, noopLogger, 1, deps()),
|
|
268
258
|
/server exploded/,
|
|
269
259
|
);
|
|
270
260
|
|
|
@@ -282,7 +272,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
282
272
|
|
|
283
273
|
await handleAppendSentMessage(
|
|
284
274
|
event,
|
|
285
|
-
|
|
275
|
+
noopLogger,
|
|
286
276
|
APPEND_SENT_MAX_ATTEMPTS,
|
|
287
277
|
deps(),
|
|
288
278
|
);
|
|
@@ -301,7 +291,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
301
291
|
it("settles the row as unfiled when a terminal auth failure acks the record", async () => {
|
|
302
292
|
h.ackWithoutWork = true;
|
|
303
293
|
|
|
304
|
-
await handleAppendSentMessage(event,
|
|
294
|
+
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
305
295
|
|
|
306
296
|
const patch = called("outboxMessage.update")[0]?.args[2] as {
|
|
307
297
|
status: string;
|
|
@@ -313,7 +303,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
313
303
|
|
|
314
304
|
it("leaves the row alone when the APPEND landed but the delete did not", async () => {
|
|
315
305
|
await assert.rejects(
|
|
316
|
-
handleAppendSentMessage(event,
|
|
306
|
+
handleAppendSentMessage(event, noopLogger, 1, depsWithFailingDelete()),
|
|
317
307
|
/storage down/,
|
|
318
308
|
);
|
|
319
309
|
|
|
@@ -325,7 +315,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
325
315
|
it("stops redelivering a landed APPEND at the budget instead of filing another copy", async () => {
|
|
326
316
|
await handleAppendSentMessage(
|
|
327
317
|
event,
|
|
328
|
-
|
|
318
|
+
noopLogger,
|
|
329
319
|
APPEND_SENT_MAX_ATTEMPTS,
|
|
330
320
|
depsWithFailingDelete(),
|
|
331
321
|
);
|
|
@@ -335,7 +325,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
335
325
|
});
|
|
336
326
|
|
|
337
327
|
it("records the uid the APPEND produced before it deletes the row", async () => {
|
|
338
|
-
await handleAppendSentMessage(event,
|
|
328
|
+
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
339
329
|
|
|
340
330
|
// Order is the whole of it: a uid written after the delete is a uid a
|
|
341
331
|
// redelivery never sees, and the redelivery is what files the second copy.
|
|
@@ -352,7 +342,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
352
342
|
// nothing, and "filed" still has to be told apart from "not filed".
|
|
353
343
|
h.append = async () => ({ uid: 0, uidValidity: 0 });
|
|
354
344
|
|
|
355
|
-
await handleAppendSentMessage(event,
|
|
345
|
+
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
356
346
|
|
|
357
347
|
assert.deepEqual(patches(), [{ appendedUid: -1 }]);
|
|
358
348
|
assert.equal(called("outboxMessage.delete").length, 1);
|
|
@@ -361,7 +351,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
361
351
|
it("files nothing a second time when a redelivery finds a recorded uid (#858)", async () => {
|
|
362
352
|
h.outbox = { ...h.outbox, appendedUid: 55 };
|
|
363
353
|
|
|
364
|
-
await handleAppendSentMessage(event,
|
|
354
|
+
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
365
355
|
|
|
366
356
|
// The copy is already in the user's Sent folder. All this redelivery owes
|
|
367
357
|
// is the delete the last attempt could not make.
|
|
@@ -382,7 +372,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
382
372
|
h.outbox = { ...h.outbox, appendedUid: 55 };
|
|
383
373
|
|
|
384
374
|
await assert.rejects(
|
|
385
|
-
handleAppendSentMessage(event,
|
|
375
|
+
handleAppendSentMessage(event, noopLogger, 1, depsWithFailingDelete()),
|
|
386
376
|
/storage down/,
|
|
387
377
|
);
|
|
388
378
|
|
|
@@ -392,7 +382,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
392
382
|
|
|
393
383
|
it("keeps the row when the uid cannot be recorded", async () => {
|
|
394
384
|
await assert.rejects(
|
|
395
|
-
handleAppendSentMessage(event,
|
|
385
|
+
handleAppendSentMessage(event, noopLogger, 1, depsWithFailingUpdate()),
|
|
396
386
|
/storage down/,
|
|
397
387
|
);
|
|
398
388
|
|
|
@@ -425,7 +415,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
425
415
|
},
|
|
426
416
|
};
|
|
427
417
|
|
|
428
|
-
await handleAppendSentMessage(event,
|
|
418
|
+
await handleAppendSentMessage(event, noopLogger, 1, failing);
|
|
429
419
|
|
|
430
420
|
assert.equal(called("connection.append").length, 0);
|
|
431
421
|
assert.deepEqual(patches(), []);
|
|
@@ -436,7 +426,7 @@ describe("handleAppendSentMessage", () => {
|
|
|
436
426
|
|
|
437
427
|
await handleAppendSentMessage(
|
|
438
428
|
event,
|
|
439
|
-
|
|
429
|
+
noopLogger,
|
|
440
430
|
APPEND_SENT_MAX_ATTEMPTS,
|
|
441
431
|
depsWithFailingDelete(),
|
|
442
432
|
);
|
|
@@ -6,21 +6,11 @@ import {
|
|
|
6
6
|
S3Client,
|
|
7
7
|
} from "@aws-sdk/client-s3";
|
|
8
8
|
import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
9
|
-
import
|
|
9
|
+
import { noopLogger } from "@remit/logger-lambda/noop-logger";
|
|
10
10
|
import { mockClient } from "aws-sdk-client-mock";
|
|
11
11
|
import type { DeleteAccountObjectsEvent } from "../events.js";
|
|
12
12
|
import { handleDeleteAccountObjects } from "./delete-account-objects.js";
|
|
13
13
|
|
|
14
|
-
const noopLog = {
|
|
15
|
-
info: () => {},
|
|
16
|
-
warn: () => {},
|
|
17
|
-
error: () => {},
|
|
18
|
-
debug: () => {},
|
|
19
|
-
fatal: () => {},
|
|
20
|
-
trace: () => {},
|
|
21
|
-
child: () => noopLog,
|
|
22
|
-
} as unknown as Logger;
|
|
23
|
-
|
|
24
14
|
const s3Mock = mockClient(S3Client);
|
|
25
15
|
const sqsMock = mockClient(SQSClient);
|
|
26
16
|
|
|
@@ -45,7 +35,7 @@ describe("handleDeleteAccountObjects", () => {
|
|
|
45
35
|
});
|
|
46
36
|
s3Mock.on(DeleteObjectsCommand).resolves({});
|
|
47
37
|
|
|
48
|
-
await handleDeleteAccountObjects(event,
|
|
38
|
+
await handleDeleteAccountObjects(event, noopLogger);
|
|
49
39
|
|
|
50
40
|
const listCall = s3Mock.commandCalls(ListObjectsV2Command)[0];
|
|
51
41
|
assert.equal(listCall.args[0].input.Prefix, "accounts/cfg-1/");
|
|
@@ -72,7 +62,7 @@ describe("handleDeleteAccountObjects", () => {
|
|
|
72
62
|
});
|
|
73
63
|
s3Mock.on(DeleteObjectsCommand).resolves({});
|
|
74
64
|
|
|
75
|
-
await handleDeleteAccountObjects(event,
|
|
65
|
+
await handleDeleteAccountObjects(event, noopLogger);
|
|
76
66
|
|
|
77
67
|
const listCalls = s3Mock.commandCalls(ListObjectsV2Command);
|
|
78
68
|
assert.equal(listCalls.length, 2);
|
|
@@ -83,7 +73,7 @@ describe("handleDeleteAccountObjects", () => {
|
|
|
83
73
|
it("skips the delete call when a page holds no keys", async () => {
|
|
84
74
|
s3Mock.on(ListObjectsV2Command).resolves({ IsTruncated: false });
|
|
85
75
|
|
|
86
|
-
await handleDeleteAccountObjects(event,
|
|
76
|
+
await handleDeleteAccountObjects(event, noopLogger);
|
|
87
77
|
|
|
88
78
|
assert.equal(s3Mock.commandCalls(DeleteObjectsCommand).length, 0);
|
|
89
79
|
});
|
|
@@ -94,7 +84,7 @@ describe("handleDeleteAccountObjects", () => {
|
|
|
94
84
|
|
|
95
85
|
await handleDeleteAccountObjects(
|
|
96
86
|
{ ...event, continuationToken: "tok-mid" },
|
|
97
|
-
|
|
87
|
+
noopLogger,
|
|
98
88
|
() => 5_000,
|
|
99
89
|
);
|
|
100
90
|
|
|
@@ -119,7 +109,7 @@ describe("handleDeleteAccountObjects", () => {
|
|
|
119
109
|
});
|
|
120
110
|
s3Mock.on(DeleteObjectsCommand).resolves({});
|
|
121
111
|
|
|
122
|
-
await handleDeleteAccountObjects(event,
|
|
112
|
+
await handleDeleteAccountObjects(event, noopLogger, () => 120_000);
|
|
123
113
|
|
|
124
114
|
assert.equal(s3Mock.commandCalls(ListObjectsV2Command).length, 1);
|
|
125
115
|
assert.equal(sqsMock.commandCalls(SendMessageCommand).length, 0);
|
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { beforeEach, describe, it } from "node:test";
|
|
3
3
|
import type { RoleResolution } from "@remit/data-ports/folder-role";
|
|
4
|
-
import
|
|
4
|
+
import { noopLogger } from "@remit/logger-lambda/noop-logger";
|
|
5
5
|
import type { EmptyTrashEvent } from "../events.js";
|
|
6
6
|
import { type EmptyTrashDeps, handleEmptyTrash } from "./empty-trash.js";
|
|
7
7
|
|
|
8
|
-
const noopLog = {
|
|
9
|
-
info: () => {},
|
|
10
|
-
warn: () => {},
|
|
11
|
-
error: () => {},
|
|
12
|
-
debug: () => {},
|
|
13
|
-
fatal: () => {},
|
|
14
|
-
trace: () => {},
|
|
15
|
-
child: () => noopLog,
|
|
16
|
-
} as unknown as Logger;
|
|
17
|
-
|
|
18
8
|
interface Call {
|
|
19
9
|
method: string;
|
|
20
10
|
args: unknown[];
|
|
@@ -33,6 +23,10 @@ interface LocalMessage {
|
|
|
33
23
|
messageId: string;
|
|
34
24
|
uid: number;
|
|
35
25
|
status: string;
|
|
26
|
+
syncStatus?: string;
|
|
27
|
+
mailboxId?: string;
|
|
28
|
+
originalMailboxId?: string;
|
|
29
|
+
originalUid?: number;
|
|
36
30
|
}
|
|
37
31
|
|
|
38
32
|
type TrashMailbox = { mailboxId: string; fullPath: string };
|
|
@@ -77,6 +71,29 @@ const deleting = (messageId: string, uid: number): LocalMessage => ({
|
|
|
77
71
|
status: "deleting",
|
|
78
72
|
});
|
|
79
73
|
|
|
74
|
+
/** Mid-move into Trash: the folder is written, the uid is still the source's. */
|
|
75
|
+
const movingIntoTrash = (messageId: string, uid: number): LocalMessage => ({
|
|
76
|
+
messageId,
|
|
77
|
+
uid,
|
|
78
|
+
status: "moving",
|
|
79
|
+
syncStatus: "pending",
|
|
80
|
+
mailboxId: "trash-mbx",
|
|
81
|
+
originalMailboxId: "inbox-mbx",
|
|
82
|
+
originalUid: uid,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/** The same row between a failed attempt and its redelivery. */
|
|
86
|
+
const retryingIntoTrash = (messageId: string, uid: number): LocalMessage => ({
|
|
87
|
+
...movingIntoTrash(messageId, uid),
|
|
88
|
+
syncStatus: "failed",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
/** The same row after an empty marked the folder, `status` overwritten. */
|
|
92
|
+
const markedMidMove = (messageId: string, uid: number): LocalMessage => ({
|
|
93
|
+
...movingIntoTrash(messageId, uid),
|
|
94
|
+
status: "deleting",
|
|
95
|
+
});
|
|
96
|
+
|
|
80
97
|
const fresh = (): Harness => ({
|
|
81
98
|
calls: [],
|
|
82
99
|
account: { accountId: "acc-1", accountConfigId: "cfg-1" },
|
|
@@ -197,7 +214,7 @@ describe("handleEmptyTrash", () => {
|
|
|
197
214
|
});
|
|
198
215
|
|
|
199
216
|
it("expunges every server uid and both local rows for each trashed message", async () => {
|
|
200
|
-
await handleEmptyTrash(event,
|
|
217
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
201
218
|
|
|
202
219
|
assert.deepEqual(called("connection.deleteMessages")[0]?.args, [[10, 11]]);
|
|
203
220
|
assert.deepEqual(
|
|
@@ -218,7 +235,7 @@ describe("handleEmptyTrash", () => {
|
|
|
218
235
|
{ messageId: "msg-late", uid: 12, status: "active" },
|
|
219
236
|
];
|
|
220
237
|
|
|
221
|
-
await handleEmptyTrash(event,
|
|
238
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
222
239
|
|
|
223
240
|
assert.deepEqual(
|
|
224
241
|
called("message.delete").map((c) => c.args[0]),
|
|
@@ -232,7 +249,7 @@ describe("handleEmptyTrash", () => {
|
|
|
232
249
|
// exists anywhere, with nothing left to clear the mark.
|
|
233
250
|
h.connection.search = async () => [];
|
|
234
251
|
|
|
235
|
-
await handleEmptyTrash(event,
|
|
252
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
236
253
|
|
|
237
254
|
assert.equal(called("connection.deleteMessages").length, 0);
|
|
238
255
|
assert.equal(called("message.delete").length, 0);
|
|
@@ -247,7 +264,7 @@ describe("handleEmptyTrash", () => {
|
|
|
247
264
|
h.localMessages = [deleting("msg-2", 11)];
|
|
248
265
|
h.connection.search = async () => [];
|
|
249
266
|
|
|
250
|
-
await handleEmptyTrash(event,
|
|
267
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
251
268
|
|
|
252
269
|
assert.equal(called("message.delete").length, 0);
|
|
253
270
|
assert.deepEqual(revertedMessageIds(), ["msg-2"]);
|
|
@@ -261,7 +278,7 @@ describe("handleEmptyTrash", () => {
|
|
|
261
278
|
h.messagesWithoutListingRow = ["msg-expunging"];
|
|
262
279
|
h.connection.search = async () => [];
|
|
263
280
|
|
|
264
|
-
await handleEmptyTrash(event,
|
|
281
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
265
282
|
|
|
266
283
|
assert.deepEqual(revertedMessageIds(), ["msg-1"]);
|
|
267
284
|
});
|
|
@@ -269,7 +286,7 @@ describe("handleEmptyTrash", () => {
|
|
|
269
286
|
it("deletes the message even when it has no thread row", async () => {
|
|
270
287
|
h.threadMessage = false;
|
|
271
288
|
|
|
272
|
-
await handleEmptyTrash(event,
|
|
289
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
273
290
|
|
|
274
291
|
assert.equal(called("message.delete").length, 2);
|
|
275
292
|
assert.equal(called("threadMessage.delete").length, 0);
|
|
@@ -282,7 +299,7 @@ describe("handleEmptyTrash", () => {
|
|
|
282
299
|
deletedAt: Date.now(),
|
|
283
300
|
};
|
|
284
301
|
|
|
285
|
-
await handleEmptyTrash(event,
|
|
302
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
286
303
|
|
|
287
304
|
assert.equal(h.getConnectionCount, 0);
|
|
288
305
|
});
|
|
@@ -290,7 +307,10 @@ describe("handleEmptyTrash", () => {
|
|
|
290
307
|
it("throws when the account no longer exists", async () => {
|
|
291
308
|
h.account = null;
|
|
292
309
|
|
|
293
|
-
await assert.rejects(
|
|
310
|
+
await assert.rejects(
|
|
311
|
+
handleEmptyTrash(event, noopLogger, deps()),
|
|
312
|
+
/not found/,
|
|
313
|
+
);
|
|
294
314
|
});
|
|
295
315
|
|
|
296
316
|
it("acks terminally without connecting when the Trash mailbox was deleted", async () => {
|
|
@@ -298,7 +318,7 @@ describe("handleEmptyTrash", () => {
|
|
|
298
318
|
name: "NotFoundError",
|
|
299
319
|
});
|
|
300
320
|
|
|
301
|
-
await handleEmptyTrash(event,
|
|
321
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
302
322
|
|
|
303
323
|
assert.equal(h.getConnectionCount, 0);
|
|
304
324
|
assert.equal(called("message.delete").length, 0);
|
|
@@ -312,7 +332,7 @@ describe("handleEmptyTrash", () => {
|
|
|
312
332
|
trashMailboxPath: "Trash",
|
|
313
333
|
} as unknown as EmptyTrashEvent;
|
|
314
334
|
|
|
315
|
-
await handleEmptyTrash(unversioned,
|
|
335
|
+
await handleEmptyTrash(unversioned, noopLogger, deps());
|
|
316
336
|
|
|
317
337
|
assert.equal(h.getConnectionCount, 0, "no connection is ever opened");
|
|
318
338
|
assert.equal(called("connection.deleteMessages").length, 0);
|
|
@@ -326,7 +346,7 @@ describe("handleEmptyTrash", () => {
|
|
|
326
346
|
mailbox: { mailboxId: "other-mbx", fullPath: "INBOX/Bak" },
|
|
327
347
|
};
|
|
328
348
|
|
|
329
|
-
await handleEmptyTrash(event,
|
|
349
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
330
350
|
|
|
331
351
|
assert.equal(h.getConnectionCount, 0);
|
|
332
352
|
assert.equal(called("connection.deleteMessages").length, 0);
|
|
@@ -339,7 +359,7 @@ describe("handleEmptyTrash", () => {
|
|
|
339
359
|
mailbox: { mailboxId: "trash-mbx", fullPath: "Trash" },
|
|
340
360
|
};
|
|
341
361
|
|
|
342
|
-
await handleEmptyTrash(event,
|
|
362
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
343
363
|
|
|
344
364
|
assert.equal(called("connection.deleteMessages").length, 0);
|
|
345
365
|
assert.deepEqual(revertedMessageIds(), ["msg-1", "msg-2"]);
|
|
@@ -351,7 +371,7 @@ describe("handleEmptyTrash", () => {
|
|
|
351
371
|
h.connection.openBox = async () => ({ uidvalidity: 77 });
|
|
352
372
|
h.mailbox = { mailboxId: "trash-mbx", uidValidity: 77 };
|
|
353
373
|
|
|
354
|
-
await handleEmptyTrash(event,
|
|
374
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
355
375
|
|
|
356
376
|
assert.equal(called("connection.deleteMessages").length, 0);
|
|
357
377
|
assert.deepEqual(revertedMessageIds(), ["msg-1", "msg-2"]);
|
|
@@ -367,7 +387,7 @@ describe("handleEmptyTrash", () => {
|
|
|
367
387
|
h.connection.openBox = async () => ({ uidvalidity: 77 });
|
|
368
388
|
h.mailbox = { mailboxId: "trash-mbx", uidValidity: 77 };
|
|
369
389
|
|
|
370
|
-
await handleEmptyTrash(event,
|
|
390
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
371
391
|
|
|
372
392
|
assert.deepEqual(revertedMessageIds(), ["msg-1"]);
|
|
373
393
|
assert.deepEqual(undeletedThreadMessageIds(), ["tm-msg-1"]);
|
|
@@ -378,7 +398,7 @@ describe("handleEmptyTrash", () => {
|
|
|
378
398
|
// marked `deleting` hides healthy mail until the user notices.
|
|
379
399
|
h.connection.openBox = async () => ({ uidvalidity: 999 });
|
|
380
400
|
|
|
381
|
-
await handleEmptyTrash(event,
|
|
401
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
382
402
|
|
|
383
403
|
assert.equal(
|
|
384
404
|
(called("mailbox.update")[0]?.args[2] as { cursorState?: string })
|
|
@@ -397,7 +417,7 @@ describe("handleEmptyTrash", () => {
|
|
|
397
417
|
cursorState: "rebuilding",
|
|
398
418
|
};
|
|
399
419
|
|
|
400
|
-
await handleEmptyTrash(event,
|
|
420
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
401
421
|
|
|
402
422
|
assert.equal(h.getConnectionCount, 0);
|
|
403
423
|
assert.equal(called("message.delete").length, 0);
|
|
@@ -410,10 +430,121 @@ describe("handleEmptyTrash", () => {
|
|
|
410
430
|
};
|
|
411
431
|
|
|
412
432
|
await assert.rejects(
|
|
413
|
-
handleEmptyTrash(event,
|
|
433
|
+
handleEmptyTrash(event, noopLogger, deps()),
|
|
414
434
|
/server exploded/,
|
|
415
435
|
);
|
|
416
436
|
|
|
417
437
|
assert.equal(h.disconnectCount, 1);
|
|
418
438
|
});
|
|
419
439
|
});
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Issue #1217. The sweep matched every row the Trash listing returned against
|
|
443
|
+
* the expunged uids. A row whose move into Trash has not settled carries the
|
|
444
|
+
* SOURCE folder's uid, so that match answers for whatever Trash holds at that
|
|
445
|
+
* uid — a different message, whose Message and thread rows the sweep then
|
|
446
|
+
* deletes while the mail it named is still sitting in the folder it never left.
|
|
447
|
+
*/
|
|
448
|
+
describe("handleEmptyTrash and an unsettled placement", () => {
|
|
449
|
+
beforeEach(() => {
|
|
450
|
+
h = fresh();
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("deletes no row of a message whose move into Trash has not settled", async () => {
|
|
454
|
+
h.localMessages = [
|
|
455
|
+
deleting("msg-1", 10),
|
|
456
|
+
movingIntoTrash("msg-moving", 10),
|
|
457
|
+
];
|
|
458
|
+
|
|
459
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
460
|
+
|
|
461
|
+
assert.deepEqual(
|
|
462
|
+
called("message.delete").map((c) => c.args[0]),
|
|
463
|
+
["msg-1"],
|
|
464
|
+
"uid 10 is msg-1's; the in-flight row only borrows it from the inbox",
|
|
465
|
+
);
|
|
466
|
+
assert.equal(called("threadMessage.delete").length, 1);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("leaves a row whose move is mid-retry rather than sweeping it", async () => {
|
|
470
|
+
// `syncStatus: failed` under `status: moving` is an ordinary transient
|
|
471
|
+
// attempt about to be redelivered (`data-ports/message-settlement.ts`),
|
|
472
|
+
// not a give-up. Either way the pair is a lie while it stands, and the
|
|
473
|
+
// uid is somebody else's.
|
|
474
|
+
h.localMessages = [retryingIntoTrash("msg-retrying", 11)];
|
|
475
|
+
|
|
476
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
477
|
+
|
|
478
|
+
assert.equal(called("message.delete").length, 0);
|
|
479
|
+
assert.equal(called("threadMessage.delete").length, 0);
|
|
480
|
+
assert.deepEqual(revertedMessageIds(), []);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("hands back, rather than binds, a mid-move row an earlier mark rewrote", async () => {
|
|
484
|
+
// Defence in depth for the shape `emptyTrash` used to write: the mark
|
|
485
|
+
// overwrote `status` and the row reached here reading settled while its
|
|
486
|
+
// uid still belonged to the inbox. The uid is not bound — but the mark is
|
|
487
|
+
// cleared, because nothing else is coming to clear it and a row left
|
|
488
|
+
// `deleting` is hidden from every listing for good.
|
|
489
|
+
h.localMessages = [deleting("msg-1", 10), markedMidMove("msg-marked", 11)];
|
|
490
|
+
|
|
491
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
492
|
+
|
|
493
|
+
assert.deepEqual(
|
|
494
|
+
called("message.delete").map((c) => c.args[0]),
|
|
495
|
+
["msg-1"],
|
|
496
|
+
);
|
|
497
|
+
assert.deepEqual(revertedMessageIds(), ["msg-marked"]);
|
|
498
|
+
assert.deepEqual(undeletedThreadMessageIds(), ["tm-msg-marked"]);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("sweeps a settled row whose Trash uid matches the one it left behind", async () => {
|
|
502
|
+
// Two folders count uids independently, so a move can land on the same
|
|
503
|
+
// number it started from. `updateUid` drops `originalUid` when it settles,
|
|
504
|
+
// which is what keeps this row apart from one still mid-move — without it
|
|
505
|
+
// the sweep would expunge the message on the server and then refuse its
|
|
506
|
+
// rows, leaving mail that no longer exists hidden in Trash for good.
|
|
507
|
+
h.localMessages = [
|
|
508
|
+
{
|
|
509
|
+
messageId: "msg-collided",
|
|
510
|
+
uid: 10,
|
|
511
|
+
status: "deleting",
|
|
512
|
+
syncStatus: "pending",
|
|
513
|
+
mailboxId: "trash-mbx",
|
|
514
|
+
originalMailboxId: "inbox-mbx",
|
|
515
|
+
},
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
519
|
+
|
|
520
|
+
assert.deepEqual(
|
|
521
|
+
called("message.delete").map((c) => c.args[0]),
|
|
522
|
+
["msg-collided"],
|
|
523
|
+
);
|
|
524
|
+
assert.deepEqual(revertedMessageIds(), []);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("sweeps a settled row that reached Trash by a move", async () => {
|
|
528
|
+
// The confirmed pair: `updateUid` wrote Trash's own uid over the
|
|
529
|
+
// source's, leaving `originalUid` behind as history. Refusing this would
|
|
530
|
+
// leave the folder marked for a deletion that already happened.
|
|
531
|
+
h.localMessages = [
|
|
532
|
+
{
|
|
533
|
+
messageId: "msg-settled",
|
|
534
|
+
uid: 11,
|
|
535
|
+
status: "deleting",
|
|
536
|
+
syncStatus: "pending",
|
|
537
|
+
mailboxId: "trash-mbx",
|
|
538
|
+
originalMailboxId: "inbox-mbx",
|
|
539
|
+
originalUid: 42,
|
|
540
|
+
},
|
|
541
|
+
];
|
|
542
|
+
|
|
543
|
+
await handleEmptyTrash(event, noopLogger, deps());
|
|
544
|
+
|
|
545
|
+
assert.deepEqual(
|
|
546
|
+
called("message.delete").map((c) => c.args[0]),
|
|
547
|
+
["msg-settled"],
|
|
548
|
+
);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
@@ -5,6 +5,7 @@ import { isCurrentSchemaVersion } from "@remit/data-ports/mutation-events";
|
|
|
5
5
|
import { MessageStatus, MessageSyncStatus } from "@remit/domain-enums";
|
|
6
6
|
import type { Logger } from "@remit/logger-lambda";
|
|
7
7
|
import {
|
|
8
|
+
carriesForeignUid,
|
|
8
9
|
guardConnectionCursor,
|
|
9
10
|
isCursorRebuildNeeded,
|
|
10
11
|
MailboxCursorPausedError,
|
|
@@ -248,12 +249,29 @@ export const handleEmptyTrash = async (
|
|
|
248
249
|
const expunged = new Set(uids);
|
|
249
250
|
const localMessages =
|
|
250
251
|
await messageService.listAllByMailbox(trashMailboxId);
|
|
251
|
-
let deletedCount = 0;
|
|
252
252
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
// Reconciles, never waits (imap-mutations R2). A row whose move
|
|
254
|
+
// into Trash has not settled names this folder while still
|
|
255
|
+
// carrying the SOURCE folder's uid, so matching it against the
|
|
256
|
+
// expunge answers for whatever Trash held at that uid — another
|
|
257
|
+
// message, deleted here in both its rows. Waiting is not open to
|
|
258
|
+
// this handler the way it is to the API-side mutators: every event
|
|
259
|
+
// of an account shares one FIFO group, so the move that would
|
|
260
|
+
// settle the row cannot run until this returns, and the ceiling
|
|
261
|
+
// would be spent to reach the same answer.
|
|
262
|
+
//
|
|
263
|
+
// `carriesForeignUid`, not the placement binding: the binding reads
|
|
264
|
+
// `status`, and `status` is exactly what an operation marking this
|
|
265
|
+
// folder overwrites. This is the second half of the gate
|
|
266
|
+
// `emptyTrash` applies before it marks anything.
|
|
267
|
+
const swept = localMessages.filter(
|
|
268
|
+
(message) =>
|
|
269
|
+
!carriesForeignUid(message) && expunged.has(message.uid),
|
|
270
|
+
);
|
|
271
|
+
const sweptIds = new Set(swept.map((message) => message.messageId));
|
|
272
|
+
const deletedCount = swept.length;
|
|
256
273
|
|
|
274
|
+
for (const message of swept) {
|
|
257
275
|
await messageService.delete(message.messageId);
|
|
258
276
|
|
|
259
277
|
const threadMessage = await threadMessageService.findByMessageId(
|
|
@@ -268,12 +286,16 @@ export const handleEmptyTrash = async (
|
|
|
268
286
|
}
|
|
269
287
|
}
|
|
270
288
|
|
|
271
|
-
// Everything
|
|
272
|
-
// that will never come: mail another client already
|
|
273
|
-
// that reached Trash after the SEARCH,
|
|
274
|
-
// partial sweep
|
|
289
|
+
// Everything this sweep did not carry through is still marked for a
|
|
290
|
+
// deletion that will never come: mail another client already
|
|
291
|
+
// emptied, mail that reached Trash after the SEARCH, a redelivery
|
|
292
|
+
// finishing a partial sweep, and a row whose uid the sweep would
|
|
293
|
+
// not bind. All of them must come back rather than sit invisible —
|
|
294
|
+
// the last one especially, since nothing else is coming to clear
|
|
295
|
+
// its mark, and a row hidden in a folder it never left is worse
|
|
296
|
+
// than one the next sync re-projects.
|
|
275
297
|
const revertedCount = await handBackMarkedRows(
|
|
276
|
-
localMessages.filter((message) => !
|
|
298
|
+
localMessages.filter((message) => !sweptIds.has(message.messageId)),
|
|
277
299
|
);
|
|
278
300
|
|
|
279
301
|
log.info(
|