@contentgrowth/content-emailing 0.7.1 → 0.7.3

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.js CHANGED
@@ -520,6 +520,226 @@ function createDOCacheProvider(doStub, instanceName = "global") {
520
520
  };
521
521
  }
522
522
 
523
+ // src/backend/EmailLogger.js
524
+ var EmailLogger = class {
525
+ /**
526
+ * @param {Object} db - D1 database binding
527
+ * @param {Object} options - Configuration options
528
+ * @param {string} [options.tableName='system_email_logs'] - Table name for logs
529
+ */
530
+ constructor(db, options = {}) {
531
+ this.db = db;
532
+ this.tableName = options.tableName || "system_email_logs";
533
+ }
534
+ /**
535
+ * Creates a logger callback function for use with EmailService.
536
+ * Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
537
+ */
538
+ createCallback() {
539
+ return async (entry) => {
540
+ await this.log(entry);
541
+ };
542
+ }
543
+ /**
544
+ * Log an email event (pending, sent, or failed)
545
+ * @param {Object} entry - Log entry
546
+ */
547
+ async log(entry) {
548
+ const {
549
+ event,
550
+ recipientEmail,
551
+ recipientUserId,
552
+ templateId,
553
+ subject,
554
+ provider,
555
+ messageId,
556
+ batchId,
557
+ error,
558
+ errorCode,
559
+ metadata
560
+ } = entry;
561
+ try {
562
+ if (event === "pending") {
563
+ const id = crypto.randomUUID().replace(/-/g, "");
564
+ await this.db.prepare(`
565
+ INSERT INTO ${this.tableName}
566
+ (id, batch_id, recipient_email, recipient_user_id, template_id, subject, status, metadata, created_at)
567
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, strftime('%s', 'now'))
568
+ `).bind(
569
+ id,
570
+ batchId || null,
571
+ recipientEmail,
572
+ recipientUserId || null,
573
+ templateId || "direct",
574
+ subject || null,
575
+ metadata ? JSON.stringify(metadata) : null
576
+ ).run();
577
+ } else if (event === "sent") {
578
+ await this.db.prepare(`
579
+ UPDATE ${this.tableName}
580
+ SET status = 'sent',
581
+ provider = ?,
582
+ provider_message_id = ?,
583
+ sent_at = strftime('%s', 'now')
584
+ WHERE recipient_email = ?
585
+ AND template_id = ?
586
+ AND status = 'pending'
587
+ ORDER BY created_at DESC
588
+ LIMIT 1
589
+ `).bind(
590
+ provider || null,
591
+ messageId || null,
592
+ recipientEmail,
593
+ templateId || "direct"
594
+ ).run();
595
+ } else if (event === "failed") {
596
+ await this.db.prepare(`
597
+ UPDATE ${this.tableName}
598
+ SET status = 'failed',
599
+ provider = ?,
600
+ error_message = ?,
601
+ error_code = ?
602
+ WHERE recipient_email = ?
603
+ AND template_id = ?
604
+ AND status = 'pending'
605
+ ORDER BY created_at DESC
606
+ LIMIT 1
607
+ `).bind(
608
+ provider || null,
609
+ error || null,
610
+ errorCode || null,
611
+ recipientEmail,
612
+ templateId || "direct"
613
+ ).run();
614
+ }
615
+ } catch (e) {
616
+ console.error("[EmailLogger] Failed to log:", e);
617
+ }
618
+ }
619
+ /**
620
+ * Query email logs with filtering
621
+ * @param {Object} options - Query options
622
+ */
623
+ async query(options = {}) {
624
+ const {
625
+ recipientEmail,
626
+ recipientUserId,
627
+ templateId,
628
+ status,
629
+ batchId,
630
+ limit = 50,
631
+ offset = 0
632
+ } = options;
633
+ const conditions = [];
634
+ const bindings = [];
635
+ if (recipientEmail) {
636
+ conditions.push("recipient_email = ?");
637
+ bindings.push(recipientEmail);
638
+ }
639
+ if (recipientUserId) {
640
+ conditions.push("recipient_user_id = ?");
641
+ bindings.push(recipientUserId);
642
+ }
643
+ if (templateId) {
644
+ conditions.push("template_id = ?");
645
+ bindings.push(templateId);
646
+ }
647
+ if (status) {
648
+ conditions.push("status = ?");
649
+ bindings.push(status);
650
+ }
651
+ if (batchId) {
652
+ conditions.push("batch_id = ?");
653
+ bindings.push(batchId);
654
+ }
655
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
656
+ const countResult = await this.db.prepare(
657
+ `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
658
+ ).bind(...bindings).first();
659
+ const { results } = await this.db.prepare(`
660
+ SELECT id, batch_id, recipient_email, recipient_user_id, template_id, subject,
661
+ status, provider, provider_message_id, error_message, error_code, metadata,
662
+ created_at, sent_at
663
+ FROM ${this.tableName}
664
+ ${whereClause}
665
+ ORDER BY created_at DESC
666
+ LIMIT ? OFFSET ?
667
+ `).bind(...bindings, limit, offset).all();
668
+ const logs = (results || []).map((row) => ({
669
+ id: row.id,
670
+ batchId: row.batch_id,
671
+ recipientEmail: row.recipient_email,
672
+ recipientUserId: row.recipient_user_id,
673
+ templateId: row.template_id,
674
+ subject: row.subject,
675
+ status: row.status,
676
+ provider: row.provider,
677
+ providerMessageId: row.provider_message_id,
678
+ errorMessage: row.error_message,
679
+ errorCode: row.error_code,
680
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
681
+ createdAt: row.created_at,
682
+ sentAt: row.sent_at
683
+ }));
684
+ return { logs, total: countResult?.count || 0 };
685
+ }
686
+ /**
687
+ * Get email sending statistics
688
+ * @param {number} sinceDays - Number of days to look back
689
+ */
690
+ async getStats(sinceDays = 7) {
691
+ const sinceTimestamp = Math.floor(Date.now() / 1e3) - sinceDays * 24 * 60 * 60;
692
+ const statusResult = await this.db.prepare(`
693
+ SELECT status, COUNT(*) as count
694
+ FROM ${this.tableName}
695
+ WHERE created_at >= ?
696
+ GROUP BY status
697
+ `).bind(sinceTimestamp).all();
698
+ const templateResult = await this.db.prepare(`
699
+ SELECT template_id, COUNT(*) as count
700
+ FROM ${this.tableName}
701
+ WHERE created_at >= ?
702
+ GROUP BY template_id
703
+ `).bind(sinceTimestamp).all();
704
+ const stats = {
705
+ total: 0,
706
+ sent: 0,
707
+ failed: 0,
708
+ pending: 0,
709
+ byTemplate: {}
710
+ };
711
+ (statusResult.results || []).forEach((row) => {
712
+ const count = row.count || 0;
713
+ stats.total += count;
714
+ if (row.status === "sent") stats.sent = count;
715
+ if (row.status === "failed") stats.failed = count;
716
+ if (row.status === "pending") stats.pending = count;
717
+ });
718
+ (templateResult.results || []).forEach((row) => {
719
+ stats.byTemplate[row.template_id] = row.count;
720
+ });
721
+ return stats;
722
+ }
723
+ /**
724
+ * Get recent failed emails for debugging
725
+ * @param {number} limit - Number of failed emails to retrieve
726
+ */
727
+ async getRecentFailures(limit = 20) {
728
+ const { results } = await this.db.prepare(`
729
+ SELECT id, recipient_email, template_id, subject, error_message, error_code, created_at
730
+ FROM ${this.tableName}
731
+ WHERE status = 'failed'
732
+ ORDER BY created_at DESC
733
+ LIMIT ?
734
+ `).bind(limit).all();
735
+ return results || [];
736
+ }
737
+ };
738
+ function createEmailLoggerCallback(db, tableName = "system_email_logs") {
739
+ const logger = new EmailLogger(db, { tableName });
740
+ return logger.createCallback();
741
+ }
742
+
523
743
  // src/backend/EmailService.js
524
744
  var EmailService = class {
525
745
  /**
@@ -527,6 +747,7 @@ var EmailService = class {
527
747
  * @param {Object} config - Configuration options
528
748
  * @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
529
749
  * @param {Object} [config.defaults] - Default settings (fromName, fromAddress)
750
+ * @param {boolean|Function} [config.emailLogger] - Email logger: true (default), false (disabled), or custom callback
530
751
  * @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
531
752
  */
532
753
  constructor(env, config = {}, cacheProvider = null) {
@@ -545,10 +766,6 @@ var EmailService = class {
545
766
  // Updater function to save settings to backend
546
767
  // Signature: async (profile, tenantId, settings) => void
547
768
  settingsUpdater: config.settingsUpdater || null,
548
- // Email Logger callback for tracking all email sends
549
- // Signature: async (logEntry) => void
550
- // logEntry: { event: 'pending'|'sent'|'failed', recipientEmail, templateId?, subject?, provider?, messageId?, error?, metadata? }
551
- emailLogger: config.emailLogger || null,
552
769
  // Branding configuration for email templates
553
770
  branding: {
554
771
  brandName: config.branding?.brandName || "Your App",
@@ -558,6 +775,16 @@ var EmailService = class {
558
775
  },
559
776
  ...config
560
777
  };
778
+ if (config.emailLogger === false) {
779
+ this.emailLogger = null;
780
+ } else if (typeof config.emailLogger === "function") {
781
+ this.emailLogger = config.emailLogger;
782
+ } else if (env.DB) {
783
+ const logger = new EmailLogger(env.DB);
784
+ this.emailLogger = logger.createCallback();
785
+ } else {
786
+ this.emailLogger = null;
787
+ }
561
788
  if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
562
789
  this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
563
790
  } else {
@@ -811,9 +1038,9 @@ var EmailService = class {
811
1038
  const htmlContent = html || htmlBody;
812
1039
  const textContent = text || textBody;
813
1040
  const templateId = metadata?.templateId || "direct";
814
- if (this.config.emailLogger) {
1041
+ if (this.emailLogger) {
815
1042
  try {
816
- await this.config.emailLogger({
1043
+ await this.emailLogger({
817
1044
  event: "pending",
818
1045
  recipientEmail: to,
819
1046
  recipientUserId: userId,
@@ -850,9 +1077,9 @@ var EmailService = class {
850
1077
  break;
851
1078
  default:
852
1079
  console.error(`[EmailService] Unknown provider: ${useProvider}`);
853
- if (this.config.emailLogger) {
1080
+ if (this.emailLogger) {
854
1081
  try {
855
- await this.config.emailLogger({
1082
+ await this.emailLogger({
856
1083
  event: "failed",
857
1084
  recipientEmail: to,
858
1085
  recipientUserId: userId,
@@ -870,9 +1097,9 @@ var EmailService = class {
870
1097
  }
871
1098
  if (result) {
872
1099
  const messageId = providerMessageId || crypto.randomUUID();
873
- if (this.config.emailLogger) {
1100
+ if (this.emailLogger) {
874
1101
  try {
875
- await this.config.emailLogger({
1102
+ await this.emailLogger({
876
1103
  event: "sent",
877
1104
  recipientEmail: to,
878
1105
  recipientUserId: userId,
@@ -890,9 +1117,9 @@ var EmailService = class {
890
1117
  return { success: true, messageId };
891
1118
  } else {
892
1119
  console.error("[EmailService] Failed to send email to:", to);
893
- if (this.config.emailLogger) {
1120
+ if (this.emailLogger) {
894
1121
  try {
895
- await this.config.emailLogger({
1122
+ await this.emailLogger({
896
1123
  event: "failed",
897
1124
  recipientEmail: to,
898
1125
  recipientUserId: userId,
@@ -910,9 +1137,9 @@ var EmailService = class {
910
1137
  }
911
1138
  } catch (error) {
912
1139
  console.error("[EmailService] Error sending email:", error);
913
- if (this.config.emailLogger) {
1140
+ if (this.emailLogger) {
914
1141
  try {
915
- await this.config.emailLogger({
1142
+ await this.emailLogger({
916
1143
  event: "failed",
917
1144
  recipientEmail: to,
918
1145
  recipientUserId: userId,
@@ -1116,226 +1343,6 @@ var EmailService = class {
1116
1343
  }
1117
1344
  };
1118
1345
 
1119
- // src/backend/EmailLogger.js
1120
- var EmailLogger = class {
1121
- /**
1122
- * @param {Object} db - D1 database binding
1123
- * @param {Object} options - Configuration options
1124
- * @param {string} [options.tableName='system_email_logs'] - Table name for logs
1125
- */
1126
- constructor(db, options = {}) {
1127
- this.db = db;
1128
- this.tableName = options.tableName || "system_email_logs";
1129
- }
1130
- /**
1131
- * Creates a logger callback function for use with EmailService.
1132
- * Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
1133
- */
1134
- createCallback() {
1135
- return async (entry) => {
1136
- await this.log(entry);
1137
- };
1138
- }
1139
- /**
1140
- * Log an email event (pending, sent, or failed)
1141
- * @param {Object} entry - Log entry
1142
- */
1143
- async log(entry) {
1144
- const {
1145
- event,
1146
- recipientEmail,
1147
- recipientUserId,
1148
- templateId,
1149
- subject,
1150
- provider,
1151
- messageId,
1152
- batchId,
1153
- error,
1154
- errorCode,
1155
- metadata
1156
- } = entry;
1157
- try {
1158
- if (event === "pending") {
1159
- const id = crypto.randomUUID().replace(/-/g, "");
1160
- await this.db.prepare(`
1161
- INSERT INTO ${this.tableName}
1162
- (id, batch_id, recipient_email, recipient_user_id, template_id, subject, status, metadata, created_at)
1163
- VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, strftime('%s', 'now'))
1164
- `).bind(
1165
- id,
1166
- batchId || null,
1167
- recipientEmail,
1168
- recipientUserId || null,
1169
- templateId || "direct",
1170
- subject || null,
1171
- metadata ? JSON.stringify(metadata) : null
1172
- ).run();
1173
- } else if (event === "sent") {
1174
- await this.db.prepare(`
1175
- UPDATE ${this.tableName}
1176
- SET status = 'sent',
1177
- provider = ?,
1178
- provider_message_id = ?,
1179
- sent_at = strftime('%s', 'now')
1180
- WHERE recipient_email = ?
1181
- AND template_id = ?
1182
- AND status = 'pending'
1183
- ORDER BY created_at DESC
1184
- LIMIT 1
1185
- `).bind(
1186
- provider || null,
1187
- messageId || null,
1188
- recipientEmail,
1189
- templateId || "direct"
1190
- ).run();
1191
- } else if (event === "failed") {
1192
- await this.db.prepare(`
1193
- UPDATE ${this.tableName}
1194
- SET status = 'failed',
1195
- provider = ?,
1196
- error_message = ?,
1197
- error_code = ?
1198
- WHERE recipient_email = ?
1199
- AND template_id = ?
1200
- AND status = 'pending'
1201
- ORDER BY created_at DESC
1202
- LIMIT 1
1203
- `).bind(
1204
- provider || null,
1205
- error || null,
1206
- errorCode || null,
1207
- recipientEmail,
1208
- templateId || "direct"
1209
- ).run();
1210
- }
1211
- } catch (e) {
1212
- console.error("[EmailLogger] Failed to log:", e);
1213
- }
1214
- }
1215
- /**
1216
- * Query email logs with filtering
1217
- * @param {Object} options - Query options
1218
- */
1219
- async query(options = {}) {
1220
- const {
1221
- recipientEmail,
1222
- recipientUserId,
1223
- templateId,
1224
- status,
1225
- batchId,
1226
- limit = 50,
1227
- offset = 0
1228
- } = options;
1229
- const conditions = [];
1230
- const bindings = [];
1231
- if (recipientEmail) {
1232
- conditions.push("recipient_email = ?");
1233
- bindings.push(recipientEmail);
1234
- }
1235
- if (recipientUserId) {
1236
- conditions.push("recipient_user_id = ?");
1237
- bindings.push(recipientUserId);
1238
- }
1239
- if (templateId) {
1240
- conditions.push("template_id = ?");
1241
- bindings.push(templateId);
1242
- }
1243
- if (status) {
1244
- conditions.push("status = ?");
1245
- bindings.push(status);
1246
- }
1247
- if (batchId) {
1248
- conditions.push("batch_id = ?");
1249
- bindings.push(batchId);
1250
- }
1251
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1252
- const countResult = await this.db.prepare(
1253
- `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
1254
- ).bind(...bindings).first();
1255
- const { results } = await this.db.prepare(`
1256
- SELECT id, batch_id, recipient_email, recipient_user_id, template_id, subject,
1257
- status, provider, provider_message_id, error_message, error_code, metadata,
1258
- created_at, sent_at
1259
- FROM ${this.tableName}
1260
- ${whereClause}
1261
- ORDER BY created_at DESC
1262
- LIMIT ? OFFSET ?
1263
- `).bind(...bindings, limit, offset).all();
1264
- const logs = (results || []).map((row) => ({
1265
- id: row.id,
1266
- batchId: row.batch_id,
1267
- recipientEmail: row.recipient_email,
1268
- recipientUserId: row.recipient_user_id,
1269
- templateId: row.template_id,
1270
- subject: row.subject,
1271
- status: row.status,
1272
- provider: row.provider,
1273
- providerMessageId: row.provider_message_id,
1274
- errorMessage: row.error_message,
1275
- errorCode: row.error_code,
1276
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
1277
- createdAt: row.created_at,
1278
- sentAt: row.sent_at
1279
- }));
1280
- return { logs, total: countResult?.count || 0 };
1281
- }
1282
- /**
1283
- * Get email sending statistics
1284
- * @param {number} sinceDays - Number of days to look back
1285
- */
1286
- async getStats(sinceDays = 7) {
1287
- const sinceTimestamp = Math.floor(Date.now() / 1e3) - sinceDays * 24 * 60 * 60;
1288
- const statusResult = await this.db.prepare(`
1289
- SELECT status, COUNT(*) as count
1290
- FROM ${this.tableName}
1291
- WHERE created_at >= ?
1292
- GROUP BY status
1293
- `).bind(sinceTimestamp).all();
1294
- const templateResult = await this.db.prepare(`
1295
- SELECT template_id, COUNT(*) as count
1296
- FROM ${this.tableName}
1297
- WHERE created_at >= ?
1298
- GROUP BY template_id
1299
- `).bind(sinceTimestamp).all();
1300
- const stats = {
1301
- total: 0,
1302
- sent: 0,
1303
- failed: 0,
1304
- pending: 0,
1305
- byTemplate: {}
1306
- };
1307
- (statusResult.results || []).forEach((row) => {
1308
- const count = row.count || 0;
1309
- stats.total += count;
1310
- if (row.status === "sent") stats.sent = count;
1311
- if (row.status === "failed") stats.failed = count;
1312
- if (row.status === "pending") stats.pending = count;
1313
- });
1314
- (templateResult.results || []).forEach((row) => {
1315
- stats.byTemplate[row.template_id] = row.count;
1316
- });
1317
- return stats;
1318
- }
1319
- /**
1320
- * Get recent failed emails for debugging
1321
- * @param {number} limit - Number of failed emails to retrieve
1322
- */
1323
- async getRecentFailures(limit = 20) {
1324
- const { results } = await this.db.prepare(`
1325
- SELECT id, recipient_email, template_id, subject, error_message, error_code, created_at
1326
- FROM ${this.tableName}
1327
- WHERE status = 'failed'
1328
- ORDER BY created_at DESC
1329
- LIMIT ?
1330
- `).bind(limit).all();
1331
- return results || [];
1332
- }
1333
- };
1334
- function createEmailLoggerCallback(db, tableName = "system_email_logs") {
1335
- const logger = new EmailLogger(db, { tableName });
1336
- return logger.createCallback();
1337
- }
1338
-
1339
1346
  // src/backend/routes/index.js
1340
1347
  import { Hono as Hono3 } from "hono";
1341
1348