@remit/backend 0.0.29 → 0.0.30

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.29",
3
+ "version": "0.0.30",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -213,7 +213,14 @@ const candidate = (
213
213
  over: Partial<OrganizeCandidate["message"]> = {},
214
214
  ): OrganizeCandidate => ({
215
215
  messageId,
216
- message: { from: "", fromName: "", subject: "", text: "", ...over },
216
+ message: {
217
+ from: "",
218
+ fromName: "",
219
+ subject: "",
220
+ text: "",
221
+ listId: "",
222
+ ...over,
223
+ },
217
224
  });
218
225
 
219
226
  describe("matchOrganize", () => {
@@ -597,3 +604,115 @@ describe("back-apply pipeline (matchOrganize -> applyOrganize)", () => {
597
604
  );
598
605
  });
599
606
  });
607
+
608
+ describe("matchOrganize with ListId and FromDomain clauses", () => {
609
+ const senderChunk = (
610
+ messageId: string,
611
+ fromEmail: string,
612
+ over: Partial<ChunkMetadata> = {},
613
+ ): VectorRecord => ({
614
+ chunkId: `${messageId}#sender`,
615
+ vector: ANCHOR_VECTOR,
616
+ metadata: metadata({
617
+ messageId,
618
+ chunkType: "sender",
619
+ textPreview: fromEmail,
620
+ ...over,
621
+ }),
622
+ });
623
+
624
+ it("matches a ListId clause exactly off the vector-free projection", async () => {
625
+ const deps = vectorlessDeps([
626
+ candidate("msg-1", { listId: "weekly.news.example.com" }),
627
+ candidate("msg-2", { listId: "news.example.com" }),
628
+ candidate("msg-3", {}),
629
+ ]);
630
+
631
+ const { messageIds } = await matchOrganize(deps, ACCOUNT_CONFIG_ID, {
632
+ ...predicate(),
633
+ anchorMessageId: "None",
634
+ literalClauses: [{ field: "ListId", value: "weekly.news.example.com" }],
635
+ });
636
+
637
+ assert.deepEqual(messageIds, ["msg-1"]);
638
+ });
639
+
640
+ it("matches a FromDomain clause public-suffix aware off the vector-free projection", async () => {
641
+ const deps = vectorlessDeps([
642
+ candidate("msg-1", { from: "notifications@github.com" }),
643
+ candidate("msg-2", { from: "ci@sub.github.com" }),
644
+ candidate("msg-3", { from: "attacker@github.com.evil.example" }),
645
+ ]);
646
+
647
+ const { messageIds } = await matchOrganize(deps, ACCOUNT_CONFIG_ID, {
648
+ ...predicate(),
649
+ anchorMessageId: "None",
650
+ literalClauses: [{ field: "FromDomain", value: "github.com" }],
651
+ });
652
+
653
+ assert.deepEqual(messageIds, ["msg-1", "msg-2"]);
654
+ });
655
+
656
+ it("round-trips ListId and FromDomain through the semantic-arm chunk projection", async () => {
657
+ const store = createMemoryVectorStore();
658
+ await store.upsert([
659
+ senderChunk("msg-1", "ci@github.com", {
660
+ listId: "actions.github.com",
661
+ }),
662
+ bodyChunk("msg-1", ANCHOR_VECTOR, { listId: "actions.github.com" }),
663
+ senderChunk("msg-2", "hi@othersender.example", {
664
+ listId: "other.list.example",
665
+ }),
666
+ bodyChunk("msg-2", ANCHOR_VECTOR, { listId: "other.list.example" }),
667
+ ]);
668
+
669
+ const byListId = await matchOrganize(matchDeps(store), ACCOUNT_CONFIG_ID, {
670
+ ...predicate(),
671
+ literalClauses: [{ field: "ListId", value: "actions.github.com" }],
672
+ });
673
+ assert.deepEqual(byListId.messageIds, ["msg-1"]);
674
+
675
+ const byFromDomain = await matchOrganize(
676
+ matchDeps(store),
677
+ ACCOUNT_CONFIG_ID,
678
+ {
679
+ ...predicate(),
680
+ literalClauses: [{ field: "FromDomain", value: "github.com" }],
681
+ },
682
+ );
683
+ assert.deepEqual(byFromDomain.messageIds, ["msg-1"]);
684
+ });
685
+
686
+ it("applies a ListId predicate to exactly the previewed set (preview == apply)", async () => {
687
+ const deps = vectorlessDeps([
688
+ candidate("msg-1", { listId: "weekly.news.example.com" }),
689
+ candidate("msg-2", { listId: "weekly.news.example.com" }),
690
+ candidate("msg-3", { listId: "other.example.com" }),
691
+ ]);
692
+ const p = predicate({
693
+ anchorMessageId: "None",
694
+ actionLabelId: "lbl-list",
695
+ literalClauses: [{ field: "ListId", value: "weekly.news.example.com" }],
696
+ });
697
+
698
+ const previewed = await matchOrganize(deps, ACCOUNT_CONFIG_ID, p);
699
+ const applied = await matchOrganize(deps, ACCOUNT_CONFIG_ID, p);
700
+ assert.deepEqual(previewed, applied);
701
+ assert.deepEqual(previewed.messageIds, ["msg-1", "msg-2"]);
702
+
703
+ const tracked = trackingClient();
704
+ const result = await applyOrganize(
705
+ { client: tracked.client },
706
+ ACCOUNT_CONFIG_ID,
707
+ applied.messageIds,
708
+ p,
709
+ );
710
+
711
+ assert.equal(result.applied, 2);
712
+ assert.equal(result.failed, 0);
713
+ assert.deepEqual(tracked.labeled.map((row) => row.messageId).sort(), [
714
+ "msg-1",
715
+ "msg-2",
716
+ ]);
717
+ });
718
+ });
@@ -131,11 +131,13 @@ const filterMessageFromChunks = (
131
131
  let subject = "";
132
132
  let fromName = "";
133
133
  let from = "";
134
+ let listId = "";
134
135
  const textParts: string[] = [];
135
136
  for (const record of records) {
136
137
  const meta = record.metadata;
137
138
  if (meta.subject && !subject) subject = meta.subject;
138
139
  if (meta.fromName && !fromName) fromName = meta.fromName;
140
+ if (meta.listId && !listId) listId = meta.listId;
139
141
  if (meta.chunkType === "sender" && meta.textPreview && !from) {
140
142
  from = meta.textPreview;
141
143
  }
@@ -146,7 +148,7 @@ const filterMessageFromChunks = (
146
148
  textParts.push(meta.textPreview);
147
149
  }
148
150
  }
149
- return { from, fromName, subject, text: textParts.join("\n") };
151
+ return { from, fromName, subject, listId, text: textParts.join("\n") };
150
152
  };
151
153
 
152
154
  /**
@@ -355,8 +357,10 @@ const buildSemanticFromEnv = (): OrganizeSemanticDeps => {
355
357
  * deployment that ships no vector pipeline. Deduped by message id — the same
356
358
  * mail filed in two folders is one candidate.
357
359
  *
358
- * `From`/`Subject` come from the row verbatim (full fidelity). `text` (the body)
359
- * is left empty: the thread row carries only a truncated `snippet`, and matching
360
+ * `From`/`Subject`/`listId` come from the row verbatim (full fidelity), so a
361
+ * `From`, `Subject`, `FromDomain` or `ListId` clause matches here exactly as it
362
+ * does at index time. `text` (the body) is left empty: the thread row carries
363
+ * only a truncated `snippet`, and matching
360
364
  * a body-content clause against a preview would silently diverge from the live
361
365
  * index-time filter's full-body match. Body-content (`HasWords`) clauses are
362
366
  * rejected before this is read (see {@link assertNoBodyContentClause}), so the
@@ -384,6 +388,7 @@ const listAccountFilterMessagesFromClient =
384
388
  fromName: row.fromName ?? "",
385
389
  subject: row.subject ?? "",
386
390
  text: "",
391
+ listId: row.listId ?? "",
387
392
  },
388
393
  });
389
394
  if (candidates.length >= limit) return candidates;