@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.
@@ -276,6 +276,222 @@ function createDOCacheProvider(doStub, instanceName = "global") {
276
276
  };
277
277
  }
278
278
 
279
+ // src/backend/EmailLogger.js
280
+ var EmailLogger = class {
281
+ /**
282
+ * @param {Object} db - D1 database binding
283
+ * @param {Object} options - Configuration options
284
+ * @param {string} [options.tableName='system_email_logs'] - Table name for logs
285
+ */
286
+ constructor(db, options = {}) {
287
+ this.db = db;
288
+ this.tableName = options.tableName || "system_email_logs";
289
+ }
290
+ /**
291
+ * Creates a logger callback function for use with EmailService.
292
+ * Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
293
+ */
294
+ createCallback() {
295
+ return async (entry) => {
296
+ await this.log(entry);
297
+ };
298
+ }
299
+ /**
300
+ * Log an email event (pending, sent, or failed)
301
+ * @param {Object} entry - Log entry
302
+ */
303
+ async log(entry) {
304
+ const {
305
+ event,
306
+ recipientEmail,
307
+ recipientUserId,
308
+ templateId,
309
+ subject,
310
+ provider,
311
+ messageId,
312
+ batchId,
313
+ error,
314
+ errorCode,
315
+ metadata
316
+ } = entry;
317
+ try {
318
+ if (event === "pending") {
319
+ const id = crypto.randomUUID().replace(/-/g, "");
320
+ await this.db.prepare(`
321
+ INSERT INTO ${this.tableName}
322
+ (id, batch_id, recipient_email, recipient_user_id, template_id, subject, status, metadata, created_at)
323
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, strftime('%s', 'now'))
324
+ `).bind(
325
+ id,
326
+ batchId || null,
327
+ recipientEmail,
328
+ recipientUserId || null,
329
+ templateId || "direct",
330
+ subject || null,
331
+ metadata ? JSON.stringify(metadata) : null
332
+ ).run();
333
+ } else if (event === "sent") {
334
+ await this.db.prepare(`
335
+ UPDATE ${this.tableName}
336
+ SET status = 'sent',
337
+ provider = ?,
338
+ provider_message_id = ?,
339
+ sent_at = strftime('%s', 'now')
340
+ WHERE recipient_email = ?
341
+ AND template_id = ?
342
+ AND status = 'pending'
343
+ ORDER BY created_at DESC
344
+ LIMIT 1
345
+ `).bind(
346
+ provider || null,
347
+ messageId || null,
348
+ recipientEmail,
349
+ templateId || "direct"
350
+ ).run();
351
+ } else if (event === "failed") {
352
+ await this.db.prepare(`
353
+ UPDATE ${this.tableName}
354
+ SET status = 'failed',
355
+ provider = ?,
356
+ error_message = ?,
357
+ error_code = ?
358
+ WHERE recipient_email = ?
359
+ AND template_id = ?
360
+ AND status = 'pending'
361
+ ORDER BY created_at DESC
362
+ LIMIT 1
363
+ `).bind(
364
+ provider || null,
365
+ error || null,
366
+ errorCode || null,
367
+ recipientEmail,
368
+ templateId || "direct"
369
+ ).run();
370
+ }
371
+ } catch (e) {
372
+ console.error("[EmailLogger] Failed to log:", e);
373
+ }
374
+ }
375
+ /**
376
+ * Query email logs with filtering
377
+ * @param {Object} options - Query options
378
+ */
379
+ async query(options = {}) {
380
+ const {
381
+ recipientEmail,
382
+ recipientUserId,
383
+ templateId,
384
+ status,
385
+ batchId,
386
+ limit = 50,
387
+ offset = 0
388
+ } = options;
389
+ const conditions = [];
390
+ const bindings = [];
391
+ if (recipientEmail) {
392
+ conditions.push("recipient_email = ?");
393
+ bindings.push(recipientEmail);
394
+ }
395
+ if (recipientUserId) {
396
+ conditions.push("recipient_user_id = ?");
397
+ bindings.push(recipientUserId);
398
+ }
399
+ if (templateId) {
400
+ conditions.push("template_id = ?");
401
+ bindings.push(templateId);
402
+ }
403
+ if (status) {
404
+ conditions.push("status = ?");
405
+ bindings.push(status);
406
+ }
407
+ if (batchId) {
408
+ conditions.push("batch_id = ?");
409
+ bindings.push(batchId);
410
+ }
411
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
412
+ const countResult = await this.db.prepare(
413
+ `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
414
+ ).bind(...bindings).first();
415
+ const { results } = await this.db.prepare(`
416
+ SELECT id, batch_id, recipient_email, recipient_user_id, template_id, subject,
417
+ status, provider, provider_message_id, error_message, error_code, metadata,
418
+ created_at, sent_at
419
+ FROM ${this.tableName}
420
+ ${whereClause}
421
+ ORDER BY created_at DESC
422
+ LIMIT ? OFFSET ?
423
+ `).bind(...bindings, limit, offset).all();
424
+ const logs = (results || []).map((row) => ({
425
+ id: row.id,
426
+ batchId: row.batch_id,
427
+ recipientEmail: row.recipient_email,
428
+ recipientUserId: row.recipient_user_id,
429
+ templateId: row.template_id,
430
+ subject: row.subject,
431
+ status: row.status,
432
+ provider: row.provider,
433
+ providerMessageId: row.provider_message_id,
434
+ errorMessage: row.error_message,
435
+ errorCode: row.error_code,
436
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
437
+ createdAt: row.created_at,
438
+ sentAt: row.sent_at
439
+ }));
440
+ return { logs, total: countResult?.count || 0 };
441
+ }
442
+ /**
443
+ * Get email sending statistics
444
+ * @param {number} sinceDays - Number of days to look back
445
+ */
446
+ async getStats(sinceDays = 7) {
447
+ const sinceTimestamp = Math.floor(Date.now() / 1e3) - sinceDays * 24 * 60 * 60;
448
+ const statusResult = await this.db.prepare(`
449
+ SELECT status, COUNT(*) as count
450
+ FROM ${this.tableName}
451
+ WHERE created_at >= ?
452
+ GROUP BY status
453
+ `).bind(sinceTimestamp).all();
454
+ const templateResult = await this.db.prepare(`
455
+ SELECT template_id, COUNT(*) as count
456
+ FROM ${this.tableName}
457
+ WHERE created_at >= ?
458
+ GROUP BY template_id
459
+ `).bind(sinceTimestamp).all();
460
+ const stats = {
461
+ total: 0,
462
+ sent: 0,
463
+ failed: 0,
464
+ pending: 0,
465
+ byTemplate: {}
466
+ };
467
+ (statusResult.results || []).forEach((row) => {
468
+ const count = row.count || 0;
469
+ stats.total += count;
470
+ if (row.status === "sent") stats.sent = count;
471
+ if (row.status === "failed") stats.failed = count;
472
+ if (row.status === "pending") stats.pending = count;
473
+ });
474
+ (templateResult.results || []).forEach((row) => {
475
+ stats.byTemplate[row.template_id] = row.count;
476
+ });
477
+ return stats;
478
+ }
479
+ /**
480
+ * Get recent failed emails for debugging
481
+ * @param {number} limit - Number of failed emails to retrieve
482
+ */
483
+ async getRecentFailures(limit = 20) {
484
+ const { results } = await this.db.prepare(`
485
+ SELECT id, recipient_email, template_id, subject, error_message, error_code, created_at
486
+ FROM ${this.tableName}
487
+ WHERE status = 'failed'
488
+ ORDER BY created_at DESC
489
+ LIMIT ?
490
+ `).bind(limit).all();
491
+ return results || [];
492
+ }
493
+ };
494
+
279
495
  // src/backend/EmailService.js
280
496
  var EmailService = class {
281
497
  /**
@@ -283,6 +499,7 @@ var EmailService = class {
283
499
  * @param {Object} config - Configuration options
284
500
  * @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
285
501
  * @param {Object} [config.defaults] - Default settings (fromName, fromAddress)
502
+ * @param {boolean|Function} [config.emailLogger] - Email logger: true (default), false (disabled), or custom callback
286
503
  * @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
287
504
  */
288
505
  constructor(env, config = {}, cacheProvider = null) {
@@ -301,10 +518,6 @@ var EmailService = class {
301
518
  // Updater function to save settings to backend
302
519
  // Signature: async (profile, tenantId, settings) => void
303
520
  settingsUpdater: config.settingsUpdater || null,
304
- // Email Logger callback for tracking all email sends
305
- // Signature: async (logEntry) => void
306
- // logEntry: { event: 'pending'|'sent'|'failed', recipientEmail, templateId?, subject?, provider?, messageId?, error?, metadata? }
307
- emailLogger: config.emailLogger || null,
308
521
  // Branding configuration for email templates
309
522
  branding: {
310
523
  brandName: config.branding?.brandName || "Your App",
@@ -314,6 +527,16 @@ var EmailService = class {
314
527
  },
315
528
  ...config
316
529
  };
530
+ if (config.emailLogger === false) {
531
+ this.emailLogger = null;
532
+ } else if (typeof config.emailLogger === "function") {
533
+ this.emailLogger = config.emailLogger;
534
+ } else if (env.DB) {
535
+ const logger = new EmailLogger(env.DB);
536
+ this.emailLogger = logger.createCallback();
537
+ } else {
538
+ this.emailLogger = null;
539
+ }
317
540
  if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
318
541
  this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
319
542
  } else {
@@ -567,9 +790,9 @@ var EmailService = class {
567
790
  const htmlContent = html || htmlBody;
568
791
  const textContent = text || textBody;
569
792
  const templateId = metadata?.templateId || "direct";
570
- if (this.config.emailLogger) {
793
+ if (this.emailLogger) {
571
794
  try {
572
- await this.config.emailLogger({
795
+ await this.emailLogger({
573
796
  event: "pending",
574
797
  recipientEmail: to,
575
798
  recipientUserId: userId,
@@ -606,9 +829,9 @@ var EmailService = class {
606
829
  break;
607
830
  default:
608
831
  console.error(`[EmailService] Unknown provider: ${useProvider}`);
609
- if (this.config.emailLogger) {
832
+ if (this.emailLogger) {
610
833
  try {
611
- await this.config.emailLogger({
834
+ await this.emailLogger({
612
835
  event: "failed",
613
836
  recipientEmail: to,
614
837
  recipientUserId: userId,
@@ -626,9 +849,9 @@ var EmailService = class {
626
849
  }
627
850
  if (result) {
628
851
  const messageId = providerMessageId || crypto.randomUUID();
629
- if (this.config.emailLogger) {
852
+ if (this.emailLogger) {
630
853
  try {
631
- await this.config.emailLogger({
854
+ await this.emailLogger({
632
855
  event: "sent",
633
856
  recipientEmail: to,
634
857
  recipientUserId: userId,
@@ -646,9 +869,9 @@ var EmailService = class {
646
869
  return { success: true, messageId };
647
870
  } else {
648
871
  console.error("[EmailService] Failed to send email to:", to);
649
- if (this.config.emailLogger) {
872
+ if (this.emailLogger) {
650
873
  try {
651
- await this.config.emailLogger({
874
+ await this.emailLogger({
652
875
  event: "failed",
653
876
  recipientEmail: to,
654
877
  recipientUserId: userId,
@@ -666,9 +889,9 @@ var EmailService = class {
666
889
  }
667
890
  } catch (error) {
668
891
  console.error("[EmailService] Error sending email:", error);
669
- if (this.config.emailLogger) {
892
+ if (this.emailLogger) {
670
893
  try {
671
- await this.config.emailLogger({
894
+ await this.emailLogger({
672
895
  event: "failed",
673
896
  recipientEmail: to,
674
897
  recipientUserId: userId,