mailery 0.18.0 → 0.19.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.
- package/dist/admin/spa/index-B4zQqLvr.js.map +1 -1
- package/dist/index.cjs +62 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +62 -29
- package/dist/index.js.map +1 -1
- package/dist/{null-BFipn5tF.d.cts → null-DxTHQSrJ.d.cts} +18 -5
- package/dist/{null-BFipn5tF.d.ts → null-DxTHQSrJ.d.ts} +18 -5
- package/dist/testing.cjs +59 -26
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +59 -26
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1717,11 +1717,11 @@ var ZERO_COUNTERS = {
|
|
|
1717
1717
|
complained: 0,
|
|
1718
1718
|
failedToSend: 0
|
|
1719
1719
|
};
|
|
1720
|
-
var
|
|
1721
|
-
bounceRate:
|
|
1722
|
-
hardBounceRate:
|
|
1723
|
-
complaintRate:
|
|
1724
|
-
failureRate:
|
|
1720
|
+
var EMPTY_RATES = {
|
|
1721
|
+
bounceRate: null,
|
|
1722
|
+
hardBounceRate: null,
|
|
1723
|
+
complaintRate: null,
|
|
1724
|
+
failureRate: null
|
|
1725
1725
|
};
|
|
1726
1726
|
async function recordHealthCounter(ctx, counter2, dims, by = 1) {
|
|
1727
1727
|
const windowMs = ctx.config.circuitBreaker.windowMinutes * 60 * 1e3;
|
|
@@ -1751,7 +1751,7 @@ async function upsertCounter(ctx, _id, senderDomain, kind, counter2, by, windowM
|
|
|
1751
1751
|
trippedAt: null,
|
|
1752
1752
|
trippedReason: null,
|
|
1753
1753
|
manuallyResumedAt: null,
|
|
1754
|
-
rates: { ...
|
|
1754
|
+
rates: { ...EMPTY_RATES }
|
|
1755
1755
|
},
|
|
1756
1756
|
$set: { updatedAt: /* @__PURE__ */ new Date() }
|
|
1757
1757
|
},
|
|
@@ -1778,7 +1778,7 @@ async function evaluateHealth(ctx) {
|
|
|
1778
1778
|
windowStartedAt: /* @__PURE__ */ new Date(),
|
|
1779
1779
|
windowDurationMs: windowMs,
|
|
1780
1780
|
counters: { ...ZERO_COUNTERS },
|
|
1781
|
-
rates: { ...
|
|
1781
|
+
rates: { ...EMPTY_RATES },
|
|
1782
1782
|
status: "healthy",
|
|
1783
1783
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1784
1784
|
}
|
|
@@ -1786,13 +1786,21 @@ async function evaluateHealth(ctx) {
|
|
|
1786
1786
|
);
|
|
1787
1787
|
continue;
|
|
1788
1788
|
}
|
|
1789
|
-
const c = doc.counters;
|
|
1790
|
-
const
|
|
1789
|
+
const c = { ...ZERO_COUNTERS, ...doc.counters };
|
|
1790
|
+
const perSent = (n) => c.sent > 0 ? n / c.sent : null;
|
|
1791
|
+
const attempts = c.sent + c.failedToSend;
|
|
1791
1792
|
const rates = {
|
|
1792
|
-
bounceRate: c.bounced
|
|
1793
|
-
hardBounceRate: c.hardBounced
|
|
1794
|
-
complaintRate: c.complained
|
|
1795
|
-
|
|
1793
|
+
bounceRate: perSent(c.bounced),
|
|
1794
|
+
hardBounceRate: perSent(c.hardBounced),
|
|
1795
|
+
complaintRate: perSent(c.complained),
|
|
1796
|
+
// Failed attempts over all attempts, so it stays within 0–100%.
|
|
1797
|
+
failureRate: attempts > 0 ? c.failedToSend / attempts : null
|
|
1798
|
+
};
|
|
1799
|
+
const pct2 = {
|
|
1800
|
+
bounce: (rates.bounceRate ?? 0) * 100,
|
|
1801
|
+
hardBounce: (rates.hardBounceRate ?? 0) * 100,
|
|
1802
|
+
complaint: (rates.complaintRate ?? 0) * 100,
|
|
1803
|
+
failure: (rates.failureRate ?? 0) * 100
|
|
1796
1804
|
};
|
|
1797
1805
|
await ctx.collections.health.updateOne(
|
|
1798
1806
|
{ _id: doc._id },
|
|
@@ -1802,12 +1810,12 @@ async function evaluateHealth(ctx) {
|
|
|
1802
1810
|
if (c.sent < cb.minSendsBeforeEval) continue;
|
|
1803
1811
|
if (doc.status === "tripped") continue;
|
|
1804
1812
|
let trippedReason = null;
|
|
1805
|
-
if (
|
|
1806
|
-
trippedReason = `hard bounce rate ${
|
|
1807
|
-
} else if (
|
|
1808
|
-
trippedReason = `complaint rate ${
|
|
1809
|
-
} else if (
|
|
1810
|
-
trippedReason = `combined bounce rate ${
|
|
1813
|
+
if (pct2.hardBounce >= cb.hardBounceRatePctTrip) {
|
|
1814
|
+
trippedReason = `hard bounce rate ${pct2.hardBounce.toFixed(2)}% >= ${cb.hardBounceRatePctTrip}%`;
|
|
1815
|
+
} else if (pct2.complaint >= cb.complaintRatePctTrip) {
|
|
1816
|
+
trippedReason = `complaint rate ${pct2.complaint.toFixed(2)}% >= ${cb.complaintRatePctTrip}%`;
|
|
1817
|
+
} else if (pct2.bounce >= cb.combinedBounceRatePctTrip) {
|
|
1818
|
+
trippedReason = `combined bounce rate ${pct2.bounce.toFixed(2)}% >= ${cb.combinedBounceRatePctTrip}%`;
|
|
1811
1819
|
}
|
|
1812
1820
|
if (trippedReason) {
|
|
1813
1821
|
const result = await ctx.collections.health.updateOne(
|
|
@@ -1842,7 +1850,7 @@ async function evaluateHealth(ctx) {
|
|
|
1842
1850
|
}
|
|
1843
1851
|
continue;
|
|
1844
1852
|
}
|
|
1845
|
-
if (
|
|
1853
|
+
if (pct2.failure >= cb.failedToSendRatePctDegrade) {
|
|
1846
1854
|
if (doc.status !== "degraded") {
|
|
1847
1855
|
await ctx.collections.health.updateOne(
|
|
1848
1856
|
{ _id: doc._id },
|
|
@@ -2804,6 +2812,10 @@ async function dispatchSend(sendId, ctx) {
|
|
|
2804
2812
|
await markFailed(send._id, "contact_missing", ctx);
|
|
2805
2813
|
return;
|
|
2806
2814
|
}
|
|
2815
|
+
if (!send.emailAtSend?.trim() || !contact.email?.trim()) {
|
|
2816
|
+
await markFailed(send._id, "no_recipient: the contact has no email address", ctx);
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2807
2819
|
const run = send.flowRunId ? await ctx.collections.flowRuns.findOne({ _id: send.flowRunId }) : null;
|
|
2808
2820
|
if (run && run.status === "exited" && run.exitReason?.startsWith("aborted_by_host")) {
|
|
2809
2821
|
await ctx.collections.sends.updateOne(
|
|
@@ -3000,6 +3012,10 @@ async function processOneRunStep(runId, ctx) {
|
|
|
3000
3012
|
await exitFlowRun(run, "contact_missing", ctx);
|
|
3001
3013
|
return;
|
|
3002
3014
|
}
|
|
3015
|
+
if (!contact.email?.trim()) {
|
|
3016
|
+
await exitFlowRun(run, "contact_no_email", ctx);
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3003
3019
|
const sub = await ctx.collections.subscriptions.findOne({ externalId: run.externalId });
|
|
3004
3020
|
if (sub && sub.status !== "subscribed") {
|
|
3005
3021
|
await exitFlowRun(run, sub.status, ctx);
|
|
@@ -3895,8 +3911,8 @@ async function runDnsblChecks(ctx, opts = {}) {
|
|
|
3895
3911
|
}
|
|
3896
3912
|
let listedCount = 0;
|
|
3897
3913
|
async function processPair(p) {
|
|
3898
|
-
const queryName = buildQueryName(p.target, p.targetKind, p.list.host);
|
|
3899
|
-
const lookup2 = queryName ? await queryDnsbl(resolver, queryName) : { result: "error", returnCodes: [], errorMessage: "unsupported target format" };
|
|
3914
|
+
const queryName = buildQueryName(p.target, p.targetKind, spamhausQueryHost(p.list.host, cfg.spamhausDqsKey));
|
|
3915
|
+
const lookup2 = queryName ? await queryDnsbl(resolver, queryName, p.list.host) : { result: "error", returnCodes: [], errorMessage: "unsupported target format" };
|
|
3900
3916
|
if (lookup2.transient) return;
|
|
3901
3917
|
if (lookup2.result === "listed") listedCount++;
|
|
3902
3918
|
await ctx.collections.dnsblChecks.updateOne(
|
|
@@ -3942,6 +3958,10 @@ function collectTargets(ctx, cfg) {
|
|
|
3942
3958
|
ips: (cfg.dedicatedIps ?? []).filter((ip) => net__default.default.isIP(ip) !== 0)
|
|
3943
3959
|
};
|
|
3944
3960
|
}
|
|
3961
|
+
function spamhausQueryHost(listHost, dqsKey) {
|
|
3962
|
+
const zone = dqsKey ? /^([a-z0-9-]+)\.spamhaus\.org$/i.exec(listHost)?.[1] : void 0;
|
|
3963
|
+
return zone ? `${dqsKey}.${zone}.dq.spamhaus.net` : listHost;
|
|
3964
|
+
}
|
|
3945
3965
|
function buildQueryName(target, kind, listHost) {
|
|
3946
3966
|
if (kind === "ip") {
|
|
3947
3967
|
const v = net__default.default.isIP(target);
|
|
@@ -3953,10 +3973,10 @@ function buildQueryName(target, kind, listHost) {
|
|
|
3953
3973
|
return `${domain}.${listHost}`;
|
|
3954
3974
|
}
|
|
3955
3975
|
var TRANSIENT_DNS_CODES = /* @__PURE__ */ new Set(["ESERVFAIL", "EREFUSED", "ETIMEOUT", "ETIMEDOUT", "ECONNRESET", "ECONNREFUSED"]);
|
|
3956
|
-
async function queryDnsbl(resolver, query) {
|
|
3976
|
+
async function queryDnsbl(resolver, query, listHost) {
|
|
3957
3977
|
try {
|
|
3958
3978
|
const records = await resolver.resolve4(query);
|
|
3959
|
-
return interpretRecords(records);
|
|
3979
|
+
return interpretRecords(records, listHost);
|
|
3960
3980
|
} catch (err) {
|
|
3961
3981
|
const code = err?.code;
|
|
3962
3982
|
if (code === "ENOTFOUND" || code === "ENODATA") {
|
|
@@ -3977,16 +3997,29 @@ async function queryDnsbl(resolver, query) {
|
|
|
3977
3997
|
};
|
|
3978
3998
|
}
|
|
3979
3999
|
}
|
|
3980
|
-
|
|
4000
|
+
var REFUSAL_REASONS = {
|
|
4001
|
+
"127.255.255.252": "malformed query name for this list",
|
|
4002
|
+
"127.255.255.254": "queried through a public or shared DNS resolver, which Spamhaus blocks. Set dnsbl.spamhausDqsKey (free Spamhaus DQS key) or use a private resolver",
|
|
4003
|
+
"127.255.255.255": "this resolver has exceeded the free query limit"
|
|
4004
|
+
};
|
|
4005
|
+
function interpretRecords(records, listHost) {
|
|
3981
4006
|
if (!records || records.length === 0) {
|
|
3982
4007
|
return { result: "clean", returnCodes: [], errorMessage: null };
|
|
3983
4008
|
}
|
|
4009
|
+
if (listHost && REGISTRABLE_ONLY_LISTS.has(listHost) && records.every((r) => r === "127.0.0.1")) {
|
|
4010
|
+
return {
|
|
4011
|
+
result: "error",
|
|
4012
|
+
returnCodes: records,
|
|
4013
|
+
errorMessage: `${listHost} refused the query (127.0.0.1): queried through a public or shared DNS resolver, or over the free query limit. Use a private resolver`
|
|
4014
|
+
};
|
|
4015
|
+
}
|
|
3984
4016
|
const errorish = records.filter((r) => r.startsWith("127.255.255."));
|
|
3985
4017
|
if (errorish.length === records.length) {
|
|
4018
|
+
const reasons = [...new Set(records.map((r) => REFUSAL_REASONS[r] ?? "unrecognised refusal code"))];
|
|
3986
4019
|
return {
|
|
3987
4020
|
result: "error",
|
|
3988
4021
|
returnCodes: records,
|
|
3989
|
-
errorMessage: `list
|
|
4022
|
+
errorMessage: `not a listing \u2014 the list refused the query (${records.join(", ")}): ${reasons.join("; ")}`
|
|
3990
4023
|
};
|
|
3991
4024
|
}
|
|
3992
4025
|
const listed = records.filter((r) => r.startsWith("127.") && !r.startsWith("127.255.255."));
|
|
@@ -6436,7 +6469,7 @@ async function checkDnsbl(mailer) {
|
|
|
6436
6469
|
label: "DNS block-list checks",
|
|
6437
6470
|
severity: "warn",
|
|
6438
6471
|
message: `all ${docs.length} checks errored`,
|
|
6439
|
-
hint: "Lookups against block lists are failing \u2014 usually a DNS-resolution problem on the host running mailery, or a list
|
|
6472
|
+
hint: "Lookups against block lists are failing \u2014 usually a DNS-resolution problem on the host running mailery, or a list refusing your public resolver (Spamhaus answers 127.255.255.254; set dnsbl.spamhausDqsKey). Verify outbound DNS works."
|
|
6440
6473
|
};
|
|
6441
6474
|
}
|
|
6442
6475
|
return {
|
|
@@ -9546,7 +9579,7 @@ function unitToMs2(value, unit) {
|
|
|
9546
9579
|
}
|
|
9547
9580
|
|
|
9548
9581
|
// src/server/api/agent.ts
|
|
9549
|
-
var VERSION = "0.
|
|
9582
|
+
var VERSION = "0.19.1" ;
|
|
9550
9583
|
var broadcastCountSchema = zod.z.object({
|
|
9551
9584
|
recipientCap: zod.z.number().int().positive().max(1e7).nullable().optional()
|
|
9552
9585
|
});
|
|
@@ -11613,7 +11646,7 @@ var DEDUPE_POLICIES = [
|
|
|
11613
11646
|
];
|
|
11614
11647
|
|
|
11615
11648
|
// src/server/index.ts
|
|
11616
|
-
var VERSION2 = "0.
|
|
11649
|
+
var VERSION2 = "0.19.1" ;
|
|
11617
11650
|
|
|
11618
11651
|
exports.DEDUPE_POLICIES = DEDUPE_POLICIES;
|
|
11619
11652
|
exports.DEFAULT_BOT_UA_RE = DEFAULT_BOT_UA_RE;
|