budibase/packages/worker/src/utilities/email.ts

256 lines
7.7 KiB
TypeScript
Raw Normal View History

2022-11-16 19:13:34 +01:00
import env from "../environment"
import { EmailTemplatePurpose, TemplateType, Config } from "../constants"
import { getTemplateByPurpose } from "../constants/templates"
import { getSettingsTemplateContext } from "./templates"
import { processString } from "@budibase/string-templates"
import { getResetPasswordCode, getInviteCode } from "./redis"
import { User } from "@budibase/types"
import { tenancy, db as dbCore, PouchLike } from "@budibase/backend-core"
const nodemailer = require("nodemailer")
2022-11-16 19:13:34 +01:00
type SendEmailOpts = {
// workspaceId If finer grain controls being used then this will lookup config for workspace.
workspaceId?: string
// user If sending to an existing user the object can be provided, this is used in the context.
user: User
// from If sending from an address that is not what is configured in the SMTP config.
from?: string
// contents If sending a custom email then can supply contents which will be added to it.
contents?: string
// subject A custom subject can be specified if the config one is not desired.
subject?: string
// info Pass in a structure of information to be stored alongside the invitation.
info?: any
cc?: boolean
bcc?: boolean
automation?: boolean
}
const TEST_MODE = false
2022-11-16 19:13:34 +01:00
const TYPE = TemplateType.EMAIL
const FULL_EMAIL_PURPOSES = [
EmailTemplatePurpose.INVITATION,
EmailTemplatePurpose.PASSWORD_RECOVERY,
EmailTemplatePurpose.WELCOME,
EmailTemplatePurpose.CUSTOM,
]
2022-11-16 19:13:34 +01:00
function createSMTPTransport(config: any) {
let options: any
let secure = config.secure
// default it if not specified
if (secure == null) {
secure = config.port === 465
}
if (!TEST_MODE) {
options = {
port: config.port,
host: config.host,
secure: secure,
auth: config.auth,
}
options.tls = {
rejectUnauthorized: false,
}
if (config.connectionTimeout) {
options.connectionTimeout = config.connectionTimeout
}
} else {
options = {
port: 587,
host: "smtp.ethereal.email",
secure: false,
auth: {
user: "don.bahringer@ethereal.email",
pass: "yCKSH8rWyUPbnhGYk9",
},
}
}
return nodemailer.createTransport(options)
}
2022-11-16 19:13:34 +01:00
async function getLinkCode(
purpose: EmailTemplatePurpose,
email: string,
user: User,
info: any = null
) {
switch (purpose) {
case EmailTemplatePurpose.PASSWORD_RECOVERY:
2022-11-16 19:13:34 +01:00
return getResetPasswordCode(user._id!, info)
case EmailTemplatePurpose.INVITATION:
return getInviteCode(email, info)
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.
* @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.
*/
2022-11-16 19:13:34 +01:00
async function buildEmail(
purpose: EmailTemplatePurpose,
email: string,
context: any,
{ user, contents }: any = {}
) {
// this isn't a full email
if (FULL_EMAIL_PURPOSES.indexOf(purpose) === -1) {
throw `Unable to build an email of type ${purpose}`
}
let [base, body] = await Promise.all([
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
getTemplateByPurpose(TYPE, purpose),
])
if (!base || !body) {
throw "Unable to build email, missing base components"
}
base = base.contents
body = body.contents
let name = user ? user.name : undefined
if (user && !name && user.firstName) {
name = user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName
}
context = {
...context,
contents,
email,
name,
user: user || {},
}
body = await processString(body, context)
// this should now be the complete email HTML
return processString(base, {
...context,
body,
})
}
/**
* Utility function for finding most valid SMTP configuration.
* @param {object} db The CouchDB database which is to be looked up within.
* @param {string|null} workspaceId If using finer grain control of configs a workspace can be used.
2021-09-27 17:28:39 +02:00
* @param {boolean|null} automation Whether or not the configuration is being fetched for an email automation.
* @return {Promise<object|null>} returns the SMTP configuration if it exists
*/
2022-11-16 19:13:34 +01:00
async function getSmtpConfiguration(
db: dbCore.PouchLike,
2022-11-16 19:13:34 +01:00
workspaceId?: string,
automation?: boolean
) {
const params: any = {
type: Config.SMTP,
}
if (workspaceId) {
params.workspace = workspaceId
}
2021-09-27 15:57:22 +02:00
2022-11-16 19:13:34 +01:00
const customConfig = await dbCore.getScopedConfig(db, params)
2021-09-27 16:10:06 +02:00
if (customConfig) {
return customConfig
2021-09-27 17:28:39 +02:00
}
2021-09-27 16:10:06 +02:00
// Use an SMTP fallback configuration from env variables
2021-09-27 17:28:39 +02:00
if (!automation && env.SMTP_FALLBACK_ENABLED) {
2021-09-27 15:57:22 +02:00
return {
port: env.SMTP_PORT,
host: env.SMTP_HOST,
secure: false,
2021-10-04 16:58:05 +02:00
from: env.SMTP_FROM_ADDRESS,
2021-09-27 15:57:22 +02:00
auth: {
user: env.SMTP_USER,
pass: env.SMTP_PASSWORD,
},
}
}
}
/**
* Checks if a SMTP config exists based on passed in parameters.
* @return {Promise<boolean>} returns true if there is a configuration that can be used.
*/
2022-11-16 19:13:34 +01:00
export async function isEmailConfigured(workspaceId?: string) {
2021-09-27 15:57:22 +02:00
// when "testing" or smtp fallback is enabled simply return true
if (TEST_MODE || env.SMTP_FALLBACK_ENABLED) {
return true
}
2022-11-16 19:13:34 +01:00
const db = tenancy.getGlobalDB()
const config = await getSmtpConfiguration(db, workspaceId)
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).
2022-11-16 19:13:34 +01:00
* @param {object} opts The options for sending the email.
* @return {Promise<object>} returns details about the attempt to send email, e.g. if it is successful; based on
* nodemailer response.
*/
2022-11-16 19:13:34 +01:00
export async function sendEmail(
email: string,
purpose: EmailTemplatePurpose,
opts: SendEmailOpts
) {
const db = tenancy.getGlobalDB()
let config =
(await getSmtpConfiguration(db, opts?.workspaceId, opts?.automation)) || {}
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
2022-11-16 19:13:34 +01:00
const code = await getLinkCode(purpose, email, opts.user, opts?.info)
let context
if (code) {
context = await getSettingsTemplateContext(purpose, code)
}
2022-07-05 10:21:59 +02:00
2022-11-16 19:13:34 +01:00
let message: any = {
from: opts?.from || config.from,
html: await buildEmail(purpose, email, context, {
2022-11-16 19:13:34 +01:00
user: opts?.user,
contents: opts?.contents,
}),
}
2022-07-05 10:21:59 +02:00
2022-07-26 13:17:01 +02:00
message = {
...message,
to: email,
2022-11-16 19:13:34 +01:00
cc: opts?.cc,
bcc: opts?.bcc,
2022-07-05 10:21:59 +02:00
}
2022-11-16 19:13:34 +01:00
if (opts?.subject || config.subject) {
message.subject = await processString(
opts?.subject || config.subject,
context
)
2021-05-19 17:39:17 +02:00
}
const response = await transport.sendMail(message)
if (TEST_MODE) {
console.log("Test email URL: " + nodemailer.getTestMessageUrl(response))
}
return response
}
/**
* Given an SMTP configuration this runs it through nodemailer to see if it is in fact functional.
* @param {object} config an SMTP configuration - this is based on the nodemailer API.
* @return {Promise<boolean>} returns true if the configuration is valid.
*/
2022-11-16 19:13:34 +01:00
export async function verifyConfig(config: any) {
const transport = createSMTPTransport(config)
await transport.verify()
}