@remit/mailbox-service 0.0.102 → 0.0.104

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.102",
3
+ "version": "0.0.104",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -25,6 +25,10 @@
25
25
  "types": "./src/account-credentials.ts",
26
26
  "default": "./src/account-credentials.ts"
27
27
  },
28
+ "./outbox-attachment-content": {
29
+ "types": "./src/outbox-attachment-content.ts",
30
+ "default": "./src/outbox-attachment-content.ts"
31
+ },
28
32
  "./types": {
29
33
  "types": "./src/types.ts",
30
34
  "default": "./src/types.ts"
@@ -160,7 +160,7 @@ describe("unsubscribed auto-mark-read is decided once (issue #499)", () => {
160
160
  "m-1",
161
161
  "acc-1",
162
162
  "cfg-1",
163
- "INBOX",
163
+ { fullPath: "INBOX", placement: "inbox" },
164
164
  async () => bodyConnection(),
165
165
  );
166
166
 
@@ -181,7 +181,7 @@ describe("unsubscribed auto-mark-read is decided once (issue #499)", () => {
181
181
  "m-1",
182
182
  "acc-1",
183
183
  "cfg-1",
184
- "INBOX",
184
+ { fullPath: "INBOX", placement: "inbox" },
185
185
  async () => bodyConnection(),
186
186
  );
187
187
 
@@ -204,7 +204,7 @@ describe("unsubscribed auto-mark-read is decided once (issue #499)", () => {
204
204
  ["m-1"],
205
205
  "acc-1",
206
206
  "cfg-1",
207
- "INBOX",
207
+ { fullPath: "INBOX", placement: "inbox" },
208
208
  async () => bodyConnection(),
209
209
  true,
210
210
  );
@@ -346,7 +346,7 @@ const readBody = async (
346
346
  messageId,
347
347
  "acc-1",
348
348
  "cfg-1",
349
- "INBOX",
349
+ { fullPath: "INBOX", placement: "inbox" },
350
350
  async () => connection,
351
351
  );
352
352
  };
@@ -195,7 +195,7 @@ const syncBatch = (harness: Harness) =>
195
195
  ["m-1"],
196
196
  "acc-1",
197
197
  "cfg-1",
198
- "INBOX",
198
+ { fullPath: "INBOX", placement: "inbox" },
199
199
  async () =>
200
200
  ({
201
201
  openBox: async () => {},
@@ -212,7 +212,7 @@ const syncFromImap = (harness: Harness) =>
212
212
  "m-1",
213
213
  "acc-1",
214
214
  "cfg-1",
215
- "INBOX",
215
+ { fullPath: "INBOX", placement: "inbox" },
216
216
  async () =>
217
217
  ({
218
218
  openBox: async () => {},
@@ -199,7 +199,7 @@ describe("Address.flags.category overrides classification at sync time (issue #2
199
199
  "m-1",
200
200
  "acc-1",
201
201
  "cfg-1",
202
- "INBOX",
202
+ { fullPath: "INBOX", placement: "inbox" },
203
203
  async () => connection,
204
204
  );
205
205
 
@@ -229,7 +229,7 @@ describe("Address.flags.category overrides classification at sync time (issue #2
229
229
  "m-1",
230
230
  "acc-1",
231
231
  "cfg-1",
232
- "INBOX",
232
+ { fullPath: "INBOX", placement: "inbox" },
233
233
  async () => connection,
234
234
  );
235
235
 
@@ -251,7 +251,7 @@ describe("Address.flags.category overrides classification at sync time (issue #2
251
251
  "m-1",
252
252
  "acc-1",
253
253
  "cfg-1",
254
- "INBOX",
254
+ { fullPath: "INBOX", placement: "inbox" },
255
255
  async () => connection,
256
256
  );
257
257
 
@@ -276,7 +276,7 @@ describe("Address.flags.category overrides classification at sync time (issue #2
276
276
  "m-1",
277
277
  "acc-1",
278
278
  "cfg-1",
279
- "INBOX",
279
+ { fullPath: "INBOX", placement: "inbox" },
280
280
  async () => connection,
281
281
  );
282
282
 
@@ -296,7 +296,7 @@ describe("Address.flags.category overrides classification at sync time (issue #2
296
296
  ["m-1"],
297
297
  "acc-1",
298
298
  "cfg-1",
299
- "INBOX",
299
+ { fullPath: "INBOX", placement: "inbox" },
300
300
  async () =>
301
301
  ({
302
302
  openBox: async () => {},
@@ -336,7 +336,7 @@ describe("Address.flags.category overrides classification at sync time (issue #2
336
336
  ["m-1"],
337
337
  "acc-1",
338
338
  "cfg-1",
339
- "INBOX",
339
+ { fullPath: "INBOX", placement: "inbox" },
340
340
  async () => {
341
341
  throw new Error("an examined message must not be fetched again");
342
342
  },
@@ -376,7 +376,7 @@ describe("Address.flags.category overrides classification at sync time (issue #2
376
376
  "m-1",
377
377
  "acc-1",
378
378
  "cfg-1",
379
- "INBOX",
379
+ { fullPath: "INBOX", placement: "inbox" },
380
380
  async () => connection,
381
381
  );
382
382
 
@@ -413,7 +413,7 @@ describe("Address.flags.category overrides classification at sync time (issue #2
413
413
  ["m-1"],
414
414
  "acc-1",
415
415
  "cfg-1",
416
- "INBOX",
416
+ { fullPath: "INBOX", placement: "inbox" },
417
417
  async () => connection,
418
418
  true,
419
419
  );
@@ -492,7 +492,7 @@ describe("Address.flags.category overrides classification at sync time (issue #2
492
492
  "m-1",
493
493
  "acc-1",
494
494
  "cfg-1",
495
- "INBOX",
495
+ { fullPath: "INBOX", placement: "inbox" },
496
496
  async () => connection,
497
497
  ),
498
498
  /ProvisionedThroughputExceededException/,
@@ -150,7 +150,7 @@ describe("Message.category survives a re-entrant classification pass", () => {
150
150
  "m-1",
151
151
  "acc-1",
152
152
  "cfg-1",
153
- "INBOX",
153
+ { fullPath: "INBOX", placement: "inbox" },
154
154
  async () => connection,
155
155
  );
156
156
 
@@ -188,7 +188,7 @@ describe("Message.category survives a re-entrant classification pass", () => {
188
188
  ["m-1"],
189
189
  "acc-1",
190
190
  "cfg-1",
191
- "INBOX",
191
+ { fullPath: "INBOX", placement: "inbox" },
192
192
  async () => connection,
193
193
  true,
194
194
  );
@@ -220,7 +220,7 @@ describe("Message.category survives a re-entrant classification pass", () => {
220
220
  "m-1",
221
221
  "acc-1",
222
222
  "cfg-1",
223
- "INBOX",
223
+ { fullPath: "INBOX", placement: "inbox" },
224
224
  async () => connection,
225
225
  );
226
226
 
@@ -181,7 +181,7 @@ const readBody = async (service: BodySyncService, mailboxPath = "INBOX") => {
181
181
  "m-1",
182
182
  "acc-1",
183
183
  "cfg-1",
184
- mailboxPath,
184
+ { fullPath: mailboxPath, placement: "other" },
185
185
  async () => connection,
186
186
  );
187
187
  };
@@ -161,7 +161,7 @@ const readBody = async (
161
161
  "m-1",
162
162
  "acc-1",
163
163
  "cfg-1",
164
- mailboxPath,
164
+ { fullPath: mailboxPath, placement: "other" },
165
165
  async () => connection,
166
166
  );
167
167
  };
@@ -187,7 +187,7 @@ describe("placement survives a re-entrant computePlacement pass (issue #383)", (
187
187
  "m-1",
188
188
  "acc-1",
189
189
  "cfg-1",
190
- "INBOX",
190
+ { fullPath: "INBOX", placement: "inbox" },
191
191
  async () => connection,
192
192
  );
193
193
 
@@ -233,7 +233,7 @@ describe("placement survives a re-entrant computePlacement pass (issue #383)", (
233
233
  ["m-1"],
234
234
  "acc-1",
235
235
  "cfg-1",
236
- "INBOX",
236
+ { fullPath: "INBOX", placement: "inbox" },
237
237
  async () => connection,
238
238
  true,
239
239
  );
@@ -268,7 +268,7 @@ describe("placement survives a re-entrant computePlacement pass (issue #383)", (
268
268
  "m-1",
269
269
  "acc-1",
270
270
  "cfg-1",
271
- "INBOX",
271
+ { fullPath: "INBOX", placement: "inbox" },
272
272
  async () => connection,
273
273
  );
274
274
 
@@ -309,7 +309,7 @@ describe("placement survives a re-entrant computePlacement pass (issue #383)", (
309
309
  "m-1",
310
310
  "acc-1",
311
311
  "cfg-1",
312
- "INBOX",
312
+ { fullPath: "INBOX", placement: "inbox" },
313
313
  async () => connection,
314
314
  );
315
315
 
@@ -162,8 +162,12 @@ const connection = () =>
162
162
  }) as unknown as IImapConnection;
163
163
 
164
164
  const sync = (service: BodySyncService) =>
165
- service.syncBodies(["m-1"], "acc-1", "cfg-1", "INBOX", async () =>
166
- connection(),
165
+ service.syncBodies(
166
+ ["m-1"],
167
+ "acc-1",
168
+ "cfg-1",
169
+ { fullPath: "INBOX", placement: "inbox" },
170
+ async () => connection(),
167
171
  );
168
172
 
169
173
  const parseRefusal = () =>
@@ -258,7 +262,7 @@ describe("body sync skips what is already quarantined", () => {
258
262
  ["m-1"],
259
263
  "acc-1",
260
264
  "cfg-1",
261
- "INBOX",
265
+ { fullPath: "INBOX", placement: "inbox" },
262
266
  async () => {
263
267
  throw new Error("a quarantined message must not be fetched again");
264
268
  },
@@ -275,7 +275,7 @@ describe("issue #1011: forced re-store does not re-fire standing rules", () => {
275
275
  ["m-1"],
276
276
  "acc-1",
277
277
  "cfg-1",
278
- "INBOX",
278
+ { fullPath: "INBOX", placement: "inbox" },
279
279
  async () => connection,
280
280
  true,
281
281
  );
@@ -330,7 +330,7 @@ describe("issue #1011: forced re-store does not re-fire standing rules", () => {
330
330
  ["m-1"],
331
331
  "acc-1",
332
332
  "cfg-1",
333
- "INBOX",
333
+ { fullPath: "INBOX", placement: "inbox" },
334
334
  async () => connection,
335
335
  true,
336
336
  );
@@ -374,7 +374,7 @@ describe("issue #1011: forced re-store does not re-fire standing rules", () => {
374
374
  "m-1",
375
375
  "acc-1",
376
376
  "cfg-1",
377
- "INBOX",
377
+ { fullPath: "INBOX", placement: "inbox" },
378
378
  async () => noSuchKeyConn(DEMOTE_EML),
379
379
  );
380
380
 
@@ -415,7 +415,7 @@ describe("issue #1011: forced re-store does not re-fire standing rules", () => {
415
415
  "m-1",
416
416
  "acc-1",
417
417
  "cfg-1",
418
- "INBOX",
418
+ { fullPath: "INBOX", placement: "inbox" },
419
419
  async () => noSuchKeyConn(INVITATION_EML),
420
420
  );
421
421
 
@@ -569,7 +569,7 @@ describe("issue #1011: forced re-store does not re-fire standing rules", () => {
569
569
  ["m-stored", "m-new"],
570
570
  "acc-1",
571
571
  "cfg-1",
572
- "INBOX",
572
+ { fullPath: "INBOX", placement: "inbox" },
573
573
  async () => connection,
574
574
  true,
575
575
  );
@@ -167,8 +167,12 @@ const imapConnection = () =>
167
167
  }) as unknown as IImapConnection;
168
168
 
169
169
  const syncPass = (harness: Harness) =>
170
- harness.service.syncBodies(["m-1"], "acc-1", "cfg-1", "INBOX", async () =>
171
- imapConnection(),
170
+ harness.service.syncBodies(
171
+ ["m-1"],
172
+ "acc-1",
173
+ "cfg-1",
174
+ { fullPath: "INBOX", placement: "inbox" },
175
+ async () => imapConnection(),
172
176
  );
173
177
 
174
178
  describe("a body the classifier declined to categorize is examined once", () => {
@@ -129,7 +129,7 @@ const readBody = async (
129
129
  "m-1",
130
130
  "acc-1",
131
131
  "cfg-1",
132
- "INBOX",
132
+ { fullPath: "INBOX", placement: "inbox" },
133
133
  async () => connection,
134
134
  );
135
135
  };
package/src/body-sync.ts CHANGED
@@ -51,6 +51,7 @@ import {
51
51
  FilterPipeline,
52
52
  } from "./filters/pipeline.js";
53
53
  import type { FlagQueueService } from "./flag-queue.js";
54
+ import { placementOf, resolveFolderRoleMailboxes } from "./folder-placement.js";
54
55
  import {
55
56
  classifyPlacement,
56
57
  type FolderPlacement,
@@ -239,6 +240,11 @@ export interface StoreBodyPartContentsResult {
239
240
 
240
241
  export type ConnectionGetter = () => Promise<IImapConnection>;
241
242
 
243
+ export interface BodySyncFolder {
244
+ fullPath: string;
245
+ placement: FolderPlacement;
246
+ }
247
+
242
248
  export interface PlacementConfig {
243
249
  mailboxSpecialUseService: IMailboxSpecialUseRepository;
244
250
  /**
@@ -332,7 +338,7 @@ export class BodySyncService {
332
338
  * @param messageIds - The message IDs to sync bodies for
333
339
  * @param accountId - The account ID (for storage path)
334
340
  * @param accountConfigId - The account config ID (for thread updates)
335
- * @param mailboxPath - The IMAP mailbox path
341
+ * @param folder - The IMAP mailbox path and its folder placement
336
342
  * @param getConnection - Function to get a (lazy) IMAP connection
337
343
  * @param force - Bypass the "already stored" skip guard and re-fetch every
338
344
  * message in this batch even though `bodyStorageKey` is already set. Set
@@ -345,13 +351,17 @@ export class BodySyncService {
345
351
  messageIds: string[],
346
352
  accountId: string,
347
353
  accountConfigId: string,
348
- mailboxPath: string,
354
+ folder: BodySyncFolder,
349
355
  getConnection: ConnectionGetter,
350
356
  force = false,
351
357
  ): Promise<SyncBodiesResult> {
352
358
  const syncedMessageIds: string[] = [];
353
359
  let skippedCount = 0;
354
- const location = { accountId, accountConfigId, mailboxPath };
360
+ const location = {
361
+ accountId,
362
+ accountConfigId,
363
+ mailboxPath: folder.fullPath,
364
+ };
355
365
 
356
366
  // Resolve every message up front so we can issue ONE ranged FETCH for the
357
367
  // whole batch (the desktop-client pattern) instead of a SELECT + download
@@ -420,7 +430,7 @@ export class BodySyncService {
420
430
  const connection = await getConnection();
421
431
  // Single SELECT for the whole batch. openBox is idempotent, so a warm
422
432
  // connection already on this mailbox skips the SELECT entirely.
423
- await connection.openBox(mailboxPath);
433
+ await connection.openBox(folder.fullPath);
424
434
 
425
435
  let connectionLost = false;
426
436
  try {
@@ -440,6 +450,7 @@ export class BodySyncService {
440
450
  accountId,
441
451
  accountConfigId,
442
452
  source,
453
+ folder.placement,
443
454
  reStoredMessageIds.has(messageId),
444
455
  );
445
456
  } catch (error) {
@@ -628,6 +639,7 @@ export class BodySyncService {
628
639
  accountId: string,
629
640
  accountConfigId: string,
630
641
  source: Readable,
642
+ placement: FolderPlacement,
631
643
  isReStore: boolean,
632
644
  ): Promise<void> {
633
645
  const toStorage = new PassThrough();
@@ -672,6 +684,7 @@ export class BodySyncService {
672
684
  {
673
685
  uri: ref.uri,
674
686
  },
687
+ placement,
675
688
  isReStore,
676
689
  );
677
690
  }
@@ -704,6 +717,7 @@ export class BodySyncService {
704
717
  accountConfigId: string,
705
718
  body: Buffer,
706
719
  bodyRef: { uri: string },
720
+ placement: FolderPlacement,
707
721
  /**
708
722
  * Whether this call is a re-store of an already-classified message
709
723
  * rather than a first store. The two re-entrant paths (`force: true`
@@ -767,7 +781,11 @@ export class BodySyncService {
767
781
  // `flags.category` override when one applies (issue #299). The derived
768
782
  // fields are folded into the single Message update below — they are NOT
769
783
  // written here.
770
- const classification = await this.classifyMessage(accountConfigId, parsed);
784
+ const classification = await this.classifyMessage(
785
+ accountConfigId,
786
+ parsed,
787
+ placement,
788
+ );
771
789
 
772
790
  // Decide the placement move from the in-memory classification before the
773
791
  // write, so its `movedByRemit` flag and audit verdict join the same
@@ -984,7 +1002,7 @@ export class BodySyncService {
984
1002
  * @param messageId - The message ID to fetch
985
1003
  * @param accountId - The account ID (for storage path)
986
1004
  * @param accountConfigId - The account config ID (for thread updates)
987
- * @param mailboxPath - The IMAP mailbox path
1005
+ * @param folder - The IMAP mailbox path and its folder placement
988
1006
  * @param getConnection - Function to get a (lazy) IMAP connection
989
1007
  * @returns Parsed text and HTML content
990
1008
  */
@@ -992,7 +1010,7 @@ export class BodySyncService {
992
1010
  messageId: string,
993
1011
  accountId: string,
994
1012
  accountConfigId: string,
995
- mailboxPath: string,
1013
+ folder: BodySyncFolder,
996
1014
  getConnection: ConnectionGetter,
997
1015
  ): Promise<FetchBodyResult> {
998
1016
  const message = await this.messageService.get(messageId);
@@ -1030,14 +1048,18 @@ export class BodySyncService {
1030
1048
  needsStore = true;
1031
1049
  body = await this.fetchFromImap(
1032
1050
  message.uid,
1033
- mailboxPath,
1051
+ folder.fullPath,
1034
1052
  getConnection,
1035
1053
  );
1036
1054
  }
1037
1055
  } else {
1038
1056
  // Fetch from IMAP and store
1039
1057
  needsStore = true;
1040
- body = await this.fetchFromImap(message.uid, mailboxPath, getConnection);
1058
+ body = await this.fetchFromImap(
1059
+ message.uid,
1060
+ folder.fullPath,
1061
+ getConnection,
1062
+ );
1041
1063
  }
1042
1064
 
1043
1065
  let parsed: ParsedMail;
@@ -1062,6 +1084,7 @@ export class BodySyncService {
1062
1084
  accountConfigId,
1063
1085
  body,
1064
1086
  { uri: ref.uri },
1087
+ folder.placement,
1065
1088
  message.bodyStorageKey != null,
1066
1089
  );
1067
1090
  } else {
@@ -1097,8 +1120,14 @@ export class BodySyncService {
1097
1120
  private async classifyMessage(
1098
1121
  accountConfigId: string,
1099
1122
  parsed: ParsedMail,
1123
+ placement: FolderPlacement,
1100
1124
  ): Promise<UpdateMessageInput & { category: ThreadMessageCategory }> {
1101
- return classifyParsedMessage(this.addressService, accountConfigId, parsed);
1125
+ return classifyParsedMessage(
1126
+ this.addressService,
1127
+ accountConfigId,
1128
+ parsed,
1129
+ placement,
1130
+ );
1102
1131
  }
1103
1132
 
1104
1133
  private async incrementInboundCount(
@@ -1458,17 +1487,12 @@ export class BodySyncService {
1458
1487
  // true)`) skip the decision steps entirely, so only the pass that first
1459
1488
  // stores a body ever reaches this method.
1460
1489
 
1461
- const junkMailbox =
1462
- await mailboxSpecialUseService.findJunkMailbox(accountId);
1463
- const inboxMailbox =
1464
- await mailboxSpecialUseService.findInboxMailbox(accountId);
1465
-
1466
- const placement: FolderPlacement =
1467
- junkMailbox && message.mailboxId === junkMailbox.mailboxId
1468
- ? "junk"
1469
- : inboxMailbox && message.mailboxId === inboxMailbox.mailboxId
1470
- ? "inbox"
1471
- : "other";
1490
+ const roleMailboxes = await resolveFolderRoleMailboxes(
1491
+ mailboxSpecialUseService,
1492
+ accountId,
1493
+ );
1494
+ const { junkMailbox, inboxMailbox } = roleMailboxes;
1495
+ const placement = placementOf(message.mailboxId, roleMailboxes);
1472
1496
 
1473
1497
  const fromEmail = extractPrimaryFromEmail(parsed);
1474
1498
  const signals = fromEmail
@@ -13,8 +13,10 @@ import assert from "node:assert/strict";
13
13
  import { describe, it } from "node:test";
14
14
  import type {
15
15
  AccountConfigItem,
16
+ AccountItem,
16
17
  AddressItem,
17
18
  IAccountConfigRepository,
19
+ IAccountRepository,
18
20
  IAddressRepository,
19
21
  IMessageRepository,
20
22
  IThreadMessageRepository,
@@ -25,8 +27,10 @@ import type {
25
27
  UpdateThreadMessageInput,
26
28
  } from "@remit/data-ports";
27
29
  import { NotFoundError } from "@remit/data-ports/errors";
30
+ import { DisplayNameCorrespondence } from "@remit/domain-enums";
28
31
  import type { StorageService } from "@remit/storage-service";
29
32
  import { backfillClassifications } from "./classification-backfill.js";
33
+ import type { FolderPlacementLookup } from "./folder-placement.js";
30
34
 
31
35
  const PLAIN_EML = Buffer.from(
32
36
  [
@@ -52,6 +56,20 @@ const LIST_EML = Buffer.from(
52
56
  ].join("\r\n"),
53
57
  );
54
58
 
59
+ const IMPERSONATION_EML = Buffer.from(
60
+ [
61
+ "From: InfoMedics <billing@serviceupdatebank.atlassian.net>",
62
+ "To: me@example.com",
63
+ "Subject: Openstaande factuur",
64
+ "DKIM-Signature: v=1; a=rsa-sha256; d=atlassian.net; s=sel; b=xxx",
65
+ "Content-Type: text/plain",
66
+ "",
67
+ "Betaal uw factuur",
68
+ ].join("\r\n"),
69
+ );
70
+
71
+ const JUNK_MAILBOX_ID = "mb-junk";
72
+
55
73
  const row = (overrides: Partial<ThreadMessageItem>): ThreadMessageItem =>
56
74
  ({
57
75
  threadMessageId: "tm-1",
@@ -97,6 +115,8 @@ const asAccount = (accountConfigId: string): AccountConfigItem =>
97
115
 
98
116
  interface Harness {
99
117
  accountConfigService: Pick<IAccountConfigRepository, "listAll">;
118
+ accountService: Pick<IAccountRepository, "listAllByAccountConfig">;
119
+ mailboxSpecialUseService: FolderPlacementLookup;
100
120
  addressService: Pick<IAddressRepository, "getAddress">;
101
121
  threadMessageService: Pick<
102
122
  IThreadMessageRepository,
@@ -134,6 +154,20 @@ const buildHarness = (options: {
134
154
  listAll: async () => accounts,
135
155
  };
136
156
 
157
+ const accountService: Pick<IAccountRepository, "listAllByAccountConfig"> = {
158
+ listAllByAccountConfig: async (accountConfigId: string) => [
159
+ { accountId: `account-${accountConfigId}` } as unknown as AccountItem,
160
+ ],
161
+ };
162
+
163
+ const mailboxSpecialUseService: FolderPlacementLookup = {
164
+ findJunkMailbox: async () => ({
165
+ mailboxId: JUNK_MAILBOX_ID,
166
+ fullPath: "Junk",
167
+ }),
168
+ findInboxMailbox: async () => ({ mailboxId: "mb-1", fullPath: "INBOX" }),
169
+ };
170
+
137
171
  const addressService = {
138
172
  getAddress: (async () => {
139
173
  if (options.addressFlags === undefined)
@@ -203,6 +237,8 @@ const buildHarness = (options: {
203
237
 
204
238
  return {
205
239
  accountConfigService,
240
+ accountService,
241
+ mailboxSpecialUseService,
206
242
  addressService,
207
243
  threadMessageService,
208
244
  messageService,
@@ -237,6 +273,42 @@ describe("backfillClassifications", () => {
237
273
  );
238
274
  });
239
275
 
276
+ it("compares the sender of a Junk row the provider rated clean, and not of an inbox row", async () => {
277
+ const harness = buildHarness({
278
+ rows: [
279
+ row({
280
+ threadMessageId: "tm-junk",
281
+ messageId: "m-junk",
282
+ mailboxId: JUNK_MAILBOX_ID,
283
+ }),
284
+ row({ threadMessageId: "tm-inbox", messageId: "m-inbox" }),
285
+ ],
286
+ messages: [
287
+ message({
288
+ messageId: "m-junk",
289
+ mailboxId: JUNK_MAILBOX_ID,
290
+ bodyStorageKey: "s3://m-junk",
291
+ }),
292
+ message({ messageId: "m-inbox", bodyStorageKey: "s3://m-inbox" }),
293
+ ],
294
+ retrieve: async () => IMPERSONATION_EML,
295
+ });
296
+
297
+ await backfillClassifications(harness);
298
+
299
+ const correspondence = new Map(
300
+ harness.messageUpdates.map((update) => [
301
+ update.messageId,
302
+ update.input.authenticity?.displayNameCorrespondence,
303
+ ]),
304
+ );
305
+ assert.equal(
306
+ correspondence.get("m-junk"),
307
+ DisplayNameCorrespondence.Unrelated,
308
+ );
309
+ assert.equal(correspondence.get("m-inbox"), undefined);
310
+ });
311
+
240
312
  it("denormalizes the classifier's answer onto the thread row", async () => {
241
313
  const harness = buildHarness({
242
314
  rows: [row({ threadMessageId: "tm-1", messageId: "m-1" })],