@astralibx/email-rule-engine 6.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/dist/index.mjs CHANGED
@@ -71,7 +71,18 @@ function createEmailTemplateSchema(platformValues, audienceValues, categoryValue
71
71
  textBody: String,
72
72
  subjects: { type: [{ type: String }], required: true, validate: [(v) => v.length >= 1, "At least one subject is required"] },
73
73
  bodies: { type: [{ type: String }], required: true, validate: [(v) => v.length >= 1, "At least one body is required"] },
74
- fields: { type: Schema.Types.Mixed, default: {} },
74
+ preheaders: [{ type: String }],
75
+ fields: {
76
+ type: Schema.Types.Mixed,
77
+ default: {},
78
+ validate: {
79
+ validator: (v) => {
80
+ if (!v || typeof v !== "object") return true;
81
+ return Object.values(v).every((val) => typeof val === "string");
82
+ },
83
+ message: "All field values must be strings"
84
+ }
85
+ },
75
86
  variables: [{ type: String }],
76
87
  version: { type: Number, default: 1 },
77
88
  isActive: { type: Boolean, default: true, index: true }
@@ -106,6 +117,7 @@ function createEmailTemplateSchema(platformValues, audienceValues, categoryValue
106
117
  textBody: input.textBody,
107
118
  subjects: input.subjects,
108
119
  bodies: input.bodies,
120
+ preheaders: input.preheaders || [],
109
121
  fields: input.fields || {},
110
122
  variables: input.variables || [],
111
123
  version: 1,
@@ -216,6 +228,7 @@ function createEmailRuleSendSchema(collectionPrefix) {
216
228
  subject: { type: String },
217
229
  subjectIndex: { type: Number },
218
230
  bodyIndex: { type: Number },
231
+ preheaderIndex: { type: Number },
219
232
  failureReason: { type: String }
220
233
  },
221
234
  {
@@ -440,7 +453,7 @@ var TemplateRenderService = class {
440
453
  const textBodyFn = textBody ? Handlebars.compile(textBody, { strict: true }) : void 0;
441
454
  return { subjectFn, bodyFn, textBodyFn };
442
455
  }
443
- compileBatchVariants(subjects, bodies, textBody) {
456
+ compileBatchVariants(subjects, bodies, textBody, preheaders) {
444
457
  const subjectFns = subjects.map((s) => Handlebars.compile(s, { strict: true }));
445
458
  const bodyFns = bodies.map((b) => {
446
459
  const mjmlSource = wrapInMjml(b);
@@ -448,7 +461,8 @@ var TemplateRenderService = class {
448
461
  return Handlebars.compile(htmlWithHandlebars, { strict: true });
449
462
  });
450
463
  const textBodyFn = textBody ? Handlebars.compile(textBody, { strict: true }) : void 0;
451
- return { subjectFns, bodyFns, textBodyFn };
464
+ const preheaderFns = preheaders && preheaders.length > 0 ? preheaders.map((p) => Handlebars.compile(p, { strict: true })) : void 0;
465
+ return { subjectFns, bodyFns, textBodyFn, preheaderFns };
452
466
  }
453
467
  renderFromCompiled(compiled, data) {
454
468
  const subject = compiled.subjectFn(data);
@@ -560,6 +574,7 @@ var UPDATEABLE_FIELDS = /* @__PURE__ */ new Set([
560
574
  "textBody",
561
575
  "subjects",
562
576
  "bodies",
577
+ "preheaders",
563
578
  "variables",
564
579
  "isActive",
565
580
  "fields"
@@ -602,7 +617,7 @@ var TemplateService = class {
602
617
  throw new TemplateSyntaxError(`Template validation failed: ${validation.errors.join("; ")}`, validation.errors);
603
618
  }
604
619
  }
605
- const allContent = [...subjects, ...bodies, input.textBody || ""].join(" ");
620
+ const allContent = [...subjects, ...bodies, ...input.preheaders || [], input.textBody || ""].join(" ");
606
621
  const variables = input.variables || this.renderService.extractVariables(allContent);
607
622
  return this.EmailTemplate.createTemplate({
608
623
  ...input,
@@ -630,11 +645,12 @@ var TemplateService = class {
630
645
  }
631
646
  }
632
647
  }
633
- if (input.textBody || input.subjects || input.bodies) {
648
+ if (input.textBody || input.subjects || input.bodies || input.preheaders) {
634
649
  const subjects = input.subjects ?? template.subjects;
635
650
  const bodies = input.bodies ?? template.bodies;
651
+ const preheaders = input.preheaders ?? template.preheaders ?? [];
636
652
  const textBody = input.textBody ?? template.textBody;
637
- const allContent = [...subjects, ...bodies, textBody || ""].join(" ");
653
+ const allContent = [...subjects, ...bodies, ...preheaders, textBody || ""].join(" ");
638
654
  input.variables = this.renderService.extractVariables(allContent);
639
655
  }
640
656
  const setFields = {};
@@ -644,7 +660,7 @@ var TemplateService = class {
644
660
  }
645
661
  }
646
662
  const update = { $set: setFields };
647
- if (input.textBody || input.subjects || input.bodies) {
663
+ if (input.textBody || input.subjects || input.bodies || input.preheaders) {
648
664
  update["$inc"] = { version: 1 };
649
665
  }
650
666
  return this.EmailTemplate.findByIdAndUpdate(
@@ -896,6 +912,21 @@ var RedisLock = class {
896
912
  var MS_PER_DAY = 864e5;
897
913
  var DEFAULT_LOCK_TTL_MS = 30 * 60 * 1e3;
898
914
  var IDENTIFIER_CHUNK_SIZE = 50;
915
+ function getLocalDate(date, timezone) {
916
+ if (!timezone) return date;
917
+ const parts = new Intl.DateTimeFormat("en-US", {
918
+ timeZone: timezone,
919
+ year: "numeric",
920
+ month: "2-digit",
921
+ day: "2-digit",
922
+ hour: "2-digit",
923
+ minute: "2-digit",
924
+ second: "2-digit",
925
+ hour12: false
926
+ }).formatToParts(date);
927
+ const get = (type) => parts.find((p) => p.type === type)?.value || "0";
928
+ return /* @__PURE__ */ new Date(`${get("year")}-${get("month")}-${get("day")}T${get("hour")}:${get("minute")}:${get("second")}`);
929
+ }
899
930
  async function processInChunks(items, fn, chunkSize) {
900
931
  const results = [];
901
932
  for (let i = 0; i < items.length; i += chunkSize) {
@@ -969,9 +1000,18 @@ var RuleRunnerService = class {
969
1000
  const throttleConfig = await this.EmailThrottleConfig.getConfig();
970
1001
  const allActiveRules = await this.EmailRule.findActive();
971
1002
  const now = /* @__PURE__ */ new Date();
1003
+ const tz = this.config.options?.sendWindow?.timezone;
972
1004
  const activeRules = allActiveRules.filter((rule) => {
973
- if (rule.validFrom && now < new Date(rule.validFrom)) return false;
974
- if (rule.validTill && now > new Date(rule.validTill)) return false;
1005
+ if (rule.validFrom) {
1006
+ const localNow = getLocalDate(now, tz);
1007
+ const localValidFrom = getLocalDate(new Date(rule.validFrom), tz);
1008
+ if (localNow < localValidFrom) return false;
1009
+ }
1010
+ if (rule.validTill) {
1011
+ const localNow = getLocalDate(now, tz);
1012
+ const localValidTill = getLocalDate(new Date(rule.validTill), tz);
1013
+ if (localNow > localValidTill) return false;
1014
+ }
975
1015
  return true;
976
1016
  });
977
1017
  this.config.hooks?.onRunStart?.({ rulesCount: activeRules.length, triggeredBy });
@@ -1128,10 +1168,12 @@ var RuleRunnerService = class {
1128
1168
  sendMap.set(uid, send);
1129
1169
  }
1130
1170
  }
1171
+ const preheaders = template.preheaders || [];
1131
1172
  const compiledVariants = this.templateRenderer.compileBatchVariants(
1132
1173
  template.subjects,
1133
1174
  template.bodies,
1134
- template.textBody
1175
+ template.textBody,
1176
+ preheaders
1135
1177
  );
1136
1178
  let totalProcessed = 0;
1137
1179
  for (let i = 0; i < emailsToProcess.length; i++) {
@@ -1187,6 +1229,15 @@ var RuleRunnerService = class {
1187
1229
  let finalHtml = renderedHtml;
1188
1230
  let finalText = renderedText;
1189
1231
  let finalSubject = renderedSubject;
1232
+ let pi;
1233
+ if (compiledVariants.preheaderFns && compiledVariants.preheaderFns.length > 0) {
1234
+ pi = Math.floor(Math.random() * compiledVariants.preheaderFns.length);
1235
+ const renderedPreheader = compiledVariants.preheaderFns[pi](templateData);
1236
+ if (renderedPreheader) {
1237
+ 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>`;
1238
+ finalHtml = finalHtml.replace(/(<body[^>]*>)/i, `$1${preheaderHtml}`);
1239
+ }
1240
+ }
1190
1241
  if (this.config.hooks?.beforeSend) {
1191
1242
  try {
1192
1243
  const modified = await this.config.hooks.beforeSend({
@@ -1229,7 +1280,7 @@ var RuleRunnerService = class {
1229
1280
  dedupKey,
1230
1281
  identifier.id,
1231
1282
  void 0,
1232
- { status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi }
1283
+ { status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi, preheaderIndex: pi }
1233
1284
  );
1234
1285
  const current = throttleMap.get(dedupKey) || { today: 0, thisWeek: 0, lastSentDate: null };
1235
1286
  throttleMap.set(dedupKey, {
@@ -1262,24 +1313,16 @@ var RuleRunnerService = class {
1262
1313
  $inc: { totalSent: stats.sent, totalSkipped: stats.skipped }
1263
1314
  });
1264
1315
  if (rule.sendOnce) {
1265
- const allIdentifiers = rule.target.identifiers;
1316
+ const allIdentifiers = rule.target.identifiers || [];
1317
+ const totalIdentifiers = new Set(allIdentifiers.map((e) => e.toLowerCase().trim()).filter(Boolean)).size;
1266
1318
  const sends = await this.EmailRuleSend.find({
1267
1319
  ruleId: rule._id
1268
1320
  }).lean();
1269
1321
  const sentOrProcessedIds = new Set(
1270
1322
  sends.filter((s) => s.status !== "throttled").map((s) => String(s.userId || s.emailIdentifierId))
1271
1323
  );
1272
- let pendingCount = 0;
1273
- for (const email of allIdentifiers) {
1274
- const identifier = identifierMap.get(email.toLowerCase().trim());
1275
- if (!identifier) continue;
1276
- if (!sentOrProcessedIds.has(String(identifier.id))) {
1277
- pendingCount++;
1278
- }
1279
- }
1280
1324
  const throttledCount = sends.filter((s) => s.status === "throttled").length;
1281
- pendingCount += throttledCount;
1282
- if (pendingCount === 0) {
1325
+ if (sentOrProcessedIds.size >= totalIdentifiers && throttledCount === 0) {
1283
1326
  await this.EmailRule.findByIdAndUpdate(rule._id, { $set: { isActive: false } });
1284
1327
  this.logger.info(`Rule '${rule.name}' auto-disabled \u2014 all identifiers processed`);
1285
1328
  }
@@ -1324,10 +1367,12 @@ var RuleRunnerService = class {
1324
1367
  identifierMap.set(result.email, { id: result.id, contactId: result.contactId });
1325
1368
  }
1326
1369
  }
1370
+ const preheadersQ = template.preheaders || [];
1327
1371
  const compiledVariants = this.templateRenderer.compileBatchVariants(
1328
1372
  template.subjects,
1329
1373
  template.bodies,
1330
- template.textBody
1374
+ template.textBody,
1375
+ preheadersQ
1331
1376
  );
1332
1377
  const ruleId = rule._id.toString();
1333
1378
  const templateId = rule.templateId.toString();
@@ -1390,6 +1435,15 @@ var RuleRunnerService = class {
1390
1435
  let finalHtml = renderedHtml;
1391
1436
  let finalText = renderedText;
1392
1437
  let finalSubject = renderedSubject;
1438
+ let pi;
1439
+ if (compiledVariants.preheaderFns && compiledVariants.preheaderFns.length > 0) {
1440
+ pi = Math.floor(Math.random() * compiledVariants.preheaderFns.length);
1441
+ const renderedPreheader = compiledVariants.preheaderFns[pi](templateData);
1442
+ if (renderedPreheader) {
1443
+ 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>`;
1444
+ finalHtml = finalHtml.replace(/(<body[^>]*>)/i, `$1${preheaderHtml}`);
1445
+ }
1446
+ }
1393
1447
  if (this.config.hooks?.beforeSend) {
1394
1448
  try {
1395
1449
  const modified = await this.config.hooks.beforeSend({
@@ -1432,7 +1486,7 @@ var RuleRunnerService = class {
1432
1486
  userId,
1433
1487
  identifier.id,
1434
1488
  void 0,
1435
- { status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi }
1489
+ { status: "sent", accountId: agentSelection.accountId, subject: finalSubject, subjectIndex: si, bodyIndex: bi, preheaderIndex: pi }
1436
1490
  );
1437
1491
  const current = throttleMap.get(userId) || { today: 0, thisWeek: 0, lastSentDate: null };
1438
1492
  throttleMap.set(userId, {
@@ -1661,7 +1715,7 @@ function createTemplateController(templateService, options) {
1661
1715
  }
1662
1716
  async function create(req, res) {
1663
1717
  try {
1664
- const { name, subjects, bodies, category, audience, platform } = req.body;
1718
+ const { name, subjects, bodies, category, audience, platform, preheaders } = req.body;
1665
1719
  if (!name || !subjects || subjects.length === 0 || !bodies || bodies.length === 0 || !category || !audience || !platform) {
1666
1720
  return res.status(400).json({ success: false, error: "name, subjects, bodies, category, audience, and platform are required" });
1667
1721
  }