@remit/backend 2.7.0 → 2.8.0

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.
@@ -1,6 +1,6 @@
1
- import type { ClassificationConfigured, ClassificationNotConfigured, ClassificationPreset, ClassificationUsage, UpdateUserSettingsInput } from "@remit/api-openapi-types";
1
+ import type { ClassificationConfigured, ClassificationNotConfigured, ClassificationPreset, ClassificationPresetId, ClassificationUsage, UpdateUserSettingsInput } from "@remit/api-openapi-types";
2
2
  import type { IAccountSettingRepository } from "@remit/data-ports";
3
- import { type SavedClassificationProvider, type StoredClassification } from "@remit/data-ports/classification-presets";
3
+ import { type ClassificationResolverDeps, type ResolvedClassification, type ResolvedClassificationProvider } from "@remit/data-ports/classification-resolver";
4
4
  import { type ClassificationLabelDeps } from "@remit/mailbox-service";
5
5
  import { type SecretsService, type SerializedEncryptedPayload } from "@remit/secrets-service";
6
6
  import type { UserSettingsOutcome } from "./user-settings.js";
@@ -32,16 +32,25 @@ export interface ClassificationProviderSave {
32
32
  apiKey: ApiKeyChange;
33
33
  }
34
34
  type ClassificationInput = Pick<UpdateUserSettingsInput, "classificationPresetId" | "classificationModel" | "classificationBaseUrl" | "classificationApiKey" | "classificationEnabled" | "classificationDailyLimit">;
35
- export declare const decideClassificationProvider: (input: ClassificationInput, stored: StoredClassification) => Decision<ClassificationProviderSave>;
36
- export declare const publicClassification: (stored: StoredClassification, usage: ClassificationUsage[]) => ClassificationNotConfigured | ClassificationConfigured;
37
- export declare const readClassificationUsage: (accountSetting: Pick<IAccountSettingRepository, "getMany">, accountConfigId: string, accountIds: readonly string[], saved: Pick<SavedClassificationProvider, "presetId" | "dailyLimit">, now: number) => Promise<ClassificationUsage[]>;
35
+ export type KnownClassification = Exclude<ResolvedClassification, {
36
+ state: "UnknownPreset";
37
+ }>;
38
+ export declare const readKnownClassification: (deps: ClassificationResolverDeps, accountConfigId: string) => Promise<KnownClassification>;
39
+ export declare const decideClassificationProvider: (input: ClassificationInput, known: KnownClassification) => Decision<ClassificationProviderSave>;
40
+ export declare const publicClassification: (resolved: ResolvedClassificationProvider | {
41
+ state: "NotConfigured";
42
+ }, usage: ClassificationUsage[]) => ClassificationNotConfigured | ClassificationConfigured;
43
+ export declare const readClassificationUsage: (accountSetting: Pick<IAccountSettingRepository, "getMany">, accountConfigId: string, accountIds: readonly string[], saved: {
44
+ presetId: ClassificationPresetId;
45
+ dailyLimit: number;
46
+ }, now: number) => Promise<ClassificationUsage[]>;
38
47
  export declare const classificationPresetsResponse: () => ClassificationPreset[];
39
48
  interface ClassificationChange {
40
49
  provider: ClassificationProviderSave | undefined;
41
50
  enabled: boolean | undefined;
42
51
  dailyLimit: number | undefined;
43
52
  }
44
- export declare const decideClassificationChange: (input: ClassificationInput, stored: StoredClassification) => Decision<ClassificationChange>;
53
+ export declare const decideClassificationChange: (input: ClassificationInput, known: KnownClassification) => Decision<ClassificationChange>;
45
54
  export declare const writeClassificationChange: (deps: ClassificationSettingsDeps, accountConfigId: string, change: ClassificationChange) => Promise<void>;
46
55
  export declare const clearClassification: (accountSetting: Pick<IAccountSettingRepository, "delete">, accountConfigId: string) => Promise<void>;
47
56
  export {};
@@ -1,8 +1,16 @@
1
1
  import { parseAccountSettingValue } from "@remit/data-ports/account-settings";
2
2
  import { CLASSIFICATION_PRESETS, CLASSIFICATION_SETTING_NAMES, classificationUsageName, utcDateOf, } from "@remit/data-ports/classification-presets";
3
+ import { ClassificationSource, readResolvedClassification, } from "@remit/data-ports/classification-resolver";
3
4
  import { AccountSettingName } from "@remit/domain-enums";
4
5
  import { ensureClassificationLabels, } from "@remit/mailbox-service";
5
6
  import { serializeEncryptedPayload, } from "@remit/secrets-service";
7
+ export const readKnownClassification = async (deps, accountConfigId) => {
8
+ const resolved = await readResolvedClassification(deps, accountConfigId);
9
+ if (resolved.state === "UnknownPreset") {
10
+ throw new Error(`Stored classification preset is unknown: ${resolved.presetId}`);
11
+ }
12
+ return resolved;
13
+ };
6
14
  const refuse = (code, message) => ({
7
15
  ok: false,
8
16
  error: { code, message },
@@ -40,21 +48,31 @@ const apiKeyFor = (preset, requested, keptKey) => {
40
48
  }
41
49
  return refuse("ClassificationApiKeyRequired", `classificationApiKey is required for ${preset.label}`);
42
50
  };
43
- export const decideClassificationProvider = (input, stored) => {
44
- const saved = stored.state === "Configured" ? stored : undefined;
45
- const presetId = input.classificationPresetId ?? saved?.presetId;
51
+ const presetIdOf = (known) => {
52
+ if (known.state === "NotConfigured")
53
+ return undefined;
54
+ if (known.state === "Unrunnable")
55
+ return known.presetId;
56
+ return known.presetId.value;
57
+ };
58
+ export const decideClassificationProvider = (input, known) => {
59
+ const presetId = input.classificationPresetId ?? presetIdOf(known);
46
60
  if (presetId === undefined) {
47
61
  return refuse("ClassificationNotConfigured", "classificationPresetId is required: no classification provider is saved");
48
62
  }
49
63
  const preset = CLASSIFICATION_PRESETS[presetId];
50
- const kept = saved?.presetId === presetId ? saved : undefined;
51
- const baseUrl = baseUrlFor(preset, (input.classificationBaseUrl ?? kept?.baseUrl ?? "").trim());
64
+ const kept = known.state === "Configured" && known.presetId.value === presetId
65
+ ? known
66
+ : undefined;
67
+ const baseUrl = baseUrlFor(preset, (input.classificationBaseUrl ?? kept?.baseUrl.value ?? "").trim());
52
68
  if (!baseUrl.ok)
53
69
  return baseUrl;
54
- const apiKey = apiKeyFor(preset, (input.classificationApiKey ?? "").trim(), kept?.apiKeyCiphertext);
70
+ const apiKey = apiKeyFor(preset, (input.classificationApiKey ?? "").trim(), kept?.apiKey.source === ClassificationSource.Saved
71
+ ? kept.apiKey.ciphertext
72
+ : undefined);
55
73
  if (!apiKey.ok)
56
74
  return apiKey;
57
- const model = (input.classificationModel ?? kept?.model ?? "").trim();
75
+ const model = (input.classificationModel ?? kept?.model.value ?? "").trim();
58
76
  return {
59
77
  ok: true,
60
78
  value: {
@@ -65,11 +83,20 @@ export const decideClassificationProvider = (input, stored) => {
65
83
  },
66
84
  };
67
85
  };
68
- export const publicClassification = (stored, usage) => {
69
- if (stored.state === "NotConfigured")
70
- return stored;
71
- const { apiKeyCiphertext: _ciphertext, ...configured } = stored;
72
- return { ...configured, usage };
86
+ export const publicClassification = (resolved, usage) => {
87
+ if (resolved.state === "NotConfigured")
88
+ return resolved;
89
+ return {
90
+ state: "Configured",
91
+ presetId: resolved.presetId.value,
92
+ model: resolved.model.value,
93
+ baseUrl: resolved.baseUrl.value,
94
+ apiKeySaved: resolved.apiKey.source === ClassificationSource.Saved,
95
+ enabled: resolved.enabled.value,
96
+ dailyLimit: resolved.dailyLimit.value,
97
+ savedAt: resolved.classifiesAfter,
98
+ usage,
99
+ };
73
100
  };
74
101
  const CLASSIFICATION_INPUT_TOKENS_PER_CALL = 1_500;
75
102
  const nextUtcMidnight = (epochMs) => {
@@ -109,15 +136,15 @@ export const readClassificationUsage = async (accountSetting, accountConfigId, a
109
136
  });
110
137
  };
111
138
  export const classificationPresetsResponse = () => Object.values(CLASSIFICATION_PRESETS);
112
- export const decideClassificationChange = (input, stored) => {
139
+ export const decideClassificationChange = (input, known) => {
113
140
  let provider;
114
141
  if (savesClassificationProvider(input)) {
115
- const decided = decideClassificationProvider(input, stored);
142
+ const decided = decideClassificationProvider(input, known);
116
143
  if (!decided.ok)
117
144
  return decided;
118
145
  provider = decided.value;
119
146
  }
120
- const configured = provider !== undefined || stored.state === "Configured";
147
+ const configured = provider !== undefined || known.state === "Configured";
121
148
  if (input.classificationEnabled === true && !configured) {
122
149
  return refuse("ClassificationNotConfigured", "classificationEnabled needs a saved classification provider");
123
150
  }
@@ -1,10 +1,9 @@
1
1
  import type { ClassificationTestFailed, ClassificationTestInput, ClassificationTestPassed } from "@remit/api-openapi-types";
2
- import type { IAccountSettingRepository } from "@remit/data-ports";
2
+ import type { ClassificationResolverDeps } from "@remit/data-ports/classification-resolver";
3
3
  import { type ClassifierLogger } from "@remit/mailbox-service/classifier";
4
4
  import { type SecretsService } from "@remit/secrets-service";
5
5
  import type { UserSettingsOutcome } from "./user-settings.js";
6
- export interface ClassificationTestDeps {
7
- accountSetting: Pick<IAccountSettingRepository, "getMany">;
6
+ export interface ClassificationTestDeps extends ClassificationResolverDeps {
8
7
  secrets: Pick<SecretsService, "decrypt">;
9
8
  logger: ClassifierLogger;
10
9
  }
@@ -1,8 +1,7 @@
1
- import { readStoredClassification } from "@remit/data-ports/classification-presets";
2
1
  import { ClassificationFailureKind as FailureKind, ClassificationField as Field, } from "@remit/domain-enums";
3
2
  import { CLASSIFIER_LABELS, CLASSIFIER_SAMPLE_MESSAGE, classifyWithProvider, } from "@remit/mailbox-service/classifier";
4
3
  import { deserializeEncryptedPayload, } from "@remit/secrets-service";
5
- import { decideClassificationProvider, } from "./classification-settings.js";
4
+ import { decideClassificationProvider, readKnownClassification, } from "./classification-settings.js";
6
5
  const REACH_FIX = "Check the endpoint runs and the Remit server can reach it";
7
6
  const DEFAULT_MODEL_FIX = "Use the preset's default model";
8
7
  const FAILURE_HELP = {
@@ -59,7 +58,7 @@ export const testClassificationProvider = async (deps, accountConfigId, input) =
59
58
  classificationModel: input.model,
60
59
  classificationBaseUrl: input.baseUrl,
61
60
  classificationApiKey: input.apiKey,
62
- }, await readStoredClassification(deps.accountSetting, accountConfigId));
61
+ }, await readKnownClassification(deps, accountConfigId));
63
62
  if (!decided.ok)
64
63
  return decided;
65
64
  const save = decided.value;
@@ -52,6 +52,8 @@ type ThreadSearch = {
52
52
  starred?: boolean;
53
53
  attachments?: boolean;
54
54
  category?: MessageCategory[];
55
+ senderTrust?: SenderTrust[];
56
+ dkimMismatch?: boolean;
55
57
  };
56
58
  /**
57
59
  * Minimal client surface `executeThreadSearch` needs. Declared structurally
@@ -84,21 +86,12 @@ export type ThreadSearchParams = {
84
86
  limit?: number;
85
87
  };
86
88
  /**
87
- * Run a per-mailbox thread search: select the index/window read, optionally
88
- * count, and resolve off-row criteria by enriching+filtering the window. The
89
- * `results` toggle omits `items` (count-only) and `count` adds the match count.
89
+ * Run a per-mailbox thread search: select the index/window read and optionally
90
+ * count. The `results` toggle omits `items` (count-only) and `count` adds the
91
+ * match count.
90
92
  *
91
93
  * `count` names the whole match, never the page: a page size bounds the rows a
92
94
  * response carries and has no bearing on how many messages match (#509).
93
- *
94
- * An off-row criterion is the one shape that has no such number. `senderTrust`
95
- * and `dkimMismatch` are resolved by enriching a window, so counting them means
96
- * enriching every candidate row in the mailbox. `count` is therefore absent
97
- * (#305) — a page's post-filter length labelled `count` is a page length
98
- * presented as a total, which is the defect the field exists to avoid. Absence
99
- * is the same answer the response already gives for a count nobody asked for,
100
- * so no caller needs a second shape to read it. A count-only request under an
101
- * off-row criterion therefore reads nothing at all.
102
95
  */
103
96
  export declare const executeThreadSearch: (client: ThreadSearchClient, accountConfigId: string, mailboxId: string, params: ThreadSearchParams) => Promise<ThreadSearchResponse>;
104
97
  /**
@@ -1,7 +1,6 @@
1
1
  import { NotFoundError } from "@remit/data-ports/errors";
2
2
  import { isValidMessageId } from "@remit/data-ports/id";
3
3
  import { enrichThreadRows, } from "../derive/enrichThreadRows.js";
4
- import { filterByOffRowCriteria, hasOffRowCriteria, } from "../derive/filterThreadCriteria.js";
5
4
  const DEFAULT_THREADS_PAGE_SIZE = 50;
6
5
  // Project to fields used by ListThreadsResponse — keep in sync with handler mapping.
7
6
  // Includes ThreadMessage table key fields (accountConfigId, threadMessageId) and
@@ -63,21 +62,12 @@ const toArray = (value) => {
63
62
  return Array.isArray(value) ? value : [value];
64
63
  };
65
64
  /**
66
- * Run a per-mailbox thread search: select the index/window read, optionally
67
- * count, and resolve off-row criteria by enriching+filtering the window. The
68
- * `results` toggle omits `items` (count-only) and `count` adds the match count.
65
+ * Run a per-mailbox thread search: select the index/window read and optionally
66
+ * count. The `results` toggle omits `items` (count-only) and `count` adds the
67
+ * match count.
69
68
  *
70
69
  * `count` names the whole match, never the page: a page size bounds the rows a
71
70
  * response carries and has no bearing on how many messages match (#509).
72
- *
73
- * An off-row criterion is the one shape that has no such number. `senderTrust`
74
- * and `dkimMismatch` are resolved by enriching a window, so counting them means
75
- * enriching every candidate row in the mailbox. `count` is therefore absent
76
- * (#305) — a page's post-filter length labelled `count` is a page length
77
- * presented as a total, which is the defect the field exists to avoid. Absence
78
- * is the same answer the response already gives for a count nobody asked for,
79
- * so no caller needs a second shape to read it. A count-only request under an
80
- * off-row criterion therefore reads nothing at all.
81
71
  */
82
72
  export const executeThreadSearch = async (client, accountConfigId, mailboxId, params) => {
83
73
  const search = {
@@ -88,8 +78,6 @@ export const executeThreadSearch = async (client, accountConfigId, mailboxId, pa
88
78
  starred: params.starred,
89
79
  attachments: params.attachments,
90
80
  category: params.category,
91
- };
92
- const offRow = {
93
81
  senderTrust: params.senderTrust,
94
82
  dkimMismatch: params.dkimMismatch,
95
83
  };
@@ -100,16 +88,6 @@ export const executeThreadSearch = async (client, accountConfigId, mailboxId, pa
100
88
  order: params.order,
101
89
  limit: params.limit,
102
90
  });
103
- if (hasOffRowCriteria(offRow)) {
104
- if (!wantResults)
105
- return {};
106
- const window = await client.threadMessage.searchByMailboxWindow(accountConfigId, mailboxId, search, options);
107
- const enriched = await enrichThreadRows(window.items, client, accountConfigId);
108
- return {
109
- items: filterByOffRowCriteria(enriched, offRow),
110
- continuationToken: window.continuationToken,
111
- };
112
- }
113
91
  const response = {};
114
92
  if (wantResults) {
115
93
  const window = await client.threadMessage.searchByMailboxWindow(accountConfigId, mailboxId, search, options);
@@ -1,10 +1,13 @@
1
1
  import type { UpdateUserSettingsInput, UserSettingsResponse } from "@remit/api-openapi-types";
2
- import type { IAccountRepository, IAccountSettingRepository } from "@remit/data-ports";
2
+ import type { IAccountConfigRepository, IAccountRepository, IAccountSettingRepository } from "@remit/data-ports";
3
+ import type { ClassificationDeploymentDefault } from "@remit/data-ports/classification-presets";
3
4
  import type { ClassificationLabelDeps } from "@remit/mailbox-service";
4
5
  import type { SecretsService } from "@remit/secrets-service";
5
6
  import type { OperationHandler, UserSettingsOperationIds } from "../types.js";
6
7
  export interface UserSettingsDeps {
7
8
  accountSetting: Pick<IAccountSettingRepository, "get" | "getMany" | "upsert" | "delete">;
9
+ accountConfig: Pick<IAccountConfigRepository, "get">;
10
+ classificationDefault: ClassificationDeploymentDefault;
8
11
  accounts: Pick<IAccountRepository, "listAllByAccountConfig">;
9
12
  secrets: Pick<SecretsService, "encrypt">;
10
13
  label: ClassificationLabelDeps;
@@ -1,10 +1,9 @@
1
1
  import { invitationSenderInEffect, sendingAccountFor, } from "@remit/calendar-service";
2
2
  import { parseAccountSettingValue } from "@remit/data-ports/account-settings";
3
- import { readStoredClassification } from "@remit/data-ports/classification-presets";
4
3
  import { AccountSettingName } from "@remit/domain-enums";
5
4
  import { logger } from "@remit/logger";
6
5
  import { badRequest } from "./calendar.js";
7
- import { classificationPresetsResponse, clearClassification, decideClassificationChange, publicClassification, readClassificationUsage, writeClassificationChange, } from "./classification-settings.js";
6
+ import { classificationPresetsResponse, clearClassification, decideClassificationChange, publicClassification, readClassificationUsage, readKnownClassification, writeClassificationChange, } from "./classification-settings.js";
8
7
  import { testClassificationProvider } from "./classification-test.js";
9
8
  /** The account the reader chose to send invitations from, if they chose one. */
10
9
  export const chosenInvitationSender = async (accountSetting, accountConfigId) => {
@@ -17,19 +16,25 @@ export const readUserSettings = async (deps, accountConfigId) => {
17
16
  const chosen = await chosenInvitationSender(deps.accountSetting, accountConfigId);
18
17
  const accounts = await deps.accounts.listAllByAccountConfig(accountConfigId);
19
18
  const inEffect = invitationSenderInEffect(accounts, chosen);
20
- const stored = await readStoredClassification(deps.accountSetting, accountConfigId);
21
- const usage = stored.state === "Configured"
22
- ? await readClassificationUsage(deps.accountSetting, accountConfigId, accounts.map((account) => account.accountId), stored, Date.now())
19
+ const resolved = await readKnownClassification(deps, accountConfigId);
20
+ if (resolved.state === "Unrunnable") {
21
+ throw new Error(`Classification provider ${resolved.presetId} cannot run: ${resolved.reason}`);
22
+ }
23
+ const usage = resolved.state === "Configured"
24
+ ? await readClassificationUsage(deps.accountSetting, accountConfigId, accounts.map((account) => account.accountId), {
25
+ presetId: resolved.presetId.value,
26
+ dailyLimit: resolved.dailyLimit.value,
27
+ }, Date.now())
23
28
  : [];
24
29
  return {
25
30
  ...(inEffect && { defaultInvitationSenderAccountId: inEffect.accountId }),
26
31
  defaultInvitationSenderChosen: inEffect !== undefined && inEffect.accountId === chosen,
27
32
  classificationPresets: classificationPresetsResponse(),
28
- classification: publicClassification(stored, usage),
33
+ classification: publicClassification(resolved, usage),
29
34
  };
30
35
  };
31
36
  export const updateUserSettingsFor = async (deps, accountConfigId, input) => {
32
- const classification = decideClassificationChange(input, await readStoredClassification(deps.accountSetting, accountConfigId));
37
+ const classification = decideClassificationChange(input, await readKnownClassification(deps, accountConfigId));
33
38
  if (!classification.ok)
34
39
  return classification;
35
40
  const sender = input.defaultInvitationSenderAccountId;
@@ -54,6 +59,8 @@ export const updateUserSettingsFor = async (deps, accountConfigId, input) => {
54
59
  };
55
60
  const depsOf = (ctx) => ({
56
61
  accountSetting: ctx.client.accountSetting,
62
+ accountConfig: ctx.client.accountConfig,
63
+ classificationDefault: ctx.client.classificationDefault,
57
64
  accounts: ctx.client.account,
58
65
  secrets: ctx.client.secrets,
59
66
  label: ctx.client.label,
@@ -79,6 +86,8 @@ export const UserSettingsOperations = {
79
86
  UserSettingsOperations_testClassificationProvider: async (context, ctx) => {
80
87
  const tested = await testClassificationProvider({
81
88
  accountSetting: ctx.client.accountSetting,
89
+ accountConfig: ctx.client.accountConfig,
90
+ classificationDefault: ctx.client.classificationDefault,
82
91
  secrets: ctx.client.secrets,
83
92
  logger,
84
93
  }, ctx.identity.accountConfigId, context.request.requestBody);
@@ -161,6 +161,8 @@ export const composeReadPathServices = (core, queues, sync) => ({
161
161
  accountService: core.account,
162
162
  }, {
163
163
  accountSetting: core.accountSetting,
164
+ accountConfig: core.accountConfig,
165
+ classificationDefault: core.classificationDefault,
164
166
  secrets: core.secrets,
165
167
  labelService: core.label,
166
168
  messageLabelService: core.messageLabel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "2.7.0",
3
+ "version": "2.8.0",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -1,23 +0,0 @@
1
- import type { SenderTrust, ThreadMessageResponse } from "@remit/api-openapi-types";
2
- /**
3
- * Off-row search criteria — fields that live on the underlying Message/Address,
4
- * not on the ThreadMessage row. They are resolved by enriching the windowed
5
- * rows (see enrichThreadRows) and filtering in app code, because no index or
6
- * FilterExpression can serve them.
7
- *
8
- * `senderTrust` derives from AddressItem.flags and `dkimMismatch` from
9
- * MessageItem.authenticity. `category` is not one of these: it is denormalized
10
- * onto the ThreadMessage row and filtered in SQL, inside the window.
11
- */
12
- export interface OffRowCriteria {
13
- senderTrust?: SenderTrust[];
14
- dkimMismatch?: boolean;
15
- }
16
- export declare const hasOffRowCriteria: (criteria: OffRowCriteria) => boolean;
17
- /**
18
- * Filter enriched rows by the off-row criteria. Each active criterion is an
19
- * any-of set (AND across criteria, OR within a set). A row with no
20
- * `authenticity` signal never matches a `dkimMismatch` filter (absence means no
21
- * signal, not a verdict).
22
- */
23
- export declare const filterByOffRowCriteria: (rows: ThreadMessageResponse[], criteria: OffRowCriteria) => ThreadMessageResponse[];
@@ -1,24 +0,0 @@
1
- export const hasOffRowCriteria = (criteria) => Boolean(criteria.senderTrust?.length) || criteria.dkimMismatch !== undefined;
2
- /**
3
- * Filter enriched rows by the off-row criteria. Each active criterion is an
4
- * any-of set (AND across criteria, OR within a set). A row with no
5
- * `authenticity` signal never matches a `dkimMismatch` filter (absence means no
6
- * signal, not a verdict).
7
- */
8
- export const filterByOffRowCriteria = (rows, criteria) => {
9
- if (!hasOffRowCriteria(criteria))
10
- return rows;
11
- const trustSet = criteria.senderTrust?.length
12
- ? new Set(criteria.senderTrust)
13
- : undefined;
14
- const { dkimMismatch } = criteria;
15
- return rows.filter((row) => {
16
- if (trustSet && !trustSet.has(row.senderTrust))
17
- return false;
18
- if (dkimMismatch !== undefined &&
19
- row.authenticity?.dkimMismatch !== dkimMismatch) {
20
- return false;
21
- }
22
- return true;
23
- });
24
- };