@flusys/nestjs-email 1.0.0-rc
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 +552 -0
- package/cjs/config/email-config.service.js +81 -0
- package/cjs/config/email.constants.js +22 -0
- package/cjs/config/index.js +19 -0
- package/cjs/controllers/email-config.controller.js +101 -0
- package/cjs/controllers/email-send.controller.js +142 -0
- package/cjs/controllers/email-template.controller.js +128 -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 +444 -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 +133 -0
- package/cjs/entities/email-template-with-company.entity.js +65 -0
- package/cjs/entities/email-template.entity.js +30 -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 +28 -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 +4 -0
- package/cjs/interfaces/email-template.interface.js +4 -0
- package/cjs/interfaces/index.js +21 -0
- package/cjs/modules/email.module.js +161 -0
- package/cjs/modules/index.js +18 -0
- package/cjs/providers/email-factory.service.js +144 -0
- package/cjs/providers/email-provider.registry.js +41 -0
- package/cjs/providers/index.js +22 -0
- package/cjs/providers/mailgun-provider.js +107 -0
- package/cjs/providers/sendgrid-provider.js +135 -0
- package/cjs/providers/smtp-provider.js +166 -0
- package/cjs/services/email-datasource.provider.js +187 -0
- package/cjs/services/email-provider-config.service.js +150 -0
- package/cjs/services/email-send.service.js +211 -0
- package/cjs/services/email-template.service.js +158 -0
- package/cjs/services/index.js +21 -0
- package/cjs/utils/email-templates.util.js +129 -0
- package/cjs/utils/index.js +18 -0
- package/config/email-config.service.d.ts +16 -0
- package/config/email.constants.d.ts +2 -0
- package/config/index.d.ts +2 -0
- package/controllers/email-config.controller.d.ts +17 -0
- package/controllers/email-send.controller.d.ts +11 -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 +30 -0
- package/dtos/email-send.dto.d.ts +46 -0
- package/dtos/email-template.dto.d.ts +39 -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 +71 -0
- package/fesm/config/email.constants.js +4 -0
- package/fesm/config/index.js +2 -0
- package/fesm/controllers/email-config.controller.js +91 -0
- package/fesm/controllers/email-send.controller.js +132 -0
- package/fesm/controllers/email-template.controller.js +118 -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 +217 -0
- package/fesm/dtos/email-send.dto.js +414 -0
- package/fesm/dtos/email-template.dto.js +262 -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 +123 -0
- package/fesm/entities/email-template-with-company.entity.js +55 -0
- package/fesm/entities/email-template.entity.js +20 -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 +11 -0
- package/fesm/interfaces/email-config.interface.js +1 -0
- package/fesm/interfaces/email-module-options.interface.js +1 -0
- package/fesm/interfaces/email-provider.interface.js +1 -0
- package/fesm/interfaces/email-template.interface.js +1 -0
- package/fesm/interfaces/index.js +4 -0
- package/fesm/modules/email.module.js +151 -0
- package/fesm/modules/index.js +1 -0
- package/fesm/providers/email-factory.service.js +93 -0
- package/fesm/providers/email-provider.registry.js +31 -0
- package/fesm/providers/index.js +5 -0
- package/fesm/providers/mailgun-provider.js +97 -0
- package/fesm/providers/sendgrid-provider.js +125 -0
- package/fesm/providers/smtp-provider.js +115 -0
- package/fesm/services/email-datasource.provider.js +136 -0
- package/fesm/services/email-provider-config.service.js +140 -0
- package/fesm/services/email-send.service.js +201 -0
- package/fesm/services/email-template.service.js +148 -0
- package/fesm/services/index.js +4 -0
- package/fesm/utils/email-templates.util.js +111 -0
- package/fesm/utils/index.js +1 -0
- package/index.d.ts +10 -0
- package/interfaces/email-config.interface.d.ts +34 -0
- package/interfaces/email-module-options.interface.d.ts +25 -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
- package/utils/email-templates.util.d.ts +2 -0
- package/utils/index.d.ts +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
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, InternalServerErrorException, 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
|
+
generateCacheKey(config) {
|
|
34
|
+
const configString = JSON.stringify(config.config, Object.keys(config.config || {}).sort());
|
|
35
|
+
const configHash = crypto.createHash('sha256').update(configString).digest('hex').substring(0, 16);
|
|
36
|
+
return `${config.provider}-${configHash}`;
|
|
37
|
+
}
|
|
38
|
+
async createProvider(config) {
|
|
39
|
+
const providerKey = this.generateCacheKey(config);
|
|
40
|
+
if (this.providerCache.has(providerKey)) return this.providerCache.get(providerKey);
|
|
41
|
+
const ProviderClass = EmailProviderRegistry.get(config.provider);
|
|
42
|
+
if (!ProviderClass) {
|
|
43
|
+
throw new NotFoundException(`Email provider '${config.provider}' is not registered. Available: ${EmailProviderRegistry.getAll().join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const instance = new ProviderClass();
|
|
47
|
+
if ('initialize' in instance && typeof instance.initialize === 'function') {
|
|
48
|
+
await instance.initialize(config.config);
|
|
49
|
+
}
|
|
50
|
+
this.providerCache.set(providerKey, instance);
|
|
51
|
+
this.logger.log(`Created email provider: ${config.provider} (key: ${providerKey})`);
|
|
52
|
+
return instance;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this.logger.error(`Failed to create provider ${config.provider}:`, error);
|
|
55
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
56
|
+
throw new InternalServerErrorException(`Failed to initialize email provider '${config.provider}': ${errorMessage}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
isProviderAvailable(providerName) {
|
|
60
|
+
return EmailProviderRegistry.has(providerName);
|
|
61
|
+
}
|
|
62
|
+
getAvailableProviders() {
|
|
63
|
+
return EmailProviderRegistry.getAll();
|
|
64
|
+
}
|
|
65
|
+
async onModuleDestroy() {
|
|
66
|
+
this.logger.log('Cleaning up email provider connections...');
|
|
67
|
+
const closePromises = [];
|
|
68
|
+
for (const [key, provider] of this.providerCache.entries()){
|
|
69
|
+
if ('close' in provider && typeof provider.close === 'function') {
|
|
70
|
+
closePromises.push(provider.close().then(()=>this.logger.debug(`Closed provider: ${key}`)).catch((err)=>this.logger.warn(`Failed to close provider ${key}: ${err.message}`)));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
await Promise.allSettled(closePromises);
|
|
74
|
+
this.providerCache.clear();
|
|
75
|
+
this.logger.log('Email provider cleanup complete');
|
|
76
|
+
}
|
|
77
|
+
constructor(emailConfigService){
|
|
78
|
+
_define_property(this, "emailConfigService", void 0);
|
|
79
|
+
_define_property(this, "logger", void 0);
|
|
80
|
+
_define_property(this, "providerCache", void 0);
|
|
81
|
+
this.emailConfigService = emailConfigService;
|
|
82
|
+
this.logger = new Logger(EmailFactoryService.name);
|
|
83
|
+
this.providerCache = new Map();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
EmailFactoryService = _ts_decorate([
|
|
87
|
+
Injectable(),
|
|
88
|
+
_ts_param(0, Inject(EmailConfigService)),
|
|
89
|
+
_ts_metadata("design:type", Function),
|
|
90
|
+
_ts_metadata("design:paramtypes", [
|
|
91
|
+
typeof EmailConfigService === "undefined" ? Object : EmailConfigService
|
|
92
|
+
])
|
|
93
|
+
], EmailFactoryService);
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
export class EmailProviderRegistry {
|
|
15
|
+
static register(providerName, providerClass) {
|
|
16
|
+
this.providers.set(providerName.toLowerCase(), providerClass);
|
|
17
|
+
}
|
|
18
|
+
static get(providerName) {
|
|
19
|
+
return this.providers.get(providerName.toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
static has(providerName) {
|
|
22
|
+
return this.providers.has(providerName.toLowerCase());
|
|
23
|
+
}
|
|
24
|
+
static getAll() {
|
|
25
|
+
return Array.from(this.providers.keys());
|
|
26
|
+
}
|
|
27
|
+
static clear() {
|
|
28
|
+
this.providers.clear();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
_define_property(EmailProviderRegistry, "providers", new Map());
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
export class MailgunProvider {
|
|
16
|
+
async initialize(config) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
try {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
20
|
+
const Mailgun = (await new Function('return import("mailgun.js")')()).default;
|
|
21
|
+
const FormData = (await new Function('return import("form-data")')()).default;
|
|
22
|
+
const mailgun = new Mailgun(FormData);
|
|
23
|
+
const url = config.region === 'eu' ? 'https://api.eu.mailgun.net' : 'https://api.mailgun.net';
|
|
24
|
+
this.client = mailgun.client({
|
|
25
|
+
username: 'api',
|
|
26
|
+
key: config.apiKey,
|
|
27
|
+
url
|
|
28
|
+
});
|
|
29
|
+
this.logger.log(`Mailgun Provider initialized for domain: ${config.domain}`);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
32
|
+
this.logger.error('Failed to initialize Mailgun:', errorMessage);
|
|
33
|
+
throw new Error('Mailgun initialization failed. Install: npm install mailgun.js form-data');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async sendEmail(options) {
|
|
37
|
+
if (!this.client || !this.config) return {
|
|
38
|
+
success: false,
|
|
39
|
+
error: 'Mailgun provider not initialized'
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
const to = Array.isArray(options.to) ? options.to.join(', ') : options.to;
|
|
43
|
+
const cc = options.cc ? Array.isArray(options.cc) ? options.cc.join(', ') : options.cc : undefined;
|
|
44
|
+
const bcc = options.bcc ? Array.isArray(options.bcc) ? options.bcc.join(', ') : options.bcc : undefined;
|
|
45
|
+
const messageData = {
|
|
46
|
+
from: options.fromName ? `${options.fromName} <${options.from}>` : options.from,
|
|
47
|
+
to,
|
|
48
|
+
subject: options.subject
|
|
49
|
+
};
|
|
50
|
+
if (options.html) messageData.html = options.html;
|
|
51
|
+
if (options.text) messageData.text = options.text;
|
|
52
|
+
if (cc) messageData.cc = cc;
|
|
53
|
+
if (bcc) messageData.bcc = bcc;
|
|
54
|
+
if (options.replyTo) messageData['h:Reply-To'] = options.replyTo;
|
|
55
|
+
if (options.attachments?.length) {
|
|
56
|
+
messageData.attachment = options.attachments.map((a)=>({
|
|
57
|
+
filename: a.filename,
|
|
58
|
+
data: typeof a.content === 'string' ? Buffer.from(a.content, 'base64') : a.content,
|
|
59
|
+
contentType: a.contentType
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
const result = await this.client.messages.create(this.config.domain, messageData);
|
|
63
|
+
this.logger.log(`Email sent via Mailgun: ${result.id}`);
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
messageId: result.id
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
this.logger.error('Mailgun send failed:', error);
|
|
70
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
error: errorMessage
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async sendBulkEmails(options) {
|
|
78
|
+
return Promise.all(options.map((opt)=>this.sendEmail(opt)));
|
|
79
|
+
}
|
|
80
|
+
async healthCheck() {
|
|
81
|
+
if (!this.client || !this.config) return false;
|
|
82
|
+
try {
|
|
83
|
+
await this.client.domains.get(this.config.domain);
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async close() {
|
|
90
|
+
this.client = null;
|
|
91
|
+
}
|
|
92
|
+
constructor(){
|
|
93
|
+
_define_property(this, "logger", new Logger(MailgunProvider.name));
|
|
94
|
+
_define_property(this, "client", null);
|
|
95
|
+
_define_property(this, "config", null);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
export class SendGridProvider {
|
|
16
|
+
async initialize(config) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
try {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
20
|
+
const sgMailModule = await new Function('return import("@sendgrid/mail")')();
|
|
21
|
+
this.sgMail = sgMailModule.default || sgMailModule;
|
|
22
|
+
this.sgMail.setApiKey(config.apiKey);
|
|
23
|
+
this.logger.log('SendGrid Provider initialized');
|
|
24
|
+
} catch (error) {
|
|
25
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
26
|
+
this.logger.error('Failed to initialize SendGrid:', errorMessage);
|
|
27
|
+
throw new Error('SendGrid initialization failed. Install: npm install @sendgrid/mail');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async sendEmail(options) {
|
|
31
|
+
if (!this.sgMail) return {
|
|
32
|
+
success: false,
|
|
33
|
+
error: 'SendGrid provider not initialized'
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
const msg = {
|
|
37
|
+
to: Array.isArray(options.to) ? options.to : [
|
|
38
|
+
options.to
|
|
39
|
+
],
|
|
40
|
+
from: options.fromName ? {
|
|
41
|
+
email: options.from,
|
|
42
|
+
name: options.fromName
|
|
43
|
+
} : options.from,
|
|
44
|
+
subject: options.subject
|
|
45
|
+
};
|
|
46
|
+
if (options.html) msg.html = options.html;
|
|
47
|
+
if (options.text) msg.text = options.text;
|
|
48
|
+
if (options.cc) msg.cc = Array.isArray(options.cc) ? options.cc : [
|
|
49
|
+
options.cc
|
|
50
|
+
];
|
|
51
|
+
if (options.bcc) msg.bcc = Array.isArray(options.bcc) ? options.bcc : [
|
|
52
|
+
options.bcc
|
|
53
|
+
];
|
|
54
|
+
if (options.replyTo) msg.replyTo = options.replyTo;
|
|
55
|
+
if (options.attachments?.length) {
|
|
56
|
+
msg.attachments = options.attachments.map((a)=>({
|
|
57
|
+
filename: a.filename,
|
|
58
|
+
content: typeof a.content === 'string' ? a.content : a.content.toString('base64'),
|
|
59
|
+
type: a.contentType,
|
|
60
|
+
disposition: 'attachment'
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
const [response] = await this.sgMail.send(msg);
|
|
64
|
+
const messageId = response.headers['x-message-id'] || response.headers['X-Message-Id'];
|
|
65
|
+
this.logger.log(`Email sent via SendGrid: ${messageId}`);
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
messageId
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
this.logger.error('SendGrid send failed:', error);
|
|
72
|
+
const sgError = error;
|
|
73
|
+
const errorMessage = sgError.response?.body?.errors?.[0]?.message || sgError.message || 'Unknown error';
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
error: errorMessage
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async sendBulkEmails(options) {
|
|
81
|
+
if (!this.sgMail) return options.map(()=>({
|
|
82
|
+
success: false,
|
|
83
|
+
error: 'SendGrid provider not initialized'
|
|
84
|
+
}));
|
|
85
|
+
const messages = options.map((opt)=>{
|
|
86
|
+
const msg = {
|
|
87
|
+
to: Array.isArray(opt.to) ? opt.to : [
|
|
88
|
+
opt.to
|
|
89
|
+
],
|
|
90
|
+
from: opt.fromName ? {
|
|
91
|
+
email: opt.from,
|
|
92
|
+
name: opt.fromName
|
|
93
|
+
} : opt.from,
|
|
94
|
+
subject: opt.subject
|
|
95
|
+
};
|
|
96
|
+
if (opt.html) msg.html = opt.html;
|
|
97
|
+
if (opt.text) msg.text = opt.text;
|
|
98
|
+
return msg;
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
await this.sgMail.send(messages);
|
|
102
|
+
return options.map(()=>({
|
|
103
|
+
success: true
|
|
104
|
+
}));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this.logger.error('SendGrid bulk send failed:', error);
|
|
107
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
108
|
+
return options.map(()=>({
|
|
109
|
+
success: false,
|
|
110
|
+
error: errorMessage
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async healthCheck() {
|
|
115
|
+
return !!(this.sgMail && this.config?.apiKey?.startsWith('SG.'));
|
|
116
|
+
}
|
|
117
|
+
async close() {
|
|
118
|
+
this.sgMail = null;
|
|
119
|
+
}
|
|
120
|
+
constructor(){
|
|
121
|
+
_define_property(this, "logger", new Logger(SendGridProvider.name));
|
|
122
|
+
_define_property(this, "sgMail", null);
|
|
123
|
+
_define_property(this, "config", null);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
export class SmtpProvider {
|
|
16
|
+
async initialize(config) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.logger.log(`Initializing SMTP: host=${config.host}, port=${config.port}`);
|
|
19
|
+
const nodemailer = await import('nodemailer');
|
|
20
|
+
const isSecure = config.secure ?? config.port === 465;
|
|
21
|
+
this.transporter = nodemailer.createTransport({
|
|
22
|
+
host: config.host,
|
|
23
|
+
port: config.port,
|
|
24
|
+
secure: isSecure,
|
|
25
|
+
auth: config.auth,
|
|
26
|
+
connectionTimeout: 10000,
|
|
27
|
+
greetingTimeout: 10000,
|
|
28
|
+
socketTimeout: 30000,
|
|
29
|
+
tls: {
|
|
30
|
+
// Secure default: validate certificates unless explicitly disabled
|
|
31
|
+
rejectUnauthorized: config.tls?.rejectUnauthorized ?? true,
|
|
32
|
+
minVersion: config.tls?.minVersion ?? 'TLSv1.2'
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
const verifyPromise = this.transporter.verify();
|
|
37
|
+
const timeoutPromise = new Promise((_, reject)=>setTimeout(()=>reject(new Error('SMTP verification timeout')), 10000));
|
|
38
|
+
await Promise.race([
|
|
39
|
+
verifyPromise,
|
|
40
|
+
timeoutPromise
|
|
41
|
+
]);
|
|
42
|
+
this.logger.log(`SMTP Provider verified: ${config.host}:${config.port}`);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
45
|
+
this.logger.warn(`SMTP verification failed: ${errorMessage}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async sendEmail(options) {
|
|
49
|
+
if (!this.transporter) return {
|
|
50
|
+
success: false,
|
|
51
|
+
error: 'SMTP provider not initialized'
|
|
52
|
+
};
|
|
53
|
+
const toAddress = Array.isArray(options.to) ? options.to.join(', ') : options.to;
|
|
54
|
+
try {
|
|
55
|
+
const mailOptions = {
|
|
56
|
+
from: options.fromName ? `"${options.fromName}" <${options.from}>` : options.from,
|
|
57
|
+
to: toAddress,
|
|
58
|
+
cc: options.cc ? Array.isArray(options.cc) ? options.cc.join(', ') : options.cc : undefined,
|
|
59
|
+
bcc: options.bcc ? Array.isArray(options.bcc) ? options.bcc.join(', ') : options.bcc : undefined,
|
|
60
|
+
subject: options.subject,
|
|
61
|
+
text: options.text,
|
|
62
|
+
html: options.html,
|
|
63
|
+
replyTo: options.replyTo,
|
|
64
|
+
attachments: options.attachments?.map((a)=>({
|
|
65
|
+
filename: a.filename,
|
|
66
|
+
content: a.content,
|
|
67
|
+
contentType: a.contentType,
|
|
68
|
+
encoding: a.encoding
|
|
69
|
+
}))
|
|
70
|
+
};
|
|
71
|
+
const sendPromise = this.transporter.sendMail(mailOptions);
|
|
72
|
+
const timeoutPromise = new Promise((_, reject)=>setTimeout(()=>reject(new Error('SMTP send timeout after 30 seconds')), 30000));
|
|
73
|
+
const info = await Promise.race([
|
|
74
|
+
sendPromise,
|
|
75
|
+
timeoutPromise
|
|
76
|
+
]);
|
|
77
|
+
this.logger.log(`Email sent via SMTP: ${info.messageId}`);
|
|
78
|
+
return {
|
|
79
|
+
success: true,
|
|
80
|
+
messageId: info.messageId
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
84
|
+
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
85
|
+
this.logger.error(`SMTP send failed: ${errorMessage}`, errorStack);
|
|
86
|
+
return {
|
|
87
|
+
success: false,
|
|
88
|
+
error: errorMessage
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async sendBulkEmails(options) {
|
|
93
|
+
return Promise.all(options.map((opt)=>this.sendEmail(opt)));
|
|
94
|
+
}
|
|
95
|
+
async healthCheck() {
|
|
96
|
+
if (!this.transporter) return false;
|
|
97
|
+
try {
|
|
98
|
+
await this.transporter.verify();
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async close() {
|
|
105
|
+
if (this.transporter) {
|
|
106
|
+
this.transporter.close();
|
|
107
|
+
this.transporter = null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
constructor(){
|
|
111
|
+
_define_property(this, "logger", new Logger(SmtpProvider.name));
|
|
112
|
+
_define_property(this, "transporter", null);
|
|
113
|
+
_define_property(this, "config", null);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
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 { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
|
|
29
|
+
import { Inject, Injectable, InternalServerErrorException, Logger, Optional, Scope } from '@nestjs/common';
|
|
30
|
+
import { REQUEST } from '@nestjs/core';
|
|
31
|
+
import { Request } from 'express';
|
|
32
|
+
import { EmailModuleOptions } from '../interfaces';
|
|
33
|
+
import { EMAIL_MODULE_OPTIONS } from '../config/email.constants';
|
|
34
|
+
export class EmailDataSourceProvider extends MultiTenantDataSourceService {
|
|
35
|
+
static buildParentOptions(options) {
|
|
36
|
+
return {
|
|
37
|
+
bootstrapAppConfig: options.bootstrapAppConfig,
|
|
38
|
+
defaultDatabaseConfig: options.config?.defaultDatabaseConfig,
|
|
39
|
+
tenantDefaultDatabaseConfig: options.config?.tenantDefaultDatabaseConfig,
|
|
40
|
+
tenants: options.config?.tenants
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
getEnableCompanyFeature() {
|
|
44
|
+
return this.emailOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
45
|
+
}
|
|
46
|
+
getEnableCompanyFeatureForTenant(tenant) {
|
|
47
|
+
return tenant?.enableCompanyFeature ?? this.getEnableCompanyFeature();
|
|
48
|
+
}
|
|
49
|
+
getEnableCompanyFeatureForCurrentTenant() {
|
|
50
|
+
return this.getEnableCompanyFeatureForTenant(this.getCurrentTenant() ?? undefined);
|
|
51
|
+
}
|
|
52
|
+
async getEmailEntities(enableCompanyFeature) {
|
|
53
|
+
const enable = enableCompanyFeature ?? this.getEnableCompanyFeature();
|
|
54
|
+
const { EmailConfig, EmailTemplate } = await import('../entities');
|
|
55
|
+
if (enable) {
|
|
56
|
+
const { EmailConfigWithCompany, EmailTemplateWithCompany } = await import('../entities');
|
|
57
|
+
return [
|
|
58
|
+
EmailConfigWithCompany,
|
|
59
|
+
EmailTemplateWithCompany
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
return [
|
|
63
|
+
EmailConfig,
|
|
64
|
+
EmailTemplate
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
async createDataSourceFromConfig(config) {
|
|
68
|
+
const currentTenant = this.getCurrentTenant();
|
|
69
|
+
const enableCompanyFeature = this.getEnableCompanyFeatureForTenant(currentTenant ?? undefined);
|
|
70
|
+
const entities = await this.getEmailEntities(enableCompanyFeature);
|
|
71
|
+
return super.createDataSourceFromConfig(config, entities);
|
|
72
|
+
}
|
|
73
|
+
async getSingleDataSource() {
|
|
74
|
+
if (EmailDataSourceProvider.singleDataSource?.isInitialized) {
|
|
75
|
+
return EmailDataSourceProvider.singleDataSource;
|
|
76
|
+
}
|
|
77
|
+
if (EmailDataSourceProvider.singleConnectionLock) {
|
|
78
|
+
return EmailDataSourceProvider.singleConnectionLock;
|
|
79
|
+
}
|
|
80
|
+
const config = this.getDefaultDatabaseConfig();
|
|
81
|
+
if (!config) {
|
|
82
|
+
throw new InternalServerErrorException('No database config available. Provide defaultDatabaseConfig or tenantDefaultDatabaseConfig.');
|
|
83
|
+
}
|
|
84
|
+
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
85
|
+
EmailDataSourceProvider.singleConnectionLock = connectionPromise;
|
|
86
|
+
try {
|
|
87
|
+
const dataSource = await connectionPromise;
|
|
88
|
+
EmailDataSourceProvider.singleDataSource = dataSource;
|
|
89
|
+
return dataSource;
|
|
90
|
+
} finally{
|
|
91
|
+
EmailDataSourceProvider.singleConnectionLock = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async getOrCreateTenantConnection(tenant) {
|
|
95
|
+
const existing = EmailDataSourceProvider.tenantConnections.get(tenant.id);
|
|
96
|
+
if (existing?.isInitialized) {
|
|
97
|
+
return existing;
|
|
98
|
+
}
|
|
99
|
+
const pendingConnection = EmailDataSourceProvider.connectionLocks.get(tenant.id);
|
|
100
|
+
if (pendingConnection) {
|
|
101
|
+
return pendingConnection;
|
|
102
|
+
}
|
|
103
|
+
const config = this.buildTenantDatabaseConfig(tenant);
|
|
104
|
+
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
105
|
+
EmailDataSourceProvider.connectionLocks.set(tenant.id, connectionPromise);
|
|
106
|
+
try {
|
|
107
|
+
const dataSource = await connectionPromise;
|
|
108
|
+
EmailDataSourceProvider.tenantConnections.set(tenant.id, dataSource);
|
|
109
|
+
return dataSource;
|
|
110
|
+
} finally{
|
|
111
|
+
EmailDataSourceProvider.connectionLocks.delete(tenant.id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
constructor(emailOptions, request){
|
|
115
|
+
super(EmailDataSourceProvider.buildParentOptions(emailOptions), request), _define_property(this, "emailOptions", void 0), _define_property(this, "logger", void 0), this.emailOptions = emailOptions, this.logger = new Logger(EmailDataSourceProvider.name);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
_define_property(EmailDataSourceProvider, "tenantConnections", new Map());
|
|
119
|
+
_define_property(EmailDataSourceProvider, "singleDataSource", null);
|
|
120
|
+
_define_property(EmailDataSourceProvider, "tenantsRegistry", new Map());
|
|
121
|
+
_define_property(EmailDataSourceProvider, "initialized", false);
|
|
122
|
+
_define_property(EmailDataSourceProvider, "connectionLocks", new Map());
|
|
123
|
+
_define_property(EmailDataSourceProvider, "singleConnectionLock", null);
|
|
124
|
+
EmailDataSourceProvider = _ts_decorate([
|
|
125
|
+
Injectable({
|
|
126
|
+
scope: Scope.REQUEST
|
|
127
|
+
}),
|
|
128
|
+
_ts_param(0, Inject(EMAIL_MODULE_OPTIONS)),
|
|
129
|
+
_ts_param(1, Optional()),
|
|
130
|
+
_ts_param(1, Inject(REQUEST)),
|
|
131
|
+
_ts_metadata("design:type", Function),
|
|
132
|
+
_ts_metadata("design:paramtypes", [
|
|
133
|
+
typeof EmailModuleOptions === "undefined" ? Object : EmailModuleOptions,
|
|
134
|
+
typeof Request === "undefined" ? Object : Request
|
|
135
|
+
])
|
|
136
|
+
], EmailDataSourceProvider);
|