@contentgrowth/content-emailing 0.6.1 → 0.7.1
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 +111 -4
- package/dist/backend/EmailService.cjs.map +1 -1
- package/dist/backend/EmailService.d.cts +7 -1
- package/dist/backend/EmailService.d.ts +7 -1
- package/dist/backend/EmailService.js +111 -4
- package/dist/backend/EmailService.js.map +1 -1
- package/dist/backend/routes/index.cjs +111 -4
- package/dist/backend/routes/index.cjs.map +1 -1
- package/dist/backend/routes/index.js +111 -4
- package/dist/backend/routes/index.js.map +1 -1
- package/dist/frontend/index.cjs +144 -1
- package/dist/frontend/index.cjs.map +1 -1
- package/dist/frontend/index.d.cts +66 -1
- package/dist/frontend/index.d.ts +66 -1
- package/dist/frontend/index.js +143 -1
- package/dist/frontend/index.js.map +1 -1
- package/dist/index.cjs +336 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +334 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema.sql +32 -0
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
|
@@ -545,6 +545,10 @@ var EmailService = class {
|
|
|
545
545
|
// Updater function to save settings to backend
|
|
546
546
|
// Signature: async (profile, tenantId, settings) => void
|
|
547
547
|
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,
|
|
548
552
|
// Branding configuration for email templates
|
|
549
553
|
branding: {
|
|
550
554
|
brandName: config.branding?.brandName || "Your App",
|
|
@@ -720,11 +724,28 @@ var EmailService = class {
|
|
|
720
724
|
}
|
|
721
725
|
}
|
|
722
726
|
// --- Rendering ---
|
|
727
|
+
/**
|
|
728
|
+
* Pre-process template data to auto-format URLs
|
|
729
|
+
* Scans for strings starting with http:// or https:// and wraps them in Markdown links
|
|
730
|
+
*/
|
|
731
|
+
_preprocessData(data) {
|
|
732
|
+
if (!data || typeof data !== "object") return data;
|
|
733
|
+
const processed = { ...data };
|
|
734
|
+
for (const [key, value] of Object.entries(processed)) {
|
|
735
|
+
if (typeof value === "string" && (value.startsWith("http://") || value.startsWith("https://"))) {
|
|
736
|
+
if (!value.trim().startsWith("[") && !value.includes("](")) {
|
|
737
|
+
processed[key] = `[${value}](${value})`;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return processed;
|
|
742
|
+
}
|
|
723
743
|
async renderTemplate(templateId, data) {
|
|
724
744
|
const template = await this.getTemplate(templateId);
|
|
725
745
|
if (!template) throw new Error(`Template not found: ${templateId}`);
|
|
726
|
-
const
|
|
727
|
-
|
|
746
|
+
const processedData = this._preprocessData(data);
|
|
747
|
+
const subject = Mustache.render(template.subject_template, processedData);
|
|
748
|
+
let markdown = Mustache.render(template.body_markdown, processedData);
|
|
728
749
|
markdown = markdown.replace(/\\n/g, "\n");
|
|
729
750
|
marked.use({
|
|
730
751
|
mangle: false,
|
|
@@ -786,13 +807,30 @@ var EmailService = class {
|
|
|
786
807
|
* @param {Object} [params.metadata] - Additional metadata
|
|
787
808
|
* @returns {Promise<Object>} Delivery result
|
|
788
809
|
*/
|
|
789
|
-
async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {} }) {
|
|
810
|
+
async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {}, batchId = null, userId = null }) {
|
|
790
811
|
const htmlContent = html || htmlBody;
|
|
791
812
|
const textContent = text || textBody;
|
|
813
|
+
const templateId = metadata?.templateId || "direct";
|
|
814
|
+
if (this.config.emailLogger) {
|
|
815
|
+
try {
|
|
816
|
+
await this.config.emailLogger({
|
|
817
|
+
event: "pending",
|
|
818
|
+
recipientEmail: to,
|
|
819
|
+
recipientUserId: userId,
|
|
820
|
+
templateId,
|
|
821
|
+
subject,
|
|
822
|
+
batchId,
|
|
823
|
+
metadata
|
|
824
|
+
});
|
|
825
|
+
} catch (e) {
|
|
826
|
+
console.warn("[EmailService] emailLogger pending failed:", e);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
792
829
|
try {
|
|
793
830
|
const settings = await this.loadSettings(profile, tenantId);
|
|
794
831
|
const useProvider = provider || settings.provider || "mailchannels";
|
|
795
832
|
let result;
|
|
833
|
+
let providerMessageId = null;
|
|
796
834
|
switch (useProvider) {
|
|
797
835
|
case "mailchannels":
|
|
798
836
|
result = await this.sendViaMailChannels(to, subject, htmlContent, textContent, settings, metadata);
|
|
@@ -802,22 +840,91 @@ var EmailService = class {
|
|
|
802
840
|
break;
|
|
803
841
|
case "resend":
|
|
804
842
|
result = await this.sendViaResend(to, subject, htmlContent, textContent, settings, metadata);
|
|
843
|
+
if (result && typeof result === "object" && result.id) {
|
|
844
|
+
providerMessageId = result.id;
|
|
845
|
+
result = true;
|
|
846
|
+
}
|
|
805
847
|
break;
|
|
806
848
|
case "sendpulse":
|
|
807
849
|
result = await this.sendViaSendPulse(to, subject, htmlContent, textContent, settings, metadata);
|
|
808
850
|
break;
|
|
809
851
|
default:
|
|
810
852
|
console.error(`[EmailService] Unknown provider: ${useProvider}`);
|
|
853
|
+
if (this.config.emailLogger) {
|
|
854
|
+
try {
|
|
855
|
+
await this.config.emailLogger({
|
|
856
|
+
event: "failed",
|
|
857
|
+
recipientEmail: to,
|
|
858
|
+
recipientUserId: userId,
|
|
859
|
+
templateId,
|
|
860
|
+
subject,
|
|
861
|
+
provider: useProvider,
|
|
862
|
+
batchId,
|
|
863
|
+
error: `Unknown email provider: ${useProvider}`,
|
|
864
|
+
metadata
|
|
865
|
+
});
|
|
866
|
+
} catch (e) {
|
|
867
|
+
}
|
|
868
|
+
}
|
|
811
869
|
return { success: false, error: `Unknown email provider: ${useProvider}` };
|
|
812
870
|
}
|
|
813
871
|
if (result) {
|
|
814
|
-
|
|
872
|
+
const messageId = providerMessageId || crypto.randomUUID();
|
|
873
|
+
if (this.config.emailLogger) {
|
|
874
|
+
try {
|
|
875
|
+
await this.config.emailLogger({
|
|
876
|
+
event: "sent",
|
|
877
|
+
recipientEmail: to,
|
|
878
|
+
recipientUserId: userId,
|
|
879
|
+
templateId,
|
|
880
|
+
subject,
|
|
881
|
+
provider: useProvider,
|
|
882
|
+
messageId,
|
|
883
|
+
batchId,
|
|
884
|
+
metadata
|
|
885
|
+
});
|
|
886
|
+
} catch (e) {
|
|
887
|
+
console.warn("[EmailService] emailLogger sent failed:", e);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return { success: true, messageId };
|
|
815
891
|
} else {
|
|
816
892
|
console.error("[EmailService] Failed to send email to:", to);
|
|
893
|
+
if (this.config.emailLogger) {
|
|
894
|
+
try {
|
|
895
|
+
await this.config.emailLogger({
|
|
896
|
+
event: "failed",
|
|
897
|
+
recipientEmail: to,
|
|
898
|
+
recipientUserId: userId,
|
|
899
|
+
templateId,
|
|
900
|
+
subject,
|
|
901
|
+
provider: useProvider,
|
|
902
|
+
batchId,
|
|
903
|
+
error: "Failed to send email",
|
|
904
|
+
metadata
|
|
905
|
+
});
|
|
906
|
+
} catch (e) {
|
|
907
|
+
}
|
|
908
|
+
}
|
|
817
909
|
return { success: false, error: "Failed to send email" };
|
|
818
910
|
}
|
|
819
911
|
} catch (error) {
|
|
820
912
|
console.error("[EmailService] Error sending email:", error);
|
|
913
|
+
if (this.config.emailLogger) {
|
|
914
|
+
try {
|
|
915
|
+
await this.config.emailLogger({
|
|
916
|
+
event: "failed",
|
|
917
|
+
recipientEmail: to,
|
|
918
|
+
recipientUserId: userId,
|
|
919
|
+
templateId,
|
|
920
|
+
subject,
|
|
921
|
+
batchId,
|
|
922
|
+
error: error.message,
|
|
923
|
+
metadata
|
|
924
|
+
});
|
|
925
|
+
} catch (e) {
|
|
926
|
+
}
|
|
927
|
+
}
|
|
821
928
|
return { success: false, error: error.message };
|
|
822
929
|
}
|
|
823
930
|
}
|
|
@@ -1009,6 +1116,226 @@ var EmailService = class {
|
|
|
1009
1116
|
}
|
|
1010
1117
|
};
|
|
1011
1118
|
|
|
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
|
+
|
|
1012
1339
|
// src/backend/routes/index.js
|
|
1013
1340
|
import { Hono as Hono3 } from "hono";
|
|
1014
1341
|
|
|
@@ -1677,7 +2004,7 @@ var TemplateManager = ({
|
|
|
1677
2004
|
},
|
|
1678
2005
|
/* @__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" })),
|
|
1679
2006
|
"Create Template"
|
|
1680
|
-
)), 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: "
|
|
2007
|
+
)), 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(
|
|
1681
2008
|
"button",
|
|
1682
2009
|
{
|
|
1683
2010
|
key: type,
|
|
@@ -1801,11 +2128,13 @@ var TemplateManager = ({
|
|
|
1801
2128
|
)))));
|
|
1802
2129
|
};
|
|
1803
2130
|
export {
|
|
2131
|
+
EmailLogger,
|
|
1804
2132
|
EmailService,
|
|
1805
2133
|
EmailingCacheDO,
|
|
1806
2134
|
TemplateEditor,
|
|
1807
2135
|
TemplateManager,
|
|
1808
2136
|
createDOCacheProvider,
|
|
2137
|
+
createEmailLoggerCallback,
|
|
1809
2138
|
createEmailRoutes,
|
|
1810
2139
|
createTemplateRoutes,
|
|
1811
2140
|
createTrackingRoutes,
|