mailery 0.3.2 → 0.5.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-Dmsla2eL.js +41 -0
- package/dist/admin/spa/index-Dmsla2eL.js.map +1 -0
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/template-editor-Bn28OPMU.js +502 -0
- package/dist/admin/spa/{template-editor-dMzrGcva.js.map → template-editor-Bn28OPMU.js.map} +1 -1
- package/dist/cli.cjs +231 -56
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +231 -56
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2733 -222
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +2729 -222
- package/dist/index.js.map +1 -1
- package/dist/{null-CGaNDDlQ.d.cts → null-BYB6v4Iv.d.cts} +324 -2
- package/dist/{null-CGaNDDlQ.d.ts → null-BYB6v4Iv.d.ts} +324 -2
- package/dist/testing.cjs +894 -94
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +891 -94
- package/dist/testing.js.map +1 -1
- package/package.json +8 -1
- package/dist/admin/spa/index-B1Mp8lSz.js +0 -41
- package/dist/admin/spa/index-B1Mp8lSz.js.map +0 -1
- package/dist/admin/spa/template-editor-dMzrGcva.js +0 -502
package/dist/testing.cjs
CHANGED
|
@@ -8,6 +8,9 @@ var IORedis = require('ioredis');
|
|
|
8
8
|
var Handlebars = require('handlebars');
|
|
9
9
|
var htmlToText = require('html-to-text');
|
|
10
10
|
var mjml2html = require('mjml');
|
|
11
|
+
var dns = require('dns/promises');
|
|
12
|
+
var net = require('net');
|
|
13
|
+
var psl = require('psl');
|
|
11
14
|
|
|
12
15
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
16
|
|
|
@@ -16,6 +19,9 @@ var sgMail__default = /*#__PURE__*/_interopDefault(sgMail);
|
|
|
16
19
|
var IORedis__default = /*#__PURE__*/_interopDefault(IORedis);
|
|
17
20
|
var Handlebars__default = /*#__PURE__*/_interopDefault(Handlebars);
|
|
18
21
|
var mjml2html__default = /*#__PURE__*/_interopDefault(mjml2html);
|
|
22
|
+
var dns__default = /*#__PURE__*/_interopDefault(dns);
|
|
23
|
+
var net__default = /*#__PURE__*/_interopDefault(net);
|
|
24
|
+
var psl__default = /*#__PURE__*/_interopDefault(psl);
|
|
19
25
|
|
|
20
26
|
var __create = Object.create;
|
|
21
27
|
var __defProp = Object.defineProperty;
|
|
@@ -4165,7 +4171,7 @@ var require_getport = __commonJS({
|
|
|
4165
4171
|
exports.tryPort = tryPort;
|
|
4166
4172
|
exports.resetPortsCache = resetPortsCache;
|
|
4167
4173
|
var tslib_1 = (init_tslib_es6(), __toCommonJS(tslib_es6_exports));
|
|
4168
|
-
var
|
|
4174
|
+
var net2 = tslib_1.__importStar(__require("net"));
|
|
4169
4175
|
var debug_1 = tslib_1.__importDefault(require_src());
|
|
4170
4176
|
var log = (0, debug_1.default)("MongoMS:GetPort");
|
|
4171
4177
|
exports.MIN_PORT = 1024;
|
|
@@ -4213,7 +4219,7 @@ var require_getport = __commonJS({
|
|
|
4213
4219
|
}
|
|
4214
4220
|
function tryPort(port) {
|
|
4215
4221
|
return new Promise((res, rej) => {
|
|
4216
|
-
const server =
|
|
4222
|
+
const server = net2.createServer();
|
|
4217
4223
|
if (typeof server.unref === "function") {
|
|
4218
4224
|
server.unref();
|
|
4219
4225
|
}
|
|
@@ -9179,7 +9185,7 @@ var require_dist = __commonJS({
|
|
|
9179
9185
|
};
|
|
9180
9186
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9181
9187
|
exports.Agent = void 0;
|
|
9182
|
-
var
|
|
9188
|
+
var net2 = __importStar2(__require("net"));
|
|
9183
9189
|
var http = __importStar2(__require("http"));
|
|
9184
9190
|
var https_1 = __require("https");
|
|
9185
9191
|
__exportStar2(require_helpers(), exports);
|
|
@@ -9219,7 +9225,7 @@ var require_dist = __commonJS({
|
|
|
9219
9225
|
if (!this.sockets[name]) {
|
|
9220
9226
|
this.sockets[name] = [];
|
|
9221
9227
|
}
|
|
9222
|
-
const fakeSocket = new
|
|
9228
|
+
const fakeSocket = new net2.Socket({ writable: false });
|
|
9223
9229
|
this.sockets[name].push(fakeSocket);
|
|
9224
9230
|
this.totalSocketCount++;
|
|
9225
9231
|
return fakeSocket;
|
|
@@ -9429,7 +9435,7 @@ var require_dist2 = __commonJS({
|
|
|
9429
9435
|
};
|
|
9430
9436
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9431
9437
|
exports.HttpsProxyAgent = void 0;
|
|
9432
|
-
var
|
|
9438
|
+
var net2 = __importStar2(__require("net"));
|
|
9433
9439
|
var tls = __importStar2(__require("tls"));
|
|
9434
9440
|
var assert_1 = __importDefault2(__require("assert"));
|
|
9435
9441
|
var debug_1 = __importDefault2(require_src());
|
|
@@ -9438,7 +9444,7 @@ var require_dist2 = __commonJS({
|
|
|
9438
9444
|
var parse_proxy_response_1 = require_parse_proxy_response();
|
|
9439
9445
|
var debug = (0, debug_1.default)("https-proxy-agent");
|
|
9440
9446
|
var setServernameFromNonIpHost = (options2) => {
|
|
9441
|
-
if (options2.servername === void 0 && options2.host && !
|
|
9447
|
+
if (options2.servername === void 0 && options2.host && !net2.isIP(options2.host)) {
|
|
9442
9448
|
return {
|
|
9443
9449
|
...options2,
|
|
9444
9450
|
servername: options2.host
|
|
@@ -9478,10 +9484,10 @@ var require_dist2 = __commonJS({
|
|
|
9478
9484
|
socket = tls.connect(setServernameFromNonIpHost(this.connectOpts));
|
|
9479
9485
|
} else {
|
|
9480
9486
|
debug("Creating `net.Socket`: %o", this.connectOpts);
|
|
9481
|
-
socket =
|
|
9487
|
+
socket = net2.connect(this.connectOpts);
|
|
9482
9488
|
}
|
|
9483
9489
|
const headers = typeof this.proxyHeaders === "function" ? this.proxyHeaders() : { ...this.proxyHeaders };
|
|
9484
|
-
const host =
|
|
9490
|
+
const host = net2.isIPv6(opts.host) ? `[${opts.host}]` : opts.host;
|
|
9485
9491
|
let payload = `CONNECT ${host}:${opts.port} HTTP/1.1\r
|
|
9486
9492
|
`;
|
|
9487
9493
|
if (proxy.username || proxy.password) {
|
|
@@ -9514,7 +9520,7 @@ var require_dist2 = __commonJS({
|
|
|
9514
9520
|
return socket;
|
|
9515
9521
|
}
|
|
9516
9522
|
socket.destroy();
|
|
9517
|
-
const fakeSocket = new
|
|
9523
|
+
const fakeSocket = new net2.Socket({ writable: false });
|
|
9518
9524
|
fakeSocket.readable = true;
|
|
9519
9525
|
req.once("socket", (s) => {
|
|
9520
9526
|
debug("Replaying proxy buffer for failed request");
|
|
@@ -14263,6 +14269,17 @@ var predicateSchema = zod.z.lazy(
|
|
|
14263
14269
|
);
|
|
14264
14270
|
|
|
14265
14271
|
// src/server/config.ts
|
|
14272
|
+
var DEFAULT_DOMAIN_DNSBL_LISTS = [
|
|
14273
|
+
{ host: "dbl.spamhaus.org", label: "Spamhaus DBL" },
|
|
14274
|
+
{ host: "multi.surbl.org", label: "SURBL" },
|
|
14275
|
+
{ host: "multi.uribl.com", label: "URIBL" }
|
|
14276
|
+
];
|
|
14277
|
+
var DEFAULT_IP_DNSBL_LISTS = [
|
|
14278
|
+
{ host: "zen.spamhaus.org", label: "Spamhaus ZEN" },
|
|
14279
|
+
{ host: "b.barracudacentral.org", label: "Barracuda" },
|
|
14280
|
+
{ host: "dnsbl.sorbs.net", label: "SORBS" },
|
|
14281
|
+
{ host: "bl.spamcop.net", label: "SpamCop" }
|
|
14282
|
+
];
|
|
14266
14283
|
var DEFAULTS = {
|
|
14267
14284
|
collectionPrefix: "mailer_",
|
|
14268
14285
|
requireDoubleOptIn: false,
|
|
@@ -14323,6 +14340,10 @@ function resolveConfig(c) {
|
|
|
14323
14340
|
}
|
|
14324
14341
|
|
|
14325
14342
|
// src/server/models/index.ts
|
|
14343
|
+
var HEALTH_AGG_ID = "agg";
|
|
14344
|
+
function healthBucketId(senderDomain, kind) {
|
|
14345
|
+
return `d:${senderDomain ?? "_unknown"}|k:${kind}`;
|
|
14346
|
+
}
|
|
14326
14347
|
function getCollections(db, prefix = "mailer_") {
|
|
14327
14348
|
return {
|
|
14328
14349
|
subscriptions: db.collection(`${prefix}subscriptions`),
|
|
@@ -14340,7 +14361,14 @@ function getCollections(db, prefix = "mailer_") {
|
|
|
14340
14361
|
auditLog: db.collection(`${prefix}audit_log`),
|
|
14341
14362
|
webhookEvents: db.collection(`${prefix}webhook_events`),
|
|
14342
14363
|
health: db.collection(`${prefix}health`),
|
|
14343
|
-
contactTags: db.collection(`${prefix}contact_tags`)
|
|
14364
|
+
contactTags: db.collection(`${prefix}contact_tags`),
|
|
14365
|
+
dnsblChecks: db.collection(`${prefix}dnsbl_checks`),
|
|
14366
|
+
postmasterSnapshots: db.collection(`${prefix}postmaster_snapshots`),
|
|
14367
|
+
sndsSnapshots: db.collection(`${prefix}snds_snapshots`),
|
|
14368
|
+
dmarcReports: db.collection(`${prefix}dmarc_reports`),
|
|
14369
|
+
dmarcFailures: db.collection(`${prefix}dmarc_failures`),
|
|
14370
|
+
dmarcSourceTags: db.collection(`${prefix}dmarc_source_tags`),
|
|
14371
|
+
mailTesterScores: db.collection(`${prefix}mail_tester_scores`)
|
|
14344
14372
|
};
|
|
14345
14373
|
}
|
|
14346
14374
|
async function ensureIndexes(db, prefix = "mailer_") {
|
|
@@ -14412,8 +14440,58 @@ async function ensureIndexes(db, prefix = "mailer_") {
|
|
|
14412
14440
|
c.contactTags.createIndexes([
|
|
14413
14441
|
{ key: { externalId: 1, tag: 1 }, unique: true },
|
|
14414
14442
|
{ key: { tag: 1 } }
|
|
14443
|
+
]),
|
|
14444
|
+
c.health.createIndexes([
|
|
14445
|
+
{ key: { senderDomain: 1, kind: 1 } },
|
|
14446
|
+
{ key: { status: 1 } },
|
|
14447
|
+
// setup-status reads the most-recently-touched health doc as a heartbeat.
|
|
14448
|
+
{ key: { updatedAt: -1 } }
|
|
14449
|
+
]),
|
|
14450
|
+
c.dnsblChecks.createIndexes([
|
|
14451
|
+
{ key: { target: 1, list: 1 }, unique: true },
|
|
14452
|
+
// Supports the admin /dnsbl GET sort: result asc, then target asc, list asc.
|
|
14453
|
+
{ key: { result: 1, target: 1, list: 1 } },
|
|
14454
|
+
// TTL — stale rows for targets the operator removed disappear after
|
|
14455
|
+
// 60 days without manual cleanup. The puller refreshes runAt on
|
|
14456
|
+
// every active target so existing targets stay indefinitely.
|
|
14457
|
+
{ key: { runAt: 1 }, expireAfterSeconds: 60 * 24 * 60 * 60 }
|
|
14458
|
+
]),
|
|
14459
|
+
c.postmasterSnapshots.createIndexes([
|
|
14460
|
+
{ key: { domain: 1, date: 1 }, unique: true },
|
|
14461
|
+
{ key: { domain: 1, fetchedAt: -1 } },
|
|
14462
|
+
{ key: { domainReputation: 1 } }
|
|
14463
|
+
]),
|
|
14464
|
+
c.sndsSnapshots.createIndexes([
|
|
14465
|
+
{ key: { ip: 1, activityStart: 1 }, unique: true },
|
|
14466
|
+
{ key: { ip: 1, fetchedAt: -1 } },
|
|
14467
|
+
{ key: { filterResult: 1 } }
|
|
14468
|
+
]),
|
|
14469
|
+
c.dmarcReports.createIndexes([
|
|
14470
|
+
{ key: { reportId: 1, orgName: 1 }, unique: true },
|
|
14471
|
+
{ key: { domain: 1, rangeEnd: -1 } },
|
|
14472
|
+
// Cross-domain "most recent reports" queries scan a lot without this.
|
|
14473
|
+
{ key: { rangeEnd: -1 } },
|
|
14474
|
+
{ key: { receivedAt: -1 } }
|
|
14475
|
+
]),
|
|
14476
|
+
c.dmarcFailures.createIndexes([
|
|
14477
|
+
{ key: { reportId: 1, sourceIp: 1 }, unique: true },
|
|
14478
|
+
{ key: { domain: 1, day: -1 } },
|
|
14479
|
+
{ key: { sourceIp: 1, day: -1 } },
|
|
14480
|
+
{ key: { receivedAt: 1 } }
|
|
14481
|
+
// for retention pruning
|
|
14482
|
+
]),
|
|
14483
|
+
c.dmarcSourceTags.createIndexes([
|
|
14484
|
+
{ key: { ip: 1 }, unique: true }
|
|
14485
|
+
]),
|
|
14486
|
+
c.mailTesterScores.createIndexes([
|
|
14487
|
+
{ key: { contentKey: 1 }, unique: true },
|
|
14488
|
+
{ key: { templateSlug: 1, fetchedAt: -1 } },
|
|
14489
|
+
// TTL — Mongo auto-deletes expired scores so we never serve stale data.
|
|
14490
|
+
{ key: { expiresAt: 1 }, expireAfterSeconds: 0 }
|
|
14415
14491
|
])
|
|
14416
14492
|
]);
|
|
14493
|
+
await c.health.deleteOne({ _id: "singleton" }).catch(() => {
|
|
14494
|
+
});
|
|
14417
14495
|
}
|
|
14418
14496
|
var EventRegistry = class {
|
|
14419
14497
|
policies = /* @__PURE__ */ new Map();
|
|
@@ -14426,6 +14504,9 @@ var EventRegistry = class {
|
|
|
14426
14504
|
policy(name) {
|
|
14427
14505
|
return this.policies.get(name);
|
|
14428
14506
|
}
|
|
14507
|
+
list() {
|
|
14508
|
+
return Array.from(this.policies, ([name, dedupePolicy]) => ({ name, dedupePolicy }));
|
|
14509
|
+
}
|
|
14429
14510
|
/**
|
|
14430
14511
|
* Derive a dedupeKey for an event call. Returns null when no policy is
|
|
14431
14512
|
* registered AND no key was passed — caller should throw.
|
|
@@ -15075,21 +15156,59 @@ async function isSuppressed(collections, email, kind) {
|
|
|
15075
15156
|
return { suppressed: false };
|
|
15076
15157
|
}
|
|
15077
15158
|
|
|
15159
|
+
// src/server/templates/sender-domain.ts
|
|
15160
|
+
function extractDomain(email) {
|
|
15161
|
+
if (typeof email !== "string") return null;
|
|
15162
|
+
const at = email.lastIndexOf("@");
|
|
15163
|
+
if (at <= 0 || at === email.length - 1) return null;
|
|
15164
|
+
return email.slice(at + 1).toLowerCase().trim();
|
|
15165
|
+
}
|
|
15166
|
+
|
|
15078
15167
|
// src/server/runner/health.ts
|
|
15079
|
-
|
|
15168
|
+
var ZERO_COUNTERS = {
|
|
15169
|
+
sent: 0,
|
|
15170
|
+
delivered: 0,
|
|
15171
|
+
bounced: 0,
|
|
15172
|
+
hardBounced: 0,
|
|
15173
|
+
softBounced: 0,
|
|
15174
|
+
complained: 0,
|
|
15175
|
+
failedToSend: 0
|
|
15176
|
+
};
|
|
15177
|
+
var ZERO_RATES = {
|
|
15178
|
+
bounceRate: 0,
|
|
15179
|
+
hardBounceRate: 0,
|
|
15180
|
+
complaintRate: 0,
|
|
15181
|
+
failureRate: 0
|
|
15182
|
+
};
|
|
15183
|
+
async function recordHealthCounter(ctx, counter2, dims, by = 1) {
|
|
15184
|
+
const windowMs = ctx.config.circuitBreaker.windowMinutes * 60 * 1e3;
|
|
15185
|
+
const writes = [];
|
|
15186
|
+
writes.push(upsertCounter(ctx, HEALTH_AGG_ID, null, null, counter2, by, windowMs));
|
|
15187
|
+
if (dims) {
|
|
15188
|
+
const domain = dims.fromEmail ? extractDomain(dims.fromEmail) : null;
|
|
15189
|
+
if (domain) {
|
|
15190
|
+
const id = healthBucketId(domain, dims.kind);
|
|
15191
|
+
writes.push(upsertCounter(ctx, id, domain, dims.kind, counter2, by, windowMs));
|
|
15192
|
+
}
|
|
15193
|
+
}
|
|
15194
|
+
await Promise.all(writes);
|
|
15195
|
+
}
|
|
15196
|
+
async function upsertCounter(ctx, _id, senderDomain, kind, counter2, by, windowMs) {
|
|
15080
15197
|
await ctx.collections.health.updateOne(
|
|
15081
|
-
{ _id
|
|
15198
|
+
{ _id },
|
|
15082
15199
|
{
|
|
15083
15200
|
$inc: { [`counters.${counter2}`]: by },
|
|
15084
15201
|
$setOnInsert: {
|
|
15085
|
-
_id
|
|
15202
|
+
_id,
|
|
15203
|
+
senderDomain,
|
|
15204
|
+
kind,
|
|
15086
15205
|
windowStartedAt: /* @__PURE__ */ new Date(),
|
|
15087
|
-
windowDurationMs:
|
|
15206
|
+
windowDurationMs: windowMs,
|
|
15088
15207
|
status: "healthy",
|
|
15089
15208
|
trippedAt: null,
|
|
15090
15209
|
trippedReason: null,
|
|
15091
15210
|
manuallyResumedAt: null,
|
|
15092
|
-
rates: {
|
|
15211
|
+
rates: { ...ZERO_RATES }
|
|
15093
15212
|
},
|
|
15094
15213
|
$set: { updatedAt: /* @__PURE__ */ new Date() }
|
|
15095
15214
|
},
|
|
@@ -15099,73 +15218,107 @@ async function recordHealthCounter(ctx, counter2, by = 1) {
|
|
|
15099
15218
|
async function evaluateHealth(ctx) {
|
|
15100
15219
|
const cb = ctx.config.circuitBreaker;
|
|
15101
15220
|
const windowMs = cb.windowMinutes * 60 * 1e3;
|
|
15102
|
-
const
|
|
15103
|
-
if (
|
|
15104
|
-
const
|
|
15105
|
-
if (
|
|
15106
|
-
await ctx
|
|
15107
|
-
{ _id: "singleton" },
|
|
15108
|
-
{
|
|
15109
|
-
$set: {
|
|
15110
|
-
windowStartedAt: /* @__PURE__ */ new Date(),
|
|
15111
|
-
windowDurationMs: windowMs,
|
|
15112
|
-
counters: { sent: 0, delivered: 0, bounced: 0, hardBounced: 0, softBounced: 0, complained: 0, failedToSend: 0 },
|
|
15113
|
-
rates: { bounceRate: 0, hardBounceRate: 0, complaintRate: 0, failureRate: 0 },
|
|
15114
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
15115
|
-
}
|
|
15116
|
-
}
|
|
15117
|
-
);
|
|
15118
|
-
return;
|
|
15221
|
+
const docs = await ctx.collections.health.find({}).toArray();
|
|
15222
|
+
if (docs.length === 0) return;
|
|
15223
|
+
const hasAgg = docs.some((d) => d._id === HEALTH_AGG_ID);
|
|
15224
|
+
if (!hasAgg) {
|
|
15225
|
+
await upsertCounter(ctx, HEALTH_AGG_ID, null, null, "sent", 0, windowMs);
|
|
15119
15226
|
}
|
|
15120
|
-
const
|
|
15121
|
-
|
|
15122
|
-
|
|
15123
|
-
|
|
15124
|
-
|
|
15125
|
-
|
|
15126
|
-
|
|
15127
|
-
|
|
15128
|
-
|
|
15129
|
-
|
|
15130
|
-
|
|
15131
|
-
|
|
15132
|
-
|
|
15133
|
-
|
|
15134
|
-
|
|
15135
|
-
|
|
15136
|
-
|
|
15137
|
-
|
|
15138
|
-
|
|
15139
|
-
|
|
15140
|
-
|
|
15141
|
-
|
|
15142
|
-
|
|
15227
|
+
for (const doc of docs) {
|
|
15228
|
+
const isAgg = doc._id === HEALTH_AGG_ID;
|
|
15229
|
+
const windowAge = Date.now() - new Date(doc.windowStartedAt).getTime();
|
|
15230
|
+
if (windowAge > windowMs && doc.status !== "tripped") {
|
|
15231
|
+
await ctx.collections.health.updateOne(
|
|
15232
|
+
{ _id: doc._id },
|
|
15233
|
+
{
|
|
15234
|
+
$set: {
|
|
15235
|
+
windowStartedAt: /* @__PURE__ */ new Date(),
|
|
15236
|
+
windowDurationMs: windowMs,
|
|
15237
|
+
counters: { ...ZERO_COUNTERS },
|
|
15238
|
+
rates: { ...ZERO_RATES },
|
|
15239
|
+
status: "healthy",
|
|
15240
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
15241
|
+
}
|
|
15242
|
+
}
|
|
15243
|
+
);
|
|
15244
|
+
continue;
|
|
15245
|
+
}
|
|
15246
|
+
const c = doc.counters;
|
|
15247
|
+
const total = c.sent || 1;
|
|
15248
|
+
const rates = {
|
|
15249
|
+
bounceRate: c.bounced / total,
|
|
15250
|
+
hardBounceRate: c.hardBounced / total,
|
|
15251
|
+
complaintRate: c.complained / total,
|
|
15252
|
+
failureRate: c.failedToSend / total
|
|
15253
|
+
};
|
|
15143
15254
|
await ctx.collections.health.updateOne(
|
|
15144
|
-
{ _id:
|
|
15145
|
-
{ $set: {
|
|
15255
|
+
{ _id: doc._id },
|
|
15256
|
+
{ $set: { rates, updatedAt: /* @__PURE__ */ new Date() } }
|
|
15146
15257
|
);
|
|
15147
|
-
if (
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15258
|
+
if (isAgg) continue;
|
|
15259
|
+
if (c.sent < cb.minSendsBeforeEval) continue;
|
|
15260
|
+
if (doc.status === "tripped") continue;
|
|
15261
|
+
let trippedReason = null;
|
|
15262
|
+
if (rates.hardBounceRate * 100 >= cb.hardBounceRatePctTrip) {
|
|
15263
|
+
trippedReason = `hard bounce rate ${(rates.hardBounceRate * 100).toFixed(2)}% >= ${cb.hardBounceRatePctTrip}%`;
|
|
15264
|
+
} else if (rates.complaintRate * 100 >= cb.complaintRatePctTrip) {
|
|
15265
|
+
trippedReason = `complaint rate ${(rates.complaintRate * 100).toFixed(2)}% >= ${cb.complaintRatePctTrip}%`;
|
|
15266
|
+
} else if (rates.bounceRate * 100 >= cb.combinedBounceRatePctTrip) {
|
|
15267
|
+
trippedReason = `combined bounce rate ${(rates.bounceRate * 100).toFixed(2)}% >= ${cb.combinedBounceRatePctTrip}%`;
|
|
15268
|
+
}
|
|
15269
|
+
if (trippedReason) {
|
|
15270
|
+
const result = await ctx.collections.health.updateOne(
|
|
15271
|
+
{ _id: doc._id, status: { $in: ["healthy", "degraded"] } },
|
|
15272
|
+
{ $set: { status: "tripped", trippedAt: /* @__PURE__ */ new Date(), trippedReason, updatedAt: /* @__PURE__ */ new Date() } }
|
|
15273
|
+
);
|
|
15274
|
+
if (result.modifiedCount > 0) {
|
|
15275
|
+
if (ctx.audit) {
|
|
15276
|
+
try {
|
|
15277
|
+
await ctx.audit({
|
|
15278
|
+
actor: "system:circuit-breaker",
|
|
15279
|
+
action: "health.trip",
|
|
15280
|
+
resource: {
|
|
15281
|
+
collection: "mailer_health",
|
|
15282
|
+
id: String(doc._id),
|
|
15283
|
+
slug: `${doc.senderDomain ?? "_unknown"}|${doc.kind}`
|
|
15284
|
+
},
|
|
15285
|
+
diffSummary: trippedReason
|
|
15286
|
+
});
|
|
15287
|
+
} catch {
|
|
15288
|
+
}
|
|
15289
|
+
}
|
|
15290
|
+
if (ctx.config.onCircuitBreakerTrip) {
|
|
15291
|
+
try {
|
|
15292
|
+
await ctx.config.onCircuitBreakerTrip({
|
|
15293
|
+
reason: `[${doc.senderDomain ?? "_unknown"} / ${doc.kind}] ${trippedReason}`,
|
|
15294
|
+
rates
|
|
15295
|
+
});
|
|
15296
|
+
} catch {
|
|
15297
|
+
}
|
|
15298
|
+
}
|
|
15151
15299
|
}
|
|
15300
|
+
continue;
|
|
15152
15301
|
}
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
|
|
15156
|
-
|
|
15302
|
+
if (rates.failureRate * 100 >= cb.failedToSendRatePctDegrade) {
|
|
15303
|
+
if (doc.status !== "degraded") {
|
|
15304
|
+
await ctx.collections.health.updateOne(
|
|
15305
|
+
{ _id: doc._id },
|
|
15306
|
+
{ $set: { status: "degraded", updatedAt: /* @__PURE__ */ new Date() } }
|
|
15307
|
+
);
|
|
15308
|
+
}
|
|
15309
|
+
} else if (doc.status === "degraded") {
|
|
15157
15310
|
await ctx.collections.health.updateOne(
|
|
15158
|
-
{ _id:
|
|
15159
|
-
{ $set: { status: "
|
|
15311
|
+
{ _id: doc._id },
|
|
15312
|
+
{ $set: { status: "healthy", updatedAt: /* @__PURE__ */ new Date() } }
|
|
15160
15313
|
);
|
|
15161
15314
|
}
|
|
15162
|
-
} else if (doc.status === "degraded") {
|
|
15163
|
-
await ctx.collections.health.updateOne(
|
|
15164
|
-
{ _id: "singleton" },
|
|
15165
|
-
{ $set: { status: "healthy", updatedAt: /* @__PURE__ */ new Date() } }
|
|
15166
|
-
);
|
|
15167
15315
|
}
|
|
15168
15316
|
}
|
|
15317
|
+
async function getBucketStatus(ctx, fromEmail, kind) {
|
|
15318
|
+
const domain = fromEmail ? extractDomain(fromEmail) : null;
|
|
15319
|
+
const id = healthBucketId(domain, kind);
|
|
15320
|
+
return ctx.collections.health.findOne({ _id: id });
|
|
15321
|
+
}
|
|
15169
15322
|
|
|
15170
15323
|
// src/server/runner/send.ts
|
|
15171
15324
|
async function handleSend(run, step, contact, flow, ctx) {
|
|
@@ -15249,8 +15402,8 @@ async function dispatchSend(sendId, ctx) {
|
|
|
15249
15402
|
return;
|
|
15250
15403
|
}
|
|
15251
15404
|
if (send.kind === "marketing") {
|
|
15252
|
-
const
|
|
15253
|
-
if (
|
|
15405
|
+
const bucket = await getBucketStatus(ctx, send.fromEmail, send.kind);
|
|
15406
|
+
if (bucket?.status === "tripped") {
|
|
15254
15407
|
await ctx.queues.send.add("send", { sendId: String(send._id) }, { delay: 6e4 });
|
|
15255
15408
|
return;
|
|
15256
15409
|
}
|
|
@@ -15319,13 +15472,13 @@ async function dispatchSend(sendId, ctx) {
|
|
|
15319
15472
|
}
|
|
15320
15473
|
}
|
|
15321
15474
|
);
|
|
15322
|
-
await recordHealthCounter(ctx, "sent");
|
|
15475
|
+
await recordHealthCounter(ctx, "sent", { fromEmail: send.fromEmail, kind: send.kind });
|
|
15323
15476
|
} catch (err) {
|
|
15324
15477
|
await ctx.collections.sends.updateOne(
|
|
15325
15478
|
{ _id: send._id },
|
|
15326
15479
|
{ $set: { status: "failed", errorMessage: String(err?.message ?? err) } }
|
|
15327
15480
|
);
|
|
15328
|
-
await recordHealthCounter(ctx, "failedToSend");
|
|
15481
|
+
await recordHealthCounter(ctx, "failedToSend", { fromEmail: send.fromEmail, kind: send.kind });
|
|
15329
15482
|
if (ctx.config.onSendFailure) {
|
|
15330
15483
|
try {
|
|
15331
15484
|
await ctx.config.onSendFailure({ send, error: err });
|
|
@@ -15909,7 +16062,15 @@ async function promoteSoftBounces(ctx) {
|
|
|
15909
16062
|
const cutoff = new Date(Date.now() - windowDays * 864e5);
|
|
15910
16063
|
const offenders = await ctx.collections.sends.aggregate([
|
|
15911
16064
|
{ $match: { status: "bounced", bounceType: "soft", queuedAt: { $gt: cutoff } } },
|
|
15912
|
-
{ $
|
|
16065
|
+
{ $sort: { queuedAt: 1 } },
|
|
16066
|
+
{
|
|
16067
|
+
$group: {
|
|
16068
|
+
_id: "$emailAtSend",
|
|
16069
|
+
count: { $sum: 1 },
|
|
16070
|
+
lastFromEmail: { $last: "$fromEmail" },
|
|
16071
|
+
lastKind: { $last: "$kind" }
|
|
16072
|
+
}
|
|
16073
|
+
},
|
|
15913
16074
|
{ $match: { count: { $gte: threshold } } },
|
|
15914
16075
|
{ $limit: 200 }
|
|
15915
16076
|
]).toArray();
|
|
@@ -15941,32 +16102,649 @@ async function promoteSoftBounces(ctx) {
|
|
|
15941
16102
|
{ emailAtSubscribe: email },
|
|
15942
16103
|
{ $set: { status: "bounced", updatedAt: /* @__PURE__ */ new Date() } }
|
|
15943
16104
|
);
|
|
15944
|
-
await recordHealthCounter(
|
|
16105
|
+
await recordHealthCounter(
|
|
16106
|
+
ctx,
|
|
16107
|
+
"hardBounced",
|
|
16108
|
+
o.lastKind ? { fromEmail: o.lastFromEmail, kind: o.lastKind } : null
|
|
16109
|
+
);
|
|
16110
|
+
}
|
|
16111
|
+
}
|
|
16112
|
+
var REGISTRABLE_ONLY_LISTS = /* @__PURE__ */ new Set(["multi.surbl.org", "multi.uribl.com"]);
|
|
16113
|
+
var DNS_CONCURRENCY = 8;
|
|
16114
|
+
var defaultResolver = {
|
|
16115
|
+
resolve4: (hostname) => dns__default.default.resolve4(hostname)
|
|
16116
|
+
};
|
|
16117
|
+
async function runDnsblChecks(ctx, opts = {}) {
|
|
16118
|
+
const cfg = ctx.config.dnsbl ?? {};
|
|
16119
|
+
const intervalHours = cfg.intervalHours ?? 24;
|
|
16120
|
+
if (!opts.force && intervalHours <= 0) {
|
|
16121
|
+
return { ran: false, reason: "disabled" };
|
|
16122
|
+
}
|
|
16123
|
+
const targets = collectTargets(ctx, cfg);
|
|
16124
|
+
if (targets.domains.length === 0 && targets.ips.length === 0) {
|
|
16125
|
+
return { ran: false, reason: "no_targets" };
|
|
16126
|
+
}
|
|
16127
|
+
const resolver = opts.resolver ?? defaultResolver;
|
|
16128
|
+
const domainLists = cfg.domainLists ?? DEFAULT_DOMAIN_DNSBL_LISTS;
|
|
16129
|
+
const ipLists = cfg.ipLists ?? DEFAULT_IP_DNSBL_LISTS;
|
|
16130
|
+
const pairs = [];
|
|
16131
|
+
for (const d of targets.domains) {
|
|
16132
|
+
for (const l of domainLists) pairs.push({ target: d, targetKind: "domain", list: l });
|
|
16133
|
+
}
|
|
16134
|
+
for (const ip of targets.ips) {
|
|
16135
|
+
for (const l of ipLists) pairs.push({ target: ip, targetKind: "ip", list: l });
|
|
16136
|
+
}
|
|
16137
|
+
const throttleCutoff = opts.force ? null : Date.now() - intervalHours * 60 * 60 * 1e3;
|
|
16138
|
+
let duePairs = pairs;
|
|
16139
|
+
if (throttleCutoff != null) {
|
|
16140
|
+
const existing = await ctx.collections.dnsblChecks.find(
|
|
16141
|
+
{
|
|
16142
|
+
$or: pairs.map((p) => ({ target: p.target, list: p.list.host }))
|
|
16143
|
+
},
|
|
16144
|
+
{ projection: { target: 1, list: 1, runAt: 1 } }
|
|
16145
|
+
).toArray();
|
|
16146
|
+
const fresh = /* @__PURE__ */ new Set();
|
|
16147
|
+
for (const e of existing) {
|
|
16148
|
+
if (new Date(e.runAt).getTime() > throttleCutoff) {
|
|
16149
|
+
fresh.add(`${e.target}|${e.list}`);
|
|
16150
|
+
}
|
|
16151
|
+
}
|
|
16152
|
+
duePairs = pairs.filter((p) => !fresh.has(`${p.target}|${p.list.host}`));
|
|
16153
|
+
if (duePairs.length === 0) {
|
|
16154
|
+
return { ran: false, reason: "not_due", totalChecks: 0, listedCount: 0 };
|
|
16155
|
+
}
|
|
16156
|
+
}
|
|
16157
|
+
let listedCount = 0;
|
|
16158
|
+
async function processPair(p) {
|
|
16159
|
+
const queryName = buildQueryName(p.target, p.targetKind, p.list.host);
|
|
16160
|
+
const lookup = queryName ? await queryDnsbl(resolver, queryName) : { result: "error", returnCodes: [], errorMessage: "unsupported target format" };
|
|
16161
|
+
if (lookup.transient) return;
|
|
16162
|
+
if (lookup.result === "listed") listedCount++;
|
|
16163
|
+
await ctx.collections.dnsblChecks.updateOne(
|
|
16164
|
+
{ target: p.target, list: p.list.host },
|
|
16165
|
+
{
|
|
16166
|
+
$set: {
|
|
16167
|
+
target: p.target,
|
|
16168
|
+
targetKind: p.targetKind,
|
|
16169
|
+
list: p.list.host,
|
|
16170
|
+
listLabel: p.list.label,
|
|
16171
|
+
result: lookup.result,
|
|
16172
|
+
returnCodes: lookup.returnCodes,
|
|
16173
|
+
errorMessage: lookup.errorMessage,
|
|
16174
|
+
runAt: /* @__PURE__ */ new Date()
|
|
16175
|
+
}
|
|
16176
|
+
},
|
|
16177
|
+
{ upsert: true }
|
|
16178
|
+
);
|
|
16179
|
+
}
|
|
16180
|
+
let idx = 0;
|
|
16181
|
+
await Promise.all(
|
|
16182
|
+
Array.from({ length: Math.min(DNS_CONCURRENCY, duePairs.length) }, async () => {
|
|
16183
|
+
while (idx < duePairs.length) {
|
|
16184
|
+
const my = idx++;
|
|
16185
|
+
await processPair(duePairs[my]);
|
|
16186
|
+
}
|
|
16187
|
+
})
|
|
16188
|
+
);
|
|
16189
|
+
return { ran: true, totalChecks: duePairs.length, listedCount };
|
|
16190
|
+
}
|
|
16191
|
+
function collectTargets(ctx, cfg) {
|
|
16192
|
+
const domains = /* @__PURE__ */ new Set();
|
|
16193
|
+
const registry = ctx.config.senderDomains;
|
|
16194
|
+
if (registry) {
|
|
16195
|
+
for (const d of Object.keys(registry)) domains.add(d.toLowerCase());
|
|
16196
|
+
}
|
|
16197
|
+
const fromDomain = ctx.config.fromDefaults?.email ? extractDomain(ctx.config.fromDefaults.email) : null;
|
|
16198
|
+
if (fromDomain) domains.add(fromDomain);
|
|
16199
|
+
const txnDomain = ctx.config.transactionalFromDefaults?.email ? extractDomain(ctx.config.transactionalFromDefaults.email) : null;
|
|
16200
|
+
if (txnDomain) domains.add(txnDomain);
|
|
16201
|
+
return {
|
|
16202
|
+
domains: Array.from(domains),
|
|
16203
|
+
ips: (cfg.dedicatedIps ?? []).filter((ip) => net__default.default.isIP(ip) !== 0)
|
|
16204
|
+
};
|
|
16205
|
+
}
|
|
16206
|
+
function buildQueryName(target, kind, listHost) {
|
|
16207
|
+
if (kind === "ip") {
|
|
16208
|
+
const v = net__default.default.isIP(target);
|
|
16209
|
+
if (v === 4) return `${reverseIPv4(target)}.${listHost}`;
|
|
16210
|
+
if (v === 6) return `${reverseIPv6Nibbles(target)}.${listHost}`;
|
|
16211
|
+
return null;
|
|
15945
16212
|
}
|
|
16213
|
+
const domain = REGISTRABLE_ONLY_LISTS.has(listHost) ? registrableDomain(target) : target;
|
|
16214
|
+
return `${domain}.${listHost}`;
|
|
16215
|
+
}
|
|
16216
|
+
var TRANSIENT_DNS_CODES = /* @__PURE__ */ new Set(["ESERVFAIL", "EREFUSED", "ETIMEOUT", "ETIMEDOUT", "ECONNRESET", "ECONNREFUSED"]);
|
|
16217
|
+
async function queryDnsbl(resolver, query) {
|
|
16218
|
+
try {
|
|
16219
|
+
const records = await resolver.resolve4(query);
|
|
16220
|
+
return interpretRecords(records);
|
|
16221
|
+
} catch (err) {
|
|
16222
|
+
const code = err?.code;
|
|
16223
|
+
if (code === "ENOTFOUND" || code === "ENODATA") {
|
|
16224
|
+
return { result: "clean", returnCodes: [], errorMessage: null };
|
|
16225
|
+
}
|
|
16226
|
+
if (code && TRANSIENT_DNS_CODES.has(code)) {
|
|
16227
|
+
return {
|
|
16228
|
+
transient: true,
|
|
16229
|
+
result: "error",
|
|
16230
|
+
returnCodes: [],
|
|
16231
|
+
errorMessage: String(err?.message ?? err)
|
|
16232
|
+
};
|
|
16233
|
+
}
|
|
16234
|
+
return {
|
|
16235
|
+
result: "error",
|
|
16236
|
+
returnCodes: [],
|
|
16237
|
+
errorMessage: String(err?.message ?? err)
|
|
16238
|
+
};
|
|
16239
|
+
}
|
|
16240
|
+
}
|
|
16241
|
+
function interpretRecords(records) {
|
|
16242
|
+
if (!records || records.length === 0) {
|
|
16243
|
+
return { result: "clean", returnCodes: [], errorMessage: null };
|
|
16244
|
+
}
|
|
16245
|
+
const errorish = records.filter((r) => r.startsWith("127.255.255."));
|
|
16246
|
+
if (errorish.length === records.length) {
|
|
16247
|
+
return {
|
|
16248
|
+
result: "error",
|
|
16249
|
+
returnCodes: records,
|
|
16250
|
+
errorMessage: `list returned reserved code(s): ${records.join(", ")}`
|
|
16251
|
+
};
|
|
16252
|
+
}
|
|
16253
|
+
const listed = records.filter((r) => r.startsWith("127.") && !r.startsWith("127.255.255."));
|
|
16254
|
+
if (listed.length > 0) {
|
|
16255
|
+
return { result: "listed", returnCodes: records, errorMessage: null };
|
|
16256
|
+
}
|
|
16257
|
+
return { result: "clean", returnCodes: records, errorMessage: null };
|
|
16258
|
+
}
|
|
16259
|
+
function reverseIPv4(ip) {
|
|
16260
|
+
return ip.split(".").reverse().join(".");
|
|
16261
|
+
}
|
|
16262
|
+
function reverseIPv6Nibbles(ip) {
|
|
16263
|
+
const v4Match = /:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(ip);
|
|
16264
|
+
let normalized = ip;
|
|
16265
|
+
if (v4Match) {
|
|
16266
|
+
const octets = v4Match[1].split(".").map((o) => Number(o));
|
|
16267
|
+
if (octets.length === 4 && octets.every((o) => o >= 0 && o <= 255)) {
|
|
16268
|
+
const hi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
|
|
16269
|
+
const lo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
|
|
16270
|
+
normalized = ip.slice(0, v4Match.index) + ":" + hi + ":" + lo;
|
|
16271
|
+
}
|
|
16272
|
+
}
|
|
16273
|
+
const sides = normalized.split("::");
|
|
16274
|
+
let groups;
|
|
16275
|
+
if (sides.length === 1) {
|
|
16276
|
+
groups = sides[0].split(":");
|
|
16277
|
+
} else {
|
|
16278
|
+
const left = sides[0].split(":").filter(Boolean);
|
|
16279
|
+
const right = sides[1].split(":").filter(Boolean);
|
|
16280
|
+
const missing = 8 - left.length - right.length;
|
|
16281
|
+
groups = [...left, ...Array(missing).fill("0"), ...right];
|
|
16282
|
+
}
|
|
16283
|
+
if (groups.length !== 8) {
|
|
16284
|
+
throw new Error(`unexpected IPv6 group count for ${ip}: ${groups.length}`);
|
|
16285
|
+
}
|
|
16286
|
+
const padded = groups.map((g) => g.toLowerCase().padStart(4, "0")).join("");
|
|
16287
|
+
return padded.split("").reverse().join(".");
|
|
16288
|
+
}
|
|
16289
|
+
function registrableDomain(domain) {
|
|
16290
|
+
const parsed = psl__default.default.parse(domain);
|
|
16291
|
+
if ("domain" in parsed && parsed.domain) return parsed.domain;
|
|
16292
|
+
return domain;
|
|
15946
16293
|
}
|
|
15947
16294
|
|
|
15948
|
-
// src/server/runner/
|
|
15949
|
-
var
|
|
15950
|
-
|
|
16295
|
+
// src/server/runner/postmaster.ts
|
|
16296
|
+
var OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
16297
|
+
var POSTMASTER_BASE = "https://gmailpostmastertools.googleapis.com/v1";
|
|
16298
|
+
var FETCH_TIMEOUT_MS = 15e3;
|
|
16299
|
+
async function fetchWithTimeout(fetcher, url, init = {}, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
16300
|
+
const ctl = new AbortController();
|
|
16301
|
+
const t = setTimeout(() => ctl.abort(), timeoutMs);
|
|
16302
|
+
try {
|
|
16303
|
+
return await fetcher(url, { ...init, signal: ctl.signal });
|
|
16304
|
+
} finally {
|
|
16305
|
+
clearTimeout(t);
|
|
16306
|
+
}
|
|
16307
|
+
}
|
|
16308
|
+
function createPostmasterClient(cfg, fetcher = globalThis.fetch) {
|
|
16309
|
+
let cached = null;
|
|
16310
|
+
async function getAccessToken() {
|
|
16311
|
+
if (cached && cached.expiresAt > Date.now() + 5 * 60 * 1e3) {
|
|
16312
|
+
return cached.accessToken;
|
|
16313
|
+
}
|
|
16314
|
+
const body = new URLSearchParams({
|
|
16315
|
+
client_id: cfg.clientId,
|
|
16316
|
+
client_secret: cfg.clientSecret,
|
|
16317
|
+
refresh_token: cfg.refreshToken,
|
|
16318
|
+
grant_type: "refresh_token"
|
|
16319
|
+
});
|
|
16320
|
+
const res = await fetchWithTimeout(fetcher, OAUTH_TOKEN_URL, {
|
|
16321
|
+
method: "POST",
|
|
16322
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
16323
|
+
body: body.toString()
|
|
16324
|
+
});
|
|
16325
|
+
if (!res.ok) {
|
|
16326
|
+
throw new Error(`Postmaster OAuth token refresh failed: ${res.status} ${await safeText(res)}`);
|
|
16327
|
+
}
|
|
16328
|
+
const data = await res.json();
|
|
16329
|
+
cached = {
|
|
16330
|
+
accessToken: data.access_token,
|
|
16331
|
+
expiresAt: Date.now() + data.expires_in * 1e3
|
|
16332
|
+
};
|
|
16333
|
+
return cached.accessToken;
|
|
16334
|
+
}
|
|
16335
|
+
async function authedGet(path) {
|
|
16336
|
+
const token = await getAccessToken();
|
|
16337
|
+
const res = await fetchWithTimeout(fetcher, `${POSTMASTER_BASE}${path}`, {
|
|
16338
|
+
method: "GET",
|
|
16339
|
+
headers: { authorization: `Bearer ${token}` }
|
|
16340
|
+
});
|
|
16341
|
+
if (!res.ok) {
|
|
16342
|
+
throw new Error(`Postmaster GET ${path} failed: ${res.status} ${await safeText(res)}`);
|
|
16343
|
+
}
|
|
16344
|
+
return await res.json();
|
|
16345
|
+
}
|
|
16346
|
+
return {
|
|
16347
|
+
async listDomains() {
|
|
16348
|
+
const data = await authedGet("/domains");
|
|
16349
|
+
return data.domains ?? [];
|
|
16350
|
+
},
|
|
16351
|
+
async getLatestTrafficStats(domain) {
|
|
16352
|
+
const data = await authedGet(
|
|
16353
|
+
`/domains/${encodeURIComponent(domain)}/trafficStats?pageSize=1`
|
|
16354
|
+
);
|
|
16355
|
+
return data.trafficStats?.[0] ?? null;
|
|
16356
|
+
}
|
|
16357
|
+
};
|
|
16358
|
+
}
|
|
16359
|
+
async function safeText(res) {
|
|
16360
|
+
try {
|
|
16361
|
+
return await res.text();
|
|
16362
|
+
} catch {
|
|
16363
|
+
return "<no body>";
|
|
16364
|
+
}
|
|
16365
|
+
}
|
|
16366
|
+
async function runPostmasterPull(ctx, opts = {}) {
|
|
16367
|
+
const cfg = ctx.config.postmaster;
|
|
16368
|
+
if (!cfg) return { ran: false, reason: "not_configured" };
|
|
16369
|
+
const intervalHours = cfg.intervalHours ?? 24;
|
|
16370
|
+
if (!opts.force && intervalHours <= 0) {
|
|
16371
|
+
return { ran: false, reason: "disabled" };
|
|
16372
|
+
}
|
|
16373
|
+
const client = opts.client ?? createPostmasterClient(cfg, opts.fetcher);
|
|
16374
|
+
const allDomains = resolveDomains(ctx, cfg);
|
|
16375
|
+
if (allDomains.length === 0) {
|
|
16376
|
+
return { ran: false, reason: "no_domains" };
|
|
16377
|
+
}
|
|
16378
|
+
let domains = allDomains;
|
|
16379
|
+
if (!opts.force) {
|
|
16380
|
+
const cutoff = Date.now() - intervalHours * 60 * 60 * 1e3;
|
|
16381
|
+
const latest = await ctx.collections.postmasterSnapshots.find({ domain: { $in: allDomains } }, { projection: { domain: 1, fetchedAt: 1 } }).sort({ fetchedAt: -1 }).limit(allDomains.length * 8).toArray();
|
|
16382
|
+
const lastByDomain = /* @__PURE__ */ new Map();
|
|
16383
|
+
for (const s of latest) {
|
|
16384
|
+
const ts = new Date(s.fetchedAt).getTime();
|
|
16385
|
+
const cur = lastByDomain.get(s.domain) ?? 0;
|
|
16386
|
+
if (ts > cur) lastByDomain.set(s.domain, ts);
|
|
16387
|
+
}
|
|
16388
|
+
domains = allDomains.filter((d) => (lastByDomain.get(d) ?? 0) < cutoff);
|
|
16389
|
+
if (domains.length === 0) {
|
|
16390
|
+
return { ran: false, reason: "not_due" };
|
|
16391
|
+
}
|
|
16392
|
+
}
|
|
16393
|
+
const fetches = await Promise.all(
|
|
16394
|
+
domains.map(async (domain) => {
|
|
16395
|
+
try {
|
|
16396
|
+
const stat = await client.getLatestTrafficStats(domain);
|
|
16397
|
+
return { domain, stat, error: null };
|
|
16398
|
+
} catch (err) {
|
|
16399
|
+
console.error(`mailery: postmaster fetch failed for ${domain}`, err);
|
|
16400
|
+
return { domain, stat: null, error: err };
|
|
16401
|
+
}
|
|
16402
|
+
})
|
|
16403
|
+
);
|
|
16404
|
+
let fetched = 0;
|
|
16405
|
+
const trippedDomains = [];
|
|
16406
|
+
for (const { domain, stat } of fetches) {
|
|
16407
|
+
if (!stat) continue;
|
|
16408
|
+
const snapshot = toSnapshot(domain, stat);
|
|
16409
|
+
if (!snapshot) continue;
|
|
16410
|
+
fetched++;
|
|
16411
|
+
await ctx.collections.postmasterSnapshots.updateOne(
|
|
16412
|
+
{ domain: snapshot.domain, date: snapshot.date },
|
|
16413
|
+
{ $set: snapshot },
|
|
16414
|
+
{ upsert: true }
|
|
16415
|
+
);
|
|
16416
|
+
if (snapshot.domainReputation === "BAD") {
|
|
16417
|
+
const kindsToTrip = kindsForDomain(ctx, domain);
|
|
16418
|
+
for (const kind of kindsToTrip) {
|
|
16419
|
+
const tripped = await tripBucket(ctx, domain, kind, snapshot);
|
|
16420
|
+
if (tripped) trippedDomains.push(`${domain}|${kind}`);
|
|
16421
|
+
}
|
|
16422
|
+
}
|
|
16423
|
+
}
|
|
16424
|
+
return { ran: true, fetched, trippedDomains };
|
|
16425
|
+
}
|
|
16426
|
+
function kindsForDomain(ctx, domain) {
|
|
16427
|
+
const registry = ctx.config.senderDomains ?? {};
|
|
16428
|
+
const entry = registry[domain.toLowerCase()];
|
|
16429
|
+
if (!entry) return [];
|
|
16430
|
+
if (entry.kind === "both") return ["marketing", "transactional"];
|
|
16431
|
+
return [entry.kind];
|
|
16432
|
+
}
|
|
16433
|
+
function resolveDomains(ctx, cfg) {
|
|
16434
|
+
if (cfg.domains && cfg.domains.length > 0) {
|
|
16435
|
+
return cfg.domains.map((d) => d.toLowerCase());
|
|
16436
|
+
}
|
|
16437
|
+
const out = /* @__PURE__ */ new Set();
|
|
16438
|
+
const registry = ctx.config.senderDomains;
|
|
16439
|
+
if (registry) {
|
|
16440
|
+
for (const d of Object.keys(registry)) out.add(d.toLowerCase());
|
|
16441
|
+
}
|
|
16442
|
+
const f = ctx.config.fromDefaults?.email ? extractDomain(ctx.config.fromDefaults.email) : null;
|
|
16443
|
+
if (f) out.add(f);
|
|
16444
|
+
const t = ctx.config.transactionalFromDefaults?.email ? extractDomain(ctx.config.transactionalFromDefaults.email) : null;
|
|
16445
|
+
if (t) out.add(t);
|
|
16446
|
+
return Array.from(out);
|
|
16447
|
+
}
|
|
16448
|
+
function toSnapshot(domain, stat) {
|
|
16449
|
+
const m = /\/trafficStats\/(\d{8})$/.exec(stat.name ?? "");
|
|
16450
|
+
if (!m) {
|
|
16451
|
+
console.error(`mailery: postmaster snapshot for ${domain} has unrecognized name "${stat.name}" \u2014 skipping`);
|
|
16452
|
+
return null;
|
|
16453
|
+
}
|
|
16454
|
+
const yyyymmdd = m[1];
|
|
16455
|
+
const date = `${yyyymmdd.slice(0, 4)}-${yyyymmdd.slice(4, 6)}-${yyyymmdd.slice(6, 8)}`;
|
|
16456
|
+
return {
|
|
16457
|
+
domain,
|
|
16458
|
+
date,
|
|
16459
|
+
domainReputation: stat.domainReputation ?? null,
|
|
16460
|
+
ipReputations: stat.ipReputations ? stat.ipReputations.map((r) => ({
|
|
16461
|
+
reputation: r.reputation,
|
|
16462
|
+
ipCount: typeof r.ipCount === "string" ? Number(r.ipCount) : r.ipCount
|
|
16463
|
+
})) : null,
|
|
16464
|
+
userReportedSpamRatio: stat.userReportedSpamRatio ?? null,
|
|
16465
|
+
spfSuccessRatio: stat.spfSuccessRatio ?? null,
|
|
16466
|
+
dkimSuccessRatio: stat.dkimSuccessRatio ?? null,
|
|
16467
|
+
dmarcSuccessRatio: stat.dmarcSuccessRatio ?? null,
|
|
16468
|
+
outboundEncryptionRatio: stat.outboundEncryptionRatio ?? null,
|
|
16469
|
+
inboundEncryptionRatio: stat.inboundEncryptionRatio ?? null,
|
|
16470
|
+
deliveryErrors: stat.deliveryErrors ?? null,
|
|
16471
|
+
spammyFeedbackLoops: stat.spammyFeedbackLoops ?? null,
|
|
16472
|
+
fetchedAt: /* @__PURE__ */ new Date()
|
|
16473
|
+
};
|
|
16474
|
+
}
|
|
16475
|
+
async function tripBucket(ctx, domain, kind, snapshot) {
|
|
16476
|
+
const id = healthBucketId(domain, kind);
|
|
16477
|
+
const reason = `Postmaster Tools reports ${domain} reputation = BAD on ${snapshot.date}`;
|
|
16478
|
+
const prior = await ctx.collections.health.findOne({ _id: id });
|
|
16479
|
+
if (prior?.status === "tripped") {
|
|
16480
|
+
return false;
|
|
16481
|
+
}
|
|
15951
16482
|
await ctx.collections.health.updateOne(
|
|
15952
|
-
{ _id:
|
|
16483
|
+
{ _id: id },
|
|
15953
16484
|
{
|
|
15954
|
-
$set: {
|
|
16485
|
+
$set: {
|
|
16486
|
+
status: "tripped",
|
|
16487
|
+
trippedAt: /* @__PURE__ */ new Date(),
|
|
16488
|
+
trippedReason: reason,
|
|
16489
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
16490
|
+
},
|
|
15955
16491
|
$setOnInsert: {
|
|
15956
|
-
_id:
|
|
16492
|
+
_id: id,
|
|
16493
|
+
senderDomain: domain,
|
|
16494
|
+
kind,
|
|
15957
16495
|
windowStartedAt: /* @__PURE__ */ new Date(),
|
|
15958
16496
|
windowDurationMs: ctx.config.circuitBreaker.windowMinutes * 60 * 1e3,
|
|
15959
|
-
status: "healthy",
|
|
15960
|
-
trippedAt: null,
|
|
15961
|
-
trippedReason: null,
|
|
15962
16497
|
manuallyResumedAt: null,
|
|
15963
16498
|
counters: { sent: 0, delivered: 0, bounced: 0, hardBounced: 0, softBounced: 0, complained: 0, failedToSend: 0 },
|
|
15964
16499
|
rates: { bounceRate: 0, hardBounceRate: 0, complaintRate: 0, failureRate: 0 }
|
|
15965
16500
|
}
|
|
15966
16501
|
},
|
|
15967
16502
|
{ upsert: true }
|
|
15968
|
-
)
|
|
15969
|
-
|
|
16503
|
+
);
|
|
16504
|
+
if (ctx.config.onCircuitBreakerTrip) {
|
|
16505
|
+
try {
|
|
16506
|
+
await ctx.config.onCircuitBreakerTrip({ reason, rates: { userReportedSpamRatio: snapshot.userReportedSpamRatio ?? 0 } });
|
|
16507
|
+
} catch {
|
|
16508
|
+
}
|
|
16509
|
+
}
|
|
16510
|
+
return true;
|
|
16511
|
+
}
|
|
16512
|
+
|
|
16513
|
+
// src/server/runner/snds.ts
|
|
16514
|
+
var SNDS_DATA_URL = "https://postmaster.live.com/snds/data.aspx";
|
|
16515
|
+
var FETCH_TIMEOUT_MS2 = 3e4;
|
|
16516
|
+
async function runSndsPull(ctx, opts = {}) {
|
|
16517
|
+
const cfg = ctx.config.snds;
|
|
16518
|
+
if (!cfg?.accessKey) return { ran: false, reason: "not_configured" };
|
|
16519
|
+
const intervalHours = cfg.intervalHours ?? 24;
|
|
16520
|
+
if (!opts.force && intervalHours <= 0) return { ran: false, reason: "disabled" };
|
|
16521
|
+
if (!opts.force) {
|
|
16522
|
+
const latest = await ctx.collections.sndsSnapshots.find({}).sort({ fetchedAt: -1 }).limit(1).toArray();
|
|
16523
|
+
if (latest[0]) {
|
|
16524
|
+
const ageMs = Date.now() - new Date(latest[0].fetchedAt).getTime();
|
|
16525
|
+
if (ageMs < intervalHours * 60 * 60 * 1e3) {
|
|
16526
|
+
return { ran: false, reason: "not_due" };
|
|
16527
|
+
}
|
|
16528
|
+
}
|
|
16529
|
+
}
|
|
16530
|
+
const fetcher = opts.fetcher ?? globalThis.fetch;
|
|
16531
|
+
const url = `${SNDS_DATA_URL}?key=${encodeURIComponent(cfg.accessKey)}`;
|
|
16532
|
+
const ctl = new AbortController();
|
|
16533
|
+
const t = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS2);
|
|
16534
|
+
let res;
|
|
16535
|
+
try {
|
|
16536
|
+
res = await fetcher(url, { method: "GET", signal: ctl.signal });
|
|
16537
|
+
} catch (err) {
|
|
16538
|
+
throw new Error(`SNDS data fetch failed: ${redactKey(String(err?.message ?? err), cfg.accessKey)}`);
|
|
16539
|
+
} finally {
|
|
16540
|
+
clearTimeout(t);
|
|
16541
|
+
}
|
|
16542
|
+
if (!res.ok) {
|
|
16543
|
+
throw new Error(`SNDS data fetch failed: ${res.status} ${redactKey(await safeText2(res), cfg.accessKey)}`);
|
|
16544
|
+
}
|
|
16545
|
+
const csv = await res.text();
|
|
16546
|
+
const rows = parseSndsCsv(csv);
|
|
16547
|
+
const ipFilter = cfg.ips?.length ? new Set(cfg.ips) : null;
|
|
16548
|
+
const now = /* @__PURE__ */ new Date();
|
|
16549
|
+
const ops = rows.filter((row) => !ipFilter || ipFilter.has(row.ip)).map((row) => ({
|
|
16550
|
+
updateOne: {
|
|
16551
|
+
filter: { ip: row.ip, activityStart: row.activityStart },
|
|
16552
|
+
update: { $set: { ...row, fetchedAt: now } },
|
|
16553
|
+
upsert: true
|
|
16554
|
+
}
|
|
16555
|
+
}));
|
|
16556
|
+
let persisted = 0;
|
|
16557
|
+
if (ops.length > 0) {
|
|
16558
|
+
const r = await ctx.collections.sndsSnapshots.bulkWrite(ops, { ordered: false });
|
|
16559
|
+
persisted = (r.modifiedCount ?? 0) + (r.upsertedCount ?? 0);
|
|
16560
|
+
}
|
|
16561
|
+
return { ran: true, rowsParsed: rows.length, rowsPersisted: persisted };
|
|
16562
|
+
}
|
|
16563
|
+
function redactKey(s, key) {
|
|
16564
|
+
if (!key) return s;
|
|
16565
|
+
return s.split(key).join("<redacted>").split(encodeURIComponent(key)).join("<redacted>");
|
|
16566
|
+
}
|
|
16567
|
+
async function safeText2(res) {
|
|
16568
|
+
try {
|
|
16569
|
+
return await res.text();
|
|
16570
|
+
} catch {
|
|
16571
|
+
return "<no body>";
|
|
16572
|
+
}
|
|
16573
|
+
}
|
|
16574
|
+
function parseSndsCsv(csv) {
|
|
16575
|
+
const out = [];
|
|
16576
|
+
for (const raw of csv.split(/\r?\n/)) {
|
|
16577
|
+
if (!raw.trim()) continue;
|
|
16578
|
+
const fields = splitCsvRow(raw);
|
|
16579
|
+
if (fields.length < 7) continue;
|
|
16580
|
+
const parsed = parseRow(fields);
|
|
16581
|
+
if (parsed) out.push(parsed);
|
|
16582
|
+
}
|
|
16583
|
+
return out;
|
|
16584
|
+
}
|
|
16585
|
+
function splitCsvRow(row) {
|
|
16586
|
+
const out = [];
|
|
16587
|
+
let cur = "";
|
|
16588
|
+
let inQuotes = false;
|
|
16589
|
+
for (let i = 0; i < row.length; i++) {
|
|
16590
|
+
const ch = row[i];
|
|
16591
|
+
if (inQuotes) {
|
|
16592
|
+
if (ch === '"') {
|
|
16593
|
+
if (row[i + 1] === '"') {
|
|
16594
|
+
cur += '"';
|
|
16595
|
+
i++;
|
|
16596
|
+
} else {
|
|
16597
|
+
inQuotes = false;
|
|
16598
|
+
}
|
|
16599
|
+
} else {
|
|
16600
|
+
cur += ch;
|
|
16601
|
+
}
|
|
16602
|
+
} else if (ch === '"') {
|
|
16603
|
+
inQuotes = true;
|
|
16604
|
+
} else if (ch === ",") {
|
|
16605
|
+
out.push(cur);
|
|
16606
|
+
cur = "";
|
|
16607
|
+
} else {
|
|
16608
|
+
cur += ch;
|
|
16609
|
+
}
|
|
16610
|
+
}
|
|
16611
|
+
out.push(cur);
|
|
16612
|
+
return out.map((f) => f.trim());
|
|
16613
|
+
}
|
|
16614
|
+
function parseRow(fields) {
|
|
16615
|
+
const [
|
|
16616
|
+
ip,
|
|
16617
|
+
activityStart,
|
|
16618
|
+
activityEnd,
|
|
16619
|
+
rcptCommands,
|
|
16620
|
+
dataCommands,
|
|
16621
|
+
messageRecipients,
|
|
16622
|
+
filterResult,
|
|
16623
|
+
complaintRate,
|
|
16624
|
+
trapMessageCount,
|
|
16625
|
+
sampleHelo,
|
|
16626
|
+
sampleMailFrom
|
|
16627
|
+
] = fields;
|
|
16628
|
+
const start = parseSndsDate(activityStart ?? "");
|
|
16629
|
+
const end = parseSndsDate(activityEnd ?? "");
|
|
16630
|
+
if (!ip || !start || !end) return null;
|
|
16631
|
+
return {
|
|
16632
|
+
ip,
|
|
16633
|
+
activityStart: start,
|
|
16634
|
+
activityEnd: end,
|
|
16635
|
+
rcptCommands: toInt(rcptCommands),
|
|
16636
|
+
dataCommands: toInt(dataCommands),
|
|
16637
|
+
messageRecipients: toInt(messageRecipients),
|
|
16638
|
+
filterResult: normalizeFilter(filterResult ?? ""),
|
|
16639
|
+
complaintRate: parseComplaintRate(complaintRate ?? ""),
|
|
16640
|
+
trapMessageCount: toInt(trapMessageCount),
|
|
16641
|
+
sampleHelo: sampleHelo ? sampleHelo : null,
|
|
16642
|
+
sampleMailFrom: sampleMailFrom ? sampleMailFrom : null
|
|
16643
|
+
};
|
|
16644
|
+
}
|
|
16645
|
+
function toInt(s) {
|
|
16646
|
+
if (!s) return 0;
|
|
16647
|
+
const n = Number(s.replace(/[^\d-]/g, ""));
|
|
16648
|
+
return Number.isFinite(n) ? n : 0;
|
|
16649
|
+
}
|
|
16650
|
+
function normalizeFilter(s) {
|
|
16651
|
+
const u = s.toUpperCase().trim();
|
|
16652
|
+
if (u === "GREEN" || u === "YELLOW" || u === "RED") return u;
|
|
16653
|
+
return "UNKNOWN";
|
|
16654
|
+
}
|
|
16655
|
+
function parseComplaintRate(s) {
|
|
16656
|
+
const trimmed = s.trim();
|
|
16657
|
+
if (!trimmed || trimmed === "-" || /n\/?a/i.test(trimmed)) return null;
|
|
16658
|
+
const cleaned = trimmed.replace(/%/g, "");
|
|
16659
|
+
let m = /^<\s*(\d+(?:\.\d+)?)$/.exec(cleaned);
|
|
16660
|
+
if (m) return Number(m[1]) / 100;
|
|
16661
|
+
m = /^>\s*(\d+(?:\.\d+)?)$/.exec(cleaned);
|
|
16662
|
+
if (m) return Number(m[1]) / 100;
|
|
16663
|
+
m = /^(\d+(?:\.\d+)?)\s*-\s*(\d+(?:\.\d+)?)$/.exec(cleaned);
|
|
16664
|
+
if (m) return Number(m[2]) / 100;
|
|
16665
|
+
m = /^(\d+(?:\.\d+)?)$/.exec(cleaned);
|
|
16666
|
+
if (m) return Number(m[1]) / 100;
|
|
16667
|
+
return null;
|
|
16668
|
+
}
|
|
16669
|
+
function parseSndsDate(s) {
|
|
16670
|
+
const trimmed = s.trim();
|
|
16671
|
+
if (!trimmed) return null;
|
|
16672
|
+
const m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}):(\d{2})(?::(\d{2}))?\s*(AM|PM)?$/i.exec(trimmed);
|
|
16673
|
+
if (m) {
|
|
16674
|
+
const month = Number(m[1]) - 1;
|
|
16675
|
+
const day = Number(m[2]);
|
|
16676
|
+
const year = Number(m[3]);
|
|
16677
|
+
let hour = Number(m[4]);
|
|
16678
|
+
const minute = Number(m[5]);
|
|
16679
|
+
const second = Number(m[6] ?? 0);
|
|
16680
|
+
const meridiem = m[7]?.toUpperCase();
|
|
16681
|
+
if (meridiem === "PM" && hour < 12) hour += 12;
|
|
16682
|
+
if (meridiem === "AM" && hour === 12) hour = 0;
|
|
16683
|
+
const wallMs = Date.UTC(year, month, day, hour, minute, second);
|
|
16684
|
+
if (!Number.isFinite(wallMs)) return null;
|
|
16685
|
+
const offsetHours = isUsPacificDst(year, month, day) ? 7 : 8;
|
|
16686
|
+
return new Date(wallMs + offsetHours * 60 * 60 * 1e3);
|
|
16687
|
+
}
|
|
16688
|
+
const d = new Date(trimmed);
|
|
16689
|
+
if (!isNaN(d.getTime())) return d;
|
|
16690
|
+
return null;
|
|
16691
|
+
}
|
|
16692
|
+
function isUsPacificDst(year, month0, day) {
|
|
16693
|
+
if (month0 > 2 && month0 < 10) return true;
|
|
16694
|
+
if (month0 < 2 || month0 > 10) return false;
|
|
16695
|
+
if (month0 === 2) {
|
|
16696
|
+
const dst2 = nthSundayOfMonth(year, 2, 2);
|
|
16697
|
+
return day >= dst2;
|
|
16698
|
+
}
|
|
16699
|
+
const dst = nthSundayOfMonth(year, 10, 1);
|
|
16700
|
+
return day < dst;
|
|
16701
|
+
}
|
|
16702
|
+
function nthSundayOfMonth(year, month0, n) {
|
|
16703
|
+
const first = new Date(Date.UTC(year, month0, 1));
|
|
16704
|
+
const dayOfWeek = first.getUTCDay();
|
|
16705
|
+
const firstSunday = 1 + (7 - dayOfWeek) % 7;
|
|
16706
|
+
return firstSunday + (n - 1) * 7;
|
|
16707
|
+
}
|
|
16708
|
+
var _lastPruneAt = 0;
|
|
16709
|
+
var PRUNE_INTERVAL_MS = 60 * 60 * 1e3;
|
|
16710
|
+
async function pruneDmarcFailures(ctx, opts = {}) {
|
|
16711
|
+
const cfg = ctx.config.dmarc;
|
|
16712
|
+
const days = cfg?.retentionDays ?? 90;
|
|
16713
|
+
if (days <= 0) return 0;
|
|
16714
|
+
if (!opts.force && Date.now() - _lastPruneAt < PRUNE_INTERVAL_MS) return 0;
|
|
16715
|
+
_lastPruneAt = Date.now();
|
|
16716
|
+
const cutoff = new Date(Date.now() - days * 864e5);
|
|
16717
|
+
const r = await ctx.collections.dmarcFailures.deleteMany({ receivedAt: { $lt: cutoff } });
|
|
16718
|
+
return r.deletedCount ?? 0;
|
|
16719
|
+
}
|
|
16720
|
+
|
|
16721
|
+
// src/server/runner/tick.ts
|
|
16722
|
+
var STRANDED_SEND_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
16723
|
+
async function runTick(ctx) {
|
|
16724
|
+
try {
|
|
16725
|
+
await ctx.collections.health.updateOne(
|
|
16726
|
+
{ _id: HEALTH_AGG_ID },
|
|
16727
|
+
{
|
|
16728
|
+
$set: { updatedAt: /* @__PURE__ */ new Date() },
|
|
16729
|
+
$setOnInsert: {
|
|
16730
|
+
_id: HEALTH_AGG_ID,
|
|
16731
|
+
senderDomain: null,
|
|
16732
|
+
kind: null,
|
|
16733
|
+
windowStartedAt: /* @__PURE__ */ new Date(),
|
|
16734
|
+
windowDurationMs: ctx.config.circuitBreaker.windowMinutes * 60 * 1e3,
|
|
16735
|
+
status: "healthy",
|
|
16736
|
+
trippedAt: null,
|
|
16737
|
+
trippedReason: null,
|
|
16738
|
+
manuallyResumedAt: null,
|
|
16739
|
+
counters: { sent: 0, delivered: 0, bounced: 0, hardBounced: 0, softBounced: 0, complained: 0, failedToSend: 0 },
|
|
16740
|
+
rates: { bounceRate: 0, hardBounceRate: 0, complaintRate: 0, failureRate: 0 }
|
|
16741
|
+
}
|
|
16742
|
+
},
|
|
16743
|
+
{ upsert: true }
|
|
16744
|
+
);
|
|
16745
|
+
} catch (err) {
|
|
16746
|
+
console.error("mailery: heartbeat write failed", err);
|
|
16747
|
+
}
|
|
15970
16748
|
await processNewlyFiredEventTriggers(ctx).catch((err) => {
|
|
15971
16749
|
console.error("mailery: triggers scan failed", err);
|
|
15972
16750
|
});
|
|
@@ -15988,6 +16766,20 @@ async function runTick(ctx) {
|
|
|
15988
16766
|
await promoteSoftBounces(ctx).catch((err) => {
|
|
15989
16767
|
console.error("mailery: soft-bounce promotion failed", err);
|
|
15990
16768
|
});
|
|
16769
|
+
await Promise.all([
|
|
16770
|
+
runDnsblChecks(ctx).catch((err) => {
|
|
16771
|
+
console.error("mailery: dnsbl checks failed", err);
|
|
16772
|
+
}),
|
|
16773
|
+
runPostmasterPull(ctx).catch((err) => {
|
|
16774
|
+
console.error("mailery: postmaster pull failed", err);
|
|
16775
|
+
}),
|
|
16776
|
+
runSndsPull(ctx).catch((err) => {
|
|
16777
|
+
console.error("mailery: snds pull failed", err);
|
|
16778
|
+
}),
|
|
16779
|
+
pruneDmarcFailures(ctx).catch((err) => {
|
|
16780
|
+
console.error("mailery: dmarc prune failed", err);
|
|
16781
|
+
})
|
|
16782
|
+
]);
|
|
15991
16783
|
}
|
|
15992
16784
|
async function sweepStrandedSends(ctx) {
|
|
15993
16785
|
const cutoff = new Date(Date.now() - STRANDED_SEND_THRESHOLD_MS);
|
|
@@ -16045,6 +16837,10 @@ async function processScheduledBroadcasts2(ctx) {
|
|
|
16045
16837
|
}
|
|
16046
16838
|
|
|
16047
16839
|
// src/server/runner/webhook.ts
|
|
16840
|
+
function dimsFromSend(send) {
|
|
16841
|
+
if (!send) return null;
|
|
16842
|
+
return { fromEmail: send.fromEmail, kind: send.kind };
|
|
16843
|
+
}
|
|
16048
16844
|
async function applyWebhookEvent(event, ctx) {
|
|
16049
16845
|
const send = await ctx.collections.sends.findOne(
|
|
16050
16846
|
event.providerMessageId ? { $or: [{ providerMessageId: event.providerMessageId }, { emailAtSend: event.email }] } : { emailAtSend: event.email },
|
|
@@ -16058,7 +16854,7 @@ async function applyWebhookEvent(event, ctx) {
|
|
|
16058
16854
|
{ $set: { status: "delivered", deliveredAt: event.occurredAt } }
|
|
16059
16855
|
);
|
|
16060
16856
|
}
|
|
16061
|
-
await recordHealthCounter(ctx, "delivered");
|
|
16857
|
+
await recordHealthCounter(ctx, "delivered", dimsFromSend(send));
|
|
16062
16858
|
break;
|
|
16063
16859
|
case "open":
|
|
16064
16860
|
if (send) {
|
|
@@ -16113,8 +16909,11 @@ async function applyWebhookEvent(event, ctx) {
|
|
|
16113
16909
|
{ $set: { status: "bounced", updatedAt: /* @__PURE__ */ new Date() } }
|
|
16114
16910
|
);
|
|
16115
16911
|
}
|
|
16116
|
-
|
|
16117
|
-
|
|
16912
|
+
{
|
|
16913
|
+
const dims = dimsFromSend(send);
|
|
16914
|
+
await recordHealthCounter(ctx, "bounced", dims);
|
|
16915
|
+
await recordHealthCounter(ctx, bounceType === "hard" ? "hardBounced" : "softBounced", dims);
|
|
16916
|
+
}
|
|
16118
16917
|
break;
|
|
16119
16918
|
}
|
|
16120
16919
|
case "complaint":
|
|
@@ -16130,7 +16929,7 @@ async function applyWebhookEvent(event, ctx) {
|
|
|
16130
16929
|
{ emailAtSubscribe: event.email },
|
|
16131
16930
|
{ $set: { status: "complained", updatedAt: /* @__PURE__ */ new Date() } }
|
|
16132
16931
|
);
|
|
16133
|
-
await recordHealthCounter(ctx, "complained");
|
|
16932
|
+
await recordHealthCounter(ctx, "complained", dimsFromSend(send));
|
|
16134
16933
|
break;
|
|
16135
16934
|
case "unsubscribe":
|
|
16136
16935
|
if (send) {
|
|
@@ -16199,7 +16998,8 @@ var Mailer = class _Mailer {
|
|
|
16199
16998
|
providers: this.providers,
|
|
16200
16999
|
queues: this.queues,
|
|
16201
17000
|
config: this.config,
|
|
16202
|
-
handlebarsHelpers: this.config.handlebarsHelpers
|
|
17001
|
+
handlebarsHelpers: this.config.handlebarsHelpers,
|
|
17002
|
+
audit: (entry) => this.audit(entry)
|
|
16203
17003
|
};
|
|
16204
17004
|
}
|
|
16205
17005
|
/**
|