@astralibx/email-rule-engine 5.0.0 → 7.0.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 CHANGED
@@ -102,21 +102,18 @@ The `createEmailRuleEngine(config)` factory accepts an `EmailRuleEngineConfig` o
102
102
 
103
103
  See [docs/configuration.md](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/configuration.md) for the full reference with examples.
104
104
 
105
- ## Documentation
106
-
107
- | Document | Description |
108
- |----------|-------------|
109
- | [Configuration](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/configuration.md) | Full config reference -- db, redis, adapters, platforms, options, hooks, logger |
110
- | [Adapters](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/adapters.md) | All 6 adapters with type signatures and example implementations |
111
- | [Templates](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/templates.md) | Creating templates, MJML + Handlebars syntax, built-in helpers |
112
- | [Rules](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/rules.md) | Targeting conditions, operators, sendOnce/resend, dry runs |
113
- | [Throttling](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/throttling.md) | Per-user limits, global caps, bypass rules, tracking |
114
- | [API Routes](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/api-routes.md) | All REST endpoints with curl examples |
115
- | [Programmatic API](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/programmatic-api.md) | Using services directly -- runner, templateService, ruleService |
116
- | [Execution Flow](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/execution-flow.md) | Step-by-step runner flow and error behavior |
117
- | [Error Handling](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/error-handling.md) | All error classes with codes and when thrown |
118
- | [Constants](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/constants.md) | All exported constants and derived types |
119
- | [Migration v1 to v2](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/migration-v1-to-v2.md) | Breaking changes from v1 |
105
+ ## Getting Started Guide
106
+
107
+ 1. [Configuration](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/configuration.md) Set up database, Redis, and options
108
+ 2. [Adapters](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/adapters.md) — Implement the 6 required adapter functions
109
+ 3. [Templates](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/templates.md) Create email templates with MJML + Handlebars
110
+ 4. [Rules](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/rules.md) Define targeting rules with conditions or explicit lists
111
+ 5. [Execution Flow](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/execution-flow.md) Understand how the runner processes rules
112
+ 6. [Throttling](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/throttling.md) Configure per-user send limits
113
+
114
+ Reference: [API Routes](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/api-routes.md) | [Programmatic API](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/programmatic-api.md) | [Types](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/types.md) | [Constants](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/constants.md) | [Error Handling](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/error-handling.md)
115
+
116
+ > **Important:** Configure throttle settings before running rules. Default limits (1/day, 2/week) may be too restrictive. See [Throttling](https://github.com/Hariprakash1997/astralib/blob/main/packages/email-rule-engine/docs/throttling.md).
120
117
 
121
118
  ## License
122
119
 
package/dist/index.d.mts CHANGED
@@ -92,6 +92,8 @@ interface EmailRule {
92
92
  cooldownDays?: number;
93
93
  autoApprove: boolean;
94
94
  maxPerRun?: number;
95
+ validFrom?: Date;
96
+ validTill?: Date;
95
97
  bypassThrottle: boolean;
96
98
  emailType: EmailType;
97
99
  totalSent: number;
@@ -112,6 +114,8 @@ interface CreateEmailRuleInput {
112
114
  cooldownDays?: number;
113
115
  autoApprove?: boolean;
114
116
  maxPerRun?: number;
117
+ validFrom?: Date;
118
+ validTill?: Date;
115
119
  bypassThrottle?: boolean;
116
120
  emailType?: EmailType;
117
121
  }
@@ -127,6 +131,8 @@ interface UpdateEmailRuleInput {
127
131
  cooldownDays?: number;
128
132
  autoApprove?: boolean;
129
133
  maxPerRun?: number;
134
+ validFrom?: Date;
135
+ validTill?: Date;
130
136
  bypassThrottle?: boolean;
131
137
  emailType?: EmailType;
132
138
  }
@@ -289,6 +295,8 @@ interface EmailTemplate {
289
295
  textBody?: string;
290
296
  subjects: string[];
291
297
  bodies: string[];
298
+ preheaders?: string[];
299
+ fields?: Record<string, string>;
292
300
  variables: string[];
293
301
  version: number;
294
302
  isActive: boolean;
@@ -305,6 +313,8 @@ interface CreateEmailTemplateInput {
305
313
  textBody?: string;
306
314
  subjects: string[];
307
315
  bodies: string[];
316
+ preheaders?: string[];
317
+ fields?: Record<string, string>;
308
318
  variables?: string[];
309
319
  }
310
320
  interface UpdateEmailTemplateInput {
@@ -316,6 +326,8 @@ interface UpdateEmailTemplateInput {
316
326
  textBody?: string;
317
327
  subjects?: string[];
318
328
  bodies?: string[];
329
+ preheaders?: string[];
330
+ fields?: Record<string, string>;
319
331
  variables?: string[];
320
332
  isActive?: boolean;
321
333
  }
@@ -373,6 +385,7 @@ interface IEmailRuleSend {
373
385
  subject?: string;
374
386
  subjectIndex?: number;
375
387
  bodyIndex?: number;
388
+ preheaderIndex?: number;
376
389
  failureReason?: string;
377
390
  }
378
391
  type EmailRuleSendDocument = HydratedDocument<IEmailRuleSend>;
@@ -386,6 +399,7 @@ interface EmailRuleSendStatics {
386
399
  subject?: string;
387
400
  subjectIndex?: number;
388
401
  bodyIndex?: number;
402
+ preheaderIndex?: number;
389
403
  failureReason?: string;
390
404
  }): Promise<EmailRuleSendDocument>;
391
405
  }
@@ -616,10 +630,11 @@ declare class TemplateRenderService {
616
630
  constructor();
617
631
  renderSingle(subject: string, body: string, data: Record<string, unknown>, textBody?: string): RenderResult;
618
632
  compileBatch(subject: string, body: string, textBody?: string): CompiledTemplate;
619
- compileBatchVariants(subjects: string[], bodies: string[], textBody?: string): {
633
+ compileBatchVariants(subjects: string[], bodies: string[], textBody?: string, preheaders?: string[]): {
620
634
  subjectFns: HandlebarsTemplateDelegate[];
621
635
  bodyFns: HandlebarsTemplateDelegate[];
622
636
  textBodyFn?: HandlebarsTemplateDelegate;
637
+ preheaderFns?: HandlebarsTemplateDelegate[];
623
638
  };
624
639
  renderFromCompiled(compiled: CompiledTemplate, data: Record<string, unknown>): RenderResult;
625
640
  renderPreview(subject: string, body: string, data: Record<string, unknown>, textBody?: string): RenderResult;
package/dist/index.d.ts CHANGED
@@ -92,6 +92,8 @@ interface EmailRule {
92
92
  cooldownDays?: number;
93
93
  autoApprove: boolean;
94
94
  maxPerRun?: number;
95
+ validFrom?: Date;
96
+ validTill?: Date;
95
97
  bypassThrottle: boolean;
96
98
  emailType: EmailType;
97
99
  totalSent: number;
@@ -112,6 +114,8 @@ interface CreateEmailRuleInput {
112
114
  cooldownDays?: number;
113
115
  autoApprove?: boolean;
114
116
  maxPerRun?: number;
117
+ validFrom?: Date;
118
+ validTill?: Date;
115
119
  bypassThrottle?: boolean;
116
120
  emailType?: EmailType;
117
121
  }
@@ -127,6 +131,8 @@ interface UpdateEmailRuleInput {
127
131
  cooldownDays?: number;
128
132
  autoApprove?: boolean;
129
133
  maxPerRun?: number;
134
+ validFrom?: Date;
135
+ validTill?: Date;
130
136
  bypassThrottle?: boolean;
131
137
  emailType?: EmailType;
132
138
  }
@@ -289,6 +295,8 @@ interface EmailTemplate {
289
295
  textBody?: string;
290
296
  subjects: string[];
291
297
  bodies: string[];
298
+ preheaders?: string[];
299
+ fields?: Record<string, string>;
292
300
  variables: string[];
293
301
  version: number;
294
302
  isActive: boolean;
@@ -305,6 +313,8 @@ interface CreateEmailTemplateInput {
305
313
  textBody?: string;
306
314
  subjects: string[];
307
315
  bodies: string[];
316
+ preheaders?: string[];
317
+ fields?: Record<string, string>;
308
318
  variables?: string[];
309
319
  }
310
320
  interface UpdateEmailTemplateInput {
@@ -316,6 +326,8 @@ interface UpdateEmailTemplateInput {
316
326
  textBody?: string;
317
327
  subjects?: string[];
318
328
  bodies?: string[];
329
+ preheaders?: string[];
330
+ fields?: Record<string, string>;
319
331
  variables?: string[];
320
332
  isActive?: boolean;
321
333
  }
@@ -373,6 +385,7 @@ interface IEmailRuleSend {
373
385
  subject?: string;
374
386
  subjectIndex?: number;
375
387
  bodyIndex?: number;
388
+ preheaderIndex?: number;
376
389
  failureReason?: string;
377
390
  }
378
391
  type EmailRuleSendDocument = HydratedDocument<IEmailRuleSend>;
@@ -386,6 +399,7 @@ interface EmailRuleSendStatics {
386
399
  subject?: string;
387
400
  subjectIndex?: number;
388
401
  bodyIndex?: number;
402
+ preheaderIndex?: number;
389
403
  failureReason?: string;
390
404
  }): Promise<EmailRuleSendDocument>;
391
405
  }
@@ -616,10 +630,11 @@ declare class TemplateRenderService {
616
630
  constructor();
617
631
  renderSingle(subject: string, body: string, data: Record<string, unknown>, textBody?: string): RenderResult;
618
632
  compileBatch(subject: string, body: string, textBody?: string): CompiledTemplate;
619
- compileBatchVariants(subjects: string[], bodies: string[], textBody?: string): {
633
+ compileBatchVariants(subjects: string[], bodies: string[], textBody?: string, preheaders?: string[]): {
620
634
  subjectFns: HandlebarsTemplateDelegate[];
621
635
  bodyFns: HandlebarsTemplateDelegate[];
622
636
  textBodyFn?: HandlebarsTemplateDelegate;
637
+ preheaderFns?: HandlebarsTemplateDelegate[];
623
638
  };
624
639
  renderFromCompiled(compiled: CompiledTemplate, data: Record<string, unknown>): RenderResult;
625
640
  renderPreview(subject: string, body: string, data: Record<string, unknown>, textBody?: string): RenderResult;
package/dist/index.js CHANGED
@@ -79,6 +79,18 @@ function createEmailTemplateSchema(platformValues, audienceValues, categoryValue
79
79
  textBody: String,
80
80
  subjects: { type: [{ type: String }], required: true, validate: [(v) => v.length >= 1, "At least one subject is required"] },
81
81
  bodies: { type: [{ type: String }], required: true, validate: [(v) => v.length >= 1, "At least one body is required"] },
82
+ preheaders: [{ type: String }],
83
+ fields: {
84
+ type: mongoose.Schema.Types.Mixed,
85
+ default: {},
86
+ validate: {
87
+ validator: (v) => {
88
+ if (!v || typeof v !== "object") return true;
89
+ return Object.values(v).every((val) => typeof val === "string");
90
+ },
91
+ message: "All field values must be strings"
92
+ }
93
+ },
82
94
  variables: [{ type: String }],
83
95
  version: { type: Number, default: 1 },
84
96
  isActive: { type: Boolean, default: true, index: true }
@@ -113,6 +125,8 @@ function createEmailTemplateSchema(platformValues, audienceValues, categoryValue
113
125
  textBody: input.textBody,
114
126
  subjects: input.subjects,
115
127
  bodies: input.bodies,
128
+ preheaders: input.preheaders || [],
129
+ fields: input.fields || {},
116
130
  variables: input.variables || [],
117
131
  version: 1,
118
132
  isActive: true
@@ -161,6 +175,8 @@ function createEmailRuleSchema(platformValues, audienceValues, collectionPrefix)
161
175
  cooldownDays: Number,
162
176
  autoApprove: { type: Boolean, default: true },
163
177
  maxPerRun: Number,
178
+ validFrom: { type: Date },
179
+ validTill: { type: Date },
164
180
  bypassThrottle: { type: Boolean, default: false },
165
181
  emailType: { type: String, enum: Object.values(EMAIL_TYPE), default: EMAIL_TYPE.Automated },
166
182
  totalSent: { type: Number, default: 0 },
@@ -191,6 +207,8 @@ function createEmailRuleSchema(platformValues, audienceValues, collectionPrefix)
191
207
  cooldownDays: input.cooldownDays,
192
208
  autoApprove: input.autoApprove ?? true,
193
209
  maxPerRun: input.maxPerRun,
210
+ validFrom: input.validFrom,
211
+ validTill: input.validTill,
194
212
  bypassThrottle: input.bypassThrottle ?? false,
195
213
  emailType: input.emailType ?? EMAIL_TYPE.Automated,
196
214
  totalSent: 0,
@@ -218,6 +236,7 @@ function createEmailRuleSendSchema(collectionPrefix) {
218
236
  subject: { type: String },
219
237
  subjectIndex: { type: Number },
220
238
  bodyIndex: { type: Number },
239
+ preheaderIndex: { type: Number },
221
240
  failureReason: { type: String }
222
241
  },
223
242
  {
@@ -442,7 +461,7 @@ var TemplateRenderService = class {
442
461
  const textBodyFn = textBody ? Handlebars__default.default.compile(textBody, { strict: true }) : void 0;
443
462
  return { subjectFn, bodyFn, textBodyFn };
444
463
  }
445
- compileBatchVariants(subjects, bodies, textBody) {
464
+ compileBatchVariants(subjects, bodies, textBody, preheaders) {
446
465
  const subjectFns = subjects.map((s) => Handlebars__default.default.compile(s, { strict: true }));
447
466
  const bodyFns = bodies.map((b) => {
448
467
  const mjmlSource = wrapInMjml(b);
@@ -450,7 +469,8 @@ var TemplateRenderService = class {
450
469
  return Handlebars__default.default.compile(htmlWithHandlebars, { strict: true });
451
470
  });
452
471
  const textBodyFn = textBody ? Handlebars__default.default.compile(textBody, { strict: true }) : void 0;
453
- return { subjectFns, bodyFns, textBodyFn };
472
+ const preheaderFns = preheaders && preheaders.length > 0 ? preheaders.map((p) => Handlebars__default.default.compile(p, { strict: true })) : void 0;
473
+ return { subjectFns, bodyFns, textBodyFn, preheaderFns };
454
474
  }
455
475
  renderFromCompiled(compiled, data) {
456
476
  const subject = compiled.subjectFn(data);
@@ -562,8 +582,10 @@ var UPDATEABLE_FIELDS = /* @__PURE__ */ new Set([
562
582
  "textBody",
563
583
  "subjects",
564
584
  "bodies",
585
+ "preheaders",
565
586
  "variables",
566
- "isActive"
587
+ "isActive",
588
+ "fields"
567
589
  ]);
568
590
  function slugify(name) {
569
591
  return name.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -603,7 +625,7 @@ var TemplateService = class {
603
625
  throw new TemplateSyntaxError(`Template validation failed: ${validation.errors.join("; ")}`, validation.errors);
604
626
  }
605
627
  }
606
- const allContent = [...subjects, ...bodies, input.textBody || ""].join(" ");
628
+ const allContent = [...subjects, ...bodies, ...input.preheaders || [], input.textBody || ""].join(" ");
607
629
  const variables = input.variables || this.renderService.extractVariables(allContent);
608
630
  return this.EmailTemplate.createTemplate({
609
631
  ...input,
@@ -631,11 +653,12 @@ var TemplateService = class {
631
653
  }
632
654
  }
633
655
  }
634
- if (input.textBody || input.subjects || input.bodies) {
656
+ if (input.textBody || input.subjects || input.bodies || input.preheaders) {
635
657
  const subjects = input.subjects ?? template.subjects;
636
658
  const bodies = input.bodies ?? template.bodies;
659
+ const preheaders = input.preheaders ?? template.preheaders ?? [];
637
660
  const textBody = input.textBody ?? template.textBody;
638
- const allContent = [...subjects, ...bodies, textBody || ""].join(" ");
661
+ const allContent = [...subjects, ...bodies, ...preheaders, textBody || ""].join(" ");
639
662
  input.variables = this.renderService.extractVariables(allContent);
640
663
  }
641
664
  const setFields = {};
@@ -645,7 +668,7 @@ var TemplateService = class {
645
668
  }
646
669
  }
647
670
  const update = { $set: setFields };
648
- if (input.textBody || input.subjects || input.bodies) {
671
+ if (input.textBody || input.subjects || input.bodies || input.preheaders) {
649
672
  update["$inc"] = { version: 1 };
650
673
  }
651
674
  return this.EmailTemplate.findByIdAndUpdate(
@@ -727,7 +750,9 @@ var UPDATEABLE_FIELDS2 = /* @__PURE__ */ new Set([
727
750
  "autoApprove",
728
751
  "maxPerRun",
729
752
  "bypassThrottle",
730
- "emailType"
753
+ "emailType",
754
+ "validFrom",
755
+ "validTill"
731
756
  ]);
732
757
  function validateRuleTemplateCompat(targetRole, targetPlatform, template) {
733
758
  const templateAudience = template.audience;
@@ -895,6 +920,21 @@ var RedisLock = class {
895
920
  var MS_PER_DAY = 864e5;
896
921
  var DEFAULT_LOCK_TTL_MS = 30 * 60 * 1e3;
897
922
  var IDENTIFIER_CHUNK_SIZE = 50;
923
+ function getLocalDate(date, timezone) {
924
+ if (!timezone) return date;
925
+ const parts = new Intl.DateTimeFormat("en-US", {
926
+ timeZone: timezone,
927
+ year: "numeric",
928
+ month: "2-digit",
929
+ day: "2-digit",
930
+ hour: "2-digit",
931
+ minute: "2-digit",
932
+ second: "2-digit",
933
+ hour12: false
934
+ }).formatToParts(date);
935
+ const get = (type) => parts.find((p) => p.type === type)?.value || "0";
936
+ return /* @__PURE__ */ new Date(`${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}`);
937
+ }
898
938
  async function processInChunks(items, fn, chunkSize) {
899
939
  const results = [];
900
940
  for (let i = 0; i < items.length; i += chunkSize) {
@@ -966,7 +1006,22 @@ var RuleRunnerService = class {
966
1006
  let runStatus = "completed";
967
1007
  try {
968
1008
  const throttleConfig = await this.EmailThrottleConfig.getConfig();
969
- const activeRules = await this.EmailRule.findActive();
1009
+ const allActiveRules = await this.EmailRule.findActive();
1010
+ const now = /* @__PURE__ */ new Date();
1011
+ const tz = this.config.options?.sendWindow?.timezone;
1012
+ const activeRules = allActiveRules.filter((rule) => {
1013
+ if (rule.validFrom) {
1014
+ const localNow = getLocalDate(now, tz);
1015
+ const localValidFrom = getLocalDate(new Date(rule.validFrom), tz);
1016
+ if (localNow < localValidFrom) return false;
1017
+ }
1018
+ if (rule.validTill) {
1019
+ const localNow = getLocalDate(now, tz);
1020
+ const localValidTill = getLocalDate(new Date(rule.validTill), tz);
1021
+ if (localNow > localValidTill) return false;
1022
+ }
1023
+ return true;
1024
+ });
970
1025
  this.config.hooks?.onRunStart?.({ rulesCount: activeRules.length, triggeredBy });
971
1026
  await this.updateRunProgress(runId, {
972
1027
  progress: { rulesTotal: activeRules.length, rulesCompleted: 0, sent: 0, failed: 0, skipped: 0, invalid: 0 }
@@ -1121,10 +1176,12 @@ var RuleRunnerService = class {
1121
1176
  sendMap.set(uid, send);
1122
1177
  }
1123
1178
  }
1179
+ const preheaders = template.preheaders || [];
1124
1180
  const compiledVariants = this.templateRenderer.compileBatchVariants(
1125
1181
  template.subjects,
1126
1182
  template.bodies,
1127
- template.textBody
1183
+ template.textBody,
1184
+ preheaders
1128
1185
  );
1129
1186
  let totalProcessed = 0;
1130
1187
  for (let i = 0; i < emailsToProcess.length; i++) {
@@ -1170,7 +1227,8 @@ var RuleRunnerService = class {
1170
1227
  continue;
1171
1228
  }
1172
1229
  const user = { _id: identifier.id, email };
1173
- const templateData = this.config.adapters.resolveData(user);
1230
+ const resolvedData = this.config.adapters.resolveData(user);
1231
+ const templateData = { ...template.fields || {}, ...resolvedData };
1174
1232
  const si = Math.floor(Math.random() * compiledVariants.subjectFns.length);
1175
1233
  const bi = Math.floor(Math.random() * compiledVariants.bodyFns.length);
1176
1234
  const renderedSubject = compiledVariants.subjectFns[si](templateData);
@@ -1179,6 +1237,15 @@ var RuleRunnerService = class {
1179
1237
  let finalHtml = renderedHtml;
1180
1238
  let finalText = renderedText;
1181
1239
  let finalSubject = renderedSubject;
1240
+ let pi;
1241
+ if (compiledVariants.preheaderFns && compiledVariants.preheaderFns.length > 0) {
1242
+ pi = Math.floor(Math.random() * compiledVariants.preheaderFns.length);
1243
+ const renderedPreheader = compiledVariants.preheaderFns[pi](templateData);
1244
+ if (renderedPreheader) {
1245
+ const preheaderHtml = `<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">${renderedPreheader}</div>`;
1246
+ finalHtml = finalHtml.replace(/(<body[^>]*>)/i, `$1${preheaderHtml}`);
1247
+ }
1248
+ }
1182
1249
  if (this.config.hooks?.beforeSend) {
1183
1250
  try {
1184
1251
  const modified = await this.config.hooks.beforeSend({
@@ -1221,7 +1288,7 @@ var RuleRunnerService = class {
1221
1288
  dedupKey,
1222
1289
  identifier.id,
1223
1290
  void 0,
1224
- { status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi }
1291
+ { status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi, preheaderIndex: pi }
1225
1292
  );
1226
1293
  const current = throttleMap.get(dedupKey) || { today: 0, thisWeek: 0, lastSentDate: null };
1227
1294
  throttleMap.set(dedupKey, {
@@ -1253,6 +1320,21 @@ var RuleRunnerService = class {
1253
1320
  $set: { lastRunAt: /* @__PURE__ */ new Date(), lastRunStats: stats },
1254
1321
  $inc: { totalSent: stats.sent, totalSkipped: stats.skipped }
1255
1322
  });
1323
+ if (rule.sendOnce) {
1324
+ const allIdentifiers = rule.target.identifiers || [];
1325
+ const totalIdentifiers = new Set(allIdentifiers.map((e) => e.toLowerCase().trim()).filter(Boolean)).size;
1326
+ const sends = await this.EmailRuleSend.find({
1327
+ ruleId: rule._id
1328
+ }).lean();
1329
+ const sentOrProcessedIds = new Set(
1330
+ sends.filter((s) => s.status !== "throttled").map((s) => String(s.userId || s.emailIdentifierId))
1331
+ );
1332
+ const throttledCount = sends.filter((s) => s.status === "throttled").length;
1333
+ if (sentOrProcessedIds.size >= totalIdentifiers && throttledCount === 0) {
1334
+ await this.EmailRule.findByIdAndUpdate(rule._id, { $set: { isActive: false } });
1335
+ this.logger.info(`Rule '${rule.name}' auto-disabled \u2014 all identifiers processed`);
1336
+ }
1337
+ }
1256
1338
  this.config.hooks?.onRuleComplete?.({ ruleId, ruleName: rule.name, stats });
1257
1339
  return stats;
1258
1340
  }
@@ -1293,10 +1375,12 @@ var RuleRunnerService = class {
1293
1375
  identifierMap.set(result.email, { id: result.id, contactId: result.contactId });
1294
1376
  }
1295
1377
  }
1378
+ const preheadersQ = template.preheaders || [];
1296
1379
  const compiledVariants = this.templateRenderer.compileBatchVariants(
1297
1380
  template.subjects,
1298
1381
  template.bodies,
1299
- template.textBody
1382
+ template.textBody,
1383
+ preheadersQ
1300
1384
  );
1301
1385
  const ruleId = rule._id.toString();
1302
1386
  const templateId = rule.templateId.toString();
@@ -1349,7 +1433,8 @@ var RuleRunnerService = class {
1349
1433
  this.config.hooks?.onSend?.({ ruleId, ruleName: rule.name, email, status: "skipped" });
1350
1434
  continue;
1351
1435
  }
1352
- const templateData = this.config.adapters.resolveData(user);
1436
+ const resolvedData = this.config.adapters.resolveData(user);
1437
+ const templateData = { ...template.fields || {}, ...resolvedData };
1353
1438
  const si = Math.floor(Math.random() * compiledVariants.subjectFns.length);
1354
1439
  const bi = Math.floor(Math.random() * compiledVariants.bodyFns.length);
1355
1440
  const renderedSubject = compiledVariants.subjectFns[si](templateData);
@@ -1358,6 +1443,15 @@ var RuleRunnerService = class {
1358
1443
  let finalHtml = renderedHtml;
1359
1444
  let finalText = renderedText;
1360
1445
  let finalSubject = renderedSubject;
1446
+ let pi;
1447
+ if (compiledVariants.preheaderFns && compiledVariants.preheaderFns.length > 0) {
1448
+ pi = Math.floor(Math.random() * compiledVariants.preheaderFns.length);
1449
+ const renderedPreheader = compiledVariants.preheaderFns[pi](templateData);
1450
+ if (renderedPreheader) {
1451
+ const preheaderHtml = `<div style="display:none;font-size:1px;color:#ffffff;line-height:1px;max-height:0px;max-width:0px;opacity:0;overflow:hidden;">${renderedPreheader}</div>`;
1452
+ finalHtml = finalHtml.replace(/(<body[^>]*>)/i, `$1${preheaderHtml}`);
1453
+ }
1454
+ }
1361
1455
  if (this.config.hooks?.beforeSend) {
1362
1456
  try {
1363
1457
  const modified = await this.config.hooks.beforeSend({
@@ -1400,7 +1494,7 @@ var RuleRunnerService = class {
1400
1494
  userId,
1401
1495
  identifier.id,
1402
1496
  void 0,
1403
- { status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi }
1497
+ { status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi, preheaderIndex: pi }
1404
1498
  );
1405
1499
  const current = throttleMap.get(userId) || { today: 0, thisWeek: 0, lastSentDate: null };
1406
1500
  throttleMap.set(userId, {
@@ -1629,7 +1723,7 @@ function createTemplateController(templateService, options) {
1629
1723
  }
1630
1724
  async function create(req, res) {
1631
1725
  try {
1632
- const { name, subjects, bodies, category, audience, platform } = req.body;
1726
+ const { name, subjects, bodies, category, audience, platform, preheaders } = req.body;
1633
1727
  if (!name || !subjects || subjects.length === 0 || !bodies || bodies.length === 0 || !category || !audience || !platform) {
1634
1728
  return res.status(400).json({ success: false, error: "name, subjects, bodies, category, audience, and platform are required" });
1635
1729
  }