@contentgrowth/content-emailing 0.6.2 → 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) {
@@ -302,6 +519,16 @@ var EmailService = class {
302
519
  },
303
520
  ...config
304
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
+ }
305
532
  if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
306
533
  this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
307
534
  } else {
@@ -551,13 +778,30 @@ var EmailService = class {
551
778
  * @param {Object} [params.metadata] - Additional metadata
552
779
  * @returns {Promise<Object>} Delivery result
553
780
  */
554
- async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {} }) {
781
+ async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {}, batchId = null, userId = null }) {
555
782
  const htmlContent = html || htmlBody;
556
783
  const textContent = text || textBody;
784
+ const templateId = metadata?.templateId || "direct";
785
+ if (this.emailLogger) {
786
+ try {
787
+ await this.emailLogger({
788
+ event: "pending",
789
+ recipientEmail: to,
790
+ recipientUserId: userId,
791
+ templateId,
792
+ subject,
793
+ batchId,
794
+ metadata
795
+ });
796
+ } catch (e) {
797
+ console.warn("[EmailService] emailLogger pending failed:", e);
798
+ }
799
+ }
557
800
  try {
558
801
  const settings = await this.loadSettings(profile, tenantId);
559
802
  const useProvider = provider || settings.provider || "mailchannels";
560
803
  let result;
804
+ let providerMessageId = null;
561
805
  switch (useProvider) {
562
806
  case "mailchannels":
563
807
  result = await this.sendViaMailChannels(to, subject, htmlContent, textContent, settings, metadata);
@@ -567,22 +811,91 @@ var EmailService = class {
567
811
  break;
568
812
  case "resend":
569
813
  result = await this.sendViaResend(to, subject, htmlContent, textContent, settings, metadata);
814
+ if (result && typeof result === "object" && result.id) {
815
+ providerMessageId = result.id;
816
+ result = true;
817
+ }
570
818
  break;
571
819
  case "sendpulse":
572
820
  result = await this.sendViaSendPulse(to, subject, htmlContent, textContent, settings, metadata);
573
821
  break;
574
822
  default:
575
823
  console.error(`[EmailService] Unknown provider: ${useProvider}`);
824
+ if (this.emailLogger) {
825
+ try {
826
+ await this.emailLogger({
827
+ event: "failed",
828
+ recipientEmail: to,
829
+ recipientUserId: userId,
830
+ templateId,
831
+ subject,
832
+ provider: useProvider,
833
+ batchId,
834
+ error: `Unknown email provider: ${useProvider}`,
835
+ metadata
836
+ });
837
+ } catch (e) {
838
+ }
839
+ }
576
840
  return { success: false, error: `Unknown email provider: ${useProvider}` };
577
841
  }
578
842
  if (result) {
579
- return { success: true, messageId: crypto.randomUUID() };
843
+ const messageId = providerMessageId || crypto.randomUUID();
844
+ if (this.emailLogger) {
845
+ try {
846
+ await this.emailLogger({
847
+ event: "sent",
848
+ recipientEmail: to,
849
+ recipientUserId: userId,
850
+ templateId,
851
+ subject,
852
+ provider: useProvider,
853
+ messageId,
854
+ batchId,
855
+ metadata
856
+ });
857
+ } catch (e) {
858
+ console.warn("[EmailService] emailLogger sent failed:", e);
859
+ }
860
+ }
861
+ return { success: true, messageId };
580
862
  } else {
581
863
  console.error("[EmailService] Failed to send email to:", to);
864
+ if (this.emailLogger) {
865
+ try {
866
+ await this.emailLogger({
867
+ event: "failed",
868
+ recipientEmail: to,
869
+ recipientUserId: userId,
870
+ templateId,
871
+ subject,
872
+ provider: useProvider,
873
+ batchId,
874
+ error: "Failed to send email",
875
+ metadata
876
+ });
877
+ } catch (e) {
878
+ }
879
+ }
582
880
  return { success: false, error: "Failed to send email" };
583
881
  }
584
882
  } catch (error) {
585
883
  console.error("[EmailService] Error sending email:", error);
884
+ if (this.emailLogger) {
885
+ try {
886
+ await this.emailLogger({
887
+ event: "failed",
888
+ recipientEmail: to,
889
+ recipientUserId: userId,
890
+ templateId,
891
+ subject,
892
+ batchId,
893
+ error: error.message,
894
+ metadata
895
+ });
896
+ } catch (e) {
897
+ }
898
+ }
586
899
  return { success: false, error: error.message };
587
900
  }
588
901
  }