@remit/mailbox-service 0.0.74 → 0.0.75

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.74",
3
+ "version": "0.0.75",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -8,7 +8,7 @@ import type {
8
8
  IThreadMessageRepository,
9
9
  } from "@remit/data-ports";
10
10
  import { type MessageMoveConfig, MessageMoveService } from "./message-move.js";
11
- import { trashRole } from "./test-helpers/folder-roles.js";
11
+ import { NO_JUNK_ROLES, trashRole } from "./test-helpers/folder-roles.js";
12
12
 
13
13
  const ACCOUNT = "acc-1";
14
14
  const ACCOUNT_CONFIG = "cfg-1";
@@ -57,6 +57,7 @@ const buildWorld = (trashExists: boolean) => {
57
57
  const mailboxSpecialUseService = {
58
58
  findTrashMailbox: async () => trash,
59
59
  resolveTrashRole: async () => trashRole(trash),
60
+ resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
60
61
  } as unknown as IMailboxSpecialUseRepository;
61
62
 
62
63
  const addressService = {
@@ -15,6 +15,7 @@ import {
15
15
  StaleTrashAppointmentError,
16
16
  UnconfirmedTrashMailboxError,
17
17
  } from "./message-move.js";
18
+ import { NO_JUNK_ROLES } from "./test-helpers/folder-roles.js";
18
19
 
19
20
  const ACCOUNT = "acc-1";
20
21
  const ACCOUNT_CONFIG = "cfg-1";
@@ -80,6 +81,7 @@ const buildWorld = (
80
81
 
81
82
  const mailboxSpecialUseService = {
82
83
  resolveTrashRole: async () => trashResolution,
84
+ resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
83
85
  } as unknown as IMailboxSpecialUseRepository;
84
86
 
85
87
  const addressService = {
@@ -15,7 +15,11 @@ import { MailboxCursorState } from "@remit/domain-enums";
15
15
  import type { ManagedConnectionFactory } from "./connection-factory.js";
16
16
  import { type MessageMoveConfig, MessageMoveService } from "./message-move.js";
17
17
  import { MessageSyncService } from "./message-sync.js";
18
- import { noFolderRoles, trashRole } from "./test-helpers/folder-roles.js";
18
+ import {
19
+ NO_JUNK_ROLES,
20
+ noFolderRoles,
21
+ trashRole,
22
+ } from "./test-helpers/folder-roles.js";
19
23
  import type { IImapConnection, ImapMessage } from "./types.js";
20
24
 
21
25
  const stubAddressService = (): IAddressRepository =>
@@ -176,6 +180,7 @@ const buildWorld = () => {
176
180
  const mailboxSpecialUseService = {
177
181
  findTrashMailbox: async () => null,
178
182
  resolveTrashRole: async () => trashRole(null),
183
+ resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
179
184
  } as unknown as IMailboxSpecialUseRepository;
180
185
 
181
186
  const config: MessageMoveConfig = {
@@ -22,6 +22,7 @@ import {
22
22
  StaleTrashAppointmentError,
23
23
  UnconfirmedTrashMailboxError,
24
24
  } from "./message-move.js";
25
+ import { NO_JUNK_ROLES } from "./test-helpers/folder-roles.js";
25
26
 
26
27
  const ACCOUNT = "acc-1";
27
28
  const ACCOUNT_CONFIG = "cfg-1";
@@ -100,6 +101,7 @@ const buildWorld = (
100
101
  } as unknown as IAddressRepository,
101
102
  mailboxSpecialUseService: {
102
103
  resolveTrashRole: async () => trashResolution,
104
+ resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
103
105
  } as unknown as IMailboxSpecialUseRepository,
104
106
  threadMessageService,
105
107
  sqsQueueUrl: "http://localhost:9324/000000000000/remit-messages.fifo",
@@ -8,7 +8,7 @@ import type {
8
8
  IThreadMessageRepository,
9
9
  } from "@remit/data-ports";
10
10
  import { type MessageMoveConfig, MessageMoveService } from "./message-move.js";
11
- import { trashRole } from "./test-helpers/folder-roles.js";
11
+ import { NO_JUNK_ROLES, trashRole } from "./test-helpers/folder-roles.js";
12
12
 
13
13
  const stubAddressService = (): IAddressRepository =>
14
14
  ({
@@ -68,6 +68,7 @@ const buildWorld = () => {
68
68
  const mailboxSpecialUseService = {
69
69
  findTrashMailbox: async () => null,
70
70
  resolveTrashRole: async () => trashRole(null),
71
+ resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
71
72
  } as unknown as IMailboxSpecialUseRepository;
72
73
 
73
74
  const config: MessageMoveConfig = {
@@ -395,6 +395,12 @@ export class MessageMoveService {
395
395
 
396
396
  // Process move to trash
397
397
  if (trashMailbox && moveToTrashMessages.length > 0) {
398
+ // Every message in the batch shares the config, so its Junk and Trash
399
+ // folders resolve once rather than once per message.
400
+ const configJunkRoles =
401
+ await this.mailboxSpecialUseService.resolveJunkRolesForConfig(
402
+ accountConfigId,
403
+ );
398
404
  for (const { messageId, message, sourceMailbox } of moveToTrashMessages) {
399
405
  // Update local state optimistically
400
406
  await this.messageService.updateForMove(messageId, {
@@ -413,7 +419,10 @@ export class MessageMoveService {
413
419
  true,
414
420
  );
415
421
 
416
- await this.addressService.reconcileJunkOnlyForMessage(messageId);
422
+ await this.addressService.reconcileJunkOnlyForMessage(
423
+ messageId,
424
+ configJunkRoles,
425
+ );
417
426
 
418
427
  events.push({
419
428
  type: "MESSAGE_DELETE",
@@ -548,7 +557,12 @@ export class MessageMoveService {
548
557
  isMovingToTrash,
549
558
  );
550
559
 
551
- await this.addressService.reconcileJunkOnlyForMessage(messageId);
560
+ await this.addressService.reconcileJunkOnlyForMessage(
561
+ messageId,
562
+ await this.mailboxSpecialUseService.resolveJunkRolesForConfig(
563
+ accountConfigId,
564
+ ),
565
+ );
552
566
 
553
567
  // If moving FROM Trash, clear isDeleted
554
568
  if (isMovingFromTrash) {
@@ -19,7 +19,7 @@ import {
19
19
  addressSightingIn,
20
20
  MessageSyncService,
21
21
  } from "./message-sync.js";
22
- import { folderRoles } from "./test-helpers/folder-roles.js";
22
+ import { folderRoles, NO_JUNK_ROLES } from "./test-helpers/folder-roles.js";
23
23
  import type { ImapEnvelope, ImapMessage } from "./types.js";
24
24
 
25
25
  const stub = <T>(): T => ({}) as T;
@@ -141,6 +141,7 @@ const save = async (
141
141
  ).saveMessage(mailbox, "acct-1", "cfg-1", msg, {
142
142
  junkMailboxId: role === "Junk" ? mailbox.mailboxId : null,
143
143
  trashMailboxId: role === "Trash" ? mailbox.mailboxId : null,
144
+ configJunkRoles: NO_JUNK_ROLES,
144
145
  });
145
146
 
146
147
  return saved;
@@ -155,6 +156,7 @@ describe("what a mailbox says about the addresses on its messages", () => {
155
156
  const ROLES = (junk: string | null, trash: string | null) => ({
156
157
  junkMailboxId: junk,
157
158
  trashMailboxId: trash,
159
+ configJunkRoles: NO_JUNK_ROLES,
158
160
  });
159
161
 
160
162
  it("reads the folder the account appointed, not the folder’s name", () => {
@@ -13,6 +13,7 @@ import type {
13
13
  ThreadMessageItem,
14
14
  } from "@remit/data-ports";
15
15
  import { storedDisplayName } from "@remit/data-ports/display-name";
16
+ import type { JunkRoleMailboxes } from "@remit/data-ports/folder-role";
16
17
  import {
17
18
  deriveAddressId,
18
19
  deriveBodyPartId,
@@ -92,6 +93,14 @@ export type AddressSighting = "junk" | "discarded" | "correspondent";
92
93
  export interface AccountFolderRoles {
93
94
  readonly junkMailboxId: string | null;
94
95
  readonly trashMailboxId: string | null;
96
+ /**
97
+ * The same two roles across every account under the config, for the
98
+ * reconcile that follows a save into Junk. The address book is keyed by
99
+ * config, so a predicate over an address weighs sightings from all of them —
100
+ * resolving only this account withholds a sender on one and restores it on
101
+ * the next.
102
+ */
103
+ readonly configJunkRoles: JunkRoleMailboxes;
95
104
  }
96
105
 
97
106
  /**
@@ -462,7 +471,7 @@ export class MessageSyncService {
462
471
  // which catches its own error and reports a `failed` outcome instead of
463
472
  // rejecting. So one bad message can no longer abort the whole batch (the
464
473
  // poison pill that previously froze the mailbox, #817).
465
- const roles = await this.folderRolesFor(accountId);
474
+ const roles = await this.folderRolesFor(accountId, accountConfigId);
466
475
  const outcomes = await pMap(
467
476
  applicable,
468
477
  (msg) =>
@@ -697,7 +706,7 @@ export class MessageSyncService {
697
706
  const newMessages =
698
707
  newUids.length > 0 ? await this.fetchMessageBatch(newUids) : [];
699
708
  const applicable = newMessages.filter((msg) => msg.envelope !== undefined);
700
- const roles = await this.folderRolesFor(accountId);
709
+ const roles = await this.folderRolesFor(accountId, accountConfigId);
701
710
  const outcomes = await pMap(
702
711
  applicable,
703
712
  (msg) =>
@@ -925,7 +934,7 @@ export class MessageSyncService {
925
934
  );
926
935
  }
927
936
 
928
- const roles = await this.folderRolesFor(accountId);
937
+ const roles = await this.folderRolesFor(accountId, accountConfigId);
929
938
  const outcomes = await pMap(
930
939
  applicable,
931
940
  (msg) =>
@@ -1280,14 +1289,19 @@ export class MessageSyncService {
1280
1289
  * batch rather than per message: the lookup reads an appointment row and
1281
1290
  * the account’s mailbox list, and every message in a batch shares both.
1282
1291
  */
1283
- private async folderRolesFor(accountId: string): Promise<AccountFolderRoles> {
1284
- const [junk, trash] = await Promise.all([
1292
+ private async folderRolesFor(
1293
+ accountId: string,
1294
+ accountConfigId: string,
1295
+ ): Promise<AccountFolderRoles> {
1296
+ const [junk, trash, configJunkRoles] = await Promise.all([
1285
1297
  this.mailboxSpecialUseService.findJunkMailbox(accountId),
1286
1298
  this.mailboxSpecialUseService.findTrashMailbox(accountId),
1299
+ this.mailboxSpecialUseService.resolveJunkRolesForConfig(accountConfigId),
1287
1300
  ]);
1288
1301
  return {
1289
1302
  junkMailboxId: junk?.mailboxId ?? null,
1290
1303
  trashMailboxId: trash?.mailboxId ?? null,
1304
+ configJunkRoles,
1291
1305
  };
1292
1306
  }
1293
1307
 
@@ -1432,7 +1446,10 @@ export class MessageSyncService {
1432
1446
  owned = created || item.mailboxId === mailboxId;
1433
1447
 
1434
1448
  if (sighting === "junk") {
1435
- await repos.address.reconcileJunkOnlyForMessage(messageId);
1449
+ await repos.address.reconcileJunkOnlyForMessage(
1450
+ messageId,
1451
+ roles.configJunkRoles,
1452
+ );
1436
1453
  }
1437
1454
 
1438
1455
  await this.createThreadForMessage(
@@ -20,6 +20,7 @@ import type {
20
20
  UpdateMessageMoveInput,
21
21
  } from "@remit/data-ports";
22
22
  import { PlacementMoveService } from "./placement-move.js";
23
+ import { noFolderRoles } from "./test-helpers/folder-roles.js";
23
24
 
24
25
  const ACCOUNT_ID = "acc-pm";
25
26
  const ACCOUNT_CONFIG_ID = "cfg-pm";
@@ -111,6 +112,7 @@ const buildHarness = (): Harness => {
111
112
  threadMessageService,
112
113
  markerService,
113
114
  addressService,
115
+ mailboxSpecialUseService: noFolderRoles,
114
116
  sqsQueueUrl: "https://sqs.eu-west-1.amazonaws.com/000/message-mgmt",
115
117
  moveSettleTimeoutMs: 200,
116
118
  moveSettlePollMs: 10,
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { SendMessageCommand, type SQSClient } from "@aws-sdk/client-sqs";
3
3
  import type {
4
4
  IAddressRepository,
5
+ IMailboxSpecialUseRepository,
5
6
  IMessagePlacementMoveRepository,
6
7
  IMessageRepository,
7
8
  IThreadMessageRepository,
@@ -44,6 +45,12 @@ export interface PlacementMoveConfig {
44
45
  threadMessageService: IThreadMessageRepository;
45
46
  markerService: IMessagePlacementMoveRepository;
46
47
  addressService: IAddressRepository;
48
+ /**
49
+ * Required, not optional: this mover carries automatic spam placement, and
50
+ * one built without the folder-role seam would silently leave the sender of
51
+ * every auto-filed spam suggestible in the compose picker.
52
+ */
53
+ mailboxSpecialUseService: IMailboxSpecialUseRepository;
47
54
  sqsQueueUrl: string;
48
55
  sqsEndpoint?: string;
49
56
  logger?: PlacementMoveLogger;
@@ -87,6 +94,7 @@ export class PlacementMoveService {
87
94
  private threadMessageService: IThreadMessageRepository;
88
95
  private markerService: IMessagePlacementMoveRepository;
89
96
  private addressService: IAddressRepository;
97
+ private mailboxSpecialUseService: IMailboxSpecialUseRepository;
90
98
  private sqs: SQSClient;
91
99
  private queueUrl: string;
92
100
  private log: PlacementMoveLogger;
@@ -98,6 +106,7 @@ export class PlacementMoveService {
98
106
  this.threadMessageService = config.threadMessageService;
99
107
  this.markerService = config.markerService;
100
108
  this.addressService = config.addressService;
109
+ this.mailboxSpecialUseService = config.mailboxSpecialUseService;
101
110
  this.queueUrl = config.sqsQueueUrl;
102
111
  this.log = config.logger ?? noopLogger;
103
112
  this.moveSettleTimeoutMs =
@@ -200,7 +209,12 @@ export class PlacementMoveService {
200
209
  originalUid: message.uid,
201
210
  });
202
211
 
203
- await this.addressService.reconcileJunkOnlyForMessage(messageId);
212
+ await this.addressService.reconcileJunkOnlyForMessage(
213
+ messageId,
214
+ await this.mailboxSpecialUseService.resolveJunkRolesForConfig(
215
+ accountConfigId,
216
+ ),
217
+ );
204
218
 
205
219
  // The queue kick is a serious operational step, not a routine one — a
206
220
  // failure here MUST propagate (never swallowed): the marker stays
@@ -19,7 +19,7 @@ import {
19
19
  NoJunkMailboxError,
20
20
  SpamReportService,
21
21
  } from "./spam-report.js";
22
- import { trashRole } from "./test-helpers/folder-roles.js";
22
+ import { NO_JUNK_ROLES, trashRole } from "./test-helpers/folder-roles.js";
23
23
 
24
24
  const ACCOUNT = "acc-1";
25
25
  const ACCOUNT_CONFIG = "cfg-1";
@@ -235,6 +235,7 @@ const buildWorld = (
235
235
  findJunkMailbox: async () => junkMailbox,
236
236
  findTrashMailbox: async () => null,
237
237
  resolveTrashRole: async () => trashRole(null),
238
+ resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
238
239
  } as unknown as IMailboxSpecialUseRepository;
239
240
 
240
241
  const mailboxService = {
@@ -1,5 +1,8 @@
1
1
  import type { IMailboxSpecialUseRepository } from "@remit/data-ports";
2
- import type { RoleResolution } from "@remit/data-ports/folder-role";
2
+ import type {
3
+ JunkRoleMailboxes,
4
+ RoleResolution,
5
+ } from "@remit/data-ports/folder-role";
3
6
 
4
7
  type RoleMailbox = { mailboxId: string; fullPath: string };
5
8
 
@@ -24,6 +27,7 @@ export const noFolderRoles = {
24
27
  findJunkMailbox: async () => null,
25
28
  findTrashMailbox: async () => null,
26
29
  resolveTrashRole: async () => trashRole(null),
30
+ resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
27
31
  } as unknown as IMailboxSpecialUseRepository;
28
32
 
29
33
  /** A folder-role map naming the mailboxes that hold Junk and Trash. */
@@ -41,11 +45,22 @@ export const folderRoles = (roles: {
41
45
  : null,
42
46
  findTrashMailbox: async () => trash,
43
47
  resolveTrashRole: async () => trashRole(trash),
48
+ resolveJunkRolesForConfig: async () => ({
49
+ junkMailboxIds: roles.junkMailboxId ? [roles.junkMailboxId] : [],
50
+ trashMailboxIds: roles.trashMailboxId ? [roles.trashMailboxId] : [],
51
+ }),
44
52
  } as unknown as IMailboxSpecialUseRepository;
45
53
  };
46
54
 
55
+ /** An account whose config resolves no Junk and no Trash anywhere. */
56
+ export const NO_JUNK_ROLES: JunkRoleMailboxes = {
57
+ junkMailboxIds: [],
58
+ trashMailboxIds: [],
59
+ };
60
+
47
61
  /** The same account, as the resolved map `saveMessage` is handed. */
48
62
  export const NO_FOLDER_ROLES = {
49
63
  junkMailboxId: null,
50
64
  trashMailboxId: null,
65
+ configJunkRoles: NO_JUNK_ROLES,
51
66
  };