@flusys/nestjs-email 6.4.0 → 7.0.0-beta.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/cjs/config/event-actions.js +34 -0
- package/cjs/config/index.js +1 -0
- package/cjs/config/message-keys.js +6 -0
- package/cjs/modules/email.module.js +1 -0
- package/cjs/services/email-provider-config.service.js +25 -0
- package/cjs/services/email-provider-config.service.spec.js +18 -0
- package/cjs/services/email-send.service.js +40 -4
- package/cjs/services/email-send.service.spec.js +68 -0
- package/config/event-actions.d.ts +10 -0
- package/config/index.d.ts +1 -0
- package/config/message-keys.d.ts +3 -0
- package/fesm/config/event-actions.js +13 -0
- package/fesm/config/index.js +1 -0
- package/fesm/config/message-keys.js +3 -0
- package/fesm/modules/email.module.js +2 -1
- package/fesm/services/email-provider-config.service.js +26 -1
- package/fesm/services/email-provider-config.service.spec.js +18 -0
- package/fesm/services/email-send.service.js +40 -4
- package/fesm/services/email-send.service.spec.js +68 -0
- package/interfaces/email-module-options.interface.d.ts +2 -0
- package/package.json +3 -3
- package/services/email-provider-config.service.d.ts +3 -2
- package/services/email-send.service.d.ts +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain event actions published by the email module, alongside the CRUD
|
|
3
|
+
* lifecycle actions every ApiService emits.
|
|
4
|
+
*/ "use strict";
|
|
5
|
+
Object.defineProperty(exports, "__esModule", {
|
|
6
|
+
value: true
|
|
7
|
+
});
|
|
8
|
+
function _export(target, all) {
|
|
9
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
_export(exports, {
|
|
15
|
+
get EMAIL_EVENT_ACTIONS () {
|
|
16
|
+
return EMAIL_EVENT_ACTIONS;
|
|
17
|
+
},
|
|
18
|
+
get EMAIL_EVENT_ENTITIES () {
|
|
19
|
+
return EMAIL_EVENT_ENTITIES;
|
|
20
|
+
},
|
|
21
|
+
get EMAIL_EVENT_MODULE () {
|
|
22
|
+
return EMAIL_EVENT_MODULE;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const EMAIL_EVENT_ACTIONS = {
|
|
26
|
+
SENT: 'sent',
|
|
27
|
+
FAILED: 'failed'
|
|
28
|
+
};
|
|
29
|
+
const EMAIL_EVENT_ENTITIES = {
|
|
30
|
+
MESSAGE: 'email_message',
|
|
31
|
+
CONFIG: 'emailConfig',
|
|
32
|
+
TEMPLATE: 'emailTemplate'
|
|
33
|
+
};
|
|
34
|
+
const EMAIL_EVENT_MODULE = 'email';
|
package/cjs/config/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
_export_star(require("./email.constants"), exports);
|
|
6
|
+
_export_star(require("./event-actions"), exports);
|
|
6
7
|
_export_star(require("./message-keys"), exports);
|
|
7
8
|
function _export_star(from, to) {
|
|
8
9
|
Object.keys(from).forEach(function(k) {
|
|
@@ -10,6 +10,9 @@ function _export(target, all) {
|
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
12
|
_export(exports, {
|
|
13
|
+
get EMAIL_CONFIG_MESSAGES () {
|
|
14
|
+
return EMAIL_CONFIG_MESSAGES;
|
|
15
|
+
},
|
|
13
16
|
get EMAIL_SEND_MESSAGES () {
|
|
14
17
|
return EMAIL_SEND_MESSAGES;
|
|
15
18
|
},
|
|
@@ -21,6 +24,9 @@ const EMAIL_TEMPLATE_MESSAGES = {
|
|
|
21
24
|
GET_SUCCESS: 'email.template.get.success',
|
|
22
25
|
NOT_FOUND: 'email.template.not.found'
|
|
23
26
|
};
|
|
27
|
+
const EMAIL_CONFIG_MESSAGES = {
|
|
28
|
+
DEFAULT_DELETE_FORBIDDEN: 'email.config.default.delete.forbidden'
|
|
29
|
+
};
|
|
24
30
|
const EMAIL_SEND_MESSAGES = {
|
|
25
31
|
SUCCESS: 'email.send.success',
|
|
26
32
|
FAILED: 'email.send.failed',
|
|
@@ -12,6 +12,8 @@ const _classes = require("@flusys/nestjs-shared/classes");
|
|
|
12
12
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
13
|
const _utils = require("@flusys/nestjs-shared/utils");
|
|
14
14
|
const _common = require("@nestjs/common");
|
|
15
|
+
const _typeorm = require("typeorm");
|
|
16
|
+
const _config = require("../config");
|
|
15
17
|
const _entities = require("../entities");
|
|
16
18
|
const _emailconfigservice = require("./email-config.service");
|
|
17
19
|
const _emaildatasourceprovider = require("./email-datasource.provider");
|
|
@@ -46,6 +48,29 @@ let EmailProviderConfigService = class EmailProviderConfigService extends _class
|
|
|
46
48
|
resolveEntity() {
|
|
47
49
|
return this.emailConfig.isCompanyFeatureEnabled() ? _entities.EmailConfigWithCompany : _entities.EmailConfig;
|
|
48
50
|
}
|
|
51
|
+
async beforeDeleteOperation(dto, _user, _queryRunner) {
|
|
52
|
+
const ids = Array.isArray(dto.id) ? dto.id : [
|
|
53
|
+
dto.id
|
|
54
|
+
];
|
|
55
|
+
await this.ensureDataSourceRepository();
|
|
56
|
+
const defaultConfigs = await this.repository.find({
|
|
57
|
+
where: {
|
|
58
|
+
id: (0, _typeorm.In)(ids),
|
|
59
|
+
isDefault: true,
|
|
60
|
+
deletedAt: (0, _typeorm.IsNull)()
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
if (defaultConfigs.length > 0) {
|
|
64
|
+
const names = defaultConfigs.map((c)=>c.name).join(', ');
|
|
65
|
+
throw new _common.ForbiddenException({
|
|
66
|
+
message: `Cannot delete the default email config(s): ${names}. Set another config as default first.`,
|
|
67
|
+
messageKey: _config.EMAIL_CONFIG_MESSAGES.DEFAULT_DELETE_FORBIDDEN,
|
|
68
|
+
messageVariables: {
|
|
69
|
+
names
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
49
74
|
async convertSingleDtoToEntity(dto, user) {
|
|
50
75
|
const entity = {
|
|
51
76
|
...dto
|
|
@@ -244,4 +244,22 @@ describe('EmailProviderConfigService', ()=>{
|
|
|
244
244
|
});
|
|
245
245
|
});
|
|
246
246
|
});
|
|
247
|
+
describe('beforeDeleteOperation', ()=>{
|
|
248
|
+
it('rejects deleting the default email config', async ()=>{
|
|
249
|
+
mockRepo.find.mockResolvedValue([
|
|
250
|
+
buildEmailConfig({
|
|
251
|
+
isDefault: true
|
|
252
|
+
})
|
|
253
|
+
]);
|
|
254
|
+
await expect(service.beforeDeleteOperation({
|
|
255
|
+
id: 'config-uuid-1'
|
|
256
|
+
}, null, {})).rejects.toThrow('Cannot delete the default email config');
|
|
257
|
+
});
|
|
258
|
+
it('allows deleting a non-default email config', async ()=>{
|
|
259
|
+
mockRepo.find.mockResolvedValue([]);
|
|
260
|
+
await expect(service.beforeDeleteOperation({
|
|
261
|
+
id: 'config-uuid-1'
|
|
262
|
+
}, null, {})).resolves.toBeUndefined();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
247
265
|
});
|
|
@@ -9,10 +9,11 @@ Object.defineProperty(exports, "EmailSendService", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _nestjsshared = require("@flusys/nestjs-shared");
|
|
12
|
-
const
|
|
12
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
13
13
|
const _interfaces = require("@flusys/nestjs-shared/interfaces");
|
|
14
14
|
const _utils = require("@flusys/nestjs-shared/utils");
|
|
15
15
|
const _common = require("@nestjs/common");
|
|
16
|
+
const _config = require("../config");
|
|
16
17
|
const _emailconfigservice = require("./email-config.service");
|
|
17
18
|
const _dtos = require("../dtos");
|
|
18
19
|
const _providers = require("../providers");
|
|
@@ -51,7 +52,7 @@ let EmailSendService = class EmailSendService {
|
|
|
51
52
|
async sendEmail(dto, user) {
|
|
52
53
|
const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
|
|
53
54
|
const sender = this.resolveSender(dto, config);
|
|
54
|
-
|
|
55
|
+
const result = await provider.sendEmail({
|
|
55
56
|
to: dto.to,
|
|
56
57
|
cc: dto.cc,
|
|
57
58
|
bcc: dto.bcc,
|
|
@@ -62,13 +63,17 @@ let EmailSendService = class EmailSendService {
|
|
|
62
63
|
replyTo: dto.replyTo,
|
|
63
64
|
attachments: this.buildAttachments(dto.attachments)
|
|
64
65
|
});
|
|
66
|
+
await this.publishSendEvent(result, dto.to, user, {
|
|
67
|
+
emailConfigId: config.id
|
|
68
|
+
});
|
|
69
|
+
return result;
|
|
65
70
|
}
|
|
66
71
|
async sendTemplateEmail(dto, user) {
|
|
67
72
|
const template = await this.resolveTemplate(dto, user);
|
|
68
73
|
const { subject, html, text } = this.buildTemplateContent(template, dto.variables);
|
|
69
74
|
const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
|
|
70
75
|
const sender = this.resolveSender(dto, config);
|
|
71
|
-
|
|
76
|
+
const result = await provider.sendEmail({
|
|
72
77
|
to: dto.to,
|
|
73
78
|
cc: dto.cc,
|
|
74
79
|
bcc: dto.bcc,
|
|
@@ -79,6 +84,12 @@ let EmailSendService = class EmailSendService {
|
|
|
79
84
|
replyTo: dto.replyTo,
|
|
80
85
|
attachments: this.buildAttachments(dto.attachments)
|
|
81
86
|
});
|
|
87
|
+
await this.publishSendEvent(result, dto.to, user, {
|
|
88
|
+
emailConfigId: config.id,
|
|
89
|
+
templateId: template.id,
|
|
90
|
+
templateSlug: template.slug
|
|
91
|
+
});
|
|
92
|
+
return result;
|
|
82
93
|
}
|
|
83
94
|
async sendTestEmail(emailConfigId, recipient, user) {
|
|
84
95
|
const { provider, config } = await this.getEmailProviderWithConfig(emailConfigId, user);
|
|
@@ -94,6 +105,10 @@ let EmailSendService = class EmailSendService {
|
|
|
94
105
|
from: config.fromEmail || undefined,
|
|
95
106
|
fromName: config.fromName || this.emailConfigService.getDefaultFromName()
|
|
96
107
|
});
|
|
108
|
+
await this.publishSendEvent(result, recipient, user, {
|
|
109
|
+
emailConfigId: config.id,
|
|
110
|
+
isTest: true
|
|
111
|
+
});
|
|
97
112
|
return result;
|
|
98
113
|
}
|
|
99
114
|
// ─── Private: Provider & Config Resolution ──────────────────────────────────
|
|
@@ -137,7 +152,7 @@ let EmailSendService = class EmailSendService {
|
|
|
137
152
|
}
|
|
138
153
|
// ─── Private: Template Resolution ───────────────────────────────────────────
|
|
139
154
|
async resolveTemplate(dto, user) {
|
|
140
|
-
let template
|
|
155
|
+
let template;
|
|
141
156
|
if (dto.templateId) {
|
|
142
157
|
template = await this.emailTemplateService.findByIdDirect(dto.templateId);
|
|
143
158
|
} else if (dto.templateSlug) {
|
|
@@ -213,6 +228,27 @@ let EmailSendService = class EmailSendService {
|
|
|
213
228
|
</div>
|
|
214
229
|
`;
|
|
215
230
|
}
|
|
231
|
+
async publishSendEvent(result, to, user, metadata) {
|
|
232
|
+
const recipients = Array.isArray(to) ? to : [
|
|
233
|
+
to
|
|
234
|
+
];
|
|
235
|
+
await (0, _classes.publishDomainEvent)({
|
|
236
|
+
module: _config.EMAIL_EVENT_MODULE,
|
|
237
|
+
entity: _config.EMAIL_EVENT_ENTITIES.MESSAGE,
|
|
238
|
+
action: result.success ? _config.EMAIL_EVENT_ACTIONS.SENT : _config.EMAIL_EVENT_ACTIONS.FAILED,
|
|
239
|
+
ids: result.messageId ? [
|
|
240
|
+
result.messageId
|
|
241
|
+
] : [],
|
|
242
|
+
metadata: {
|
|
243
|
+
...metadata,
|
|
244
|
+
recipientCount: recipients.length,
|
|
245
|
+
...result.error ? {
|
|
246
|
+
error: result.error
|
|
247
|
+
} : {}
|
|
248
|
+
},
|
|
249
|
+
user: user ?? null
|
|
250
|
+
});
|
|
251
|
+
}
|
|
216
252
|
constructor(emailFactory, emailConfigService, emailProviderConfigService, emailTemplateService){
|
|
217
253
|
_define_property(this, "emailFactory", void 0);
|
|
218
254
|
_define_property(this, "emailConfigService", void 0);
|
|
@@ -8,6 +8,7 @@ const _loggedusermock = require("@test-utils/mocks/logged-user.mock");
|
|
|
8
8
|
const _providers = require("../providers");
|
|
9
9
|
const _emailconfigservice = require("./email-config.service");
|
|
10
10
|
const _emailproviderconfigservice = require("./email-provider-config.service");
|
|
11
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
11
12
|
const _emailsendservice = require("./email-send.service");
|
|
12
13
|
const _emailtemplateservice = require("./email-template.service");
|
|
13
14
|
function buildConfig(overrides = {}) {
|
|
@@ -543,4 +544,71 @@ describe('EmailSendService', ()=>{
|
|
|
543
544
|
expect(payload.html).not.toContain('<script>alert(1)</script>');
|
|
544
545
|
});
|
|
545
546
|
});
|
|
547
|
+
describe('domain events', ()=>{
|
|
548
|
+
let publish;
|
|
549
|
+
beforeEach(()=>{
|
|
550
|
+
publish = jest.fn().mockResolvedValue(undefined);
|
|
551
|
+
_classes.EventBusRegistry.reset();
|
|
552
|
+
_classes.EventBusRegistry.setBus({
|
|
553
|
+
publish,
|
|
554
|
+
subscribe: jest.fn(),
|
|
555
|
+
close: jest.fn()
|
|
556
|
+
});
|
|
557
|
+
_classes.EventBusRegistry.configureModule('email', {
|
|
558
|
+
enabled: true
|
|
559
|
+
});
|
|
560
|
+
mockEmailProviderConfigService.findByIdDirect.mockResolvedValue(buildConfig());
|
|
561
|
+
});
|
|
562
|
+
afterEach(()=>{
|
|
563
|
+
_classes.EventBusRegistry.reset();
|
|
564
|
+
});
|
|
565
|
+
it('publishes email.email_message.sent for a successful send', async ()=>{
|
|
566
|
+
await service.sendEmail({
|
|
567
|
+
to: 'user@example.com',
|
|
568
|
+
subject: 'Hi',
|
|
569
|
+
emailConfigId: 'config-uuid-1'
|
|
570
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
571
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
572
|
+
name: 'email.email_message.sent',
|
|
573
|
+
payload: expect.objectContaining({
|
|
574
|
+
ids: [
|
|
575
|
+
'msg-1'
|
|
576
|
+
],
|
|
577
|
+
metadata: expect.objectContaining({
|
|
578
|
+
recipientCount: 1
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
}));
|
|
582
|
+
});
|
|
583
|
+
it('publishes email.email_message.failed when the provider reports a failure', async ()=>{
|
|
584
|
+
mockProvider.sendEmail.mockResolvedValue({
|
|
585
|
+
success: false,
|
|
586
|
+
error: 'smtp refused'
|
|
587
|
+
});
|
|
588
|
+
await service.sendEmail({
|
|
589
|
+
to: 'user@example.com',
|
|
590
|
+
subject: 'Hi',
|
|
591
|
+
emailConfigId: 'config-uuid-1'
|
|
592
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
593
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
594
|
+
name: 'email.email_message.failed',
|
|
595
|
+
payload: expect.objectContaining({
|
|
596
|
+
metadata: expect.objectContaining({
|
|
597
|
+
error: 'smtp refused'
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
}));
|
|
601
|
+
});
|
|
602
|
+
it('does not publish when the email module has events disabled', async ()=>{
|
|
603
|
+
_classes.EventBusRegistry.configureModule('email', {
|
|
604
|
+
enabled: false
|
|
605
|
+
});
|
|
606
|
+
await service.sendEmail({
|
|
607
|
+
to: 'user@example.com',
|
|
608
|
+
subject: 'Hi',
|
|
609
|
+
emailConfigId: 'config-uuid-1'
|
|
610
|
+
}, (0, _loggedusermock.buildMockUser)());
|
|
611
|
+
expect(publish).not.toHaveBeenCalled();
|
|
612
|
+
});
|
|
613
|
+
});
|
|
546
614
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const EMAIL_EVENT_ACTIONS: {
|
|
2
|
+
readonly SENT: "sent";
|
|
3
|
+
readonly FAILED: "failed";
|
|
4
|
+
};
|
|
5
|
+
export declare const EMAIL_EVENT_ENTITIES: {
|
|
6
|
+
readonly MESSAGE: "email_message";
|
|
7
|
+
readonly CONFIG: "emailConfig";
|
|
8
|
+
readonly TEMPLATE: "emailTemplate";
|
|
9
|
+
};
|
|
10
|
+
export declare const EMAIL_EVENT_MODULE = "email";
|
package/config/index.d.ts
CHANGED
package/config/message-keys.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ export declare const EMAIL_TEMPLATE_MESSAGES: {
|
|
|
2
2
|
readonly GET_SUCCESS: "email.template.get.success";
|
|
3
3
|
readonly NOT_FOUND: "email.template.not.found";
|
|
4
4
|
};
|
|
5
|
+
export declare const EMAIL_CONFIG_MESSAGES: {
|
|
6
|
+
readonly DEFAULT_DELETE_FORBIDDEN: "email.config.default.delete.forbidden";
|
|
7
|
+
};
|
|
5
8
|
export declare const EMAIL_SEND_MESSAGES: {
|
|
6
9
|
readonly SUCCESS: "email.send.success";
|
|
7
10
|
readonly FAILED: "email.send.failed";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain event actions published by the email module, alongside the CRUD
|
|
3
|
+
* lifecycle actions every ApiService emits.
|
|
4
|
+
*/ export const EMAIL_EVENT_ACTIONS = {
|
|
5
|
+
SENT: 'sent',
|
|
6
|
+
FAILED: 'failed'
|
|
7
|
+
};
|
|
8
|
+
/** Every entity the email module publishes under, ApiService backed or not */ export const EMAIL_EVENT_ENTITIES = {
|
|
9
|
+
MESSAGE: 'email_message',
|
|
10
|
+
CONFIG: 'emailConfig',
|
|
11
|
+
TEMPLATE: 'emailTemplate'
|
|
12
|
+
};
|
|
13
|
+
export const EMAIL_EVENT_MODULE = 'email';
|
package/fesm/config/index.js
CHANGED
|
@@ -3,6 +3,9 @@ export const EMAIL_TEMPLATE_MESSAGES = {
|
|
|
3
3
|
GET_SUCCESS: 'email.template.get.success',
|
|
4
4
|
NOT_FOUND: 'email.template.not.found'
|
|
5
5
|
};
|
|
6
|
+
export const EMAIL_CONFIG_MESSAGES = {
|
|
7
|
+
DEFAULT_DELETE_FORBIDDEN: 'email.config.default.delete.forbidden'
|
|
8
|
+
};
|
|
6
9
|
export const EMAIL_SEND_MESSAGES = {
|
|
7
10
|
SUCCESS: 'email.send.success',
|
|
8
11
|
FAILED: 'email.send.failed',
|
|
@@ -4,7 +4,7 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
4
4
|
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
}
|
|
7
|
-
import { CacheModule, UtilsModule } from '@flusys/nestjs-shared/modules';
|
|
7
|
+
import { CacheModule, UtilsModule, createModuleEventsProvider } from '@flusys/nestjs-shared/modules';
|
|
8
8
|
import { Module } from '@nestjs/common';
|
|
9
9
|
import { EMAIL_MODULE_OPTIONS } from '../config/email.constants';
|
|
10
10
|
import { EmailConfigController, EmailSendController, EmailTemplateController } from '../controllers';
|
|
@@ -68,6 +68,7 @@ export class EmailModule {
|
|
|
68
68
|
useValue: options
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
providers.push(createModuleEventsProvider('email', EMAIL_MODULE_OPTIONS));
|
|
71
72
|
return providers;
|
|
72
73
|
}
|
|
73
74
|
static createAsyncProviders(options) {
|
|
@@ -28,7 +28,9 @@ function _ts_param(paramIndex, decorator) {
|
|
|
28
28
|
import { HybridCache, ApiService } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
30
|
import { applyCompanyFilter, buildCompanyWhereCondition } from '@flusys/nestjs-shared/utils';
|
|
31
|
-
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
|
+
import { ForbiddenException, Inject, Injectable, Scope } from '@nestjs/common';
|
|
32
|
+
import { In, IsNull } from 'typeorm';
|
|
33
|
+
import { EMAIL_CONFIG_MESSAGES } from '../config';
|
|
32
34
|
import { EmailConfig, EmailConfigWithCompany } from '../entities';
|
|
33
35
|
import { EmailConfigService } from './email-config.service';
|
|
34
36
|
import { EmailDataSourceProvider } from './email-datasource.provider';
|
|
@@ -36,6 +38,29 @@ export class EmailProviderConfigService extends ApiService {
|
|
|
36
38
|
resolveEntity() {
|
|
37
39
|
return this.emailConfig.isCompanyFeatureEnabled() ? EmailConfigWithCompany : EmailConfig;
|
|
38
40
|
}
|
|
41
|
+
async beforeDeleteOperation(dto, _user, _queryRunner) {
|
|
42
|
+
const ids = Array.isArray(dto.id) ? dto.id : [
|
|
43
|
+
dto.id
|
|
44
|
+
];
|
|
45
|
+
await this.ensureDataSourceRepository();
|
|
46
|
+
const defaultConfigs = await this.repository.find({
|
|
47
|
+
where: {
|
|
48
|
+
id: In(ids),
|
|
49
|
+
isDefault: true,
|
|
50
|
+
deletedAt: IsNull()
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (defaultConfigs.length > 0) {
|
|
54
|
+
const names = defaultConfigs.map((c)=>c.name).join(', ');
|
|
55
|
+
throw new ForbiddenException({
|
|
56
|
+
message: `Cannot delete the default email config(s): ${names}. Set another config as default first.`,
|
|
57
|
+
messageKey: EMAIL_CONFIG_MESSAGES.DEFAULT_DELETE_FORBIDDEN,
|
|
58
|
+
messageVariables: {
|
|
59
|
+
names
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
39
64
|
async convertSingleDtoToEntity(dto, user) {
|
|
40
65
|
const entity = {
|
|
41
66
|
...dto
|
|
@@ -240,4 +240,22 @@ describe('EmailProviderConfigService', ()=>{
|
|
|
240
240
|
});
|
|
241
241
|
});
|
|
242
242
|
});
|
|
243
|
+
describe('beforeDeleteOperation', ()=>{
|
|
244
|
+
it('rejects deleting the default email config', async ()=>{
|
|
245
|
+
mockRepo.find.mockResolvedValue([
|
|
246
|
+
buildEmailConfig({
|
|
247
|
+
isDefault: true
|
|
248
|
+
})
|
|
249
|
+
]);
|
|
250
|
+
await expect(service.beforeDeleteOperation({
|
|
251
|
+
id: 'config-uuid-1'
|
|
252
|
+
}, null, {})).rejects.toThrow('Cannot delete the default email config');
|
|
253
|
+
});
|
|
254
|
+
it('allows deleting a non-default email config', async ()=>{
|
|
255
|
+
mockRepo.find.mockResolvedValue([]);
|
|
256
|
+
await expect(service.beforeDeleteOperation({
|
|
257
|
+
id: 'config-uuid-1'
|
|
258
|
+
}, null, {})).resolves.toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
243
261
|
});
|
|
@@ -26,10 +26,11 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { LogAction } from '@flusys/nestjs-shared';
|
|
29
|
-
import {
|
|
29
|
+
import { publishDomainEvent } from '@flusys/nestjs-shared/classes';
|
|
30
30
|
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
31
31
|
import { escapeHtmlVariables, validateCompanyOwnership } from '@flusys/nestjs-shared/utils';
|
|
32
32
|
import { BadRequestException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
|
|
33
|
+
import { EMAIL_EVENT_ACTIONS, EMAIL_EVENT_ENTITIES, EMAIL_EVENT_MODULE, EMAIL_SEND_MESSAGES } from '../config';
|
|
33
34
|
import { EmailConfigService } from './email-config.service';
|
|
34
35
|
import { SendEmailDto, SendTemplateEmailDto } from '../dtos';
|
|
35
36
|
import { EmailFactoryService } from '../providers';
|
|
@@ -41,7 +42,7 @@ export class EmailSendService {
|
|
|
41
42
|
async sendEmail(dto, user) {
|
|
42
43
|
const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
|
|
43
44
|
const sender = this.resolveSender(dto, config);
|
|
44
|
-
|
|
45
|
+
const result = await provider.sendEmail({
|
|
45
46
|
to: dto.to,
|
|
46
47
|
cc: dto.cc,
|
|
47
48
|
bcc: dto.bcc,
|
|
@@ -52,13 +53,17 @@ export class EmailSendService {
|
|
|
52
53
|
replyTo: dto.replyTo,
|
|
53
54
|
attachments: this.buildAttachments(dto.attachments)
|
|
54
55
|
});
|
|
56
|
+
await this.publishSendEvent(result, dto.to, user, {
|
|
57
|
+
emailConfigId: config.id
|
|
58
|
+
});
|
|
59
|
+
return result;
|
|
55
60
|
}
|
|
56
61
|
async sendTemplateEmail(dto, user) {
|
|
57
62
|
const template = await this.resolveTemplate(dto, user);
|
|
58
63
|
const { subject, html, text } = this.buildTemplateContent(template, dto.variables);
|
|
59
64
|
const { provider, config } = await this.getEmailProviderWithConfig(dto.emailConfigId, user);
|
|
60
65
|
const sender = this.resolveSender(dto, config);
|
|
61
|
-
|
|
66
|
+
const result = await provider.sendEmail({
|
|
62
67
|
to: dto.to,
|
|
63
68
|
cc: dto.cc,
|
|
64
69
|
bcc: dto.bcc,
|
|
@@ -69,6 +74,12 @@ export class EmailSendService {
|
|
|
69
74
|
replyTo: dto.replyTo,
|
|
70
75
|
attachments: this.buildAttachments(dto.attachments)
|
|
71
76
|
});
|
|
77
|
+
await this.publishSendEvent(result, dto.to, user, {
|
|
78
|
+
emailConfigId: config.id,
|
|
79
|
+
templateId: template.id,
|
|
80
|
+
templateSlug: template.slug
|
|
81
|
+
});
|
|
82
|
+
return result;
|
|
72
83
|
}
|
|
73
84
|
async sendTestEmail(emailConfigId, recipient, user) {
|
|
74
85
|
const { provider, config } = await this.getEmailProviderWithConfig(emailConfigId, user);
|
|
@@ -84,6 +95,10 @@ export class EmailSendService {
|
|
|
84
95
|
from: config.fromEmail || undefined,
|
|
85
96
|
fromName: config.fromName || this.emailConfigService.getDefaultFromName()
|
|
86
97
|
});
|
|
98
|
+
await this.publishSendEvent(result, recipient, user, {
|
|
99
|
+
emailConfigId: config.id,
|
|
100
|
+
isTest: true
|
|
101
|
+
});
|
|
87
102
|
return result;
|
|
88
103
|
}
|
|
89
104
|
// ─── Private: Provider & Config Resolution ──────────────────────────────────
|
|
@@ -127,7 +142,7 @@ export class EmailSendService {
|
|
|
127
142
|
}
|
|
128
143
|
// ─── Private: Template Resolution ───────────────────────────────────────────
|
|
129
144
|
async resolveTemplate(dto, user) {
|
|
130
|
-
let template
|
|
145
|
+
let template;
|
|
131
146
|
if (dto.templateId) {
|
|
132
147
|
template = await this.emailTemplateService.findByIdDirect(dto.templateId);
|
|
133
148
|
} else if (dto.templateSlug) {
|
|
@@ -203,6 +218,27 @@ export class EmailSendService {
|
|
|
203
218
|
</div>
|
|
204
219
|
`;
|
|
205
220
|
}
|
|
221
|
+
async publishSendEvent(result, to, user, metadata) {
|
|
222
|
+
const recipients = Array.isArray(to) ? to : [
|
|
223
|
+
to
|
|
224
|
+
];
|
|
225
|
+
await publishDomainEvent({
|
|
226
|
+
module: EMAIL_EVENT_MODULE,
|
|
227
|
+
entity: EMAIL_EVENT_ENTITIES.MESSAGE,
|
|
228
|
+
action: result.success ? EMAIL_EVENT_ACTIONS.SENT : EMAIL_EVENT_ACTIONS.FAILED,
|
|
229
|
+
ids: result.messageId ? [
|
|
230
|
+
result.messageId
|
|
231
|
+
] : [],
|
|
232
|
+
metadata: {
|
|
233
|
+
...metadata,
|
|
234
|
+
recipientCount: recipients.length,
|
|
235
|
+
...result.error ? {
|
|
236
|
+
error: result.error
|
|
237
|
+
} : {}
|
|
238
|
+
},
|
|
239
|
+
user: user ?? null
|
|
240
|
+
});
|
|
241
|
+
}
|
|
206
242
|
constructor(emailFactory, emailConfigService, emailProviderConfigService, emailTemplateService){
|
|
207
243
|
_define_property(this, "emailFactory", void 0);
|
|
208
244
|
_define_property(this, "emailConfigService", void 0);
|
|
@@ -4,6 +4,7 @@ import { buildMockUser } from '@test-utils/mocks/logged-user.mock';
|
|
|
4
4
|
import { EmailFactoryService } from '../providers';
|
|
5
5
|
import { EmailConfigService } from './email-config.service';
|
|
6
6
|
import { EmailProviderConfigService } from './email-provider-config.service';
|
|
7
|
+
import { EventBusRegistry } from '@flusys/nestjs-shared/classes';
|
|
7
8
|
import { EmailSendService } from './email-send.service';
|
|
8
9
|
import { EmailTemplateService } from './email-template.service';
|
|
9
10
|
function buildConfig(overrides = {}) {
|
|
@@ -539,4 +540,71 @@ describe('EmailSendService', ()=>{
|
|
|
539
540
|
expect(payload.html).not.toContain('<script>alert(1)</script>');
|
|
540
541
|
});
|
|
541
542
|
});
|
|
543
|
+
describe('domain events', ()=>{
|
|
544
|
+
let publish;
|
|
545
|
+
beforeEach(()=>{
|
|
546
|
+
publish = jest.fn().mockResolvedValue(undefined);
|
|
547
|
+
EventBusRegistry.reset();
|
|
548
|
+
EventBusRegistry.setBus({
|
|
549
|
+
publish,
|
|
550
|
+
subscribe: jest.fn(),
|
|
551
|
+
close: jest.fn()
|
|
552
|
+
});
|
|
553
|
+
EventBusRegistry.configureModule('email', {
|
|
554
|
+
enabled: true
|
|
555
|
+
});
|
|
556
|
+
mockEmailProviderConfigService.findByIdDirect.mockResolvedValue(buildConfig());
|
|
557
|
+
});
|
|
558
|
+
afterEach(()=>{
|
|
559
|
+
EventBusRegistry.reset();
|
|
560
|
+
});
|
|
561
|
+
it('publishes email.email_message.sent for a successful send', async ()=>{
|
|
562
|
+
await service.sendEmail({
|
|
563
|
+
to: 'user@example.com',
|
|
564
|
+
subject: 'Hi',
|
|
565
|
+
emailConfigId: 'config-uuid-1'
|
|
566
|
+
}, buildMockUser());
|
|
567
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
568
|
+
name: 'email.email_message.sent',
|
|
569
|
+
payload: expect.objectContaining({
|
|
570
|
+
ids: [
|
|
571
|
+
'msg-1'
|
|
572
|
+
],
|
|
573
|
+
metadata: expect.objectContaining({
|
|
574
|
+
recipientCount: 1
|
|
575
|
+
})
|
|
576
|
+
})
|
|
577
|
+
}));
|
|
578
|
+
});
|
|
579
|
+
it('publishes email.email_message.failed when the provider reports a failure', async ()=>{
|
|
580
|
+
mockProvider.sendEmail.mockResolvedValue({
|
|
581
|
+
success: false,
|
|
582
|
+
error: 'smtp refused'
|
|
583
|
+
});
|
|
584
|
+
await service.sendEmail({
|
|
585
|
+
to: 'user@example.com',
|
|
586
|
+
subject: 'Hi',
|
|
587
|
+
emailConfigId: 'config-uuid-1'
|
|
588
|
+
}, buildMockUser());
|
|
589
|
+
expect(publish).toHaveBeenCalledWith(expect.objectContaining({
|
|
590
|
+
name: 'email.email_message.failed',
|
|
591
|
+
payload: expect.objectContaining({
|
|
592
|
+
metadata: expect.objectContaining({
|
|
593
|
+
error: 'smtp refused'
|
|
594
|
+
})
|
|
595
|
+
})
|
|
596
|
+
}));
|
|
597
|
+
});
|
|
598
|
+
it('does not publish when the email module has events disabled', async ()=>{
|
|
599
|
+
EventBusRegistry.configureModule('email', {
|
|
600
|
+
enabled: false
|
|
601
|
+
});
|
|
602
|
+
await service.sendEmail({
|
|
603
|
+
to: 'user@example.com',
|
|
604
|
+
subject: 'Hi',
|
|
605
|
+
emailConfigId: 'config-uuid-1'
|
|
606
|
+
}, buildMockUser());
|
|
607
|
+
expect(publish).not.toHaveBeenCalled();
|
|
608
|
+
});
|
|
609
|
+
});
|
|
542
610
|
});
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { IBootstrapAppConfig, IDataSourceServiceOptions, IDynamicModuleConfig, IModuleOptionsFactory } from '@flusys/nestjs-core';
|
|
2
|
+
import { IModuleEventsConfig } from '@flusys/nestjs-shared/interfaces';
|
|
2
3
|
import { ModuleMetadata, Type } from '@nestjs/common';
|
|
3
4
|
export interface IEmailModuleConfig extends IDataSourceServiceOptions {
|
|
4
5
|
defaultProvider?: string;
|
|
5
6
|
rateLimitPerMinute?: number;
|
|
6
7
|
enableLogging?: boolean;
|
|
8
|
+
events?: IModuleEventsConfig;
|
|
7
9
|
}
|
|
8
10
|
export interface EmailModuleOptions extends IDynamicModuleConfig {
|
|
9
11
|
bootstrapAppConfig?: IBootstrapAppConfig;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-email",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0-beta.1",
|
|
4
4
|
"description": "Modular email package with SMTP, SendGrid, and Mailgun providers",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
107
|
"dependencies": {
|
|
108
|
-
"@flusys/nestjs-core": "
|
|
109
|
-
"@flusys/nestjs-shared": "
|
|
108
|
+
"@flusys/nestjs-core": "7.0.0-beta.1",
|
|
109
|
+
"@flusys/nestjs-shared": "7.0.0-beta.1"
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -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 { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
4
4
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
5
|
-
import { EntityTarget, SelectQueryBuilder } from 'typeorm';
|
|
5
|
+
import { EntityTarget, QueryRunner, SelectQueryBuilder } from 'typeorm';
|
|
6
6
|
import { CreateEmailConfigDto, UpdateEmailConfigDto } from '../dtos';
|
|
7
7
|
import { EmailConfig, EmailConfigBase } from '../entities';
|
|
8
8
|
import { IEmailConfig } from '../interfaces';
|
|
@@ -14,6 +14,7 @@ export declare class EmailProviderConfigService extends ApiService<CreateEmailCo
|
|
|
14
14
|
private readonly emailConfig;
|
|
15
15
|
constructor(cacheManager: HybridCache, utilsService: UtilsService, emailConfig: EmailConfigService, dataSourceProvider: EmailDataSourceProvider);
|
|
16
16
|
protected resolveEntity(): EntityTarget<EmailConfigBase>;
|
|
17
|
+
protected beforeDeleteOperation(dto: DeleteDto, _user: ILoggedUserInfo | null, _queryRunner: QueryRunner): Promise<void>;
|
|
17
18
|
convertSingleDtoToEntity(dto: CreateEmailConfigDto | UpdateEmailConfigDto, user: ILoggedUserInfo | null): Promise<EmailConfigBase>;
|
|
18
19
|
getSelectQuery(query: SelectQueryBuilder<EmailConfigBase>, _user: ILoggedUserInfo | null, select?: string[]): Promise<{
|
|
19
20
|
query: SelectQueryBuilder<EmailConfig>;
|