mailery 0.5.1 → 0.7.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-CjQTOX9H.js} +8 -8
- package/dist/admin/spa/index-CjQTOX9H.js.map +1 -0
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/{template-editor-Bn28OPMU.js → template-editor-yH0Oz4Ix.js} +105 -105
- package/dist/admin/spa/template-editor-yH0Oz4Ix.js.map +1 -0
- package/dist/index.cjs +453 -30
- 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 +451 -31
- package/dist/index.js.map +1 -1
- package/dist/{null-BYB6v4Iv.d.cts → null-B0rPgE5_.d.cts} +126 -3
- package/dist/{null-BYB6v4Iv.d.ts → null-B0rPgE5_.d.ts} +126 -3
- package/dist/testing.cjs +305 -17
- 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 +305 -17
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/admin/spa/index-Dmsla2eL.js.map +0 -1
- package/dist/admin/spa/template-editor-Bn28OPMU.js.map +0 -1
package/dist/testing.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');
|
|
@@ -63,6 +63,55 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
63
63
|
));
|
|
64
64
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
65
65
|
|
|
66
|
+
// src/server/adapters/vars.ts
|
|
67
|
+
var vars_exports = {};
|
|
68
|
+
__export(vars_exports, {
|
|
69
|
+
RESERVED_VAR_KEYS: () => RESERVED_VAR_KEYS,
|
|
70
|
+
assertNoReservedVarKeys: () => assertNoReservedVarKeys,
|
|
71
|
+
defineVars: () => defineVars,
|
|
72
|
+
resolveVars: () => resolveVars,
|
|
73
|
+
varsJsonSchema: () => varsJsonSchema
|
|
74
|
+
});
|
|
75
|
+
function defineVars(adapter) {
|
|
76
|
+
return adapter;
|
|
77
|
+
}
|
|
78
|
+
function assertNoReservedVarKeys(adapter) {
|
|
79
|
+
const json = varsJsonSchema(adapter);
|
|
80
|
+
const props = json && typeof json === "object" ? json.properties : void 0;
|
|
81
|
+
if (!props) return;
|
|
82
|
+
const clashes = RESERVED_VAR_KEYS.filter((k) => k in props);
|
|
83
|
+
if (clashes.length > 0) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`varsAdapter schema declares reserved key(s): ${clashes.join(", ")}. These names are provided by mailery itself \u2014 rename them in your schema.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function varsJsonSchema(adapter) {
|
|
90
|
+
return zod.z.toJSONSchema(adapter.schema, { io: "output" });
|
|
91
|
+
}
|
|
92
|
+
async function resolveVars(adapter, contact, info) {
|
|
93
|
+
if (!adapter) return {};
|
|
94
|
+
const resolved = await adapter.resolve(contact, info);
|
|
95
|
+
if (!resolved || typeof resolved !== "object") return {};
|
|
96
|
+
const out = { ...resolved };
|
|
97
|
+
for (const k of RESERVED_VAR_KEYS) delete out[k];
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
var RESERVED_VAR_KEYS;
|
|
101
|
+
var init_vars = __esm({
|
|
102
|
+
"src/server/adapters/vars.ts"() {
|
|
103
|
+
RESERVED_VAR_KEYS = [
|
|
104
|
+
"contact",
|
|
105
|
+
"vars",
|
|
106
|
+
"event",
|
|
107
|
+
"unsubscribeUrl",
|
|
108
|
+
"viewInBrowserUrl",
|
|
109
|
+
"preferenceCenterUrl",
|
|
110
|
+
"senderAddress"
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
66
115
|
// src/server/adapters/mongo.ts
|
|
67
116
|
var mongo_exports = {};
|
|
68
117
|
__export(mongo_exports, {
|
|
@@ -3005,19 +3054,19 @@ var require_range = __commonJS({
|
|
|
3005
3054
|
var replaceCaret = (comp, options2) => {
|
|
3006
3055
|
debug("caret", comp, options2);
|
|
3007
3056
|
const r = options2.loose ? re[t.CARETLOOSE] : re[t.CARET];
|
|
3008
|
-
const
|
|
3057
|
+
const z3 = options2.includePrerelease ? "-0" : "";
|
|
3009
3058
|
return comp.replace(r, (_, M, m, p, pr) => {
|
|
3010
3059
|
debug("caret", comp, _, M, m, p, pr);
|
|
3011
3060
|
let ret;
|
|
3012
3061
|
if (isX(M)) {
|
|
3013
3062
|
ret = "";
|
|
3014
3063
|
} else if (isX(m)) {
|
|
3015
|
-
ret = `>=${M}.0.0${
|
|
3064
|
+
ret = `>=${M}.0.0${z3} <${+M + 1}.0.0-0`;
|
|
3016
3065
|
} else if (isX(p)) {
|
|
3017
3066
|
if (M === "0") {
|
|
3018
|
-
ret = `>=${M}.${m}.0${
|
|
3067
|
+
ret = `>=${M}.${m}.0${z3} <${M}.${+m + 1}.0-0`;
|
|
3019
3068
|
} else {
|
|
3020
|
-
ret = `>=${M}.${m}.0${
|
|
3069
|
+
ret = `>=${M}.${m}.0${z3} <${+M + 1}.0.0-0`;
|
|
3021
3070
|
}
|
|
3022
3071
|
} else if (pr) {
|
|
3023
3072
|
debug("replaceCaret pr", pr);
|
|
@@ -3034,9 +3083,9 @@ var require_range = __commonJS({
|
|
|
3034
3083
|
debug("no pr");
|
|
3035
3084
|
if (M === "0") {
|
|
3036
3085
|
if (m === "0") {
|
|
3037
|
-
ret = `>=${M}.${m}.${p}${
|
|
3086
|
+
ret = `>=${M}.${m}.${p}${z3} <${M}.${m}.${+p + 1}-0`;
|
|
3038
3087
|
} else {
|
|
3039
|
-
ret = `>=${M}.${m}.${p}${
|
|
3088
|
+
ret = `>=${M}.${m}.${p}${z3} <${M}.${+m + 1}.0-0`;
|
|
3040
3089
|
}
|
|
3041
3090
|
} else {
|
|
3042
3091
|
ret = `>=${M}.${m}.${p} <${+M + 1}.0.0-0`;
|
|
@@ -14156,6 +14205,12 @@ var tagInputSchema = zod.z.object({
|
|
|
14156
14205
|
externalId: externalIdSchema,
|
|
14157
14206
|
tag: zod.z.string().min(1).max(128)
|
|
14158
14207
|
});
|
|
14208
|
+
var abortFlowInputSchema = zod.z.object({
|
|
14209
|
+
flowSlug: slugSchema,
|
|
14210
|
+
externalId: externalIdSchema,
|
|
14211
|
+
reason: zod.z.string().min(1).max(200).optional()
|
|
14212
|
+
});
|
|
14213
|
+
var abortAllFlowsInputSchema = abortFlowInputSchema.omit({ flowSlug: true });
|
|
14159
14214
|
var sendOneOffInputSchema = zod.z.object({
|
|
14160
14215
|
templateSlug: slugSchema,
|
|
14161
14216
|
externalId: externalIdSchema,
|
|
@@ -14185,7 +14240,13 @@ var flowStepSchema = zod.z.lazy(
|
|
|
14185
14240
|
type: zod.z.literal("send"),
|
|
14186
14241
|
templateSlug: slugSchema,
|
|
14187
14242
|
providerOverride: zod.z.string().optional(),
|
|
14188
|
-
vars: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
|
|
14243
|
+
vars: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
|
|
14244
|
+
delivery: zod.z.object({
|
|
14245
|
+
weekdaysOnly: zod.z.boolean().optional(),
|
|
14246
|
+
timeOfDay: zod.z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "expected HH:mm").optional(),
|
|
14247
|
+
useContactTimezone: zod.z.boolean().optional(),
|
|
14248
|
+
timezone: zod.z.string().optional()
|
|
14249
|
+
}).optional()
|
|
14189
14250
|
}),
|
|
14190
14251
|
zod.z.object({
|
|
14191
14252
|
type: zod.z.literal("tag"),
|
|
@@ -14920,6 +14981,7 @@ async function tryEnterFlow(flow, event, ctx) {
|
|
|
14920
14981
|
flowSlug: flow.slug,
|
|
14921
14982
|
flowVersion: flow.version,
|
|
14922
14983
|
emailAtEntry: sub.emailAtSubscribe,
|
|
14984
|
+
triggerEvent: { name: event.name, properties: event.properties ?? {}, occurredAt: event.occurredAt },
|
|
14923
14985
|
enteredAt: /* @__PURE__ */ new Date(),
|
|
14924
14986
|
status: "active",
|
|
14925
14987
|
currentStepIndex: 0,
|
|
@@ -15017,6 +15079,105 @@ function effectiveLowerBound(ctx, opts) {
|
|
|
15017
15079
|
}
|
|
15018
15080
|
return null;
|
|
15019
15081
|
}
|
|
15082
|
+
|
|
15083
|
+
// src/server/runner/delivery-window.ts
|
|
15084
|
+
var TIME_OF_DAY_GRACE_MS = 60 * 6e4;
|
|
15085
|
+
function computeDeliveryTime(now, window2, contactTimezone) {
|
|
15086
|
+
const tz = pickTimezone(window2, contactTimezone);
|
|
15087
|
+
let candidate = now;
|
|
15088
|
+
if (window2.timeOfDay) {
|
|
15089
|
+
const [hh, mm] = window2.timeOfDay.split(":").map(Number);
|
|
15090
|
+
const local = localParts(candidate, tz);
|
|
15091
|
+
const todaySlot = utcFromLocal(local.y, local.mo, local.d, hh, mm, tz);
|
|
15092
|
+
if (candidate.getTime() < todaySlot.getTime()) {
|
|
15093
|
+
candidate = todaySlot;
|
|
15094
|
+
} else if (candidate.getTime() - todaySlot.getTime() > TIME_OF_DAY_GRACE_MS) {
|
|
15095
|
+
const next = addLocalDays(local, 1);
|
|
15096
|
+
candidate = utcFromLocal(next.y, next.mo, next.d, hh, mm, tz);
|
|
15097
|
+
}
|
|
15098
|
+
}
|
|
15099
|
+
if (window2.weekdaysOnly) {
|
|
15100
|
+
for (let guard = 0; guard < 3; guard++) {
|
|
15101
|
+
const local = localParts(candidate, tz);
|
|
15102
|
+
if (local.weekday !== "Sat" && local.weekday !== "Sun") break;
|
|
15103
|
+
const shift = local.weekday === "Sat" ? 2 : 1;
|
|
15104
|
+
const moved = addLocalDays(local, shift);
|
|
15105
|
+
candidate = utcFromLocal(moved.y, moved.mo, moved.d, local.hh, local.mi, tz);
|
|
15106
|
+
}
|
|
15107
|
+
}
|
|
15108
|
+
return candidate;
|
|
15109
|
+
}
|
|
15110
|
+
function pickTimezone(window2, contactTimezone) {
|
|
15111
|
+
const candidates = [
|
|
15112
|
+
window2.useContactTimezone ? contactTimezone : void 0,
|
|
15113
|
+
window2.timezone,
|
|
15114
|
+
"UTC"
|
|
15115
|
+
];
|
|
15116
|
+
for (const tz of candidates) {
|
|
15117
|
+
if (tz && isValidTimezone(tz)) return tz;
|
|
15118
|
+
}
|
|
15119
|
+
return "UTC";
|
|
15120
|
+
}
|
|
15121
|
+
var validatedZones = /* @__PURE__ */ new Map();
|
|
15122
|
+
function isValidTimezone(tz) {
|
|
15123
|
+
const cached = validatedZones.get(tz);
|
|
15124
|
+
if (cached !== void 0) return cached;
|
|
15125
|
+
let ok = true;
|
|
15126
|
+
try {
|
|
15127
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
15128
|
+
} catch {
|
|
15129
|
+
ok = false;
|
|
15130
|
+
}
|
|
15131
|
+
validatedZones.set(tz, ok);
|
|
15132
|
+
return ok;
|
|
15133
|
+
}
|
|
15134
|
+
var partFormatters = /* @__PURE__ */ new Map();
|
|
15135
|
+
function formatterFor(tz) {
|
|
15136
|
+
let f = partFormatters.get(tz);
|
|
15137
|
+
if (!f) {
|
|
15138
|
+
f = new Intl.DateTimeFormat("en-US", {
|
|
15139
|
+
timeZone: tz,
|
|
15140
|
+
year: "numeric",
|
|
15141
|
+
month: "2-digit",
|
|
15142
|
+
day: "2-digit",
|
|
15143
|
+
hour: "2-digit",
|
|
15144
|
+
minute: "2-digit",
|
|
15145
|
+
second: "2-digit",
|
|
15146
|
+
weekday: "short",
|
|
15147
|
+
hour12: false
|
|
15148
|
+
});
|
|
15149
|
+
partFormatters.set(tz, f);
|
|
15150
|
+
}
|
|
15151
|
+
return f;
|
|
15152
|
+
}
|
|
15153
|
+
function localParts(date, tz) {
|
|
15154
|
+
const parts = {};
|
|
15155
|
+
for (const p of formatterFor(tz).formatToParts(date)) parts[p.type] = p.value;
|
|
15156
|
+
return {
|
|
15157
|
+
y: Number(parts.year),
|
|
15158
|
+
mo: Number(parts.month),
|
|
15159
|
+
d: Number(parts.day),
|
|
15160
|
+
hh: Number(parts.hour) % 24,
|
|
15161
|
+
// Intl emits '24' for midnight in some locales
|
|
15162
|
+
mi: Number(parts.minute),
|
|
15163
|
+
ss: Number(parts.second),
|
|
15164
|
+
weekday: parts.weekday
|
|
15165
|
+
};
|
|
15166
|
+
}
|
|
15167
|
+
function utcFromLocal(y, mo, d, hh, mi, tz) {
|
|
15168
|
+
let ts = Date.UTC(y, mo - 1, d, hh, mi, 0);
|
|
15169
|
+
for (let i = 0; i < 2; i++) {
|
|
15170
|
+
const p = localParts(new Date(ts), tz);
|
|
15171
|
+
const asUtc = Date.UTC(p.y, p.mo - 1, p.d, p.hh, p.mi, p.ss);
|
|
15172
|
+
const offset = asUtc - ts;
|
|
15173
|
+
ts = Date.UTC(y, mo - 1, d, hh, mi, 0) - offset;
|
|
15174
|
+
}
|
|
15175
|
+
return new Date(ts);
|
|
15176
|
+
}
|
|
15177
|
+
function addLocalDays(p, days) {
|
|
15178
|
+
const dt = new Date(Date.UTC(p.y, p.mo - 1, p.d + days));
|
|
15179
|
+
return { y: dt.getUTCFullYear(), mo: dt.getUTCMonth() + 1, d: dt.getUTCDate() };
|
|
15180
|
+
}
|
|
15020
15181
|
async function compileTemplate(mjml) {
|
|
15021
15182
|
const out = await mjml2html__default.default(mjml, { validationLevel: "soft", minify: false });
|
|
15022
15183
|
const plainText = derivePlaintext(out.html);
|
|
@@ -15134,6 +15295,9 @@ function makeHandlebars(extra) {
|
|
|
15134
15295
|
return hb;
|
|
15135
15296
|
}
|
|
15136
15297
|
|
|
15298
|
+
// src/server/runner/send.ts
|
|
15299
|
+
init_vars();
|
|
15300
|
+
|
|
15137
15301
|
// src/server/runner/suppression.ts
|
|
15138
15302
|
var SCOPES_BY_KIND = {
|
|
15139
15303
|
marketing: ["all", "marketing"],
|
|
@@ -15414,13 +15578,29 @@ async function dispatchSend(sendId, ctx) {
|
|
|
15414
15578
|
return;
|
|
15415
15579
|
}
|
|
15416
15580
|
const run = send.flowRunId ? await ctx.collections.flowRuns.findOne({ _id: send.flowRunId }) : null;
|
|
15417
|
-
|
|
15418
|
-
|
|
15419
|
-
|
|
15420
|
-
|
|
15421
|
-
|
|
15422
|
-
|
|
15423
|
-
|
|
15581
|
+
if (run && run.status === "exited" && run.exitReason?.startsWith("aborted_by_host")) {
|
|
15582
|
+
await ctx.collections.sends.updateOne(
|
|
15583
|
+
{ _id: send._id },
|
|
15584
|
+
{ $set: { status: "cancelled", errorMessage: `cancelled: ${run.exitReason}`, updatedAt: /* @__PURE__ */ new Date() } }
|
|
15585
|
+
);
|
|
15586
|
+
return;
|
|
15587
|
+
}
|
|
15588
|
+
let renderCtx;
|
|
15589
|
+
let rendered;
|
|
15590
|
+
try {
|
|
15591
|
+
const resolved = await resolveVars(ctx.varsAdapter, contact, {
|
|
15592
|
+
reason: "send",
|
|
15593
|
+
templateSlug: template.slug,
|
|
15594
|
+
flowSlug: run?.flowSlug,
|
|
15595
|
+
eventName: run?.triggerEvent?.name,
|
|
15596
|
+
eventProperties: run?.triggerEvent?.properties
|
|
15597
|
+
});
|
|
15598
|
+
renderCtx = buildRenderContext(contact, run, send.vars ?? {}, ctx, resolved);
|
|
15599
|
+
rendered = await renderTemplate(template, renderCtx, { helpers: ctx.handlebarsHelpers });
|
|
15600
|
+
} catch (err) {
|
|
15601
|
+
await markFailed(send._id, `render error: ${String(err?.message ?? err)}`, ctx);
|
|
15602
|
+
throw err;
|
|
15603
|
+
}
|
|
15424
15604
|
const tracking = applyTracking(rendered.html, {
|
|
15425
15605
|
sendId: String(send._id),
|
|
15426
15606
|
publicUrl: ctx.config.publicUrl,
|
|
@@ -15496,7 +15676,7 @@ function pickProviderName(stepOverride, tpl, ctx) {
|
|
|
15496
15676
|
}
|
|
15497
15677
|
return ctx.config.defaultProvider;
|
|
15498
15678
|
}
|
|
15499
|
-
function buildRenderContext(contact, run, vars, ctx) {
|
|
15679
|
+
function buildRenderContext(contact, run, vars, ctx, resolved = {}) {
|
|
15500
15680
|
const scope = "marketing";
|
|
15501
15681
|
const expiresAt = new Date(Date.now() + ctx.config.unsubscribeTokenLifetimeDays * 24 * 60 * 60 * 1e3);
|
|
15502
15682
|
const token = signUnsubscribeToken(
|
|
@@ -15505,8 +15685,10 @@ function buildRenderContext(contact, run, vars, ctx) {
|
|
|
15505
15685
|
);
|
|
15506
15686
|
const unsubscribeUrl = `${ctx.config.publicUrl}/m/unsub/${token}`;
|
|
15507
15687
|
return {
|
|
15688
|
+
...resolved,
|
|
15508
15689
|
contact,
|
|
15509
15690
|
vars,
|
|
15691
|
+
event: run?.triggerEvent?.properties ?? {},
|
|
15510
15692
|
unsubscribeUrl,
|
|
15511
15693
|
senderAddress: ctx.config.senderAddress
|
|
15512
15694
|
};
|
|
@@ -15591,8 +15773,15 @@ async function processOneRunStep(runId, ctx) {
|
|
|
15591
15773
|
return handleCondition(run, step, contact, ctx);
|
|
15592
15774
|
case "branch":
|
|
15593
15775
|
return handleBranch(run, step, contact, ctx);
|
|
15594
|
-
case "send":
|
|
15776
|
+
case "send": {
|
|
15777
|
+
if (step.delivery) {
|
|
15778
|
+
const deliverAt = computeDeliveryTime(/* @__PURE__ */ new Date(), step.delivery, contact.timezone);
|
|
15779
|
+
if (deliverAt.getTime() > Date.now() + 3e4) {
|
|
15780
|
+
return deferSendForWindow(run, deliverAt, ctx);
|
|
15781
|
+
}
|
|
15782
|
+
}
|
|
15595
15783
|
return handleSend(run, step, contact, flow, ctx);
|
|
15784
|
+
}
|
|
15596
15785
|
case "tag":
|
|
15597
15786
|
return handleTag(run, step, ctx);
|
|
15598
15787
|
case "fire_event":
|
|
@@ -15743,6 +15932,34 @@ async function handleWebhookStep(run, step, ctx) {
|
|
|
15743
15932
|
}
|
|
15744
15933
|
}
|
|
15745
15934
|
}
|
|
15935
|
+
async function deferSendForWindow(run, deliverAt, ctx) {
|
|
15936
|
+
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
15937
|
+
// Only write once per deferral — if nextActionAt already points at (or
|
|
15938
|
+
// past) the slot, another worker/tick got here first.
|
|
15939
|
+
{ _id: run._id, currentStepIndex: run.currentStepIndex, nextActionAt: { $lt: deliverAt } },
|
|
15940
|
+
{
|
|
15941
|
+
$set: { nextActionAt: deliverAt, updatedAt: /* @__PURE__ */ new Date() },
|
|
15942
|
+
$push: {
|
|
15943
|
+
history: {
|
|
15944
|
+
stepIndex: run.currentStepIndex,
|
|
15945
|
+
action: "send_deferred",
|
|
15946
|
+
at: /* @__PURE__ */ new Date(),
|
|
15947
|
+
details: { until: deliverAt }
|
|
15948
|
+
}
|
|
15949
|
+
}
|
|
15950
|
+
},
|
|
15951
|
+
{ returnDocument: "after" }
|
|
15952
|
+
);
|
|
15953
|
+
if (!updated) return;
|
|
15954
|
+
await ctx.queues.advance.add(
|
|
15955
|
+
"advance",
|
|
15956
|
+
{ flowRunId: String(run._id) },
|
|
15957
|
+
{
|
|
15958
|
+
delay: Math.max(0, deliverAt.getTime() - Date.now()),
|
|
15959
|
+
jobId: `advance:${run._id}:${run.currentStepIndex}:window:${deliverAt.getTime()}`
|
|
15960
|
+
}
|
|
15961
|
+
);
|
|
15962
|
+
}
|
|
15746
15963
|
async function advanceStep(run, ctx, log, opts = {}) {
|
|
15747
15964
|
const stepInc = opts.stepInc ?? 1;
|
|
15748
15965
|
const updated = await ctx.collections.flowRuns.findOneAndUpdate(
|
|
@@ -16995,6 +17212,7 @@ var Mailer = class _Mailer {
|
|
|
16995
17212
|
db: this.db,
|
|
16996
17213
|
collections: this.collections,
|
|
16997
17214
|
adapter: this.adapter,
|
|
17215
|
+
varsAdapter: this.config.varsAdapter,
|
|
16998
17216
|
providers: this.providers,
|
|
16999
17217
|
queues: this.queues,
|
|
17000
17218
|
config: this.config,
|
|
@@ -17074,6 +17292,10 @@ var Mailer = class _Mailer {
|
|
|
17074
17292
|
if (!config.providers[config.defaultProvider]) {
|
|
17075
17293
|
throw new Error(`defaultProvider "${config.defaultProvider}" not in providers map`);
|
|
17076
17294
|
}
|
|
17295
|
+
if (config.varsAdapter) {
|
|
17296
|
+
const { assertNoReservedVarKeys: assertNoReservedVarKeys2 } = await Promise.resolve().then(() => (init_vars(), vars_exports));
|
|
17297
|
+
assertNoReservedVarKeys2(config.varsAdapter);
|
|
17298
|
+
}
|
|
17077
17299
|
const collections = getCollections(config.db, config.collectionPrefix);
|
|
17078
17300
|
await ensureIndexes(config.db, config.collectionPrefix);
|
|
17079
17301
|
const queueDriver = await createQueueDriver(config.queue, config.db);
|
|
@@ -17289,6 +17511,72 @@ var Mailer = class _Mailer {
|
|
|
17289
17511
|
await this.collections.contactTags.deleteOne({ externalId: parsed.externalId, tag: parsed.tag });
|
|
17290
17512
|
}
|
|
17291
17513
|
}
|
|
17514
|
+
// -------------------------------------------------------------------------
|
|
17515
|
+
// Flow abort
|
|
17516
|
+
// -------------------------------------------------------------------------
|
|
17517
|
+
/**
|
|
17518
|
+
* Abort every active run of one flow for a contact, immediately. Runs parked
|
|
17519
|
+
* in a `wait` exit too — their delayed wake-up jobs find the run exited and
|
|
17520
|
+
* no-op. Also cancels any of the flow's emails still sitting in the send
|
|
17521
|
+
* queue for this contact (queued or awaiting retry), so an abort means no
|
|
17522
|
+
* further mail, not just no further steps.
|
|
17523
|
+
*
|
|
17524
|
+
* No-op (returns zero counts) when nothing is active. Safe to call from the
|
|
17525
|
+
* same handler that processes the business event ("user upgraded").
|
|
17526
|
+
*/
|
|
17527
|
+
async abortFlow(flowSlug, externalId, opts = {}) {
|
|
17528
|
+
const parsed = abortFlowInputSchema.parse({ flowSlug, externalId, reason: opts.reason });
|
|
17529
|
+
const flow = await this.collections.flows.findOne(
|
|
17530
|
+
{ slug: parsed.flowSlug },
|
|
17531
|
+
{ projection: { _id: 1 } }
|
|
17532
|
+
);
|
|
17533
|
+
if (!flow) throw new Error(`abortFlow: unknown flow slug "${parsed.flowSlug}"`);
|
|
17534
|
+
const result = await this.abortActiveRuns(
|
|
17535
|
+
{ externalId: parsed.externalId, flowId: flow._id },
|
|
17536
|
+
parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
|
|
17537
|
+
);
|
|
17538
|
+
if (result.abortedRuns > 0 || result.cancelledSends > 0) {
|
|
17539
|
+
await this.audit({
|
|
17540
|
+
actor: "host",
|
|
17541
|
+
action: "flow.abort",
|
|
17542
|
+
resource: { collection: "mailer_flow_runs", slug: parsed.flowSlug },
|
|
17543
|
+
diffSummary: `abortFlow slug=${parsed.flowSlug} externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
|
|
17544
|
+
});
|
|
17545
|
+
}
|
|
17546
|
+
return result;
|
|
17547
|
+
}
|
|
17548
|
+
/**
|
|
17549
|
+
* Abort every active flow run for a contact across all flows. Same semantics
|
|
17550
|
+
* as `abortFlow` — for "stop everything" events (account deleted, churned).
|
|
17551
|
+
*/
|
|
17552
|
+
async abortAllFlows(externalId, opts = {}) {
|
|
17553
|
+
const parsed = abortAllFlowsInputSchema.parse({ externalId, reason: opts.reason });
|
|
17554
|
+
const result = await this.abortActiveRuns(
|
|
17555
|
+
{ externalId: parsed.externalId },
|
|
17556
|
+
parsed.reason ? `aborted_by_host:${parsed.reason}` : "aborted_by_host"
|
|
17557
|
+
);
|
|
17558
|
+
if (result.abortedRuns > 0 || result.cancelledSends > 0) {
|
|
17559
|
+
await this.audit({
|
|
17560
|
+
actor: "host",
|
|
17561
|
+
action: "flow.abort_all",
|
|
17562
|
+
resource: { collection: "mailer_flow_runs" },
|
|
17563
|
+
diffSummary: `abortAllFlows externalId=${parsed.externalId} runs=${result.abortedRuns} sends=${result.cancelledSends}${parsed.reason ? ` reason=${parsed.reason}` : ""}`
|
|
17564
|
+
});
|
|
17565
|
+
}
|
|
17566
|
+
return result;
|
|
17567
|
+
}
|
|
17568
|
+
async abortActiveRuns(filter, exitReason) {
|
|
17569
|
+
const runs = await this.collections.flowRuns.find({ ...filter, status: "active" }).toArray();
|
|
17570
|
+
if (runs.length === 0) return { abortedRuns: 0, cancelledSends: 0 };
|
|
17571
|
+
for (const run of runs) {
|
|
17572
|
+
await exitFlowRun(run, exitReason, this.runnerContext);
|
|
17573
|
+
}
|
|
17574
|
+
const cancelled = await this.collections.sends.updateMany(
|
|
17575
|
+
{ flowRunId: { $in: runs.map((r) => r._id) }, status: { $in: ["queued", "failed"] } },
|
|
17576
|
+
{ $set: { status: "cancelled", errorMessage: `cancelled: ${exitReason}`, updatedAt: /* @__PURE__ */ new Date() } }
|
|
17577
|
+
);
|
|
17578
|
+
return { abortedRuns: runs.length, cancelledSends: cancelled.modifiedCount };
|
|
17579
|
+
}
|
|
17292
17580
|
/**
|
|
17293
17581
|
* GDPR right-to-erasure. Hard-deletes the contact's PII and leaves a hashed
|
|
17294
17582
|
* suppression row to block re-import. INVARIANT 9.
|