@bumail/queue 0.13.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 +44 -4
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +442 -26
- package/dist/index.js.map +21 -13
- package/dist/queue/deliver.d.ts +2 -0
- package/dist/queue/deliver.d.ts.map +1 -1
- package/dist/queue/events.d.ts +3 -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 +10 -0
- package/dist/queue/options.d.ts.map +1 -1
- package/dist/queue/outcome.d.ts.map +1 -1
- package/dist/queue/session.d.ts +4 -0
- package/dist/queue/session.d.ts.map +1 -1
- package/dist/queue/settings/index.d.ts +3 -0
- package/dist/queue/settings/index.d.ts.map +1 -1
- 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 +63 -1
- package/docs/roadmap.md +6 -4
- package/docs/troubleshooting.md +49 -0
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -471,6 +471,67 @@ class Events {
|
|
|
471
471
|
}
|
|
472
472
|
}
|
|
473
473
|
|
|
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
|
+
}
|
|
534
|
+
|
|
474
535
|
// src/queue/settings/numbers.ts
|
|
475
536
|
var MINUTE = 60000;
|
|
476
537
|
var HOUR = 60 * MINUTE;
|
|
@@ -690,7 +751,8 @@ function settingsOf(options) {
|
|
|
690
751
|
now: clock ? () => clock.now() : Date.now,
|
|
691
752
|
send: sender.send,
|
|
692
753
|
close: sender.close,
|
|
693
|
-
random: options.random ?? Math.random
|
|
754
|
+
random: options.random ?? Math.random,
|
|
755
|
+
sts: stsOf(options.mtaSts, options.resolver)
|
|
694
756
|
};
|
|
695
757
|
}
|
|
696
758
|
|
|
@@ -722,7 +784,8 @@ var ERROR_STATUS = {
|
|
|
722
784
|
TLS_UNAVAILABLE: "7.10",
|
|
723
785
|
TLS_FAILED: "7.0",
|
|
724
786
|
AUTH_UNAVAILABLE: "7.0",
|
|
725
|
-
INVALID_OPTION: "3.5"
|
|
787
|
+
INVALID_OPTION: "3.5",
|
|
788
|
+
MX_NOT_ALLOWED: "7.5"
|
|
726
789
|
};
|
|
727
790
|
function diagnosticOfError(error, temporary, max, host) {
|
|
728
791
|
const text = error instanceof Error ? error.message : String(error);
|
|
@@ -787,6 +850,8 @@ function fallsBack(reply, error) {
|
|
|
787
850
|
return false;
|
|
788
851
|
if (isSmtpError(error) && error.code.startsWith("TLS_"))
|
|
789
852
|
return false;
|
|
853
|
+
if (isSmtpError(error) && error.tlsResult !== undefined)
|
|
854
|
+
return false;
|
|
790
855
|
if (GREYLIST.test(reply.text))
|
|
791
856
|
return false;
|
|
792
857
|
if (reply.code === undefined) {
|
|
@@ -800,6 +865,320 @@ function fallsBack(reply, error) {
|
|
|
800
865
|
return REPUTATION.test(reply.text);
|
|
801
866
|
}
|
|
802
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
|
+
|
|
803
1182
|
// src/queue/route.ts
|
|
804
1183
|
function routeOf(settings, domain) {
|
|
805
1184
|
return settings.routes.get(domain) ?? settings.options.route ?? "mx";
|
|
@@ -863,26 +1242,64 @@ function doubtful(updates, error, key) {
|
|
|
863
1242
|
return updates.map((u) => refused.has(u.address) ? u : marked(u, key));
|
|
864
1243
|
}
|
|
865
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
|
+
}
|
|
866
1268
|
async function session(ctx, item, message, domain, group, route, override) {
|
|
867
1269
|
const { settings } = ctx;
|
|
868
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
|
+
}
|
|
869
1276
|
try {
|
|
1277
|
+
const base = sendOptionsOf(settings, domain, item.from, group, override);
|
|
870
1278
|
const options = {
|
|
871
|
-
...
|
|
1279
|
+
...sts && "resolver" in base ? { ...base, ...sts.options } : base,
|
|
872
1280
|
...dsnOptionsOf(item, group)
|
|
873
1281
|
};
|
|
874
1282
|
const result = await settings.send(message, options);
|
|
1283
|
+
sts?.delivered(result);
|
|
875
1284
|
return {
|
|
876
1285
|
updates: outcomesOf(result, group, max),
|
|
1286
|
+
...sts?.enforcing ? { enforced: true } : {},
|
|
877
1287
|
...result.reused === undefined ? {} : { reused: result.reused }
|
|
878
1288
|
};
|
|
879
1289
|
} catch (error) {
|
|
880
1290
|
if (isRouteError(error))
|
|
881
1291
|
ctx.events.emit("error", { error, id: item.id });
|
|
882
|
-
const
|
|
1292
|
+
const failed = outcomesOfError(error, group, max, hostOf(route));
|
|
1293
|
+
const judged = sts?.failed(error, failed);
|
|
1294
|
+
const updates = judged ? judged.updates : failed;
|
|
883
1295
|
const key = route === "mx" ? "mayHaveReachedMx" : "mayHaveReachedFallback";
|
|
884
1296
|
const doubt = (route === "mx" || override !== undefined) && isSmtpError(error) && error.afterData;
|
|
885
|
-
return {
|
|
1297
|
+
return {
|
|
1298
|
+
updates: doubt ? doubtful(updates, error, key) : updates,
|
|
1299
|
+
error,
|
|
1300
|
+
...judged?.refused ? { refused: true } : {},
|
|
1301
|
+
...sts?.enforcing ? { enforced: true } : {}
|
|
1302
|
+
};
|
|
886
1303
|
}
|
|
887
1304
|
}
|
|
888
1305
|
|
|
@@ -897,7 +1314,7 @@ var stillHeld = (u) => u.status === "failed" ? { ...u, status: "deferred" } : u;
|
|
|
897
1314
|
async function fallBackAtOnce(ctx, item, message, domain, first, fallback, used) {
|
|
898
1315
|
const mxHeld = new Set(item.recipients.filter((r) => r.mayHaveReachedMx).map((r) => r.address));
|
|
899
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]));
|
|
900
|
-
if (mx.size === 0)
|
|
1317
|
+
if (mx.size === 0 || first.refused || first.enforced)
|
|
901
1318
|
return first.updates;
|
|
902
1319
|
const second = await session(ctx, item, message, domain, [...mx.keys()], fallback, fallback);
|
|
903
1320
|
for (const [address, u] of mx) {
|
|
@@ -973,20 +1390,6 @@ async function attemptGroup(ctx, item, message, domain, recipients, used) {
|
|
|
973
1390
|
return [...kept, ...updates, ...bad.map(badAddress)];
|
|
974
1391
|
}
|
|
975
1392
|
|
|
976
|
-
// src/queue/retry.ts
|
|
977
|
-
function retryDelay(attempts, retry, random) {
|
|
978
|
-
const exponent = Math.max(0, attempts - 1);
|
|
979
|
-
const delay = Math.min(retry.first * retry.factor ** exponent, retry.max);
|
|
980
|
-
return delay + delay * retry.jitter * Math.min(1, Math.max(0, random()));
|
|
981
|
-
}
|
|
982
|
-
var giveUpAt = (createdAt, retry) => createdAt + retry.giveUpAfter;
|
|
983
|
-
function nextAttemptAt(createdAt, now, attempts, retry, random) {
|
|
984
|
-
const end = giveUpAt(createdAt, retry);
|
|
985
|
-
if (now >= end)
|
|
986
|
-
return;
|
|
987
|
-
return Math.min(now + retryDelay(attempts, retry, random), end);
|
|
988
|
-
}
|
|
989
|
-
|
|
990
1393
|
// src/queue/notify.ts
|
|
991
1394
|
var MAY_HAVE_REACHED = "An earlier attempt may have reached the receiving server, so the message may have been delivered.";
|
|
992
1395
|
var MAY_HAVE_REACHED_FALLBACK = "An earlier attempt may have reached the fallback smarthost, so the message may have been delivered.";
|
|
@@ -1094,10 +1497,14 @@ async function tellDsns(ctx, item, pending, completed) {
|
|
|
1094
1497
|
|
|
1095
1498
|
// src/queue/settle.ts
|
|
1096
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 };
|
|
1097
1504
|
return {
|
|
1098
1505
|
address: update.address,
|
|
1099
1506
|
status: "failed",
|
|
1100
|
-
reply
|
|
1507
|
+
reply,
|
|
1101
1508
|
...update.mayHaveReachedMx ? { mayHaveReachedMx: true } : {},
|
|
1102
1509
|
...update.mayHaveReachedFallback ? { mayHaveReachedFallback: true } : {}
|
|
1103
1510
|
};
|
|
@@ -1197,12 +1604,18 @@ async function deliverItem(ctx, item) {
|
|
|
1197
1604
|
if (await store.get(item.id) === undefined)
|
|
1198
1605
|
return;
|
|
1199
1606
|
const failed = [...groupsOf(item).values()].flat().map(unreadable);
|
|
1200
|
-
await
|
|
1607
|
+
await record2(ctx, item, failed, false, undefined, new Map);
|
|
1201
1608
|
return;
|
|
1202
1609
|
}
|
|
1203
1610
|
const groups = groupsOf(item);
|
|
1204
1611
|
let skipped = false;
|
|
1205
1612
|
const used = new Map;
|
|
1613
|
+
const grouped = {
|
|
1614
|
+
...ctx,
|
|
1615
|
+
halted: () => {
|
|
1616
|
+
skipped = true;
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1206
1619
|
const outcomes = item.from !== "" && !isMailbox3(item.from) ? [[...groups.values()].flat().map(badSender)] : await Promise.all([...groups].map(async ([domain, group]) => {
|
|
1207
1620
|
const release = await ctx.domains.acquire(domain);
|
|
1208
1621
|
try {
|
|
@@ -1210,14 +1623,14 @@ async function deliverItem(ctx, item) {
|
|
|
1210
1623
|
skipped = true;
|
|
1211
1624
|
return [];
|
|
1212
1625
|
}
|
|
1213
|
-
return await attemptGroup(
|
|
1626
|
+
return await attemptGroup(grouped, item, message, domain, group, used);
|
|
1214
1627
|
} finally {
|
|
1215
1628
|
release();
|
|
1216
1629
|
}
|
|
1217
1630
|
}));
|
|
1218
|
-
await
|
|
1631
|
+
await record2(ctx, item, outcomes.flat(), skipped, message, used);
|
|
1219
1632
|
}
|
|
1220
|
-
async function
|
|
1633
|
+
async function record2(ctx, item, updates, skipped, message, used) {
|
|
1221
1634
|
const { settings, store, events } = ctx;
|
|
1222
1635
|
const now = settings.now();
|
|
1223
1636
|
const settled = settle(item, updates, skipped, now, settings);
|
|
@@ -1485,12 +1898,14 @@ class Worker {
|
|
|
1485
1898
|
async stop() {
|
|
1486
1899
|
this.#restart = false;
|
|
1487
1900
|
this.#stopping = true;
|
|
1901
|
+
this.#ctx.settings.sts?.abort();
|
|
1488
1902
|
this.#wake?.();
|
|
1489
1903
|
await this.#loop;
|
|
1490
1904
|
await Promise.all(this.#inFlight);
|
|
1491
1905
|
try {
|
|
1492
1906
|
await this.#ctx.settings.close();
|
|
1493
1907
|
} finally {
|
|
1908
|
+
this.#ctx.settings.sts?.reset();
|
|
1494
1909
|
this.#loop = undefined;
|
|
1495
1910
|
this.#stopping = false;
|
|
1496
1911
|
}
|
|
@@ -1540,10 +1955,11 @@ function createQueue(options) {
|
|
|
1540
1955
|
export {
|
|
1541
1956
|
DEFAULT_MAX_RETURN,
|
|
1542
1957
|
QueueError2 as QueueError,
|
|
1958
|
+
createMemoryPolicyCache,
|
|
1543
1959
|
createQueue,
|
|
1544
1960
|
deliveredDsn,
|
|
1545
1961
|
failedDsn
|
|
1546
1962
|
};
|
|
1547
1963
|
|
|
1548
|
-
//# debugId=
|
|
1964
|
+
//# debugId=7BA7A5C5D4F3B31B64756E2164756E21
|
|
1549
1965
|
//# sourceMappingURL=index.js.map
|