mailery 0.18.0 → 0.19.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/dist/admin/spa/index-B4zQqLvr.js.map +1 -1
- package/dist/index.cjs +51 -25
- 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 +51 -25
- 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 +48 -22
- 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 +48 -22
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
|
@@ -623,6 +623,15 @@ interface DnsblConfig {
|
|
|
623
623
|
dedicatedIps?: string[];
|
|
624
624
|
/** Hours between automatic runs. Default 24. Set to 0 to disable scheduled runs. */
|
|
625
625
|
intervalHours?: number;
|
|
626
|
+
/**
|
|
627
|
+
* Spamhaus Data Query Service key (free at spamhaus.com for low volume).
|
|
628
|
+
* Spamhaus refuses queries that arrive through public or shared resolvers —
|
|
629
|
+
* Google, Cloudflare, and cloud VPC resolvers such as AWS's — answering
|
|
630
|
+
* 127.255.255.254 instead of a verdict. With a key, every `*.spamhaus.org`
|
|
631
|
+
* list is queried as `<key>.<zone>.dq.spamhaus.net`, which answers from
|
|
632
|
+
* anywhere. Rows and the UI keep the public list name; the key is never stored.
|
|
633
|
+
*/
|
|
634
|
+
spamhausDqsKey?: string;
|
|
626
635
|
}
|
|
627
636
|
interface MailTesterConfig {
|
|
628
637
|
/** API key from your Mail-Tester paid plan. */
|
|
@@ -944,7 +953,7 @@ interface MailerConfig {
|
|
|
944
953
|
getAdminActor?: (req: any) => string;
|
|
945
954
|
onCircuitBreakerTrip?: (info: {
|
|
946
955
|
reason: string;
|
|
947
|
-
rates: Record<string, number>;
|
|
956
|
+
rates: Record<string, number | null>;
|
|
948
957
|
}) => Promise<void> | void;
|
|
949
958
|
onSendFailure?: (info: {
|
|
950
959
|
send: any;
|
|
@@ -1424,11 +1433,15 @@ interface HealthDoc {
|
|
|
1424
1433
|
complained: number;
|
|
1425
1434
|
failedToSend: number;
|
|
1426
1435
|
};
|
|
1436
|
+
/**
|
|
1437
|
+
* Bounce/complaint rates are per send in the window; failureRate is failed
|
|
1438
|
+
* attempts over all attempts. Null when the window has no denominator yet.
|
|
1439
|
+
*/
|
|
1427
1440
|
rates: {
|
|
1428
|
-
bounceRate: number;
|
|
1429
|
-
hardBounceRate: number;
|
|
1430
|
-
complaintRate: number;
|
|
1431
|
-
failureRate: number;
|
|
1441
|
+
bounceRate: number | null;
|
|
1442
|
+
hardBounceRate: number | null;
|
|
1443
|
+
complaintRate: number | null;
|
|
1444
|
+
failureRate: number | null;
|
|
1432
1445
|
};
|
|
1433
1446
|
status: HealthStatus;
|
|
1434
1447
|
trippedAt: Date | null;
|
|
@@ -623,6 +623,15 @@ interface DnsblConfig {
|
|
|
623
623
|
dedicatedIps?: string[];
|
|
624
624
|
/** Hours between automatic runs. Default 24. Set to 0 to disable scheduled runs. */
|
|
625
625
|
intervalHours?: number;
|
|
626
|
+
/**
|
|
627
|
+
* Spamhaus Data Query Service key (free at spamhaus.com for low volume).
|
|
628
|
+
* Spamhaus refuses queries that arrive through public or shared resolvers —
|
|
629
|
+
* Google, Cloudflare, and cloud VPC resolvers such as AWS's — answering
|
|
630
|
+
* 127.255.255.254 instead of a verdict. With a key, every `*.spamhaus.org`
|
|
631
|
+
* list is queried as `<key>.<zone>.dq.spamhaus.net`, which answers from
|
|
632
|
+
* anywhere. Rows and the UI keep the public list name; the key is never stored.
|
|
633
|
+
*/
|
|
634
|
+
spamhausDqsKey?: string;
|
|
626
635
|
}
|
|
627
636
|
interface MailTesterConfig {
|
|
628
637
|
/** API key from your Mail-Tester paid plan. */
|
|
@@ -944,7 +953,7 @@ interface MailerConfig {
|
|
|
944
953
|
getAdminActor?: (req: any) => string;
|
|
945
954
|
onCircuitBreakerTrip?: (info: {
|
|
946
955
|
reason: string;
|
|
947
|
-
rates: Record<string, number>;
|
|
956
|
+
rates: Record<string, number | null>;
|
|
948
957
|
}) => Promise<void> | void;
|
|
949
958
|
onSendFailure?: (info: {
|
|
950
959
|
send: any;
|
|
@@ -1424,11 +1433,15 @@ interface HealthDoc {
|
|
|
1424
1433
|
complained: number;
|
|
1425
1434
|
failedToSend: number;
|
|
1426
1435
|
};
|
|
1436
|
+
/**
|
|
1437
|
+
* Bounce/complaint rates are per send in the window; failureRate is failed
|
|
1438
|
+
* attempts over all attempts. Null when the window has no denominator yet.
|
|
1439
|
+
*/
|
|
1427
1440
|
rates: {
|
|
1428
|
-
bounceRate: number;
|
|
1429
|
-
hardBounceRate: number;
|
|
1430
|
-
complaintRate: number;
|
|
1431
|
-
failureRate: number;
|
|
1441
|
+
bounceRate: number | null;
|
|
1442
|
+
hardBounceRate: number | null;
|
|
1443
|
+
complaintRate: number | null;
|
|
1444
|
+
failureRate: number | null;
|
|
1432
1445
|
};
|
|
1433
1446
|
status: HealthStatus;
|
|
1434
1447
|
trippedAt: Date | null;
|
package/dist/testing.cjs
CHANGED
|
@@ -15416,11 +15416,11 @@ var ZERO_COUNTERS = {
|
|
|
15416
15416
|
complained: 0,
|
|
15417
15417
|
failedToSend: 0
|
|
15418
15418
|
};
|
|
15419
|
-
var
|
|
15420
|
-
bounceRate:
|
|
15421
|
-
hardBounceRate:
|
|
15422
|
-
complaintRate:
|
|
15423
|
-
failureRate:
|
|
15419
|
+
var EMPTY_RATES = {
|
|
15420
|
+
bounceRate: null,
|
|
15421
|
+
hardBounceRate: null,
|
|
15422
|
+
complaintRate: null,
|
|
15423
|
+
failureRate: null
|
|
15424
15424
|
};
|
|
15425
15425
|
async function recordHealthCounter(ctx, counter2, dims, by = 1) {
|
|
15426
15426
|
const windowMs = ctx.config.circuitBreaker.windowMinutes * 60 * 1e3;
|
|
@@ -15450,7 +15450,7 @@ async function upsertCounter(ctx, _id, senderDomain, kind, counter2, by, windowM
|
|
|
15450
15450
|
trippedAt: null,
|
|
15451
15451
|
trippedReason: null,
|
|
15452
15452
|
manuallyResumedAt: null,
|
|
15453
|
-
rates: { ...
|
|
15453
|
+
rates: { ...EMPTY_RATES }
|
|
15454
15454
|
},
|
|
15455
15455
|
$set: { updatedAt: /* @__PURE__ */ new Date() }
|
|
15456
15456
|
},
|
|
@@ -15477,7 +15477,7 @@ async function evaluateHealth(ctx) {
|
|
|
15477
15477
|
windowStartedAt: /* @__PURE__ */ new Date(),
|
|
15478
15478
|
windowDurationMs: windowMs,
|
|
15479
15479
|
counters: { ...ZERO_COUNTERS },
|
|
15480
|
-
rates: { ...
|
|
15480
|
+
rates: { ...EMPTY_RATES },
|
|
15481
15481
|
status: "healthy",
|
|
15482
15482
|
updatedAt: /* @__PURE__ */ new Date()
|
|
15483
15483
|
}
|
|
@@ -15485,13 +15485,21 @@ async function evaluateHealth(ctx) {
|
|
|
15485
15485
|
);
|
|
15486
15486
|
continue;
|
|
15487
15487
|
}
|
|
15488
|
-
const c = doc.counters;
|
|
15489
|
-
const
|
|
15488
|
+
const c = { ...ZERO_COUNTERS, ...doc.counters };
|
|
15489
|
+
const perSent = (n) => c.sent > 0 ? n / c.sent : null;
|
|
15490
|
+
const attempts = c.sent + c.failedToSend;
|
|
15490
15491
|
const rates = {
|
|
15491
|
-
bounceRate: c.bounced
|
|
15492
|
-
hardBounceRate: c.hardBounced
|
|
15493
|
-
complaintRate: c.complained
|
|
15494
|
-
|
|
15492
|
+
bounceRate: perSent(c.bounced),
|
|
15493
|
+
hardBounceRate: perSent(c.hardBounced),
|
|
15494
|
+
complaintRate: perSent(c.complained),
|
|
15495
|
+
// Failed attempts over all attempts, so it stays within 0–100%.
|
|
15496
|
+
failureRate: attempts > 0 ? c.failedToSend / attempts : null
|
|
15497
|
+
};
|
|
15498
|
+
const pct2 = {
|
|
15499
|
+
bounce: (rates.bounceRate ?? 0) * 100,
|
|
15500
|
+
hardBounce: (rates.hardBounceRate ?? 0) * 100,
|
|
15501
|
+
complaint: (rates.complaintRate ?? 0) * 100,
|
|
15502
|
+
failure: (rates.failureRate ?? 0) * 100
|
|
15495
15503
|
};
|
|
15496
15504
|
await ctx.collections.health.updateOne(
|
|
15497
15505
|
{ _id: doc._id },
|
|
@@ -15501,12 +15509,12 @@ async function evaluateHealth(ctx) {
|
|
|
15501
15509
|
if (c.sent < cb.minSendsBeforeEval) continue;
|
|
15502
15510
|
if (doc.status === "tripped") continue;
|
|
15503
15511
|
let trippedReason = null;
|
|
15504
|
-
if (
|
|
15505
|
-
trippedReason = `hard bounce rate ${
|
|
15506
|
-
} else if (
|
|
15507
|
-
trippedReason = `complaint rate ${
|
|
15508
|
-
} else if (
|
|
15509
|
-
trippedReason = `combined bounce rate ${
|
|
15512
|
+
if (pct2.hardBounce >= cb.hardBounceRatePctTrip) {
|
|
15513
|
+
trippedReason = `hard bounce rate ${pct2.hardBounce.toFixed(2)}% >= ${cb.hardBounceRatePctTrip}%`;
|
|
15514
|
+
} else if (pct2.complaint >= cb.complaintRatePctTrip) {
|
|
15515
|
+
trippedReason = `complaint rate ${pct2.complaint.toFixed(2)}% >= ${cb.complaintRatePctTrip}%`;
|
|
15516
|
+
} else if (pct2.bounce >= cb.combinedBounceRatePctTrip) {
|
|
15517
|
+
trippedReason = `combined bounce rate ${pct2.bounce.toFixed(2)}% >= ${cb.combinedBounceRatePctTrip}%`;
|
|
15510
15518
|
}
|
|
15511
15519
|
if (trippedReason) {
|
|
15512
15520
|
const result = await ctx.collections.health.updateOne(
|
|
@@ -15541,7 +15549,7 @@ async function evaluateHealth(ctx) {
|
|
|
15541
15549
|
}
|
|
15542
15550
|
continue;
|
|
15543
15551
|
}
|
|
15544
|
-
if (
|
|
15552
|
+
if (pct2.failure >= cb.failedToSendRatePctDegrade) {
|
|
15545
15553
|
if (doc.status !== "degraded") {
|
|
15546
15554
|
await ctx.collections.health.updateOne(
|
|
15547
15555
|
{ _id: doc._id },
|
|
@@ -16444,6 +16452,10 @@ async function dispatchSend(sendId, ctx) {
|
|
|
16444
16452
|
await markFailed(send._id, "contact_missing", ctx);
|
|
16445
16453
|
return;
|
|
16446
16454
|
}
|
|
16455
|
+
if (!send.emailAtSend?.trim() || !contact.email?.trim()) {
|
|
16456
|
+
await markFailed(send._id, "no_recipient: the contact has no email address", ctx);
|
|
16457
|
+
return;
|
|
16458
|
+
}
|
|
16447
16459
|
const run = send.flowRunId ? await ctx.collections.flowRuns.findOne({ _id: send.flowRunId }) : null;
|
|
16448
16460
|
if (run && run.status === "exited" && run.exitReason?.startsWith("aborted_by_host")) {
|
|
16449
16461
|
await ctx.collections.sends.updateOne(
|
|
@@ -16640,6 +16652,10 @@ async function processOneRunStep(runId, ctx) {
|
|
|
16640
16652
|
await exitFlowRun(run, "contact_missing", ctx);
|
|
16641
16653
|
return;
|
|
16642
16654
|
}
|
|
16655
|
+
if (!contact.email?.trim()) {
|
|
16656
|
+
await exitFlowRun(run, "contact_no_email", ctx);
|
|
16657
|
+
return;
|
|
16658
|
+
}
|
|
16643
16659
|
const sub = await ctx.collections.subscriptions.findOne({ externalId: run.externalId });
|
|
16644
16660
|
if (sub && sub.status !== "subscribed") {
|
|
16645
16661
|
await exitFlowRun(run, sub.status, ctx);
|
|
@@ -17508,7 +17524,7 @@ async function runDnsblChecks(ctx, opts = {}) {
|
|
|
17508
17524
|
}
|
|
17509
17525
|
let listedCount = 0;
|
|
17510
17526
|
async function processPair(p) {
|
|
17511
|
-
const queryName = buildQueryName(p.target, p.targetKind, p.list.host);
|
|
17527
|
+
const queryName = buildQueryName(p.target, p.targetKind, spamhausQueryHost(p.list.host, cfg.spamhausDqsKey));
|
|
17512
17528
|
const lookup2 = queryName ? await queryDnsbl(resolver, queryName) : { result: "error", returnCodes: [], errorMessage: "unsupported target format" };
|
|
17513
17529
|
if (lookup2.transient) return;
|
|
17514
17530
|
if (lookup2.result === "listed") listedCount++;
|
|
@@ -17555,6 +17571,10 @@ function collectTargets(ctx, cfg) {
|
|
|
17555
17571
|
ips: (cfg.dedicatedIps ?? []).filter((ip) => net__default.default.isIP(ip) !== 0)
|
|
17556
17572
|
};
|
|
17557
17573
|
}
|
|
17574
|
+
function spamhausQueryHost(listHost, dqsKey) {
|
|
17575
|
+
const zone = dqsKey ? /^([a-z0-9-]+)\.spamhaus\.org$/i.exec(listHost)?.[1] : void 0;
|
|
17576
|
+
return zone ? `${dqsKey}.${zone}.dq.spamhaus.net` : listHost;
|
|
17577
|
+
}
|
|
17558
17578
|
function buildQueryName(target, kind, listHost) {
|
|
17559
17579
|
if (kind === "ip") {
|
|
17560
17580
|
const v = net__default.default.isIP(target);
|
|
@@ -17590,16 +17610,22 @@ async function queryDnsbl(resolver, query) {
|
|
|
17590
17610
|
};
|
|
17591
17611
|
}
|
|
17592
17612
|
}
|
|
17613
|
+
var REFUSAL_REASONS = {
|
|
17614
|
+
"127.255.255.252": "malformed query name for this list",
|
|
17615
|
+
"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",
|
|
17616
|
+
"127.255.255.255": "this resolver has exceeded the free query limit"
|
|
17617
|
+
};
|
|
17593
17618
|
function interpretRecords(records) {
|
|
17594
17619
|
if (!records || records.length === 0) {
|
|
17595
17620
|
return { result: "clean", returnCodes: [], errorMessage: null };
|
|
17596
17621
|
}
|
|
17597
17622
|
const errorish = records.filter((r) => r.startsWith("127.255.255."));
|
|
17598
17623
|
if (errorish.length === records.length) {
|
|
17624
|
+
const reasons = [...new Set(records.map((r) => REFUSAL_REASONS[r] ?? "unrecognised refusal code"))];
|
|
17599
17625
|
return {
|
|
17600
17626
|
result: "error",
|
|
17601
17627
|
returnCodes: records,
|
|
17602
|
-
errorMessage: `list
|
|
17628
|
+
errorMessage: `not a listing \u2014 the list refused the query (${records.join(", ")}): ${reasons.join("; ")}`
|
|
17603
17629
|
};
|
|
17604
17630
|
}
|
|
17605
17631
|
const listed = records.filter((r) => r.startsWith("127.") && !r.startsWith("127.255.255."));
|