@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.
package/dist/index.d.cts CHANGED
@@ -5,3 +5,68 @@ export { createEmailRoutes, createTemplateRoutes, createTrackingRoutes } from '.
5
5
  export { encodeTrackingLinks, extractVariables, getWebsiteUrl, markdownToPlainText, resetWebsiteUrlCache, wrapInEmailTemplate } from './common/index.cjs';
6
6
  import 'react';
7
7
  import 'hono';
8
+
9
+ /**
10
+ * Create a simple logger callback for EmailService that logs to D1.
11
+ * This is a convenience function for quick setup.
12
+ *
13
+ * Usage:
14
+ * const emailService = new EmailService(env, {
15
+ * emailLogger: createEmailLoggerCallback(env.DB)
16
+ * });
17
+ */
18
+ declare function createEmailLoggerCallback(db: any, tableName?: string): (entry: any) => Promise<void>;
19
+ /**
20
+ * Email Logger Utility
21
+ *
22
+ * Provides built-in email logging to D1 database.
23
+ * Can be used directly or passed to EmailService as the emailLogger callback.
24
+ */
25
+ declare class EmailLogger {
26
+ /**
27
+ * @param {Object} db - D1 database binding
28
+ * @param {Object} options - Configuration options
29
+ * @param {string} [options.tableName='system_email_logs'] - Table name for logs
30
+ */
31
+ constructor(db: any, options?: {
32
+ tableName?: string;
33
+ });
34
+ db: any;
35
+ tableName: string;
36
+ /**
37
+ * Creates a logger callback function for use with EmailService.
38
+ * Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
39
+ */
40
+ createCallback(): (entry: any) => Promise<void>;
41
+ /**
42
+ * Log an email event (pending, sent, or failed)
43
+ * @param {Object} entry - Log entry
44
+ */
45
+ log(entry: any): Promise<void>;
46
+ /**
47
+ * Query email logs with filtering
48
+ * @param {Object} options - Query options
49
+ */
50
+ query(options?: any): Promise<{
51
+ logs: any;
52
+ total: any;
53
+ }>;
54
+ /**
55
+ * Get email sending statistics
56
+ * @param {number} sinceDays - Number of days to look back
57
+ */
58
+ getStats(sinceDays?: number): Promise<{
59
+ total: number;
60
+ sent: number;
61
+ failed: number;
62
+ pending: number;
63
+ byTemplate: {};
64
+ }>;
65
+ /**
66
+ * Get recent failed emails for debugging
67
+ * @param {number} limit - Number of failed emails to retrieve
68
+ */
69
+ getRecentFailures(limit?: number): Promise<any>;
70
+ }
71
+
72
+ export { EmailLogger, createEmailLoggerCallback };
package/dist/index.d.ts CHANGED
@@ -5,3 +5,68 @@ export { createEmailRoutes, createTemplateRoutes, createTrackingRoutes } from '.
5
5
  export { encodeTrackingLinks, extractVariables, getWebsiteUrl, markdownToPlainText, resetWebsiteUrlCache, wrapInEmailTemplate } from './common/index.js';
6
6
  import 'react';
7
7
  import 'hono';
8
+
9
+ /**
10
+ * Create a simple logger callback for EmailService that logs to D1.
11
+ * This is a convenience function for quick setup.
12
+ *
13
+ * Usage:
14
+ * const emailService = new EmailService(env, {
15
+ * emailLogger: createEmailLoggerCallback(env.DB)
16
+ * });
17
+ */
18
+ declare function createEmailLoggerCallback(db: any, tableName?: string): (entry: any) => Promise<void>;
19
+ /**
20
+ * Email Logger Utility
21
+ *
22
+ * Provides built-in email logging to D1 database.
23
+ * Can be used directly or passed to EmailService as the emailLogger callback.
24
+ */
25
+ declare class EmailLogger {
26
+ /**
27
+ * @param {Object} db - D1 database binding
28
+ * @param {Object} options - Configuration options
29
+ * @param {string} [options.tableName='system_email_logs'] - Table name for logs
30
+ */
31
+ constructor(db: any, options?: {
32
+ tableName?: string;
33
+ });
34
+ db: any;
35
+ tableName: string;
36
+ /**
37
+ * Creates a logger callback function for use with EmailService.
38
+ * Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
39
+ */
40
+ createCallback(): (entry: any) => Promise<void>;
41
+ /**
42
+ * Log an email event (pending, sent, or failed)
43
+ * @param {Object} entry - Log entry
44
+ */
45
+ log(entry: any): Promise<void>;
46
+ /**
47
+ * Query email logs with filtering
48
+ * @param {Object} options - Query options
49
+ */
50
+ query(options?: any): Promise<{
51
+ logs: any;
52
+ total: any;
53
+ }>;
54
+ /**
55
+ * Get email sending statistics
56
+ * @param {number} sinceDays - Number of days to look back
57
+ */
58
+ getStats(sinceDays?: number): Promise<{
59
+ total: number;
60
+ sent: number;
61
+ failed: number;
62
+ pending: number;
63
+ byTemplate: {};
64
+ }>;
65
+ /**
66
+ * Get recent failed emails for debugging
67
+ * @param {number} limit - Number of failed emails to retrieve
68
+ */
69
+ getRecentFailures(limit?: number): Promise<any>;
70
+ }
71
+
72
+ export { EmailLogger, createEmailLoggerCallback };
package/dist/index.js CHANGED
@@ -520,6 +520,226 @@ function createDOCacheProvider(doStub, instanceName = "global") {
520
520
  };
521
521
  }
522
522
 
523
+ // src/backend/EmailLogger.js
524
+ var EmailLogger = class {
525
+ /**
526
+ * @param {Object} db - D1 database binding
527
+ * @param {Object} options - Configuration options
528
+ * @param {string} [options.tableName='system_email_logs'] - Table name for logs
529
+ */
530
+ constructor(db, options = {}) {
531
+ this.db = db;
532
+ this.tableName = options.tableName || "system_email_logs";
533
+ }
534
+ /**
535
+ * Creates a logger callback function for use with EmailService.
536
+ * Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
537
+ */
538
+ createCallback() {
539
+ return async (entry) => {
540
+ await this.log(entry);
541
+ };
542
+ }
543
+ /**
544
+ * Log an email event (pending, sent, or failed)
545
+ * @param {Object} entry - Log entry
546
+ */
547
+ async log(entry) {
548
+ const {
549
+ event,
550
+ recipientEmail,
551
+ recipientUserId,
552
+ templateId,
553
+ subject,
554
+ provider,
555
+ messageId,
556
+ batchId,
557
+ error,
558
+ errorCode,
559
+ metadata
560
+ } = entry;
561
+ try {
562
+ if (event === "pending") {
563
+ const id = crypto.randomUUID().replace(/-/g, "");
564
+ await this.db.prepare(`
565
+ INSERT INTO ${this.tableName}
566
+ (id, batch_id, recipient_email, recipient_user_id, template_id, subject, status, metadata, created_at)
567
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, strftime('%s', 'now'))
568
+ `).bind(
569
+ id,
570
+ batchId || null,
571
+ recipientEmail,
572
+ recipientUserId || null,
573
+ templateId || "direct",
574
+ subject || null,
575
+ metadata ? JSON.stringify(metadata) : null
576
+ ).run();
577
+ } else if (event === "sent") {
578
+ await this.db.prepare(`
579
+ UPDATE ${this.tableName}
580
+ SET status = 'sent',
581
+ provider = ?,
582
+ provider_message_id = ?,
583
+ sent_at = strftime('%s', 'now')
584
+ WHERE recipient_email = ?
585
+ AND template_id = ?
586
+ AND status = 'pending'
587
+ ORDER BY created_at DESC
588
+ LIMIT 1
589
+ `).bind(
590
+ provider || null,
591
+ messageId || null,
592
+ recipientEmail,
593
+ templateId || "direct"
594
+ ).run();
595
+ } else if (event === "failed") {
596
+ await this.db.prepare(`
597
+ UPDATE ${this.tableName}
598
+ SET status = 'failed',
599
+ provider = ?,
600
+ error_message = ?,
601
+ error_code = ?
602
+ WHERE recipient_email = ?
603
+ AND template_id = ?
604
+ AND status = 'pending'
605
+ ORDER BY created_at DESC
606
+ LIMIT 1
607
+ `).bind(
608
+ provider || null,
609
+ error || null,
610
+ errorCode || null,
611
+ recipientEmail,
612
+ templateId || "direct"
613
+ ).run();
614
+ }
615
+ } catch (e) {
616
+ console.error("[EmailLogger] Failed to log:", e);
617
+ }
618
+ }
619
+ /**
620
+ * Query email logs with filtering
621
+ * @param {Object} options - Query options
622
+ */
623
+ async query(options = {}) {
624
+ const {
625
+ recipientEmail,
626
+ recipientUserId,
627
+ templateId,
628
+ status,
629
+ batchId,
630
+ limit = 50,
631
+ offset = 0
632
+ } = options;
633
+ const conditions = [];
634
+ const bindings = [];
635
+ if (recipientEmail) {
636
+ conditions.push("recipient_email = ?");
637
+ bindings.push(recipientEmail);
638
+ }
639
+ if (recipientUserId) {
640
+ conditions.push("recipient_user_id = ?");
641
+ bindings.push(recipientUserId);
642
+ }
643
+ if (templateId) {
644
+ conditions.push("template_id = ?");
645
+ bindings.push(templateId);
646
+ }
647
+ if (status) {
648
+ conditions.push("status = ?");
649
+ bindings.push(status);
650
+ }
651
+ if (batchId) {
652
+ conditions.push("batch_id = ?");
653
+ bindings.push(batchId);
654
+ }
655
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
656
+ const countResult = await this.db.prepare(
657
+ `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
658
+ ).bind(...bindings).first();
659
+ const { results } = await this.db.prepare(`
660
+ SELECT id, batch_id, recipient_email, recipient_user_id, template_id, subject,
661
+ status, provider, provider_message_id, error_message, error_code, metadata,
662
+ created_at, sent_at
663
+ FROM ${this.tableName}
664
+ ${whereClause}
665
+ ORDER BY created_at DESC
666
+ LIMIT ? OFFSET ?
667
+ `).bind(...bindings, limit, offset).all();
668
+ const logs = (results || []).map((row) => ({
669
+ id: row.id,
670
+ batchId: row.batch_id,
671
+ recipientEmail: row.recipient_email,
672
+ recipientUserId: row.recipient_user_id,
673
+ templateId: row.template_id,
674
+ subject: row.subject,
675
+ status: row.status,
676
+ provider: row.provider,
677
+ providerMessageId: row.provider_message_id,
678
+ errorMessage: row.error_message,
679
+ errorCode: row.error_code,
680
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
681
+ createdAt: row.created_at,
682
+ sentAt: row.sent_at
683
+ }));
684
+ return { logs, total: countResult?.count || 0 };
685
+ }
686
+ /**
687
+ * Get email sending statistics
688
+ * @param {number} sinceDays - Number of days to look back
689
+ */
690
+ async getStats(sinceDays = 7) {
691
+ const sinceTimestamp = Math.floor(Date.now() / 1e3) - sinceDays * 24 * 60 * 60;
692
+ const statusResult = await this.db.prepare(`
693
+ SELECT status, COUNT(*) as count
694
+ FROM ${this.tableName}
695
+ WHERE created_at >= ?
696
+ GROUP BY status
697
+ `).bind(sinceTimestamp).all();
698
+ const templateResult = await this.db.prepare(`
699
+ SELECT template_id, COUNT(*) as count
700
+ FROM ${this.tableName}
701
+ WHERE created_at >= ?
702
+ GROUP BY template_id
703
+ `).bind(sinceTimestamp).all();
704
+ const stats = {
705
+ total: 0,
706
+ sent: 0,
707
+ failed: 0,
708
+ pending: 0,
709
+ byTemplate: {}
710
+ };
711
+ (statusResult.results || []).forEach((row) => {
712
+ const count = row.count || 0;
713
+ stats.total += count;
714
+ if (row.status === "sent") stats.sent = count;
715
+ if (row.status === "failed") stats.failed = count;
716
+ if (row.status === "pending") stats.pending = count;
717
+ });
718
+ (templateResult.results || []).forEach((row) => {
719
+ stats.byTemplate[row.template_id] = row.count;
720
+ });
721
+ return stats;
722
+ }
723
+ /**
724
+ * Get recent failed emails for debugging
725
+ * @param {number} limit - Number of failed emails to retrieve
726
+ */
727
+ async getRecentFailures(limit = 20) {
728
+ const { results } = await this.db.prepare(`
729
+ SELECT id, recipient_email, template_id, subject, error_message, error_code, created_at
730
+ FROM ${this.tableName}
731
+ WHERE status = 'failed'
732
+ ORDER BY created_at DESC
733
+ LIMIT ?
734
+ `).bind(limit).all();
735
+ return results || [];
736
+ }
737
+ };
738
+ function createEmailLoggerCallback(db, tableName = "system_email_logs") {
739
+ const logger = new EmailLogger(db, { tableName });
740
+ return logger.createCallback();
741
+ }
742
+
523
743
  // src/backend/EmailService.js
524
744
  var EmailService = class {
525
745
  /**
@@ -527,6 +747,7 @@ var EmailService = class {
527
747
  * @param {Object} config - Configuration options
528
748
  * @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
529
749
  * @param {Object} [config.defaults] - Default settings (fromName, fromAddress)
750
+ * @param {boolean|Function} [config.emailLogger] - Email logger: true (default), false (disabled), or custom callback
530
751
  * @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
531
752
  */
532
753
  constructor(env, config = {}, cacheProvider = null) {
@@ -554,6 +775,16 @@ var EmailService = class {
554
775
  },
555
776
  ...config
556
777
  };
778
+ if (config.emailLogger === false) {
779
+ this.emailLogger = null;
780
+ } else if (typeof config.emailLogger === "function") {
781
+ this.emailLogger = config.emailLogger;
782
+ } else if (env.DB) {
783
+ const logger = new EmailLogger(env.DB);
784
+ this.emailLogger = logger.createCallback();
785
+ } else {
786
+ this.emailLogger = null;
787
+ }
557
788
  if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
558
789
  this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
559
790
  } else {
@@ -803,13 +1034,30 @@ var EmailService = class {
803
1034
  * @param {Object} [params.metadata] - Additional metadata
804
1035
  * @returns {Promise<Object>} Delivery result
805
1036
  */
806
- async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {} }) {
1037
+ async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {}, batchId = null, userId = null }) {
807
1038
  const htmlContent = html || htmlBody;
808
1039
  const textContent = text || textBody;
1040
+ const templateId = metadata?.templateId || "direct";
1041
+ if (this.emailLogger) {
1042
+ try {
1043
+ await this.emailLogger({
1044
+ event: "pending",
1045
+ recipientEmail: to,
1046
+ recipientUserId: userId,
1047
+ templateId,
1048
+ subject,
1049
+ batchId,
1050
+ metadata
1051
+ });
1052
+ } catch (e) {
1053
+ console.warn("[EmailService] emailLogger pending failed:", e);
1054
+ }
1055
+ }
809
1056
  try {
810
1057
  const settings = await this.loadSettings(profile, tenantId);
811
1058
  const useProvider = provider || settings.provider || "mailchannels";
812
1059
  let result;
1060
+ let providerMessageId = null;
813
1061
  switch (useProvider) {
814
1062
  case "mailchannels":
815
1063
  result = await this.sendViaMailChannels(to, subject, htmlContent, textContent, settings, metadata);
@@ -819,22 +1067,91 @@ var EmailService = class {
819
1067
  break;
820
1068
  case "resend":
821
1069
  result = await this.sendViaResend(to, subject, htmlContent, textContent, settings, metadata);
1070
+ if (result && typeof result === "object" && result.id) {
1071
+ providerMessageId = result.id;
1072
+ result = true;
1073
+ }
822
1074
  break;
823
1075
  case "sendpulse":
824
1076
  result = await this.sendViaSendPulse(to, subject, htmlContent, textContent, settings, metadata);
825
1077
  break;
826
1078
  default:
827
1079
  console.error(`[EmailService] Unknown provider: ${useProvider}`);
1080
+ if (this.emailLogger) {
1081
+ try {
1082
+ await this.emailLogger({
1083
+ event: "failed",
1084
+ recipientEmail: to,
1085
+ recipientUserId: userId,
1086
+ templateId,
1087
+ subject,
1088
+ provider: useProvider,
1089
+ batchId,
1090
+ error: `Unknown email provider: ${useProvider}`,
1091
+ metadata
1092
+ });
1093
+ } catch (e) {
1094
+ }
1095
+ }
828
1096
  return { success: false, error: `Unknown email provider: ${useProvider}` };
829
1097
  }
830
1098
  if (result) {
831
- return { success: true, messageId: crypto.randomUUID() };
1099
+ const messageId = providerMessageId || crypto.randomUUID();
1100
+ if (this.emailLogger) {
1101
+ try {
1102
+ await this.emailLogger({
1103
+ event: "sent",
1104
+ recipientEmail: to,
1105
+ recipientUserId: userId,
1106
+ templateId,
1107
+ subject,
1108
+ provider: useProvider,
1109
+ messageId,
1110
+ batchId,
1111
+ metadata
1112
+ });
1113
+ } catch (e) {
1114
+ console.warn("[EmailService] emailLogger sent failed:", e);
1115
+ }
1116
+ }
1117
+ return { success: true, messageId };
832
1118
  } else {
833
1119
  console.error("[EmailService] Failed to send email to:", to);
1120
+ if (this.emailLogger) {
1121
+ try {
1122
+ await this.emailLogger({
1123
+ event: "failed",
1124
+ recipientEmail: to,
1125
+ recipientUserId: userId,
1126
+ templateId,
1127
+ subject,
1128
+ provider: useProvider,
1129
+ batchId,
1130
+ error: "Failed to send email",
1131
+ metadata
1132
+ });
1133
+ } catch (e) {
1134
+ }
1135
+ }
834
1136
  return { success: false, error: "Failed to send email" };
835
1137
  }
836
1138
  } catch (error) {
837
1139
  console.error("[EmailService] Error sending email:", error);
1140
+ if (this.emailLogger) {
1141
+ try {
1142
+ await this.emailLogger({
1143
+ event: "failed",
1144
+ recipientEmail: to,
1145
+ recipientUserId: userId,
1146
+ templateId,
1147
+ subject,
1148
+ batchId,
1149
+ error: error.message,
1150
+ metadata
1151
+ });
1152
+ } catch (e) {
1153
+ }
1154
+ }
838
1155
  return { success: false, error: error.message };
839
1156
  }
840
1157
  }
@@ -1694,7 +2011,7 @@ var TemplateManager = ({
1694
2011
  },
1695
2012
  /* @__PURE__ */ React3.createElement("svg", { className: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, /* @__PURE__ */ React3.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 4v16m8-8H4" })),
1696
2013
  "Create Template"
1697
- )), error && /* @__PURE__ */ React3.createElement("div", { className: "mb-4 p-3 rounded-lg bg-red-50 text-red-800 border border-red-200" }, error, /* @__PURE__ */ React3.createElement("button", { onClick: () => setError(null), className: "ml-2 underline" }, "Dismiss")), /* @__PURE__ */ React3.createElement("div", { className: "grid grid-cols-4 gap-4 mb-6" }, /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-gray-900" }, templates.length), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Total")), /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-green-600" }, templates.filter((t) => t.is_active).length), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Active")), /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-yellow-600" }, templates.filter((t) => !t.is_active).length), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Inactive")), /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-blue-600" }, new Set(templates.map((t) => t.template_type)).size), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Types"))), /* @__PURE__ */ React3.createElement("div", { className: "flex gap-2 mb-6 border-b border-gray-200 pb-3 flex-wrap" }, uniqueTypes.map((type) => /* @__PURE__ */ React3.createElement(
2014
+ )), error && /* @__PURE__ */ React3.createElement("div", { className: "mb-4 p-3 rounded-lg bg-red-50 text-red-800 border border-red-200" }, error, /* @__PURE__ */ React3.createElement("button", { onClick: () => setError(null), className: "ml-2 underline" }, "Dismiss")), /* @__PURE__ */ React3.createElement("div", { className: "flex gap-4 mb-6" }, /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4 flex-1" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-gray-900" }, templates.length), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Total")), /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4 flex-1" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-green-600" }, templates.filter((t) => t.is_active).length), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Active")), /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4 flex-1" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-yellow-600" }, templates.filter((t) => !t.is_active).length), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Inactive")), /* @__PURE__ */ React3.createElement("div", { className: "bg-white rounded-lg border border-gray-200 p-4 flex-1" }, /* @__PURE__ */ React3.createElement("div", { className: "text-2xl font-bold text-blue-600" }, new Set(templates.map((t) => t.template_type)).size), /* @__PURE__ */ React3.createElement("div", { className: "text-sm text-gray-500" }, "Types"))), /* @__PURE__ */ React3.createElement("div", { className: "flex gap-2 mb-6 border-b border-gray-200 pb-3 flex-wrap" }, uniqueTypes.map((type) => /* @__PURE__ */ React3.createElement(
1698
2015
  "button",
1699
2016
  {
1700
2017
  key: type,
@@ -1818,11 +2135,13 @@ var TemplateManager = ({
1818
2135
  )))));
1819
2136
  };
1820
2137
  export {
2138
+ EmailLogger,
1821
2139
  EmailService,
1822
2140
  EmailingCacheDO,
1823
2141
  TemplateEditor,
1824
2142
  TemplateManager,
1825
2143
  createDOCacheProvider,
2144
+ createEmailLoggerCallback,
1826
2145
  createEmailRoutes,
1827
2146
  createTemplateRoutes,
1828
2147
  createTrackingRoutes,