@remit/mailbox-service 0.0.112 → 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} +16 -72
- package/dist/flag-push-terminal.js +63 -0
- package/dist/flag-push.d.ts +69 -0
- package/dist/flag-push.js +111 -0
- package/dist/flag-queue.d.ts +175 -0
- package/dist/flag-queue.js +291 -0
- package/dist/folder-operations.d.ts +39 -0
- package/dist/folder-operations.js +112 -0
- package/dist/folder-placement.d.ts +13 -0
- package/dist/folder-placement.js +23 -0
- package/dist/heuristics/authenticationResults.d.ts +10 -0
- package/dist/heuristics/authenticationResults.js +187 -0
- package/dist/heuristics/classifyByHeaders.d.ts +82 -0
- package/dist/heuristics/classifyByHeaders.js +365 -0
- package/dist/heuristics/classifyPlacement.d.ts +67 -0
- package/dist/heuristics/classifyPlacement.js +153 -0
- package/dist/heuristics/machineSenders.d.ts +21 -0
- package/{src/heuristics/machineSenders.ts → dist/heuristics/machineSenders.js} +34 -40
- package/dist/heuristics/senderMismatch.d.ts +81 -0
- package/dist/heuristics/senderMismatch.js +192 -0
- package/dist/heuristics/socialDomains.d.ts +14 -0
- package/{src/heuristics/socialDomains.ts → dist/heuristics/socialDomains.js} +9 -9
- package/dist/heuristics/transactionalDomains.d.ts +18 -0
- package/{src/heuristics/transactionalDomains.ts → dist/heuristics/transactionalDomains.js} +8 -8
- package/dist/imapflow-connection.d.ts +377 -0
- package/dist/imapflow-connection.js +1147 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +65 -0
- package/dist/list-id-backfill.d.ts +83 -0
- package/dist/list-id-backfill.js +132 -0
- package/dist/mailbox-cursor-rebuild.d.ts +74 -0
- package/dist/mailbox-cursor-rebuild.js +71 -0
- package/{src/mailbox-cursor.ts → dist/mailbox-cursor.d.ts} +20 -98
- package/dist/mailbox-cursor.js +102 -0
- package/dist/mailbox-intent.d.ts +41 -0
- package/{src/mailbox-intent.ts → dist/mailbox-intent.js} +38 -64
- package/dist/mailbox-management.d.ts +240 -0
- package/dist/mailbox-management.js +576 -0
- package/dist/mailbox-presence.d.ts +52 -0
- package/{src/mailbox-presence.ts → dist/mailbox-presence.js} +19 -30
- package/dist/mailbox-queue.d.ts +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} +13 -68
- package/dist/placement-settled.js +75 -0
- package/dist/quarantine.d.ts +95 -0
- package/dist/quarantine.js +131 -0
- package/dist/refusal.d.ts +31 -0
- package/dist/refusal.js +3 -0
- package/{src/restore-source-placement.ts → dist/restore-source-placement.d.ts} +29 -97
- package/dist/restore-source-placement.js +79 -0
- package/dist/search-text-backfill.d.ts +15 -0
- package/dist/search-text-backfill.js +33 -0
- package/dist/snippet.d.ts +61 -0
- package/dist/snippet.js +129 -0
- package/dist/spam-report.d.ts +86 -0
- package/dist/spam-report.js +241 -0
- package/dist/sync-warning.d.ts +21 -0
- package/dist/sync-warning.js +26 -0
- package/dist/sync-watermarks.d.ts +138 -0
- package/dist/sync-watermarks.js +205 -0
- package/dist/test-helpers/fake-imap.d.ts +151 -0
- package/dist/test-helpers/fake-imap.js +508 -0
- package/dist/test-helpers/folder-roles.d.ts +34 -0
- package/dist/test-helpers/folder-roles.js +47 -0
- package/dist/test-helpers/mime-tree-generator.d.ts +82 -0
- package/dist/test-helpers/mime-tree-generator.js +329 -0
- package/dist/test-helpers/test-store.d.ts +69 -0
- package/dist/test-helpers/test-store.js +193 -0
- package/dist/text/normalizer.d.ts +20 -0
- package/dist/text/normalizer.js +102 -0
- package/dist/thread-message-rows.d.ts +69 -0
- package/dist/thread-message-rows.js +40 -0
- package/dist/thread-root.d.ts +7 -0
- package/dist/thread-root.js +25 -0
- package/dist/types.d.ts +343 -0
- package/dist/types.js +19 -0
- package/package.json +86 -88
- package/src/account-credentials.ts +0 -210
- package/src/attribute-mapper.test.ts +0 -104
- package/src/attribute-mapper.ts +0 -148
- package/src/body-parse.test.ts +0 -55
- package/src/body-part-mapper.property.test.ts +0 -354
- package/src/body-part-mapper.ts +0 -410
- package/src/body-sync-queue.test.ts +0 -64
- package/src/body-sync-queue.ts +0 -131
- package/src/body-sync-terminal.test.ts +0 -148
- package/src/body-sync-terminal.ts +0 -174
- package/src/body-sync.test.ts +0 -925
- package/src/body-sync.ts +0 -1891
- package/src/calendar-parts.ts +0 -41
- package/src/classification-backfill.test.ts +0 -285
- package/src/classification-backfill.ts +0 -363
- package/src/classify-message.test.ts +0 -142
- package/src/classify-message.ts +0 -116
- package/src/connection-factory.ts +0 -102
- package/src/connection-test.ts +0 -208
- package/src/credentials.test.ts +0 -181
- package/src/external-move.test.ts +0 -262
- package/src/external-move.ts +0 -285
- package/src/filter-operations.test.ts +0 -319
- package/src/filter-operations.ts +0 -377
- package/src/filter-update.test.ts +0 -313
- package/src/filters/filter-config.test.ts +0 -87
- package/src/filters/filter-config.ts +0 -34
- package/src/filters/list-id.test.ts +0 -50
- package/src/filters/match.test.ts +0 -392
- package/src/filters/match.ts +0 -225
- package/src/filters/pipeline.test.ts +0 -199
- package/src/filters/pipeline.ts +0 -170
- package/src/flag-push.test.ts +0 -112
- package/src/flag-push.ts +0 -198
- package/src/flag-queue.test.ts +0 -197
- package/src/flag-queue.ts +0 -509
- package/src/folder-operations.test.ts +0 -273
- package/src/folder-operations.ts +0 -217
- package/src/folder-placement.ts +0 -56
- package/src/forwarded-message.test.ts +0 -122
- package/src/heuristics/authenticationResults.test.ts +0 -189
- package/src/heuristics/authenticationResults.ts +0 -212
- package/src/heuristics/classifyByHeaders.test.ts +0 -889
- package/src/heuristics/classifyByHeaders.ts +0 -467
- package/src/heuristics/classifyPlacement.test.ts +0 -366
- package/src/heuristics/classifyPlacement.ts +0 -211
- package/src/heuristics/classifyRealMail.test.ts +0 -242
- package/src/heuristics/machineSenders.test.ts +0 -101
- package/src/heuristics/senderMismatch.test.ts +0 -346
- package/src/heuristics/senderMismatch.ts +0 -259
- package/src/imapflow-connect-retry.test.ts +0 -97
- package/src/imapflow-connection.test.ts +0 -503
- package/src/imapflow-connection.ts +0 -1476
- package/src/index.ts +0 -397
- package/src/list-id-backfill.test.ts +0 -207
- package/src/list-id-backfill.ts +0 -274
- package/src/mailbox-cursor-rebuild.test.ts +0 -171
- package/src/mailbox-cursor-rebuild.ts +0 -146
- package/src/mailbox-cursor.test.ts +0 -141
- package/src/mailbox-delete-intent.test.ts +0 -119
- package/src/mailbox-management.test.ts +0 -243
- package/src/mailbox-management.ts +0 -838
- package/src/mailbox-presence.test.ts +0 -74
- package/src/mailbox-queue.ts +0 -397
- package/src/mailbox-rename-intent.test.ts +0 -220
- package/src/mailbox-rename-subtree.test.ts +0 -287
- package/src/mailbox-sync.test.ts +0 -286
- package/src/mailbox-sync.ts +0 -724
- package/src/message-move.test.ts +0 -726
- package/src/message-move.ts +0 -1518
- package/src/message-parser.ts +0 -26
- package/src/message-presence.test.ts +0 -50
- package/src/message-read.ts +0 -87
- package/src/message-save-retry.test.ts +0 -90
- package/src/message-sync-flags.test.ts +0 -167
- package/src/message-sync.test.ts +0 -762
- package/src/message-sync.ts +0 -2133
- package/src/mime-walker.test.ts +0 -311
- package/src/mime-walker.ts +0 -306
- package/src/organize-job.ts +0 -93
- package/src/outbox-attachment-content.test.ts +0 -153
- package/src/outbox-attachment-content.ts +0 -104
- package/src/outbox-attachment-filename.test.ts +0 -132
- package/src/outbox-attachment-filename.ts +0 -76
- package/src/outbox-attachment.test.ts +0 -626
- package/src/outbox-attachment.ts +0 -536
- package/src/outbox-failed-recovery.test.ts +0 -299
- package/src/outbox-queue.test.ts +0 -315
- package/src/outbox-queue.ts +0 -525
- package/src/ownership.ts +0 -140
- package/src/placement-move-terminal.ts +0 -145
- package/src/placement-move.ts +0 -363
- package/src/placement-settled.test.ts +0 -147
- package/src/placement.test.ts +0 -404
- package/src/quarantine.test.ts +0 -133
- package/src/quarantine.ts +0 -229
- package/src/refusal.ts +0 -29
- package/src/snippet.test.ts +0 -213
- package/src/snippet.ts +0 -152
- package/src/spam-report.test.ts +0 -329
- package/src/spam-report.ts +0 -357
- package/src/sync-warning.test.ts +0 -213
- package/src/sync-warning.ts +0 -56
- package/src/sync-watermarks.test.ts +0 -409
- package/src/sync-watermarks.ts +0 -319
- package/src/system-flag-wire-format.test.ts +0 -77
- package/src/test-helpers/fake-imap.ts +0 -690
- package/src/test-helpers/folder-roles.ts +0 -66
- package/src/test-helpers/mime-tree-generator.ts +0 -461
- package/src/test-helpers/test-queue.ts +0 -56
- package/src/test-helpers/test-store.ts +0 -299
- package/src/text/normalizer.test.ts +0 -223
- package/src/text/normalizer.ts +0 -140
- package/src/thread-message-rows.ts +0 -77
- package/src/thread-root.test.ts +0 -29
- package/src/thread-root.ts +0 -51
- package/src/types/mailparser-augment.d.ts +0 -23
- package/src/types/natural-porter-stemmer.d.ts +0 -50
- package/src/types/stopword.d.ts +0 -69
- package/src/types.ts +0 -371
- package/tsconfig.json +0 -8
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { after, before, describe, it } from "node:test";
|
|
3
|
-
import { DisplayNameCorrespondence } from "@remit/domain-enums";
|
|
4
|
-
import { parseMessageBody } from "./body-parse.js";
|
|
5
|
-
import {
|
|
6
|
-
classifyParsedMessage,
|
|
7
|
-
extractPrimaryFromEmail,
|
|
8
|
-
} from "./classify-message.js";
|
|
9
|
-
import { openStore, type Store } from "./test-helpers/test-store.js";
|
|
10
|
-
|
|
11
|
-
const PLAIN_EML = Buffer.from(
|
|
12
|
-
[
|
|
13
|
-
"From: Alice <alice@example.com>",
|
|
14
|
-
"To: me@example.com",
|
|
15
|
-
"Subject: Hi",
|
|
16
|
-
"Content-Type: text/plain",
|
|
17
|
-
"",
|
|
18
|
-
"hi",
|
|
19
|
-
].join("\r\n"),
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
const LIST_EML = Buffer.from(
|
|
23
|
-
[
|
|
24
|
-
"From: Alice <alice@example.com>",
|
|
25
|
-
"To: me@example.com",
|
|
26
|
-
"Subject: Newsletter",
|
|
27
|
-
"List-Id: <news.example.com>",
|
|
28
|
-
"List-Unsubscribe: <https://example.com/unsub>",
|
|
29
|
-
"Content-Type: text/plain",
|
|
30
|
-
"",
|
|
31
|
-
"this week in news",
|
|
32
|
-
].join("\r\n"),
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const IMPERSONATION_EML = Buffer.from(
|
|
36
|
-
[
|
|
37
|
-
"From: InfoMedics <billing@serviceupdatebank.atlassian.net>",
|
|
38
|
-
"To: me@example.com",
|
|
39
|
-
"Subject: Openstaande factuur",
|
|
40
|
-
"DKIM-Signature: v=1; a=rsa-sha256; d=atlassian.net; s=sel; b=xxx",
|
|
41
|
-
"Content-Type: text/html",
|
|
42
|
-
"",
|
|
43
|
-
'<a href="https://betaal-vordering.example.org/pay">Betaal uw factuur</a>',
|
|
44
|
-
].join("\r\n"),
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
let store: Store;
|
|
48
|
-
before(async () => {
|
|
49
|
-
store = await openStore();
|
|
50
|
-
});
|
|
51
|
-
after(() => store.close());
|
|
52
|
-
|
|
53
|
-
describe("classifyParsedMessage", () => {
|
|
54
|
-
it("answers from the header rule table when the sender has no override", async () => {
|
|
55
|
-
const parsed = await parseMessageBody(LIST_EML);
|
|
56
|
-
|
|
57
|
-
const classification = await classifyParsedMessage(
|
|
58
|
-
store.address,
|
|
59
|
-
"acc-1",
|
|
60
|
-
parsed,
|
|
61
|
-
"inbox",
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
assert.equal(classification.category, "newsletter");
|
|
65
|
-
assert.equal(classification.hasListUnsubscribe, true);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("classifies mail with no From address by headers alone", async () => {
|
|
69
|
-
const parsed = await parseMessageBody(
|
|
70
|
-
Buffer.from(
|
|
71
|
-
[
|
|
72
|
-
"To: me@example.com",
|
|
73
|
-
"Subject: Hi",
|
|
74
|
-
"Content-Type: text/plain",
|
|
75
|
-
"",
|
|
76
|
-
"hi",
|
|
77
|
-
].join("\r\n"),
|
|
78
|
-
),
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
const classification = await classifyParsedMessage(
|
|
82
|
-
store.address,
|
|
83
|
-
"acc-1",
|
|
84
|
-
parsed,
|
|
85
|
-
"inbox",
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
assert.equal(classification.category, "personal");
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe("classifyParsedMessage sender mismatch", () => {
|
|
93
|
-
it("compares the sender of mail in Junk the provider rated clean", async () => {
|
|
94
|
-
const parsed = await parseMessageBody(IMPERSONATION_EML);
|
|
95
|
-
|
|
96
|
-
const classification = await classifyParsedMessage(
|
|
97
|
-
store.address,
|
|
98
|
-
"acc-1",
|
|
99
|
-
parsed,
|
|
100
|
-
"junk",
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
assert.equal(
|
|
104
|
-
classification.authenticity?.displayNameCorrespondence,
|
|
105
|
-
DisplayNameCorrespondence.Unrelated,
|
|
106
|
-
);
|
|
107
|
-
assert.deepEqual(classification.authenticity?.offDomainLinkDomains, [
|
|
108
|
-
"example.org",
|
|
109
|
-
]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("compares nothing for the same mail outside Junk without a provider verdict", async () => {
|
|
113
|
-
const parsed = await parseMessageBody(IMPERSONATION_EML);
|
|
114
|
-
|
|
115
|
-
const classification = await classifyParsedMessage(
|
|
116
|
-
store.address,
|
|
117
|
-
"acc-1",
|
|
118
|
-
parsed,
|
|
119
|
-
"inbox",
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
assert.equal(
|
|
123
|
-
classification.authenticity?.displayNameCorrespondence,
|
|
124
|
-
undefined,
|
|
125
|
-
);
|
|
126
|
-
assert.equal(classification.authenticity?.offDomainLinkDomains, undefined);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe("extractPrimaryFromEmail", () => {
|
|
131
|
-
it("reads the first From address", async () => {
|
|
132
|
-
const parsed = await parseMessageBody(PLAIN_EML);
|
|
133
|
-
assert.equal(extractPrimaryFromEmail(parsed), "alice@example.com");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("answers null when there is no From address to read", async () => {
|
|
137
|
-
const parsed = await parseMessageBody(
|
|
138
|
-
Buffer.from(["To: me@example.com", "", "hi"].join("\r\n")),
|
|
139
|
-
);
|
|
140
|
-
assert.equal(extractPrimaryFromEmail(parsed), null);
|
|
141
|
-
});
|
|
142
|
-
});
|
package/src/classify-message.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IAddressRepository,
|
|
3
|
-
ThreadMessageItem,
|
|
4
|
-
UpdateMessageInput,
|
|
5
|
-
} from "@remit/data-ports";
|
|
6
|
-
import { NotFoundError } from "@remit/data-ports/errors";
|
|
7
|
-
import { deriveAddressId } from "@remit/data-ports/id";
|
|
8
|
-
import type { ParsedMail } from "mailparser";
|
|
9
|
-
import {
|
|
10
|
-
classifyByHeaders,
|
|
11
|
-
extractAuthenticity,
|
|
12
|
-
extractAuthResult,
|
|
13
|
-
extractHasListUnsubscribe,
|
|
14
|
-
extractProviderSpam,
|
|
15
|
-
} from "./heuristics/classifyByHeaders.js";
|
|
16
|
-
import type { FolderPlacement } from "./heuristics/classifyPlacement.js";
|
|
17
|
-
import { extractSenderMismatch } from "./heuristics/senderMismatch.js";
|
|
18
|
-
|
|
19
|
-
type ThreadMessageCategory = ThreadMessageItem["category"];
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The From address the classifier reads, when there is one to read. Shared by
|
|
23
|
-
* every path that classifies or counts a sender (body-sync's classification,
|
|
24
|
-
* inbound counter and placement signals, and the classification backfill) so
|
|
25
|
-
* they all read the same address off the same bytes.
|
|
26
|
-
*/
|
|
27
|
-
export const extractPrimaryFromEmail = (parsed: ParsedMail): string | null => {
|
|
28
|
-
const from = parsed.from;
|
|
29
|
-
if (!from || !from.value || from.value.length === 0) return null;
|
|
30
|
-
const address = from.value[0]?.address;
|
|
31
|
-
if (!address) return null;
|
|
32
|
-
return address.toLowerCase();
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The one-sided half of `Address.flags.category` (issue #299, RFC 039
|
|
37
|
-
* Decision 3): a real value here overrides `classifyByHeaders` outright.
|
|
38
|
-
* Only a genuinely-absent Address is "no override" — any other failure
|
|
39
|
-
* (throttle, infra) propagates, because this feeds the write-once
|
|
40
|
-
* `Message.category` at the moment it is decided, where a masked infra
|
|
41
|
-
* failure would silently classify by headers when the user asked for
|
|
42
|
-
* something else.
|
|
43
|
-
*/
|
|
44
|
-
const resolveCategoryOverride = async (
|
|
45
|
-
addressService: Pick<IAddressRepository, "getAddress">,
|
|
46
|
-
accountConfigId: string,
|
|
47
|
-
fromEmail: string,
|
|
48
|
-
): Promise<ThreadMessageCategory | undefined> => {
|
|
49
|
-
try {
|
|
50
|
-
const addressId = deriveAddressId(accountConfigId, fromEmail);
|
|
51
|
-
const address = await addressService.getAddress(accountConfigId, addressId);
|
|
52
|
-
return address.flags?.category?.value;
|
|
53
|
-
} catch (err) {
|
|
54
|
-
if (!(err instanceof NotFoundError)) throw err;
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Header classification, with the sender's `Address.flags.category` override
|
|
61
|
-
* (issue #299, RFC 039 Decision 3) substituted for the header-derived
|
|
62
|
-
* category when one is set. Returns the subset of the Message update that
|
|
63
|
-
* carries the derived fields; the caller folds it into a single UpdateItem
|
|
64
|
-
* alongside `bodyStorageKey` (body-sync) or `classificationState` (the
|
|
65
|
-
* classification backfill, issue #1197). Optional signals are omitted when
|
|
66
|
-
* absent so we never overwrite an existing value with `undefined`.
|
|
67
|
-
*
|
|
68
|
-
* The override wins outright rather than blending with the heuristic — RFC
|
|
69
|
-
* 039 Decision 3 treats a direct reclassification as final, the same as
|
|
70
|
-
* `flags.blocked`/`vip` already override placement. Every caller gates on
|
|
71
|
-
* `hasDecidedCategory` before this result reaches a write, so a message that
|
|
72
|
-
* already carries a real category is never re-touched regardless of what
|
|
73
|
-
* this returns.
|
|
74
|
-
*/
|
|
75
|
-
export const classifyParsedMessage = async (
|
|
76
|
-
addressService: Pick<IAddressRepository, "getAddress">,
|
|
77
|
-
accountConfigId: string,
|
|
78
|
-
parsed: ParsedMail,
|
|
79
|
-
placement: FolderPlacement,
|
|
80
|
-
): Promise<UpdateMessageInput & { category: ThreadMessageCategory }> => {
|
|
81
|
-
const headerCategory = classifyByHeaders(parsed);
|
|
82
|
-
const authenticity = extractAuthenticity(parsed);
|
|
83
|
-
const authResult = extractAuthResult(parsed);
|
|
84
|
-
const providerSpam = extractProviderSpam(parsed);
|
|
85
|
-
const hasListUnsubscribe = extractHasListUnsubscribe(parsed);
|
|
86
|
-
|
|
87
|
-
// A passing SPF/DKIM/DMARC check proves the sending domain, not the
|
|
88
|
-
// identity the message claims — on a shared-tenant host the verified
|
|
89
|
-
// subdomain belongs to whoever signed up. These two comparisons say
|
|
90
|
-
// whether the claim holds, and run only over mail the provider already
|
|
91
|
-
// called spam or that sits in the Junk folder.
|
|
92
|
-
const senderMismatch =
|
|
93
|
-
authenticity === null
|
|
94
|
-
? {}
|
|
95
|
-
: extractSenderMismatch(parsed, {
|
|
96
|
-
fromDomain: authenticity.fromDomain,
|
|
97
|
-
providerSpamClassified: providerSpam?.classified === true,
|
|
98
|
-
placement,
|
|
99
|
-
bulkSender: hasListUnsubscribe,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const fromEmail = extractPrimaryFromEmail(parsed);
|
|
103
|
-
const categoryOverride = fromEmail
|
|
104
|
-
? await resolveCategoryOverride(addressService, accountConfigId, fromEmail)
|
|
105
|
-
: undefined;
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
category: categoryOverride ?? headerCategory,
|
|
109
|
-
hasListUnsubscribe,
|
|
110
|
-
...(authenticity !== null
|
|
111
|
-
? { authenticity: { ...authenticity, ...senderMismatch } }
|
|
112
|
-
: {}),
|
|
113
|
-
...(authResult !== null ? { authResult } : {}),
|
|
114
|
-
...(providerSpam !== null ? { providerSpam } : {}),
|
|
115
|
-
};
|
|
116
|
-
};
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Connection factory for IMAP implementations
|
|
3
|
-
*
|
|
4
|
-
* Uses ImapFlow for all IMAP connections.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
createImapFlowConnectionFromAccount,
|
|
9
|
-
createImapFlowConnectionWithCredentials,
|
|
10
|
-
ImapFlowConnection,
|
|
11
|
-
} from "./imapflow-connection.js";
|
|
12
|
-
import type {
|
|
13
|
-
IImapConnection,
|
|
14
|
-
ImapConnectionConfig,
|
|
15
|
-
MailCredentials,
|
|
16
|
-
} from "./types.js";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Create an IMAP connection
|
|
20
|
-
*/
|
|
21
|
-
export const createConnection = (
|
|
22
|
-
config: ImapConnectionConfig,
|
|
23
|
-
): IImapConnection => {
|
|
24
|
-
return new ImapFlowConnection(config);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Create an IMAP connection from account data using password credentials.
|
|
29
|
-
*/
|
|
30
|
-
export const createConnectionFromAccount = (
|
|
31
|
-
account: {
|
|
32
|
-
imapHost: string;
|
|
33
|
-
imapPort: number;
|
|
34
|
-
imapTls: boolean;
|
|
35
|
-
username: string;
|
|
36
|
-
},
|
|
37
|
-
password: string,
|
|
38
|
-
): IImapConnection => {
|
|
39
|
-
return createImapFlowConnectionFromAccount(account, password);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Create an IMAP connection from account data using a MailCredentials union.
|
|
44
|
-
* Use this instead of createConnectionFromAccount for OAuth accounts.
|
|
45
|
-
*/
|
|
46
|
-
export const createConnectionWithCredentials = (
|
|
47
|
-
account: {
|
|
48
|
-
imapHost: string;
|
|
49
|
-
imapPort: number;
|
|
50
|
-
imapTls: boolean;
|
|
51
|
-
username: string;
|
|
52
|
-
},
|
|
53
|
-
credentials: MailCredentials,
|
|
54
|
-
): IImapConnection => {
|
|
55
|
-
return createImapFlowConnectionWithCredentials(account, credentials);
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Managed connection factory that caches and reuses a single connection.
|
|
60
|
-
*
|
|
61
|
-
* The factory handles connection lifecycle - callers should not disconnect.
|
|
62
|
-
* Call close() on the factory to disconnect when done.
|
|
63
|
-
*/
|
|
64
|
-
export interface ManagedConnectionFactory {
|
|
65
|
-
/** Get the cached connection (creates on first call) */
|
|
66
|
-
getConnection(): IImapConnection;
|
|
67
|
-
/** Disconnect and cleanup */
|
|
68
|
-
close(): Promise<void>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Create a managed connection factory that caches a single connection.
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* ```typescript
|
|
76
|
-
* const factory = createManagedConnectionFactory(config);
|
|
77
|
-
* const conn = factory.getConnection();
|
|
78
|
-
* await conn.connect();
|
|
79
|
-
* // ... reuse conn via factory.getConnection() ...
|
|
80
|
-
* await factory.close(); // Disconnect when done
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
export const createManagedConnectionFactory = (
|
|
84
|
-
config: ImapConnectionConfig,
|
|
85
|
-
): ManagedConnectionFactory => {
|
|
86
|
-
let connection: ImapFlowConnection | null = null;
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
getConnection: () => {
|
|
90
|
-
if (!connection) {
|
|
91
|
-
connection = new ImapFlowConnection(config);
|
|
92
|
-
}
|
|
93
|
-
return connection;
|
|
94
|
-
},
|
|
95
|
-
close: async () => {
|
|
96
|
-
if (connection) {
|
|
97
|
-
await connection.disconnect();
|
|
98
|
-
connection = null;
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
};
|
package/src/connection-test.ts
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Connection testing utilities for IMAP and SMTP servers
|
|
3
|
-
*
|
|
4
|
-
* Used to validate credentials before saving account configuration.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { createConnection as createNetConnection } from "node:net";
|
|
8
|
-
import { connect as tlsConnect } from "node:tls";
|
|
9
|
-
import { createTransport } from "nodemailer";
|
|
10
|
-
import type { MailCredentials } from "./types.js";
|
|
11
|
-
import { MailConnectionError } from "./types.js";
|
|
12
|
-
|
|
13
|
-
export interface TestResult {
|
|
14
|
-
success: boolean;
|
|
15
|
-
error?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ImapTestConfig {
|
|
19
|
-
host: string;
|
|
20
|
-
port: number;
|
|
21
|
-
secure: boolean;
|
|
22
|
-
startTls?: boolean;
|
|
23
|
-
credentials: MailCredentials;
|
|
24
|
-
user: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface SmtpTestConfig {
|
|
28
|
-
host: string;
|
|
29
|
-
port: number;
|
|
30
|
-
secure: boolean;
|
|
31
|
-
credentials: MailCredentials;
|
|
32
|
-
user: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// TODO(#468): Wire OAuth testConnection once authType/oauthRefreshTokenHash are
|
|
36
|
-
// available on Account. For OAuth accounts, mint a token via MailOAuthService
|
|
37
|
-
// before calling testImapConnection / testSmtpConnection, and map
|
|
38
|
-
// RefreshTokenError(reauth-required) to MailConnectionError("auth", ...).
|
|
39
|
-
|
|
40
|
-
const TEST_TIMEOUT_MS = 12_000;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Build nodemailer auth object from credentials.
|
|
44
|
-
*/
|
|
45
|
-
const buildSmtpAuth = (
|
|
46
|
-
user: string,
|
|
47
|
-
credentials: MailCredentials,
|
|
48
|
-
):
|
|
49
|
-
| { user: string; pass: string }
|
|
50
|
-
| { type: "OAuth2"; user: string; accessToken: string } => {
|
|
51
|
-
if (credentials.kind === "password") {
|
|
52
|
-
return { user, pass: credentials.password };
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
type: "OAuth2" as const,
|
|
56
|
-
user,
|
|
57
|
-
accessToken: credentials.accessToken,
|
|
58
|
-
};
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Test IMAP connection with provided credentials.
|
|
63
|
-
*
|
|
64
|
-
* Uses a raw socket to send AUTH PLAIN directly — bypasses the CAPABILITY
|
|
65
|
-
* round-trip that IMAP clients typically perform first, avoiding deadlocks
|
|
66
|
-
* with servers that pipeline their tagged responses.
|
|
67
|
-
*
|
|
68
|
-
* Supports plaintext, direct TLS (secure: true), and STARTTLS (startTls: true).
|
|
69
|
-
*/
|
|
70
|
-
export const testImapConnection = async (
|
|
71
|
-
config: ImapTestConfig,
|
|
72
|
-
): Promise<TestResult> => {
|
|
73
|
-
if (config.credentials.kind !== "password") {
|
|
74
|
-
return { success: false, error: "OAuth not supported for connection test" };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const { user, credentials } = config;
|
|
78
|
-
const authPlain = Buffer.from(
|
|
79
|
-
`\x00${user}\x00${credentials.password}`,
|
|
80
|
-
).toString("base64");
|
|
81
|
-
|
|
82
|
-
return new Promise((resolve) => {
|
|
83
|
-
let settled = false;
|
|
84
|
-
const done = (result: TestResult) => {
|
|
85
|
-
if (settled) return;
|
|
86
|
-
settled = true;
|
|
87
|
-
clearTimeout(timeoutId);
|
|
88
|
-
resolve(result);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const timeoutId = setTimeout(
|
|
92
|
-
() => done({ success: false, error: "Connection timed out" }),
|
|
93
|
-
TEST_TIMEOUT_MS,
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
let buf = "";
|
|
97
|
-
let phase: "greeting" | "starttls" | "tls-upgrade" | "auth" | "done" =
|
|
98
|
-
"greeting";
|
|
99
|
-
// biome-ignore lint/suspicious/noExplicitAny: socket type varies between net.Socket and tls.TLSSocket
|
|
100
|
-
let sock: any;
|
|
101
|
-
|
|
102
|
-
const onData = (chunk: Buffer) => {
|
|
103
|
-
buf += chunk.toString();
|
|
104
|
-
const lines = buf.split("\r\n");
|
|
105
|
-
buf = lines.pop() ?? "";
|
|
106
|
-
|
|
107
|
-
for (const line of lines) {
|
|
108
|
-
if (phase === "greeting" && /^\* OK/i.test(line)) {
|
|
109
|
-
if (config.startTls) {
|
|
110
|
-
phase = "starttls";
|
|
111
|
-
sock.write("A001 STARTTLS\r\n");
|
|
112
|
-
} else {
|
|
113
|
-
phase = "auth";
|
|
114
|
-
sock.write(`A001 AUTHENTICATE PLAIN ${authPlain}\r\n`);
|
|
115
|
-
}
|
|
116
|
-
} else if (phase === "starttls" && /^A001 OK/i.test(line)) {
|
|
117
|
-
phase = "tls-upgrade";
|
|
118
|
-
// Upgrade socket to TLS in place
|
|
119
|
-
const tlsSock = tlsConnect({
|
|
120
|
-
socket: sock,
|
|
121
|
-
host: config.host,
|
|
122
|
-
rejectUnauthorized: false,
|
|
123
|
-
});
|
|
124
|
-
tlsSock.on("data", onData);
|
|
125
|
-
tlsSock.on("error", onError);
|
|
126
|
-
tlsSock.once("secureConnect", () => {
|
|
127
|
-
phase = "auth";
|
|
128
|
-
sock = tlsSock;
|
|
129
|
-
sock.write(`A002 AUTHENTICATE PLAIN ${authPlain}\r\n`);
|
|
130
|
-
});
|
|
131
|
-
} else if (
|
|
132
|
-
phase === "auth" &&
|
|
133
|
-
(/^A00[12] OK/i.test(line) || /^A001 OK/i.test(line))
|
|
134
|
-
) {
|
|
135
|
-
phase = "done";
|
|
136
|
-
sock.write("A099 LOGOUT\r\n");
|
|
137
|
-
done({ success: true });
|
|
138
|
-
} else if (
|
|
139
|
-
phase === "auth" &&
|
|
140
|
-
(/^A00[12] NO/i.test(line) || /^A00[12] BAD/i.test(line))
|
|
141
|
-
) {
|
|
142
|
-
phase = "done";
|
|
143
|
-
sock.destroy();
|
|
144
|
-
done({ success: false, error: "Authentication failed" });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const onError = (err: Error) => {
|
|
150
|
-
done({ success: false, error: err.message });
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
if (config.secure) {
|
|
154
|
-
sock = tlsConnect({
|
|
155
|
-
host: config.host,
|
|
156
|
-
port: config.port,
|
|
157
|
-
rejectUnauthorized: false,
|
|
158
|
-
});
|
|
159
|
-
} else {
|
|
160
|
-
sock = createNetConnection({ host: config.host, port: config.port });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
sock.on("data", onData);
|
|
164
|
-
sock.on("error", onError);
|
|
165
|
-
});
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Test SMTP connection with provided credentials
|
|
170
|
-
*
|
|
171
|
-
* Uses nodemailer's verify() method to test authentication.
|
|
172
|
-
*/
|
|
173
|
-
export const testSmtpConnection = async (
|
|
174
|
-
config: SmtpTestConfig,
|
|
175
|
-
): Promise<TestResult> => {
|
|
176
|
-
const transport = createTransport({
|
|
177
|
-
host: config.host,
|
|
178
|
-
port: config.port,
|
|
179
|
-
secure: config.secure,
|
|
180
|
-
auth: buildSmtpAuth(config.user, config.credentials),
|
|
181
|
-
connectionTimeout: TEST_TIMEOUT_MS,
|
|
182
|
-
greetingTimeout: TEST_TIMEOUT_MS,
|
|
183
|
-
socketTimeout: TEST_TIMEOUT_MS,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const attempt = transport
|
|
187
|
-
.verify()
|
|
188
|
-
.then(() => ({ success: true as const }))
|
|
189
|
-
.catch((error: unknown) => {
|
|
190
|
-
if (error instanceof MailConnectionError) {
|
|
191
|
-
return { success: false as const, error: error.message };
|
|
192
|
-
}
|
|
193
|
-
return {
|
|
194
|
-
success: false as const,
|
|
195
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
196
|
-
};
|
|
197
|
-
})
|
|
198
|
-
.finally(() => transport.close());
|
|
199
|
-
|
|
200
|
-
const timeout = new Promise<TestResult>((resolve) =>
|
|
201
|
-
setTimeout(() => {
|
|
202
|
-
transport.close();
|
|
203
|
-
resolve({ success: false, error: "Connection timed out" });
|
|
204
|
-
}, TEST_TIMEOUT_MS),
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
return Promise.race([attempt, timeout]);
|
|
208
|
-
};
|