2021-04-23 14:49:47 +02:00
|
|
|
const nodemailer = require("nodemailer")
|
2021-04-27 18:29:05 +02:00
|
|
|
const CouchDB = require("../db")
|
2021-05-06 11:51:21 +02:00
|
|
|
const { StaticDatabases, getScopedConfig } = require("@budibase/auth").db
|
2021-04-27 18:30:19 +02:00
|
|
|
const { EmailTemplatePurpose, TemplateTypes, Configs } = require("../constants")
|
2021-04-27 18:29:05 +02:00
|
|
|
const { getTemplateByPurpose } = require("../constants/templates")
|
|
|
|
const { getSettingsTemplateContext } = require("./templates")
|
|
|
|
const { processString } = require("@budibase/string-templates")
|
2021-05-05 13:11:06 +02:00
|
|
|
const { getResetPasswordCode, getInviteCode } = require("../utilities/redis")
|
2021-04-23 14:49:47 +02:00
|
|
|
|
2021-05-11 16:08:59 +02:00
|
|
|
const TEST_MODE = false
|
2021-04-27 18:29:05 +02:00
|
|
|
const GLOBAL_DB = StaticDatabases.GLOBAL.name
|
|
|
|
const TYPE = TemplateTypes.EMAIL
|
|
|
|
|
|
|
|
const FULL_EMAIL_PURPOSES = [
|
|
|
|
EmailTemplatePurpose.INVITATION,
|
|
|
|
EmailTemplatePurpose.PASSWORD_RECOVERY,
|
|
|
|
EmailTemplatePurpose.WELCOME,
|
2021-05-11 16:08:59 +02:00
|
|
|
EmailTemplatePurpose.CUSTOM,
|
2021-04-27 18:29:05 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
function createSMTPTransport(config) {
|
2021-05-11 16:08:59 +02:00
|
|
|
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",
|
|
|
|
},
|
2021-04-23 14:49:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nodemailer.createTransport(options)
|
|
|
|
}
|
|
|
|
|
2021-05-05 13:11:06 +02:00
|
|
|
async function getLinkCode(purpose, email, user) {
|
|
|
|
switch (purpose) {
|
|
|
|
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
|
|
|
return getResetPasswordCode(user._id)
|
|
|
|
case EmailTemplatePurpose.INVITATION:
|
|
|
|
return getInviteCode(email)
|
|
|
|
default:
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
2021-05-11 16:08:59 +02:00
|
|
|
* @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.
|
2021-05-05 13:11:06 +02:00
|
|
|
* @return {Promise<string>} returns the built email HTML if all provided parameters were valid.
|
|
|
|
*/
|
2021-05-11 16:08:59 +02:00
|
|
|
async function buildEmail(purpose, email, context, { user, contents } = {}) {
|
2021-04-27 18:29:05 +02:00
|
|
|
// this isn't a full email
|
|
|
|
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
|
|
|
|
throw `Unable to build an email of type ${purpose}`
|
|
|
|
}
|
2021-05-11 13:02:29 +02:00
|
|
|
let [base, body] = await Promise.all([
|
2021-04-27 18:29:05 +02:00
|
|
|
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
|
|
|
|
getTemplateByPurpose(TYPE, purpose),
|
|
|
|
])
|
2021-05-11 13:02:29 +02:00
|
|
|
if (!base || !body) {
|
2021-04-27 18:29:05 +02:00
|
|
|
throw "Unable to build email, missing base components"
|
|
|
|
}
|
|
|
|
base = base.contents
|
|
|
|
body = body.contents
|
2021-05-11 16:08:59 +02:00
|
|
|
context = {
|
|
|
|
...context,
|
|
|
|
contents,
|
2021-04-27 18:29:05 +02:00
|
|
|
email,
|
|
|
|
user: user || {},
|
|
|
|
}
|
|
|
|
|
|
|
|
body = await processString(body, context)
|
|
|
|
// this should now be the complete email HTML
|
|
|
|
return processString(base, {
|
|
|
|
...context,
|
|
|
|
body,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-05-05 13:11:06 +02:00
|
|
|
/**
|
|
|
|
* Utility function for finding most valid SMTP configuration.
|
|
|
|
* @param {object} db The CouchDB database which is to be looked up within.
|
|
|
|
* @param {string|null} groupId If using finer grain control of configs a group can be used.
|
|
|
|
* @return {Promise<object|null>} returns the SMTP configuration if it exists
|
|
|
|
*/
|
|
|
|
async function getSmtpConfiguration(db, groupId = null) {
|
2021-04-27 18:29:05 +02:00
|
|
|
const params = {
|
|
|
|
type: Configs.SMTP,
|
|
|
|
}
|
|
|
|
if (groupId) {
|
|
|
|
params.group = groupId
|
|
|
|
}
|
2021-05-06 11:51:21 +02:00
|
|
|
return getScopedConfig(db, params)
|
2021-05-05 13:11:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if a SMTP config exists based on passed in parameters.
|
|
|
|
* @param groupId
|
|
|
|
* @return {Promise<boolean>} returns true if there is a configuration that can be used.
|
|
|
|
*/
|
|
|
|
exports.isEmailConfigured = async (groupId = null) => {
|
|
|
|
const db = new CouchDB(GLOBAL_DB)
|
|
|
|
const config = await getSmtpConfiguration(db, groupId)
|
|
|
|
return config != null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given an email address and an email purpose this will retrieve the SMTP configuration and
|
|
|
|
* send an email using it.
|
|
|
|
* @param {string} email The email address to send to.
|
|
|
|
* @param {string} purpose The purpose of the email being sent (e.g. reset password).
|
2021-05-05 16:10:28 +02:00
|
|
|
* @param {string|undefined} groupId If finer grain controls being used then this will lookup config for group.
|
2021-05-11 13:02:29 +02:00
|
|
|
* @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.
|
2021-05-11 16:08:59 +02:00
|
|
|
* @param {string|undefined} subject A custom subject can be specified if the config one is not desired.
|
2021-05-05 13:11:06 +02:00
|
|
|
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
|
|
|
|
* nodemailer response.
|
|
|
|
*/
|
2021-05-11 13:02:29 +02:00
|
|
|
exports.sendEmail = async (
|
|
|
|
email,
|
|
|
|
purpose,
|
2021-05-11 16:08:59 +02:00
|
|
|
{ groupId, user, from, contents, subject } = {}
|
2021-05-11 13:02:29 +02:00
|
|
|
) => {
|
2021-05-05 13:11:06 +02:00
|
|
|
const db = new CouchDB(GLOBAL_DB)
|
2021-05-11 16:08:59 +02:00
|
|
|
let config = await getSmtpConfiguration(db, groupId) || {}
|
|
|
|
if (Object.keys(config).length === 0 && !TEST_MODE) {
|
2021-05-05 16:10:28 +02:00
|
|
|
throw "Unable to find SMTP configuration."
|
2021-04-27 18:29:05 +02:00
|
|
|
}
|
|
|
|
const transport = createSMTPTransport(config)
|
2021-05-11 16:08:59 +02:00
|
|
|
// if there is a link code needed this will retrieve it
|
|
|
|
const code = await getLinkCode(purpose, email, user)
|
|
|
|
const context = await getSettingsTemplateContext(purpose, code)
|
2021-04-27 18:29:05 +02:00
|
|
|
const message = {
|
2021-05-11 13:02:29 +02:00
|
|
|
from: from || config.from,
|
2021-05-11 16:08:59 +02:00
|
|
|
subject: await processString(subject || config.subject, context),
|
2021-04-27 18:29:05 +02:00
|
|
|
to: email,
|
2021-05-11 16:08:59 +02:00
|
|
|
html: await buildEmail(purpose, email, context, { user, contents }),
|
|
|
|
}
|
|
|
|
const response = await transport.sendMail(message)
|
|
|
|
if (TEST_MODE) {
|
|
|
|
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
|
2021-04-27 18:29:05 +02:00
|
|
|
}
|
2021-05-11 16:08:59 +02:00
|
|
|
return response
|
2021-04-27 18:29:05 +02:00
|
|
|
}
|
|
|
|
|
2021-05-05 13:11:06 +02:00
|
|
|
/**
|
|
|
|
* Given an SMTP configuration this runs it through nodemailer to see if it is infact functional.
|
|
|
|
* @param {object} config an SMTP configuration - this is based on the nodemailer API.
|
|
|
|
* @return {Promise<boolean>} returns true if the configuration is valid.
|
|
|
|
*/
|
2021-04-23 14:49:47 +02:00
|
|
|
exports.verifyConfig = async config => {
|
2021-04-27 18:29:05 +02:00
|
|
|
const transport = createSMTPTransport(config)
|
2021-04-23 14:49:47 +02:00
|
|
|
await transport.verify()
|
|
|
|
}
|