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.
@@ -1,6 +1,6 @@
1
1
  import { Db } from 'mongodb';
2
- import { C as ContactAdapter, a as Contact, A as AdapterFilter, b as AdapterSort, M as MailProvider, S as SendArgs, c as SendResult, N as NormalizedEvent, F as FlowStep, a6 as FlowTrigger, q as FlowGoal, Y as TemplateKind, T as TemplateDoc, g as FlowDoc, D as DeliveryWindow, R as RunnerContext, e as Mailer, a7 as QueueDriverConfig } from './null-BFipn5tF.cjs';
3
- export { w as NullProvider } from './null-BFipn5tF.cjs';
2
+ import { C as ContactAdapter, a as Contact, A as AdapterFilter, b as AdapterSort, M as MailProvider, S as SendArgs, c as SendResult, N as NormalizedEvent, F as FlowStep, a6 as FlowTrigger, q as FlowGoal, Y as TemplateKind, T as TemplateDoc, g as FlowDoc, D as DeliveryWindow, R as RunnerContext, e as Mailer, a7 as QueueDriverConfig } from './null-DxTHQSrJ.cjs';
3
+ export { w as NullProvider } from './null-DxTHQSrJ.cjs';
4
4
  import 'zod';
5
5
  import 'handlebars';
6
6
  import 'ioredis';
package/dist/testing.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Db } from 'mongodb';
2
- import { C as ContactAdapter, a as Contact, A as AdapterFilter, b as AdapterSort, M as MailProvider, S as SendArgs, c as SendResult, N as NormalizedEvent, F as FlowStep, a6 as FlowTrigger, q as FlowGoal, Y as TemplateKind, T as TemplateDoc, g as FlowDoc, D as DeliveryWindow, R as RunnerContext, e as Mailer, a7 as QueueDriverConfig } from './null-BFipn5tF.js';
3
- export { w as NullProvider } from './null-BFipn5tF.js';
2
+ import { C as ContactAdapter, a as Contact, A as AdapterFilter, b as AdapterSort, M as MailProvider, S as SendArgs, c as SendResult, N as NormalizedEvent, F as FlowStep, a6 as FlowTrigger, q as FlowGoal, Y as TemplateKind, T as TemplateDoc, g as FlowDoc, D as DeliveryWindow, R as RunnerContext, e as Mailer, a7 as QueueDriverConfig } from './null-DxTHQSrJ.js';
3
+ export { w as NullProvider } from './null-DxTHQSrJ.js';
4
4
  import 'zod';
5
5
  import 'handlebars';
6
6
  import 'ioredis';
package/dist/testing.js CHANGED
@@ -15401,11 +15401,11 @@ var ZERO_COUNTERS = {
15401
15401
  complained: 0,
15402
15402
  failedToSend: 0
15403
15403
  };
15404
- var ZERO_RATES = {
15405
- bounceRate: 0,
15406
- hardBounceRate: 0,
15407
- complaintRate: 0,
15408
- failureRate: 0
15404
+ var EMPTY_RATES = {
15405
+ bounceRate: null,
15406
+ hardBounceRate: null,
15407
+ complaintRate: null,
15408
+ failureRate: null
15409
15409
  };
15410
15410
  async function recordHealthCounter(ctx, counter2, dims, by = 1) {
15411
15411
  const windowMs = ctx.config.circuitBreaker.windowMinutes * 60 * 1e3;
@@ -15435,7 +15435,7 @@ async function upsertCounter(ctx, _id, senderDomain, kind, counter2, by, windowM
15435
15435
  trippedAt: null,
15436
15436
  trippedReason: null,
15437
15437
  manuallyResumedAt: null,
15438
- rates: { ...ZERO_RATES }
15438
+ rates: { ...EMPTY_RATES }
15439
15439
  },
15440
15440
  $set: { updatedAt: /* @__PURE__ */ new Date() }
15441
15441
  },
@@ -15462,7 +15462,7 @@ async function evaluateHealth(ctx) {
15462
15462
  windowStartedAt: /* @__PURE__ */ new Date(),
15463
15463
  windowDurationMs: windowMs,
15464
15464
  counters: { ...ZERO_COUNTERS },
15465
- rates: { ...ZERO_RATES },
15465
+ rates: { ...EMPTY_RATES },
15466
15466
  status: "healthy",
15467
15467
  updatedAt: /* @__PURE__ */ new Date()
15468
15468
  }
@@ -15470,13 +15470,21 @@ async function evaluateHealth(ctx) {
15470
15470
  );
15471
15471
  continue;
15472
15472
  }
15473
- const c = doc.counters;
15474
- const total = c.sent || 1;
15473
+ const c = { ...ZERO_COUNTERS, ...doc.counters };
15474
+ const perSent = (n) => c.sent > 0 ? n / c.sent : null;
15475
+ const attempts = c.sent + c.failedToSend;
15475
15476
  const rates = {
15476
- bounceRate: c.bounced / total,
15477
- hardBounceRate: c.hardBounced / total,
15478
- complaintRate: c.complained / total,
15479
- failureRate: c.failedToSend / total
15477
+ bounceRate: perSent(c.bounced),
15478
+ hardBounceRate: perSent(c.hardBounced),
15479
+ complaintRate: perSent(c.complained),
15480
+ // Failed attempts over all attempts, so it stays within 0–100%.
15481
+ failureRate: attempts > 0 ? c.failedToSend / attempts : null
15482
+ };
15483
+ const pct2 = {
15484
+ bounce: (rates.bounceRate ?? 0) * 100,
15485
+ hardBounce: (rates.hardBounceRate ?? 0) * 100,
15486
+ complaint: (rates.complaintRate ?? 0) * 100,
15487
+ failure: (rates.failureRate ?? 0) * 100
15480
15488
  };
15481
15489
  await ctx.collections.health.updateOne(
15482
15490
  { _id: doc._id },
@@ -15486,12 +15494,12 @@ async function evaluateHealth(ctx) {
15486
15494
  if (c.sent < cb.minSendsBeforeEval) continue;
15487
15495
  if (doc.status === "tripped") continue;
15488
15496
  let trippedReason = null;
15489
- if (rates.hardBounceRate * 100 >= cb.hardBounceRatePctTrip) {
15490
- trippedReason = `hard bounce rate ${(rates.hardBounceRate * 100).toFixed(2)}% >= ${cb.hardBounceRatePctTrip}%`;
15491
- } else if (rates.complaintRate * 100 >= cb.complaintRatePctTrip) {
15492
- trippedReason = `complaint rate ${(rates.complaintRate * 100).toFixed(2)}% >= ${cb.complaintRatePctTrip}%`;
15493
- } else if (rates.bounceRate * 100 >= cb.combinedBounceRatePctTrip) {
15494
- trippedReason = `combined bounce rate ${(rates.bounceRate * 100).toFixed(2)}% >= ${cb.combinedBounceRatePctTrip}%`;
15497
+ if (pct2.hardBounce >= cb.hardBounceRatePctTrip) {
15498
+ trippedReason = `hard bounce rate ${pct2.hardBounce.toFixed(2)}% >= ${cb.hardBounceRatePctTrip}%`;
15499
+ } else if (pct2.complaint >= cb.complaintRatePctTrip) {
15500
+ trippedReason = `complaint rate ${pct2.complaint.toFixed(2)}% >= ${cb.complaintRatePctTrip}%`;
15501
+ } else if (pct2.bounce >= cb.combinedBounceRatePctTrip) {
15502
+ trippedReason = `combined bounce rate ${pct2.bounce.toFixed(2)}% >= ${cb.combinedBounceRatePctTrip}%`;
15495
15503
  }
15496
15504
  if (trippedReason) {
15497
15505
  const result = await ctx.collections.health.updateOne(
@@ -15526,7 +15534,7 @@ async function evaluateHealth(ctx) {
15526
15534
  }
15527
15535
  continue;
15528
15536
  }
15529
- if (rates.failureRate * 100 >= cb.failedToSendRatePctDegrade) {
15537
+ if (pct2.failure >= cb.failedToSendRatePctDegrade) {
15530
15538
  if (doc.status !== "degraded") {
15531
15539
  await ctx.collections.health.updateOne(
15532
15540
  { _id: doc._id },
@@ -16429,6 +16437,10 @@ async function dispatchSend(sendId, ctx) {
16429
16437
  await markFailed(send._id, "contact_missing", ctx);
16430
16438
  return;
16431
16439
  }
16440
+ if (!send.emailAtSend?.trim() || !contact.email?.trim()) {
16441
+ await markFailed(send._id, "no_recipient: the contact has no email address", ctx);
16442
+ return;
16443
+ }
16432
16444
  const run = send.flowRunId ? await ctx.collections.flowRuns.findOne({ _id: send.flowRunId }) : null;
16433
16445
  if (run && run.status === "exited" && run.exitReason?.startsWith("aborted_by_host")) {
16434
16446
  await ctx.collections.sends.updateOne(
@@ -16625,6 +16637,10 @@ async function processOneRunStep(runId, ctx) {
16625
16637
  await exitFlowRun(run, "contact_missing", ctx);
16626
16638
  return;
16627
16639
  }
16640
+ if (!contact.email?.trim()) {
16641
+ await exitFlowRun(run, "contact_no_email", ctx);
16642
+ return;
16643
+ }
16628
16644
  const sub = await ctx.collections.subscriptions.findOne({ externalId: run.externalId });
16629
16645
  if (sub && sub.status !== "subscribed") {
16630
16646
  await exitFlowRun(run, sub.status, ctx);
@@ -17493,8 +17509,8 @@ async function runDnsblChecks(ctx, opts = {}) {
17493
17509
  }
17494
17510
  let listedCount = 0;
17495
17511
  async function processPair(p) {
17496
- const queryName = buildQueryName(p.target, p.targetKind, p.list.host);
17497
- const lookup2 = queryName ? await queryDnsbl(resolver, queryName) : { result: "error", returnCodes: [], errorMessage: "unsupported target format" };
17512
+ const queryName = buildQueryName(p.target, p.targetKind, spamhausQueryHost(p.list.host, cfg.spamhausDqsKey));
17513
+ const lookup2 = queryName ? await queryDnsbl(resolver, queryName, p.list.host) : { result: "error", returnCodes: [], errorMessage: "unsupported target format" };
17498
17514
  if (lookup2.transient) return;
17499
17515
  if (lookup2.result === "listed") listedCount++;
17500
17516
  await ctx.collections.dnsblChecks.updateOne(
@@ -17540,6 +17556,10 @@ function collectTargets(ctx, cfg) {
17540
17556
  ips: (cfg.dedicatedIps ?? []).filter((ip) => net.isIP(ip) !== 0)
17541
17557
  };
17542
17558
  }
17559
+ function spamhausQueryHost(listHost, dqsKey) {
17560
+ const zone = dqsKey ? /^([a-z0-9-]+)\.spamhaus\.org$/i.exec(listHost)?.[1] : void 0;
17561
+ return zone ? `${dqsKey}.${zone}.dq.spamhaus.net` : listHost;
17562
+ }
17543
17563
  function buildQueryName(target, kind, listHost) {
17544
17564
  if (kind === "ip") {
17545
17565
  const v = net.isIP(target);
@@ -17551,10 +17571,10 @@ function buildQueryName(target, kind, listHost) {
17551
17571
  return `${domain}.${listHost}`;
17552
17572
  }
17553
17573
  var TRANSIENT_DNS_CODES = /* @__PURE__ */ new Set(["ESERVFAIL", "EREFUSED", "ETIMEOUT", "ETIMEDOUT", "ECONNRESET", "ECONNREFUSED"]);
17554
- async function queryDnsbl(resolver, query) {
17574
+ async function queryDnsbl(resolver, query, listHost) {
17555
17575
  try {
17556
17576
  const records = await resolver.resolve4(query);
17557
- return interpretRecords(records);
17577
+ return interpretRecords(records, listHost);
17558
17578
  } catch (err) {
17559
17579
  const code = err?.code;
17560
17580
  if (code === "ENOTFOUND" || code === "ENODATA") {
@@ -17575,16 +17595,29 @@ async function queryDnsbl(resolver, query) {
17575
17595
  };
17576
17596
  }
17577
17597
  }
17578
- function interpretRecords(records) {
17598
+ var REFUSAL_REASONS = {
17599
+ "127.255.255.252": "malformed query name for this list",
17600
+ "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",
17601
+ "127.255.255.255": "this resolver has exceeded the free query limit"
17602
+ };
17603
+ function interpretRecords(records, listHost) {
17579
17604
  if (!records || records.length === 0) {
17580
17605
  return { result: "clean", returnCodes: [], errorMessage: null };
17581
17606
  }
17607
+ if (listHost && REGISTRABLE_ONLY_LISTS.has(listHost) && records.every((r) => r === "127.0.0.1")) {
17608
+ return {
17609
+ result: "error",
17610
+ returnCodes: records,
17611
+ 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`
17612
+ };
17613
+ }
17582
17614
  const errorish = records.filter((r) => r.startsWith("127.255.255."));
17583
17615
  if (errorish.length === records.length) {
17616
+ const reasons = [...new Set(records.map((r) => REFUSAL_REASONS[r] ?? "unrecognised refusal code"))];
17584
17617
  return {
17585
17618
  result: "error",
17586
17619
  returnCodes: records,
17587
- errorMessage: `list returned reserved code(s): ${records.join(", ")}`
17620
+ errorMessage: `not a listing \u2014 the list refused the query (${records.join(", ")}): ${reasons.join("; ")}`
17588
17621
  };
17589
17622
  }
17590
17623
  const listed = records.filter((r) => r.startsWith("127.") && !r.startsWith("127.255.255."));