@remit/imap-worker 0.0.80 → 0.0.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/imap-worker",
3
- "version": "0.0.80",
3
+ "version": "0.0.81",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -1,6 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { beforeEach, describe, it } from "node:test";
3
3
  import { noopLogger } from "@remit/logger-lambda/noop-logger";
4
+ import { OutboxAttachmentUnavailableError } from "@remit/mailbox-service/outbox-attachment-content";
4
5
  import type { AppendSentMessageEvent } from "../events.js";
5
6
  import {
6
7
  APPEND_SENT_MAX_ATTEMPTS,
@@ -31,6 +32,11 @@ interface Harness {
31
32
  * without ever running the work. */
32
33
  ackWithoutWork: boolean;
33
34
  disconnectCount: number;
35
+ attachments: Array<{
36
+ filename: string;
37
+ contentType: string;
38
+ content: Buffer;
39
+ }>;
34
40
  }
35
41
 
36
42
  let h: Harness;
@@ -65,6 +71,7 @@ const fresh = (): Harness => ({
65
71
  },
66
72
  ackWithoutWork: false,
67
73
  disconnectCount: 0,
74
+ attachments: [],
68
75
  });
69
76
 
70
77
  const deps = (): AppendSentMessageDeps =>
@@ -83,6 +90,10 @@ const deps = (): AppendSentMessageDeps =>
83
90
  },
84
91
  outboxAttachment: {
85
92
  discardAll: record("outboxAttachment.discardAll"),
93
+ contentsFor: async (...args: unknown[]) => {
94
+ h.calls.push({ method: "outboxAttachment.contentsFor", args });
95
+ return h.attachments;
96
+ },
86
97
  },
87
98
  mailboxSpecialUse: {
88
99
  findSentMailbox: async () => h.sentMailbox,
@@ -190,6 +201,85 @@ describe("handleAppendSentMessage", () => {
190
201
  ]);
191
202
  });
192
203
 
204
+ it("files the Sent copy with the same files the wire copy carried", async () => {
205
+ h.attachments = [
206
+ {
207
+ filename: "numbers.csv",
208
+ contentType: "text/csv",
209
+ content: Buffer.from("q1,q2\n1,2\n"),
210
+ },
211
+ ];
212
+
213
+ await handleAppendSentMessage(event, noopLogger, 1, deps());
214
+
215
+ assert.deepEqual(called("outboxAttachment.contentsFor")[0]?.args, [
216
+ "cfg-1",
217
+ "acc-1",
218
+ "out-1",
219
+ ]);
220
+ const raw = String(called("connection.append")[0]?.args[1] as Buffer);
221
+ assert.match(raw, /^Content-Type: multipart\/mixed;/m);
222
+ assert.match(
223
+ raw,
224
+ /^Content-Disposition: attachment; filename=numbers\.csv$/m,
225
+ );
226
+ assert.match(raw, /^Content-Type: text\/csv; name=numbers\.csv$/m);
227
+ });
228
+
229
+ const unreadable = new OutboxAttachmentUnavailableError(
230
+ "numbers.csv",
231
+ "nothing is stored at key/numbers",
232
+ );
233
+
234
+ const depsWithUnreadableFile = (): AppendSentMessageDeps => {
235
+ const base = deps();
236
+ return {
237
+ ...base,
238
+ getClient: async (): Promise<BackendClient> => {
239
+ const client = await base.getClient();
240
+ client.outboxAttachment.contentsFor = async () => {
241
+ throw unreadable;
242
+ };
243
+ return client;
244
+ },
245
+ };
246
+ };
247
+
248
+ it("retries a copy whose files could not be read, below the budget", async () => {
249
+ const failing = depsWithUnreadableFile();
250
+
251
+ await assert.rejects(
252
+ () => handleAppendSentMessage(event, noopLogger, 1, failing),
253
+ unreadable,
254
+ );
255
+ assert.equal(called("connection.append").length, 0);
256
+ assert.deepEqual(patches(), [], "the row is left for the retry");
257
+ });
258
+
259
+ it("settles a delivered message whose files cannot be read as sent, not filed, at the budget", async () => {
260
+ const failing = depsWithUnreadableFile();
261
+
262
+ await handleAppendSentMessage(
263
+ event,
264
+ noopLogger,
265
+ APPEND_SENT_MAX_ATTEMPTS,
266
+ failing,
267
+ );
268
+ assert.deepEqual(patches(), [
269
+ {
270
+ status: "unfiled",
271
+ lastError:
272
+ 'Sent, but not filed: the copy for INBOX/Sent could not be built because "numbers.csv" could not be read: nothing is stored at key/numbers.',
273
+ },
274
+ ]);
275
+ assert.equal(called("connection.append").length, 0);
276
+ assert.equal(
277
+ called("outboxMessage.delete").length,
278
+ 0,
279
+ "the row stays in the Outbox with its reason",
280
+ );
281
+ });
282
+
193
283
  it("builds the message from the outbox row's own headers", async () => {
194
284
  await handleAppendSentMessage(event, noopLogger, 1, deps());
195
285
 
@@ -7,6 +7,7 @@ import {
7
7
  import { isNotFoundError } from "@remit/data-ports/errors";
8
8
  import { OutboxMessageStatus } from "@remit/domain-enums";
9
9
  import type { Logger } from "@remit/logger-lambda";
10
+ import { OutboxAttachmentUnavailableError } from "@remit/mailbox-service/outbox-attachment-content";
10
11
  import {
11
12
  buildMailMessage,
12
13
  renderRawMessage,
@@ -28,7 +29,9 @@ const ROW_SURVIVED_ITS_DELETE =
28
29
  "Sent message was filed but its outbox row survived its delete and stays hidden until the boot-time repair";
29
30
 
30
31
  const unfiledAppendRefused = (fullPath: string, error: unknown): string =>
31
- `Sent, but not filed: the mail server refused to store a copy in ${fullPath} (${error instanceof Error ? error.message : String(error)}).`;
32
+ error instanceof OutboxAttachmentUnavailableError
33
+ ? `Sent, but not filed: the copy for ${fullPath} could not be built because ${error.message}.`
34
+ : `Sent, but not filed: the mail server refused to store a copy in ${fullPath} (${error instanceof Error ? error.message : String(error)}).`;
32
35
 
33
36
  export const getAppendSentMaxAttempts = (
34
37
  processEnv: NodeJS.ProcessEnv = process.env,
@@ -188,12 +191,22 @@ export const handleAppendSentMessage = async (
188
191
  account,
189
192
  log,
190
193
  async (credentials) => {
194
+ // Inside the retried unit: a file that cannot be read is retried below
195
+ // the budget and settles `unfiled` at it, rather than dead-lettering a
196
+ // delivered message out of every view.
197
+ const attachments = await outboxAttachmentService.contentsFor(
198
+ account.accountConfigId,
199
+ accountId,
200
+ outboxMessageId,
201
+ );
191
202
  const scope = createConnectionScopeWithCredentials(account, credentials);
192
203
 
193
204
  await scope
194
205
  .getConnection()
195
206
  .then(async (connection) => {
196
- const rawMessage = await renderRawMessage(buildMailMessage(outbox));
207
+ const rawMessage = await renderRawMessage(
208
+ buildMailMessage(outbox, attachments),
209
+ );
197
210
 
198
211
  const result = await connection.append(
199
212
  sentMailbox.fullPath,