@remit/mailbox-service 0.0.111 → 0.0.113
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} +19 -77
- 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} +39 -63
- 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 +126 -0
- package/dist/mailbox-queue.js +214 -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 +284 -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} +19 -50
- 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 -101
- 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 -73
- package/src/account-credentials.ts +0 -210
- package/src/adversarial-mime.e2e.test.ts +0 -307
- 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 -360
- package/src/body-sync-auto-read-writeonce.test.ts +0 -219
- package/src/body-sync-calendar-suggestion.test.ts +0 -433
- package/src/body-sync-category-denormalize.test.ts +0 -352
- package/src/body-sync-category-override.test.ts +0 -501
- package/src/body-sync-category-writeonce.test.ts +0 -234
- package/src/body-sync-filter.test.ts +0 -237
- package/src/body-sync-message-gone.test.ts +0 -139
- package/src/body-sync-placement-flags.test.ts +0 -347
- package/src/body-sync-placement-writeonce.test.ts +0 -337
- package/src/body-sync-quarantine.test.ts +0 -276
- package/src/body-sync-queue.test.ts +0 -124
- package/src/body-sync-queue.ts +0 -131
- package/src/body-sync-resync.test.ts +0 -605
- package/src/body-sync-terminal.test.ts +0 -310
- package/src/body-sync-terminal.ts +0 -176
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/body-sync.ts +0 -1970
- package/src/calendar-parts.ts +0 -41
- package/src/classification-backfill.test.ts +0 -615
- package/src/classification-backfill.ts +0 -363
- package/src/classify-message.test.ts +0 -205
- 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 -369
- package/src/external-move.ts +0 -272
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/filter-config.test.ts +0 -229
- package/src/filters/filter-config.ts +0 -69
- package/src/filters/list-id.test.ts +0 -50
- package/src/filters/match.test.ts +0 -429
- package/src/filters/match.ts +0 -274
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/filters/pipeline.test.ts +0 -330
- package/src/filters/pipeline.ts +0 -284
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-push.test.ts +0 -408
- package/src/flag-push.ts +0 -198
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/flag-queue.ts +0 -509
- package/src/folder-placement.ts +0 -56
- 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 -134
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- package/src/imapflow-connection.test.ts +0 -469
- package/src/imapflow-connection.ts +0 -1475
- package/src/index.ts +0 -350
- package/src/list-id-backfill.test.ts +0 -573
- 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 -249
- package/src/mailbox-delete-intent.test.ts +0 -246
- package/src/mailbox-management.test.ts +0 -356
- package/src/mailbox-management.ts +0 -839
- package/src/mailbox-presence.test.ts +0 -72
- package/src/mailbox-queue.ts +0 -415
- package/src/mailbox-rename-intent.test.ts +0 -389
- package/src/mailbox-rename-subtree.test.ts +0 -664
- package/src/mailbox-sync.test.ts +0 -848
- package/src/mailbox-sync.ts +0 -724
- package/src/message-delete-junk-reconcile.test.ts +0 -117
- package/src/message-delete-no-trash.test.ts +0 -245
- package/src/message-delete-unsettled-placement.test.ts +0 -396
- package/src/message-move-copy.test.ts +0 -562
- package/src/message-move-empty-trash.test.ts +0 -326
- package/src/message-move-same-mailbox.test.ts +0 -139
- package/src/message-move-unsettled-placement.test.ts +0 -378
- package/src/message-move.ts +0 -1348
- package/src/message-parser.ts +0 -26
- package/src/message-presence.test.ts +0 -50
- package/src/message-sync-address-spoof.test.ts +0 -293
- package/src/message-sync-category.test.ts +0 -142
- package/src/message-sync-changedsince.test.ts +0 -840
- package/src/message-sync-flags.test.ts +0 -158
- package/src/message-sync-junk-harvest.test.ts +0 -244
- package/src/message-sync-quarantine.test.ts +0 -416
- package/src/message-sync-repoint.test.ts +0 -419
- package/src/message-sync.ts +0 -1963
- package/src/mime-walker.test.ts +0 -311
- package/src/mime-walker.ts +0 -305
- 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 -738
- package/src/outbox-attachment.ts +0 -536
- package/src/outbox-failed-recovery.test.ts +0 -370
- package/src/outbox-queue-settled-row.test.ts +0 -140
- package/src/outbox-queue.test.ts +0 -291
- package/src/outbox-queue.ts +0 -508
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-terminal.ts +0 -148
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-move.ts +0 -336
- package/src/placement-settled.test.ts +0 -147
- package/src/placement-transition-race.test.ts +0 -482
- package/src/quarantine.test.ts +0 -191
- package/src/quarantine.ts +0 -228
- package/src/snippet.test.ts +0 -213
- package/src/snippet.ts +0 -152
- package/src/spam-report.test.ts +0 -801
- package/src/spam-report.ts +0 -357
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- 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/folder-roles.ts +0 -66
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- package/src/test-helpers/mime-tree-generator.ts +0 -461
- 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/types/mailparser-augment.d.ts +0 -15
- 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/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
- package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
- package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
- package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
- package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
- package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
- package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
- package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
- package/tsconfig.json +0 -8
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A body the classifier had nothing to say about is examined once (issue #331).
|
|
3
|
-
*
|
|
4
|
-
* `Message.category` is NOT NULL with an `uncategorized` default, so that value
|
|
5
|
-
* means two things at once: a message the classifier has not reached, and one it
|
|
6
|
-
* reached with nothing to say. Keying "needs classifying" on it made every later
|
|
7
|
-
* sync pass re-read the body from storage and rewrite both rows for the same
|
|
8
|
-
* answer — a read amplification that scales with sync frequency and never
|
|
9
|
-
* converges.
|
|
10
|
-
*
|
|
11
|
-
* `Message.classificationState` is the fact itself, written in the same
|
|
12
|
-
* UpdateItem as the answer. It is persistence-only — no response model carries
|
|
13
|
-
* it — and it is what makes the declined cohort addressable later without a
|
|
14
|
-
* sync pass guessing at it.
|
|
15
|
-
*
|
|
16
|
-
* The sender in these tests carries a `flags.category` override of
|
|
17
|
-
* `uncategorized` — the Reclassify dialog's own value for "no category" — which
|
|
18
|
-
* is how a message ends up stored and `uncategorized` at the same time.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import assert from "node:assert/strict";
|
|
22
|
-
import { Readable } from "node:stream";
|
|
23
|
-
import { describe, it } from "node:test";
|
|
24
|
-
import type {
|
|
25
|
-
AddressItem,
|
|
26
|
-
IAddressRepository,
|
|
27
|
-
IEnvelopeRepository,
|
|
28
|
-
IMessageRepository,
|
|
29
|
-
IThreadMessageRepository,
|
|
30
|
-
MessageItem,
|
|
31
|
-
ThreadMessageItem,
|
|
32
|
-
UpdateMessageInput,
|
|
33
|
-
UpdateThreadMessageInput,
|
|
34
|
-
} from "@remit/data-ports";
|
|
35
|
-
import { NotFoundError } from "@remit/data-ports/errors";
|
|
36
|
-
import { MessageCategory } from "@remit/domain-enums";
|
|
37
|
-
import type { StorageService } from "@remit/storage-service";
|
|
38
|
-
import { BodySyncService } from "./body-sync.js";
|
|
39
|
-
import type { IImapConnection } from "./types.js";
|
|
40
|
-
|
|
41
|
-
const LINKEDIN_EML = Buffer.from(
|
|
42
|
-
[
|
|
43
|
-
"From: LinkedIn <messages-noreply@linkedin.com>",
|
|
44
|
-
"To: me@example.com",
|
|
45
|
-
"Subject: You have a new invitation",
|
|
46
|
-
"List-Unsubscribe: <https://www.linkedin.com/e/unsub>",
|
|
47
|
-
"Content-Type: text/plain",
|
|
48
|
-
"",
|
|
49
|
-
"invitation",
|
|
50
|
-
].join("\r\n"),
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
interface Harness {
|
|
54
|
-
service: BodySyncService;
|
|
55
|
-
message: MessageItem;
|
|
56
|
-
rows: ThreadMessageItem[];
|
|
57
|
-
messageUpdates: Array<{ messageId: string; input: UpdateMessageInput }>;
|
|
58
|
-
threadUpdates: UpdateThreadMessageInput[];
|
|
59
|
-
retrieved: string[];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const buildHarness = (
|
|
63
|
-
message: Partial<MessageItem> & Pick<MessageItem, "messageId">,
|
|
64
|
-
overrideCategory?: (typeof MessageCategory)[keyof typeof MessageCategory],
|
|
65
|
-
): Harness => {
|
|
66
|
-
const messageUpdates: Array<{
|
|
67
|
-
messageId: string;
|
|
68
|
-
input: UpdateMessageInput;
|
|
69
|
-
}> = [];
|
|
70
|
-
const threadUpdates: UpdateThreadMessageInput[] = [];
|
|
71
|
-
const retrieved: string[] = [];
|
|
72
|
-
|
|
73
|
-
const messageRow = {
|
|
74
|
-
uid: 1,
|
|
75
|
-
mailboxId: "mb-1",
|
|
76
|
-
category: MessageCategory.uncategorized,
|
|
77
|
-
...message,
|
|
78
|
-
} as unknown as MessageItem;
|
|
79
|
-
|
|
80
|
-
const rows: ThreadMessageItem[] = [
|
|
81
|
-
{
|
|
82
|
-
threadMessageId: "tm-1",
|
|
83
|
-
messageId: message.messageId,
|
|
84
|
-
mailboxId: "mb-1",
|
|
85
|
-
sentDate: 1,
|
|
86
|
-
isRead: false,
|
|
87
|
-
isDeleted: false,
|
|
88
|
-
hasStars: false,
|
|
89
|
-
hasAttachment: false,
|
|
90
|
-
category: messageRow.category,
|
|
91
|
-
} as unknown as ThreadMessageItem,
|
|
92
|
-
];
|
|
93
|
-
|
|
94
|
-
const messageService = {
|
|
95
|
-
get: async () => messageRow,
|
|
96
|
-
update: async (messageId: string, input: UpdateMessageInput) => {
|
|
97
|
-
messageUpdates.push({ messageId, input });
|
|
98
|
-
Object.assign(messageRow, input);
|
|
99
|
-
},
|
|
100
|
-
} as unknown as IMessageRepository;
|
|
101
|
-
|
|
102
|
-
const threadMessageService = {
|
|
103
|
-
findAllByMessageId: async () => rows,
|
|
104
|
-
update: async (
|
|
105
|
-
_accountConfigId: string,
|
|
106
|
-
threadMessageId: string,
|
|
107
|
-
input: UpdateThreadMessageInput,
|
|
108
|
-
) => {
|
|
109
|
-
threadUpdates.push(input);
|
|
110
|
-
const row = rows.find((r) => r.threadMessageId === threadMessageId);
|
|
111
|
-
if (row) Object.assign(row, input);
|
|
112
|
-
},
|
|
113
|
-
} as unknown as IThreadMessageRepository;
|
|
114
|
-
|
|
115
|
-
const storageService = {
|
|
116
|
-
retrieve: async (key: string) => {
|
|
117
|
-
retrieved.push(key);
|
|
118
|
-
return LINKEDIN_EML;
|
|
119
|
-
},
|
|
120
|
-
storeMessageBody: async () => ({ uri: `s3://bodies/${message.messageId}` }),
|
|
121
|
-
storeMessageBodyStream: async () => ({
|
|
122
|
-
uri: `s3://bodies/${message.messageId}`,
|
|
123
|
-
}),
|
|
124
|
-
storeParsedBody: async () => {},
|
|
125
|
-
listBodyParts: async () => [],
|
|
126
|
-
} as unknown as StorageService;
|
|
127
|
-
|
|
128
|
-
const addressService = {
|
|
129
|
-
getAddress: async () => {
|
|
130
|
-
if (overrideCategory === undefined) {
|
|
131
|
-
throw new NotFoundError("Address not found");
|
|
132
|
-
}
|
|
133
|
-
return {
|
|
134
|
-
flags: { category: { value: overrideCategory, setAt: 1_000 } },
|
|
135
|
-
} as unknown as AddressItem;
|
|
136
|
-
},
|
|
137
|
-
incrementInboundCount: async () => {},
|
|
138
|
-
} as unknown as IAddressRepository;
|
|
139
|
-
|
|
140
|
-
const service = new BodySyncService(
|
|
141
|
-
messageService,
|
|
142
|
-
storageService,
|
|
143
|
-
threadMessageService,
|
|
144
|
-
addressService,
|
|
145
|
-
{ listBodyParts: async () => [] } as unknown as IEnvelopeRepository,
|
|
146
|
-
{ info: () => {}, error: () => {} },
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
service,
|
|
151
|
-
message: messageRow,
|
|
152
|
-
rows,
|
|
153
|
-
messageUpdates,
|
|
154
|
-
threadUpdates,
|
|
155
|
-
retrieved,
|
|
156
|
-
};
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const imapConnection = () =>
|
|
160
|
-
({
|
|
161
|
-
openBox: async () => {},
|
|
162
|
-
async *fetchMessageBodies(uids: number[]) {
|
|
163
|
-
for (const uid of uids) {
|
|
164
|
-
yield { uid, source: Readable.from([LINKEDIN_EML]) };
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
}) as unknown as IImapConnection;
|
|
168
|
-
|
|
169
|
-
const syncPass = (harness: Harness) =>
|
|
170
|
-
harness.service.syncBodies(
|
|
171
|
-
["m-1"],
|
|
172
|
-
"acc-1",
|
|
173
|
-
"cfg-1",
|
|
174
|
-
{ fullPath: "INBOX", placement: "inbox" },
|
|
175
|
-
async () => imapConnection(),
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
describe("a body the classifier declined to categorize is examined once", () => {
|
|
179
|
-
it("does not read or rewrite the stored body on the pass after the one that stored it", async () => {
|
|
180
|
-
const harness = buildHarness(
|
|
181
|
-
{ messageId: "m-1" },
|
|
182
|
-
MessageCategory.uncategorized,
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
const first = await syncPass(harness);
|
|
186
|
-
|
|
187
|
-
assert.deepEqual(first.syncedMessageIds, ["m-1"]);
|
|
188
|
-
assert.equal(harness.message.category, MessageCategory.uncategorized);
|
|
189
|
-
assert.equal(harness.message.bodyStorageKey, "s3://bodies/m-1");
|
|
190
|
-
assert.equal(harness.messageUpdates.length, 1);
|
|
191
|
-
// The answer and the record that it was given, in one write.
|
|
192
|
-
assert.equal(
|
|
193
|
-
harness.messageUpdates[0].input.classificationState,
|
|
194
|
-
"Examined",
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
const second = await syncPass(harness);
|
|
198
|
-
|
|
199
|
-
assert.equal(second.skippedCount, 1);
|
|
200
|
-
assert.deepEqual(second.failedMessageIds, []);
|
|
201
|
-
assert.deepEqual(harness.retrieved, []);
|
|
202
|
-
assert.equal(harness.messageUpdates.length, 1);
|
|
203
|
-
assert.equal(harness.threadUpdates.length, 1);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("leaves a message that is already stored and examined alone", async () => {
|
|
207
|
-
const harness = buildHarness(
|
|
208
|
-
{
|
|
209
|
-
messageId: "m-1",
|
|
210
|
-
bodyStorageKey: "s3://bodies/m-1",
|
|
211
|
-
category: MessageCategory.uncategorized,
|
|
212
|
-
classificationState: "Examined",
|
|
213
|
-
} as Partial<MessageItem> & Pick<MessageItem, "messageId">,
|
|
214
|
-
MessageCategory.uncategorized,
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
const result = await syncPass(harness);
|
|
218
|
-
|
|
219
|
-
assert.equal(result.skippedCount, 1);
|
|
220
|
-
assert.deepEqual(harness.retrieved, []);
|
|
221
|
-
assert.deepEqual(harness.messageUpdates, []);
|
|
222
|
-
assert.deepEqual(harness.threadUpdates, []);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("leaves mail stored before the classifier ran marked as never examined", async () => {
|
|
226
|
-
const harness = buildHarness(
|
|
227
|
-
{
|
|
228
|
-
messageId: "m-1",
|
|
229
|
-
bodyStorageKey: "s3://bodies/m-1",
|
|
230
|
-
category: MessageCategory.uncategorized,
|
|
231
|
-
classificationState: "NotExamined",
|
|
232
|
-
} as Partial<MessageItem> & Pick<MessageItem, "messageId">,
|
|
233
|
-
MessageCategory.uncategorized,
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
const result = await syncPass(harness);
|
|
237
|
-
|
|
238
|
-
// A sync pass does not re-derive this cohort — that is the amplification
|
|
239
|
-
// #331 removes. It leaves the row saying so, which is what lets a
|
|
240
|
-
// deliberate backfill select it later.
|
|
241
|
-
assert.equal(result.skippedCount, 1);
|
|
242
|
-
assert.deepEqual(harness.retrieved, []);
|
|
243
|
-
assert.deepEqual(harness.messageUpdates, []);
|
|
244
|
-
assert.equal(harness.message.classificationState, "NotExamined");
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("still fetches and classifies a message whose body has never been stored", async () => {
|
|
248
|
-
const harness = buildHarness({ messageId: "m-1" });
|
|
249
|
-
|
|
250
|
-
const result = await syncPass(harness);
|
|
251
|
-
|
|
252
|
-
assert.deepEqual(result.syncedMessageIds, ["m-1"]);
|
|
253
|
-
assert.equal(harness.messageUpdates.length, 1);
|
|
254
|
-
assert.equal(
|
|
255
|
-
harness.messageUpdates[0].input.category,
|
|
256
|
-
MessageCategory.social,
|
|
257
|
-
);
|
|
258
|
-
assert.equal(harness.rows[0].category, MessageCategory.social);
|
|
259
|
-
});
|
|
260
|
-
});
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issue #302 (RFC 039 Decision 3): `Address.flags.unsubscribed` is
|
|
3
|
-
* documented as "auto-mark-read until sender stops" and settable from
|
|
4
|
-
* `IntelligencePane.tsx`, but nothing consumed it — every message from an
|
|
5
|
-
* unsubscribed sender still arrived unread like any other. These tests drive
|
|
6
|
-
* `BodySyncService` end-to-end (read-path body materialization →
|
|
7
|
-
* `applyPostStoreSteps`) and assert on the actual `FlagQueueService.markAsRead`
|
|
8
|
-
* call, so a regression that drops the flag read — not just a helper in
|
|
9
|
-
* isolation — shows up here.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { describe, it } from "node:test";
|
|
14
|
-
import type {
|
|
15
|
-
AddressItem,
|
|
16
|
-
IAddressRepository,
|
|
17
|
-
IEnvelopeRepository,
|
|
18
|
-
IMessageRepository,
|
|
19
|
-
IThreadMessageRepository,
|
|
20
|
-
} from "@remit/data-ports";
|
|
21
|
-
import type { StorageService } from "@remit/storage-service";
|
|
22
|
-
import { BodySyncService } from "./body-sync.js";
|
|
23
|
-
import type { FlagQueueService } from "./flag-queue.js";
|
|
24
|
-
import type { IImapConnection } from "./types.js";
|
|
25
|
-
|
|
26
|
-
const PLAIN_EML = (fromEmail: string) =>
|
|
27
|
-
Buffer.from(
|
|
28
|
-
[
|
|
29
|
-
`From: Sender <${fromEmail}>`,
|
|
30
|
-
"To: me@example.com",
|
|
31
|
-
"Subject: Hello",
|
|
32
|
-
"Content-Type: text/plain",
|
|
33
|
-
"",
|
|
34
|
-
"body",
|
|
35
|
-
].join("\r\n"),
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
interface MarkReadCall {
|
|
39
|
-
accountConfigId: string;
|
|
40
|
-
messageId: string;
|
|
41
|
-
accountId: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface Harness {
|
|
45
|
-
service: BodySyncService;
|
|
46
|
-
markReadCalls: MarkReadCall[];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const buildHarness = (
|
|
50
|
-
flags: AddressItem["flags"],
|
|
51
|
-
withUnsubscribeConfig = true,
|
|
52
|
-
): Harness => {
|
|
53
|
-
const markReadCalls: MarkReadCall[] = [];
|
|
54
|
-
|
|
55
|
-
const messageService = {
|
|
56
|
-
get: async () => ({
|
|
57
|
-
messageId: "m-1",
|
|
58
|
-
mailboxId: "mb-inbox",
|
|
59
|
-
uid: 1,
|
|
60
|
-
}),
|
|
61
|
-
update: async () => {},
|
|
62
|
-
} as unknown as IMessageRepository;
|
|
63
|
-
|
|
64
|
-
const threadMessageService = {
|
|
65
|
-
findAllByMessageId: async () => [
|
|
66
|
-
{
|
|
67
|
-
threadMessageId: "tm-1",
|
|
68
|
-
sentDate: 1,
|
|
69
|
-
mailboxId: "mb-inbox",
|
|
70
|
-
isRead: false,
|
|
71
|
-
isDeleted: false,
|
|
72
|
-
hasStars: false,
|
|
73
|
-
hasAttachment: false,
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
update: async () => {},
|
|
77
|
-
} as unknown as IThreadMessageRepository;
|
|
78
|
-
|
|
79
|
-
const storageService = {
|
|
80
|
-
storeMessageBody: async () => ({ uri: "s3://bodies/m-1" }),
|
|
81
|
-
storeParsedBody: async () => {},
|
|
82
|
-
listBodyParts: async () => [],
|
|
83
|
-
} as unknown as StorageService;
|
|
84
|
-
|
|
85
|
-
const addressService = {
|
|
86
|
-
getAddress: async () => ({ flags }) as unknown as AddressItem,
|
|
87
|
-
incrementInboundCount: async () => {},
|
|
88
|
-
} as unknown as IAddressRepository;
|
|
89
|
-
|
|
90
|
-
const envelopeService = {
|
|
91
|
-
listBodyParts: async () => [],
|
|
92
|
-
} as unknown as IEnvelopeRepository;
|
|
93
|
-
|
|
94
|
-
const flagQueueService = {
|
|
95
|
-
markAsRead: async (
|
|
96
|
-
accountConfigId: string,
|
|
97
|
-
messageId: string,
|
|
98
|
-
accountId: string,
|
|
99
|
-
) => {
|
|
100
|
-
markReadCalls.push({ accountConfigId, messageId, accountId });
|
|
101
|
-
},
|
|
102
|
-
} as unknown as FlagQueueService;
|
|
103
|
-
|
|
104
|
-
const service = new BodySyncService(
|
|
105
|
-
messageService,
|
|
106
|
-
storageService,
|
|
107
|
-
threadMessageService,
|
|
108
|
-
addressService,
|
|
109
|
-
envelopeService,
|
|
110
|
-
{ info: () => {}, error: () => {} },
|
|
111
|
-
undefined,
|
|
112
|
-
undefined,
|
|
113
|
-
undefined,
|
|
114
|
-
withUnsubscribeConfig ? { flagQueueService } : undefined,
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
return { service, markReadCalls };
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const readBody = async (
|
|
121
|
-
service: BodySyncService,
|
|
122
|
-
fromEmail = "someone@example.com",
|
|
123
|
-
) => {
|
|
124
|
-
const connection = {
|
|
125
|
-
openBox: async () => {},
|
|
126
|
-
fetchMessageBody: async () => PLAIN_EML(fromEmail),
|
|
127
|
-
} as unknown as IImapConnection;
|
|
128
|
-
return service.fetchAndGetBody(
|
|
129
|
-
"m-1",
|
|
130
|
-
"acc-1",
|
|
131
|
-
"cfg-1",
|
|
132
|
-
{ fullPath: "INBOX", placement: "inbox" },
|
|
133
|
-
async () => connection,
|
|
134
|
-
);
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
describe("Address.flags.unsubscribed drives auto-mark-read (issue #302)", () => {
|
|
138
|
-
it("marks a message from an unsubscribed sender as read at sync time", async () => {
|
|
139
|
-
const harness = buildHarness({ unsubscribed: { value: true, setAt: 1 } });
|
|
140
|
-
|
|
141
|
-
await readBody(harness.service);
|
|
142
|
-
|
|
143
|
-
assert.deepEqual(harness.markReadCalls, [
|
|
144
|
-
{ accountConfigId: "cfg-1", messageId: "m-1", accountId: "acc-1" },
|
|
145
|
-
]);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("leaves read state alone for a sender without the flag", async () => {
|
|
149
|
-
const harness = buildHarness({});
|
|
150
|
-
|
|
151
|
-
await readBody(harness.service);
|
|
152
|
-
|
|
153
|
-
assert.deepEqual(harness.markReadCalls, []);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("leaves read state alone once the flag is unset (no caching of the decision)", async () => {
|
|
157
|
-
const harness = buildHarness({
|
|
158
|
-
unsubscribed: { value: false, setAt: 1 },
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
await readBody(harness.service);
|
|
162
|
-
|
|
163
|
-
assert.deepEqual(harness.markReadCalls, []);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("is a no-op when body sync was built without an UnsubscribeConfig", async () => {
|
|
167
|
-
const harness = buildHarness(
|
|
168
|
-
{ unsubscribed: { value: true, setAt: 1 } },
|
|
169
|
-
false,
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
await readBody(harness.service);
|
|
173
|
-
|
|
174
|
-
assert.deepEqual(harness.markReadCalls, []);
|
|
175
|
-
});
|
|
176
|
-
});
|