@bumail/queue 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +44 -4
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +442 -26
  5. package/dist/index.js.map +21 -13
  6. package/dist/queue/deliver.d.ts +2 -0
  7. package/dist/queue/deliver.d.ts.map +1 -1
  8. package/dist/queue/events.d.ts +3 -0
  9. package/dist/queue/events.d.ts.map +1 -1
  10. package/dist/queue/fallback-session.d.ts.map +1 -1
  11. package/dist/queue/fallback.d.ts.map +1 -1
  12. package/dist/queue/options.d.ts +10 -0
  13. package/dist/queue/options.d.ts.map +1 -1
  14. package/dist/queue/outcome.d.ts.map +1 -1
  15. package/dist/queue/session.d.ts +4 -0
  16. package/dist/queue/session.d.ts.map +1 -1
  17. package/dist/queue/settings/index.d.ts +3 -0
  18. package/dist/queue/settings/index.d.ts.map +1 -1
  19. package/dist/queue/settle.d.ts.map +1 -1
  20. package/dist/queue/worker.d.ts.map +1 -1
  21. package/dist/sts/attempt.d.ts +15 -0
  22. package/dist/sts/attempt.d.ts.map +1 -0
  23. package/dist/sts/bound.d.ts +15 -0
  24. package/dist/sts/bound.d.ts.map +1 -0
  25. package/dist/sts/cache.d.ts +27 -0
  26. package/dist/sts/cache.d.ts.map +1 -0
  27. package/dist/sts/discover.d.ts +52 -0
  28. package/dist/sts/discover.d.ts.map +1 -0
  29. package/dist/sts/options.d.ts +77 -0
  30. package/dist/sts/options.d.ts.map +1 -0
  31. package/dist/sts/refusal.d.ts +10 -0
  32. package/dist/sts/refusal.d.ts.map +1 -0
  33. package/dist/sts/run.d.ts +55 -0
  34. package/dist/sts/run.d.ts.map +1 -0
  35. package/dist/sts/settings.d.ts +5 -0
  36. package/dist/sts/settings.d.ts.map +1 -0
  37. package/dist/sts/tally.d.ts +20 -0
  38. package/dist/sts/tally.d.ts.map +1 -0
  39. package/docs/guide.md +63 -1
  40. package/docs/roadmap.md +6 -4
  41. package/docs/troubleshooting.md +49 -0
  42. package/package.json +7 -5
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
- ...sendOptionsOf(settings, domain, item.from, group, override),
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 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;
883
1295
  const key = route === "mx" ? "mayHaveReachedMx" : "mayHaveReachedFallback";
884
1296
  const doubt = (route === "mx" || override !== undefined) && isSmtpError(error) && error.afterData;
885
- 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
+ };
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: { text: "", ...update.reply, status: "4.4.7" },
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 record(ctx, item, failed, false, undefined, new Map);
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(ctx, item, message, domain, group, used);
1626
+ return await attemptGroup(grouped, item, message, domain, group, used);
1214
1627
  } finally {
1215
1628
  release();
1216
1629
  }
1217
1630
  }));
1218
- await record(ctx, item, outcomes.flat(), skipped, message, used);
1631
+ await record2(ctx, item, outcomes.flat(), skipped, message, used);
1219
1632
  }
1220
- async function record(ctx, item, updates, skipped, message, used) {
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=C2B635B10EF5403B64756E2164756E21
1964
+ //# debugId=7BA7A5C5D4F3B31B64756E2164756E21
1549
1965
  //# sourceMappingURL=index.js.map