@flusys/nestjs-email 1.1.0-beta → 2.0.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 (100) hide show
  1. package/README.md +589 -0
  2. package/cjs/config/email.constants.js +0 -18
  3. package/cjs/config/index.js +0 -1
  4. package/cjs/controllers/email-config.controller.js +46 -4
  5. package/cjs/controllers/email-send.controller.js +13 -26
  6. package/cjs/controllers/email-template.controller.js +60 -11
  7. package/cjs/docs/email-swagger.config.js +18 -80
  8. package/cjs/dtos/email-config.dto.js +6 -106
  9. package/cjs/dtos/email-send.dto.js +101 -123
  10. package/cjs/dtos/email-template.dto.js +41 -103
  11. package/cjs/entities/email-config-with-company.entity.js +2 -2
  12. package/cjs/entities/email-config.entity.js +92 -3
  13. package/cjs/entities/email-template-with-company.entity.js +5 -3
  14. package/cjs/entities/email-template.entity.js +119 -3
  15. package/cjs/entities/index.js +34 -19
  16. package/cjs/index.js +1 -0
  17. package/cjs/interfaces/email-provider.interface.js +1 -3
  18. package/cjs/modules/email.module.js +50 -104
  19. package/cjs/providers/email-factory.service.js +37 -109
  20. package/cjs/providers/email-provider.registry.js +5 -15
  21. package/cjs/providers/mailgun-provider.js +54 -58
  22. package/cjs/providers/sendgrid-provider.js +68 -92
  23. package/cjs/providers/smtp-provider.js +58 -69
  24. package/cjs/{config → services}/email-config.service.js +9 -32
  25. package/cjs/services/email-datasource.provider.js +17 -104
  26. package/cjs/services/email-provider-config.service.js +28 -58
  27. package/cjs/services/email-send.service.js +120 -125
  28. package/cjs/services/email-template.service.js +62 -85
  29. package/cjs/services/index.js +2 -1
  30. package/cjs/utils/email-templates.util.js +64 -0
  31. package/cjs/utils/index.js +18 -0
  32. package/config/email.constants.d.ts +0 -9
  33. package/config/index.d.ts +0 -1
  34. package/controllers/email-send.controller.d.ts +5 -12
  35. package/controllers/email-template.controller.d.ts +5 -7
  36. package/dtos/email-config.dto.d.ts +5 -13
  37. package/dtos/email-send.dto.d.ts +17 -21
  38. package/dtos/email-template.dto.d.ts +5 -16
  39. package/entities/email-config-with-company.entity.d.ts +2 -2
  40. package/entities/email-config.entity.d.ts +10 -2
  41. package/entities/email-template-with-company.entity.d.ts +2 -2
  42. package/entities/email-template.entity.d.ts +13 -2
  43. package/entities/index.d.ts +9 -3
  44. package/fesm/config/email.constants.js +0 -9
  45. package/fesm/config/index.js +0 -1
  46. package/fesm/controllers/email-config.controller.js +49 -7
  47. package/fesm/controllers/email-send.controller.js +13 -26
  48. package/fesm/controllers/email-template.controller.js +61 -12
  49. package/fesm/docs/email-swagger.config.js +21 -86
  50. package/fesm/dtos/email-config.dto.js +9 -115
  51. package/fesm/dtos/email-send.dto.js +103 -139
  52. package/fesm/dtos/email-template.dto.js +43 -111
  53. package/fesm/entities/email-config-with-company.entity.js +2 -2
  54. package/fesm/entities/email-config.entity.js +93 -4
  55. package/fesm/entities/email-template-with-company.entity.js +5 -3
  56. package/fesm/entities/email-template.entity.js +120 -4
  57. package/fesm/entities/index.js +22 -16
  58. package/fesm/index.js +1 -0
  59. package/fesm/interfaces/email-config.interface.js +1 -3
  60. package/fesm/interfaces/email-module-options.interface.js +1 -3
  61. package/fesm/interfaces/email-provider.interface.js +1 -5
  62. package/fesm/interfaces/email-template.interface.js +1 -3
  63. package/fesm/modules/email.module.js +52 -106
  64. package/fesm/providers/email-factory.service.js +38 -69
  65. package/fesm/providers/email-provider.registry.js +6 -19
  66. package/fesm/providers/mailgun-provider.js +55 -63
  67. package/fesm/providers/sendgrid-provider.js +69 -97
  68. package/fesm/providers/smtp-provider.js +59 -73
  69. package/fesm/{config → services}/email-config.service.js +9 -32
  70. package/fesm/services/email-datasource.provider.js +18 -64
  71. package/fesm/services/email-provider-config.service.js +26 -56
  72. package/fesm/services/email-send.service.js +118 -123
  73. package/fesm/services/email-template.service.js +60 -83
  74. package/fesm/services/index.js +2 -1
  75. package/fesm/utils/email-templates.util.js +47 -0
  76. package/fesm/utils/index.js +1 -0
  77. package/index.d.ts +1 -0
  78. package/interfaces/email-config.interface.d.ts +6 -0
  79. package/interfaces/email-module-options.interface.d.ts +0 -5
  80. package/modules/email.module.d.ts +1 -2
  81. package/package.json +4 -4
  82. package/providers/email-factory.service.d.ts +4 -7
  83. package/providers/mailgun-provider.d.ts +6 -2
  84. package/providers/sendgrid-provider.d.ts +6 -2
  85. package/providers/smtp-provider.d.ts +7 -2
  86. package/services/email-config.service.d.ts +12 -0
  87. package/services/email-datasource.provider.d.ts +3 -6
  88. package/services/email-provider-config.service.d.ts +3 -3
  89. package/services/email-send.service.d.ts +11 -3
  90. package/services/email-template.service.d.ts +5 -4
  91. package/services/index.d.ts +2 -1
  92. package/utils/email-templates.util.d.ts +2 -0
  93. package/utils/index.d.ts +1 -0
  94. package/cjs/entities/email-config-base.entity.js +0 -111
  95. package/cjs/entities/email-template-base.entity.js +0 -134
  96. package/config/email-config.service.d.ts +0 -13
  97. package/entities/email-config-base.entity.d.ts +0 -11
  98. package/entities/email-template-base.entity.d.ts +0 -14
  99. package/fesm/entities/email-config-base.entity.js +0 -101
  100. package/fesm/entities/email-template-base.entity.js +0 -124
@@ -17,93 +17,62 @@ function _ts_decorate(decorators, target, key, desc) {
17
17
  else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
18
18
  return c > 3 && r && Object.defineProperty(target, key, r), r;
19
19
  }
20
- function _ts_metadata(k, v) {
21
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
22
- }
23
- function _ts_param(paramIndex, decorator) {
24
- return function(target, key) {
25
- decorator(target, key, paramIndex);
26
- };
27
- }
28
- import { Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
29
- import * as crypto from 'crypto';
30
- import { EmailConfigService } from '../config/email-config.service';
20
+ import { Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common';
21
+ import { createHash } from 'crypto';
31
22
  import { EmailProviderRegistry } from './email-provider.registry';
32
23
  export class EmailFactoryService {
33
- /**
34
- * Generate a unique cache key for a provider configuration
35
- */ generateCacheKey(config) {
36
- const configString = JSON.stringify(config.config, Object.keys(config.config || {}).sort());
37
- const configHash = crypto.createHash('sha256').update(configString).digest('hex').substring(0, 16);
38
- return `${config.provider}-${configHash}`;
39
- }
40
- /**
41
- * Create or retrieve a cached email provider instance
42
- */ async createProvider(config) {
43
- const providerKey = this.generateCacheKey(config);
44
- // Return cached provider if exists
45
- if (this.providerCache.has(providerKey)) {
46
- return this.providerCache.get(providerKey);
47
- }
48
- // Get provider class from registry
24
+ // ─── Public API ─────────────────────────────────────────────────────────────
25
+ async createProvider(config) {
26
+ const cacheKey = this.generateCacheKey(config);
27
+ const cached = this.providerCache.get(cacheKey);
28
+ if (cached) return cached;
49
29
  const ProviderClass = EmailProviderRegistry.get(config.provider);
50
30
  if (!ProviderClass) {
51
- throw new NotFoundException(`Email provider '${config.provider}' is not registered. ` + `Available providers: ${EmailProviderRegistry.getAll().join(', ')}`);
31
+ throw new NotFoundException(`Email provider '${config.provider}' is not registered. Available: ${EmailProviderRegistry.getAll().join(', ')}`);
52
32
  }
53
33
  try {
54
- // Create new instance
55
34
  const instance = new ProviderClass();
56
- // Initialize if provider has initialize method
57
- if ('initialize' in instance && typeof instance.initialize === 'function') {
58
- await instance.initialize(config.config);
59
- }
60
- // Cache the instance
61
- this.providerCache.set(providerKey, instance);
62
- this.logger.log(`Created email provider: ${config.provider} (key: ${providerKey})`);
35
+ await instance.initialize?.(config.config);
36
+ this.providerCache.set(cacheKey, instance);
37
+ this.logger.log(`Created email provider: ${config.provider} (key: ${cacheKey})`);
63
38
  return instance;
64
39
  } catch (error) {
65
40
  this.logger.error(`Failed to create provider ${config.provider}:`, error);
66
- throw new Error(`Failed to initialize email provider '${config.provider}': ${error?.message || 'Unknown error'}`);
41
+ throw new InternalServerErrorException(`Failed to initialize email provider '${config.provider}': ${this.extractErrorMessage(error)}`);
67
42
  }
68
43
  }
69
- /**
70
- * Check if a provider is available (registered)
71
- */ isProviderAvailable(providerName) {
72
- return EmailProviderRegistry.has(providerName);
73
- }
74
- /**
75
- * Get list of available providers
76
- */ getAvailableProviders() {
77
- return EmailProviderRegistry.getAll();
78
- }
79
- /**
80
- * Cleanup on module destroy
81
- */ async onModuleDestroy() {
44
+ // ─── Lifecycle ──────────────────────────────────────────────────────────────
45
+ async onModuleDestroy() {
82
46
  this.logger.log('Cleaning up email provider connections...');
83
- const closePromises = [];
84
- for (const [key, provider] of this.providerCache.entries()){
85
- if ('close' in provider && typeof provider.close === 'function') {
86
- closePromises.push(provider.close().then(()=>this.logger.debug(`Closed provider: ${key}`)).catch((err)=>this.logger.warn(`Failed to close provider ${key}: ${err.message}`)));
87
- }
88
- }
47
+ const closePromises = Array.from(this.providerCache.entries()).map(([key, provider])=>this.closeProvider(key, provider));
89
48
  await Promise.allSettled(closePromises);
90
49
  this.providerCache.clear();
91
50
  this.logger.log('Email provider cleanup complete');
92
51
  }
93
- constructor(emailConfigService){
94
- _define_property(this, "emailConfigService", void 0);
95
- _define_property(this, "logger", void 0);
96
- _define_property(this, "providerCache", void 0);
97
- this.emailConfigService = emailConfigService;
98
- this.logger = new Logger(EmailFactoryService.name);
99
- this.providerCache = new Map();
52
+ // ─── Private Helpers ────────────────────────────────────────────────────────
53
+ generateCacheKey(config) {
54
+ const sortedKeys = Object.keys(config.config || {}).sort();
55
+ const configString = JSON.stringify(config.config, sortedKeys);
56
+ const hash = createHash('sha256').update(configString).digest('hex').substring(0, 16);
57
+ return `${config.provider}-${hash}`;
58
+ }
59
+ async closeProvider(key, provider) {
60
+ if (!provider.close) return;
61
+ try {
62
+ await provider.close();
63
+ this.logger.debug(`Closed provider: ${key}`);
64
+ } catch (err) {
65
+ this.logger.warn(`Failed to close provider ${key}: ${this.extractErrorMessage(err)}`);
66
+ }
67
+ }
68
+ extractErrorMessage(error) {
69
+ return error instanceof Error ? error.message : 'Unknown error';
70
+ }
71
+ constructor(){
72
+ _define_property(this, "logger", new Logger(EmailFactoryService.name));
73
+ _define_property(this, "providerCache", new Map());
100
74
  }
101
75
  }
102
76
  EmailFactoryService = _ts_decorate([
103
- Injectable(),
104
- _ts_param(0, Inject(EmailConfigService)),
105
- _ts_metadata("design:type", Function),
106
- _ts_metadata("design:paramtypes", [
107
- typeof EmailConfigService === "undefined" ? Object : EmailConfigService
108
- ])
77
+ Injectable()
109
78
  ], EmailFactoryService);
@@ -11,33 +11,20 @@ function _define_property(obj, key, value) {
11
11
  }
12
12
  return obj;
13
13
  }
14
- /**
15
- * Registry for email providers
16
- * Allows dynamic registration of providers at runtime
17
- */ export class EmailProviderRegistry {
18
- /**
19
- * Register an email provider
20
- */ static register(providerName, providerClass) {
14
+ export class EmailProviderRegistry {
15
+ static register(providerName, providerClass) {
21
16
  this.providers.set(providerName.toLowerCase(), providerClass);
22
17
  }
23
- /**
24
- * Get a registered provider class
25
- */ static get(providerName) {
18
+ static get(providerName) {
26
19
  return this.providers.get(providerName.toLowerCase());
27
20
  }
28
- /**
29
- * Check if a provider is registered
30
- */ static has(providerName) {
21
+ static has(providerName) {
31
22
  return this.providers.has(providerName.toLowerCase());
32
23
  }
33
- /**
34
- * Get all registered provider names
35
- */ static getAll() {
24
+ static getAll() {
36
25
  return Array.from(this.providers.keys());
37
26
  }
38
- /**
39
- * Clear all providers (useful for testing)
40
- */ static clear() {
27
+ static clear() {
41
28
  this.providers.clear();
42
29
  }
43
30
  }
@@ -12,23 +12,17 @@ function _define_property(obj, key, value) {
12
12
  return obj;
13
13
  }
14
14
  import { Logger } from '@nestjs/common';
15
- /**
16
- * Mailgun Email Provider
17
- * Uses mailgun.js package for sending emails
18
- * Install: npm install mailgun.js form-data
19
- */ export class MailgunProvider {
20
- /**
21
- * Initialize the Mailgun provider with configuration
22
- */ async initialize(config) {
23
- this.config = config;
15
+ const MAILGUN_API_URL = 'https://api.mailgun.net';
16
+ const MAILGUN_EU_API_URL = 'https://api.eu.mailgun.net';
17
+ export class MailgunProvider {
18
+ async initialize(config) {
19
+ this.domain = config.domain;
24
20
  try {
25
- // Dynamic import mailgun.js and form-data - use eval to bypass TypeScript module resolution
26
- // eslint-disable-next-line @typescript-eslint/no-implied-eval
21
+ // Dynamic import for optional dependency
27
22
  const Mailgun = (await new Function('return import("mailgun.js")')()).default;
28
23
  const FormData = (await new Function('return import("form-data")')()).default;
29
24
  const mailgun = new Mailgun(FormData);
30
- // Determine API URL based on region
31
- const url = config.region === 'eu' ? 'https://api.eu.mailgun.net' : 'https://api.mailgun.net';
25
+ const url = config.region === 'eu' ? MAILGUN_EU_API_URL : MAILGUN_API_URL;
32
26
  this.client = mailgun.client({
33
27
  username: 'api',
34
28
  key: config.apiKey,
@@ -36,84 +30,82 @@ import { Logger } from '@nestjs/common';
36
30
  });
37
31
  this.logger.log(`Mailgun Provider initialized for domain: ${config.domain}`);
38
32
  } catch (error) {
39
- this.logger.error('Failed to initialize Mailgun:', error.message);
40
- throw new Error('Mailgun initialization failed. Make sure mailgun.js and form-data are installed: npm install mailgun.js form-data');
33
+ this.logger.error('Failed to initialize Mailgun:', this.extractError(error));
34
+ throw new Error('Mailgun initialization failed. Install: npm install mailgun.js form-data');
41
35
  }
42
36
  }
43
- /**
44
- * Send a single email
45
- */ async sendEmail(options) {
46
- if (!this.client || !this.config) {
37
+ async sendEmail(options) {
38
+ if (!this.client || !this.domain) {
47
39
  return {
48
40
  success: false,
49
41
  error: 'Mailgun provider not initialized'
50
42
  };
51
43
  }
52
44
  try {
53
- const to = Array.isArray(options.to) ? options.to.join(', ') : options.to;
54
- const cc = options.cc ? Array.isArray(options.cc) ? options.cc.join(', ') : options.cc : undefined;
55
- const bcc = options.bcc ? Array.isArray(options.bcc) ? options.bcc.join(', ') : options.bcc : undefined;
56
- const messageData = {
57
- from: options.fromName ? `${options.fromName} <${options.from}>` : options.from,
58
- to,
59
- subject: options.subject
60
- };
61
- // Only include html/text if they have values (required for plain text templates)
62
- if (options.html) messageData.html = options.html;
63
- if (options.text) messageData.text = options.text;
64
- // Optional fields
65
- if (cc) messageData.cc = cc;
66
- if (bcc) messageData.bcc = bcc;
67
- if (options.replyTo) messageData['h:Reply-To'] = options.replyTo;
68
- // Handle attachments
69
- if (options.attachments && options.attachments.length > 0) {
70
- messageData.attachment = options.attachments.map((a)=>({
71
- filename: a.filename,
72
- data: typeof a.content === 'string' ? Buffer.from(a.content, 'base64') : a.content,
73
- contentType: a.contentType
74
- }));
75
- }
76
- const result = await this.client.messages.create(this.config.domain, messageData);
45
+ const messageData = this.buildMessageData(options);
46
+ const result = await this.client.messages.create(this.domain, messageData);
77
47
  this.logger.log(`Email sent via Mailgun: ${result.id}`);
78
48
  return {
79
49
  success: true,
80
50
  messageId: result.id
81
51
  };
82
52
  } catch (error) {
83
- this.logger.error('Mailgun send failed:', error);
84
- return {
85
- success: false,
86
- error: error.message
87
- };
53
+ return this.handleError('Mailgun send failed', error);
88
54
  }
89
55
  }
90
- /**
91
- * Send multiple emails (batch)
92
- */ async sendBulkEmails(options) {
93
- // Mailgun supports batch sending with recipient variables
94
- // For simplicity, we send individually here
56
+ async sendBulkEmails(options) {
95
57
  return Promise.all(options.map((opt)=>this.sendEmail(opt)));
96
58
  }
97
- /**
98
- * Health check for the provider
99
- */ async healthCheck() {
100
- if (!this.client || !this.config) return false;
59
+ async healthCheck() {
60
+ if (!this.client || !this.domain) return false;
101
61
  try {
102
- // Verify domain exists
103
- await this.client.domains.get(this.config.domain);
62
+ await this.client.domains.get(this.domain);
104
63
  return true;
105
64
  } catch {
106
65
  return false;
107
66
  }
108
67
  }
109
- /**
110
- * Cleanup
111
- */ async close() {
68
+ async close() {
112
69
  this.client = null;
113
70
  }
71
+ // ─── Private Helpers ────────────────────────────────────────────────────────
72
+ buildMessageData(options) {
73
+ const data = {
74
+ from: options.fromName ? `${options.fromName} <${options.from}>` : options.from,
75
+ to: this.joinAddresses(options.to),
76
+ subject: options.subject
77
+ };
78
+ if (options.html) data.html = options.html;
79
+ if (options.text) data.text = options.text;
80
+ if (options.cc) data.cc = this.joinAddresses(options.cc);
81
+ if (options.bcc) data.bcc = this.joinAddresses(options.bcc);
82
+ if (options.replyTo) data['h:Reply-To'] = options.replyTo;
83
+ if (options.attachments?.length) {
84
+ data.attachment = options.attachments.map((a)=>({
85
+ filename: a.filename,
86
+ data: typeof a.content === 'string' ? Buffer.from(a.content, 'base64') : a.content,
87
+ contentType: a.contentType
88
+ }));
89
+ }
90
+ return data;
91
+ }
92
+ joinAddresses(addresses) {
93
+ return Array.isArray(addresses) ? addresses.join(', ') : addresses;
94
+ }
95
+ extractError(error) {
96
+ return error instanceof Error ? error.message : 'Unknown error';
97
+ }
98
+ handleError(context, error) {
99
+ const message = this.extractError(error);
100
+ this.logger.error(`${context}: ${message}`, error);
101
+ return {
102
+ success: false,
103
+ error: message
104
+ };
105
+ }
114
106
  constructor(){
115
107
  _define_property(this, "logger", new Logger(MailgunProvider.name));
116
108
  _define_property(this, "client", null);
117
- _define_property(this, "config", null);
109
+ _define_property(this, "domain", null);
118
110
  }
119
111
  }
@@ -12,70 +12,27 @@ function _define_property(obj, key, value) {
12
12
  return obj;
13
13
  }
14
14
  import { Logger } from '@nestjs/common';
15
- /**
16
- * SendGrid Email Provider
17
- * Uses @sendgrid/mail package for sending emails
18
- * Install: npm install @sendgrid/mail
19
- */ export class SendGridProvider {
20
- /**
21
- * Initialize the SendGrid provider with configuration
22
- */ async initialize(config) {
23
- this.config = config;
15
+ export class SendGridProvider {
16
+ async initialize(config) {
17
+ this.apiKey = config.apiKey;
24
18
  try {
25
- // Dynamic import @sendgrid/mail - use eval to bypass TypeScript module resolution
26
- // eslint-disable-next-line @typescript-eslint/no-implied-eval
19
+ // Dynamic import to avoid bundling issues
27
20
  const sgMailModule = await new Function('return import("@sendgrid/mail")')();
28
21
  this.sgMail = sgMailModule.default || sgMailModule;
29
22
  this.sgMail.setApiKey(config.apiKey);
30
23
  this.logger.log('SendGrid Provider initialized');
31
24
  } catch (error) {
32
- this.logger.error('Failed to initialize SendGrid:', error.message);
33
- throw new Error('SendGrid initialization failed. Make sure @sendgrid/mail is installed: npm install @sendgrid/mail');
25
+ this.logger.error('Failed to initialize SendGrid:', this.extractError(error));
26
+ throw new Error('SendGrid initialization failed. Install: npm install @sendgrid/mail');
34
27
  }
35
28
  }
36
- /**
37
- * Send a single email
38
- */ async sendEmail(options) {
39
- if (!this.sgMail) {
40
- return {
41
- success: false,
42
- error: 'SendGrid provider not initialized'
43
- };
44
- }
29
+ async sendEmail(options) {
30
+ if (!this.sgMail) return {
31
+ success: false,
32
+ error: 'SendGrid provider not initialized'
33
+ };
45
34
  try {
46
- const msg = {
47
- to: Array.isArray(options.to) ? options.to : [
48
- options.to
49
- ],
50
- from: options.fromName ? {
51
- email: options.from,
52
- name: options.fromName
53
- } : options.from,
54
- subject: options.subject
55
- };
56
- // Only include html/text if they have values (required for plain text templates)
57
- if (options.html) msg.html = options.html;
58
- if (options.text) msg.text = options.text;
59
- // Optional fields
60
- if (options.cc) {
61
- msg.cc = Array.isArray(options.cc) ? options.cc : [
62
- options.cc
63
- ];
64
- }
65
- if (options.bcc) {
66
- msg.bcc = Array.isArray(options.bcc) ? options.bcc : [
67
- options.bcc
68
- ];
69
- }
70
- if (options.replyTo) msg.replyTo = options.replyTo;
71
- if (options.attachments?.length) {
72
- msg.attachments = options.attachments.map((a)=>({
73
- filename: a.filename,
74
- content: typeof a.content === 'string' ? a.content : a.content.toString('base64'),
75
- type: a.contentType,
76
- disposition: 'attachment'
77
- }));
78
- }
35
+ const msg = this.buildMessage(options, true);
79
36
  const [response] = await this.sgMail.send(msg);
80
37
  const messageId = response.headers['x-message-id'] || response.headers['X-Message-Id'];
81
38
  this.logger.log(`Email sent via SendGrid: ${messageId}`);
@@ -84,67 +41,82 @@ import { Logger } from '@nestjs/common';
84
41
  messageId
85
42
  };
86
43
  } catch (error) {
87
- this.logger.error('SendGrid send failed:', error);
88
- const errorMessage = error.response?.body?.errors?.[0]?.message || error.message;
89
- return {
90
- success: false,
91
- error: errorMessage
92
- };
44
+ return this.handleError('SendGrid send failed', error);
93
45
  }
94
46
  }
95
- /**
96
- * Send multiple emails (batch)
97
- */ async sendBulkEmails(options) {
98
- if (!this.sgMail) {
99
- return options.map(()=>({
100
- success: false,
101
- error: 'SendGrid provider not initialized'
102
- }));
103
- }
104
- // SendGrid supports batch sending with sendMultiple
105
- const messages = options.map((opt)=>{
106
- const msg = {
107
- to: Array.isArray(opt.to) ? opt.to : [
108
- opt.to
109
- ],
110
- from: opt.fromName ? {
111
- email: opt.from,
112
- name: opt.fromName
113
- } : opt.from,
114
- subject: opt.subject
115
- };
116
- if (opt.html) msg.html = opt.html;
117
- if (opt.text) msg.text = opt.text;
118
- return msg;
119
- });
47
+ async sendBulkEmails(options) {
48
+ if (!this.sgMail) return options.map(()=>({
49
+ success: false,
50
+ error: 'SendGrid provider not initialized'
51
+ }));
120
52
  try {
53
+ const messages = options.map((opt)=>this.buildMessage(opt, false));
121
54
  await this.sgMail.send(messages);
122
55
  return options.map(()=>({
123
56
  success: true
124
57
  }));
125
58
  } catch (error) {
126
59
  this.logger.error('SendGrid bulk send failed:', error);
60
+ const errorMessage = this.extractError(error);
127
61
  return options.map(()=>({
128
62
  success: false,
129
- error: error.message
63
+ error: errorMessage
130
64
  }));
131
65
  }
132
66
  }
133
- /**
134
- * Health check for the provider
135
- */ async healthCheck() {
136
- // SendGrid doesn't have a direct health check API
137
- // We verify the API key format is valid
138
- return !!(this.sgMail && this.config?.apiKey?.startsWith('SG.'));
67
+ async healthCheck() {
68
+ return !!(this.sgMail && this.apiKey?.startsWith('SG.'));
139
69
  }
140
- /**
141
- * Cleanup (no-op for SendGrid)
142
- */ async close() {
70
+ async close() {
143
71
  this.sgMail = null;
144
72
  }
73
+ // ─── Private Helpers ────────────────────────────────────────────────────────
74
+ buildMessage(options, includeExtras) {
75
+ const msg = {
76
+ to: this.toArray(options.to),
77
+ from: options.fromName ? {
78
+ email: options.from,
79
+ name: options.fromName
80
+ } : options.from,
81
+ subject: options.subject
82
+ };
83
+ if (options.html) msg.html = options.html;
84
+ if (options.text) msg.text = options.text;
85
+ if (includeExtras) {
86
+ if (options.cc) msg.cc = this.toArray(options.cc);
87
+ if (options.bcc) msg.bcc = this.toArray(options.bcc);
88
+ if (options.replyTo) msg.replyTo = options.replyTo;
89
+ if (options.attachments?.length) {
90
+ msg.attachments = options.attachments.map((a)=>({
91
+ filename: a.filename,
92
+ content: typeof a.content === 'string' ? a.content : a.content.toString('base64'),
93
+ type: a.contentType,
94
+ disposition: 'attachment'
95
+ }));
96
+ }
97
+ }
98
+ return msg;
99
+ }
100
+ toArray(value) {
101
+ return Array.isArray(value) ? value : [
102
+ value
103
+ ];
104
+ }
105
+ extractError(error) {
106
+ const sgError = error;
107
+ return sgError.response?.body?.errors?.[0]?.message || sgError.message || 'Unknown error';
108
+ }
109
+ handleError(context, error) {
110
+ const message = this.extractError(error);
111
+ this.logger.error(`${context}: ${message}`, error);
112
+ return {
113
+ success: false,
114
+ error: message
115
+ };
116
+ }
145
117
  constructor(){
146
118
  _define_property(this, "logger", new Logger(SendGridProvider.name));
147
119
  _define_property(this, "sgMail", null);
148
- _define_property(this, "config", null);
120
+ _define_property(this, "apiKey", null);
149
121
  }
150
122
  }