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_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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 = {
|
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)",
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}.`,
|
||||||
|
|
|
@ -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.",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue