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.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as Mailer, T as TemplateDoc, d as SuppressionScope, F as FlowStep, e as SegmentFilter, P as Predicate } from './null-
|
|
2
|
-
export { f as AuditLogDoc, B as BroadcastDoc, g as BroadcastStatus, h as CircuitBreakerThresholds, i as Collections, j as ContactTagDoc, E as EventDoc, k as FlowDoc, l as FlowGoal, m as FlowRunDoc, n as FlowRunStatus, o as FlowVersionDoc, H as HealthDoc, p as HealthStatus, L as LeadDoc, q as MailerConfig, r as NullProvider, O as OutboxDoc, s as RedisOptions, t as SegmentDefinition, u as SendDoc, v as SendStatus, w as
|
|
1
|
+
import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as Mailer, T as TemplateDoc, d as SuppressionScope, F as FlowStep, e as SegmentFilter, P as Predicate } from './null-DaisDvB_.cjs';
|
|
2
|
+
export { f as AuditLogDoc, B as BroadcastDoc, g as BroadcastStatus, h as CircuitBreakerThresholds, i as Collections, j as ContactTagDoc, E as EventDoc, k as FlowDoc, l as FlowGoal, m as FlowRunDoc, n as FlowRunStatus, o as FlowVersionDoc, H as HealthDoc, p as HealthStatus, L as LeadDoc, q as MailerConfig, r as NullProvider, O as OutboxDoc, s as RedisOptions, t as SegmentDefinition, u as SendDoc, v as SendStatus, w as SenderDomainConfig, x as SenderDomainRegistry, y as SenderDomainValidation, z as SubscriptionDoc, D as SubscriptionStatus, G as SuppressionDoc, I as SuppressionReason, J as TemplateKind, K as TemplateVersionDoc, W as WebhookEventDoc, Q as ensureIndexes, U as getCollections, V as validateSenderDomain } from './null-DaisDvB_.cjs';
|
|
3
3
|
import { ObjectId, Db, Filter } from 'mongodb';
|
|
4
4
|
import { Request, Router } from 'express';
|
|
5
5
|
import Handlebars from 'handlebars';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as Mailer, T as TemplateDoc, d as SuppressionScope, F as FlowStep, e as SegmentFilter, P as Predicate } from './null-
|
|
2
|
-
export { f as AuditLogDoc, B as BroadcastDoc, g as BroadcastStatus, h as CircuitBreakerThresholds, i as Collections, j as ContactTagDoc, E as EventDoc, k as FlowDoc, l as FlowGoal, m as FlowRunDoc, n as FlowRunStatus, o as FlowVersionDoc, H as HealthDoc, p as HealthStatus, L as LeadDoc, q as MailerConfig, r as NullProvider, O as OutboxDoc, s as RedisOptions, t as SegmentDefinition, u as SendDoc, v as SendStatus, w as
|
|
1
|
+
import { R as RunnerContext, N as NormalizedEvent, C as ContactAdapter, a as Contact, A as AdapterFilter, M as MailProvider, S as SendArgs, b as SendResult, c as Mailer, T as TemplateDoc, d as SuppressionScope, F as FlowStep, e as SegmentFilter, P as Predicate } from './null-DaisDvB_.js';
|
|
2
|
+
export { f as AuditLogDoc, B as BroadcastDoc, g as BroadcastStatus, h as CircuitBreakerThresholds, i as Collections, j as ContactTagDoc, E as EventDoc, k as FlowDoc, l as FlowGoal, m as FlowRunDoc, n as FlowRunStatus, o as FlowVersionDoc, H as HealthDoc, p as HealthStatus, L as LeadDoc, q as MailerConfig, r as NullProvider, O as OutboxDoc, s as RedisOptions, t as SegmentDefinition, u as SendDoc, v as SendStatus, w as SenderDomainConfig, x as SenderDomainRegistry, y as SenderDomainValidation, z as SubscriptionDoc, D as SubscriptionStatus, G as SuppressionDoc, I as SuppressionReason, J as TemplateKind, K as TemplateVersionDoc, W as WebhookEventDoc, Q as ensureIndexes, U as getCollections, V as validateSenderDomain } from './null-DaisDvB_.js';
|
|
3
3
|
import { ObjectId, Db, Filter } from 'mongodb';
|
|
4
4
|
import { Request, Router } from 'express';
|
|
5
5
|
import Handlebars from 'handlebars';
|
package/dist/index.js
CHANGED
|
@@ -2188,6 +2188,25 @@ async function promoteSoftBounces(ctx) {
|
|
|
2188
2188
|
// src/server/runner/tick.ts
|
|
2189
2189
|
var STRANDED_SEND_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
2190
2190
|
async function runTick(ctx) {
|
|
2191
|
+
await ctx.collections.health.updateOne(
|
|
2192
|
+
{ _id: "singleton" },
|
|
2193
|
+
{
|
|
2194
|
+
$set: { updatedAt: /* @__PURE__ */ new Date() },
|
|
2195
|
+
$setOnInsert: {
|
|
2196
|
+
_id: "singleton",
|
|
2197
|
+
windowStartedAt: /* @__PURE__ */ new Date(),
|
|
2198
|
+
windowDurationMs: ctx.config.circuitBreaker.windowMinutes * 60 * 1e3,
|
|
2199
|
+
status: "healthy",
|
|
2200
|
+
trippedAt: null,
|
|
2201
|
+
trippedReason: null,
|
|
2202
|
+
manuallyResumedAt: null,
|
|
2203
|
+
counters: { sent: 0, delivered: 0, bounced: 0, hardBounced: 0, softBounced: 0, complained: 0, failedToSend: 0 },
|
|
2204
|
+
rates: { bounceRate: 0, hardBounceRate: 0, complaintRate: 0, failureRate: 0 }
|
|
2205
|
+
}
|
|
2206
|
+
},
|
|
2207
|
+
{ upsert: true }
|
|
2208
|
+
).catch(() => {
|
|
2209
|
+
});
|
|
2191
2210
|
await processNewlyFiredEventTriggers(ctx).catch((err) => {
|
|
2192
2211
|
console.error("mailery: triggers scan failed", err);
|
|
2193
2212
|
});
|
|
@@ -2910,6 +2929,43 @@ var Mailer = class _Mailer {
|
|
|
2910
2929
|
}
|
|
2911
2930
|
};
|
|
2912
2931
|
|
|
2932
|
+
// src/server/templates/sender-domain.ts
|
|
2933
|
+
function validateSenderDomain(fromEmail, templateKind, registry) {
|
|
2934
|
+
if (!registry || Object.keys(registry).length === 0) return { ok: true };
|
|
2935
|
+
const domain = extractDomain(fromEmail);
|
|
2936
|
+
if (!domain) {
|
|
2937
|
+
return {
|
|
2938
|
+
ok: false,
|
|
2939
|
+
code: "invalid_email",
|
|
2940
|
+
reason: `invalid fromEmail "${fromEmail}" \u2014 expected "name@domain"`
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
const entry = registry[domain];
|
|
2944
|
+
if (!entry) {
|
|
2945
|
+
const known = Object.keys(registry).join(", ");
|
|
2946
|
+
return {
|
|
2947
|
+
ok: false,
|
|
2948
|
+
code: "unregistered_domain",
|
|
2949
|
+
reason: `sender domain "${domain}" is not declared in senderDomains (allowed: ${known})`
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
if (entry.kind === "both") return { ok: true };
|
|
2953
|
+
if (entry.kind !== templateKind) {
|
|
2954
|
+
return {
|
|
2955
|
+
ok: false,
|
|
2956
|
+
code: "wrong_kind",
|
|
2957
|
+
reason: `sender domain "${domain}" is configured for ${entry.kind} email, but this template's kind is ${templateKind}`
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2960
|
+
return { ok: true };
|
|
2961
|
+
}
|
|
2962
|
+
function extractDomain(email) {
|
|
2963
|
+
if (typeof email !== "string") return null;
|
|
2964
|
+
const at = email.lastIndexOf("@");
|
|
2965
|
+
if (at <= 0 || at === email.length - 1) return null;
|
|
2966
|
+
return email.slice(at + 1).toLowerCase().trim();
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2913
2969
|
// src/server/index.ts
|
|
2914
2970
|
init_mongo();
|
|
2915
2971
|
|
|
@@ -2939,6 +2995,227 @@ var NullProvider = class {
|
|
|
2939
2995
|
|
|
2940
2996
|
// src/server/index.ts
|
|
2941
2997
|
init_sendgrid();
|
|
2998
|
+
|
|
2999
|
+
// src/server/api/setup-status.ts
|
|
3000
|
+
async function runSetupChecks(mailer) {
|
|
3001
|
+
const checks = [];
|
|
3002
|
+
checks.push(await checkMongo(mailer));
|
|
3003
|
+
checks.push(await checkQueue(mailer));
|
|
3004
|
+
if (mailer.config.queue.driver !== "noop" && !mailer.config.workerless) {
|
|
3005
|
+
checks.push(await checkWorkersHeartbeat(mailer));
|
|
3006
|
+
}
|
|
3007
|
+
checks.push(await checkCircuitBreaker(mailer));
|
|
3008
|
+
checks.push(...checkFromDefaultsAgainstRegistry(mailer));
|
|
3009
|
+
checks.push(...await checkPublishedTemplates(mailer));
|
|
3010
|
+
checks.push(await checkPostalAddress(mailer));
|
|
3011
|
+
checks.push(await checkDoiTemplate(mailer));
|
|
3012
|
+
const overall = checks.some((c) => c.severity === "error") ? "error" : checks.some((c) => c.severity === "warn") ? "warn" : "ok";
|
|
3013
|
+
return {
|
|
3014
|
+
overall,
|
|
3015
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3016
|
+
checks
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
async function checkMongo(mailer) {
|
|
3020
|
+
try {
|
|
3021
|
+
await mailer.db.admin().ping();
|
|
3022
|
+
return { name: "mongo", label: "MongoDB connection", severity: "ok", message: "reachable" };
|
|
3023
|
+
} catch (err) {
|
|
3024
|
+
return {
|
|
3025
|
+
name: "mongo",
|
|
3026
|
+
label: "MongoDB connection",
|
|
3027
|
+
severity: "error",
|
|
3028
|
+
message: `MongoDB ping failed: ${err?.message ?? err}`,
|
|
3029
|
+
hint: "Mailery is configured against an unreachable Mongo. Sends, flow advancement, and admin reads will all fail."
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
async function checkQueue(mailer) {
|
|
3034
|
+
const driver = mailer.config.queue.driver;
|
|
3035
|
+
if (driver === "noop") {
|
|
3036
|
+
return {
|
|
3037
|
+
name: "queue",
|
|
3038
|
+
label: "Queue driver",
|
|
3039
|
+
severity: "ok",
|
|
3040
|
+
message: "driver: noop (synchronous-only mode)"
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
try {
|
|
3044
|
+
await mailer.queues.send.getWaitingCount();
|
|
3045
|
+
return { name: "queue", label: "Queue driver", severity: "ok", message: `driver: ${driver}` };
|
|
3046
|
+
} catch (err) {
|
|
3047
|
+
return {
|
|
3048
|
+
name: "queue",
|
|
3049
|
+
label: "Queue driver",
|
|
3050
|
+
severity: "error",
|
|
3051
|
+
message: `${driver} queue is not responding: ${err?.message ?? err}`,
|
|
3052
|
+
hint: driver === "bull" ? "Check Redis connectivity (queue.redis.url)." : "Check the @hokify/agenda + Mongo connection."
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
async function checkWorkersHeartbeat(mailer) {
|
|
3057
|
+
const h = await mailer.collections.health.findOne({ _id: "singleton" });
|
|
3058
|
+
const tickIntervalMs = mailer.config.tickIntervalSeconds * 1e3;
|
|
3059
|
+
const staleAfterMs = Math.max(tickIntervalMs * 3, 3e4);
|
|
3060
|
+
if (!h) {
|
|
3061
|
+
return {
|
|
3062
|
+
name: "workers_heartbeat",
|
|
3063
|
+
label: "Background workers",
|
|
3064
|
+
severity: "warn",
|
|
3065
|
+
message: "no tick has run yet",
|
|
3066
|
+
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."
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
const ageMs = Date.now() - new Date(h.updatedAt).getTime();
|
|
3070
|
+
if (ageMs > staleAfterMs) {
|
|
3071
|
+
return {
|
|
3072
|
+
name: "workers_heartbeat",
|
|
3073
|
+
label: "Background workers",
|
|
3074
|
+
severity: "error",
|
|
3075
|
+
message: `last tick ${humanDuration(ageMs)} ago (expected within ${humanDuration(tickIntervalMs)})`,
|
|
3076
|
+
hint: "Workers appear to be down. Sends and flow advancement are halted. Restart your worker process (`mailer.startWorkers()`)."
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
return {
|
|
3080
|
+
name: "workers_heartbeat",
|
|
3081
|
+
label: "Background workers",
|
|
3082
|
+
severity: "ok",
|
|
3083
|
+
message: `last tick ${humanDuration(ageMs)} ago`
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
async function checkCircuitBreaker(mailer) {
|
|
3087
|
+
const h = await mailer.collections.health.findOne({ _id: "singleton" });
|
|
3088
|
+
if (!h || h.status === "healthy") {
|
|
3089
|
+
return { name: "circuit_breaker", label: "Circuit breaker", severity: "ok", message: "healthy" };
|
|
3090
|
+
}
|
|
3091
|
+
if (h.status === "degraded") {
|
|
3092
|
+
return {
|
|
3093
|
+
name: "circuit_breaker",
|
|
3094
|
+
label: "Circuit breaker",
|
|
3095
|
+
severity: "warn",
|
|
3096
|
+
message: "degraded (high failure rate)",
|
|
3097
|
+
hint: "Marketing sends still flow but failure rate is above the degraded threshold. Investigate provider errors before they escalate to tripped."
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
3100
|
+
return {
|
|
3101
|
+
name: "circuit_breaker",
|
|
3102
|
+
label: "Circuit breaker",
|
|
3103
|
+
severity: "error",
|
|
3104
|
+
message: `tripped: ${h.trippedReason ?? "unknown reason"}`,
|
|
3105
|
+
hint: "Marketing sends are held. Investigate the underlying bounce / complaint cause, then POST /api/health/resume."
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
function checkFromDefaultsAgainstRegistry(mailer) {
|
|
3109
|
+
const registry = mailer.config.senderDomains;
|
|
3110
|
+
if (!registry || Object.keys(registry).length === 0) return [];
|
|
3111
|
+
const out = [];
|
|
3112
|
+
const from = mailer.config.fromDefaults?.email;
|
|
3113
|
+
const tx = mailer.config.transactionalFromDefaults?.email;
|
|
3114
|
+
if (from) {
|
|
3115
|
+
const r = validateSenderDomain(from, "marketing", registry);
|
|
3116
|
+
if (!r.ok) {
|
|
3117
|
+
out.push({
|
|
3118
|
+
name: "from_defaults_marketing",
|
|
3119
|
+
label: "fromDefaults vs senderDomains",
|
|
3120
|
+
severity: "error",
|
|
3121
|
+
message: r.reason,
|
|
3122
|
+
hint: "New marketing templates that fall back to fromDefaults will fail to publish."
|
|
3123
|
+
});
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
if (tx) {
|
|
3127
|
+
const r = validateSenderDomain(tx, "transactional", registry);
|
|
3128
|
+
if (!r.ok) {
|
|
3129
|
+
out.push({
|
|
3130
|
+
name: "transactional_from_defaults",
|
|
3131
|
+
label: "transactionalFromDefaults vs senderDomains",
|
|
3132
|
+
severity: "error",
|
|
3133
|
+
message: r.reason,
|
|
3134
|
+
hint: "New transactional templates that fall back to transactionalFromDefaults will fail to publish."
|
|
3135
|
+
});
|
|
3136
|
+
}
|
|
3137
|
+
} else if (from) {
|
|
3138
|
+
const r = validateSenderDomain(from, "transactional", registry);
|
|
3139
|
+
if (!r.ok) {
|
|
3140
|
+
out.push({
|
|
3141
|
+
name: "transactional_fallback",
|
|
3142
|
+
label: "Transactional fallback",
|
|
3143
|
+
severity: "warn",
|
|
3144
|
+
message: `transactionalFromDefaults is unset and fromDefaults (${from}) is invalid for transactional templates`,
|
|
3145
|
+
hint: 'Set transactionalFromDefaults to a transactional-kind domain, or set senderDomains entry for the existing one to "both".'
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
return out;
|
|
3150
|
+
}
|
|
3151
|
+
async function checkPublishedTemplates(mailer) {
|
|
3152
|
+
const registry = mailer.config.senderDomains;
|
|
3153
|
+
if (!registry || Object.keys(registry).length === 0) return [];
|
|
3154
|
+
const published = await mailer.collections.templates.find({ publishedAt: { $ne: null } }, { projection: { slug: 1, kind: 1, fromEmail: 1 } }).toArray();
|
|
3155
|
+
const broken = [];
|
|
3156
|
+
for (const tpl of published) {
|
|
3157
|
+
const r = validateSenderDomain(tpl.fromEmail, tpl.kind, registry);
|
|
3158
|
+
if (!r.ok) broken.push({ slug: tpl.slug, reason: r.reason });
|
|
3159
|
+
}
|
|
3160
|
+
if (broken.length === 0) return [];
|
|
3161
|
+
const list = broken.slice(0, 5).map((b) => `${b.slug} (${b.reason})`).join("; ");
|
|
3162
|
+
const more = broken.length > 5 ? ` \u2026and ${broken.length - 5} more` : "";
|
|
3163
|
+
return [
|
|
3164
|
+
{
|
|
3165
|
+
name: "published_template_domains",
|
|
3166
|
+
label: "Published templates",
|
|
3167
|
+
severity: "error",
|
|
3168
|
+
message: `${broken.length} published template${broken.length === 1 ? "" : "s"} use a fromEmail that no longer matches senderDomains: ${list}${more}`,
|
|
3169
|
+
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."
|
|
3170
|
+
}
|
|
3171
|
+
];
|
|
3172
|
+
}
|
|
3173
|
+
async function checkPostalAddress(mailer) {
|
|
3174
|
+
if (mailer.config.senderAddress) {
|
|
3175
|
+
return { name: "postal_address", label: "CAN-SPAM postal address", severity: "ok", message: "set" };
|
|
3176
|
+
}
|
|
3177
|
+
const marketingCount = await mailer.collections.templates.countDocuments({
|
|
3178
|
+
kind: "marketing",
|
|
3179
|
+
publishedAt: { $ne: null }
|
|
3180
|
+
});
|
|
3181
|
+
if (marketingCount === 0) {
|
|
3182
|
+
return { name: "postal_address", label: "CAN-SPAM postal address", severity: "ok", message: "no published marketing templates yet" };
|
|
3183
|
+
}
|
|
3184
|
+
return {
|
|
3185
|
+
name: "postal_address",
|
|
3186
|
+
label: "CAN-SPAM postal address",
|
|
3187
|
+
severity: "warn",
|
|
3188
|
+
message: `${marketingCount} published marketing template${marketingCount === 1 ? "" : "s"} but senderAddress is unset`,
|
|
3189
|
+
hint: "CAN-SPAM requires a postal address in marketing emails. Set `senderAddress` in your Mailer config and reference it via `{{senderAddress}}` in your templates."
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
async function checkDoiTemplate(mailer) {
|
|
3193
|
+
if (!mailer.config.requireDoubleOptIn) {
|
|
3194
|
+
return { name: "doi_template", label: "DOI template", severity: "ok", message: "DOI not required" };
|
|
3195
|
+
}
|
|
3196
|
+
const tpl = await mailer.collections.templates.findOne({
|
|
3197
|
+
slug: mailer.config.doiTemplateSlug,
|
|
3198
|
+
publishedAt: { $ne: null }
|
|
3199
|
+
});
|
|
3200
|
+
if (tpl) {
|
|
3201
|
+
return { name: "doi_template", label: "DOI template", severity: "ok", message: `template "${tpl.slug}" published` };
|
|
3202
|
+
}
|
|
3203
|
+
return {
|
|
3204
|
+
name: "doi_template",
|
|
3205
|
+
label: "DOI template",
|
|
3206
|
+
severity: "error",
|
|
3207
|
+
message: `requireDoubleOptIn is true but no published template with slug "${mailer.config.doiTemplateSlug}"`,
|
|
3208
|
+
hint: "New subscriptions will silently fail to send confirmation emails. Create and publish a template with this slug, or unset requireDoubleOptIn."
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
function humanDuration(ms) {
|
|
3212
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
3213
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
3214
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
|
|
3215
|
+
return `${Math.round(ms / 36e5)}h`;
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
// src/server/api/admin.ts
|
|
2942
3219
|
var __filename$1 = fileURLToPath(import.meta.url);
|
|
2943
3220
|
var __dirname$1 = path.dirname(__filename$1);
|
|
2944
3221
|
function defaultSpaDir() {
|
|
@@ -3160,6 +3437,13 @@ function apiRouter(mailer) {
|
|
|
3160
3437
|
res.json(rows);
|
|
3161
3438
|
})
|
|
3162
3439
|
);
|
|
3440
|
+
r.get(
|
|
3441
|
+
"/setup-status",
|
|
3442
|
+
asyncHandler(async (_req, res) => {
|
|
3443
|
+
const status = await runSetupChecks(mailer);
|
|
3444
|
+
res.json(status);
|
|
3445
|
+
})
|
|
3446
|
+
);
|
|
3163
3447
|
r.get(
|
|
3164
3448
|
"/health",
|
|
3165
3449
|
asyncHandler(async (_req, res) => {
|
|
@@ -3333,6 +3617,15 @@ function apiRouter(mailer) {
|
|
|
3333
3617
|
if (kind !== "marketing" && kind !== "transactional") {
|
|
3334
3618
|
return res.status(400).json({ error: "validation_failed", message: "kind must be marketing or transactional" });
|
|
3335
3619
|
}
|
|
3620
|
+
const resolvedFromEmail = fromEmail ?? (kind === "transactional" ? mailer.config.transactionalFromDefaults?.email : void 0) ?? mailer.config.fromDefaults?.email ?? "noreply@example.com";
|
|
3621
|
+
const senderCheck = validateSenderDomain(resolvedFromEmail, kind, mailer.config.senderDomains);
|
|
3622
|
+
if (!senderCheck.ok) {
|
|
3623
|
+
return res.status(400).json({
|
|
3624
|
+
error: "sender_domain_invalid",
|
|
3625
|
+
code: senderCheck.code,
|
|
3626
|
+
message: senderCheck.reason
|
|
3627
|
+
});
|
|
3628
|
+
}
|
|
3336
3629
|
const now = /* @__PURE__ */ new Date();
|
|
3337
3630
|
try {
|
|
3338
3631
|
await c.templates.insertOne({
|
|
@@ -3340,8 +3633,8 @@ function apiRouter(mailer) {
|
|
|
3340
3633
|
name,
|
|
3341
3634
|
description: "",
|
|
3342
3635
|
kind,
|
|
3343
|
-
fromName: fromName ?? mailer.config.fromDefaults?.name ?? "Mailery",
|
|
3344
|
-
fromEmail:
|
|
3636
|
+
fromName: fromName ?? (kind === "transactional" ? mailer.config.transactionalFromDefaults?.name : void 0) ?? mailer.config.fromDefaults?.name ?? "Mailery",
|
|
3637
|
+
fromEmail: resolvedFromEmail,
|
|
3345
3638
|
replyTo: null,
|
|
3346
3639
|
providerOverride: null,
|
|
3347
3640
|
subject: subject ?? `Untitled \u2014 ${name}`,
|
|
@@ -3399,6 +3692,18 @@ function apiRouter(mailer) {
|
|
|
3399
3692
|
if (typeof fromEmail === "string") set.fromEmail = fromEmail;
|
|
3400
3693
|
if (typeof replyTo === "string" || replyTo === null) set.replyTo = replyTo;
|
|
3401
3694
|
if (kind === "marketing" || kind === "transactional") set.kind = kind;
|
|
3695
|
+
if (typeof fromEmail === "string" || kind === "marketing" || kind === "transactional") {
|
|
3696
|
+
const resultingKind = set.kind ?? tpl.kind;
|
|
3697
|
+
const resultingFromEmail = set.fromEmail ?? tpl.fromEmail;
|
|
3698
|
+
const senderCheck = validateSenderDomain(resultingFromEmail, resultingKind, mailer.config.senderDomains);
|
|
3699
|
+
if (!senderCheck.ok) {
|
|
3700
|
+
return res.status(400).json({
|
|
3701
|
+
error: "sender_domain_invalid",
|
|
3702
|
+
code: senderCheck.code,
|
|
3703
|
+
message: senderCheck.reason
|
|
3704
|
+
});
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3402
3707
|
if (typeof trackOpens === "boolean") set.trackOpens = trackOpens;
|
|
3403
3708
|
if (typeof trackClicks === "boolean") set.trackClicks = trackClicks;
|
|
3404
3709
|
await c.templates.updateOne({ _id: tpl._id }, { $set: set });
|
|
@@ -3417,6 +3722,14 @@ function apiRouter(mailer) {
|
|
|
3417
3722
|
if (!tpl) return res.status(404).json({ error: "not_found" });
|
|
3418
3723
|
const draft = tpl.draft;
|
|
3419
3724
|
if (!draft) return res.status(400).json({ error: "no_draft" });
|
|
3725
|
+
const senderCheck = validateSenderDomain(tpl.fromEmail, tpl.kind, mailer.config.senderDomains);
|
|
3726
|
+
if (!senderCheck.ok) {
|
|
3727
|
+
return res.status(400).json({
|
|
3728
|
+
error: "sender_domain_invalid",
|
|
3729
|
+
code: senderCheck.code,
|
|
3730
|
+
message: senderCheck.reason
|
|
3731
|
+
});
|
|
3732
|
+
}
|
|
3420
3733
|
let compiled;
|
|
3421
3734
|
if (draft.editorJson) {
|
|
3422
3735
|
compiled = await compileMailyTemplate(draft.editorJson);
|
|
@@ -4027,6 +4340,6 @@ var DEDUPE_POLICIES = [
|
|
|
4027
4340
|
// src/server/index.ts
|
|
4028
4341
|
var VERSION = "0.1.0";
|
|
4029
4342
|
|
|
4030
|
-
export { DEDUPE_POLICIES, FLOW_STEP_KINDS, Mailer, MongoContactAdapter, NullProvider, PREDICATE_KINDS, SEGMENT_FILTER_KINDS, SendGridProvider, VERSION, applyTracking, applyWebhookEvent, compileMailyTemplate, compileTemplate, createAdminRouter, createPublicRouter, defaultFlowStep, defaultPredicate, defaultSegmentFilter, derivePlaintext, dispatchSend, ensureIndexes, getCollections, predicateKind, processNewlyFiredEventTriggers, processOneRunStep, renderTemplate, runTick, sha256Hex, signUnsubscribeToken, sweepStrandedFlowRuns, verifyUnsubscribeToken };
|
|
4343
|
+
export { DEDUPE_POLICIES, FLOW_STEP_KINDS, Mailer, MongoContactAdapter, NullProvider, PREDICATE_KINDS, SEGMENT_FILTER_KINDS, SendGridProvider, VERSION, applyTracking, applyWebhookEvent, compileMailyTemplate, compileTemplate, createAdminRouter, createPublicRouter, defaultFlowStep, defaultPredicate, defaultSegmentFilter, derivePlaintext, dispatchSend, ensureIndexes, getCollections, predicateKind, processNewlyFiredEventTriggers, processOneRunStep, renderTemplate, runTick, sha256Hex, signUnsubscribeToken, sweepStrandedFlowRuns, validateSenderDomain, verifyUnsubscribeToken };
|
|
4031
4344
|
//# sourceMappingURL=index.js.map
|
|
4032
4345
|
//# sourceMappingURL=index.js.map
|