@remit/mailbox-service 0.0.112 → 0.0.114
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/dist/account-credentials.d.ts +83 -0
- package/dist/account-credentials.js +104 -0
- package/dist/attribute-mapper.d.ts +36 -0
- package/dist/attribute-mapper.js +119 -0
- package/dist/body-parse.d.ts +28 -0
- package/{src/body-parse.ts → dist/body-parse.js} +18 -26
- package/dist/body-part-mapper.d.ts +92 -0
- package/dist/body-part-mapper.js +301 -0
- package/dist/body-sync-queue.d.ts +59 -0
- package/dist/body-sync-queue.js +56 -0
- package/dist/body-sync-terminal.d.ts +58 -0
- package/dist/body-sync-terminal.js +95 -0
- package/dist/body-sync.d.ts +351 -0
- package/dist/body-sync.js +1204 -0
- package/dist/calendar-parts.d.ts +23 -0
- package/dist/calendar-parts.js +18 -0
- package/dist/classification-backfill.d.ts +91 -0
- package/dist/classification-backfill.js +180 -0
- package/dist/classify-message.d.ts +31 -0
- package/dist/classify-message.js +89 -0
- package/dist/connection-factory.d.ts +54 -0
- package/dist/connection-factory.js +54 -0
- package/dist/connection-test.d.ts +41 -0
- package/dist/connection-test.js +152 -0
- package/dist/external-move.d.ts +94 -0
- package/dist/external-move.js +173 -0
- package/dist/filter-operations.d.ts +42 -0
- package/dist/filter-operations.js +211 -0
- package/dist/filters/filter-config.d.ts +14 -0
- package/dist/filters/filter-config.js +10 -0
- package/{src/filters/list-id.ts → dist/filters/list-id.d.ts} +2 -16
- package/dist/filters/list-id.js +26 -0
- package/dist/filters/match.d.ts +69 -0
- package/dist/filters/match.js +165 -0
- package/dist/filters/pipeline.d.ts +64 -0
- package/dist/filters/pipeline.js +100 -0
- package/{src/flag-push-terminal.ts → dist/flag-push-terminal.d.ts} +16 -72
- package/dist/flag-push-terminal.js +63 -0
- package/dist/flag-push.d.ts +69 -0
- package/dist/flag-push.js +111 -0
- package/dist/flag-queue.d.ts +175 -0
- package/dist/flag-queue.js +291 -0
- package/dist/folder-operations.d.ts +39 -0
- package/dist/folder-operations.js +112 -0
- package/dist/folder-placement.d.ts +13 -0
- package/dist/folder-placement.js +23 -0
- package/dist/heuristics/authenticationResults.d.ts +10 -0
- package/dist/heuristics/authenticationResults.js +187 -0
- package/dist/heuristics/classifyByHeaders.d.ts +82 -0
- package/dist/heuristics/classifyByHeaders.js +365 -0
- package/dist/heuristics/classifyPlacement.d.ts +67 -0
- package/dist/heuristics/classifyPlacement.js +153 -0
- package/dist/heuristics/machineSenders.d.ts +21 -0
- package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
- package/dist/heuristics/senderMismatch.d.ts +81 -0
- package/dist/heuristics/senderMismatch.js +192 -0
- package/dist/heuristics/socialDomains.d.ts +14 -0
- package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
- package/dist/heuristics/transactionalDomains.d.ts +18 -0
- package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
- package/dist/imapflow-connection.d.ts +377 -0
- package/dist/imapflow-connection.js +1147 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +65 -0
- package/dist/list-id-backfill.d.ts +83 -0
- package/dist/list-id-backfill.js +132 -0
- package/dist/mailbox-cursor-rebuild.d.ts +74 -0
- package/dist/mailbox-cursor-rebuild.js +71 -0
- package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
- package/dist/mailbox-cursor.js +102 -0
- package/dist/mailbox-intent.d.ts +41 -0
- package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +38 -64
- package/dist/mailbox-management.d.ts +240 -0
- package/dist/mailbox-management.js +576 -0
- package/dist/mailbox-presence.d.ts +52 -0
- package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
- package/dist/mailbox-queue.d.ts +129 -0
- package/dist/mailbox-queue.js +216 -0
- package/dist/mailbox-sync.d.ts +93 -0
- package/dist/mailbox-sync.js +490 -0
- package/dist/mailbox-upstream.d.ts +38 -0
- package/{src/mailbox-upstream.ts → dist/mailbox-upstream.js} +37 -47
- package/dist/message-category.d.ts +51 -0
- package/{src/message-category.ts → dist/message-category.js} +28 -71
- package/dist/message-move.d.ts +285 -0
- package/dist/message-move.js +858 -0
- package/dist/message-parser.d.ts +14 -0
- package/dist/message-parser.js +17 -0
- package/{src/message-presence.ts → dist/message-presence.d.ts} +1 -13
- package/dist/message-presence.js +29 -0
- package/dist/message-read.d.ts +51 -0
- package/dist/message-read.js +31 -0
- package/dist/message-sync.d.ts +345 -0
- package/dist/message-sync.js +1358 -0
- package/dist/mime-walker.d.ts +102 -0
- package/dist/mime-walker.js +198 -0
- package/dist/organize-job.d.ts +20 -0
- package/dist/organize-job.js +37 -0
- package/dist/outbox-attachment-content.d.ts +37 -0
- package/dist/outbox-attachment-content.js +50 -0
- package/dist/outbox-attachment-filename.d.ts +19 -0
- package/dist/outbox-attachment-filename.js +64 -0
- package/dist/outbox-attachment.d.ts +148 -0
- package/dist/outbox-attachment.js +273 -0
- package/dist/outbox-queue.d.ts +73 -0
- package/dist/outbox-queue.js +280 -0
- package/{src/outbox-status.ts → dist/outbox-status.d.ts} +1 -6
- package/dist/outbox-status.js +18 -0
- package/dist/ownership.d.ts +24 -0
- package/dist/ownership.js +83 -0
- package/dist/placement-move-terminal.d.ts +67 -0
- package/dist/placement-move-terminal.js +88 -0
- package/dist/placement-move.d.ts +83 -0
- package/dist/placement-move.js +216 -0
- package/{src/placement-settled.ts → dist/placement-settled.d.ts} +13 -68
- package/dist/placement-settled.js +75 -0
- package/dist/quarantine.d.ts +95 -0
- package/dist/quarantine.js +131 -0
- package/dist/refusal.d.ts +31 -0
- package/dist/refusal.js +3 -0
- package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -97
- package/dist/restore-source-placement.js +79 -0
- package/dist/search-text-backfill.d.ts +15 -0
- package/dist/search-text-backfill.js +33 -0
- package/dist/snippet.d.ts +61 -0
- package/dist/snippet.js +129 -0
- package/dist/spam-report.d.ts +86 -0
- package/dist/spam-report.js +241 -0
- package/dist/sync-warning.d.ts +21 -0
- package/dist/sync-warning.js +26 -0
- package/dist/sync-watermarks.d.ts +138 -0
- package/dist/sync-watermarks.js +205 -0
- package/dist/test-helpers/fake-imap.d.ts +151 -0
- package/dist/test-helpers/fake-imap.js +508 -0
- package/dist/test-helpers/folder-roles.d.ts +34 -0
- package/dist/test-helpers/folder-roles.js +47 -0
- package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
- package/dist/test-helpers/mime-tree-generator.js +329 -0
- package/dist/test-helpers/test-store.d.ts +69 -0
- package/dist/test-helpers/test-store.js +193 -0
- package/dist/text/normalizer.d.ts +20 -0
- package/dist/text/normalizer.js +102 -0
- package/dist/thread-message-rows.d.ts +69 -0
- package/dist/thread-message-rows.js +40 -0
- package/dist/thread-root.d.ts +7 -0
- package/dist/thread-root.js +25 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +19 -0
- package/package.json +86 -88
- package/src/account-credentials.ts +0 -210
- package/src/attribute-mapper.test.ts +0 -104
- package/src/attribute-mapper.ts +0 -148
- package/src/body-parse.test.ts +0 -55
- package/src/body-part-mapper.property.test.ts +0 -354
- package/src/body-part-mapper.ts +0 -410
- package/src/body-sync-queue.test.ts +0 -64
- package/src/body-sync-queue.ts +0 -131
- package/src/body-sync-terminal.test.ts +0 -148
- package/src/body-sync-terminal.ts +0 -174
- package/src/body-sync.test.ts +0 -925
- package/src/body-sync.ts +0 -1891
- package/src/calendar-parts.ts +0 -41
- package/src/classification-backfill.test.ts +0 -285
- package/src/classification-backfill.ts +0 -363
- package/src/classify-message.test.ts +0 -142
- package/src/classify-message.ts +0 -116
- package/src/connection-factory.ts +0 -102
- package/src/connection-test.ts +0 -208
- package/src/credentials.test.ts +0 -181
- package/src/external-move.test.ts +0 -262
- package/src/external-move.ts +0 -285
- package/src/filter-operations.test.ts +0 -319
- package/src/filter-operations.ts +0 -377
- package/src/filter-update.test.ts +0 -313
- package/src/filters/filter-config.test.ts +0 -87
- package/src/filters/filter-config.ts +0 -34
- package/src/filters/list-id.test.ts +0 -50
- package/src/filters/match.test.ts +0 -392
- package/src/filters/match.ts +0 -225
- package/src/filters/pipeline.test.ts +0 -199
- package/src/filters/pipeline.ts +0 -170
- package/src/flag-push.test.ts +0 -112
- package/src/flag-push.ts +0 -198
- package/src/flag-queue.test.ts +0 -197
- package/src/flag-queue.ts +0 -509
- package/src/folder-operations.test.ts +0 -273
- package/src/folder-operations.ts +0 -217
- package/src/folder-placement.ts +0 -56
- package/src/forwarded-message.test.ts +0 -122
- package/src/heuristics/authenticationResults.test.ts +0 -189
- package/src/heuristics/authenticationResults.ts +0 -212
- package/src/heuristics/classifyByHeaders.test.ts +0 -889
- package/src/heuristics/classifyByHeaders.ts +0 -467
- package/src/heuristics/classifyPlacement.test.ts +0 -366
- package/src/heuristics/classifyPlacement.ts +0 -211
- package/src/heuristics/classifyRealMail.test.ts +0 -242
- package/src/heuristics/machineSenders.test.ts +0 -101
- package/src/heuristics/senderMismatch.test.ts +0 -346
- package/src/heuristics/senderMismatch.ts +0 -259
- package/src/imapflow-connect-retry.test.ts +0 -97
- package/src/imapflow-connection.test.ts +0 -503
- package/src/imapflow-connection.ts +0 -1476
- package/src/index.ts +0 -397
- package/src/list-id-backfill.test.ts +0 -207
- package/src/list-id-backfill.ts +0 -274
- package/src/mailbox-cursor-rebuild.test.ts +0 -171
- package/src/mailbox-cursor-rebuild.ts +0 -146
- package/src/mailbox-cursor.test.ts +0 -141
- package/src/mailbox-delete-intent.test.ts +0 -119
- package/src/mailbox-management.test.ts +0 -243
- package/src/mailbox-management.ts +0 -838
- package/src/mailbox-presence.test.ts +0 -74
- package/src/mailbox-queue.ts +0 -397
- package/src/mailbox-rename-intent.test.ts +0 -220
- package/src/mailbox-rename-subtree.test.ts +0 -287
- package/src/mailbox-sync.test.ts +0 -286
- package/src/mailbox-sync.ts +0 -724
- package/src/message-move.test.ts +0 -726
- package/src/message-move.ts +0 -1518
- package/src/message-parser.ts +0 -26
- package/src/message-presence.test.ts +0 -50
- package/src/message-read.ts +0 -87
- package/src/message-save-retry.test.ts +0 -90
- package/src/message-sync-flags.test.ts +0 -167
- package/src/message-sync.test.ts +0 -762
- package/src/message-sync.ts +0 -2133
- package/src/mime-walker.test.ts +0 -311
- package/src/mime-walker.ts +0 -306
- package/src/organize-job.ts +0 -93
- package/src/outbox-attachment-content.test.ts +0 -153
- package/src/outbox-attachment-content.ts +0 -104
- package/src/outbox-attachment-filename.test.ts +0 -132
- package/src/outbox-attachment-filename.ts +0 -76
- package/src/outbox-attachment.test.ts +0 -626
- package/src/outbox-attachment.ts +0 -536
- package/src/outbox-failed-recovery.test.ts +0 -299
- package/src/outbox-queue.test.ts +0 -315
- package/src/outbox-queue.ts +0 -525
- package/src/ownership.ts +0 -140
- package/src/placement-move-terminal.ts +0 -145
- package/src/placement-move.ts +0 -363
- package/src/placement-settled.test.ts +0 -147
- package/src/placement.test.ts +0 -404
- package/src/quarantine.test.ts +0 -133
- package/src/quarantine.ts +0 -229
- package/src/refusal.ts +0 -29
- package/src/snippet.test.ts +0 -213
- package/src/snippet.ts +0 -152
- package/src/spam-report.test.ts +0 -329
- package/src/spam-report.ts +0 -357
- package/src/sync-warning.test.ts +0 -213
- package/src/sync-warning.ts +0 -56
- package/src/sync-watermarks.test.ts +0 -409
- package/src/sync-watermarks.ts +0 -319
- package/src/system-flag-wire-format.test.ts +0 -77
- package/src/test-helpers/fake-imap.ts +0 -690
- package/src/test-helpers/folder-roles.ts +0 -66
- package/src/test-helpers/mime-tree-generator.ts +0 -461
- package/src/test-helpers/test-queue.ts +0 -56
- package/src/test-helpers/test-store.ts +0 -299
- package/src/text/normalizer.test.ts +0 -223
- package/src/text/normalizer.ts +0 -140
- package/src/thread-message-rows.ts +0 -77
- package/src/thread-root.test.ts +0 -29
- package/src/thread-root.ts +0 -51
- package/src/types/mailparser-augment.d.ts +0 -23
- package/src/types/natural-porter-stemmer.d.ts +0 -50
- package/src/types/stopword.d.ts +0 -69
- package/src/types.ts +0 -371
- package/tsconfig.json +0 -8
package/src/calendar-parts.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/// <reference path="./types/mailparser-augment.d.ts" />
|
|
2
|
-
|
|
3
|
-
import { ROOT_PART_PATH } from "@remit/data-ports/id";
|
|
4
|
-
import { CalendarSuggestionSource } from "@remit/domain-enums";
|
|
5
|
-
import type { Attachment, ParsedMail } from "mailparser";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* One `text/calendar` part of a message, and where it sat.
|
|
9
|
-
*
|
|
10
|
-
* The distinction between an inline part and an attached one is not cosmetic:
|
|
11
|
-
* a scheduling client sends the invitation inline as part of a
|
|
12
|
-
* `multipart/alternative`, while a client that cannot speak iTIP attaches an
|
|
13
|
-
* `.ics` file. Both carry the same bytes, and a card says which it read.
|
|
14
|
-
*/
|
|
15
|
-
export interface CalendarPart {
|
|
16
|
-
partPath: string;
|
|
17
|
-
source:
|
|
18
|
-
| typeof CalendarSuggestionSource.IcalendarPart
|
|
19
|
-
| typeof CalendarSuggestionSource.IcalendarAttachment;
|
|
20
|
-
icalData: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const isCalendar = (attachment: Attachment): boolean =>
|
|
24
|
-
(attachment.contentType ?? "").toLowerCase().startsWith("text/calendar");
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Every `text/calendar` part of a parsed message, in declaration order.
|
|
28
|
-
*
|
|
29
|
-
* Reads mailparser's `attachments[]`, which is where every non-`text/plain`,
|
|
30
|
-
* non-`text/html` leaf lands — the same list the header classifier already
|
|
31
|
-
* looks at to route an invitation to `transactional`.
|
|
32
|
-
*/
|
|
33
|
-
export const calendarParts = (parsed: ParsedMail): CalendarPart[] =>
|
|
34
|
-
(parsed.attachments ?? []).filter(isCalendar).map((attachment) => ({
|
|
35
|
-
partPath: attachment.partId ?? ROOT_PART_PATH,
|
|
36
|
-
source:
|
|
37
|
-
attachment.contentDisposition === "attachment"
|
|
38
|
-
? CalendarSuggestionSource.IcalendarAttachment
|
|
39
|
-
: CalendarSuggestionSource.IcalendarPart,
|
|
40
|
-
icalData: attachment.content.toString("utf8"),
|
|
41
|
-
}));
|
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
-
import type {
|
|
4
|
-
AccountItem,
|
|
5
|
-
MailboxItem,
|
|
6
|
-
UpdateMessageInput,
|
|
7
|
-
} from "@remit/data-ports";
|
|
8
|
-
import { deriveAddressId } from "@remit/data-ports/id";
|
|
9
|
-
import {
|
|
10
|
-
DisplayNameCorrespondence,
|
|
11
|
-
MessageCategory,
|
|
12
|
-
MessageClassificationState,
|
|
13
|
-
} from "@remit/domain-enums";
|
|
14
|
-
import type { StorageService } from "@remit/storage-service";
|
|
15
|
-
import {
|
|
16
|
-
backfillClassifications,
|
|
17
|
-
type ClassificationBackfillCheckpoint,
|
|
18
|
-
} from "./classification-backfill.js";
|
|
19
|
-
import { eml, FakeImapServer } from "./test-helpers/fake-imap.js";
|
|
20
|
-
import {
|
|
21
|
-
arriveAt,
|
|
22
|
-
openStore,
|
|
23
|
-
type Store,
|
|
24
|
-
seedAccount,
|
|
25
|
-
seedMailbox,
|
|
26
|
-
storeBody,
|
|
27
|
-
} from "./test-helpers/test-store.js";
|
|
28
|
-
|
|
29
|
-
const PLAIN = eml({ From: "Alice <alice@example.com>", Subject: "Hi" }, "hi");
|
|
30
|
-
|
|
31
|
-
const LIST = eml(
|
|
32
|
-
{
|
|
33
|
-
From: "Alice <alice@example.com>",
|
|
34
|
-
Subject: "Newsletter",
|
|
35
|
-
"List-Id": "<news.example.com>",
|
|
36
|
-
"List-Unsubscribe": "<https://example.com/unsub>",
|
|
37
|
-
},
|
|
38
|
-
"this week in news",
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const IMPERSONATION = eml(
|
|
42
|
-
{
|
|
43
|
-
From: "InfoMedics <billing@serviceupdatebank.atlassian.net>",
|
|
44
|
-
Subject: "Openstaande factuur",
|
|
45
|
-
"DKIM-Signature": "v=1; a=rsa-sha256; d=atlassian.net; s=sel; b=xxx",
|
|
46
|
-
},
|
|
47
|
-
"Betaal uw factuur",
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
let store: Store;
|
|
51
|
-
let storage: StorageService;
|
|
52
|
-
let imap: FakeImapServer;
|
|
53
|
-
let account: AccountItem;
|
|
54
|
-
let inbox: MailboxItem;
|
|
55
|
-
let junk: MailboxItem;
|
|
56
|
-
|
|
57
|
-
beforeEach(async () => {
|
|
58
|
-
store = await openStore();
|
|
59
|
-
storage = store.storage();
|
|
60
|
-
imap = new FakeImapServer();
|
|
61
|
-
imap.addFolder("INBOX");
|
|
62
|
-
imap.addFolder("Junk");
|
|
63
|
-
account = await seedAccount(store);
|
|
64
|
-
inbox = await seedMailbox(store, account, "INBOX");
|
|
65
|
-
junk = await seedMailbox(store, account, "Junk", { specialUse: "Junk" });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
afterEach(() => store.close());
|
|
69
|
-
|
|
70
|
-
const stored = async (
|
|
71
|
-
raw: Buffer,
|
|
72
|
-
mailbox: MailboxItem = inbox,
|
|
73
|
-
update: UpdateMessageInput = {},
|
|
74
|
-
owner: AccountItem = account,
|
|
75
|
-
): Promise<string> => {
|
|
76
|
-
const messageId = await arriveAt(store, imap, owner, mailbox, raw);
|
|
77
|
-
await storeBody(store, storage, owner, messageId, raw);
|
|
78
|
-
await store.message.update(messageId, update);
|
|
79
|
-
return messageId;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const deps = () => ({
|
|
83
|
-
accountConfigService: store.accountConfig,
|
|
84
|
-
accountService: store.account,
|
|
85
|
-
mailboxSpecialUseService: store.mailboxSpecialUse,
|
|
86
|
-
addressService: store.address,
|
|
87
|
-
threadMessageService: store.threadMessage,
|
|
88
|
-
messageService: store.message,
|
|
89
|
-
storageService: storage,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const threadRow = async (messageId: string) =>
|
|
93
|
-
(
|
|
94
|
-
await store.threadMessage.findAllByMessageId(
|
|
95
|
-
account.accountConfigId,
|
|
96
|
-
messageId,
|
|
97
|
-
)
|
|
98
|
-
)[0];
|
|
99
|
-
|
|
100
|
-
describe("backfillClassifications", () => {
|
|
101
|
-
it("classifies a never-examined body and denormalizes the answer", async () => {
|
|
102
|
-
const messageId = await stored(LIST);
|
|
103
|
-
|
|
104
|
-
const result = await backfillClassifications(deps());
|
|
105
|
-
|
|
106
|
-
assert.equal(result.backfilled, 1);
|
|
107
|
-
const row = await store.message.get(messageId);
|
|
108
|
-
assert.equal(row.category, MessageCategory.newsletter);
|
|
109
|
-
assert.equal(row.hasListUnsubscribe, true);
|
|
110
|
-
assert.equal(row.classificationState, MessageClassificationState.Examined);
|
|
111
|
-
const thread = await threadRow(messageId);
|
|
112
|
-
assert.equal(thread.category, MessageCategory.newsletter);
|
|
113
|
-
assert.equal(thread.listId, "news.example.com");
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("compares the sender of a Junk row the provider rated clean, not of an inbox row", async () => {
|
|
117
|
-
const inJunk = await stored(IMPERSONATION, junk);
|
|
118
|
-
const inInbox = await stored(IMPERSONATION);
|
|
119
|
-
|
|
120
|
-
await backfillClassifications(deps());
|
|
121
|
-
|
|
122
|
-
assert.equal(
|
|
123
|
-
(await store.message.get(inJunk)).authenticity?.displayNameCorrespondence,
|
|
124
|
-
DisplayNameCorrespondence.Unrelated,
|
|
125
|
-
);
|
|
126
|
-
assert.equal(
|
|
127
|
-
(await store.message.get(inInbox)).authenticity
|
|
128
|
-
?.displayNameCorrespondence,
|
|
129
|
-
undefined,
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("applies the sender's category override over the headers", async () => {
|
|
134
|
-
const messageId = await stored(PLAIN);
|
|
135
|
-
await store.address.updateAddress(
|
|
136
|
-
account.accountConfigId,
|
|
137
|
-
deriveAddressId(account.accountConfigId, "alice@example.com"),
|
|
138
|
-
{ flags: { category: { value: MessageCategory.marketing, setAt: 1 } } },
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
await backfillClassifications(deps());
|
|
142
|
-
|
|
143
|
-
assert.equal(
|
|
144
|
-
(await store.message.get(messageId)).category,
|
|
145
|
-
MessageCategory.marketing,
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("marks an already-categorized row examined and keeps its category", async () => {
|
|
150
|
-
const messageId = await stored(PLAIN, inbox, {
|
|
151
|
-
category: MessageCategory.newsletter,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
const result = await backfillClassifications(deps());
|
|
155
|
-
|
|
156
|
-
assert.equal(result.alreadyCategorized, 1);
|
|
157
|
-
const row = await store.message.get(messageId);
|
|
158
|
-
assert.equal(row.category, MessageCategory.newsletter);
|
|
159
|
-
assert.equal(row.classificationState, MessageClassificationState.Examined);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it("leaves examined rows and rows with no stored body alone", async () => {
|
|
163
|
-
await stored(PLAIN, inbox, {
|
|
164
|
-
classificationState: MessageClassificationState.Examined,
|
|
165
|
-
});
|
|
166
|
-
const unsynced = await arriveAt(store, imap, account, inbox, PLAIN);
|
|
167
|
-
|
|
168
|
-
const result = await backfillClassifications(deps());
|
|
169
|
-
|
|
170
|
-
assert.equal(result.alreadyExamined, 1);
|
|
171
|
-
assert.equal(result.skippedNoBody, 1);
|
|
172
|
-
assert.equal(
|
|
173
|
-
(await store.message.get(unsynced)).classificationState,
|
|
174
|
-
MessageClassificationState.NotExamined,
|
|
175
|
-
);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("examines a message once however many thread rows index it", async () => {
|
|
179
|
-
const messageId = await stored(PLAIN);
|
|
180
|
-
await store.threadMessage.create({
|
|
181
|
-
...(await threadRow(messageId)),
|
|
182
|
-
threadId: "drifted-root",
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const result = await backfillClassifications(deps());
|
|
186
|
-
|
|
187
|
-
assert.equal(result.backfilled, 1);
|
|
188
|
-
assert.equal(result.alreadyExamined, 1);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("contains a failure to the one message and keeps going", async () => {
|
|
192
|
-
const lost = await stored(PLAIN, inbox, {
|
|
193
|
-
bodyStorageKey: "file:///nonexistent/remit/body.eml",
|
|
194
|
-
});
|
|
195
|
-
const fine = await stored(PLAIN);
|
|
196
|
-
|
|
197
|
-
const result = await backfillClassifications(deps());
|
|
198
|
-
|
|
199
|
-
assert.equal(result.failed, 1);
|
|
200
|
-
assert.deepEqual(result.failedThreadMessageIds, [
|
|
201
|
-
(await threadRow(lost)).threadMessageId,
|
|
202
|
-
]);
|
|
203
|
-
assert.equal(
|
|
204
|
-
(await store.message.get(fine)).classificationState,
|
|
205
|
-
MessageClassificationState.Examined,
|
|
206
|
-
);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("scans accounts in account-config order whatever order they were created in", async () => {
|
|
210
|
-
const second = await seedAccount(store, { accountConfigId: "cfg-a" });
|
|
211
|
-
const third = await seedAccount(store, { accountConfigId: "cfg-b" });
|
|
212
|
-
for (const owner of [third, second]) {
|
|
213
|
-
imap.addFolder(`INBOX-${owner.accountConfigId}`);
|
|
214
|
-
await stored(
|
|
215
|
-
PLAIN,
|
|
216
|
-
await seedMailbox(store, owner, `INBOX-${owner.accountConfigId}`),
|
|
217
|
-
{},
|
|
218
|
-
owner,
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
const order: string[] = [];
|
|
222
|
-
|
|
223
|
-
await backfillClassifications(deps(), {
|
|
224
|
-
onProgress: (progress) => order.push(progress.accountConfigId),
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
assert.deepEqual(
|
|
228
|
-
order.filter((id) => id !== account.accountConfigId),
|
|
229
|
-
["cfg-a", "cfg-b"],
|
|
230
|
-
);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("resumes from its checkpoint after an interruption", async () => {
|
|
234
|
-
for (let i = 0; i < 3; i++) await stored(PLAIN);
|
|
235
|
-
let checkpoint: ClassificationBackfillCheckpoint | undefined;
|
|
236
|
-
let cleared = false;
|
|
237
|
-
const checkpointStore = {
|
|
238
|
-
load: async () => checkpoint,
|
|
239
|
-
save: async (next: ClassificationBackfillCheckpoint) => {
|
|
240
|
-
checkpoint = next;
|
|
241
|
-
},
|
|
242
|
-
clear: async () => {
|
|
243
|
-
checkpoint = undefined;
|
|
244
|
-
cleared = true;
|
|
245
|
-
},
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
await assert.rejects(
|
|
249
|
-
backfillClassifications(deps(), {
|
|
250
|
-
batchSize: 2,
|
|
251
|
-
checkpointStore,
|
|
252
|
-
onProgress: () => {
|
|
253
|
-
throw new Error("killed");
|
|
254
|
-
},
|
|
255
|
-
}),
|
|
256
|
-
);
|
|
257
|
-
const resumed = await backfillClassifications(deps(), {
|
|
258
|
-
batchSize: 2,
|
|
259
|
-
checkpointStore,
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
assert.equal(resumed.scanned, 1);
|
|
263
|
-
assert.equal(resumed.backfilled, 1);
|
|
264
|
-
assert.equal(cleared, true);
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("restarts the pass when the checkpointed account no longer exists", async () => {
|
|
268
|
-
for (let i = 0; i < 3; i++) await stored(PLAIN);
|
|
269
|
-
|
|
270
|
-
const result = await backfillClassifications(deps(), {
|
|
271
|
-
batchSize: 2,
|
|
272
|
-
checkpointStore: {
|
|
273
|
-
load: async () => ({
|
|
274
|
-
accountConfigId: "cfg-removed",
|
|
275
|
-
continuationToken: "2",
|
|
276
|
-
}),
|
|
277
|
-
save: async () => {},
|
|
278
|
-
clear: async () => {},
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
assert.equal(result.scanned, 3);
|
|
283
|
-
assert.equal(result.backfilled, 3);
|
|
284
|
-
});
|
|
285
|
-
});
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IAccountConfigRepository,
|
|
3
|
-
IAccountRepository,
|
|
4
|
-
IAddressRepository,
|
|
5
|
-
IMessageRepository,
|
|
6
|
-
IThreadMessageRepository,
|
|
7
|
-
ThreadMessageItem,
|
|
8
|
-
} from "@remit/data-ports";
|
|
9
|
-
import { MessageClassificationState } from "@remit/domain-enums";
|
|
10
|
-
import type { StorageService } from "@remit/storage-service";
|
|
11
|
-
import { parseMessageBody } from "./body-parse.js";
|
|
12
|
-
import { extractSnippet } from "./body-sync.js";
|
|
13
|
-
import { classifyParsedMessage } from "./classify-message.js";
|
|
14
|
-
import { extractListId } from "./filters/list-id.js";
|
|
15
|
-
import {
|
|
16
|
-
type FolderPlacementLookup,
|
|
17
|
-
resolveAccountFolderPlacements,
|
|
18
|
-
} from "./folder-placement.js";
|
|
19
|
-
import type { FolderPlacement } from "./heuristics/classifyPlacement.js";
|
|
20
|
-
import {
|
|
21
|
-
denormalizeMessageCategory,
|
|
22
|
-
hasDecidedCategory,
|
|
23
|
-
} from "./message-category.js";
|
|
24
|
-
|
|
25
|
-
const DEFAULT_BATCH_SIZE = 200;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Where the full-corpus pass left off: the identity of the account it was
|
|
29
|
-
* working through, and the page cursor within that account. Resuming looks the
|
|
30
|
-
* account up by id and re-opens it at its saved cursor, rather than re-scanning
|
|
31
|
-
* the whole corpus from the top after an interruption. A checkpoint naming no
|
|
32
|
-
* configured account restarts the pass.
|
|
33
|
-
*/
|
|
34
|
-
export interface ClassificationBackfillCheckpoint {
|
|
35
|
-
accountConfigId: string;
|
|
36
|
-
continuationToken?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Persistence for {@link ClassificationBackfillCheckpoint}, injected so the
|
|
41
|
-
* pass stays testable with an in-memory fake; the real entrypoint backs it
|
|
42
|
-
* with a file. `clear()` runs once the whole corpus has been scanned, so a
|
|
43
|
-
* later run starts fresh rather than reading a stale finished-run checkpoint.
|
|
44
|
-
*/
|
|
45
|
-
export interface ClassificationBackfillCheckpointStore {
|
|
46
|
-
load(): Promise<ClassificationBackfillCheckpoint | undefined>;
|
|
47
|
-
save(checkpoint: ClassificationBackfillCheckpoint): Promise<void>;
|
|
48
|
-
clear(): Promise<void>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface ClassificationBackfillDeps {
|
|
52
|
-
accountConfigService: Pick<IAccountConfigRepository, "listAll">;
|
|
53
|
-
accountService: Pick<IAccountRepository, "listAllByAccountConfig">;
|
|
54
|
-
mailboxSpecialUseService: FolderPlacementLookup;
|
|
55
|
-
/** Reads the sender's `Address.flags.category` override (issue #299). */
|
|
56
|
-
addressService: Pick<IAddressRepository, "getAddress">;
|
|
57
|
-
threadMessageService: Pick<
|
|
58
|
-
IThreadMessageRepository,
|
|
59
|
-
"listByAccount" | "findAllByMessageId" | "update"
|
|
60
|
-
>;
|
|
61
|
-
messageService: Pick<IMessageRepository, "get" | "update">;
|
|
62
|
-
storageService: Pick<StorageService, "retrieve">;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface ClassificationBackfillLogger {
|
|
66
|
-
info(obj: Record<string, unknown>, msg: string): void;
|
|
67
|
-
error?(obj: Record<string, unknown>, msg: string): void;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface ClassificationBackfillTotals {
|
|
71
|
-
scanned: number;
|
|
72
|
-
alreadyExamined: number;
|
|
73
|
-
alreadyCategorized: number;
|
|
74
|
-
skippedNoBody: number;
|
|
75
|
-
backfilled: number;
|
|
76
|
-
failed: number;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface ClassificationBackfillResult
|
|
80
|
-
extends ClassificationBackfillTotals {
|
|
81
|
-
failedThreadMessageIds: string[];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface ClassificationBackfillProgress
|
|
85
|
-
extends ClassificationBackfillTotals {
|
|
86
|
-
accountConfigId: string;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface ClassificationBackfillOptions {
|
|
90
|
-
/** Rows fetched per `listByAccount` page. */
|
|
91
|
-
batchSize?: number;
|
|
92
|
-
checkpointStore?: ClassificationBackfillCheckpointStore;
|
|
93
|
-
logger?: ClassificationBackfillLogger;
|
|
94
|
-
/** Called once per page, after that page's rows are settled. */
|
|
95
|
-
onProgress?: (progress: ClassificationBackfillProgress) => void;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const emptyTotals = (): ClassificationBackfillTotals => ({
|
|
99
|
-
scanned: 0,
|
|
100
|
-
alreadyExamined: 0,
|
|
101
|
-
alreadyCategorized: 0,
|
|
102
|
-
skippedNoBody: 0,
|
|
103
|
-
backfilled: 0,
|
|
104
|
-
failed: 0,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Read the stored raw source, classify it once, and record that the
|
|
109
|
-
* classifier ran. Kept as its own function (rather than inline in a try/catch)
|
|
110
|
-
* so the caller can contain a failure with `.then(fulfilled, rejected)`
|
|
111
|
-
* instead of a block catch, which keeps the `try` around the one call whose
|
|
112
|
-
* failure this contains.
|
|
113
|
-
*
|
|
114
|
-
* The ThreadMessage rows are denormalized BEFORE the Message update, for the
|
|
115
|
-
* same reason the deleted in-sync backfill did it that way (issue #320): the
|
|
116
|
-
* signal this pass selects by — `classificationState` — is written in the
|
|
117
|
-
* Message update, so a failure between the two writes leaves both undone and
|
|
118
|
-
* the rerun redoes them. Writing the Message first would strand the
|
|
119
|
-
* denormalized rows at `uncategorized` forever — the row reads as examined and
|
|
120
|
-
* the retry returns early.
|
|
121
|
-
*/
|
|
122
|
-
const classifyAndApply = async (
|
|
123
|
-
deps: ClassificationBackfillDeps,
|
|
124
|
-
accountConfigId: string,
|
|
125
|
-
row: ThreadMessageItem,
|
|
126
|
-
bodyStorageKey: string,
|
|
127
|
-
placement: FolderPlacement,
|
|
128
|
-
): Promise<void> => {
|
|
129
|
-
const body = await deps.storageService.retrieve(bodyStorageKey);
|
|
130
|
-
const parsed = await parseMessageBody(body);
|
|
131
|
-
const classification = await classifyParsedMessage(
|
|
132
|
-
deps.addressService,
|
|
133
|
-
accountConfigId,
|
|
134
|
-
parsed,
|
|
135
|
-
placement,
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
// The same denormalized fields the body-store path writes (see
|
|
139
|
-
// `applyPostStoreSteps`), not just the category: the snippet and `List-Id`
|
|
140
|
-
// are derived from the same bytes already in hand, and the rows this pass
|
|
141
|
-
// reaches never got them — the pass that stored the body is the pass that
|
|
142
|
-
// classifies, and it never ran for this cohort.
|
|
143
|
-
await denormalizeMessageCategory(
|
|
144
|
-
{ threadMessageService: deps.threadMessageService },
|
|
145
|
-
accountConfigId,
|
|
146
|
-
row.messageId,
|
|
147
|
-
{
|
|
148
|
-
category: classification.category,
|
|
149
|
-
snippet: extractSnippet(parsed),
|
|
150
|
-
listId: extractListId(parsed),
|
|
151
|
-
},
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
await deps.messageService.update(row.messageId, {
|
|
155
|
-
...classification,
|
|
156
|
-
classificationState: MessageClassificationState.Examined,
|
|
157
|
-
});
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* One-time, resumable pass that classifies the stored-body cohort the
|
|
162
|
-
* classifier has never examined (issue #1197): rows whose bodies were synced
|
|
163
|
-
* before the classifier reached them, which carry the `NotExamined` sentinel
|
|
164
|
-
* on `Message.classificationState` and so stay `uncategorized` in the list —
|
|
165
|
-
* the field is what selects the cohort, and nothing on the sync path selects
|
|
166
|
-
* it. Read-only against the stored raw source: it never opens IMAP, and it
|
|
167
|
-
* has no placement or filter side effects — the same bounds the in-sync
|
|
168
|
-
* backfill #1173 removed had. Index-time moves and filter actions are
|
|
169
|
-
* decisions that already ran (or were declined) when the body first landed;
|
|
170
|
-
* re-running them would move mail the user has since filed by hand.
|
|
171
|
-
*
|
|
172
|
-
* A row is a candidate when its Message carries `NotExamined` — the named
|
|
173
|
-
* pending state, never inferred from `category` or from `bodyStorageKey` —
|
|
174
|
-
* and has a `bodyStorageKey`; a candidate without a stored body has nothing
|
|
175
|
-
* local to read, and the ordinary sync path will classify it once the body
|
|
176
|
-
* lands. A candidate whose category is already decided is marked `Examined`
|
|
177
|
-
* without re-deriving anything: those rows were classified by a pass that
|
|
178
|
-
* predates the field, and RFC 034 Decision 3.1's write-once category must not
|
|
179
|
-
* be recomputed (#355). The classification itself is
|
|
180
|
-
* {@link classifyParsedMessage} — the same rule table, and the same sender
|
|
181
|
-
* override, the body-store path classifies with.
|
|
182
|
-
*
|
|
183
|
-
* Chunked by `listByAccount`'s existing keyset pagination, one account at a
|
|
184
|
-
* time in account-id order, so an interrupted run and its resume walk the
|
|
185
|
-
* same sequence. A failure reading, parsing, or classifying one message is
|
|
186
|
-
* contained to that message — logged, counted, and the pass continues: one
|
|
187
|
-
* unreadable object must not strand the rest of the corpus.
|
|
188
|
-
*/
|
|
189
|
-
export const backfillClassifications = async (
|
|
190
|
-
deps: ClassificationBackfillDeps,
|
|
191
|
-
options: ClassificationBackfillOptions = {},
|
|
192
|
-
): Promise<ClassificationBackfillResult> => {
|
|
193
|
-
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
194
|
-
const { logger, checkpointStore } = options;
|
|
195
|
-
|
|
196
|
-
const accounts = [...(await deps.accountConfigService.listAll())].sort(
|
|
197
|
-
(left, right) => left.accountConfigId.localeCompare(right.accountConfigId),
|
|
198
|
-
);
|
|
199
|
-
const startingCheckpoint = await checkpointStore?.load();
|
|
200
|
-
const checkpointedIndex = startingCheckpoint
|
|
201
|
-
? accounts.findIndex(
|
|
202
|
-
(account) =>
|
|
203
|
-
account.accountConfigId === startingCheckpoint.accountConfigId,
|
|
204
|
-
)
|
|
205
|
-
: -1;
|
|
206
|
-
|
|
207
|
-
if (startingCheckpoint && checkpointedIndex === -1) {
|
|
208
|
-
logger?.info(
|
|
209
|
-
{ accountConfigId: startingCheckpoint.accountConfigId },
|
|
210
|
-
"Classification backfill checkpoint names no configured account; restarting the pass",
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const startIndex = checkpointedIndex === -1 ? 0 : checkpointedIndex;
|
|
215
|
-
const startContinuationToken =
|
|
216
|
-
checkpointedIndex === -1
|
|
217
|
-
? undefined
|
|
218
|
-
: startingCheckpoint?.continuationToken;
|
|
219
|
-
|
|
220
|
-
const totals = emptyTotals();
|
|
221
|
-
const failedThreadMessageIds: string[] = [];
|
|
222
|
-
// Messages settled this run. More than one ThreadMessage row can index one
|
|
223
|
-
// Message, and the candidate fact lives on the Message, so without this a
|
|
224
|
-
// second row would re-read the same body and re-classify it within the
|
|
225
|
-
// same page — idempotent in outcome, wasteful in storage reads.
|
|
226
|
-
const examinedMessageIds = new Set<string>();
|
|
227
|
-
|
|
228
|
-
for (
|
|
229
|
-
let accountIndex = startIndex;
|
|
230
|
-
accountIndex < accounts.length;
|
|
231
|
-
accountIndex++
|
|
232
|
-
) {
|
|
233
|
-
const account = accounts[accountIndex];
|
|
234
|
-
let continuationToken: string | undefined =
|
|
235
|
-
accountIndex === startIndex ? startContinuationToken : undefined;
|
|
236
|
-
const mailAccounts = await deps.accountService.listAllByAccountConfig(
|
|
237
|
-
account.accountConfigId,
|
|
238
|
-
);
|
|
239
|
-
const placements = await resolveAccountFolderPlacements(
|
|
240
|
-
deps.mailboxSpecialUseService,
|
|
241
|
-
mailAccounts.map((mailAccount) => mailAccount.accountId),
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
do {
|
|
245
|
-
const page = await deps.threadMessageService.listByAccount(
|
|
246
|
-
account.accountConfigId,
|
|
247
|
-
{ limit: batchSize, continuationToken },
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
totals.scanned += page.items.length;
|
|
251
|
-
|
|
252
|
-
if (page.items.length > 0) {
|
|
253
|
-
const messages = await deps.messageService.get(
|
|
254
|
-
page.items.map((row) => row.messageId),
|
|
255
|
-
);
|
|
256
|
-
const messageByMessageId = new Map(
|
|
257
|
-
messages.map((message) => [message.messageId, message]),
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
for (const row of page.items) {
|
|
261
|
-
const message = messageByMessageId.get(row.messageId);
|
|
262
|
-
if (!message?.bodyStorageKey) {
|
|
263
|
-
totals.skippedNoBody++;
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (
|
|
268
|
-
message.classificationState !==
|
|
269
|
-
MessageClassificationState.NotExamined ||
|
|
270
|
-
examinedMessageIds.has(row.messageId)
|
|
271
|
-
) {
|
|
272
|
-
totals.alreadyExamined++;
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Classified by a pass that predates the field: record that it
|
|
277
|
-
// ran, and touch nothing else — the write-once category is not
|
|
278
|
-
// recomputed (#355), and the derived fields that pass wrote are
|
|
279
|
-
// already on the row.
|
|
280
|
-
if (hasDecidedCategory(message.category)) {
|
|
281
|
-
const outcome = await deps.messageService
|
|
282
|
-
.update(row.messageId, {
|
|
283
|
-
classificationState: MessageClassificationState.Examined,
|
|
284
|
-
})
|
|
285
|
-
.then(
|
|
286
|
-
() => null,
|
|
287
|
-
(error: unknown) => error,
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
if (outcome !== null) {
|
|
291
|
-
totals.failed++;
|
|
292
|
-
failedThreadMessageIds.push(row.threadMessageId);
|
|
293
|
-
logger?.error?.(
|
|
294
|
-
{
|
|
295
|
-
threadMessageId: row.threadMessageId,
|
|
296
|
-
messageId: row.messageId,
|
|
297
|
-
error:
|
|
298
|
-
outcome instanceof Error
|
|
299
|
-
? outcome.message
|
|
300
|
-
: String(outcome),
|
|
301
|
-
},
|
|
302
|
-
"Classification backfill failed to record an already-categorized message; leaving it for a later pass",
|
|
303
|
-
);
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
examinedMessageIds.add(row.messageId);
|
|
308
|
-
totals.alreadyCategorized++;
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const outcome = await classifyAndApply(
|
|
313
|
-
deps,
|
|
314
|
-
account.accountConfigId,
|
|
315
|
-
row,
|
|
316
|
-
message.bodyStorageKey,
|
|
317
|
-
placements.get(row.mailboxId) ?? "other",
|
|
318
|
-
).then(
|
|
319
|
-
() => null,
|
|
320
|
-
(error: unknown) => error,
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
if (outcome !== null) {
|
|
324
|
-
totals.failed++;
|
|
325
|
-
failedThreadMessageIds.push(row.threadMessageId);
|
|
326
|
-
logger?.error?.(
|
|
327
|
-
{
|
|
328
|
-
threadMessageId: row.threadMessageId,
|
|
329
|
-
messageId: row.messageId,
|
|
330
|
-
error:
|
|
331
|
-
outcome instanceof Error ? outcome.message : String(outcome),
|
|
332
|
-
},
|
|
333
|
-
"Classification backfill failed for a message; leaving it for a later pass",
|
|
334
|
-
);
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
examinedMessageIds.add(row.messageId);
|
|
339
|
-
totals.backfilled++;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
continuationToken = page.continuationToken;
|
|
344
|
-
await checkpointStore?.save({
|
|
345
|
-
accountConfigId: account.accountConfigId,
|
|
346
|
-
continuationToken,
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
logger?.info(
|
|
350
|
-
{ accountConfigId: account.accountConfigId, ...totals },
|
|
351
|
-
"Classification backfill progress",
|
|
352
|
-
);
|
|
353
|
-
options.onProgress?.({
|
|
354
|
-
accountConfigId: account.accountConfigId,
|
|
355
|
-
...totals,
|
|
356
|
-
});
|
|
357
|
-
} while (continuationToken);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
await checkpointStore?.clear();
|
|
361
|
-
|
|
362
|
-
return { ...totals, failedThreadMessageIds };
|
|
363
|
-
};
|