@remit/mailbox-service 0.0.40 → 0.0.41

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/mailbox-service",
3
- "version": "0.0.40",
3
+ "version": "0.0.41",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -69,7 +69,7 @@ const MAILBOXES = {
69
69
  };
70
70
 
71
71
  const buildHarness = (
72
- message: { messageId: string; mailboxId: string },
72
+ message: { messageId: string; mailboxId: string; movedByRemit?: boolean },
73
73
  flags: AddressItem["flags"],
74
74
  ): Harness => {
75
75
  const moves: MoveCall[] = [];
@@ -79,11 +79,7 @@ const buildHarness = (
79
79
  }> = [];
80
80
 
81
81
  const messageService = {
82
- get: async () => ({
83
- messageId: message.messageId,
84
- mailboxId: message.mailboxId,
85
- uid: 1,
86
- }),
82
+ get: async () => ({ ...message, uid: 1 }),
87
83
  update: async (messageId: string, input: UpdateMessageInput) => {
88
84
  messageUpdates.push({ messageId, input });
89
85
  },
@@ -300,3 +296,56 @@ describe("Address.flags.autoArchive drives placement (issue #300)", () => {
300
296
  ]);
301
297
  });
302
298
  });
299
+
300
+ /**
301
+ * Issue #398: not every `leave` verdict means "nothing confident to say".
302
+ * `already-moved-by-remit` is the guard against re-deciding a settled
303
+ * placement, and mail sitting in Junk gets a `leave` because the demote branch
304
+ * only fires outside Junk. Auto-archive is a filing preference relative to the
305
+ * Inbox and must yield to both, so these drive `BodySyncService` end-to-end and
306
+ * assert on the absence of a move.
307
+ */
308
+ describe("autoArchive respects a leave verdict that decided something (issue #398)", () => {
309
+ it("leaves a blocked autoArchive sender's message in Junk", async () => {
310
+ const harness = buildHarness(
311
+ { messageId: "m-1", mailboxId: MAILBOXES.junk.mailboxId },
312
+ {
313
+ blocked: { value: true, setAt: 1_000 },
314
+ autoArchive: { value: true, setAt: 1_000 },
315
+ },
316
+ );
317
+
318
+ await readBody(harness.service, "Junk");
319
+
320
+ assert.deepEqual(harness.moves, []);
321
+ });
322
+
323
+ it("leaves an autoArchive sender's provider-junked message in Junk", async () => {
324
+ const harness = buildHarness(
325
+ { messageId: "m-1", mailboxId: MAILBOXES.junk.mailboxId },
326
+ { autoArchive: { value: true, setAt: 1_000 } },
327
+ );
328
+
329
+ // Provider-spam + dmarc-pass from an untrusted sender is an unsure
330
+ // `leave`: too weak to rescue, and auto-archive is not a second chance
331
+ // at leaving Junk.
332
+ await readBody(harness.service, "Junk", SPAM_FLAGGED_DMARC_PASS_EML);
333
+
334
+ assert.deepEqual(harness.moves, []);
335
+ });
336
+
337
+ it("leaves a message Remit already placed alone on a re-entrant pass", async () => {
338
+ const harness = buildHarness(
339
+ {
340
+ messageId: "m-1",
341
+ mailboxId: MAILBOXES.inbox.mailboxId,
342
+ movedByRemit: true,
343
+ },
344
+ { autoArchive: { value: true, setAt: 1_000 } },
345
+ );
346
+
347
+ await readBody(harness.service);
348
+
349
+ assert.deepEqual(harness.moves, []);
350
+ });
351
+ });
package/src/body-sync.ts CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  import {
49
49
  classifyPlacement,
50
50
  type FolderPlacement,
51
+ type PlacementVerdict,
51
52
  resolveBlockedVsTrust,
52
53
  } from "./heuristics/classifyPlacement.js";
53
54
  import type { PlacementMoveService } from "./placement-move.js";
@@ -199,6 +200,26 @@ const hasDecidedCategory = (
199
200
  const hasDecidedPlacement = (placementDecidedAt: number | undefined): boolean =>
200
201
  placementDecidedAt !== undefined;
201
202
 
203
+ /**
204
+ * Issue #398: `flags.autoArchive` is a filing preference relative to the Inbox.
205
+ * A `leave` verdict reaches it for two unrelated reasons and only one of them
206
+ * means "nothing confident to say":
207
+ *
208
+ * - `already-moved-by-remit` is {@link classifyPlacement}'s guard against
209
+ * re-deciding a settled placement, and auto-archive is a re-decision. The
210
+ * re-entrant paths `hasDecidedPlacement` names reach it on messages Remit
211
+ * itself placed, or that a user moved back afterwards.
212
+ * - A message already in Junk gets an unsure `leave` whatever the sender's
213
+ * `blocked` flag says, since the demote branch only fires outside Junk.
214
+ * Moving mail out of Junk is a rescue, which this file reserves for a
215
+ * confident signal plus sender trust.
216
+ */
217
+ const autoArchiveApplies = (
218
+ verdict: PlacementVerdict,
219
+ placement: FolderPlacement,
220
+ ): boolean =>
221
+ placement !== "junk" && !verdict.reasons.includes("already-moved-by-remit");
222
+
202
223
  export const toParsedBody = (parsed: ParsedMail): ParsedBody => ({
203
224
  text: parsed.text ?? null,
204
225
  html: typeof parsed.html === "string" ? parsed.html : null,
@@ -1459,12 +1480,14 @@ export class BodySyncService {
1459
1480
  // junk/inbox verdict computed above. Still marked decided (issue #383):
1460
1481
  // "leave" is itself a verdict, not "not yet evaluated".
1461
1482
  if (verdict.action === "leave") {
1462
- const outcome = await this.resolveAutoArchive(
1463
- mailboxSpecialUseService,
1464
- message,
1465
- accountId,
1466
- signals.autoArchive,
1467
- );
1483
+ const outcome = autoArchiveApplies(verdict, placement)
1484
+ ? await this.resolveAutoArchive(
1485
+ mailboxSpecialUseService,
1486
+ message,
1487
+ accountId,
1488
+ signals.autoArchive,
1489
+ )
1490
+ : {};
1468
1491
  return { ...outcome, placementDecidedAt: Date.now() };
1469
1492
  }
1470
1493
 
@@ -1526,7 +1549,8 @@ export class BodySyncService {
1526
1549
  * straight to Archive, skipping Inbox. Only reached from
1527
1550
  * {@link computePlacement} when {@link classifyPlacement} had no confident
1528
1551
  * junk/inbox verdict of its own — `blocked`/DKIM/DMARC always take priority
1529
- * over this filing preference.
1552
+ * over this filing preference — and only for the `leave` verdicts
1553
+ * {@link autoArchiveApplies} admits.
1530
1554
  *
1531
1555
  * No {@link MessagePlacementVerdict} is recorded: `PlacementAction` (the
1532
1556
  * audit enum) has only `MoveToInbox`/`MoveToJunk` — issue #300 is scoped to