@remit/imap-worker 0.0.89 → 0.0.91

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.
Files changed (171) hide show
  1. package/dist/account-check.d.ts +27 -0
  2. package/dist/account-check.js +70 -0
  3. package/dist/body-sync-gate.d.ts +12 -0
  4. package/dist/body-sync-gate.js +53 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +193 -0
  7. package/dist/config-env.d.ts +5 -0
  8. package/dist/config-env.js +37 -0
  9. package/dist/connection-pool.d.ts +22 -0
  10. package/dist/connection-pool.js +97 -0
  11. package/dist/connection-scope.d.ts +8 -0
  12. package/dist/connection-scope.js +10 -0
  13. package/dist/context.d.ts +46 -0
  14. package/dist/context.js +15 -0
  15. package/dist/e2e-processor-shim.d.ts +2 -0
  16. package/dist/e2e-processor-shim.js +181 -0
  17. package/dist/emit.d.ts +15 -0
  18. package/dist/emit.js +76 -0
  19. package/dist/events.d.ts +203 -0
  20. package/dist/events.js +1 -0
  21. package/dist/failure-kind.d.ts +10 -0
  22. package/{src/failure-kind.ts → dist/failure-kind.js} +6 -6
  23. package/dist/handlers/append-sent-message.d.ts +3 -0
  24. package/dist/handlers/append-sent-message.js +152 -0
  25. package/dist/handlers/delete-account-objects.d.ts +3 -0
  26. package/dist/handlers/delete-account-objects.js +35 -0
  27. package/dist/handlers/empty-trash.d.ts +14 -0
  28. package/dist/handlers/empty-trash.js +217 -0
  29. package/dist/handlers/flag-push.d.ts +38 -0
  30. package/dist/handlers/flag-push.js +195 -0
  31. package/dist/handlers/imported-folder.d.ts +4 -0
  32. package/dist/handlers/imported-folder.js +23 -0
  33. package/dist/handlers/mailbox-management.d.ts +6 -0
  34. package/dist/handlers/mailbox-management.js +294 -0
  35. package/dist/handlers/mailbox-sync-order.d.ts +21 -0
  36. package/dist/handlers/mailbox-sync-order.js +50 -0
  37. package/dist/handlers/message-copy.d.ts +40 -0
  38. package/dist/handlers/message-copy.js +371 -0
  39. package/dist/handlers/message-delete-terminal.d.ts +60 -0
  40. package/dist/handlers/message-delete-terminal.js +77 -0
  41. package/dist/handlers/message-delete.d.ts +57 -0
  42. package/dist/handlers/message-delete.js +520 -0
  43. package/{src/handlers/message-move-terminal.ts → dist/handlers/message-move-terminal.d.ts} +18 -105
  44. package/dist/handlers/message-move-terminal.js +94 -0
  45. package/dist/handlers/message-move.d.ts +146 -0
  46. package/dist/handlers/message-move.js +490 -0
  47. package/dist/handlers/placement-move-push.d.ts +62 -0
  48. package/dist/handlers/placement-move-push.js +307 -0
  49. package/dist/handlers/probe-message-id.d.ts +2 -0
  50. package/dist/handlers/probe-message-id.js +6 -0
  51. package/dist/handlers/sync-mailboxes.d.ts +45 -0
  52. package/dist/handlers/sync-mailboxes.js +233 -0
  53. package/dist/handlers/sync-message-body.d.ts +23 -0
  54. package/dist/handlers/sync-message-body.js +182 -0
  55. package/dist/handlers/sync-messages.d.ts +29 -0
  56. package/dist/handlers/sync-messages.js +285 -0
  57. package/dist/index.d.ts +9 -0
  58. package/dist/index.js +67 -0
  59. package/dist/lambda-context.d.ts +8 -0
  60. package/dist/lambda-context.js +33 -0
  61. package/dist/mailbox-row.d.ts +9 -0
  62. package/dist/mailbox-row.js +6 -0
  63. package/dist/poller.d.ts +1 -0
  64. package/dist/poller.js +51 -0
  65. package/dist/process.d.ts +6 -0
  66. package/dist/process.js +3 -0
  67. package/dist/processor.d.ts +3 -0
  68. package/dist/processor.js +44 -0
  69. package/dist/queues.d.ts +10 -0
  70. package/dist/queues.js +1 -0
  71. package/dist/s3-account-objects.d.ts +3 -0
  72. package/dist/s3-account-objects.js +26 -0
  73. package/dist/scheduler/attachment-sweep.d.ts +11 -0
  74. package/dist/scheduler/attachment-sweep.js +23 -0
  75. package/dist/scheduler/calendar-subscriptions.d.ts +26 -0
  76. package/dist/scheduler/calendar-subscriptions.js +35 -0
  77. package/dist/scheduler/config.d.ts +27 -0
  78. package/{src/scheduler/config.ts → dist/scheduler/config.js} +7 -26
  79. package/{src/scheduler/decide-due.ts → dist/scheduler/decide-due.d.ts} +3 -12
  80. package/dist/scheduler/decide-due.js +17 -0
  81. package/dist/scheduler/handler.d.ts +21 -0
  82. package/{src/scheduler/handler.ts → dist/scheduler/handler.js} +24 -36
  83. package/{src/scheduler/loop.ts → dist/scheduler/loop.d.ts} +7 -23
  84. package/dist/scheduler/loop.js +8 -0
  85. package/dist/scheduler/run-tick.d.ts +62 -0
  86. package/dist/scheduler/run-tick.js +164 -0
  87. package/dist/scheduler/runner.d.ts +2 -0
  88. package/{src/scheduler/runner.ts → dist/scheduler/runner.js} +58 -84
  89. package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
  90. package/dist/scheduler/stranded-placement-redrive.js +172 -0
  91. package/dist/sqs-queues.d.ts +4 -0
  92. package/dist/sqs-queues.js +22 -0
  93. package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
  94. package/dist/with-oauth-lifecycle-deps.js +17 -0
  95. package/dist/with-oauth-lifecycle.d.ts +56 -0
  96. package/dist/with-oauth-lifecycle.js +92 -0
  97. package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
  98. package/dist/worker-version.js +12 -0
  99. package/package.json +75 -69
  100. package/src/account-check.test.ts +0 -149
  101. package/src/account-check.ts +0 -101
  102. package/src/body-sync-gate.test.ts +0 -185
  103. package/src/body-sync-gate.ts +0 -86
  104. package/src/cli.ts +0 -224
  105. package/src/connection-scope.test.ts +0 -266
  106. package/src/connection-scope.ts +0 -314
  107. package/src/credential-less-sync-fence.test.ts +0 -152
  108. package/src/e2e-processor-shim.ts +0 -232
  109. package/src/emit.test.ts +0 -127
  110. package/src/emit.ts +0 -117
  111. package/src/events.ts +0 -252
  112. package/src/failure-kind.test.ts +0 -42
  113. package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
  114. package/src/handlers/append-sent-message.ts +0 -264
  115. package/src/handlers/delete-account-objects.test.ts +0 -128
  116. package/src/handlers/delete-account-objects.ts +0 -94
  117. package/src/handlers/empty-trash.sqlite.test.ts +0 -209
  118. package/src/handlers/empty-trash.ts +0 -352
  119. package/src/handlers/flag-push.ts +0 -285
  120. package/src/handlers/imported-folder.test.ts +0 -63
  121. package/src/handlers/imported-folder.ts +0 -38
  122. package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
  123. package/src/handlers/mailbox-management.ts +0 -486
  124. package/src/handlers/mailbox-sync-order.test.ts +0 -93
  125. package/src/handlers/mailbox-sync-order.ts +0 -65
  126. package/src/handlers/message-copy.sqlite.test.ts +0 -298
  127. package/src/handlers/message-copy.ts +0 -566
  128. package/src/handlers/message-delete-terminal.ts +0 -147
  129. package/src/handlers/message-delete.sqlite.test.ts +0 -291
  130. package/src/handlers/message-delete.ts +0 -885
  131. package/src/handlers/message-move.sqlite.test.ts +0 -271
  132. package/src/handlers/message-move.test.ts +0 -216
  133. package/src/handlers/message-move.ts +0 -752
  134. package/src/handlers/placement-move-push.test.ts +0 -137
  135. package/src/handlers/placement-move-push.ts +0 -475
  136. package/src/handlers/placement-push.sqlite.test.ts +0 -298
  137. package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
  138. package/src/handlers/probe-message-id.ts +0 -15
  139. package/src/handlers/sync-mailboxes.test.ts +0 -183
  140. package/src/handlers/sync-mailboxes.ts +0 -421
  141. package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
  142. package/src/handlers/sync-message-body.test.ts +0 -150
  143. package/src/handlers/sync-message-body.ts +0 -365
  144. package/src/handlers/sync-messages.sqlite.test.ts +0 -183
  145. package/src/handlers/sync-messages.ts +0 -525
  146. package/src/index.test.ts +0 -22
  147. package/src/index.ts +0 -86
  148. package/src/mail-sync-selection.sqlite.test.ts +0 -234
  149. package/src/mailbox-row.ts +0 -20
  150. package/src/pending-sync.sqlite.test.ts +0 -166
  151. package/src/poller.ts +0 -55
  152. package/src/processor.ts +0 -60
  153. package/src/queues.ts +0 -49
  154. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -75
  155. package/src/scheduler/attachment-sweep.ts +0 -46
  156. package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
  157. package/src/scheduler/calendar-subscriptions.ts +0 -103
  158. package/src/scheduler/config.test.ts +0 -40
  159. package/src/scheduler/decide-due.test.ts +0 -44
  160. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
  161. package/src/scheduler/loop.test.ts +0 -143
  162. package/src/scheduler/run-tick.sqlite.test.ts +0 -191
  163. package/src/scheduler/run-tick.ts +0 -281
  164. package/src/scheduler/stranded-placement-redrive.ts +0 -265
  165. package/src/stranded-placement.sqlite.test.ts +0 -794
  166. package/src/test-helpers/test-world.ts +0 -427
  167. package/src/thread-join.sqlite.test.ts +0 -106
  168. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  169. package/src/with-oauth-lifecycle-deps.ts +0 -71
  170. package/src/with-oauth-lifecycle.ts +0 -158
  171. package/tsconfig.json +0 -8
@@ -1,147 +0,0 @@
1
- import type {
2
- IMessageRepository,
3
- IThreadMessageRepository,
4
- MessageItem,
5
- } from "@remit/data-ports";
6
- import { MessageMutation, MessageSyncStatus } from "@remit/domain-enums";
7
- import {
8
- type IImapConnection,
9
- isMessageGoneFromOpenMailbox,
10
- restoreSourcePlacement,
11
- } from "@remit/mailbox-service";
12
-
13
- export interface MessageDeleteTerminalLogger {
14
- info(obj: Record<string, unknown>, msg: string): void;
15
- error(obj: Record<string, unknown>, msg: string): void;
16
- }
17
-
18
- export interface ResolveExhaustedMessageDeleteDeps {
19
- messageService: Pick<
20
- IMessageRepository,
21
- "deleteIfPlacement" | "transitionPlacement"
22
- >;
23
- threadMessageService: Pick<
24
- IThreadMessageRepository,
25
- "findAllByMessageId" | "deleteMany" | "update"
26
- >;
27
- log: MessageDeleteTerminalLogger;
28
- }
29
-
30
- export interface ResolveExhaustedMessageDeleteInput {
31
- /** Where the row stood when the caller read it; the delete holds only while it still does. */
32
- placement: Pick<MessageItem, "status" | "mailboxId" | "uid">;
33
- accountId: string;
34
- accountConfigId: string;
35
- messageId: string;
36
- sourceMailboxId: string;
37
- uid: number;
38
- sourceMailboxPath: string;
39
- getConnection: () => Promise<IImapConnection>;
40
- }
41
-
42
- export type MessageDeleteTerminalOutcome = "reconciled" | "broken";
43
-
44
- export interface ResolveExhaustedMessageDeleteResult {
45
- outcome: MessageDeleteTerminalOutcome;
46
- }
47
-
48
- /**
49
- * Resolve a MESSAGE_DELETE failure that has exhausted its redelivery budget
50
- * into exactly one of two terminal outcomes, mirroring
51
- * `resolveExhaustedMessageMoveFailure` for the same failure taxonomy (issue
52
- * #655) — no third, softer outcome.
53
- *
54
- * 1. RECONCILED (expected) — the message no longer exists at the delete's
55
- * source on IMAP, confirmed by {@link isMessageGoneFromOpenMailbox} rather
56
- * than by a FETCH coming back empty. The move to Trash landed server-side,
57
- * or a foreign client moved or expunged the message; from here those are
58
- * indistinguishable and have the same answer. The stale rows are deleted via
59
- * `deleteIfPlacement` and the caller resyncs the affected folders,
60
- * so whichever folder actually holds the message re-projects it with the
61
- * server's own UID. Metric only, no alarm — routine.
62
- * 2. BROKEN — the message is still at the source, so the delete never took
63
- * effect, but it keeps failing: broken code or a broken account, not a
64
- * transient blip. The row is put back where the server has just said the
65
- * message is: `mailboxId` and `uid` return to the delete's source pair, and
66
- * the thread rows lose the deletion mark the optimistic transition
67
- * wrote (issue #1098).
68
- *
69
- * Settling `status` to `active` while leaving `mailboxId` on Trash is what
70
- * this replaces. `bindsForeignUid` refuses a mismatched pair only while
71
- * `status` is `moving`, so clearing it read as consistent, and the UI then
72
- * offered "Delete permanently" on a Trash mailbox with the source folder's
73
- * uid — an expunge naming a different message. Restoring the pair is not a
74
- * revert on ambiguity (what PR #652 was pulled for): the presence probe
75
- * above has just answered, so this writes a placement the server confirmed.
76
- *
77
- * A server that cannot be reached at exhaustion time never reaches either
78
- * verdict: the probe throws and the record dead-letters with the row untouched.
79
- * Absence is only ever concluded from an answer the server gave.
80
- */
81
- export const resolveExhaustedMessageDeleteFailure = async (
82
- deps: ResolveExhaustedMessageDeleteDeps,
83
- input: ResolveExhaustedMessageDeleteInput,
84
- ): Promise<ResolveExhaustedMessageDeleteResult> => {
85
- const {
86
- accountId,
87
- accountConfigId,
88
- messageId,
89
- sourceMailboxId,
90
- uid,
91
- sourceMailboxPath,
92
- getConnection,
93
- } = input;
94
-
95
- const connection = await getConnection();
96
- await connection.openBox(sourceMailboxPath, true);
97
-
98
- if (await isMessageGoneFromOpenMailbox(connection, uid)) {
99
- const reconciled = await deps.messageService.deleteIfPlacement(
100
- messageId,
101
- input.placement,
102
- );
103
- deps.log.info(
104
- {
105
- metric: "message_delete_stale_row_reconciled",
106
- accountId,
107
- accountConfigId,
108
- messageId,
109
- uid,
110
- sourceMailboxPath,
111
- reconciled,
112
- },
113
- "Message no longer at its delete source after retry exhaustion (the delete landed server-side, or an external delete or move); stale row reconciled",
114
- );
115
- return { outcome: "reconciled" };
116
- }
117
-
118
- deps.log.error(
119
- {
120
- alert: "message_delete_failed",
121
- accountId,
122
- accountConfigId,
123
- messageId,
124
- uid,
125
- sourceMailboxId,
126
- sourceMailboxPath,
127
- },
128
- "Delete could not be pushed to IMAP after retry exhaustion; the message is still at its source — row moved back to the source folder",
129
- );
130
-
131
- await restoreSourcePlacement(deps, {
132
- accountConfigId,
133
- messageId,
134
- sourceMailboxId,
135
- uid,
136
- // The probe above just confirmed the pair, so the row is a faithful
137
- // projection of the source again — and the delete the user asked for did
138
- // not happen, with nothing left to try it. `abandoned` is the value that
139
- // says so without claiming a retry is coming (imap-mutations R3); the
140
- // alert stays for the operator, and the mutation names itself so the
141
- // reading pane offers the right way out (issue #1229).
142
- syncStatus: MessageSyncStatus.abandoned,
143
- abandonedMutation: MessageMutation.delete,
144
- });
145
-
146
- return { outcome: "broken" };
147
- };
@@ -1,291 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { after, before, describe, it } from "node:test";
3
- import type { RemitClient } from "@remit/backend/client";
4
- import {
5
- MailboxCursorState,
6
- MailboxSpecialUse,
7
- MessageMutation,
8
- MessageStatus,
9
- MessageSyncStatus,
10
- } from "@remit/domain-enums";
11
- import type { MessageDeleteEvent } from "../events.js";
12
- import { mail, openStore, type World } from "../test-helpers/test-world.js";
13
- import { handleMessageDelete } from "./message-delete.js";
14
-
15
- let store: Awaited<ReturnType<typeof openStore>>;
16
-
17
- before(async () => {
18
- store = await openStore();
19
- });
20
-
21
- after(() => store.close());
22
-
23
- interface Arranged {
24
- w: World;
25
- messageId: string;
26
- uid: number;
27
- messageIdHeader: string;
28
- inboxId: string;
29
- trashId: string;
30
- event: MessageDeleteEvent;
31
- }
32
-
33
- /**
34
- * A message in INBOX, deleted through the service the API calls, so the row
35
- * the handler finds and the event it gets are the ones a person's delete
36
- * leaves behind.
37
- */
38
- const arrange = async (
39
- options: { permanent?: boolean; olderCopyInTrash?: boolean } = {},
40
- ): Promise<Arranged> => {
41
- const w = await store.world();
42
- const inbox = await w.folder("INBOX");
43
- const trash = await w.folder("Trash", {
44
- specialUse: MailboxSpecialUse.Trash,
45
- });
46
- const seeded = await w.message(inbox);
47
- if (options.olderCopyInTrash)
48
- w.imap.deliver("Trash", mail(seeded.messageIdHeader));
49
-
50
- await w.client.messageMove.deleteMessage(
51
- w.account.accountConfigId,
52
- seeded.messageId,
53
- w.account.accountId,
54
- options.permanent ? { permanent: true } : { toTrash: true },
55
- );
56
- const event = w
57
- .queued()
58
- .find(
59
- (body) => body.type === "MESSAGE_DELETE",
60
- ) as unknown as MessageDeleteEvent;
61
- assert.ok(event, "the delete enqueued a MESSAGE_DELETE");
62
-
63
- return {
64
- w,
65
- messageId: seeded.messageId,
66
- uid: seeded.uid,
67
- messageIdHeader: seeded.messageIdHeader,
68
- inboxId: inbox.mailboxId,
69
- trashId: trash.mailboxId,
70
- event,
71
- };
72
- };
73
-
74
- const run = (a: Arranged, receiveCount = 1, event = a.event) =>
75
- handleMessageDelete(event, a.w.log, receiveCount, a.w.scope);
76
-
77
- const resyncs = (w: World): unknown[] =>
78
- w
79
- .queued()
80
- .filter((body) => body.type === "SYNC_MESSAGES")
81
- .map((body) => body.mailboxId);
82
-
83
- const client = (): RemitClient => store.client;
84
-
85
- describe("a move to Trash", () => {
86
- it("lands in Trash and settles the row on the uid the server gave it", async () => {
87
- const a = await arrange();
88
-
89
- await run(a);
90
-
91
- assert.deepEqual(a.w.imap.uids("INBOX"), []);
92
- const [trashUid] = a.w.imap.uids("Trash", a.messageIdHeader);
93
- const row = await a.w.row(a.messageId);
94
- assert.equal(row?.mailboxId, a.trashId);
95
- assert.equal(row?.uid, trashUid);
96
- assert.equal(row?.status, MessageStatus.active);
97
- assert.deepEqual(await a.w.threadRows(a.messageId), [
98
- { mailboxId: a.trashId, uid: trashUid, isDeleted: true },
99
- ]);
100
- });
101
-
102
- // Issue #1122. Without UIDPLUS the destination is asked by Message-ID, and
103
- // Trash already holds an older copy: binding the row to it lets Empty Trash
104
- // expunge the wrong one and leaves the fresh copy unreachable.
105
- it("settles on the copy this move delivered when the server sends no COPYUID", async () => {
106
- const a = await arrange({ olderCopyInTrash: true });
107
- a.w.imap.uidplus = false;
108
-
109
- await run(a);
110
-
111
- const copies = a.w.imap.uids("Trash", a.messageIdHeader);
112
- assert.equal(copies.length, 2);
113
- assert.equal((await a.w.row(a.messageId))?.uid, Math.max(...copies));
114
- });
115
-
116
- // One Message-ID can have several server copies. A source that still holds
117
- // the uid proves the MOVE never ran, so the older copy in Trash is not this
118
- // message and must never settle the row.
119
- it("never settles on an older copy in Trash while the source still holds the message", async () => {
120
- const a = await arrange({ olderCopyInTrash: true });
121
- a.w.imap.uidplus = false;
122
- a.w.imap.ignoreNext.add("MOVE");
123
-
124
- await assert.rejects(run(a), /unconfirmed/);
125
-
126
- const row = await a.w.row(a.messageId);
127
- assert.equal(row?.uid, a.uid);
128
- assert.equal(row?.status, MessageStatus.moving);
129
- assert.equal(row?.syncStatus, MessageSyncStatus.failed);
130
- });
131
-
132
- it("issues nothing when a redelivery finds the delete already settled", async () => {
133
- const a = await arrange();
134
- await run(a);
135
- const settled = await a.w.row(a.messageId);
136
- const commands = a.w.imap.log.length;
137
-
138
- await run(a);
139
-
140
- assert.equal(
141
- a.w.imap.log.length,
142
- commands,
143
- "no IMAP command on redelivery",
144
- );
145
- assert.deepEqual(await a.w.row(a.messageId), settled);
146
- });
147
-
148
- it("marks the row failed and redelivers when the connection drops mid-move", async () => {
149
- const a = await arrange();
150
- a.w.imap.failNext.MOVE = new Error("Connection reset");
151
-
152
- await assert.rejects(run(a), /Connection reset/);
153
-
154
- const row = await a.w.row(a.messageId);
155
- assert.equal(row?.status, MessageStatus.moving);
156
- assert.equal(row?.syncStatus, MessageSyncStatus.failed);
157
- assert.deepEqual(a.w.imap.uids("INBOX"), [a.uid]);
158
- });
159
-
160
- // Issue #980. At the ceiling the server is asked where the message is, and
161
- // a message that never left INBOX puts the row back there.
162
- it("puts the row back on its source at the ceiling when the message never left", async () => {
163
- const a = await arrange();
164
- a.w.imap.failNext.MOVE = new Error("Connection reset");
165
-
166
- await run(a, 3);
167
-
168
- const row = await a.w.row(a.messageId);
169
- assert.equal(row?.mailboxId, a.inboxId);
170
- assert.equal(row?.uid, a.uid);
171
- assert.notEqual(row?.status, MessageStatus.moving);
172
- assert.deepEqual(resyncs(a.w), [a.inboxId, a.trashId]);
173
- });
174
-
175
- it("hands the row back without creating a Trash the server does not have", async () => {
176
- const a = await arrange();
177
- a.w.imap.removeFolder("Trash");
178
-
179
- await run(a);
180
-
181
- assert.deepEqual(a.w.imap.commands("CREATE"), []);
182
- const row = await a.w.row(a.messageId);
183
- assert.equal(row?.mailboxId, a.inboxId);
184
- assert.equal(row?.uid, a.uid);
185
- assert.equal(row?.syncStatus, MessageSyncStatus.abandoned);
186
- assert.equal(row?.abandonedMutation, MessageMutation.delete);
187
- });
188
-
189
- it("hands the row back without connecting while the source cursor is rebuilding", async () => {
190
- const a = await arrange();
191
- await client().mailbox.update(a.w.account.accountId, a.inboxId, {
192
- cursorState: MailboxCursorState.rebuilding,
193
- });
194
-
195
- await run(a);
196
-
197
- assert.equal(a.w.imap.connections, 0);
198
- const row = await a.w.row(a.messageId);
199
- assert.equal(row?.mailboxId, a.inboxId);
200
- assert.equal(row?.status, MessageStatus.active);
201
- assert.equal(
202
- a.w.logs.filter(
203
- (line) => line.fields.metric === "message_delete_dropped_on_pause",
204
- ).length,
205
- 1,
206
- );
207
- assert.deepEqual(resyncs(a.w), [a.inboxId, a.trashId]);
208
- });
209
-
210
- // The folder was deleted and recreated on the server: uid 1 now names
211
- // whatever the server put there. Nothing may be moved by it.
212
- it("moves nothing when the source's UIDVALIDITY changed under it", async () => {
213
- const a = await arrange();
214
- a.w.imap.setUidValidity("INBOX", 2);
215
-
216
- await run(a);
217
-
218
- assert.deepEqual(a.w.imap.commands("MOVE"), []);
219
- assert.equal((await a.w.row(a.messageId))?.mailboxId, a.inboxId);
220
- const inbox = await client().mailbox.get(a.w.account.accountId, a.inboxId);
221
- assert.notEqual(inbox.cursorState, MailboxCursorState.normal);
222
- });
223
-
224
- it("moves from the folder's current path when it was renamed after enqueue", async () => {
225
- const a = await arrange();
226
- a.w.imap.renameFolder("INBOX", "Old/Inbox");
227
- await client().mailbox.update(a.w.account.accountId, a.inboxId, {
228
- fullPath: "Old/Inbox",
229
- });
230
-
231
- await run(a);
232
-
233
- assert.deepEqual(a.w.imap.uids("Old/Inbox"), []);
234
- assert.equal(a.w.imap.uids("Trash", a.messageIdHeader).length, 1);
235
- });
236
-
237
- it("acks without connecting when the source mailbox was deleted", async () => {
238
- const a = await arrange();
239
- await client().mailbox.delete(a.w.account.accountId, a.inboxId);
240
-
241
- await run(a);
242
-
243
- assert.equal(a.w.imap.connections, 0);
244
- });
245
-
246
- it("refuses an event minted under an unknown contract and hands the row back", async () => {
247
- const a = await arrange();
248
-
249
- await run(a, 1, {
250
- ...a.event,
251
- schemaVersion: 99,
252
- } as unknown as MessageDeleteEvent);
253
-
254
- assert.equal(a.w.imap.connections, 0);
255
- const row = await a.w.row(a.messageId);
256
- assert.equal(row?.mailboxId, a.inboxId);
257
- assert.equal(row?.syncStatus, MessageSyncStatus.abandoned);
258
- });
259
- });
260
-
261
- describe("a permanent delete", () => {
262
- it("expunges the message and removes every local row", async () => {
263
- const a = await arrange({ permanent: true });
264
-
265
- await run(a);
266
-
267
- assert.deepEqual(a.w.imap.uids("INBOX"), []);
268
- assert.equal(await a.w.row(a.messageId), undefined);
269
- assert.deepEqual(await a.w.threadRows(a.messageId), []);
270
- });
271
-
272
- it("cleans up locally when another client already expunged it", async () => {
273
- const a = await arrange({ permanent: true });
274
- a.w.imap.empty("INBOX");
275
- a.w.imap.failNext.EXPUNGE = new Error("Message not found (NONEXISTENT)");
276
-
277
- await run(a);
278
-
279
- assert.equal(await a.w.row(a.messageId), undefined);
280
- });
281
-
282
- it("keeps the row when the server still holds the message after a failed expunge", async () => {
283
- const a = await arrange({ permanent: true });
284
- a.w.imap.failNext.EXPUNGE = new Error("Message not found (NONEXISTENT)");
285
-
286
- await assert.rejects(run(a), /NONEXISTENT/);
287
-
288
- assert.equal((await a.w.row(a.messageId))?.status, MessageStatus.deleting);
289
- assert.deepEqual(a.w.imap.uids("INBOX"), [a.uid]);
290
- });
291
- });