@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.
- package/README.md +589 -0
- package/cjs/config/email.constants.js +0 -18
- package/cjs/config/index.js +0 -1
- package/cjs/controllers/email-config.controller.js +46 -4
- package/cjs/controllers/email-send.controller.js +13 -26
- package/cjs/controllers/email-template.controller.js +60 -11
- package/cjs/docs/email-swagger.config.js +18 -80
- package/cjs/dtos/email-config.dto.js +6 -106
- package/cjs/dtos/email-send.dto.js +101 -123
- package/cjs/dtos/email-template.dto.js +41 -103
- package/cjs/entities/email-config-with-company.entity.js +2 -2
- package/cjs/entities/email-config.entity.js +92 -3
- package/cjs/entities/email-template-with-company.entity.js +5 -3
- package/cjs/entities/email-template.entity.js +119 -3
- package/cjs/entities/index.js +34 -19
- package/cjs/index.js +1 -0
- package/cjs/interfaces/email-provider.interface.js +1 -3
- package/cjs/modules/email.module.js +50 -104
- package/cjs/providers/email-factory.service.js +37 -109
- package/cjs/providers/email-provider.registry.js +5 -15
- package/cjs/providers/mailgun-provider.js +54 -58
- package/cjs/providers/sendgrid-provider.js +68 -92
- package/cjs/providers/smtp-provider.js +58 -69
- package/cjs/{config → services}/email-config.service.js +9 -32
- package/cjs/services/email-datasource.provider.js +17 -104
- package/cjs/services/email-provider-config.service.js +28 -58
- package/cjs/services/email-send.service.js +120 -125
- package/cjs/services/email-template.service.js +62 -85
- package/cjs/services/index.js +2 -1
- package/cjs/utils/email-templates.util.js +64 -0
- package/cjs/utils/index.js +18 -0
- package/config/email.constants.d.ts +0 -9
- package/config/index.d.ts +0 -1
- package/controllers/email-send.controller.d.ts +5 -12
- package/controllers/email-template.controller.d.ts +5 -7
- package/dtos/email-config.dto.d.ts +5 -13
- package/dtos/email-send.dto.d.ts +17 -21
- package/dtos/email-template.dto.d.ts +5 -16
- package/entities/email-config-with-company.entity.d.ts +2 -2
- package/entities/email-config.entity.d.ts +10 -2
- package/entities/email-template-with-company.entity.d.ts +2 -2
- package/entities/email-template.entity.d.ts +13 -2
- package/entities/index.d.ts +9 -3
- package/fesm/config/email.constants.js +0 -9
- package/fesm/config/index.js +0 -1
- package/fesm/controllers/email-config.controller.js +49 -7
- package/fesm/controllers/email-send.controller.js +13 -26
- package/fesm/controllers/email-template.controller.js +61 -12
- package/fesm/docs/email-swagger.config.js +21 -86
- package/fesm/dtos/email-config.dto.js +9 -115
- package/fesm/dtos/email-send.dto.js +103 -139
- package/fesm/dtos/email-template.dto.js +43 -111
- package/fesm/entities/email-config-with-company.entity.js +2 -2
- package/fesm/entities/email-config.entity.js +93 -4
- package/fesm/entities/email-template-with-company.entity.js +5 -3
- package/fesm/entities/email-template.entity.js +120 -4
- package/fesm/entities/index.js +22 -16
- package/fesm/index.js +1 -0
- package/fesm/interfaces/email-config.interface.js +1 -3
- package/fesm/interfaces/email-module-options.interface.js +1 -3
- package/fesm/interfaces/email-provider.interface.js +1 -5
- package/fesm/interfaces/email-template.interface.js +1 -3
- package/fesm/modules/email.module.js +52 -106
- package/fesm/providers/email-factory.service.js +38 -69
- package/fesm/providers/email-provider.registry.js +6 -19
- package/fesm/providers/mailgun-provider.js +55 -63
- package/fesm/providers/sendgrid-provider.js +69 -97
- package/fesm/providers/smtp-provider.js +59 -73
- package/fesm/{config → services}/email-config.service.js +9 -32
- package/fesm/services/email-datasource.provider.js +18 -64
- package/fesm/services/email-provider-config.service.js +26 -56
- package/fesm/services/email-send.service.js +118 -123
- package/fesm/services/email-template.service.js +60 -83
- package/fesm/services/index.js +2 -1
- package/fesm/utils/email-templates.util.js +47 -0
- package/fesm/utils/index.js +1 -0
- package/index.d.ts +1 -0
- package/interfaces/email-config.interface.d.ts +6 -0
- package/interfaces/email-module-options.interface.d.ts +0 -5
- package/modules/email.module.d.ts +1 -2
- package/package.json +4 -4
- package/providers/email-factory.service.d.ts +4 -7
- package/providers/mailgun-provider.d.ts +6 -2
- package/providers/sendgrid-provider.d.ts +6 -2
- package/providers/smtp-provider.d.ts +7 -2
- package/services/email-config.service.d.ts +12 -0
- package/services/email-datasource.provider.d.ts +3 -6
- package/services/email-provider-config.service.d.ts +3 -3
- package/services/email-send.service.d.ts +11 -3
- package/services/email-template.service.d.ts +5 -4
- package/services/index.d.ts +2 -1
- package/utils/email-templates.util.d.ts +2 -0
- package/utils/index.d.ts +1 -0
- package/cjs/entities/email-config-base.entity.js +0 -111
- package/cjs/entities/email-template-base.entity.js +0 -134
- package/config/email-config.service.d.ts +0 -13
- package/entities/email-config-base.entity.d.ts +0 -11
- package/entities/email-template-base.entity.d.ts +0 -14
- package/fesm/entities/email-config-base.entity.js +0 -101
- 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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
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.
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
41
|
+
throw new InternalServerErrorException(`Failed to initialize email provider '${config.provider}': ${this.extractErrorMessage(error)}`);
|
|
67
42
|
}
|
|
68
43
|
}
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
40
|
-
throw new Error('Mailgun initialization failed.
|
|
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
|
-
|
|
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
|
|
54
|
-
const
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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, "
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
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
|
|
33
|
-
throw new Error('SendGrid initialization failed.
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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.
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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:
|
|
63
|
+
error: errorMessage
|
|
130
64
|
}));
|
|
131
65
|
}
|
|
132
66
|
}
|
|
133
|
-
|
|
134
|
-
|
|
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, "
|
|
120
|
+
_define_property(this, "apiKey", null);
|
|
149
121
|
}
|
|
150
122
|
}
|