@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
|
@@ -268,6 +268,222 @@ function createDOCacheProvider(doStub, instanceName = "global") {
|
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
// src/backend/EmailLogger.js
|
|
272
|
+
var EmailLogger = class {
|
|
273
|
+
/**
|
|
274
|
+
* @param {Object} db - D1 database binding
|
|
275
|
+
* @param {Object} options - Configuration options
|
|
276
|
+
* @param {string} [options.tableName='system_email_logs'] - Table name for logs
|
|
277
|
+
*/
|
|
278
|
+
constructor(db, options = {}) {
|
|
279
|
+
this.db = db;
|
|
280
|
+
this.tableName = options.tableName || "system_email_logs";
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Creates a logger callback function for use with EmailService.
|
|
284
|
+
* Usage: new EmailService(env, { emailLogger: emailLogger.createCallback() })
|
|
285
|
+
*/
|
|
286
|
+
createCallback() {
|
|
287
|
+
return async (entry) => {
|
|
288
|
+
await this.log(entry);
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Log an email event (pending, sent, or failed)
|
|
293
|
+
* @param {Object} entry - Log entry
|
|
294
|
+
*/
|
|
295
|
+
async log(entry) {
|
|
296
|
+
const {
|
|
297
|
+
event,
|
|
298
|
+
recipientEmail,
|
|
299
|
+
recipientUserId,
|
|
300
|
+
templateId,
|
|
301
|
+
subject,
|
|
302
|
+
provider,
|
|
303
|
+
messageId,
|
|
304
|
+
batchId,
|
|
305
|
+
error,
|
|
306
|
+
errorCode,
|
|
307
|
+
metadata
|
|
308
|
+
} = entry;
|
|
309
|
+
try {
|
|
310
|
+
if (event === "pending") {
|
|
311
|
+
const id = crypto.randomUUID().replace(/-/g, "");
|
|
312
|
+
await this.db.prepare(`
|
|
313
|
+
INSERT INTO ${this.tableName}
|
|
314
|
+
(id, batch_id, recipient_email, recipient_user_id, template_id, subject, status, metadata, created_at)
|
|
315
|
+
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, strftime('%s', 'now'))
|
|
316
|
+
`).bind(
|
|
317
|
+
id,
|
|
318
|
+
batchId || null,
|
|
319
|
+
recipientEmail,
|
|
320
|
+
recipientUserId || null,
|
|
321
|
+
templateId || "direct",
|
|
322
|
+
subject || null,
|
|
323
|
+
metadata ? JSON.stringify(metadata) : null
|
|
324
|
+
).run();
|
|
325
|
+
} else if (event === "sent") {
|
|
326
|
+
await this.db.prepare(`
|
|
327
|
+
UPDATE ${this.tableName}
|
|
328
|
+
SET status = 'sent',
|
|
329
|
+
provider = ?,
|
|
330
|
+
provider_message_id = ?,
|
|
331
|
+
sent_at = strftime('%s', 'now')
|
|
332
|
+
WHERE recipient_email = ?
|
|
333
|
+
AND template_id = ?
|
|
334
|
+
AND status = 'pending'
|
|
335
|
+
ORDER BY created_at DESC
|
|
336
|
+
LIMIT 1
|
|
337
|
+
`).bind(
|
|
338
|
+
provider || null,
|
|
339
|
+
messageId || null,
|
|
340
|
+
recipientEmail,
|
|
341
|
+
templateId || "direct"
|
|
342
|
+
).run();
|
|
343
|
+
} else if (event === "failed") {
|
|
344
|
+
await this.db.prepare(`
|
|
345
|
+
UPDATE ${this.tableName}
|
|
346
|
+
SET status = 'failed',
|
|
347
|
+
provider = ?,
|
|
348
|
+
error_message = ?,
|
|
349
|
+
error_code = ?
|
|
350
|
+
WHERE recipient_email = ?
|
|
351
|
+
AND template_id = ?
|
|
352
|
+
AND status = 'pending'
|
|
353
|
+
ORDER BY created_at DESC
|
|
354
|
+
LIMIT 1
|
|
355
|
+
`).bind(
|
|
356
|
+
provider || null,
|
|
357
|
+
error || null,
|
|
358
|
+
errorCode || null,
|
|
359
|
+
recipientEmail,
|
|
360
|
+
templateId || "direct"
|
|
361
|
+
).run();
|
|
362
|
+
}
|
|
363
|
+
} catch (e) {
|
|
364
|
+
console.error("[EmailLogger] Failed to log:", e);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Query email logs with filtering
|
|
369
|
+
* @param {Object} options - Query options
|
|
370
|
+
*/
|
|
371
|
+
async query(options = {}) {
|
|
372
|
+
const {
|
|
373
|
+
recipientEmail,
|
|
374
|
+
recipientUserId,
|
|
375
|
+
templateId,
|
|
376
|
+
status,
|
|
377
|
+
batchId,
|
|
378
|
+
limit = 50,
|
|
379
|
+
offset = 0
|
|
380
|
+
} = options;
|
|
381
|
+
const conditions = [];
|
|
382
|
+
const bindings = [];
|
|
383
|
+
if (recipientEmail) {
|
|
384
|
+
conditions.push("recipient_email = ?");
|
|
385
|
+
bindings.push(recipientEmail);
|
|
386
|
+
}
|
|
387
|
+
if (recipientUserId) {
|
|
388
|
+
conditions.push("recipient_user_id = ?");
|
|
389
|
+
bindings.push(recipientUserId);
|
|
390
|
+
}
|
|
391
|
+
if (templateId) {
|
|
392
|
+
conditions.push("template_id = ?");
|
|
393
|
+
bindings.push(templateId);
|
|
394
|
+
}
|
|
395
|
+
if (status) {
|
|
396
|
+
conditions.push("status = ?");
|
|
397
|
+
bindings.push(status);
|
|
398
|
+
}
|
|
399
|
+
if (batchId) {
|
|
400
|
+
conditions.push("batch_id = ?");
|
|
401
|
+
bindings.push(batchId);
|
|
402
|
+
}
|
|
403
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
404
|
+
const countResult = await this.db.prepare(
|
|
405
|
+
`SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`
|
|
406
|
+
).bind(...bindings).first();
|
|
407
|
+
const { results } = await this.db.prepare(`
|
|
408
|
+
SELECT id, batch_id, recipient_email, recipient_user_id, template_id, subject,
|
|
409
|
+
status, provider, provider_message_id, error_message, error_code, metadata,
|
|
410
|
+
created_at, sent_at
|
|
411
|
+
FROM ${this.tableName}
|
|
412
|
+
${whereClause}
|
|
413
|
+
ORDER BY created_at DESC
|
|
414
|
+
LIMIT ? OFFSET ?
|
|
415
|
+
`).bind(...bindings, limit, offset).all();
|
|
416
|
+
const logs = (results || []).map((row) => ({
|
|
417
|
+
id: row.id,
|
|
418
|
+
batchId: row.batch_id,
|
|
419
|
+
recipientEmail: row.recipient_email,
|
|
420
|
+
recipientUserId: row.recipient_user_id,
|
|
421
|
+
templateId: row.template_id,
|
|
422
|
+
subject: row.subject,
|
|
423
|
+
status: row.status,
|
|
424
|
+
provider: row.provider,
|
|
425
|
+
providerMessageId: row.provider_message_id,
|
|
426
|
+
errorMessage: row.error_message,
|
|
427
|
+
errorCode: row.error_code,
|
|
428
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
429
|
+
createdAt: row.created_at,
|
|
430
|
+
sentAt: row.sent_at
|
|
431
|
+
}));
|
|
432
|
+
return { logs, total: countResult?.count || 0 };
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Get email sending statistics
|
|
436
|
+
* @param {number} sinceDays - Number of days to look back
|
|
437
|
+
*/
|
|
438
|
+
async getStats(sinceDays = 7) {
|
|
439
|
+
const sinceTimestamp = Math.floor(Date.now() / 1e3) - sinceDays * 24 * 60 * 60;
|
|
440
|
+
const statusResult = await this.db.prepare(`
|
|
441
|
+
SELECT status, COUNT(*) as count
|
|
442
|
+
FROM ${this.tableName}
|
|
443
|
+
WHERE created_at >= ?
|
|
444
|
+
GROUP BY status
|
|
445
|
+
`).bind(sinceTimestamp).all();
|
|
446
|
+
const templateResult = await this.db.prepare(`
|
|
447
|
+
SELECT template_id, COUNT(*) as count
|
|
448
|
+
FROM ${this.tableName}
|
|
449
|
+
WHERE created_at >= ?
|
|
450
|
+
GROUP BY template_id
|
|
451
|
+
`).bind(sinceTimestamp).all();
|
|
452
|
+
const stats = {
|
|
453
|
+
total: 0,
|
|
454
|
+
sent: 0,
|
|
455
|
+
failed: 0,
|
|
456
|
+
pending: 0,
|
|
457
|
+
byTemplate: {}
|
|
458
|
+
};
|
|
459
|
+
(statusResult.results || []).forEach((row) => {
|
|
460
|
+
const count = row.count || 0;
|
|
461
|
+
stats.total += count;
|
|
462
|
+
if (row.status === "sent") stats.sent = count;
|
|
463
|
+
if (row.status === "failed") stats.failed = count;
|
|
464
|
+
if (row.status === "pending") stats.pending = count;
|
|
465
|
+
});
|
|
466
|
+
(templateResult.results || []).forEach((row) => {
|
|
467
|
+
stats.byTemplate[row.template_id] = row.count;
|
|
468
|
+
});
|
|
469
|
+
return stats;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Get recent failed emails for debugging
|
|
473
|
+
* @param {number} limit - Number of failed emails to retrieve
|
|
474
|
+
*/
|
|
475
|
+
async getRecentFailures(limit = 20) {
|
|
476
|
+
const { results } = await this.db.prepare(`
|
|
477
|
+
SELECT id, recipient_email, template_id, subject, error_message, error_code, created_at
|
|
478
|
+
FROM ${this.tableName}
|
|
479
|
+
WHERE status = 'failed'
|
|
480
|
+
ORDER BY created_at DESC
|
|
481
|
+
LIMIT ?
|
|
482
|
+
`).bind(limit).all();
|
|
483
|
+
return results || [];
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
271
487
|
// src/backend/EmailService.js
|
|
272
488
|
var EmailService = class {
|
|
273
489
|
/**
|
|
@@ -275,6 +491,7 @@ var EmailService = class {
|
|
|
275
491
|
* @param {Object} config - Configuration options
|
|
276
492
|
* @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
|
|
277
493
|
* @param {Object} [config.defaults] - Default settings (fromName, fromAddress)
|
|
494
|
+
* @param {boolean|Function} [config.emailLogger] - Email logger: true (default), false (disabled), or custom callback
|
|
278
495
|
* @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
|
|
279
496
|
*/
|
|
280
497
|
constructor(env, config = {}, cacheProvider = null) {
|
|
@@ -293,10 +510,6 @@ var EmailService = class {
|
|
|
293
510
|
// Updater function to save settings to backend
|
|
294
511
|
// Signature: async (profile, tenantId, settings) => void
|
|
295
512
|
settingsUpdater: config.settingsUpdater || null,
|
|
296
|
-
// Email Logger callback for tracking all email sends
|
|
297
|
-
// Signature: async (logEntry) => void
|
|
298
|
-
// logEntry: { event: 'pending'|'sent'|'failed', recipientEmail, templateId?, subject?, provider?, messageId?, error?, metadata? }
|
|
299
|
-
emailLogger: config.emailLogger || null,
|
|
300
513
|
// Branding configuration for email templates
|
|
301
514
|
branding: {
|
|
302
515
|
brandName: config.branding?.brandName || "Your App",
|
|
@@ -306,6 +519,16 @@ var EmailService = class {
|
|
|
306
519
|
},
|
|
307
520
|
...config
|
|
308
521
|
};
|
|
522
|
+
if (config.emailLogger === false) {
|
|
523
|
+
this.emailLogger = null;
|
|
524
|
+
} else if (typeof config.emailLogger === "function") {
|
|
525
|
+
this.emailLogger = config.emailLogger;
|
|
526
|
+
} else if (env.DB) {
|
|
527
|
+
const logger = new EmailLogger(env.DB);
|
|
528
|
+
this.emailLogger = logger.createCallback();
|
|
529
|
+
} else {
|
|
530
|
+
this.emailLogger = null;
|
|
531
|
+
}
|
|
309
532
|
if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
|
|
310
533
|
this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
|
|
311
534
|
} else {
|
|
@@ -559,9 +782,9 @@ var EmailService = class {
|
|
|
559
782
|
const htmlContent = html || htmlBody;
|
|
560
783
|
const textContent = text || textBody;
|
|
561
784
|
const templateId = metadata?.templateId || "direct";
|
|
562
|
-
if (this.
|
|
785
|
+
if (this.emailLogger) {
|
|
563
786
|
try {
|
|
564
|
-
await this.
|
|
787
|
+
await this.emailLogger({
|
|
565
788
|
event: "pending",
|
|
566
789
|
recipientEmail: to,
|
|
567
790
|
recipientUserId: userId,
|
|
@@ -598,9 +821,9 @@ var EmailService = class {
|
|
|
598
821
|
break;
|
|
599
822
|
default:
|
|
600
823
|
console.error(`[EmailService] Unknown provider: ${useProvider}`);
|
|
601
|
-
if (this.
|
|
824
|
+
if (this.emailLogger) {
|
|
602
825
|
try {
|
|
603
|
-
await this.
|
|
826
|
+
await this.emailLogger({
|
|
604
827
|
event: "failed",
|
|
605
828
|
recipientEmail: to,
|
|
606
829
|
recipientUserId: userId,
|
|
@@ -618,9 +841,9 @@ var EmailService = class {
|
|
|
618
841
|
}
|
|
619
842
|
if (result) {
|
|
620
843
|
const messageId = providerMessageId || crypto.randomUUID();
|
|
621
|
-
if (this.
|
|
844
|
+
if (this.emailLogger) {
|
|
622
845
|
try {
|
|
623
|
-
await this.
|
|
846
|
+
await this.emailLogger({
|
|
624
847
|
event: "sent",
|
|
625
848
|
recipientEmail: to,
|
|
626
849
|
recipientUserId: userId,
|
|
@@ -638,9 +861,9 @@ var EmailService = class {
|
|
|
638
861
|
return { success: true, messageId };
|
|
639
862
|
} else {
|
|
640
863
|
console.error("[EmailService] Failed to send email to:", to);
|
|
641
|
-
if (this.
|
|
864
|
+
if (this.emailLogger) {
|
|
642
865
|
try {
|
|
643
|
-
await this.
|
|
866
|
+
await this.emailLogger({
|
|
644
867
|
event: "failed",
|
|
645
868
|
recipientEmail: to,
|
|
646
869
|
recipientUserId: userId,
|
|
@@ -658,9 +881,9 @@ var EmailService = class {
|
|
|
658
881
|
}
|
|
659
882
|
} catch (error) {
|
|
660
883
|
console.error("[EmailService] Error sending email:", error);
|
|
661
|
-
if (this.
|
|
884
|
+
if (this.emailLogger) {
|
|
662
885
|
try {
|
|
663
|
-
await this.
|
|
886
|
+
await this.emailLogger({
|
|
664
887
|
event: "failed",
|
|
665
888
|
recipientEmail: to,
|
|
666
889
|
recipientUserId: userId,
|