@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
package/src/quarantine.test.ts
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type {
|
|
4
|
-
IMailboxSpecialUseRepository,
|
|
5
|
-
IQuarantineRepository,
|
|
6
|
-
MessageData,
|
|
7
|
-
MessageItem,
|
|
8
|
-
QuarantineItem,
|
|
9
|
-
QuarantineUpsertInput,
|
|
10
|
-
} from "@remit/data-ports";
|
|
11
|
-
import {
|
|
12
|
-
QuarantinedUids,
|
|
13
|
-
QuarantineService,
|
|
14
|
-
resolveMailboxRole,
|
|
15
|
-
shapeFromMessageData,
|
|
16
|
-
} from "./quarantine.js";
|
|
17
|
-
|
|
18
|
-
const noopLog = { info: () => {}, warn: () => {} };
|
|
19
|
-
|
|
20
|
-
const buildService = (
|
|
21
|
-
specialUse: string[] = [],
|
|
22
|
-
): {
|
|
23
|
-
service: QuarantineService;
|
|
24
|
-
writes: QuarantineUpsertInput[];
|
|
25
|
-
listed: string[];
|
|
26
|
-
} => {
|
|
27
|
-
const writes: QuarantineUpsertInput[] = [];
|
|
28
|
-
const listed: string[] = [];
|
|
29
|
-
const repository = {
|
|
30
|
-
listByAccountConfigId: async (accountConfigId: string) => {
|
|
31
|
-
listed.push(accountConfigId);
|
|
32
|
-
return [] as QuarantineItem[];
|
|
33
|
-
},
|
|
34
|
-
upsert: async (input: QuarantineUpsertInput) => {
|
|
35
|
-
writes.push(input);
|
|
36
|
-
},
|
|
37
|
-
} satisfies IQuarantineRepository;
|
|
38
|
-
|
|
39
|
-
const mailboxSpecialUseService = {
|
|
40
|
-
listByMailboxId: async () => specialUse.map((use) => ({ specialUse: use })),
|
|
41
|
-
} as unknown as IMailboxSpecialUseRepository;
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
service: new QuarantineService(
|
|
45
|
-
repository,
|
|
46
|
-
mailboxSpecialUseService,
|
|
47
|
-
"sha-abc",
|
|
48
|
-
noopLog,
|
|
49
|
-
),
|
|
50
|
-
writes,
|
|
51
|
-
listed,
|
|
52
|
-
};
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const context = {
|
|
56
|
-
accountId: "acct-1",
|
|
57
|
-
accountConfigId: "cfg-1",
|
|
58
|
-
mailboxId: "mbx-1",
|
|
59
|
-
mailboxPath: "INBOX",
|
|
60
|
-
uidValidity: 1_712_000_000,
|
|
61
|
-
attempts: 2,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const failure = {
|
|
65
|
-
stage: "BodyParse" as const,
|
|
66
|
-
code: "UnreadableBody" as const,
|
|
67
|
-
message: "the parser said no",
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
describe("QuarantineService.record", () => {
|
|
71
|
-
it("stamps the worker build, not the client's", async () => {
|
|
72
|
-
const { service, writes } = buildService();
|
|
73
|
-
await service.record(context, 40217, failure);
|
|
74
|
-
assert.equal(writes[0]?.workerVersion, "sha-abc");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("names the message by mailbox, uidValidity and uid", async () => {
|
|
78
|
-
const { service, writes } = buildService();
|
|
79
|
-
await service.record(context, 40217, failure);
|
|
80
|
-
assert.deepEqual(
|
|
81
|
-
{
|
|
82
|
-
mailboxId: writes[0]?.mailboxId,
|
|
83
|
-
uidValidity: writes[0]?.uidValidity,
|
|
84
|
-
uid: writes[0]?.uid,
|
|
85
|
-
},
|
|
86
|
-
{ mailboxId: "mbx-1", uidValidity: 1_712_000_000, uid: 40217 },
|
|
87
|
-
);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("supplies no quarantineId, so the random-id default is unreachable", async () => {
|
|
91
|
-
const { service, writes } = buildService();
|
|
92
|
-
await service.record(context, 40217, failure);
|
|
93
|
-
assert.ok(!("quarantineId" in (writes[0] as object)));
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("omits an absent diagnostic rather than writing a null through", async () => {
|
|
97
|
-
const { service, writes } = buildService();
|
|
98
|
-
await service.record(
|
|
99
|
-
{ ...context, mailboxPath: "Clients/Acme" },
|
|
100
|
-
1,
|
|
101
|
-
failure,
|
|
102
|
-
);
|
|
103
|
-
assert.ok(!("failurePartPath" in (writes[0] as object)));
|
|
104
|
-
assert.ok(!("mailboxRole" in (writes[0] as object)));
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("reads the folder's role off the server's SPECIAL-USE", async () => {
|
|
108
|
-
const { service, writes } = buildService(["Junk"]);
|
|
109
|
-
await service.record({ ...context, mailboxPath: "Spam" }, 1, failure);
|
|
110
|
-
assert.equal(writes[0]?.mailboxRole, "Junk");
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe("resolveMailboxRole", () => {
|
|
115
|
-
it("gives INBOX its role without a SPECIAL-USE flag, which it never has", () => {
|
|
116
|
-
assert.equal(resolveMailboxRole("INBOX", []), "Inbox");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("leaves a plain folder roleless instead of inventing one", () => {
|
|
120
|
-
assert.equal(resolveMailboxRole("Clients/Acme", []), undefined);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("drops a SPECIAL-USE that is not a canonical role", () => {
|
|
124
|
-
assert.equal(resolveMailboxRole("Priority", ["Important"]), undefined);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe("QuarantinedUids", () => {
|
|
129
|
-
const entries = [
|
|
130
|
-
{ mailboxId: "mbx-1", uidValidity: 10, uid: 5 },
|
|
131
|
-
] as QuarantineItem[];
|
|
132
|
-
|
|
133
|
-
it("matches a uid on the same mailbox and UIDVALIDITY", () => {
|
|
134
|
-
assert.equal(new QuarantinedUids(entries).has("mbx-1", 10, 5), true);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("does not match the same uid on a new UIDVALIDITY, which is a different message", () => {
|
|
138
|
-
assert.equal(new QuarantinedUids(entries).has("mbx-1", 11, 5), false);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it("does not match the same uid in another mailbox", () => {
|
|
142
|
-
assert.equal(new QuarantinedUids(entries).has("mbx-2", 10, 5), false);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe("shapeFromMessageData", () => {
|
|
147
|
-
const message = {
|
|
148
|
-
rfc822Size: 2048,
|
|
149
|
-
messageIdHeader: "<xyz@example.com>",
|
|
150
|
-
} as MessageItem;
|
|
151
|
-
|
|
152
|
-
const data = {
|
|
153
|
-
bodyPart: [
|
|
154
|
-
{
|
|
155
|
-
bodyPartId: "bp-0",
|
|
156
|
-
partPath: "0",
|
|
157
|
-
mediaType: "multipart",
|
|
158
|
-
mediaSubtype: "alternative",
|
|
159
|
-
transferEncoding: "7bit",
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
bodyPartId: "bp-1",
|
|
163
|
-
partPath: "1",
|
|
164
|
-
mediaType: "text",
|
|
165
|
-
mediaSubtype: "plain",
|
|
166
|
-
transferEncoding: "quoted-printable",
|
|
167
|
-
},
|
|
168
|
-
],
|
|
169
|
-
bodyPartParameter: [
|
|
170
|
-
{
|
|
171
|
-
bodyPartId: "bp-0",
|
|
172
|
-
parameterName: "charset",
|
|
173
|
-
parameterValue: "iso-8859-1",
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
} as unknown as MessageData;
|
|
177
|
-
|
|
178
|
-
it("rebuilds the tree from the rows metadata sync already wrote", () => {
|
|
179
|
-
assert.deepEqual(shapeFromMessageData(message, data).structure, [
|
|
180
|
-
{ depth: 0, contentType: "multipart/alternative" },
|
|
181
|
-
{ depth: 1, contentType: "text/plain" },
|
|
182
|
-
]);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("takes the root part's declared charset and encoding", () => {
|
|
186
|
-
const shape = shapeFromMessageData(message, data);
|
|
187
|
-
assert.equal(shape.charset, "iso-8859-1");
|
|
188
|
-
assert.equal(shape.transferEncoding, "7bit");
|
|
189
|
-
assert.equal(shape.contentType, "multipart/alternative");
|
|
190
|
-
});
|
|
191
|
-
});
|
package/src/quarantine.ts
DELETED
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IMailboxSpecialUseRepository,
|
|
3
|
-
IQuarantineRepository,
|
|
4
|
-
MessageData,
|
|
5
|
-
MessageItem,
|
|
6
|
-
QuarantineItem,
|
|
7
|
-
QuarantineMimeNodeItem,
|
|
8
|
-
} from "@remit/data-ports";
|
|
9
|
-
import { quarantineMessageIdHash, ROOT_PART_PATH } from "@remit/data-ports/id";
|
|
10
|
-
import { CanonicalMailboxRole } from "@remit/domain-enums";
|
|
11
|
-
|
|
12
|
-
type CanonicalRole = NonNullable<QuarantineItem["mailboxRole"]>;
|
|
13
|
-
type FailureStage = QuarantineItem["failureStage"];
|
|
14
|
-
type FailureCode = QuarantineItem["failureCode"];
|
|
15
|
-
|
|
16
|
-
const CANONICAL_ROLES = new Set<string>(Object.values(CanonicalMailboxRole));
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* The canonical role of the folder a quarantined message arrived in, read from
|
|
20
|
-
* the server's own RFC 6154 SPECIAL-USE declaration — the same source the
|
|
21
|
-
* appointment flow seeds from. A folder the server declares nothing about has
|
|
22
|
-
* no role, which is the normal state for a plain folder and is why the field
|
|
23
|
-
* is optional. `Important` is a SPECIAL-USE with no canonical role and drops
|
|
24
|
-
* out here rather than being invented into one.
|
|
25
|
-
*/
|
|
26
|
-
export const resolveMailboxRole = (
|
|
27
|
-
mailboxPath: string,
|
|
28
|
-
specialUse: readonly string[],
|
|
29
|
-
): CanonicalRole | undefined => {
|
|
30
|
-
if (mailboxPath.toUpperCase() === "INBOX") return CanonicalMailboxRole.Inbox;
|
|
31
|
-
const matched = specialUse.find((use) => CANONICAL_ROLES.has(use));
|
|
32
|
-
return matched as CanonicalRole | undefined;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/** Everything about the quarantine that is the same for every uid in a round. */
|
|
36
|
-
export interface QuarantineContext {
|
|
37
|
-
accountId: string;
|
|
38
|
-
accountConfigId: string;
|
|
39
|
-
mailboxId: string;
|
|
40
|
-
mailboxPath: string;
|
|
41
|
-
uidValidity: number;
|
|
42
|
-
/** Rounds tried before the message was set aside. */
|
|
43
|
-
attempts: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* The repro fingerprint: what the message declared itself to be. Every field
|
|
48
|
-
* is optional-by-absence because a message can fail before its shape was read,
|
|
49
|
-
* and a required column would force the writer to invent a value or drop the
|
|
50
|
-
* record.
|
|
51
|
-
*/
|
|
52
|
-
export interface QuarantineMessageShape {
|
|
53
|
-
contentType?: string;
|
|
54
|
-
transferEncoding?: string;
|
|
55
|
-
charset?: string;
|
|
56
|
-
sizeBytes?: number;
|
|
57
|
-
structure: QuarantineMimeNodeItem[];
|
|
58
|
-
messageIdHash?: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface QuarantineFailure {
|
|
62
|
-
stage: FailureStage;
|
|
63
|
-
code: FailureCode;
|
|
64
|
-
/** Parser error text. Stored, shown on screen, never published. */
|
|
65
|
-
message: string;
|
|
66
|
-
partPath?: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const EMPTY_SHAPE: QuarantineMessageShape = { structure: [] };
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Message shape off the rows metadata sync already wrote. The body path has no
|
|
73
|
-
* FETCH result to read — it streams raw bytes — but the MIME tree it needs was
|
|
74
|
-
* walked into BodyPart rows when the message's metadata was synced, so the
|
|
75
|
-
* fingerprint comes from there rather than from re-reading headers the parser
|
|
76
|
-
* has just refused.
|
|
77
|
-
*/
|
|
78
|
-
export const shapeFromMessageData = (
|
|
79
|
-
message: MessageItem,
|
|
80
|
-
data: MessageData,
|
|
81
|
-
): QuarantineMessageShape => {
|
|
82
|
-
const parts = [...data.bodyPart].sort((a, b) =>
|
|
83
|
-
a.partPath.localeCompare(b.partPath, "en", { numeric: true }),
|
|
84
|
-
);
|
|
85
|
-
const root = parts.find((part) => part.partPath === ROOT_PART_PATH);
|
|
86
|
-
const rootCharset = root
|
|
87
|
-
? data.bodyPartParameter.find(
|
|
88
|
-
(param) =>
|
|
89
|
-
param.bodyPartId === root.bodyPartId &&
|
|
90
|
-
param.parameterName.toLowerCase() === "charset",
|
|
91
|
-
)?.parameterValue
|
|
92
|
-
: undefined;
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
...(root ? { contentType: `${root.mediaType}/${root.mediaSubtype}` } : {}),
|
|
96
|
-
...(root ? { transferEncoding: root.transferEncoding } : {}),
|
|
97
|
-
...(rootCharset ? { charset: rootCharset } : {}),
|
|
98
|
-
...(message.rfc822Size ? { sizeBytes: message.rfc822Size } : {}),
|
|
99
|
-
structure: parts.map((part) => ({
|
|
100
|
-
depth: partPathDepth(part.partPath),
|
|
101
|
-
contentType: `${part.mediaType}/${part.mediaSubtype}`,
|
|
102
|
-
})),
|
|
103
|
-
...hashOf(message.messageIdHeader),
|
|
104
|
-
};
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const partPathDepth = (partPath: string): number =>
|
|
108
|
-
partPath === ROOT_PART_PATH ? 0 : partPath.split(".").length;
|
|
109
|
-
|
|
110
|
-
const hashOf = (
|
|
111
|
-
messageIdHeader: string | undefined,
|
|
112
|
-
): { messageIdHash?: string } => {
|
|
113
|
-
const messageIdHash = quarantineMessageIdHash(messageIdHeader);
|
|
114
|
-
return messageIdHash ? { messageIdHash } : {};
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* The set of messages already set aside, held for the duration of one sync
|
|
119
|
-
* round. A round loads it once and filters against it in memory: the list is
|
|
120
|
-
* small by design — a growing one is a bug being reported, not a page to
|
|
121
|
-
* paginate — and a lookup per message would put a query on the hot path for a
|
|
122
|
-
* state that is almost always empty.
|
|
123
|
-
*/
|
|
124
|
-
export class QuarantinedUids {
|
|
125
|
-
private readonly keys: ReadonlySet<string>;
|
|
126
|
-
|
|
127
|
-
constructor(entries: readonly QuarantineItem[]) {
|
|
128
|
-
this.keys = new Set(
|
|
129
|
-
entries.map((entry) =>
|
|
130
|
-
uidKey(entry.mailboxId, entry.uidValidity, entry.uid),
|
|
131
|
-
),
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
get size(): number {
|
|
136
|
-
return this.keys.size;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
has(mailboxId: string, uidValidity: number, uid: number): boolean {
|
|
140
|
-
return this.keys.has(uidKey(mailboxId, uidValidity, uid));
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const uidKey = (mailboxId: string, uidValidity: number, uid: number): string =>
|
|
145
|
-
`${mailboxId}:${uidValidity}:${uid}`;
|
|
146
|
-
|
|
147
|
-
export interface QuarantineLogger {
|
|
148
|
-
info(obj: Record<string, unknown>, msg: string): void;
|
|
149
|
-
warn(obj: Record<string, unknown>, msg: string): void;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Writes the record a message becomes when the sync path cannot apply it
|
|
154
|
-
* (issue #72), and loads the set a round filters against.
|
|
155
|
-
*
|
|
156
|
-
* Only a message defect reaches this class. Deciding that is the caller's job
|
|
157
|
-
* and it is made at exactly one kind of catch site — one narrow enough that
|
|
158
|
-
* the error can only have come from the message itself. An S3, queue or
|
|
159
|
-
* database failure propagates; writing one here would advance a cursor past
|
|
160
|
-
* mail that is fine and tell the user it was unreadable.
|
|
161
|
-
*/
|
|
162
|
-
export class QuarantineService {
|
|
163
|
-
constructor(
|
|
164
|
-
private readonly repository: IQuarantineRepository,
|
|
165
|
-
private readonly mailboxSpecialUseService: IMailboxSpecialUseRepository,
|
|
166
|
-
private readonly workerVersion: string,
|
|
167
|
-
private readonly log: QuarantineLogger,
|
|
168
|
-
) {}
|
|
169
|
-
|
|
170
|
-
async load(accountConfigId: string): Promise<QuarantinedUids> {
|
|
171
|
-
return new QuarantinedUids(
|
|
172
|
-
await this.repository.listByAccountConfigId(accountConfigId),
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Set a message aside. Resolves only once the row is durable, because the
|
|
178
|
-
* caller's next act is to let a cursor move past this uid — and a cursor
|
|
179
|
-
* that moves past work no record survives is the silent loss this whole
|
|
180
|
-
* feature exists to end.
|
|
181
|
-
*/
|
|
182
|
-
async record(
|
|
183
|
-
context: QuarantineContext,
|
|
184
|
-
uid: number,
|
|
185
|
-
failure: QuarantineFailure,
|
|
186
|
-
shape: QuarantineMessageShape = EMPTY_SHAPE,
|
|
187
|
-
): Promise<void> {
|
|
188
|
-
// Resolved here rather than per round: a round almost never writes a row,
|
|
189
|
-
// so the lookup belongs on the write and not on the hot path.
|
|
190
|
-
const specialUse = await this.mailboxSpecialUseService.listByMailboxId(
|
|
191
|
-
context.mailboxId,
|
|
192
|
-
);
|
|
193
|
-
const mailboxRole = resolveMailboxRole(
|
|
194
|
-
context.mailboxPath,
|
|
195
|
-
specialUse.map((entry) => entry.specialUse),
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
await this.repository.upsert({
|
|
199
|
-
accountConfigId: context.accountConfigId,
|
|
200
|
-
accountId: context.accountId,
|
|
201
|
-
mailboxId: context.mailboxId,
|
|
202
|
-
uidValidity: context.uidValidity,
|
|
203
|
-
uid,
|
|
204
|
-
...(mailboxRole ? { mailboxRole } : {}),
|
|
205
|
-
mailboxPath: context.mailboxPath,
|
|
206
|
-
quarantinedAt: Date.now(),
|
|
207
|
-
attempts: context.attempts,
|
|
208
|
-
failureStage: failure.stage,
|
|
209
|
-
failureCode: failure.code,
|
|
210
|
-
failureMessage: failure.message,
|
|
211
|
-
...(failure.partPath ? { failurePartPath: failure.partPath } : {}),
|
|
212
|
-
workerVersion: this.workerVersion,
|
|
213
|
-
...shape,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
this.log.warn(
|
|
217
|
-
{
|
|
218
|
-
mailboxId: context.mailboxId,
|
|
219
|
-
mailboxPath: context.mailboxPath,
|
|
220
|
-
uid,
|
|
221
|
-
uidValidity: context.uidValidity,
|
|
222
|
-
failureStage: failure.stage,
|
|
223
|
-
failureCode: failure.code,
|
|
224
|
-
},
|
|
225
|
-
"Message quarantined; cursor may advance past it",
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
}
|
package/src/snippet.test.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
extractSnippetFromEmail,
|
|
5
|
-
generateSnippet,
|
|
6
|
-
normalizeSubject,
|
|
7
|
-
removeQuotedContent,
|
|
8
|
-
} from "./snippet.js";
|
|
9
|
-
|
|
10
|
-
describe("removeQuotedContent", () => {
|
|
11
|
-
it("removes content after > quote marker", () => {
|
|
12
|
-
const text = "Hello\nWorld\n> quoted line\nmore quoted";
|
|
13
|
-
assert.strictEqual(removeQuotedContent(text), "Hello\nWorld");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("removes content after 'On ... wrote:' pattern", () => {
|
|
17
|
-
const text = "Hello\nOn Monday, John wrote:\nQuoted content";
|
|
18
|
-
assert.strictEqual(removeQuotedContent(text), "Hello");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("removes content after original message separator", () => {
|
|
22
|
-
const text = "Hello\n--- Original Message ---\nQuoted content";
|
|
23
|
-
assert.strictEqual(removeQuotedContent(text), "Hello");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("removes content after Outlook underline separator", () => {
|
|
27
|
-
const text = "Hello\n___\nQuoted content";
|
|
28
|
-
assert.strictEqual(removeQuotedContent(text), "Hello");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("removes content after Outlook forward header", () => {
|
|
32
|
-
const text = "Hello\nFrom: John Sent: Today To: Jane\nQuoted content";
|
|
33
|
-
assert.strictEqual(removeQuotedContent(text), "Hello");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns full text when no quote markers", () => {
|
|
37
|
-
const text = "Hello\nWorld\nNo quotes here";
|
|
38
|
-
assert.strictEqual(removeQuotedContent(text), text);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe("normalizeSubject", () => {
|
|
43
|
-
describe("prefix removal", () => {
|
|
44
|
-
it("removes Re: prefix", () => {
|
|
45
|
-
const result = normalizeSubject("Re: Meeting tomorrow");
|
|
46
|
-
assert.ok(!result.includes("re"));
|
|
47
|
-
assert.ok(result.includes("meet"));
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("removes Fwd: prefix", () => {
|
|
51
|
-
const result = normalizeSubject("Fwd: Important document");
|
|
52
|
-
assert.ok(!result.includes("fwd"));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("removes numbered Re: prefix", () => {
|
|
56
|
-
const result = normalizeSubject("Re[3]: Meeting tomorrow");
|
|
57
|
-
assert.ok(!result.includes("re"));
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("removes multiple prefixes", () => {
|
|
61
|
-
const result = normalizeSubject("Re: Fwd: Re: Original subject");
|
|
62
|
-
assert.ok(!result.includes("re"));
|
|
63
|
-
assert.ok(!result.includes("fwd"));
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("removes German Aw: prefix", () => {
|
|
67
|
-
const result = normalizeSubject("Aw: Besprechung morgen");
|
|
68
|
-
assert.ok(!result.includes("aw"));
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("removes Portuguese Res: prefix", () => {
|
|
72
|
-
const result = normalizeSubject("Res: Reunião amanhã");
|
|
73
|
-
assert.ok(!result.includes("res"));
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe("language detection", () => {
|
|
78
|
-
it("auto-detects English and uses English stemming", () => {
|
|
79
|
-
const result = normalizeSubject(
|
|
80
|
-
"Meeting tomorrow about running the project quickly",
|
|
81
|
-
);
|
|
82
|
-
// Should stem "running" to "run", "quickly" to "quick"
|
|
83
|
-
assert.ok(result.includes("run") || result.includes("quick"));
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("auto-detects German and uses German processing", () => {
|
|
87
|
-
const result = normalizeSubject(
|
|
88
|
-
"Besprechung über die Verbindung und den Projektstand morgen",
|
|
89
|
-
);
|
|
90
|
-
// Should process with German stemmer
|
|
91
|
-
assert.ok(result.length > 0);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("auto-detects French and uses French processing", () => {
|
|
95
|
-
const result = normalizeSubject(
|
|
96
|
-
"Réunion demain matin pour discuter du projet",
|
|
97
|
-
);
|
|
98
|
-
// Should preserve French accented characters and process
|
|
99
|
-
assert.ok(result.includes("réunion") || result.includes("reunion"));
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("uses provided language override", () => {
|
|
103
|
-
const result = normalizeSubject("Meeting tomorrow", "en");
|
|
104
|
-
assert.ok(result.includes("meet"));
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
describe("normalization", () => {
|
|
109
|
-
it("removes stopwords", () => {
|
|
110
|
-
const result = normalizeSubject("The meeting is about the project");
|
|
111
|
-
// "the" and "is" should be removed
|
|
112
|
-
assert.ok(!result.split(" ").includes("the"));
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("handles Unicode characters", () => {
|
|
116
|
-
const result = normalizeSubject("Réunion à Paris");
|
|
117
|
-
// Should not strip accented characters
|
|
118
|
-
assert.ok(result.includes("réunion") || result.includes("reunion"));
|
|
119
|
-
assert.ok(result.includes("pari"));
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("deduplicates consecutive words", () => {
|
|
123
|
-
const result = normalizeSubject("Meeting meeting meeting update");
|
|
124
|
-
// After stemming, should dedupe
|
|
125
|
-
const words = result.split(" ");
|
|
126
|
-
for (let i = 1; i < words.length; i++) {
|
|
127
|
-
assert.notStrictEqual(
|
|
128
|
-
words[i],
|
|
129
|
-
words[i - 1],
|
|
130
|
-
"Should not have consecutive duplicates",
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe("generateSnippet", () => {
|
|
138
|
-
it("generates snippet from text", () => {
|
|
139
|
-
const text = "Hello, this is a test message.";
|
|
140
|
-
assert.strictEqual(generateSnippet(text), text);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("removes quoted content", () => {
|
|
144
|
-
const text = "Hello\n> quoted line";
|
|
145
|
-
assert.strictEqual(generateSnippet(text), "Hello");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("normalizes whitespace", () => {
|
|
149
|
-
const text = "Hello world\n\nmultiple spaces";
|
|
150
|
-
assert.strictEqual(generateSnippet(text), "Hello world multiple spaces");
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("truncates long text at word boundary", () => {
|
|
154
|
-
const text = "The quick brown fox jumps over the lazy dog repeatedly";
|
|
155
|
-
const result = generateSnippet(text, 30);
|
|
156
|
-
assert.ok(result.length <= 30);
|
|
157
|
-
assert.ok(result.endsWith("..."));
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("truncates single long word", () => {
|
|
161
|
-
const text = "supercalifragilisticexpialidocious";
|
|
162
|
-
const result = generateSnippet(text, 20);
|
|
163
|
-
assert.ok(result.endsWith("..."));
|
|
164
|
-
// When no word boundary found, truncates at maxLength-1 + "..."
|
|
165
|
-
// so result is at most maxLength + 2 (for the extra chars in "...")
|
|
166
|
-
assert.ok(result.length <= 22);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe("extractSnippetFromEmail", () => {
|
|
171
|
-
it("prefers plain text over HTML", () => {
|
|
172
|
-
const result = extractSnippetFromEmail("Plain text", "<p>HTML</p>");
|
|
173
|
-
assert.strictEqual(result, "Plain text");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it("falls back to HTML when no plain text", () => {
|
|
177
|
-
const result = extractSnippetFromEmail(undefined, "<p>HTML content</p>");
|
|
178
|
-
assert.ok(result.includes("HTML content"));
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("strips HTML tags", () => {
|
|
182
|
-
const result = extractSnippetFromEmail(
|
|
183
|
-
undefined,
|
|
184
|
-
"<p><strong>Bold</strong> text</p>",
|
|
185
|
-
);
|
|
186
|
-
assert.ok(!result.includes("<"));
|
|
187
|
-
assert.ok(!result.includes(">"));
|
|
188
|
-
assert.ok(result.includes("Bold"));
|
|
189
|
-
assert.ok(result.includes("text"));
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("removes script and style tags", () => {
|
|
193
|
-
const html = "<style>body{}</style><script>alert(1)</script><p>Content</p>";
|
|
194
|
-
const result = extractSnippetFromEmail(undefined, html);
|
|
195
|
-
assert.ok(!result.includes("body"));
|
|
196
|
-
assert.ok(!result.includes("alert"));
|
|
197
|
-
assert.ok(result.includes("Content"));
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("decodes HTML entities", () => {
|
|
201
|
-
const html = "<p>& < > " </p>";
|
|
202
|
-
const result = extractSnippetFromEmail(undefined, html);
|
|
203
|
-
assert.ok(result.includes("&"));
|
|
204
|
-
assert.ok(result.includes("<"));
|
|
205
|
-
assert.ok(result.includes(">"));
|
|
206
|
-
assert.ok(result.includes('"'));
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("returns empty string when no content", () => {
|
|
210
|
-
const result = extractSnippetFromEmail(undefined, undefined);
|
|
211
|
-
assert.strictEqual(result, "");
|
|
212
|
-
});
|
|
213
|
-
});
|