@flusys/nestjs-email 1.1.0-beta → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +589 -0
- package/cjs/config/email.constants.js +0 -18
- package/cjs/config/index.js +0 -1
- package/cjs/controllers/email-config.controller.js +46 -4
- package/cjs/controllers/email-send.controller.js +13 -26
- package/cjs/controllers/email-template.controller.js +60 -11
- package/cjs/docs/email-swagger.config.js +18 -80
- package/cjs/dtos/email-config.dto.js +6 -106
- package/cjs/dtos/email-send.dto.js +101 -123
- package/cjs/dtos/email-template.dto.js +41 -103
- package/cjs/entities/email-config-with-company.entity.js +2 -2
- package/cjs/entities/email-config.entity.js +92 -3
- package/cjs/entities/email-template-with-company.entity.js +5 -3
- package/cjs/entities/email-template.entity.js +119 -3
- package/cjs/entities/index.js +34 -19
- package/cjs/index.js +1 -0
- package/cjs/interfaces/email-provider.interface.js +1 -3
- package/cjs/modules/email.module.js +50 -104
- package/cjs/providers/email-factory.service.js +37 -109
- package/cjs/providers/email-provider.registry.js +5 -15
- package/cjs/providers/mailgun-provider.js +54 -58
- package/cjs/providers/sendgrid-provider.js +68 -92
- package/cjs/providers/smtp-provider.js +58 -69
- package/cjs/{config → services}/email-config.service.js +9 -32
- package/cjs/services/email-datasource.provider.js +17 -104
- package/cjs/services/email-provider-config.service.js +28 -58
- package/cjs/services/email-send.service.js +120 -125
- package/cjs/services/email-template.service.js +62 -85
- package/cjs/services/index.js +2 -1
- package/cjs/utils/email-templates.util.js +64 -0
- package/cjs/utils/index.js +18 -0
- package/config/email.constants.d.ts +0 -9
- package/config/index.d.ts +0 -1
- package/controllers/email-send.controller.d.ts +5 -12
- package/controllers/email-template.controller.d.ts +5 -7
- package/dtos/email-config.dto.d.ts +5 -13
- package/dtos/email-send.dto.d.ts +17 -21
- package/dtos/email-template.dto.d.ts +5 -16
- package/entities/email-config-with-company.entity.d.ts +2 -2
- package/entities/email-config.entity.d.ts +10 -2
- package/entities/email-template-with-company.entity.d.ts +2 -2
- package/entities/email-template.entity.d.ts +13 -2
- package/entities/index.d.ts +9 -3
- package/fesm/config/email.constants.js +0 -9
- package/fesm/config/index.js +0 -1
- package/fesm/controllers/email-config.controller.js +49 -7
- package/fesm/controllers/email-send.controller.js +13 -26
- package/fesm/controllers/email-template.controller.js +61 -12
- package/fesm/docs/email-swagger.config.js +21 -86
- package/fesm/dtos/email-config.dto.js +9 -115
- package/fesm/dtos/email-send.dto.js +103 -139
- package/fesm/dtos/email-template.dto.js +43 -111
- package/fesm/entities/email-config-with-company.entity.js +2 -2
- package/fesm/entities/email-config.entity.js +93 -4
- package/fesm/entities/email-template-with-company.entity.js +5 -3
- package/fesm/entities/email-template.entity.js +120 -4
- package/fesm/entities/index.js +22 -16
- package/fesm/index.js +1 -0
- package/fesm/interfaces/email-config.interface.js +1 -3
- package/fesm/interfaces/email-module-options.interface.js +1 -3
- package/fesm/interfaces/email-provider.interface.js +1 -5
- package/fesm/interfaces/email-template.interface.js +1 -3
- package/fesm/modules/email.module.js +52 -106
- package/fesm/providers/email-factory.service.js +38 -69
- package/fesm/providers/email-provider.registry.js +6 -19
- package/fesm/providers/mailgun-provider.js +55 -63
- package/fesm/providers/sendgrid-provider.js +69 -97
- package/fesm/providers/smtp-provider.js +59 -73
- package/fesm/{config → services}/email-config.service.js +9 -32
- package/fesm/services/email-datasource.provider.js +18 -64
- package/fesm/services/email-provider-config.service.js +26 -56
- package/fesm/services/email-send.service.js +118 -123
- package/fesm/services/email-template.service.js +60 -83
- package/fesm/services/index.js +2 -1
- package/fesm/utils/email-templates.util.js +47 -0
- package/fesm/utils/index.js +1 -0
- package/index.d.ts +1 -0
- package/interfaces/email-config.interface.d.ts +6 -0
- package/interfaces/email-module-options.interface.d.ts +0 -5
- package/modules/email.module.d.ts +1 -2
- package/package.json +4 -4
- package/providers/email-factory.service.d.ts +4 -7
- package/providers/mailgun-provider.d.ts +6 -2
- package/providers/sendgrid-provider.d.ts +6 -2
- package/providers/smtp-provider.d.ts +7 -2
- package/services/email-config.service.d.ts +12 -0
- package/services/email-datasource.provider.d.ts +3 -6
- package/services/email-provider-config.service.d.ts +3 -3
- package/services/email-send.service.d.ts +11 -3
- package/services/email-template.service.d.ts +5 -4
- package/services/index.d.ts +2 -1
- package/utils/email-templates.util.d.ts +2 -0
- package/utils/index.d.ts +1 -0
- package/cjs/entities/email-config-base.entity.js +0 -111
- package/cjs/entities/email-template-base.entity.js +0 -134
- package/config/email-config.service.d.ts +0 -13
- package/entities/email-config-base.entity.d.ts +0 -11
- package/entities/email-template-base.entity.d.ts +0 -14
- package/fesm/entities/email-config-base.entity.js +0 -101
- package/fesm/entities/email-template-base.entity.js +0 -124
|
@@ -25,64 +25,18 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
import { escapeHtmlVariables, validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
|
|
28
29
|
import { BadRequestException, Inject, Injectable, Logger, NotFoundException, Scope } from '@nestjs/common';
|
|
29
|
-
import { EmailConfigService } from '
|
|
30
|
+
import { EmailConfigService } from './email-config.service';
|
|
30
31
|
import { EmailFactoryService } from '../providers';
|
|
31
32
|
import { EmailProviderConfigService } from './email-provider-config.service';
|
|
32
33
|
import { EmailTemplateService } from './email-template.service';
|
|
34
|
+
/** Valid identifier pattern for template variables: {{varName}} */ const VARIABLE_PATTERN = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
|
|
33
35
|
export class EmailSendService {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*/ async getEmailProviderWithConfig(emailConfigId, user) {
|
|
37
|
-
let emailConfig;
|
|
38
|
-
if (emailConfigId) {
|
|
39
|
-
const config = await this.emailProviderConfigService.findByIdDirect(emailConfigId);
|
|
40
|
-
if (!config) {
|
|
41
|
-
throw new NotFoundException('Email configuration not found');
|
|
42
|
-
}
|
|
43
|
-
// Validate company ownership
|
|
44
|
-
if (this.emailConfigService.isCompanyFeatureEnabled() && user?.companyId) {
|
|
45
|
-
const configWithCompany = config;
|
|
46
|
-
if (configWithCompany.companyId && configWithCompany.companyId !== user.companyId) {
|
|
47
|
-
throw new BadRequestException('Email configuration belongs to another company');
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (!config.isActive) {
|
|
51
|
-
throw new BadRequestException('Email configuration is inactive');
|
|
52
|
-
}
|
|
53
|
-
emailConfig = config;
|
|
54
|
-
} else {
|
|
55
|
-
const defaultConfig = await this.emailProviderConfigService.getDefaultConfig(user);
|
|
56
|
-
if (!defaultConfig) {
|
|
57
|
-
throw new NotFoundException('No default email configuration found. Please create one.');
|
|
58
|
-
}
|
|
59
|
-
emailConfig = defaultConfig;
|
|
60
|
-
}
|
|
61
|
-
const providerConfig = {
|
|
62
|
-
provider: emailConfig.provider,
|
|
63
|
-
config: emailConfig.config
|
|
64
|
-
};
|
|
65
|
-
const provider = await this.emailFactory.createProvider(providerConfig);
|
|
66
|
-
return {
|
|
67
|
-
provider,
|
|
68
|
-
config: emailConfig
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Interpolate variables in template content
|
|
73
|
-
* Replaces {{variableName}} with actual values
|
|
74
|
-
*/ interpolateVariables(content, variables) {
|
|
75
|
-
if (!variables || Object.keys(variables).length === 0) {
|
|
76
|
-
return content;
|
|
77
|
-
}
|
|
78
|
-
return content.replace(/\{\{(\w+)\}\}/g, (match, varName)=>{
|
|
79
|
-
return variables[varName] !== undefined ? String(variables[varName]) : match;
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Send email directly (without template)
|
|
84
|
-
*/ async sendEmail(dto, user) {
|
|
36
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
37
|
+
async sendEmail(dto, user) {
|
|
85
38
|
const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
|
|
39
|
+
const sender = this.resolveSender(dto, config);
|
|
86
40
|
const result = await provider.sendEmail({
|
|
87
41
|
to: dto.to,
|
|
88
42
|
cc: dto.cc,
|
|
@@ -90,59 +44,18 @@ export class EmailSendService {
|
|
|
90
44
|
subject: dto.subject,
|
|
91
45
|
html: dto.html,
|
|
92
46
|
text: dto.text,
|
|
93
|
-
|
|
94
|
-
fromName: dto.fromName || config.fromName || this.emailConfigService.getDefaultFromName(),
|
|
47
|
+
...sender,
|
|
95
48
|
replyTo: dto.replyTo,
|
|
96
|
-
attachments: dto.attachments
|
|
97
|
-
filename: a.filename,
|
|
98
|
-
content: Buffer.from(a.content, 'base64'),
|
|
99
|
-
contentType: a.contentType
|
|
100
|
-
}))
|
|
49
|
+
attachments: this.buildAttachments(dto.attachments)
|
|
101
50
|
});
|
|
102
|
-
this.
|
|
51
|
+
this.logResult(dto.to, result);
|
|
103
52
|
return result;
|
|
104
53
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Get template by ID or slug
|
|
109
|
-
let template;
|
|
110
|
-
if (dto.templateId) {
|
|
111
|
-
template = await this.emailTemplateService.findByIdDirect(dto.templateId);
|
|
112
|
-
} else if (dto.templateSlug) {
|
|
113
|
-
template = await this.emailTemplateService.findBySlug(dto.templateSlug, user);
|
|
114
|
-
} else {
|
|
115
|
-
throw new BadRequestException('templateId or templateSlug is required');
|
|
116
|
-
}
|
|
117
|
-
if (!template) {
|
|
118
|
-
throw new NotFoundException('Email template not found');
|
|
119
|
-
}
|
|
120
|
-
if (!template.isActive) {
|
|
121
|
-
throw new BadRequestException('Email template is inactive');
|
|
122
|
-
}
|
|
123
|
-
// Validate company ownership
|
|
124
|
-
if (this.emailConfigService.isCompanyFeatureEnabled() && user?.companyId) {
|
|
125
|
-
const templateWithCompany = template;
|
|
126
|
-
if (templateWithCompany.companyId && templateWithCompany.companyId !== user.companyId) {
|
|
127
|
-
throw new BadRequestException('Email template belongs to another company');
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// Interpolate variables in subject and content
|
|
131
|
-
const subject = this.interpolateVariables(template.subject, dto.variables || {});
|
|
132
|
-
// Check isHtml to determine which content to send
|
|
133
|
-
let html;
|
|
134
|
-
let text;
|
|
135
|
-
if (template.isHtml) {
|
|
136
|
-
// HTML template - send htmlContent, with textContent as fallback
|
|
137
|
-
html = this.interpolateVariables(template.htmlContent, dto.variables || {});
|
|
138
|
-
text = template.textContent ? this.interpolateVariables(template.textContent, dto.variables || {}) : undefined;
|
|
139
|
-
} else {
|
|
140
|
-
// Plain text template - send only textContent, no HTML
|
|
141
|
-
text = template.textContent ? this.interpolateVariables(template.textContent, dto.variables || {}) : this.interpolateVariables(template.htmlContent, dto.variables || {}); // fallback to htmlContent if textContent is empty
|
|
142
|
-
html = undefined;
|
|
143
|
-
}
|
|
144
|
-
// Get provider and send
|
|
54
|
+
async sendTemplateEmail(dto, user) {
|
|
55
|
+
const template = await this.resolveTemplate(dto, user);
|
|
56
|
+
const { subject, html, text } = this.buildTemplateContent(template, dto.variables);
|
|
145
57
|
const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
|
|
58
|
+
const sender = this.resolveSender(dto, config);
|
|
146
59
|
const result = await provider.sendEmail({
|
|
147
60
|
to: dto.to,
|
|
148
61
|
cc: dto.cc,
|
|
@@ -150,43 +63,125 @@ export class EmailSendService {
|
|
|
150
63
|
subject,
|
|
151
64
|
html,
|
|
152
65
|
text,
|
|
153
|
-
|
|
154
|
-
fromName: dto.fromName || config.fromName || this.emailConfigService.getDefaultFromName(),
|
|
66
|
+
...sender,
|
|
155
67
|
replyTo: dto.replyTo,
|
|
156
|
-
attachments: dto.attachments
|
|
157
|
-
filename: a.filename,
|
|
158
|
-
content: Buffer.from(a.content, 'base64'),
|
|
159
|
-
contentType: a.contentType
|
|
160
|
-
}))
|
|
68
|
+
attachments: this.buildAttachments(dto.attachments)
|
|
161
69
|
});
|
|
162
|
-
this.
|
|
70
|
+
this.logResult(dto.to, result, template.slug);
|
|
163
71
|
return result;
|
|
164
72
|
}
|
|
165
|
-
|
|
166
|
-
* Send test email (for testing configuration)
|
|
167
|
-
*/ async sendTestEmail(emailConfigId, recipient, user) {
|
|
73
|
+
async sendTestEmail(emailConfigId, recipient, user) {
|
|
168
74
|
const { provider, config } = await this.getEmailProviderWithConfig(emailConfigId, user);
|
|
75
|
+
const safeConfig = escapeHtmlVariables({
|
|
76
|
+
name: config.name,
|
|
77
|
+
provider: config.provider.toUpperCase()
|
|
78
|
+
});
|
|
169
79
|
const result = await provider.sendEmail({
|
|
170
80
|
to: recipient,
|
|
171
81
|
subject: 'Test Email from FLUSYS',
|
|
172
|
-
html:
|
|
173
|
-
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
174
|
-
<h1 style="color: #333;">Test Email</h1>
|
|
175
|
-
<p>This is a test email from FLUSYS Email Service.</p>
|
|
176
|
-
<p><strong>Configuration:</strong> ${config.name}</p>
|
|
177
|
-
<p><strong>Provider:</strong> ${config.provider.toUpperCase()}</p>
|
|
178
|
-
<p style="color: #666; font-size: 12px;">
|
|
179
|
-
If you received this email, your email configuration is working correctly.
|
|
180
|
-
</p>
|
|
181
|
-
</div>
|
|
182
|
-
`,
|
|
82
|
+
html: this.buildTestEmailHtml(safeConfig),
|
|
183
83
|
text: `Test Email from FLUSYS\n\nThis is a test email.\nConfiguration: ${config.name}\nProvider: ${config.provider.toUpperCase()}`,
|
|
184
84
|
from: config.fromEmail || undefined,
|
|
185
85
|
fromName: config.fromName || this.emailConfigService.getDefaultFromName()
|
|
186
86
|
});
|
|
187
|
-
this.
|
|
87
|
+
this.logResult(recipient, result);
|
|
188
88
|
return result;
|
|
189
89
|
}
|
|
90
|
+
// ─── Private: Provider & Config Resolution ──────────────────────────────────
|
|
91
|
+
async getEmailProviderWithConfig(emailConfigId, user) {
|
|
92
|
+
const emailConfig = emailConfigId ? await this.resolveConfigById(emailConfigId, user) : await this.resolveDefaultConfig(user);
|
|
93
|
+
const providerConfig = {
|
|
94
|
+
provider: emailConfig.provider,
|
|
95
|
+
config: emailConfig.config
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
provider: await this.emailFactory.createProvider(providerConfig),
|
|
99
|
+
config: emailConfig
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async resolveConfigById(id, user) {
|
|
103
|
+
const config = await this.emailProviderConfigService.findByIdDirect(id);
|
|
104
|
+
if (!config) throw new NotFoundException('Email configuration not found');
|
|
105
|
+
validateCompanyOwnership(config, user, this.emailConfigService.isCompanyFeatureEnabled(), 'Email configuration');
|
|
106
|
+
if (!config.isActive) throw new BadRequestException('Email configuration is inactive');
|
|
107
|
+
return config;
|
|
108
|
+
}
|
|
109
|
+
async resolveDefaultConfig(user) {
|
|
110
|
+
const config = await this.emailProviderConfigService.getDefaultConfig(user);
|
|
111
|
+
if (!config) throw new NotFoundException('No default email configuration found');
|
|
112
|
+
return config;
|
|
113
|
+
}
|
|
114
|
+
// ─── Private: Template Resolution ───────────────────────────────────────────
|
|
115
|
+
async resolveTemplate(dto, user) {
|
|
116
|
+
let template = null;
|
|
117
|
+
if (dto.templateId) {
|
|
118
|
+
template = await this.emailTemplateService.findByIdDirect(dto.templateId);
|
|
119
|
+
} else if (dto.templateSlug) {
|
|
120
|
+
template = await this.emailTemplateService.findBySlug(dto.templateSlug, user);
|
|
121
|
+
} else {
|
|
122
|
+
throw new BadRequestException('templateId or templateSlug is required');
|
|
123
|
+
}
|
|
124
|
+
if (!template) throw new NotFoundException('Email template not found');
|
|
125
|
+
if (!template.isActive) throw new BadRequestException('Email template is inactive');
|
|
126
|
+
validateCompanyOwnership(template, user, this.emailConfigService.isCompanyFeatureEnabled(), 'Email template');
|
|
127
|
+
return template;
|
|
128
|
+
}
|
|
129
|
+
buildTemplateContent(template, variables) {
|
|
130
|
+
const vars = variables || {};
|
|
131
|
+
const subject = this.interpolateVariables(template.subject, vars, false);
|
|
132
|
+
if (template.isHtml) {
|
|
133
|
+
return {
|
|
134
|
+
subject,
|
|
135
|
+
html: this.interpolateVariables(template.htmlContent, vars, true),
|
|
136
|
+
text: template.textContent ? this.interpolateVariables(template.textContent, vars, false) : undefined
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
subject,
|
|
141
|
+
html: undefined,
|
|
142
|
+
text: template.textContent ? this.interpolateVariables(template.textContent, vars, false) : this.interpolateVariables(template.htmlContent, vars, false)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
// ─── Private: Helpers ───────────────────────────────────────────────────────
|
|
146
|
+
resolveSender(dto, config) {
|
|
147
|
+
return {
|
|
148
|
+
from: dto.from || config.fromEmail || undefined,
|
|
149
|
+
fromName: dto.fromName || config.fromName || this.emailConfigService.getDefaultFromName()
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
buildAttachments(attachments) {
|
|
153
|
+
return attachments?.map((a)=>({
|
|
154
|
+
filename: a.filename,
|
|
155
|
+
content: Buffer.from(a.content, 'base64'),
|
|
156
|
+
contentType: a.contentType
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
interpolateVariables(content, variables, escapeHtml) {
|
|
160
|
+
if (!variables || Object.keys(variables).length === 0) return content;
|
|
161
|
+
const safeVars = escapeHtml ? escapeHtmlVariables(variables) : variables;
|
|
162
|
+
return content.replace(VARIABLE_PATTERN, (match, varName)=>{
|
|
163
|
+
return varName in safeVars && safeVars[varName] !== undefined ? String(safeVars[varName]) : match;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
logResult(to, result, templateSlug) {
|
|
167
|
+
const recipient = Array.isArray(to) ? to.join(', ') : to;
|
|
168
|
+
const status = result.success ? 'SUCCESS' : 'FAILED';
|
|
169
|
+
const message = templateSlug ? `Template email sent to ${recipient} using template "${templateSlug}": ${status}` : `Email sent to ${recipient}: ${status}`;
|
|
170
|
+
this.logger.log(message);
|
|
171
|
+
}
|
|
172
|
+
buildTestEmailHtml(safeConfig) {
|
|
173
|
+
return `
|
|
174
|
+
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
|
175
|
+
<h1 style="color: #333;">Test Email</h1>
|
|
176
|
+
<p>This is a test email from FLUSYS Email Service.</p>
|
|
177
|
+
<p><strong>Configuration:</strong> ${safeConfig.name}</p>
|
|
178
|
+
<p><strong>Provider:</strong> ${safeConfig.provider}</p>
|
|
179
|
+
<p style="color: #666; font-size: 12px;">
|
|
180
|
+
If you received this email, your email configuration is working correctly.
|
|
181
|
+
</p>
|
|
182
|
+
</div>
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
190
185
|
constructor(emailFactory, emailConfigService, emailProviderConfigService, emailTemplateService){
|
|
191
186
|
_define_property(this, "emailFactory", void 0);
|
|
192
187
|
_define_property(this, "emailConfigService", void 0);
|
|
@@ -27,133 +27,110 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
|
|
30
31
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
|
-
import { EmailConfigService } from '
|
|
32
|
+
import { EmailConfigService } from './email-config.service';
|
|
32
33
|
import { EmailTemplate, EmailTemplateWithCompany } from '../entities';
|
|
33
34
|
import { EmailDataSourceProvider } from './email-datasource.provider';
|
|
35
|
+
const DEFAULT_SELECT_FIELDS = [
|
|
36
|
+
'id',
|
|
37
|
+
'name',
|
|
38
|
+
'slug',
|
|
39
|
+
'description',
|
|
40
|
+
'subject',
|
|
41
|
+
'schema',
|
|
42
|
+
'htmlContent',
|
|
43
|
+
'textContent',
|
|
44
|
+
'schemaVersion',
|
|
45
|
+
'isActive',
|
|
46
|
+
'isHtml',
|
|
47
|
+
'metadata',
|
|
48
|
+
'createdAt',
|
|
49
|
+
'updatedAt'
|
|
50
|
+
];
|
|
34
51
|
export class EmailTemplateService extends RequestScopedApiService {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*/ resolveEntity() {
|
|
38
|
-
const enableCompanyFeature = this.emailConfig.isCompanyFeatureEnabled();
|
|
39
|
-
return enableCompanyFeature ? EmailTemplateWithCompany : EmailTemplate;
|
|
52
|
+
resolveEntity() {
|
|
53
|
+
return this.emailConfig.isCompanyFeatureEnabled() ? EmailTemplateWithCompany : EmailTemplate;
|
|
40
54
|
}
|
|
41
|
-
|
|
42
|
-
* Get DataSource provider for this service
|
|
43
|
-
*/ getDataSourceProvider() {
|
|
55
|
+
getDataSourceProvider() {
|
|
44
56
|
return this.dataSourceProvider;
|
|
45
57
|
}
|
|
46
58
|
async convertSingleDtoToEntity(dto, user) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (updateDto.id) {
|
|
50
|
-
await this.ensureRepositoryInitialized();
|
|
51
|
-
const existing = await this.repository.findOne({
|
|
52
|
-
where: {
|
|
53
|
-
id: updateDto.id
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
if (existing) {
|
|
57
|
-
// Auto-increment schema version if schema changed
|
|
58
|
-
if (dto.schema && JSON.stringify(dto.schema) !== JSON.stringify(existing.schema)) {
|
|
59
|
-
dto.schemaVersion = existing.schemaVersion + 1;
|
|
60
|
-
this.logger.log(`Schema changed for template ${updateDto.id}, incrementing version from ${existing.schemaVersion} to ${dto.schemaVersion}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
let templateEntity = {
|
|
65
|
-
...dto
|
|
66
|
-
};
|
|
67
|
-
// Set company fields if company feature is enabled
|
|
59
|
+
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
60
|
+
await this.incrementSchemaVersionIfChanged(dto, entity);
|
|
68
61
|
if (this.emailConfig.isCompanyFeatureEnabled()) {
|
|
69
|
-
|
|
62
|
+
entity.companyId = user?.companyId ?? null;
|
|
70
63
|
}
|
|
71
|
-
return
|
|
64
|
+
return entity;
|
|
72
65
|
}
|
|
73
66
|
async getSelectQuery(query, _user, select) {
|
|
74
|
-
if (!select
|
|
67
|
+
if (!select?.length) {
|
|
75
68
|
select = [
|
|
76
|
-
|
|
77
|
-
'name',
|
|
78
|
-
'slug',
|
|
79
|
-
'description',
|
|
80
|
-
'subject',
|
|
81
|
-
'schema',
|
|
82
|
-
'htmlContent',
|
|
83
|
-
'textContent',
|
|
84
|
-
'schemaVersion',
|
|
85
|
-
'isActive',
|
|
86
|
-
'isHtml',
|
|
87
|
-
'metadata',
|
|
88
|
-
'createdAt',
|
|
89
|
-
'updatedAt'
|
|
69
|
+
...DEFAULT_SELECT_FIELDS
|
|
90
70
|
];
|
|
91
|
-
if (this.emailConfig.isCompanyFeatureEnabled())
|
|
92
|
-
select.push('companyId');
|
|
93
|
-
}
|
|
71
|
+
if (this.emailConfig.isCompanyFeatureEnabled()) select.push('companyId');
|
|
94
72
|
}
|
|
95
|
-
|
|
96
|
-
query.select(selectFields);
|
|
73
|
+
query.select(select.map((field)=>`${this.entityName}.${field}`));
|
|
97
74
|
return {
|
|
98
75
|
query,
|
|
99
76
|
isRaw: false
|
|
100
77
|
};
|
|
101
78
|
}
|
|
102
|
-
|
|
103
|
-
* Override: Extra query manipulation - Auto-filter by user's company
|
|
104
|
-
*/ async getExtraManipulateQuery(query, filterDto, user) {
|
|
79
|
+
async getExtraManipulateQuery(query, filterDto, user) {
|
|
105
80
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
111
|
-
}
|
|
81
|
+
applyCompanyFilter(query, {
|
|
82
|
+
isCompanyFeatureEnabled: this.emailConfig.isCompanyFeatureEnabled(),
|
|
83
|
+
entityAlias: 'emailTemplate'
|
|
84
|
+
}, user);
|
|
112
85
|
query.orderBy(`${this.entityName}.createdAt`, 'DESC');
|
|
113
86
|
return result;
|
|
114
87
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
*/ async findByIdDirect(id) {
|
|
88
|
+
// ─── Public Query Methods ───────────────────────────────────────────────────
|
|
89
|
+
async findByIdDirect(id) {
|
|
118
90
|
await this.ensureRepositoryInitialized();
|
|
119
|
-
return
|
|
91
|
+
return this.repository.findOne({
|
|
120
92
|
where: {
|
|
121
93
|
id
|
|
122
94
|
}
|
|
123
95
|
});
|
|
124
96
|
}
|
|
125
|
-
|
|
126
|
-
* Find template by slug (scoped to user's company if enabled)
|
|
127
|
-
*/ async findBySlug(slug, user) {
|
|
97
|
+
async findBySlug(slug, user) {
|
|
128
98
|
await this.ensureRepositoryInitialized();
|
|
129
|
-
const where = {
|
|
99
|
+
const where = buildCompanyWhereCondition({
|
|
130
100
|
slug,
|
|
131
101
|
isActive: true
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
where.companyId = user.companyId;
|
|
135
|
-
}
|
|
136
|
-
return await this.repository.findOne({
|
|
102
|
+
}, this.emailConfig.isCompanyFeatureEnabled(), user);
|
|
103
|
+
return this.repository.findOne({
|
|
137
104
|
where
|
|
138
105
|
});
|
|
139
106
|
}
|
|
140
|
-
|
|
141
|
-
* Get all active templates (scoped to user's company if enabled)
|
|
142
|
-
*/ async getActiveTemplates(user) {
|
|
107
|
+
async getActiveTemplates(user) {
|
|
143
108
|
await this.ensureRepositoryInitialized();
|
|
144
|
-
const where = {
|
|
109
|
+
const where = buildCompanyWhereCondition({
|
|
145
110
|
isActive: true
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
where.companyId = user.companyId;
|
|
149
|
-
}
|
|
150
|
-
return await this.repository.find({
|
|
111
|
+
}, this.emailConfig.isCompanyFeatureEnabled(), user);
|
|
112
|
+
return this.repository.find({
|
|
151
113
|
where,
|
|
152
114
|
order: {
|
|
153
115
|
name: 'ASC'
|
|
154
116
|
}
|
|
155
117
|
});
|
|
156
118
|
}
|
|
119
|
+
// ─── Private Helpers ────────────────────────────────────────────────────────
|
|
120
|
+
async incrementSchemaVersionIfChanged(dto, entity) {
|
|
121
|
+
const updateDto = dto;
|
|
122
|
+
if (!updateDto.id || !dto.schema) return;
|
|
123
|
+
await this.ensureRepositoryInitialized();
|
|
124
|
+
const existing = await this.repository.findOne({
|
|
125
|
+
where: {
|
|
126
|
+
id: updateDto.id
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
if (existing && JSON.stringify(dto.schema) !== JSON.stringify(existing.schema)) {
|
|
130
|
+
entity.schemaVersion = existing.schemaVersion + 1;
|
|
131
|
+
this.logger.log(`Schema version incremented to ${entity.schemaVersion} for template ${updateDto.id}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
157
134
|
constructor(cacheManager, utilsService, emailConfig, dataSourceProvider){
|
|
158
135
|
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;
|
|
159
136
|
}
|
package/fesm/services/index.js
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// ─── Shared Styles ───────────────────────────────────────────────────────────
|
|
2
|
+
const BASE_STYLES = `
|
|
3
|
+
body { font-family: Arial, sans-serif; background-color: #f4f4f7; margin: 0; padding: 0; }
|
|
4
|
+
.email-container { max-width: 500px; margin: 40px auto; background-color: #ffffff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); text-align: center; }
|
|
5
|
+
h1 { color: #333; }
|
|
6
|
+
p { font-size: 16px; color: #555; }
|
|
7
|
+
.action-box { display: inline-block; margin: 20px 0; padding: 14px 28px; background-color: #007BFF; color: #ffffff; border-radius: 8px; font-weight: bold; text-decoration: none; }
|
|
8
|
+
`;
|
|
9
|
+
function wrapEmailTemplate(title, extraStyles, content) {
|
|
10
|
+
return `<!DOCTYPE html>
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="UTF-8" />
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
15
|
+
<title>${title}</title>
|
|
16
|
+
<style>${BASE_STYLES}${extraStyles}</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<div class="email-container">${content}</div>
|
|
20
|
+
</body>
|
|
21
|
+
</html>`;
|
|
22
|
+
}
|
|
23
|
+
function getGreeting(userName) {
|
|
24
|
+
return `<p>Hi ${userName || 'Sir/Madam'},</p>`;
|
|
25
|
+
}
|
|
26
|
+
// ─── Public Templates ────────────────────────────────────────────────────────
|
|
27
|
+
export function getOtpEmailFormat(otp, userName) {
|
|
28
|
+
const extraStyles = `.action-box { font-size: 24px; letter-spacing: 6px; }`;
|
|
29
|
+
const content = `
|
|
30
|
+
${getGreeting(userName)}
|
|
31
|
+
<p>Use the code below to verify your identity:</p>
|
|
32
|
+
<div class="action-box">${otp}</div>
|
|
33
|
+
<p>This OTP is valid for a limited time. Do not share it with anyone.</p>
|
|
34
|
+
<p>If you didn't request this, please ignore this email.</p>
|
|
35
|
+
`;
|
|
36
|
+
return wrapEmailTemplate('Your OTP Code', extraStyles, content);
|
|
37
|
+
}
|
|
38
|
+
export function getResetPasswordEmailFormat(resetLink, userName) {
|
|
39
|
+
const extraStyles = `.action-box { font-size: 16px; }`;
|
|
40
|
+
const content = `
|
|
41
|
+
${getGreeting(userName)}
|
|
42
|
+
<p>We received a request to reset your password. Click the button below to reset it:</p>
|
|
43
|
+
<a href="${resetLink}" class="action-box" target="_blank">Reset Password</a>
|
|
44
|
+
<p>If you didn't request a password reset, you can safely ignore this email.</p>
|
|
45
|
+
`;
|
|
46
|
+
return wrapEmailTemplate('Reset Your Password', extraStyles, content);
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './email-templates.util';
|
package/index.d.ts
CHANGED
|
@@ -7,8 +7,13 @@ export interface IEmailConfig extends IIdentity {
|
|
|
7
7
|
fromEmail: string | null;
|
|
8
8
|
fromName: string | null;
|
|
9
9
|
isActive: boolean;
|
|
10
|
+
isDefault: boolean;
|
|
10
11
|
companyId?: string | null;
|
|
11
12
|
}
|
|
13
|
+
export interface ISmtpTlsConfig {
|
|
14
|
+
rejectUnauthorized?: boolean;
|
|
15
|
+
minVersion?: 'TLSv1.2' | 'TLSv1.3';
|
|
16
|
+
}
|
|
12
17
|
export interface ISmtpConfig {
|
|
13
18
|
host: string;
|
|
14
19
|
port: number;
|
|
@@ -17,6 +22,7 @@ export interface ISmtpConfig {
|
|
|
17
22
|
user: string;
|
|
18
23
|
pass: string;
|
|
19
24
|
};
|
|
25
|
+
tls?: ISmtpTlsConfig;
|
|
20
26
|
}
|
|
21
27
|
export interface ISendGridConfig {
|
|
22
28
|
apiKey: string;
|
|
@@ -5,17 +5,12 @@ export interface IEmailModuleConfig extends IDataSourceServiceOptions {
|
|
|
5
5
|
rateLimitPerMinute?: number;
|
|
6
6
|
enableLogging?: boolean;
|
|
7
7
|
}
|
|
8
|
-
export interface IEmailModuleConfigFull {
|
|
9
|
-
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
10
|
-
config?: IEmailModuleConfig;
|
|
11
|
-
}
|
|
12
8
|
export interface EmailModuleOptions extends IDynamicModuleConfig {
|
|
13
9
|
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
14
10
|
config?: IEmailModuleConfig;
|
|
15
11
|
}
|
|
16
12
|
export interface EmailOptionsFactory extends IModuleOptionsFactory<IEmailModuleConfig> {
|
|
17
13
|
createEmailOptions(): Promise<IEmailModuleConfig> | IEmailModuleConfig;
|
|
18
|
-
createOptions(): Promise<IEmailModuleConfig> | IEmailModuleConfig;
|
|
19
14
|
}
|
|
20
15
|
export interface EmailModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'>, IDynamicModuleConfig {
|
|
21
16
|
bootstrapAppConfig: IBootstrapAppConfig;
|
|
@@ -3,7 +3,6 @@ import { EmailModuleAsyncOptions, EmailModuleOptions } from '../interfaces';
|
|
|
3
3
|
export declare class EmailModule {
|
|
4
4
|
static forRoot(options: EmailModuleOptions): DynamicModule;
|
|
5
5
|
static forRootAsync(options: EmailModuleAsyncOptions): DynamicModule;
|
|
6
|
-
private static
|
|
7
|
-
private static getProviders;
|
|
6
|
+
private static buildProviders;
|
|
8
7
|
private static createAsyncProviders;
|
|
9
8
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-email",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Modular email package with SMTP, SendGrid, and Mailgun providers",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"class-transformer": "^0.5.0",
|
|
84
84
|
"class-validator": "^0.14.0",
|
|
85
85
|
"typeorm": "^0.3.0",
|
|
86
|
-
"nodemailer": "^
|
|
86
|
+
"nodemailer": "^7.0.0",
|
|
87
87
|
"@sendgrid/mail": "^8.0.0",
|
|
88
88
|
"mailgun.js": "^10.0.0"
|
|
89
89
|
},
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
}
|
|
100
100
|
},
|
|
101
101
|
"dependencies": {
|
|
102
|
-
"@flusys/nestjs-core": "
|
|
103
|
-
"@flusys/nestjs-shared": "
|
|
102
|
+
"@flusys/nestjs-core": "2.0.0",
|
|
103
|
+
"@flusys/nestjs-shared": "2.0.0"
|
|
104
104
|
}
|
|
105
105
|
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { OnModuleDestroy } from '@nestjs/common';
|
|
2
|
-
import { EmailConfigService } from '../config/email-config.service';
|
|
3
2
|
import { IEmailProvider, IEmailProviderConfig } from '../interfaces';
|
|
4
3
|
export declare class EmailFactoryService implements OnModuleDestroy {
|
|
5
|
-
private readonly emailConfigService;
|
|
6
4
|
private readonly logger;
|
|
7
|
-
private providerCache;
|
|
8
|
-
constructor(emailConfigService: EmailConfigService);
|
|
9
|
-
private generateCacheKey;
|
|
5
|
+
private readonly providerCache;
|
|
10
6
|
createProvider(config: IEmailProviderConfig): Promise<IEmailProvider>;
|
|
11
|
-
isProviderAvailable(providerName: string): boolean;
|
|
12
|
-
getAvailableProviders(): string[];
|
|
13
7
|
onModuleDestroy(): Promise<void>;
|
|
8
|
+
private generateCacheKey;
|
|
9
|
+
private closeProvider;
|
|
10
|
+
private extractErrorMessage;
|
|
14
11
|
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { IEmailProvider, IEmailSendOptions, IEmailSendResult, IMailgunConfig } from '../interfaces';
|
|
2
2
|
export declare class MailgunProvider implements IEmailProvider {
|
|
3
|
-
private logger;
|
|
3
|
+
private readonly logger;
|
|
4
4
|
private client;
|
|
5
|
-
private
|
|
5
|
+
private domain;
|
|
6
6
|
initialize(config: IMailgunConfig): Promise<void>;
|
|
7
7
|
sendEmail(options: IEmailSendOptions): Promise<IEmailSendResult>;
|
|
8
8
|
sendBulkEmails(options: IEmailSendOptions[]): Promise<IEmailSendResult[]>;
|
|
9
9
|
healthCheck(): Promise<boolean>;
|
|
10
10
|
close(): Promise<void>;
|
|
11
|
+
private buildMessageData;
|
|
12
|
+
private joinAddresses;
|
|
13
|
+
private extractError;
|
|
14
|
+
private handleError;
|
|
11
15
|
}
|