@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.
- package/dist/backend/EmailService.cjs +237 -14
- package/dist/backend/EmailService.cjs.map +1 -1
- package/dist/backend/EmailService.d.cts +4 -1
- package/dist/backend/EmailService.d.ts +4 -1
- package/dist/backend/EmailService.js +237 -14
- package/dist/backend/EmailService.js.map +1 -1
- package/dist/backend/routes/index.cjs +237 -14
- package/dist/backend/routes/index.cjs.map +1 -1
- package/dist/backend/routes/index.js +237 -14
- package/dist/backend/routes/index.js.map +1 -1
- package/dist/index.cjs +241 -234
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +241 -234
- package/dist/index.js.map +1 -1
- package/package.json +13 -2
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) {
|
|
@@ -545,10 +766,6 @@ var EmailService = class {
|
|
|
545
766
|
// Updater function to save settings to backend
|
|
546
767
|
// Signature: async (profile, tenantId, settings) => void
|
|
547
768
|
settingsUpdater: config.settingsUpdater || null,
|
|
548
|
-
// Email Logger callback for tracking all email sends
|
|
549
|
-
// Signature: async (logEntry) => void
|
|
550
|
-
// logEntry: { event: 'pending'|'sent'|'failed', recipientEmail, templateId?, subject?, provider?, messageId?, error?, metadata? }
|
|
551
|
-
emailLogger: config.emailLogger || null,
|
|
552
769
|
// Branding configuration for email templates
|
|
553
770
|
branding: {
|
|
554
771
|
brandName: config.branding?.brandName || "Your App",
|
|
@@ -558,6 +775,16 @@ var EmailService = class {
|
|
|
558
775
|
},
|
|
559
776
|
...config
|
|
560
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
|
+
}
|
|
561
788
|
if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
|
|
562
789
|
this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
|
|
563
790
|
} else {
|
|
@@ -811,9 +1038,9 @@ var EmailService = class {
|
|
|
811
1038
|
const htmlContent = html || htmlBody;
|
|
812
1039
|
const textContent = text || textBody;
|
|
813
1040
|
const templateId = metadata?.templateId || "direct";
|
|
814
|
-
if (this.
|
|
1041
|
+
if (this.emailLogger) {
|
|
815
1042
|
try {
|
|
816
|
-
await this.
|
|
1043
|
+
await this.emailLogger({
|
|
817
1044
|
event: "pending",
|
|
818
1045
|
recipientEmail: to,
|
|
819
1046
|
recipientUserId: userId,
|
|
@@ -850,9 +1077,9 @@ var EmailService = class {
|
|
|
850
1077
|
break;
|
|
851
1078
|
default:
|
|
852
1079
|
console.error(`[EmailService] Unknown provider: ${useProvider}`);
|
|
853
|
-
if (this.
|
|
1080
|
+
if (this.emailLogger) {
|
|
854
1081
|
try {
|
|
855
|
-
await this.
|
|
1082
|
+
await this.emailLogger({
|
|
856
1083
|
event: "failed",
|
|
857
1084
|
recipientEmail: to,
|
|
858
1085
|
recipientUserId: userId,
|
|
@@ -870,9 +1097,9 @@ var EmailService = class {
|
|
|
870
1097
|
}
|
|
871
1098
|
if (result) {
|
|
872
1099
|
const messageId = providerMessageId || crypto.randomUUID();
|
|
873
|
-
if (this.
|
|
1100
|
+
if (this.emailLogger) {
|
|
874
1101
|
try {
|
|
875
|
-
await this.
|
|
1102
|
+
await this.emailLogger({
|
|
876
1103
|
event: "sent",
|
|
877
1104
|
recipientEmail: to,
|
|
878
1105
|
recipientUserId: userId,
|
|
@@ -890,9 +1117,9 @@ var EmailService = class {
|
|
|
890
1117
|
return { success: true, messageId };
|
|
891
1118
|
} else {
|
|
892
1119
|
console.error("[EmailService] Failed to send email to:", to);
|
|
893
|
-
if (this.
|
|
1120
|
+
if (this.emailLogger) {
|
|
894
1121
|
try {
|
|
895
|
-
await this.
|
|
1122
|
+
await this.emailLogger({
|
|
896
1123
|
event: "failed",
|
|
897
1124
|
recipientEmail: to,
|
|
898
1125
|
recipientUserId: userId,
|
|
@@ -910,9 +1137,9 @@ var EmailService = class {
|
|
|
910
1137
|
}
|
|
911
1138
|
} catch (error) {
|
|
912
1139
|
console.error("[EmailService] Error sending email:", error);
|
|
913
|
-
if (this.
|
|
1140
|
+
if (this.emailLogger) {
|
|
914
1141
|
try {
|
|
915
|
-
await this.
|
|
1142
|
+
await this.emailLogger({
|
|
916
1143
|
event: "failed",
|
|
917
1144
|
recipientEmail: to,
|
|
918
1145
|
recipientUserId: userId,
|
|
@@ -1116,226 +1343,6 @@ var EmailService = class {
|
|
|
1116
1343
|
}
|
|
1117
1344
|
};
|
|
1118
1345
|
|
|
1119
|
-
// src/backend/EmailLogger.js
|
|
1120
|
-
var EmailLogger = class {
|
|
1121
|
-
/**
|
|
1122
|
-
* @param {Object} db - D1 database binding
|
|
1123
|
-
* @param {Object} options - Configuration options
|
|
1124
|
-
* @param {string} [options.tableName='system_email_logs'] - Table name for logs
|
|
1125
|
-
*/
|
|
1126
|
-
constructor(db, options = {}) {
|
|
1127
|
-
this.db = db;
|
|
1128
|
-
this.tableName = options.tableName || "system_email_logs";
|
|
1129
|
-
}
|
|
1130
|
-
/**
|
|
1131
|
-
* Creates a logger callback function for use with EmailService.
|
|
1132
|
-
* Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
|
|
1133
|
-
*/
|
|
1134
|
-
createCallback() {
|
|
1135
|
-
return async (entry) => {
|
|
1136
|
-
await this.log(entry);
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
/**
|
|
1140
|
-
* Log an email event (pending, sent, or failed)
|
|
1141
|
-
* @param {Object} entry - Log entry
|
|
1142
|
-
*/
|
|
1143
|
-
async log(entry) {
|
|
1144
|
-
const {
|
|
1145
|
-
event,
|
|
1146
|
-
recipientEmail,
|
|
1147
|
-
recipientUserId,
|
|
1148
|
-
templateId,
|
|
1149
|
-
subject,
|
|
1150
|
-
provider,
|
|
1151
|
-
messageId,
|
|
1152
|
-
batchId,
|
|
1153
|
-
error,
|
|
1154
|
-
errorCode,
|
|
1155
|
-
metadata
|
|
1156
|
-
} = entry;
|
|
1157
|
-
try {
|
|
1158
|
-
if (event === "pending") {
|
|
1159
|
-
const id = crypto.randomUUID().replace(/-/g, "");
|
|
1160
|
-
await this.db.prepare(`
|
|
1161
|
-
INSERT INTO ${this.tableName}
|
|
1162
|
-
(id, batch_id, recipient_email, recipient_user_id, template_id, subject, status, metadata, created_at)
|
|
1163
|
-
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, strftime('%s', 'now'))
|
|
1164
|
-
`).bind(
|
|
1165
|
-
id,
|
|
1166
|
-
batchId || null,
|
|
1167
|
-
recipientEmail,
|
|
1168
|
-
recipientUserId || null,
|
|
1169
|
-
templateId || "direct",
|
|
1170
|
-
subject || null,
|
|
1171
|
-
metadata ? JSON.stringify(metadata) : null
|
|
1172
|
-
).run();
|
|
1173
|
-
} else if (event === "sent") {
|
|
1174
|
-
await this.db.prepare(`
|
|
1175
|
-
UPDATE ${this.tableName}
|
|
1176
|
-
SET status = 'sent',
|
|
1177
|
-
provider = ?,
|
|
1178
|
-
provider_message_id = ?,
|
|
1179
|
-
sent_at = strftime('%s', 'now')
|
|
1180
|
-
WHERE recipient_email = ?
|
|
1181
|
-
AND template_id = ?
|
|
1182
|
-
AND status = 'pending'
|
|
1183
|
-
ORDER BY created_at DESC
|
|
1184
|
-
LIMIT 1
|
|
1185
|
-
`).bind(
|
|
1186
|
-
provider || null,
|
|
1187
|
-
messageId || null,
|
|
1188
|
-
recipientEmail,
|
|
1189
|
-
templateId || "direct"
|
|
1190
|
-
).run();
|
|
1191
|
-
} else if (event === "failed") {
|
|
1192
|
-
await this.db.prepare(`
|
|
1193
|
-
UPDATE ${this.tableName}
|
|
1194
|
-
SET status = 'failed',
|
|
1195
|
-
provider = ?,
|
|
1196
|
-
error_message = ?,
|
|
1197
|
-
error_code = ?
|
|
1198
|
-
WHERE recipient_email = ?
|
|
1199
|
-
AND template_id = ?
|
|
1200
|
-
AND status = 'pending'
|
|
1201
|
-
ORDER BY created_at DESC
|
|
1202
|
-
LIMIT 1
|
|
1203
|
-
`).bind(
|
|
1204
|
-
provider || null,
|
|
1205
|
-
error || null,
|
|
1206
|
-
errorCode || null,
|
|
1207
|
-
recipientEmail,
|
|
1208
|
-
templateId || "direct"
|
|
1209
|
-
).run();
|
|
1210
|
-
}
|
|
1211
|
-
} catch (e) {
|
|
1212
|
-
console.error("[EmailLogger] Failed to log:", e);
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
/**
|
|
1216
|
-
* Query email logs with filtering
|
|
1217
|
-
* @param {Object} options - Query options
|
|
1218
|
-
*/
|
|
1219
|
-
async query(options = {}) {
|
|
1220
|
-
const {
|
|
1221
|
-
recipientEmail,
|
|
1222
|
-
recipientUserId,
|
|
1223
|
-
templateId,
|
|
1224
|
-
status,
|
|
1225
|
-
batchId,
|
|
1226
|
-
limit = 50,
|
|
1227
|
-
offset = 0
|
|
1228
|
-
} = options;
|
|
1229
|
-
const conditions = [];
|
|
1230
|
-
const bindings = [];
|
|
1231
|
-
if (recipientEmail) {
|
|
1232
|
-
conditions.push("recipient_email = ?");
|
|
1233
|
-
bindings.push(recipientEmail);
|
|
1234
|
-
}
|
|
1235
|
-
if (recipientUserId) {
|
|
1236
|
-
conditions.push("recipient_user_id = ?");
|
|
1237
|
-
bindings.push(recipientUserId);
|
|
1238
|
-
}
|
|
1239
|
-
if (templateId) {
|
|
1240
|
-
conditions.push("template_id = ?");
|
|
1241
|
-
bindings.push(templateId);
|
|
1242
|
-
}
|
|
1243
|
-
if (status) {
|
|
1244
|
-
conditions.push("status = ?");
|
|
1245
|
-
bindings.push(status);
|
|
1246
|
-
}
|
|
1247
|
-
if (batchId) {
|
|
1248
|
-
conditions.push("batch_id = ?");
|
|
1249
|
-
bindings.push(batchId);
|
|
1250
|
-
}
|
|
1251
|
-
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1252
|
-
const countResult = await this.db.prepare(
|
|
1253
|
-
`SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
|
|
1254
|
-
).bind(...bindings).first();
|
|
1255
|
-
const { results } = await this.db.prepare(`
|
|
1256
|
-
SELECT id, batch_id, recipient_email, recipient_user_id, template_id, subject,
|
|
1257
|
-
status, provider, provider_message_id, error_message, error_code, metadata,
|
|
1258
|
-
created_at, sent_at
|
|
1259
|
-
FROM ${this.tableName}
|
|
1260
|
-
${whereClause}
|
|
1261
|
-
ORDER BY created_at DESC
|
|
1262
|
-
LIMIT ? OFFSET ?
|
|
1263
|
-
`).bind(...bindings, limit, offset).all();
|
|
1264
|
-
const logs = (results || []).map((row) => ({
|
|
1265
|
-
id: row.id,
|
|
1266
|
-
batchId: row.batch_id,
|
|
1267
|
-
recipientEmail: row.recipient_email,
|
|
1268
|
-
recipientUserId: row.recipient_user_id,
|
|
1269
|
-
templateId: row.template_id,
|
|
1270
|
-
subject: row.subject,
|
|
1271
|
-
status: row.status,
|
|
1272
|
-
provider: row.provider,
|
|
1273
|
-
providerMessageId: row.provider_message_id,
|
|
1274
|
-
errorMessage: row.error_message,
|
|
1275
|
-
errorCode: row.error_code,
|
|
1276
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
1277
|
-
createdAt: row.created_at,
|
|
1278
|
-
sentAt: row.sent_at
|
|
1279
|
-
}));
|
|
1280
|
-
return { logs, total: countResult?.count || 0 };
|
|
1281
|
-
}
|
|
1282
|
-
/**
|
|
1283
|
-
* Get email sending statistics
|
|
1284
|
-
* @param {number} sinceDays - Number of days to look back
|
|
1285
|
-
*/
|
|
1286
|
-
async getStats(sinceDays = 7) {
|
|
1287
|
-
const sinceTimestamp = Math.floor(Date.now() / 1e3) - sinceDays * 24 * 60 * 60;
|
|
1288
|
-
const statusResult = await this.db.prepare(`
|
|
1289
|
-
SELECT status, COUNT(*) as count
|
|
1290
|
-
FROM ${this.tableName}
|
|
1291
|
-
WHERE created_at >= ?
|
|
1292
|
-
GROUP BY status
|
|
1293
|
-
`).bind(sinceTimestamp).all();
|
|
1294
|
-
const templateResult = await this.db.prepare(`
|
|
1295
|
-
SELECT template_id, COUNT(*) as count
|
|
1296
|
-
FROM ${this.tableName}
|
|
1297
|
-
WHERE created_at >= ?
|
|
1298
|
-
GROUP BY template_id
|
|
1299
|
-
`).bind(sinceTimestamp).all();
|
|
1300
|
-
const stats = {
|
|
1301
|
-
total: 0,
|
|
1302
|
-
sent: 0,
|
|
1303
|
-
failed: 0,
|
|
1304
|
-
pending: 0,
|
|
1305
|
-
byTemplate: {}
|
|
1306
|
-
};
|
|
1307
|
-
(statusResult.results || []).forEach((row) => {
|
|
1308
|
-
const count = row.count || 0;
|
|
1309
|
-
stats.total += count;
|
|
1310
|
-
if (row.status === "sent") stats.sent = count;
|
|
1311
|
-
if (row.status === "failed") stats.failed = count;
|
|
1312
|
-
if (row.status === "pending") stats.pending = count;
|
|
1313
|
-
});
|
|
1314
|
-
(templateResult.results || []).forEach((row) => {
|
|
1315
|
-
stats.byTemplate[row.template_id] = row.count;
|
|
1316
|
-
});
|
|
1317
|
-
return stats;
|
|
1318
|
-
}
|
|
1319
|
-
/**
|
|
1320
|
-
* Get recent failed emails for debugging
|
|
1321
|
-
* @param {number} limit - Number of failed emails to retrieve
|
|
1322
|
-
*/
|
|
1323
|
-
async getRecentFailures(limit = 20) {
|
|
1324
|
-
const { results } = await this.db.prepare(`
|
|
1325
|
-
SELECT id, recipient_email, template_id, subject, error_message, error_code, created_at
|
|
1326
|
-
FROM ${this.tableName}
|
|
1327
|
-
WHERE status = 'failed'
|
|
1328
|
-
ORDER BY created_at DESC
|
|
1329
|
-
LIMIT ?
|
|
1330
|
-
`).bind(limit).all();
|
|
1331
|
-
return results || [];
|
|
1332
|
-
}
|
|
1333
|
-
};
|
|
1334
|
-
function createEmailLoggerCallback(db, tableName = "system_email_logs") {
|
|
1335
|
-
const logger = new EmailLogger(db, { tableName });
|
|
1336
|
-
return logger.createCallback();
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
1346
|
// src/backend/routes/index.js
|
|
1340
1347
|
import { Hono as Hono3 } from "hono";
|
|
1341
1348
|
|