mailery 0.2.0 → 0.2.2
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-csgOs39s.js +41 -0
- package/dist/admin/spa/index-csgOs39s.js.map +1 -0
- package/dist/admin/spa/index.html +1 -1
- package/dist/admin/spa/{template-editor-DUZfzI5F.js → template-editor-BuYSO1FP.js} +2 -2
- package/dist/admin/spa/{template-editor-DUZfzI5F.js.map → template-editor-BuYSO1FP.js.map} +1 -1
- package/dist/index.cjs +316 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +316 -3
- package/dist/index.js.map +1 -1
- package/dist/{null-CWw3Gpbl.d.cts → null-DaisDvB_.d.cts} +58 -16
- package/dist/{null-CWw3Gpbl.d.ts → null-DaisDvB_.d.ts} +58 -16
- package/dist/testing.cjs +19 -0
- 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 +19 -0
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/admin/spa/index-DlhleV6M.js +0 -41
- package/dist/admin/spa/index-DlhleV6M.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -2202,6 +2202,25 @@ async function promoteSoftBounces(ctx) {
|
|
|
2202
2202
|
// src/server/runner/tick.ts
|
|
2203
2203
|
var STRANDED_SEND_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2204
2204
|
async function runTick(ctx) {
|
|
2205
|
+
await ctx.collections.health.updateOne(
|
|
2206
|
+
{ _id: "singleton" },
|
|
2207
|
+
{
|
|
2208
|
+
$set: { updatedAt: /* @__PURE__ */ new Date() },
|
|
2209
|
+
$setOnInsert: {
|
|
2210
|
+
_id: "singleton",
|
|
2211
|
+
windowStartedAt: /* @__PURE__ */ new Date(),
|
|
2212
|
+
windowDurationMs: ctx.config.circuitBreaker.windowMinutes * 60 * 1e3,
|
|
2213
|
+
status: "healthy",
|
|
2214
|
+
trippedAt: null,
|
|
2215
|
+
trippedReason: null,
|
|
2216
|
+
manuallyResumedAt: null,
|
|
2217
|
+
counters: { sent: 0, delivered: 0, bounced: 0, hardBounced: 0, softBounced: 0, complained: 0, failedToSend: 0 },
|
|
2218
|
+
rates: { bounceRate: 0, hardBounceRate: 0, complaintRate: 0, failureRate: 0 }
|
|
2219
|
+
}
|
|
2220
|
+
},
|
|
2221
|
+
{ upsert: true }
|
|
2222
|
+
).catch(() => {
|
|
2223
|
+
});
|
|
2205
2224
|
await processNewlyFiredEventTriggers(ctx).catch((err) => {
|
|
2206
2225
|
console.error("mailery: triggers scan failed", err);
|
|
2207
2226
|
});
|
|
@@ -2924,6 +2943,43 @@ var Mailer = class _Mailer {
|
|
|
2924
2943
|
}
|
|
2925
2944
|
};
|
|
2926
2945
|
|
|
2946
|
+
// src/server/templates/sender-domain.ts
|
|
2947
|
+
function validateSenderDomain(fromEmail, templateKind, registry) {
|
|
2948
|
+
if (!registry || Object.keys(registry).length === 0) return { ok: true };
|
|
2949
|
+
const domain = extractDomain(fromEmail);
|
|
2950
|
+
if (!domain) {
|
|
2951
|
+
return {
|
|
2952
|
+
ok: false,
|
|
2953
|
+
code: "invalid_email",
|
|
2954
|
+
reason: `invalid fromEmail "${fromEmail}" \u2014 expected "name@domain"`
|
|
2955
|
+
};
|
|
2956
|
+
}
|
|
2957
|
+
const entry = registry[domain];
|
|
2958
|
+
if (!entry) {
|
|
2959
|
+
const known = Object.keys(registry).join(", ");
|
|
2960
|
+
return {
|
|
2961
|
+
ok: false,
|
|
2962
|
+
code: "unregistered_domain",
|
|
2963
|
+
reason: `sender domain "${domain}" is not declared in senderDomains (allowed: ${known})`
|
|
2964
|
+
};
|
|
2965
|
+
}
|
|
2966
|
+
if (entry.kind === "both") return { ok: true };
|
|
2967
|
+
if (entry.kind !== templateKind) {
|
|
2968
|
+
return {
|
|
2969
|
+
ok: false,
|
|
2970
|
+
code: "wrong_kind",
|
|
2971
|
+
reason: `sender domain "${domain}" is configured for ${entry.kind} email, but this template's kind is ${templateKind}`
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
return { ok: true };
|
|
2975
|
+
}
|
|
2976
|
+
function extractDomain(email) {
|
|
2977
|
+
if (typeof email !== "string") return null;
|
|
2978
|
+
const at = email.lastIndexOf("@");
|
|
2979
|
+
if (at <= 0 || at === email.length - 1) return null;
|
|
2980
|
+
return email.slice(at + 1).toLowerCase().trim();
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2927
2983
|
// src/server/index.ts
|
|
2928
2984
|
init_mongo();
|
|
2929
2985
|
|
|
@@ -2953,6 +3009,227 @@ var NullProvider = class {
|
|
|
2953
3009
|
|
|
2954
3010
|
// src/server/index.ts
|
|
2955
3011
|
init_sendgrid();
|
|
3012
|
+
|
|
3013
|
+
// src/server/api/setup-status.ts
|
|
3014
|
+
async function runSetupChecks(mailer) {
|
|
3015
|
+
const checks = [];
|
|
3016
|
+
checks.push(await checkMongo(mailer));
|
|
3017
|
+
checks.push(await checkQueue(mailer));
|
|
3018
|
+
if (mailer.config.queue.driver !== "noop" && !mailer.config.workerless) {
|
|
3019
|
+
checks.push(await checkWorkersHeartbeat(mailer));
|
|
3020
|
+
}
|
|
3021
|
+
checks.push(await checkCircuitBreaker(mailer));
|
|
3022
|
+
checks.push(...checkFromDefaultsAgainstRegistry(mailer));
|
|
3023
|
+
checks.push(...await checkPublishedTemplates(mailer));
|
|
3024
|
+
checks.push(await checkPostalAddress(mailer));
|
|
3025
|
+
checks.push(await checkDoiTemplate(mailer));
|
|
3026
|
+
const overall = checks.some((c) => c.severity === "error") ? "error" : checks.some((c) => c.severity === "warn") ? "warn" : "ok";
|
|
3027
|
+
return {
|
|
3028
|
+
overall,
|
|
3029
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3030
|
+
checks
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
async function checkMongo(mailer) {
|
|
3034
|
+
try {
|
|
3035
|
+
await mailer.db.admin().ping();
|
|
3036
|
+
return { name: "mongo", label: "MongoDB connection", severity: "ok", message: "reachable" };
|
|
3037
|
+
} catch (err) {
|
|
3038
|
+
return {
|
|
3039
|
+
name: "mongo",
|
|
3040
|
+
label: "MongoDB connection",
|
|
3041
|
+
severity: "error",
|
|
3042
|
+
message: `MongoDB ping failed: ${err?.message ?? err}`,
|
|
3043
|
+
hint: "Mailery is configured against an unreachable Mongo. Sends, flow advancement, and admin reads will all fail."
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
async function checkQueue(mailer) {
|
|
3048
|
+
const driver = mailer.config.queue.driver;
|
|
3049
|
+
if (driver === "noop") {
|
|
3050
|
+
return {
|
|
3051
|
+
name: "queue",
|
|
3052
|
+
label: "Queue driver",
|
|
3053
|
+
severity: "ok",
|
|
3054
|
+
message: "driver: noop (synchronous-only mode)"
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
try {
|
|
3058
|
+
await mailer.queues.send.getWaitingCount();
|
|
3059
|
+
return { name: "queue", label: "Queue driver", severity: "ok", message: `driver: ${driver}` };
|
|
3060
|
+
} catch (err) {
|
|
3061
|
+
return {
|
|
3062
|
+
name: "queue",
|
|
3063
|
+
label: "Queue driver",
|
|
3064
|
+
severity: "error",
|
|
3065
|
+
message: `${driver} queue is not responding: ${err?.message ?? err}`,
|
|
3066
|
+
hint: driver === "bull" ? "Check Redis connectivity (queue.redis.url)." : "Check the @hokify/agenda + Mongo connection."
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
async function checkWorkersHeartbeat(mailer) {
|
|
3071
|
+
const h = await mailer.collections.health.findOne({ _id: "singleton" });
|
|
3072
|
+
const tickIntervalMs = mailer.config.tickIntervalSeconds * 1e3;
|
|
3073
|
+
const staleAfterMs = Math.max(tickIntervalMs * 3, 3e4);
|
|
3074
|
+
if (!h) {
|
|
3075
|
+
return {
|
|
3076
|
+
name: "workers_heartbeat",
|
|
3077
|
+
label: "Background workers",
|
|
3078
|
+
severity: "warn",
|
|
3079
|
+
message: "no tick has run yet",
|
|
3080
|
+
hint: "If your separate worker process is started, the heartbeat will appear within one tick interval. If you forgot to run `mailer.startWorkers()`, sends will sit queued indefinitely."
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
const ageMs = Date.now() - new Date(h.updatedAt).getTime();
|
|
3084
|
+
if (ageMs > staleAfterMs) {
|
|
3085
|
+
return {
|
|
3086
|
+
name: "workers_heartbeat",
|
|
3087
|
+
label: "Background workers",
|
|
3088
|
+
severity: "error",
|
|
3089
|
+
message: `last tick ${humanDuration(ageMs)} ago (expected within ${humanDuration(tickIntervalMs)})`,
|
|
3090
|
+
hint: "Workers appear to be down. Sends and flow advancement are halted. Restart your worker process (`mailer.startWorkers()`)."
|
|
3091
|
+
};
|
|
3092
|
+
}
|
|
3093
|
+
return {
|
|
3094
|
+
name: "workers_heartbeat",
|
|
3095
|
+
label: "Background workers",
|
|
3096
|
+
severity: "ok",
|
|
3097
|
+
message: `last tick ${humanDuration(ageMs)} ago`
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
async function checkCircuitBreaker(mailer) {
|
|
3101
|
+
const h = await mailer.collections.health.findOne({ _id: "singleton" });
|
|
3102
|
+
if (!h || h.status === "healthy") {
|
|
3103
|
+
return { name: "circuit_breaker", label: "Circuit breaker", severity: "ok", message: "healthy" };
|
|
3104
|
+
}
|
|
3105
|
+
if (h.status === "degraded") {
|
|
3106
|
+
return {
|
|
3107
|
+
name: "circuit_breaker",
|
|
3108
|
+
label: "Circuit breaker",
|
|
3109
|
+
severity: "warn",
|
|
3110
|
+
message: "degraded (high failure rate)",
|
|
3111
|
+
hint: "Marketing sends still flow but failure rate is above the degraded threshold. Investigate provider errors before they escalate to tripped."
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
return {
|
|
3115
|
+
name: "circuit_breaker",
|
|
3116
|
+
label: "Circuit breaker",
|
|
3117
|
+
severity: "error",
|
|
3118
|
+
message: `tripped: ${h.trippedReason ?? "unknown reason"}`,
|
|
3119
|
+
hint: "Marketing sends are held. Investigate the underlying bounce / complaint cause, then POST /api/health/resume."
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
function checkFromDefaultsAgainstRegistry(mailer) {
|
|
3123
|
+
const registry = mailer.config.senderDomains;
|
|
3124
|
+
if (!registry || Object.keys(registry).length === 0) return [];
|
|
3125
|
+
const out = [];
|
|
3126
|
+
const from = mailer.config.fromDefaults?.email;
|
|
3127
|
+
const tx = mailer.config.transactionalFromDefaults?.email;
|
|
3128
|
+
if (from) {
|
|
3129
|
+
const r = validateSenderDomain(from, "marketing", registry);
|
|
3130
|
+
if (!r.ok) {
|
|
3131
|
+
out.push({
|
|
3132
|
+
name: "from_defaults_marketing",
|
|
3133
|
+
label: "fromDefaults vs senderDomains",
|
|
3134
|
+
severity: "error",
|
|
3135
|
+
message: r.reason,
|
|
3136
|
+
hint: "New marketing templates that fall back to fromDefaults will fail to publish."
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
if (tx) {
|
|
3141
|
+
const r = validateSenderDomain(tx, "transactional", registry);
|
|
3142
|
+
if (!r.ok) {
|
|
3143
|
+
out.push({
|
|
3144
|
+
name: "transactional_from_defaults",
|
|
3145
|
+
label: "transactionalFromDefaults vs senderDomains",
|
|
3146
|
+
severity: "error",
|
|
3147
|
+
message: r.reason,
|
|
3148
|
+
hint: "New transactional templates that fall back to transactionalFromDefaults will fail to publish."
|
|
3149
|
+
});
|
|
3150
|
+
}
|
|
3151
|
+
} else if (from) {
|
|
3152
|
+
const r = validateSenderDomain(from, "transactional", registry);
|
|
3153
|
+
if (!r.ok) {
|
|
3154
|
+
out.push({
|
|
3155
|
+
name: "transactional_fallback",
|
|
3156
|
+
label: "Transactional fallback",
|
|
3157
|
+
severity: "warn",
|
|
3158
|
+
message: `transactionalFromDefaults is unset and fromDefaults (${from}) is invalid for transactional templates`,
|
|
3159
|
+
hint: 'Set transactionalFromDefaults to a transactional-kind domain, or set senderDomains entry for the existing one to "both".'
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
return out;
|
|
3164
|
+
}
|
|
3165
|
+
async function checkPublishedTemplates(mailer) {
|
|
3166
|
+
const registry = mailer.config.senderDomains;
|
|
3167
|
+
if (!registry || Object.keys(registry).length === 0) return [];
|
|
3168
|
+
const published = await mailer.collections.templates.find({ publishedAt: { $ne: null } }, { projection: { slug: 1, kind: 1, fromEmail: 1 } }).toArray();
|
|
3169
|
+
const broken = [];
|
|
3170
|
+
for (const tpl of published) {
|
|
3171
|
+
const r = validateSenderDomain(tpl.fromEmail, tpl.kind, registry);
|
|
3172
|
+
if (!r.ok) broken.push({ slug: tpl.slug, reason: r.reason });
|
|
3173
|
+
}
|
|
3174
|
+
if (broken.length === 0) return [];
|
|
3175
|
+
const list = broken.slice(0, 5).map((b) => `${b.slug} (${b.reason})`).join("; ");
|
|
3176
|
+
const more = broken.length > 5 ? ` \u2026and ${broken.length - 5} more` : "";
|
|
3177
|
+
return [
|
|
3178
|
+
{
|
|
3179
|
+
name: "published_template_domains",
|
|
3180
|
+
label: "Published templates",
|
|
3181
|
+
severity: "error",
|
|
3182
|
+
message: `${broken.length} published template${broken.length === 1 ? "" : "s"} use a fromEmail that no longer matches senderDomains: ${list}${more}`,
|
|
3183
|
+
hint: "These templates will still send with their stored fromEmail until re-published. Edit each template and republish to surface the validation, or update senderDomains."
|
|
3184
|
+
}
|
|
3185
|
+
];
|
|
3186
|
+
}
|
|
3187
|
+
async function checkPostalAddress(mailer) {
|
|
3188
|
+
if (mailer.config.senderAddress) {
|
|
3189
|
+
return { name: "postal_address", label: "CAN-SPAM postal address", severity: "ok", message: "set" };
|
|
3190
|
+
}
|
|
3191
|
+
const marketingCount = await mailer.collections.templates.countDocuments({
|
|
3192
|
+
kind: "marketing",
|
|
3193
|
+
publishedAt: { $ne: null }
|
|
3194
|
+
});
|
|
3195
|
+
if (marketingCount === 0) {
|
|
3196
|
+
return { name: "postal_address", label: "CAN-SPAM postal address", severity: "ok", message: "no published marketing templates yet" };
|
|
3197
|
+
}
|
|
3198
|
+
return {
|
|
3199
|
+
name: "postal_address",
|
|
3200
|
+
label: "CAN-SPAM postal address",
|
|
3201
|
+
severity: "warn",
|
|
3202
|
+
message: `${marketingCount} published marketing template${marketingCount === 1 ? "" : "s"} but senderAddress is unset`,
|
|
3203
|
+
hint: "CAN-SPAM requires a postal address in marketing emails. Set `senderAddress` in your Mailer config and reference it via `{{senderAddress}}` in your templates."
|
|
3204
|
+
};
|
|
3205
|
+
}
|
|
3206
|
+
async function checkDoiTemplate(mailer) {
|
|
3207
|
+
if (!mailer.config.requireDoubleOptIn) {
|
|
3208
|
+
return { name: "doi_template", label: "DOI template", severity: "ok", message: "DOI not required" };
|
|
3209
|
+
}
|
|
3210
|
+
const tpl = await mailer.collections.templates.findOne({
|
|
3211
|
+
slug: mailer.config.doiTemplateSlug,
|
|
3212
|
+
publishedAt: { $ne: null }
|
|
3213
|
+
});
|
|
3214
|
+
if (tpl) {
|
|
3215
|
+
return { name: "doi_template", label: "DOI template", severity: "ok", message: `template "${tpl.slug}" published` };
|
|
3216
|
+
}
|
|
3217
|
+
return {
|
|
3218
|
+
name: "doi_template",
|
|
3219
|
+
label: "DOI template",
|
|
3220
|
+
severity: "error",
|
|
3221
|
+
message: `requireDoubleOptIn is true but no published template with slug "${mailer.config.doiTemplateSlug}"`,
|
|
3222
|
+
hint: "New subscriptions will silently fail to send confirmation emails. Create and publish a template with this slug, or unset requireDoubleOptIn."
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
function humanDuration(ms) {
|
|
3226
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
3227
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
3228
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
|
|
3229
|
+
return `${Math.round(ms / 36e5)}h`;
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
// src/server/api/admin.ts
|
|
2956
3233
|
var __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
2957
3234
|
var __dirname$1 = path__default.default.dirname(__filename$1);
|
|
2958
3235
|
function defaultSpaDir() {
|
|
@@ -3174,6 +3451,13 @@ function apiRouter(mailer) {
|
|
|
3174
3451
|
res.json(rows);
|
|
3175
3452
|
})
|
|
3176
3453
|
);
|
|
3454
|
+
r.get(
|
|
3455
|
+
"/setup-status",
|
|
3456
|
+
asyncHandler(async (_req, res) => {
|
|
3457
|
+
const status = await runSetupChecks(mailer);
|
|
3458
|
+
res.json(status);
|
|
3459
|
+
})
|
|
3460
|
+
);
|
|
3177
3461
|
r.get(
|
|
3178
3462
|
"/health",
|
|
3179
3463
|
asyncHandler(async (_req, res) => {
|
|
@@ -3347,6 +3631,15 @@ function apiRouter(mailer) {
|
|
|
3347
3631
|
if (kind !== "marketing" && kind !== "transactional") {
|
|
3348
3632
|
return res.status(400).json({ error: "validation_failed", message: "kind must be marketing or transactional" });
|
|
3349
3633
|
}
|
|
3634
|
+
const resolvedFromEmail = fromEmail ?? (kind === "transactional" ? mailer.config.transactionalFromDefaults?.email : void 0) ?? mailer.config.fromDefaults?.email ?? "noreply@example.com";
|
|
3635
|
+
const senderCheck = validateSenderDomain(resolvedFromEmail, kind, mailer.config.senderDomains);
|
|
3636
|
+
if (!senderCheck.ok) {
|
|
3637
|
+
return res.status(400).json({
|
|
3638
|
+
error: "sender_domain_invalid",
|
|
3639
|
+
code: senderCheck.code,
|
|
3640
|
+
message: senderCheck.reason
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3350
3643
|
const now = /* @__PURE__ */ new Date();
|
|
3351
3644
|
try {
|
|
3352
3645
|
await c.templates.insertOne({
|
|
@@ -3354,8 +3647,8 @@ function apiRouter(mailer) {
|
|
|
3354
3647
|
name,
|
|
3355
3648
|
description: "",
|
|
3356
3649
|
kind,
|
|
3357
|
-
fromName: fromName ?? mailer.config.fromDefaults?.name ?? "Mailery",
|
|
3358
|
-
fromEmail:
|
|
3650
|
+
fromName: fromName ?? (kind === "transactional" ? mailer.config.transactionalFromDefaults?.name : void 0) ?? mailer.config.fromDefaults?.name ?? "Mailery",
|
|
3651
|
+
fromEmail: resolvedFromEmail,
|
|
3359
3652
|
replyTo: null,
|
|
3360
3653
|
providerOverride: null,
|
|
3361
3654
|
subject: subject ?? `Untitled \u2014 ${name}`,
|
|
@@ -3413,6 +3706,18 @@ function apiRouter(mailer) {
|
|
|
3413
3706
|
if (typeof fromEmail === "string") set.fromEmail = fromEmail;
|
|
3414
3707
|
if (typeof replyTo === "string" || replyTo === null) set.replyTo = replyTo;
|
|
3415
3708
|
if (kind === "marketing" || kind === "transactional") set.kind = kind;
|
|
3709
|
+
if (typeof fromEmail === "string" || kind === "marketing" || kind === "transactional") {
|
|
3710
|
+
const resultingKind = set.kind ?? tpl.kind;
|
|
3711
|
+
const resultingFromEmail = set.fromEmail ?? tpl.fromEmail;
|
|
3712
|
+
const senderCheck = validateSenderDomain(resultingFromEmail, resultingKind, mailer.config.senderDomains);
|
|
3713
|
+
if (!senderCheck.ok) {
|
|
3714
|
+
return res.status(400).json({
|
|
3715
|
+
error: "sender_domain_invalid",
|
|
3716
|
+
code: senderCheck.code,
|
|
3717
|
+
message: senderCheck.reason
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3416
3721
|
if (typeof trackOpens === "boolean") set.trackOpens = trackOpens;
|
|
3417
3722
|
if (typeof trackClicks === "boolean") set.trackClicks = trackClicks;
|
|
3418
3723
|
await c.templates.updateOne({ _id: tpl._id }, { $set: set });
|
|
@@ -3431,6 +3736,14 @@ function apiRouter(mailer) {
|
|
|
3431
3736
|
if (!tpl) return res.status(404).json({ error: "not_found" });
|
|
3432
3737
|
const draft = tpl.draft;
|
|
3433
3738
|
if (!draft) return res.status(400).json({ error: "no_draft" });
|
|
3739
|
+
const senderCheck = validateSenderDomain(tpl.fromEmail, tpl.kind, mailer.config.senderDomains);
|
|
3740
|
+
if (!senderCheck.ok) {
|
|
3741
|
+
return res.status(400).json({
|
|
3742
|
+
error: "sender_domain_invalid",
|
|
3743
|
+
code: senderCheck.code,
|
|
3744
|
+
message: senderCheck.reason
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3434
3747
|
let compiled;
|
|
3435
3748
|
if (draft.editorJson) {
|
|
3436
3749
|
compiled = await compileMailyTemplate(draft.editorJson);
|
|
@@ -4069,6 +4382,7 @@ exports.runTick = runTick;
|
|
|
4069
4382
|
exports.sha256Hex = sha256Hex;
|
|
4070
4383
|
exports.signUnsubscribeToken = signUnsubscribeToken;
|
|
4071
4384
|
exports.sweepStrandedFlowRuns = sweepStrandedFlowRuns;
|
|
4385
|
+
exports.validateSenderDomain = validateSenderDomain;
|
|
4072
4386
|
exports.verifyUnsubscribeToken = verifyUnsubscribeToken;
|
|
4073
4387
|
//# sourceMappingURL=index.cjs.map
|
|
4074
4388
|
//# sourceMappingURL=index.cjs.map
|