@contentgrowth/content-emailing 0.4.1 → 0.5.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 (46) hide show
  1. package/dist/backend/EmailService.cjs +728 -0
  2. package/dist/backend/EmailService.cjs.map +1 -0
  3. package/dist/backend/EmailService.js +694 -0
  4. package/dist/backend/EmailService.js.map +1 -0
  5. package/dist/backend/EmailingCacheDO.cjs +390 -0
  6. package/dist/backend/EmailingCacheDO.cjs.map +1 -0
  7. package/dist/backend/EmailingCacheDO.js +365 -0
  8. package/dist/backend/EmailingCacheDO.js.map +1 -0
  9. package/dist/backend/routes/index.cjs +992 -0
  10. package/dist/backend/routes/index.cjs.map +1 -0
  11. package/dist/backend/routes/index.js +956 -0
  12. package/dist/backend/routes/index.js.map +1 -0
  13. package/dist/cli.cjs +53 -0
  14. package/dist/cli.cjs.map +1 -0
  15. package/dist/cli.js +53 -0
  16. package/dist/cli.js.map +1 -0
  17. package/dist/common/index.cjs +267 -0
  18. package/dist/common/index.cjs.map +1 -0
  19. package/{src/common/htmlWrapper.js → dist/common/index.js} +75 -18
  20. package/dist/common/index.js.map +1 -0
  21. package/dist/index.cjs +1537 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.js +1488 -0
  24. package/dist/index.js.map +1 -0
  25. package/package.json +27 -12
  26. package/examples/.env.example +0 -16
  27. package/examples/README.md +0 -55
  28. package/examples/mocks/MockD1.js +0 -311
  29. package/examples/mocks/MockEmailSender.js +0 -64
  30. package/examples/mocks/index.js +0 -5
  31. package/examples/package-lock.json +0 -73
  32. package/examples/package.json +0 -18
  33. package/examples/portal/index.html +0 -919
  34. package/examples/server.js +0 -314
  35. package/release.sh +0 -56
  36. package/src/backend/EmailService.js +0 -537
  37. package/src/backend/EmailingCacheDO.js +0 -466
  38. package/src/backend/routes/index.js +0 -30
  39. package/src/backend/routes/templates.js +0 -98
  40. package/src/backend/routes/tracking.js +0 -215
  41. package/src/backend/routes.js +0 -98
  42. package/src/common/index.js +0 -11
  43. package/src/common/utils.js +0 -141
  44. package/src/frontend/TemplateEditor.jsx +0 -117
  45. package/src/frontend/TemplateManager.jsx +0 -117
  46. package/src/index.js +0 -24
@@ -0,0 +1,694 @@
1
+ // src/backend/EmailService.js
2
+ import { marked } from "marked";
3
+ import Mustache from "mustache";
4
+
5
+ // src/common/htmlWrapper.js
6
+ function wrapInEmailTemplate(contentHtml, subject, data = {}) {
7
+ const portalUrl = data.portalUrl || "https://app.x0start.com";
8
+ const unsubscribeUrl = data.unsubscribeUrl || "{{unsubscribe_url}}";
9
+ const brandName = data.brandName || "X0 Start";
10
+ return `
11
+ <!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title>${subject}</title>
17
+ <style>
18
+ body {
19
+ margin: 0;
20
+ padding: 0;
21
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
22
+ background-color: #f5f5f5;
23
+ line-height: 1.6;
24
+ }
25
+ .email-wrapper {
26
+ background-color: #f5f5f5;
27
+ padding: 40px 20px;
28
+ }
29
+ .email-container {
30
+ max-width: 600px;
31
+ margin: 0 auto;
32
+ background-color: #ffffff;
33
+ border-radius: 8px;
34
+ overflow: hidden;
35
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
36
+ }
37
+ .email-header {
38
+ padding: 40px 40px 20px;
39
+ text-align: center;
40
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
41
+ color: #ffffff;
42
+ }
43
+ .email-header h1 {
44
+ margin: 0;
45
+ font-size: 28px;
46
+ font-weight: 600;
47
+ }
48
+ .email-content {
49
+ padding: 30px 40px;
50
+ color: #333333;
51
+ }
52
+ .email-content h1 {
53
+ font-size: 24px;
54
+ margin-top: 0;
55
+ margin-bottom: 20px;
56
+ color: #333333;
57
+ }
58
+ .email-content h2 {
59
+ font-size: 20px;
60
+ margin-top: 30px;
61
+ margin-bottom: 15px;
62
+ color: #333333;
63
+ }
64
+ .email-content h3 {
65
+ font-size: 16px;
66
+ margin-top: 20px;
67
+ margin-bottom: 10px;
68
+ color: #333333;
69
+ }
70
+ .email-content p {
71
+ margin: 0 0 15px;
72
+ color: #666666;
73
+ }
74
+ .email-content a {
75
+ color: #667eea;
76
+ text-decoration: none;
77
+ }
78
+ .email-content ul, .email-content ol {
79
+ margin: 0 0 15px;
80
+ padding-left: 25px;
81
+ }
82
+ .email-content li {
83
+ margin-bottom: 8px;
84
+ color: #666666;
85
+ }
86
+ .email-content blockquote {
87
+ margin: 20px 0;
88
+ padding: 15px 20px;
89
+ background-color: #f8f9fa;
90
+ border-left: 4px solid #667eea;
91
+ color: #666666;
92
+ }
93
+ .email-content code {
94
+ padding: 2px 6px;
95
+ background-color: #f8f9fa;
96
+ border-radius: 3px;
97
+ font-family: 'Courier New', monospace;
98
+ font-size: 14px;
99
+ }
100
+ .email-content pre {
101
+ padding: 15px;
102
+ background-color: #f8f9fa;
103
+ border-radius: 6px;
104
+ overflow-x: auto;
105
+ }
106
+ .email-content pre code {
107
+ padding: 0;
108
+ background: none;
109
+ }
110
+ .btn {
111
+ display: inline-block;
112
+ padding: 12px 24px;
113
+ background-color: #667eea;
114
+ color: #ffffff !important;
115
+ text-decoration: none;
116
+ border-radius: 6px;
117
+ font-weight: 600;
118
+ margin: 10px 0;
119
+ }
120
+ .btn:hover {
121
+ background-color: #5568d3;
122
+ }
123
+ .email-footer {
124
+ padding: 20px 40px;
125
+ background-color: #f8f9fa;
126
+ text-align: center;
127
+ font-size: 12px;
128
+ color: #666666;
129
+ }
130
+ .email-footer a {
131
+ color: #667eea;
132
+ text-decoration: none;
133
+ }
134
+ hr {
135
+ border: none;
136
+ border-top: 1px solid #e0e0e0;
137
+ margin: 30px 0;
138
+ }
139
+ </style>
140
+ </head>
141
+ <body>
142
+ <div class="email-wrapper">
143
+ <div class="email-container">
144
+ <div class="email-content">
145
+ ${contentHtml}
146
+ </div>
147
+ <div class="email-footer">
148
+ <p style="margin: 0 0 10px;">
149
+ You're receiving this email from ${brandName}.
150
+ </p>
151
+ <p style="margin: 0;">
152
+ <a href="${unsubscribeUrl}">Unsubscribe</a> |
153
+ <a href="${portalUrl}/settings/notifications">Manage Preferences</a>
154
+ </p>
155
+ </div>
156
+ </div>
157
+ </div>
158
+ </body>
159
+ </html>
160
+ `.trim();
161
+ }
162
+
163
+ // src/backend/EmailingCacheDO.js
164
+ function createDOCacheProvider(doStub, instanceName = "global") {
165
+ if (!doStub) {
166
+ return null;
167
+ }
168
+ const stub = doStub.get(doStub.idFromName(instanceName));
169
+ return {
170
+ async getTemplate(templateId) {
171
+ try {
172
+ const response = await stub.fetch(`http://do/get?templateId=${templateId}`);
173
+ const data = await response.json();
174
+ return data.template || null;
175
+ } catch (e) {
176
+ console.warn("[DOCacheProvider] Failed to get template:", e);
177
+ return null;
178
+ }
179
+ },
180
+ async getSettings(profile, tenantId) {
181
+ const key = `${profile}:${tenantId || ""}`;
182
+ try {
183
+ const response = await stub.fetch(`http://do/settings/get?key=${encodeURIComponent(key)}`);
184
+ const data = await response.json();
185
+ return data.settings || null;
186
+ } catch (e) {
187
+ return null;
188
+ }
189
+ },
190
+ async putSettings(profile, tenantId, settings) {
191
+ const key = `${profile}:${tenantId || ""}`;
192
+ try {
193
+ await stub.fetch("http://do/settings/put", {
194
+ method: "POST",
195
+ headers: { "Content-Type": "application/json" },
196
+ body: JSON.stringify({ key, settings })
197
+ });
198
+ } catch (e) {
199
+ console.warn("[DOCacheProvider] Failed to cache settings:", e);
200
+ }
201
+ },
202
+ async putTemplate(template) {
203
+ try {
204
+ await stub.fetch("http://do/invalidate", {
205
+ method: "POST",
206
+ headers: { "Content-Type": "application/json" },
207
+ body: JSON.stringify({ templateId: template.template_id })
208
+ });
209
+ } catch (e) {
210
+ console.warn("[DOCacheProvider] Failed to invalidate template:", e);
211
+ }
212
+ },
213
+ async deleteTemplate(templateId) {
214
+ try {
215
+ await stub.fetch("http://do/invalidate", {
216
+ method: "POST",
217
+ headers: { "Content-Type": "application/json" },
218
+ body: JSON.stringify({ templateId })
219
+ });
220
+ } catch (e) {
221
+ console.warn("[DOCacheProvider] Failed to invalidate template:", e);
222
+ }
223
+ },
224
+ async invalidateSettings(profile, tenantId) {
225
+ const key = `${profile}:${tenantId || ""}`;
226
+ try {
227
+ await stub.fetch("http://do/settings/invalidate", {
228
+ method: "POST",
229
+ headers: { "Content-Type": "application/json" },
230
+ body: JSON.stringify({ key })
231
+ });
232
+ } catch (e) {
233
+ console.warn("[DOCacheProvider] Failed to invalidate settings:", e);
234
+ }
235
+ }
236
+ };
237
+ }
238
+
239
+ // src/backend/EmailService.js
240
+ var EmailService = class {
241
+ /**
242
+ * @param {Object} env - Cloudflare environment bindings (DB, etc.)
243
+ * @param {Object} config - Configuration options
244
+ * @param {string} [config.emailTablePrefix='system_email_'] - Prefix for D1 tables
245
+ * @param {Object} [config.defaults] - Default settings (fromName, fromAddress)
246
+ * @param {Object} [cacheProvider] - Optional cache interface (DO stub or KV wrapper)
247
+ */
248
+ constructor(env, config = {}, cacheProvider = null) {
249
+ var _a, _b, _c;
250
+ this.env = env;
251
+ this.db = env.DB;
252
+ this.config = {
253
+ emailTablePrefix: config.emailTablePrefix || config.tableNamePrefix || "system_email_",
254
+ defaults: config.defaults || {
255
+ fromName: "System",
256
+ fromAddress: "noreply@example.com",
257
+ provider: "mailchannels"
258
+ },
259
+ // Loader function to fetch settings from backend (DB, KV, etc.)
260
+ // Signature: async (profile, tenantId) => SettingsObject
261
+ settingsLoader: config.settingsLoader || null,
262
+ // Updater function to save settings to backend
263
+ // Signature: async (profile, tenantId, settings) => void
264
+ settingsUpdater: config.settingsUpdater || null,
265
+ // Branding configuration for email templates
266
+ branding: {
267
+ brandName: ((_a = config.branding) == null ? void 0 : _a.brandName) || "Your App",
268
+ portalUrl: ((_b = config.branding) == null ? void 0 : _b.portalUrl) || "https://app.example.com",
269
+ primaryColor: ((_c = config.branding) == null ? void 0 : _c.primaryColor) || "#667eea",
270
+ ...config.branding
271
+ },
272
+ ...config
273
+ };
274
+ if (!cacheProvider && env.EMAIL_TEMPLATE_CACHE) {
275
+ this.cache = createDOCacheProvider(env.EMAIL_TEMPLATE_CACHE);
276
+ } else {
277
+ this.cache = cacheProvider;
278
+ }
279
+ }
280
+ // --- Configuration & Settings ---
281
+ /**
282
+ * Load email configuration
283
+ * @param {string} profile - 'system' or 'tenant' (or custom profile string)
284
+ * @param {string} tenantId - Context ID (optional)
285
+ * @returns {Promise<Object>} Email configuration
286
+ */
287
+ async loadSettings(profile = "system", tenantId = null) {
288
+ if (this.cache && this.cache.getSettings) {
289
+ try {
290
+ const cached = await this.cache.getSettings(profile, tenantId);
291
+ if (cached) return this._normalizeConfig(cached);
292
+ } catch (e) {
293
+ }
294
+ }
295
+ let settings = null;
296
+ if (this.config.settingsLoader) {
297
+ try {
298
+ settings = await this.config.settingsLoader(profile, tenantId);
299
+ } catch (e) {
300
+ console.warn("[EmailService] settingsLoader failed:", e);
301
+ }
302
+ }
303
+ if (settings) {
304
+ if (this.cache && this.cache.putSettings) {
305
+ try {
306
+ this.cache.putSettings(profile, tenantId, settings);
307
+ } catch (e) {
308
+ }
309
+ }
310
+ return this._normalizeConfig(settings);
311
+ }
312
+ return {
313
+ ...this.config.defaults
314
+ };
315
+ }
316
+ /**
317
+ * Normalize config keys to standard format
318
+ * Handles both snake_case (DB) and camelCase inputs
319
+ */
320
+ _normalizeConfig(config) {
321
+ return {
322
+ provider: config.email_provider || config.provider || this.config.defaults.provider,
323
+ fromAddress: config.email_from_address || config.fromAddress || this.config.defaults.fromAddress,
324
+ fromName: config.email_from_name || config.fromName || this.config.defaults.fromName,
325
+ // Provider-specific settings (normalize DB keys to service keys)
326
+ sendgridApiKey: config.sendgrid_api_key || config.sendgridApiKey,
327
+ resendApiKey: config.resend_api_key || config.resendApiKey,
328
+ sendpulseClientId: config.sendpulse_client_id || config.sendpulseClientId,
329
+ sendpulseClientSecret: config.sendpulse_client_secret || config.sendpulseClientSecret,
330
+ // SMTP
331
+ smtpHost: config.smtp_host || config.smtpHost,
332
+ smtpPort: config.smtp_port || config.smtpPort,
333
+ smtpUsername: config.smtp_username || config.smtpUsername,
334
+ smtpPassword: config.smtp_password || config.smtpPassword,
335
+ // Tracking
336
+ trackingUrl: config.tracking_url || config.trackingUrl,
337
+ // Pass through others
338
+ // Pass through others
339
+ ...config,
340
+ smtpSecure: config.smtp_secure === "true"
341
+ };
342
+ }
343
+ // --- Template Management ---
344
+ async getTemplate(templateId) {
345
+ if (this.cache) {
346
+ try {
347
+ const cached = await this.cache.getTemplate(templateId);
348
+ if (cached) return cached;
349
+ } catch (e) {
350
+ console.warn("[EmailService] Template cache lookup failed:", e);
351
+ }
352
+ }
353
+ const table = `${this.config.emailTablePrefix}templates`;
354
+ return await this.db.prepare(`SELECT * FROM ${table} WHERE template_id = ?`).bind(templateId).first();
355
+ }
356
+ async getAllTemplates() {
357
+ const table = `${this.config.emailTablePrefix}templates`;
358
+ const result = await this.db.prepare(`SELECT * FROM ${table} ORDER BY template_name`).all();
359
+ return result.results || [];
360
+ }
361
+ /**
362
+ * Save email configuration
363
+ * @param {Object} settings - Config object
364
+ * @param {string} profile - 'system' or 'tenant'
365
+ * @param {string} tenantId - Context ID
366
+ */
367
+ async saveSettings(settings, profile = "system", tenantId = null) {
368
+ if (this.config.settingsUpdater) {
369
+ try {
370
+ await this.config.settingsUpdater(profile, tenantId, settings);
371
+ } catch (e) {
372
+ console.error("[EmailService] settingsUpdater failed:", e);
373
+ throw e;
374
+ }
375
+ } else if (profile === "system" && this.db) {
376
+ }
377
+ if (this.cache && this.cache.invalidateSettings) {
378
+ try {
379
+ await this.cache.invalidateSettings(profile, tenantId);
380
+ } catch (e) {
381
+ console.warn("[EmailService] Failed to invalidate settings cache:", e);
382
+ }
383
+ }
384
+ }
385
+ async saveTemplate(template, userId = "system") {
386
+ const table = `${this.config.emailTablePrefix}templates`;
387
+ const now = Math.floor(Date.now() / 1e3);
388
+ const existing = await this.getTemplate(template.template_id);
389
+ if (existing) {
390
+ await this.db.prepare(`
391
+ UPDATE ${table} SET
392
+ template_name = ?, template_type = ?, subject_template = ?,
393
+ body_markdown = ?, variables = ?, description = ?, is_active = ?,
394
+ updated_at = ?, updated_by = ?
395
+ WHERE template_id = ?
396
+ `).bind(
397
+ template.template_name,
398
+ template.template_type,
399
+ template.subject_template,
400
+ template.body_markdown,
401
+ template.variables,
402
+ template.description,
403
+ template.is_active,
404
+ now,
405
+ userId,
406
+ template.template_id
407
+ ).run();
408
+ } else {
409
+ await this.db.prepare(`
410
+ INSERT INTO ${table} (
411
+ template_id, template_name, template_type, subject_template,
412
+ body_markdown, variables, description, is_active, created_at, updated_at, updated_by
413
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
414
+ `).bind(
415
+ template.template_id,
416
+ template.template_name,
417
+ template.template_type,
418
+ template.subject_template,
419
+ template.body_markdown,
420
+ template.variables || "[]",
421
+ template.description,
422
+ template.is_active || 1,
423
+ now,
424
+ now,
425
+ userId
426
+ ).run();
427
+ }
428
+ if (this.cache) {
429
+ await this.cache.putTemplate(template);
430
+ }
431
+ }
432
+ async deleteTemplate(templateId) {
433
+ const table = `${this.config.emailTablePrefix}templates`;
434
+ await this.db.prepare(`DELETE FROM ${table} WHERE template_id = ?`).bind(templateId).run();
435
+ if (this.cache) {
436
+ await this.cache.deleteTemplate(templateId);
437
+ }
438
+ }
439
+ // --- Rendering ---
440
+ async renderTemplate(templateId, data) {
441
+ const template = await this.getTemplate(templateId);
442
+ if (!template) throw new Error(`Template not found: ${templateId}`);
443
+ const subject = Mustache.render(template.subject_template, data);
444
+ const markdown = Mustache.render(template.body_markdown, data);
445
+ marked.use({ mangle: false, headerIds: false });
446
+ const htmlContent = marked.parse(markdown);
447
+ const html = this.wrapInBaseTemplate(htmlContent, subject, data);
448
+ const plainText = markdown.replace(/<[^>]*>/g, "");
449
+ return { subject, html, plainText };
450
+ }
451
+ wrapInBaseTemplate(content, subject, data = {}) {
452
+ const templateData = {
453
+ ...data,
454
+ brandName: data.brandName || this.config.branding.brandName,
455
+ portalUrl: data.portalUrl || this.config.branding.portalUrl,
456
+ unsubscribeUrl: data.unsubscribeUrl || "{{unsubscribe_url}}"
457
+ };
458
+ return wrapInEmailTemplate(content, subject, templateData);
459
+ }
460
+ // --- Delivery ---
461
+ /**
462
+ * Send a single email
463
+ * @param {Object} params - Email parameters
464
+ * @param {string} params.to - Recipient email
465
+ * @param {string} params.subject - Email subject
466
+ * @param {string} params.html - HTML body
467
+ * @param {string} params.text - Plain text body
468
+ * @param {string} [params.provider] - Override provider
469
+ * @param {string} [params.profile='system'] - 'system' or 'tenant'
470
+ * @param {string} [params.tenantId] - Required if profile is 'tenant'
471
+ * @param {Object} [params.metadata] - Additional metadata
472
+ * @returns {Promise<Object>} Delivery result
473
+ */
474
+ async sendEmail({ to, subject, html, htmlBody, text, textBody, provider, profile = "system", tenantId = null, metadata = {} }) {
475
+ const htmlContent = html || htmlBody;
476
+ const textContent = text || textBody;
477
+ try {
478
+ const settings = await this.loadSettings(profile, tenantId);
479
+ const useProvider = provider || settings.provider || "mailchannels";
480
+ let result;
481
+ switch (useProvider) {
482
+ case "mailchannels":
483
+ result = await this.sendViaMailChannels(to, subject, htmlContent, textContent, settings, metadata);
484
+ break;
485
+ case "sendgrid":
486
+ result = await this.sendViaSendGrid(to, subject, htmlContent, textContent, settings, metadata);
487
+ break;
488
+ case "resend":
489
+ result = await this.sendViaResend(to, subject, htmlContent, textContent, settings, metadata);
490
+ break;
491
+ case "sendpulse":
492
+ result = await this.sendViaSendPulse(to, subject, htmlContent, textContent, settings, metadata);
493
+ break;
494
+ default:
495
+ console.error(`[EmailService] Unknown provider: ${useProvider}`);
496
+ return { success: false, error: `Unknown email provider: ${useProvider}` };
497
+ }
498
+ if (result) {
499
+ return { success: true, messageId: crypto.randomUUID() };
500
+ } else {
501
+ console.error("[EmailService] Failed to send email to:", to);
502
+ return { success: false, error: "Failed to send email" };
503
+ }
504
+ } catch (error) {
505
+ console.error("[EmailService] Error sending email:", error);
506
+ return { success: false, error: error.message };
507
+ }
508
+ }
509
+ /**
510
+ * Send multiple emails in batch
511
+ * @param {Array} emails - Array of email objects
512
+ * @returns {Promise<Array>} Array of delivery results
513
+ */
514
+ async sendBatch(emails) {
515
+ console.log("[EmailService] Sending batch of", emails.length, "emails");
516
+ const results = await Promise.all(
517
+ emails.map((email) => this.sendEmail(email))
518
+ );
519
+ return results;
520
+ }
521
+ /**
522
+ * Send email via MailChannels HTTP API
523
+ * MailChannels is specifically designed for Cloudflare Workers
524
+ */
525
+ async sendViaMailChannels(to, subject, html, text, settings, metadata) {
526
+ try {
527
+ const response = await fetch("https://api.mailchannels.net/tx/v1/send", {
528
+ method: "POST",
529
+ headers: { "Content-Type": "application/json" },
530
+ body: JSON.stringify({
531
+ personalizations: [{ to: [{ email: to, name: metadata.recipientName || "" }] }],
532
+ from: { email: settings.fromAddress, name: settings.fromName },
533
+ subject,
534
+ content: [
535
+ { type: "text/plain", value: text || html.replace(/<[^>]*>/g, "") },
536
+ { type: "text/html", value: html }
537
+ ]
538
+ })
539
+ });
540
+ if (response.status === 202) {
541
+ return true;
542
+ } else {
543
+ const contentType = response.headers.get("content-type");
544
+ const errorBody = (contentType == null ? void 0 : contentType.includes("application/json")) ? await response.json() : await response.text();
545
+ console.error("[EmailService] MailChannels error:", response.status, errorBody);
546
+ return false;
547
+ }
548
+ } catch (error) {
549
+ console.error("[EmailService] MailChannels exception:", error.message);
550
+ return false;
551
+ }
552
+ }
553
+ /**
554
+ * Send email via SendGrid HTTP API
555
+ */
556
+ async sendViaSendGrid(to, subject, html, text, settings, metadata) {
557
+ try {
558
+ if (!settings.sendgridApiKey) {
559
+ console.error("[EmailService] SendGrid API key missing");
560
+ return false;
561
+ }
562
+ const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
563
+ method: "POST",
564
+ headers: {
565
+ "Authorization": `Bearer ${settings.sendgridApiKey}`,
566
+ "Content-Type": "application/json"
567
+ },
568
+ body: JSON.stringify({
569
+ personalizations: [{ to: [{ email: to, name: metadata.recipientName || "" }] }],
570
+ from: { email: settings.fromAddress, name: settings.fromName },
571
+ subject,
572
+ content: [
573
+ { type: "text/html", value: html },
574
+ { type: "text/plain", value: text || html.replace(/<[^>]*>/g, "") }
575
+ ]
576
+ })
577
+ });
578
+ if (response.status === 202) {
579
+ return true;
580
+ } else {
581
+ const errorText = await response.text();
582
+ console.error("[EmailService] SendGrid error:", response.status, errorText);
583
+ return false;
584
+ }
585
+ } catch (error) {
586
+ console.error("[EmailService] SendGrid exception:", error.message);
587
+ return false;
588
+ }
589
+ }
590
+ /**
591
+ * Send email via Resend HTTP API
592
+ */
593
+ async sendViaResend(to, subject, html, text, settings, metadata) {
594
+ try {
595
+ if (!settings.resendApiKey) {
596
+ console.error("[EmailService] Resend API key missing");
597
+ return false;
598
+ }
599
+ const response = await fetch("https://api.resend.com/emails", {
600
+ method: "POST",
601
+ headers: {
602
+ "Authorization": `Bearer ${settings.resendApiKey}`,
603
+ "Content-Type": "application/json"
604
+ },
605
+ body: JSON.stringify({
606
+ from: `${settings.fromName} <${settings.fromAddress}>`,
607
+ to: [to],
608
+ subject,
609
+ html,
610
+ text: text || html.replace(/<[^>]*>/g, "")
611
+ })
612
+ });
613
+ if (response.ok) {
614
+ return true;
615
+ } else {
616
+ const errorText = await response.text();
617
+ console.error("[EmailService] Resend error:", response.status, errorText);
618
+ return false;
619
+ }
620
+ } catch (error) {
621
+ console.error("[EmailService] Resend exception:", error.message);
622
+ return false;
623
+ }
624
+ }
625
+ /**
626
+ * Send email via SendPulse HTTP API
627
+ * SendPulse offers 15,000 free emails/month
628
+ */
629
+ async sendViaSendPulse(to, subject, html, text, settings, metadata) {
630
+ try {
631
+ if (!settings.sendpulseClientId || !settings.sendpulseClientSecret) {
632
+ console.error("[EmailService] SendPulse credentials missing");
633
+ return false;
634
+ }
635
+ const tokenParams = new URLSearchParams({
636
+ grant_type: "client_credentials",
637
+ client_id: settings.sendpulseClientId,
638
+ client_secret: settings.sendpulseClientSecret
639
+ });
640
+ const tokenResponse = await fetch("https://api.sendpulse.com/oauth/access_token", {
641
+ method: "POST",
642
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
643
+ body: tokenParams.toString()
644
+ });
645
+ if (!tokenResponse.ok) {
646
+ const error = await tokenResponse.text();
647
+ console.error("[EmailService] SendPulse auth error:", error);
648
+ return false;
649
+ }
650
+ const tokenData = await tokenResponse.json();
651
+ if (!tokenData.access_token) {
652
+ console.error("[EmailService] SendPulse: No access token in response");
653
+ return false;
654
+ }
655
+ const { access_token } = tokenData;
656
+ const toBase64 = (str) => {
657
+ if (!str) return "";
658
+ return Buffer.from(String(str)).toString("base64");
659
+ };
660
+ const htmlSafe = html || "";
661
+ const textSafe = text || (htmlSafe ? htmlSafe.replace(/<[^>]*>/g, "") : "");
662
+ const response = await fetch("https://api.sendpulse.com/smtp/emails", {
663
+ method: "POST",
664
+ headers: {
665
+ "Authorization": `Bearer ${access_token}`,
666
+ "Content-Type": "application/json"
667
+ },
668
+ body: JSON.stringify({
669
+ email: {
670
+ html: toBase64(htmlSafe),
671
+ text: toBase64(textSafe),
672
+ subject,
673
+ from: { name: settings.fromName, email: settings.fromAddress },
674
+ to: [{ name: metadata.recipientName || "", email: to }]
675
+ }
676
+ })
677
+ });
678
+ if (response.ok) {
679
+ return true;
680
+ } else {
681
+ const errorText = await response.text();
682
+ console.error("[EmailService] SendPulse send error:", response.status, errorText);
683
+ return false;
684
+ }
685
+ } catch (error) {
686
+ console.error("[EmailService] SendPulse exception:", error.message);
687
+ return false;
688
+ }
689
+ }
690
+ };
691
+ export {
692
+ EmailService
693
+ };
694
+ //# sourceMappingURL=EmailService.js.map