@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
|
@@ -276,6 +276,222 @@ function createDOCacheProvider(doStub, instanceName = "global") {
|
|
|
276
276
|
};
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
// src/backend/EmailLogger.js
|
|
280
|
+
var EmailLogger = class {
|
|
281
|
+
/**
|
|
282
|
+
* @param {Object} db - D1 database binding
|
|
283
|
+
* @param {Object} options - Configuration options
|
|
284
|
+
* @param {string} [options.tableName='system_email_logs'] - Table name for logs
|
|
285
|
+
*/
|
|
286
|
+
constructor(db, options = {}) {
|
|
287
|
+
this.db = db;
|
|
288
|
+
this.tableName = options.tableName || "system_email_logs";
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Creates a logger callback function for use with EmailService.
|
|
292
|
+
* Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
|
|
293
|
+
*/
|
|
294
|
+
createCallback() {
|
|
295
|
+
return async (entry) => {
|
|
296
|
+
await this.log(entry);
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Log an email event (pending, sent, or failed)
|
|
301
|
+
* @param {Object} entry - Log entry
|
|
302
|
+
*/
|
|
303
|
+
async log(entry) {
|
|
304
|
+
const {
|
|
305
|
+
event,
|
|
306
|
+
recipientEmail,
|
|
307
|
+
recipientUserId,
|
|
308
|
+
templateId,
|
|
309
|
+
subject,
|
|
310
|
+
provider,
|
|
311
|
+
messageId,
|
|
312
|
+
batchId,
|
|
313
|
+
error,
|
|
314
|
+
errorCode,
|
|
315
|
+
metadata
|
|
316
|
+
} = entry;
|
|
317
|
+
try {
|
|
318
|
+
if (event === "pending") {
|
|
319
|
+
const id = crypto.randomUUID().replace(/-/g, "");
|
|
320
|
+
await this.db.prepare(`
|
|
321
|
+
INSERT INTO ${this.tableName}
|
|
322
|
+
(id, batch_id, recipient_email, recipient_user_id, template_id, subject, status, metadata, created_at)
|
|
323
|
+
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, strftime('%s', 'now'))
|
|
324
|
+
`).bind(
|
|
325
|
+
id,
|
|
326
|
+
batchId || null,
|
|
327
|
+
recipientEmail,
|
|
328
|
+
recipientUserId || null,
|
|
329
|
+
templateId || "direct",
|
|
330
|
+
subject || null,
|
|
331
|
+
metadata ? JSON.stringify(metadata) : null
|
|
332
|
+
).run();
|
|
333
|
+
} else if (event === "sent") {
|
|
334
|
+
await this.db.prepare(`
|
|
335
|
+
UPDATE ${this.tableName}
|
|
336
|
+
SET status = 'sent',
|
|
337
|
+
provider = ?,
|
|
338
|
+
provider_message_id = ?,
|
|
339
|
+
sent_at = strftime('%s', 'now')
|
|
340
|
+
WHERE recipient_email = ?
|
|
341
|
+
AND template_id = ?
|
|
342
|
+
AND status = 'pending'
|
|
343
|
+
ORDER BY created_at DESC
|
|
344
|
+
LIMIT 1
|
|
345
|
+
`).bind(
|
|
346
|
+
provider || null,
|
|
347
|
+
messageId || null,
|
|
348
|
+
recipientEmail,
|
|
349
|
+
templateId || "direct"
|
|
350
|
+
).run();
|
|
351
|
+
} else if (event === "failed") {
|
|
352
|
+
await this.db.prepare(`
|
|
353
|
+
UPDATE ${this.tableName}
|
|
354
|
+
SET status = 'failed',
|
|
355
|
+
provider = ?,
|
|
356
|
+
error_message = ?,
|
|
357
|
+
error_code = ?
|
|
358
|
+
WHERE recipient_email = ?
|
|
359
|
+
AND template_id = ?
|
|
360
|
+
AND status = 'pending'
|
|
361
|
+
ORDER BY created_at DESC
|
|
362
|
+
LIMIT 1
|
|
363
|
+
`).bind(
|
|
364
|
+
provider || null,
|
|
365
|
+
error || null,
|
|
366
|
+
errorCode || null,
|
|
367
|
+
recipientEmail,
|
|
368
|
+
templateId || "direct"
|
|
369
|
+
).run();
|
|
370
|
+
}
|
|
371
|
+
} catch (e) {
|
|
372
|
+
console.error("[EmailLogger] Failed to log:", e);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Query email logs with filtering
|
|
377
|
+
* @param {Object} options - Query options
|
|
378
|
+
*/
|
|
379
|
+
async query(options = {}) {
|
|
380
|
+
const {
|
|
381
|
+
recipientEmail,
|
|
382
|
+
recipientUserId,
|
|
383
|
+
templateId,
|
|
384
|
+
status,
|
|
385
|
+
batchId,
|
|
386
|
+
limit = 50,
|
|
387
|
+
offset = 0
|
|
388
|
+
} = options;
|
|
389
|
+
const conditions = [];
|
|
390
|
+
const bindings = [];
|
|
391
|
+
if (recipientEmail) {
|
|
392
|
+
conditions.push("recipient_email = ?");
|
|
393
|
+
bindings.push(recipientEmail);
|
|
394
|
+
}
|
|
395
|
+
if (recipientUserId) {
|
|
396
|
+
conditions.push("recipient_user_id = ?");
|
|
397
|
+
bindings.push(recipientUserId);
|
|
398
|
+
}
|
|
399
|
+
if (templateId) {
|
|
400
|
+
conditions.push("template_id = ?");
|
|
401
|
+
bindings.push(templateId);
|
|
402
|
+
}
|
|
403
|
+
if (status) {
|
|
404
|
+
conditions.push("status = ?");
|
|
405
|
+
bindings.push(status);
|
|
406
|
+
}
|
|
407
|
+
if (batchId) {
|
|
408
|
+
conditions.push("batch_id = ?");
|
|
409
|
+
bindings.push(batchId);
|
|
410
|
+
}
|
|
411
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
412
|
+
const countResult = await this.db.prepare(
|
|
413
|
+
`SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
|
|
414
|
+
).bind(...bindings).first();
|
|
415
|
+
const { results } = await this.db.prepare(`
|
|
416
|
+
SELECT id, batch_id, recipient_email, recipient_user_id, template_id, subject,
|
|
417
|
+
status, provider, provider_message_id, error_message, error_code, metadata,
|
|
418
|
+
created_at, sent_at
|
|
419
|
+
FROM ${this.tableName}
|
|
420
|
+
${whereClause}
|
|
421
|
+
ORDER BY created_at DESC
|
|
422
|
+
LIMIT ? OFFSET ?
|
|
423
|
+
`).bind(...bindings, limit, offset).all();
|
|
424
|
+
const logs = (results || []).map((row) => ({
|
|
425
|
+
id: row.id,
|
|
426
|
+
batchId: row.batch_id,
|
|
427
|
+
recipientEmail: row.recipient_email,
|
|
428
|
+
recipientUserId: row.recipient_user_id,
|
|
429
|
+
templateId: row.template_id,
|
|
430
|
+
subject: row.subject,
|
|
431
|
+
status: row.status,
|
|
432
|
+
provider: row.provider,
|
|
433
|
+
providerMessageId: row.provider_message_id,
|
|
434
|
+
errorMessage: row.error_message,
|
|
435
|
+
errorCode: row.error_code,
|
|
436
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
437
|
+
createdAt: row.created_at,
|
|
438
|
+
sentAt: row.sent_at
|
|
439
|
+
}));
|
|
440
|
+
return { logs, total: countResult?.count || 0 };
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Get email sending statistics
|
|
444
|
+
* @param {number} sinceDays - Number of days to look back
|
|
445
|
+
*/
|
|
446
|
+
async getStats(sinceDays = 7) {
|
|
447
|
+
const sinceTimestamp = Math.floor(Date.now() / 1e3) - sinceDays * 24 * 60 * 60;
|
|
448
|
+
const statusResult = await this.db.prepare(`
|
|
449
|
+
SELECT status, COUNT(*) as count
|
|
450
|
+
FROM ${this.tableName}
|
|
451
|
+
WHERE created_at >= ?
|
|
452
|
+
GROUP BY status
|
|
453
|
+
`).bind(sinceTimestamp).all();
|
|
454
|
+
const templateResult = await this.db.prepare(`
|
|
455
|
+
SELECT template_id, COUNT(*) as count
|
|
456
|
+
FROM ${this.tableName}
|
|
457
|
+
WHERE created_at >= ?
|
|
458
|
+
GROUP BY template_id
|
|
459
|
+
`).bind(sinceTimestamp).all();
|
|
460
|
+
const stats = {
|
|
461
|
+
total: 0,
|
|
462
|
+
sent: 0,
|
|
463
|
+
failed: 0,
|
|
464
|
+
pending: 0,
|
|
465
|
+
byTemplate: {}
|
|
466
|
+
};
|
|
467
|
+
(statusResult.results || []).forEach((row) => {
|
|
468
|
+
const count = row.count || 0;
|
|
469
|
+
stats.total += count;
|
|
470
|
+
if (row.status === "sent") stats.sent = count;
|
|
471
|
+
if (row.status === "failed") stats.failed = count;
|
|
472
|
+
if (row.status === "pending") stats.pending = count;
|
|
473
|
+
});
|
|
474
|
+
(templateResult.results || []).forEach((row) => {
|
|
475
|
+
stats.byTemplate[row.template_id] = row.count;
|
|
476
|
+
});
|
|
477
|
+
return stats;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Get recent failed emails for debugging
|
|
481
|
+
* @param {number} limit - Number of failed emails to retrieve
|
|
482
|
+
*/
|
|
483
|
+
async getRecentFailures(limit = 20) {
|
|
484
|
+
const { results } = await this.db.prepare(`
|
|
485
|
+
SELECT id, recipient_email, template_id, subject, error_message, error_code, created_at
|
|
486
|
+
FROM ${this.tableName}
|
|
487
|
+
WHERE status = 'failed'
|
|
488
|
+
ORDER BY created_at DESC
|
|
489
|
+
LIMIT ?
|
|
490
|
+
`).bind(limit).all();
|
|
491
|
+
return results || [];
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
279
495
|
// src/backend/EmailService.js
|
|
280
496
|
var EmailService = class {
|
|
281
497
|
/**
|
|
@@ -283,6 +499,7 @@ var EmailService = class {
|
|
|
283
499
|
* @param {Object} config - Configuration options
|
|
284
500
|
* @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
|
|
285
501
|
* @param {Object} [config.defaults] - Default settings (fromName, fromAddress)
|
|
502
|
+
* @param {boolean|Function} [config.emailLogger] - Email logger: true (default), false (disabled), or custom callback
|
|
286
503
|
* @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
|
|
287
504
|
*/
|
|
288
505
|
constructor(env, config = {}, cacheProvider = null) {
|
|
@@ -301,10 +518,6 @@ var EmailService = class {
|
|
|
301
518
|
// Updater function to save settings to backend
|
|
302
519
|
// Signature: async (profile, tenantId, settings) => void
|
|
303
520
|
settingsUpdater: config.settingsUpdater || null,
|
|
304
|
-
// Email Logger callback for tracking all email sends
|
|
305
|
-
// Signature: async (logEntry) => void
|
|
306
|
-
// logEntry: { event: 'pending'|'sent'|'failed', recipientEmail, templateId?, subject?, provider?, messageId?, error?, metadata? }
|
|
307
|
-
emailLogger: config.emailLogger || null,
|
|
308
521
|
// Branding configuration for email templates
|
|
309
522
|
branding: {
|
|
310
523
|
brandName: config.branding?.brandName || "Your App",
|
|
@@ -314,6 +527,16 @@ var EmailService = class {
|
|
|
314
527
|
},
|
|
315
528
|
...config
|
|
316
529
|
};
|
|
530
|
+
if (config.emailLogger === false) {
|
|
531
|
+
this.emailLogger = null;
|
|
532
|
+
} else if (typeof config.emailLogger === "function") {
|
|
533
|
+
this.emailLogger = config.emailLogger;
|
|
534
|
+
} else if (env.DB) {
|
|
535
|
+
const logger = new EmailLogger(env.DB);
|
|
536
|
+
this.emailLogger = logger.createCallback();
|
|
537
|
+
} else {
|
|
538
|
+
this.emailLogger = null;
|
|
539
|
+
}
|
|
317
540
|
if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
|
|
318
541
|
this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
|
|
319
542
|
} else {
|
|
@@ -567,9 +790,9 @@ var EmailService = class {
|
|
|
567
790
|
const htmlContent = html || htmlBody;
|
|
568
791
|
const textContent = text || textBody;
|
|
569
792
|
const templateId = metadata?.templateId || "direct";
|
|
570
|
-
if (this.
|
|
793
|
+
if (this.emailLogger) {
|
|
571
794
|
try {
|
|
572
|
-
await this.
|
|
795
|
+
await this.emailLogger({
|
|
573
796
|
event: "pending",
|
|
574
797
|
recipientEmail: to,
|
|
575
798
|
recipientUserId: userId,
|
|
@@ -606,9 +829,9 @@ var EmailService = class {
|
|
|
606
829
|
break;
|
|
607
830
|
default:
|
|
608
831
|
console.error(`[EmailService] Unknown provider: ${useProvider}`);
|
|
609
|
-
if (this.
|
|
832
|
+
if (this.emailLogger) {
|
|
610
833
|
try {
|
|
611
|
-
await this.
|
|
834
|
+
await this.emailLogger({
|
|
612
835
|
event: "failed",
|
|
613
836
|
recipientEmail: to,
|
|
614
837
|
recipientUserId: userId,
|
|
@@ -626,9 +849,9 @@ var EmailService = class {
|
|
|
626
849
|
}
|
|
627
850
|
if (result) {
|
|
628
851
|
const messageId = providerMessageId || crypto.randomUUID();
|
|
629
|
-
if (this.
|
|
852
|
+
if (this.emailLogger) {
|
|
630
853
|
try {
|
|
631
|
-
await this.
|
|
854
|
+
await this.emailLogger({
|
|
632
855
|
event: "sent",
|
|
633
856
|
recipientEmail: to,
|
|
634
857
|
recipientUserId: userId,
|
|
@@ -646,9 +869,9 @@ var EmailService = class {
|
|
|
646
869
|
return { success: true, messageId };
|
|
647
870
|
} else {
|
|
648
871
|
console.error("[EmailService] Failed to send email to:", to);
|
|
649
|
-
if (this.
|
|
872
|
+
if (this.emailLogger) {
|
|
650
873
|
try {
|
|
651
|
-
await this.
|
|
874
|
+
await this.emailLogger({
|
|
652
875
|
event: "failed",
|
|
653
876
|
recipientEmail: to,
|
|
654
877
|
recipientUserId: userId,
|
|
@@ -666,9 +889,9 @@ var EmailService = class {
|
|
|
666
889
|
}
|
|
667
890
|
} catch (error) {
|
|
668
891
|
console.error("[EmailService] Error sending email:", error);
|
|
669
|
-
if (this.
|
|
892
|
+
if (this.emailLogger) {
|
|
670
893
|
try {
|
|
671
|
-
await this.
|
|
894
|
+
await this.emailLogger({
|
|
672
895
|
event: "failed",
|
|
673
896
|
recipientEmail: to,
|
|
674
897
|
recipientUserId: userId,
|