From 9f0fce098e002614702c9ed26290c3671feceae2 Mon Sep 17 00:00:00 2001 From: Edouard Date: Fri, 22 Mar 2024 00:14:09 +0100 Subject: [PATCH] feat: SMTP integration and email notifications (#631) --- docker-compose-dev.yml | 8 +++ docker-compose.yml | 8 +++ server/.env.sample | 8 +++ server/api/controllers/cards/create.js | 5 +- .../api/controllers/comment-actions/create.js | 5 +- server/api/helpers/actions/create-one.js | 7 +++ server/api/helpers/cards/create-one.js | 5 ++ server/api/helpers/cards/update-one.js | 1 + .../api/helpers/notifications/create-one.js | 57 +++++++++++++++++++ server/api/helpers/utils/send-email.js | 31 ++++++++++ server/api/hooks/smtp/index.js | 35 ++++++++++++ server/config/custom.js | 7 +++ server/package-lock.json | 14 +++++ server/package.json | 1 + 14 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 server/api/helpers/utils/send-email.js create mode 100644 server/api/hooks/smtp/index.js diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 48c501ae..9fe70859 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -31,6 +31,14 @@ services: # - DEFAULT_ADMIN_NAME=Demo Demo # - DEFAULT_ADMIN_USERNAME=demo + # Email Notifications (https://nodemailer.com/smtp/) + # - SMTP_HOST= + # - SMTP_PORT=587 + # - SMTP_SECURE=true + # - SMTP_USER= + # - SMTP_PASSWORD= + # - SMTP_FROM="Demo Demo" + # - OIDC_ISSUER= # - OIDC_CLIENT_ID= # - OIDC_CLIENT_SECRET= diff --git a/docker-compose.yml b/docker-compose.yml index 3d63069a..51ac55cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,14 @@ services: # - DEFAULT_ADMIN_NAME=Demo Demo # - DEFAULT_ADMIN_USERNAME=demo + # Email Notifications (https://nodemailer.com/smtp/) + # - SMTP_HOST= + # - SMTP_PORT=587 + # - SMTP_SECURE=true + # - SMTP_USER= + # - SMTP_PASSWORD= + # - SMTP_FROM="Demo Demo" + # - OIDC_ISSUER= # - OIDC_CLIENT_ID= # - OIDC_CLIENT_SECRET= diff --git a/server/.env.sample b/server/.env.sample index 8d962b64..fb5a8288 100644 --- a/server/.env.sample +++ b/server/.env.sample @@ -22,6 +22,14 @@ SECRET_KEY=notsecretkey # DEFAULT_ADMIN_NAME=Demo Demo # DEFAULT_ADMIN_USERNAME=demo +# Email Notifications (https://nodemailer.com/smtp/) +# SMTP_HOST= +# SMTP_PORT=587 +# SMTP_SECURE=true +# SMTP_USER= +# SMTP_PASSWORD= +# SMTP_FROM="Demo Demo" + # OIDC_ISSUER= # OIDC_CLIENT_ID= # OIDC_CLIENT_SECRET= diff --git a/server/api/controllers/cards/create.js b/server/api/controllers/cards/create.js index ab0c444e..b847c3e0 100755 --- a/server/api/controllers/cards/create.js +++ b/server/api/controllers/cards/create.js @@ -78,12 +78,12 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { list } = await sails.helpers.lists + const { board, list } = await sails.helpers.lists .getProjectPath(inputs.listId) .intercept('pathNotFound', () => Errors.LIST_NOT_FOUND); const boardMembership = await BoardMembership.findOne({ - boardId: list.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -99,6 +99,7 @@ module.exports = { const card = await sails.helpers.cards.createOne .with({ + board, values: { ...values, list, diff --git a/server/api/controllers/comment-actions/create.js b/server/api/controllers/comment-actions/create.js index ddf4b991..7712a3a5 100755 --- a/server/api/controllers/comment-actions/create.js +++ b/server/api/controllers/comment-actions/create.js @@ -32,12 +32,12 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - const { card } = await sails.helpers.cards + const { board, card } = await sails.helpers.cards .getProjectPath(inputs.cardId) .intercept('pathNotFound', () => Errors.CARD_NOT_FOUND); const boardMembership = await BoardMembership.findOne({ - boardId: card.boardId, + boardId: board.id, userId: currentUser.id, }); @@ -55,6 +55,7 @@ module.exports = { }; const action = await sails.helpers.actions.createOne.with({ + board, values: { ...values, card, diff --git a/server/api/helpers/actions/create-one.js b/server/api/helpers/actions/create-one.js index e96df93f..78d5e0c5 100644 --- a/server/api/helpers/actions/create-one.js +++ b/server/api/helpers/actions/create-one.js @@ -21,6 +21,10 @@ module.exports = { custom: valuesValidator, required: true, }, + board: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -56,6 +60,9 @@ module.exports = { userId, action, }, + user: values.user, + board: inputs.board, + card: values.card, }), ), ); diff --git a/server/api/helpers/cards/create-one.js b/server/api/helpers/cards/create-one.js index e1a96b42..847a00ca 100644 --- a/server/api/helpers/cards/create-one.js +++ b/server/api/helpers/cards/create-one.js @@ -25,6 +25,10 @@ module.exports = { custom: valuesValidator, required: true, }, + board: { + type: 'ref', + required: true, + }, request: { type: 'ref', }, @@ -104,6 +108,7 @@ module.exports = { }, user: values.creatorUser, }, + board: inputs.board, }); return card; diff --git a/server/api/helpers/cards/update-one.js b/server/api/helpers/cards/update-one.js index 1a78f8db..4363f9b8 100644 --- a/server/api/helpers/cards/update-one.js +++ b/server/api/helpers/cards/update-one.js @@ -232,6 +232,7 @@ module.exports = { toList: _.pick(values.list, ['id', 'name']), }, }, + board: inputs.board, }); } diff --git a/server/api/helpers/notifications/create-one.js b/server/api/helpers/notifications/create-one.js index cc6a0b63..271a9179 100644 --- a/server/api/helpers/notifications/create-one.js +++ b/server/api/helpers/notifications/create-one.js @@ -14,6 +14,40 @@ const valuesValidator = (value) => { return true; }; +// TODO: use templates (views) to build html +const buildAndSendEmail = async (user, board, card, action, notifiableUser) => { + let emailData; + switch (action.type) { + case Action.Types.MOVE_CARD: + emailData = { + subject: `${user.name} moved ${card.name} from ${action.data.fromList.name} to ${action.data.toList.name} on ${board.name}`, + html: + `

${user.name} moved ` + + `${card.name} ` + + `from ${action.data.fromList.name} to ${action.data.toList.name} ` + + `on ${board.name}

`, + }; + break; + case Action.Types.COMMENT_CARD: + emailData = { + subject: `${user.name} left a new comment to ${card.name} on ${board.name}`, + html: + `

${user.name} left a new comment to ` + + `${card.name} ` + + `on ${board.name}

` + + `

${action.data.text}

`, + }; + break; + default: + return; + } + + await sails.helpers.utils.sendEmail.with({ + ...emailData, + to: notifiableUser.email, + }); +}; + module.exports = { inputs: { values: { @@ -21,6 +55,18 @@ module.exports = { custom: valuesValidator, required: true, }, + user: { + type: 'ref', + required: true, + }, + board: { + type: 'ref', + required: true, + }, + card: { + type: 'ref', + required: true, + }, }, async fn(inputs) { @@ -40,6 +86,17 @@ module.exports = { item: notification, }); + if (sails.hooks.smtp.isActive()) { + let notifiableUser; + if (values.user) { + notifiableUser = values.user; + } else { + notifiableUser = await sails.helpers.users.getOne(notification.userId); + } + + buildAndSendEmail(inputs.user, inputs.board, inputs.card, values.action, notifiableUser); + } + return notification; }, }; diff --git a/server/api/helpers/utils/send-email.js b/server/api/helpers/utils/send-email.js new file mode 100644 index 00000000..5d9be724 --- /dev/null +++ b/server/api/helpers/utils/send-email.js @@ -0,0 +1,31 @@ +module.exports = { + inputs: { + to: { + type: 'string', + required: true, + }, + subject: { + type: 'string', + required: true, + }, + html: { + type: 'string', + required: true, + }, + }, + + async fn(inputs) { + const transporter = sails.hooks.smtp.getTransporter(); // TODO: check if active? + + try { + const info = await transporter.sendMail({ + ...inputs, + from: sails.config.custom.smtpFrom, + }); + + sails.log.info('Email sent: %s', info.messageId); + } catch (error) { + sails.log.error(error); + } + }, +}; diff --git a/server/api/hooks/smtp/index.js b/server/api/hooks/smtp/index.js new file mode 100644 index 00000000..4cd05038 --- /dev/null +++ b/server/api/hooks/smtp/index.js @@ -0,0 +1,35 @@ +const nodemailer = require('nodemailer'); + +module.exports = function smtpServiceHook(sails) { + let transporter = null; + + return { + /** + * Runs when this Sails app loads/lifts. + */ + + async initialize() { + if (sails.config.custom.smtpHost) { + transporter = nodemailer.createTransport({ + pool: true, + host: sails.config.custom.smtpHost, + port: sails.config.custom.smtpPort, + secure: sails.config.custom.smtpSecure, + auth: sails.config.custom.smtpUser && { + user: sails.config.custom.smtpUser, + pass: sails.config.custom.smtpPassword, + }, + }); + sails.log.info('SMTP hook has been loaded successfully'); + } + }, + + getTransporter() { + return transporter; + }, + + isActive() { + return transporter !== null; + }, + }; +}; diff --git a/server/config/custom.js b/server/config/custom.js index f074575f..76473245 100644 --- a/server/config/custom.js +++ b/server/config/custom.js @@ -34,6 +34,13 @@ module.exports.custom = { defaultAdminEmail: process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase(), + smtpHost: process.env.SMTP_HOST, + smtpPort: process.env.SMTP_PORT || 587, + smtpSecure: process.env.SMTP_SECURE === 'true', + smtpUser: process.env.SMTP_USER, + smtpPassword: process.env.SMTP_PASSWORD, + smtpFrom: process.env.SMTP_FROM, + oidcIssuer: process.env.OIDC_ISSUER, oidcClientId: process.env.OIDC_CLIENT_ID, oidcClientSecret: process.env.OIDC_CLIENT_SECRET, diff --git a/server/package-lock.json b/server/package-lock.json index 05527a7f..93453a83 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -15,6 +15,7 @@ "lodash": "^4.17.21", "moment": "^2.29.4", "move-file": "^2.1.0", + "nodemailer": "^6.9.12", "openid-client": "^5.6.1", "rimraf": "^5.0.5", "sails": "^1.5.7", @@ -5257,6 +5258,14 @@ } } }, + "node_modules/nodemailer": { + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.12.tgz", + "integrity": "sha512-pnLo7g37Br3jXbF0bl5DekBJihm2q+3bB3l2o/B060sWmb5l+VqeScAQCBqaQ+5ezRZFzW5SciZNGdRDEbq89w==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/nodemon": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", @@ -12852,6 +12861,11 @@ "whatwg-url": "^5.0.0" } }, + "nodemailer": { + "version": "6.9.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.12.tgz", + "integrity": "sha512-pnLo7g37Br3jXbF0bl5DekBJihm2q+3bB3l2o/B060sWmb5l+VqeScAQCBqaQ+5ezRZFzW5SciZNGdRDEbq89w==" + }, "nodemon": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", diff --git a/server/package.json b/server/package.json index 59bb2f78..67fc5e01 100644 --- a/server/package.json +++ b/server/package.json @@ -36,6 +36,7 @@ "lodash": "^4.17.21", "moment": "^2.29.4", "move-file": "^2.1.0", + "nodemailer": "^6.9.12", "openid-client": "^5.6.1", "rimraf": "^5.0.5", "sails": "^1.5.7",