@contentgrowth/content-emailing 0.6.2 → 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 +315 -2
- package/dist/backend/EmailService.cjs.map +1 -1
- package/dist/backend/EmailService.d.cts +5 -1
- package/dist/backend/EmailService.d.ts +5 -1
- package/dist/backend/EmailService.js +315 -2
- package/dist/backend/EmailService.js.map +1 -1
- package/dist/backend/routes/index.cjs +315 -2
- package/dist/backend/routes/index.cjs.map +1 -1
- package/dist/backend/routes/index.js +315 -2
- 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 +324 -3
- 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 +322 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/schema.sql +32 -0
|
@@ -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) {
|
|
@@ -302,6 +519,16 @@ var EmailService = class {
|
|
|
302
519
|
},
|
|
303
520
|
...config
|
|
304
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
|
+
}
|
|
305
532
|
if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
|
|
306
533
|
this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
|
|
307
534
|
} else {
|
|
@@ -551,13 +778,30 @@ var EmailService = class {
|
|
|
551
778
|
* @param {Object} [params.metadata] - Additional metadata
|
|
552
779
|
* @returns {Promise<Object>} Delivery result
|
|
553
780
|
*/
|
|
554
|
-
async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {} }) {
|
|
781
|
+
async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {}, batchId = null, userId = null }) {
|
|
555
782
|
const htmlContent = html || htmlBody;
|
|
556
783
|
const textContent = text || textBody;
|
|
784
|
+
const templateId = metadata?.templateId || "direct";
|
|
785
|
+
if (this.emailLogger) {
|
|
786
|
+
try {
|
|
787
|
+
await this.emailLogger({
|
|
788
|
+
event: "pending",
|
|
789
|
+
recipientEmail: to,
|
|
790
|
+
recipientUserId: userId,
|
|
791
|
+
templateId,
|
|
792
|
+
subject,
|
|
793
|
+
batchId,
|
|
794
|
+
metadata
|
|
795
|
+
});
|
|
796
|
+
} catch (e) {
|
|
797
|
+
console.warn("[EmailService] emailLogger pending failed:", e);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
557
800
|
try {
|
|
558
801
|
const settings = await this.loadSettings(profile, tenantId);
|
|
559
802
|
const useProvider = provider || settings.provider || "mailchannels";
|
|
560
803
|
let result;
|
|
804
|
+
let providerMessageId = null;
|
|
561
805
|
switch (useProvider) {
|
|
562
806
|
case "mailchannels":
|
|
563
807
|
result = await this.sendViaMailChannels(to, subject, htmlContent, textContent, settings, metadata);
|
|
@@ -567,22 +811,91 @@ var EmailService = class {
|
|
|
567
811
|
break;
|
|
568
812
|
case "resend":
|
|
569
813
|
result = await this.sendViaResend(to, subject, htmlContent, textContent, settings, metadata);
|
|
814
|
+
if (result && typeof result === "object" && result.id) {
|
|
815
|
+
providerMessageId = result.id;
|
|
816
|
+
result = true;
|
|
817
|
+
}
|
|
570
818
|
break;
|
|
571
819
|
case "sendpulse":
|
|
572
820
|
result = await this.sendViaSendPulse(to, subject, htmlContent, textContent, settings, metadata);
|
|
573
821
|
break;
|
|
574
822
|
default:
|
|
575
823
|
console.error(`[EmailService] Unknown provider: ${useProvider}`);
|
|
824
|
+
if (this.emailLogger) {
|
|
825
|
+
try {
|
|
826
|
+
await this.emailLogger({
|
|
827
|
+
event: "failed",
|
|
828
|
+
recipientEmail: to,
|
|
829
|
+
recipientUserId: userId,
|
|
830
|
+
templateId,
|
|
831
|
+
subject,
|
|
832
|
+
provider: useProvider,
|
|
833
|
+
batchId,
|
|
834
|
+
error: `Unknown email provider: ${useProvider}`,
|
|
835
|
+
metadata
|
|
836
|
+
});
|
|
837
|
+
} catch (e) {
|
|
838
|
+
}
|
|
839
|
+
}
|
|
576
840
|
return { success: false, error: `Unknown email provider: ${useProvider}` };
|
|
577
841
|
}
|
|
578
842
|
if (result) {
|
|
579
|
-
|
|
843
|
+
const messageId = providerMessageId || crypto.randomUUID();
|
|
844
|
+
if (this.emailLogger) {
|
|
845
|
+
try {
|
|
846
|
+
await this.emailLogger({
|
|
847
|
+
event: "sent",
|
|
848
|
+
recipientEmail: to,
|
|
849
|
+
recipientUserId: userId,
|
|
850
|
+
templateId,
|
|
851
|
+
subject,
|
|
852
|
+
provider: useProvider,
|
|
853
|
+
messageId,
|
|
854
|
+
batchId,
|
|
855
|
+
metadata
|
|
856
|
+
});
|
|
857
|
+
} catch (e) {
|
|
858
|
+
console.warn("[EmailService] emailLogger sent failed:", e);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return { success: true, messageId };
|
|
580
862
|
} else {
|
|
581
863
|
console.error("[EmailService] Failed to send email to:", to);
|
|
864
|
+
if (this.emailLogger) {
|
|
865
|
+
try {
|
|
866
|
+
await this.emailLogger({
|
|
867
|
+
event: "failed",
|
|
868
|
+
recipientEmail: to,
|
|
869
|
+
recipientUserId: userId,
|
|
870
|
+
templateId,
|
|
871
|
+
subject,
|
|
872
|
+
provider: useProvider,
|
|
873
|
+
batchId,
|
|
874
|
+
error: "Failed to send email",
|
|
875
|
+
metadata
|
|
876
|
+
});
|
|
877
|
+
} catch (e) {
|
|
878
|
+
}
|
|
879
|
+
}
|
|
582
880
|
return { success: false, error: "Failed to send email" };
|
|
583
881
|
}
|
|
584
882
|
} catch (error) {
|
|
585
883
|
console.error("[EmailService] Error sending email:", error);
|
|
884
|
+
if (this.emailLogger) {
|
|
885
|
+
try {
|
|
886
|
+
await this.emailLogger({
|
|
887
|
+
event: "failed",
|
|
888
|
+
recipientEmail: to,
|
|
889
|
+
recipientUserId: userId,
|
|
890
|
+
templateId,
|
|
891
|
+
subject,
|
|
892
|
+
batchId,
|
|
893
|
+
error: error.message,
|
|
894
|
+
metadata
|
|
895
|
+
});
|
|
896
|
+
} catch (e) {
|
|
897
|
+
}
|
|
898
|
+
}
|
|
586
899
|
return { success: false, error: error.message };
|
|
587
900
|
}
|
|
588
901
|
}
|