@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.
@@ -268,6 +268,222 @@ function createDOCacheProvider(doStub, instanceName = "global") {
268
268
  };
269
269
  }
270
270
 
271
+ // src/backend/EmailLogger.js
272
+ var EmailLogger = class {
273
+ /**
274
+ * @param {Object} db - D1 database binding
275
+ * @param {Object} options - Configuration options
276
+ * @param {string} [options.tableName='system_email_logs'] - Table name for logs
277
+ */
278
+ constructor(db, options = {}) {
279
+ this.db = db;
280
+ this.tableName = options.tableName || "system_email_logs";
281
+ }
282
+ /**
283
+ * Creates a logger callback function for use with EmailService.
284
+ * Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
285
+ */
286
+ createCallback() {
287
+ return async (entry) => {
288
+ await this.log(entry);
289
+ };
290
+ }
291
+ /**
292
+ * Log an email event (pending, sent, or failed)
293
+ * @param {Object} entry - Log entry
294
+ */
295
+ async log(entry) {
296
+ const {
297
+ event,
298
+ recipientEmail,
299
+ recipientUserId,
300
+ templateId,
301
+ subject,
302
+ provider,
303
+ messageId,
304
+ batchId,
305
+ error,
306
+ errorCode,
307
+ metadata
308
+ } = entry;
309
+ try {
310
+ if (event === "pending") {
311
+ const id = crypto.randomUUID().replace(/-/g, "");
312
+ await this.db.prepare(`
313
+ INSERT INTO ${this.tableName}
314
+ (id, batch_id, recipient_email, recipient_user_id, template_id, subject, status, metadata, created_at)
315
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, strftime('%s', 'now'))
316
+ `).bind(
317
+ id,
318
+ batchId || null,
319
+ recipientEmail,
320
+ recipientUserId || null,
321
+ templateId || "direct",
322
+ subject || null,
323
+ metadata ? JSON.stringify(metadata) : null
324
+ ).run();
325
+ } else if (event === "sent") {
326
+ await this.db.prepare(`
327
+ UPDATE ${this.tableName}
328
+ SET status = 'sent',
329
+ provider = ?,
330
+ provider_message_id = ?,
331
+ sent_at = strftime('%s', 'now')
332
+ WHERE recipient_email = ?
333
+ AND template_id = ?
334
+ AND status = 'pending'
335
+ ORDER BY created_at DESC
336
+ LIMIT 1
337
+ `).bind(
338
+ provider || null,
339
+ messageId || null,
340
+ recipientEmail,
341
+ templateId || "direct"
342
+ ).run();
343
+ } else if (event === "failed") {
344
+ await this.db.prepare(`
345
+ UPDATE ${this.tableName}
346
+ SET status = 'failed',
347
+ provider = ?,
348
+ error_message = ?,
349
+ error_code = ?
350
+ WHERE recipient_email = ?
351
+ AND template_id = ?
352
+ AND status = 'pending'
353
+ ORDER BY created_at DESC
354
+ LIMIT 1
355
+ `).bind(
356
+ provider || null,
357
+ error || null,
358
+ errorCode || null,
359
+ recipientEmail,
360
+ templateId || "direct"
361
+ ).run();
362
+ }
363
+ } catch (e) {
364
+ console.error("[EmailLogger] Failed to log:", e);
365
+ }
366
+ }
367
+ /**
368
+ * Query email logs with filtering
369
+ * @param {Object} options - Query options
370
+ */
371
+ async query(options = {}) {
372
+ const {
373
+ recipientEmail,
374
+ recipientUserId,
375
+ templateId,
376
+ status,
377
+ batchId,
378
+ limit = 50,
379
+ offset = 0
380
+ } = options;
381
+ const conditions = [];
382
+ const bindings = [];
383
+ if (recipientEmail) {
384
+ conditions.push("recipient_email = ?");
385
+ bindings.push(recipientEmail);
386
+ }
387
+ if (recipientUserId) {
388
+ conditions.push("recipient_user_id = ?");
389
+ bindings.push(recipientUserId);
390
+ }
391
+ if (templateId) {
392
+ conditions.push("template_id = ?");
393
+ bindings.push(templateId);
394
+ }
395
+ if (status) {
396
+ conditions.push("status = ?");
397
+ bindings.push(status);
398
+ }
399
+ if (batchId) {
400
+ conditions.push("batch_id = ?");
401
+ bindings.push(batchId);
402
+ }
403
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
404
+ const countResult = await this.db.prepare(
405
+ `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
406
+ ).bind(...bindings).first();
407
+ const { results } = await this.db.prepare(`
408
+ SELECT id, batch_id, recipient_email, recipient_user_id, template_id, subject,
409
+ status, provider, provider_message_id, error_message, error_code, metadata,
410
+ created_at, sent_at
411
+ FROM ${this.tableName}
412
+ ${whereClause}
413
+ ORDER BY created_at DESC
414
+ LIMIT ? OFFSET ?
415
+ `).bind(...bindings, limit, offset).all();
416
+ const logs = (results || []).map((row) => ({
417
+ id: row.id,
418
+ batchId: row.batch_id,
419
+ recipientEmail: row.recipient_email,
420
+ recipientUserId: row.recipient_user_id,
421
+ templateId: row.template_id,
422
+ subject: row.subject,
423
+ status: row.status,
424
+ provider: row.provider,
425
+ providerMessageId: row.provider_message_id,
426
+ errorMessage: row.error_message,
427
+ errorCode: row.error_code,
428
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
429
+ createdAt: row.created_at,
430
+ sentAt: row.sent_at
431
+ }));
432
+ return { logs, total: countResult?.count || 0 };
433
+ }
434
+ /**
435
+ * Get email sending statistics
436
+ * @param {number} sinceDays - Number of days to look back
437
+ */
438
+ async getStats(sinceDays = 7) {
439
+ const sinceTimestamp = Math.floor(Date.now() / 1e3) - sinceDays * 24 * 60 * 60;
440
+ const statusResult = await this.db.prepare(`
441
+ SELECT status, COUNT(*) as count
442
+ FROM ${this.tableName}
443
+ WHERE created_at >= ?
444
+ GROUP BY status
445
+ `).bind(sinceTimestamp).all();
446
+ const templateResult = await this.db.prepare(`
447
+ SELECT template_id, COUNT(*) as count
448
+ FROM ${this.tableName}
449
+ WHERE created_at >= ?
450
+ GROUP BY template_id
451
+ `).bind(sinceTimestamp).all();
452
+ const stats = {
453
+ total: 0,
454
+ sent: 0,
455
+ failed: 0,
456
+ pending: 0,
457
+ byTemplate: {}
458
+ };
459
+ (statusResult.results || []).forEach((row) => {
460
+ const count = row.count || 0;
461
+ stats.total += count;
462
+ if (row.status === "sent") stats.sent = count;
463
+ if (row.status === "failed") stats.failed = count;
464
+ if (row.status === "pending") stats.pending = count;
465
+ });
466
+ (templateResult.results || []).forEach((row) => {
467
+ stats.byTemplate[row.template_id] = row.count;
468
+ });
469
+ return stats;
470
+ }
471
+ /**
472
+ * Get recent failed emails for debugging
473
+ * @param {number} limit - Number of failed emails to retrieve
474
+ */
475
+ async getRecentFailures(limit = 20) {
476
+ const { results } = await this.db.prepare(`
477
+ SELECT id, recipient_email, template_id, subject, error_message, error_code, created_at
478
+ FROM ${this.tableName}
479
+ WHERE status = 'failed'
480
+ ORDER BY created_at DESC
481
+ LIMIT ?
482
+ `).bind(limit).all();
483
+ return results || [];
484
+ }
485
+ };
486
+
271
487
  // src/backend/EmailService.js
272
488
  var EmailService = class {
273
489
  /**
@@ -275,6 +491,7 @@ var EmailService = class {
275
491
  * @param {Object} config - Configuration options
276
492
  * @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
277
493
  * @param {Object} [config.defaults] - Default settings (fromName, fromAddress)
494
+ * @param {boolean|Function} [config.emailLogger] - Email logger: true (default), false (disabled), or custom callback
278
495
  * @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
279
496
  */
280
497
  constructor(env, config = {}, cacheProvider = null) {
@@ -293,10 +510,6 @@ var EmailService = class {
293
510
  // Updater function to save settings to backend
294
511
  // Signature: async (profile, tenantId, settings) => void
295
512
  settingsUpdater: config.settingsUpdater || null,
296
- // Email Logger callback for tracking all email sends
297
- // Signature: async (logEntry) => void
298
- // logEntry: { event: 'pending'|'sent'|'failed', recipientEmail, templateId?, subject?, provider?, messageId?, error?, metadata? }
299
- emailLogger: config.emailLogger || null,
300
513
  // Branding configuration for email templates
301
514
  branding: {
302
515
  brandName: config.branding?.brandName || "Your App",
@@ -306,6 +519,16 @@ var EmailService = class {
306
519
  },
307
520
  ...config
308
521
  };
522
+ if (config.emailLogger === false) {
523
+ this.emailLogger = null;
524
+ } else if (typeof config.emailLogger === "function") {
525
+ this.emailLogger = config.emailLogger;
526
+ } else if (env.DB) {
527
+ const logger = new EmailLogger(env.DB);
528
+ this.emailLogger = logger.createCallback();
529
+ } else {
530
+ this.emailLogger = null;
531
+ }
309
532
  if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
310
533
  this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
311
534
  } else {
@@ -559,9 +782,9 @@ var EmailService = class {
559
782
  const htmlContent = html || htmlBody;
560
783
  const textContent = text || textBody;
561
784
  const templateId = metadata?.templateId || "direct";
562
- if (this.config.emailLogger) {
785
+ if (this.emailLogger) {
563
786
  try {
564
- await this.config.emailLogger({
787
+ await this.emailLogger({
565
788
  event: "pending",
566
789
  recipientEmail: to,
567
790
  recipientUserId: userId,
@@ -598,9 +821,9 @@ var EmailService = class {
598
821
  break;
599
822
  default:
600
823
  console.error(`[EmailService] Unknown provider: ${useProvider}`);
601
- if (this.config.emailLogger) {
824
+ if (this.emailLogger) {
602
825
  try {
603
- await this.config.emailLogger({
826
+ await this.emailLogger({
604
827
  event: "failed",
605
828
  recipientEmail: to,
606
829
  recipientUserId: userId,
@@ -618,9 +841,9 @@ var EmailService = class {
618
841
  }
619
842
  if (result) {
620
843
  const messageId = providerMessageId || crypto.randomUUID();
621
- if (this.config.emailLogger) {
844
+ if (this.emailLogger) {
622
845
  try {
623
- await this.config.emailLogger({
846
+ await this.emailLogger({
624
847
  event: "sent",
625
848
  recipientEmail: to,
626
849
  recipientUserId: userId,
@@ -638,9 +861,9 @@ var EmailService = class {
638
861
  return { success: true, messageId };
639
862
  } else {
640
863
  console.error("[EmailService] Failed to send email to:", to);
641
- if (this.config.emailLogger) {
864
+ if (this.emailLogger) {
642
865
  try {
643
- await this.config.emailLogger({
866
+ await this.emailLogger({
644
867
  event: "failed",
645
868
  recipientEmail: to,
646
869
  recipientUserId: userId,
@@ -658,9 +881,9 @@ var EmailService = class {
658
881
  }
659
882
  } catch (error) {
660
883
  console.error("[EmailService] Error sending email:", error);
661
- if (this.config.emailLogger) {
884
+ if (this.emailLogger) {
662
885
  try {
663
- await this.config.emailLogger({
886
+ await this.emailLogger({
664
887
  event: "failed",
665
888
  recipientEmail: to,
666
889
  recipientUserId: userId,