@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.
Files changed (100) hide show
  1. package/README.md +589 -0
  2. package/cjs/config/email.constants.js +0 -18
  3. package/cjs/config/index.js +0 -1
  4. package/cjs/controllers/email-config.controller.js +46 -4
  5. package/cjs/controllers/email-send.controller.js +13 -26
  6. package/cjs/controllers/email-template.controller.js +60 -11
  7. package/cjs/docs/email-swagger.config.js +18 -80
  8. package/cjs/dtos/email-config.dto.js +6 -106
  9. package/cjs/dtos/email-send.dto.js +101 -123
  10. package/cjs/dtos/email-template.dto.js +41 -103
  11. package/cjs/entities/email-config-with-company.entity.js +2 -2
  12. package/cjs/entities/email-config.entity.js +92 -3
  13. package/cjs/entities/email-template-with-company.entity.js +5 -3
  14. package/cjs/entities/email-template.entity.js +119 -3
  15. package/cjs/entities/index.js +34 -19
  16. package/cjs/index.js +1 -0
  17. package/cjs/interfaces/email-provider.interface.js +1 -3
  18. package/cjs/modules/email.module.js +50 -104
  19. package/cjs/providers/email-factory.service.js +37 -109
  20. package/cjs/providers/email-provider.registry.js +5 -15
  21. package/cjs/providers/mailgun-provider.js +54 -58
  22. package/cjs/providers/sendgrid-provider.js +68 -92
  23. package/cjs/providers/smtp-provider.js +58 -69
  24. package/cjs/{config → services}/email-config.service.js +9 -32
  25. package/cjs/services/email-datasource.provider.js +17 -104
  26. package/cjs/services/email-provider-config.service.js +28 -58
  27. package/cjs/services/email-send.service.js +120 -125
  28. package/cjs/services/email-template.service.js +62 -85
  29. package/cjs/services/index.js +2 -1
  30. package/cjs/utils/email-templates.util.js +64 -0
  31. package/cjs/utils/index.js +18 -0
  32. package/config/email.constants.d.ts +0 -9
  33. package/config/index.d.ts +0 -1
  34. package/controllers/email-send.controller.d.ts +5 -12
  35. package/controllers/email-template.controller.d.ts +5 -7
  36. package/dtos/email-config.dto.d.ts +5 -13
  37. package/dtos/email-send.dto.d.ts +17 -21
  38. package/dtos/email-template.dto.d.ts +5 -16
  39. package/entities/email-config-with-company.entity.d.ts +2 -2
  40. package/entities/email-config.entity.d.ts +10 -2
  41. package/entities/email-template-with-company.entity.d.ts +2 -2
  42. package/entities/email-template.entity.d.ts +13 -2
  43. package/entities/index.d.ts +9 -3
  44. package/fesm/config/email.constants.js +0 -9
  45. package/fesm/config/index.js +0 -1
  46. package/fesm/controllers/email-config.controller.js +49 -7
  47. package/fesm/controllers/email-send.controller.js +13 -26
  48. package/fesm/controllers/email-template.controller.js +61 -12
  49. package/fesm/docs/email-swagger.config.js +21 -86
  50. package/fesm/dtos/email-config.dto.js +9 -115
  51. package/fesm/dtos/email-send.dto.js +103 -139
  52. package/fesm/dtos/email-template.dto.js +43 -111
  53. package/fesm/entities/email-config-with-company.entity.js +2 -2
  54. package/fesm/entities/email-config.entity.js +93 -4
  55. package/fesm/entities/email-template-with-company.entity.js +5 -3
  56. package/fesm/entities/email-template.entity.js +120 -4
  57. package/fesm/entities/index.js +22 -16
  58. package/fesm/index.js +1 -0
  59. package/fesm/interfaces/email-config.interface.js +1 -3
  60. package/fesm/interfaces/email-module-options.interface.js +1 -3
  61. package/fesm/interfaces/email-provider.interface.js +1 -5
  62. package/fesm/interfaces/email-template.interface.js +1 -3
  63. package/fesm/modules/email.module.js +52 -106
  64. package/fesm/providers/email-factory.service.js +38 -69
  65. package/fesm/providers/email-provider.registry.js +6 -19
  66. package/fesm/providers/mailgun-provider.js +55 -63
  67. package/fesm/providers/sendgrid-provider.js +69 -97
  68. package/fesm/providers/smtp-provider.js +59 -73
  69. package/fesm/{config → services}/email-config.service.js +9 -32
  70. package/fesm/services/email-datasource.provider.js +18 -64
  71. package/fesm/services/email-provider-config.service.js +26 -56
  72. package/fesm/services/email-send.service.js +118 -123
  73. package/fesm/services/email-template.service.js +60 -83
  74. package/fesm/services/index.js +2 -1
  75. package/fesm/utils/email-templates.util.js +47 -0
  76. package/fesm/utils/index.js +1 -0
  77. package/index.d.ts +1 -0
  78. package/interfaces/email-config.interface.d.ts +6 -0
  79. package/interfaces/email-module-options.interface.d.ts +0 -5
  80. package/modules/email.module.d.ts +1 -2
  81. package/package.json +4 -4
  82. package/providers/email-factory.service.d.ts +4 -7
  83. package/providers/mailgun-provider.d.ts +6 -2
  84. package/providers/sendgrid-provider.d.ts +6 -2
  85. package/providers/smtp-provider.d.ts +7 -2
  86. package/services/email-config.service.d.ts +12 -0
  87. package/services/email-datasource.provider.d.ts +3 -6
  88. package/services/email-provider-config.service.d.ts +3 -3
  89. package/services/email-send.service.d.ts +11 -3
  90. package/services/email-template.service.d.ts +5 -4
  91. package/services/index.d.ts +2 -1
  92. package/utils/email-templates.util.d.ts +2 -0
  93. package/utils/index.d.ts +1 -0
  94. package/cjs/entities/email-config-base.entity.js +0 -111
  95. package/cjs/entities/email-template-base.entity.js +0 -134
  96. package/config/email-config.service.d.ts +0 -13
  97. package/entities/email-config-base.entity.d.ts +0 -11
  98. package/entities/email-template-base.entity.d.ts +0 -14
  99. package/fesm/entities/email-config-base.entity.js +0 -101
  100. 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 '../config';
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
- * Get email provider based on config ID or default
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
- from: dto.from || config.fromEmail || undefined,
94
- fromName: dto.fromName || config.fromName || this.emailConfigService.getDefaultFromName(),
47
+ ...sender,
95
48
  replyTo: dto.replyTo,
96
- attachments: dto.attachments?.map((a)=>({
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.logger.log(`Email sent to ${Array.isArray(dto.to) ? dto.to.join(', ') : dto.to}: ${result.success ? 'SUCCESS' : 'FAILED'}`);
51
+ this.logResult(dto.to, result);
103
52
  return result;
104
53
  }
105
- /**
106
- * Send email using template
107
- */ async sendTemplateEmail(dto, user) {
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
- from: dto.from || config.fromEmail || undefined,
154
- fromName: dto.fromName || config.fromName || this.emailConfigService.getDefaultFromName(),
66
+ ...sender,
155
67
  replyTo: dto.replyTo,
156
- attachments: dto.attachments?.map((a)=>({
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.logger.log(`Template email sent to ${Array.isArray(dto.to) ? dto.to.join(', ') : dto.to} using template "${template.slug}": ${result.success ? 'SUCCESS' : 'FAILED'}`);
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.logger.log(`Test email sent to ${recipient}: ${result.success ? 'SUCCESS' : 'FAILED'}`);
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 '../config';
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
- * Resolve entity class for this service
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
- // For updates, fetch existing template to handle versioning
48
- const updateDto = dto;
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
- templateEntity.companyId = user?.companyId ?? null;
62
+ entity.companyId = user?.companyId ?? null;
70
63
  }
71
- return templateEntity;
64
+ return entity;
72
65
  }
73
66
  async getSelectQuery(query, _user, select) {
74
- if (!select || !select.length) {
67
+ if (!select?.length) {
75
68
  select = [
76
- 'id',
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
- const selectFields = select.map((field)=>`${this.entityName}.${field}`);
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
- const enableCompanyFeature = this.emailConfig.isCompanyFeatureEnabled();
107
- if (enableCompanyFeature && user?.companyId) {
108
- query.andWhere('emailTemplate.companyId = :companyId', {
109
- companyId: user.companyId
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
- * Find template by ID directly (bypasses company filtering)
117
- */ async findByIdDirect(id) {
88
+ // ─── Public Query Methods ───────────────────────────────────────────────────
89
+ async findByIdDirect(id) {
118
90
  await this.ensureRepositoryInitialized();
119
- return await this.repository.findOne({
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
- if (this.emailConfig.isCompanyFeatureEnabled() && user?.companyId) {
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
- if (this.emailConfig.isCompanyFeatureEnabled() && user?.companyId) {
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
  }
@@ -1,4 +1,5 @@
1
+ export * from './email-config.service';
1
2
  export * from './email-datasource.provider';
2
3
  export * from './email-provider-config.service';
3
- export * from './email-template.service';
4
4
  export * from './email-send.service';
5
+ export * from './email-template.service';
@@ -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,3 +7,4 @@ export * from './dtos';
7
7
  export * from './enums';
8
8
  export * from './interfaces';
9
9
  export * from './providers';
10
+ export * from './utils';
@@ -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 getControllers;
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": "1.1.0-beta",
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": "^6.9.0",
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": "1.1.0-beta",
103
- "@flusys/nestjs-shared": "1.1.0-beta"
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 config;
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
  }