@contentgrowth/content-emailing 0.7.1 → 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
@@ -570,6 +570,226 @@ function createDOCacheProvider(doStub, instanceName = "global") {
570
570
  };
571
571
  }
572
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
+
573
793
  // src/backend/EmailService.js
574
794
  var EmailService = class {
575
795
  /**
@@ -577,6 +797,7 @@ var EmailService = class {
577
797
  * @param {Object} config - Configuration options
578
798
  * @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
579
799
  * @param {Object} [config.defaults] - Default settings (fromName, fromAddress)
800
+ * @param {boolean|Function} [config.emailLogger] - Email logger: true (default), false (disabled), or custom callback
580
801
  * @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
581
802
  */
582
803
  constructor(env, config = {}, cacheProvider = null) {
@@ -595,10 +816,6 @@ var EmailService = class {
595
816
  // Updater function to save settings to backend
596
817
  // Signature: async (profile, tenantId, settings) => void
597
818
  settingsUpdater: config.settingsUpdater || null,
598
- // Email Logger callback for tracking all email sends
599
- // Signature: async (logEntry) => void
600
- // logEntry: { event: 'pending'|'sent'|'failed', recipientEmail, templateId?, subject?, provider?, messageId?, error?, metadata? }
601
- emailLogger: config.emailLogger || null,
602
819
  // Branding configuration for email templates
603
820
  branding: {
604
821
  brandName: config.branding?.brandName || "Your App",
@@ -608,6 +825,16 @@ var EmailService = class {
608
825
  },
609
826
  ...config
610
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
+ }
611
838
  if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
612
839
  this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
613
840
  } else {
@@ -861,9 +1088,9 @@ var EmailService = class {
861
1088
  const htmlContent = html || htmlBody;
862
1089
  const textContent = text || textBody;
863
1090
  const templateId = metadata?.templateId || "direct";
864
- if (this.config.emailLogger) {
1091
+ if (this.emailLogger) {
865
1092
  try {
866
- await this.config.emailLogger({
1093
+ await this.emailLogger({
867
1094
  event: "pending",
868
1095
  recipientEmail: to,
869
1096
  recipientUserId: userId,
@@ -900,9 +1127,9 @@ var EmailService = class {
900
1127
  break;
901
1128
  default:
902
1129
  console.error(`[EmailService] Unknown provider: ${useProvider}`);
903
- if (this.config.emailLogger) {
1130
+ if (this.emailLogger) {
904
1131
  try {
905
- await this.config.emailLogger({
1132
+ await this.emailLogger({
906
1133
  event: "failed",
907
1134
  recipientEmail: to,
908
1135
  recipientUserId: userId,
@@ -920,9 +1147,9 @@ var EmailService = class {
920
1147
  }
921
1148
  if (result) {
922
1149
  const messageId = providerMessageId || crypto.randomUUID();
923
- if (this.config.emailLogger) {
1150
+ if (this.emailLogger) {
924
1151
  try {
925
- await this.config.emailLogger({
1152
+ await this.emailLogger({
926
1153
  event: "sent",
927
1154
  recipientEmail: to,
928
1155
  recipientUserId: userId,
@@ -940,9 +1167,9 @@ var EmailService = class {
940
1167
  return { success: true, messageId };
941
1168
  } else {
942
1169
  console.error("[EmailService] Failed to send email to:", to);
943
- if (this.config.emailLogger) {
1170
+ if (this.emailLogger) {
944
1171
  try {
945
- await this.config.emailLogger({
1172
+ await this.emailLogger({
946
1173
  event: "failed",
947
1174
  recipientEmail: to,
948
1175
  recipientUserId: userId,
@@ -960,9 +1187,9 @@ var EmailService = class {
960
1187
  }
961
1188
  } catch (error) {
962
1189
  console.error("[EmailService] Error sending email:", error);
963
- if (this.config.emailLogger) {
1190
+ if (this.emailLogger) {
964
1191
  try {
965
- await this.config.emailLogger({
1192
+ await this.emailLogger({
966
1193
  event: "failed",
967
1194
  recipientEmail: to,
968
1195
  recipientUserId: userId,
@@ -1166,226 +1393,6 @@ var EmailService = class {
1166
1393
  }
1167
1394
  };
1168
1395
 
1169
- // src/backend/EmailLogger.js
1170
- var EmailLogger = class {
1171
- /**
1172
- * @param {Object} db - D1 database binding
1173
- * @param {Object} options - Configuration options
1174
- * @param {string} [options.tableName='system_email_logs'] - Table name for logs
1175
- */
1176
- constructor(db, options = {}) {
1177
- this.db = db;
1178
- this.tableName = options.tableName || "system_email_logs";
1179
- }
1180
- /**
1181
- * Creates a logger callback function for use with EmailService.
1182
- * Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
1183
- */
1184
- createCallback() {
1185
- return async (entry) => {
1186
- await this.log(entry);
1187
- };
1188
- }
1189
- /**
1190
- * Log an email event (pending, sent, or failed)
1191
- * @param {Object} entry - Log entry
1192
- */
1193
- async log(entry) {
1194
- const {
1195
- event,
1196
- recipientEmail,
1197
- recipientUserId,
1198
- templateId,
1199
- subject,
1200
- provider,
1201
- messageId,
1202
- batchId,
1203
- error,
1204
- errorCode,
1205
- metadata
1206
- } = entry;
1207
- try {
1208
- if (event === "pending") {
1209
- const id = crypto.randomUUID().replace(/-/g, "");
1210
- await this.db.prepare(`
1211
- INSERT INTO ${this.tableName}
1212
- (id, batch_id, recipient_email, recipient_user_id, template_id, subject, status, metadata, created_at)
1213
- VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, strftime('%s', 'now'))
1214
- `).bind(
1215
- id,
1216
- batchId || null,
1217
- recipientEmail,
1218
- recipientUserId || null,
1219
- templateId || "direct",
1220
- subject || null,
1221
- metadata ? JSON.stringify(metadata) : null
1222
- ).run();
1223
- } else if (event === "sent") {
1224
- await this.db.prepare(`
1225
- UPDATE ${this.tableName}
1226
- SET status = 'sent',
1227
- provider = ?,
1228
- provider_message_id = ?,
1229
- sent_at = strftime('%s', 'now')
1230
- WHERE recipient_email = ?
1231
- AND template_id = ?
1232
- AND status = 'pending'
1233
- ORDER BY created_at DESC
1234
- LIMIT 1
1235
- `).bind(
1236
- provider || null,
1237
- messageId || null,
1238
- recipientEmail,
1239
- templateId || "direct"
1240
- ).run();
1241
- } else if (event === "failed") {
1242
- await this.db.prepare(`
1243
- UPDATE ${this.tableName}
1244
- SET status = 'failed',
1245
- provider = ?,
1246
- error_message = ?,
1247
- error_code = ?
1248
- WHERE recipient_email = ?
1249
- AND template_id = ?
1250
- AND status = 'pending'
1251
- ORDER BY created_at DESC
1252
- LIMIT 1
1253
- `).bind(
1254
- provider || null,
1255
- error || null,
1256
- errorCode || null,
1257
- recipientEmail,
1258
- templateId || "direct"
1259
- ).run();
1260
- }
1261
- } catch (e) {
1262
- console.error("[EmailLogger] Failed to log:", e);
1263
- }
1264
- }
1265
- /**
1266
- * Query email logs with filtering
1267
- * @param {Object} options - Query options
1268
- */
1269
- async query(options = {}) {
1270
- const {
1271
- recipientEmail,
1272
- recipientUserId,
1273
- templateId,
1274
- status,
1275
- batchId,
1276
- limit = 50,
1277
- offset = 0
1278
- } = options;
1279
- const conditions = [];
1280
- const bindings = [];
1281
- if (recipientEmail) {
1282
- conditions.push("recipient_email = ?");
1283
- bindings.push(recipientEmail);
1284
- }
1285
- if (recipientUserId) {
1286
- conditions.push("recipient_user_id = ?");
1287
- bindings.push(recipientUserId);
1288
- }
1289
- if (templateId) {
1290
- conditions.push("template_id = ?");
1291
- bindings.push(templateId);
1292
- }
1293
- if (status) {
1294
- conditions.push("status = ?");
1295
- bindings.push(status);
1296
- }
1297
- if (batchId) {
1298
- conditions.push("batch_id = ?");
1299
- bindings.push(batchId);
1300
- }
1301
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
1302
- const countResult = await this.db.prepare(
1303
- `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
1304
- ).bind(...bindings).first();
1305
- const { results } = await this.db.prepare(`
1306
- SELECT id, batch_id, recipient_email, recipient_user_id, template_id, subject,
1307
- status, provider, provider_message_id, error_message, error_code, metadata,
1308
- created_at, sent_at
1309
- FROM ${this.tableName}
1310
- ${whereClause}
1311
- ORDER BY created_at DESC
1312
- LIMIT ? OFFSET ?
1313
- `).bind(...bindings, limit, offset).all();
1314
- const logs = (results || []).map((row) => ({
1315
- id: row.id,
1316
- batchId: row.batch_id,
1317
- recipientEmail: row.recipient_email,
1318
- recipientUserId: row.recipient_user_id,
1319
- templateId: row.template_id,
1320
- subject: row.subject,
1321
- status: row.status,
1322
- provider: row.provider,
1323
- providerMessageId: row.provider_message_id,
1324
- errorMessage: row.error_message,
1325
- errorCode: row.error_code,
1326
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
1327
- createdAt: row.created_at,
1328
- sentAt: row.sent_at
1329
- }));
1330
- return { logs, total: countResult?.count || 0 };
1331
- }
1332
- /**
1333
- * Get email sending statistics
1334
- * @param {number} sinceDays - Number of days to look back
1335
- */
1336
- async getStats(sinceDays = 7) {
1337
- const sinceTimestamp = Math.floor(Date.now() / 1e3) - sinceDays * 24 * 60 * 60;
1338
- const statusResult = await this.db.prepare(`
1339
- SELECT status, COUNT(*) as count
1340
- FROM ${this.tableName}
1341
- WHERE created_at >= ?
1342
- GROUP BY status
1343
- `).bind(sinceTimestamp).all();
1344
- const templateResult = await this.db.prepare(`
1345
- SELECT template_id, COUNT(*) as count
1346
- FROM ${this.tableName}
1347
- WHERE created_at >= ?
1348
- GROUP BY template_id
1349
- `).bind(sinceTimestamp).all();
1350
- const stats = {
1351
- total: 0,
1352
- sent: 0,
1353
- failed: 0,
1354
- pending: 0,
1355
- byTemplate: {}
1356
- };
1357
- (statusResult.results || []).forEach((row) => {
1358
- const count = row.count || 0;
1359
- stats.total += count;
1360
- if (row.status === "sent") stats.sent = count;
1361
- if (row.status === "failed") stats.failed = count;
1362
- if (row.status === "pending") stats.pending = count;
1363
- });
1364
- (templateResult.results || []).forEach((row) => {
1365
- stats.byTemplate[row.template_id] = row.count;
1366
- });
1367
- return stats;
1368
- }
1369
- /**
1370
- * Get recent failed emails for debugging
1371
- * @param {number} limit - Number of failed emails to retrieve
1372
- */
1373
- async getRecentFailures(limit = 20) {
1374
- const { results } = await this.db.prepare(`
1375
- SELECT id, recipient_email, template_id, subject, error_message, error_code, created_at
1376
- FROM ${this.tableName}
1377
- WHERE status = 'failed'
1378
- ORDER BY created_at DESC
1379
- LIMIT ?
1380
- `).bind(limit).all();
1381
- return results || [];
1382
- }
1383
- };
1384
- function createEmailLoggerCallback(db, tableName = "system_email_logs") {
1385
- const logger = new EmailLogger(db, { tableName });
1386
- return logger.createCallback();
1387
- }
1388
-
1389
1396
  // src/backend/routes/index.js
1390
1397
  var import_hono3 = require("hono");
1391
1398