mailery 0.5.1 → 0.8.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/README.md +3 -1
- package/dist/admin/spa/{index-Dmsla2eL.js → index-B3ndbezm.js} +8 -8
- package/dist/admin/spa/index-B3ndbezm.js.map +1 -0
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/{template-editor-Bn28OPMU.js → template-editor-DwMKy5sw.js} +105 -105
- package/dist/admin/spa/template-editor-DwMKy5sw.js.map +1 -0
- package/dist/index.cjs +477 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -4
- package/dist/index.d.ts +26 -4
- package/dist/index.js +475 -41
- package/dist/index.js.map +1 -1
- package/dist/{null-BYB6v4Iv.d.cts → null-BCxlHRJ0.d.cts} +134 -3
- package/dist/{null-BYB6v4Iv.d.ts → null-BCxlHRJ0.d.ts} +134 -3
- package/dist/testing.cjs +634 -82
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +233 -13
- package/dist/testing.d.ts +233 -13
- package/dist/testing.js +628 -83
- package/dist/testing.js.map +1 -1
- package/package.json +4 -1
- package/dist/admin/spa/index-Dmsla2eL.js.map +0 -1
- package/dist/admin/spa/template-editor-Bn28OPMU.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var zod = require('zod');
|
|
3
4
|
var mongodb = require('mongodb');
|
|
4
5
|
var crypto2 = require('crypto');
|
|
5
6
|
var sgMail = require('@sendgrid/mail');
|
|
6
|
-
var zod = require('zod');
|
|
7
7
|
var IORedis = require('ioredis');
|
|
8
8
|
var Handlebars = require('handlebars');
|
|
9
9
|
var htmlToText = require('html-to-text');
|
|
@@ -49,6 +49,55 @@ var __export = (target, all) => {
|
|
|
49
49
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
// src/server/adapters/vars.ts
|
|
53
|
+
var vars_exports = {};
|
|
54
|
+
__export(vars_exports, {
|
|
55
|
+
RESERVED_VAR_KEYS: () => exports.RESERVED_VAR_KEYS,
|
|
56
|
+
assertNoReservedVarKeys: () => assertNoReservedVarKeys,
|
|
57
|
+
defineVars: () => defineVars,
|
|
58
|
+
resolveVars: () => resolveVars,
|
|
59
|
+
varsJsonSchema: () => varsJsonSchema
|
|
60
|
+
});
|
|
61
|
+
function defineVars(adapter) {
|
|
62
|
+
return adapter;
|
|
63
|
+
}
|
|
64
|
+
function assertNoReservedVarKeys(adapter) {
|
|
65
|
+
const json = varsJsonSchema(adapter);
|
|
66
|
+
const props = json && typeof json === "object" ? json.properties : void 0;
|
|
67
|
+
if (!props) return;
|
|
68
|
+
const clashes = exports.RESERVED_VAR_KEYS.filter((k) => k in props);
|
|
69
|
+
if (clashes.length > 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`varsAdapter schema declares reserved key(s): ${clashes.join(", ")}. These names are provided by mailery itself \u2014 rename them in your schema.`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function varsJsonSchema(adapter) {
|
|
76
|
+
return zod.z.toJSONSchema(adapter.schema, { io: "output" });
|
|
77
|
+
}
|
|
78
|
+
async function resolveVars(adapter, contact, info) {
|
|
79
|
+
if (!adapter) return {};
|
|
80
|
+
const resolved = await adapter.resolve(contact, info);
|
|
81
|
+
if (!resolved || typeof resolved !== "object") return {};
|
|
82
|
+
const out = { ...resolved };
|
|
83
|
+
for (const k of exports.RESERVED_VAR_KEYS) delete out[k];
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
exports.RESERVED_VAR_KEYS = void 0;
|
|
87
|
+
var init_vars = __esm({
|
|
88
|
+
"src/server/adapters/vars.ts"() {
|
|
89
|
+
exports.RESERVED_VAR_KEYS = [
|
|
90
|
+
"contact",
|
|
91
|
+
"vars",
|
|
92
|
+
"event",
|
|
93
|
+
"unsubscribeUrl",
|
|
94
|
+
"viewInBrowserUrl",
|
|
95
|
+
"preferenceCenterUrl",
|
|
96
|
+
"senderAddress"
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
52
101
|
// src/server/adapters/mongo.ts
|
|
53
102
|
var mongo_exports = {};
|
|
54
103
|
__export(mongo_exports, {
|
|
@@ -362,6 +411,12 @@ var tagInputSchema = zod.z.object({
|
|
|
362
411
|
externalId: externalIdSchema,
|
|
363
412
|
tag: zod.z.string().min(1).max(128)
|
|
364
413
|
});
|
|
414
|
+
var abortFlowInputSchema = zod.z.object({
|
|
415
|
+
flowSlug: slugSchema,
|
|
416
|
+
externalId: externalIdSchema,
|
|
417
|
+
reason: zod.z.string().min(1).max(200).optional()
|
|
418
|
+
});
|
|
419
|
+
var abortAllFlowsInputSchema = abortFlowInputSchema.omit({ flowSlug: true });
|
|
365
420
|
var sendOneOffInputSchema = zod.z.object({
|
|
366
421
|
templateSlug: slugSchema,
|
|
367
422
|
externalId: externalIdSchema,
|
|
@@ -391,7 +446,13 @@ var flowStepSchema = zod.z.lazy(
|
|
|
391
446
|
type: zod.z.literal("send"),
|
|
392
447
|
templateSlug: slugSchema,
|
|
393
448
|
providerOverride: zod.z.string().optional(),
|
|
394
|
-
vars: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
449
|
+
vars: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
450
|
+
delivery: zod.z.object({
|
|
451
|
+
weekdaysOnly: zod.z.boolean().optional(),
|
|
452
|
+
timeOfDay: zod.z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "expected HH:mm").optional(),
|
|
453
|
+
useContactTimezone: zod.z.boolean().optional(),
|
|
454
|
+
timezone: zod.z.string().optional()
|
|
455
|
+
}).optional()
|
|
395
456
|
}),
|
|
396
457
|
zod.z.object({
|
|
397
458
|
type: zod.z.literal("tag"),
|
|
@@ -1181,6 +1242,7 @@ async function tryEnterFlow(flow, event, ctx) {
|
|
|
1181
1242
|
flowSlug: flow.slug,
|
|
1182
1243
|
flowVersion: flow.version,
|
|
1183
1244
|
emailAtEntry: sub.emailAtSubscribe,
|
|
1245
|
+
triggerEvent: { name: event.name, properties: event.properties ?? {}, occurredAt: event.occurredAt },
|
|
1184
1246
|
enteredAt: /* @__PURE__ */ new Date(),
|
|
1185
1247
|
status: "active",
|
|
1186
1248
|
currentStepIndex: 0,
|
|
@@ -1278,6 +1340,105 @@ function effectiveLowerBound(ctx, opts) {
|
|
|
1278
1340
|
}
|
|
1279
1341
|
return null;
|
|
1280
1342
|
}
|
|
1343
|
+
|
|
1344
|
+
// src/server/runner/delivery-window.ts
|
|
1345
|
+
var TIME_OF_DAY_GRACE_MS = 60 * 6e4;
|
|
1346
|
+
function computeDeliveryTime(now, window, contactTimezone) {
|
|
1347
|
+
const tz = pickTimezone(window, contactTimezone);
|
|
1348
|
+
let candidate = now;
|
|
1349
|
+
if (window.timeOfDay) {
|
|
1350
|
+
const [hh, mm] = window.timeOfDay.split(":").map(Number);
|
|
1351
|
+
const local = localParts(candidate, tz);
|
|
1352
|
+
const todaySlot = utcFromLocal(local.y, local.mo, local.d, hh, mm, tz);
|
|
1353
|
+
if (candidate.getTime() < todaySlot.getTime()) {
|
|
1354
|
+
candidate = todaySlot;
|
|
1355
|
+
} else if (candidate.getTime() - todaySlot.getTime() > TIME_OF_DAY_GRACE_MS) {
|
|
1356
|
+
const next = addLocalDays(local, 1);
|
|
1357
|
+
candidate = utcFromLocal(next.y, next.mo, next.d, hh, mm, tz);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (window.weekdaysOnly) {
|
|
1361
|
+
for (let guard = 0; guard < 3; guard++) {
|
|
1362
|
+
const local = localParts(candidate, tz);
|
|
1363
|
+
if (local.weekday !== "Sat" && local.weekday !== "Sun") break;
|
|
1364
|
+
const shift = local.weekday === "Sat" ? 2 : 1;
|
|
1365
|
+
const moved = addLocalDays(local, shift);
|
|
1366
|
+
candidate = utcFromLocal(moved.y, moved.mo, moved.d, local.hh, local.mi, tz);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return candidate;
|
|
1370
|
+
}
|
|
1371
|
+
function pickTimezone(window, contactTimezone) {
|
|
1372
|
+
const candidates = [
|
|
1373
|
+
window.useContactTimezone ? contactTimezone : void 0,
|
|
1374
|
+
window.timezone,
|
|
1375
|
+
"UTC"
|
|
1376
|
+
];
|
|
1377
|
+
for (const tz of candidates) {
|
|
1378
|
+
if (tz && isValidTimezone(tz)) return tz;
|
|
1379
|
+
}
|
|
1380
|
+
return "UTC";
|
|
1381
|
+
}
|
|
1382
|
+
var validatedZones = /* @__PURE__ */ new Map();
|
|
1383
|
+
function isValidTimezone(tz) {
|
|
1384
|
+
const cached = validatedZones.get(tz);
|
|
1385
|
+
if (cached !== void 0) return cached;
|
|
1386
|
+
let ok = true;
|
|
1387
|
+
try {
|
|
1388
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
1389
|
+
} catch {
|
|
1390
|
+
ok = false;
|
|
1391
|
+
}
|
|
1392
|
+
validatedZones.set(tz, ok);
|
|
1393
|
+
return ok;
|
|
1394
|
+
}
|
|
1395
|
+
var partFormatters = /* @__PURE__ */ new Map();
|
|
1396
|
+
function formatterFor(tz) {
|
|
1397
|
+
let f = partFormatters.get(tz);
|
|
1398
|
+
if (!f) {
|
|
1399
|
+
f = new Intl.DateTimeFormat("en-US", {
|
|
1400
|
+
timeZone: tz,
|
|
1401
|
+
year: "numeric",
|
|
1402
|
+
month: "2-digit",
|
|
1403
|
+
day: "2-digit",
|
|
1404
|
+
hour: "2-digit",
|
|
1405
|
+
minute: "2-digit",
|
|
1406
|
+
second: "2-digit",
|
|
1407
|
+
weekday: "short",
|
|
1408
|
+
hour12: false
|
|
1409
|
+
});
|
|
1410
|
+
partFormatters.set(tz, f);
|
|
1411
|
+
}
|
|
1412
|
+
return f;
|
|
1413
|
+
}
|
|
1414
|
+
function localParts(date, tz) {
|
|
1415
|
+
const parts = {};
|
|
1416
|
+
for (const p of formatterFor(tz).formatToParts(date)) parts[p.type] = p.value;
|
|
1417
|
+
return {
|
|
1418
|
+
y: Number(parts.year),
|
|
1419
|
+
mo: Number(parts.month),
|
|
1420
|
+
d: Number(parts.day),
|
|
1421
|
+
hh: Number(parts.hour) % 24,
|
|
1422
|
+
// Intl emits '24' for midnight in some locales
|
|
1423
|
+
mi: Number(parts.minute),
|
|
1424
|
+
ss: Number(parts.second),
|
|
1425
|
+
weekday: parts.weekday
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
function utcFromLocal(y, mo, d, hh, mi, tz) {
|
|
1429
|
+
let ts = Date.UTC(y, mo - 1, d, hh, mi, 0);
|
|
1430
|
+
for (let i = 0; i < 2; i++) {
|
|
1431
|
+
const p = localParts(new Date(ts), tz);
|
|
1432
|
+
const asUtc = Date.UTC(p.y, p.mo - 1, p.d, p.hh, p.mi, p.ss);
|
|
1433
|
+
const offset = asUtc - ts;
|
|
1434
|
+
ts = Date.UTC(y, mo - 1, d, hh, mi, 0) - offset;
|
|
1435
|
+
}
|
|
1436
|
+
return new Date(ts);
|
|
1437
|
+
}
|
|
1438
|
+
function addLocalDays(p, days) {
|
|
1439
|
+
const dt = new Date(Date.UTC(p.y, p.mo - 1, p.d + days));
|
|
1440
|
+
return { y: dt.getUTCFullYear(), mo: dt.getUTCMonth() + 1, d: dt.getUTCDate() };
|
|
1441
|
+
}
|
|
1281
1442
|
async function compileTemplate(mjml) {
|
|
1282
1443
|
const out = await mjml2html__default.default(mjml, { validationLevel: "soft", minify: false });
|
|
1283
1444
|
const plainText = derivePlaintext(out.html);
|
|
@@ -1401,6 +1562,9 @@ function makeHandlebars(extra) {
|
|
|
1401
1562
|
return hb;
|
|
1402
1563
|
}
|
|
1403
1564
|
|
|
1565
|
+
// src/server/runner/send.ts
|
|
1566
|
+
init_vars();
|
|
1567
|
+
|
|
1404
1568
|
// src/server/runner/suppression.ts
|
|
1405
1569
|
var SCOPES_BY_KIND = {
|
|
1406
1570
|
marketing: ["all", "marketing"],
|
|
@@ -1719,13 +1883,29 @@ async function dispatchSend(sendId, ctx) {
|
|
|
1719
1883
|
return;
|
|
1720
1884
|
}
|
|
1721
1885
|
const run = send.flowRunId ? await ctx.collections.flowRuns.findOne({ _id: send.flowRunId }) : null;
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1886
|
+
if (run && run.status === "exited" && run.exitReason?.startsWith("aborted_by_host")) {
|
|
1887
|
+
await ctx.collections.sends.updateOne(
|
|
1888
|
+
{ _id: send._id },
|
|
1889
|
+
{ $set: { status: "cancelled", errorMessage: `cancelled: ${run.exitReason}`, updatedAt: /* @__PURE__ */ new Date() } }
|
|
1890
|
+
);
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
let renderCtx;
|
|
1894
|
+
let rendered;
|
|
1895
|
+
try {
|
|
1896
|
+
const resolved = await resolveVars(ctx.varsAdapter, contact, {
|
|
1897
|
+
reason: "send",
|
|
1898
|
+
templateSlug: template.slug,
|
|
1899
|
+
flowSlug: run?.flowSlug,
|
|
1900
|
+
eventName: run?.triggerEvent?.name,
|
|
1901
|
+
eventProperties: run?.triggerEvent?.properties
|
|
1902
|
+
});
|
|
1903
|
+
renderCtx = buildRenderContext(contact, run, send.vars ?? {}, ctx, resolved);
|
|
1904
|
+
rendered = await renderTemplate(template, renderCtx, { helpers: ctx.handlebarsHelpers });
|
|
1905
|
+
} catch (err) {
|
|
1906
|
+
await markFailed(send._id, `render error: ${String(err?.message ?? err)}`, ctx);
|
|
1907
|
+
throw err;
|
|
1908
|
+
}
|
|
1729
1909
|
const tracking = applyTracking(rendered.html, {
|
|
1730
1910
|
sendId: String(send._id),
|
|
1731
1911
|
publicUrl: ctx.config.publicUrl,
|
|
@@ -1752,6 +1932,10 @@ async function dispatchSend(sendId, ctx) {
|
|
|
1752
1932
|
await markFailed(send._id, `provider_unknown: ${send.provider}`, ctx);
|
|
1753
1933
|
return;
|
|
1754
1934
|
}
|
|
1935
|
+
const headers = send.kind === "marketing" ? {
|
|
1936
|
+
"List-Unsubscribe": `<${renderCtx.unsubscribeUrl}>`,
|
|
1937
|
+
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
|
|
1938
|
+
} : {};
|
|
1755
1939
|
try {
|
|
1756
1940
|
const result = await provider.send({
|
|
1757
1941
|
to: send.emailAtSend,
|
|
@@ -1761,10 +1945,7 @@ async function dispatchSend(sendId, ctx) {
|
|
|
1761
1945
|
subject: rendered.subject,
|
|
1762
1946
|
html: tracking.html,
|
|
1763
1947
|
text: rendered.plainText,
|
|
1764
|
-
headers
|
|
1765
|
-
"List-Unsubscribe": `<${renderCtx.unsubscribeUrl}>`,
|
|
1766
|
-
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click"
|
|
1767
|
-
},
|
|
1948
|
+
headers,
|
|
1768
1949
|
messageMeta: { sendId: String(send._id) }
|
|
1769
1950
|
});
|
|
1770
1951
|
await ctx.collections.sends.updateOne(
|
|
@@ -1801,7 +1982,7 @@ function pickProviderName(stepOverride, tpl, ctx) {
|
|
|
1801
1982
|
}
|
|
1802
1983
|
return ctx.config.defaultProvider;
|
|
1803
1984
|
}
|
|
1804
|
-
function buildRenderContext(contact, run, vars, ctx) {
|
|
1985
|
+
function buildRenderContext(contact, run, vars, ctx, resolved = {}) {
|
|
1805
1986
|
const scope = "marketing";
|
|
1806
1987
|
const expiresAt = new Date(Date.now() + ctx.config.unsubscribeTokenLifetimeDays * 24 * 60 * 60 * 1e3);
|
|
1807
1988
|
const token = signUnsubscribeToken(
|
|
@@ -1810,8 +1991,10 @@ function buildRenderContext(contact, run, vars, ctx) {
|
|
|
1810
1991
|
);
|
|
1811
1992
|
const unsubscribeUrl = `${ctx.config.publicUrl}/m/unsub/${token}`;
|
|
1812
1993
|
return {
|
|
1994
|
+
...resolved,
|
|
1813
1995
|
contact,
|
|
1814
1996
|
vars,
|
|
1997
|
+
event: run?.triggerEvent?.properties ?? {},
|
|
1815
1998
|
unsubscribeUrl,
|
|
1816
1999
|
senderAddress: ctx.config.senderAddress
|
|
1817
2000
|
};
|
|
@@ -1896,8 +2079,15 @@ async function processOneRunStep(runId, ctx) {
|
|
|
1896
2079
|
return handleCondition(run, step, contact, ctx);
|
|
1897
2080
|
case "branch":
|
|
1898
2081
|
return handleBranch(run, step, contact, ctx);
|
|
1899
|
-
case "send":
|
|
2082
|
+
case "send": {
|
|
2083
|
+
if (step.delivery) {
|
|
2084
|
+
const deliverAt = computeDeliveryTime(/* @__PURE__ */ new Date(), step.delivery, contact.timezone);
|
|
2085
|
+
if (deliverAt.getTime() > Date.now() + 3e4) {
|
|
2086
|
+
return deferSendForWindow(run, deliverAt, ctx);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
1900
2089
|
return handleSend(run, step, contact, flow, ctx);
|
|
2090
|
+
}
|
|
1901
2091
|
case "tag":
|
|
1902
2092
|
return handleTag(run, step, ctx);
|
|
1903
2093
|
case "fire_event":
|
|
@@ -2048,6 +2238,34 @@ async function handleWebhookStep(run, step, ctx) {
|
|
|
2048
2238
|
}
|
|
2049
2239
|
}
|
|
2050
2240
|
}
|
|
2241
|
+
async function deferSendForWindow(run, deliverAt, ctx) {
|
|
2242
|
+
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
2243
|
+
// Only write once per deferral — if nextActionAt already points at (or
|
|
2244
|
+
// past) the slot, another worker/tick got here first.
|
|
2245
|
+
{ _id: run._id, currentStepIndex: run.currentStepIndex, nextActionAt: { $lt: deliverAt } },
|
|
2246
|
+
{
|
|
2247
|
+
$set: { nextActionAt: deliverAt, updatedAt: /* @__PURE__ */ new Date() },
|
|
2248
|
+
$push: {
|
|
2249
|
+
history: {
|
|
2250
|
+
stepIndex: run.currentStepIndex,
|
|
2251
|
+
action: "send_deferred",
|
|
2252
|
+
at: /* @__PURE__ */ new Date(),
|
|
2253
|
+
details: { until: deliverAt }
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
},
|
|
2257
|
+
{ returnDocument: "after" }
|
|
2258
|
+
);
|
|
2259
|
+
if (!updated) return;
|
|
2260
|
+
await ctx.queues.advance.add(
|
|
2261
|
+
"advance",
|
|
2262
|
+
{ flowRunId: String(run._id) },
|
|
2263
|
+
{
|
|
2264
|
+
delay: Math.max(0, deliverAt.getTime() - Date.now()),
|
|
2265
|
+
jobId: `advance:${run._id}:${run.currentStepIndex}:window:${deliverAt.getTime()}`
|
|
2266
|
+
}
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2051
2269
|
async function advanceStep(run, ctx, log, opts = {}) {
|
|
2052
2270
|
const stepInc = opts.stepInc ?? 1;
|
|
2053
2271
|
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
@@ -3572,6 +3790,7 @@ var Mailer = class _Mailer {
|
|
|
3572
3790
|
db: this.db,
|
|
3573
3791
|
collections: this.collections,
|
|
3574
3792
|
adapter: this.adapter,
|
|
3793
|
+
varsAdapter: this.config.varsAdapter,
|
|
3575
3794
|
providers: this.providers,
|
|
3576
3795
|
queues: this.queues,
|
|
3577
3796
|
config: this.config,
|
|
@@ -3651,6 +3870,10 @@ var Mailer = class _Mailer {
|
|
|
3651
3870
|
if (!config.providers[config.defaultProvider]) {
|
|
3652
3871
|
throw new Error(`defaultProvider "${config.defaultProvider}" not in providers map`);
|
|
3653
3872
|
}
|
|
3873
|
+
if (config.varsAdapter) {
|
|
3874
|
+
const { assertNoReservedVarKeys: assertNoReservedVarKeys2 } = await Promise.resolve().then(() => (init_vars(), vars_exports));
|
|
3875
|
+
assertNoReservedVarKeys2(config.varsAdapter);
|
|
3876
|
+
}
|
|
3654
3877
|
const collections = getCollections(config.db, config.collectionPrefix);
|
|
3655
3878
|
await ensureIndexes(config.db, config.collectionPrefix);
|
|
3656
3879
|
const queueDriver = await createQueueDriver(config.queue, config.db);
|
|
@@ -3866,6 +4089,72 @@ var Mailer = class _Mailer {
|
|
|
3866
4089
|
await this.collections.contactTags.deleteOne({ externalId: parsed.externalId, tag: parsed.tag });
|
|
3867
4090
|
}
|
|
3868
4091
|
}
|
|
4092
|
+
// -------------------------------------------------------------------------
|
|
4093
|
+
// Flow abort
|
|
4094
|
+
// -------------------------------------------------------------------------
|
|
4095
|
+
/**
|
|
4096
|
+
* Abort every active run of one flow for a contact, immediately. Runs parked
|
|
4097
|
+
* in a `wait` exit too — their delayed wake-up jobs find the run exited and
|
|
4098
|
+
* no-op. Also cancels any of the flow's emails still sitting in the send
|
|
4099
|
+
* queue for this contact (queued or awaiting retry), so an abort means no
|
|
4100
|
+
* further mail, not just no further steps.
|
|
4101
|
+
*
|
|
4102
|
+
* No-op (returns zero counts) when nothing is active. Safe to call from the
|
|
4103
|
+
* same handler that processes the business event ("user upgraded").
|
|
4104
|
+
*/
|
|
4105
|
+
async abortFlow(flowSlug, externalId, opts = {}) {
|
|
4106
|
+
const parsed = abortFlowInputSchema.parse({ flowSlug, externalId, reason: opts.reason });
|
|
4107
|
+
const flow = await this.collections.flows.findOne(
|
|
4108
|
+
{ slug: parsed.flowSlug },
|
|
4109
|
+
{ projection: { _id: 1 } }
|
|
4110
|
+
);
|
|
4111
|
+
if (!flow) throw new Error(`abortFlow: unknown flow slug "${parsed.flowSlug}"`);
|
|
4112
|
+
const result = await this.abortActiveRuns(
|
|
4113
|
+
{ externalId: parsed.externalId, flowId: flow._id },
|
|
4114
|
+
parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
|
|
4115
|
+
);
|
|
4116
|
+
if (result.abortedRuns > 0 || result.cancelledSends > 0) {
|
|
4117
|
+
await this.audit({
|
|
4118
|
+
actor: "host",
|
|
4119
|
+
action: "flow.abort",
|
|
4120
|
+
resource: { collection: "mailer_flow_runs", slug: parsed.flowSlug },
|
|
4121
|
+
diffSummary: `abortFlow slug=${parsed.flowSlug} externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
|
|
4122
|
+
});
|
|
4123
|
+
}
|
|
4124
|
+
return result;
|
|
4125
|
+
}
|
|
4126
|
+
/**
|
|
4127
|
+
* Abort every active flow run for a contact across all flows. Same semantics
|
|
4128
|
+
* as `abortFlow` — for "stop everything" events (account deleted, churned).
|
|
4129
|
+
*/
|
|
4130
|
+
async abortAllFlows(externalId, opts = {}) {
|
|
4131
|
+
const parsed = abortAllFlowsInputSchema.parse({ externalId, reason: opts.reason });
|
|
4132
|
+
const result = await this.abortActiveRuns(
|
|
4133
|
+
{ externalId: parsed.externalId },
|
|
4134
|
+
parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
|
|
4135
|
+
);
|
|
4136
|
+
if (result.abortedRuns > 0 || result.cancelledSends > 0) {
|
|
4137
|
+
await this.audit({
|
|
4138
|
+
actor: "host",
|
|
4139
|
+
action: "flow.abort_all",
|
|
4140
|
+
resource: { collection: "mailer_flow_runs" },
|
|
4141
|
+
diffSummary: `abortAllFlows externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
|
|
4142
|
+
});
|
|
4143
|
+
}
|
|
4144
|
+
return result;
|
|
4145
|
+
}
|
|
4146
|
+
async abortActiveRuns(filter, exitReason) {
|
|
4147
|
+
const runs = await this.collections.flowRuns.find({ ...filter, status: "active" }).toArray();
|
|
4148
|
+
if (runs.length === 0) return { abortedRuns: 0, cancelledSends: 0 };
|
|
4149
|
+
for (const run of runs) {
|
|
4150
|
+
await exitFlowRun(run, exitReason, this.runnerContext);
|
|
4151
|
+
}
|
|
4152
|
+
const cancelled = await this.collections.sends.updateMany(
|
|
4153
|
+
{ flowRunId: { $in: runs.map((r) => r._id) }, status: { $in: ["queued", "failed"] } },
|
|
4154
|
+
{ $set: { status: "cancelled", errorMessage: `cancelled: ${exitReason}`, updatedAt: /* @__PURE__ */ new Date() } }
|
|
4155
|
+
);
|
|
4156
|
+
return { abortedRuns: runs.length, cancelledSends: cancelled.modifiedCount };
|
|
4157
|
+
}
|
|
3869
4158
|
/**
|
|
3870
4159
|
* GDPR right-to-erasure. Hard-deletes the contact's PII and leaves a hashed
|
|
3871
4160
|
* suppression row to block re-import. INVARIANT 9.
|
|
@@ -4068,6 +4357,7 @@ var Mailer = class _Mailer {
|
|
|
4068
4357
|
|
|
4069
4358
|
// src/server/index.ts
|
|
4070
4359
|
init_mongo();
|
|
4360
|
+
init_vars();
|
|
4071
4361
|
|
|
4072
4362
|
// src/server/providers/null.ts
|
|
4073
4363
|
var counter = 0;
|
|
@@ -4206,6 +4496,18 @@ ${input.plainText}`);
|
|
|
4206
4496
|
hint: "Front-load the important words so the truncated preview still makes sense."
|
|
4207
4497
|
});
|
|
4208
4498
|
}
|
|
4499
|
+
if (config.varsJsonSchema) {
|
|
4500
|
+
const sources = [input.subject, input.preheader, input.mjml, JSON.stringify(input.editorJson ?? null)];
|
|
4501
|
+
const unknown = findUnknownVariables(sources.join("\n"), config.varsJsonSchema);
|
|
4502
|
+
if (unknown.length > 0) {
|
|
4503
|
+
issues.push({
|
|
4504
|
+
rule: "unknown_variable",
|
|
4505
|
+
severity: "warning",
|
|
4506
|
+
message: `Template references variable(s) not in the vars schema: ${unknown.join(", ")}.`,
|
|
4507
|
+
hint: "These render as empty strings. Check for typos, or add the key to your varsAdapter schema. Paths inside {{#each}}/{{#with}} blocks are relative and not checked."
|
|
4508
|
+
});
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4209
4511
|
const linkCount = countMatches(input.html, /<a\s[^>]*\bhref\s*=/gi);
|
|
4210
4512
|
if (linkCount > 10) {
|
|
4211
4513
|
issues.push({
|
|
@@ -4268,6 +4570,60 @@ function findSpamSignals(text) {
|
|
|
4268
4570
|
if (/!{3,}/.test(text)) out.add('excessive "!!!"');
|
|
4269
4571
|
return Array.from(out);
|
|
4270
4572
|
}
|
|
4573
|
+
var BUILTIN_VAR_ROOTS = /* @__PURE__ */ new Set([
|
|
4574
|
+
"contact",
|
|
4575
|
+
"vars",
|
|
4576
|
+
"event",
|
|
4577
|
+
"unsubscribeUrl",
|
|
4578
|
+
"viewInBrowserUrl",
|
|
4579
|
+
"preferenceCenterUrl",
|
|
4580
|
+
"senderAddress",
|
|
4581
|
+
"this"
|
|
4582
|
+
]);
|
|
4583
|
+
var PATH_TOKEN = /^@?[A-Za-z_][\w$]*(\.[A-Za-z_][\w$]*)*$/;
|
|
4584
|
+
function findUnknownVariables(source, schema) {
|
|
4585
|
+
const hasBlockScopes = /\{\{[#^]\s*(each|with)\b/.test(source);
|
|
4586
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
4587
|
+
const re = /\{\{\{?\s*([^{}]+?)\s*\}?\}\}/g;
|
|
4588
|
+
let m;
|
|
4589
|
+
while (m = re.exec(source)) {
|
|
4590
|
+
const expr = m[1].trim();
|
|
4591
|
+
if (!expr || /^[#^/>!]/.test(expr) || expr === "else") continue;
|
|
4592
|
+
if (expr.includes("(")) continue;
|
|
4593
|
+
const parts = expr.split(/\s+/);
|
|
4594
|
+
const candidates = parts.length > 1 ? parts.slice(1) : parts;
|
|
4595
|
+
for (const raw of candidates) {
|
|
4596
|
+
if (!PATH_TOKEN.test(raw)) continue;
|
|
4597
|
+
if (raw.startsWith("@")) continue;
|
|
4598
|
+
const segments = raw.split(".");
|
|
4599
|
+
if (BUILTIN_VAR_ROOTS.has(segments[0])) continue;
|
|
4600
|
+
if (hasBlockScopes && segments.length === 1) continue;
|
|
4601
|
+
if (!schemaHasPath(schema, segments)) unknown.add(raw);
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
return Array.from(unknown);
|
|
4605
|
+
}
|
|
4606
|
+
function schemaHasPath(node, segments) {
|
|
4607
|
+
if (segments.length === 0) return true;
|
|
4608
|
+
if (!node || typeof node !== "object") return true;
|
|
4609
|
+
const n = node;
|
|
4610
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
4611
|
+
if (Array.isArray(n[key])) {
|
|
4612
|
+
return n[key].some((branch) => schemaHasPath(branch, segments));
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
if (Array.isArray(n.type) && n.type.includes("object") && !n.properties) return true;
|
|
4616
|
+
if (n.type === "array") {
|
|
4617
|
+
if (segments[0] === "length") return true;
|
|
4618
|
+
return schemaHasPath(n.items, segments);
|
|
4619
|
+
}
|
|
4620
|
+
const props = n.properties;
|
|
4621
|
+
if (!props || typeof props !== "object") return true;
|
|
4622
|
+
const [head, ...rest] = segments;
|
|
4623
|
+
if (head in props) return schemaHasPath(props[head], rest);
|
|
4624
|
+
if (n.additionalProperties === void 0 || n.additionalProperties === false) return false;
|
|
4625
|
+
return true;
|
|
4626
|
+
}
|
|
4271
4627
|
function isAllCaps(subject) {
|
|
4272
4628
|
const letters = subject.replace(/[^a-zA-Z]/g, "");
|
|
4273
4629
|
if (letters.length < 5) return false;
|
|
@@ -4275,6 +4631,9 @@ function isAllCaps(subject) {
|
|
|
4275
4631
|
return upper / letters.length > 0.5;
|
|
4276
4632
|
}
|
|
4277
4633
|
|
|
4634
|
+
// src/server/api/admin.ts
|
|
4635
|
+
init_vars();
|
|
4636
|
+
|
|
4278
4637
|
// src/server/api/setup-status.ts
|
|
4279
4638
|
async function runSetupChecks(mailer) {
|
|
4280
4639
|
const checks = [];
|
|
@@ -4713,7 +5072,7 @@ async function checkPostalAddress(mailer) {
|
|
|
4713
5072
|
label: "CAN-SPAM postal address",
|
|
4714
5073
|
severity: "warn",
|
|
4715
5074
|
message: `${marketingCount} published marketing template${marketingCount === 1 ? "" : "s"} but senderAddress is unset`,
|
|
4716
|
-
hint: "CAN-SPAM requires a postal address in marketing emails. Set `senderAddress` in your Mailer config and reference
|
|
5075
|
+
hint: "CAN-SPAM requires a postal address in marketing emails. Set `senderAddress` in your Mailer config and reference the `{{senderAddress}}` render variable in your templates."
|
|
4717
5076
|
};
|
|
4718
5077
|
}
|
|
4719
5078
|
async function checkDoiTemplate(mailer) {
|
|
@@ -5007,19 +5366,30 @@ async function persistScore(ctx, input) {
|
|
|
5007
5366
|
}
|
|
5008
5367
|
async function evaluateMailTesterGate(ctx, input) {
|
|
5009
5368
|
const cfg = ctx.config.mailTester;
|
|
5010
|
-
if (!cfg?.apiKey) return { allowed: true, reason: null, score: null };
|
|
5369
|
+
if (!cfg?.apiKey) return { allowed: true, reason: null, score: null, code: null };
|
|
5011
5370
|
const minScore = cfg.minScore ?? 8;
|
|
5012
5371
|
const key = mailTesterContentKey(input);
|
|
5013
5372
|
const cached = await findCachedScore(ctx, key);
|
|
5014
|
-
if (!cached)
|
|
5373
|
+
if (!cached) {
|
|
5374
|
+
if (cfg.requireScore) {
|
|
5375
|
+
return {
|
|
5376
|
+
allowed: false,
|
|
5377
|
+
reason: "No Mail-Tester score for this exact content. Run a deliverability check before publishing.",
|
|
5378
|
+
score: null,
|
|
5379
|
+
code: "no_score"
|
|
5380
|
+
};
|
|
5381
|
+
}
|
|
5382
|
+
return { allowed: true, reason: null, score: null, code: null };
|
|
5383
|
+
}
|
|
5015
5384
|
if (cached.score < minScore) {
|
|
5016
5385
|
return {
|
|
5017
5386
|
allowed: false,
|
|
5018
5387
|
reason: `Mail-Tester score ${cached.score.toFixed(1)} is below minimum ${minScore.toFixed(1)}`,
|
|
5019
|
-
score: cached
|
|
5388
|
+
score: cached,
|
|
5389
|
+
code: "low_score"
|
|
5020
5390
|
};
|
|
5021
5391
|
}
|
|
5022
|
-
return { allowed: true, reason: null, score: cached };
|
|
5392
|
+
return { allowed: true, reason: null, score: cached, code: null };
|
|
5023
5393
|
}
|
|
5024
5394
|
|
|
5025
5395
|
// src/server/api/admin.ts
|
|
@@ -5054,12 +5424,19 @@ function createAdminRouter(mailer, opts = {}) {
|
|
|
5054
5424
|
function apiRouter(mailer, opts = {}) {
|
|
5055
5425
|
const r = express.Router();
|
|
5056
5426
|
const c = mailer.collections;
|
|
5427
|
+
const varsSchema = mailer.config.varsAdapter ? varsJsonSchema(mailer.config.varsAdapter) : null;
|
|
5057
5428
|
function getMailTesterClient() {
|
|
5058
5429
|
if (opts.mailTesterClient) return opts.mailTesterClient;
|
|
5059
5430
|
const cfg = mailer.config.mailTester;
|
|
5060
5431
|
if (!cfg?.apiKey) return null;
|
|
5061
5432
|
return createMailTesterClient(cfg);
|
|
5062
5433
|
}
|
|
5434
|
+
r.get(
|
|
5435
|
+
"/vars-schema",
|
|
5436
|
+
asyncHandler(async (_req, res) => {
|
|
5437
|
+
res.json({ schema: varsSchema, builtins: exports.RESERVED_VAR_KEYS });
|
|
5438
|
+
})
|
|
5439
|
+
);
|
|
5063
5440
|
r.get(
|
|
5064
5441
|
"/me",
|
|
5065
5442
|
asyncHandler(async (req, res) => {
|
|
@@ -6006,6 +6383,7 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6006
6383
|
res.json({
|
|
6007
6384
|
configured,
|
|
6008
6385
|
minScore: cfg?.minScore ?? 8,
|
|
6386
|
+
requireScore: !!cfg?.requireScore,
|
|
6009
6387
|
cacheHours: cfg?.cacheHours ?? 24,
|
|
6010
6388
|
score: cached
|
|
6011
6389
|
});
|
|
@@ -6146,9 +6524,11 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6146
6524
|
compileFailed: true
|
|
6147
6525
|
});
|
|
6148
6526
|
}
|
|
6527
|
+
if (!html && typeof tpl.body?.html === "string") html = tpl.body.html;
|
|
6528
|
+
if (!plainText && typeof tpl.body?.plainText === "string") plainText = tpl.body.plainText;
|
|
6149
6529
|
const lint = lintTemplate(
|
|
6150
6530
|
{ subject, preheader, mjml, editorJson, html, plainText, kind, fromEmail },
|
|
6151
|
-
{ senderDomains: mailer.config.senderDomains }
|
|
6531
|
+
{ senderDomains: mailer.config.senderDomains, varsJsonSchema: varsSchema }
|
|
6152
6532
|
);
|
|
6153
6533
|
res.json({ ...lint, compileFailed: false });
|
|
6154
6534
|
})
|
|
@@ -6199,7 +6579,7 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6199
6579
|
kind: tpl.kind,
|
|
6200
6580
|
fromEmail: tpl.fromEmail
|
|
6201
6581
|
},
|
|
6202
|
-
{ senderDomains: mailer.config.senderDomains }
|
|
6582
|
+
{ senderDomains: mailer.config.senderDomains, varsJsonSchema: varsSchema }
|
|
6203
6583
|
);
|
|
6204
6584
|
if (lint.errors.length > 0) {
|
|
6205
6585
|
return res.status(422).json({
|
|
@@ -6218,9 +6598,10 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6218
6598
|
if (!gate.allowed) {
|
|
6219
6599
|
return res.status(422).json({
|
|
6220
6600
|
error: "mail_tester_blocked",
|
|
6601
|
+
code: gate.code,
|
|
6221
6602
|
message: gate.reason,
|
|
6222
6603
|
score: gate.score,
|
|
6223
|
-
hint: "Re-run the deliverability check after fixing the feedback, or POST `bypassMailTester: true` to publish anyway."
|
|
6604
|
+
hint: gate.code === "no_score" ? "POST to `/templates/:slug/mail-tester-check`, poll `/mail-tester-result`, then publish \u2014 or POST `bypassMailTester: true` to publish anyway." : "Re-run the deliverability check after fixing the feedback, or POST `bypassMailTester: true` to publish anyway."
|
|
6224
6605
|
});
|
|
6225
6606
|
}
|
|
6226
6607
|
}
|
|
@@ -6291,40 +6672,93 @@ function apiRouter(mailer, opts = {}) {
|
|
|
6291
6672
|
html = tpl.body.html;
|
|
6292
6673
|
plainText = tpl.body.plainText;
|
|
6293
6674
|
}
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6675
|
+
let contact;
|
|
6676
|
+
const contactId = typeof req.body?.contactId === "string" ? req.body.contactId : null;
|
|
6677
|
+
if (contactId) {
|
|
6678
|
+
const found = await mailer.adapter.getById(contactId);
|
|
6679
|
+
if (!found) return res.status(404).json({ error: "contact_not_found", contactId });
|
|
6680
|
+
contact = found;
|
|
6681
|
+
} else {
|
|
6682
|
+
contact = req.body?.sampleContact ?? {
|
|
6683
|
+
externalId: "preview-contact",
|
|
6684
|
+
email: "preview@example.com",
|
|
6685
|
+
tags: [],
|
|
6686
|
+
fields: { firstName: "Alex" }
|
|
6687
|
+
};
|
|
6688
|
+
}
|
|
6689
|
+
const eventProperties = req.body?.eventProperties && typeof req.body.eventProperties === "object" ? req.body.eventProperties : void 0;
|
|
6690
|
+
let resolved = {};
|
|
6691
|
+
try {
|
|
6692
|
+
resolved = await resolveVars(mailer.config.varsAdapter, contact, {
|
|
6693
|
+
reason: "preview",
|
|
6694
|
+
templateSlug: tpl.slug,
|
|
6695
|
+
eventProperties
|
|
6696
|
+
});
|
|
6697
|
+
} catch (err) {
|
|
6698
|
+
return res.status(502).json({
|
|
6699
|
+
error: "vars_resolve_failed",
|
|
6700
|
+
message: `varsAdapter.resolve threw: ${String(err?.message ?? err)}`
|
|
6701
|
+
});
|
|
6702
|
+
}
|
|
6300
6703
|
const renderCtx = {
|
|
6301
|
-
|
|
6704
|
+
...resolved,
|
|
6705
|
+
contact,
|
|
6302
6706
|
vars: req.body?.vars ?? {},
|
|
6707
|
+
event: eventProperties ?? {},
|
|
6303
6708
|
unsubscribeUrl: `${mailer.config.publicUrl}/m/unsub/preview`,
|
|
6304
6709
|
senderAddress: mailer.config.senderAddress
|
|
6305
6710
|
};
|
|
6306
6711
|
const previewTpl = { ...tpl, body: { ...tpl.body, html, plainText } };
|
|
6307
6712
|
const rendered = await renderTemplate(previewTpl, renderCtx, { helpers: mailer.config.handlebarsHelpers });
|
|
6308
|
-
return res.json({
|
|
6713
|
+
return res.json({
|
|
6714
|
+
subject: rendered.subject,
|
|
6715
|
+
preheader: rendered.preheader,
|
|
6716
|
+
html: rendered.html,
|
|
6717
|
+
plainText: rendered.plainText,
|
|
6718
|
+
contact: { externalId: contact.externalId, email: contact.email }
|
|
6719
|
+
});
|
|
6309
6720
|
})
|
|
6310
6721
|
);
|
|
6311
6722
|
r.post(
|
|
6312
6723
|
"/templates/:slug/send-test",
|
|
6313
6724
|
asyncHandler(async (req, res) => {
|
|
6314
|
-
const { to, sampleData } = req.body ?? {};
|
|
6725
|
+
const { to, sampleData, contactId, eventProperties } = req.body ?? {};
|
|
6315
6726
|
if (!to) return res.status(400).json({ error: "to_required" });
|
|
6316
6727
|
const tpl = await c.templates.findOne({ slug: req.params.slug });
|
|
6317
6728
|
if (!tpl) return res.status(404).json({ error: "not_found" });
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
}
|
|
6324
|
-
|
|
6729
|
+
let contact;
|
|
6730
|
+
if (typeof contactId === "string" && contactId) {
|
|
6731
|
+
const found = await mailer.adapter.getById(contactId);
|
|
6732
|
+
if (!found) return res.status(404).json({ error: "contact_not_found", contactId });
|
|
6733
|
+
contact = { ...found, email: to };
|
|
6734
|
+
} else {
|
|
6735
|
+
contact = sampleData?.contact ?? {
|
|
6736
|
+
externalId: "test-recipient",
|
|
6737
|
+
email: to,
|
|
6738
|
+
tags: [],
|
|
6739
|
+
fields: { firstName: "Test" }
|
|
6740
|
+
};
|
|
6741
|
+
contact.email = to;
|
|
6742
|
+
}
|
|
6743
|
+
const evProps = eventProperties && typeof eventProperties === "object" ? eventProperties : void 0;
|
|
6744
|
+
let resolved = {};
|
|
6745
|
+
try {
|
|
6746
|
+
resolved = await resolveVars(mailer.config.varsAdapter, contact, {
|
|
6747
|
+
reason: "test",
|
|
6748
|
+
templateSlug: tpl.slug,
|
|
6749
|
+
eventProperties: evProps
|
|
6750
|
+
});
|
|
6751
|
+
} catch (err) {
|
|
6752
|
+
return res.status(502).json({
|
|
6753
|
+
error: "vars_resolve_failed",
|
|
6754
|
+
message: `varsAdapter.resolve threw: ${String(err?.message ?? err)}`
|
|
6755
|
+
});
|
|
6756
|
+
}
|
|
6325
6757
|
const renderCtx = {
|
|
6758
|
+
...resolved,
|
|
6326
6759
|
contact,
|
|
6327
6760
|
vars: sampleData?.vars ?? {},
|
|
6761
|
+
event: evProps ?? {},
|
|
6328
6762
|
unsubscribeUrl: `${mailer.config.publicUrl}/m/unsub/test`,
|
|
6329
6763
|
senderAddress: mailer.config.senderAddress
|
|
6330
6764
|
};
|
|
@@ -6975,7 +7409,7 @@ var DEDUPE_POLICIES = [
|
|
|
6975
7409
|
];
|
|
6976
7410
|
|
|
6977
7411
|
// src/server/index.ts
|
|
6978
|
-
var VERSION = "0.
|
|
7412
|
+
var VERSION = "0.8.0";
|
|
6979
7413
|
|
|
6980
7414
|
exports.DEDUPE_POLICIES = DEDUPE_POLICIES;
|
|
6981
7415
|
exports.FLOW_STEP_KINDS = FLOW_STEP_KINDS;
|
|
@@ -6988,11 +7422,13 @@ exports.applyTracking = applyTracking;
|
|
|
6988
7422
|
exports.applyWebhookEvent = applyWebhookEvent;
|
|
6989
7423
|
exports.compileMailyTemplate = compileMailyTemplate;
|
|
6990
7424
|
exports.compileTemplate = compileTemplate;
|
|
7425
|
+
exports.computeDeliveryTime = computeDeliveryTime;
|
|
6991
7426
|
exports.createAdminRouter = createAdminRouter;
|
|
6992
7427
|
exports.createPublicRouter = createPublicRouter;
|
|
6993
7428
|
exports.defaultFlowStep = defaultFlowStep;
|
|
6994
7429
|
exports.defaultPredicate = defaultPredicate;
|
|
6995
7430
|
exports.defaultSegmentFilter = defaultSegmentFilter;
|
|
7431
|
+
exports.defineVars = defineVars;
|
|
6996
7432
|
exports.derivePlaintext = derivePlaintext;
|
|
6997
7433
|
exports.dispatchSend = dispatchSend;
|
|
6998
7434
|
exports.ensureIndexes = ensureIndexes;
|
|
@@ -7006,6 +7442,7 @@ exports.sha256Hex = sha256Hex;
|
|
|
7006
7442
|
exports.signUnsubscribeToken = signUnsubscribeToken;
|
|
7007
7443
|
exports.sweepStrandedFlowRuns = sweepStrandedFlowRuns;
|
|
7008
7444
|
exports.validateSenderDomain = validateSenderDomain;
|
|
7445
|
+
exports.varsJsonSchema = varsJsonSchema;
|
|
7009
7446
|
exports.verifyUnsubscribeToken = verifyUnsubscribeToken;
|
|
7010
7447
|
//# sourceMappingURL=index.cjs.map
|
|
7011
7448
|
//# sourceMappingURL=index.cjs.map
|