@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.
Files changed (45) hide show
  1. package/README.md +65 -4
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +517 -32
  5. package/dist/index.js.map +23 -14
  6. package/dist/queue/attempt.d.ts.map +1 -1
  7. package/dist/queue/deliver.d.ts +2 -0
  8. package/dist/queue/deliver.d.ts.map +1 -1
  9. package/dist/queue/events.d.ts +10 -0
  10. package/dist/queue/events.d.ts.map +1 -1
  11. package/dist/queue/fallback-session.d.ts.map +1 -1
  12. package/dist/queue/fallback.d.ts.map +1 -1
  13. package/dist/queue/options.d.ts +25 -1
  14. package/dist/queue/options.d.ts.map +1 -1
  15. package/dist/queue/outcome.d.ts.map +1 -1
  16. package/dist/queue/session.d.ts +9 -1
  17. package/dist/queue/session.d.ts.map +1 -1
  18. package/dist/queue/settings/index.d.ts +8 -2
  19. package/dist/queue/settings/index.d.ts.map +1 -1
  20. package/dist/queue/settings/sender.d.ts +16 -0
  21. package/dist/queue/settings/sender.d.ts.map +1 -0
  22. package/dist/queue/settle.d.ts.map +1 -1
  23. package/dist/queue/worker.d.ts.map +1 -1
  24. package/dist/sts/attempt.d.ts +15 -0
  25. package/dist/sts/attempt.d.ts.map +1 -0
  26. package/dist/sts/bound.d.ts +15 -0
  27. package/dist/sts/bound.d.ts.map +1 -0
  28. package/dist/sts/cache.d.ts +27 -0
  29. package/dist/sts/cache.d.ts.map +1 -0
  30. package/dist/sts/discover.d.ts +52 -0
  31. package/dist/sts/discover.d.ts.map +1 -0
  32. package/dist/sts/options.d.ts +77 -0
  33. package/dist/sts/options.d.ts.map +1 -0
  34. package/dist/sts/refusal.d.ts +10 -0
  35. package/dist/sts/refusal.d.ts.map +1 -0
  36. package/dist/sts/run.d.ts +55 -0
  37. package/dist/sts/run.d.ts.map +1 -0
  38. package/dist/sts/settings.d.ts +5 -0
  39. package/dist/sts/settings.d.ts.map +1 -0
  40. package/dist/sts/tally.d.ts +20 -0
  41. package/dist/sts/tally.d.ts.map +1 -0
  42. package/docs/guide.md +98 -2
  43. package/docs/roadmap.md +15 -7
  44. package/docs/troubleshooting.md +75 -0
  45. package/package.json +5 -3
package/dist/index.js CHANGED
@@ -471,8 +471,66 @@ class Events {
471
471
  }
472
472
  }
473
473
 
474
- // src/queue/settings/index.ts
475
- import { sendMail } from "@bumail/smtp/client";
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: options.send ?? sendMail,
643
- random: options.random ?? Math.random
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
- ...sendOptionsOf(settings, domain, item.from, group, override),
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
- return { updates: outcomesOf(result, group, max) };
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 updates = outcomesOfError(error, group, max, hostOf(route));
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 { updates: doubt ? doubtful(updates, error, key) : updates, error };
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: { text: "", ...update.reply, status: "4.4.7" },
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 record(ctx, item, failed, false, undefined, new Map);
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(ctx, item, message, domain, group, used);
1626
+ return await attemptGroup(grouped, item, message, domain, group, used);
1149
1627
  } finally {
1150
1628
  release();
1151
1629
  }
1152
1630
  }));
1153
- await record(ctx, item, outcomes.flat(), skipped, message, used);
1631
+ await record2(ctx, item, outcomes.flat(), skipped, message, used);
1154
1632
  }
1155
- async function record(ctx, item, updates, skipped, message, used) {
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
- this.#loop = undefined;
1427
- this.#stopping = false;
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=9190A6EB36D3FC0664756E2164756E21
1964
+ //# debugId=7BA7A5C5D4F3B31B64756E2164756E21
1480
1965
  //# sourceMappingURL=index.js.map