Commit 45ebb742 authored by FORESTIER Fabien's avatar FORESTIER Fabien
Browse files

Remove rabbitMQ from the stack and directly send mails to SMTP server

parent 309e3899
Pipeline #2446 passed with stages
in 3 minutes and 52 seconds
......@@ -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
......
......@@ -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
{
"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",
......
......@@ -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;
......
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: {
......
......@@ -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());
}
}
}
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;
}
......
......@@ -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 {
......
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
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
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
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>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment