2022-11-16 19:13:34 +01:00
|
|
|
import env from "../environment"
|
2023-02-23 14:42:10 +01:00
|
|
|
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"
|
2023-06-08 15:25:35 +02:00
|
|
|
import { User, SendEmailOpts, SMTPInnerConfig } from "@budibase/types"
|
2023-11-17 17:20:10 +01:00
|
|
|
import { configs, cache } from "@budibase/backend-core"
|
2023-06-08 15:25:35 +02:00
|
|
|
import ical from "ical-generator"
|
2023-11-20 21:52:29 +01:00
|
|
|
|
2021-04-23 14:49:47 +02:00
|
|
|
const nodemailer = require("nodemailer")
|
2022-11-16 19:13:34 +01:00
|
|
|
|
2023-02-21 09:23:53 +01:00
|
|
|
const TEST_MODE = env.ENABLE_EMAIL_TEST_MODE && env.isDev()
|
2022-11-16 19:13:34 +01:00
|
|
|
const TYPE = TemplateType.EMAIL
|
2021-04-27 18:29:05 +02:00
|
|
|
|
|
|
|
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
|
|
|
]
|
|
|
|
|
2023-02-23 14:42:10 +01:00
|
|
|
function createSMTPTransport(config?: SMTPInnerConfig) {
|
2022-11-16 19:13:34 +01:00
|
|
|
let options: any
|
2023-02-23 14:42:10 +01:00
|
|
|
let secure = config?.secure
|
2021-06-10 12:25:06 +02:00
|
|
|
// default it if not specified
|
|
|
|
if (secure == null) {
|
2023-02-23 14:42:10 +01:00
|
|
|
secure = config?.port === 465
|
2021-06-10 12:25:06 +02:00
|
|
|
}
|
2021-05-11 16:08:59 +02:00
|
|
|
if (!TEST_MODE) {
|
|
|
|
options = {
|
2023-02-23 14:42:10 +01:00
|
|
|
port: config?.port,
|
|
|
|
host: config?.host,
|
2021-06-10 12:25:06 +02:00
|
|
|
secure: secure,
|
2023-02-23 14:42:10 +01:00
|
|
|
auth: config?.auth,
|
2021-05-11 16:08:59 +02:00
|
|
|
}
|
2021-06-09 16:45:54 +02:00
|
|
|
options.tls = {
|
|
|
|
rejectUnauthorized: false,
|
2021-05-11 16:08:59 +02:00
|
|
|
}
|
2023-02-23 14:42:10 +01:00
|
|
|
if (config?.connectionTimeout) {
|
2021-10-26 15:31:56 +02:00
|
|
|
options.connectionTimeout = config.connectionTimeout
|
|
|
|
}
|
2021-05-11 16:08:59 +02:00
|
|
|
} else {
|
|
|
|
options = {
|
|
|
|
port: 587,
|
|
|
|
host: "smtp.ethereal.email",
|
|
|
|
secure: false,
|
|
|
|
auth: {
|
2023-04-24 10:31:48 +02:00
|
|
|
user: "seamus99@ethereal.email",
|
|
|
|
pass: "5ghVteZAqj6jkKJF9R",
|
2021-05-11 16:08:59 +02:00
|
|
|
},
|
2021-04-23 14:49:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nodemailer.createTransport(options)
|
|
|
|
}
|
|
|
|
|
2022-11-16 19:13:34 +01:00
|
|
|
async function getLinkCode(
|
|
|
|
purpose: EmailTemplatePurpose,
|
|
|
|
email: string,
|
|
|
|
user: User,
|
|
|
|
info: any = null
|
|
|
|
) {
|
2021-05-05 13:11:06 +02:00
|
|
|
switch (purpose) {
|
|
|
|
case EmailTemplatePurpose.PASSWORD_RECOVERY:
|
2023-11-17 17:20:10 +01:00
|
|
|
return cache.passwordReset.createCode(user._id!, info)
|
2021-05-05 13:11:06 +02:00
|
|
|
case EmailTemplatePurpose.INVITATION:
|
2023-11-17 17:20:10 +01:00
|
|
|
return cache.invite.createCode(email, info)
|
2021-05-05 13:11:06 +02:00
|
|
|
default:
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Builds an email using handlebars and the templates found in the system (default or otherwise).
|
2023-10-17 17:46:32 +02:00
|
|
|
* @param purpose the purpose of the email being built, e.g. invitation, password reset.
|
|
|
|
* @param email the address which it is being sent to for contextual purposes.
|
|
|
|
* @param context the context which is being used for building the email (hbs context).
|
|
|
|
* @param user if being sent to an existing user then the object can be provided for context.
|
|
|
|
* @param contents if using a custom template can supply contents for context.
|
|
|
|
* @return returns the built email HTML if all provided parameters were valid.
|
2021-05-05 13:11:06 +02:00
|
|
|
*/
|
2022-11-16 19:13:34 +01:00
|
|
|
async function buildEmail(
|
|
|
|
purpose: EmailTemplatePurpose,
|
|
|
|
email: string,
|
|
|
|
context: any,
|
|
|
|
{ user, contents }: any = {}
|
|
|
|
) {
|
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-08-02 19:34:43 +02:00
|
|
|
getTemplateByPurpose(TYPE, EmailTemplatePurpose.BASE),
|
|
|
|
getTemplateByPurpose(TYPE, purpose),
|
2021-04-27 18:29:05 +02:00
|
|
|
])
|
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) {
|
2021-04-27 18:29:05 +02:00
|
|
|
throw "Unable to build email, missing base components"
|
|
|
|
}
|
2023-03-13 13:33:16 +01:00
|
|
|
|
2021-10-04 18:59:25 +02:00
|
|
|
let name = user ? user.name : undefined
|
|
|
|
if (user && !name && user.firstName) {
|
|
|
|
name = user.lastName ? `${user.firstName} ${user.lastName}` : user.firstName
|
|
|
|
}
|
2021-05-11 16:08:59 +02:00
|
|
|
context = {
|
2023-03-16 10:07:21 +01:00
|
|
|
...context,
|
2021-05-11 16:08:59 +02:00
|
|
|
contents,
|
2021-04-27 18:29:05 +02:00
|
|
|
email,
|
2021-10-04 18:59:25 +02:00
|
|
|
name,
|
2021-04-27 18:29:05 +02:00
|
|
|
user: user || {},
|
|
|
|
}
|
|
|
|
|
2023-03-13 13:33:16 +01:00
|
|
|
// this should now be the core email HTML
|
2023-11-07 19:14:52 +01:00
|
|
|
return processString(base.contents, {
|
2021-04-27 18:29:05 +02:00
|
|
|
...context,
|
2023-11-07 19:14:52 +01:00
|
|
|
body: await processString(core + body?.contents, context),
|
2021-04-27 18:29:05 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-05-05 13:11:06 +02:00
|
|
|
/**
|
|
|
|
* Checks if a SMTP config exists based on passed in parameters.
|
2023-10-17 17:46:32 +02:00
|
|
|
* @return returns true if there is a configuration that can be used.
|
2021-05-05 13:11:06 +02:00
|
|
|
*/
|
2023-02-23 14:42:10 +01:00
|
|
|
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) {
|
2021-05-18 16:48:28 +02:00
|
|
|
return true
|
|
|
|
}
|
2023-02-23 14:42:10 +01:00
|
|
|
const config = await configs.getSMTPConfig()
|
2021-05-05 13:11:06 +02:00
|
|
|
return config != null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given an email address and an email purpose this will retrieve the SMTP configuration and
|
|
|
|
* send an email using it.
|
2023-10-17 17:46:32 +02:00
|
|
|
* @param email The email address to send to.
|
|
|
|
* @param purpose The purpose of the email being sent (e.g. reset password).
|
|
|
|
* @param opts The options for sending the email.
|
|
|
|
* @return returns details about the attempt to send email, e.g. if it is successful; based on
|
2021-05-05 13:11:06 +02:00
|
|
|
* nodemailer response.
|
|
|
|
*/
|
2022-11-16 19:13:34 +01:00
|
|
|
export async function sendEmail(
|
|
|
|
email: string,
|
|
|
|
purpose: EmailTemplatePurpose,
|
|
|
|
opts: SendEmailOpts
|
|
|
|
) {
|
2023-02-23 14:42:10 +01:00
|
|
|
const config = await configs.getSMTPConfig(opts?.automation)
|
|
|
|
if (!config && !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
|
2022-11-16 19:13:34 +01:00
|
|
|
const code = await getLinkCode(purpose, email, opts.user, opts?.info)
|
2023-02-23 14:42:10 +01:00
|
|
|
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 = {
|
2023-02-23 14:42:10 +01:00
|
|
|
from: opts?.from || config?.from,
|
2021-08-05 10:59:08 +02:00
|
|
|
html: await buildEmail(purpose, email, context, {
|
2022-11-16 19:13:34 +01:00
|
|
|
user: opts?.user,
|
|
|
|
contents: opts?.contents,
|
2021-08-05 10:59:08 +02:00
|
|
|
}),
|
2021-05-11 16:08:59 +02:00
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
2023-02-23 14:42:10 +01:00
|
|
|
if (opts?.subject || config?.subject) {
|
2022-11-16 19:13:34 +01:00
|
|
|
message.subject = await processString(
|
2023-02-23 14:42:10 +01:00
|
|
|
(opts?.subject || config?.subject) as string,
|
2022-11-16 19:13:34 +01:00
|
|
|
context
|
|
|
|
)
|
2021-05-19 17:39:17 +02:00
|
|
|
}
|
2023-06-08 15:25:35 +02:00
|
|
|
if (opts?.invite) {
|
|
|
|
const calendar = ical({
|
|
|
|
name: "Invite",
|
|
|
|
})
|
|
|
|
calendar.createEvent({
|
|
|
|
start: opts.invite.startTime,
|
|
|
|
end: opts.invite.endTime,
|
|
|
|
summary: opts.invite.summary,
|
|
|
|
location: opts.invite.location,
|
|
|
|
url: opts.invite.url,
|
|
|
|
})
|
|
|
|
message = {
|
|
|
|
...message,
|
|
|
|
icalEvent: {
|
|
|
|
method: "request",
|
|
|
|
content: calendar.toString(),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-11 16:08:59 +02:00
|
|
|
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
|
|
|
/**
|
2021-07-09 18:50:01 +02:00
|
|
|
* Given an SMTP configuration this runs it through nodemailer to see if it is in fact functional.
|
2023-10-17 17:46:32 +02:00
|
|
|
* @param config an SMTP configuration - this is based on the nodemailer API.
|
|
|
|
* @return returns true if the configuration is valid.
|
2021-05-05 13:11:06 +02:00
|
|
|
*/
|
2023-02-28 14:36:54 +01:00
|
|
|
export async function verifyConfig(config: SMTPInnerConfig) {
|
2021-04-27 18:29:05 +02:00
|
|
|
const transport = createSMTPTransport(config)
|
2021-04-23 14:49:47 +02:00
|
|
|
await transport.verify()
|
|
|
|
}
|