2023-03-15 12:21:52 +01:00
|
|
|
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'
|
2023-03-15 08:35:16 +01:00
|
|
|
import { DefaultBotNotificationEmail } from '@typebot.io/emails'
|
2022-11-29 10:02:40 +01:00
|
|
|
import {
|
|
|
|
PublicTypebot,
|
2023-03-07 14:41:57 +01:00
|
|
|
ResultInSession,
|
2022-11-29 10:02:40 +01:00
|
|
|
SendEmailBlock,
|
|
|
|
SendEmailOptions,
|
|
|
|
SessionState,
|
2023-03-09 08:46:36 +01:00
|
|
|
SmtpCredentials,
|
2023-03-15 08:35:16 +01:00
|
|
|
} from '@typebot.io/schemas'
|
2022-11-29 10:02:40 +01:00
|
|
|
import { createTransport } from 'nodemailer'
|
|
|
|
import Mail from 'nodemailer/lib/mailer'
|
2023-03-15 08:35:16 +01:00
|
|
|
import { byId, isEmpty, isNotDefined, omit } from '@typebot.io/lib'
|
|
|
|
import { parseAnswers } from '@typebot.io/lib/results'
|
|
|
|
import { decrypt } from '@typebot.io/lib/api'
|
2023-03-15 12:21:52 +01:00
|
|
|
import { defaultFrom, defaultTransportOptions } from './constants'
|
|
|
|
import { ExecuteIntegrationResponse } from '@/features/chat/types'
|
|
|
|
import { saveErrorLog } from '@/features/logs/saveErrorLog'
|
|
|
|
import { saveSuccessLog } from '@/features/logs/saveSuccessLog'
|
2022-11-29 10:02:40 +01:00
|
|
|
|
|
|
|
export const executeSendEmailBlock = async (
|
2023-03-07 14:41:57 +01:00
|
|
|
{ result, typebot }: SessionState,
|
2022-11-29 10:02:40 +01:00
|
|
|
block: SendEmailBlock
|
|
|
|
): Promise<ExecuteIntegrationResponse> => {
|
|
|
|
const { options } = block
|
|
|
|
const { variables } = typebot
|
2023-03-07 14:41:57 +01:00
|
|
|
const isPreview = !result.id
|
2023-01-25 11:27:47 +01:00
|
|
|
if (isPreview)
|
|
|
|
return {
|
|
|
|
outgoingEdgeId: block.outgoingEdgeId,
|
|
|
|
logs: [
|
|
|
|
{
|
|
|
|
status: 'info',
|
|
|
|
description: 'Emails are not sent in preview mode',
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
2022-11-29 10:02:40 +01:00
|
|
|
await sendEmail({
|
|
|
|
typebotId: typebot.id,
|
2023-03-07 14:41:57 +01:00
|
|
|
result,
|
2022-11-29 10:02:40 +01:00
|
|
|
credentialsId: options.credentialsId,
|
|
|
|
recipients: options.recipients.map(parseVariables(variables)),
|
|
|
|
subject: parseVariables(variables)(options.subject ?? ''),
|
|
|
|
body: parseVariables(variables)(options.body ?? ''),
|
|
|
|
cc: (options.cc ?? []).map(parseVariables(variables)),
|
|
|
|
bcc: (options.bcc ?? []).map(parseVariables(variables)),
|
|
|
|
replyTo: options.replyTo
|
|
|
|
? parseVariables(variables)(options.replyTo)
|
|
|
|
: undefined,
|
|
|
|
fileUrls:
|
|
|
|
variables.find(byId(options.attachmentsVariableId))?.value ?? undefined,
|
|
|
|
isCustomBody: options.isCustomBody,
|
|
|
|
isBodyCode: options.isBodyCode,
|
|
|
|
})
|
|
|
|
return { outgoingEdgeId: block.outgoingEdgeId }
|
|
|
|
}
|
|
|
|
|
|
|
|
const sendEmail = async ({
|
|
|
|
typebotId,
|
2023-03-07 14:41:57 +01:00
|
|
|
result,
|
2022-11-29 10:02:40 +01:00
|
|
|
credentialsId,
|
|
|
|
recipients,
|
|
|
|
body,
|
|
|
|
subject,
|
|
|
|
cc,
|
|
|
|
bcc,
|
|
|
|
replyTo,
|
|
|
|
isBodyCode,
|
|
|
|
isCustomBody,
|
|
|
|
fileUrls,
|
|
|
|
}: SendEmailOptions & {
|
|
|
|
typebotId: string
|
2023-03-07 14:41:57 +01:00
|
|
|
result: ResultInSession
|
2023-02-23 14:44:37 +01:00
|
|
|
fileUrls?: string | string[]
|
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,
|
|
|
|
typebotId,
|
2023-03-07 14:41:57 +01:00
|
|
|
result,
|
2022-11-29 10:02:40 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
if (!emailBody) {
|
|
|
|
await saveErrorLog({
|
2023-03-07 14:41:57 +01:00
|
|
|
resultId: result.id,
|
2022-11-29 10:02:40 +01:00
|
|
|
message: 'Email not sent',
|
|
|
|
details: {
|
2023-02-24 07:47:13 +01:00
|
|
|
error: 'No email body found',
|
2022-11-29 10:02:40 +01:00
|
|
|
transportConfig,
|
|
|
|
recipients,
|
|
|
|
subject,
|
|
|
|
cc,
|
|
|
|
bcc,
|
|
|
|
replyTo,
|
|
|
|
emailBody,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
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,
|
2023-02-23 14:44:37 +01:00
|
|
|
attachments: fileUrls
|
|
|
|
? (typeof fileUrls === 'string' ? fileUrls.split(', ') : fileUrls).map(
|
|
|
|
(url) => ({ path: url })
|
|
|
|
)
|
|
|
|
: undefined,
|
2022-11-29 10:02:40 +01:00
|
|
|
...emailBody,
|
|
|
|
}
|
|
|
|
try {
|
2022-12-22 17:02:34 +01:00
|
|
|
await transporter.sendMail(email)
|
2022-11-29 10:02:40 +01:00
|
|
|
await saveSuccessLog({
|
2023-03-07 14:41:57 +01:00
|
|
|
resultId: result.id,
|
2022-11-29 10:02:40 +01:00
|
|
|
message: 'Email successfully sent',
|
|
|
|
details: {
|
|
|
|
transportConfig: {
|
|
|
|
...transportConfig,
|
|
|
|
auth: { user: transportConfig.auth.user, pass: '******' },
|
|
|
|
},
|
|
|
|
email,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
} catch (err) {
|
|
|
|
await saveErrorLog({
|
2023-03-07 14:41:57 +01:00
|
|
|
resultId: result.id,
|
2022-11-29 10:02:40 +01:00
|
|
|
message: 'Email not sent',
|
|
|
|
details: {
|
2023-02-24 07:47:13 +01:00
|
|
|
error: err,
|
2022-11-29 10:02:40 +01:00
|
|
|
transportConfig: {
|
|
|
|
...transportConfig,
|
|
|
|
auth: { user: transportConfig.auth.user, pass: '******' },
|
|
|
|
},
|
|
|
|
email,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const getEmailInfo = async (
|
|
|
|
credentialsId: string
|
2023-03-09 08:46:36 +01:00
|
|
|
): 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
|
2023-03-09 08:46:36 +01:00
|
|
|
return decrypt(credentials.data, credentials.iv) as SmtpCredentials['data']
|
2022-11-29 10:02:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const getEmailBody = async ({
|
|
|
|
body,
|
|
|
|
isCustomBody,
|
|
|
|
isBodyCode,
|
|
|
|
typebotId,
|
2023-03-07 14:41:57 +01:00
|
|
|
result,
|
2022-11-29 10:02:40 +01:00
|
|
|
}: {
|
|
|
|
typebotId: string
|
2023-03-07 14:41:57 +01:00
|
|
|
result: ResultInSession
|
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 typebot = (await prisma.publicTypebot.findUnique({
|
|
|
|
where: { typebotId },
|
|
|
|
})) as unknown as PublicTypebot
|
|
|
|
if (!typebot) return
|
2023-03-07 14:41:57 +01:00
|
|
|
const answers = parseAnswers(typebot, [])(result)
|
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,
|
|
|
|
}
|
|
|
|
}
|