@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,140 @@
|
|
|
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 { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
|
+
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
|
|
31
|
+
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
32
|
+
import { EmailConfigService } from '../config';
|
|
33
|
+
import { EmailConfig, EmailConfigWithCompany } from '../entities';
|
|
34
|
+
import { EmailDataSourceProvider } from './email-datasource.provider';
|
|
35
|
+
export class EmailProviderConfigService extends RequestScopedApiService {
|
|
36
|
+
resolveEntity() {
|
|
37
|
+
return this.emailConfig.isCompanyFeatureEnabled() ? EmailConfigWithCompany : EmailConfig;
|
|
38
|
+
}
|
|
39
|
+
getDataSourceProvider() {
|
|
40
|
+
return this.dataSourceProvider;
|
|
41
|
+
}
|
|
42
|
+
async convertSingleDtoToEntity(dto, user) {
|
|
43
|
+
const entity = {
|
|
44
|
+
...dto
|
|
45
|
+
};
|
|
46
|
+
if (this.emailConfig.isCompanyFeatureEnabled()) {
|
|
47
|
+
entity.companyId = user?.companyId ?? null;
|
|
48
|
+
}
|
|
49
|
+
return entity;
|
|
50
|
+
}
|
|
51
|
+
async getSelectQuery(query, _user, select) {
|
|
52
|
+
if (!select?.length) {
|
|
53
|
+
select = [
|
|
54
|
+
'id',
|
|
55
|
+
'name',
|
|
56
|
+
'provider',
|
|
57
|
+
'config',
|
|
58
|
+
'fromEmail',
|
|
59
|
+
'fromName',
|
|
60
|
+
'isActive',
|
|
61
|
+
'isDefault',
|
|
62
|
+
'createdAt',
|
|
63
|
+
'updatedAt'
|
|
64
|
+
];
|
|
65
|
+
if (this.emailConfig.isCompanyFeatureEnabled()) select.push('companyId');
|
|
66
|
+
}
|
|
67
|
+
query.select(select.map((field)=>`${this.entityName}.${field}`));
|
|
68
|
+
return {
|
|
69
|
+
query,
|
|
70
|
+
isRaw: false
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async getExtraManipulateQuery(query, filterDto, user) {
|
|
74
|
+
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
75
|
+
applyCompanyFilter(query, {
|
|
76
|
+
isCompanyFeatureEnabled: this.emailConfig.isCompanyFeatureEnabled(),
|
|
77
|
+
entityAlias: 'emailConfig'
|
|
78
|
+
}, user);
|
|
79
|
+
query.orderBy(`${this.entityName}.createdAt`, 'DESC');
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
async findByIdDirect(id) {
|
|
83
|
+
await this.ensureRepositoryInitialized();
|
|
84
|
+
return this.repository.findOne({
|
|
85
|
+
where: {
|
|
86
|
+
id
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
async getDefaultConfig(user) {
|
|
91
|
+
await this.ensureRepositoryInitialized();
|
|
92
|
+
const baseWhere = buildCompanyWhereCondition({
|
|
93
|
+
isActive: true
|
|
94
|
+
}, this.emailConfig.isCompanyFeatureEnabled(), user);
|
|
95
|
+
const defaultConfig = await this.repository.findOne({
|
|
96
|
+
where: {
|
|
97
|
+
...baseWhere,
|
|
98
|
+
isDefault: true
|
|
99
|
+
},
|
|
100
|
+
order: {
|
|
101
|
+
createdAt: 'ASC'
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return defaultConfig || this.repository.findOne({
|
|
105
|
+
where: baseWhere,
|
|
106
|
+
order: {
|
|
107
|
+
createdAt: 'ASC'
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
async getConfigByProvider(provider, user) {
|
|
112
|
+
await this.ensureRepositoryInitialized();
|
|
113
|
+
const where = buildCompanyWhereCondition({
|
|
114
|
+
provider,
|
|
115
|
+
isActive: true
|
|
116
|
+
}, this.emailConfig.isCompanyFeatureEnabled(), user);
|
|
117
|
+
return this.repository.find({
|
|
118
|
+
where
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
constructor(cacheManager, utilsService, emailConfig, dataSourceProvider){
|
|
122
|
+
super('emailConfig', null, cacheManager, utilsService, EmailProviderConfigService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "emailConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.emailConfig = emailConfig, this.dataSourceProvider = dataSourceProvider;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
EmailProviderConfigService = _ts_decorate([
|
|
126
|
+
Injectable({
|
|
127
|
+
scope: Scope.REQUEST
|
|
128
|
+
}),
|
|
129
|
+
_ts_param(0, Inject('CACHE_INSTANCE')),
|
|
130
|
+
_ts_param(1, Inject(UtilsService)),
|
|
131
|
+
_ts_param(2, Inject(EmailConfigService)),
|
|
132
|
+
_ts_param(3, Inject(EmailDataSourceProvider)),
|
|
133
|
+
_ts_metadata("design:type", Function),
|
|
134
|
+
_ts_metadata("design:paramtypes", [
|
|
135
|
+
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
136
|
+
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
137
|
+
typeof EmailConfigService === "undefined" ? Object : EmailConfigService,
|
|
138
|
+
typeof EmailDataSourceProvider === "undefined" ? Object : EmailDataSourceProvider
|
|
139
|
+
])
|
|
140
|
+
], EmailProviderConfigService);
|
|
@@ -0,0 +1,201 @@
|
|
|
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 { escapeHtmlVariables, validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
|
|
29
|
+
import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
|
|
30
|
+
import { EmailConfigService } from '../config';
|
|
31
|
+
import { EmailFactoryService } from '../providers';
|
|
32
|
+
import { EmailProviderConfigService } from './email-provider-config.service';
|
|
33
|
+
import { EmailTemplateService } from './email-template.service';
|
|
34
|
+
export class EmailSendService {
|
|
35
|
+
async getEmailProviderWithConfig(emailConfigId, user) {
|
|
36
|
+
let emailConfig;
|
|
37
|
+
if (emailConfigId) {
|
|
38
|
+
const config = await this.emailProviderConfigService.findByIdDirect(emailConfigId);
|
|
39
|
+
if (!config) throw new NotFoundException('Email configuration not found');
|
|
40
|
+
validateCompanyOwnership(config, user, this.emailConfigService.isCompanyFeatureEnabled(), 'Email configuration');
|
|
41
|
+
if (!config.isActive) throw new BadRequestException('Email configuration is inactive');
|
|
42
|
+
emailConfig = config;
|
|
43
|
+
} else {
|
|
44
|
+
const defaultConfig = await this.emailProviderConfigService.getDefaultConfig(user);
|
|
45
|
+
if (!defaultConfig) throw new NotFoundException('No default email configuration found');
|
|
46
|
+
emailConfig = defaultConfig;
|
|
47
|
+
}
|
|
48
|
+
const providerConfig = {
|
|
49
|
+
provider: emailConfig.provider,
|
|
50
|
+
config: emailConfig.config
|
|
51
|
+
};
|
|
52
|
+
const provider = await this.emailFactory.createProvider(providerConfig);
|
|
53
|
+
return {
|
|
54
|
+
provider,
|
|
55
|
+
config: emailConfig
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Interpolate template variables in content.
|
|
60
|
+
* Variable names must be valid identifiers (start with letter/underscore).
|
|
61
|
+
*
|
|
62
|
+
* @param content - The content with {{variable}} placeholders
|
|
63
|
+
* @param variables - The variables to interpolate
|
|
64
|
+
* @param isHtmlContent - Whether to HTML-escape values (prevents XSS)
|
|
65
|
+
*/ interpolateVariables(content, variables, isHtmlContent = false) {
|
|
66
|
+
if (!variables || Object.keys(variables).length === 0) return content;
|
|
67
|
+
// Escape HTML entities in variables when inserting into HTML content
|
|
68
|
+
const safeVariables = isHtmlContent ? escapeHtmlVariables(variables) : variables;
|
|
69
|
+
// Valid identifier: starts with letter or underscore, followed by word chars
|
|
70
|
+
const VALID_VARIABLE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
|
|
71
|
+
return content.replace(VALID_VARIABLE_PATTERN, (match, varName)=>{
|
|
72
|
+
if (varName in safeVariables && safeVariables[varName] !== undefined) {
|
|
73
|
+
return String(safeVariables[varName]);
|
|
74
|
+
}
|
|
75
|
+
return match;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async sendEmail(dto, user) {
|
|
79
|
+
const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
|
|
80
|
+
const result = await provider.sendEmail({
|
|
81
|
+
to: dto.to,
|
|
82
|
+
cc: dto.cc,
|
|
83
|
+
bcc: dto.bcc,
|
|
84
|
+
subject: dto.subject,
|
|
85
|
+
html: dto.html,
|
|
86
|
+
text: dto.text,
|
|
87
|
+
from: dto.from || config.fromEmail || undefined,
|
|
88
|
+
fromName: dto.fromName || config.fromName || this.emailConfigService.getDefaultFromName(),
|
|
89
|
+
replyTo: dto.replyTo,
|
|
90
|
+
attachments: dto.attachments?.map((a)=>({
|
|
91
|
+
filename: a.filename,
|
|
92
|
+
content: Buffer.from(a.content, 'base64'),
|
|
93
|
+
contentType: a.contentType
|
|
94
|
+
}))
|
|
95
|
+
});
|
|
96
|
+
this.logger.log(`Email sent to ${Array.isArray(dto.to) ? dto.to.join(', ') : dto.to}: ${result.success ? 'SUCCESS' : 'FAILED'}`);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
async sendTemplateEmail(dto, user) {
|
|
100
|
+
let template;
|
|
101
|
+
if (dto.templateId) {
|
|
102
|
+
template = await this.emailTemplateService.findByIdDirect(dto.templateId);
|
|
103
|
+
} else if (dto.templateSlug) {
|
|
104
|
+
template = await this.emailTemplateService.findBySlug(dto.templateSlug, user);
|
|
105
|
+
} else {
|
|
106
|
+
throw new BadRequestException('templateId or templateSlug is required');
|
|
107
|
+
}
|
|
108
|
+
if (!template) throw new NotFoundException('Email template not found');
|
|
109
|
+
if (!template.isActive) throw new BadRequestException('Email template is inactive');
|
|
110
|
+
validateCompanyOwnership(template, user, this.emailConfigService.isCompanyFeatureEnabled(), 'Email template');
|
|
111
|
+
// Subject is plain text, no HTML escaping needed
|
|
112
|
+
const subject = this.interpolateVariables(template.subject, dto.variables || {}, false);
|
|
113
|
+
let html;
|
|
114
|
+
let text;
|
|
115
|
+
if (template.isHtml) {
|
|
116
|
+
// HTML content - escape variables to prevent XSS
|
|
117
|
+
html = this.interpolateVariables(template.htmlContent, dto.variables || {}, true);
|
|
118
|
+
// Text content is plain text, no escaping needed
|
|
119
|
+
text = template.textContent ? this.interpolateVariables(template.textContent, dto.variables || {}, false) : undefined;
|
|
120
|
+
} else {
|
|
121
|
+
// Plain text template - no HTML escaping
|
|
122
|
+
text = template.textContent ? this.interpolateVariables(template.textContent, dto.variables || {}, false) : this.interpolateVariables(template.htmlContent, dto.variables || {}, false);
|
|
123
|
+
html = undefined;
|
|
124
|
+
}
|
|
125
|
+
const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
|
|
126
|
+
const result = await provider.sendEmail({
|
|
127
|
+
to: dto.to,
|
|
128
|
+
cc: dto.cc,
|
|
129
|
+
bcc: dto.bcc,
|
|
130
|
+
subject,
|
|
131
|
+
html,
|
|
132
|
+
text,
|
|
133
|
+
from: dto.from || config.fromEmail || undefined,
|
|
134
|
+
fromName: dto.fromName || config.fromName || this.emailConfigService.getDefaultFromName(),
|
|
135
|
+
replyTo: dto.replyTo,
|
|
136
|
+
attachments: dto.attachments?.map((a)=>({
|
|
137
|
+
filename: a.filename,
|
|
138
|
+
content: Buffer.from(a.content, 'base64'),
|
|
139
|
+
contentType: a.contentType
|
|
140
|
+
}))
|
|
141
|
+
});
|
|
142
|
+
this.logger.log(`Template email sent to ${Array.isArray(dto.to) ? dto.to.join(', ') : dto.to} using template "${template.slug}": ${result.success ? 'SUCCESS' : 'FAILED'}`);
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
async sendTestEmail(emailConfigId, recipient, user) {
|
|
146
|
+
const { provider, config } = await this.getEmailProviderWithConfig(emailConfigId, user);
|
|
147
|
+
// Escape config values to prevent XSS in test emails
|
|
148
|
+
const safeConfig = escapeHtmlVariables({
|
|
149
|
+
name: config.name,
|
|
150
|
+
provider: config.provider.toUpperCase()
|
|
151
|
+
});
|
|
152
|
+
const result = await provider.sendEmail({
|
|
153
|
+
to: recipient,
|
|
154
|
+
subject: 'Test Email from FLUSYS',
|
|
155
|
+
html: `
|
|
156
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
157
|
+
<h1 style="color: #333;">Test Email</h1>
|
|
158
|
+
<p>This is a test email from FLUSYS Email Service.</p>
|
|
159
|
+
<p><strong>Configuration:</strong> ${safeConfig.name}</p>
|
|
160
|
+
<p><strong>Provider:</strong> ${safeConfig.provider}</p>
|
|
161
|
+
<p style="color: #666; font-size: 12px;">
|
|
162
|
+
If you received this email, your email configuration is working correctly.
|
|
163
|
+
</p>
|
|
164
|
+
</div>
|
|
165
|
+
`,
|
|
166
|
+
text: `Test Email from FLUSYS\n\nThis is a test email.\nConfiguration: ${config.name}\nProvider: ${config.provider.toUpperCase()}`,
|
|
167
|
+
from: config.fromEmail || undefined,
|
|
168
|
+
fromName: config.fromName || this.emailConfigService.getDefaultFromName()
|
|
169
|
+
});
|
|
170
|
+
this.logger.log(`Test email sent to ${recipient}: ${result.success ? 'SUCCESS' : 'FAILED'}`);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
constructor(emailFactory, emailConfigService, emailProviderConfigService, emailTemplateService){
|
|
174
|
+
_define_property(this, "emailFactory", void 0);
|
|
175
|
+
_define_property(this, "emailConfigService", void 0);
|
|
176
|
+
_define_property(this, "emailProviderConfigService", void 0);
|
|
177
|
+
_define_property(this, "emailTemplateService", void 0);
|
|
178
|
+
_define_property(this, "logger", void 0);
|
|
179
|
+
this.emailFactory = emailFactory;
|
|
180
|
+
this.emailConfigService = emailConfigService;
|
|
181
|
+
this.emailProviderConfigService = emailProviderConfigService;
|
|
182
|
+
this.emailTemplateService = emailTemplateService;
|
|
183
|
+
this.logger = new Logger(EmailSendService.name);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
EmailSendService = _ts_decorate([
|
|
187
|
+
Injectable({
|
|
188
|
+
scope: Scope.REQUEST
|
|
189
|
+
}),
|
|
190
|
+
_ts_param(0, Inject(EmailFactoryService)),
|
|
191
|
+
_ts_param(1, Inject(EmailConfigService)),
|
|
192
|
+
_ts_param(2, Inject(EmailProviderConfigService)),
|
|
193
|
+
_ts_param(3, Inject(EmailTemplateService)),
|
|
194
|
+
_ts_metadata("design:type", Function),
|
|
195
|
+
_ts_metadata("design:paramtypes", [
|
|
196
|
+
typeof EmailFactoryService === "undefined" ? Object : EmailFactoryService,
|
|
197
|
+
typeof EmailConfigService === "undefined" ? Object : EmailConfigService,
|
|
198
|
+
typeof EmailProviderConfigService === "undefined" ? Object : EmailProviderConfigService,
|
|
199
|
+
typeof EmailTemplateService === "undefined" ? Object : EmailTemplateService
|
|
200
|
+
])
|
|
201
|
+
], EmailSendService);
|
|
@@ -0,0 +1,148 @@
|
|
|
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 { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
|
+
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
|
|
31
|
+
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
32
|
+
import { EmailConfigService } from '../config';
|
|
33
|
+
import { EmailTemplate, EmailTemplateWithCompany } from '../entities';
|
|
34
|
+
import { EmailDataSourceProvider } from './email-datasource.provider';
|
|
35
|
+
export class EmailTemplateService extends RequestScopedApiService {
|
|
36
|
+
resolveEntity() {
|
|
37
|
+
return this.emailConfig.isCompanyFeatureEnabled() ? EmailTemplateWithCompany : EmailTemplate;
|
|
38
|
+
}
|
|
39
|
+
getDataSourceProvider() {
|
|
40
|
+
return this.dataSourceProvider;
|
|
41
|
+
}
|
|
42
|
+
async convertSingleDtoToEntity(dto, user) {
|
|
43
|
+
const updateDto = dto;
|
|
44
|
+
if (updateDto.id) {
|
|
45
|
+
await this.ensureRepositoryInitialized();
|
|
46
|
+
const existing = await this.repository.findOne({
|
|
47
|
+
where: {
|
|
48
|
+
id: updateDto.id
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
if (existing && dto.schema && JSON.stringify(dto.schema) !== JSON.stringify(existing.schema)) {
|
|
52
|
+
dto.schemaVersion = existing.schemaVersion + 1;
|
|
53
|
+
this.logger.log(`Schema version incremented to ${dto.schemaVersion} for template ${updateDto.id}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const entity = {
|
|
57
|
+
...dto
|
|
58
|
+
};
|
|
59
|
+
if (this.emailConfig.isCompanyFeatureEnabled()) {
|
|
60
|
+
entity.companyId = user?.companyId ?? null;
|
|
61
|
+
}
|
|
62
|
+
return entity;
|
|
63
|
+
}
|
|
64
|
+
async getSelectQuery(query, _user, select) {
|
|
65
|
+
if (!select?.length) {
|
|
66
|
+
select = [
|
|
67
|
+
'id',
|
|
68
|
+
'name',
|
|
69
|
+
'slug',
|
|
70
|
+
'description',
|
|
71
|
+
'subject',
|
|
72
|
+
'schema',
|
|
73
|
+
'htmlContent',
|
|
74
|
+
'textContent',
|
|
75
|
+
'schemaVersion',
|
|
76
|
+
'isActive',
|
|
77
|
+
'isHtml',
|
|
78
|
+
'metadata',
|
|
79
|
+
'createdAt',
|
|
80
|
+
'updatedAt'
|
|
81
|
+
];
|
|
82
|
+
if (this.emailConfig.isCompanyFeatureEnabled()) select.push('companyId');
|
|
83
|
+
}
|
|
84
|
+
query.select(select.map((field)=>`${this.entityName}.${field}`));
|
|
85
|
+
return {
|
|
86
|
+
query,
|
|
87
|
+
isRaw: false
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async getExtraManipulateQuery(query, filterDto, user) {
|
|
91
|
+
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
92
|
+
applyCompanyFilter(query, {
|
|
93
|
+
isCompanyFeatureEnabled: this.emailConfig.isCompanyFeatureEnabled(),
|
|
94
|
+
entityAlias: 'emailTemplate'
|
|
95
|
+
}, user);
|
|
96
|
+
query.orderBy(`${this.entityName}.createdAt`, 'DESC');
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
async findByIdDirect(id) {
|
|
100
|
+
await this.ensureRepositoryInitialized();
|
|
101
|
+
return this.repository.findOne({
|
|
102
|
+
where: {
|
|
103
|
+
id
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async findBySlug(slug, user) {
|
|
108
|
+
await this.ensureRepositoryInitialized();
|
|
109
|
+
const where = buildCompanyWhereCondition({
|
|
110
|
+
slug,
|
|
111
|
+
isActive: true
|
|
112
|
+
}, this.emailConfig.isCompanyFeatureEnabled(), user);
|
|
113
|
+
return this.repository.findOne({
|
|
114
|
+
where
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async getActiveTemplates(user) {
|
|
118
|
+
await this.ensureRepositoryInitialized();
|
|
119
|
+
const where = buildCompanyWhereCondition({
|
|
120
|
+
isActive: true
|
|
121
|
+
}, this.emailConfig.isCompanyFeatureEnabled(), user);
|
|
122
|
+
return this.repository.find({
|
|
123
|
+
where,
|
|
124
|
+
order: {
|
|
125
|
+
name: 'ASC'
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
constructor(cacheManager, utilsService, emailConfig, dataSourceProvider){
|
|
130
|
+
super('emailTemplate', null, cacheManager, utilsService, EmailTemplateService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "emailConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.emailConfig = emailConfig, this.dataSourceProvider = dataSourceProvider;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
EmailTemplateService = _ts_decorate([
|
|
134
|
+
Injectable({
|
|
135
|
+
scope: Scope.REQUEST
|
|
136
|
+
}),
|
|
137
|
+
_ts_param(0, Inject('CACHE_INSTANCE')),
|
|
138
|
+
_ts_param(1, Inject(UtilsService)),
|
|
139
|
+
_ts_param(2, Inject(EmailConfigService)),
|
|
140
|
+
_ts_param(3, Inject(EmailDataSourceProvider)),
|
|
141
|
+
_ts_metadata("design:type", Function),
|
|
142
|
+
_ts_metadata("design:paramtypes", [
|
|
143
|
+
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
144
|
+
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
145
|
+
typeof EmailConfigService === "undefined" ? Object : EmailConfigService,
|
|
146
|
+
typeof EmailDataSourceProvider === "undefined" ? Object : EmailDataSourceProvider
|
|
147
|
+
])
|
|
148
|
+
], EmailTemplateService);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export function getOtpEmailFormat(otp, userName) {
|
|
2
|
+
return `
|
|
3
|
+
<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8" />
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
8
|
+
<title>Your OTP Code</title>
|
|
9
|
+
<style>
|
|
10
|
+
body {
|
|
11
|
+
font-family: Arial, sans-serif;
|
|
12
|
+
background-color: #f4f4f7;
|
|
13
|
+
margin: 0;
|
|
14
|
+
padding: 0;
|
|
15
|
+
}
|
|
16
|
+
.email-container {
|
|
17
|
+
max-width: 500px;
|
|
18
|
+
margin: 40px auto;
|
|
19
|
+
background-color: #ffffff;
|
|
20
|
+
padding: 30px;
|
|
21
|
+
border-radius: 8px;
|
|
22
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
23
|
+
text-align: center;
|
|
24
|
+
}
|
|
25
|
+
h1 {
|
|
26
|
+
color: #333;
|
|
27
|
+
}
|
|
28
|
+
p {
|
|
29
|
+
font-size: 16px;
|
|
30
|
+
color: #555;
|
|
31
|
+
}
|
|
32
|
+
.otp-box {
|
|
33
|
+
display: inline-block;
|
|
34
|
+
margin: 20px 0;
|
|
35
|
+
padding: 14px 28px;
|
|
36
|
+
font-size: 24px;
|
|
37
|
+
letter-spacing: 6px;
|
|
38
|
+
background-color: #007BFF;
|
|
39
|
+
color: white;
|
|
40
|
+
border-radius: 8px;
|
|
41
|
+
font-weight: bold;
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<div class="email-container">
|
|
47
|
+
<p>Hi ${userName || 'Sir/Madam'},</p>
|
|
48
|
+
<p>Use the code below to verify your identity:</p>
|
|
49
|
+
<div class="otp-box">${otp}</div>
|
|
50
|
+
<p>This OTP is valid for a limited time. Do not share it with anyone.</p>
|
|
51
|
+
<p>If you didn't request this, please ignore this email.</p>
|
|
52
|
+
</div>
|
|
53
|
+
</body>
|
|
54
|
+
</html>
|
|
55
|
+
`;
|
|
56
|
+
}
|
|
57
|
+
export function getResetPasswordEmailFormat(resetLink, userName) {
|
|
58
|
+
return `
|
|
59
|
+
<!DOCTYPE html>
|
|
60
|
+
<html lang="en">
|
|
61
|
+
<head>
|
|
62
|
+
<meta charset="UTF-8" />
|
|
63
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
64
|
+
<title>Reset Your Password</title>
|
|
65
|
+
<style>
|
|
66
|
+
body {
|
|
67
|
+
font-family: Arial, sans-serif;
|
|
68
|
+
background-color: #f4f4f7;
|
|
69
|
+
margin: 0;
|
|
70
|
+
padding: 0;
|
|
71
|
+
}
|
|
72
|
+
.email-container {
|
|
73
|
+
max-width: 500px;
|
|
74
|
+
margin: 40px auto;
|
|
75
|
+
background-color: #ffffff;
|
|
76
|
+
padding: 30px;
|
|
77
|
+
border-radius: 8px;
|
|
78
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
79
|
+
text-align: center;
|
|
80
|
+
}
|
|
81
|
+
h1 {
|
|
82
|
+
color: #333;
|
|
83
|
+
}
|
|
84
|
+
p {
|
|
85
|
+
font-size: 16px;
|
|
86
|
+
color: #555;
|
|
87
|
+
}
|
|
88
|
+
.reset-button {
|
|
89
|
+
display: inline-block;
|
|
90
|
+
margin: 20px 0;
|
|
91
|
+
padding: 14px 28px;
|
|
92
|
+
font-size: 16px;
|
|
93
|
+
background-color: #007BFF;
|
|
94
|
+
color: #ffffff !important;
|
|
95
|
+
text-decoration: none;
|
|
96
|
+
border-radius: 6px;
|
|
97
|
+
font-weight: bold;
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
100
|
+
</head>
|
|
101
|
+
<body>
|
|
102
|
+
<div class="email-container">
|
|
103
|
+
<p>Hi ${userName || 'Sir/Madam'},</p>
|
|
104
|
+
<p>We received a request to reset your password. Click the button below to reset it:</p>
|
|
105
|
+
<a href="${resetLink}" class="reset-button" target="_blank">Reset Password</a>
|
|
106
|
+
<p>If you didn't request a password reset, you can safely ignore this email.</p>
|
|
107
|
+
</div>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
110
|
+
`;
|
|
111
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './email-templates.util';
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './controllers';
|
|
2
|
+
export * from './services';
|
|
3
|
+
export * from './entities';
|
|
4
|
+
export * from './modules';
|
|
5
|
+
export * from './config';
|
|
6
|
+
export * from './dtos';
|
|
7
|
+
export * from './enums';
|
|
8
|
+
export * from './interfaces';
|
|
9
|
+
export * from './providers';
|
|
10
|
+
export * from './utils';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { IIdentity } from '@flusys/nestjs-shared/interfaces';
|
|
2
|
+
import { EmailProviderTypeEnum } from '../enums';
|
|
3
|
+
export interface IEmailConfig extends IIdentity {
|
|
4
|
+
name: string;
|
|
5
|
+
provider: EmailProviderTypeEnum;
|
|
6
|
+
config: Record<string, any>;
|
|
7
|
+
fromEmail: string | null;
|
|
8
|
+
fromName: string | null;
|
|
9
|
+
isActive: boolean;
|
|
10
|
+
isDefault: boolean;
|
|
11
|
+
companyId?: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface ISmtpTlsConfig {
|
|
14
|
+
rejectUnauthorized?: boolean;
|
|
15
|
+
minVersion?: 'TLSv1.2' | 'TLSv1.3';
|
|
16
|
+
}
|
|
17
|
+
export interface ISmtpConfig {
|
|
18
|
+
host: string;
|
|
19
|
+
port: number;
|
|
20
|
+
secure?: boolean;
|
|
21
|
+
auth?: {
|
|
22
|
+
user: string;
|
|
23
|
+
pass: string;
|
|
24
|
+
};
|
|
25
|
+
tls?: ISmtpTlsConfig;
|
|
26
|
+
}
|
|
27
|
+
export interface ISendGridConfig {
|
|
28
|
+
apiKey: string;
|
|
29
|
+
}
|
|
30
|
+
export interface IMailgunConfig {
|
|
31
|
+
apiKey: string;
|
|
32
|
+
domain: string;
|
|
33
|
+
region?: 'us' | 'eu';
|
|
34
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { IBootstrapAppConfig, IDataSourceServiceOptions, IDynamicModuleConfig, IModuleOptionsFactory } from '@flusys/nestjs-core';
|
|
2
|
+
import { ModuleMetadata, Type } from '@nestjs/common';
|
|
3
|
+
export interface IEmailModuleConfig extends IDataSourceServiceOptions {
|
|
4
|
+
defaultProvider?: string;
|
|
5
|
+
rateLimitPerMinute?: number;
|
|
6
|
+
enableLogging?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface IEmailModuleConfigFull {
|
|
9
|
+
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
10
|
+
config?: IEmailModuleConfig;
|
|
11
|
+
}
|
|
12
|
+
export interface EmailModuleOptions extends IDynamicModuleConfig {
|
|
13
|
+
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
14
|
+
config?: IEmailModuleConfig;
|
|
15
|
+
}
|
|
16
|
+
export interface EmailOptionsFactory extends IModuleOptionsFactory<IEmailModuleConfig> {
|
|
17
|
+
createEmailOptions(): Promise<IEmailModuleConfig> | IEmailModuleConfig;
|
|
18
|
+
}
|
|
19
|
+
export interface EmailModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'>, IDynamicModuleConfig {
|
|
20
|
+
bootstrapAppConfig: IBootstrapAppConfig;
|
|
21
|
+
useFactory?: (...args: any[]) => Promise<IEmailModuleConfig> | IEmailModuleConfig;
|
|
22
|
+
inject?: any[];
|
|
23
|
+
useClass?: Type<EmailOptionsFactory>;
|
|
24
|
+
useExisting?: Type<EmailOptionsFactory>;
|
|
25
|
+
}
|