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:
parent
1d643b6315
commit
92cc0bc7cd
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)",
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}.`,
|
||||
|
|
|
@ -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.",
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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
|
||||
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<object>} 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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue