mailery 0.10.1 → 0.11.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/dist/index.cjs CHANGED
@@ -322,7 +322,10 @@ var init_sendgrid = __esm({
322
322
  replyTo: args.replyTo,
323
323
  subject: args.subject,
324
324
  text: args.text,
325
- html: args.html,
325
+ // Omit the key entirely rather than sending `html: undefined` — a
326
+ // text_only template must produce a single-part text/plain message, and
327
+ // an explicit undefined can still serialize into an empty HTML part.
328
+ ...args.html ? { html: args.html } : {},
326
329
  headers: args.headers,
327
330
  customArgs: args.messageMeta,
328
331
  trackingSettings: {
@@ -2161,7 +2164,8 @@ async function dispatchSend(sendId, ctx) {
2161
2164
  await markFailed(send._id, `render error: ${String(err?.message ?? err)}`, ctx);
2162
2165
  throw err;
2163
2166
  }
2164
- const tracking = applyTracking(rendered.html, {
2167
+ const textOnly = template.bodyFormat === "text_only";
2168
+ const tracking = textOnly ? { html: "", links: [] } : applyTracking(rendered.html, {
2165
2169
  sendId: String(send._id),
2166
2170
  publicUrl: ctx.config.publicUrl,
2167
2171
  trackOpens: template.trackOpens ?? ctx.config.trackOpens,
@@ -2173,7 +2177,9 @@ async function dispatchSend(sendId, ctx) {
2173
2177
  {
2174
2178
  $set: {
2175
2179
  links: tracking.links,
2176
- bodyHash: sha256(tracking.html),
2180
+ // Hash what actually goes out, so a text_only send's fingerprint
2181
+ // tracks the text body rather than an HTML part it never had.
2182
+ bodyHash: sha256(textOnly ? rendered.plainText : tracking.html),
2177
2183
  status: "sending",
2178
2184
  fromName: rendered.fromName,
2179
2185
  fromEmail: rendered.fromEmail,
@@ -2198,7 +2204,7 @@ async function dispatchSend(sendId, ctx) {
2198
2204
  fromEmail: rendered.fromEmail,
2199
2205
  replyTo: rendered.replyTo ?? void 0,
2200
2206
  subject: rendered.subject,
2201
- html: tracking.html,
2207
+ ...textOnly ? {} : { html: tracking.html },
2202
2208
  text: rendered.plainText,
2203
2209
  headers,
2204
2210
  messageMeta: { sendId: String(send._id) }
@@ -4829,12 +4835,15 @@ function isAllCaps(subject) {
4829
4835
  // src/server/api/admin.ts
4830
4836
  init_vars();
4831
4837
 
4838
+ // src/shared/enums.ts
4839
+ var TEMPLATE_BODY_FORMATS = ["multipart", "text_only"];
4840
+
4832
4841
  // src/server/api/setup-status.ts
4833
4842
  async function runSetupChecks(mailer) {
4834
4843
  const checks = [];
4835
4844
  checks.push(await checkMongo(mailer));
4836
4845
  checks.push(await checkQueue(mailer));
4837
- if (mailer.config.queue.driver !== "noop" && !mailer.config.workerless) {
4846
+ if (mailer.config.queue.driver !== "noop") {
4838
4847
  checks.push(await checkWorkersHeartbeat(mailer));
4839
4848
  }
4840
4849
  checks.push(await checkCircuitBreaker(mailer));
@@ -4899,13 +4908,14 @@ async function checkWorkersHeartbeat(mailer) {
4899
4908
  );
4900
4909
  const tickIntervalMs = mailer.config.tickIntervalSeconds * 1e3;
4901
4910
  const staleAfterMs = Math.max(tickIntervalMs * 3, 3e4);
4911
+ const workerless = mailer.config.workerless;
4902
4912
  if (!h) {
4903
4913
  return {
4904
4914
  name: "workers_heartbeat",
4905
4915
  label: "Background workers",
4906
4916
  severity: "warn",
4907
- message: "no tick has run yet",
4908
- 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."
4917
+ message: workerless ? "no tick has run yet (workers run in another process)" : "no tick has run yet",
4918
+ hint: workerless ? "This process is workerless; the heartbeat comes from your separate worker process. If that process is started, the heartbeat will appear within one tick interval \u2014 a fresh install legitimately has none yet." : "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."
4909
4919
  };
4910
4920
  }
4911
4921
  const ageMs = Date.now() - new Date(h.updatedAt).getTime();
@@ -4915,14 +4925,14 @@ async function checkWorkersHeartbeat(mailer) {
4915
4925
  label: "Background workers",
4916
4926
  severity: "error",
4917
4927
  message: `last tick ${humanDuration(ageMs)} ago (expected within ${humanDuration(tickIntervalMs)})`,
4918
- hint: "Workers appear to be down. Sends and flow advancement are halted. Restart your worker process (`mailer.startWorkers()`)."
4928
+ hint: workerless ? "Workers run in another process, and that process appears to be down. Sends and flow advancement are halted. Restart the process that calls `mailer.startWorkers()`." : "Workers appear to be down. Sends and flow advancement are halted. Restart your worker process (`mailer.startWorkers()`)."
4919
4929
  };
4920
4930
  }
4921
4931
  return {
4922
4932
  name: "workers_heartbeat",
4923
4933
  label: "Background workers",
4924
4934
  severity: "ok",
4925
- message: `last tick ${humanDuration(ageMs)} ago`
4935
+ message: `last tick ${humanDuration(ageMs)} ago${workerless ? " (worker process)" : ""}`
4926
4936
  };
4927
4937
  }
4928
4938
  async function checkCircuitBreaker(mailer) {
@@ -6487,6 +6497,7 @@ function apiRouter(mailer, opts = {}) {
6487
6497
  lastModifiedAt: now
6488
6498
  },
6489
6499
  tags: [],
6500
+ bodyFormat: "multipart",
6490
6501
  trackOpens: kind === "marketing",
6491
6502
  trackClicks: kind === "marketing",
6492
6503
  stats: { sent: 0, delivered: 0, opened: 0, clicked: 0, bounced: 0, complained: 0, unsubscribed: 0, lastSentAt: null },
@@ -6512,7 +6523,7 @@ function apiRouter(mailer, opts = {}) {
6512
6523
  asyncHandler(async (req, res) => {
6513
6524
  const tpl = await c.templates.findOne({ slug: req.params.slug });
6514
6525
  if (!tpl) return res.status(404).json({ error: "not_found" });
6515
- const { subject, preheader, mjml, editorJson, notes, name, fromName, fromEmail, replyTo, kind, trackOpens, trackClicks } = req.body ?? {};
6526
+ const { subject, preheader, mjml, editorJson, notes, name, fromName, fromEmail, replyTo, kind, bodyFormat, trackOpens, trackClicks } = req.body ?? {};
6516
6527
  const set = {
6517
6528
  "draft.lastModifiedBy": req.actor,
6518
6529
  "draft.lastModifiedAt": /* @__PURE__ */ new Date(),
@@ -6540,6 +6551,7 @@ function apiRouter(mailer, opts = {}) {
6540
6551
  });
6541
6552
  }
6542
6553
  }
6554
+ if (TEMPLATE_BODY_FORMATS.includes(bodyFormat)) set.bodyFormat = bodyFormat;
6543
6555
  if (typeof trackOpens === "boolean") set.trackOpens = trackOpens;
6544
6556
  if (typeof trackClicks === "boolean") set.trackClicks = trackClicks;
6545
6557
  await c.templates.updateOne({ _id: tpl._id }, { $set: set });
@@ -6625,7 +6637,9 @@ function apiRouter(mailer, opts = {}) {
6625
6637
  fromEmail: tpl.fromEmail,
6626
6638
  replyTo: tpl.replyTo ?? void 0,
6627
6639
  subject: draft.subject,
6628
- html: compiled.html,
6640
+ // Match the real send shape — a text_only template's deliverability
6641
+ // should be scored on the single-part message it actually sends.
6642
+ ...tpl.bodyFormat === "text_only" ? {} : { html: compiled.html },
6629
6643
  text: compiled.plainText,
6630
6644
  headers: {},
6631
6645
  messageMeta: { mailTesterCheckId: checkId }
@@ -6970,7 +6984,9 @@ function apiRouter(mailer, opts = {}) {
6970
6984
  fromName: rendered.fromName,
6971
6985
  fromEmail: rendered.fromEmail,
6972
6986
  subject: `[TEST] ${rendered.subject}`,
6973
- html: tracking.html,
6987
+ // Match the real send shape, or a test of a text_only template would
6988
+ // arrive as HTML and hide exactly what the author is checking.
6989
+ ...tpl.bodyFormat === "text_only" ? {} : { html: tracking.html },
6974
6990
  text: rendered.plainText
6975
6991
  });
6976
6992
  await mailer.audit({
@@ -7610,7 +7626,7 @@ var DEDUPE_POLICIES = [
7610
7626
  ];
7611
7627
 
7612
7628
  // src/server/index.ts
7613
- var VERSION = "0.10.1" ;
7629
+ var VERSION = "0.11.0" ;
7614
7630
 
7615
7631
  exports.DEDUPE_POLICIES = DEDUPE_POLICIES;
7616
7632
  exports.FLOW_STEP_KINDS = FLOW_STEP_KINDS;