@cogitator-ai/channels 0.16.0 → 0.18.0
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/README.md +78 -14
- package/dist/approvals.d.ts +3 -0
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +1 -0
- package/dist/approvals.js.map +1 -1
- package/dist/assistant-config.d.ts +60 -0
- package/dist/assistant-config.d.ts.map +1 -1
- package/dist/assistant-config.js +102 -0
- package/dist/assistant-config.js.map +1 -1
- package/dist/channels/email/address.d.ts +44 -0
- package/dist/channels/email/address.d.ts.map +1 -0
- package/dist/channels/email/address.js +211 -0
- package/dist/channels/email/address.js.map +1 -0
- package/dist/channels/email/auth.d.ts +59 -0
- package/dist/channels/email/auth.d.ts.map +1 -0
- package/dist/channels/email/auth.js +212 -0
- package/dist/channels/email/auth.js.map +1 -0
- package/dist/channels/email/channel.d.ts +256 -0
- package/dist/channels/email/channel.d.ts.map +1 -0
- package/dist/channels/email/channel.js +701 -0
- package/dist/channels/email/channel.js.map +1 -0
- package/dist/channels/email/errors.d.ts +11 -0
- package/dist/channels/email/errors.d.ts.map +1 -0
- package/dist/channels/email/errors.js +13 -0
- package/dist/channels/email/errors.js.map +1 -0
- package/dist/channels/email/imap.d.ts +125 -0
- package/dist/channels/email/imap.d.ts.map +1 -0
- package/dist/channels/email/imap.js +309 -0
- package/dist/channels/email/imap.js.map +1 -0
- package/dist/channels/email/index.d.ts +7 -0
- package/dist/channels/email/index.d.ts.map +1 -0
- package/dist/channels/email/index.js +3 -0
- package/dist/channels/email/index.js.map +1 -0
- package/dist/channels/email/mime.d.ts +71 -0
- package/dist/channels/email/mime.d.ts.map +1 -0
- package/dist/channels/email/mime.js +83 -0
- package/dist/channels/email/mime.js.map +1 -0
- package/dist/channels/email/multipart.d.ts +28 -0
- package/dist/channels/email/multipart.d.ts.map +1 -0
- package/dist/channels/email/multipart.js +102 -0
- package/dist/channels/email/multipart.js.map +1 -0
- package/dist/channels/email/providers.d.ts +49 -0
- package/dist/channels/email/providers.d.ts.map +1 -0
- package/dist/channels/email/providers.js +292 -0
- package/dist/channels/email/providers.js.map +1 -0
- package/dist/channels/email/reply.d.ts +9 -0
- package/dist/channels/email/reply.d.ts.map +1 -0
- package/dist/channels/email/reply.js +122 -0
- package/dist/channels/email/reply.js.map +1 -0
- package/dist/channels/teams/activity.d.ts +108 -0
- package/dist/channels/teams/activity.d.ts.map +1 -0
- package/dist/channels/teams/activity.js +206 -0
- package/dist/channels/teams/activity.js.map +1 -0
- package/dist/channels/teams/auth.d.ts +65 -0
- package/dist/channels/teams/auth.d.ts.map +1 -0
- package/dist/channels/teams/auth.js +213 -0
- package/dist/channels/teams/auth.js.map +1 -0
- package/dist/channels/teams/cards.d.ts +23 -0
- package/dist/channels/teams/cards.d.ts.map +1 -0
- package/dist/channels/teams/cards.js +97 -0
- package/dist/channels/teams/cards.js.map +1 -0
- package/dist/channels/teams/channel.d.ts +303 -0
- package/dist/channels/teams/channel.d.ts.map +1 -0
- package/dist/channels/teams/channel.js +1071 -0
- package/dist/channels/teams/channel.js.map +1 -0
- package/dist/channels/teams/client.d.ts +72 -0
- package/dist/channels/teams/client.d.ts.map +1 -0
- package/dist/channels/teams/client.js +161 -0
- package/dist/channels/teams/client.js.map +1 -0
- package/dist/channels/teams/index.d.ts +7 -0
- package/dist/channels/teams/index.d.ts.map +1 -0
- package/dist/channels/teams/index.js +6 -0
- package/dist/channels/teams/index.js.map +1 -0
- package/dist/channels/teams/pacer.d.ts +47 -0
- package/dist/channels/teams/pacer.d.ts.map +1 -0
- package/dist/channels/teams/pacer.js +174 -0
- package/dist/channels/teams/pacer.js.map +1 -0
- package/dist/channels/teams/token.d.ts +47 -0
- package/dist/channels/teams/token.d.ts.map +1 -0
- package/dist/channels/teams/token.js +107 -0
- package/dist/channels/teams/token.js.map +1 -0
- package/dist/channels/telegram.d.ts.map +1 -1
- package/dist/channels/telegram.js +3 -0
- package/dist/channels/telegram.js.map +1 -1
- package/dist/formatters/html.d.ts +16 -0
- package/dist/formatters/html.d.ts.map +1 -0
- package/dist/formatters/html.js +535 -0
- package/dist/formatters/html.js.map +1 -0
- package/dist/formatters/markdown-images.d.ts +8 -0
- package/dist/formatters/markdown-images.d.ts.map +1 -0
- package/dist/formatters/markdown-images.js +181 -0
- package/dist/formatters/markdown-images.js.map +1 -0
- package/dist/formatters/markdown.d.ts.map +1 -1
- package/dist/formatters/markdown.js +22 -3
- package/dist/formatters/markdown.js.map +1 -1
- package/dist/gateway.d.ts +23 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +113 -16
- package/dist/gateway.js.map +1 -1
- package/dist/inbound-debounce.d.ts +4 -1
- package/dist/inbound-debounce.d.ts.map +1 -1
- package/dist/inbound-debounce.js +4 -2
- package/dist/inbound-debounce.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/runtime-builder.d.ts +9 -0
- package/dist/runtime-builder.d.ts.map +1 -1
- package/dist/runtime-builder.js +145 -1
- package/dist/runtime-builder.js.map +1 -1
- package/package.json +25 -8
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
const ADDRESS = /^[^\s@<>()",;:]+@[^\s@<>()",;:.]+(?:\.[^\s@<>()",;:.]+)+$/;
|
|
2
|
+
/** An address in the form the channel keys senders by: trimmed and in lower case. */
|
|
3
|
+
export function normalizeAddress(address) {
|
|
4
|
+
return address.trim().replace(/^<|>$/g, '').toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
/** Whether `text` is a plain address, `local@domain.tld`. */
|
|
7
|
+
export function isAddress(text) {
|
|
8
|
+
return ADDRESS.test(text);
|
|
9
|
+
}
|
|
10
|
+
/** The domain of an address, in lower case. */
|
|
11
|
+
export function domainOf(address) {
|
|
12
|
+
const at = address.lastIndexOf('@');
|
|
13
|
+
return at === -1 ? '' : address.slice(at + 1).toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
function decodeBytes(bytes, charset) {
|
|
16
|
+
try {
|
|
17
|
+
return new TextDecoder(charset.toLowerCase()).decode(bytes);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return new TextDecoder('utf-8').decode(bytes);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Header text with its RFC 2047 encoded words (`=?utf-8?B?...?=`, `=?utf-8?Q?...?=`) decoded. */
|
|
24
|
+
export function decodeMimeWords(text) {
|
|
25
|
+
return text
|
|
26
|
+
.replace(/(=\?[^?\s]+\?[bq]\?[^?\s]*\?=)\s+(?==\?[^?\s]+\?[bq]\?)/gi, '$1')
|
|
27
|
+
.replace(/=\?([^?\s*]+)(?:\*[^?\s]*)?\?([bq])\?([^?\s]*)\?=/gi, (word, charset, encoding, data) => {
|
|
28
|
+
if (encoding.toLowerCase() === 'b') {
|
|
29
|
+
return decodeBytes(Buffer.from(data, 'base64'), charset);
|
|
30
|
+
}
|
|
31
|
+
const bytes = [];
|
|
32
|
+
const text = data.replace(/_/g, ' ');
|
|
33
|
+
for (let i = 0; i < text.length; i++) {
|
|
34
|
+
if (text[i] === '=' && /^[0-9a-f]{2}$/i.test(text.slice(i + 1, i + 3))) {
|
|
35
|
+
bytes.push(parseInt(text.slice(i + 1, i + 3), 16));
|
|
36
|
+
i += 2;
|
|
37
|
+
}
|
|
38
|
+
else
|
|
39
|
+
bytes.push(text.charCodeAt(i) & 0xff);
|
|
40
|
+
}
|
|
41
|
+
return bytes.length ? decodeBytes(Uint8Array.from(bytes), charset) : word;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Splits the first mailbox of an address header (RFC 5322) in one pass: quoted strings and
|
|
46
|
+
* comments are taken as text, so an address written inside them (`"x <ceo@victim.com>"`) is
|
|
47
|
+
* part of the name and never the address. The mailbox ends at the first comma outside them.
|
|
48
|
+
*/
|
|
49
|
+
function mailboxParts(text) {
|
|
50
|
+
let phrase = '';
|
|
51
|
+
let bare = '';
|
|
52
|
+
let comment;
|
|
53
|
+
let angle;
|
|
54
|
+
let quoted = false;
|
|
55
|
+
let depth = 0;
|
|
56
|
+
let currentComment = '';
|
|
57
|
+
let inAngle = false;
|
|
58
|
+
let angleText = '';
|
|
59
|
+
for (let i = 0; i < text.length; i++) {
|
|
60
|
+
const char = text[i];
|
|
61
|
+
if (quoted) {
|
|
62
|
+
if (char === '\\' && i + 1 < text.length)
|
|
63
|
+
phrase += text[++i];
|
|
64
|
+
else if (char === '"')
|
|
65
|
+
quoted = false;
|
|
66
|
+
else
|
|
67
|
+
phrase += char;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (depth > 0) {
|
|
71
|
+
if (char === '\\' && i + 1 < text.length)
|
|
72
|
+
currentComment += text[++i];
|
|
73
|
+
else if (char === '(') {
|
|
74
|
+
depth++;
|
|
75
|
+
currentComment += char;
|
|
76
|
+
}
|
|
77
|
+
else if (char === ')') {
|
|
78
|
+
depth--;
|
|
79
|
+
if (depth === 0)
|
|
80
|
+
comment ??= currentComment.trim();
|
|
81
|
+
else
|
|
82
|
+
currentComment += char;
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
currentComment += char;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (inAngle) {
|
|
89
|
+
if (char === '>') {
|
|
90
|
+
inAngle = false;
|
|
91
|
+
angle = angleText.trim();
|
|
92
|
+
}
|
|
93
|
+
else
|
|
94
|
+
angleText += char;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (char === '"')
|
|
98
|
+
quoted = true;
|
|
99
|
+
else if (char === '(') {
|
|
100
|
+
depth = 1;
|
|
101
|
+
currentComment = '';
|
|
102
|
+
}
|
|
103
|
+
else if (char === '<') {
|
|
104
|
+
inAngle = true;
|
|
105
|
+
angleText = '';
|
|
106
|
+
}
|
|
107
|
+
else if (char === ',')
|
|
108
|
+
break;
|
|
109
|
+
else {
|
|
110
|
+
if (angle === undefined)
|
|
111
|
+
phrase += char;
|
|
112
|
+
bare += char;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { ...(angle !== undefined && { angle }), phrase, bare, ...(comment && { comment }) };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The first mailbox of an address header such as `From`: `"Ann Lee" <ann@example.com>`,
|
|
119
|
+
* `Ann <ann@example.com>` or `ann@example.com (Ann)`, with encoded words in the name decoded.
|
|
120
|
+
* The address is the angle address outside quoted strings and comments, so a display name that
|
|
121
|
+
* holds another address cannot pass for it.
|
|
122
|
+
*/
|
|
123
|
+
export function parseMailbox(value) {
|
|
124
|
+
if (!value)
|
|
125
|
+
return undefined;
|
|
126
|
+
const parts = mailboxParts(value.replace(/\r?\n[ \t]+/g, ' ').trim());
|
|
127
|
+
if (parts.angle !== undefined) {
|
|
128
|
+
const address = normalizeAddress(parts.angle);
|
|
129
|
+
if (!isAddress(address))
|
|
130
|
+
return undefined;
|
|
131
|
+
const name = decodeMimeWords(parts.phrase.replace(/\s+/g, ' ').trim()).trim();
|
|
132
|
+
return name ? { address, name } : { address };
|
|
133
|
+
}
|
|
134
|
+
const address = normalizeAddress(parts.bare.trim());
|
|
135
|
+
if (!isAddress(address))
|
|
136
|
+
return undefined;
|
|
137
|
+
return parts.comment ? { address, name: decodeMimeWords(parts.comment) } : { address };
|
|
138
|
+
}
|
|
139
|
+
/** The message ids (`<id@host>`) of a `Message-ID`, `In-Reply-To` or `References` header. */
|
|
140
|
+
export function messageIdsOf(value) {
|
|
141
|
+
if (!value)
|
|
142
|
+
return [];
|
|
143
|
+
const ids = value.match(/<[^<>\s]+>/g);
|
|
144
|
+
if (ids)
|
|
145
|
+
return ids;
|
|
146
|
+
const bare = value.trim();
|
|
147
|
+
return bare && !/\s/.test(bare) ? [`<${bare.replace(/^<|>$/g, '')}>`] : [];
|
|
148
|
+
}
|
|
149
|
+
/** The first message id of a header, if it has one. */
|
|
150
|
+
export function messageIdOf(value) {
|
|
151
|
+
return messageIdsOf(value)[0];
|
|
152
|
+
}
|
|
153
|
+
/** A header block (`Name: value` lines, folded lines continuing with whitespace) as headers. */
|
|
154
|
+
export function parseHeaderBlock(block) {
|
|
155
|
+
const headers = [];
|
|
156
|
+
for (const line of block.replace(/\r\n?/g, '\n').split('\n')) {
|
|
157
|
+
if (!line)
|
|
158
|
+
break;
|
|
159
|
+
if (/^[ \t]/.test(line) && headers.length > 0) {
|
|
160
|
+
const last = headers[headers.length - 1];
|
|
161
|
+
last[1] = `${last[1]} ${line.trim()}`;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const colon = line.indexOf(':');
|
|
165
|
+
if (colon <= 0)
|
|
166
|
+
continue;
|
|
167
|
+
headers.push([line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim()]);
|
|
168
|
+
}
|
|
169
|
+
return headers;
|
|
170
|
+
}
|
|
171
|
+
/** The first value of a header, by its name in any letter case. */
|
|
172
|
+
export function headerOf(headers, name) {
|
|
173
|
+
const wanted = name.toLowerCase();
|
|
174
|
+
return headers.find(([key]) => key === wanted)?.[1];
|
|
175
|
+
}
|
|
176
|
+
/** Every value of a header, top to bottom, by its name in any letter case. */
|
|
177
|
+
export function headersOf(headers, name) {
|
|
178
|
+
const wanted = name.toLowerCase();
|
|
179
|
+
return headers.filter(([key]) => key === wanted).map(([, value]) => value);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Whether `domain` is aligned with the author's domain `fromDomain` in the relaxed DMARC sense:
|
|
183
|
+
* the same domain, or one a subdomain of the other.
|
|
184
|
+
*/
|
|
185
|
+
export function alignedDomains(domain, fromDomain) {
|
|
186
|
+
const a = domain.toLowerCase().replace(/\.$/, '');
|
|
187
|
+
const b = fromDomain.toLowerCase().replace(/\.$/, '');
|
|
188
|
+
if (!a || !b)
|
|
189
|
+
return false;
|
|
190
|
+
return a === b || a.endsWith(`.${b}`) || b.endsWith(`.${a}`);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Whether an allow list lets `address` in. Entries are addresses (`ann@example.com`), domains
|
|
194
|
+
* (`example.com` or `@example.com`, without subdomains), subdomain wildcards (`*.example.com`)
|
|
195
|
+
* or `*` for everyone, in any letter case.
|
|
196
|
+
*/
|
|
197
|
+
export function allowsSender(list, address) {
|
|
198
|
+
const sender = normalizeAddress(address);
|
|
199
|
+
const domain = domainOf(sender);
|
|
200
|
+
return list.some((raw) => {
|
|
201
|
+
const entry = raw.trim().toLowerCase();
|
|
202
|
+
if (entry === '*')
|
|
203
|
+
return true;
|
|
204
|
+
if (entry.startsWith('*.'))
|
|
205
|
+
return domain.endsWith(entry.slice(1));
|
|
206
|
+
if (entry.includes('@') && !entry.startsWith('@'))
|
|
207
|
+
return entry === sender;
|
|
208
|
+
return entry.replace(/^@/, '') === domain;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=address.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"address.js","sourceRoot":"","sources":["../../../src/channels/email/address.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type EmailHeaders } from './address.js';
|
|
2
|
+
/**
|
|
3
|
+
* What the receiving side found about the sender: `pass` when DKIM, SPF or DMARC passed for a
|
|
4
|
+
* domain aligned with the `From` address, `fail` when results were reported and none of them
|
|
5
|
+
* did, `none` when nothing was reported.
|
|
6
|
+
*/
|
|
7
|
+
export type SenderAuthentication = 'pass' | 'fail' | 'none';
|
|
8
|
+
/**
|
|
9
|
+
* Combines the results of one source, such as the methods of one `Authentication-Results`:
|
|
10
|
+
* any pass passes (mail forwarded on breaks SPF but keeps DKIM), else any fail fails.
|
|
11
|
+
*/
|
|
12
|
+
export declare function combineAuthentication(...verdicts: readonly SenderAuthentication[]): SenderAuthentication;
|
|
13
|
+
/**
|
|
14
|
+
* The verdict of one `Authentication-Results` header (RFC 8601) for the author domain
|
|
15
|
+
* `fromDomain`, and its authserv-id. A DMARC result for the `From` domain decides when there is
|
|
16
|
+
* one: DMARC already weighs DKIM and SPF alignment. Otherwise a DKIM or SPF pass for an aligned
|
|
17
|
+
* domain passes, and any definite result (a fail, or a pass for another domain, as a spoofer
|
|
18
|
+
* signing with a domain of its own gets) fails, while `none` or `neutral` say nothing. Parts
|
|
19
|
+
* are split outside quoted strings, so an address like `"a;dkim=pass"@evil.example` stays one
|
|
20
|
+
* value.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseAuthenticationResults(value: string, fromDomain: string): {
|
|
23
|
+
authservId: string;
|
|
24
|
+
verdict: SenderAuthentication;
|
|
25
|
+
};
|
|
26
|
+
/** Where the sender authentication of a message's headers comes from, and whom to trust. */
|
|
27
|
+
export interface HeaderAuthenticationOptions {
|
|
28
|
+
/**
|
|
29
|
+
* `imap`: the `Authentication-Results` of `trustedAuthservIds`. `postmark`: the SpamAssassin
|
|
30
|
+
* `X-Spam-Tests` Postmark adds, whose earlier copies SpamAssassin removes, and the
|
|
31
|
+
* `Authentication-Results` of `trustedAuthservIds`.
|
|
32
|
+
*/
|
|
33
|
+
source: 'imap' | 'postmark';
|
|
34
|
+
/** The authserv-ids (the receiving servers) whose `Authentication-Results` count */
|
|
35
|
+
trustedAuthservIds?: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Combines the verdicts of independent sources: a fail of any of them fails, since one source
|
|
39
|
+
* passing does not undo what another found, then any pass passes.
|
|
40
|
+
*/
|
|
41
|
+
export declare function combineSources(...verdicts: readonly SenderAuthentication[]): SenderAuthentication;
|
|
42
|
+
/**
|
|
43
|
+
* The sender verdict a message's headers carry, for the author domain `fromDomain`. A sender can
|
|
44
|
+
* add any header, so only those the receiving side wrote count: the topmost
|
|
45
|
+
* `Authentication-Results` of one of `trustedAuthservIds` (without them none is trusted), and
|
|
46
|
+
* Postmark's `X-Spam-Tests`. A fail of either source fails.
|
|
47
|
+
*/
|
|
48
|
+
export declare function headerAuthentication(headers: EmailHeaders, fromDomain: string, options: HeaderAuthenticationOptions): SenderAuthentication;
|
|
49
|
+
/**
|
|
50
|
+
* Compares two secrets in constant time for their contents: unequal lengths still run one full
|
|
51
|
+
* comparison (of the expected secret with itself), so only the length of the secret can show.
|
|
52
|
+
*/
|
|
53
|
+
export declare function secretsEqual(given: string, expected: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Whether `signature` is the hex HMAC SHA-256 of `timestamp` and `token` with Mailgun's webhook
|
|
56
|
+
* signing key, as Mailgun signs the requests of its routes (in JSON mode `token` is the body).
|
|
57
|
+
*/
|
|
58
|
+
export declare function verifyMailgunSignature(timestamp: string, token: string | Uint8Array, signature: string | undefined, signingKey: string): boolean;
|
|
59
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/channels/email/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAA6B,KAAK,YAAY,EAAE,MAAM,WAAW,CAAC;AAEzE;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,QAAQ,EAAE,SAAS,oBAAoB,EAAE,GAC3C,oBAAoB,CAItB;AAyFD;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,GACjB;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,oBAAoB,CAAA;CAAE,CAqBvD;AAED,4FAA4F;AAC5F,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;IAC5B,oFAAoF;IACpF,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,QAAQ,EAAE,SAAS,oBAAoB,EAAE,GAAG,oBAAoB,CAIjG;AAgBD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CAatB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKrE;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,UAAU,EAC1B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,UAAU,EAAE,MAAM,GACjB,OAAO,CAOT"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { alignedDomains, headersOf } from './address.js';
|
|
3
|
+
/**
|
|
4
|
+
* Combines the results of one source, such as the methods of one `Authentication-Results`:
|
|
5
|
+
* any pass passes (mail forwarded on breaks SPF but keeps DKIM), else any fail fails.
|
|
6
|
+
*/
|
|
7
|
+
export function combineAuthentication(...verdicts) {
|
|
8
|
+
if (verdicts.includes('pass'))
|
|
9
|
+
return 'pass';
|
|
10
|
+
if (verdicts.includes('fail'))
|
|
11
|
+
return 'fail';
|
|
12
|
+
return 'none';
|
|
13
|
+
}
|
|
14
|
+
/** Results that say something about the sender, unlike `none`, `neutral` or `temperror`. */
|
|
15
|
+
const DEFINITE = new Set(['pass', 'fail', 'softfail', 'permerror', 'hardfail']);
|
|
16
|
+
/**
|
|
17
|
+
* The text of a header without its comments, in one pass that knows quoted strings and `\`
|
|
18
|
+
* escapes: a `(` inside quotes is text, and an escaped quote does not end them.
|
|
19
|
+
*/
|
|
20
|
+
function withoutComments(text) {
|
|
21
|
+
let out = '';
|
|
22
|
+
let depth = 0;
|
|
23
|
+
let quoted = false;
|
|
24
|
+
for (let i = 0; i < text.length; i++) {
|
|
25
|
+
const char = text[i];
|
|
26
|
+
if (char === '\\' && i + 1 < text.length) {
|
|
27
|
+
if (depth === 0)
|
|
28
|
+
out += char + text[i + 1];
|
|
29
|
+
i++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (depth === 0 && char === '"')
|
|
33
|
+
quoted = !quoted;
|
|
34
|
+
if (!quoted && char === '(')
|
|
35
|
+
depth++;
|
|
36
|
+
else if (!quoted && char === ')' && depth > 0)
|
|
37
|
+
depth--;
|
|
38
|
+
else if (depth === 0)
|
|
39
|
+
out += char;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** `text` split at `separator` and at whitespace too when `alsoSpace`, never inside quotes. */
|
|
44
|
+
function splitOutsideQuotes(text, separator, alsoSpace = false) {
|
|
45
|
+
const parts = [];
|
|
46
|
+
let current = '';
|
|
47
|
+
let quoted = false;
|
|
48
|
+
for (let i = 0; i < text.length; i++) {
|
|
49
|
+
const char = text[i];
|
|
50
|
+
if (char === '\\' && i + 1 < text.length) {
|
|
51
|
+
current += char + text[++i];
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (char === '"')
|
|
55
|
+
quoted = !quoted;
|
|
56
|
+
if (!quoted && (char === separator || (alsoSpace && /\s/.test(char)))) {
|
|
57
|
+
if (current)
|
|
58
|
+
parts.push(current);
|
|
59
|
+
current = '';
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
current += char;
|
|
63
|
+
}
|
|
64
|
+
if (current)
|
|
65
|
+
parts.push(current);
|
|
66
|
+
return parts;
|
|
67
|
+
}
|
|
68
|
+
/** The results of a header, its `;`-separated parts read outside quoted strings. */
|
|
69
|
+
function authResultsOf(value) {
|
|
70
|
+
const [id = '', ...segments] = splitOutsideQuotes(withoutComments(value), ';');
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const segment of segments) {
|
|
73
|
+
const [first = '', ...rest] = splitOutsideQuotes(segment.trim(), ' ', true);
|
|
74
|
+
const method = /^(dkim|spf|dmarc)=([a-z]+)$/i.exec(first);
|
|
75
|
+
if (!method)
|
|
76
|
+
continue;
|
|
77
|
+
const properties = new Map();
|
|
78
|
+
for (const token of rest) {
|
|
79
|
+
const equals = token.indexOf('=');
|
|
80
|
+
if (equals <= 0)
|
|
81
|
+
continue;
|
|
82
|
+
const name = token.slice(0, equals).toLowerCase();
|
|
83
|
+
if (!properties.has(name))
|
|
84
|
+
properties.set(name, token.slice(equals + 1));
|
|
85
|
+
}
|
|
86
|
+
results.push({ method: method[1].toLowerCase(), result: method[2].toLowerCase(), properties });
|
|
87
|
+
}
|
|
88
|
+
return { authservId: id.trim().split(/\s+/)[0]?.toLowerCase() ?? '', results };
|
|
89
|
+
}
|
|
90
|
+
/** The domain a property names: of an address (after its last `@`) or the value itself. */
|
|
91
|
+
function propertyDomain(result, ...names) {
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
const value = result.properties.get(name);
|
|
94
|
+
if (!value)
|
|
95
|
+
continue;
|
|
96
|
+
const bare = value.replace(/^"|"$/g, '');
|
|
97
|
+
const at = bare.lastIndexOf('@');
|
|
98
|
+
const domain = (at === -1 ? bare : bare.slice(at + 1)).replace(/^"|"$/g, '').toLowerCase();
|
|
99
|
+
if (domain && !/[\s";]/.test(domain))
|
|
100
|
+
return domain;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The verdict of one `Authentication-Results` header (RFC 8601) for the author domain
|
|
106
|
+
* `fromDomain`, and its authserv-id. A DMARC result for the `From` domain decides when there is
|
|
107
|
+
* one: DMARC already weighs DKIM and SPF alignment. Otherwise a DKIM or SPF pass for an aligned
|
|
108
|
+
* domain passes, and any definite result (a fail, or a pass for another domain, as a spoofer
|
|
109
|
+
* signing with a domain of its own gets) fails, while `none` or `neutral` say nothing. Parts
|
|
110
|
+
* are split outside quoted strings, so an address like `"a;dkim=pass"@evil.example` stays one
|
|
111
|
+
* value.
|
|
112
|
+
*/
|
|
113
|
+
export function parseAuthenticationResults(value, fromDomain) {
|
|
114
|
+
const { authservId, results } = authResultsOf(value);
|
|
115
|
+
const dmarc = results.find((result) => {
|
|
116
|
+
if (result.method !== 'dmarc')
|
|
117
|
+
return false;
|
|
118
|
+
const from = propertyDomain(result, 'header.from');
|
|
119
|
+
return !from || alignedDomains(from, fromDomain);
|
|
120
|
+
});
|
|
121
|
+
if (dmarc?.result === 'pass')
|
|
122
|
+
return { authservId, verdict: 'pass' };
|
|
123
|
+
if (dmarc && DEFINITE.has(dmarc.result))
|
|
124
|
+
return { authservId, verdict: 'fail' };
|
|
125
|
+
let reported = false;
|
|
126
|
+
for (const result of results) {
|
|
127
|
+
if (result.method === 'dmarc')
|
|
128
|
+
continue;
|
|
129
|
+
if (DEFINITE.has(result.result))
|
|
130
|
+
reported = true;
|
|
131
|
+
if (result.result !== 'pass')
|
|
132
|
+
continue;
|
|
133
|
+
const domain = result.method === 'dkim'
|
|
134
|
+
? propertyDomain(result, 'header.d', 'header.i')
|
|
135
|
+
: propertyDomain(result, 'smtp.mailfrom', 'smtp.helo');
|
|
136
|
+
if (domain && alignedDomains(domain, fromDomain))
|
|
137
|
+
return { authservId, verdict: 'pass' };
|
|
138
|
+
}
|
|
139
|
+
return { authservId, verdict: reported ? 'fail' : 'none' };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Combines the verdicts of independent sources: a fail of any of them fails, since one source
|
|
143
|
+
* passing does not undo what another found, then any pass passes.
|
|
144
|
+
*/
|
|
145
|
+
export function combineSources(...verdicts) {
|
|
146
|
+
if (verdicts.includes('fail'))
|
|
147
|
+
return 'fail';
|
|
148
|
+
if (verdicts.includes('pass'))
|
|
149
|
+
return 'pass';
|
|
150
|
+
return 'none';
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The verdict of Postmark's SpamAssassin tests: `DKIM_VALID_AU` is a valid signature of the
|
|
154
|
+
* author's domain, an SPF fail or an invalid signature fails. More than one `X-Spam-Tests`
|
|
155
|
+
* header fails, since one of them came from the sender.
|
|
156
|
+
*/
|
|
157
|
+
function spamTestsAuthentication(headers) {
|
|
158
|
+
const tests = headersOf(headers, 'x-spam-tests');
|
|
159
|
+
if (tests.length > 1)
|
|
160
|
+
return 'fail';
|
|
161
|
+
if (tests.length === 0)
|
|
162
|
+
return 'none';
|
|
163
|
+
if (/\bDKIM_VALID_AU\b/.test(tests[0]))
|
|
164
|
+
return 'pass';
|
|
165
|
+
if (/\b(SPF_FAIL|SPF_SOFTFAIL|DKIM_INVALID)\b/.test(tests[0]))
|
|
166
|
+
return 'fail';
|
|
167
|
+
return 'none';
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The sender verdict a message's headers carry, for the author domain `fromDomain`. A sender can
|
|
171
|
+
* add any header, so only those the receiving side wrote count: the topmost
|
|
172
|
+
* `Authentication-Results` of one of `trustedAuthservIds` (without them none is trusted), and
|
|
173
|
+
* Postmark's `X-Spam-Tests`. A fail of either source fails.
|
|
174
|
+
*/
|
|
175
|
+
export function headerAuthentication(headers, fromDomain, options) {
|
|
176
|
+
const verdicts = [];
|
|
177
|
+
const trusted = options.trustedAuthservIds?.map((id) => id.toLowerCase()) ?? [];
|
|
178
|
+
if (trusted.length > 0) {
|
|
179
|
+
for (const value of headersOf(headers, 'authentication-results')) {
|
|
180
|
+
const parsed = parseAuthenticationResults(value, fromDomain);
|
|
181
|
+
if (!trusted.includes(parsed.authservId))
|
|
182
|
+
continue;
|
|
183
|
+
verdicts.push(parsed.verdict);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (options.source === 'postmark')
|
|
188
|
+
verdicts.push(spamTestsAuthentication(headers));
|
|
189
|
+
return combineSources(...verdicts);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Compares two secrets in constant time for their contents: unequal lengths still run one full
|
|
193
|
+
* comparison (of the expected secret with itself), so only the length of the secret can show.
|
|
194
|
+
*/
|
|
195
|
+
export function secretsEqual(given, expected) {
|
|
196
|
+
const givenBytes = Buffer.from(given);
|
|
197
|
+
const expectedBytes = Buffer.from(expected);
|
|
198
|
+
const sameLength = givenBytes.length === expectedBytes.length;
|
|
199
|
+
return timingSafeEqual(sameLength ? givenBytes : expectedBytes, expectedBytes) && sameLength;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Whether `signature` is the hex HMAC SHA-256 of `timestamp` and `token` with Mailgun's webhook
|
|
203
|
+
* signing key, as Mailgun signs the requests of its routes (in JSON mode `token` is the body).
|
|
204
|
+
*/
|
|
205
|
+
export function verifyMailgunSignature(timestamp, token, signature, signingKey) {
|
|
206
|
+
if (!signature || !/^[0-9a-f]+$/i.test(signature))
|
|
207
|
+
return false;
|
|
208
|
+
const expected = Buffer.from(createHmac('sha256', signingKey).update(timestamp).update(token).digest('hex'));
|
|
209
|
+
const given = Buffer.from(signature.toLowerCase());
|
|
210
|
+
return given.length === expected.length && timingSafeEqual(given, expected);
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/channels/email/auth.ts"],"names":[],"mappings":"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"}
|