@contentgrowth/content-emailing 0.6.2 → 0.7.2

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
@@ -29,11 +29,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
29
29
  // src/index.js
30
30
  var index_exports = {};
31
31
  __export(index_exports, {
32
+ EmailLogger: () => EmailLogger,
32
33
  EmailService: () => EmailService,
33
34
  EmailingCacheDO: () => EmailingCacheDO,
34
35
  TemplateEditor: () => TemplateEditor,
35
36
  TemplateManager: () => TemplateManager,
36
37
  createDOCacheProvider: () => createDOCacheProvider,
38
+ createEmailLoggerCallback: () => createEmailLoggerCallback,
37
39
  createEmailRoutes: () => createEmailRoutes,
38
40
  createTemplateRoutes: () => createTemplateRoutes,
39
41
  createTrackingRoutes: () => createTrackingRoutes,
@@ -568,6 +570,226 @@ function createDOCacheProvider(doStub, instanceName = "global") {
568
570
  };
569
571
  }
570
572
 
573
+ // src/backend/EmailLogger.js
574
+ var EmailLogger = class {
575
+ /**
576
+ * @param {Object} db - D1 database binding
577
+ * @param {Object} options - Configuration options
578
+ * @param {string} [options.tableName='system_email_logs'] - Table name for logs
579
+ */
580
+ constructor(db, options = {}) {
581
+ this.db = db;
582
+ this.tableName = options.tableName || "system_email_logs";
583
+ }
584
+ /**
585
+ * Creates a logger callback function for use with EmailService.
586
+ * Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
587
+ */
588
+ createCallback() {
589
+ return async (entry) => {
590
+ await this.log(entry);
591
+ };
592
+ }
593
+ /**
594
+ * Log an email event (pending, sent, or failed)
595
+ * @param {Object} entry - Log entry
596
+ */
597
+ async log(entry) {
598
+ const {
599
+ event,
600
+ recipientEmail,
601
+ recipientUserId,
602
+ templateId,
603
+ subject,
604
+ provider,
605
+ messageId,
606
+ batchId,
607
+ error,
608
+ errorCode,
609
+ metadata
610
+ } = entry;
611
+ try {
612
+ if (event === "pending") {
613
+ const id = crypto.randomUUID().replace(/-/g, "");
614
+ await this.db.prepare(`
615
+ INSERT INTO ${this.tableName}
616
+ (id, batch_id, recipient_email, recipient_user_id, template_id, subject, status, metadata, created_at)
617
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, strftime('%s', 'now'))
618
+ `).bind(
619
+ id,
620
+ batchId || null,
621
+ recipientEmail,
622
+ recipientUserId || null,
623
+ templateId || "direct",
624
+ subject || null,
625
+ metadata ? JSON.stringify(metadata) : null
626
+ ).run();
627
+ } else if (event === "sent") {
628
+ await this.db.prepare(`
629
+ UPDATE ${this.tableName}
630
+ SET status = 'sent',
631
+ provider = ?,
632
+ provider_message_id = ?,
633
+ sent_at = strftime('%s', 'now')
634
+ WHERE recipient_email = ?
635
+ AND template_id = ?
636
+ AND status = 'pending'
637
+ ORDER BY created_at DESC
638
+ LIMIT 1
639
+ `).bind(
640
+ provider || null,
641
+ messageId || null,
642
+ recipientEmail,
643
+ templateId || "direct"
644
+ ).run();
645
+ } else if (event === "failed") {
646
+ await this.db.prepare(`
647
+ UPDATE ${this.tableName}
648
+ SET status = 'failed',
649
+ provider = ?,
650
+ error_message = ?,
651
+ error_code = ?
652
+ WHERE recipient_email = ?
653
+ AND template_id = ?
654
+ AND status = 'pending'
655
+ ORDER BY created_at DESC
656
+ LIMIT 1
657
+ `).bind(
658
+ provider || null,
659
+ error || null,
660
+ errorCode || null,
661
+ recipientEmail,
662
+ templateId || "direct"
663
+ ).run();
664
+ }
665
+ } catch (e) {
666
+ console.error("[EmailLogger] Failed to log:", e);
667
+ }
668
+ }
669
+ /**
670
+ * Query email logs with filtering
671
+ * @param {Object} options - Query options
672
+ */
673
+ async query(options = {}) {
674
+ const {
675
+ recipientEmail,
676
+ recipientUserId,
677
+ templateId,
678
+ status,
679
+ batchId,
680
+ limit = 50,
681
+ offset = 0
682
+ } = options;
683
+ const conditions = [];
684
+ const bindings = [];
685
+ if (recipientEmail) {
686
+ conditions.push("recipient_email = ?");
687
+ bindings.push(recipientEmail);
688
+ }
689
+ if (recipientUserId) {
690
+ conditions.push("recipient_user_id = ?");
691
+ bindings.push(recipientUserId);
692
+ }
693
+ if (templateId) {
694
+ conditions.push("template_id = ?");
695
+ bindings.push(templateId);
696
+ }
697
+ if (status) {
698
+ conditions.push("status = ?");
699
+ bindings.push(status);
700
+ }
701
+ if (batchId) {
702
+ conditions.push("batch_id = ?");
703
+ bindings.push(batchId);
704
+ }
705
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
706
+ const countResult = await this.db.prepare(
707
+ `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
708
+ ).bind(...bindings).first();
709
+ const { results } = await this.db.prepare(`
710
+ SELECT id, batch_id, recipient_email, recipient_user_id, template_id, subject,
711
+ status, provider, provider_message_id, error_message, error_code, metadata,
712
+ created_at, sent_at
713
+ FROM ${this.tableName}
714
+ ${whereClause}
715
+ ORDER BY created_at DESC
716
+ LIMIT ? OFFSET ?
717
+ `).bind(...bindings, limit, offset).all();
718
+ const logs = (results || []).map((row) => ({
719
+ id: row.id,
720
+ batchId: row.batch_id,
721
+ recipientEmail: row.recipient_email,
722
+ recipientUserId: row.recipient_user_id,
723
+ templateId: row.template_id,
724
+ subject: row.subject,
725
+ status: row.status,
726
+ provider: row.provider,
727
+ providerMessageId: row.provider_message_id,
728
+ errorMessage: row.error_message,
729
+ errorCode: row.error_code,
730
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
731
+ createdAt: row.created_at,
732
+ sentAt: row.sent_at
733
+ }));
734
+ return { logs, total: countResult?.count || 0 };
735
+ }
736
+ /**
737
+ * Get email sending statistics
738
+ * @param {number} sinceDays - Number of days to look back
739
+ */
740
+ async getStats(sinceDays = 7) {
741
+ const sinceTimestamp = Math.floor(Date.now() / 1e3) - sinceDays * 24 * 60 * 60;
742
+ const statusResult = await this.db.prepare(`
743
+ SELECT status, COUNT(*) as count
744
+ FROM ${this.tableName}
745
+ WHERE created_at >= ?
746
+ GROUP BY status
747
+ `).bind(sinceTimestamp).all();
748
+ const templateResult = await this.db.prepare(`
749
+ SELECT template_id, COUNT(*) as count
750
+ FROM ${this.tableName}
751
+ WHERE created_at >= ?
752
+ GROUP BY template_id
753
+ `).bind(sinceTimestamp).all();
754
+ const stats = {
755
+ total: 0,
756
+ sent: 0,
757
+ failed: 0,
758
+ pending: 0,
759
+ byTemplate: {}
760
+ };
761
+ (statusResult.results || []).forEach((row) => {
762
+ const count = row.count || 0;
763
+ stats.total += count;
764
+ if (row.status === "sent") stats.sent = count;
765
+ if (row.status === "failed") stats.failed = count;
766
+ if (row.status === "pending") stats.pending = count;
767
+ });
768
+ (templateResult.results || []).forEach((row) => {
769
+ stats.byTemplate[row.template_id] = row.count;
770
+ });
771
+ return stats;
772
+ }
773
+ /**
774
+ * Get recent failed emails for debugging
775
+ * @param {number} limit - Number of failed emails to retrieve
776
+ */
777
+ async getRecentFailures(limit = 20) {
778
+ const { results } = await this.db.prepare(`
779
+ SELECT id, recipient_email, template_id, subject, error_message, error_code, created_at
780
+ FROM ${this.tableName}
781
+ WHERE status = 'failed'
782
+ ORDER BY created_at DESC
783
+ LIMIT ?
784
+ `).bind(limit).all();
785
+ return results || [];
786
+ }
787
+ };
788
+ function createEmailLoggerCallback(db, tableName = "system_email_logs") {
789
+ const logger = new EmailLogger(db, { tableName });
790
+ return logger.createCallback();
791
+ }
792
+
571
793
  // src/backend/EmailService.js
572
794
  var EmailService = class {
573
795
  /**
@@ -575,6 +797,7 @@ var EmailService = class {
575
797
  * @param {Object} config - Configuration options
576
798
  * @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
577
799
  * @param {Object} [config.defaults] - Default settings (fromName, fromAddress)
800
+ * @param {boolean|Function} [config.emailLogger] - Email logger: true (default), false (disabled), or custom callback
578
801
  * @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
579
802
  */
580
803
  constructor(env, config = {}, cacheProvider = null) {
@@ -602,6 +825,16 @@ var EmailService = class {
602
825
  },
603
826
  ...config
604
827
  };
828
+ if (config.emailLogger === false) {
829
+ this.emailLogger = null;
830
+ } else if (typeof config.emailLogger === "function") {
831
+ this.emailLogger = config.emailLogger;
832
+ } else if (env.DB) {
833
+ const logger = new EmailLogger(env.DB);
834
+ this.emailLogger = logger.createCallback();
835
+ } else {
836
+ this.emailLogger = null;
837
+ }
605
838
  if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
606
839
  this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
607
840
  } else {
@@ -851,13 +1084,30 @@ var EmailService = class {
851
1084
  * @param {Object} [params.metadata] - Additional metadata
852
1085
  * @returns {Promise<Object>} Delivery result
853
1086
  */
854
- async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {} }) {
1087
+ async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {}, batchId = null, userId = null }) {
855
1088
  const htmlContent = html || htmlBody;
856
1089
  const textContent = text || textBody;
1090
+ const templateId = metadata?.templateId || "direct";
1091
+ if (this.emailLogger) {
1092
+ try {
1093
+ await this.emailLogger({
1094
+ event: "pending",
1095
+ recipientEmail: to,
1096
+ recipientUserId: userId,
1097
+ templateId,
1098
+ subject,
1099
+ batchId,
1100
+ metadata
1101
+ });
1102
+ } catch (e) {
1103
+ console.warn("[EmailService] emailLogger pending failed:", e);
1104
+ }
1105
+ }
857
1106
  try {
858
1107
  const settings = await this.loadSettings(profile, tenantId);
859
1108
  const useProvider = provider || settings.provider || "mailchannels";
860
1109
  let result;
1110
+ let providerMessageId = null;
861
1111
  switch (useProvider) {
862
1112
  case "mailchannels":
863
1113
  result = await this.sendViaMailChannels(to, subject, htmlContent, textContent, settings, metadata);
@@ -867,22 +1117,91 @@ var EmailService = class {
867
1117
  break;
868
1118
  case "resend":
869
1119
  result = await this.sendViaResend(to, subject, htmlContent, textContent, settings, metadata);
1120
+ if (result && typeof result === "object" && result.id) {
1121
+ providerMessageId = result.id;
1122
+ result = true;
1123
+ }
870
1124
  break;
871
1125
  case "sendpulse":
872
1126
  result = await this.sendViaSendPulse(to, subject, htmlContent, textContent, settings, metadata);
873
1127
  break;
874
1128
  default:
875
1129
  console.error(`[EmailService] Unknown provider: ${useProvider}`);
1130
+ if (this.emailLogger) {
1131
+ try {
1132
+ await this.emailLogger({
1133
+ event: "failed",
1134
+ recipientEmail: to,
1135
+ recipientUserId: userId,
1136
+ templateId,
1137
+ subject,
1138
+ provider: useProvider,
1139
+ batchId,
1140
+ error: `Unknown email provider: ${useProvider}`,
1141
+ metadata
1142
+ });
1143
+ } catch (e) {
1144
+ }
1145
+ }
876
1146
  return { success: false, error: `Unknown email provider: ${useProvider}` };
877
1147
  }
878
1148
  if (result) {
879
- return { success: true, messageId: crypto.randomUUID() };
1149
+ const messageId = providerMessageId || crypto.randomUUID();
1150
+ if (this.emailLogger) {
1151
+ try {
1152
+ await this.emailLogger({
1153
+ event: "sent",
1154
+ recipientEmail: to,
1155
+ recipientUserId: userId,
1156
+ templateId,
1157
+ subject,
1158
+ provider: useProvider,
1159
+ messageId,
1160
+ batchId,
1161
+ metadata
1162
+ });
1163
+ } catch (e) {
1164
+ console.warn("[EmailService] emailLogger sent failed:", e);
1165
+ }
1166
+ }
1167
+ return { success: true, messageId };
880
1168
  } else {
881
1169
  console.error("[EmailService] Failed to send email to:", to);
1170
+ if (this.emailLogger) {
1171
+ try {
1172
+ await this.emailLogger({
1173
+ event: "failed",
1174
+ recipientEmail: to,
1175
+ recipientUserId: userId,
1176
+ templateId,
1177
+ subject,
1178
+ provider: useProvider,
1179
+ batchId,
1180
+ error: "Failed to send email",
1181
+ metadata
1182
+ });
1183
+ } catch (e) {
1184
+ }
1185
+ }
882
1186
  return { success: false, error: "Failed to send email" };
883
1187
  }
884
1188
  } catch (error) {
885
1189
  console.error("[EmailService] Error sending email:", error);
1190
+ if (this.emailLogger) {
1191
+ try {
1192
+ await this.emailLogger({
1193
+ event: "failed",
1194
+ recipientEmail: to,
1195
+ recipientUserId: userId,
1196
+ templateId,
1197
+ subject,
1198
+ batchId,
1199
+ error: error.message,
1200
+ metadata
1201
+ });
1202
+ } catch (e) {
1203
+ }
1204
+ }
886
1205
  return { success: false, error: error.message };
887
1206
  }
888
1207
  }
@@ -1742,7 +2061,7 @@ var TemplateManager = ({
1742
2061
  },
1743
2062
  /* @__PURE__ */ import_react3.default.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ import_react3.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 4v16m8-8H4" })),
1744
2063
  "Create Template"
1745
- )), error && /* @__PURE__ */ import_react3.default.createElement("div", { className: "mb-4 p-3 rounded-lg bg-red-50 text-red-800 border border-red-200" }, error, /* @__PURE__ */ import_react3.default.createElement("button", { onClick: () => setError(null), className: "ml-2 underline" }, "Dismiss")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "grid grid-cols-4 gap-4 mb-6" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-gray-900" }, templates.length), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Total")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-green-600" }, templates.filter((t) => t.is_active).length), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Active")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-yellow-600" }, templates.filter((t) => !t.is_active).length), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Inactive")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-blue-600" }, new Set(templates.map((t) => t.template_type)).size), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Types"))), /* @__PURE__ */ import_react3.default.createElement("div", { className: "flex gap-2 mb-6 border-b border-gray-200 pb-3 flex-wrap" }, uniqueTypes.map((type) => /* @__PURE__ */ import_react3.default.createElement(
2064
+ )), error && /* @__PURE__ */ import_react3.default.createElement("div", { className: "mb-4 p-3 rounded-lg bg-red-50 text-red-800 border border-red-200" }, error, /* @__PURE__ */ import_react3.default.createElement("button", { onClick: () => setError(null), className: "ml-2 underline" }, "Dismiss")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "flex gap-4 mb-6" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4 flex-1" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-gray-900" }, templates.length), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Total")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4 flex-1" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-green-600" }, templates.filter((t) => t.is_active).length), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Active")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4 flex-1" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-yellow-600" }, templates.filter((t) => !t.is_active).length), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Inactive")), /* @__PURE__ */ import_react3.default.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4 flex-1" }, /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-2xl font-bold text-blue-600" }, new Set(templates.map((t) => t.template_type)).size), /* @__PURE__ */ import_react3.default.createElement("div", { className: "text-sm text-gray-500" }, "Types"))), /* @__PURE__ */ import_react3.default.createElement("div", { className: "flex gap-2 mb-6 border-b border-gray-200 pb-3 flex-wrap" }, uniqueTypes.map((type) => /* @__PURE__ */ import_react3.default.createElement(
1746
2065
  "button",
1747
2066
  {
1748
2067
  key: type,
@@ -1867,11 +2186,13 @@ var TemplateManager = ({
1867
2186
  };
1868
2187
  // Annotate the CommonJS export names for ESM import in node:
1869
2188
  0 && (module.exports = {
2189
+ EmailLogger,
1870
2190
  EmailService,
1871
2191
  EmailingCacheDO,
1872
2192
  TemplateEditor,
1873
2193
  TemplateManager,
1874
2194
  createDOCacheProvider,
2195
+ createEmailLoggerCallback,
1875
2196
  createEmailRoutes,
1876
2197
  createTemplateRoutes,
1877
2198
  createTrackingRoutes,