@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.
- 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 +1 -1
|
@@ -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.
|
|
752
|
+
if (this.emailLogger) {
|
|
530
753
|
try {
|
|
531
|
-
await this.
|
|
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.
|
|
791
|
+
if (this.emailLogger) {
|
|
569
792
|
try {
|
|
570
|
-
await this.
|
|
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.
|
|
811
|
+
if (this.emailLogger) {
|
|
589
812
|
try {
|
|
590
|
-
await this.
|
|
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.
|
|
831
|
+
if (this.emailLogger) {
|
|
609
832
|
try {
|
|
610
|
-
await this.
|
|
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.
|
|
851
|
+
if (this.emailLogger) {
|
|
629
852
|
try {
|
|
630
|
-
await this.
|
|
853
|
+
await this.emailLogger({
|
|
631
854
|
event: "failed",
|
|
632
855
|
recipientEmail: to,
|
|
633
856
|
recipientUserId: userId,
|