@remit/imap-worker 0.0.90 → 0.0.92

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 (176) 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 +22 -0
  76. package/dist/scheduler/calendar-subscriptions.js +35 -0
  77. package/dist/scheduler/config.d.ts +29 -0
  78. package/{src/scheduler/config.ts → dist/scheduler/config.js} +9 -28
  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/dist/scheduler/handler.js +50 -0
  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 +66 -0
  86. package/dist/scheduler/run-tick.js +164 -0
  87. package/dist/scheduler/runner.d.ts +2 -0
  88. package/dist/scheduler/runner.js +102 -0
  89. package/dist/scheduler/stranded-placement-redrive.d.ts +16 -0
  90. package/dist/scheduler/stranded-placement-redrive.js +172 -0
  91. package/dist/scheduler/tick.d.ts +5 -0
  92. package/dist/scheduler/tick.js +5 -0
  93. package/dist/sqs-queues.d.ts +4 -0
  94. package/dist/sqs-queues.js +22 -0
  95. package/dist/with-oauth-lifecycle-deps.d.ts +5 -0
  96. package/dist/with-oauth-lifecycle-deps.js +17 -0
  97. package/dist/with-oauth-lifecycle.d.ts +56 -0
  98. package/dist/with-oauth-lifecycle.js +92 -0
  99. package/{src/worker-version.ts → dist/worker-version.d.ts} +1 -2
  100. package/dist/worker-version.js +12 -0
  101. package/package.json +79 -69
  102. package/src/account-check.test.ts +0 -149
  103. package/src/account-check.ts +0 -101
  104. package/src/body-sync-gate.test.ts +0 -185
  105. package/src/body-sync-gate.ts +0 -86
  106. package/src/cli.ts +0 -218
  107. package/src/connection-scope.test.ts +0 -266
  108. package/src/connection-scope.ts +0 -314
  109. package/src/credential-less-sync-fence.test.ts +0 -152
  110. package/src/e2e-processor-shim.ts +0 -232
  111. package/src/emit.test.ts +0 -145
  112. package/src/emit.ts +0 -115
  113. package/src/events.ts +0 -252
  114. package/src/failure-kind.test.ts +0 -42
  115. package/src/handlers/append-sent-message.sqlite.test.ts +0 -176
  116. package/src/handlers/append-sent-message.ts +0 -264
  117. package/src/handlers/delete-account-objects.test.ts +0 -126
  118. package/src/handlers/delete-account-objects.ts +0 -96
  119. package/src/handlers/empty-trash.sqlite.test.ts +0 -209
  120. package/src/handlers/empty-trash.ts +0 -352
  121. package/src/handlers/flag-push.ts +0 -285
  122. package/src/handlers/imported-folder.test.ts +0 -63
  123. package/src/handlers/imported-folder.ts +0 -38
  124. package/src/handlers/mailbox-management.sqlite.test.ts +0 -265
  125. package/src/handlers/mailbox-management.ts +0 -486
  126. package/src/handlers/mailbox-sync-order.test.ts +0 -93
  127. package/src/handlers/mailbox-sync-order.ts +0 -65
  128. package/src/handlers/message-copy.sqlite.test.ts +0 -298
  129. package/src/handlers/message-copy.ts +0 -569
  130. package/src/handlers/message-delete-terminal.ts +0 -147
  131. package/src/handlers/message-delete.sqlite.test.ts +0 -291
  132. package/src/handlers/message-delete.ts +0 -888
  133. package/src/handlers/message-move.sqlite.test.ts +0 -271
  134. package/src/handlers/message-move.test.ts +0 -216
  135. package/src/handlers/message-move.ts +0 -755
  136. package/src/handlers/placement-move-push.test.ts +0 -137
  137. package/src/handlers/placement-move-push.ts +0 -478
  138. package/src/handlers/placement-push.sqlite.test.ts +0 -299
  139. package/src/handlers/probe-message-id.sqlite.test.ts +0 -192
  140. package/src/handlers/probe-message-id.ts +0 -15
  141. package/src/handlers/sync-mailboxes.test.ts +0 -183
  142. package/src/handlers/sync-mailboxes.ts +0 -425
  143. package/src/handlers/sync-message-body.sqlite.test.ts +0 -314
  144. package/src/handlers/sync-message-body.test.ts +0 -150
  145. package/src/handlers/sync-message-body.ts +0 -366
  146. package/src/handlers/sync-messages.sqlite.test.ts +0 -195
  147. package/src/handlers/sync-messages.ts +0 -537
  148. package/src/index.test.ts +0 -22
  149. package/src/index.ts +0 -82
  150. package/src/mail-sync-selection.sqlite.test.ts +0 -227
  151. package/src/mailbox-row.ts +0 -20
  152. package/src/pending-sync.sqlite.test.ts +0 -165
  153. package/src/poller.ts +0 -55
  154. package/src/processor.ts +0 -62
  155. package/src/queues.ts +0 -40
  156. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -74
  157. package/src/scheduler/attachment-sweep.ts +0 -46
  158. package/src/scheduler/calendar-subscriptions.sqlite.test.ts +0 -147
  159. package/src/scheduler/calendar-subscriptions.ts +0 -103
  160. package/src/scheduler/config.test.ts +0 -40
  161. package/src/scheduler/decide-due.test.ts +0 -44
  162. package/src/scheduler/half-deleted-accounts.sqlite.test.ts +0 -196
  163. package/src/scheduler/handler.ts +0 -69
  164. package/src/scheduler/loop.test.ts +0 -143
  165. package/src/scheduler/run-tick.sqlite.test.ts +0 -191
  166. package/src/scheduler/run-tick.ts +0 -281
  167. package/src/scheduler/runner.ts +0 -125
  168. package/src/scheduler/stranded-placement-redrive.ts +0 -265
  169. package/src/stranded-placement.sqlite.test.ts +0 -799
  170. package/src/test-helpers/test-world.ts +0 -425
  171. package/src/test-helpers/worker-queues.ts +0 -11
  172. package/src/thread-join.sqlite.test.ts +0 -106
  173. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  174. package/src/with-oauth-lifecycle-deps.ts +0 -71
  175. package/src/with-oauth-lifecycle.ts +0 -158
  176. 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, a.w.queues, 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
- });