@blamejs/core 0.10.6 → 0.10.8
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 +2 -0
- package/index.js +8 -1
- package/lib/ai-content-detect.js +268 -0
- package/lib/ai-input.js +58 -8
- package/lib/ai-model-manifest.js +363 -0
- package/lib/atomic-file.js +83 -0
- package/lib/audit.js +3 -0
- package/lib/content-credentials.js +140 -0
- package/lib/crypto.js +30 -0
- package/lib/external-db.js +2 -2
- package/lib/guard-dsn.js +8 -5
- package/lib/guard-list-id.js +14 -10
- package/lib/guard-list-unsubscribe.js +67 -1
- package/lib/guard-message-id.js +26 -0
- package/lib/mail-arc-sign.js +21 -1
- package/lib/mail-auth.js +2 -1
- package/lib/mail-dkim.js +50 -9
- package/lib/mail-server-imap.js +104 -10
- package/lib/mail-server-mx.js +94 -14
- package/lib/mail-server-submission.js +135 -1
- package/lib/mail-store.js +6 -1
- package/lib/network-dns-resolver.js +1 -2
- package/lib/network-dns.js +4 -4
- package/lib/promise-pool.js +162 -0
- package/lib/safe-mime.js +51 -4
- package/lib/sd-notify.js +269 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/mail-server-mx.js
CHANGED
|
@@ -236,6 +236,13 @@ function create(opts) {
|
|
|
236
236
|
var localDomains = (opts.localDomains || []).map(function (d) { return String(d).toLowerCase(); });
|
|
237
237
|
var relayAllowedFor = opts.relayAllowedFor || [];
|
|
238
238
|
var profile = opts.profile || "strict";
|
|
239
|
+
// SMTPUTF8 (RFC 6531) — single switch threaded end-to-end. The MX
|
|
240
|
+
// listener doesn't advertise SMTPUTF8 to the peer regardless, so
|
|
241
|
+
// this defaults `false` (refuse non-ASCII bytes in every command
|
|
242
|
+
// line). Operators that want to accept SMTPUTF8 for downstream
|
|
243
|
+
// relay flip this `true` and the same switch reaches every
|
|
244
|
+
// `guardSmtpCommand.validate` call.
|
|
245
|
+
var allowSmtpUtf8 = opts.allowSmtpUtf8 === true;
|
|
239
246
|
|
|
240
247
|
// Default-on per-IP rate limit. Operators pass `rateLimit: false` to
|
|
241
248
|
// disable (only for tests / closed networks), pass a rate-limit
|
|
@@ -331,6 +338,15 @@ function create(opts) {
|
|
|
331
338
|
var connectionId = "mxconn-" + bCrypto.generateToken(8); // allow:raw-byte-literal — connection-id length
|
|
332
339
|
connections.add(socket);
|
|
333
340
|
|
|
341
|
+
// Backpressure observer — `_writeReply` flips `_bpEmitted` after
|
|
342
|
+
// the first audit emission per socket to bound the audit volume.
|
|
343
|
+
socket._bpEmit = function () {
|
|
344
|
+
_emit("mail.server.mx.write_backpressure",
|
|
345
|
+
{ connectionId: connectionId, remoteAddress: remoteAddress,
|
|
346
|
+
stage: state && state.stage, bufferedBytes: socket.writableLength || 0 },
|
|
347
|
+
"warning");
|
|
348
|
+
};
|
|
349
|
+
|
|
334
350
|
var state = {
|
|
335
351
|
id: connectionId,
|
|
336
352
|
remoteAddress: remoteAddress,
|
|
@@ -452,7 +468,11 @@ function create(opts) {
|
|
|
452
468
|
// Per-line guard — refuse bare LF / NUL / C0 / DEL / oversize
|
|
453
469
|
// BEFORE state-machine dispatch.
|
|
454
470
|
try {
|
|
455
|
-
guardSmtpCommand.validate(line, {
|
|
471
|
+
guardSmtpCommand.validate(line, {
|
|
472
|
+
profile: profile,
|
|
473
|
+
maxLineBytes: maxLineBytes,
|
|
474
|
+
allowSmtpUtf8: allowSmtpUtf8,
|
|
475
|
+
});
|
|
456
476
|
} catch (err) {
|
|
457
477
|
if (err.code === "guard-smtp-command/bare-lf" ||
|
|
458
478
|
err.code === "guard-smtp-command/bare-cr" ||
|
|
@@ -633,17 +653,19 @@ function create(opts) {
|
|
|
633
653
|
}
|
|
634
654
|
var paramStr = match[2] || "";
|
|
635
655
|
var sizeMatch = paramStr.match(RE_SIZE);
|
|
656
|
+
var declaredSize = null;
|
|
636
657
|
if (sizeMatch) {
|
|
637
|
-
|
|
658
|
+
declaredSize = parseInt(sizeMatch[1], 10);
|
|
638
659
|
if (declaredSize > maxMessageBytes) {
|
|
639
660
|
_writeReply(socket, REPLY_552_SIZE_EXCEEDED,
|
|
640
661
|
"5.3.4 Message size exceeds fixed maximum (" + maxMessageBytes + " bytes)");
|
|
641
662
|
return;
|
|
642
663
|
}
|
|
643
664
|
}
|
|
644
|
-
state.mailFrom
|
|
645
|
-
state.
|
|
646
|
-
state.
|
|
665
|
+
state.mailFrom = mailFrom;
|
|
666
|
+
state.declaredSize = declaredSize;
|
|
667
|
+
state.stage = "rcpt";
|
|
668
|
+
state.rcpts = [];
|
|
647
669
|
_emit("mail.server.mx.mail_from",
|
|
648
670
|
{ connectionId: state.id, mailFrom: mailFrom });
|
|
649
671
|
_writeReply(socket, REPLY_250_OK, "2.1.0 Sender OK");
|
|
@@ -692,6 +714,7 @@ function create(opts) {
|
|
|
692
714
|
var rcptVerdict = _validateDomainHardened(rcptDomain, "rcpt_to");
|
|
693
715
|
if (!rcptVerdict.ok) {
|
|
694
716
|
rateLimit.noteRcptFailure(state.remoteAddress);
|
|
717
|
+
_trackRefusedRcpt(state, rcpt, "domain-refused");
|
|
695
718
|
_writeReply(socket, REPLY_501_BAD_ARGS,
|
|
696
719
|
"5.5.4 RCPT TO domain refused (" +
|
|
697
720
|
(rcptVerdict.issues && rcptVerdict.issues[0] && rcptVerdict.issues[0].kind) + ")");
|
|
@@ -704,6 +727,7 @@ function create(opts) {
|
|
|
704
727
|
if (localDomains.indexOf(rcptDomain) === -1 &&
|
|
705
728
|
!_isRelayAllowed(state.remoteAddress, rcpt)) {
|
|
706
729
|
rateLimit.noteRcptFailure(state.remoteAddress);
|
|
730
|
+
_trackRefusedRcpt(state, rcpt, "relay-denied");
|
|
707
731
|
_emit("mail.server.mx.relay_refused",
|
|
708
732
|
{ connectionId: state.id, mailFrom: state.mailFrom, rcptTo: rcpt,
|
|
709
733
|
remoteAddress: state.remoteAddress }, "denied");
|
|
@@ -739,9 +763,32 @@ function create(opts) {
|
|
|
739
763
|
// body is the raw bytes BEFORE dot-stuffing reversal. RFC 5321
|
|
740
764
|
// §4.5.2 — a single leading "." is doubled on the wire; undo.
|
|
741
765
|
var dedotted = safeSmtp.dotUnstuff(body);
|
|
766
|
+
// RFC 1870 §6.3 — reconcile MAIL FROM SIZE= against the actual
|
|
767
|
+
// DATA byte count. The pre-DATA reservation at MAIL FROM time
|
|
768
|
+
// (above) is advisory; the sender's declared size is a HINT,
|
|
769
|
+
// not a guarantee. If the actual unstuffed body exceeds the
|
|
770
|
+
// declared SIZE= (with a small slack to absorb header lines the
|
|
771
|
+
// sender didn't count), refuse with 552 — defends against
|
|
772
|
+
// senders that probe maxMessageBytes by understating SIZE.
|
|
773
|
+
if (typeof state.declaredSize === "number" && isFinite(state.declaredSize)) {
|
|
774
|
+
if (dedotted.length > state.declaredSize) {
|
|
775
|
+
_emit("mail.server.mx.size_overrun", {
|
|
776
|
+
connectionId: state.id,
|
|
777
|
+
mailFrom: state.mailFrom,
|
|
778
|
+
declaredSize: state.declaredSize,
|
|
779
|
+
actualSize: dedotted.length,
|
|
780
|
+
}, "denied");
|
|
781
|
+
_writeReply(socket, REPLY_552_SIZE_EXCEEDED,
|
|
782
|
+
"5.3.4 Message exceeds declared SIZE=" + state.declaredSize +
|
|
783
|
+
" bytes (got " + dedotted.length + "; RFC 1870 §6.3)");
|
|
784
|
+
_resetTransaction(state);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
742
788
|
// operator-supplied agent handoff — when wired, persist via
|
|
743
789
|
// agent + write the 250 reply. When not wired, accept-and-drop
|
|
744
790
|
// (audit-only mode useful for staging deployments).
|
|
791
|
+
var refusedSnapshot = Array.isArray(state.refusedRcpts) ? state.refusedRcpts.slice() : [];
|
|
745
792
|
if (opts.agent && typeof opts.agent.handoff === "function") {
|
|
746
793
|
opts.agent.handoff({
|
|
747
794
|
mailFrom: state.mailFrom,
|
|
@@ -753,7 +800,8 @@ function create(opts) {
|
|
|
753
800
|
connectionId: state.id,
|
|
754
801
|
}).then(function (ack) {
|
|
755
802
|
_emit("mail.server.mx.delivered",
|
|
756
|
-
{ connectionId: state.id, messageId: ack && ack.messageId,
|
|
803
|
+
{ connectionId: state.id, messageId: ack && ack.messageId,
|
|
804
|
+
sizeBytes: dedotted.length, refusedRcpts: refusedSnapshot });
|
|
757
805
|
_writeReply(socket, REPLY_250_OK,
|
|
758
806
|
"2.6.0 Message accepted" + (ack && ack.messageId ? " <" + ack.messageId + ">" : ""));
|
|
759
807
|
_resetTransaction(state);
|
|
@@ -769,18 +817,32 @@ function create(opts) {
|
|
|
769
817
|
}
|
|
770
818
|
_emit("mail.server.mx.data_accepted",
|
|
771
819
|
{ connectionId: state.id, mailFrom: state.mailFrom, rcptCount: state.rcpts.length,
|
|
772
|
-
sizeBytes: dedotted.length });
|
|
820
|
+
sizeBytes: dedotted.length, refusedRcpts: refusedSnapshot });
|
|
773
821
|
_writeReply(socket, REPLY_250_OK, "2.6.0 Message queued (audit-only)");
|
|
774
822
|
_resetTransaction(state);
|
|
775
823
|
}
|
|
776
824
|
|
|
777
825
|
function _resetTransaction(state) {
|
|
778
|
-
state.mailFrom
|
|
779
|
-
state.
|
|
780
|
-
state.
|
|
826
|
+
state.mailFrom = null;
|
|
827
|
+
state.declaredSize = null;
|
|
828
|
+
state.rcpts = [];
|
|
829
|
+
state.refusedRcpts = [];
|
|
830
|
+
state.stage = "ehlo";
|
|
781
831
|
state.messageBytes = 0;
|
|
782
832
|
}
|
|
783
833
|
|
|
834
|
+
// Track up to MAX_REFUSED_RCPTS_PER_TXN refused recipients so the
|
|
835
|
+
// `data_accepted` / `delivered` audit can surface the bounded list
|
|
836
|
+
// for observability. Bounded to keep the audit metadata size
|
|
837
|
+
// predictable; the per-IP recipient-failure rate-limit elsewhere
|
|
838
|
+
// bounds long-run scanner damage.
|
|
839
|
+
var MAX_REFUSED_RCPTS_PER_TXN = 32; // allow:raw-byte-literal — bounded audit-metadata list cap
|
|
840
|
+
function _trackRefusedRcpt(state, rcpt, reason) {
|
|
841
|
+
if (!Array.isArray(state.refusedRcpts)) state.refusedRcpts = [];
|
|
842
|
+
if (state.refusedRcpts.length >= MAX_REFUSED_RCPTS_PER_TXN) return;
|
|
843
|
+
state.refusedRcpts.push({ rcptTo: rcpt, reason: reason });
|
|
844
|
+
}
|
|
845
|
+
|
|
784
846
|
function _requiresStartTls() {
|
|
785
847
|
// Strict / balanced require STARTTLS before MAIL FROM.
|
|
786
848
|
// Permissive accepts plaintext — operator-acknowledged downgrade
|
|
@@ -857,10 +919,26 @@ function create(opts) {
|
|
|
857
919
|
|
|
858
920
|
// ---- Wire-protocol helpers --------------------------------------------------
|
|
859
921
|
|
|
922
|
+
// Write back-pressure observability — when `socket.write()` returns
|
|
923
|
+
// false the kernel send-buffer is full and the server is dropping
|
|
924
|
+
// behind the network. Listeners attach a `_bpEmit` function to the
|
|
925
|
+
// socket; we invoke it once per socket-lifetime on the first
|
|
926
|
+
// backpressure event so the audit log surfaces stalled connections
|
|
927
|
+
// without flooding on every reply.
|
|
928
|
+
function _observeBackpressure(socket, ok) {
|
|
929
|
+
if (ok) return;
|
|
930
|
+
if (typeof socket._bpEmit !== "function") return;
|
|
931
|
+
if (socket._bpEmitted) return;
|
|
932
|
+
socket._bpEmitted = true;
|
|
933
|
+
try { socket._bpEmit(socket); } catch (_e) { /* drop-silent */ }
|
|
934
|
+
}
|
|
935
|
+
|
|
860
936
|
function _writeReply(socket, code, text) {
|
|
861
937
|
// Single-line reply per RFC 5321 §4.2 — code SP text CRLF.
|
|
862
|
-
try {
|
|
863
|
-
|
|
938
|
+
try {
|
|
939
|
+
var ok = socket.write(code + " " + text + "\r\n");
|
|
940
|
+
_observeBackpressure(socket, ok);
|
|
941
|
+
} catch (_e) { /* socket already closed */ }
|
|
864
942
|
}
|
|
865
943
|
|
|
866
944
|
function _writeMultiline(socket, code, lines) {
|
|
@@ -868,8 +946,10 @@ function _writeMultiline(socket, code, lines) {
|
|
|
868
946
|
// continuation, code SP text CRLF for the final line.
|
|
869
947
|
for (var i = 0; i < lines.length; i += 1) {
|
|
870
948
|
var sep = i === lines.length - 1 ? " " : "-";
|
|
871
|
-
try {
|
|
872
|
-
|
|
949
|
+
try {
|
|
950
|
+
var ok = socket.write(code + sep + lines[i] + "\r\n");
|
|
951
|
+
_observeBackpressure(socket, ok);
|
|
952
|
+
} catch (_e) { /* socket already closed */ }
|
|
873
953
|
}
|
|
874
954
|
}
|
|
875
955
|
|
|
@@ -153,6 +153,69 @@ var RE_RCPT_TO = /^RCPT\s+TO:\s*<([^>]+)>(?:\s+.*)?$/i;
|
|
|
153
153
|
var RE_SIZE = /SIZE=(\d+)/i;
|
|
154
154
|
var RE_AUTH = /^AUTH\s+([A-Za-z0-9_-]{1,32})(?:\s+(.*))?$/i;
|
|
155
155
|
|
|
156
|
+
// Header/body boundary scanner. RFC 5322 §2.1 — header section ends
|
|
157
|
+
// at the first empty line (CRLF CRLF). `Buffer#indexOf` runs a
|
|
158
|
+
// SIMD-accelerated needle scan over the haystack without an
|
|
159
|
+
// interpreter-level char-by-char walk, and the 4-byte literal
|
|
160
|
+
// `_CRLF_CRLF` is a module-level singleton so the JIT folds it.
|
|
161
|
+
var _CRLF_CRLF = Buffer.from([0x0d, 0x0a, 0x0d, 0x0a]); // allow:raw-byte-literal — RFC 5322 §2.1 header/body separator
|
|
162
|
+
function _findHeaderEnd(buf) {
|
|
163
|
+
return buf.indexOf(_CRLF_CRLF);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Walk a header block and return every unfolded `DKIM-Signature:`
|
|
167
|
+
// value. RFC 5322 §2.2.3 / RFC 6376 §3.5 — DKIM signatures are
|
|
168
|
+
// permitted to fold and a message MAY carry multiple signatures.
|
|
169
|
+
function _extractDkimSignatures(headerBlock) {
|
|
170
|
+
var lines = headerBlock.replace(/\r\n/g, "\n").split("\n"); // allow:regex-no-length-cap — headerBlock length bounded by maxMessageBytes
|
|
171
|
+
var result = [];
|
|
172
|
+
var current = null;
|
|
173
|
+
for (var i = 0; i < lines.length; i += 1) {
|
|
174
|
+
var line = lines[i];
|
|
175
|
+
if (line.length === 0) break; // end of header block
|
|
176
|
+
if (line.charAt(0) === " " || line.charAt(0) === "\t") {
|
|
177
|
+
if (current !== null) current += " " + line.replace(/^[ \t]+/, ""); // allow:regex-no-length-cap — line length bounded by maxLineBytes // allow:duplicate-regex — RFC 5322 header continuation trim
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (current !== null) {
|
|
181
|
+
result.push(current);
|
|
182
|
+
current = null;
|
|
183
|
+
}
|
|
184
|
+
if (/^DKIM-Signature\s*:/i.test(line)) { // allow:regex-no-length-cap — line length bounded by maxLineBytes
|
|
185
|
+
current = line.slice(line.indexOf(":") + 1).replace(/^\s+/, ""); // allow:regex-no-length-cap — line length bounded by maxLineBytes // allow:duplicate-regex — leading-WS trim
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (current !== null) result.push(current);
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Pull the `d=` (signing domain) tag out of a DKIM-Signature value.
|
|
193
|
+
// RFC 6376 §3.5 — tag-list `tag=value` separated by `;`. Returns
|
|
194
|
+
// null if not present.
|
|
195
|
+
function _extractDkimDTag(sigValue) {
|
|
196
|
+
var tags = sigValue.split(";");
|
|
197
|
+
for (var i = 0; i < tags.length; i += 1) {
|
|
198
|
+
var t = tags[i].replace(/^\s+|\s+$/g, ""); // allow:regex-no-length-cap — tag length bounded by header line cap // allow:duplicate-regex — trim shape
|
|
199
|
+
if (t.length > 2 && t.charAt(0) === "d" && t.charAt(1) === "=") {
|
|
200
|
+
return t.slice(2).replace(/\s+/g, ""); // allow:regex-no-length-cap — value length bounded by tag length // allow:duplicate-regex — internal-WS strip
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Domain part of the authenticated identity, falling back to the
|
|
207
|
+
// envelope-sender domain when the actor doesn't carry one.
|
|
208
|
+
function _actorDomain(actor, mailFrom) {
|
|
209
|
+
if (actor && typeof actor.domain === "string" && actor.domain.length > 0) return actor.domain;
|
|
210
|
+
if (actor && typeof actor.id === "string" && actor.id.indexOf("@") !== -1) {
|
|
211
|
+
return actor.id.slice(actor.id.lastIndexOf("@") + 1);
|
|
212
|
+
}
|
|
213
|
+
if (typeof mailFrom === "string" && mailFrom.indexOf("@") !== -1) {
|
|
214
|
+
return mailFrom.slice(mailFrom.lastIndexOf("@") + 1);
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
156
219
|
/**
|
|
157
220
|
* @primitive b.mail.server.submission.create
|
|
158
221
|
* @signature b.mail.server.submission.create(opts)
|
|
@@ -208,6 +271,30 @@ function create(opts) {
|
|
|
208
271
|
"mail.server.submission.", MailServerSubmissionError, "mail-server-submission/bad-bound");
|
|
209
272
|
|
|
210
273
|
var profile = opts.profile || "strict";
|
|
274
|
+
// SMTPUTF8 (RFC 6531) — single switch threaded end-to-end into
|
|
275
|
+
// `guardSmtpCommand.validate`. Defaults `false`; submission
|
|
276
|
+
// operators that accept EAI envelopes flip this `true`.
|
|
277
|
+
var allowSmtpUtf8 = opts.allowSmtpUtf8 === true;
|
|
278
|
+
|
|
279
|
+
// Outbound DKIM-required gate (Yahoo / Google 2024 bulk-sender
|
|
280
|
+
// alignment + RFC 6376 §1). Under `strict` profile the listener
|
|
281
|
+
// refuses outbound DATA that doesn't carry at least one
|
|
282
|
+
// `DKIM-Signature:` header; `dkimRequireMode` chooses whether the
|
|
283
|
+
// signer must match the authenticated identity's domain (`self`)
|
|
284
|
+
// or just be present (`any`). Operators that act as a smarthost
|
|
285
|
+
// relay for downstream MTAs that DKIM-sign themselves want `any`;
|
|
286
|
+
// primary senders want `self`. Default-off outside strict so
|
|
287
|
+
// unauthenticated `permissive` profiles don't break.
|
|
288
|
+
var requireDkim = opts.requireDkim === undefined
|
|
289
|
+
? (profile === "strict")
|
|
290
|
+
: opts.requireDkim === true;
|
|
291
|
+
var dkimRequireMode = opts.dkimRequireMode || "any";
|
|
292
|
+
if (dkimRequireMode !== "self" && dkimRequireMode !== "any" && dkimRequireMode !== "off") {
|
|
293
|
+
throw new MailServerSubmissionError("mail-server-submission/bad-dkim-require-mode",
|
|
294
|
+
"mail.server.submission.create: dkimRequireMode must be 'self', 'any', or 'off' (got '" +
|
|
295
|
+
dkimRequireMode + "')");
|
|
296
|
+
}
|
|
297
|
+
if (dkimRequireMode === "off") requireDkim = false;
|
|
211
298
|
|
|
212
299
|
if (profile !== "permissive" && !opts.auth) {
|
|
213
300
|
throw new MailServerSubmissionError("mail-server-submission/no-auth",
|
|
@@ -424,7 +511,11 @@ function create(opts) {
|
|
|
424
511
|
|
|
425
512
|
// guardSmtpCommand check (smuggling + shape).
|
|
426
513
|
try {
|
|
427
|
-
guardSmtpCommand.validate(line, {
|
|
514
|
+
guardSmtpCommand.validate(line, {
|
|
515
|
+
profile: profile,
|
|
516
|
+
maxLineBytes: maxLineBytes,
|
|
517
|
+
allowSmtpUtf8: allowSmtpUtf8,
|
|
518
|
+
});
|
|
428
519
|
} catch (err) {
|
|
429
520
|
if (err.code === "guard-smtp-command/bare-lf" ||
|
|
430
521
|
err.code === "guard-smtp-command/bare-cr" ||
|
|
@@ -911,6 +1002,49 @@ function create(opts) {
|
|
|
911
1002
|
|
|
912
1003
|
function _finalizeDataBody(state, socket, body) {
|
|
913
1004
|
var dedotted = safeSmtp.dotUnstuff(body);
|
|
1005
|
+
|
|
1006
|
+
// Outbound DKIM-required gate. Scan the header block for a
|
|
1007
|
+
// `DKIM-Signature:` line; under `self` mode also require at
|
|
1008
|
+
// least one signature whose `d=` tag matches the authenticated
|
|
1009
|
+
// identity's domain part.
|
|
1010
|
+
if (requireDkim) {
|
|
1011
|
+
var headerEnd = _findHeaderEnd(dedotted);
|
|
1012
|
+
var headerBlock = headerEnd === -1
|
|
1013
|
+
? dedotted.toString("utf8")
|
|
1014
|
+
: dedotted.subarray(0, headerEnd).toString("utf8");
|
|
1015
|
+
var dkimSigs = _extractDkimSignatures(headerBlock);
|
|
1016
|
+
var dkimOk = false;
|
|
1017
|
+
if (dkimSigs.length > 0) {
|
|
1018
|
+
if (dkimRequireMode === "any") {
|
|
1019
|
+
dkimOk = true;
|
|
1020
|
+
} else if (dkimRequireMode === "self") {
|
|
1021
|
+
var actorDomain = _actorDomain(state.actor, state.mailFrom);
|
|
1022
|
+
for (var i = 0; i < dkimSigs.length; i += 1) {
|
|
1023
|
+
var d = _extractDkimDTag(dkimSigs[i]);
|
|
1024
|
+
if (d && actorDomain && d.toLowerCase() === actorDomain.toLowerCase()) {
|
|
1025
|
+
dkimOk = true;
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (!dkimOk) {
|
|
1032
|
+
_emit("mail.server.submission.data_refused", {
|
|
1033
|
+
connectionId: state.id,
|
|
1034
|
+
reason: "dkim-required",
|
|
1035
|
+
dkimRequireMode: dkimRequireMode,
|
|
1036
|
+
mailFrom: state.mailFrom,
|
|
1037
|
+
sigCount: dkimSigs.length,
|
|
1038
|
+
actor: state.actor && state.actor.id,
|
|
1039
|
+
}, "denied");
|
|
1040
|
+
_writeReply(socket, REPLY_550_MAILBOX_UNAVAIL,
|
|
1041
|
+
"5.7.20 DKIM-Signature required on outbound submission " +
|
|
1042
|
+
"(dkimRequireMode='" + dkimRequireMode + "'; RFC 6376; bulk-sender 2024)");
|
|
1043
|
+
_resetTransaction(state);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
914
1048
|
if (opts.agent && typeof opts.agent.handoff === "function") {
|
|
915
1049
|
opts.agent.handoff({
|
|
916
1050
|
mailFrom: state.mailFrom,
|
package/lib/mail-store.js
CHANGED
|
@@ -368,7 +368,12 @@ function _appendMessage(args) {
|
|
|
368
368
|
});
|
|
369
369
|
|
|
370
370
|
// Allocate objectid + modseq atomically.
|
|
371
|
-
|
|
371
|
+
// RFC 8474 §1.5.1: objectid SHOULD be sufficiently long to make
|
|
372
|
+
// collision improbable across the lifetime of the account. 16-byte
|
|
373
|
+
// token = 32-char hex = 128 bits, well above the birthday bound
|
|
374
|
+
// for any plausible message corpus. The prior `.slice(0, 24)` cut
|
|
375
|
+
// entropy to 96 bits; removed.
|
|
376
|
+
var objectid = "obj_" + bCrypto.generateToken(16); // allow:raw-byte-literal — 16-byte token, 32-char hex JMAP objectid (RFC 8474 §1.5.1)
|
|
372
377
|
var modseq = (folder.modseq_max || 0) + 1;
|
|
373
378
|
if (!threadRootId) threadRootId = objectid; // root of new thread
|
|
374
379
|
|
|
@@ -106,7 +106,6 @@
|
|
|
106
106
|
|
|
107
107
|
var C = require("./constants");
|
|
108
108
|
var https = require("node:https");
|
|
109
|
-
var nodeCrypto = require("node:crypto");
|
|
110
109
|
var bCrypto = require("./crypto");
|
|
111
110
|
var { defineClass } = require("./framework-error");
|
|
112
111
|
var networkDns = require("./network-dns");
|
|
@@ -499,7 +498,7 @@ function _encodeWireQuery(name, qtype) {
|
|
|
499
498
|
var nameLen = 1;
|
|
500
499
|
for (var i = 0; i < parts.length; i += 1) nameLen += 1 + Buffer.byteLength(parts[i], "ascii");
|
|
501
500
|
var buf = Buffer.alloc(12 + nameLen + 4); // allow:raw-byte-literal — RFC 1035 §4.1.1 header (12) + question tail (4) + name
|
|
502
|
-
var id =
|
|
501
|
+
var id = bCrypto.randomInt(0, 0x10000); // allow:raw-byte-literal — RFC 1035 §4.1.1 16-bit query ID space
|
|
503
502
|
buf.writeUInt16BE(id, 0);
|
|
504
503
|
buf.writeUInt16BE(0x0100, 2); // allow:raw-byte-literal — RFC 1035 §4.1.1 RD=1 flags
|
|
505
504
|
buf.writeUInt16BE(1, 4); // allow:raw-byte-literal — RFC 1035 §4.1.1 qdcount
|
package/lib/network-dns.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
var dns = require("node:dns");
|
|
4
4
|
var net = require("node:net");
|
|
5
|
-
var nodeCrypto = require("node:crypto");
|
|
6
5
|
var https = require("node:https");
|
|
7
6
|
var nodeTls = require("node:tls");
|
|
8
7
|
var dnsPromises = dns.promises;
|
|
@@ -274,9 +273,10 @@ function _encodeDnsQuery(host, qtype) {
|
|
|
274
273
|
for (var i = 0; i < parts.length; i++) nameLen += 1 + Buffer.byteLength(parts[i], "ascii");
|
|
275
274
|
var buf = Buffer.alloc(12 + nameLen + 4);
|
|
276
275
|
// Cryptographic RNG for the 16-bit DNS query ID — frustrates poisoning
|
|
277
|
-
// attempts that guess the transaction ID.
|
|
278
|
-
//
|
|
279
|
-
|
|
276
|
+
// attempts that guess the transaction ID. Routes through `b.crypto.randomInt`
|
|
277
|
+
// (which wraps nodeCrypto.randomInt) so every framework random-int draw
|
|
278
|
+
// is greppable through one substrate.
|
|
279
|
+
var id = bCrypto.randomInt(0, 0x10000);
|
|
280
280
|
buf.writeUInt16BE(id, 0);
|
|
281
281
|
buf.writeUInt16BE(0x0100, 2);
|
|
282
282
|
buf.writeUInt16BE(1, 4);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.promisePool
|
|
4
|
+
* @nav Async
|
|
5
|
+
* @title Promise Pool
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* Bounded-concurrency task runner for promise-returning work — the
|
|
9
|
+
* common gap between `b.workerPool` (worker_threads for CPU-bound
|
|
10
|
+
* work) and `b.queue` (durable cross-process messaging). Wraps the
|
|
11
|
+
* typical "I have N parallel I/O fan-outs and want at most K in
|
|
12
|
+
* flight at any moment" pattern with back-pressure on enqueue
|
|
13
|
+
* (so the caller can't out-run the worker side) and a clean drain
|
|
14
|
+
* path that composes with `b.appShutdown`.
|
|
15
|
+
*
|
|
16
|
+
* Two enqueue paths:
|
|
17
|
+
*
|
|
18
|
+
* - `pool.run(taskFn)` returns a Promise that resolves to the
|
|
19
|
+
* task's return value (or rejects with the task's error). When
|
|
20
|
+
* the pool is at capacity, `run` waits until a slot frees
|
|
21
|
+
* BEFORE the task starts — back-pressure is part of the
|
|
22
|
+
* contract, not an opt.
|
|
23
|
+
*
|
|
24
|
+
* - `pool.fire(taskFn)` is the synchronous-enqueue variant for
|
|
25
|
+
* fan-out from non-async contexts. Returns the same Promise
|
|
26
|
+
* but the call itself can't await — useful inside event
|
|
27
|
+
* handlers that fire-and-forget.
|
|
28
|
+
*
|
|
29
|
+
* Drain semantics: `pool.drain()` resolves when every queued and
|
|
30
|
+
* in-flight task settles. Callers wire this into shutdown via
|
|
31
|
+
* `b.appShutdown.create({ priority: 50, run: () => pool.drain() })`
|
|
32
|
+
* so the process doesn't tear down with work mid-flight.
|
|
33
|
+
*
|
|
34
|
+
* The pool does NOT retry failed tasks; rejection of a task's
|
|
35
|
+
* promise is the caller's signal. Operators that want retry compose
|
|
36
|
+
* `b.retry.withRetry` inside the task body.
|
|
37
|
+
*
|
|
38
|
+
* @card
|
|
39
|
+
* Bounded-concurrency promise pool — back-pressure on enqueue, drain-on-shutdown, no hidden retry. The thing every consumer reaches for p-limit for.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
var validateOpts = require("./validate-opts");
|
|
43
|
+
var numericBounds = require("./numeric-bounds");
|
|
44
|
+
var { defineClass } = require("./framework-error");
|
|
45
|
+
|
|
46
|
+
var PromisePoolError = defineClass("PromisePoolError", { alwaysPermanent: true });
|
|
47
|
+
|
|
48
|
+
var MAX_CONCURRENCY = 65536; // allow:raw-byte-literal — uint16 ceiling on parallel I/O fan-out
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @primitive b.promisePool.create
|
|
52
|
+
* @signature b.promisePool.create(opts)
|
|
53
|
+
* @since 0.10.8
|
|
54
|
+
* @status stable
|
|
55
|
+
* @related b.workerPool.create, b.appShutdown.create, b.retry.withRetry
|
|
56
|
+
*
|
|
57
|
+
* Build a bounded-concurrency pool. Returns
|
|
58
|
+
* `{ run, fire, drain, size, inFlight, queued, closed }`. The pool is
|
|
59
|
+
* closed via `drain({ close: true })`; subsequent enqueues throw.
|
|
60
|
+
*
|
|
61
|
+
* @opts
|
|
62
|
+
* concurrency: number, // required; integer in [1, 65536]
|
|
63
|
+
* queueLimit: number, // default Infinity; once exceeded, enqueue throws
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* var pool = b.promisePool.create({ concurrency: 8 });
|
|
67
|
+
* var results = await Promise.all(items.map(function (item) {
|
|
68
|
+
* return pool.run(function () { return fetchOne(item); });
|
|
69
|
+
* }));
|
|
70
|
+
* await pool.drain({ close: true });
|
|
71
|
+
*/
|
|
72
|
+
function create(opts) {
|
|
73
|
+
validateOpts.requireObject(opts, "b.promisePool.create",
|
|
74
|
+
PromisePoolError, "promise-pool/bad-opts");
|
|
75
|
+
numericBounds.requirePositiveFiniteIntIfPresent(opts.concurrency,
|
|
76
|
+
"b.promisePool.create: concurrency", PromisePoolError, "promise-pool/bad-concurrency");
|
|
77
|
+
if (opts.concurrency === undefined || opts.concurrency > MAX_CONCURRENCY) {
|
|
78
|
+
throw new PromisePoolError("promise-pool/bad-concurrency",
|
|
79
|
+
"b.promisePool.create: concurrency must be an integer in [1, " +
|
|
80
|
+
MAX_CONCURRENCY + "] (got " + opts.concurrency + ")");
|
|
81
|
+
}
|
|
82
|
+
var queueLimit = opts.queueLimit === undefined ? Infinity : opts.queueLimit;
|
|
83
|
+
if (queueLimit !== Infinity) {
|
|
84
|
+
numericBounds.requirePositiveFiniteIntIfPresent(queueLimit + 1,
|
|
85
|
+
"b.promisePool.create: queueLimit (must be non-negative int)", PromisePoolError,
|
|
86
|
+
"promise-pool/bad-queue-limit");
|
|
87
|
+
}
|
|
88
|
+
var concurrency = opts.concurrency;
|
|
89
|
+
var inFlight = 0;
|
|
90
|
+
var queue = []; // FIFO of pending { taskFn, resolve, reject }
|
|
91
|
+
var drainWaiters = [];
|
|
92
|
+
var closed = false;
|
|
93
|
+
|
|
94
|
+
function _pump() {
|
|
95
|
+
while (inFlight < concurrency && queue.length > 0) {
|
|
96
|
+
var slot = queue.shift();
|
|
97
|
+
inFlight += 1;
|
|
98
|
+
Promise.resolve().then(function () { return slot.taskFn(); })
|
|
99
|
+
.then(function (val) { slot.resolve(val); _settle(); })
|
|
100
|
+
.catch(function (err) { slot.reject(err); _settle(); });
|
|
101
|
+
}
|
|
102
|
+
if (inFlight === 0 && queue.length === 0 && drainWaiters.length > 0) {
|
|
103
|
+
var waiters = drainWaiters.slice();
|
|
104
|
+
drainWaiters.length = 0;
|
|
105
|
+
for (var i = 0; i < waiters.length; i += 1) waiters[i]();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function _settle() {
|
|
110
|
+
inFlight -= 1;
|
|
111
|
+
_pump();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _enqueue(taskFn) {
|
|
115
|
+
if (typeof taskFn !== "function") {
|
|
116
|
+
throw new PromisePoolError("promise-pool/bad-task",
|
|
117
|
+
"b.promisePool: task must be a function returning a value or Promise");
|
|
118
|
+
}
|
|
119
|
+
if (closed) {
|
|
120
|
+
throw new PromisePoolError("promise-pool/closed",
|
|
121
|
+
"b.promisePool: pool is closed (drain({close:true}) was called)");
|
|
122
|
+
}
|
|
123
|
+
if (queue.length >= queueLimit) {
|
|
124
|
+
throw new PromisePoolError("promise-pool/queue-full",
|
|
125
|
+
"b.promisePool: queueLimit=" + queueLimit + " reached");
|
|
126
|
+
}
|
|
127
|
+
return new Promise(function (resolve, reject) {
|
|
128
|
+
queue.push({ taskFn: taskFn, resolve: resolve, reject: reject });
|
|
129
|
+
_pump();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function run(taskFn) { return _enqueue(taskFn); }
|
|
134
|
+
function fire(taskFn) { return _enqueue(taskFn); }
|
|
135
|
+
|
|
136
|
+
function drain(drainOpts) {
|
|
137
|
+
drainOpts = drainOpts || {};
|
|
138
|
+
return new Promise(function (resolve) {
|
|
139
|
+
function _done() {
|
|
140
|
+
if (drainOpts.close === true) closed = true;
|
|
141
|
+
resolve();
|
|
142
|
+
}
|
|
143
|
+
if (inFlight === 0 && queue.length === 0) { _done(); return; }
|
|
144
|
+
drainWaiters.push(_done);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
run: run,
|
|
150
|
+
fire: fire,
|
|
151
|
+
drain: drain,
|
|
152
|
+
size: function () { return concurrency; },
|
|
153
|
+
inFlight: function () { return inFlight; },
|
|
154
|
+
queued: function () { return queue.length; },
|
|
155
|
+
closed: function () { return closed; },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = {
|
|
160
|
+
create: create,
|
|
161
|
+
PromisePoolError: PromisePoolError,
|
|
162
|
+
};
|