@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
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
- });