diff --git a/package.json b/package.json index 13222b3..614606d 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "@czarpoliedros/email": "^0.2.7", "@nestjs/common": "^9.0.0", "@nestjs/config": "^2.2.0", "@nestjs/core": "^9.0.0", @@ -29,6 +28,7 @@ "@nestjs/platform-express": "^9.0.0", "amqp-connection-manager": "^4.1.14", "amqplib": "^0.10.3", + "nodemailer": "^6.9.13", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0" diff --git a/src/app.module.ts b/src/app.module.ts index 8c9c110..107d273 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; import { NotifierModule } from './notifier/notifier.module'; +import { EmailModule } from './email/email.module'; @Module({ - imports: [NotifierModule], + imports: [NotifierModule, EmailModule], controllers: [], providers: [], }) diff --git a/src/email/constants.ts b/src/email/constants.ts new file mode 100644 index 0000000..fbd25da --- /dev/null +++ b/src/email/constants.ts @@ -0,0 +1 @@ +export const SMTP_CONFIG_OPTIONS = 'SMTP_CONFIG_OPTIONS'; diff --git a/src/email/dto/email-request.dto.ts b/src/email/dto/email-request.dto.ts new file mode 100644 index 0000000..9f2aa70 --- /dev/null +++ b/src/email/dto/email-request.dto.ts @@ -0,0 +1,7 @@ +export class EmailRequest { + from: string; + to: string; + subject: string; + body: string; + body_html?: string; +} diff --git a/src/email/email.module.ts b/src/email/email.module.ts new file mode 100644 index 0000000..b5b64c8 --- /dev/null +++ b/src/email/email.module.ts @@ -0,0 +1,38 @@ +import { DynamicModule, Global, Module } from '@nestjs/common'; +import { SMTP_CONFIG_OPTIONS } from './constants'; +import { EmailService } from './email.service'; +import { createTransport } from 'nodemailer'; +import { SmtpOptions } from './interfaces/smtp-options.interface'; + +@Global() +@Module({}) +export class EmailModule { + public static forRoot(smtpOptions: SmtpOptions): DynamicModule { + const transporter = createTransport({ + name: smtpOptions.host, + host: smtpOptions.host, + port: smtpOptions.port, + secure: smtpOptions.secure, + auth: { + user: smtpOptions.email, + pass: smtpOptions.password, + }, + tls: { + ciphers: 'SSLv3', + }, + }); + + return { + module: EmailModule, + providers: [ + { + provide: SMTP_CONFIG_OPTIONS, + useValue: transporter, + }, + EmailService, + ], + exports: [EmailService], + global: true, + }; + } +} diff --git a/src/email/email.service.spec.ts b/src/email/email.service.spec.ts new file mode 100644 index 0000000..93a1248 --- /dev/null +++ b/src/email/email.service.spec.ts @@ -0,0 +1,38 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { mock } from 'jest-mock-extended'; +import { SentMessageInfo, Transporter } from 'nodemailer'; +import { SMTP_CONFIG_OPTIONS } from './constants'; +import { EmailService } from './email.service'; + +describe('EmailService', () => { + let service: EmailService; + const transporterMock = mock>(); + + beforeEach(async () => { + const smtpOptionsProvider = { + provide: SMTP_CONFIG_OPTIONS, + useValue: transporterMock, + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [smtpOptionsProvider, EmailService], + }).compile(); + + service = module.get(EmailService); + }); + + it('should send email', async () => { + let sendMail = false; + transporterMock.verify.mockReturnValue(Promise.resolve(true)); + transporterMock.sendMail.mockImplementation(async () => (sendMail = true)); + await service.sendMail({ + from: 'test@test.com', + to: 'to@to.com', + subject: 'Email sent', + body: 'body', + body_html: '

body

', + }); + + expect(sendMail).toBeTruthy(); + }); +}); diff --git a/src/email/email.service.ts b/src/email/email.service.ts new file mode 100644 index 0000000..641bc0b --- /dev/null +++ b/src/email/email.service.ts @@ -0,0 +1,46 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { SentMessageInfo, Transporter } from 'nodemailer'; +import { EmailRequest } from './dto/email-request.dto'; +import { SMTP_CONFIG_OPTIONS } from './constants'; +import { ConnectionFailed } from './errors/connection-failed.error'; + +interface IEmailService { + sendMail(emailRequest: EmailRequest): Promise; +} + +@Injectable() +export class EmailService implements IEmailService { + constructor( + @Inject(SMTP_CONFIG_OPTIONS) + private transporter: Transporter, + ) {} + + async sendMail({ + from, + to, + subject, + body, + body_html = '', + }: EmailRequest): Promise { + const verifier = await this.transporter.verify(); + + if (!verifier) + throw new ConnectionFailed( + 'Connection failed. Verify your credentials or connection.', + ); + + const mail = { + from: from, + to: to, + subject: subject, + text: body, + html: body_html, + }; + + try { + await this.transporter.sendMail(mail); + } catch (err) { + throw err; + } + } +} diff --git a/src/email/errors/connection-failed.error.ts b/src/email/errors/connection-failed.error.ts new file mode 100644 index 0000000..ae17093 --- /dev/null +++ b/src/email/errors/connection-failed.error.ts @@ -0,0 +1,5 @@ +export class ConnectionFailed extends Error { + constructor(message?: string) { + super(message); + } +} diff --git a/src/email/interfaces/index.ts b/src/email/interfaces/index.ts new file mode 100644 index 0000000..3feb148 --- /dev/null +++ b/src/email/interfaces/index.ts @@ -0,0 +1 @@ +export * from './smtp-options.interface'; diff --git a/src/email/interfaces/smtp-options.interface.ts b/src/email/interfaces/smtp-options.interface.ts new file mode 100644 index 0000000..9728b80 --- /dev/null +++ b/src/email/interfaces/smtp-options.interface.ts @@ -0,0 +1,26 @@ +export interface SmtpOptions { + /** + * host + */ + host: string; + + /** + * port + */ + port: number; + + /** + * email + */ + email: string; + + /** + * password + */ + password: string; + + /** + * secure + */ + secure: boolean; +} diff --git a/src/notifier/notifier.controller.ts b/src/notifier/notifier.controller.ts index 8ea6a2d..a84292a 100644 --- a/src/notifier/notifier.controller.ts +++ b/src/notifier/notifier.controller.ts @@ -1,20 +1,14 @@ import { Controller, Logger } from '@nestjs/common'; import { Ctx, EventPattern, Payload, RmqContext } from '@nestjs/microservices'; -import { SmsStrategy } from './strategies/sms.strategy'; -import { EmailStrategy } from './strategies/email.strategy'; import { ConfirmChannel, Message } from 'amqplib'; import { ManagerCreatedRequest } from './dto/request/manager-created-request.dto'; -import { EmailService } from '@czarpoliedros/email'; +import { EmailService } from 'src/email/email.service'; @Controller() export class NotifierController { private readonly logger = new Logger(NotifierController.name); - constructor( - private readonly emailStrategy: EmailStrategy, - private readonly smsStrategy: SmsStrategy, - private readonly emailService: EmailService, - ) {} + constructor(private readonly emailService: EmailService) {} @EventPattern('send_email') async handleSendEmail( diff --git a/src/notifier/notifier.module.ts b/src/notifier/notifier.module.ts index be04644..9e6f46c 100644 --- a/src/notifier/notifier.module.ts +++ b/src/notifier/notifier.module.ts @@ -1,9 +1,7 @@ -import { EmailModule } from '@czarpoliedros/email'; import { ConfigModule } from '@nestjs/config'; -import { SmsStrategy } from './strategies/sms.strategy'; -import { EmailStrategy } from './strategies/email.strategy'; import { Module } from '@nestjs/common'; import { NotifierController } from './notifier.controller'; +import { EmailModule } from '../email/email.module'; @Module({ imports: [ @@ -13,10 +11,10 @@ import { NotifierController } from './notifier.controller'; port: Number(process.env.SMTP_PORT), email: process.env.SMTP_USER, password: process.env.SMTP_PASSWORD, - secure: Boolean(process.env.SMTP_SECURE), + secure: false, }), ], controllers: [NotifierController], - providers: [EmailStrategy, SmsStrategy], + providers: [], }) export class NotifierModule {} diff --git a/src/notifier/strategies/email.strategy.ts b/src/notifier/strategies/email.strategy.ts deleted file mode 100644 index 3eb9f31..0000000 --- a/src/notifier/strategies/email.strategy.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { EmailService } from '@czarpoliedros/email'; -import { NotifyStrategy } from './notify-strategy'; - -@Injectable() -export class EmailStrategy implements NotifyStrategy { - private readonly logger = new Logger(EmailStrategy.name); - - constructor(private readonly emailService: EmailService) {} - - async send(body: string): Promise { - const to = 'carlos@czar.dev'; - - try { - await this.emailService.sendMail({ - from: 'carlos@czar.dev', - to: to, - subject: 'hello world', - body: body, - body_html: `

${body}

`, - }); - - this.logger.log(`Email sent to ${to}`); - } catch (err) { - this.logger.error(err); - throw err; - } - } -} diff --git a/src/notifier/strategies/notify-strategy.ts b/src/notifier/strategies/notify-strategy.ts deleted file mode 100644 index 6988ea2..0000000 --- a/src/notifier/strategies/notify-strategy.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface NotifyStrategy { - send(body: string): Promise; -} diff --git a/src/notifier/strategies/sms.strategy.ts b/src/notifier/strategies/sms.strategy.ts deleted file mode 100644 index ade4c48..0000000 --- a/src/notifier/strategies/sms.strategy.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { NotifyStrategy } from './notify-strategy'; - -@Injectable() -export class SmsStrategy implements NotifyStrategy { - async send(body: string): Promise { - console.log('sending sms...'); - } -} diff --git a/yarn.lock b/yarn.lock index 16e8ab3..29a2a4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -346,19 +346,6 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@czarpoliedros/email@^0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@czarpoliedros/email/-/email-0.2.7.tgz#e4d5802ff92889d6f34ede8a4b9f1ca8b2f068e2" - integrity sha512-F4xd0b1cBiFHuFCk/PCqsJf3EvcBqHno8VJuZDTZVZhJYF9Vg4bGgL+tP9BQJa41MkXELm0av37gCj2yJeYJHQ== - dependencies: - "@nestjs/common" "^9.0.11" - "@nestjs/core" "^9.0.0" - "@nestjs/platform-express" "^9.0.11" - nodemailer "^6.9.13" - reflect-metadata "^0.1.13" - rimraf "^3.0.2" - rxjs "^7.2.0" - "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -700,7 +687,7 @@ webpack "5.82.1" webpack-node-externals "3.0.0" -"@nestjs/common@^9.0.0", "@nestjs/common@^9.0.11": +"@nestjs/common@^9.0.0": version "9.4.3" resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-9.4.3.tgz#f907c5315b4273f7675864a05c4dda7056632b87" integrity sha512-Gd6D4IaYj01o14Bwv81ukidn4w3bPHCblMUq+SmUmWLyosK+XQmInCS09SbDDZyL8jy86PngtBLTdhJ2bXSUig== @@ -739,7 +726,7 @@ iterare "1.2.1" tslib "2.5.3" -"@nestjs/platform-express@^9.0.0", "@nestjs/platform-express@^9.0.11": +"@nestjs/platform-express@^9.0.0": version "9.4.3" resolved "https://registry.yarnpkg.com/@nestjs/platform-express/-/platform-express-9.4.3.tgz#f61b75686bdfce566be3b54fa7bb20a4d87ed619" integrity sha512-FpdczWoRSC0zz2dNL9u2AQLXKXRVtq4HgHklAhbL59X0uy+mcxhlSThG7DHzDMkoSnuuHY8ojDVf7mDxk+GtCw==