@remit/backend 0.0.105 → 0.0.106

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/backend",
3
- "version": "0.0.105",
3
+ "version": "0.0.106",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -1,5 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
+ import { MessagePlacementUnsettledError } from "@remit/data-ports/errors";
3
4
  import {
4
5
  MoveNotSettledError,
5
6
  NoJunkMailboxError,
@@ -41,6 +42,32 @@ describe("settleSpamReportBulk", () => {
41
42
  assert.notEqual(reason, GENERIC_FAILURE_REASON);
42
43
  });
43
44
 
45
+ // A refusal the move gate raises used to collapse to the generic retry copy,
46
+ // which is a dead end for the reason no retry clears (#665).
47
+ it("words a placement refusal from its reason, never from the server's sentence", async () => {
48
+ const outcome = await settleSpamReportBulk(
49
+ ["msg-1", "msg-2"],
50
+ async (messageId) => {
51
+ throw new MessagePlacementUnsettledError(
52
+ `Message ${messageId} was not acted on: its folder and uid do not name the same message`,
53
+ "acc-1",
54
+ messageId,
55
+ messageId === "msg-1" ? "in_flight" : "unverified",
56
+ );
57
+ },
58
+ );
59
+
60
+ const [inFlight, unverified] = outcome.failures?.map((f) => f.reason) ?? [];
61
+ assert.match(inFlight ?? "", /try again in a moment/i);
62
+ assert.match(unverified ?? "", /sync the folder/i);
63
+ assert.doesNotMatch(unverified ?? "", /try again in a moment/i);
64
+ for (const reason of [inFlight, unverified]) {
65
+ assert.notEqual(reason, GENERIC_FAILURE_REASON);
66
+ assert.doesNotMatch(reason ?? "", /folder and uid/);
67
+ assert.doesNotMatch(reason ?? "", /acc-1/);
68
+ }
69
+ });
70
+
44
71
  it("omits failures entirely when every message succeeds", async () => {
45
72
  const outcome = await settleSpamReportBulk(
46
73
  ["msg-1", "msg-2"],
@@ -7,6 +7,7 @@ import type { MailboxItem } from "@remit/data-ports";
7
7
  import {
8
8
  BadRequestError,
9
9
  ForbiddenError,
10
+ MessagePlacementUnsettledError,
10
11
  NotFoundError,
11
12
  UnrecoverableBodyError,
12
13
  } from "@remit/data-ports/errors";
@@ -356,13 +357,35 @@ export const GENERIC_FAILURE_REASON =
356
357
  * each names what happened and what the user can do about it, which the
357
358
  * generic text cannot.
358
359
  */
359
- const DESIGNED_FAILURES = [MoveNotSettledError, NoJunkMailboxError];
360
+ const DESIGNED_FAILURES = [
361
+ MoveNotSettledError,
362
+ NoJunkMailboxError,
363
+ MessagePlacementUnsettledError,
364
+ ];
360
365
 
361
366
  const isDesignedFailure = (reason: unknown): reason is Error =>
362
367
  DESIGNED_FAILURES.some((designed) => reason instanceof designed);
363
368
 
364
- const failureReason = (reason: unknown): string =>
365
- isDesignedFailure(reason) ? reason.message : GENERIC_FAILURE_REASON;
369
+ /**
370
+ * The placement refusal is the one designed outcome whose own message is not
371
+ * the text to ship: it names a uuid and no remedy, and the two reasons do not
372
+ * share a remedy — an in-flight move settles on its own, an abandoned one only
373
+ * clears once the folder is resynced. Worded here off `details.reason`, the way
374
+ * the delete path words the same refusal for the client.
375
+ */
376
+ const placementRefusalReason = (
377
+ error: MessagePlacementUnsettledError,
378
+ ): string =>
379
+ error.publicApiError?.details?.reason === "unverified"
380
+ ? "An earlier move of this message never finished, so where it sits is unknown. Sync the folder, then report it again."
381
+ : "This message is still being moved on the mail server. Try again in a moment.";
382
+
383
+ const failureReason = (reason: unknown): string => {
384
+ if (!isDesignedFailure(reason)) return GENERIC_FAILURE_REASON;
385
+ if (reason instanceof MessagePlacementUnsettledError)
386
+ return placementRefusalReason(reason);
387
+ return reason.message;
388
+ };
366
389
 
367
390
  /**
368
391
  * Drive one report-spam/not-spam operation per message, concurrently. Each