@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,242 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Classification against realistic mail, drawn from the senders reported in
|
|
3
|
-
* issue #45 ("only personal and marketing seem to work").
|
|
4
|
-
*
|
|
5
|
-
* The rule table is easy to satisfy with synthetic one-header fixtures and
|
|
6
|
-
* still wrong on real mail, because real bulk senders set several signals at
|
|
7
|
-
* once. Every case here carries the full header set the sender actually emits,
|
|
8
|
-
* so the test fails when rule ORDER regresses even though each individual rule
|
|
9
|
-
* still works.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import assert from "node:assert/strict";
|
|
13
|
-
import { describe, it } from "node:test";
|
|
14
|
-
import { MessageCategory } from "@remit/domain-enums";
|
|
15
|
-
import { simpleParser } from "mailparser";
|
|
16
|
-
import { classifyByHeaders } from "./classifyByHeaders.js";
|
|
17
|
-
|
|
18
|
-
const classify = async (lines: string[]) =>
|
|
19
|
-
classifyByHeaders(await simpleParser(Buffer.from(lines.join("\r\n"))));
|
|
20
|
-
|
|
21
|
-
describe("classifyByHeaders on realistic mail", () => {
|
|
22
|
-
describe("platform notifications", () => {
|
|
23
|
-
it("classifies an npm publish notification as automated", async () => {
|
|
24
|
-
const category = await classify([
|
|
25
|
-
"From: npm <notifications@npmjs.com>",
|
|
26
|
-
"To: me@example.com",
|
|
27
|
-
"Subject: A new version of left-pad was published",
|
|
28
|
-
"DKIM-Signature: v=1; a=rsa-sha256; d=npmjs.com; s=s1; h=from:to",
|
|
29
|
-
"Content-Type: text/plain",
|
|
30
|
-
"",
|
|
31
|
-
"published",
|
|
32
|
-
]);
|
|
33
|
-
assert.equal(category, MessageCategory.automated);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("classifies an npm mail relayed through SES as automated", async () => {
|
|
37
|
-
const category = await classify([
|
|
38
|
-
"Return-Path: <bounces@amazonses.com>",
|
|
39
|
-
"From: npm <support@npmjs.com>",
|
|
40
|
-
"To: me@example.com",
|
|
41
|
-
"Subject: A new device signed in to your npm account",
|
|
42
|
-
"Feedback-ID: 1.eu-west-1.abc:AmazonSES",
|
|
43
|
-
"DKIM-Signature: v=1; a=rsa-sha256; d=amazonses.com; s=x; h=from:to",
|
|
44
|
-
"Content-Type: text/html",
|
|
45
|
-
"",
|
|
46
|
-
"<p>signed in</p>",
|
|
47
|
-
]);
|
|
48
|
-
assert.equal(category, MessageCategory.automated);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("classifies a no-reply notification with no bulk headers as automated", async () => {
|
|
52
|
-
// The reported npm case: one-to-one machine mail, aligned DKIM, and no
|
|
53
|
-
// List-* or Precedence header at all. It used to reach the `personal`
|
|
54
|
-
// fallback and sit among real correspondence.
|
|
55
|
-
const category = await classify([
|
|
56
|
-
"From: CircleCI <no-reply@circleci.com>",
|
|
57
|
-
"To: me@example.com",
|
|
58
|
-
"Subject: Your build failed",
|
|
59
|
-
"DKIM-Signature: v=1; a=rsa-sha256; d=circleci.com; s=s1; h=from:to",
|
|
60
|
-
"Content-Type: text/plain",
|
|
61
|
-
"",
|
|
62
|
-
"build failed",
|
|
63
|
-
]);
|
|
64
|
-
assert.equal(category, MessageCategory.automated);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("classifies X-Auto-Response-Suppress mail as automated", async () => {
|
|
68
|
-
const category = await classify([
|
|
69
|
-
"From: Helpdesk <ticketing@corp.example>",
|
|
70
|
-
"To: me@example.com",
|
|
71
|
-
"Subject: Ticket 4711 updated",
|
|
72
|
-
"X-Auto-Response-Suppress: All",
|
|
73
|
-
"Content-Type: text/plain",
|
|
74
|
-
"",
|
|
75
|
-
"updated",
|
|
76
|
-
]);
|
|
77
|
-
assert.equal(category, MessageCategory.automated);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe("allow-listed sender domains outrank bulk headers", () => {
|
|
82
|
-
it("classifies a GitHub pull-request notification as transactional", async () => {
|
|
83
|
-
// GitHub sets List-ID, List-Unsubscribe AND Precedence: list. Before the
|
|
84
|
-
// reorder, Precedence matched first and every GitHub mail — security
|
|
85
|
-
// alerts and receipts included — landed in `automated`, contradicting
|
|
86
|
-
// the reason GitHub is on the transactional allow-list at all.
|
|
87
|
-
const category = await classify([
|
|
88
|
-
"From: contributor <notifications@github.com>",
|
|
89
|
-
"To: me@example.com",
|
|
90
|
-
"Subject: Re: [org/repo] Fix the thing (PR #45)",
|
|
91
|
-
"List-ID: org/repo <repo.org.github.com>",
|
|
92
|
-
"List-Unsubscribe: <https://github.com/unsub>",
|
|
93
|
-
"Precedence: list",
|
|
94
|
-
"DKIM-Signature: v=1; a=rsa-sha256; d=github.com; s=pf2014; h=from:to",
|
|
95
|
-
"Content-Type: text/plain",
|
|
96
|
-
"",
|
|
97
|
-
"comment",
|
|
98
|
-
]);
|
|
99
|
-
assert.equal(category, MessageCategory.transactional);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("classifies a GitHub security alert as transactional", async () => {
|
|
103
|
-
const category = await classify([
|
|
104
|
-
"From: GitHub <noreply@github.com>",
|
|
105
|
-
"To: me@example.com",
|
|
106
|
-
"Subject: [org/repo] Dependabot alert",
|
|
107
|
-
"Precedence: bulk",
|
|
108
|
-
"Content-Type: text/plain",
|
|
109
|
-
"",
|
|
110
|
-
"alert",
|
|
111
|
-
]);
|
|
112
|
-
assert.equal(category, MessageCategory.transactional);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("classifies a LinkedIn notification as social", async () => {
|
|
116
|
-
// The reported LinkedIn case. List-Unsubscribe matched before the social
|
|
117
|
-
// allow-list, so LinkedIn mail was filed as generic `marketing` and the
|
|
118
|
-
// Social bucket stayed empty.
|
|
119
|
-
const category = await classify([
|
|
120
|
-
"Return-Path: <s-hbhcfzp@bounce.linkedin.com>",
|
|
121
|
-
"From: LinkedIn <messages-noreply@linkedin.com>",
|
|
122
|
-
"To: me@example.com",
|
|
123
|
-
"Subject: You have a new invitation",
|
|
124
|
-
"List-Unsubscribe: <https://www.linkedin.com/e/unsub>",
|
|
125
|
-
"DKIM-Signature: v=1; a=rsa-sha256; d=linkedin.com; s=proddkim; h=from:to",
|
|
126
|
-
"Content-Type: text/html",
|
|
127
|
-
"",
|
|
128
|
-
"<p>invitation</p>",
|
|
129
|
-
]);
|
|
130
|
-
assert.equal(category, MessageCategory.social);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("classifies a LinkedIn job alert from a subdomain as social", async () => {
|
|
134
|
-
const category = await classify([
|
|
135
|
-
"From: LinkedIn Job Alerts <jobalerts-noreply@e.linkedin.com>",
|
|
136
|
-
"To: me@example.com",
|
|
137
|
-
"Subject: 20 new jobs for you",
|
|
138
|
-
"Precedence: bulk",
|
|
139
|
-
"List-Unsubscribe: <https://www.linkedin.com/e/unsub>",
|
|
140
|
-
"Content-Type: text/html",
|
|
141
|
-
"",
|
|
142
|
-
"<p>jobs</p>",
|
|
143
|
-
]);
|
|
144
|
-
assert.equal(category, MessageCategory.social);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe("bulk mail keeps its intent bucket", () => {
|
|
149
|
-
it("classifies a marketing blast that also sets Precedence: bulk as marketing", async () => {
|
|
150
|
-
// Nearly every marketing platform sets Precedence: bulk. Matching it
|
|
151
|
-
// first swallowed the whole Marketing bucket into `automated`.
|
|
152
|
-
const category = await classify([
|
|
153
|
-
"From: Shop <deals@shop.example>",
|
|
154
|
-
"To: me@example.com",
|
|
155
|
-
"Subject: 50% off everything",
|
|
156
|
-
"Precedence: bulk",
|
|
157
|
-
"List-Unsubscribe: <https://shop.example/unsub>",
|
|
158
|
-
"List-Unsubscribe-Post: List-Unsubscribe=One-Click",
|
|
159
|
-
"Content-Type: text/html",
|
|
160
|
-
"",
|
|
161
|
-
"<p>sale</p>",
|
|
162
|
-
]);
|
|
163
|
-
assert.equal(category, MessageCategory.marketing);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("classifies a newsletter that also sets Precedence: list as newsletter", async () => {
|
|
167
|
-
const category = await classify([
|
|
168
|
-
"From: Some Writer <writer@substack.example>",
|
|
169
|
-
"To: me@example.com",
|
|
170
|
-
"Subject: This week's issue",
|
|
171
|
-
"Precedence: list",
|
|
172
|
-
"List-ID: <someletter.substack.example>",
|
|
173
|
-
"List-Unsubscribe: <https://substack.example/unsub>",
|
|
174
|
-
"Content-Type: text/html",
|
|
175
|
-
"",
|
|
176
|
-
"<p>news</p>",
|
|
177
|
-
]);
|
|
178
|
-
assert.equal(category, MessageCategory.newsletter);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it("classifies a mailing-list post as newsletter, not automated", async () => {
|
|
182
|
-
const category = await classify([
|
|
183
|
-
"From: Contributor <dev@lists.example>",
|
|
184
|
-
"To: dev@lists.example",
|
|
185
|
-
"Subject: [PATCH v2] fix the parser",
|
|
186
|
-
"Precedence: list",
|
|
187
|
-
"List-ID: <dev.lists.example>",
|
|
188
|
-
"List-Unsubscribe: <mailto:dev-unsubscribe@lists.example>",
|
|
189
|
-
"Content-Type: text/plain",
|
|
190
|
-
"",
|
|
191
|
-
"patch",
|
|
192
|
-
]);
|
|
193
|
-
assert.equal(category, MessageCategory.newsletter);
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe("personal mail stays personal", () => {
|
|
198
|
-
it("classifies a person writing from Gmail as personal", async () => {
|
|
199
|
-
const category = await classify([
|
|
200
|
-
"From: Alice <alice@gmail.com>",
|
|
201
|
-
"To: me@example.com",
|
|
202
|
-
"Subject: lunch?",
|
|
203
|
-
"DKIM-Signature: v=1; a=rsa-sha256; d=gmail.com; s=20230601; h=from:to",
|
|
204
|
-
"Content-Type: text/plain",
|
|
205
|
-
"",
|
|
206
|
-
"lunch tomorrow?",
|
|
207
|
-
]);
|
|
208
|
-
assert.equal(category, MessageCategory.personal);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("does not treat a human support mailbox as a machine sender", async () => {
|
|
212
|
-
// `support@` is answered by people. Adding it to the machine local-parts
|
|
213
|
-
// would quietly bury real correspondence in `automated`.
|
|
214
|
-
const category = await classify([
|
|
215
|
-
"From: Acme Support <support@acme.example>",
|
|
216
|
-
"To: me@example.com",
|
|
217
|
-
"Subject: Re: your question",
|
|
218
|
-
"Content-Type: text/plain",
|
|
219
|
-
"",
|
|
220
|
-
"answering your question",
|
|
221
|
-
]);
|
|
222
|
-
assert.equal(category, MessageCategory.personal);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("classifies a calendar invite from a colleague as transactional", async () => {
|
|
226
|
-
const category = await classify([
|
|
227
|
-
"From: Bob <bob@corp.example>",
|
|
228
|
-
"To: me@example.com",
|
|
229
|
-
"Subject: Invitation: standup",
|
|
230
|
-
'Content-Type: multipart/mixed; boundary="b1"',
|
|
231
|
-
"",
|
|
232
|
-
"--b1",
|
|
233
|
-
"Content-Type: text/calendar; method=REQUEST",
|
|
234
|
-
"",
|
|
235
|
-
"BEGIN:VCALENDAR",
|
|
236
|
-
"END:VCALENDAR",
|
|
237
|
-
"--b1--",
|
|
238
|
-
]);
|
|
239
|
-
assert.equal(category, MessageCategory.transactional);
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
});
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { hasMachineHeader, isMachineLocalPart } from "./machineSenders.js";
|
|
4
|
-
|
|
5
|
-
describe("isMachineLocalPart", () => {
|
|
6
|
-
it("matches the no-reply spellings", () => {
|
|
7
|
-
for (const localPart of [
|
|
8
|
-
"noreply",
|
|
9
|
-
"no-reply",
|
|
10
|
-
"no_reply",
|
|
11
|
-
"NoReply",
|
|
12
|
-
"donotreply",
|
|
13
|
-
"do-not-reply",
|
|
14
|
-
]) {
|
|
15
|
-
assert.equal(isMachineLocalPart(localPart), true, localPart);
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("matches no-reply prefixes used by platforms", () => {
|
|
20
|
-
for (const localPart of [
|
|
21
|
-
"noreply-github",
|
|
22
|
-
"no-reply+abc123",
|
|
23
|
-
"messages-noreply",
|
|
24
|
-
]) {
|
|
25
|
-
assert.equal(isMachineLocalPart(localPart), true, localPart);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("matches notification and bounce mailboxes", () => {
|
|
30
|
-
for (const localPart of [
|
|
31
|
-
"notifications",
|
|
32
|
-
"notify",
|
|
33
|
-
"alerts",
|
|
34
|
-
"bounces",
|
|
35
|
-
"mailer-daemon",
|
|
36
|
-
"postmaster",
|
|
37
|
-
]) {
|
|
38
|
-
assert.equal(isMachineLocalPart(localPart), true, localPart);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("does not match mailboxes a person answers", () => {
|
|
43
|
-
// A wrong entry here silently buries real correspondence in `automated`,
|
|
44
|
-
// which is the failure mode this whole change exists to remove.
|
|
45
|
-
for (const localPart of [
|
|
46
|
-
"support",
|
|
47
|
-
"info",
|
|
48
|
-
"contact",
|
|
49
|
-
"hello",
|
|
50
|
-
"sales",
|
|
51
|
-
"alice",
|
|
52
|
-
"team",
|
|
53
|
-
]) {
|
|
54
|
-
assert.equal(isMachineLocalPart(localPart), false, localPart);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("does not read a machine name across two words of a person's name", () => {
|
|
59
|
-
// `bruno.reply` strips to "brunoreply", which contains "noreply" spanning
|
|
60
|
-
// the boundary between the two words. Matching on whole words is what
|
|
61
|
-
// keeps Bruno out of `automated`.
|
|
62
|
-
for (const localPart of [
|
|
63
|
-
"bruno.reply",
|
|
64
|
-
"bruno-reply",
|
|
65
|
-
"bruno_reply",
|
|
66
|
-
"juno.replies",
|
|
67
|
-
"toni.fyi",
|
|
68
|
-
"bruno.reply+newsletter",
|
|
69
|
-
]) {
|
|
70
|
-
assert.equal(isMachineLocalPart(localPart), false, localPart);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("still matches the qualified machine forms", () => {
|
|
75
|
-
for (const localPart of [
|
|
76
|
-
"noreply-github",
|
|
77
|
-
"messages-noreply",
|
|
78
|
-
"jobalerts-noreply",
|
|
79
|
-
"mailer.daemon",
|
|
80
|
-
"do-not-reply",
|
|
81
|
-
"team.notifications",
|
|
82
|
-
]) {
|
|
83
|
-
assert.equal(isMachineLocalPart(localPart), true, localPart);
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
describe("hasMachineHeader", () => {
|
|
89
|
-
it("matches Feedback-ID regardless of case", () => {
|
|
90
|
-
assert.equal(hasMachineHeader(["from", "Feedback-ID"]), true);
|
|
91
|
-
assert.equal(hasMachineHeader(["feedback-id"]), true);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("matches X-Auto-Response-Suppress", () => {
|
|
95
|
-
assert.equal(hasMachineHeader(["x-auto-response-suppress"]), true);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("does not match ordinary headers", () => {
|
|
99
|
-
assert.equal(hasMachineHeader(["from", "to", "subject", "date"]), false);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { DisplayNameCorrespondence } from "@remit/domain-enums";
|
|
4
|
-
import { simpleParser } from "mailparser";
|
|
5
|
-
import {
|
|
6
|
-
classifyDisplayNameCorrespondence,
|
|
7
|
-
extractOffDomainLinkDomains,
|
|
8
|
-
extractSenderMismatch,
|
|
9
|
-
} from "./senderMismatch.js";
|
|
10
|
-
|
|
11
|
-
const parse = async (lines: string[]) =>
|
|
12
|
-
simpleParser(Buffer.from(lines.join("\r\n")));
|
|
13
|
-
|
|
14
|
-
describe("classifyDisplayNameCorrespondence", () => {
|
|
15
|
-
it("corresponds when the name is a label of the From domain", () => {
|
|
16
|
-
assert.equal(
|
|
17
|
-
classifyDisplayNameCorrespondence("GitHub", "notifications.github.com"),
|
|
18
|
-
DisplayNameCorrespondence.Corresponds,
|
|
19
|
-
);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("corresponds when a word of a decorated name is a label of the From domain", () => {
|
|
23
|
-
assert.equal(
|
|
24
|
-
classifyDisplayNameCorrespondence(
|
|
25
|
-
"GitHub Actions",
|
|
26
|
-
"notifications.github.com",
|
|
27
|
-
),
|
|
28
|
-
DisplayNameCorrespondence.Corresponds,
|
|
29
|
-
);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("corresponds across a multi-part public suffix", () => {
|
|
33
|
-
assert.equal(
|
|
34
|
-
classifyDisplayNameCorrespondence("Sainsbury's", "mail.sainsburys.co.uk"),
|
|
35
|
-
DisplayNameCorrespondence.Corresponds,
|
|
36
|
-
);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("is unrelated when the name appears nowhere in the From domain", () => {
|
|
40
|
-
assert.equal(
|
|
41
|
-
classifyDisplayNameCorrespondence(
|
|
42
|
-
"InfoMedics",
|
|
43
|
-
"serviceupdatebank.atlassian.net",
|
|
44
|
-
),
|
|
45
|
-
DisplayNameCorrespondence.Unrelated,
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("is a lookalike when a digit stands in for a letter of the domain", () => {
|
|
50
|
-
assert.equal(
|
|
51
|
-
classifyDisplayNameCorrespondence("InfoMedics", "1nfomedics.nl"),
|
|
52
|
-
DisplayNameCorrespondence.Lookalike,
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("is a lookalike for a short brand one character off", () => {
|
|
57
|
-
assert.equal(
|
|
58
|
-
classifyDisplayNameCorrespondence("PayPal", "paypa1.com"),
|
|
59
|
-
DisplayNameCorrespondence.Lookalike,
|
|
60
|
-
);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("claims nothing when the display name is empty", () => {
|
|
64
|
-
assert.equal(
|
|
65
|
-
classifyDisplayNameCorrespondence("", "example.com"),
|
|
66
|
-
DisplayNameCorrespondence.NoClaim,
|
|
67
|
-
);
|
|
68
|
-
assert.equal(
|
|
69
|
-
classifyDisplayNameCorrespondence(undefined, "example.com"),
|
|
70
|
-
DisplayNameCorrespondence.NoClaim,
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("claims nothing when the display name is an address at the sending domain", () => {
|
|
75
|
-
assert.equal(
|
|
76
|
-
classifyDisplayNameCorrespondence(
|
|
77
|
-
"billing@serviceupdatebank.atlassian.net",
|
|
78
|
-
"serviceupdatebank.atlassian.net",
|
|
79
|
-
),
|
|
80
|
-
DisplayNameCorrespondence.NoClaim,
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("claims nothing when the display name spells the envelope address itself", () => {
|
|
85
|
-
assert.equal(
|
|
86
|
-
classifyDisplayNameCorrespondence("matthijs@ischen.nl", "ischen.nl"),
|
|
87
|
-
DisplayNameCorrespondence.NoClaim,
|
|
88
|
-
);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it("claims nothing when the display name is an address on a sibling subdomain", () => {
|
|
92
|
-
assert.equal(
|
|
93
|
-
classifyDisplayNameCorrespondence(
|
|
94
|
-
"noreply@example.co.uk",
|
|
95
|
-
"mail.example.co.uk",
|
|
96
|
-
),
|
|
97
|
-
DisplayNameCorrespondence.NoClaim,
|
|
98
|
-
);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("is a foreign address when the display name spells an address over another domain", () => {
|
|
102
|
-
assert.equal(
|
|
103
|
-
classifyDisplayNameCorrespondence(
|
|
104
|
-
"matthijs@ischen.nl",
|
|
105
|
-
"secresaludguaviare.gov.co",
|
|
106
|
-
),
|
|
107
|
-
DisplayNameCorrespondence.ForeignAddress,
|
|
108
|
-
);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("is a foreign address when the spelled-out address is decorated with a name", () => {
|
|
112
|
-
assert.equal(
|
|
113
|
-
classifyDisplayNameCorrespondence(
|
|
114
|
-
"Matthijs (matthijs@ischen.nl)",
|
|
115
|
-
"secresaludguaviare.gov.co",
|
|
116
|
-
),
|
|
117
|
-
DisplayNameCorrespondence.ForeignAddress,
|
|
118
|
-
);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("compares an at sign that spells no address as an ordinary name", () => {
|
|
122
|
-
assert.equal(
|
|
123
|
-
classifyDisplayNameCorrespondence(
|
|
124
|
-
"Support @ InfoMedics",
|
|
125
|
-
"infomedics.nl",
|
|
126
|
-
),
|
|
127
|
-
DisplayNameCorrespondence.Corresponds,
|
|
128
|
-
);
|
|
129
|
-
assert.equal(
|
|
130
|
-
classifyDisplayNameCorrespondence(
|
|
131
|
-
"Support @ InfoMedics",
|
|
132
|
-
"serviceupdatebank.atlassian.net",
|
|
133
|
-
),
|
|
134
|
-
DisplayNameCorrespondence.Unrelated,
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("does not read the public suffix as a match for a brand containing it", () => {
|
|
139
|
-
assert.equal(
|
|
140
|
-
classifyDisplayNameCorrespondence("Netflix", "mailer.example.net"),
|
|
141
|
-
DisplayNameCorrespondence.Unrelated,
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Live phishing shape: a short, valuable brand name embedded as a
|
|
146
|
-
// coincidental substring of a longer, attacker-chosen domain. "ing" sits
|
|
147
|
-
// inside "secureingverify" the same way "irs"/"dhl"/"ups"/"kpn" sit inside
|
|
148
|
-
// countless lookalike domains — none of that is the domain naming the
|
|
149
|
-
// brand.
|
|
150
|
-
it("does not match a short brand name that is merely embedded in a longer domain label (ING)", () => {
|
|
151
|
-
assert.equal(
|
|
152
|
-
classifyDisplayNameCorrespondence(
|
|
153
|
-
"ING Fraudedesk",
|
|
154
|
-
"secure-ing-verify.tk",
|
|
155
|
-
),
|
|
156
|
-
DisplayNameCorrespondence.Unrelated,
|
|
157
|
-
);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it("still matches a short brand name against its own real domain", () => {
|
|
161
|
-
assert.equal(
|
|
162
|
-
classifyDisplayNameCorrespondence("ING", "ing.nl"),
|
|
163
|
-
DisplayNameCorrespondence.Corresponds,
|
|
164
|
-
);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe("extractOffDomainLinkDomains", () => {
|
|
169
|
-
it("names only the registrable domains that leave the sender's own", async () => {
|
|
170
|
-
const parsed = await parse([
|
|
171
|
-
"From: Jira <jira@serviceupdatebank.atlassian.net>",
|
|
172
|
-
"To: bob@example.com",
|
|
173
|
-
"Subject: Vordering",
|
|
174
|
-
"Content-Type: text/html",
|
|
175
|
-
"",
|
|
176
|
-
'<a href="https://serviceupdatebank.atlassian.net/browse/X">ticket</a>',
|
|
177
|
-
'<a href="https://betaal-vordering.example.org/pay">betaal nu</a>',
|
|
178
|
-
'<a href="https://cdn.betaal-vordering.example.org/logo.png">logo</a>',
|
|
179
|
-
]);
|
|
180
|
-
assert.deepEqual(
|
|
181
|
-
extractOffDomainLinkDomains(parsed, "serviceupdatebank.atlassian.net"),
|
|
182
|
-
["example.org"],
|
|
183
|
-
);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("compares public-suffix-aware, so a co.uk sibling is not off-domain", async () => {
|
|
187
|
-
const parsed = await parse([
|
|
188
|
-
"From: Shop <shop@mail.example.co.uk>",
|
|
189
|
-
"To: bob@example.com",
|
|
190
|
-
"Subject: order",
|
|
191
|
-
"Content-Type: text/html",
|
|
192
|
-
"",
|
|
193
|
-
'<a href="https://www.example.co.uk/orders">orders</a>',
|
|
194
|
-
]);
|
|
195
|
-
assert.deepEqual(
|
|
196
|
-
extractOffDomainLinkDomains(parsed, "mail.example.co.uk"),
|
|
197
|
-
[],
|
|
198
|
-
);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("ignores mailto, anchors and relative hrefs", async () => {
|
|
202
|
-
const parsed = await parse([
|
|
203
|
-
"From: Shop <shop@example.com>",
|
|
204
|
-
"To: bob@example.com",
|
|
205
|
-
"Subject: order",
|
|
206
|
-
"Content-Type: text/html",
|
|
207
|
-
"",
|
|
208
|
-
'<a href="mailto:help@elsewhere.example">mail us</a>',
|
|
209
|
-
'<a href="#top">top</a>',
|
|
210
|
-
'<a href="/orders">orders</a>',
|
|
211
|
-
'<a href="tel:+31201234567">call</a>',
|
|
212
|
-
]);
|
|
213
|
-
assert.deepEqual(extractOffDomainLinkDomains(parsed, "example.com"), []);
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("reads bare URLs out of a plain-text body", async () => {
|
|
217
|
-
const parsed = await parse([
|
|
218
|
-
"From: Shop <shop@example.com>",
|
|
219
|
-
"To: bob@example.com",
|
|
220
|
-
"Subject: order",
|
|
221
|
-
"",
|
|
222
|
-
"Betaal hier: https://betaal.elsewhere.example/pay?id=1",
|
|
223
|
-
]);
|
|
224
|
-
assert.deepEqual(extractOffDomainLinkDomains(parsed, "example.com"), [
|
|
225
|
-
"elsewhere.example",
|
|
226
|
-
]);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
describe("extractSenderMismatch", () => {
|
|
231
|
-
const infoMedicsPhish = [
|
|
232
|
-
"From: InfoMedics <jira@serviceupdatebank.atlassian.net>",
|
|
233
|
-
"To: bob@example.com",
|
|
234
|
-
"Subject: Vordering",
|
|
235
|
-
"Authentication-Results: mx.example.com; dmarc=pass; spf=pass; dkim=pass",
|
|
236
|
-
"X-HalOne-Spam-Probability: 1",
|
|
237
|
-
"DKIM-Signature: v=1; a=rsa-sha256; d=custmx.one.com; s=sel; b=xxx",
|
|
238
|
-
"Content-Type: text/html",
|
|
239
|
-
"",
|
|
240
|
-
'<a href="https://betaal-vordering.example.org/pay">Betaal uw factuur</a>',
|
|
241
|
-
];
|
|
242
|
-
|
|
243
|
-
it("flags the display name and the links on a spam-classified message", async () => {
|
|
244
|
-
const parsed = await parse(infoMedicsPhish);
|
|
245
|
-
assert.deepEqual(
|
|
246
|
-
extractSenderMismatch(parsed, {
|
|
247
|
-
fromDomain: "serviceupdatebank.atlassian.net",
|
|
248
|
-
providerSpamClassified: true,
|
|
249
|
-
placement: "other",
|
|
250
|
-
bulkSender: false,
|
|
251
|
-
}),
|
|
252
|
-
{
|
|
253
|
-
displayNameCorrespondence: DisplayNameCorrespondence.Unrelated,
|
|
254
|
-
offDomainLinkDomains: ["example.org"],
|
|
255
|
-
},
|
|
256
|
-
);
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("flags a message sitting in Junk that the provider rated clean", async () => {
|
|
260
|
-
const parsed = await parse(infoMedicsPhish);
|
|
261
|
-
assert.deepEqual(
|
|
262
|
-
extractSenderMismatch(parsed, {
|
|
263
|
-
fromDomain: "serviceupdatebank.atlassian.net",
|
|
264
|
-
providerSpamClassified: false,
|
|
265
|
-
placement: "junk",
|
|
266
|
-
bulkSender: false,
|
|
267
|
-
}),
|
|
268
|
-
{
|
|
269
|
-
displayNameCorrespondence: DisplayNameCorrespondence.Unrelated,
|
|
270
|
-
offDomainLinkDomains: ["example.org"],
|
|
271
|
-
},
|
|
272
|
-
);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
it("flags a provider-classified message outside Junk", async () => {
|
|
276
|
-
const parsed = await parse(infoMedicsPhish);
|
|
277
|
-
assert.deepEqual(
|
|
278
|
-
extractSenderMismatch(parsed, {
|
|
279
|
-
fromDomain: "serviceupdatebank.atlassian.net",
|
|
280
|
-
providerSpamClassified: true,
|
|
281
|
-
placement: "inbox",
|
|
282
|
-
bulkSender: false,
|
|
283
|
-
}),
|
|
284
|
-
{
|
|
285
|
-
displayNameCorrespondence: DisplayNameCorrespondence.Unrelated,
|
|
286
|
-
offDomainLinkDomains: ["example.org"],
|
|
287
|
-
},
|
|
288
|
-
);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
for (const placement of ["inbox", "other"] as const) {
|
|
292
|
-
it(`compares nothing in ${placement} when the provider did not call the message spam`, async () => {
|
|
293
|
-
const parsed = await parse(infoMedicsPhish);
|
|
294
|
-
assert.deepEqual(
|
|
295
|
-
extractSenderMismatch(parsed, {
|
|
296
|
-
fromDomain: "serviceupdatebank.atlassian.net",
|
|
297
|
-
providerSpamClassified: false,
|
|
298
|
-
placement,
|
|
299
|
-
bulkSender: false,
|
|
300
|
-
}),
|
|
301
|
-
{},
|
|
302
|
-
);
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
it("flags a display name spelling the recipient's own address over a foreign domain", async () => {
|
|
307
|
-
const parsed = await parse([
|
|
308
|
-
"From: matthijs@ischen.nl <aramirez@secresaludguaviare.gov.co>",
|
|
309
|
-
"To: matthijs@ischen.nl",
|
|
310
|
-
"Subject: Re: factura",
|
|
311
|
-
"X-HalOne-Spam-Probability: 1",
|
|
312
|
-
"",
|
|
313
|
-
"hola",
|
|
314
|
-
]);
|
|
315
|
-
assert.equal(
|
|
316
|
-
extractSenderMismatch(parsed, {
|
|
317
|
-
fromDomain: "secresaludguaviare.gov.co",
|
|
318
|
-
providerSpamClassified: true,
|
|
319
|
-
placement: "other",
|
|
320
|
-
bulkSender: false,
|
|
321
|
-
}).displayNameCorrespondence,
|
|
322
|
-
DisplayNameCorrespondence.ForeignAddress,
|
|
323
|
-
);
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it("leaves the display name uncompared for a bulk sender", async () => {
|
|
327
|
-
const parsed = await parse([
|
|
328
|
-
"From: Dutch Cycling Weekly <bounce-9f2@mailer.esp.example>",
|
|
329
|
-
"To: bob@example.com",
|
|
330
|
-
"Subject: This week in cycling",
|
|
331
|
-
"List-Unsubscribe: <https://mailer.esp.example/u/9f2>",
|
|
332
|
-
"X-HalOne-Spam-Probability: 1",
|
|
333
|
-
"Content-Type: text/html",
|
|
334
|
-
"",
|
|
335
|
-
'<a href="https://dutchcyclingweekly.example.org/issue/12">read</a>',
|
|
336
|
-
]);
|
|
337
|
-
const signals = extractSenderMismatch(parsed, {
|
|
338
|
-
fromDomain: "mailer.esp.example",
|
|
339
|
-
providerSpamClassified: true,
|
|
340
|
-
placement: "other",
|
|
341
|
-
bulkSender: true,
|
|
342
|
-
});
|
|
343
|
-
assert.equal(signals.displayNameCorrespondence, undefined);
|
|
344
|
-
assert.deepEqual(signals.offDomainLinkDomains, ["example.org"]);
|
|
345
|
-
});
|
|
346
|
-
});
|