diff --git a/config/env/common.js b/config/env/common.js index 28a73a0..3ffae82 100644 --- a/config/env/common.js +++ b/config/env/common.js @@ -5,5 +5,8 @@ module.exports = { port: process.env.PORT || 5001, - logPass: 'test' + logPass: 'test', + emailServer: process.env.EMAILSERVER ? process.env.EMAILSERVER : 'mail.someserver.com', + emailUser: process.env.EMAILUSER ? process.env.EMAILUSER : 'noreply@someserver.com', + emailPassword: process.env.EMAILPASS ? process.env.EMAILPASS : 'emailpassword' } diff --git a/package-lock.json b/package-lock.json index c763e09..d1a8144 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5864,6 +5864,11 @@ "process-on-spawn": "^1.0.0" } }, + "nodemailer": { + "version": "6.4.10", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.10.tgz", + "integrity": "sha512-j+pS9CURhPgk6r0ENr7dji+As2xZiHSvZeVnzKniLOw1eRAyM/7flP0u65tCnsapV8JFu+t0l/5VeHsCZEeh9g==" + }, "nodemon": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz", diff --git a/package.json b/package.json index 042c652..779a4f6 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "koa-static": "^5.0.0", "line-reader": "^0.4.0", "mongoose": "^5.5.12", + "nodemailer": "^6.4.10", "passport-local": "^1.0.0", "winston": "^3.2.1", "winston-daily-rotate-file": "^4.0.0" diff --git a/src/lib/nodemailer.js b/src/lib/nodemailer.js new file mode 100644 index 0000000..f839682 --- /dev/null +++ b/src/lib/nodemailer.js @@ -0,0 +1,170 @@ +/* + A library for controlling the sending email. +*/ + +'use strict' +const nodemailer = require('nodemailer') + +const config = require('../../config') + +const wlogger = require('./wlogger') + +let _this + +class NodeMailer { + constructor () { + this.nodemailer = nodemailer + this.config = config + + _this = this + } + + // Validate email + async validateEmail (email) { + // eslint-disable-next-line no-useless-escape + if (/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email)) { + return true + } + return false + } + + // Handles the sending of data via email. + async sendEmail (data) { + try { + // Validate input + if (!data.email || typeof data.email !== 'string') { + throw new Error("Property 'email' must be a string!") + } + const isEmail = await _this.validateEmail(data.email) + if (!isEmail) { + throw new Error("Property 'email' must be email format!") + } + + if (!data.to || !Array.isArray(data.to)) { + throw new Error("Property 'to' must be a array!") + } + + const isEmailList = await _this.validateEmailArray(data.to) + if (!isEmailList) { + throw new Error("Property 'to' must be array of email format!") + } + + if (!data.formMessage || typeof data.formMessage !== 'string') { + throw new Error("Property 'message' must be a string!") + } + + if (!data.subject || typeof data.subject !== 'string') { + throw new Error("Property 'subject' must be a string!") + } + + if (!data.payloadTitle || typeof data.payloadTitle !== 'string') { + throw new Error("Property 'payloadTitle' must be a string!") + } + + // create reusable transporter object using the default SMTP transport + const transporter = await _this.nodemailer.createTransport({ + host: _this.config.emailServer, + port: 587, + secure: false, // true for 465, false for other ports + auth: { + user: _this.config.emailUser, // generated ethereal user + pass: _this.config.emailPassword // generated ethereal password + } + }) + // console.log(`transporter: ${JSON.stringify(transporter)}`) + + const msg = data.formMessage.replace(/(\r\n|\n|\r)/g, '
') + + const now = new Date() + + const subject = data.subject + const to = data.to + const emailUser = data.email // email from the user who initiated the sharing email + const payload = data.payloadTitle + + const bodyJson = data + delete bodyJson.to + delete bodyJson.subject + delete bodyJson.formMessage + + // Prototyping email output. + bodyJson.email = 'noreply@launchpadip.net' + delete bodyJson.email + delete bodyJson.emailList + delete bodyJson.payloadTitle + + bodyJson.message = msg + + // Html body + let htmlData = '' + + // maps the object and converts it into html format + Object.keys(bodyJson).forEach(function (key) { + htmlData += `${key}: ${bodyJson[key]}
` + }) + // This paragraph just be added to the message for + // emails that are not password reset ones + const paragraphTag = `

${emailUser} would like to share the document ${payload} with you through + LaunchpadIP.net. +

` + + const htmlMsg = `

${subject}:

+ ${payload === 'Email reset' ? '' : paragraphTag} +

+ time: ${now.toLocaleString()}
+ ${htmlData} +

` + // send mail with defined transport object + const info = await transporter.sendMail({ + // from: `${data.email}`, // sender address + from: 'noreply@launchpadip.net', + to: `${to}`, // list of receivers + // subject: `Pearson ${subject}`, // Subject line + subject: subject, + // html: 'This is a test email' // html body + html: htmlMsg + }) + console.log('Message sent: %s', info.messageId) + } catch (err) { + console.log('Error in sendEmail()') + throw err + } + } + + async validateEmailArray (emailList) { + try { + if (!emailList || !Array.isArray(emailList)) { + throw new Error("Property 'emailList' must be a array!") + } + // Email list can't be empty + if (!emailList.length > 0) { + throw new Error("Property 'emailList' cant be empty!") + } + + // Iterates the array and validates each email format + const isValid = await new Promise(resolve => { + emailList.map(async (value, i) => { + const isEmail = await _this.validateEmail(value) + + if (!isEmail) { + resolve(false) + } + if (i >= emailList.length - 1) { + resolve(true) + } + }) + }) + + if (!isValid) { + throw new Error('Array must contain emails format!') + } + + return true + } catch (err) { + wlogger.error('Error in lib/nodemailer.js/validateEmailArray()') + throw err + } + } +} + +module.exports = NodeMailer diff --git a/src/modules/contact/controller.js b/src/modules/contact/controller.js new file mode 100644 index 0000000..504762b --- /dev/null +++ b/src/modules/contact/controller.js @@ -0,0 +1,76 @@ +/* eslint-disable no-useless-escape */ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + +const config = require('../../../config') + +const NodeMailer = require('../../lib/nodemailer') +const nodemailer = new NodeMailer() + +let _this + +class Contact { + constructor () { + _this = this + _this.config = config + _this.nodemailer = nodemailer + } + + async email (ctx) { + try { + const data = ctx.request.body + + const emailObj = data.obj + // Validate input + if (!emailObj.email || typeof emailObj.email !== 'string') { + throw new Error("Property 'email' must be a string!") + } + const isEmail = await _this.nodemailer.validateEmail(emailObj.email) + + if (!isEmail) { + throw new Error("Property 'email' must be email format!") + } + + if (!emailObj.formMessage || typeof emailObj.formMessage !== 'string') { + throw new Error("Property 'message' must be a string!") + } + + if (!emailObj.payloadTitle || typeof emailObj.payloadTitle !== 'string') { + throw new Error("Property 'payloadTitle' must be a string!") + } + + // If an email list exists, the email will be sended to that list + // otherwhise will be sended by default to the variable "_this.config.emailUser" + let _to = [_this.config.emailUser] + + // Email list is optional + if (emailObj.emailList) { + if ( + !Array.isArray(emailObj.emailList) || + !emailObj.emailList.length > 0 + ) { + throw new Error("Property 'emailList' must be a array of emails!") + } else { + _to = emailObj.emailList + } + } + + console.log(`Trying send message to : ${_to}`) + + emailObj.subject = 'Someone wants to share a document with you.' + emailObj.to = _to + + await _this.nodemailer.sendEmail(emailObj) + + ctx.body = { + success: true + } + } catch (err) { + ctx.body = { + success: false + } + // console.error(`Error: `, err) + throw err + } + } +} +module.exports = Contact diff --git a/src/modules/contact/router.js b/src/modules/contact/router.js new file mode 100644 index 0000000..0f0bf3c --- /dev/null +++ b/src/modules/contact/router.js @@ -0,0 +1,17 @@ +// const ensureUser = require('../../midleware/validators') + +const ContactController = require('./controller') +const contactController = new ContactController() + +// export const baseUrl = '/users' +module.exports.baseUrl = '/contact' + +module.exports.routes = [ + { + method: 'POST', + route: '/email', + handlers: [ + contactController.email + ] + } +] diff --git a/test/a03-nodemailer.spec.js b/test/a03-nodemailer.spec.js new file mode 100644 index 0000000..e048ccf --- /dev/null +++ b/test/a03-nodemailer.spec.js @@ -0,0 +1,171 @@ +const assert = require('chai').assert + +const NodeMailer = require('../src/lib/nodemailer') +const nodemailer = new NodeMailer() + +describe('NodeMailer', () => { + describe('sendEmail()', () => { + it('should throw error if email property is not provided', async () => { + try { + const data = { + formMessage: 'test msg', + name: 'test name', + subject: 'test subject', + to: ['test2@email.com'] + } + await nodemailer.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'Property \'email\' must be a string!') + } + }) + it('should throw error if email property is wrong format', async () => { + try { + const data = { + email: 'test', + formMessage: 'test msg', + name: 'test name', + subject: 'test subject', + to: ['test2@email.com'] + } + await nodemailer.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'Property \'email\' must be email format!') + } + }) + it('should throw error if formMessage property is not provided', async () => { + try { + const data = { + email: 'test@email.com', + name: 'test name', + subject: 'test subject', + to: ['test2@email.com'] + } + await nodemailer.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'Property \'message\' must be a string!') + } + }) + it('should throw error if property is not provided', async () => { + try { + const data = { + email: 'test@email.com', + name: 'test name', + subject: 'test subject' + } + await nodemailer.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'Property \'to\' must be a array!') + } + }) + + it('should throw error if is wrong format', async () => { + try { + const data = { + email: 'test@email.com', + formMessage: 'test msg', + name: 'test name', + subject: 'test subject', + to: ['test'] + } + await nodemailer.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'Array must contain emails format!') + } + }) + + it('should throw error if subject Property is not provided', async () => { + try { + const data = { + email: 'test@email.com', + formMessage: 'test msg', + name: 'test name', + to: ['test2@email.com'] + } + await nodemailer.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'Property \'subject\' must be a string!') + } + }) + it('should throw error if payloadTitle property is not provided', async () => { + try { + const data = { + email: 'test@email.com', + formMessage: 'test msg', + name: 'test name', + subject: 'test subject', + to: ['test2@email.com'] + } + await nodemailer.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'Property \'payloadTitle\' must be a string!') + } + }) + + /* it('should send email', async () => { + try { + const data = { + email: 'test@email.com', + formMessage: 'test msg', + name: 'test name', + to: 'test2@email.com', + subject: 'test subject', + payloadTitle: 'test title' + } + await nodemailer.sendEmail(data) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'Property \'subject\' must be a string!') + } + }) */ + }) + describe('validateEmailArray()', () => { + it('should throw error if email list is not provided ', async () => { + try { + await nodemailer.validateEmailArray() + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'Property \'emailList\' must be a array!') + } + }) + it('should throw error if email list is empty', async () => { + try { + const emailList = [] + await nodemailer.validateEmailArray(emailList) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'Property \'emailList\' cant be empty!') + } + }) + it('should throw error if email list contain wrong format', async () => { + try { + const emailList = [ + 'wrongEmail', + 'bad format' + ] + await nodemailer.validateEmailArray(emailList) + assert(false, 'Unexpected result') + } catch (err) { + assert.include(err.message, 'Array must contain emails format!') + } + }) + it('should return true if email list contain email format', async () => { + try { + const emailList = [ + 'test@email.com', + 'simple@email.com' + ] + const result = await nodemailer.validateEmailArray(emailList) + assert.isTrue(result) + } catch (err) { + assert(false, 'Unexpected result') + } + }) + }) +}) diff --git a/test/a04-contact.spec.js b/test/a04-contact.spec.js new file mode 100644 index 0000000..cc07a16 --- /dev/null +++ b/test/a04-contact.spec.js @@ -0,0 +1,143 @@ +const config = require('../config') +const axios = require('axios').default +const assert = require('chai').assert + +// Mock data +// const mockData = require('./mocks/contact-mocks') + +const LOCALHOST = `http://localhost:${config.port}` + +describe('Contact', () => { + + describe('POST /contact/email', () => { + it('should throw error if email property is not provided', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/contact/email`, + data: { + obj: { + formMessage: 'message' + } + } + } + + await axios(options) + + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 500) + assert.include(err.response.data, "Property 'email' must be a string!") + } + }) + it('should throw error if email property is wrong format', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/contact/email`, + data: { + obj: { + email: 'email', + formMessage: 'test message' + } + } + } + + await axios(options) + + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 500) + assert.include( + err.response.data, + "Property 'email' must be email format!" + ) + } + }) + it('should throw error if formMessage property is not provided', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/contact/email`, + data: { + obj: { + email: 'email@email.com' + } + } + } + + await axios(options) + + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 500) + assert.include( + err.response.data, + "Property 'message' must be a string!" + ) + } + }) + it('should throw error if payloadTitle property is not provided', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/contact/email`, + data: { + obj: { + email: 'email@email.com', + formMessage: 'test message' + } + } + } + + await axios(options) + + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 500) + assert.include( + err.response.data, + "Property 'payloadTitle' must be a string!" + ) + } + }) + /* I presented problems here trying to use sandbox in this test */ + /* it('should send email with all input', async () => { + // Mock live network calls. + sandbox.stub( + contactController.transporter, 'sendMail') + .resolves(mockData) + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/contact/email`, + data: { + obj: { + email: 'email@email.com', + formMessage: 'test email', + name: 'test name', + payloadtitle: 'test title' + } + } + } + const result = await axios(options) + console.log('RESULT', result.data) + } catch (err) { + // console.log(err) + assert(false, 'Unexpected result') + } + }) */ + }) +})