2
0
Files
bot/packages/bot-engine/blocks/integrations/sendEmail/executeSendEmailBlock.tsx

273 lines
7.1 KiB
TypeScript
Raw Normal View History

import { DefaultBotNotificationEmail, render } from '@typebot.io/emails'
2022-11-29 10:02:40 +01:00
import {
AnswerInSessionState,
ReplyLog,
2022-11-29 10:02:40 +01:00
SendEmailBlock,
SendEmailOptions,
SessionState,
SmtpCredentials,
TypebotInSession,
Variable,
} from '@typebot.io/schemas'
2022-11-29 10:02:40 +01:00
import { createTransport } from 'nodemailer'
import Mail from 'nodemailer/lib/mailer'
import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
import { getDefinedVariables, parseAnswers } from '@typebot.io/lib/results'
import { decrypt } from '@typebot.io/lib/api'
import { defaultFrom, defaultTransportOptions } from './constants'
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
import { env } from '@typebot.io/env'
import { ExecuteIntegrationResponse } from '../../../types'
import prisma from '@typebot.io/lib/prisma'
import { parseVariables } from '../../../variables/parseVariables'
2022-11-29 10:02:40 +01:00
export const executeSendEmailBlock = async (
state: SessionState,
2022-11-29 10:02:40 +01:00
block: SendEmailBlock
): Promise<ExecuteIntegrationResponse> => {
const logs: ReplyLog[] = []
2022-11-29 10:02:40 +01:00
const { options } = block
const { typebot, resultId, answers } = state.typebotsQueue[0]
const isPreview = !resultId
if (isPreview)
return {
outgoingEdgeId: block.outgoingEdgeId,
logs: [
{
status: 'info',
description: 'Emails are not sent in preview mode',
},
],
}
const bodyUniqueVariable = findUniqueVariableValue(typebot.variables)(
options.body
)
const body = bodyUniqueVariable
? stringifyUniqueVariableValueAsHtml(bodyUniqueVariable)
: parseVariables(typebot.variables, { isInsideHtml: true })(
options.body ?? ''
)
try {
const sendEmailLogs = await sendEmail({
typebot,
answers,
credentialsId: options.credentialsId,
recipients: options.recipients.map(parseVariables(typebot.variables)),
subject: parseVariables(typebot.variables)(options.subject ?? ''),
body,
cc: (options.cc ?? []).map(parseVariables(typebot.variables)),
bcc: (options.bcc ?? []).map(parseVariables(typebot.variables)),
replyTo: options.replyTo
? parseVariables(typebot.variables)(options.replyTo)
: undefined,
fileUrls: getFileUrls(typebot.variables)(options.attachmentsVariableId),
isCustomBody: options.isCustomBody,
isBodyCode: options.isBodyCode,
})
if (sendEmailLogs) logs.push(...sendEmailLogs)
} catch (err) {
logs.push({
status: 'error',
details: err,
description: `Email not sent`,
})
}
return { outgoingEdgeId: block.outgoingEdgeId, logs }
2022-11-29 10:02:40 +01:00
}
const sendEmail = async ({
typebot,
answers,
2022-11-29 10:02:40 +01:00
credentialsId,
recipients,
body,
subject,
cc,
bcc,
replyTo,
isBodyCode,
isCustomBody,
fileUrls,
}: SendEmailOptions & {
typebot: TypebotInSession
answers: AnswerInSessionState[]
fileUrls?: string | string[]
}): Promise<ReplyLog[] | undefined> => {
const logs: ReplyLog[] = []
2022-11-29 10:02:40 +01:00
const { name: replyToName } = parseEmailRecipient(replyTo)
const { host, port, isTlsEnabled, username, password, from } =
(await getEmailInfo(credentialsId)) ?? {}
if (!from) return
const transportConfig = {
host,
port,
secure: isTlsEnabled ?? undefined,
auth: {
user: username,
pass: password,
},
}
const emailBody = await getEmailBody({
body,
isCustomBody,
isBodyCode,
typebot,
answersInSession: answers,
2022-11-29 10:02:40 +01:00
})
if (!emailBody) {
logs.push({
status: 'error',
description: 'Email not sent',
2022-11-29 10:02:40 +01:00
details: {
error: 'No email body found',
2022-11-29 10:02:40 +01:00
transportConfig,
recipients,
subject,
cc,
bcc,
replyTo,
emailBody,
},
})
return logs
2022-11-29 10:02:40 +01:00
}
const transporter = createTransport(transportConfig)
const fromName = isEmpty(replyToName) ? from.name : replyToName
const email: Mail.Options = {
from: fromName ? `"${fromName}" <${from.email}>` : from.email,
cc,
bcc,
to: recipients,
replyTo,
subject,
attachments: fileUrls
? (typeof fileUrls === 'string' ? fileUrls.split(', ') : fileUrls).map(
(url) => ({ path: url })
)
: undefined,
2022-11-29 10:02:40 +01:00
...emailBody,
}
try {
await transporter.sendMail(email)
logs.push({
status: 'success',
description: 'Email successfully sent',
2022-11-29 10:02:40 +01:00
details: {
transportConfig: {
...transportConfig,
auth: { user: transportConfig.auth.user, pass: '******' },
},
email,
},
})
} catch (err) {
logs.push({
status: 'error',
description: 'Email not sent',
2022-11-29 10:02:40 +01:00
details: {
error: err instanceof Error ? err.toString() : err,
2022-11-29 10:02:40 +01:00
transportConfig: {
...transportConfig,
auth: { user: transportConfig.auth.user, pass: '******' },
},
email,
},
})
}
return logs
2022-11-29 10:02:40 +01:00
}
const getEmailInfo = async (
credentialsId: string
): Promise<SmtpCredentials['data'] | undefined> => {
2022-11-29 10:02:40 +01:00
if (credentialsId === 'default')
return {
host: defaultTransportOptions.host,
2023-08-28 11:00:25 +02:00
port: defaultTransportOptions.port,
2022-11-29 10:02:40 +01:00
username: defaultTransportOptions.auth.user,
password: defaultTransportOptions.auth.pass,
isTlsEnabled: undefined,
from: defaultFrom,
}
const credentials = await prisma.credentials.findUnique({
where: { id: credentialsId },
})
if (!credentials) return
return (await decrypt(
credentials.data,
credentials.iv
)) as SmtpCredentials['data']
2022-11-29 10:02:40 +01:00
}
const getEmailBody = async ({
body,
isCustomBody,
isBodyCode,
typebot,
answersInSession,
2022-11-29 10:02:40 +01:00
}: {
typebot: TypebotInSession
answersInSession: AnswerInSessionState[]
2022-11-29 10:02:40 +01:00
} & Pick<SendEmailOptions, 'isCustomBody' | 'isBodyCode' | 'body'>): Promise<
{ html?: string; text?: string } | undefined
> => {
if (isCustomBody || (isNotDefined(isCustomBody) && !isEmpty(body)))
return {
html: isBodyCode ? body : undefined,
text: !isBodyCode ? body : undefined,
}
const answers = parseAnswers({
variables: getDefinedVariables(typebot.variables),
answers: answersInSession,
})
2022-11-29 10:02:40 +01:00
return {
html: render(
<DefaultBotNotificationEmail
resultsUrl={`${env.NEXTAUTH_URL}/typebots/${typebot.id}/results`}
2022-11-29 10:02:40 +01:00
answers={omit(answers, 'submittedAt')}
/>
).html,
}
}
const parseEmailRecipient = (
recipient?: string
): { email?: string; name?: string } => {
if (!recipient) return {}
if (recipient.includes('<')) {
const [name, email] = recipient.split('<')
return {
name: name.replace(/>/g, '').trim().replace(/"/g, ''),
email: email.replace('>', '').trim(),
}
}
return {
email: recipient,
}
}
const getFileUrls =
(variables: Variable[]) =>
(variableId: string | undefined): string | string[] | undefined => {
const fileUrls = variables.find(byId(variableId))?.value
if (!fileUrls) return
if (typeof fileUrls === 'string') return fileUrls
return fileUrls.filter(isDefined)
}
const stringifyUniqueVariableValueAsHtml = (
value: Variable['value']
): string => {
if (!value) return ''
if (typeof value === 'string') return value.replace(/\n/g, '<br />')
return value.map(stringifyUniqueVariableValueAsHtml).join('<br />')
}