@remit/imap-worker 0.0.69 → 0.0.70
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
CHANGED
|
@@ -33,6 +33,10 @@ interface LocalMessage {
|
|
|
33
33
|
messageId: string;
|
|
34
34
|
uid: number;
|
|
35
35
|
status: string;
|
|
36
|
+
syncStatus?: string;
|
|
37
|
+
mailboxId?: string;
|
|
38
|
+
originalMailboxId?: string;
|
|
39
|
+
originalUid?: number;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
type TrashMailbox = { mailboxId: string; fullPath: string };
|
|
@@ -77,6 +81,29 @@ const deleting = (messageId: string, uid: number): LocalMessage => ({
|
|
|
77
81
|
status: "deleting",
|
|
78
82
|
});
|
|
79
83
|
|
|
84
|
+
/** Mid-move into Trash: the folder is written, the uid is still the source's. */
|
|
85
|
+
const movingIntoTrash = (messageId: string, uid: number): LocalMessage => ({
|
|
86
|
+
messageId,
|
|
87
|
+
uid,
|
|
88
|
+
status: "moving",
|
|
89
|
+
syncStatus: "pending",
|
|
90
|
+
mailboxId: "trash-mbx",
|
|
91
|
+
originalMailboxId: "inbox-mbx",
|
|
92
|
+
originalUid: uid,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/** The same row between a failed attempt and its redelivery. */
|
|
96
|
+
const retryingIntoTrash = (messageId: string, uid: number): LocalMessage => ({
|
|
97
|
+
...movingIntoTrash(messageId, uid),
|
|
98
|
+
syncStatus: "failed",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/** The same row after an empty marked the folder, `status` overwritten. */
|
|
102
|
+
const markedMidMove = (messageId: string, uid: number): LocalMessage => ({
|
|
103
|
+
...movingIntoTrash(messageId, uid),
|
|
104
|
+
status: "deleting",
|
|
105
|
+
});
|
|
106
|
+
|
|
80
107
|
const fresh = (): Harness => ({
|
|
81
108
|
calls: [],
|
|
82
109
|
account: { accountId: "acc-1", accountConfigId: "cfg-1" },
|
|
@@ -417,3 +444,114 @@ describe("handleEmptyTrash", () => {
|
|
|
417
444
|
assert.equal(h.disconnectCount, 1);
|
|
418
445
|
});
|
|
419
446
|
});
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Issue #1217. The sweep matched every row the Trash listing returned against
|
|
450
|
+
* the expunged uids. A row whose move into Trash has not settled carries the
|
|
451
|
+
* SOURCE folder's uid, so that match answers for whatever Trash holds at that
|
|
452
|
+
* uid — a different message, whose Message and thread rows the sweep then
|
|
453
|
+
* deletes while the mail it named is still sitting in the folder it never left.
|
|
454
|
+
*/
|
|
455
|
+
describe("handleEmptyTrash and an unsettled placement", () => {
|
|
456
|
+
beforeEach(() => {
|
|
457
|
+
h = fresh();
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("deletes no row of a message whose move into Trash has not settled", async () => {
|
|
461
|
+
h.localMessages = [
|
|
462
|
+
deleting("msg-1", 10),
|
|
463
|
+
movingIntoTrash("msg-moving", 10),
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
await handleEmptyTrash(event, noopLog, deps());
|
|
467
|
+
|
|
468
|
+
assert.deepEqual(
|
|
469
|
+
called("message.delete").map((c) => c.args[0]),
|
|
470
|
+
["msg-1"],
|
|
471
|
+
"uid 10 is msg-1's; the in-flight row only borrows it from the inbox",
|
|
472
|
+
);
|
|
473
|
+
assert.equal(called("threadMessage.delete").length, 1);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("leaves a row whose move is mid-retry rather than sweeping it", async () => {
|
|
477
|
+
// `syncStatus: failed` under `status: moving` is an ordinary transient
|
|
478
|
+
// attempt about to be redelivered (`data-ports/message-settlement.ts`),
|
|
479
|
+
// not a give-up. Either way the pair is a lie while it stands, and the
|
|
480
|
+
// uid is somebody else's.
|
|
481
|
+
h.localMessages = [retryingIntoTrash("msg-retrying", 11)];
|
|
482
|
+
|
|
483
|
+
await handleEmptyTrash(event, noopLog, deps());
|
|
484
|
+
|
|
485
|
+
assert.equal(called("message.delete").length, 0);
|
|
486
|
+
assert.equal(called("threadMessage.delete").length, 0);
|
|
487
|
+
assert.deepEqual(revertedMessageIds(), []);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("hands back, rather than binds, a mid-move row an earlier mark rewrote", async () => {
|
|
491
|
+
// Defence in depth for the shape `emptyTrash` used to write: the mark
|
|
492
|
+
// overwrote `status` and the row reached here reading settled while its
|
|
493
|
+
// uid still belonged to the inbox. The uid is not bound — but the mark is
|
|
494
|
+
// cleared, because nothing else is coming to clear it and a row left
|
|
495
|
+
// `deleting` is hidden from every listing for good.
|
|
496
|
+
h.localMessages = [deleting("msg-1", 10), markedMidMove("msg-marked", 11)];
|
|
497
|
+
|
|
498
|
+
await handleEmptyTrash(event, noopLog, deps());
|
|
499
|
+
|
|
500
|
+
assert.deepEqual(
|
|
501
|
+
called("message.delete").map((c) => c.args[0]),
|
|
502
|
+
["msg-1"],
|
|
503
|
+
);
|
|
504
|
+
assert.deepEqual(revertedMessageIds(), ["msg-marked"]);
|
|
505
|
+
assert.deepEqual(undeletedThreadMessageIds(), ["tm-msg-marked"]);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("sweeps a settled row whose Trash uid matches the one it left behind", async () => {
|
|
509
|
+
// Two folders count uids independently, so a move can land on the same
|
|
510
|
+
// number it started from. `updateUid` drops `originalUid` when it settles,
|
|
511
|
+
// which is what keeps this row apart from one still mid-move — without it
|
|
512
|
+
// the sweep would expunge the message on the server and then refuse its
|
|
513
|
+
// rows, leaving mail that no longer exists hidden in Trash for good.
|
|
514
|
+
h.localMessages = [
|
|
515
|
+
{
|
|
516
|
+
messageId: "msg-collided",
|
|
517
|
+
uid: 10,
|
|
518
|
+
status: "deleting",
|
|
519
|
+
syncStatus: "pending",
|
|
520
|
+
mailboxId: "trash-mbx",
|
|
521
|
+
originalMailboxId: "inbox-mbx",
|
|
522
|
+
},
|
|
523
|
+
];
|
|
524
|
+
|
|
525
|
+
await handleEmptyTrash(event, noopLog, deps());
|
|
526
|
+
|
|
527
|
+
assert.deepEqual(
|
|
528
|
+
called("message.delete").map((c) => c.args[0]),
|
|
529
|
+
["msg-collided"],
|
|
530
|
+
);
|
|
531
|
+
assert.deepEqual(revertedMessageIds(), []);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("sweeps a settled row that reached Trash by a move", async () => {
|
|
535
|
+
// The confirmed pair: `updateUid` wrote Trash's own uid over the
|
|
536
|
+
// source's, leaving `originalUid` behind as history. Refusing this would
|
|
537
|
+
// leave the folder marked for a deletion that already happened.
|
|
538
|
+
h.localMessages = [
|
|
539
|
+
{
|
|
540
|
+
messageId: "msg-settled",
|
|
541
|
+
uid: 11,
|
|
542
|
+
status: "deleting",
|
|
543
|
+
syncStatus: "pending",
|
|
544
|
+
mailboxId: "trash-mbx",
|
|
545
|
+
originalMailboxId: "inbox-mbx",
|
|
546
|
+
originalUid: 42,
|
|
547
|
+
},
|
|
548
|
+
];
|
|
549
|
+
|
|
550
|
+
await handleEmptyTrash(event, noopLog, deps());
|
|
551
|
+
|
|
552
|
+
assert.deepEqual(
|
|
553
|
+
called("message.delete").map((c) => c.args[0]),
|
|
554
|
+
["msg-settled"],
|
|
555
|
+
);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
@@ -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(
|