@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 +12 -15
- package/dist/index.d.mts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +110 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +110 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
-
##
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|