@flusys/nestjs-email 9.1.2 → 9.2.1

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 CHANGED
@@ -207,6 +207,13 @@ await this.emailSend.sendEmail({
207
207
 
208
208
  `EmailModule` provides and exports `EMAIL_ADAPTER` (`nestjs-shared`), implemented by `EmailAdapter`: `send({ to, cc?, bcc?, replyTo?, emailConfigId?, subject, html?, text? }, user)` and `sendTemplate({ ...recipients, templateSlug, variables? }, user)`, both answering `{ success, messageId?, error? }`. It resolves the request-scoped `EmailSendService` per call, so singletons can inject it; a feature package injects it `@Optional()` and never imports this package (the entity builder's **Send email** flow step uses it).
209
209
 
210
+ ## 4. Caching and Company Scope
211
+
212
+ - **Email configs are never cached** in the shared cache (`CACHE_INSTANCE`): rows carry SMTP passwords and API keys. Templates are cached (`findById` / `getAll`) under the `emailTemplate` stamp, which every insert / update / delete moves after commit on every instance.
213
+ - **The send path reads the database**: `sendEmail` / `sendTemplateEmail` / `sendTestEmail` load the config and the template (by id or slug) straight from the repository, so an edited template or config is used on the next send on every instance.
214
+ - **Provider clients** (`EmailFactoryService`) are reused per provider + SHA-256 of the full config, credentials included, so an edited config gets a new client everywhere and only identical credentials share one. Concurrent first sends share one pending client; a failed initialization is dropped so the next send retries. Clients idle for 30 minutes, or beyond the 50 most recently used, are dropped and `close()`d (SMTP transports). `clearCache()` closes every client.
215
+ - **Company scope** (`enableCompanyFeature: true`): `getById` / `getAll` only return rows of the caller's company (the company is part of the cache key), and `update` / `delete` reject ids of another company with `entity.belongs.another.company`.
216
+
210
217
  ## License
211
218
 
212
219
  MIT © FLUSYS
@@ -4,6 +4,7 @@ declare const EmailConfigController_base: abstract new (service: EmailProviderCo
4
4
  readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
5
5
  service: EmailProviderConfigService;
6
6
  isEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): boolean;
7
+ assertEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): void;
7
8
  insert(addDto: CreateEmailConfigDto, user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<EmailConfigResponseDto>>;
8
9
  insertMany(addDto: CreateEmailConfigDto[], user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").BulkResponseDto<EmailConfigResponseDto>>;
9
10
  getById(id: string, body: import("@flusys/nestjs-shared").GetByIdBodyDto, user: import("@flusys/nestjs-shared").ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").SingleResponseDto<EmailConfigResponseDto>>;
@@ -7,6 +7,7 @@ declare const EmailTemplateController_base: abstract new (service: EmailTemplate
7
7
  readonly enabledEndpoints: import("@flusys/nestjs-shared").ApiEndpoint[] | "all";
8
8
  service: EmailTemplateService;
9
9
  isEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): boolean;
10
+ assertEnabled(endpoint: import("@flusys/nestjs-shared").ApiEndpoint): void;
10
11
  insert(addDto: CreateEmailTemplateDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<EmailTemplateResponseDto>>;
11
12
  insertMany(addDto: CreateEmailTemplateDto[], user: ILoggedUserInfo | null): Promise<import("@flusys/nestjs-shared").BulkResponseDto<EmailTemplateResponseDto>>;
12
13
  getById(id: string, body: import("@flusys/nestjs-shared").GetByIdBodyDto, user: ILoggedUserInfo | null): Promise<SingleResponseDto<EmailTemplateResponseDto>>;
package/fesm/184.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Identity } from "@flusys/nestjs-shared/entities";
2
2
  import { createHash } from "crypto";
3
- import { SYSTEM_MESSAGES } from "@flusys/nestjs-shared/constants";
4
3
  import * as __rspack_external__flusys_nestjs_shared_393d2cae from "@flusys/nestjs-shared";
5
4
  import * as __rspack_external__flusys_nestjs_shared_classes_6076a329 from "@flusys/nestjs-shared/classes";
5
+ import * as __rspack_external__flusys_nestjs_shared_constants_81f33d5b from "@flusys/nestjs-shared/constants";
6
6
  import * as __rspack_external__flusys_nestjs_shared_modules_24702bee from "@flusys/nestjs-shared/modules";
7
7
  import * as __rspack_external__flusys_nestjs_shared_utils_9125c1f1 from "@flusys/nestjs-shared/utils";
8
8
  import * as __rspack_external__nestjs_common_a04939da from "@nestjs/common";
@@ -464,7 +464,7 @@ function getEmailEntitiesByConfig(enableCompanyFeature) {
464
464
 
465
465
 
466
466
  },
467
- 603(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
467
+ 652(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
468
468
 
469
469
  // EXPORTS
470
470
  __webpack_require__.d(__webpack_exports__, {
@@ -527,13 +527,21 @@ function _ts_metadata(metadataKey, metadataValue) {
527
527
 
528
528
 
529
529
 
530
+ const MAX_CACHED_PROVIDERS = 50;
531
+ const PROVIDER_IDLE_TTL_MS = 30 * 60 * 1000;
530
532
  class EmailFactoryService {
531
533
  providerCache = new Map();
532
534
  // ─── Public API ─────────────────────────────────────────────────────────────
533
535
  async createProvider(config) {
534
536
  const cacheKey = this.generateCacheKey(config);
537
+ const now = Date.now();
535
538
  const cached = this.providerCache.get(cacheKey);
536
- if (cached) return cached;
539
+ if (cached) {
540
+ cached.lastUsed = now;
541
+ this.providerCache.delete(cacheKey);
542
+ this.providerCache.set(cacheKey, cached);
543
+ return cached.provider;
544
+ }
537
545
  const ProviderClass = EmailProviderRegistry.get(config.provider);
538
546
  if (!ProviderClass) {
539
547
  throw new common_.NotFoundException({
@@ -545,10 +553,32 @@ class EmailFactoryService {
545
553
  }
546
554
  });
547
555
  }
556
+ // The pending promise is cached so concurrent first sends share one client (one SMTP transport)
557
+ const entry = {
558
+ provider: this.instantiate(ProviderClass, config),
559
+ lastUsed: now
560
+ };
561
+ this.providerCache.set(cacheKey, entry);
562
+ entry.provider.catch(()=>{
563
+ if (this.providerCache.get(cacheKey) === entry) this.providerCache.delete(cacheKey);
564
+ });
565
+ this.evict(now);
566
+ return entry.provider;
567
+ }
568
+ async clearCache() {
569
+ const entries = Array.from(this.providerCache.values());
570
+ this.providerCache.clear();
571
+ await Promise.allSettled(entries.map((entry)=>this.closeEntry(entry)));
572
+ }
573
+ // ─── Lifecycle ──────────────────────────────────────────────────────────────
574
+ async onModuleDestroy() {
575
+ await this.clearCache();
576
+ }
577
+ // ─── Private Helpers ────────────────────────────────────────────────────────
578
+ async instantiate(ProviderClass, config) {
548
579
  try {
549
580
  const instance = new ProviderClass();
550
581
  await instance.initialize?.(config.config);
551
- this.providerCache.set(cacheKey, instance);
552
582
  return instance;
553
583
  } catch (error) {
554
584
  throw new common_.InternalServerErrorException({
@@ -561,27 +591,36 @@ class EmailFactoryService {
561
591
  });
562
592
  }
563
593
  }
564
- // ─── Lifecycle ──────────────────────────────────────────────────────────────
565
- async onModuleDestroy() {
566
- const closePromises = Array.from(this.providerCache.entries()).map(([, provider])=>this.closeProvider(provider));
567
- await Promise.allSettled(closePromises);
568
- this.providerCache.clear();
569
- }
570
- // ─── Private Helpers ────────────────────────────────────────────────────────
571
- generateCacheKey(config) {
572
- const sortedKeys = Object.keys(config.config || {}).sort();
573
- const configString = JSON.stringify(config.config, sortedKeys);
574
- const hash = createHash('sha256').update(configString).digest('hex').substring(0, 16);
575
- return `${config.provider}-${hash}`;
594
+ evict(now) {
595
+ for (const [key, entry] of this.providerCache){
596
+ const overCapacity = this.providerCache.size > MAX_CACHED_PROVIDERS;
597
+ if (!overCapacity && now - entry.lastUsed < PROVIDER_IDLE_TTL_MS) continue;
598
+ this.providerCache.delete(key);
599
+ void this.closeEntry(entry);
600
+ }
576
601
  }
577
- async closeProvider(provider) {
578
- if (!provider.close) return;
602
+ async closeEntry(entry) {
579
603
  try {
580
- await provider.close();
604
+ const provider = await entry.provider;
605
+ await provider.close?.();
581
606
  } catch {
582
- // Silent failure
607
+ // A client that failed to initialize or to close has nothing left to release
583
608
  }
584
609
  }
610
+ generateCacheKey(config) {
611
+ const hash = createHash('sha256').update(this.stableStringify(config.config ?? {})).digest('hex');
612
+ return `${config.provider}:${hash}`;
613
+ }
614
+ stableStringify(value) {
615
+ if (value === null || value === undefined) return 'null';
616
+ if (Array.isArray(value)) return `[${value.map((item)=>this.stableStringify(item)).join(',')}]`;
617
+ if (typeof value === 'object') {
618
+ const record = value;
619
+ const entries = Object.keys(record).filter((key)=>record[key] !== undefined).sort().map((key)=>`${JSON.stringify(key)}:${this.stableStringify(record[key])}`);
620
+ return `{${entries.join(',')}}`;
621
+ }
622
+ return JSON.stringify(value);
623
+ }
585
624
  extractErrorMessage(error) {
586
625
  return error instanceof Error ? error.message : 'Unknown error';
587
626
  }
@@ -677,7 +716,8 @@ class SmtpProvider {
677
716
  }
678
717
  // ─── Private Helpers ────────────────────────────────────────────────────────
679
718
  formatFrom(from, fromName) {
680
- return fromName ? `"${fromName}" <${from}>` : from;
719
+ if (!from) return undefined;
720
+ return fromName ? `"${fromName.replace(/["\\]/g, '\\$&')}" <${from}>` : from;
681
721
  }
682
722
  joinAddresses(addresses) {
683
723
  return Array.isArray(addresses) ? addresses.join(', ') : addresses;
@@ -701,8 +741,8 @@ class SmtpProvider {
701
741
  }
702
742
  }
703
743
 
704
- ;// CONCATENATED MODULE: external "@flusys/nestjs-shared/constants"
705
-
744
+ // EXTERNAL MODULE: external "@flusys/nestjs-shared/constants"
745
+ var constants_ = __webpack_require__(140);
706
746
  // EXTERNAL MODULE: external "@flusys/nestjs-shared/utils"
707
747
  var utils_ = __webpack_require__(216);
708
748
  ;// CONCATENATED MODULE: ./projects/nestjs-email/src/providers/sendgrid-provider.ts
@@ -717,12 +757,13 @@ class SendGridProvider {
717
757
  try {
718
758
  // Optional peer dependency - loaded at runtime so bundlers don't statically resolve it
719
759
  const sgMailModule = await (0,utils_.loadOptionalModule)('@sendgrid/mail');
720
- this.sgMail = sgMailModule.default || sgMailModule;
760
+ const MailService = sgMailModule.MailService ?? sgMailModule.default?.MailService;
761
+ this.sgMail = MailService ? new MailService() : sgMailModule.default || sgMailModule;
721
762
  this.sgMail.setApiKey(config.apiKey);
722
763
  } catch {
723
764
  throw new common_.InternalServerErrorException({
724
765
  message: 'SendGrid initialization failed. Install: npm install @sendgrid/mail',
725
- messageKey: SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
766
+ messageKey: constants_.SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
726
767
  messageVariables: {
727
768
  sdk: 'sendgrid'
728
769
  }
@@ -737,7 +778,8 @@ class SendGridProvider {
737
778
  try {
738
779
  const msg = this.buildMessage(options, true);
739
780
  const [response] = await this.sgMail.send(msg);
740
- const messageId = response.headers['x-message-id'] || response.headers['X-Message-Id'];
781
+ const headers = response?.headers ?? {};
782
+ const messageId = headers['x-message-id'] || headers['X-Message-Id'];
741
783
  return {
742
784
  success: true,
743
785
  messageId
@@ -778,7 +820,7 @@ class SendGridProvider {
778
820
  buildMessage(options, includeExtras) {
779
821
  const msg = {
780
822
  to: this.toArray(options.to),
781
- from: options.fromName ? {
823
+ from: options.fromName && options.from ? {
782
824
  email: options.from,
783
825
  name: options.fromName
784
826
  } : options.from,
@@ -839,7 +881,7 @@ class MailgunProvider {
839
881
  } catch {
840
882
  throw new common_.InternalServerErrorException({
841
883
  message: 'Mailgun initialization failed. Install: npm install mailgun.js form-data',
842
- messageKey: SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
884
+ messageKey: constants_.SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
843
885
  messageVariables: {
844
886
  sdk: 'mailgun'
845
887
  }
@@ -885,7 +927,7 @@ class MailgunProvider {
885
927
  // ─── Private Helpers ────────────────────────────────────────────────────────
886
928
  buildMessageData(options) {
887
929
  const data = {
888
- from: options.fromName ? `${options.fromName} <${options.from}>` : options.from,
930
+ from: options.fromName && options.from ? `"${options.fromName.replace(/["\\]/g, '\\$&')}" <${options.from}>` : options.from,
889
931
  to: this.joinAddresses(options.to),
890
932
  subject: options.subject
891
933
  };
@@ -1075,11 +1117,12 @@ __webpack_require__.d(__webpack_exports__, {
1075
1117
  /* import */ var _flusys_nestjs_shared_utils__rspack_import_2 = __webpack_require__(216);
1076
1118
  /* import */ var _nestjs_common__rspack_import_3 = __webpack_require__(157);
1077
1119
  /* import */ var typeorm__rspack_import_4 = __webpack_require__(158);
1078
- /* import */ var _config_index_js__rspack_import_8 = __webpack_require__(399);
1079
- /* import */ var _config_index_js__rspack_import_9 = __webpack_require__(887);
1120
+ /* import */ var _config_index_js__rspack_import_9 = __webpack_require__(399);
1121
+ /* import */ var _config_index_js__rspack_import_10 = __webpack_require__(887);
1080
1122
  /* import */ var _entities_index_js__rspack_import_5 = __webpack_require__(454);
1081
- /* import */ var _email_config_service_js__rspack_import_6 = __webpack_require__(843);
1082
- /* import */ var _email_datasource_provider_js__rspack_import_7 = __webpack_require__(522);
1123
+ /* import */ var _utils_company_ownership_util_js__rspack_import_6 = __webpack_require__(561);
1124
+ /* import */ var _email_config_service_js__rspack_import_7 = __webpack_require__(843);
1125
+ /* import */ var _email_datasource_provider_js__rspack_import_8 = __webpack_require__(522);
1083
1126
  function _ts_decorate(decorators, target, key, desc) {
1084
1127
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1085
1128
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -1112,18 +1155,25 @@ function _ts_param(paramIndex, decorator) {
1112
1155
 
1113
1156
 
1114
1157
 
1158
+
1115
1159
  class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_import_0.ApiService {
1116
1160
  cacheManager;
1117
1161
  utilsService;
1118
1162
  emailConfig;
1119
1163
  constructor(cacheManager, utilsService, emailConfig, dataSourceProvider){
1120
- super(_config_index_js__rspack_import_8/* .EMAIL_EVENT_ENTITIES.CONFIG */.y.CONFIG, cacheManager, utilsService, EmailProviderConfigService.name, true, (/* inlined export .EMAIL_EVENT_MODULE */"email"), dataSourceProvider, emailConfig.isCompanyFeatureEnabled() ? _entities_index_js__rspack_import_5/* .EmailConfigWithCompany */.Ed : _entities_index_js__rspack_import_5/* .EmailConfig */.pG), this.cacheManager = cacheManager, this.utilsService = utilsService, this.emailConfig = emailConfig;
1164
+ super(_config_index_js__rspack_import_9/* .EMAIL_EVENT_ENTITIES.CONFIG */.y.CONFIG, cacheManager, utilsService, EmailProviderConfigService.name, // Rows carry provider credentials (SMTP password, API keys) - keep them out of the shared cache
1165
+ false, (/* inlined export .EMAIL_EVENT_MODULE */"email"), dataSourceProvider, emailConfig.isCompanyFeatureEnabled() ? _entities_index_js__rspack_import_5/* .EmailConfigWithCompany */.Ed : _entities_index_js__rspack_import_5/* .EmailConfig */.pG), this.cacheManager = cacheManager, this.utilsService = utilsService, this.emailConfig = emailConfig;
1166
+ }
1167
+ async beforeUpdateOperation(dto, user) {
1168
+ await this.assertInUserCompany((Array.isArray(dto) ? dto : [
1169
+ dto
1170
+ ]).map((d)=>d.id), user);
1121
1171
  }
1122
- async beforeDeleteOperation(dto, _user, _queryRunner) {
1172
+ async beforeDeleteOperation(dto, user, _queryRunner) {
1123
1173
  const ids = Array.isArray(dto.id) ? dto.id : [
1124
1174
  dto.id
1125
1175
  ];
1126
- await this.ensureDataSourceRepository();
1176
+ await this.assertInUserCompany(ids, user);
1127
1177
  const defaultConfigs = await this.repository.find({
1128
1178
  where: {
1129
1179
  id: (0,typeorm__rspack_import_4.In)(ids),
@@ -1135,7 +1185,7 @@ class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_i
1135
1185
  const names = defaultConfigs.map((c)=>c.name).join(', ');
1136
1186
  throw new _nestjs_common__rspack_import_3.ForbiddenException({
1137
1187
  message: `Cannot delete the default email config(s): ${names}. Set another config as default first.`,
1138
- messageKey: _config_index_js__rspack_import_9/* .EMAIL_CONFIG_MESSAGES.DEFAULT_DELETE_FORBIDDEN */.T6.DEFAULT_DELETE_FORBIDDEN,
1188
+ messageKey: _config_index_js__rspack_import_10/* .EMAIL_CONFIG_MESSAGES.DEFAULT_DELETE_FORBIDDEN */.T6.DEFAULT_DELETE_FORBIDDEN,
1139
1189
  messageVariables: {
1140
1190
  names
1141
1191
  }
@@ -1143,15 +1193,14 @@ class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_i
1143
1193
  }
1144
1194
  }
1145
1195
  async convertSingleDtoToEntity(dto, user) {
1146
- const entity = {
1147
- ...dto
1148
- };
1196
+ await this.ensureDataSourceRepository();
1197
+ const entity = await super.convertSingleDtoToEntity(dto, user);
1149
1198
  if (this.emailConfig.isCompanyFeatureEnabled()) {
1150
1199
  entity.companyId = user?.companyId ?? null;
1151
1200
  }
1152
1201
  return entity;
1153
1202
  }
1154
- async getSelectQuery(query, _user, select) {
1203
+ async getSelectQuery(query, user, select) {
1155
1204
  if (!select?.length) {
1156
1205
  select = [
1157
1206
  'id',
@@ -1168,6 +1217,10 @@ class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_i
1168
1217
  if (this.emailConfig.isCompanyFeatureEnabled()) select.push('companyId');
1169
1218
  }
1170
1219
  query.select(select.map((field)=>`${this.entityName}.${field}`));
1220
+ (0,_flusys_nestjs_shared_utils__rspack_import_2.applyCompanyFilter)(query, {
1221
+ isCompanyFeatureEnabled: this.emailConfig.isCompanyFeatureEnabled(),
1222
+ entityAlias: this.entityName
1223
+ }, user);
1171
1224
  return {
1172
1225
  query,
1173
1226
  isRaw: false
@@ -1175,10 +1228,6 @@ class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_i
1175
1228
  }
1176
1229
  async getExtraManipulateQuery(query, filterDto, user) {
1177
1230
  const result = await super.getExtraManipulateQuery(query, filterDto, user);
1178
- (0,_flusys_nestjs_shared_utils__rspack_import_2.applyCompanyFilter)(query, {
1179
- isCompanyFeatureEnabled: this.emailConfig.isCompanyFeatureEnabled(),
1180
- entityAlias: 'emailConfig'
1181
- }, user);
1182
1231
  query.orderBy(`${this.entityName}.createdAt`, 'DESC');
1183
1232
  return result;
1184
1233
  }
@@ -1211,6 +1260,10 @@ class EmailProviderConfigService extends _flusys_nestjs_shared_classes__rspack_i
1211
1260
  }
1212
1261
  });
1213
1262
  }
1263
+ async assertInUserCompany(ids, user) {
1264
+ await this.ensureDataSourceRepository();
1265
+ await (0,_utils_company_ownership_util_js__rspack_import_6/* .assertRowsInUserCompany */.p)(this.repository, ids.filter(Boolean), user, this.emailConfig.isCompanyFeatureEnabled(), 'Email configuration');
1266
+ }
1214
1267
  }
1215
1268
  EmailProviderConfigService = _ts_decorate([
1216
1269
  (0,_nestjs_common__rspack_import_3.Injectable)({
@@ -1218,14 +1271,14 @@ EmailProviderConfigService = _ts_decorate([
1218
1271
  }),
1219
1272
  _ts_param(0, (0,_nestjs_common__rspack_import_3.Inject)('CACHE_INSTANCE')),
1220
1273
  _ts_param(1, (0,_nestjs_common__rspack_import_3.Inject)(_flusys_nestjs_shared_modules__rspack_import_1.UtilsService)),
1221
- _ts_param(2, (0,_nestjs_common__rspack_import_3.Inject)(_email_config_service_js__rspack_import_6/* .EmailConfigService */.m)),
1222
- _ts_param(3, (0,_nestjs_common__rspack_import_3.Inject)(_email_datasource_provider_js__rspack_import_7/* .EmailDataSourceProvider */.Z)),
1274
+ _ts_param(2, (0,_nestjs_common__rspack_import_3.Inject)(_email_config_service_js__rspack_import_7/* .EmailConfigService */.m)),
1275
+ _ts_param(3, (0,_nestjs_common__rspack_import_3.Inject)(_email_datasource_provider_js__rspack_import_8/* .EmailDataSourceProvider */.Z)),
1223
1276
  _ts_metadata("design:type", Function),
1224
1277
  _ts_metadata("design:paramtypes", [
1225
1278
  typeof _flusys_nestjs_shared_classes__rspack_import_0.HybridCache === "undefined" ? Object : _flusys_nestjs_shared_classes__rspack_import_0.HybridCache,
1226
1279
  typeof _flusys_nestjs_shared_modules__rspack_import_1.UtilsService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_1.UtilsService,
1227
- typeof _email_config_service_js__rspack_import_6/* .EmailConfigService */.m === "undefined" ? Object : _email_config_service_js__rspack_import_6/* .EmailConfigService */.m,
1228
- typeof _email_datasource_provider_js__rspack_import_7/* .EmailDataSourceProvider */.Z === "undefined" ? Object : _email_datasource_provider_js__rspack_import_7/* .EmailDataSourceProvider */.Z
1280
+ typeof _email_config_service_js__rspack_import_7/* .EmailConfigService */.m === "undefined" ? Object : _email_config_service_js__rspack_import_7/* .EmailConfigService */.m,
1281
+ typeof _email_datasource_provider_js__rspack_import_8/* .EmailDataSourceProvider */.Z === "undefined" ? Object : _email_datasource_provider_js__rspack_import_8/* .EmailDataSourceProvider */.Z
1229
1282
  ])
1230
1283
  ], EmailProviderConfigService);
1231
1284
 
@@ -1244,7 +1297,7 @@ __webpack_require__.d(__webpack_exports__, {
1244
1297
  /* import */ var _config_index_js__rspack_import_10 = __webpack_require__(399);
1245
1298
  /* import */ var _email_config_service_js__rspack_import_4 = __webpack_require__(843);
1246
1299
  /* import */ var _dtos_index_js__rspack_import_5 = __webpack_require__(684);
1247
- /* import */ var _providers_index_js__rspack_import_6 = __webpack_require__(603);
1300
+ /* import */ var _providers_index_js__rspack_import_6 = __webpack_require__(652);
1248
1301
  /* import */ var _email_provider_config_service_js__rspack_import_7 = __webpack_require__(105);
1249
1302
  /* import */ var _email_template_service_js__rspack_import_8 = __webpack_require__(759);
1250
1303
  function _ts_decorate(decorators, target, key, desc) {
@@ -1413,13 +1466,13 @@ class EmailSendService {
1413
1466
  messageKey: _config_index_js__rspack_import_9/* .EMAIL_SEND_MESSAGES.TEMPLATE_NOT_FOUND */.dP.TEMPLATE_NOT_FOUND
1414
1467
  });
1415
1468
  }
1469
+ (0,_flusys_nestjs_shared_utils__rspack_import_2.validateCompanyOwnership)(template, user, this.emailConfigService.isCompanyFeatureEnabled(), 'Email template');
1416
1470
  if (!template.isActive) {
1417
1471
  throw new _nestjs_common__rspack_import_3.BadRequestException({
1418
1472
  message: 'Email template is inactive',
1419
1473
  messageKey: _config_index_js__rspack_import_9/* .EMAIL_SEND_MESSAGES.TEMPLATE_INACTIVE */.dP.TEMPLATE_INACTIVE
1420
1474
  });
1421
1475
  }
1422
- (0,_flusys_nestjs_shared_utils__rspack_import_2.validateCompanyOwnership)(template, user, this.emailConfigService.isCompanyFeatureEnabled(), 'Email template');
1423
1476
  return template;
1424
1477
  }
1425
1478
  buildTemplateContent(template, variables) {
@@ -1456,7 +1509,11 @@ class EmailSendService {
1456
1509
  if (!variables || Object.keys(variables).length === 0) return content;
1457
1510
  const safeVars = escapeHtml ? (0,_flusys_nestjs_shared_utils__rspack_import_2.escapeHtmlVariables)(variables) : variables;
1458
1511
  return content.replace(VARIABLE_PATTERN, (match, varName)=>{
1459
- return varName in safeVars && safeVars[varName] !== undefined ? String(safeVars[varName]) : match;
1512
+ if (!Object.hasOwn(safeVars, varName)) return match;
1513
+ const value = safeVars[varName];
1514
+ if (value === undefined) return match;
1515
+ if (value === null) return '';
1516
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
1460
1517
  });
1461
1518
  }
1462
1519
  buildTestEmailHtml(safeConfig) {
@@ -1559,10 +1616,11 @@ __webpack_require__.d(__webpack_exports__, {
1559
1616
  /* import */ var _flusys_nestjs_shared_modules__rspack_import_1 = __webpack_require__(236);
1560
1617
  /* import */ var _flusys_nestjs_shared_utils__rspack_import_2 = __webpack_require__(216);
1561
1618
  /* import */ var _nestjs_common__rspack_import_3 = __webpack_require__(157);
1562
- /* import */ var _config_index_js__rspack_import_7 = __webpack_require__(399);
1619
+ /* import */ var _config_index_js__rspack_import_8 = __webpack_require__(399);
1563
1620
  /* import */ var _entities_index_js__rspack_import_4 = __webpack_require__(454);
1564
- /* import */ var _email_config_service_js__rspack_import_5 = __webpack_require__(843);
1565
- /* import */ var _email_datasource_provider_js__rspack_import_6 = __webpack_require__(522);
1621
+ /* import */ var _utils_company_ownership_util_js__rspack_import_5 = __webpack_require__(561);
1622
+ /* import */ var _email_config_service_js__rspack_import_6 = __webpack_require__(843);
1623
+ /* import */ var _email_datasource_provider_js__rspack_import_7 = __webpack_require__(522);
1566
1624
  function _ts_decorate(decorators, target, key, desc) {
1567
1625
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1568
1626
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -1594,6 +1652,7 @@ function _ts_param(paramIndex, decorator) {
1594
1652
 
1595
1653
 
1596
1654
 
1655
+
1597
1656
  const DEFAULT_SELECT_FIELDS = [
1598
1657
  'id',
1599
1658
  'name',
@@ -1614,7 +1673,17 @@ class EmailTemplateService extends _flusys_nestjs_shared_classes__rspack_import_
1614
1673
  utilsService;
1615
1674
  emailConfig;
1616
1675
  constructor(cacheManager, utilsService, emailConfig, dataSourceProvider){
1617
- super(_config_index_js__rspack_import_7/* .EMAIL_EVENT_ENTITIES.TEMPLATE */.y.TEMPLATE, cacheManager, utilsService, EmailTemplateService.name, true, (/* inlined export .EMAIL_EVENT_MODULE */"email"), dataSourceProvider, emailConfig.isCompanyFeatureEnabled() ? _entities_index_js__rspack_import_4/* .EmailTemplateWithCompany */.U7 : _entities_index_js__rspack_import_4/* .EmailTemplate */.Fy), this.cacheManager = cacheManager, this.utilsService = utilsService, this.emailConfig = emailConfig;
1676
+ super(_config_index_js__rspack_import_8/* .EMAIL_EVENT_ENTITIES.TEMPLATE */.y.TEMPLATE, cacheManager, utilsService, EmailTemplateService.name, true, (/* inlined export .EMAIL_EVENT_MODULE */"email"), dataSourceProvider, emailConfig.isCompanyFeatureEnabled() ? _entities_index_js__rspack_import_4/* .EmailTemplateWithCompany */.U7 : _entities_index_js__rspack_import_4/* .EmailTemplate */.Fy), this.cacheManager = cacheManager, this.utilsService = utilsService, this.emailConfig = emailConfig;
1677
+ }
1678
+ async beforeUpdateOperation(dto, user) {
1679
+ await this.assertInUserCompany((Array.isArray(dto) ? dto : [
1680
+ dto
1681
+ ]).map((d)=>d.id), user);
1682
+ }
1683
+ async beforeDeleteOperation(dto, user, _queryRunner) {
1684
+ await this.assertInUserCompany(Array.isArray(dto.id) ? dto.id : [
1685
+ dto.id
1686
+ ], user);
1618
1687
  }
1619
1688
  async convertSingleDtoToEntity(dto, user) {
1620
1689
  const entity = await super.convertSingleDtoToEntity(dto, user);
@@ -1624,7 +1693,7 @@ class EmailTemplateService extends _flusys_nestjs_shared_classes__rspack_import_
1624
1693
  }
1625
1694
  return entity;
1626
1695
  }
1627
- async getSelectQuery(query, _user, select) {
1696
+ async getSelectQuery(query, user, select) {
1628
1697
  if (!select?.length) {
1629
1698
  select = [
1630
1699
  ...DEFAULT_SELECT_FIELDS
@@ -1632,6 +1701,10 @@ class EmailTemplateService extends _flusys_nestjs_shared_classes__rspack_import_
1632
1701
  if (this.emailConfig.isCompanyFeatureEnabled()) select.push('companyId');
1633
1702
  }
1634
1703
  query.select(select.map((field)=>`${this.entityName}.${field}`));
1704
+ (0,_flusys_nestjs_shared_utils__rspack_import_2.applyCompanyFilter)(query, {
1705
+ isCompanyFeatureEnabled: this.emailConfig.isCompanyFeatureEnabled(),
1706
+ entityAlias: this.entityName
1707
+ }, user);
1635
1708
  return {
1636
1709
  query,
1637
1710
  isRaw: false
@@ -1639,10 +1712,6 @@ class EmailTemplateService extends _flusys_nestjs_shared_classes__rspack_import_
1639
1712
  }
1640
1713
  async getExtraManipulateQuery(query, filterDto, user) {
1641
1714
  const result = await super.getExtraManipulateQuery(query, filterDto, user);
1642
- (0,_flusys_nestjs_shared_utils__rspack_import_2.applyCompanyFilter)(query, {
1643
- isCompanyFeatureEnabled: this.emailConfig.isCompanyFeatureEnabled(),
1644
- entityAlias: 'emailTemplate'
1645
- }, user);
1646
1715
  query.orderBy(`${this.entityName}.createdAt`, 'DESC');
1647
1716
  return result;
1648
1717
  }
@@ -1666,6 +1735,10 @@ class EmailTemplateService extends _flusys_nestjs_shared_classes__rspack_import_
1666
1735
  });
1667
1736
  }
1668
1737
  // ─── Private Helpers ────────────────────────────────────────────────────────
1738
+ async assertInUserCompany(ids, user) {
1739
+ await this.ensureDataSourceRepository();
1740
+ await (0,_utils_company_ownership_util_js__rspack_import_5/* .assertRowsInUserCompany */.p)(this.repository, ids.filter(Boolean), user, this.emailConfig.isCompanyFeatureEnabled(), 'Email template');
1741
+ }
1669
1742
  async incrementSchemaVersionIfChanged(dto, entity) {
1670
1743
  const updateDto = dto;
1671
1744
  if (!updateDto.id || !dto.schema) return;
@@ -1686,14 +1759,14 @@ EmailTemplateService = _ts_decorate([
1686
1759
  }),
1687
1760
  _ts_param(0, (0,_nestjs_common__rspack_import_3.Inject)('CACHE_INSTANCE')),
1688
1761
  _ts_param(1, (0,_nestjs_common__rspack_import_3.Inject)(_flusys_nestjs_shared_modules__rspack_import_1.UtilsService)),
1689
- _ts_param(2, (0,_nestjs_common__rspack_import_3.Inject)(_email_config_service_js__rspack_import_5/* .EmailConfigService */.m)),
1690
- _ts_param(3, (0,_nestjs_common__rspack_import_3.Inject)(_email_datasource_provider_js__rspack_import_6/* .EmailDataSourceProvider */.Z)),
1762
+ _ts_param(2, (0,_nestjs_common__rspack_import_3.Inject)(_email_config_service_js__rspack_import_6/* .EmailConfigService */.m)),
1763
+ _ts_param(3, (0,_nestjs_common__rspack_import_3.Inject)(_email_datasource_provider_js__rspack_import_7/* .EmailDataSourceProvider */.Z)),
1691
1764
  _ts_metadata("design:type", Function),
1692
1765
  _ts_metadata("design:paramtypes", [
1693
1766
  typeof _flusys_nestjs_shared_classes__rspack_import_0.HybridCache === "undefined" ? Object : _flusys_nestjs_shared_classes__rspack_import_0.HybridCache,
1694
1767
  typeof _flusys_nestjs_shared_modules__rspack_import_1.UtilsService === "undefined" ? Object : _flusys_nestjs_shared_modules__rspack_import_1.UtilsService,
1695
- typeof _email_config_service_js__rspack_import_5/* .EmailConfigService */.m === "undefined" ? Object : _email_config_service_js__rspack_import_5/* .EmailConfigService */.m,
1696
- typeof _email_datasource_provider_js__rspack_import_6/* .EmailDataSourceProvider */.Z === "undefined" ? Object : _email_datasource_provider_js__rspack_import_6/* .EmailDataSourceProvider */.Z
1768
+ typeof _email_config_service_js__rspack_import_6/* .EmailConfigService */.m === "undefined" ? Object : _email_config_service_js__rspack_import_6/* .EmailConfigService */.m,
1769
+ typeof _email_datasource_provider_js__rspack_import_7/* .EmailDataSourceProvider */.Z === "undefined" ? Object : _email_datasource_provider_js__rspack_import_7/* .EmailDataSourceProvider */.Z
1697
1770
  ])
1698
1771
  ], EmailTemplateService);
1699
1772
 
@@ -1702,6 +1775,43 @@ __webpack_require__.d(__webpack_exports__, {
1702
1775
  });
1703
1776
 
1704
1777
 
1778
+ },
1779
+ 561(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
1780
+ /* import */ var _flusys_nestjs_shared_constants__rspack_import_0 = __webpack_require__(140);
1781
+ /* import */ var _nestjs_common__rspack_import_1 = __webpack_require__(157);
1782
+ /* import */ var typeorm__rspack_import_2 = __webpack_require__(158);
1783
+
1784
+
1785
+
1786
+ /**
1787
+ * ApiService write paths load rows by id alone, so a caller could update or delete another company's
1788
+ * row by its id. Every row must belong to the caller's company - a caller without a company may only
1789
+ * touch rows without one.
1790
+ */ async function assertRowsInUserCompany(repository, ids, user, isCompanyFeatureEnabled, entityName) {
1791
+ if (!isCompanyFeatureEnabled || !user || !ids.length) return;
1792
+ const rows = await repository.find({
1793
+ where: {
1794
+ id: (0,typeorm__rspack_import_2.In)(ids)
1795
+ },
1796
+ withDeleted: true
1797
+ });
1798
+ const userCompanyId = user.companyId ?? null;
1799
+ if (rows.some((row)=>(row.companyId ?? null) !== userCompanyId)) {
1800
+ throw new _nestjs_common__rspack_import_1.ForbiddenException({
1801
+ message: `${entityName} belongs to another company`,
1802
+ messageKey: _flusys_nestjs_shared_constants__rspack_import_0.AUTH_MESSAGES.ENTITY_BELOG_ANOTHER_COMPANY,
1803
+ messageVariables: {
1804
+ entity: entityName
1805
+ }
1806
+ });
1807
+ }
1808
+ }
1809
+
1810
+ __webpack_require__.d(__webpack_exports__, {
1811
+ p: () => (assertRowsInUserCompany)
1812
+ });
1813
+
1814
+
1705
1815
  },
1706
1816
  422(module) {
1707
1817
 
@@ -1714,6 +1824,12 @@ module.exports = __rspack_external__flusys_nestjs_shared_393d2cae;
1714
1824
  module.exports = __rspack_external__flusys_nestjs_shared_classes_6076a329;
1715
1825
 
1716
1826
 
1827
+ },
1828
+ 140(module) {
1829
+
1830
+ module.exports = __rspack_external__flusys_nestjs_shared_constants_81f33d5b;
1831
+
1832
+
1717
1833
  },
1718
1834
  236(module) {
1719
1835
 
package/fesm/261.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { IdentityResponseDto } from "@flusys/nestjs-shared/dtos";
2
2
  import { Expose, Type } from "class-transformer";
3
- import { IsArray, IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID, MaxLength, ValidateNested, ValidatorConstraint, registerDecorator } from "class-validator";
3
+ import { IsArray, IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsObject, IsOptional, IsString, IsUUID, MaxLength, ValidateIf, ValidateNested, ValidatorConstraint, registerDecorator } from "class-validator";
4
4
  import * as __rspack_external__nestjs_swagger_7cd450b5 from "@nestjs/swagger";
5
5
  export const __rspack_esm_id = 261;
6
6
  export const __rspack_esm_ids = [261];
@@ -692,7 +692,7 @@ email_send_dto_ts_decorate([
692
692
  description: 'Template ID (required if templateSlug not provided)'
693
693
  }),
694
694
  IsUUID(),
695
- IsOptional(),
695
+ ValidateIf((dto)=>dto.templateId != null || !dto.templateSlug),
696
696
  RequireTemplateIdOrSlug({
697
697
  message: 'Either templateId or templateSlug must be provided'
698
698
  }),
package/fesm/index.js CHANGED
@@ -552,8 +552,8 @@ var config = __webpack_require__(249);
552
552
  var dtos = __webpack_require__(684);
553
553
  // EXTERNAL MODULE: ./projects/nestjs-email/src/enums/index.ts
554
554
  var enums = __webpack_require__(315);
555
- // EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts + 7 modules
556
- var providers = __webpack_require__(603);
555
+ // EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts + 6 modules
556
+ var providers = __webpack_require__(652);
557
557
  // EXTERNAL MODULE: external "@flusys/nestjs-shared/utils"
558
558
  var utils_ = __webpack_require__(216);
559
559
  ;// CONCATENATED MODULE: ./projects/nestjs-email/src/utils/email-templates.util.ts
@@ -654,8 +654,8 @@ var email_constants = __webpack_require__(966);
654
654
  var controllers = __webpack_require__(574);
655
655
  // EXTERNAL MODULE: ./projects/nestjs-email/src/enums/email-provider-type.enum.ts
656
656
  var email_provider_type_enum = __webpack_require__(823);
657
- // EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts + 7 modules
658
- var src_providers = __webpack_require__(603);
657
+ // EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts + 6 modules
658
+ var src_providers = __webpack_require__(652);
659
659
  // EXTERNAL MODULE: ./projects/nestjs-email/src/services/index.ts
660
660
  var services = __webpack_require__(95);
661
661
  ;// CONCATENATED MODULE: ./projects/nestjs-email/src/modules/email.module.ts
@@ -473,8 +473,8 @@ var email_constants = __webpack_require__(966);
473
473
  var controllers = __webpack_require__(574);
474
474
  // EXTERNAL MODULE: ./projects/nestjs-email/src/enums/email-provider-type.enum.ts
475
475
  var email_provider_type_enum = __webpack_require__(823);
476
- // EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts + 7 modules
477
- var src_providers = __webpack_require__(603);
476
+ // EXTERNAL MODULE: ./projects/nestjs-email/src/providers/index.ts + 6 modules
477
+ var src_providers = __webpack_require__(652);
478
478
  // EXTERNAL MODULE: ./projects/nestjs-email/src/services/index.ts
479
479
  var services = __webpack_require__(95);
480
480
  ;// CONCATENATED MODULE: ./projects/nestjs-email/src/modules/email.module.ts
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "crypto";
2
- import { SYSTEM_MESSAGES } from "@flusys/nestjs-shared/constants";
3
2
  import * as __rspack_external__flusys_nestjs_shared_393d2cae from "@flusys/nestjs-shared";
3
+ import * as __rspack_external__flusys_nestjs_shared_constants_81f33d5b from "@flusys/nestjs-shared/constants";
4
4
  import * as __rspack_external__flusys_nestjs_shared_utils_9125c1f1 from "@flusys/nestjs-shared/utils";
5
5
  import * as __rspack_external__nestjs_common_a04939da from "@nestjs/common";
6
6
  export const __rspack_esm_id = 467;
@@ -35,7 +35,7 @@ __webpack_require__.d(__webpack_exports__, {
35
35
 
36
36
 
37
37
  },
38
- 603(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
38
+ 652(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
39
39
 
40
40
  // EXPORTS
41
41
  __webpack_require__.d(__webpack_exports__, {
@@ -98,13 +98,21 @@ function _ts_metadata(metadataKey, metadataValue) {
98
98
 
99
99
 
100
100
 
101
+ const MAX_CACHED_PROVIDERS = 50;
102
+ const PROVIDER_IDLE_TTL_MS = 30 * 60 * 1000;
101
103
  class EmailFactoryService {
102
104
  providerCache = new Map();
103
105
  // ─── Public API ─────────────────────────────────────────────────────────────
104
106
  async createProvider(config) {
105
107
  const cacheKey = this.generateCacheKey(config);
108
+ const now = Date.now();
106
109
  const cached = this.providerCache.get(cacheKey);
107
- if (cached) return cached;
110
+ if (cached) {
111
+ cached.lastUsed = now;
112
+ this.providerCache.delete(cacheKey);
113
+ this.providerCache.set(cacheKey, cached);
114
+ return cached.provider;
115
+ }
108
116
  const ProviderClass = EmailProviderRegistry.get(config.provider);
109
117
  if (!ProviderClass) {
110
118
  throw new common_.NotFoundException({
@@ -116,10 +124,32 @@ class EmailFactoryService {
116
124
  }
117
125
  });
118
126
  }
127
+ // The pending promise is cached so concurrent first sends share one client (one SMTP transport)
128
+ const entry = {
129
+ provider: this.instantiate(ProviderClass, config),
130
+ lastUsed: now
131
+ };
132
+ this.providerCache.set(cacheKey, entry);
133
+ entry.provider.catch(()=>{
134
+ if (this.providerCache.get(cacheKey) === entry) this.providerCache.delete(cacheKey);
135
+ });
136
+ this.evict(now);
137
+ return entry.provider;
138
+ }
139
+ async clearCache() {
140
+ const entries = Array.from(this.providerCache.values());
141
+ this.providerCache.clear();
142
+ await Promise.allSettled(entries.map((entry)=>this.closeEntry(entry)));
143
+ }
144
+ // ─── Lifecycle ──────────────────────────────────────────────────────────────
145
+ async onModuleDestroy() {
146
+ await this.clearCache();
147
+ }
148
+ // ─── Private Helpers ────────────────────────────────────────────────────────
149
+ async instantiate(ProviderClass, config) {
119
150
  try {
120
151
  const instance = new ProviderClass();
121
152
  await instance.initialize?.(config.config);
122
- this.providerCache.set(cacheKey, instance);
123
153
  return instance;
124
154
  } catch (error) {
125
155
  throw new common_.InternalServerErrorException({
@@ -132,26 +162,35 @@ class EmailFactoryService {
132
162
  });
133
163
  }
134
164
  }
135
- // ─── Lifecycle ──────────────────────────────────────────────────────────────
136
- async onModuleDestroy() {
137
- const closePromises = Array.from(this.providerCache.entries()).map(([, provider])=>this.closeProvider(provider));
138
- await Promise.allSettled(closePromises);
139
- this.providerCache.clear();
140
- }
141
- // ─── Private Helpers ────────────────────────────────────────────────────────
142
- generateCacheKey(config) {
143
- const sortedKeys = Object.keys(config.config || {}).sort();
144
- const configString = JSON.stringify(config.config, sortedKeys);
145
- const hash = createHash('sha256').update(configString).digest('hex').substring(0, 16);
146
- return `${config.provider}-${hash}`;
165
+ evict(now) {
166
+ for (const [key, entry] of this.providerCache){
167
+ const overCapacity = this.providerCache.size > MAX_CACHED_PROVIDERS;
168
+ if (!overCapacity && now - entry.lastUsed < PROVIDER_IDLE_TTL_MS) continue;
169
+ this.providerCache.delete(key);
170
+ void this.closeEntry(entry);
171
+ }
147
172
  }
148
- async closeProvider(provider) {
149
- if (!provider.close) return;
173
+ async closeEntry(entry) {
150
174
  try {
151
- await provider.close();
175
+ const provider = await entry.provider;
176
+ await provider.close?.();
152
177
  } catch {
153
- // Silent failure
178
+ // A client that failed to initialize or to close has nothing left to release
179
+ }
180
+ }
181
+ generateCacheKey(config) {
182
+ const hash = createHash('sha256').update(this.stableStringify(config.config ?? {})).digest('hex');
183
+ return `${config.provider}:${hash}`;
184
+ }
185
+ stableStringify(value) {
186
+ if (value === null || value === undefined) return 'null';
187
+ if (Array.isArray(value)) return `[${value.map((item)=>this.stableStringify(item)).join(',')}]`;
188
+ if (typeof value === 'object') {
189
+ const record = value;
190
+ const entries = Object.keys(record).filter((key)=>record[key] !== undefined).sort().map((key)=>`${JSON.stringify(key)}:${this.stableStringify(record[key])}`);
191
+ return `{${entries.join(',')}}`;
154
192
  }
193
+ return JSON.stringify(value);
155
194
  }
156
195
  extractErrorMessage(error) {
157
196
  return error instanceof Error ? error.message : 'Unknown error';
@@ -248,7 +287,8 @@ class SmtpProvider {
248
287
  }
249
288
  // ─── Private Helpers ────────────────────────────────────────────────────────
250
289
  formatFrom(from, fromName) {
251
- return fromName ? `"${fromName}" <${from}>` : from;
290
+ if (!from) return undefined;
291
+ return fromName ? `"${fromName.replace(/["\\]/g, '\\$&')}" <${from}>` : from;
252
292
  }
253
293
  joinAddresses(addresses) {
254
294
  return Array.isArray(addresses) ? addresses.join(', ') : addresses;
@@ -272,8 +312,8 @@ class SmtpProvider {
272
312
  }
273
313
  }
274
314
 
275
- ;// CONCATENATED MODULE: external "@flusys/nestjs-shared/constants"
276
-
315
+ // EXTERNAL MODULE: external "@flusys/nestjs-shared/constants"
316
+ var constants_ = __webpack_require__(140);
277
317
  // EXTERNAL MODULE: external "@flusys/nestjs-shared/utils"
278
318
  var utils_ = __webpack_require__(216);
279
319
  ;// CONCATENATED MODULE: ./projects/nestjs-email/src/providers/sendgrid-provider.ts
@@ -288,12 +328,13 @@ class SendGridProvider {
288
328
  try {
289
329
  // Optional peer dependency - loaded at runtime so bundlers don't statically resolve it
290
330
  const sgMailModule = await (0,utils_.loadOptionalModule)('@sendgrid/mail');
291
- this.sgMail = sgMailModule.default || sgMailModule;
331
+ const MailService = sgMailModule.MailService ?? sgMailModule.default?.MailService;
332
+ this.sgMail = MailService ? new MailService() : sgMailModule.default || sgMailModule;
292
333
  this.sgMail.setApiKey(config.apiKey);
293
334
  } catch {
294
335
  throw new common_.InternalServerErrorException({
295
336
  message: 'SendGrid initialization failed. Install: npm install @sendgrid/mail',
296
- messageKey: SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
337
+ messageKey: constants_.SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
297
338
  messageVariables: {
298
339
  sdk: 'sendgrid'
299
340
  }
@@ -308,7 +349,8 @@ class SendGridProvider {
308
349
  try {
309
350
  const msg = this.buildMessage(options, true);
310
351
  const [response] = await this.sgMail.send(msg);
311
- const messageId = response.headers['x-message-id'] || response.headers['X-Message-Id'];
352
+ const headers = response?.headers ?? {};
353
+ const messageId = headers['x-message-id'] || headers['X-Message-Id'];
312
354
  return {
313
355
  success: true,
314
356
  messageId
@@ -349,7 +391,7 @@ class SendGridProvider {
349
391
  buildMessage(options, includeExtras) {
350
392
  const msg = {
351
393
  to: this.toArray(options.to),
352
- from: options.fromName ? {
394
+ from: options.fromName && options.from ? {
353
395
  email: options.from,
354
396
  name: options.fromName
355
397
  } : options.from,
@@ -410,7 +452,7 @@ class MailgunProvider {
410
452
  } catch {
411
453
  throw new common_.InternalServerErrorException({
412
454
  message: 'Mailgun initialization failed. Install: npm install mailgun.js form-data',
413
- messageKey: SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
455
+ messageKey: constants_.SYSTEM_MESSAGES.SDK_NOT_INSTALLED,
414
456
  messageVariables: {
415
457
  sdk: 'mailgun'
416
458
  }
@@ -456,7 +498,7 @@ class MailgunProvider {
456
498
  // ─── Private Helpers ────────────────────────────────────────────────────────
457
499
  buildMessageData(options) {
458
500
  const data = {
459
- from: options.fromName ? `${options.fromName} <${options.from}>` : options.from,
501
+ from: options.fromName && options.from ? `"${options.fromName.replace(/["\\]/g, '\\$&')}" <${options.from}>` : options.from,
460
502
  to: this.joinAddresses(options.to),
461
503
  subject: options.subject
462
504
  };
@@ -496,6 +538,12 @@ class MailgunProvider {
496
538
  module.exports = __rspack_external__flusys_nestjs_shared_393d2cae;
497
539
 
498
540
 
541
+ },
542
+ 140(module) {
543
+
544
+ module.exports = __rspack_external__flusys_nestjs_shared_constants_81f33d5b;
545
+
546
+
499
547
  },
500
548
  216(module) {
501
549
 
@@ -515,7 +563,7 @@ import { __webpack_require__ } from '../runtime.js';
515
563
  var __webpack_exec__ = function(moduleId) { return __webpack_require__(__webpack_require__.s = moduleId); }
516
564
  import * as __rspack_chunk_1 from './index.js';
517
565
  __webpack_require__.C(__rspack_chunk_1);
518
- var __webpack_exports__ = __webpack_exec__(603);var __webpack_exports__EmailFactoryService = __webpack_exports__.c;
566
+ var __webpack_exports__ = __webpack_exec__(652);var __webpack_exports__EmailFactoryService = __webpack_exports__.c;
519
567
  var __webpack_exports__EmailProviderRegistry = __webpack_exports__.xC;
520
568
  var __webpack_exports__MailgunProvider = __webpack_exports__.Xp;
521
569
  var __webpack_exports__SendGridProvider = __webpack_exports__.Iw;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flusys/nestjs-email",
3
- "version": "9.1.2",
3
+ "version": "9.2.1",
4
4
  "description": "Modular email package with SMTP, SendGrid, and Mailgun providers",
5
5
  "type": "module",
6
6
  "main": "./fesm/index.js",
@@ -98,7 +98,7 @@
98
98
  }
99
99
  },
100
100
  "dependencies": {
101
- "@flusys/nestjs-core": "9.1.2",
102
- "@flusys/nestjs-shared": "9.1.2"
101
+ "@flusys/nestjs-core": "9.2.1",
102
+ "@flusys/nestjs-shared": "9.2.1"
103
103
  }
104
104
  }
@@ -3,8 +3,12 @@ import { type IEmailProvider, type IEmailProviderConfig } from '../interfaces/in
3
3
  export declare class EmailFactoryService implements OnModuleDestroy {
4
4
  private readonly providerCache;
5
5
  createProvider(config: IEmailProviderConfig): Promise<IEmailProvider>;
6
+ clearCache(): Promise<void>;
6
7
  onModuleDestroy(): Promise<void>;
8
+ private instantiate;
9
+ private evict;
10
+ private closeEntry;
7
11
  private generateCacheKey;
8
- private closeProvider;
12
+ private stableStringify;
9
13
  private extractErrorMessage;
10
14
  }
@@ -13,9 +13,10 @@ export declare class EmailProviderConfigService extends ApiService<CreateEmailCo
13
13
  protected utilsService: UtilsService;
14
14
  private readonly emailConfig;
15
15
  constructor(cacheManager: HybridCache, utilsService: UtilsService, emailConfig: EmailConfigService, dataSourceProvider: EmailDataSourceProvider);
16
- protected beforeDeleteOperation(dto: DeleteDto, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
16
+ protected beforeUpdateOperation(dto: UpdateEmailConfigDto | UpdateEmailConfigDto[], user: ILoggedUserInfo | null): Promise<void>;
17
+ protected beforeDeleteOperation(dto: DeleteDto, user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
17
18
  convertSingleDtoToEntity(dto: CreateEmailConfigDto | UpdateEmailConfigDto, user: ILoggedUserInfo | null): Promise<EmailConfigBase>;
18
- getSelectQuery(query: SelectQueryBuilder<EmailConfigBase>, _user: ILoggedUserInfo | null, select?: string[]): Promise<{
19
+ getSelectQuery(query: SelectQueryBuilder<EmailConfigBase>, user: ILoggedUserInfo | null, select?: string[]): Promise<{
19
20
  query: SelectQueryBuilder<EmailConfig>;
20
21
  isRaw: boolean;
21
22
  }>;
@@ -25,4 +26,5 @@ export declare class EmailProviderConfigService extends ApiService<CreateEmailCo
25
26
  }>;
26
27
  findByIdDirect(id: string): Promise<EmailConfigBase | null>;
27
28
  getDefaultConfig(user?: ILoggedUserInfo): Promise<EmailConfigBase | null>;
29
+ private assertInUserCompany;
28
30
  }
@@ -1,8 +1,8 @@
1
1
  import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
2
- import { FilterAndPaginationDto } from '@flusys/nestjs-shared/dtos';
2
+ import { DeleteDto, FilterAndPaginationDto } from '@flusys/nestjs-shared/dtos';
3
3
  import { type ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
4
4
  import { UtilsService } from '@flusys/nestjs-shared/modules';
5
- import { SelectQueryBuilder } from 'typeorm';
5
+ import { type QueryRunner, SelectQueryBuilder } from 'typeorm';
6
6
  import { CreateEmailTemplateDto, UpdateEmailTemplateDto } from '../dtos/index.js';
7
7
  import { EmailTemplate, EmailTemplateBase } from '../entities/index.js';
8
8
  import { type IEmailTemplate } from '../interfaces/index.js';
@@ -13,8 +13,10 @@ export declare class EmailTemplateService extends ApiService<CreateEmailTemplate
13
13
  protected utilsService: UtilsService;
14
14
  private readonly emailConfig;
15
15
  constructor(cacheManager: HybridCache, utilsService: UtilsService, emailConfig: EmailConfigService, dataSourceProvider: EmailDataSourceProvider);
16
+ protected beforeUpdateOperation(dto: UpdateEmailTemplateDto | UpdateEmailTemplateDto[], user: ILoggedUserInfo | null): Promise<void>;
17
+ protected beforeDeleteOperation(dto: DeleteDto, user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
16
18
  convertSingleDtoToEntity(dto: CreateEmailTemplateDto | UpdateEmailTemplateDto, user: ILoggedUserInfo | null): Promise<EmailTemplateBase>;
17
- getSelectQuery(query: SelectQueryBuilder<EmailTemplateBase>, _user: ILoggedUserInfo | null, select?: string[]): Promise<{
19
+ getSelectQuery(query: SelectQueryBuilder<EmailTemplateBase>, user: ILoggedUserInfo | null, select?: string[]): Promise<{
18
20
  query: SelectQueryBuilder<EmailTemplate>;
19
21
  isRaw: boolean;
20
22
  }>;
@@ -24,5 +26,6 @@ export declare class EmailTemplateService extends ApiService<CreateEmailTemplate
24
26
  }>;
25
27
  findByIdDirect(id: string): Promise<EmailTemplateBase | null>;
26
28
  findBySlug(slug: string, user?: ILoggedUserInfo): Promise<EmailTemplateBase | null>;
29
+ private assertInUserCompany;
27
30
  private incrementSchemaVersionIfChanged;
28
31
  }
@@ -0,0 +1,3 @@
1
+ import { type ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
2
+ import { type ObjectLiteral, type Repository } from 'typeorm';
3
+ export declare function assertRowsInUserCompany<T extends ObjectLiteral>(repository: Repository<T>, ids: string[], user: ILoggedUserInfo | null | undefined, isCompanyFeatureEnabled: boolean, entityName: string): Promise<void>;