@bumail/queue 0.12.0 → 0.14.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 +65 -4
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +517 -32
- package/dist/index.js.map +23 -14
- package/dist/queue/attempt.d.ts.map +1 -1
- package/dist/queue/deliver.d.ts +2 -0
- package/dist/queue/deliver.d.ts.map +1 -1
- package/dist/queue/events.d.ts +10 -0
- package/dist/queue/events.d.ts.map +1 -1
- package/dist/queue/fallback-session.d.ts.map +1 -1
- package/dist/queue/fallback.d.ts.map +1 -1
- package/dist/queue/options.d.ts +25 -1
- package/dist/queue/options.d.ts.map +1 -1
- package/dist/queue/outcome.d.ts.map +1 -1
- package/dist/queue/session.d.ts +9 -1
- package/dist/queue/session.d.ts.map +1 -1
- package/dist/queue/settings/index.d.ts +8 -2
- package/dist/queue/settings/index.d.ts.map +1 -1
- package/dist/queue/settings/sender.d.ts +16 -0
- package/dist/queue/settings/sender.d.ts.map +1 -0
- package/dist/queue/settle.d.ts.map +1 -1
- package/dist/queue/worker.d.ts.map +1 -1
- package/dist/sts/attempt.d.ts +15 -0
- package/dist/sts/attempt.d.ts.map +1 -0
- package/dist/sts/bound.d.ts +15 -0
- package/dist/sts/bound.d.ts.map +1 -0
- package/dist/sts/cache.d.ts +27 -0
- package/dist/sts/cache.d.ts.map +1 -0
- package/dist/sts/discover.d.ts +52 -0
- package/dist/sts/discover.d.ts.map +1 -0
- package/dist/sts/options.d.ts +77 -0
- package/dist/sts/options.d.ts.map +1 -0
- package/dist/sts/refusal.d.ts +10 -0
- package/dist/sts/refusal.d.ts.map +1 -0
- package/dist/sts/run.d.ts +55 -0
- package/dist/sts/run.d.ts.map +1 -0
- package/dist/sts/settings.d.ts +5 -0
- package/dist/sts/settings.d.ts.map +1 -0
- package/dist/sts/tally.d.ts +20 -0
- package/dist/sts/tally.d.ts.map +1 -0
- package/docs/guide.md +98 -2
- package/docs/roadmap.md +15 -7
- package/docs/troubleshooting.md +75 -0
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -471,8 +471,66 @@ class Events {
|
|
|
471
471
|
}
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
-
// src/
|
|
475
|
-
import {
|
|
474
|
+
// src/sts/settings.ts
|
|
475
|
+
import { MAX_AGE_LIMIT } from "@bumail/mta-sts";
|
|
476
|
+
|
|
477
|
+
// src/sts/cache.ts
|
|
478
|
+
function createMemoryPolicyCache() {
|
|
479
|
+
const entries = new Map;
|
|
480
|
+
return {
|
|
481
|
+
get: (domain) => entries.get(domain),
|
|
482
|
+
set(domain, entry) {
|
|
483
|
+
entries.set(domain, entry);
|
|
484
|
+
},
|
|
485
|
+
delete(domain) {
|
|
486
|
+
entries.delete(domain);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/sts/settings.ts
|
|
492
|
+
var hasTxt = (value) => typeof value?.txt === "function";
|
|
493
|
+
function stsOf(options, resolver) {
|
|
494
|
+
if (options === undefined)
|
|
495
|
+
return;
|
|
496
|
+
if (typeof options !== "object" || options === null) {
|
|
497
|
+
throw invalid(`mtaSts must be an object of options, not ${options}`);
|
|
498
|
+
}
|
|
499
|
+
const found = options.resolver ?? (hasTxt(resolver) ? resolver : undefined);
|
|
500
|
+
if (!hasTxt(found)) {
|
|
501
|
+
throw invalid("mtaSts needs a resolver with txt(name), such as @bumail/dns's nodeResolver(): give mtaSts.resolver, or a resolver that has txt");
|
|
502
|
+
}
|
|
503
|
+
const cache = options.cache ?? createMemoryPolicyCache();
|
|
504
|
+
if (typeof cache.get !== "function" || typeof cache.set !== "function") {
|
|
505
|
+
throw invalid("mtaSts.cache must be a PolicyCache with get and set");
|
|
506
|
+
}
|
|
507
|
+
if (options.fetch !== undefined && typeof options.fetch !== "function") {
|
|
508
|
+
throw invalid("mtaSts.fetch must be a function");
|
|
509
|
+
}
|
|
510
|
+
if (options.skip !== undefined && typeof options.skip !== "function") {
|
|
511
|
+
throw invalid("mtaSts.skip must be a function");
|
|
512
|
+
}
|
|
513
|
+
const { fetchTimeoutMs, maxAgeCap } = options;
|
|
514
|
+
if (fetchTimeoutMs !== undefined && (typeof fetchTimeoutMs !== "number" || !Number.isFinite(fetchTimeoutMs) || fetchTimeoutMs <= 0 || fetchTimeoutMs > 2147483647)) {
|
|
515
|
+
throw invalid(`mtaSts.fetchTimeoutMs must be a number of milliseconds above 0 and at most 2147483647, not ${fetchTimeoutMs}`);
|
|
516
|
+
}
|
|
517
|
+
if (maxAgeCap !== undefined && (!Number.isSafeInteger(maxAgeCap) || maxAgeCap < 0 || maxAgeCap > MAX_AGE_LIMIT)) {
|
|
518
|
+
throw invalid(`mtaSts.maxAgeCap must be a whole number of seconds from 0 to ${MAX_AGE_LIMIT}, not ${maxAgeCap}`);
|
|
519
|
+
}
|
|
520
|
+
let controller = new AbortController;
|
|
521
|
+
return {
|
|
522
|
+
cache,
|
|
523
|
+
resolver: found,
|
|
524
|
+
options,
|
|
525
|
+
inflight: new Map,
|
|
526
|
+
failures: new Map,
|
|
527
|
+
signal: () => controller.signal,
|
|
528
|
+
abort: () => controller.abort(),
|
|
529
|
+
reset() {
|
|
530
|
+
controller = new AbortController;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
476
534
|
|
|
477
535
|
// src/queue/settings/numbers.ts
|
|
478
536
|
var MINUTE = 60000;
|
|
@@ -606,6 +664,57 @@ function checkRoutes(options) {
|
|
|
606
664
|
}
|
|
607
665
|
}
|
|
608
666
|
|
|
667
|
+
// src/queue/settings/sender.ts
|
|
668
|
+
import {
|
|
669
|
+
createSmtpPool,
|
|
670
|
+
SmtpError,
|
|
671
|
+
sendMail
|
|
672
|
+
} from "@bumail/smtp/client";
|
|
673
|
+
function reuseOf(options) {
|
|
674
|
+
const { reuse } = options;
|
|
675
|
+
if (reuse === undefined)
|
|
676
|
+
return {};
|
|
677
|
+
if (reuse === false)
|
|
678
|
+
return false;
|
|
679
|
+
if (typeof reuse !== "object" || reuse === null) {
|
|
680
|
+
throw invalid(`reuse must be false or an object of limits, not ${String(reuse)}`);
|
|
681
|
+
}
|
|
682
|
+
const { maxMessages, idleSeconds } = reuse;
|
|
683
|
+
return {
|
|
684
|
+
...maxMessages === undefined ? {} : { maxMessages },
|
|
685
|
+
...idleSeconds === undefined ? {} : { idleSeconds }
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function poolOf(reuse) {
|
|
689
|
+
try {
|
|
690
|
+
return createSmtpPool(reuse);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
if (error instanceof SmtpError && error.code === "INVALID_OPTION") {
|
|
693
|
+
throw invalid(`reuse.${error.message.replace(/^createSmtpPool\(\): /, "")}`);
|
|
694
|
+
}
|
|
695
|
+
throw error;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function senderOf(options) {
|
|
699
|
+
const reuse = reuseOf(options);
|
|
700
|
+
let pool = reuse === false ? undefined : poolOf(reuse);
|
|
701
|
+
if (options.send)
|
|
702
|
+
return { send: options.send, close: async () => {} };
|
|
703
|
+
if (reuse === false)
|
|
704
|
+
return { send: sendMail, close: async () => {} };
|
|
705
|
+
return {
|
|
706
|
+
send: (message, sendOptions) => {
|
|
707
|
+
pool ??= poolOf(reuse);
|
|
708
|
+
return pool.sendMail(message, sendOptions);
|
|
709
|
+
},
|
|
710
|
+
async close() {
|
|
711
|
+
const closing = pool;
|
|
712
|
+
pool = undefined;
|
|
713
|
+
await closing?.close();
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
609
718
|
// src/queue/settings/index.ts
|
|
610
719
|
var HOSTNAME = /^(?=.{1,253}$)[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*$/;
|
|
611
720
|
function settingsOf(options) {
|
|
@@ -623,6 +732,7 @@ function settingsOf(options) {
|
|
|
623
732
|
checkSessions(options);
|
|
624
733
|
const owner = checkOwner(options.owner ?? crypto.randomUUID());
|
|
625
734
|
const clock = options.clock;
|
|
735
|
+
const sender = senderOf(options);
|
|
626
736
|
return {
|
|
627
737
|
options,
|
|
628
738
|
hostname,
|
|
@@ -639,8 +749,10 @@ function settingsOf(options) {
|
|
|
639
749
|
pollInterval: numberOf("pollInterval", options.pollInterval, 5000, 1, MAX_TIMER),
|
|
640
750
|
owner,
|
|
641
751
|
now: clock ? () => clock.now() : Date.now,
|
|
642
|
-
send:
|
|
643
|
-
|
|
752
|
+
send: sender.send,
|
|
753
|
+
close: sender.close,
|
|
754
|
+
random: options.random ?? Math.random,
|
|
755
|
+
sts: stsOf(options.mtaSts, options.resolver)
|
|
644
756
|
};
|
|
645
757
|
}
|
|
646
758
|
|
|
@@ -672,7 +784,8 @@ var ERROR_STATUS = {
|
|
|
672
784
|
TLS_UNAVAILABLE: "7.10",
|
|
673
785
|
TLS_FAILED: "7.0",
|
|
674
786
|
AUTH_UNAVAILABLE: "7.0",
|
|
675
|
-
INVALID_OPTION: "3.5"
|
|
787
|
+
INVALID_OPTION: "3.5",
|
|
788
|
+
MX_NOT_ALLOWED: "7.5"
|
|
676
789
|
};
|
|
677
790
|
function diagnosticOfError(error, temporary, max, host) {
|
|
678
791
|
const text = error instanceof Error ? error.message : String(error);
|
|
@@ -737,6 +850,8 @@ function fallsBack(reply, error) {
|
|
|
737
850
|
return false;
|
|
738
851
|
if (isSmtpError(error) && error.code.startsWith("TLS_"))
|
|
739
852
|
return false;
|
|
853
|
+
if (isSmtpError(error) && error.tlsResult !== undefined)
|
|
854
|
+
return false;
|
|
740
855
|
if (GREYLIST.test(reply.text))
|
|
741
856
|
return false;
|
|
742
857
|
if (reply.code === undefined) {
|
|
@@ -750,6 +865,320 @@ function fallsBack(reply, error) {
|
|
|
750
865
|
return REPUTATION.test(reply.text);
|
|
751
866
|
}
|
|
752
867
|
|
|
868
|
+
// src/queue/session.ts
|
|
869
|
+
import { POLICY_TIMEOUT_MS as POLICY_TIMEOUT_MS3 } from "@bumail/mta-sts";
|
|
870
|
+
|
|
871
|
+
// src/sts/attempt.ts
|
|
872
|
+
import { matchesMxName } from "@bumail/smtp/client";
|
|
873
|
+
var reportOf = (policy) => policy ? {
|
|
874
|
+
type: "sts",
|
|
875
|
+
domain: policy.domain,
|
|
876
|
+
mode: policy.mode,
|
|
877
|
+
mx: policy.mx,
|
|
878
|
+
text: policy.text
|
|
879
|
+
} : { type: "no-policy-found" };
|
|
880
|
+
function tlsEventOf(domain, policy, attempt, at) {
|
|
881
|
+
const base = {
|
|
882
|
+
kind: "try",
|
|
883
|
+
at,
|
|
884
|
+
domain,
|
|
885
|
+
policy: reportOf(policy),
|
|
886
|
+
mxHost: attempt.host,
|
|
887
|
+
address: attempt.address,
|
|
888
|
+
...attempt.localAddress ? { sendingAddress: attempt.localAddress } : {}
|
|
889
|
+
};
|
|
890
|
+
const failed = (tlsResult) => ({
|
|
891
|
+
...base,
|
|
892
|
+
ok: false,
|
|
893
|
+
tlsResult
|
|
894
|
+
});
|
|
895
|
+
if (policy && !matchesMxName(attempt.host, policy.mx)) {
|
|
896
|
+
return failed("validation-failure");
|
|
897
|
+
}
|
|
898
|
+
if (attempt.tlsResult)
|
|
899
|
+
return failed(attempt.tlsResult);
|
|
900
|
+
if (attempt.tls === false) {
|
|
901
|
+
if (!attempt.ok)
|
|
902
|
+
return;
|
|
903
|
+
return policy ? failed("starttls-not-supported") : { ...base, ok: true };
|
|
904
|
+
}
|
|
905
|
+
if (policy && !attempt.tls.verified)
|
|
906
|
+
return failed("certificate-not-trusted");
|
|
907
|
+
return { ...base, ok: true };
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/sts/discover.ts
|
|
911
|
+
import {
|
|
912
|
+
fetchPolicy,
|
|
913
|
+
POLICY_TIMEOUT_MS as POLICY_TIMEOUT_MS2,
|
|
914
|
+
parseStsRecord
|
|
915
|
+
} from "@bumail/mta-sts";
|
|
916
|
+
|
|
917
|
+
// src/sts/bound.ts
|
|
918
|
+
import { POLICY_TIMEOUT_MS } from "@bumail/mta-sts";
|
|
919
|
+
async function bounded(work, signal, timeoutMs) {
|
|
920
|
+
if (signal.aborted)
|
|
921
|
+
return { done: false, why: "aborted" };
|
|
922
|
+
let timer;
|
|
923
|
+
let onAbort;
|
|
924
|
+
const cut = new Promise((resolve) => {
|
|
925
|
+
timer = setTimeout(() => resolve({ done: false, why: "timeout" }), timeoutMs ?? POLICY_TIMEOUT_MS);
|
|
926
|
+
onAbort = () => resolve({ done: false, why: "aborted" });
|
|
927
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
928
|
+
});
|
|
929
|
+
try {
|
|
930
|
+
return await Promise.race([
|
|
931
|
+
Promise.resolve(work).then((value) => ({ done: true, value })),
|
|
932
|
+
cut
|
|
933
|
+
]);
|
|
934
|
+
} finally {
|
|
935
|
+
clearTimeout(timer);
|
|
936
|
+
if (onAbort)
|
|
937
|
+
signal.removeEventListener("abort", onAbort);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/sts/discover.ts
|
|
942
|
+
var FAILED_FETCH_MS = 5 * 60000;
|
|
943
|
+
var active = (domain, policy, text) => policy.mode === "none" ? {} : { policy: { domain, mode: policy.mode, mx: policy.mx, text } };
|
|
944
|
+
var usable = (entry) => {
|
|
945
|
+
const e = entry;
|
|
946
|
+
return typeof e === "object" && e !== null && typeof e.id === "string" && typeof e.text === "string" && typeof e.expiresAt === "number" && typeof e.policy === "object" && e.policy !== null && Array.isArray(e.policy.mx) && (e.policy.mode === "enforce" || e.policy.mode === "testing" || e.policy.mode === "none");
|
|
947
|
+
};
|
|
948
|
+
async function cached(sts, domain, now, left) {
|
|
949
|
+
try {
|
|
950
|
+
const got = await bounded(sts.cache.get(domain), sts.signal(), left());
|
|
951
|
+
if (!got.done)
|
|
952
|
+
return got.why === "aborted" ? { aborted: true } : undefined;
|
|
953
|
+
if (!usable(got.value))
|
|
954
|
+
return;
|
|
955
|
+
if (now < got.value.expiresAt)
|
|
956
|
+
return got.value;
|
|
957
|
+
await bounded(sts.cache.delete?.(domain), sts.signal(), left());
|
|
958
|
+
return;
|
|
959
|
+
} catch {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async function recordId(sts, domain, left) {
|
|
964
|
+
try {
|
|
965
|
+
const got = await bounded(sts.resolver.txt(`_mta-sts.${domain}`), sts.signal(), left());
|
|
966
|
+
if (!got.done)
|
|
967
|
+
return got.why === "aborted" ? { aborted: true } : undefined;
|
|
968
|
+
const parsed = parseStsRecord(got.value);
|
|
969
|
+
return parsed.ok ? parsed.value.id : undefined;
|
|
970
|
+
} catch {
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
var timedOut = (ms) => ({
|
|
975
|
+
code: "TIMEOUT",
|
|
976
|
+
message: `MTA-STS discovery took more than ${ms} ms`,
|
|
977
|
+
tlsResult: "sts-policy-fetch-error"
|
|
978
|
+
});
|
|
979
|
+
function remember(sts, key, error, now) {
|
|
980
|
+
if (sts.failures.size >= 1000) {
|
|
981
|
+
for (const [k, v] of sts.failures)
|
|
982
|
+
if (v.until <= now)
|
|
983
|
+
sts.failures.delete(k);
|
|
984
|
+
for (const k of sts.failures.keys()) {
|
|
985
|
+
if (sts.failures.size < 1000)
|
|
986
|
+
break;
|
|
987
|
+
sts.failures.delete(k);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
sts.failures.set(key, { error, until: now + FAILED_FETCH_MS });
|
|
991
|
+
}
|
|
992
|
+
async function find(sts, domain, now, fresh) {
|
|
993
|
+
const total = sts.options.fetchTimeoutMs ?? POLICY_TIMEOUT_MS2;
|
|
994
|
+
const started = performance.now();
|
|
995
|
+
const left = () => Math.max(1, total - (performance.now() - started));
|
|
996
|
+
const kept = await cached(sts, domain, now(), () => Math.min(left(), total / 4));
|
|
997
|
+
if (kept && "aborted" in kept)
|
|
998
|
+
return { aborted: true };
|
|
999
|
+
const keptDiscovery = kept ? active(domain, kept.policy, kept.text) : {};
|
|
1000
|
+
const id = await recordId(sts, domain, left);
|
|
1001
|
+
if (typeof id === "object")
|
|
1002
|
+
return { aborted: true };
|
|
1003
|
+
if (id === undefined)
|
|
1004
|
+
return keptDiscovery;
|
|
1005
|
+
if (kept && kept.id === id && !fresh)
|
|
1006
|
+
return keptDiscovery;
|
|
1007
|
+
const { fetch, maxAgeCap } = sts.options;
|
|
1008
|
+
const failureKey = `${domain}
|
|
1009
|
+
${id}`;
|
|
1010
|
+
const failed = fresh ? undefined : sts.failures.get(failureKey);
|
|
1011
|
+
if (failed && now() < failed.until) {
|
|
1012
|
+
return { ...keptDiscovery, fetchError: failed.error };
|
|
1013
|
+
}
|
|
1014
|
+
sts.failures.delete(failureKey);
|
|
1015
|
+
if (performance.now() - started >= total) {
|
|
1016
|
+
return { ...keptDiscovery, fetchError: timedOut(total) };
|
|
1017
|
+
}
|
|
1018
|
+
const budget = left();
|
|
1019
|
+
const result = await fetchPolicy(domain, {
|
|
1020
|
+
signal: sts.signal(),
|
|
1021
|
+
...fetch ? { fetch } : {},
|
|
1022
|
+
timeoutMs: budget,
|
|
1023
|
+
...maxAgeCap === undefined ? {} : { maxAgeCap }
|
|
1024
|
+
});
|
|
1025
|
+
if (!result.ok) {
|
|
1026
|
+
if (sts.signal().aborted)
|
|
1027
|
+
return { aborted: true };
|
|
1028
|
+
const shrunk = result.error.code === "TIMEOUT" && budget < total * 0.75;
|
|
1029
|
+
if (!shrunk)
|
|
1030
|
+
remember(sts, failureKey, result.error, now());
|
|
1031
|
+
return { ...keptDiscovery, fetchError: result.error };
|
|
1032
|
+
}
|
|
1033
|
+
const fetchedAt = now();
|
|
1034
|
+
try {
|
|
1035
|
+
await bounded(sts.cache.set(domain, {
|
|
1036
|
+
id,
|
|
1037
|
+
policy: result.policy,
|
|
1038
|
+
text: result.text,
|
|
1039
|
+
fetchedAt,
|
|
1040
|
+
expiresAt: fetchedAt + result.policy.maxAge * 1000
|
|
1041
|
+
}), sts.signal(), total / 4);
|
|
1042
|
+
} catch {}
|
|
1043
|
+
return active(domain, result.policy, result.text);
|
|
1044
|
+
}
|
|
1045
|
+
function discover(sts, domain, now, fresh) {
|
|
1046
|
+
const key = `${fresh ? "fresh" : "cached"}:${domain}`;
|
|
1047
|
+
const running = sts.inflight.get(key);
|
|
1048
|
+
if (running)
|
|
1049
|
+
return running;
|
|
1050
|
+
const started = find(sts, domain, now, fresh).finally(() => {
|
|
1051
|
+
sts.inflight.delete(key);
|
|
1052
|
+
});
|
|
1053
|
+
sts.inflight.set(key, started);
|
|
1054
|
+
return started;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// src/sts/refusal.ts
|
|
1058
|
+
var written = new WeakSet;
|
|
1059
|
+
var isPolicyRefusal = (reply) => reply !== undefined && written.has(reply);
|
|
1060
|
+
function refusalUpdates(domain, updates, final, why, maxText) {
|
|
1061
|
+
return updates.map((u) => {
|
|
1062
|
+
const code = u.reply?.code;
|
|
1063
|
+
const reply = {
|
|
1064
|
+
...code !== undefined && code >= 500 === final ? { code } : {},
|
|
1065
|
+
status: final ? "5.7.5" : "4.7.5",
|
|
1066
|
+
text: cleanText(`MTA-STS policy of ${domain} (enforce): ${u.reply?.text ?? why}`, maxText),
|
|
1067
|
+
...u.reply?.host ? { host: u.reply.host } : {}
|
|
1068
|
+
};
|
|
1069
|
+
written.add(reply);
|
|
1070
|
+
return {
|
|
1071
|
+
address: u.address,
|
|
1072
|
+
status: final ? "failed" : "deferred",
|
|
1073
|
+
reply
|
|
1074
|
+
};
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/sts/tally.ts
|
|
1079
|
+
var newTally = () => ({
|
|
1080
|
+
tries: [],
|
|
1081
|
+
skipped: [],
|
|
1082
|
+
tlsFailure: false
|
|
1083
|
+
});
|
|
1084
|
+
function record(tally, domain, policy, attempt, at) {
|
|
1085
|
+
if (attempt.tlsResult)
|
|
1086
|
+
tally.tlsFailure = true;
|
|
1087
|
+
const event = tlsEventOf(domain, policy, attempt, at);
|
|
1088
|
+
if (event)
|
|
1089
|
+
tally.tries.push({ event, delivered: attempt.ok });
|
|
1090
|
+
}
|
|
1091
|
+
function tell(tally, emit, domain, policy, now, reused) {
|
|
1092
|
+
for (const mxHost of tally.skipped) {
|
|
1093
|
+
emit({
|
|
1094
|
+
kind: "try",
|
|
1095
|
+
at: now(),
|
|
1096
|
+
domain,
|
|
1097
|
+
policy: reportOf(policy),
|
|
1098
|
+
mxHost,
|
|
1099
|
+
ok: false,
|
|
1100
|
+
tlsResult: "validation-failure"
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
for (const { event, delivered } of tally.tries) {
|
|
1104
|
+
emit(delivered && reused !== undefined ? { ...event, reused } : event);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/sts/run.ts
|
|
1109
|
+
async function prepare(input) {
|
|
1110
|
+
const { sts, domain, emit, maxText, now } = input;
|
|
1111
|
+
let found;
|
|
1112
|
+
try {
|
|
1113
|
+
found = await discover(sts, domain, now, input.refresh);
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
found = {
|
|
1116
|
+
fetchError: {
|
|
1117
|
+
code: "NETWORK",
|
|
1118
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1119
|
+
tlsResult: "sts-policy-fetch-error"
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
if (found.aborted)
|
|
1124
|
+
return { halt: true };
|
|
1125
|
+
const policy = found.policy;
|
|
1126
|
+
if (found.fetchError) {
|
|
1127
|
+
emit({
|
|
1128
|
+
kind: "policy",
|
|
1129
|
+
at: now(),
|
|
1130
|
+
domain,
|
|
1131
|
+
policy: reportOf(policy),
|
|
1132
|
+
ok: false,
|
|
1133
|
+
tlsResult: found.fetchError.tlsResult
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
const enforce = policy?.mode === "enforce" ? policy : undefined;
|
|
1137
|
+
const tally = newTally();
|
|
1138
|
+
const onAttempt = (attempt) => record(tally, domain, policy, attempt, now());
|
|
1139
|
+
const told = (reused) => tell(tally, emit, domain, policy, now, reused);
|
|
1140
|
+
return {
|
|
1141
|
+
options: enforce ? {
|
|
1142
|
+
mxNames: enforce.mx,
|
|
1143
|
+
tls: "required",
|
|
1144
|
+
onAttempt,
|
|
1145
|
+
onSkip: (host) => tally.skipped.push(host)
|
|
1146
|
+
} : { onAttempt },
|
|
1147
|
+
enforcing: enforce !== undefined,
|
|
1148
|
+
delivered: (result) => told(result.reused),
|
|
1149
|
+
failed(error, updates) {
|
|
1150
|
+
const smtp = isSmtpError(error) ? error : undefined;
|
|
1151
|
+
const refusal = enforce && (smtp?.tlsResult !== undefined || tally.tlsFailure && smtp?.temporary !== false);
|
|
1152
|
+
if (smtp?.code === "MX_NOT_ALLOWED") {
|
|
1153
|
+
tally.skipped.push(...smtp.hosts ?? []);
|
|
1154
|
+
}
|
|
1155
|
+
told(undefined);
|
|
1156
|
+
if (!refusal)
|
|
1157
|
+
return { updates, refused: false };
|
|
1158
|
+
const final = input.refresh && now() >= input.giveUpAt;
|
|
1159
|
+
const why = smtp ? smtp.message : "a try broke the policy";
|
|
1160
|
+
return {
|
|
1161
|
+
refused: true,
|
|
1162
|
+
updates: refusalUpdates(domain, updates, final, why, maxText)
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// src/queue/retry.ts
|
|
1169
|
+
function retryDelay(attempts, retry, random) {
|
|
1170
|
+
const exponent = Math.max(0, attempts - 1);
|
|
1171
|
+
const delay = Math.min(retry.first * retry.factor ** exponent, retry.max);
|
|
1172
|
+
return delay + delay * retry.jitter * Math.min(1, Math.max(0, random()));
|
|
1173
|
+
}
|
|
1174
|
+
var giveUpAt = (createdAt, retry) => createdAt + retry.giveUpAfter;
|
|
1175
|
+
function nextAttemptAt(createdAt, now, attempts, retry, random) {
|
|
1176
|
+
const end = giveUpAt(createdAt, retry);
|
|
1177
|
+
if (now >= end)
|
|
1178
|
+
return;
|
|
1179
|
+
return Math.min(now + retryDelay(attempts, retry, random), end);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
753
1182
|
// src/queue/route.ts
|
|
754
1183
|
function routeOf(settings, domain) {
|
|
755
1184
|
return settings.routes.get(domain) ?? settings.options.route ?? "mx";
|
|
@@ -812,23 +1241,65 @@ function doubtful(updates, error, key) {
|
|
|
812
1241
|
const refused = new Set((error.rejected ?? []).map((r) => r.recipient).filter((a) => !taken.has(a)));
|
|
813
1242
|
return updates.map((u) => refused.has(u.address) ? u : marked(u, key));
|
|
814
1243
|
}
|
|
1244
|
+
var reusedOf = (a) => a.reused === undefined ? {} : { reused: a.reused };
|
|
1245
|
+
async function stsFor(ctx, item, domain) {
|
|
1246
|
+
const { settings } = ctx;
|
|
1247
|
+
const { sts } = settings;
|
|
1248
|
+
if (!sts)
|
|
1249
|
+
return;
|
|
1250
|
+
try {
|
|
1251
|
+
if (sts.options.skip?.(item, domain) === true)
|
|
1252
|
+
return;
|
|
1253
|
+
} catch {}
|
|
1254
|
+
const now = settings.now();
|
|
1255
|
+
const end = giveUpAt(item.createdAt, settings.retry);
|
|
1256
|
+
const deadlineMs = (settings.options.deadline ?? 1800) * 1000;
|
|
1257
|
+
const prepared = await prepare({
|
|
1258
|
+
sts,
|
|
1259
|
+
domain,
|
|
1260
|
+
now: settings.now,
|
|
1261
|
+
refresh: now + deadlineMs + (sts.options.fetchTimeoutMs ?? POLICY_TIMEOUT_MS3) >= end,
|
|
1262
|
+
giveUpAt: end,
|
|
1263
|
+
emit: (event) => ctx.events.emit("tls", event),
|
|
1264
|
+
maxText: settings.limits.maxReplyText
|
|
1265
|
+
});
|
|
1266
|
+
return "halt" in prepared ? "halt" : prepared;
|
|
1267
|
+
}
|
|
815
1268
|
async function session(ctx, item, message, domain, group, route, override) {
|
|
816
1269
|
const { settings } = ctx;
|
|
817
1270
|
const max = settings.limits.maxReplyText;
|
|
1271
|
+
const sts = route === "mx" && override === undefined ? await stsFor(ctx, item, domain) : undefined;
|
|
1272
|
+
if (sts === "halt") {
|
|
1273
|
+
ctx.halted?.();
|
|
1274
|
+
return { updates: [] };
|
|
1275
|
+
}
|
|
818
1276
|
try {
|
|
1277
|
+
const base = sendOptionsOf(settings, domain, item.from, group, override);
|
|
819
1278
|
const options = {
|
|
820
|
-
...
|
|
1279
|
+
...sts && "resolver" in base ? { ...base, ...sts.options } : base,
|
|
821
1280
|
...dsnOptionsOf(item, group)
|
|
822
1281
|
};
|
|
823
1282
|
const result = await settings.send(message, options);
|
|
824
|
-
|
|
1283
|
+
sts?.delivered(result);
|
|
1284
|
+
return {
|
|
1285
|
+
updates: outcomesOf(result, group, max),
|
|
1286
|
+
...sts?.enforcing ? { enforced: true } : {},
|
|
1287
|
+
...result.reused === undefined ? {} : { reused: result.reused }
|
|
1288
|
+
};
|
|
825
1289
|
} catch (error) {
|
|
826
1290
|
if (isRouteError(error))
|
|
827
1291
|
ctx.events.emit("error", { error, id: item.id });
|
|
828
|
-
const
|
|
1292
|
+
const failed = outcomesOfError(error, group, max, hostOf(route));
|
|
1293
|
+
const judged = sts?.failed(error, failed);
|
|
1294
|
+
const updates = judged ? judged.updates : failed;
|
|
829
1295
|
const key = route === "mx" ? "mayHaveReachedMx" : "mayHaveReachedFallback";
|
|
830
1296
|
const doubt = (route === "mx" || override !== undefined) && isSmtpError(error) && error.afterData;
|
|
831
|
-
return {
|
|
1297
|
+
return {
|
|
1298
|
+
updates: doubt ? doubtful(updates, error, key) : updates,
|
|
1299
|
+
error,
|
|
1300
|
+
...judged?.refused ? { refused: true } : {},
|
|
1301
|
+
...sts?.enforcing ? { enforced: true } : {}
|
|
1302
|
+
};
|
|
832
1303
|
}
|
|
833
1304
|
}
|
|
834
1305
|
|
|
@@ -843,13 +1314,14 @@ var stillHeld = (u) => u.status === "failed" ? { ...u, status: "deferred" } : u;
|
|
|
843
1314
|
async function fallBackAtOnce(ctx, item, message, domain, first, fallback, used) {
|
|
844
1315
|
const mxHeld = new Set(item.recipients.filter((r) => r.mayHaveReachedMx).map((r) => r.address));
|
|
845
1316
|
const mx = new Map(first.updates.filter((u) => u.status !== "delivered" && !u.mayHaveReachedMx && !mxHeld.has(u.address) && fallsBack(u.reply, first.error)).map((u) => [u.address, u]));
|
|
846
|
-
if (mx.size === 0)
|
|
1317
|
+
if (mx.size === 0 || first.refused || first.enforced)
|
|
847
1318
|
return first.updates;
|
|
848
1319
|
const second = await session(ctx, item, message, domain, [...mx.keys()], fallback, fallback);
|
|
849
1320
|
for (const [address, u] of mx) {
|
|
850
1321
|
used.set(address, {
|
|
851
1322
|
route: "smarthost",
|
|
852
1323
|
fallback: true,
|
|
1324
|
+
...reusedOf(second),
|
|
853
1325
|
...u.reply ? { mxReply: u.reply } : {}
|
|
854
1326
|
});
|
|
855
1327
|
}
|
|
@@ -874,6 +1346,13 @@ async function deliverHeld(ctx, item, message, domain, held, fallback, used) {
|
|
|
874
1346
|
used.set(address, { route: "smarthost", fallback: true });
|
|
875
1347
|
}
|
|
876
1348
|
const second = await session(ctx, item, message, domain, held, fallback, fallback);
|
|
1349
|
+
for (const address of held) {
|
|
1350
|
+
used.set(address, {
|
|
1351
|
+
route: "smarthost",
|
|
1352
|
+
fallback: true,
|
|
1353
|
+
...reusedOf(second)
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
877
1356
|
return second.updates.map(stillHeld);
|
|
878
1357
|
}
|
|
879
1358
|
|
|
@@ -903,25 +1382,14 @@ async function attemptGroup(ctx, item, message, domain, recipients, used) {
|
|
|
903
1382
|
let updates = [];
|
|
904
1383
|
if (viaMx.length > 0) {
|
|
905
1384
|
const first = await session(ctx, item, message, domain, viaMx, route);
|
|
1385
|
+
for (const address of viaMx) {
|
|
1386
|
+
used.set(address, { route: label, ...reusedOf(first) });
|
|
1387
|
+
}
|
|
906
1388
|
updates = fallback !== undefined && route === "mx" ? await fallBackAtOnce(ctx, item, message, domain, first, fallback, used) : first.updates;
|
|
907
1389
|
}
|
|
908
1390
|
return [...kept, ...updates, ...bad.map(badAddress)];
|
|
909
1391
|
}
|
|
910
1392
|
|
|
911
|
-
// src/queue/retry.ts
|
|
912
|
-
function retryDelay(attempts, retry, random) {
|
|
913
|
-
const exponent = Math.max(0, attempts - 1);
|
|
914
|
-
const delay = Math.min(retry.first * retry.factor ** exponent, retry.max);
|
|
915
|
-
return delay + delay * retry.jitter * Math.min(1, Math.max(0, random()));
|
|
916
|
-
}
|
|
917
|
-
var giveUpAt = (createdAt, retry) => createdAt + retry.giveUpAfter;
|
|
918
|
-
function nextAttemptAt(createdAt, now, attempts, retry, random) {
|
|
919
|
-
const end = giveUpAt(createdAt, retry);
|
|
920
|
-
if (now >= end)
|
|
921
|
-
return;
|
|
922
|
-
return Math.min(now + retryDelay(attempts, retry, random), end);
|
|
923
|
-
}
|
|
924
|
-
|
|
925
1393
|
// src/queue/notify.ts
|
|
926
1394
|
var MAY_HAVE_REACHED = "An earlier attempt may have reached the receiving server, so the message may have been delivered.";
|
|
927
1395
|
var MAY_HAVE_REACHED_FALLBACK = "An earlier attempt may have reached the fallback smarthost, so the message may have been delivered.";
|
|
@@ -1029,10 +1497,14 @@ async function tellDsns(ctx, item, pending, completed) {
|
|
|
1029
1497
|
|
|
1030
1498
|
// src/queue/settle.ts
|
|
1031
1499
|
function expired(update) {
|
|
1500
|
+
const refusal = isPolicyRefusal(update.reply);
|
|
1501
|
+
const status = refusal ? "5.7.5" : "4.4.7";
|
|
1502
|
+
const { code, ...rest } = update.reply ?? {};
|
|
1503
|
+
const reply = refusal && code !== undefined && code < 500 ? { text: "", ...rest, status } : { text: "", ...update.reply, status };
|
|
1032
1504
|
return {
|
|
1033
1505
|
address: update.address,
|
|
1034
1506
|
status: "failed",
|
|
1035
|
-
reply
|
|
1507
|
+
reply,
|
|
1036
1508
|
...update.mayHaveReachedMx ? { mayHaveReachedMx: true } : {},
|
|
1037
1509
|
...update.mayHaveReachedFallback ? { mayHaveReachedFallback: true } : {}
|
|
1038
1510
|
};
|
|
@@ -1132,12 +1604,18 @@ async function deliverItem(ctx, item) {
|
|
|
1132
1604
|
if (await store.get(item.id) === undefined)
|
|
1133
1605
|
return;
|
|
1134
1606
|
const failed = [...groupsOf(item).values()].flat().map(unreadable);
|
|
1135
|
-
await
|
|
1607
|
+
await record2(ctx, item, failed, false, undefined, new Map);
|
|
1136
1608
|
return;
|
|
1137
1609
|
}
|
|
1138
1610
|
const groups = groupsOf(item);
|
|
1139
1611
|
let skipped = false;
|
|
1140
1612
|
const used = new Map;
|
|
1613
|
+
const grouped = {
|
|
1614
|
+
...ctx,
|
|
1615
|
+
halted: () => {
|
|
1616
|
+
skipped = true;
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1141
1619
|
const outcomes = item.from !== "" && !isMailbox3(item.from) ? [[...groups.values()].flat().map(badSender)] : await Promise.all([...groups].map(async ([domain, group]) => {
|
|
1142
1620
|
const release = await ctx.domains.acquire(domain);
|
|
1143
1621
|
try {
|
|
@@ -1145,14 +1623,14 @@ async function deliverItem(ctx, item) {
|
|
|
1145
1623
|
skipped = true;
|
|
1146
1624
|
return [];
|
|
1147
1625
|
}
|
|
1148
|
-
return await attemptGroup(
|
|
1626
|
+
return await attemptGroup(grouped, item, message, domain, group, used);
|
|
1149
1627
|
} finally {
|
|
1150
1628
|
release();
|
|
1151
1629
|
}
|
|
1152
1630
|
}));
|
|
1153
|
-
await
|
|
1631
|
+
await record2(ctx, item, outcomes.flat(), skipped, message, used);
|
|
1154
1632
|
}
|
|
1155
|
-
async function
|
|
1633
|
+
async function record2(ctx, item, updates, skipped, message, used) {
|
|
1156
1634
|
const { settings, store, events } = ctx;
|
|
1157
1635
|
const now = settings.now();
|
|
1158
1636
|
const settled = settle(item, updates, skipped, now, settings);
|
|
@@ -1420,11 +1898,17 @@ class Worker {
|
|
|
1420
1898
|
async stop() {
|
|
1421
1899
|
this.#restart = false;
|
|
1422
1900
|
this.#stopping = true;
|
|
1901
|
+
this.#ctx.settings.sts?.abort();
|
|
1423
1902
|
this.#wake?.();
|
|
1424
1903
|
await this.#loop;
|
|
1425
1904
|
await Promise.all(this.#inFlight);
|
|
1426
|
-
|
|
1427
|
-
|
|
1905
|
+
try {
|
|
1906
|
+
await this.#ctx.settings.close();
|
|
1907
|
+
} finally {
|
|
1908
|
+
this.#ctx.settings.sts?.reset();
|
|
1909
|
+
this.#loop = undefined;
|
|
1910
|
+
this.#stopping = false;
|
|
1911
|
+
}
|
|
1428
1912
|
if (this.#restart) {
|
|
1429
1913
|
this.#restart = false;
|
|
1430
1914
|
this.start();
|
|
@@ -1471,10 +1955,11 @@ function createQueue(options) {
|
|
|
1471
1955
|
export {
|
|
1472
1956
|
DEFAULT_MAX_RETURN,
|
|
1473
1957
|
QueueError2 as QueueError,
|
|
1958
|
+
createMemoryPolicyCache,
|
|
1474
1959
|
createQueue,
|
|
1475
1960
|
deliveredDsn,
|
|
1476
1961
|
failedDsn
|
|
1477
1962
|
};
|
|
1478
1963
|
|
|
1479
|
-
//# debugId=
|
|
1964
|
+
//# debugId=7BA7A5C5D4F3B31B64756E2164756E21
|
|
1480
1965
|
//# sourceMappingURL=index.js.map
|