From 45ebb742b54fcb91a519c0c0f77be1b5da1ec6c2 Mon Sep 17 00:00:00 2001 From: FORESTIER Fabien <fabien.forestier@soprasteria.com> Date: Tue, 18 Jun 2019 15:27:15 +0200 Subject: [PATCH] Remove rabbitMQ from the stack and directly send mails to SMTP server --- .gitlab-ci.yml | 10 ++---- docker-compose.yml | 24 ++----------- package-lock.json | 7 +++- package.json | 2 +- src/configuration/config.service.ts | 7 ++-- src/configuration/config.ts | 16 ++++++--- src/email/email.controller.ts | 13 +++---- src/email/email.service.ts | 55 ++++++++--------------------- src/health/health-check.service.ts | 18 ++-------- src/health/health.module.ts | 4 +++ src/health/smtp.healthIndicator.ts | 34 ++++++++++++++++++ src/helpers.ts | 14 ++++++++ template.env | 8 ++--- 13 files changed, 104 insertions(+), 108 deletions(-) create mode 100644 src/health/smtp.healthIndicator.ts create mode 100644 src/helpers.ts diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index aa83843..2280fe0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,6 +4,8 @@ stages: variables: USER_SUPPORT_MAILBOX: alpha-test@erasme.org + SMTP_HOST: mail0.erasme.org + SMTP_PORT: 25 # ADDITIONAL_FEEDBACK_EMAILS: build_development: @@ -13,8 +15,6 @@ build_development: script: - export TAG=dev - export MAIL_SERVICE_BIND_PORT=3000 - - export RABBITMQ_LISTENING_PORT=5672 - - export RABBITMQ_GUI_PORT=15672 - docker-compose build - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker-compose push @@ -28,8 +28,6 @@ build_release: script: - export TAG=$(echo $CI_COMMIT_TAG | sed 's/v//g') - export MAIL_SERVICE_BIND_PORT=3000 - - export RABBITMQ_LISTENING_PORT=5672 - - export RABBITMQ_GUI_PORT=15672 - docker-compose build - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker-compose push @@ -41,8 +39,6 @@ deploy_development: script: - export TAG=dev - export MAIL_SERVICE_BIND_PORT=3001 - - export RABBITMQ_LISTENING_PORT=5672 - - export RABBITMQ_GUI_PORT=15672 - export MAIL_SUBJECT_PREFIX=alpha - export NO_REPLY_MAIL_ADDRESS=no-reply@erasme.org - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY @@ -59,8 +55,6 @@ deploy_staging: script: - export TAG=staging - export MAIL_SERVICE_BIND_PORT=3101 - - export RABBITMQ_LISTENING_PORT=5673 - - export RABBITMQ_GUI_PORT=15673 - export MAIL_SUBJECT_PREFIX=alpha - export NO_REPLY_MAIL_ADDRESS=no-reply@erasme.org - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY diff --git a/docker-compose.yml b/docker-compose.yml index 8dba4d7..afa59fd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,11 +8,8 @@ services: ports: - ${MAIL_SERVICE_BIND_PORT}:3000 environment: - - RABBITMQ_USER=rabbitmq-user - - RABBITMQ_PASSWORD=${RABBITMQ_PASSWORD} - - RABBITMQ_HOST=rabbitmq - - RABBITMQ_LISTENING_PORT=5672 - - MAILER_QUEUE=portail-data-send-email + - SMTP_HOST=${SMTP_HOST} + - SMTP_PORT=${SMTP_PORT} - MAIL_SUBJECT_PREFIX=${MAIL_SUBJECT_PREFIX} - USER_SUPPORT_MAILBOX=${USER_SUPPORT_MAILBOX} - NO_REPLY_MAIL_ADDRESS=${NO_REPLY_MAIL_ADDRESS} @@ -20,19 +17,4 @@ services: - GROUP_HEADER=x-consumer-groups - EMAIL_WRITER_GROUP_NAME=email-writer - IMAGE_HOST=https://minio.alpha.grandlyon.com/email-template-assets - restart: unless-stopped - - rabbitmq: - image: rabbitmq:3-management-alpine - ports: - - ${RABBITMQ_LISTENING_PORT}:5672 # standard port for communication - - ${RABBITMQ_GUI_PORT}:15672 # graphic interface - environment: - - RABBITMQ_DEFAULT_USER=rabbitmq-user - - RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD} - volumes: - - rabbitmq-data-volume:/var/lib/rabbitmq - restart: unless-stopped - -volumes: - rabbitmq-data-volume: + restart: unless-stopped \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a774298..9497820 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "service-email", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7787,6 +7787,11 @@ "which": "^1.3.0" } }, + "nodemailer": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.2.1.tgz", + "integrity": "sha512-TagB7iuIi9uyNgHExo8lUDq3VK5/B0BpbkcjIgNvxbtVrjNqq0DwAOTuzALPVkK76kMhTSzIgHqg8X1uklVs6g==" + }, "nodemon": { "version": "1.18.9", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.18.9.tgz", diff --git a/package.json b/package.json index 6d970e1..7bbeb3d 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,11 @@ "@nestjs/core": "^5.1.0", "@nestjs/swagger": "^2.5.1", "@nestjs/terminus": "^5.5.0", - "amqplib": "^0.5.2", "class-transformer": "^0.1.9", "class-validator": "^0.9.1", "moment": "^2.24.0", "moment-timezone": "^0.5.23", + "nodemailer": "^6.2.1", "reflect-metadata": "^0.1.12", "rxjs": "^6.2.2", "serve-favicon": "^2.5.0", diff --git a/src/configuration/config.service.ts b/src/configuration/config.service.ts index 67c9ba1..492dbb7 100644 --- a/src/configuration/config.service.ts +++ b/src/configuration/config.service.ts @@ -7,11 +7,8 @@ export class ConfigService { constructor() { // Initializing conf with values from var env - this._config.rabbitMQ.user = process.env.RABBITMQ_USER; - this._config.rabbitMQ.password = process.env.RABBITMQ_PASSWORD; - this._config.rabbitMQ.host = process.env.RABBITMQ_HOST; - this._config.rabbitMQ.port = process.env.RABBITMQ_LISTENING_PORT; - this._config.mailerQueue = process.env.MAILER_QUEUE; + this._config.smtpConfig.host = process.env.SMTP_HOST; + this._config.smtpConfig.port = process.env.SMTP_PORT; this._config.mailSubjectPrefix = process.env.MAIL_SUBJECT_PREFIX; this._config.userSupportMailbox = process.env.USER_SUPPORT_MAILBOX; this._config.noReplyMailAddress = process.env.NO_REPLY_MAIL_ADDRESS; diff --git a/src/configuration/config.ts b/src/configuration/config.ts index 07512a4..69f00c4 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -1,11 +1,17 @@ export const config = { - rabbitMQ: { - user: null, - password: null, - host: null, + smtpConfig: { + host: '', port: null, + secure: false, + tls: { + // do not fail on invalid certs + rejectUnauthorized: false, + }, + // auth: { + // user: process.env.SMTP_USER, + // pass: process.env.SMTP_PASS + // } }, - mailerQueue: '', userSupportMailbox: '', imageHost: '', groupNames: { diff --git a/src/email/email.controller.ts b/src/email/email.controller.ts index dec2cd7..079cf5b 100644 --- a/src/email/email.controller.ts +++ b/src/email/email.controller.ts @@ -3,6 +3,7 @@ import { ContactForm, EmailWithoutFrom, FeedbackForm } from './email'; import { EmailService } from './email.service'; import { ApiBadRequestResponse, ApiOkResponse, ApiOperation, ApiInternalServerErrorResponse } from '@nestjs/swagger'; import { Groups } from '../decorators/groups.decorators'; +import { handleError } from '../helpers'; @Controller() export class EmailController { @@ -16,13 +17,13 @@ export class EmailController { @ApiOperation({ title: 'Send email to admin (emails defined as var env of the project, see docker-compose.yml file) and recap email to user email.' }) @ApiOkResponse({ description: 'OK' }) @ApiBadRequestResponse({ description: 'Missing fields' }) - @ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)' }) + @ApiInternalServerErrorResponse({ description: 'Internal error (unreachable SMTP server...)' }) @HttpCode(200) async create(@Body() contactForm: ContactForm) { try { return await this.emailService.sendContactEmails(contactForm); } catch (error) { - throw new InternalServerErrorException(); + handleError(error, new InternalServerErrorException()); } } @@ -31,13 +32,13 @@ export class EmailController { @ApiOperation({ title: 'Send email to admin with the user feedback' }) @ApiOkResponse({ description: 'OK' }) @ApiBadRequestResponse({ description: 'Missing fields' }) - @ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)' }) + @ApiInternalServerErrorResponse({ description: 'Internal error (unreachable SMTP server...)' }) @HttpCode(200) async sendFeedback(@Body() feedbackForm: FeedbackForm, @Req() req) { try { return await this.emailService.sendFeedback(feedbackForm, req.headers['user-agent']); } catch (error) { - throw new InternalServerErrorException(); + handleError(error, new InternalServerErrorException()); } } @@ -46,13 +47,13 @@ export class EmailController { @ApiOperation({ title: 'Send email.' }) @ApiOkResponse({ description: 'OK' }) @ApiBadRequestResponse({ description: 'Missing fields' }) - @ApiInternalServerErrorResponse({ description: 'Internal error, this is probably a rabbitMQ related error (unreachable service...)' }) + @ApiInternalServerErrorResponse({ description: 'Internal error (unreachable SMTP server...)' }) @HttpCode(200) async createEmail(@Body() email: EmailWithoutFrom) { try { return await this.emailService.send(email); } catch (error) { - throw new InternalServerErrorException(); + handleError(error, new InternalServerErrorException()); } } } diff --git a/src/email/email.service.ts b/src/email/email.service.ts index 366b905..d4dfa83 100644 --- a/src/email/email.service.ts +++ b/src/email/email.service.ts @@ -1,10 +1,10 @@ import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common'; -import * as amqp from 'amqplib'; import { ContactForm, Email, EmailWithoutFrom, FeedbackForm } from './email'; import { ConfigService } from '../configuration/config.service'; import { buildContactAdminEmail, buildContactUserEmail } from '../email-templates/contact'; import { buildFeedbackEmail } from '../email-templates/feedback'; import * as useragent from 'useragent'; +import * as Nodemailer from 'nodemailer'; import moment = require('moment-timezone'); moment.tz.setDefault('Europe/Paris'); @@ -12,7 +12,7 @@ moment.tz.setDefault('Europe/Paris'); export class EmailService { private logger: Logger; - config: any = {}; + private config; constructor(private configService: ConfigService) { this.logger = new Logger(EmailService.name); @@ -95,11 +95,6 @@ export class EmailService { async send(emailInfo: EmailWithoutFrom) { this.logger.log('Entering function', `${EmailService.name} - ${this.send.name}`); - let conn, ch; - // tslint:disable-next-line:max-line-length - const rabbitmqUrl = `amqp://${this.config.rabbitMQ.user}:${this.config.rabbitMQ.password}@${this.config.rabbitMQ.host}:${this.config.rabbitMQ.port}`; - const mailerQueue = this.config.mailerQueue; - let email = new Email(); email.from = this.config.noReplyMailAddress; email = Object.assign(email, emailInfo); @@ -108,44 +103,24 @@ export class EmailService { email.subject = `[${this.config.mailSubjectPrefix}] ${email.subject}`; } - // Connect to rabbitmq - try { - conn = await amqp.connect(rabbitmqUrl); - } catch (error) { - this.logger.error('Error connecting to RabbitMQ', error, `${EmailService.name} - ${this.send.name}`); - throw new InternalServerErrorException('Could not connect to rabbitMQ.'); - } - - try { - // Create a communication channel - ch = await conn.createChannel(); - } catch (error) { - this.logger.error('Error creating channel', error, `${EmailService.name} - ${this.send.name}`); - throw new InternalServerErrorException('Could not create channel.'); - } + const transporter = Nodemailer.createTransport(this.config.smtpConfig); - // Stringify and bufferise message - const buffer = Buffer.from(JSON.stringify(email)); + await transporter.verify().catch((error) => { + this.logger.error('SMTP connection failed.', error, `${EmailService.name} - ${this.send.name}`); + throw new InternalServerErrorException({ error, message: 'SMTP connection failed.' }); + }); - try { - await ch.assertQueue(mailerQueue, { durable: true }); - } catch (error) { - this.logger.error('Error asserting queue', error, `${EmailService.name} - ${this.send.name}`); - throw new InternalServerErrorException('Could not assert queue.'); - } + this.logger.log('SMTP Server is ready to receive messages', `${EmailService.name} - ${this.send.name}`); - try { - await ch.sendToQueue(mailerQueue, buffer, { persistent: true }); - } catch (error) { - this.logger.error('Error sending to queue', error, `${EmailService.name} - ${this.send.name}`); - throw new InternalServerErrorException('Could not send to queue.'); - } + await transporter.sendMail(email).catch((error) => { + this.logger.error('Couldn\'t send email.', error, `${EmailService.name} - ${this.send.name}`); + transporter.close(); + throw new InternalServerErrorException({ error, message: 'Couldn\'t send email.' }); + }); - this.logger.log( - `Sent to queue ${mailerQueue},{ from: ${email.from}, to: ${email.to}, subject: ${email.subject}}`, `${EmailService.name} - ${this.send.name}`, - ); + transporter.close(); + this.logger.log('Email sent to SMTP server', `${EmailService.name} - ${this.send.name}`); - setTimeout(() => { conn.close(); }, 500); return; } diff --git a/src/health/health-check.service.ts b/src/health/health-check.service.ts index 556584d..d50b0fc 100644 --- a/src/health/health-check.service.ts +++ b/src/health/health-check.service.ts @@ -2,33 +2,21 @@ import { TerminusEndpoint, TerminusOptionsFactory, TerminusModuleOptions, - MicroserviceHealthIndicator, } from '@nestjs/terminus'; import { Injectable } from '@nestjs/common'; -import { Transport } from '@nestjs/microservices'; -import { ConfigService } from '../configuration/config.service'; +import { SmtpHealthIndicator } from './smtp.healthIndicator'; @Injectable() export class HealthCheckService implements TerminusOptionsFactory { constructor( - private configService: ConfigService, - private readonly _microserviceHealthIndicator: MicroserviceHealthIndicator, + private readonly _smtpHealthIndicator: SmtpHealthIndicator, ) { } createTerminusOptions(): TerminusModuleOptions { - const config = this.configService.config; const healthEndpoint: TerminusEndpoint = { url: '/health', healthIndicators: [ - async () => this._microserviceHealthIndicator.pingCheck('rabbitMQ', { - transport: Transport.RMQ, - options: { - urls: [ - `amqp://${config.rabbitMQ.user}:${config.rabbitMQ.password}@${config.rabbitMQ.host}:${config.rabbitMQ.port}`, - ], - queue: config.mailerQueue, - }, - }), + async () => this._smtpHealthIndicator.checkSMTPConnection(), ], }; return { diff --git a/src/health/health.module.ts b/src/health/health.module.ts index 65e1112..e5209bd 100644 --- a/src/health/health.module.ts +++ b/src/health/health.module.ts @@ -1,12 +1,16 @@ import { Module } from '@nestjs/common'; import { TerminusModule } from '@nestjs/terminus'; import { HealthCheckService } from './health-check.service'; +import { SmtpHealthIndicator } from './smtp.healthIndicator'; @Module({ imports: [ TerminusModule.forRootAsync({ + imports: [HealthModule], useClass: HealthCheckService, }), ], + providers: [SmtpHealthIndicator], + exports: [SmtpHealthIndicator], }) export class HealthModule { } \ No newline at end of file diff --git a/src/health/smtp.healthIndicator.ts b/src/health/smtp.healthIndicator.ts new file mode 100644 index 0000000..d7f7199 --- /dev/null +++ b/src/health/smtp.healthIndicator.ts @@ -0,0 +1,34 @@ +import { HealthIndicatorResult } from '@nestjs/terminus'; +import { Injectable, Logger } from '@nestjs/common'; +import { HealthCheckError } from '@godaddy/terminus'; +import { HealthIndicator } from '@nestjs/terminus/dist/health-indicators/abstract/health-indicator'; +import * as Nodemailer from 'nodemailer'; +import { ConfigService } from '../configuration/config.service'; + +@Injectable() +export class SmtpHealthIndicator extends HealthIndicator { + + private logger: Logger; + + constructor( + private configService: ConfigService, + ) { + super(); + this.logger = new Logger(SmtpHealthIndicator.name); + } + + async checkSMTPConnection(): Promise<HealthIndicatorResult> { + + const transporter = Nodemailer.createTransport(this.configService.config.smtpConfig); + + await transporter.verify().catch((error) => { + this.logger.error('SMTP connection failed.', error, `${SmtpHealthIndicator.name} - ${this.checkSMTPConnection.name}`); + transporter.close(); + throw new HealthCheckError('SMTP connection failed', error); + }); + + transporter.close(); + return this.getStatus('smtpServer', true); + + } +} \ No newline at end of file diff --git a/src/helpers.ts b/src/helpers.ts new file mode 100644 index 0000000..7729554 --- /dev/null +++ b/src/helpers.ts @@ -0,0 +1,14 @@ +import { HttpException } from '@nestjs/common'; + +export function handleError(err, defaultErr) { + if (err instanceof HttpException) { + throw err; + } else { + if (err && err.error) { + const error = JSON.parse(err.error); + throw new HttpException(error.message, err.statusCode); + } else { + throw defaultErr; + } + } +} \ No newline at end of file diff --git a/template.env b/template.env index eb388f2..64c25e5 100644 --- a/template.env +++ b/template.env @@ -1,11 +1,7 @@ TAG=<version of the service to deploy> -RABBITMQ_USER=<rabbitMQ user> -RABBITMQ_PASSWORD=<rabbitMQ password> -RABBITMQ_HOST=<rabbitMQ host> -RABBITMQ_LISTENING_PORT=<rabiitMQ listening port, default 5672> -RABBITMQ_GUI_PORT=<RabbitMQ interface, default 15672> -MAILER_QUEUE=<sueue where to write the emails to be sent> +SMTP_HOST=<host of the SMTP server> +SMTP_PORT=<port of the SMTP server> MAIL_SERVICE_BIND_PORT=<listening port of the service> MAIL_SUBJECT_PREFIX=<prefix used in email subject> -- GitLab