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.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 MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-CU_EApC6.cjs';
2
- export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-CU_EApC6.cjs';
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 MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-CfzvqNEp.cjs';
2
+ export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-CfzvqNEp.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 MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-CU_EApC6.js';
2
- export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-CU_EApC6.js';
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 MailTesterFeedback, d as Mailer, T as TemplateDoc, e as SuppressionScope, D as DeliveryWindow, F as FlowStep, f as SegmentFilter, P as Predicate } from './null-CfzvqNEp.js';
2
+ export { g as AuditLogDoc, B as BroadcastDoc, h as BroadcastStatus, i as CircuitBreakerThresholds, j as Collections, k as ContactTagDoc, E as EventDoc, l as FlowDoc, m as FlowGoal, n as FlowRunDoc, o as FlowRunStatus, p as FlowVersionDoc, H as HealthDoc, q as HealthStatus, L as LeadDoc, r as MailerConfig, s as NullProvider, O as OutboxDoc, t as RESERVED_VAR_KEYS, u as RedisOptions, v as SegmentDefinition, w as SendDoc, x as SendStatus, y as SenderDomainConfig, z as SenderDomainRegistry, G as SenderDomainValidation, I as SubscriptionDoc, J as SubscriptionStatus, K as SuppressionDoc, Q as SuppressionReason, U as TemplateKind, V as TemplateVersionDoc, W as VarsAdapter, X as VarsResolveInfo, Y as WebhookEventDoc, Z as defineVars, _ as ensureIndexes, $ as getCollections, a0 as validateSenderDomain, a1 as varsJsonSchema } from './null-CfzvqNEp.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
@@ -304,7 +304,10 @@ var init_sendgrid = __esm({
304
304
  replyTo: args.replyTo,
305
305
  subject: args.subject,
306
306
  text: args.text,
307
- html: args.html,
307
+ // Omit the key entirely rather than sending `html: undefined` — a
308
+ // text_only template must produce a single-part text/plain message, and
309
+ // an explicit undefined can still serialize into an empty HTML part.
310
+ ...args.html ? { html: args.html } : {},
308
311
  headers: args.headers,
309
312
  customArgs: args.messageMeta,
310
313
  trackingSettings: {
@@ -2143,7 +2146,8 @@ async function dispatchSend(sendId, ctx) {
2143
2146
  await markFailed(send._id, `render error: ${String(err?.message ?? err)}`, ctx);
2144
2147
  throw err;
2145
2148
  }
2146
- const tracking = applyTracking(rendered.html, {
2149
+ const textOnly = template.bodyFormat === "text_only";
2150
+ const tracking = textOnly ? { html: "", links: [] } : applyTracking(rendered.html, {
2147
2151
  sendId: String(send._id),
2148
2152
  publicUrl: ctx.config.publicUrl,
2149
2153
  trackOpens: template.trackOpens ?? ctx.config.trackOpens,
@@ -2155,7 +2159,9 @@ async function dispatchSend(sendId, ctx) {
2155
2159
  {
2156
2160
  $set: {
2157
2161
  links: tracking.links,
2158
- bodyHash: sha256(tracking.html),
2162
+ // Hash what actually goes out, so a text_only send's fingerprint
2163
+ // tracks the text body rather than an HTML part it never had.
2164
+ bodyHash: sha256(textOnly ? rendered.plainText : tracking.html),
2159
2165
  status: "sending",
2160
2166
  fromName: rendered.fromName,
2161
2167
  fromEmail: rendered.fromEmail,
@@ -2180,7 +2186,7 @@ async function dispatchSend(sendId, ctx) {
2180
2186
  fromEmail: rendered.fromEmail,
2181
2187
  replyTo: rendered.replyTo ?? void 0,
2182
2188
  subject: rendered.subject,
2183
- html: tracking.html,
2189
+ ...textOnly ? {} : { html: tracking.html },
2184
2190
  text: rendered.plainText,
2185
2191
  headers,
2186
2192
  messageMeta: { sendId: String(send._id) }
@@ -4811,12 +4817,15 @@ function isAllCaps(subject) {
4811
4817
  // src/server/api/admin.ts
4812
4818
  init_vars();
4813
4819
 
4820
+ // src/shared/enums.ts
4821
+ var TEMPLATE_BODY_FORMATS = ["multipart", "text_only"];
4822
+
4814
4823
  // src/server/api/setup-status.ts
4815
4824
  async function runSetupChecks(mailer) {
4816
4825
  const checks = [];
4817
4826
  checks.push(await checkMongo(mailer));
4818
4827
  checks.push(await checkQueue(mailer));
4819
- if (mailer.config.queue.driver !== "noop" && !mailer.config.workerless) {
4828
+ if (mailer.config.queue.driver !== "noop") {
4820
4829
  checks.push(await checkWorkersHeartbeat(mailer));
4821
4830
  }
4822
4831
  checks.push(await checkCircuitBreaker(mailer));
@@ -4881,13 +4890,14 @@ async function checkWorkersHeartbeat(mailer) {
4881
4890
  );
4882
4891
  const tickIntervalMs = mailer.config.tickIntervalSeconds * 1e3;
4883
4892
  const staleAfterMs = Math.max(tickIntervalMs * 3, 3e4);
4893
+ const workerless = mailer.config.workerless;
4884
4894
  if (!h) {
4885
4895
  return {
4886
4896
  name: "workers_heartbeat",
4887
4897
  label: "Background workers",
4888
4898
  severity: "warn",
4889
- message: "no tick has run yet",
4890
- 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."
4899
+ message: workerless ? "no tick has run yet (workers run in another process)" : "no tick has run yet",
4900
+ 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."
4891
4901
  };
4892
4902
  }
4893
4903
  const ageMs = Date.now() - new Date(h.updatedAt).getTime();
@@ -4897,14 +4907,14 @@ async function checkWorkersHeartbeat(mailer) {
4897
4907
  label: "Background workers",
4898
4908
  severity: "error",
4899
4909
  message: `last tick ${humanDuration(ageMs)} ago (expected within ${humanDuration(tickIntervalMs)})`,
4900
- hint: "Workers appear to be down. Sends and flow advancement are halted. Restart your worker process (`mailer.startWorkers()`)."
4910
+ 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()`)."
4901
4911
  };
4902
4912
  }
4903
4913
  return {
4904
4914
  name: "workers_heartbeat",
4905
4915
  label: "Background workers",
4906
4916
  severity: "ok",
4907
- message: `last tick ${humanDuration(ageMs)} ago`
4917
+ message: `last tick ${humanDuration(ageMs)} ago${workerless ? " (worker process)" : ""}`
4908
4918
  };
4909
4919
  }
4910
4920
  async function checkCircuitBreaker(mailer) {
@@ -6469,6 +6479,7 @@ function apiRouter(mailer, opts = {}) {
6469
6479
  lastModifiedAt: now
6470
6480
  },
6471
6481
  tags: [],
6482
+ bodyFormat: "multipart",
6472
6483
  trackOpens: kind === "marketing",
6473
6484
  trackClicks: kind === "marketing",
6474
6485
  stats: { sent: 0, delivered: 0, opened: 0, clicked: 0, bounced: 0, complained: 0, unsubscribed: 0, lastSentAt: null },
@@ -6494,7 +6505,7 @@ function apiRouter(mailer, opts = {}) {
6494
6505
  asyncHandler(async (req, res) => {
6495
6506
  const tpl = await c.templates.findOne({ slug: req.params.slug });
6496
6507
  if (!tpl) return res.status(404).json({ error: "not_found" });
6497
- const { subject, preheader, mjml, editorJson, notes, name, fromName, fromEmail, replyTo, kind, trackOpens, trackClicks } = req.body ?? {};
6508
+ const { subject, preheader, mjml, editorJson, notes, name, fromName, fromEmail, replyTo, kind, bodyFormat, trackOpens, trackClicks } = req.body ?? {};
6498
6509
  const set = {
6499
6510
  "draft.lastModifiedBy": req.actor,
6500
6511
  "draft.lastModifiedAt": /* @__PURE__ */ new Date(),
@@ -6522,6 +6533,7 @@ function apiRouter(mailer, opts = {}) {
6522
6533
  });
6523
6534
  }
6524
6535
  }
6536
+ if (TEMPLATE_BODY_FORMATS.includes(bodyFormat)) set.bodyFormat = bodyFormat;
6525
6537
  if (typeof trackOpens === "boolean") set.trackOpens = trackOpens;
6526
6538
  if (typeof trackClicks === "boolean") set.trackClicks = trackClicks;
6527
6539
  await c.templates.updateOne({ _id: tpl._id }, { $set: set });
@@ -6607,7 +6619,9 @@ function apiRouter(mailer, opts = {}) {
6607
6619
  fromEmail: tpl.fromEmail,
6608
6620
  replyTo: tpl.replyTo ?? void 0,
6609
6621
  subject: draft.subject,
6610
- html: compiled.html,
6622
+ // Match the real send shape — a text_only template's deliverability
6623
+ // should be scored on the single-part message it actually sends.
6624
+ ...tpl.bodyFormat === "text_only" ? {} : { html: compiled.html },
6611
6625
  text: compiled.plainText,
6612
6626
  headers: {},
6613
6627
  messageMeta: { mailTesterCheckId: checkId }
@@ -6952,7 +6966,9 @@ function apiRouter(mailer, opts = {}) {
6952
6966
  fromName: rendered.fromName,
6953
6967
  fromEmail: rendered.fromEmail,
6954
6968
  subject: `[TEST] ${rendered.subject}`,
6955
- html: tracking.html,
6969
+ // Match the real send shape, or a test of a text_only template would
6970
+ // arrive as HTML and hide exactly what the author is checking.
6971
+ ...tpl.bodyFormat === "text_only" ? {} : { html: tracking.html },
6956
6972
  text: rendered.plainText
6957
6973
  });
6958
6974
  await mailer.audit({
@@ -7592,7 +7608,7 @@ var DEDUPE_POLICIES = [
7592
7608
  ];
7593
7609
 
7594
7610
  // src/server/index.ts
7595
- var VERSION = "0.10.1" ;
7611
+ var VERSION = "0.11.0" ;
7596
7612
 
7597
7613
  export { DEDUPE_POLICIES, FLOW_STEP_KINDS, Mailer, MongoContactAdapter, NullProvider, PREDICATE_KINDS, RESERVED_VAR_KEYS, SEGMENT_FILTER_KINDS, SendGridProvider, VERSION, applyTracking, applyWebhookEvent, compileMailyTemplate, compileTemplate, computeDeliveryTime, createAdminRouter, createPublicRouter, defaultFlowStep, defaultPredicate, defaultSegmentFilter, defineVars, derivePlaintext, dispatchSend, ensureIndexes, getCollections, predicateKind, processNewlyFiredEventTriggers, processOneRunStep, renderTemplate, runTick, sha256Hex, signUnsubscribeToken, sweepStrandedFlowRuns, validateSenderDomain, varsJsonSchema, verifyUnsubscribeToken };
7598
7614
  //# sourceMappingURL=index.js.map