Updating to support SMTP email automation action, as well as some general work around from and subject which previously we'ren't fully implemented.

This commit is contained in:
mike12345567 2021-05-11 15:08:59 +01:00
parent 1d643b6315
commit 92cc0bc7cd
12 changed files with 148 additions and 42 deletions

View File

@ -16,7 +16,7 @@ services:
MINIO_URL: http://minio-service:9000 MINIO_URL: http://minio-service:9000
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
HOSTING_KEY: ${HOSTING_KEY} INTERNAL_KEY: ${INTERNAL_KEY}
BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT} BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT}
PORT: 4002 PORT: 4002
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
@ -44,7 +44,7 @@ services:
COUCH_DB_USERNAME: ${COUCH_DB_USER} COUCH_DB_USERNAME: ${COUCH_DB_USER}
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD} COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984 COUCH_DB_URL: http://${COUCH_DB_USER}:${COUCH_DB_PASSWORD}@couchdb-service:5984
SELF_HOST_KEY: ${HOSTING_KEY} INTERNAL_KEY: ${INTERNAL_KEY}
REDIS_URL: redis-service:6379 REDIS_URL: redis-service:6379
REDIS_PASSWORD: ${REDIS_PASSWORD} REDIS_PASSWORD: ${REDIS_PASSWORD}
depends_on: depends_on:

View File

@ -1,10 +1,6 @@
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000 # Use the main port in the builder for your self hosting URL, e.g. localhost:10000
MAIN_PORT=10000 MAIN_PORT=10000
# Use this password when configuring your self hosting settings
# This should be updated
HOSTING_KEY=budibase
# This section contains all secrets pertaining to the system # This section contains all secrets pertaining to the system
# These should be updated # These should be updated
JWT_SECRET=testsecret JWT_SECRET=testsecret
@ -13,6 +9,7 @@ MINIO_SECRET_KEY=budibase
COUCH_DB_PASSWORD=budibase COUCH_DB_PASSWORD=budibase
COUCH_DB_USER=budibase COUCH_DB_USER=budibase
REDIS_PASSWORD=budibase REDIS_PASSWORD=budibase
INTERNAL_KEY=budibase
# This section contains variables that do not need to be altered under normal circumstances # This section contains variables that do not need to be altered under normal circumstances
APP_PORT=4002 APP_PORT=4002

View File

@ -1,4 +1,5 @@
const sendEmail = require("./steps/sendgridEmail") const sendgridEmail = require("./steps/sendgridEmail")
const sendSmtpEmail = require("./steps/sendSmtpEmail")
const createRow = require("./steps/createRow") const createRow = require("./steps/createRow")
const updateRow = require("./steps/updateRow") const updateRow = require("./steps/updateRow")
const deleteRow = require("./steps/deleteRow") const deleteRow = require("./steps/deleteRow")
@ -14,7 +15,8 @@ const {
} = require("../utilities/fileSystem") } = require("../utilities/fileSystem")
const BUILTIN_ACTIONS = { const BUILTIN_ACTIONS = {
SEND_EMAIL: sendEmail.run, SEND_EMAIL: sendgridEmail.run,
SEND_EMAIL_SMTP: sendSmtpEmail.run,
CREATE_ROW: createRow.run, CREATE_ROW: createRow.run,
UPDATE_ROW: updateRow.run, UPDATE_ROW: updateRow.run,
DELETE_ROW: deleteRow.run, DELETE_ROW: deleteRow.run,
@ -24,7 +26,8 @@ const BUILTIN_ACTIONS = {
EXECUTE_QUERY: executeQuery.run, EXECUTE_QUERY: executeQuery.run,
} }
const BUILTIN_DEFINITIONS = { const BUILTIN_DEFINITIONS = {
SEND_EMAIL: sendEmail.definition, SEND_EMAIL: sendgridEmail.definition,
SEND_EMAIL_SMTP: sendSmtpEmail.definition,
CREATE_ROW: createRow.definition, CREATE_ROW: createRow.definition,
UPDATE_ROW: updateRow.definition, UPDATE_ROW: updateRow.definition,
DELETE_ROW: deleteRow.definition, DELETE_ROW: deleteRow.definition,

View File

@ -0,0 +1,67 @@
const { sendSmtpEmail } = require("../../utilities/workerRequests")
module.exports.definition = {
description: "Send an email using SMTP",
tagline: "Send SMTP email to {{inputs.to}}",
icon: "ri-mail-open-line",
name: "Send Email (SMTP)",
type: "ACTION",
stepId: "SEND_EMAIL_SMTP",
inputs: {},
schema: {
inputs: {
properties: {
to: {
type: "string",
title: "Send To",
},
from: {
type: "string",
title: "Send From",
},
subject: {
type: "string",
title: "Email Subject",
},
contents: {
type: "string",
title: "HTML Contents",
},
},
required: ["to", "from", "subject", "contents"],
},
outputs: {
properties: {
success: {
type: "boolean",
description: "Whether the email was sent",
},
response: {
type: "object",
description: "A response from the email client, this may be an error",
},
},
required: ["success"],
},
},
}
module.exports.run = async function ({ inputs }) {
let { to, from, subject, contents } = inputs
if (!contents) {
contents = "<h1>No content</h1>"
}
try {
let response = await sendSmtpEmail(to, from, subject, contents)
return {
success: true,
response,
}
} catch (err) {
console.error(err)
return {
success: false,
response: err,
}
}
}

View File

@ -1,5 +1,5 @@
module.exports.definition = { module.exports.definition = {
description: "Send an email", description: "Send an email using SendGrid",
tagline: "Send email to {{inputs.to}}", tagline: "Send email to {{inputs.to}}",
icon: "ri-mail-open-line", icon: "ri-mail-open-line",
name: "Send Email (SendGrid)", name: "Send Email (SendGrid)",

View File

@ -34,17 +34,29 @@ function request(ctx, request) {
exports.request = request exports.request = request
exports.sendSmtpEmail = async (to, from, contents) => { exports.sendSmtpEmail = async (to, from, subject, contents) => {
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + `/api/`), checkSlashesInUrl(env.WORKER_URL + `/api/admin/email/send`),
request(null, { request(null, {
method: "POST", method: "POST",
headers: { headers: {
"x-budibase-api-key": env.INTERNAL_KEY, "x-budibase-api-key": env.INTERNAL_KEY,
}, },
body: {}, body: {
email: to,
from,
contents,
subject,
purpose: "custom",
},
}) })
) )
const json = await response.json()
if (json.status !== 200 && response.status !== 200) {
throw "Unable to send email."
}
return json
} }
exports.getDeployedApps = async ctx => { exports.getDeployedApps = async ctx => {

View File

@ -53,8 +53,9 @@ exports.reset = async ctx => {
) )
} }
try { try {
const user = await getGlobalUserByEmail(email) const user = await getGlobalUserByEmail(email)
await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { user }) await sendEmail(email, EmailTemplatePurpose.PASSWORD_RECOVERY, { user, subject: "{{ company }} platform password reset" })
} catch (err) { } catch (err) {
// don't throw any kind of error to the user, this might give away something // don't throw any kind of error to the user, this might give away something
} }

View File

@ -5,13 +5,13 @@ const authPkg = require("@budibase/auth")
const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name
exports.sendEmail = async ctx => { exports.sendEmail = async ctx => {
const { groupId, email, userId, purpose } = ctx.request.body const { groupId, email, userId, purpose, contents, from, subject } = ctx.request.body
let user let user
if (userId) { if (userId) {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
user = await db.get(userId) user = await db.get(userId)
} }
const response = await sendEmail(email, purpose, { groupId, user }) const response = await sendEmail(email, purpose, { groupId, user, contents, from, subject })
ctx.body = { ctx.body = {
...response, ...response,
message: `Email sent to ${email}.`, message: `Email sent to ${email}.`,

View File

@ -136,7 +136,7 @@ exports.invite = async ctx => {
if (existing) { if (existing) {
ctx.throw(400, "Email address already in use.") ctx.throw(400, "Email address already in use.")
} }
await sendEmail(email, EmailTemplatePurpose.INVITATION) await sendEmail(email, EmailTemplatePurpose.INVITATION, { subject: "{{ company }} platform invitation" })
ctx.body = { ctx.body = {
message: "Invitation has been sent.", message: "Invitation has been sent.",
} }

View File

@ -10,8 +10,11 @@ function buildEmailSendValidation() {
// prettier-ignore // prettier-ignore
return joiValidator.body(Joi.object({ return joiValidator.body(Joi.object({
email: Joi.string().email(), email: Joi.string().email(),
purpose: Joi.string().valid(...Object.values(EmailTemplatePurpose)),
groupId: Joi.string().allow("", null), groupId: Joi.string().allow("", null),
purpose: Joi.string().allow(...Object.values(EmailTemplatePurpose)), fromt: Joi.string().allow("", null),
contents: Joi.string().allow("", null),
subject: Joi.string().allow("", null),
}).required().unknown(true)) }).required().unknown(true))
} }

View File

@ -7,6 +7,7 @@ const { getSettingsTemplateContext } = require("./templates")
const { processString } = require("@budibase/string-templates") const { processString } = require("@budibase/string-templates")
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis") const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
const TEST_MODE = false
const GLOBAL_DB = StaticDatabases.GLOBAL.name const GLOBAL_DB = StaticDatabases.GLOBAL.name
const TYPE = TemplateTypes.EMAIL const TYPE = TemplateTypes.EMAIL
@ -14,18 +15,32 @@ const FULL_EMAIL_PURPOSES = [
EmailTemplatePurpose.INVITATION, EmailTemplatePurpose.INVITATION,
EmailTemplatePurpose.PASSWORD_RECOVERY, EmailTemplatePurpose.PASSWORD_RECOVERY,
EmailTemplatePurpose.WELCOME, EmailTemplatePurpose.WELCOME,
EmailTemplatePurpose.CUSTOM,
] ]
function createSMTPTransport(config) { function createSMTPTransport(config) {
const options = { let options
port: config.port, if (!TEST_MODE) {
host: config.host, options = {
secure: config.secure || false, port: config.port,
auth: config.auth, host: config.host,
} secure: config.secure || false,
if (config.selfSigned) { auth: config.auth,
options.tls = { }
rejectUnauthorized: false, if (config.selfSigned) {
options.tls = {
rejectUnauthorized: false,
}
}
} else {
options = {
port: 587,
host: "smtp.ethereal.email",
secure: false,
auth: {
user: "don.bahringer@ethereal.email",
pass: "yCKSH8rWyUPbnhGYk9",
},
} }
} }
return nodemailer.createTransport(options) return nodemailer.createTransport(options)
@ -46,10 +61,12 @@ async function getLinkCode(purpose, email, user) {
* Builds an email using handlebars and the templates found in the system (default or otherwise). * Builds an email using handlebars and the templates found in the system (default or otherwise).
* @param {string} purpose the purpose of the email being built, e.g. invitation, password reset. * @param {string} purpose the purpose of the email being built, e.g. invitation, password reset.
* @param {string} email the address which it is being sent to for contextual purposes. * @param {string} email the address which it is being sent to for contextual purposes.
* @param {object|null} user If being sent to an existing user then the object can be provided for context. * @param {object} context the context which is being used for building the email (hbs context).
* @param {object|null} user if being sent to an existing user then the object can be provided for context.
* @param {string|null} contents if using a custom template can supply contents for context.
* @return {Promise<string>} returns the built email HTML if all provided parameters were valid. * @return {Promise<string>} returns the built email HTML if all provided parameters were valid.
*/ */
async function buildEmail(purpose, email, user) { async function buildEmail(purpose, email, context, { user, contents } = {}) {
// this isn't a full email // this isn't a full email
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) { if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
throw `Unable to build an email of type ${purpose}` throw `Unable to build an email of type ${purpose}`
@ -63,11 +80,9 @@ async function buildEmail(purpose, email, user) {
} }
base = base.contents base = base.contents
body = body.contents body = body.contents
context = {
// if there is a link code needed this will retrieve it ...context,
const code = await getLinkCode(purpose, email, user) contents,
const context = {
...(await getSettingsTemplateContext(purpose, code)),
email, email,
user: user || {}, user: user || {},
} }
@ -116,27 +131,35 @@ exports.isEmailConfigured = async (groupId = null) => {
* @param {object|undefined} user If sending to an existing user the object can be provided, this is used in the context. * @param {object|undefined} user If sending to an existing user the object can be provided, this is used in the context.
* @param {string|undefined} from If sending from an address that is not what is configured in the SMTP config. * @param {string|undefined} from If sending from an address that is not what is configured in the SMTP config.
* @param {string|undefined} contents If sending a custom email then can supply contents which will be added to it. * @param {string|undefined} contents If sending a custom email then can supply contents which will be added to it.
* @param {string|undefined} subject A custom subject can be specified if the config one is not desired.
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on * @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
* nodemailer response. * nodemailer response.
*/ */
exports.sendEmail = async ( exports.sendEmail = async (
email, email,
purpose, purpose,
{ groupId, user, from, contents } = {} { groupId, user, from, contents, subject } = {}
) => { ) => {
const db = new CouchDB(GLOBAL_DB) const db = new CouchDB(GLOBAL_DB)
const config = await getSmtpConfiguration(db, groupId) let config = await getSmtpConfiguration(db, groupId) || {}
if (!config) { if (Object.keys(config).length === 0 && !TEST_MODE) {
throw "Unable to find SMTP configuration." throw "Unable to find SMTP configuration."
} }
const transport = createSMTPTransport(config) const transport = createSMTPTransport(config)
// if there is a link code needed this will retrieve it
const code = await getLinkCode(purpose, email, user)
const context = await getSettingsTemplateContext(purpose, code)
const message = { const message = {
from: from || config.from, from: from || config.from,
subject: config.subject, subject: await processString(subject || config.subject, context),
to: email, to: email,
html: await buildEmail(purpose, email, user), html: await buildEmail(purpose, email, context, { user, contents }),
} }
return transport.sendMail(message) const response = await transport.sendMail(message)
if (TEST_MODE) {
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
}
return response
} }
/** /**

View File

@ -15,8 +15,8 @@ const BASE_COMPANY = "Budibase"
exports.getSettingsTemplateContext = async (purpose, code = null) => { exports.getSettingsTemplateContext = async (purpose, code = null) => {
const db = new CouchDB(StaticDatabases.GLOBAL.name) const db = new CouchDB(StaticDatabases.GLOBAL.name)
// TODO: use more granular settings in the future if required // TODO: use more granular settings in the future if required
const settings = await getScopedConfig(db, { type: Configs.SETTINGS }) let settings = await getScopedConfig(db, { type: Configs.SETTINGS }) || {}
if (!settings.platformUrl) { if (!settings || !settings.platformUrl) {
settings.platformUrl = LOCAL_URL settings.platformUrl = LOCAL_URL
} }
const URL = settings.platformUrl const URL = settings.platformUrl