@blamejs/core 0.9.49 → 0.10.1
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/CHANGELOG.md +951 -908
- package/index.js +25 -0
- package/lib/_test/crypto-fixtures.js +67 -0
- package/lib/agent-event-bus.js +52 -6
- package/lib/agent-idempotency.js +169 -16
- package/lib/agent-orchestrator.js +263 -9
- package/lib/agent-posture-chain.js +163 -5
- package/lib/agent-saga.js +146 -16
- package/lib/agent-snapshot.js +349 -19
- package/lib/agent-stream.js +34 -2
- package/lib/agent-tenant.js +179 -23
- package/lib/agent-trace.js +84 -21
- package/lib/auth/aal.js +8 -1
- package/lib/auth/ciba.js +6 -1
- package/lib/auth/dpop.js +7 -2
- package/lib/auth/fal.js +17 -8
- package/lib/auth/jwt-external.js +128 -4
- package/lib/auth/oauth.js +232 -10
- package/lib/auth/oid4vci.js +67 -7
- package/lib/auth/openid-federation.js +71 -25
- package/lib/auth/passkey.js +140 -6
- package/lib/auth/sd-jwt-vc.js +67 -5
- package/lib/circuit-breaker.js +10 -2
- package/lib/compliance.js +176 -8
- package/lib/crypto-field.js +114 -14
- package/lib/crypto.js +216 -20
- package/lib/db.js +1 -0
- package/lib/guard-jmap.js +321 -0
- package/lib/guard-managesieve-command.js +566 -0
- package/lib/guard-pop3-command.js +317 -0
- package/lib/guard-smtp-command.js +58 -3
- package/lib/mail-agent.js +20 -7
- package/lib/mail-arc-sign.js +12 -8
- package/lib/mail-auth.js +323 -34
- package/lib/mail-crypto-pgp.js +934 -0
- package/lib/mail-crypto-smime.js +340 -0
- package/lib/mail-crypto.js +108 -0
- package/lib/mail-dav.js +1224 -0
- package/lib/mail-deploy.js +492 -0
- package/lib/mail-dkim.js +431 -26
- package/lib/mail-journal.js +435 -0
- package/lib/mail-scan.js +502 -0
- package/lib/mail-server-imap.js +64 -26
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +40 -30
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +13 -0
- package/lib/mail-server-submission.js +70 -24
- package/lib/mail-server-tls.js +445 -0
- package/lib/mail-sieve.js +557 -0
- package/lib/mail-spam-score.js +284 -0
- package/lib/mail.js +99 -0
- package/lib/metrics.js +80 -3
- package/lib/middleware/dpop.js +58 -3
- package/lib/middleware/idempotency-key.js +255 -42
- package/lib/middleware/protected-resource-metadata.js +114 -2
- package/lib/network-dns-resolver.js +33 -0
- package/lib/network-tls.js +46 -0
- package/lib/outbox.js +62 -12
- package/lib/pqc-agent.js +13 -5
- package/lib/retry.js +23 -9
- package/lib/router.js +23 -1
- package/lib/safe-ical.js +634 -0
- package/lib/safe-icap.js +502 -0
- package/lib/safe-mime.js +15 -0
- package/lib/safe-sieve.js +684 -0
- package/lib/safe-smtp.js +57 -0
- package/lib/safe-url.js +37 -0
- package/lib/safe-vcard.js +473 -0
- package/lib/self-update-standalone-verifier.js +32 -3
- package/lib/self-update.js +153 -33
- package/lib/vendor/MANIFEST.json +161 -156
- package/lib/vendor-data.js +127 -9
- package/lib/vex.js +324 -59
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module b.safeSieve
|
|
5
|
+
* @nav Mail
|
|
6
|
+
* @title Sieve parser
|
|
7
|
+
* @order 230
|
|
8
|
+
* @since 0.9.55
|
|
9
|
+
*
|
|
10
|
+
* @intro
|
|
11
|
+
* Bounded RFC 5228 Sieve parser. Produces an AST that
|
|
12
|
+
* `b.mail.sieve.run` walks at delivery time + at `agent.sieve.put`
|
|
13
|
+
* pre-validation. Caps script bytes / nesting depth / string-list
|
|
14
|
+
* length / per-string bytes per profile so a hostile script can't
|
|
15
|
+
* exhaust the parser. Refuses C0 / DEL / NUL controls outside
|
|
16
|
+
* string literals, refuses bare LF / bare CR (Sieve uses CRLF line
|
|
17
|
+
* terminators per RFC 5228 §2.1), and refuses oversized scripts at
|
|
18
|
+
* the byte level before tokenization.
|
|
19
|
+
*
|
|
20
|
+
* Grammar coverage:
|
|
21
|
+
* - `require ["module" ...]`
|
|
22
|
+
* - control: `if` / `elsif` / `else` with block-body
|
|
23
|
+
* - tests: `address` / `header` / `exists` / `size` / `envelope`
|
|
24
|
+
* (when `envelope` capability declared), plus `not` / `allof` /
|
|
25
|
+
* `anyof` / `true` / `false`
|
|
26
|
+
* - actions: `keep` / `fileinto` / `discard` / `redirect` / `stop`
|
|
27
|
+
* - match-types: `:is` (default) / `:contains` / `:matches`
|
|
28
|
+
* - comparators: `i;octet` (default) / `i;ascii-casemap`
|
|
29
|
+
* - address-parts: `:all` (default) / `:localpart` / `:domain`
|
|
30
|
+
* - string lists, quoted strings (`"..."` with backslash escapes),
|
|
31
|
+
* multi-line strings (`text:\r\n...\r\n.\r\n`)
|
|
32
|
+
* - comments: `#` line and `/* block * /`
|
|
33
|
+
*
|
|
34
|
+
* Extensions deferred (RFC 5229 variables, 5230 vacation, 5231
|
|
35
|
+
* relational, 5232 imap4flags, 5233 subaddress, 5235 spamtest /
|
|
36
|
+
* virustest, 5260 date / index, 5293 editheader, 5429 reject /
|
|
37
|
+
* extlists, 5435 enotify, 5703 mime / replace / enclose /
|
|
38
|
+
* extracttext, 6009 ihave, 6131 mailboxid, 6134 extlists, 6558
|
|
39
|
+
* mailbox, 6609 include, 6785 imapsieve, 8580 fcc) — refused at
|
|
40
|
+
* `require` time so scripts depending on them fail fast rather than
|
|
41
|
+
* silently mis-execute. The framework will light these incrementally
|
|
42
|
+
* as the operator-roadmap calls for them; until then, ship the base
|
|
43
|
+
* grammar that covers ~80% of operator-written scripts.
|
|
44
|
+
*
|
|
45
|
+
* @card
|
|
46
|
+
* Bounded Sieve (RFC 5228) parser. Produces an AST the interpreter
|
|
47
|
+
* walks under a gas counter; the 17 extension RFCs are refused at
|
|
48
|
+
* `require` time until each lights up.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
var { defineClass } = require("./framework-error");
|
|
52
|
+
|
|
53
|
+
var SafeSieveError = defineClass("SafeSieveError", { alwaysPermanent: true });
|
|
54
|
+
|
|
55
|
+
var DEFAULTS = Object.freeze({
|
|
56
|
+
maxScriptBytes: 65536, // allow:raw-byte-literal — 64 KiB
|
|
57
|
+
maxDepth: 32, // allow:raw-byte-literal — block nesting cap
|
|
58
|
+
maxIfChainLen: 32, // allow:raw-byte-literal — elsif/elsif... cap
|
|
59
|
+
maxStringListLen: 256, // allow:raw-byte-literal
|
|
60
|
+
maxStringBytes: 4096, // allow:raw-byte-literal — per-string cap
|
|
61
|
+
maxArgsPerCmd: 32, // allow:raw-byte-literal — per-command arg cap
|
|
62
|
+
maxRequiredCaps: 32, // allow:raw-byte-literal
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
var PROFILES = Object.freeze({
|
|
66
|
+
strict: Object.assign({}, DEFAULTS),
|
|
67
|
+
balanced: Object.assign({}, DEFAULTS, {
|
|
68
|
+
maxScriptBytes: 262144, // allow:raw-byte-literal — 256 KiB
|
|
69
|
+
maxDepth: 64, // allow:raw-byte-literal
|
|
70
|
+
maxIfChainLen: 64, // allow:raw-byte-literal
|
|
71
|
+
maxStringListLen: 1024, // allow:raw-byte-literal
|
|
72
|
+
maxStringBytes: 16384, // allow:raw-byte-literal
|
|
73
|
+
maxArgsPerCmd: 64, // allow:raw-byte-literal
|
|
74
|
+
}),
|
|
75
|
+
permissive: Object.assign({}, DEFAULTS, {
|
|
76
|
+
maxScriptBytes: 1048576, // allow:raw-byte-literal — 1 MiB
|
|
77
|
+
maxDepth: 128, // allow:raw-byte-literal
|
|
78
|
+
maxIfChainLen: 128, // allow:raw-byte-literal
|
|
79
|
+
maxStringListLen: 4096, // allow:raw-byte-literal
|
|
80
|
+
maxStringBytes: 65536, // allow:raw-byte-literal
|
|
81
|
+
maxArgsPerCmd: 128, // allow:raw-byte-literal
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
var COMPLIANCE_POSTURES = Object.freeze({
|
|
86
|
+
hipaa: "strict",
|
|
87
|
+
"pci-dss": "strict",
|
|
88
|
+
gdpr: "strict",
|
|
89
|
+
soc2: "strict",
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// RFC 5228 §1.2 capability identifiers. Each entry lists whether the
|
|
93
|
+
// framework's v0.9.55 interpreter implements the capability. Unknown
|
|
94
|
+
// or not-yet-implemented capabilities surface a typed parse error at
|
|
95
|
+
// `require` time per §3.2 — "If a script does not contain a require
|
|
96
|
+
// statement for a feature it uses, an implementation MUST NOT execute
|
|
97
|
+
// the script."
|
|
98
|
+
var KNOWN_CAPABILITIES = Object.freeze({
|
|
99
|
+
// RFC 5228 base implementation provides these implicitly; declaring
|
|
100
|
+
// them in `require` is allowed.
|
|
101
|
+
"fileinto": true, // §4.1 — implemented (action)
|
|
102
|
+
"envelope": true, // §5.4 — implemented (test)
|
|
103
|
+
"encoded-character": true, // §2.4.2.4 — implemented (string escape)
|
|
104
|
+
"comparator-i;octet": true,
|
|
105
|
+
"comparator-i;ascii-casemap": true,
|
|
106
|
+
// Deferred — scripts MUST declare via `require` and the framework
|
|
107
|
+
// refuses to load them until the corresponding slice ships. Listed
|
|
108
|
+
// here so the parser distinguishes "spec'd but not yet ours" from
|
|
109
|
+
// "unknown / typo".
|
|
110
|
+
"variables": false, // RFC 5229
|
|
111
|
+
"vacation": false, // RFC 5230
|
|
112
|
+
"relational": false, // RFC 5231
|
|
113
|
+
"imap4flags": false, // RFC 5232 // allow:raw-byte-literal — RFC number
|
|
114
|
+
"subaddress": false, // RFC 5233
|
|
115
|
+
"spamtest": false, // RFC 5235
|
|
116
|
+
"virustest": false, // RFC 5235
|
|
117
|
+
"date": false, // RFC 5260
|
|
118
|
+
"index": false, // RFC 5260
|
|
119
|
+
"editheader": false, // RFC 5293
|
|
120
|
+
"reject": false, // RFC 5429
|
|
121
|
+
"ereject": false, // RFC 5429
|
|
122
|
+
"enotify": false, // RFC 5435
|
|
123
|
+
"mime": false, // RFC 5703
|
|
124
|
+
"replace": false, // RFC 5703
|
|
125
|
+
"enclose": false, // RFC 5703
|
|
126
|
+
"extracttext": false, // RFC 5703
|
|
127
|
+
"ihave": false, // RFC 6009
|
|
128
|
+
"mailboxid": false, // RFC 6131
|
|
129
|
+
"extlists": false, // RFC 6134
|
|
130
|
+
"mailbox": false, // RFC 6558
|
|
131
|
+
"include": false, // RFC 6609
|
|
132
|
+
"imapsieve": false, // RFC 6785
|
|
133
|
+
"fcc": false, // RFC 8580 // allow:raw-time-literal — RFC number, not time
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function _resolveProfile(opts) {
|
|
137
|
+
if (!opts) return "strict";
|
|
138
|
+
if (typeof opts.profile === "string") return opts.profile;
|
|
139
|
+
if (typeof opts.compliancePosture === "string") {
|
|
140
|
+
return COMPLIANCE_POSTURES[opts.compliancePosture] || "strict";
|
|
141
|
+
}
|
|
142
|
+
return "strict";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _resolveCaps(opts) {
|
|
146
|
+
var name = _resolveProfile(opts);
|
|
147
|
+
var caps = PROFILES[name];
|
|
148
|
+
if (!caps) {
|
|
149
|
+
throw new SafeSieveError("safe-sieve/bad-profile",
|
|
150
|
+
"safeSieve: unknown profile '" + name + "' (expected strict|balanced|permissive)");
|
|
151
|
+
}
|
|
152
|
+
return caps;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---- Tokenizer -----------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
// Token kinds:
|
|
158
|
+
// "id" — identifier (require / if / address / ...)
|
|
159
|
+
// "tag" — `:name`
|
|
160
|
+
// "str" — string literal (quoted or multi-line)
|
|
161
|
+
// "num" — number (optionally suffixed K/M/G)
|
|
162
|
+
// "lbr"/"rbr" — `{` / `}`
|
|
163
|
+
// "lsb"/"rsb" — `[` / `]`
|
|
164
|
+
// "lp"/"rp" — `(` / `)` (rare; not in base grammar but tolerated)
|
|
165
|
+
// "comma"
|
|
166
|
+
// "semi"
|
|
167
|
+
// "eof"
|
|
168
|
+
|
|
169
|
+
function _isIdStart(c) {
|
|
170
|
+
return (c >= 0x41 && c <= 0x5A) || // A-Z
|
|
171
|
+
(c >= 0x61 && c <= 0x7A) || // a-z
|
|
172
|
+
c === 0x5F; // _
|
|
173
|
+
}
|
|
174
|
+
function _isIdCont(c) {
|
|
175
|
+
return _isIdStart(c) ||
|
|
176
|
+
(c >= 0x30 && c <= 0x39) || // 0-9
|
|
177
|
+
c === 0x2D; // -
|
|
178
|
+
}
|
|
179
|
+
function _isDigit(c) { return c >= 0x30 && c <= 0x39; }
|
|
180
|
+
|
|
181
|
+
function _tokenize(script, caps) {
|
|
182
|
+
var tokens = [];
|
|
183
|
+
var i = 0;
|
|
184
|
+
var n = script.length;
|
|
185
|
+
var line = 1;
|
|
186
|
+
var col = 1;
|
|
187
|
+
|
|
188
|
+
function _error(msg, atI) {
|
|
189
|
+
var l = line, c = col;
|
|
190
|
+
if (atI !== undefined && atI !== i) {
|
|
191
|
+
// Recompute line/col at atI (cheap — only on error path)
|
|
192
|
+
l = 1; c = 1;
|
|
193
|
+
for (var k = 0; k < atI && k < n; k++) {
|
|
194
|
+
if (script.charCodeAt(k) === 0x0A) { l++; c = 1; } else { c++; }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
throw new SafeSieveError("safe-sieve/parse-error",
|
|
198
|
+
"safeSieve.parse: " + msg + " at line " + l + ":" + c);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _advance(ch) {
|
|
202
|
+
if (ch === 0x0A) { line++; col = 1; } else { col++; }
|
|
203
|
+
i++;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
while (i < n) {
|
|
207
|
+
var c = script.charCodeAt(i);
|
|
208
|
+
|
|
209
|
+
// Whitespace + line counter.
|
|
210
|
+
if (c === 0x20 || c === 0x09) { _advance(c); continue; }
|
|
211
|
+
if (c === 0x0D) {
|
|
212
|
+
// CRLF expected; bare CR refused per RFC 5228 §2.1.
|
|
213
|
+
if (i + 1 < n && script.charCodeAt(i + 1) === 0x0A) {
|
|
214
|
+
i += 2; line++; col = 1; continue;
|
|
215
|
+
}
|
|
216
|
+
_error("bare CR (RFC 5228 §2.1 requires CRLF)");
|
|
217
|
+
}
|
|
218
|
+
if (c === 0x0A) { _advance(c); continue; }
|
|
219
|
+
|
|
220
|
+
// Control bytes outside strings refused (NUL / C0 except TAB/LF/CR).
|
|
221
|
+
if (c < 0x20 && c !== 0x09 && c !== 0x0A && c !== 0x0D) {
|
|
222
|
+
_error("control byte 0x" + c.toString(16) + " refused outside string literal"); // allow:raw-byte-literal — base-16 toString radix
|
|
223
|
+
}
|
|
224
|
+
if (c === 0x7F) _error("DEL byte refused outside string literal");
|
|
225
|
+
|
|
226
|
+
// Comments: `#` to end of line; `/* ... */` block.
|
|
227
|
+
if (c === 0x23) { // #
|
|
228
|
+
while (i < n && script.charCodeAt(i) !== 0x0A) _advance(script.charCodeAt(i));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (c === 0x2F && i + 1 < n && script.charCodeAt(i + 1) === 0x2A) { // /*
|
|
232
|
+
i += 2; col += 2;
|
|
233
|
+
while (i + 1 < n && !(script.charCodeAt(i) === 0x2A && script.charCodeAt(i + 1) === 0x2F)) {
|
|
234
|
+
_advance(script.charCodeAt(i));
|
|
235
|
+
}
|
|
236
|
+
if (i + 1 >= n) _error("unterminated block comment");
|
|
237
|
+
i += 2; col += 2;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Punctuation.
|
|
242
|
+
if (c === 0x7B) { tokens.push({ k: "lbr", line: line, col: col }); _advance(c); continue; }
|
|
243
|
+
if (c === 0x7D) { tokens.push({ k: "rbr", line: line, col: col }); _advance(c); continue; }
|
|
244
|
+
if (c === 0x5B) { tokens.push({ k: "lsb", line: line, col: col }); _advance(c); continue; }
|
|
245
|
+
if (c === 0x5D) { tokens.push({ k: "rsb", line: line, col: col }); _advance(c); continue; }
|
|
246
|
+
if (c === 0x28) { tokens.push({ k: "lp", line: line, col: col }); _advance(c); continue; }
|
|
247
|
+
if (c === 0x29) { tokens.push({ k: "rp", line: line, col: col }); _advance(c); continue; }
|
|
248
|
+
if (c === 0x2C) { tokens.push({ k: "comma", line: line, col: col }); _advance(c); continue; }
|
|
249
|
+
if (c === 0x3B) { tokens.push({ k: "semi", line: line, col: col }); _advance(c); continue; }
|
|
250
|
+
|
|
251
|
+
// Tag `:name`.
|
|
252
|
+
if (c === 0x3A) {
|
|
253
|
+
_advance(c);
|
|
254
|
+
if (i >= n || !_isIdStart(script.charCodeAt(i))) _error("`:` not followed by identifier");
|
|
255
|
+
var tagStart = i;
|
|
256
|
+
while (i < n && _isIdCont(script.charCodeAt(i))) _advance(script.charCodeAt(i));
|
|
257
|
+
tokens.push({ k: "tag", v: script.slice(tagStart, i), line: line, col: col });
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Number with optional K/M/G suffix.
|
|
262
|
+
if (_isDigit(c)) {
|
|
263
|
+
var nStart = i;
|
|
264
|
+
while (i < n && _isDigit(script.charCodeAt(i))) _advance(script.charCodeAt(i));
|
|
265
|
+
var num = parseInt(script.slice(nStart, i), 10);
|
|
266
|
+
if (i < n) {
|
|
267
|
+
var suf = script.charCodeAt(i);
|
|
268
|
+
if (suf === 0x4B || suf === 0x6B) { num *= 1024; _advance(suf); } // allow:raw-byte-literal — K
|
|
269
|
+
else if (suf === 0x4D || suf === 0x6D) { num *= 1024 * 1024; _advance(suf); } // allow:raw-byte-literal — M
|
|
270
|
+
else if (suf === 0x47 || suf === 0x67) { num *= 1024 * 1024 * 1024; _advance(suf); } // allow:raw-byte-literal — G
|
|
271
|
+
}
|
|
272
|
+
if (!Number.isFinite(num)) _error("number overflowed");
|
|
273
|
+
tokens.push({ k: "num", v: num, line: line, col: col });
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Identifier.
|
|
278
|
+
if (_isIdStart(c)) {
|
|
279
|
+
var idStart = i;
|
|
280
|
+
while (i < n && _isIdCont(script.charCodeAt(i))) _advance(script.charCodeAt(i));
|
|
281
|
+
var id = script.slice(idStart, i);
|
|
282
|
+
// `text:` introduces a multi-line string per RFC 5228 §2.4.2.
|
|
283
|
+
if (id === "text" && i < n && script.charCodeAt(i) === 0x3A) {
|
|
284
|
+
_advance(0x3A);
|
|
285
|
+
// Optional hash-comment after `text:` per §2.4.2.
|
|
286
|
+
if (i < n && script.charCodeAt(i) === 0x23) {
|
|
287
|
+
while (i < n && script.charCodeAt(i) !== 0x0A) _advance(script.charCodeAt(i));
|
|
288
|
+
}
|
|
289
|
+
// CRLF expected.
|
|
290
|
+
if (i + 1 >= n || script.charCodeAt(i) !== 0x0D || script.charCodeAt(i + 1) !== 0x0A) {
|
|
291
|
+
_error("`text:` must be followed by CRLF");
|
|
292
|
+
}
|
|
293
|
+
i += 2; line++; col = 1;
|
|
294
|
+
var bodyStart = i;
|
|
295
|
+
// Multi-line content terminated by CRLF . CRLF; lines starting
|
|
296
|
+
// with `.` are dot-stuffed per §2.4.2.
|
|
297
|
+
while (i + 2 < n) {
|
|
298
|
+
if (script.charCodeAt(i) === 0x0D &&
|
|
299
|
+
script.charCodeAt(i + 1) === 0x0A &&
|
|
300
|
+
script.charCodeAt(i + 2) === 0x2E &&
|
|
301
|
+
i + 4 < n &&
|
|
302
|
+
script.charCodeAt(i + 3) === 0x0D &&
|
|
303
|
+
script.charCodeAt(i + 4) === 0x0A) {
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
if (script.charCodeAt(i) === 0x0A) { line++; col = 1; }
|
|
307
|
+
i++;
|
|
308
|
+
}
|
|
309
|
+
if (i + 4 >= n) _error("unterminated multi-line string (missing CRLF.CRLF)");
|
|
310
|
+
var raw = script.slice(bodyStart, i);
|
|
311
|
+
i += 5; line++; col = 1; // skip `CRLF.CRLF`
|
|
312
|
+
// Dot-unstuff: lines starting with `..` collapse to `.`.
|
|
313
|
+
var body = raw.replace(/\r\n\.\./g, "\r\n.");
|
|
314
|
+
if (Buffer.byteLength(body, "utf8") > caps.maxStringBytes) {
|
|
315
|
+
_error("multi-line string " + Buffer.byteLength(body, "utf8") +
|
|
316
|
+
" bytes exceeds maxStringBytes=" + caps.maxStringBytes);
|
|
317
|
+
}
|
|
318
|
+
tokens.push({ k: "str", v: body, line: line, col: col });
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
tokens.push({ k: "id", v: id, line: line, col: col });
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Quoted string.
|
|
326
|
+
if (c === 0x22) {
|
|
327
|
+
_advance(c);
|
|
328
|
+
var sStart = i;
|
|
329
|
+
var out = "";
|
|
330
|
+
while (i < n) {
|
|
331
|
+
var ch = script.charCodeAt(i);
|
|
332
|
+
if (ch === 0x22) {
|
|
333
|
+
var lit = out + script.slice(sStart, i);
|
|
334
|
+
_advance(ch);
|
|
335
|
+
if (Buffer.byteLength(lit, "utf8") > caps.maxStringBytes) {
|
|
336
|
+
_error("string literal " + Buffer.byteLength(lit, "utf8") +
|
|
337
|
+
" bytes exceeds maxStringBytes=" + caps.maxStringBytes);
|
|
338
|
+
}
|
|
339
|
+
tokens.push({ k: "str", v: lit, line: line, col: col });
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
if (ch === 0x5C) { // backslash
|
|
343
|
+
out += script.slice(sStart, i);
|
|
344
|
+
_advance(ch);
|
|
345
|
+
if (i >= n) _error("unterminated string escape");
|
|
346
|
+
var esc = script.charCodeAt(i);
|
|
347
|
+
// RFC 5228 §2.4.2 — only `\\` and `\"` defined; other
|
|
348
|
+
// escapes pass through the backslash and the byte.
|
|
349
|
+
if (esc === 0x22) { out += '"'; _advance(esc); }
|
|
350
|
+
else if (esc === 0x5C) { out += "\\"; _advance(esc); }
|
|
351
|
+
else { out += "\\" + script[i]; _advance(esc); }
|
|
352
|
+
sStart = i;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (ch === 0x00) _error("NUL byte inside string literal");
|
|
356
|
+
if (ch === 0x0A) { line++; col = 1; }
|
|
357
|
+
_advance(ch);
|
|
358
|
+
}
|
|
359
|
+
if (i > n) _error("unterminated string literal");
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_error("unexpected byte 0x" + c.toString(16)); // allow:raw-byte-literal — base-16 toString radix
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
tokens.push({ k: "eof", line: line, col: col });
|
|
367
|
+
return tokens;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ---- Parser --------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
function _parseScript(tokens, caps, requiredCaps) {
|
|
373
|
+
var pos = 0;
|
|
374
|
+
var depth = 0;
|
|
375
|
+
|
|
376
|
+
function peek(ahead) { return tokens[pos + (ahead || 0)]; }
|
|
377
|
+
function consume(kind) {
|
|
378
|
+
var t = tokens[pos];
|
|
379
|
+
if (t.k !== kind) {
|
|
380
|
+
throw new SafeSieveError("safe-sieve/parse-error",
|
|
381
|
+
"safeSieve.parse: expected " + kind + " but got " + t.k +
|
|
382
|
+
(t.v ? " '" + t.v + "'" : "") + " at line " + t.line + ":" + t.col);
|
|
383
|
+
}
|
|
384
|
+
pos++;
|
|
385
|
+
return t;
|
|
386
|
+
}
|
|
387
|
+
function match(kind, v) {
|
|
388
|
+
var t = tokens[pos];
|
|
389
|
+
if (!t || t.k !== kind) return false;
|
|
390
|
+
if (v !== undefined && t.v !== v) return false;
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function _parseStringList() {
|
|
395
|
+
if (match("str")) {
|
|
396
|
+
var t = consume("str");
|
|
397
|
+
return [t.v];
|
|
398
|
+
}
|
|
399
|
+
consume("lsb");
|
|
400
|
+
var out = [];
|
|
401
|
+
if (!match("rsb")) {
|
|
402
|
+
out.push(consume("str").v);
|
|
403
|
+
while (match("comma")) {
|
|
404
|
+
consume("comma");
|
|
405
|
+
if (out.length >= caps.maxStringListLen) {
|
|
406
|
+
throw new SafeSieveError("safe-sieve/parse-error",
|
|
407
|
+
"safeSieve.parse: string list exceeds maxStringListLen=" + caps.maxStringListLen);
|
|
408
|
+
}
|
|
409
|
+
out.push(consume("str").v);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
consume("rsb");
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function _parseArgs() {
|
|
417
|
+
// Reads tagged-args + positional-args + an optional embedded test.
|
|
418
|
+
// For the v0.9.55 base grammar, args are: tag* (number | string |
|
|
419
|
+
// string-list)* [test]?. The grammar disambiguates tests from
|
|
420
|
+
// positional args by identifier: a known TEST name introduces a
|
|
421
|
+
// test, otherwise we treat the identifier as the start of the next
|
|
422
|
+
// command (return).
|
|
423
|
+
var tags = [];
|
|
424
|
+
var positional = [];
|
|
425
|
+
var argCount = 0;
|
|
426
|
+
while (true) {
|
|
427
|
+
var t = peek();
|
|
428
|
+
if (argCount++ > caps.maxArgsPerCmd) {
|
|
429
|
+
throw new SafeSieveError("safe-sieve/parse-error",
|
|
430
|
+
"safeSieve.parse: too many args (cap " + caps.maxArgsPerCmd + ")");
|
|
431
|
+
}
|
|
432
|
+
if (t.k === "tag") {
|
|
433
|
+
consume("tag");
|
|
434
|
+
tags.push({ name: t.v });
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (t.k === "num") {
|
|
438
|
+
consume("num");
|
|
439
|
+
positional.push({ kind: "num", v: t.v });
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (t.k === "str") {
|
|
443
|
+
consume("str");
|
|
444
|
+
positional.push({ kind: "str", v: t.v });
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (t.k === "lsb") {
|
|
448
|
+
var list = _parseStringList();
|
|
449
|
+
positional.push({ kind: "list", v: list });
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
return { tags: tags, positional: positional };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function _parseTest() {
|
|
458
|
+
var t = consume("id");
|
|
459
|
+
var name = t.v;
|
|
460
|
+
if (name === "anyof" || name === "allof") {
|
|
461
|
+
consume("lp");
|
|
462
|
+
var subs = [_parseTest()];
|
|
463
|
+
while (match("comma")) {
|
|
464
|
+
consume("comma");
|
|
465
|
+
if (subs.length >= caps.maxArgsPerCmd) {
|
|
466
|
+
throw new SafeSieveError("safe-sieve/parse-error",
|
|
467
|
+
"safeSieve.parse: too many sub-tests in " + name);
|
|
468
|
+
}
|
|
469
|
+
subs.push(_parseTest());
|
|
470
|
+
}
|
|
471
|
+
consume("rp");
|
|
472
|
+
return { kind: "test", name: name, subs: subs };
|
|
473
|
+
}
|
|
474
|
+
if (name === "not") {
|
|
475
|
+
var inner = _parseTest();
|
|
476
|
+
return { kind: "test", name: "not", subs: [inner] };
|
|
477
|
+
}
|
|
478
|
+
if (name === "true" || name === "false") {
|
|
479
|
+
return { kind: "test", name: name };
|
|
480
|
+
}
|
|
481
|
+
var args = _parseArgs();
|
|
482
|
+
return { kind: "test", name: name, args: args };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function _parseBlock() {
|
|
486
|
+
consume("lbr");
|
|
487
|
+
depth++;
|
|
488
|
+
if (depth > caps.maxDepth) {
|
|
489
|
+
throw new SafeSieveError("safe-sieve/parse-error",
|
|
490
|
+
"safeSieve.parse: block nesting exceeds maxDepth=" + caps.maxDepth);
|
|
491
|
+
}
|
|
492
|
+
var cmds = [];
|
|
493
|
+
while (!match("rbr") && !match("eof")) {
|
|
494
|
+
cmds.push(_parseCommand());
|
|
495
|
+
}
|
|
496
|
+
consume("rbr");
|
|
497
|
+
depth--;
|
|
498
|
+
return cmds;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function _parseCommand() {
|
|
502
|
+
var t = consume("id");
|
|
503
|
+
var name = t.v;
|
|
504
|
+
|
|
505
|
+
if (name === "require") {
|
|
506
|
+
var caps2 = _parseStringList();
|
|
507
|
+
consume("semi");
|
|
508
|
+
if (caps2.length + requiredCaps.length > caps.maxRequiredCaps) {
|
|
509
|
+
throw new SafeSieveError("safe-sieve/parse-error",
|
|
510
|
+
"safeSieve.parse: too many required capabilities (cap " +
|
|
511
|
+
caps.maxRequiredCaps + ")");
|
|
512
|
+
}
|
|
513
|
+
for (var i = 0; i < caps2.length; i++) {
|
|
514
|
+
var capName = caps2[i];
|
|
515
|
+
if (!Object.prototype.hasOwnProperty.call(KNOWN_CAPABILITIES, capName)) {
|
|
516
|
+
throw new SafeSieveError("safe-sieve/unknown-capability",
|
|
517
|
+
"safeSieve.parse: unknown capability '" + capName + "' at require");
|
|
518
|
+
}
|
|
519
|
+
if (KNOWN_CAPABILITIES[capName] === false) {
|
|
520
|
+
throw new SafeSieveError("safe-sieve/unimplemented-capability",
|
|
521
|
+
"safeSieve.parse: capability '" + capName + "' is RFC-defined but " +
|
|
522
|
+
"not implemented in v0.9.55 — script refused per RFC 5228 §3.2");
|
|
523
|
+
}
|
|
524
|
+
requiredCaps.push(capName);
|
|
525
|
+
}
|
|
526
|
+
return { kind: "require", caps: caps2 };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (name === "if") {
|
|
530
|
+
var test = _parseTest();
|
|
531
|
+
var thenBlock = _parseBlock();
|
|
532
|
+
var elif = [];
|
|
533
|
+
var elseBlock = null;
|
|
534
|
+
while (match("id", "elsif")) {
|
|
535
|
+
if (elif.length >= caps.maxIfChainLen) {
|
|
536
|
+
throw new SafeSieveError("safe-sieve/parse-error",
|
|
537
|
+
"safeSieve.parse: elsif chain exceeds maxIfChainLen=" + caps.maxIfChainLen);
|
|
538
|
+
}
|
|
539
|
+
consume("id");
|
|
540
|
+
var elifTest = _parseTest();
|
|
541
|
+
var elifBlock = _parseBlock();
|
|
542
|
+
elif.push({ test: elifTest, body: elifBlock });
|
|
543
|
+
}
|
|
544
|
+
if (match("id", "else")) {
|
|
545
|
+
consume("id");
|
|
546
|
+
elseBlock = _parseBlock();
|
|
547
|
+
}
|
|
548
|
+
return { kind: "if", test: test, thenBody: thenBlock, elif: elif, elseBody: elseBlock };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Action / unknown command — consume args then `;`.
|
|
552
|
+
var args = _parseArgs();
|
|
553
|
+
consume("semi");
|
|
554
|
+
return { kind: "action", name: name, args: args };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
var commands = [];
|
|
558
|
+
while (!match("eof")) {
|
|
559
|
+
commands.push(_parseCommand());
|
|
560
|
+
}
|
|
561
|
+
return { kind: "script", commands: commands, requiredCaps: requiredCaps.slice() };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ---- Public surface ------------------------------------------------------
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* @primitive b.safeSieve.parse
|
|
568
|
+
* @signature b.safeSieve.parse(script, opts?)
|
|
569
|
+
* @since 0.9.55
|
|
570
|
+
* @status stable
|
|
571
|
+
* @related b.safeSieve.validate, b.mail.sieve.run, b.guardMailSieve.validate
|
|
572
|
+
*
|
|
573
|
+
* Parse a Sieve script (RFC 5228) and return an AST. Refuses oversized
|
|
574
|
+
* scripts, control bytes, unknown capabilities, and RFC-defined-but-
|
|
575
|
+
* not-implemented capabilities at `require` time. The returned AST is
|
|
576
|
+
* the input to `b.mail.sieve.run(ast, env)`.
|
|
577
|
+
*
|
|
578
|
+
* @opts
|
|
579
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
580
|
+
* compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
581
|
+
*
|
|
582
|
+
* @example
|
|
583
|
+
* var ast = b.safeSieve.parse('require ["fileinto"];\r\n' +
|
|
584
|
+
* 'if header :contains "Subject" "[bug]" {\r\n' +
|
|
585
|
+
* ' fileinto "bugs";\r\n' +
|
|
586
|
+
* '}\r\n');
|
|
587
|
+
* // → { kind: "script", commands: [...], requiredCaps: ["fileinto"] }
|
|
588
|
+
*/
|
|
589
|
+
function parse(script, opts) {
|
|
590
|
+
if (typeof script !== "string") {
|
|
591
|
+
throw new SafeSieveError("safe-sieve/bad-input",
|
|
592
|
+
"safeSieve.parse: script must be a string");
|
|
593
|
+
}
|
|
594
|
+
var caps = _resolveCaps(opts);
|
|
595
|
+
var byteLen = Buffer.byteLength(script, "utf8");
|
|
596
|
+
if (byteLen > caps.maxScriptBytes) {
|
|
597
|
+
throw new SafeSieveError("safe-sieve/script-too-large",
|
|
598
|
+
"safeSieve.parse: script " + byteLen + " bytes exceeds maxScriptBytes=" +
|
|
599
|
+
caps.maxScriptBytes);
|
|
600
|
+
}
|
|
601
|
+
// Sieve scripts MUST use CRLF (RFC 5228 §2.1). Normalize input that
|
|
602
|
+
// arrives over a HTTP boundary where LF-only is common.
|
|
603
|
+
var norm = script;
|
|
604
|
+
if (script.indexOf("\r") === -1) {
|
|
605
|
+
norm = script.replace(/\n/g, "\r\n");
|
|
606
|
+
}
|
|
607
|
+
var tokens = _tokenize(norm, caps);
|
|
608
|
+
var requiredCaps = [];
|
|
609
|
+
return _parseScript(tokens, caps, requiredCaps);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* @primitive b.safeSieve.validate
|
|
614
|
+
* @signature b.safeSieve.validate(script, opts?)
|
|
615
|
+
* @since 0.9.55
|
|
616
|
+
* @status stable
|
|
617
|
+
* @related b.safeSieve.parse
|
|
618
|
+
*
|
|
619
|
+
* Parse-only validation — returns `{ ok, requiredCaps, issues }`
|
|
620
|
+
* shape mirroring the rest of the guard family. Operator-facing
|
|
621
|
+
* primitives that want a JMAP-style `SieveScript/validate` response
|
|
622
|
+
* (RFC 9404) compose this and surface `issues` directly.
|
|
623
|
+
*
|
|
624
|
+
* @opts
|
|
625
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
626
|
+
* compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
627
|
+
*
|
|
628
|
+
* @example
|
|
629
|
+
* var v = b.safeSieve.validate('require ["fileinto"];\r\nkeep;\r\n');
|
|
630
|
+
* v.ok; // → true
|
|
631
|
+
* v.requiredCaps; // → ["fileinto"]
|
|
632
|
+
*/
|
|
633
|
+
function validate(script, opts) {
|
|
634
|
+
try {
|
|
635
|
+
var ast = parse(script, opts);
|
|
636
|
+
return { ok: true, requiredCaps: ast.requiredCaps, issues: [] };
|
|
637
|
+
} catch (e) {
|
|
638
|
+
return {
|
|
639
|
+
ok: false,
|
|
640
|
+
requiredCaps: [],
|
|
641
|
+
issues: [{
|
|
642
|
+
kind: "parse-error",
|
|
643
|
+
severity: "high",
|
|
644
|
+
ruleId: e.code || "safe-sieve/parse-error",
|
|
645
|
+
snippet: e.message,
|
|
646
|
+
}],
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* @primitive b.safeSieve.compliancePosture
|
|
653
|
+
* @signature b.safeSieve.compliancePosture(name)
|
|
654
|
+
* @since 0.9.55
|
|
655
|
+
* @status stable
|
|
656
|
+
* @related b.safeSieve.parse, b.safeSieve.validate
|
|
657
|
+
*
|
|
658
|
+
* Look up the recommended profile name for a compliance posture
|
|
659
|
+
* (`hipaa` / `pci-dss` / `gdpr` / `soc2`). Returns `"strict"` for any
|
|
660
|
+
* known posture, `null` for unknown names. Operator-facing primitives
|
|
661
|
+
* that thread `compliancePosture` opt through to safeSieve compose
|
|
662
|
+
* this for the explicit-cast pattern when they need the name string
|
|
663
|
+
* (rather than relying on `_resolveOpts` to do the lookup).
|
|
664
|
+
*
|
|
665
|
+
* @example
|
|
666
|
+
* b.safeSieve.compliancePosture("hipaa"); // → "strict"
|
|
667
|
+
* b.safeSieve.compliancePosture("loose"); // → null
|
|
668
|
+
*/
|
|
669
|
+
function compliancePosture(name) {
|
|
670
|
+
return COMPLIANCE_POSTURES[name] || null;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
module.exports = {
|
|
674
|
+
parse: parse,
|
|
675
|
+
validate: validate,
|
|
676
|
+
compliancePosture: compliancePosture,
|
|
677
|
+
KNOWN_CAPABILITIES: KNOWN_CAPABILITIES,
|
|
678
|
+
PROFILES: PROFILES,
|
|
679
|
+
COMPLIANCE_POSTURES: COMPLIANCE_POSTURES,
|
|
680
|
+
SafeSieveError: SafeSieveError,
|
|
681
|
+
// Internal exports for the interpreter at lib/mail-sieve.js.
|
|
682
|
+
_tokenize: _tokenize,
|
|
683
|
+
_resolveCaps: _resolveCaps,
|
|
684
|
+
};
|