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

219 lines
6.7 KiB
TypeScript
Raw Normal View History

2022-11-16 19:13:34 +01:00
import env from "../environment"
import { EmailTemplatePurpose, TemplateType } from "../constants"
2023-03-13 13:33:16 +01:00
import { getTemplateByPurpose, EmailTemplates } from "../constants/templates"
2022-11-16 19:13:34 +01:00
import { getSettingsTemplateContext } from "./templates"
import { processString } from "@budibase/string-templates"
import { getResetPasswordCode, getInviteCode } from "./redis"
import { User, SMTPInnerConfig } from "@budibase/types"
import { configs } 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 = env.ENABLE_EMAIL_TEST_MODE && env.isDev()
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,
]
function createSMTPTransport(config?: SMTPInnerConfig) {
2022-11-16 19:13:34 +01:00
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: {
Per user pricing (#10378) * Update pro version to 2.4.44-alpha.9 (#10231) Co-authored-by: Budibase Staging Release Bot <> * Track installation and unique tenant id on licence activate (#10146) * changes and exports * removing the extend * Lint + tidy * Update account.ts --------- Co-authored-by: Rory Powell <rory.codes@gmail.com> Co-authored-by: mike12345567 <me@michaeldrury.co.uk> * Type updates for loading new plans (#10245) * Add new quota for max users on free plan * Split available vs purchased plan & price type definitions. Update usages of available prices and plans * Type fixes * Add types for minimums * New `PlanModel` type for `PER_USER` and `DAY_PASS` (#10247) * Add new quota for max users on free plan * Split available vs purchased plan & price type definitions. Update usages of available prices and plans * Type fixes * Add types for minimums * New `PlanModel` type for `PER_USER` and `DAY_PASS` * Add loadEnvFiles to lerna config for run command to prevent local test failures * Fix types in license test structure * Add quotas integration to user create / delete + migration (#10250) * Add new quota for max users on free plan * Split available vs purchased plan & price type definitions. Update usages of available prices and plans * Type fixes * Add types for minimums * New `PlanModel` type for `PER_USER` and `DAY_PASS` * Add loadEnvFiles to lerna config for run command to prevent local test failures * Fix types in license test structure * Add quotas integration to user create / delete * Always sync user count from view total_rows value for accuracy * Add migration to sync users * Add syncUsers.spec.ts * Lint * Types and structures for user subscription quantity sync (#10280) * Add new quota for max users on free plan * Split available vs purchased plan & price type definitions. Update usages of available prices and plans * Type fixes * Add types for minimums * New `PlanModel` type for `PER_USER` and `DAY_PASS` * Add loadEnvFiles to lerna config for run command to prevent local test failures * Fix types in license test structure * Add quotas integration to user create / delete * Always sync user count from view total_rows value for accuracy * Add migration to sync users * Add syncUsers.spec.ts * Prevent old installs from activating, track install info via get license request instead of on activation. * Add usesInvoicing to PurchasedPlan * Add min/max users to PurchasedPlan * Additional test structures for generating a license, remove maxUsers from PurchasedPlan - this is already present in the license quotas * Stripe integration for monthly prorations on annual plans * Integrate annual prorations with test clocks * Updated types, test utils and date processing for licensing (#10346) * Add new quota for max users on free plan * Split available vs purchased plan & price type definitions. Update usages of available prices and plans * Type fixes * Add types for minimums * New `PlanModel` type for `PER_USER` and `DAY_PASS` * Add loadEnvFiles to lerna config for run command to prevent local test failures * Fix types in license test structure * Add quotas integration to user create / delete * Always sync user count from view total_rows value for accuracy * Add migration to sync users * Add syncUsers.spec.ts * Prevent old installs from activating, track install info via get license request instead of on activation. * Add usesInvoicing to PurchasedPlan * Add min/max users to PurchasedPlan * Additional test structures for generating a license, remove maxUsers from PurchasedPlan - this is already present in the license quotas * Stripe integration for monthly prorations on annual plans * Integrate annual prorations with test clocks * Updated types, test utils and date processing * Lint * Pricing/billing page (#10353) * bbui updates for billing page * Require all PlanTypes in PlanMinimums for compile time safety * fix test package utils * Incoming user limits warnings (#10379) * incoming user limits warning * fix inlinealert button * add corretc button link and text to user alert * pr comments * simplify limit check * Types and test updates for subscription quantity changes in account-portal (#10372) * Add chance extensions for `arrayOf`. Update events spies with license events * Add generics to doInTenant response * Update account structure with quota usage * User count limits (#10385) * incoming user limits warning * fix inlinealert button * add corretc button link and text to user alert * pr comments * simplify limit check * user limit messaging on add users modal * user limit messaging on import users modal * update licensing store to be more generic * some styling updates * remove console log * Store tweaks * Add startDate to Quota type --------- Co-authored-by: Rory Powell <rory.codes@gmail.com> * Lint * Support custom lock options * Reactivity fixes for add user modals * Update ethereal email creds * Add warn for getting invite from code error * Extract disabling user import condition * Handling unlimited users in modals logic and adding start date processing to store * Lint * Integration testing fixes (#10389) * lint --------- Co-authored-by: Mateus Badan de Pieri <mateuspieri@gmail.com> Co-authored-by: mike12345567 <me@michaeldrury.co.uk> Co-authored-by: Peter Clement <PClmnt@users.noreply.github.com>
2023-04-24 10:31:48 +02:00
user: "seamus99@ethereal.email",
pass: "5ghVteZAqj6jkKJF9R",
},
}
}
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),
])
2023-03-13 13:33:16 +01:00
2023-03-16 10:07:21 +01:00
// Change from branding to core
2023-03-27 19:00:57 +02:00
let core = EmailTemplates[EmailTemplatePurpose.CORE]
2023-03-13 13:33:16 +01:00
2023-03-16 10:07:21 +01:00
if (!base || !body || !core) {
throw "Unable to build email, missing base components"
}
base = base.contents
body = body.contents
2023-03-13 13:33:16 +01:00
let name = user ? user.name : undefined
if (user && !name && user.firstName) {
name = user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName
}
context = {
2023-03-16 10:07:21 +01:00
...context,
contents,
email,
name,
user: user || {},
}
2023-03-16 10:07:21 +01:00
// Prepend the core template
const fullBody = core + body
2023-03-13 13:33:16 +01:00
2023-03-16 10:07:21 +01:00
body = await processString(fullBody, context)
2023-03-13 13:33:16 +01:00
// this should now be the core email HTML
return processString(base, {
...context,
2023-03-16 10:07:21 +01:00
body,
})
}
/**
* 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.
*/
export async function isEmailConfigured() {
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
}
const config = await configs.getSMTPConfig()
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 config = await configs.getSMTPConfig(opts?.automation)
if (!config && !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 = 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
}
if (opts?.subject || config?.subject) {
2022-11-16 19:13:34 +01:00
message.subject = await processString(
(opts?.subject || config?.subject) as string,
2022-11-16 19:13:34 +01:00
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.
*/
export async function verifyConfig(config: SMTPInnerConfig) {
const transport = createSMTPTransport(config)
await transport.verify()
}