@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.
@@ -235,6 +235,222 @@ function createDOCacheProvider(doStub, instanceName = "global") {
235
235
  };
236
236
  }
237
237
 
238
+ // src/backend/EmailLogger.js
239
+ var EmailLogger = class {
240
+ /**
241
+ * @param {Object} db - D1 database binding
242
+ * @param {Object} options - Configuration options
243
+ * @param {string} [options.tableName='system_email_logs'] - Table name for logs
244
+ */
245
+ constructor(db, options = {}) {
246
+ this.db = db;
247
+ this.tableName = options.tableName || "system_email_logs";
248
+ }
249
+ /**
250
+ * Creates a logger callback function for use with EmailService.
251
+ * Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
252
+ */
253
+ createCallback() {
254
+ return async (entry) => {
255
+ await this.log(entry);
256
+ };
257
+ }
258
+ /**
259
+ * Log an email event (pending, sent, or failed)
260
+ * @param {Object} entry - Log entry
261
+ */
262
+ async log(entry) {
263
+ const {
264
+ event,
265
+ recipientEmail,
266
+ recipientUserId,
267
+ templateId,
268
+ subject,
269
+ provider,
270
+ messageId,
271
+ batchId,
272
+ error,
273
+ errorCode,
274
+ metadata
275
+ } = entry;
276
+ try {
277
+ if (event === "pending") {
278
+ const id = crypto.randomUUID().replace(/-/g, "");
279
+ await this.db.prepare(`
280
+ INSERT INTO ${this.tableName}
281
+ (id, batch_id, recipient_email, recipient_user_id, template_id, subject, status, metadata, created_at)
282
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, strftime('%s', 'now'))
283
+ `).bind(
284
+ id,
285
+ batchId || null,
286
+ recipientEmail,
287
+ recipientUserId || null,
288
+ templateId || "direct",
289
+ subject || null,
290
+ metadata ? JSON.stringify(metadata) : null
291
+ ).run();
292
+ } else if (event === "sent") {
293
+ await this.db.prepare(`
294
+ UPDATE ${this.tableName}
295
+ SET status = 'sent',
296
+ provider = ?,
297
+ provider_message_id = ?,
298
+ sent_at = strftime('%s', 'now')
299
+ WHERE recipient_email = ?
300
+ AND template_id = ?
301
+ AND status = 'pending'
302
+ ORDER BY created_at DESC
303
+ LIMIT 1
304
+ `).bind(
305
+ provider || null,
306
+ messageId || null,
307
+ recipientEmail,
308
+ templateId || "direct"
309
+ ).run();
310
+ } else if (event === "failed") {
311
+ await this.db.prepare(`
312
+ UPDATE ${this.tableName}
313
+ SET status = 'failed',
314
+ provider = ?,
315
+ error_message = ?,
316
+ error_code = ?
317
+ WHERE recipient_email = ?
318
+ AND template_id = ?
319
+ AND status = 'pending'
320
+ ORDER BY created_at DESC
321
+ LIMIT 1
322
+ `).bind(
323
+ provider || null,
324
+ error || null,
325
+ errorCode || null,
326
+ recipientEmail,
327
+ templateId || "direct"
328
+ ).run();
329
+ }
330
+ } catch (e) {
331
+ console.error("[EmailLogger] Failed to log:", e);
332
+ }
333
+ }
334
+ /**
335
+ * Query email logs with filtering
336
+ * @param {Object} options - Query options
337
+ */
338
+ async query(options = {}) {
339
+ const {
340
+ recipientEmail,
341
+ recipientUserId,
342
+ templateId,
343
+ status,
344
+ batchId,
345
+ limit = 50,
346
+ offset = 0
347
+ } = options;
348
+ const conditions = [];
349
+ const bindings = [];
350
+ if (recipientEmail) {
351
+ conditions.push("recipient_email = ?");
352
+ bindings.push(recipientEmail);
353
+ }
354
+ if (recipientUserId) {
355
+ conditions.push("recipient_user_id = ?");
356
+ bindings.push(recipientUserId);
357
+ }
358
+ if (templateId) {
359
+ conditions.push("template_id = ?");
360
+ bindings.push(templateId);
361
+ }
362
+ if (status) {
363
+ conditions.push("status = ?");
364
+ bindings.push(status);
365
+ }
366
+ if (batchId) {
367
+ conditions.push("batch_id = ?");
368
+ bindings.push(batchId);
369
+ }
370
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
371
+ const countResult = await this.db.prepare(
372
+ `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
373
+ ).bind(...bindings).first();
374
+ const { results } = await this.db.prepare(`
375
+ SELECT id, batch_id, recipient_email, recipient_user_id, template_id, subject,
376
+ status, provider, provider_message_id, error_message, error_code, metadata,
377
+ created_at, sent_at
378
+ FROM ${this.tableName}
379
+ ${whereClause}
380
+ ORDER BY created_at DESC
381
+ LIMIT ? OFFSET ?
382
+ `).bind(...bindings, limit, offset).all();
383
+ const logs = (results || []).map((row) => ({
384
+ id: row.id,
385
+ batchId: row.batch_id,
386
+ recipientEmail: row.recipient_email,
387
+ recipientUserId: row.recipient_user_id,
388
+ templateId: row.template_id,
389
+ subject: row.subject,
390
+ status: row.status,
391
+ provider: row.provider,
392
+ providerMessageId: row.provider_message_id,
393
+ errorMessage: row.error_message,
394
+ errorCode: row.error_code,
395
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
396
+ createdAt: row.created_at,
397
+ sentAt: row.sent_at
398
+ }));
399
+ return { logs, total: countResult?.count || 0 };
400
+ }
401
+ /**
402
+ * Get email sending statistics
403
+ * @param {number} sinceDays - Number of days to look back
404
+ */
405
+ async getStats(sinceDays = 7) {
406
+ const sinceTimestamp = Math.floor(Date.now() / 1e3) - sinceDays * 24 * 60 * 60;
407
+ const statusResult = await this.db.prepare(`
408
+ SELECT status, COUNT(*) as count
409
+ FROM ${this.tableName}
410
+ WHERE created_at >= ?
411
+ GROUP BY status
412
+ `).bind(sinceTimestamp).all();
413
+ const templateResult = await this.db.prepare(`
414
+ SELECT template_id, COUNT(*) as count
415
+ FROM ${this.tableName}
416
+ WHERE created_at >= ?
417
+ GROUP BY template_id
418
+ `).bind(sinceTimestamp).all();
419
+ const stats = {
420
+ total: 0,
421
+ sent: 0,
422
+ failed: 0,
423
+ pending: 0,
424
+ byTemplate: {}
425
+ };
426
+ (statusResult.results || []).forEach((row) => {
427
+ const count = row.count || 0;
428
+ stats.total += count;
429
+ if (row.status === "sent") stats.sent = count;
430
+ if (row.status === "failed") stats.failed = count;
431
+ if (row.status === "pending") stats.pending = count;
432
+ });
433
+ (templateResult.results || []).forEach((row) => {
434
+ stats.byTemplate[row.template_id] = row.count;
435
+ });
436
+ return stats;
437
+ }
438
+ /**
439
+ * Get recent failed emails for debugging
440
+ * @param {number} limit - Number of failed emails to retrieve
441
+ */
442
+ async getRecentFailures(limit = 20) {
443
+ const { results } = await this.db.prepare(`
444
+ SELECT id, recipient_email, template_id, subject, error_message, error_code, created_at
445
+ FROM ${this.tableName}
446
+ WHERE status = 'failed'
447
+ ORDER BY created_at DESC
448
+ LIMIT ?
449
+ `).bind(limit).all();
450
+ return results || [];
451
+ }
452
+ };
453
+
238
454
  // src/backend/EmailService.js
239
455
  var EmailService = class {
240
456
  /**
@@ -242,6 +458,7 @@ var EmailService = class {
242
458
  * @param {Object} config - Configuration options
243
459
  * @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
244
460
  * @param {Object} [config.defaults] - Default settings (fromName, fromAddress)
461
+ * @param {boolean|Function} [config.emailLogger] - Email logger: true (default), false (disabled), or custom callback
245
462
  * @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
246
463
  */
247
464
  constructor(env, config = {}, cacheProvider = null) {
@@ -260,10 +477,6 @@ var EmailService = class {
260
477
  // Updater function to save settings to backend
261
478
  // Signature: async (profile, tenantId, settings) => void
262
479
  settingsUpdater: config.settingsUpdater || null,
263
- // Email Logger callback for tracking all email sends
264
- // Signature: async (logEntry) => void
265
- // logEntry: { event: 'pending'|'sent'|'failed', recipientEmail, templateId?, subject?, provider?, messageId?, error?, metadata? }
266
- emailLogger: config.emailLogger || null,
267
480
  // Branding configuration for email templates
268
481
  branding: {
269
482
  brandName: config.branding?.brandName || "Your App",
@@ -273,6 +486,16 @@ var EmailService = class {
273
486
  },
274
487
  ...config
275
488
  };
489
+ if (config.emailLogger === false) {
490
+ this.emailLogger = null;
491
+ } else if (typeof config.emailLogger === "function") {
492
+ this.emailLogger = config.emailLogger;
493
+ } else if (env.DB) {
494
+ const logger = new EmailLogger(env.DB);
495
+ this.emailLogger = logger.createCallback();
496
+ } else {
497
+ this.emailLogger = null;
498
+ }
276
499
  if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
277
500
  this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
278
501
  } else {
@@ -526,9 +749,9 @@ var EmailService = class {
526
749
  const htmlContent = html || htmlBody;
527
750
  const textContent = text || textBody;
528
751
  const templateId = metadata?.templateId || "direct";
529
- if (this.config.emailLogger) {
752
+ if (this.emailLogger) {
530
753
  try {
531
- await this.config.emailLogger({
754
+ await this.emailLogger({
532
755
  event: "pending",
533
756
  recipientEmail: to,
534
757
  recipientUserId: userId,
@@ -565,9 +788,9 @@ var EmailService = class {
565
788
  break;
566
789
  default:
567
790
  console.error(`[EmailService] Unknown provider: ${useProvider}`);
568
- if (this.config.emailLogger) {
791
+ if (this.emailLogger) {
569
792
  try {
570
- await this.config.emailLogger({
793
+ await this.emailLogger({
571
794
  event: "failed",
572
795
  recipientEmail: to,
573
796
  recipientUserId: userId,
@@ -585,9 +808,9 @@ var EmailService = class {
585
808
  }
586
809
  if (result) {
587
810
  const messageId = providerMessageId || crypto.randomUUID();
588
- if (this.config.emailLogger) {
811
+ if (this.emailLogger) {
589
812
  try {
590
- await this.config.emailLogger({
813
+ await this.emailLogger({
591
814
  event: "sent",
592
815
  recipientEmail: to,
593
816
  recipientUserId: userId,
@@ -605,9 +828,9 @@ var EmailService = class {
605
828
  return { success: true, messageId };
606
829
  } else {
607
830
  console.error("[EmailService] Failed to send email to:", to);
608
- if (this.config.emailLogger) {
831
+ if (this.emailLogger) {
609
832
  try {
610
- await this.config.emailLogger({
833
+ await this.emailLogger({
611
834
  event: "failed",
612
835
  recipientEmail: to,
613
836
  recipientUserId: userId,
@@ -625,9 +848,9 @@ var EmailService = class {
625
848
  }
626
849
  } catch (error) {
627
850
  console.error("[EmailService] Error sending email:", error);
628
- if (this.config.emailLogger) {
851
+ if (this.emailLogger) {
629
852
  try {
630
- await this.config.emailLogger({
853
+ await this.emailLogger({
631
854
  event: "failed",
632
855
  recipientEmail: to,
633
856
  recipientUserId: userId,