@blamejs/core 0.9.46 → 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 -893
- package/index.js +30 -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-imap-command.js +335 -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 +1102 -0
- package/lib/mail-server-jmap.js +488 -0
- package/lib/mail-server-managesieve.js +853 -0
- package/lib/mail-server-mx.js +164 -34
- package/lib/mail-server-pop3.js +836 -0
- package/lib/mail-server-rate-limit.js +269 -0
- package/lib/mail-server-submission.js +1032 -0
- 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 +130 -10
- 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 +168 -17
- 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,557 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module b.mail.sieve
|
|
5
|
+
* @nav Mail
|
|
6
|
+
* @title Sieve interpreter
|
|
7
|
+
* @order 240
|
|
8
|
+
* @since 0.9.55
|
|
9
|
+
*
|
|
10
|
+
* @intro
|
|
11
|
+
* RFC 5228 Sieve interpreter that walks the AST produced by
|
|
12
|
+
* `b.safeSieve.parse(script)` and emits an ordered action list the
|
|
13
|
+
* delivery agent applies to the inbound message. Runs under a gas
|
|
14
|
+
* counter (default 10 000 ops) so a hostile or runaway script can't
|
|
15
|
+
* stall the delivery thread. The interpreter is synchronous and
|
|
16
|
+
* pure — every test reads from the operator-supplied `env` object;
|
|
17
|
+
* the interpreter never touches the mail store, never opens a
|
|
18
|
+
* socket, never executes operator-supplied code. Side-effects (file-
|
|
19
|
+
* into / redirect / discard) materialize as result entries the
|
|
20
|
+
* caller dispatches against the store.
|
|
21
|
+
*
|
|
22
|
+
* Tests implemented at v0.9.55:
|
|
23
|
+
* - `address` — header-address-list test with `:all` / `:localpart`
|
|
24
|
+
* / `:domain` address-parts and `:is` / `:contains` / `:matches`
|
|
25
|
+
* match-types
|
|
26
|
+
* - `header` — header-value test with the same match-types
|
|
27
|
+
* - `envelope` — RFC 5228 §5.4; reads `env.envelope.{from,to}`
|
|
28
|
+
* - `exists` — header-presence test
|
|
29
|
+
* - `size` — `:over N` / `:under N` byte-count test
|
|
30
|
+
* - `not` / `allof` / `anyof` / `true` / `false`
|
|
31
|
+
*
|
|
32
|
+
* Actions implemented at v0.9.55:
|
|
33
|
+
* - `keep` — implicit default per §2.10.2
|
|
34
|
+
* - `fileinto "Folder"` — RFC 5228 §4.1
|
|
35
|
+
* - `discard` — RFC 5228 §4.4
|
|
36
|
+
* - `redirect "addr"` — RFC 5228 §4.2; tagged with the address
|
|
37
|
+
* - `stop` — RFC 5228 §3.2; halts further command execution
|
|
38
|
+
*
|
|
39
|
+
* Comparators: `i;octet` (default, exact byte) + `i;ascii-casemap`
|
|
40
|
+
* (case-insensitive ASCII). Other comparators refused at script
|
|
41
|
+
* parse time (require 'comparator-NAME' not in KNOWN_CAPABILITIES).
|
|
42
|
+
*
|
|
43
|
+
* Match-type wildcards: `:matches` uses `*` (any sequence) and `?`
|
|
44
|
+
* (one byte), per RFC 5228 §2.7.1. Both wildcards are converted to
|
|
45
|
+
* a bounded RegExp built from escaped literal byte segments — no
|
|
46
|
+
* user-controlled backtracking surface.
|
|
47
|
+
*
|
|
48
|
+
* The interpreter does NOT execute multi-script chains, sieve
|
|
49
|
+
* `include`s (RFC 6609), `notify` actions (RFC 5435), or `vacation`
|
|
50
|
+
* responses (RFC 5230); each of those will land with the
|
|
51
|
+
* corresponding extension RFC slice. Until then, scripts declaring
|
|
52
|
+
* them via `require` are refused at parse time.
|
|
53
|
+
*
|
|
54
|
+
* @card
|
|
55
|
+
* Sieve (RFC 5228) AST walker. Pure-functional, gas-bounded,
|
|
56
|
+
* side-effect-free — emits an action list the delivery agent
|
|
57
|
+
* dispatches.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
var safeSieve = require("./safe-sieve");
|
|
61
|
+
var { defineClass } = require("./framework-error");
|
|
62
|
+
var numericBounds = require("./numeric-bounds");
|
|
63
|
+
var validateOpts = require("./validate-opts");
|
|
64
|
+
|
|
65
|
+
var MailSieveError = defineClass("MailSieveError", { alwaysPermanent: true });
|
|
66
|
+
|
|
67
|
+
var DEFAULT_GAS_UNITS = 10000; // allow:raw-byte-literal — operation cap
|
|
68
|
+
var MAX_GAS_UNITS = 1_000_000; // allow:raw-byte-literal — operator opt-up cap
|
|
69
|
+
|
|
70
|
+
// ---- env helpers ---------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
function _headerValues(env, name) {
|
|
73
|
+
if (!env || !env.headers) return [];
|
|
74
|
+
var lc = String(name).toLowerCase();
|
|
75
|
+
var out = [];
|
|
76
|
+
for (var i = 0; i < env.headers.length; i++) {
|
|
77
|
+
var h = env.headers[i];
|
|
78
|
+
if (h && typeof h.name === "string" && h.name.toLowerCase() === lc) {
|
|
79
|
+
out.push(String(h.value != null ? h.value : ""));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _addressesOf(values, part) {
|
|
86
|
+
// Parse "Name <local@domain>" address-list, return the requested
|
|
87
|
+
// part: :all / :localpart / :domain. RFC 5228 §5.1.
|
|
88
|
+
var out = [];
|
|
89
|
+
for (var i = 0; i < values.length; i++) {
|
|
90
|
+
var v = values[i];
|
|
91
|
+
var pieces = v.split(","); // crude but bounded
|
|
92
|
+
for (var j = 0; j < pieces.length; j++) {
|
|
93
|
+
var p = pieces[j].trim();
|
|
94
|
+
if (!p) continue;
|
|
95
|
+
var ltIdx = p.indexOf("<");
|
|
96
|
+
var gtIdx = p.lastIndexOf(">");
|
|
97
|
+
var addr = (ltIdx !== -1 && gtIdx > ltIdx) ? p.slice(ltIdx + 1, gtIdx).trim() : p;
|
|
98
|
+
if (part === "localpart" || part === "domain") {
|
|
99
|
+
var at = addr.lastIndexOf("@");
|
|
100
|
+
if (at === -1) {
|
|
101
|
+
if (part === "localpart") out.push(addr);
|
|
102
|
+
else out.push("");
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
out.push(part === "localpart" ? addr.slice(0, at) : addr.slice(at + 1));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
out.push(addr); // :all
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _envelopeAddresses(env, key) {
|
|
115
|
+
if (!env || !env.envelope) return [];
|
|
116
|
+
var v = env.envelope[key];
|
|
117
|
+
if (v == null) return [];
|
|
118
|
+
if (Array.isArray(v)) return v.map(String);
|
|
119
|
+
return [String(v)];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- match-type ---------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
function _escapeRe(s) {
|
|
125
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _wildcardToRe(pattern, caseInsensitive) {
|
|
129
|
+
// RFC 5228 §2.7.1 — `*` matches any sequence, `?` matches one. Escape
|
|
130
|
+
// every other regex meta. Anchored both ends.
|
|
131
|
+
var out = "^";
|
|
132
|
+
for (var i = 0; i < pattern.length; i++) {
|
|
133
|
+
var c = pattern[i];
|
|
134
|
+
if (c === "*") out += ".*";
|
|
135
|
+
else if (c === "?") out += ".";
|
|
136
|
+
else out += _escapeRe(c);
|
|
137
|
+
}
|
|
138
|
+
out += "$";
|
|
139
|
+
return new RegExp(out, caseInsensitive ? "i" : ""); // allow:dynamic-regex — built from operator Sieve `:matches` pattern; every meta-char except `*`/`?` is regex-escaped, so the resulting NFA is linear in input length (no polynomial-backtrack surface)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function _matches(haystack, needle, matchType, comparator) {
|
|
143
|
+
var ci = (comparator === "i;ascii-casemap");
|
|
144
|
+
if (matchType === "is") {
|
|
145
|
+
return ci
|
|
146
|
+
? haystack.toLowerCase() === needle.toLowerCase()
|
|
147
|
+
: haystack === needle;
|
|
148
|
+
}
|
|
149
|
+
if (matchType === "contains") {
|
|
150
|
+
return ci
|
|
151
|
+
? haystack.toLowerCase().indexOf(needle.toLowerCase()) !== -1
|
|
152
|
+
: haystack.indexOf(needle) !== -1;
|
|
153
|
+
}
|
|
154
|
+
if (matchType === "matches") {
|
|
155
|
+
return _wildcardToRe(needle, ci).test(haystack);
|
|
156
|
+
}
|
|
157
|
+
throw new MailSieveError("mail-sieve/bad-match-type",
|
|
158
|
+
"unknown match-type: " + matchType);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _anyOfMatches(haystackList, needleList, matchType, comparator) {
|
|
162
|
+
for (var i = 0; i < haystackList.length; i++) {
|
|
163
|
+
for (var j = 0; j < needleList.length; j++) {
|
|
164
|
+
if (_matches(haystackList[i], needleList[j], matchType, comparator)) return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---- tag helpers --------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
function _tagAmong(tags, names, fallback) {
|
|
173
|
+
if (!tags || !tags.length) return fallback;
|
|
174
|
+
for (var i = 0; i < tags.length; i++) {
|
|
175
|
+
if (names.indexOf(tags[i].name) !== -1) return tags[i].name;
|
|
176
|
+
}
|
|
177
|
+
return fallback;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _tagComparator(tags) {
|
|
181
|
+
// `:comparator "i;octet"` — comparator name is the FOLLOWING positional
|
|
182
|
+
// string per §2.7.3. We don't have positional-vs-tag separation in args
|
|
183
|
+
// yet; for v0.9.55 only the tag presence is honored, comparator name
|
|
184
|
+
// pulled from positional[0] only when `:comparator` is present.
|
|
185
|
+
for (var i = 0; i < tags.length; i++) {
|
|
186
|
+
if (tags[i].name === "comparator" && tags[i].val) return tags[i].val;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function _strArg(positional, idx) {
|
|
192
|
+
var p = positional[idx];
|
|
193
|
+
if (!p || (p.kind !== "str" && p.kind !== "list")) return null;
|
|
194
|
+
return p.kind === "str" ? p.v : (p.v[0] || null);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function _listArg(positional, idx) {
|
|
198
|
+
var p = positional[idx];
|
|
199
|
+
if (!p) return null;
|
|
200
|
+
if (p.kind === "list") return p.v;
|
|
201
|
+
if (p.kind === "str") return [p.v];
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---- test evaluation ----------------------------------------------------
|
|
206
|
+
|
|
207
|
+
function _evalTest(test, env, ctx) {
|
|
208
|
+
ctx.gas++;
|
|
209
|
+
if (ctx.gas > ctx.maxGas) {
|
|
210
|
+
throw new MailSieveError("mail-sieve/gas-exhausted",
|
|
211
|
+
"Sieve gas exhausted (cap " + ctx.maxGas + ")");
|
|
212
|
+
}
|
|
213
|
+
var name = test.name;
|
|
214
|
+
|
|
215
|
+
if (name === "true") return true;
|
|
216
|
+
if (name === "false") return false;
|
|
217
|
+
|
|
218
|
+
if (name === "not") {
|
|
219
|
+
return !_evalTest(test.subs[0], env, ctx);
|
|
220
|
+
}
|
|
221
|
+
if (name === "allof") {
|
|
222
|
+
for (var i = 0; i < test.subs.length; i++) {
|
|
223
|
+
if (!_evalTest(test.subs[i], env, ctx)) return false;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (name === "anyof") {
|
|
228
|
+
for (var j = 0; j < test.subs.length; j++) {
|
|
229
|
+
if (_evalTest(test.subs[j], env, ctx)) return true;
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
var args = test.args || { tags: [], positional: [] };
|
|
235
|
+
|
|
236
|
+
if (name === "exists") {
|
|
237
|
+
var headerNames = _listArg(args.positional, 0);
|
|
238
|
+
if (!headerNames) return false;
|
|
239
|
+
for (var h = 0; h < headerNames.length; h++) {
|
|
240
|
+
if (_headerValues(env, headerNames[h]).length === 0) return false;
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (name === "size") {
|
|
246
|
+
var bytes = (env && typeof env.sizeBytes === "number") ? env.sizeBytes :
|
|
247
|
+
(env && env.bodyBytes ? env.bodyBytes.length : 0);
|
|
248
|
+
var num = args.positional[0] && args.positional[0].kind === "num" ?
|
|
249
|
+
args.positional[0].v : 0;
|
|
250
|
+
var mode = _tagAmong(args.tags, ["over", "under"], "over");
|
|
251
|
+
return mode === "over" ? bytes > num : bytes < num;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (name === "header") {
|
|
255
|
+
var matchType = _tagAmong(args.tags, ["is", "contains", "matches"], "is");
|
|
256
|
+
var comparator = _tagComparator(args.tags) || "i;ascii-casemap";
|
|
257
|
+
var hdrNames = _listArg(args.positional, 0) || [];
|
|
258
|
+
var keys = _listArg(args.positional, 1) || [];
|
|
259
|
+
var allValues = [];
|
|
260
|
+
for (var hh = 0; hh < hdrNames.length; hh++) {
|
|
261
|
+
var vs = _headerValues(env, hdrNames[hh]);
|
|
262
|
+
for (var vv = 0; vv < vs.length; vv++) allValues.push(vs[vv]);
|
|
263
|
+
}
|
|
264
|
+
return _anyOfMatches(allValues, keys, matchType, comparator);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (name === "address" || name === "envelope") {
|
|
268
|
+
var matchType2 = _tagAmong(args.tags, ["is", "contains", "matches"], "is");
|
|
269
|
+
var comparator2 = _tagComparator(args.tags) || "i;ascii-casemap";
|
|
270
|
+
var addrPart = _tagAmong(args.tags, ["all", "localpart", "domain"], "all");
|
|
271
|
+
var addrFields = _listArg(args.positional, 0) || [];
|
|
272
|
+
var keys2 = _listArg(args.positional, 1) || [];
|
|
273
|
+
var allAddrs = [];
|
|
274
|
+
for (var af = 0; af < addrFields.length; af++) {
|
|
275
|
+
var fieldName = addrFields[af];
|
|
276
|
+
var values;
|
|
277
|
+
if (name === "envelope") {
|
|
278
|
+
// RFC 5228 §5.4 — only "from" and "to" defined for envelope test.
|
|
279
|
+
var lc = String(fieldName).toLowerCase();
|
|
280
|
+
if (lc !== "from" && lc !== "to") continue;
|
|
281
|
+
values = _envelopeAddresses(env, lc);
|
|
282
|
+
} else {
|
|
283
|
+
values = _headerValues(env, fieldName);
|
|
284
|
+
}
|
|
285
|
+
var parts = _addressesOf(values, addrPart);
|
|
286
|
+
for (var pp = 0; pp < parts.length; pp++) allAddrs.push(parts[pp]);
|
|
287
|
+
}
|
|
288
|
+
return _anyOfMatches(allAddrs, keys2, matchType2, comparator2);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Unknown test — refuse rather than evaluate. The parser already
|
|
292
|
+
// refused any `require` that didn't resolve to KNOWN_CAPABILITIES, so
|
|
293
|
+
// reaching here means a known-capability test we forgot to wire.
|
|
294
|
+
throw new MailSieveError("mail-sieve/unknown-test",
|
|
295
|
+
"Sieve test '" + name + "' is RFC-defined but not wired in v0.9.55");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---- action evaluation --------------------------------------------------
|
|
299
|
+
|
|
300
|
+
function _runCommand(cmd, env, ctx) {
|
|
301
|
+
ctx.gas++;
|
|
302
|
+
if (ctx.gas > ctx.maxGas) {
|
|
303
|
+
throw new MailSieveError("mail-sieve/gas-exhausted",
|
|
304
|
+
"Sieve gas exhausted (cap " + ctx.maxGas + ")");
|
|
305
|
+
}
|
|
306
|
+
if (ctx.stopped) return;
|
|
307
|
+
|
|
308
|
+
if (cmd.kind === "require") return; // parser handled it
|
|
309
|
+
|
|
310
|
+
if (cmd.kind === "if") {
|
|
311
|
+
if (_evalTest(cmd.test, env, ctx)) {
|
|
312
|
+
_runBlock(cmd.thenBody, env, ctx);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
for (var i = 0; i < cmd.elif.length; i++) {
|
|
316
|
+
if (_evalTest(cmd.elif[i].test, env, ctx)) {
|
|
317
|
+
_runBlock(cmd.elif[i].body, env, ctx);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (cmd.elseBody) {
|
|
322
|
+
_runBlock(cmd.elseBody, env, ctx);
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (cmd.kind === "action") {
|
|
328
|
+
var n = cmd.name;
|
|
329
|
+
var pos = cmd.args.positional;
|
|
330
|
+
if (n === "keep") {
|
|
331
|
+
ctx.implicitKeepCancelled = false;
|
|
332
|
+
ctx.actions.push({ kind: "keep" });
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (n === "fileinto") {
|
|
336
|
+
var folder = _strArg(pos, 0);
|
|
337
|
+
if (folder == null) {
|
|
338
|
+
throw new MailSieveError("mail-sieve/bad-fileinto",
|
|
339
|
+
"fileinto requires a folder name");
|
|
340
|
+
}
|
|
341
|
+
ctx.implicitKeepCancelled = true;
|
|
342
|
+
ctx.actions.push({ kind: "fileinto", folder: folder });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (n === "discard") {
|
|
346
|
+
ctx.implicitKeepCancelled = true;
|
|
347
|
+
ctx.actions.push({ kind: "discard" });
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (n === "redirect") {
|
|
351
|
+
var addr = _strArg(pos, 0);
|
|
352
|
+
if (addr == null) {
|
|
353
|
+
throw new MailSieveError("mail-sieve/bad-redirect",
|
|
354
|
+
"redirect requires an address");
|
|
355
|
+
}
|
|
356
|
+
ctx.implicitKeepCancelled = true;
|
|
357
|
+
ctx.actions.push({ kind: "redirect", address: addr });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (n === "stop") {
|
|
361
|
+
ctx.stopped = true;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
throw new MailSieveError("mail-sieve/unknown-action",
|
|
365
|
+
"Sieve action '" + n + "' is not wired in v0.9.55");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
throw new MailSieveError("mail-sieve/bad-command",
|
|
369
|
+
"unknown command kind '" + cmd.kind + "'");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function _runBlock(commands, env, ctx) {
|
|
373
|
+
for (var i = 0; i < commands.length; i++) {
|
|
374
|
+
if (ctx.stopped) return;
|
|
375
|
+
_runCommand(commands[i], env, ctx);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* @primitive b.mail.sieve.run
|
|
381
|
+
* @signature b.mail.sieve.run(ast, env, opts?)
|
|
382
|
+
* @since 0.9.55
|
|
383
|
+
* @status stable
|
|
384
|
+
* @related b.safeSieve.parse, b.mail.agent.create
|
|
385
|
+
*
|
|
386
|
+
* Walk a parsed Sieve AST against the message environment + return the
|
|
387
|
+
* ordered action list. The interpreter is pure — it reads only from
|
|
388
|
+
* `env` and never mutates it; every action surfaces as an entry in
|
|
389
|
+
* the returned list for the caller to dispatch.
|
|
390
|
+
*
|
|
391
|
+
* @opts
|
|
392
|
+
* maxGas: number, // default 10000; cap 1_000_000
|
|
393
|
+
*
|
|
394
|
+
* @example
|
|
395
|
+
* var ast = b.safeSieve.parse('if header :contains "X-Spam" "yes" { fileinto "Junk"; }');
|
|
396
|
+
* var rv = b.mail.sieve.run(ast, {
|
|
397
|
+
* headers: [{ name: "X-Spam", value: "yes" }],
|
|
398
|
+
* envelope: { from: "sender@example.com", to: "rcpt@example.com" },
|
|
399
|
+
* sizeBytes: 1024,
|
|
400
|
+
* });
|
|
401
|
+
* // → { actions: [{ kind: "fileinto", folder: "Junk" }, { kind: "keep" }], gas: 3, stopped: false }
|
|
402
|
+
*/
|
|
403
|
+
function run(ast, env, opts) {
|
|
404
|
+
if (!ast || ast.kind !== "script") {
|
|
405
|
+
throw new MailSieveError("mail-sieve/bad-ast",
|
|
406
|
+
"mail.sieve.run: ast must be a parsed Sieve script (b.safeSieve.parse output)");
|
|
407
|
+
}
|
|
408
|
+
opts = opts || {};
|
|
409
|
+
var maxGas = opts.maxGas === undefined ? DEFAULT_GAS_UNITS : opts.maxGas;
|
|
410
|
+
if (!numericBounds.isPositiveFiniteInt(maxGas)) {
|
|
411
|
+
throw new MailSieveError("mail-sieve/bad-opt",
|
|
412
|
+
"maxGas must be a positive finite integer; got " + numericBounds.shape(maxGas));
|
|
413
|
+
}
|
|
414
|
+
if (maxGas > MAX_GAS_UNITS) {
|
|
415
|
+
throw new MailSieveError("mail-sieve/bad-opt",
|
|
416
|
+
"mail.sieve.run: maxGas " + maxGas + " exceeds cap " + MAX_GAS_UNITS);
|
|
417
|
+
}
|
|
418
|
+
var ctx = {
|
|
419
|
+
gas: 0,
|
|
420
|
+
maxGas: maxGas,
|
|
421
|
+
actions: [],
|
|
422
|
+
stopped: false,
|
|
423
|
+
implicitKeepCancelled: false,
|
|
424
|
+
};
|
|
425
|
+
_runBlock(ast.commands, env || {}, ctx);
|
|
426
|
+
// RFC 5228 §2.10.2 — if no explicit `keep` / `fileinto` / `discard` /
|
|
427
|
+
// `redirect` fired, implicit keep applies.
|
|
428
|
+
if (!ctx.implicitKeepCancelled) {
|
|
429
|
+
var sawExplicitKeep = false;
|
|
430
|
+
for (var i = 0; i < ctx.actions.length; i++) {
|
|
431
|
+
if (ctx.actions[i].kind === "keep") { sawExplicitKeep = true; break; }
|
|
432
|
+
}
|
|
433
|
+
if (!sawExplicitKeep) ctx.actions.push({ kind: "keep", implicit: true });
|
|
434
|
+
}
|
|
435
|
+
return { actions: ctx.actions, gas: ctx.gas, stopped: ctx.stopped };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* @primitive b.mail.sieve.runScript
|
|
440
|
+
* @signature b.mail.sieve.runScript(script, env, opts?)
|
|
441
|
+
* @since 0.9.55
|
|
442
|
+
* @status stable
|
|
443
|
+
* @related b.mail.sieve.run, b.safeSieve.parse
|
|
444
|
+
*
|
|
445
|
+
* Parse + run in one call. Most call sites — JMAP `SieveScript/validate`,
|
|
446
|
+
* MX delivery hook — want this shape.
|
|
447
|
+
*
|
|
448
|
+
* @opts
|
|
449
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
450
|
+
* compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
451
|
+
* maxGas: number,
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* var rv = b.mail.sieve.runScript(
|
|
455
|
+
* 'require ["fileinto"];\nif header :is "From" "boss@x.com" { fileinto "Important"; }',
|
|
456
|
+
* { headers: [{ name: "From", value: "boss@x.com" }] }
|
|
457
|
+
* );
|
|
458
|
+
* rv.actions[0].folder; // → "Important"
|
|
459
|
+
*/
|
|
460
|
+
function runScript(script, env, opts) {
|
|
461
|
+
var ast = safeSieve.parse(script, opts);
|
|
462
|
+
return run(ast, env, opts);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @primitive b.mail.sieve.create
|
|
467
|
+
* @signature b.mail.sieve.create(opts?)
|
|
468
|
+
* @since 0.9.55
|
|
469
|
+
* @status stable
|
|
470
|
+
* @related b.mail.sieve.run, b.mail.agent.create
|
|
471
|
+
*
|
|
472
|
+
* Returns a stateful Sieve handle the delivery agent + JMAP
|
|
473
|
+
* `SieveScript/validate` method compose. Distinct from the bare
|
|
474
|
+
* `b.mail.sieve.run(ast, env)` entry — the handle carries operator-
|
|
475
|
+
* supplied opts (`maxGas`, `profile`, `compliancePosture`, `audit`)
|
|
476
|
+
* so every invocation runs with the same posture.
|
|
477
|
+
*
|
|
478
|
+
* @opts
|
|
479
|
+
* maxGas: number,
|
|
480
|
+
* profile: "strict" | "balanced" | "permissive",
|
|
481
|
+
* compliancePosture: "hipaa" | "pci-dss" | "gdpr" | "soc2",
|
|
482
|
+
* audit: { safeEmit: function },
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* var sieve = b.mail.sieve.create({ profile: "strict", audit: b.audit });
|
|
486
|
+
* sieve.validateScript(operatorScript);
|
|
487
|
+
* var rv = await sieve.runScript(operatorScript, mailEnv);
|
|
488
|
+
*/
|
|
489
|
+
function create(opts) {
|
|
490
|
+
opts = validateOpts.requireObject(opts || {}, "mail.sieve.create",
|
|
491
|
+
MailSieveError, "mail-sieve/bad-opt");
|
|
492
|
+
var maxGas = opts.maxGas === undefined ? DEFAULT_GAS_UNITS : opts.maxGas;
|
|
493
|
+
if (!numericBounds.isPositiveFiniteInt(maxGas)) {
|
|
494
|
+
throw new MailSieveError("mail-sieve/bad-opt",
|
|
495
|
+
"maxGas must be a positive finite integer; got " + numericBounds.shape(maxGas));
|
|
496
|
+
}
|
|
497
|
+
if (maxGas > MAX_GAS_UNITS) {
|
|
498
|
+
throw new MailSieveError("mail-sieve/bad-opt",
|
|
499
|
+
"mail.sieve.create: maxGas " + maxGas + " exceeds cap " + MAX_GAS_UNITS);
|
|
500
|
+
}
|
|
501
|
+
var profile = opts.profile;
|
|
502
|
+
var posture = opts.compliancePosture;
|
|
503
|
+
var audit = opts.audit;
|
|
504
|
+
|
|
505
|
+
function _emit(action, outcome, metadata) {
|
|
506
|
+
if (!audit || typeof audit.safeEmit !== "function") return;
|
|
507
|
+
try {
|
|
508
|
+
audit.safeEmit({
|
|
509
|
+
action: action,
|
|
510
|
+
outcome: outcome || "success",
|
|
511
|
+
metadata: metadata || {},
|
|
512
|
+
});
|
|
513
|
+
} catch (_e) { /* drop-silent */ }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function validateScript(script) {
|
|
517
|
+
var rv = safeSieve.validate(script, { profile: profile, compliancePosture: posture });
|
|
518
|
+
if (rv.ok) {
|
|
519
|
+
_emit("mail.sieve.validate", "success", { requiredCaps: rv.requiredCaps });
|
|
520
|
+
} else {
|
|
521
|
+
_emit("mail.sieve.validate", "failure", { issues: rv.issues });
|
|
522
|
+
}
|
|
523
|
+
return rv;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function runScript_(script, env) {
|
|
527
|
+
var ast = safeSieve.parse(script, { profile: profile, compliancePosture: posture });
|
|
528
|
+
var rv = run(ast, env, { maxGas: maxGas });
|
|
529
|
+
_emit("mail.sieve.run", "success", {
|
|
530
|
+
gas: rv.gas, actionCount: rv.actions.length, stopped: rv.stopped,
|
|
531
|
+
});
|
|
532
|
+
return rv;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function runAst(ast, env) {
|
|
536
|
+
var rv = run(ast, env, { maxGas: maxGas });
|
|
537
|
+
_emit("mail.sieve.run", "success", {
|
|
538
|
+
gas: rv.gas, actionCount: rv.actions.length, stopped: rv.stopped,
|
|
539
|
+
});
|
|
540
|
+
return rv;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
validateScript: validateScript,
|
|
545
|
+
runScript: runScript_,
|
|
546
|
+
run: runAst,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
module.exports = {
|
|
551
|
+
create: create,
|
|
552
|
+
run: run,
|
|
553
|
+
runScript: runScript,
|
|
554
|
+
MailSieveError: MailSieveError,
|
|
555
|
+
DEFAULT_GAS_UNITS: DEFAULT_GAS_UNITS,
|
|
556
|
+
MAX_GAS_UNITS: MAX_GAS_UNITS,
|
|
557
|
+
};
|