@remit/imap-worker 0.0.75 → 0.0.77
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
|
@@ -596,37 +596,57 @@ describe("processMailboxManagement — the seventh state (#363, D3)", () => {
|
|
|
596
596
|
});
|
|
597
597
|
});
|
|
598
598
|
|
|
599
|
+
/** The delete intent a MAILBOX_DELETE is settled against. */
|
|
600
|
+
const recordDeleteIntent = (): void => {
|
|
601
|
+
h.rows.set(
|
|
602
|
+
"mbx-1",
|
|
603
|
+
row({ mailboxId: "mbx-1", fullPath: "Archive", syncStatus: "deleting" }),
|
|
604
|
+
);
|
|
605
|
+
};
|
|
606
|
+
|
|
599
607
|
describe("processMailboxManagement — MAILBOX_DELETE", () => {
|
|
600
608
|
beforeEach(() => {
|
|
601
609
|
h = fresh();
|
|
610
|
+
recordDeleteIntent();
|
|
602
611
|
});
|
|
603
612
|
|
|
604
|
-
it("deletes on the server and
|
|
613
|
+
it("deletes on the server and takes the folder's mail with it", async () => {
|
|
605
614
|
await processMailboxManagement(deleteEvent, noopLogger, deps());
|
|
606
615
|
|
|
607
616
|
assert.equal(called("connection.deleteMailbox")[0]?.args[0], "Archive");
|
|
608
|
-
assert.deepEqual(called("mailbox.
|
|
617
|
+
assert.deepEqual(called("mailbox.deleteMailboxWithMail")[0]?.args, [
|
|
618
|
+
"acc-1",
|
|
619
|
+
"mbx-1",
|
|
620
|
+
]);
|
|
621
|
+
assert.equal(
|
|
622
|
+
called("mailbox.delete").length,
|
|
623
|
+
0,
|
|
624
|
+
"the bare row delete is what orphaned the folder's mail",
|
|
625
|
+
);
|
|
609
626
|
});
|
|
610
627
|
|
|
611
|
-
it("
|
|
628
|
+
it("removes the folder's mail when the folder is already gone on the server", async () => {
|
|
612
629
|
h.connection.deleteMailbox = async () => {
|
|
613
630
|
throw new Error("Mailbox not found");
|
|
614
631
|
};
|
|
615
632
|
|
|
616
633
|
await processMailboxManagement(deleteEvent, noopLogger, deps());
|
|
617
634
|
|
|
618
|
-
assert.equal(called("mailbox.
|
|
635
|
+
assert.equal(called("mailbox.deleteMailboxWithMail").length, 1);
|
|
619
636
|
});
|
|
620
637
|
|
|
621
|
-
it("
|
|
638
|
+
it("marks the delete failed rather than synced when the server refuses INBOX", async () => {
|
|
639
|
+
// The API refuses a delete of INBOX, so this backstop is unreachable from
|
|
640
|
+
// it. Reached another way, the honest outcome is `failed`: the folder was
|
|
641
|
+
// never deleted, and nothing was undone.
|
|
622
642
|
h.connection.deleteMailbox = async () => {
|
|
623
643
|
throw new Error("Cannot delete INBOX");
|
|
624
644
|
};
|
|
625
645
|
|
|
626
646
|
await processMailboxManagement(deleteEvent, noopLogger, deps());
|
|
627
647
|
|
|
628
|
-
assert.equal(lastSettle().to, "
|
|
629
|
-
assert.equal(called("mailbox.
|
|
648
|
+
assert.equal(lastSettle().to, "failed");
|
|
649
|
+
assert.equal(called("mailbox.deleteMailboxWithMail").length, 0);
|
|
630
650
|
});
|
|
631
651
|
|
|
632
652
|
it("marks the mailbox failed and rethrows on any other delete error", async () => {
|
|
@@ -642,14 +662,21 @@ describe("processMailboxManagement — MAILBOX_DELETE", () => {
|
|
|
642
662
|
assert.equal(lastSettle().to, "failed");
|
|
643
663
|
});
|
|
644
664
|
|
|
645
|
-
it("
|
|
646
|
-
h.
|
|
647
|
-
|
|
648
|
-
|
|
665
|
+
it("issues no DELETE when the row is not deleting", async () => {
|
|
666
|
+
h.rows.set("mbx-1", row({ mailboxId: "mbx-1", syncStatus: "synced" }));
|
|
667
|
+
|
|
668
|
+
await processMailboxManagement(deleteEvent, noopLogger, deps());
|
|
669
|
+
|
|
670
|
+
assert.equal(called("connection.deleteMailbox").length, 0);
|
|
671
|
+
assert.equal(called("mailbox.deleteMailboxWithMail").length, 0);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("acks terminally without connecting when the folder row is gone", async () => {
|
|
649
675
|
h.mailboxRowGone = true;
|
|
650
676
|
|
|
651
677
|
await processMailboxManagement(deleteEvent, noopLogger, deps());
|
|
652
678
|
|
|
679
|
+
assert.equal(called("connection.deleteMailbox").length, 0);
|
|
653
680
|
assert.equal(h.disconnectCount, 1, "the scope is still disconnected");
|
|
654
681
|
});
|
|
655
682
|
});
|
|
@@ -665,7 +692,8 @@ describe("processMailboxManagement — a tagged NO the server means as success (
|
|
|
665
692
|
* only the message treated an already-absent folder as a failure: the row was
|
|
666
693
|
* marked failed and the event left poisoning the account's queue.
|
|
667
694
|
*/
|
|
668
|
-
it("treats it as the delete already having happened, and
|
|
695
|
+
it("treats it as the delete already having happened, mail and all", async () => {
|
|
696
|
+
recordDeleteIntent();
|
|
669
697
|
h.connection.deleteMailbox = async () => {
|
|
670
698
|
throw Object.assign(new Error("Command failed"), {
|
|
671
699
|
serverResponseCode: "NONEXISTENT",
|
|
@@ -676,11 +704,15 @@ describe("processMailboxManagement — a tagged NO the server means as success (
|
|
|
676
704
|
await assert.doesNotReject(
|
|
677
705
|
processMailboxManagement(deleteEvent, noopLogger, deps()),
|
|
678
706
|
);
|
|
679
|
-
assert.deepEqual(called("mailbox.
|
|
707
|
+
assert.deepEqual(called("mailbox.deleteMailboxWithMail")[0]?.args, [
|
|
708
|
+
"acc-1",
|
|
709
|
+
"mbx-1",
|
|
710
|
+
]);
|
|
680
711
|
assert.equal(called("mailbox.transition").length, 0);
|
|
681
712
|
});
|
|
682
713
|
|
|
683
714
|
it("still marks failed and rethrows when the server fails for any other reason", async () => {
|
|
715
|
+
recordDeleteIntent();
|
|
684
716
|
h.connection.deleteMailbox = async () => {
|
|
685
717
|
throw Object.assign(new Error("Command failed"), {
|
|
686
718
|
serverResponseCode: "SERVERBUG",
|
|
@@ -5,7 +5,6 @@ import { isNotFoundError, NotFoundError } from "@remit/data-ports/errors";
|
|
|
5
5
|
import { MailboxSyncStatus } from "@remit/domain-enums";
|
|
6
6
|
import type { Logger } from "@remit/logger-lambda";
|
|
7
7
|
import {
|
|
8
|
-
EVERY_MAILBOX_STATE,
|
|
9
8
|
FolderGoneUpstreamError,
|
|
10
9
|
FolderRenameSettleError,
|
|
11
10
|
isMailboxAbsentUpstream,
|
|
@@ -37,31 +36,6 @@ const defaultDeps: MailboxManagementDeps = {
|
|
|
37
36
|
createConnectionScope: createConnectionScopeWithCredentials,
|
|
38
37
|
};
|
|
39
38
|
|
|
40
|
-
/**
|
|
41
|
-
* Write the outcome of a folder operation back, as the conditional write that
|
|
42
|
-
* is the only way to write a folder's state
|
|
43
|
-
* (docs/architecture/folder-rename-and-delete.md D3).
|
|
44
|
-
*
|
|
45
|
-
* The from-set is every state because that is what this write-back decides
|
|
46
|
-
* against today — it is the unconditional write it replaces, moved onto the
|
|
47
|
-
* door. #362 narrows the delete's to the state its intent recorded. A null
|
|
48
|
-
* means the row is gone, which is the NotFoundError the whole-chain guards
|
|
49
|
-
* below classify as the user having deleted the folder mid-sync.
|
|
50
|
-
*/
|
|
51
|
-
const recordOutcome = async (
|
|
52
|
-
mailboxService: Pick<IMailboxRepository, "transition">,
|
|
53
|
-
accountId: string,
|
|
54
|
-
mailboxId: string,
|
|
55
|
-
to: (typeof MailboxSyncStatus)[keyof typeof MailboxSyncStatus],
|
|
56
|
-
): Promise<void> => {
|
|
57
|
-
const written = await mailboxService.transition(accountId, mailboxId, {
|
|
58
|
-
from: EVERY_MAILBOX_STATE,
|
|
59
|
-
to,
|
|
60
|
-
set: {},
|
|
61
|
-
});
|
|
62
|
-
if (!written) throw new NotFoundError(`Mailbox not found: ${mailboxId}`);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
39
|
/**
|
|
66
40
|
* The outcome of a create, written against the state a create in flight is:
|
|
67
41
|
* `pending` with no recorded rename target (D3, D10). Without the absence
|
|
@@ -438,37 +412,32 @@ const handleDelete = async (
|
|
|
438
412
|
}
|
|
439
413
|
})
|
|
440
414
|
.catch(async (error) => {
|
|
441
|
-
//
|
|
415
|
+
// The folder is already gone from the server, so the delete has
|
|
416
|
+
// happened. Its mail still goes with it (D8), which is the whole
|
|
417
|
+
// point of the settle.
|
|
442
418
|
if (isMailboxAbsentUpstream(error)) {
|
|
443
419
|
log.info(
|
|
444
|
-
{ accountId, mailboxId, path },
|
|
445
|
-
"Mailbox not found on IMAP,
|
|
446
|
-
);
|
|
447
|
-
await mailboxService.delete(accountId, mailboxId);
|
|
448
|
-
} else if (
|
|
449
|
-
error instanceof Error &&
|
|
450
|
-
error.message.includes("Cannot delete INBOX")
|
|
451
|
-
) {
|
|
452
|
-
// Restore the mailbox
|
|
453
|
-
await recordOutcome(
|
|
454
|
-
mailboxService,
|
|
455
|
-
accountId,
|
|
456
|
-
mailboxId,
|
|
457
|
-
MailboxSyncStatus.synced,
|
|
458
|
-
);
|
|
459
|
-
log.error(
|
|
460
|
-
{ accountId, mailboxId, path },
|
|
461
|
-
"Cannot delete INBOX, restoring mailbox",
|
|
420
|
+
{ accountId, mailboxId, path, intent: "delete" },
|
|
421
|
+
"Mailbox not found on IMAP, removing the local folder and its mail",
|
|
462
422
|
);
|
|
463
|
-
|
|
423
|
+
await managementService.settleDelete(accountId, mailboxId);
|
|
464
424
|
} else {
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
425
|
+
// T9, INBOX included. The API refuses a delete of INBOX, so the
|
|
426
|
+
// worker's backstop is unreachable from it; if something else
|
|
427
|
+
// reaches it, the honest outcome is `failed`, not `synced` —
|
|
428
|
+
// the folder was never deleted and nothing was undone.
|
|
429
|
+
await managementService.failDelete(accountId, mailboxId);
|
|
430
|
+
if (
|
|
431
|
+
error instanceof Error &&
|
|
432
|
+
error.message.includes("Cannot delete INBOX")
|
|
433
|
+
) {
|
|
434
|
+
log.error(
|
|
435
|
+
{ accountId, mailboxId, path, intent: "delete" },
|
|
436
|
+
"Cannot delete INBOX",
|
|
437
|
+
);
|
|
438
|
+
// Don't rethrow — no retry can make this succeed.
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
472
441
|
throw error;
|
|
473
442
|
}
|
|
474
443
|
})
|
|
@@ -225,7 +225,7 @@ const syncMailboxesForAccount = async (
|
|
|
225
225
|
account.accountConfigId,
|
|
226
226
|
accountId,
|
|
227
227
|
);
|
|
228
|
-
if (bound.bound > 0 || bound.stillPending > 0) {
|
|
228
|
+
if (bound.bound > 0 || bound.dropped > 0 || bound.stillPending > 0) {
|
|
229
229
|
log.info({ accountId, ...bound }, "Bound imported folder references");
|
|
230
230
|
}
|
|
231
231
|
|