2
0
Files
bot/apps/viewer/src/features/blocks/integrations/sendEmail/executeSendEmailBlock.tsx

260 lines
6.8 KiB
TypeScript
Raw Normal View History

import { parseVariables } from '@/features/variables/parseVariables'
2022-11-29 10:02:40 +01:00
import prisma from '@/lib/prisma'
2023-01-14 16:53:34 +01:00
import { render } from '@faire/mjml-react/utils/render'
import { DefaultBotNotificationEmail } 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 { ExecuteIntegrationResponse } from '@/features/chat/types'
import { findUniqueVariableValue } from '../../../variables/findUniqueVariableValue'
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 body =
findUniqueVariableValue(typebot.variables)(options.body)?.toString() ??
parseVariables(typebot.variables, { escapeHtml: 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,
port: defaultTransportOptions.port,
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={`${process.env.NEXTAUTH_URL}/typebots/${typebot.id}/results`}
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)
}