@remit/imap-worker 0.0.90 → 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 (172) 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} +25 -42
  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} +57 -78
  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 -218
  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 -145
  110. package/src/emit.ts +0 -115
  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 -126
  116. package/src/handlers/delete-account-objects.ts +0 -96
  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 -569
  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 -888
  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 -755
  134. package/src/handlers/placement-move-push.test.ts +0 -137
  135. package/src/handlers/placement-move-push.ts +0 -478
  136. package/src/handlers/placement-push.sqlite.test.ts +0 -299
  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 -425
  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 -366
  144. package/src/handlers/sync-messages.sqlite.test.ts +0 -195
  145. package/src/handlers/sync-messages.ts +0 -537
  146. package/src/index.test.ts +0 -22
  147. package/src/index.ts +0 -82
  148. package/src/mail-sync-selection.sqlite.test.ts +0 -227
  149. package/src/mailbox-row.ts +0 -20
  150. package/src/pending-sync.sqlite.test.ts +0 -165
  151. package/src/poller.ts +0 -55
  152. package/src/processor.ts +0 -62
  153. package/src/queues.ts +0 -40
  154. package/src/scheduler/attachment-sweep.sqlite.test.ts +0 -74
  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 -799
  166. package/src/test-helpers/test-world.ts +0 -425
  167. package/src/test-helpers/worker-queues.ts +0 -11
  168. package/src/thread-join.sqlite.test.ts +0 -106
  169. package/src/with-oauth-lifecycle-deps.sqlite.test.ts +0 -269
  170. package/src/with-oauth-lifecycle-deps.ts +0 -71
  171. package/src/with-oauth-lifecycle.ts +0 -158
  172. package/tsconfig.json +0 -8
package/src/events.ts DELETED
@@ -1,252 +0,0 @@
1
- import type {
2
- MUTATION_EVENT_SCHEMA_VERSION,
3
- PATHLESS_MUTATION_EVENT_SCHEMA_VERSION,
4
- } from "@remit/data-ports/mutation-events";
5
-
6
- export interface BaseEvent {
7
- accountId: string;
8
- eventId: string; // Idempotency key
9
- timestamp: number; // Unix timestamp
10
- }
11
-
12
- export interface SyncMailboxesEvent extends BaseEvent {
13
- type: "SYNC_MAILBOXES";
14
- /**
15
- * Set by POST /sync — the only trigger a person asks for by name. It makes
16
- * the fan-out sync every mailbox, skipping the freshness gate that keeps
17
- * incidental triggers (config load, OAuth connect, the scheduled tick) from
18
- * re-enumerating folders that were just enumerated.
19
- */
20
- explicitRequest?: boolean;
21
- }
22
-
23
- export interface SyncMessagesEvent extends BaseEvent {
24
- type: "SYNC_MESSAGES";
25
- mailboxId: string;
26
- fullSync?: boolean; // If true, ignore lastSyncUid
27
- }
28
-
29
- /**
30
- * A message to body-sync, carrying the UID resolved at envelope-sync time so
31
- * the consumer can issue one ranged FETCH without a per-message DDB lookup.
32
- */
33
- export interface SyncMessageBodyTarget {
34
- messageId: string;
35
- uid: number;
36
- }
37
-
38
- export interface SyncMessageBodyEvent extends BaseEvent {
39
- type: "SYNC_MESSAGE_BODY";
40
- mailboxId: string;
41
- /**
42
- * Message ids to sync. Always present for backward compatibility; the
43
- * consumer falls back to this list (looking up each UID) when `messages`
44
- * is absent.
45
- */
46
- messageIds: string[];
47
- /**
48
- * Preferred shape: messageId+uid pairs. When present, the consumer skips the
49
- * per-message UID lookup and fetches the whole batch in one ranged FETCH.
50
- * Optional so older in-flight events (ids only) still process.
51
- */
52
- messages?: SyncMessageBodyTarget[];
53
- /**
54
- * Set only by the read-miss re-arm cue (`BodySyncQueueService.requestBodySync`,
55
- * called when a `/content` read finds `bodyStorageKey` set but the storage
56
- * object missing). Tells the consumer to bypass the "already stored" skip
57
- * guard and re-fetch + rewrite the body even though the DB row already
58
- * carries a `bodyStorageKey` — otherwise stale metadata (key set, object
59
- * gone) makes the read-miss retry loop forever, since the bulk skip guard
60
- * would keep treating the message as already synced. Optional so older
61
- * in-flight events (no force) keep the original skip behavior.
62
- */
63
- force?: boolean;
64
- }
65
-
66
- export interface MailboxCreateEvent extends BaseEvent {
67
- type: "MAILBOX_CREATE";
68
- mailboxId: string;
69
- path: string;
70
- subscribe?: boolean;
71
- }
72
-
73
- export interface MailboxRenameEvent extends BaseEvent {
74
- type: "MAILBOX_RENAME";
75
- mailboxId: string;
76
- oldPath: string;
77
- newPath: string;
78
- }
79
-
80
- export interface MailboxDeleteEvent extends BaseEvent {
81
- type: "MAILBOX_DELETE";
82
- mailboxId: string;
83
- path: string;
84
- }
85
-
86
- export type MailboxManagementEvent =
87
- | MailboxCreateEvent
88
- | MailboxRenameEvent
89
- | MailboxDeleteEvent;
90
-
91
- /**
92
- * Event for deleting a message (move to trash or permanent delete).
93
- */
94
- interface MessageDeleteEventFields extends BaseEvent {
95
- type: "MESSAGE_DELETE";
96
- messageId: string;
97
- mailboxId: string;
98
- uid: number;
99
- operation: "move_to_trash" | "permanent_delete";
100
- destinationMailboxId?: string;
101
- }
102
-
103
- export interface MessageDeleteEventV2 extends MessageDeleteEventFields {
104
- schemaVersion: typeof MUTATION_EVENT_SCHEMA_VERSION;
105
- mailboxPath: string;
106
- destinationMailboxPath?: string;
107
- }
108
-
109
- export interface MessageDeleteEventV3 extends MessageDeleteEventFields {
110
- schemaVersion: typeof PATHLESS_MUTATION_EVENT_SCHEMA_VERSION;
111
- }
112
-
113
- export type MessageDeleteEvent = MessageDeleteEventV2 | MessageDeleteEventV3;
114
-
115
- /**
116
- * Event for moving a message to another mailbox.
117
- */
118
- interface MessageMoveEventFields extends BaseEvent {
119
- type: "MESSAGE_MOVE";
120
- messageId: string;
121
- sourceMailboxId: string;
122
- destinationMailboxId: string;
123
- uid: number;
124
- }
125
-
126
- export interface MessageMoveEventV1 extends MessageMoveEventFields {
127
- schemaVersion?: undefined;
128
- sourceMailboxPath: string;
129
- destinationMailboxPath: string;
130
- }
131
-
132
- export interface MessageMoveEventV3 extends MessageMoveEventFields {
133
- schemaVersion: typeof PATHLESS_MUTATION_EVENT_SCHEMA_VERSION;
134
- }
135
-
136
- export type MessageMoveEvent = MessageMoveEventV1 | MessageMoveEventV3;
137
-
138
- /**
139
- * Event for emptying the Trash mailbox.
140
- */
141
- interface EmptyTrashEventFields extends BaseEvent {
142
- type: "EMPTY_TRASH";
143
- trashMailboxId: string;
144
- /**
145
- * The Trash folder's UIDVALIDITY at the moment the user consented. The
146
- * handler compares it against what the SELECT serves: a path reused by a
147
- * different folder (rename plus recreate) answers with a different value,
148
- * and this is the only comparison the mutating connection cannot race.
149
- */
150
- trashUidValidity: number;
151
- }
152
-
153
- export interface EmptyTrashEventV2 extends EmptyTrashEventFields {
154
- schemaVersion: typeof MUTATION_EVENT_SCHEMA_VERSION;
155
- trashMailboxPath: string;
156
- }
157
-
158
- export interface EmptyTrashEventV3 extends EmptyTrashEventFields {
159
- schemaVersion: typeof PATHLESS_MUTATION_EVENT_SCHEMA_VERSION;
160
- }
161
-
162
- export type EmptyTrashEvent = EmptyTrashEventV2 | EmptyTrashEventV3;
163
-
164
- /**
165
- * Event for copying a message to another mailbox.
166
- */
167
- interface MessageCopyEventFields extends BaseEvent {
168
- type: "MESSAGE_COPY";
169
- sourceMessageId: string;
170
- newMessageId: string;
171
- sourceMailboxId: string;
172
- destinationMailboxId: string;
173
- uid: number;
174
- }
175
-
176
- export interface MessageCopyEventV1 extends MessageCopyEventFields {
177
- schemaVersion?: undefined;
178
- sourceMailboxPath: string;
179
- destinationMailboxPath: string;
180
- }
181
-
182
- export interface MessageCopyEventV3 extends MessageCopyEventFields {
183
- schemaVersion: typeof PATHLESS_MUTATION_EVENT_SCHEMA_VERSION;
184
- }
185
-
186
- export type MessageCopyEvent = MessageCopyEventV1 | MessageCopyEventV3;
187
-
188
- /**
189
- * Event for appending a sent message to the Sent mailbox via IMAP APPEND.
190
- */
191
- export interface AppendSentMessageEvent extends BaseEvent {
192
- type: "APPEND_SENT_MESSAGE";
193
- outboxMessageId: string;
194
- }
195
-
196
- /**
197
- * Drains one pending placement-move marker (issue #1271, epic #1281) to
198
- * IMAP. Deliberately carries no UID and no destination — both are resolved
199
- * fresh from the Message row and the `MessagePlacementMove` marker at push
200
- * time (epic invariant 1), so this event stays valid across any amount of
201
- * queue delay or an unrelated UIDVALIDITY rebuild (#1272). If the marker is
202
- * gone by the time this is processed (confirmed, superseded, or an external
203
- * delete already reconciled it), the handler is a no-op.
204
- */
205
- export interface PlacementMovePushEvent extends BaseEvent {
206
- type: "PLACEMENT_MOVE_PUSH";
207
- accountConfigId: string;
208
- messageId: string;
209
- }
210
-
211
- /**
212
- * Wake-up hint for one pending flag-push marker (issue #1273, epic #1281).
213
- * Carries only our message id + the flag field — never a UID and never the
214
- * desired add/remove transition (both live on the `MessageFlagPush` marker,
215
- * resolved fresh at push time, epic invariant 1). Produced two ways: by
216
- * `FlagPushService` (remit-mailbox-service) right after a user's flag flip
217
- * commits locally, and by the periodic per-mailbox sync tick
218
- * (`sync-messages.ts`) re-arming any marker it finds still stuck `pending`
219
- * (a prior enqueue that never landed). If the marker is gone or has moved
220
- * past `pending` by the time this is processed, the handler is a no-op.
221
- */
222
- export interface FlagPushEvent extends BaseEvent {
223
- type: "FLAG_PUSH";
224
- accountConfigId: string;
225
- messageId: string;
226
- flagName: string;
227
- }
228
-
229
- export interface DeleteAccountObjectsEvent {
230
- type: "DELETE_ACCOUNT_OBJECTS";
231
- accountConfigId: string;
232
- continuationToken?: string;
233
- }
234
-
235
- /** Union of all event types the worker can process (including non-IMAP ones). */
236
- export type WorkerEvent = ImapEvent | DeleteAccountObjectsEvent;
237
-
238
- export type MessageManagementEvent =
239
- | MessageDeleteEvent
240
- | MessageMoveEvent
241
- | EmptyTrashEvent
242
- | MessageCopyEvent;
243
-
244
- export type ImapEvent =
245
- | SyncMailboxesEvent
246
- | SyncMessagesEvent
247
- | SyncMessageBodyEvent
248
- | MailboxManagementEvent
249
- | MessageManagementEvent
250
- | AppendSentMessageEvent
251
- | PlacementMovePushEvent
252
- | FlagPushEvent;
@@ -1,42 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import { RefreshTokenError } from "@remit/mail-oauth-service";
4
- import { MailConnectionError } from "@remit/mailbox-service";
5
- import { imapFailureKind } from "./failure-kind.js";
6
-
7
- describe("imapFailureKind", () => {
8
- it("counts an IMAP authentication rejection as auth", () => {
9
- assert.equal(
10
- imapFailureKind(
11
- new MailConnectionError("auth", "IMAP authentication failed"),
12
- ),
13
- "auth",
14
- );
15
- });
16
-
17
- it("counts a token refresh that cannot mint credentials as auth", () => {
18
- assert.equal(
19
- imapFailureKind(
20
- new RefreshTokenError({
21
- kind: "reauth-required",
22
- code: "invalid_grant",
23
- }),
24
- ),
25
- "auth",
26
- );
27
- });
28
-
29
- it("keeps a network failure apart from an auth failure", () => {
30
- assert.equal(
31
- imapFailureKind(
32
- new MailConnectionError("network", "IMAP connection failed: ETIMEDOUT"),
33
- ),
34
- "network",
35
- );
36
- });
37
-
38
- it("classifies anything else as other", () => {
39
- assert.equal(imapFailureKind(new Error("bad cursor")), "other");
40
- assert.equal(imapFailureKind(undefined), "other");
41
- });
42
- });
@@ -1,176 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { after, before, describe, it } from "node:test";
3
- import type { OutboxMessageItem } from "@remit/data-ports";
4
- import { MailboxSpecialUse, OutboxMessageStatus } from "@remit/domain-enums";
5
- import type { AppendSentMessageEvent } from "../events.js";
6
- import { openStore, type World } from "../test-helpers/test-world.js";
7
- import { handleAppendSentMessage } from "./append-sent-message.js";
8
-
9
- let store: Awaited<ReturnType<typeof openStore>>;
10
-
11
- before(async () => {
12
- store = await openStore();
13
- });
14
-
15
- after(() => store.close());
16
-
17
- interface Arranged {
18
- w: World;
19
- outbox: OutboxMessageItem;
20
- event: AppendSentMessageEvent;
21
- }
22
-
23
- /**
24
- * A message composed and sent through the service the API calls, then marked
25
- * sent the way the SMTP worker marks it once the relay accepted it.
26
- */
27
- const arrange = async (
28
- options: { sentFolder?: boolean; attachment?: Buffer } = {},
29
- ): Promise<Arranged> => {
30
- const w = await store.world();
31
- await w.folder("INBOX");
32
- if (options.sentFolder !== false) {
33
- await w.folder("Sent", { specialUse: MailboxSpecialUse.Sent });
34
- }
35
- const { outbox: draft } = await w.draft(options.attachment);
36
- const outbox = await w.client.outboxQueue.send(
37
- w.account.accountConfigId,
38
- draft.outboxMessageId,
39
- );
40
- await w.client.outboxMessage.update(
41
- w.account.accountConfigId,
42
- outbox.outboxMessageId,
43
- { status: OutboxMessageStatus.sent, sentAt: Date.now() },
44
- );
45
- return {
46
- w,
47
- outbox,
48
- event: {
49
- type: "APPEND_SENT_MESSAGE",
50
- eventId: `evt-${outbox.outboxMessageId}`,
51
- timestamp: Date.now(),
52
- accountId: w.account.accountId,
53
- outboxMessageId: outbox.outboxMessageId,
54
- } as AppendSentMessageEvent,
55
- };
56
- };
57
-
58
- const run = (a: Arranged, receiveCount = 1) =>
59
- handleAppendSentMessage(a.event, a.w.log, receiveCount, a.w.scope);
60
-
61
- const outboxRow = (a: Arranged) =>
62
- a.w.client.outboxMessage
63
- .get(a.w.account.accountConfigId, a.outbox.outboxMessageId)
64
- .catch(() => undefined);
65
-
66
- describe("filing a sent message", () => {
67
- it("stores a seen copy under its own Message-ID in Sent and drops the outbox row", async () => {
68
- const a = await arrange();
69
-
70
- await run(a);
71
-
72
- const [copy] = a.w.imap.messages("Sent");
73
- assert.equal(copy?.messageIdHeader, `<${a.outbox.messageIdValue}>`);
74
- assert.ok(copy?.flags.includes("\\Seen"));
75
- assert.match(
76
- copy?.raw.toString("utf8") ?? "",
77
- /Subject: Quarterly numbers/,
78
- );
79
- assert.equal(await outboxRow(a), undefined);
80
- });
81
-
82
- // #679: the Sent copy carries the files the wire copy carried, and the row
83
- // was the only reference to them, so they go with it.
84
- it("files the copy with its attachment and removes the stored file", async () => {
85
- const a = await arrange({ attachment: Buffer.from("q1,q2\n1,2\n") });
86
-
87
- await run(a);
88
-
89
- const raw = a.w.imap.messages("Sent")[0]?.raw.toString("utf8") ?? "";
90
- assert.match(raw, /^Content-Type: multipart\/mixed;/m);
91
- assert.match(raw, /filename=numbers\.csv/);
92
- assert.deepEqual(
93
- await a.w.client.storage.listOutboxAttachments(
94
- a.w.account.accountConfigId,
95
- a.w.account.accountId,
96
- a.outbox.outboxMessageId,
97
- ),
98
- [],
99
- );
100
- });
101
-
102
- it("settles the row as unfiled when the account has no Sent folder", async () => {
103
- const a = await arrange({ sentFolder: false });
104
-
105
- await run(a);
106
-
107
- assert.equal(a.w.imap.connections, 0);
108
- const row = await outboxRow(a);
109
- assert.equal(row?.status, OutboxMessageStatus.unfiled);
110
- assert.match(
111
- String(row?.lastError),
112
- /no folder appointed to the Sent role/,
113
- );
114
- });
115
-
116
- it("files nothing while the message has not been sent yet", async () => {
117
- const a = await arrange();
118
- await a.w.client.outboxMessage.update(
119
- a.w.account.accountConfigId,
120
- a.outbox.outboxMessageId,
121
- { status: OutboxMessageStatus.queued },
122
- );
123
-
124
- await run(a);
125
-
126
- assert.deepEqual(a.w.imap.commands("APPEND"), []);
127
- assert.equal((await outboxRow(a))?.status, OutboxMessageStatus.queued);
128
- });
129
-
130
- it("keeps the row and redelivers a refused APPEND within the budget", async () => {
131
- const a = await arrange();
132
- a.w.imap.failNext.APPEND = new Error("Connection reset");
133
-
134
- await assert.rejects(run(a), /Connection reset/);
135
-
136
- assert.equal((await outboxRow(a))?.status, OutboxMessageStatus.sent);
137
- });
138
-
139
- it("settles the row as unfiled on the last attempt instead of dead-lettering it", async () => {
140
- const a = await arrange();
141
- a.w.imap.failNext.APPEND = new Error("Mailbox is full");
142
-
143
- await run(a, 3);
144
-
145
- const row = await outboxRow(a);
146
- assert.equal(row?.status, OutboxMessageStatus.unfiled);
147
- assert.match(String(row?.lastError), /Mailbox is full/);
148
- });
149
-
150
- // #858: a redelivery after the APPEND landed must not file a second copy.
151
- it("files nothing a second time when the copy was already recorded", async () => {
152
- const a = await arrange();
153
- await a.w.client.outboxMessage.update(
154
- a.w.account.accountConfigId,
155
- a.outbox.outboxMessageId,
156
- { appendedUid: 41 },
157
- );
158
-
159
- await run(a);
160
-
161
- assert.deepEqual(a.w.imap.commands("APPEND"), []);
162
- assert.equal(await outboxRow(a), undefined);
163
- });
164
-
165
- it("acks an event whose outbox row is already gone", async () => {
166
- const a = await arrange();
167
- await a.w.client.outboxMessage.delete(
168
- a.w.account.accountConfigId,
169
- a.outbox.outboxMessageId,
170
- );
171
-
172
- await run(a);
173
-
174
- assert.equal(a.w.imap.connections, 0);
175
- });
176
- });