1
0
Fork 0
mirror of synced 2024-06-27 18:40:42 +12:00

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 339554b163
commit 8faa9554ed
12 changed files with 148 additions and 42 deletions

View file

@ -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:

View file

@ -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

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 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,

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 = {
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)",

View file

@ -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 => {

View file

@ -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
}

View file

@ -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}.`,

View file

@ -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.",
}

View file

@ -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))
}

View file

@ -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
}
/**

View file

@ -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