diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index 6d9f64c07e..d04fd7c0e4 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -16,7 +16,7 @@ services: MINIO_URL: http://minio-service:9000 MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} - HOSTING_KEY: ${HOSTING_KEY} + INTERNAL_KEY: ${INTERNAL_KEY} BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT} PORT: 4002 JWT_SECRET: ${JWT_SECRET} @@ -44,7 +44,7 @@ services: COUCH_DB_USERNAME: ${COUCH_DB_USER} COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD} 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_PASSWORD: ${REDIS_PASSWORD} depends_on: diff --git a/hosting/hosting.properties b/hosting/hosting.properties index 4297ec60a1..6ad962c493 100644 --- a/hosting/hosting.properties +++ b/hosting/hosting.properties @@ -1,10 +1,6 @@ # Use the main port in the builder for your self hosting URL, e.g. localhost: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 # These should be updated JWT_SECRET=testsecret @@ -13,6 +9,7 @@ MINIO_SECRET_KEY=budibase COUCH_DB_PASSWORD=budibase COUCH_DB_USER=budibase REDIS_PASSWORD=budibase +INTERNAL_KEY=budibase # This section contains variables that do not need to be altered under normal circumstances APP_PORT=4002 diff --git a/packages/server/src/automations/actions.js b/packages/server/src/automations/actions.js index 0f40fd6aae..983e87854a 100644 --- a/packages/server/src/automations/actions.js +++ b/packages/server/src/automations/actions.js @@ -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 updateRow = require("./steps/updateRow") const deleteRow = require("./steps/deleteRow") @@ -14,7 +15,8 @@ const { } = require("../utilities/fileSystem") const BUILTIN_ACTIONS = { - SEND_EMAIL: sendEmail.run, + SEND_EMAIL: sendgridEmail.run, + SEND_EMAIL_SMTP: sendSmtpEmail.run, CREATE_ROW: createRow.run, UPDATE_ROW: updateRow.run, DELETE_ROW: deleteRow.run, @@ -24,7 +26,8 @@ const BUILTIN_ACTIONS = { EXECUTE_QUERY: executeQuery.run, } const BUILTIN_DEFINITIONS = { - SEND_EMAIL: sendEmail.definition, + SEND_EMAIL: sendgridEmail.definition, + SEND_EMAIL_SMTP: sendSmtpEmail.definition, CREATE_ROW: createRow.definition, UPDATE_ROW: updateRow.definition, DELETE_ROW: deleteRow.definition, diff --git a/packages/server/src/automations/steps/sendSmtpEmail.js b/packages/server/src/automations/steps/sendSmtpEmail.js index e69de29bb2..764972b402 100644 --- a/packages/server/src/automations/steps/sendSmtpEmail.js +++ b/packages/server/src/automations/steps/sendSmtpEmail.js @@ -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 = "

No content

" + } + try { + let response = await sendSmtpEmail(to, from, subject, contents) + return { + success: true, + response, + } + } catch (err) { + console.error(err) + return { + success: false, + response: err, + } + } +} diff --git a/packages/server/src/automations/steps/sendgridEmail.js b/packages/server/src/automations/steps/sendgridEmail.js index 26c404257e..5485116e89 100644 --- a/packages/server/src/automations/steps/sendgridEmail.js +++ b/packages/server/src/automations/steps/sendgridEmail.js @@ -1,5 +1,5 @@ module.exports.definition = { - description: "Send an email", + description: "Send an email using SendGrid", tagline: "Send email to {{inputs.to}}", icon: "ri-mail-open-line", name: "Send Email (SendGrid)", diff --git a/packages/server/src/utilities/workerRequests.js b/packages/server/src/utilities/workerRequests.js index 6bf3bf6461..8d4b1edad2 100644 --- a/packages/server/src/utilities/workerRequests.js +++ b/packages/server/src/utilities/workerRequests.js @@ -34,17 +34,29 @@ function request(ctx, request) { exports.request = request -exports.sendSmtpEmail = async (to, from, contents) => { +exports.sendSmtpEmail = async (to, from, subject, contents) => { const response = await fetch( - checkSlashesInUrl(env.WORKER_URL + `/api/`), + checkSlashesInUrl(env.WORKER_URL + `/api/admin/email/send`), request(null, { method: "POST", headers: { "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 => { diff --git a/packages/worker/src/api/controllers/admin/auth.js b/packages/worker/src/api/controllers/admin/auth.js index 598e43e8ad..30c7a264bd 100644 --- a/packages/worker/src/api/controllers/admin/auth.js +++ b/packages/worker/src/api/controllers/admin/auth.js @@ -53,8 +53,9 @@ exports.reset = async ctx => { ) } try { + 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) { // don't throw any kind of error to the user, this might give away something } diff --git a/packages/worker/src/api/controllers/admin/email.js b/packages/worker/src/api/controllers/admin/email.js index 04e85e7f44..9bfb281b9a 100644 --- a/packages/worker/src/api/controllers/admin/email.js +++ b/packages/worker/src/api/controllers/admin/email.js @@ -5,13 +5,13 @@ const authPkg = require("@budibase/auth") const GLOBAL_DB = authPkg.StaticDatabases.GLOBAL.name 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 if (userId) { const db = new CouchDB(GLOBAL_DB) 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 = { ...response, message: `Email sent to ${email}.`, diff --git a/packages/worker/src/api/controllers/admin/users.js b/packages/worker/src/api/controllers/admin/users.js index 5c35c65e27..c6507b690a 100644 --- a/packages/worker/src/api/controllers/admin/users.js +++ b/packages/worker/src/api/controllers/admin/users.js @@ -136,7 +136,7 @@ exports.invite = async ctx => { if (existing) { ctx.throw(400, "Email address already in use.") } - await sendEmail(email, EmailTemplatePurpose.INVITATION) + await sendEmail(email, EmailTemplatePurpose.INVITATION, { subject: "{{ company }} platform invitation" }) ctx.body = { message: "Invitation has been sent.", } diff --git a/packages/worker/src/api/routes/admin/email.js b/packages/worker/src/api/routes/admin/email.js index 66079c5fbb..d3d0d4faae 100644 --- a/packages/worker/src/api/routes/admin/email.js +++ b/packages/worker/src/api/routes/admin/email.js @@ -10,8 +10,11 @@ function buildEmailSendValidation() { // prettier-ignore return joiValidator.body(Joi.object({ email: Joi.string().email(), + purpose: Joi.string().valid(...Object.values(EmailTemplatePurpose)), 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)) } diff --git a/packages/worker/src/utilities/email.js b/packages/worker/src/utilities/email.js index 7a2069bbb4..6a99863341 100644 --- a/packages/worker/src/utilities/email.js +++ b/packages/worker/src/utilities/email.js @@ -7,6 +7,7 @@ const { getSettingsTemplateContext } = require("./templates") const { processString } = require("@budibase/string-templates") const { getResetPasswordCode, getInviteCode } = require("../utilities/redis") +const TEST_MODE = false const GLOBAL_DB = StaticDatabases.GLOBAL.name const TYPE = TemplateTypes.EMAIL @@ -14,18 +15,32 @@ const FULL_EMAIL_PURPOSES = [ EmailTemplatePurpose.INVITATION, EmailTemplatePurpose.PASSWORD_RECOVERY, EmailTemplatePurpose.WELCOME, + EmailTemplatePurpose.CUSTOM, ] function createSMTPTransport(config) { - const options = { - port: config.port, - host: config.host, - secure: config.secure || false, - auth: config.auth, - } - if (config.selfSigned) { - options.tls = { - rejectUnauthorized: false, + let options + if (!TEST_MODE) { + options = { + port: config.port, + host: config.host, + secure: config.secure || false, + auth: config.auth, + } + 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) @@ -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). * @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 {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} 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 if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) { throw `Unable to build an email of type ${purpose}` @@ -63,11 +80,9 @@ async function buildEmail(purpose, email, user) { } base = base.contents body = body.contents - - // if there is a link code needed this will retrieve it - const code = await getLinkCode(purpose, email, user) - const context = { - ...(await getSettingsTemplateContext(purpose, code)), + context = { + ...context, + contents, email, 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 {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} subject A custom subject can be specified if the config one is not desired. * @return {Promise} returns details about the attempt to send email, e.g. if it is successful; based on * nodemailer response. */ exports.sendEmail = async ( email, purpose, - { groupId, user, from, contents } = {} + { groupId, user, from, contents, subject } = {} ) => { const db = new CouchDB(GLOBAL_DB) - const config = await getSmtpConfiguration(db, groupId) - if (!config) { + let config = await getSmtpConfiguration(db, groupId) || {} + if (Object.keys(config).length === 0 && !TEST_MODE) { throw "Unable to find SMTP configuration." } 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 = { from: from || config.from, - subject: config.subject, + subject: await processString(subject || config.subject, context), 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 } /** diff --git a/packages/worker/src/utilities/templates.js b/packages/worker/src/utilities/templates.js index 64ef62060d..a4ee3d3c47 100644 --- a/packages/worker/src/utilities/templates.js +++ b/packages/worker/src/utilities/templates.js @@ -15,8 +15,8 @@ const BASE_COMPANY = "Budibase" exports.getSettingsTemplateContext = async (purpose, code = null) => { const db = new CouchDB(StaticDatabases.GLOBAL.name) // TODO: use more granular settings in the future if required - const settings = await getScopedConfig(db, { type: Configs.SETTINGS }) - if (!settings.platformUrl) { + let settings = await getScopedConfig(db, { type: Configs.SETTINGS }) || {} + if (!settings || !settings.platformUrl) { settings.platformUrl = LOCAL_URL } const URL = settings.platformUrl