@contentgrowth/content-emailing 0.4.1 → 0.6.0

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.
Files changed (66) hide show
  1. package/dist/TemplateManager-Db41KyPN.d.cts +77 -0
  2. package/dist/TemplateManager-Db41KyPN.d.ts +77 -0
  3. package/dist/backend/EmailService.cjs +737 -0
  4. package/dist/backend/EmailService.cjs.map +1 -0
  5. package/dist/backend/EmailService.d.cts +101 -0
  6. package/dist/backend/EmailService.d.ts +101 -0
  7. package/dist/backend/EmailService.js +703 -0
  8. package/dist/backend/EmailService.js.map +1 -0
  9. package/dist/backend/EmailingCacheDO.cjs +389 -0
  10. package/dist/backend/EmailingCacheDO.cjs.map +1 -0
  11. package/dist/backend/EmailingCacheDO.d.cts +66 -0
  12. package/dist/backend/EmailingCacheDO.d.ts +66 -0
  13. package/dist/backend/EmailingCacheDO.js +364 -0
  14. package/dist/backend/EmailingCacheDO.js.map +1 -0
  15. package/dist/backend/routes/index.cjs +1001 -0
  16. package/dist/backend/routes/index.cjs.map +1 -0
  17. package/dist/backend/routes/index.d.cts +32 -0
  18. package/dist/backend/routes/index.d.ts +32 -0
  19. package/dist/backend/routes/index.js +965 -0
  20. package/dist/backend/routes/index.js.map +1 -0
  21. package/dist/cli.cjs +53 -0
  22. package/dist/cli.cjs.map +1 -0
  23. package/dist/cli.d.cts +1 -0
  24. package/dist/cli.d.ts +1 -0
  25. package/dist/cli.js +53 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/common/index.cjs +267 -0
  28. package/dist/common/index.cjs.map +1 -0
  29. package/dist/common/index.d.cts +46 -0
  30. package/dist/common/index.d.ts +46 -0
  31. package/{src/common/htmlWrapper.js → dist/common/index.js} +75 -18
  32. package/dist/common/index.js.map +1 -0
  33. package/dist/frontend/index.cjs +665 -0
  34. package/dist/frontend/index.cjs.map +1 -0
  35. package/dist/frontend/index.d.cts +32 -0
  36. package/dist/frontend/index.d.ts +32 -0
  37. package/dist/frontend/index.js +626 -0
  38. package/dist/frontend/index.js.map +1 -0
  39. package/dist/index.cjs +1842 -0
  40. package/dist/index.cjs.map +1 -0
  41. package/dist/index.d.cts +7 -0
  42. package/dist/index.d.ts +7 -0
  43. package/dist/index.js +1793 -0
  44. package/dist/index.js.map +1 -0
  45. package/package.json +31 -13
  46. package/examples/.env.example +0 -16
  47. package/examples/README.md +0 -55
  48. package/examples/mocks/MockD1.js +0 -311
  49. package/examples/mocks/MockEmailSender.js +0 -64
  50. package/examples/mocks/index.js +0 -5
  51. package/examples/package-lock.json +0 -73
  52. package/examples/package.json +0 -18
  53. package/examples/portal/index.html +0 -919
  54. package/examples/server.js +0 -314
  55. package/release.sh +0 -56
  56. package/src/backend/EmailService.js +0 -537
  57. package/src/backend/EmailingCacheDO.js +0 -466
  58. package/src/backend/routes/index.js +0 -30
  59. package/src/backend/routes/templates.js +0 -98
  60. package/src/backend/routes/tracking.js +0 -215
  61. package/src/backend/routes.js +0 -98
  62. package/src/common/index.js +0 -11
  63. package/src/common/utils.js +0 -141
  64. package/src/frontend/TemplateEditor.jsx +0 -117
  65. package/src/frontend/TemplateManager.jsx +0 -117
  66. package/src/index.js +0 -24
@@ -0,0 +1,1001 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/backend/routes/index.js
30
+ var routes_exports = {};
31
+ __export(routes_exports, {
32
+ createEmailRoutes: () => createEmailRoutes,
33
+ createTemplateRoutes: () => createTemplateRoutes,
34
+ createTrackingRoutes: () => createTrackingRoutes
35
+ });
36
+ module.exports = __toCommonJS(routes_exports);
37
+ var import_hono3 = require("hono");
38
+
39
+ // src/backend/routes/templates.js
40
+ var import_hono = require("hono");
41
+
42
+ // src/backend/EmailService.js
43
+ var import_marked = require("marked");
44
+ var import_mustache = __toESM(require("mustache"), 1);
45
+
46
+ // src/common/htmlWrapper.js
47
+ function wrapInEmailTemplate(contentHtml, subject, data = {}) {
48
+ const portalUrl = data.portalUrl || "https://app.x0start.com";
49
+ const unsubscribeUrl = data.unsubscribeUrl || "{{unsubscribe_url}}";
50
+ const brandName = data.brandName || "X0 Start";
51
+ return `
52
+ <!DOCTYPE html>
53
+ <html lang="en">
54
+ <head>
55
+ <meta charset="UTF-8">
56
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
57
+ <title>${subject}</title>
58
+ <style>
59
+ body {
60
+ margin: 0;
61
+ padding: 0;
62
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
63
+ background-color: #f5f5f5;
64
+ line-height: 1.6;
65
+ }
66
+ .email-wrapper {
67
+ background-color: #f5f5f5;
68
+ padding: 40px 20px;
69
+ }
70
+ .email-container {
71
+ max-width: 600px;
72
+ margin: 0 auto;
73
+ background-color: #ffffff;
74
+ border-radius: 8px;
75
+ overflow: hidden;
76
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
77
+ }
78
+ .email-header {
79
+ padding: 40px 40px 20px;
80
+ text-align: center;
81
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
82
+ color: #ffffff;
83
+ }
84
+ .email-header h1 {
85
+ margin: 0;
86
+ font-size: 28px;
87
+ font-weight: 600;
88
+ }
89
+ .email-content {
90
+ padding: 30px 40px;
91
+ color: #333333;
92
+ }
93
+ .email-content h1 {
94
+ font-size: 24px;
95
+ margin-top: 0;
96
+ margin-bottom: 20px;
97
+ color: #333333;
98
+ }
99
+ .email-content h2 {
100
+ font-size: 20px;
101
+ margin-top: 30px;
102
+ margin-bottom: 15px;
103
+ color: #333333;
104
+ }
105
+ .email-content h3 {
106
+ font-size: 16px;
107
+ margin-top: 20px;
108
+ margin-bottom: 10px;
109
+ color: #333333;
110
+ }
111
+ .email-content p {
112
+ margin: 0 0 15px;
113
+ color: #666666;
114
+ }
115
+ .email-content a {
116
+ color: #667eea;
117
+ text-decoration: none;
118
+ }
119
+ .email-content ul, .email-content ol {
120
+ margin: 0 0 15px;
121
+ padding-left: 25px;
122
+ }
123
+ .email-content li {
124
+ margin-bottom: 8px;
125
+ color: #666666;
126
+ }
127
+ .email-content blockquote {
128
+ margin: 20px 0;
129
+ padding: 15px 20px;
130
+ background-color: #f8f9fa;
131
+ border-left: 4px solid #667eea;
132
+ color: #666666;
133
+ }
134
+ .email-content code {
135
+ padding: 2px 6px;
136
+ background-color: #f8f9fa;
137
+ border-radius: 3px;
138
+ font-family: 'Courier New', monospace;
139
+ font-size: 14px;
140
+ }
141
+ .email-content pre {
142
+ padding: 15px;
143
+ background-color: #f8f9fa;
144
+ border-radius: 6px;
145
+ overflow-x: auto;
146
+ }
147
+ .email-content pre code {
148
+ padding: 0;
149
+ background: none;
150
+ }
151
+ .btn {
152
+ display: inline-block;
153
+ padding: 12px 24px;
154
+ background-color: #667eea;
155
+ color: #ffffff !important;
156
+ text-decoration: none;
157
+ border-radius: 6px;
158
+ font-weight: 600;
159
+ margin: 10px 0;
160
+ }
161
+ .btn:hover {
162
+ background-color: #5568d3;
163
+ }
164
+ .email-footer {
165
+ padding: 20px 40px;
166
+ background-color: #f8f9fa;
167
+ text-align: center;
168
+ font-size: 12px;
169
+ color: #666666;
170
+ }
171
+ .email-footer a {
172
+ color: #667eea;
173
+ text-decoration: none;
174
+ }
175
+ hr {
176
+ border: none;
177
+ border-top: 1px solid #e0e0e0;
178
+ margin: 30px 0;
179
+ }
180
+ </style>
181
+ </head>
182
+ <body>
183
+ <div class="email-wrapper">
184
+ <div class="email-container">
185
+ <div class="email-content">
186
+ ${contentHtml}
187
+ </div>
188
+ <div class="email-footer">
189
+ <p style="margin: 0 0 10px;">
190
+ You're receiving this email from ${brandName}.
191
+ </p>
192
+ <p style="margin: 0;">
193
+ <a href="${unsubscribeUrl}">Unsubscribe</a> |
194
+ <a href="${portalUrl}/settings/notifications">Manage Preferences</a>
195
+ </p>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </body>
200
+ </html>
201
+ `.trim();
202
+ }
203
+
204
+ // src/backend/EmailingCacheDO.js
205
+ function createDOCacheProvider(doStub, instanceName = "global") {
206
+ if (!doStub) {
207
+ return null;
208
+ }
209
+ const stub = doStub.get(doStub.idFromName(instanceName));
210
+ return {
211
+ async getTemplate(templateId) {
212
+ try {
213
+ const response = await stub.fetch(`http://do/get?templateId=${templateId}`);
214
+ const data = await response.json();
215
+ return data.template || null;
216
+ } catch (e) {
217
+ return null;
218
+ }
219
+ },
220
+ async getSettings(profile, tenantId) {
221
+ const key = `${profile}:${tenantId || ""}`;
222
+ try {
223
+ const response = await stub.fetch(`http://do/settings/get?key=${encodeURIComponent(key)}`);
224
+ const data = await response.json();
225
+ return data.settings || null;
226
+ } catch (e) {
227
+ return null;
228
+ }
229
+ },
230
+ async putSettings(profile, tenantId, settings) {
231
+ const key = `${profile}:${tenantId || ""}`;
232
+ try {
233
+ await stub.fetch("http://do/settings/put", {
234
+ method: "POST",
235
+ headers: { "Content-Type": "application/json" },
236
+ body: JSON.stringify({ key, settings })
237
+ });
238
+ } catch (e) {
239
+ console.warn("[DOCacheProvider] Failed to cache settings:", e);
240
+ }
241
+ },
242
+ async putTemplate(template) {
243
+ try {
244
+ await stub.fetch("http://do/invalidate", {
245
+ method: "POST",
246
+ headers: { "Content-Type": "application/json" },
247
+ body: JSON.stringify({ templateId: template.template_id })
248
+ });
249
+ } catch (e) {
250
+ console.warn("[DOCacheProvider] Failed to invalidate template:", e);
251
+ }
252
+ },
253
+ async deleteTemplate(templateId) {
254
+ try {
255
+ await stub.fetch("http://do/invalidate", {
256
+ method: "POST",
257
+ headers: { "Content-Type": "application/json" },
258
+ body: JSON.stringify({ templateId })
259
+ });
260
+ } catch (e) {
261
+ console.warn("[DOCacheProvider] Failed to invalidate template:", e);
262
+ }
263
+ },
264
+ async invalidateSettings(profile, tenantId) {
265
+ const key = `${profile}:${tenantId || ""}`;
266
+ try {
267
+ await stub.fetch("http://do/settings/invalidate", {
268
+ method: "POST",
269
+ headers: { "Content-Type": "application/json" },
270
+ body: JSON.stringify({ key })
271
+ });
272
+ } catch (e) {
273
+ console.warn("[DOCacheProvider] Failed to invalidate settings:", e);
274
+ }
275
+ }
276
+ };
277
+ }
278
+
279
+ // src/backend/EmailService.js
280
+ var EmailService = class {
281
+ /**
282
+ * @param {Object} env - Cloudflare environment bindings (DB, etc.)
283
+ * @param {Object} config - Configuration options
284
+ * @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
285
+ * @param {Object} [config.defaults] - Default settings (fromName, fromAddress)
286
+ * @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
287
+ */
288
+ constructor(env, config = {}, cacheProvider = null) {
289
+ this.env = env;
290
+ this.db = env.DB;
291
+ this.config = {
292
+ emailTablePrefix: config.emailTablePrefix || config.tableNamePrefix || "system_email_",
293
+ defaults: config.defaults || {
294
+ fromName: "System",
295
+ fromAddress: "noreply@example.com",
296
+ provider: "mailchannels"
297
+ },
298
+ // Loader function to fetch settings from backend (DB, KV, etc.)
299
+ // Signature: async (profile, tenantId) => SettingsObject
300
+ settingsLoader: config.settingsLoader || null,
301
+ // Updater function to save settings to backend
302
+ // Signature: async (profile, tenantId, settings) => void
303
+ settingsUpdater: config.settingsUpdater || null,
304
+ // Branding configuration for email templates
305
+ branding: {
306
+ brandName: config.branding?.brandName || "Your App",
307
+ portalUrl: config.branding?.portalUrl || "https://app.example.com",
308
+ primaryColor: config.branding?.primaryColor || "#667eea",
309
+ ...config.branding
310
+ },
311
+ ...config
312
+ };
313
+ if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
314
+ this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
315
+ } else {
316
+ this.cache = cacheProvider;
317
+ }
318
+ }
319
+ // --- Configuration & Settings ---
320
+ /**
321
+ * Load email configuration
322
+ * @param {string} profile - 'system' or 'tenant' (or custom profile string)
323
+ * @param {string} tenantId - Context ID (optional)
324
+ * @returns {Promise<Object>} Email configuration
325
+ */
326
+ async loadSettings(profile = "system", tenantId = null) {
327
+ if (this.cache && this.cache.getSettings) {
328
+ try {
329
+ const cached = await this.cache.getSettings(profile, tenantId);
330
+ if (cached) return this._normalizeConfig(cached);
331
+ } catch (e) {
332
+ }
333
+ }
334
+ let settings = null;
335
+ if (this.config.settingsLoader) {
336
+ try {
337
+ settings = await this.config.settingsLoader(profile, tenantId);
338
+ } catch (e) {
339
+ console.warn("[EmailService] settingsLoader failed:", e);
340
+ }
341
+ }
342
+ if (settings) {
343
+ if (this.cache && this.cache.putSettings) {
344
+ try {
345
+ this.cache.putSettings(profile, tenantId, settings);
346
+ } catch (e) {
347
+ }
348
+ }
349
+ return this._normalizeConfig(settings);
350
+ }
351
+ return {
352
+ ...this.config.defaults
353
+ };
354
+ }
355
+ /**
356
+ * Normalize config keys to standard format
357
+ * Handles both snake_case (DB) and camelCase inputs
358
+ */
359
+ _normalizeConfig(config) {
360
+ return {
361
+ provider: config.email_provider || config.provider || this.config.defaults.provider,
362
+ fromAddress: config.email_from_address || config.fromAddress || this.config.defaults.fromAddress,
363
+ fromName: config.email_from_name || config.fromName || this.config.defaults.fromName,
364
+ // Provider-specific settings (normalize DB keys to service keys)
365
+ sendgridApiKey: config.sendgrid_api_key || config.sendgridApiKey,
366
+ resendApiKey: config.resend_api_key || config.resendApiKey,
367
+ sendpulseClientId: config.sendpulse_client_id || config.sendpulseClientId,
368
+ sendpulseClientSecret: config.sendpulse_client_secret || config.sendpulseClientSecret,
369
+ // SMTP
370
+ smtpHost: config.smtp_host || config.smtpHost,
371
+ smtpPort: config.smtp_port || config.smtpPort,
372
+ smtpUsername: config.smtp_username || config.smtpUsername,
373
+ smtpPassword: config.smtp_password || config.smtpPassword,
374
+ // Tracking
375
+ trackingUrl: config.tracking_url || config.trackingUrl,
376
+ // Pass through others
377
+ // Pass through others
378
+ ...config,
379
+ smtpSecure: config.smtp_secure === "true"
380
+ };
381
+ }
382
+ // --- Template Management ---
383
+ async getTemplate(templateId) {
384
+ if (this.cache) {
385
+ try {
386
+ const cached = await this.cache.getTemplate(templateId);
387
+ if (cached) return cached;
388
+ } catch (e) {
389
+ console.warn("[EmailService] Template cache lookup failed:", e);
390
+ }
391
+ }
392
+ const table = `${this.config.emailTablePrefix}templates`;
393
+ return await this.db.prepare(`SELECT * FROM ${table} WHERE template_id = ?`).bind(templateId).first();
394
+ }
395
+ async getAllTemplates() {
396
+ const table = `${this.config.emailTablePrefix}templates`;
397
+ const result = await this.db.prepare(`SELECT * FROM ${table} ORDER BY template_name`).all();
398
+ return result.results || [];
399
+ }
400
+ /**
401
+ * Save email configuration
402
+ * @param {Object} settings - Config object
403
+ * @param {string} profile - 'system' or 'tenant'
404
+ * @param {string} tenantId - Context ID
405
+ */
406
+ async saveSettings(settings, profile = "system", tenantId = null) {
407
+ if (this.config.settingsUpdater) {
408
+ try {
409
+ await this.config.settingsUpdater(profile, tenantId, settings);
410
+ } catch (e) {
411
+ console.error("[EmailService] settingsUpdater failed:", e);
412
+ throw e;
413
+ }
414
+ } else if (profile === "system" && this.db) {
415
+ }
416
+ if (this.cache && this.cache.invalidateSettings) {
417
+ try {
418
+ await this.cache.invalidateSettings(profile, tenantId);
419
+ } catch (e) {
420
+ console.warn("[EmailService] Failed to invalidate settings cache:", e);
421
+ }
422
+ }
423
+ }
424
+ async saveTemplate(template, userId = "system") {
425
+ const table = `${this.config.emailTablePrefix}templates`;
426
+ const now = Math.floor(Date.now() / 1e3);
427
+ const existing = await this.getTemplate(template.template_id);
428
+ if (existing) {
429
+ await this.db.prepare(`
430
+ UPDATE ${table} SET
431
+ template_name = ?, template_type = ?, subject_template = ?,
432
+ body_markdown = ?, variables = ?, description = ?, is_active = ?,
433
+ updated_at = ?, updated_by = ?
434
+ WHERE template_id = ?
435
+ `).bind(
436
+ template.template_name,
437
+ template.template_type,
438
+ template.subject_template,
439
+ template.body_markdown,
440
+ template.variables,
441
+ template.description,
442
+ template.is_active,
443
+ now,
444
+ userId,
445
+ template.template_id
446
+ ).run();
447
+ } else {
448
+ await this.db.prepare(`
449
+ INSERT INTO ${table} (
450
+ template_id, template_name, template_type, subject_template,
451
+ body_markdown, variables, description, is_active, created_at, updated_at, updated_by
452
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
453
+ `).bind(
454
+ template.template_id,
455
+ template.template_name,
456
+ template.template_type,
457
+ template.subject_template,
458
+ template.body_markdown,
459
+ template.variables || "[]",
460
+ template.description,
461
+ template.is_active || 1,
462
+ now,
463
+ now,
464
+ userId
465
+ ).run();
466
+ }
467
+ if (this.cache) {
468
+ await this.cache.putTemplate(template);
469
+ }
470
+ }
471
+ async deleteTemplate(templateId) {
472
+ const table = `${this.config.emailTablePrefix}templates`;
473
+ await this.db.prepare(`DELETE FROM ${table} WHERE template_id = ?`).bind(templateId).run();
474
+ if (this.cache) {
475
+ await this.cache.deleteTemplate(templateId);
476
+ }
477
+ }
478
+ // --- Rendering ---
479
+ async renderTemplate(templateId, data) {
480
+ const template = await this.getTemplate(templateId);
481
+ if (!template) throw new Error(`Template not found: ${templateId}`);
482
+ const subject = import_mustache.default.render(template.subject_template, data);
483
+ let markdown = import_mustache.default.render(template.body_markdown, data);
484
+ markdown = markdown.replace(/\\n/g, "\n");
485
+ import_marked.marked.use({
486
+ mangle: false,
487
+ headerIds: false,
488
+ breaks: true
489
+ // Convert single line breaks to <br>
490
+ });
491
+ const htmlContent = import_marked.marked.parse(markdown);
492
+ const html = this.wrapInBaseTemplate(htmlContent, subject, data);
493
+ const plainText = markdown.replace(/<[^>]*>/g, "");
494
+ return { subject, html, plainText };
495
+ }
496
+ wrapInBaseTemplate(content, subject, data = {}) {
497
+ const templateData = {
498
+ ...data,
499
+ brandName: data.brandName || this.config.branding.brandName,
500
+ portalUrl: data.portalUrl || this.config.branding.portalUrl,
501
+ unsubscribeUrl: data.unsubscribeUrl || "{{unsubscribe_url}}"
502
+ };
503
+ return wrapInEmailTemplate(content, subject, templateData);
504
+ }
505
+ // --- Delivery ---
506
+ /**
507
+ * Send a single email
508
+ * @param {Object} params - Email parameters
509
+ * @param {string} params.to - Recipient email
510
+ * @param {string} params.subject - Email subject
511
+ * @param {string} params.html - HTML body
512
+ * @param {string} params.text - Plain text body
513
+ * @param {string} [params.provider] - Override provider
514
+ * @param {string} [params.profile='system'] - 'system' or 'tenant'
515
+ * @param {string} [params.tenantId] - Required if profile is 'tenant'
516
+ * @param {Object} [params.metadata] - Additional metadata
517
+ * @returns {Promise<Object>} Delivery result
518
+ */
519
+ async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {} }) {
520
+ const htmlContent = html || htmlBody;
521
+ const textContent = text || textBody;
522
+ try {
523
+ const settings = await this.loadSettings(profile, tenantId);
524
+ const useProvider = provider || settings.provider || "mailchannels";
525
+ let result;
526
+ switch (useProvider) {
527
+ case "mailchannels":
528
+ result = await this.sendViaMailChannels(to, subject, htmlContent, textContent, settings, metadata);
529
+ break;
530
+ case "sendgrid":
531
+ result = await this.sendViaSendGrid(to, subject, htmlContent, textContent, settings, metadata);
532
+ break;
533
+ case "resend":
534
+ result = await this.sendViaResend(to, subject, htmlContent, textContent, settings, metadata);
535
+ break;
536
+ case "sendpulse":
537
+ result = await this.sendViaSendPulse(to, subject, htmlContent, textContent, settings, metadata);
538
+ break;
539
+ default:
540
+ console.error(`[EmailService] Unknown provider: ${useProvider}`);
541
+ return { success: false, error: `Unknown email provider: ${useProvider}` };
542
+ }
543
+ if (result) {
544
+ return { success: true, messageId: crypto.randomUUID() };
545
+ } else {
546
+ console.error("[EmailService] Failed to send email to:", to);
547
+ return { success: false, error: "Failed to send email" };
548
+ }
549
+ } catch (error) {
550
+ console.error("[EmailService] Error sending email:", error);
551
+ return { success: false, error: error.message };
552
+ }
553
+ }
554
+ /**
555
+ * Send multiple emails in batch
556
+ * @param {Array} emails - Array of email objects
557
+ * @returns {Promise<Array>} Array of delivery results
558
+ */
559
+ async sendBatch(emails) {
560
+ console.log("[EmailService] Sending batch of", emails.length, "emails");
561
+ const results = await Promise.all(
562
+ emails.map((email) => this.sendEmail(email))
563
+ );
564
+ return results;
565
+ }
566
+ /**
567
+ * Send email via MailChannels HTTP API
568
+ * MailChannels is specifically designed for Cloudflare Workers
569
+ */
570
+ async sendViaMailChannels(to, subject, html, text, settings, metadata) {
571
+ try {
572
+ const response = await fetch("https://api.mailchannels.net/tx/v1/send", {
573
+ method: "POST",
574
+ headers: { "Content-Type": "application/json" },
575
+ body: JSON.stringify({
576
+ personalizations: [{ to: [{ email: to, name: metadata.recipientName || "" }] }],
577
+ from: { email: settings.fromAddress, name: settings.fromName },
578
+ subject,
579
+ content: [
580
+ { type: "text/plain", value: text || html.replace(/<[^>]*>/g, "") },
581
+ { type: "text/html", value: html }
582
+ ]
583
+ })
584
+ });
585
+ if (response.status === 202) {
586
+ return true;
587
+ } else {
588
+ const contentType = response.headers.get("content-type");
589
+ const errorBody = contentType?.includes("application/json") ? await response.json() : await response.text();
590
+ console.error("[EmailService] MailChannels error:", response.status, errorBody);
591
+ return false;
592
+ }
593
+ } catch (error) {
594
+ console.error("[EmailService] MailChannels exception:", error.message);
595
+ return false;
596
+ }
597
+ }
598
+ /**
599
+ * Send email via SendGrid HTTP API
600
+ */
601
+ async sendViaSendGrid(to, subject, html, text, settings, metadata) {
602
+ try {
603
+ if (!settings.sendgridApiKey) {
604
+ console.error("[EmailService] SendGrid API key missing");
605
+ return false;
606
+ }
607
+ const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
608
+ method: "POST",
609
+ headers: {
610
+ "Authorization": `Bearer ${settings.sendgridApiKey}`,
611
+ "Content-Type": "application/json"
612
+ },
613
+ body: JSON.stringify({
614
+ personalizations: [{ to: [{ email: to, name: metadata.recipientName || "" }] }],
615
+ from: { email: settings.fromAddress, name: settings.fromName },
616
+ subject,
617
+ content: [
618
+ { type: "text/html", value: html },
619
+ { type: "text/plain", value: text || html.replace(/<[^>]*>/g, "") }
620
+ ]
621
+ })
622
+ });
623
+ if (response.status === 202) {
624
+ return true;
625
+ } else {
626
+ const errorText = await response.text();
627
+ console.error("[EmailService] SendGrid error:", response.status, errorText);
628
+ return false;
629
+ }
630
+ } catch (error) {
631
+ console.error("[EmailService] SendGrid exception:", error.message);
632
+ return false;
633
+ }
634
+ }
635
+ /**
636
+ * Send email via Resend HTTP API
637
+ */
638
+ async sendViaResend(to, subject, html, text, settings, metadata) {
639
+ try {
640
+ if (!settings.resendApiKey) {
641
+ console.error("[EmailService] Resend API key missing");
642
+ return false;
643
+ }
644
+ const response = await fetch("https://api.resend.com/emails", {
645
+ method: "POST",
646
+ headers: {
647
+ "Authorization": `Bearer ${settings.resendApiKey}`,
648
+ "Content-Type": "application/json"
649
+ },
650
+ body: JSON.stringify({
651
+ from: `${settings.fromName} <${settings.fromAddress}>`,
652
+ to: [to],
653
+ subject,
654
+ html,
655
+ text: text || html.replace(/<[^>]*>/g, "")
656
+ })
657
+ });
658
+ if (response.ok) {
659
+ return true;
660
+ } else {
661
+ const errorText = await response.text();
662
+ console.error("[EmailService] Resend error:", response.status, errorText);
663
+ return false;
664
+ }
665
+ } catch (error) {
666
+ console.error("[EmailService] Resend exception:", error.message);
667
+ return false;
668
+ }
669
+ }
670
+ /**
671
+ * Send email via SendPulse HTTP API
672
+ * SendPulse offers 15,000 free emails/month
673
+ */
674
+ async sendViaSendPulse(to, subject, html, text, settings, metadata) {
675
+ try {
676
+ if (!settings.sendpulseClientId || !settings.sendpulseClientSecret) {
677
+ console.error("[EmailService] SendPulse credentials missing");
678
+ return false;
679
+ }
680
+ const tokenParams = new URLSearchParams({
681
+ grant_type: "client_credentials",
682
+ client_id: settings.sendpulseClientId,
683
+ client_secret: settings.sendpulseClientSecret
684
+ });
685
+ const tokenResponse = await fetch("https://api.sendpulse.com/oauth/access_token", {
686
+ method: "POST",
687
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
688
+ body: tokenParams.toString()
689
+ });
690
+ if (!tokenResponse.ok) {
691
+ const error = await tokenResponse.text();
692
+ console.error("[EmailService] SendPulse auth error:", error);
693
+ return false;
694
+ }
695
+ const tokenData = await tokenResponse.json();
696
+ if (!tokenData.access_token) {
697
+ console.error("[EmailService] SendPulse: No access token in response");
698
+ return false;
699
+ }
700
+ const { access_token } = tokenData;
701
+ const toBase64 = (str) => {
702
+ if (!str) return "";
703
+ try {
704
+ return btoa(unescape(encodeURIComponent(String(str))));
705
+ } catch (e) {
706
+ console.error("[EmailService] Base64 encoding failed:", e);
707
+ return "";
708
+ }
709
+ };
710
+ const htmlSafe = html || "";
711
+ const textSafe = text || (htmlSafe ? htmlSafe.replace(/<[^>]*>/g, "") : "");
712
+ const response = await fetch("https://api.sendpulse.com/smtp/emails", {
713
+ method: "POST",
714
+ headers: {
715
+ "Authorization": `Bearer ${access_token}`,
716
+ "Content-Type": "application/json"
717
+ },
718
+ body: JSON.stringify({
719
+ email: {
720
+ html: toBase64(htmlSafe),
721
+ text: toBase64(textSafe),
722
+ subject,
723
+ from: { name: settings.fromName, email: settings.fromAddress },
724
+ to: [{ name: metadata.recipientName || "", email: to }]
725
+ }
726
+ })
727
+ });
728
+ if (response.ok) {
729
+ return true;
730
+ } else {
731
+ const errorText = await response.text();
732
+ console.error("[EmailService] SendPulse send error:", response.status, errorText);
733
+ return false;
734
+ }
735
+ } catch (error) {
736
+ console.error("[EmailService] SendPulse exception:", error.message);
737
+ return false;
738
+ }
739
+ }
740
+ };
741
+
742
+ // src/backend/routes/templates.js
743
+ function createTemplateRoutes(env, config = {}, cacheProvider = null) {
744
+ const app = new import_hono.Hono();
745
+ const emailService = new EmailService(env, config, cacheProvider);
746
+ app.get("/templates", async (c) => {
747
+ try {
748
+ const templates = await emailService.getAllTemplates();
749
+ return c.json({ success: true, templates });
750
+ } catch (err) {
751
+ return c.json({ success: false, error: err.message }, 500);
752
+ }
753
+ });
754
+ app.get("/templates/:id", async (c) => {
755
+ try {
756
+ const template = await emailService.getTemplate(c.req.param("id"));
757
+ if (!template) return c.json({ success: false, error: "Template not found" }, 404);
758
+ return c.json({ success: true, template });
759
+ } catch (err) {
760
+ return c.json({ success: false, error: err.message }, 500);
761
+ }
762
+ });
763
+ app.post("/templates", async (c) => {
764
+ try {
765
+ const data = await c.req.json();
766
+ await emailService.saveTemplate(data, "admin");
767
+ return c.json({ success: true, message: "Template saved" });
768
+ } catch (err) {
769
+ return c.json({ success: false, error: err.message }, 500);
770
+ }
771
+ });
772
+ app.delete("/templates/:id", async (c) => {
773
+ try {
774
+ await emailService.deleteTemplate(c.req.param("id"));
775
+ return c.json({ success: true, message: "Template deleted" });
776
+ } catch (err) {
777
+ return c.json({ success: false, error: err.message }, 500);
778
+ }
779
+ });
780
+ app.post("/templates/:id/preview", async (c) => {
781
+ try {
782
+ const id = c.req.param("id");
783
+ const data = await c.req.json();
784
+ const result = await emailService.renderTemplate(id, data);
785
+ return c.json({ success: true, preview: result });
786
+ } catch (err) {
787
+ return c.json({ success: false, error: err.message }, 500);
788
+ }
789
+ });
790
+ app.post("/templates/:id/test", async (c) => {
791
+ try {
792
+ const id = c.req.param("id");
793
+ const { to, data } = await c.req.json();
794
+ const { subject, html, plainText } = await emailService.renderTemplate(id, data);
795
+ const result = await emailService.sendEmail({
796
+ to,
797
+ subject: `[TEST] ${subject}`,
798
+ html,
799
+ text: plainText
800
+ });
801
+ if (result.success) {
802
+ return c.json({ success: true, message: "Test email sent" });
803
+ } else {
804
+ return c.json({ success: false, error: result.error }, 500);
805
+ }
806
+ } catch (err) {
807
+ return c.json({ success: false, error: err.message }, 500);
808
+ }
809
+ });
810
+ return app;
811
+ }
812
+
813
+ // src/backend/routes/tracking.js
814
+ var import_hono2 = require("hono");
815
+ var TRACKING_PIXEL = new Uint8Array([
816
+ 71,
817
+ 73,
818
+ 70,
819
+ 56,
820
+ 57,
821
+ 97,
822
+ 1,
823
+ 0,
824
+ 1,
825
+ 0,
826
+ 128,
827
+ 0,
828
+ 0,
829
+ 0,
830
+ 0,
831
+ 0,
832
+ 255,
833
+ 255,
834
+ 255,
835
+ 33,
836
+ 249,
837
+ 4,
838
+ 1,
839
+ 0,
840
+ 0,
841
+ 0,
842
+ 0,
843
+ 44,
844
+ 0,
845
+ 0,
846
+ 0,
847
+ 0,
848
+ 1,
849
+ 0,
850
+ 1,
851
+ 0,
852
+ 0,
853
+ 2,
854
+ 1,
855
+ 68,
856
+ 0,
857
+ 59
858
+ ]);
859
+ function createTrackingRoutes(env, config = {}) {
860
+ const app = new import_hono2.Hono();
861
+ const db = env.DB;
862
+ const tablePrefix = config.emailTablePrefix || config.tableNamePrefix || "system_email_";
863
+ app.get("/track/open/:token", async (c) => {
864
+ const token = c.req.param("token");
865
+ try {
866
+ const sendId = token;
867
+ const send = await db.prepare(`SELECT * FROM ${tablePrefix}sends WHERE send_id = ?`).bind(sendId).first();
868
+ if (send) {
869
+ const eventId = crypto.randomUUID();
870
+ const now = Math.floor(Date.now() / 1e3);
871
+ await db.prepare(
872
+ `INSERT INTO ${tablePrefix}events (
873
+ event_id, send_id, user_id, tenant_id, email_kind, event_type, metadata, created_at
874
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
875
+ ).bind(
876
+ eventId,
877
+ sendId,
878
+ send.user_id,
879
+ send.tenant_id,
880
+ send.email_kind,
881
+ "opened",
882
+ JSON.stringify({
883
+ user_agent: c.req.header("user-agent"),
884
+ referer: c.req.header("referer")
885
+ }),
886
+ now
887
+ ).run();
888
+ }
889
+ } catch (error) {
890
+ console.error("[EmailTracking] Error tracking email open:", error);
891
+ }
892
+ return new Response(TRACKING_PIXEL, {
893
+ headers: {
894
+ "Content-Type": "image/gif",
895
+ "Cache-Control": "no-cache, no-store, must-revalidate",
896
+ "Pragma": "no-cache",
897
+ "Expires": "0"
898
+ }
899
+ });
900
+ });
901
+ app.post("/track/click/:token", async (c) => {
902
+ const token = c.req.param("token");
903
+ let url;
904
+ try {
905
+ const body = await c.req.json();
906
+ url = body.url;
907
+ } catch {
908
+ return c.json({ success: false, error: "Invalid request body" }, 400);
909
+ }
910
+ if (!url) {
911
+ return c.json({ success: false, error: "Missing URL" }, 400);
912
+ }
913
+ try {
914
+ const sendId = token;
915
+ const send = await db.prepare(`SELECT * FROM ${tablePrefix}sends WHERE send_id = ?`).bind(sendId).first();
916
+ if (send) {
917
+ const eventId = crypto.randomUUID();
918
+ const now = Math.floor(Date.now() / 1e3);
919
+ await db.prepare(
920
+ `INSERT INTO ${tablePrefix}events (
921
+ event_id, send_id, user_id, tenant_id, email_kind, event_type, metadata, created_at
922
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
923
+ ).bind(
924
+ eventId,
925
+ sendId,
926
+ send.user_id,
927
+ send.tenant_id,
928
+ send.email_kind,
929
+ "clicked",
930
+ JSON.stringify({
931
+ url,
932
+ user_agent: c.req.header("user-agent"),
933
+ referer: c.req.header("referer")
934
+ }),
935
+ now
936
+ ).run();
937
+ }
938
+ return c.json({ success: true, tracked: !!send });
939
+ } catch (error) {
940
+ console.error("[EmailTracking] Error tracking email click:", error);
941
+ return c.json({ success: false, error: "Failed to track click" }, 500);
942
+ }
943
+ });
944
+ app.get("/unsubscribe/:token", async (c) => {
945
+ const unsubToken = c.req.param("token");
946
+ try {
947
+ const prefs = await db.prepare(`SELECT * FROM ${tablePrefix}preferences WHERE unsub_token = ?`).bind(unsubToken).first();
948
+ if (!prefs) {
949
+ return c.json({ success: false, error: "Invalid unsubscribe link" }, 404);
950
+ }
951
+ const currentSettings = JSON.parse(prefs.email_settings || "{}");
952
+ const alreadyUnsubscribed = Object.keys(currentSettings).length === 0;
953
+ if (!alreadyUnsubscribed) {
954
+ const now = Math.floor(Date.now() / 1e3);
955
+ await db.prepare(
956
+ `UPDATE ${tablePrefix}preferences
957
+ SET email_settings = '{}',
958
+ updated_at = ?
959
+ WHERE unsub_token = ?`
960
+ ).bind(now, unsubToken).run();
961
+ const eventId = crypto.randomUUID();
962
+ await db.prepare(
963
+ `INSERT INTO ${tablePrefix}events (
964
+ event_id, send_id, user_id, tenant_id, email_kind, event_type, metadata, created_at
965
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
966
+ ).bind(
967
+ eventId,
968
+ "unsubscribe",
969
+ prefs.user_id,
970
+ prefs.tenant_id,
971
+ "all",
972
+ "unsubscribed",
973
+ JSON.stringify({
974
+ user_agent: c.req.header("user-agent")
975
+ }),
976
+ now
977
+ ).run();
978
+ }
979
+ return c.json({ success: true, alreadyUnsubscribed });
980
+ } catch (error) {
981
+ console.error("[EmailTracking] Error processing unsubscribe:", error);
982
+ return c.json({ success: false, error: "An error occurred" }, 500);
983
+ }
984
+ });
985
+ return app;
986
+ }
987
+
988
+ // src/backend/routes/index.js
989
+ function createEmailRoutes(env, config = {}, cacheProvider = null) {
990
+ const app = new import_hono3.Hono();
991
+ app.route("/api/email", createTemplateRoutes(env, config, cacheProvider));
992
+ app.route("/email", createTrackingRoutes(env, config));
993
+ return app;
994
+ }
995
+ // Annotate the CommonJS export names for ESM import in node:
996
+ 0 && (module.exports = {
997
+ createEmailRoutes,
998
+ createTemplateRoutes,
999
+ createTrackingRoutes
1000
+ });
1001
+ //# sourceMappingURL=index.cjs.map