@flusys/nestjs-email 1.1.0-beta
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/cjs/config/email-config.service.js +94 -0
- package/cjs/config/email.constants.js +40 -0
- package/cjs/config/index.js +19 -0
- package/cjs/controllers/email-config.controller.js +59 -0
- package/cjs/controllers/email-send.controller.js +142 -0
- package/cjs/controllers/email-template.controller.js +84 -0
- package/cjs/controllers/index.js +20 -0
- package/cjs/docs/email-swagger.config.js +176 -0
- package/cjs/docs/index.js +11 -0
- package/cjs/dtos/email-config.dto.js +238 -0
- package/cjs/dtos/email-send.dto.js +376 -0
- package/cjs/dtos/email-template.dto.js +283 -0
- package/cjs/dtos/index.js +20 -0
- package/cjs/entities/email-config-base.entity.js +111 -0
- package/cjs/entities/email-config-with-company.entity.js +63 -0
- package/cjs/entities/email-config.entity.js +25 -0
- package/cjs/entities/email-template-base.entity.js +134 -0
- package/cjs/entities/email-template-with-company.entity.js +63 -0
- package/cjs/entities/email-template.entity.js +25 -0
- package/cjs/entities/index.js +41 -0
- package/cjs/enums/email-provider-type.enum.js +18 -0
- package/cjs/enums/index.js +18 -0
- package/cjs/index.js +27 -0
- package/cjs/interfaces/email-config.interface.js +4 -0
- package/cjs/interfaces/email-module-options.interface.js +4 -0
- package/cjs/interfaces/email-provider.interface.js +6 -0
- package/cjs/interfaces/email-template.interface.js +4 -0
- package/cjs/interfaces/index.js +21 -0
- package/cjs/modules/email.module.js +177 -0
- package/cjs/modules/index.js +18 -0
- package/cjs/providers/email-factory.service.js +160 -0
- package/cjs/providers/email-provider.registry.js +51 -0
- package/cjs/providers/index.js +22 -0
- package/cjs/providers/mailgun-provider.js +125 -0
- package/cjs/providers/sendgrid-provider.js +156 -0
- package/cjs/providers/smtp-provider.js +185 -0
- package/cjs/services/email-datasource.provider.js +215 -0
- package/cjs/services/email-provider-config.service.js +180 -0
- package/cjs/services/email-send.service.js +228 -0
- package/cjs/services/email-template.service.js +186 -0
- package/cjs/services/index.js +21 -0
- package/config/email-config.service.d.ts +13 -0
- package/config/email.constants.d.ts +11 -0
- package/config/index.d.ts +2 -0
- package/controllers/email-config.controller.d.ts +17 -0
- package/controllers/email-send.controller.d.ts +19 -0
- package/controllers/email-template.controller.d.ts +25 -0
- package/controllers/index.d.ts +3 -0
- package/docs/email-swagger.config.d.ts +3 -0
- package/docs/index.d.ts +1 -0
- package/dtos/email-config.dto.d.ts +33 -0
- package/dtos/email-send.dto.d.ts +45 -0
- package/dtos/email-template.dto.d.ts +42 -0
- package/dtos/index.d.ts +3 -0
- package/entities/email-config-base.entity.d.ts +11 -0
- package/entities/email-config-with-company.entity.d.ts +4 -0
- package/entities/email-config.entity.d.ts +3 -0
- package/entities/email-template-base.entity.d.ts +14 -0
- package/entities/email-template-with-company.entity.d.ts +4 -0
- package/entities/email-template.entity.d.ts +3 -0
- package/entities/index.d.ts +7 -0
- package/enums/email-provider-type.enum.d.ts +5 -0
- package/enums/index.d.ts +1 -0
- package/fesm/config/email-config.service.js +84 -0
- package/fesm/config/email.constants.js +13 -0
- package/fesm/config/index.js +2 -0
- package/fesm/controllers/email-config.controller.js +49 -0
- package/fesm/controllers/email-send.controller.js +132 -0
- package/fesm/controllers/email-template.controller.js +74 -0
- package/fesm/controllers/index.js +3 -0
- package/fesm/docs/email-swagger.config.js +172 -0
- package/fesm/docs/index.js +1 -0
- package/fesm/dtos/email-config.dto.js +223 -0
- package/fesm/dtos/email-send.dto.js +360 -0
- package/fesm/dtos/email-template.dto.js +268 -0
- package/fesm/dtos/index.js +3 -0
- package/fesm/entities/email-config-base.entity.js +101 -0
- package/fesm/entities/email-config-with-company.entity.js +53 -0
- package/fesm/entities/email-config.entity.js +15 -0
- package/fesm/entities/email-template-base.entity.js +124 -0
- package/fesm/entities/email-template-with-company.entity.js +53 -0
- package/fesm/entities/email-template.entity.js +15 -0
- package/fesm/entities/index.js +20 -0
- package/fesm/enums/email-provider-type.enum.js +8 -0
- package/fesm/enums/index.js +1 -0
- package/fesm/index.js +10 -0
- package/fesm/interfaces/email-config.interface.js +3 -0
- package/fesm/interfaces/email-module-options.interface.js +3 -0
- package/fesm/interfaces/email-provider.interface.js +5 -0
- package/fesm/interfaces/email-template.interface.js +3 -0
- package/fesm/interfaces/index.js +4 -0
- package/fesm/modules/email.module.js +167 -0
- package/fesm/modules/index.js +1 -0
- package/fesm/providers/email-factory.service.js +109 -0
- package/fesm/providers/email-provider.registry.js +44 -0
- package/fesm/providers/index.js +5 -0
- package/fesm/providers/mailgun-provider.js +119 -0
- package/fesm/providers/sendgrid-provider.js +150 -0
- package/fesm/providers/smtp-provider.js +137 -0
- package/fesm/services/email-datasource.provider.js +164 -0
- package/fesm/services/email-provider-config.service.js +170 -0
- package/fesm/services/email-send.service.js +218 -0
- package/fesm/services/email-template.service.js +176 -0
- package/fesm/services/index.js +4 -0
- package/index.d.ts +9 -0
- package/interfaces/email-config.interface.d.ts +28 -0
- package/interfaces/email-module-options.interface.d.ts +26 -0
- package/interfaces/email-provider.interface.d.ts +34 -0
- package/interfaces/email-template.interface.d.ts +64 -0
- package/interfaces/index.d.ts +4 -0
- package/modules/email.module.d.ts +9 -0
- package/modules/index.d.ts +1 -0
- package/package.json +105 -0
- package/providers/email-factory.service.d.ts +14 -0
- package/providers/email-provider.registry.d.ts +10 -0
- package/providers/index.d.ts +5 -0
- package/providers/mailgun-provider.d.ts +11 -0
- package/providers/sendgrid-provider.d.ts +11 -0
- package/providers/smtp-provider.d.ts +11 -0
- package/services/email-datasource.provider.d.ts +25 -0
- package/services/email-provider-config.service.d.ts +32 -0
- package/services/email-send.service.d.ts +20 -0
- package/services/email-template.service.d.ts +31 -0
- package/services/index.d.ts +4 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
15
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
16
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
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
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
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';
|
|
31
|
+
import { EmailProviderRegistry } from './email-provider.registry';
|
|
32
|
+
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
|
|
49
|
+
const ProviderClass = EmailProviderRegistry.get(config.provider);
|
|
50
|
+
if (!ProviderClass) {
|
|
51
|
+
throw new NotFoundException(`Email provider '${config.provider}' is not registered. ` + `Available providers: ${EmailProviderRegistry.getAll().join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
// Create new instance
|
|
55
|
+
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})`);
|
|
63
|
+
return instance;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
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'}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
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() {
|
|
82
|
+
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
|
+
}
|
|
89
|
+
await Promise.allSettled(closePromises);
|
|
90
|
+
this.providerCache.clear();
|
|
91
|
+
this.logger.log('Email provider cleanup complete');
|
|
92
|
+
}
|
|
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();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
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
|
+
])
|
|
109
|
+
], EmailFactoryService);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
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) {
|
|
21
|
+
this.providers.set(providerName.toLowerCase(), providerClass);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get a registered provider class
|
|
25
|
+
*/ static get(providerName) {
|
|
26
|
+
return this.providers.get(providerName.toLowerCase());
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if a provider is registered
|
|
30
|
+
*/ static has(providerName) {
|
|
31
|
+
return this.providers.has(providerName.toLowerCase());
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get all registered provider names
|
|
35
|
+
*/ static getAll() {
|
|
36
|
+
return Array.from(this.providers.keys());
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Clear all providers (useful for testing)
|
|
40
|
+
*/ static clear() {
|
|
41
|
+
this.providers.clear();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
_define_property(EmailProviderRegistry, "providers", new Map());
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
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;
|
|
24
|
+
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
|
|
27
|
+
const Mailgun = (await new Function('return import("mailgun.js")')()).default;
|
|
28
|
+
const FormData = (await new Function('return import("form-data")')()).default;
|
|
29
|
+
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';
|
|
32
|
+
this.client = mailgun.client({
|
|
33
|
+
username: 'api',
|
|
34
|
+
key: config.apiKey,
|
|
35
|
+
url
|
|
36
|
+
});
|
|
37
|
+
this.logger.log(`Mailgun Provider initialized for domain: ${config.domain}`);
|
|
38
|
+
} 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');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Send a single email
|
|
45
|
+
*/ async sendEmail(options) {
|
|
46
|
+
if (!this.client || !this.config) {
|
|
47
|
+
return {
|
|
48
|
+
success: false,
|
|
49
|
+
error: 'Mailgun provider not initialized'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
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);
|
|
77
|
+
this.logger.log(`Email sent via Mailgun: ${result.id}`);
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
messageId: result.id
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.logger.error('Mailgun send failed:', error);
|
|
84
|
+
return {
|
|
85
|
+
success: false,
|
|
86
|
+
error: error.message
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
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
|
|
95
|
+
return Promise.all(options.map((opt)=>this.sendEmail(opt)));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Health check for the provider
|
|
99
|
+
*/ async healthCheck() {
|
|
100
|
+
if (!this.client || !this.config) return false;
|
|
101
|
+
try {
|
|
102
|
+
// Verify domain exists
|
|
103
|
+
await this.client.domains.get(this.config.domain);
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Cleanup
|
|
111
|
+
*/ async close() {
|
|
112
|
+
this.client = null;
|
|
113
|
+
}
|
|
114
|
+
constructor(){
|
|
115
|
+
_define_property(this, "logger", new Logger(MailgunProvider.name));
|
|
116
|
+
_define_property(this, "client", null);
|
|
117
|
+
_define_property(this, "config", null);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
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;
|
|
24
|
+
try {
|
|
25
|
+
// Dynamic import @sendgrid/mail - use eval to bypass TypeScript module resolution
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
27
|
+
const sgMailModule = await new Function('return import("@sendgrid/mail")')();
|
|
28
|
+
this.sgMail = sgMailModule.default || sgMailModule;
|
|
29
|
+
this.sgMail.setApiKey(config.apiKey);
|
|
30
|
+
this.logger.log('SendGrid Provider initialized');
|
|
31
|
+
} 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');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
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
|
+
}
|
|
45
|
+
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
|
+
}
|
|
79
|
+
const [response] = await this.sgMail.send(msg);
|
|
80
|
+
const messageId = response.headers['x-message-id'] || response.headers['X-Message-Id'];
|
|
81
|
+
this.logger.log(`Email sent via SendGrid: ${messageId}`);
|
|
82
|
+
return {
|
|
83
|
+
success: true,
|
|
84
|
+
messageId
|
|
85
|
+
};
|
|
86
|
+
} 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
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
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
|
+
});
|
|
120
|
+
try {
|
|
121
|
+
await this.sgMail.send(messages);
|
|
122
|
+
return options.map(()=>({
|
|
123
|
+
success: true
|
|
124
|
+
}));
|
|
125
|
+
} catch (error) {
|
|
126
|
+
this.logger.error('SendGrid bulk send failed:', error);
|
|
127
|
+
return options.map(()=>({
|
|
128
|
+
success: false,
|
|
129
|
+
error: error.message
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
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.'));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Cleanup (no-op for SendGrid)
|
|
142
|
+
*/ async close() {
|
|
143
|
+
this.sgMail = null;
|
|
144
|
+
}
|
|
145
|
+
constructor(){
|
|
146
|
+
_define_property(this, "logger", new Logger(SendGridProvider.name));
|
|
147
|
+
_define_property(this, "sgMail", null);
|
|
148
|
+
_define_property(this, "config", null);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
function _define_property(obj, key, value) {
|
|
2
|
+
if (key in obj) {
|
|
3
|
+
Object.defineProperty(obj, key, {
|
|
4
|
+
value: value,
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true
|
|
8
|
+
});
|
|
9
|
+
} else {
|
|
10
|
+
obj[key] = value;
|
|
11
|
+
}
|
|
12
|
+
return obj;
|
|
13
|
+
}
|
|
14
|
+
import { Logger } from '@nestjs/common';
|
|
15
|
+
/**
|
|
16
|
+
* SMTP Email Provider using nodemailer
|
|
17
|
+
* Default provider - always available
|
|
18
|
+
*/ export class SmtpProvider {
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the SMTP provider with configuration
|
|
21
|
+
*/ async initialize(config) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.logger.log(`Initializing SMTP: host=${config.host}, port=${config.port}, secure=${config.secure}, user=${config.auth?.user}`);
|
|
24
|
+
// Dynamic import nodemailer
|
|
25
|
+
const nodemailer = await import('nodemailer');
|
|
26
|
+
// Determine secure mode: port 465 = implicit TLS, port 587 = STARTTLS
|
|
27
|
+
const isSecure = config.secure ?? config.port === 465;
|
|
28
|
+
this.transporter = nodemailer.createTransport({
|
|
29
|
+
host: config.host,
|
|
30
|
+
port: config.port,
|
|
31
|
+
secure: isSecure,
|
|
32
|
+
auth: config.auth,
|
|
33
|
+
// Timeout settings to prevent hanging
|
|
34
|
+
connectionTimeout: 10000,
|
|
35
|
+
greetingTimeout: 10000,
|
|
36
|
+
socketTimeout: 30000,
|
|
37
|
+
// TLS options for better compatibility
|
|
38
|
+
tls: {
|
|
39
|
+
rejectUnauthorized: false,
|
|
40
|
+
minVersion: 'TLSv1.2'
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
this.logger.log(`SMTP transporter created with secure=${isSecure}`);
|
|
44
|
+
// Verify connection with timeout
|
|
45
|
+
try {
|
|
46
|
+
const verifyPromise = this.transporter.verify();
|
|
47
|
+
const timeoutPromise = new Promise((_, reject)=>setTimeout(()=>reject(new Error('SMTP verification timeout')), 10000));
|
|
48
|
+
await Promise.race([
|
|
49
|
+
verifyPromise,
|
|
50
|
+
timeoutPromise
|
|
51
|
+
]);
|
|
52
|
+
this.logger.log(`SMTP Provider verified successfully: ${config.host}:${config.port}`);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this.logger.warn(`SMTP verification failed: ${error.message}`);
|
|
55
|
+
// Don't throw - allow initialization even if verification fails
|
|
56
|
+
// Connection will be retried on send
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Send a single email
|
|
61
|
+
*/ async sendEmail(options) {
|
|
62
|
+
if (!this.transporter) {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
error: 'SMTP provider not initialized'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const toAddress = Array.isArray(options.to) ? options.to.join(', ') : options.to;
|
|
69
|
+
this.logger.log(`Sending email to: ${toAddress}, subject: ${options.subject}`);
|
|
70
|
+
try {
|
|
71
|
+
const mailOptions = {
|
|
72
|
+
from: options.fromName ? `"${options.fromName}" <${options.from}>` : options.from,
|
|
73
|
+
to: toAddress,
|
|
74
|
+
cc: options.cc ? Array.isArray(options.cc) ? options.cc.join(', ') : options.cc : undefined,
|
|
75
|
+
bcc: options.bcc ? Array.isArray(options.bcc) ? options.bcc.join(', ') : options.bcc : undefined,
|
|
76
|
+
subject: options.subject,
|
|
77
|
+
text: options.text,
|
|
78
|
+
html: options.html,
|
|
79
|
+
replyTo: options.replyTo,
|
|
80
|
+
attachments: options.attachments?.map((a)=>({
|
|
81
|
+
filename: a.filename,
|
|
82
|
+
content: a.content,
|
|
83
|
+
contentType: a.contentType,
|
|
84
|
+
encoding: a.encoding
|
|
85
|
+
}))
|
|
86
|
+
};
|
|
87
|
+
this.logger.log(`Mail options: from=${mailOptions.from}, to=${mailOptions.to}`);
|
|
88
|
+
// Send with timeout
|
|
89
|
+
const sendPromise = this.transporter.sendMail(mailOptions);
|
|
90
|
+
const timeoutPromise = new Promise((_, reject)=>setTimeout(()=>reject(new Error('SMTP send timeout after 30 seconds')), 30000));
|
|
91
|
+
const info = await Promise.race([
|
|
92
|
+
sendPromise,
|
|
93
|
+
timeoutPromise
|
|
94
|
+
]);
|
|
95
|
+
this.logger.log(`Email sent via SMTP: ${info.messageId}`);
|
|
96
|
+
return {
|
|
97
|
+
success: true,
|
|
98
|
+
messageId: info.messageId
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.logger.error(`SMTP send failed: ${error.message}`, error.stack);
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
error: error.message
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Send multiple emails (batch)
|
|
110
|
+
*/ async sendBulkEmails(options) {
|
|
111
|
+
return Promise.all(options.map((opt)=>this.sendEmail(opt)));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Health check for the provider
|
|
115
|
+
*/ async healthCheck() {
|
|
116
|
+
if (!this.transporter) return false;
|
|
117
|
+
try {
|
|
118
|
+
await this.transporter.verify();
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Close the transporter
|
|
126
|
+
*/ async close() {
|
|
127
|
+
if (this.transporter) {
|
|
128
|
+
this.transporter.close();
|
|
129
|
+
this.transporter = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
constructor(){
|
|
133
|
+
_define_property(this, "logger", new Logger(SmtpProvider.name));
|
|
134
|
+
_define_property(this, "transporter", null);
|
|
135
|
+
_define_property(this, "config", null);
|
|
136
|
+
}
|
|
137
|
+
}
|