feat(bot): ✨ Custom email body and HTML content
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
import { Stack, useDisclosure, Text } from '@chakra-ui/react'
|
import {
|
||||||
|
Stack,
|
||||||
|
useDisclosure,
|
||||||
|
Text,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
Switch,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { CodeEditor } from 'components/shared/CodeEditor'
|
||||||
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
import { CredentialsDropdown } from 'components/shared/CredentialsDropdown'
|
||||||
|
import { SwitchWithLabel } from 'components/shared/SwitchWithLabel'
|
||||||
import { Input, Textarea } from 'components/shared/Textbox'
|
import { Input, Textarea } from 'components/shared/Textbox'
|
||||||
import { CredentialsType, SendEmailOptions } from 'models'
|
import { CredentialsType, SendEmailOptions } from 'models'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
@@ -66,6 +75,18 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
replyTo,
|
replyTo,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleIsCustomBodyChange = (isCustomBody: boolean) =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
isCustomBody,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleIsBodyCodeChange = () =>
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
isBodyCode: options.isBodyCode ? !options.isBodyCode : true,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Stack>
|
<Stack>
|
||||||
@@ -121,15 +142,42 @@ export const SendEmailSettings = ({ options, onOptionsChange }: Props) => {
|
|||||||
defaultValue={options.subject ?? ''}
|
defaultValue={options.subject ?? ''}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<SwitchWithLabel
|
||||||
<Text>Body: </Text>
|
id={'custom-body'}
|
||||||
<Textarea
|
label={'Custom content?'}
|
||||||
data-testid="body-input"
|
initialValue={options.isCustomBody ?? false}
|
||||||
minH="300px"
|
onCheckChange={handleIsCustomBodyChange}
|
||||||
onChange={handleBodyChange}
|
/>
|
||||||
defaultValue={options.body ?? ''}
|
{options.isCustomBody && (
|
||||||
/>
|
<Stack>
|
||||||
</Stack>
|
<Flex justifyContent="space-between">
|
||||||
|
<Text>Content: </Text>
|
||||||
|
<HStack>
|
||||||
|
<Text fontSize="sm">Text</Text>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isChecked={options.isBodyCode ?? false}
|
||||||
|
onChange={handleIsBodyCodeChange}
|
||||||
|
/>
|
||||||
|
<Text fontSize="sm">Code</Text>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
{options.isBodyCode ? (
|
||||||
|
<CodeEditor
|
||||||
|
value={options.body ?? ''}
|
||||||
|
onChange={handleBodyChange}
|
||||||
|
lang="html"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Textarea
|
||||||
|
data-testid="body-input"
|
||||||
|
minH="300px"
|
||||||
|
onChange={handleBodyChange}
|
||||||
|
defaultValue={options.body ?? ''}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
<SmtpConfigModal
|
<SmtpConfigModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const GroupNode = ({ group, groupIndex }: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsConnecting(
|
setIsConnecting(
|
||||||
connectingIds?.target?.groupId === group.id &&
|
connectingIds?.target?.groupId === group.id &&
|
||||||
isNotDefined(connectingIds.target?.groupId)
|
isNotDefined(connectingIds.target?.blockId)
|
||||||
)
|
)
|
||||||
}, [connectingIds, group.id])
|
}, [connectingIds, group.id])
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ test.describe('Send email block', () => {
|
|||||||
'email1@gmail.com, email2@gmail.com'
|
'email1@gmail.com, email2@gmail.com'
|
||||||
)
|
)
|
||||||
await page.fill('[data-testid="subject-input"]', 'Email subject')
|
await page.fill('[data-testid="subject-input"]', 'Email subject')
|
||||||
|
await page.click('text="Custom body?"')
|
||||||
await page.fill('[data-testid="body-input"]', 'Here is my email')
|
await page.fill('[data-testid="body-input"]', 'Here is my email')
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Preview')
|
||||||
|
|||||||
188
apps/viewer/assets/newLeadEmailContent.ts
Normal file
188
apps/viewer/assets/newLeadEmailContent.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
const emailRegex =
|
||||||
|
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||||
|
|
||||||
|
const parseResult = (val?: string) =>
|
||||||
|
val && emailRegex.test(val) ? `<a href="mailto:${val}">${val}</a>` : val ?? ''
|
||||||
|
|
||||||
|
export const newLeadEmailContent = (
|
||||||
|
resultUrl: string,
|
||||||
|
answers: { [key: string]: string }
|
||||||
|
) => `
|
||||||
|
<!doctype html>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>
|
||||||
|
</title>
|
||||||
|
<!--[if !mso]><!-->
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<!--<![endif]-->
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style type="text/css">
|
||||||
|
#outlook a {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table,
|
||||||
|
td {
|
||||||
|
border-collapse: collapse;
|
||||||
|
mso-table-lspace: 0pt;
|
||||||
|
mso-table-rspace: 0pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
height: auto;
|
||||||
|
line-height: 100%;
|
||||||
|
outline: none;
|
||||||
|
text-decoration: none;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: block;
|
||||||
|
margin: 13px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!--[if mso]>
|
||||||
|
<noscript>
|
||||||
|
<xml>
|
||||||
|
<o:OfficeDocumentSettings>
|
||||||
|
<o:AllowPNG/>
|
||||||
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
|
</o:OfficeDocumentSettings>
|
||||||
|
</xml>
|
||||||
|
</noscript>
|
||||||
|
<![endif]-->
|
||||||
|
<!--[if lte mso 11]>
|
||||||
|
<style type="text/css">
|
||||||
|
.mj-outlook-group-fix { width:100% !important; }
|
||||||
|
</style>
|
||||||
|
<![endif]-->
|
||||||
|
<style type="text/css">
|
||||||
|
@media only screen and (min-width:480px) {
|
||||||
|
.mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style media="screen and (min-width:480px)">
|
||||||
|
.moz-text-html .mj-column-per-100 {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style type="text/css">
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="word-spacing:normal;background-color:#ffffff;">
|
||||||
|
<div style="background-color:#ffffff;">
|
||||||
|
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="margin:0px auto;max-width:600px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="border:1px solid #E2E8F0;direction:ltr;font-size:0px;padding:0;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:598px;" width="598" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:598px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:0;padding-bottom:20px;padding-top:20px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align:top;padding:0;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
||||||
|
<div style="font-family:Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif;font-size:16px;line-height:23px;text-align:left;color:#000000;">Your typebot has collected a <strong>new lead!</strong> 🥳</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
${Object.keys(answers)
|
||||||
|
.map(
|
||||||
|
(key) => `<tr>
|
||||||
|
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
||||||
|
<div style="font-family:Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif;font-size:16px;line-height:23px;text-align:left;color:#000000;">${key}: ${parseResult(
|
||||||
|
answers[key]
|
||||||
|
)}</div>
|
||||||
|
</td>
|
||||||
|
</tr>`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table></td></tr><tr><td class="" width="600px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:598px;" width="598" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||||
|
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:598px;">
|
||||||
|
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="direction:ltr;font-size:0px;padding:0;padding-bottom:20px;text-align:center;">
|
||||||
|
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||||
|
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align:top;padding:0;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style width="100%">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" vertical-align="middle" style="font-size:0px;padding:0;word-break:break-word;">
|
||||||
|
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
|
||||||
|
<tr>
|
||||||
|
<td align="center" bgcolor="#0042da" role="presentation" style="border:none;border-radius:5px;cursor:auto;mso-padding-alt:10px 25px;background:#0042da;" valign="middle">
|
||||||
|
<a href="${resultUrl}" style="display:inline-block;background:#0042da;color:white;font-family:Helvetica Neue, Helvetica, Helvetica, Arial, sans-serif;font-size:16px;font-weight:500;line-height:23px;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:5px;" target="_blank"> Check results </a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
`
|
||||||
@@ -1,12 +1,30 @@
|
|||||||
import prisma from 'libs/prisma'
|
import prisma from 'libs/prisma'
|
||||||
import { SendEmailOptions, SmtpCredentialsData } from 'models'
|
import {
|
||||||
|
ResultValues,
|
||||||
|
SendEmailOptions,
|
||||||
|
SmtpCredentialsData,
|
||||||
|
Typebot,
|
||||||
|
} from 'models'
|
||||||
import { NextApiRequest, NextApiResponse } from 'next'
|
import { NextApiRequest, NextApiResponse } from 'next'
|
||||||
import { createTransport, getTestMessageUrl } from 'nodemailer'
|
import { createTransport, getTestMessageUrl } from 'nodemailer'
|
||||||
import { decrypt, initMiddleware, methodNotAllowed } from 'utils'
|
import {
|
||||||
|
decrypt,
|
||||||
|
initMiddleware,
|
||||||
|
isNotDefined,
|
||||||
|
methodNotAllowed,
|
||||||
|
omit,
|
||||||
|
parseAnswers,
|
||||||
|
} from 'utils'
|
||||||
|
|
||||||
import Cors from 'cors'
|
import Cors from 'cors'
|
||||||
import { withSentry } from '@sentry/nextjs'
|
import { withSentry } from '@sentry/nextjs'
|
||||||
import { saveErrorLog, saveSuccessLog } from 'services/api/utils'
|
import {
|
||||||
|
getLinkedTypebots,
|
||||||
|
saveErrorLog,
|
||||||
|
saveSuccessLog,
|
||||||
|
} from 'services/api/utils'
|
||||||
|
import Mail from 'nodemailer/lib/mailer'
|
||||||
|
import { newLeadEmailContent } from 'assets/newLeadEmailContent'
|
||||||
|
|
||||||
const cors = initMiddleware(Cors())
|
const cors = initMiddleware(Cors())
|
||||||
|
|
||||||
@@ -21,17 +39,29 @@ const defaultTransportOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultFrom = {
|
const defaultFrom = {
|
||||||
name: process.env.NEXT_PUBLIC_SMTP_FROM?.split(' <')[0].replace(/"/g, ''),
|
name: process.env.SMTP_FROM?.split(' <')[0].replace(/"/g, ''),
|
||||||
email: process.env.NEXT_PUBLIC_SMTP_FROM?.match(/\<(.*)\>/)?.pop(),
|
email: process.env.SMTP_FROM?.match(/\<(.*)\>/)?.pop(),
|
||||||
}
|
}
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
await cors(req, res)
|
await cors(req, res)
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
|
const typebotId = req.query.typebotId as string
|
||||||
const resultId = req.query.resultId as string | undefined
|
const resultId = req.query.resultId as string | undefined
|
||||||
const { credentialsId, recipients, body, subject, cc, bcc, replyTo } = (
|
const {
|
||||||
|
credentialsId,
|
||||||
|
recipients,
|
||||||
|
body,
|
||||||
|
subject,
|
||||||
|
cc,
|
||||||
|
bcc,
|
||||||
|
replyTo,
|
||||||
|
isBodyCode,
|
||||||
|
isCustomBody,
|
||||||
|
resultValues,
|
||||||
|
} = (
|
||||||
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
typeof req.body === 'string' ? JSON.parse(req.body) : req.body
|
||||||
) as SendEmailOptions
|
) as SendEmailOptions & { resultValues: ResultValues }
|
||||||
|
|
||||||
const { host, port, isTlsEnabled, username, password, from } =
|
const { host, port, isTlsEnabled, username, password, from } =
|
||||||
(await getEmailInfo(credentialsId)) ?? {}
|
(await getEmailInfo(credentialsId)) ?? {}
|
||||||
@@ -47,15 +77,35 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
pass: password,
|
pass: password,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emailBody = await getEmailBody({
|
||||||
|
body,
|
||||||
|
isCustomBody,
|
||||||
|
isBodyCode,
|
||||||
|
typebotId,
|
||||||
|
resultValues,
|
||||||
|
})
|
||||||
|
if (!emailBody) {
|
||||||
|
await saveErrorLog(resultId, 'Email not sent', {
|
||||||
|
transportConfig,
|
||||||
|
recipients,
|
||||||
|
subject,
|
||||||
|
cc,
|
||||||
|
bcc,
|
||||||
|
replyTo,
|
||||||
|
emailBody,
|
||||||
|
})
|
||||||
|
return res.status(404).send({ message: "Couldn't find email body" })
|
||||||
|
}
|
||||||
const transporter = createTransport(transportConfig)
|
const transporter = createTransport(transportConfig)
|
||||||
const email = {
|
const email: Mail.Options = {
|
||||||
from: `"${from.name}" <${from.email}>`,
|
from: `"${from.name}" <${from.email}>`,
|
||||||
cc,
|
cc,
|
||||||
bcc,
|
bcc,
|
||||||
to: recipients,
|
to: recipients,
|
||||||
replyTo,
|
replyTo,
|
||||||
subject,
|
subject,
|
||||||
text: body,
|
...emailBody,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const info = await transporter.sendMail(email)
|
const info = await transporter.sendMail(email)
|
||||||
@@ -97,4 +147,39 @@ const getEmailInfo = async (
|
|||||||
return decrypt(credentials.data, credentials.iv) as SmtpCredentialsData
|
return decrypt(credentials.data, credentials.iv) as SmtpCredentialsData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getEmailBody = async ({
|
||||||
|
body,
|
||||||
|
isCustomBody,
|
||||||
|
isBodyCode,
|
||||||
|
typebotId,
|
||||||
|
resultValues,
|
||||||
|
}: { typebotId: string; resultValues: ResultValues } & Pick<
|
||||||
|
SendEmailOptions,
|
||||||
|
'isCustomBody' | 'isBodyCode' | 'body'
|
||||||
|
>): Promise<{ html?: string; text?: string } | undefined> => {
|
||||||
|
if (isCustomBody || isNotDefined(isCustomBody))
|
||||||
|
return {
|
||||||
|
html: isBodyCode ? body : undefined,
|
||||||
|
text: !isBodyCode ? body : undefined,
|
||||||
|
}
|
||||||
|
const typebot = (await prisma.typebot.findUnique({
|
||||||
|
where: { id: typebotId },
|
||||||
|
})) as unknown as Typebot
|
||||||
|
if (!typebot) return
|
||||||
|
const linkedTypebots = await getLinkedTypebots(typebot)
|
||||||
|
const answers = parseAnswers({
|
||||||
|
groups: [...typebot.groups, ...linkedTypebots.flatMap((t) => t.groups)],
|
||||||
|
variables: [
|
||||||
|
...typebot.variables,
|
||||||
|
...linkedTypebots.flatMap((t) => t.variables),
|
||||||
|
],
|
||||||
|
})(resultValues)
|
||||||
|
return {
|
||||||
|
html: newLeadEmailContent(
|
||||||
|
`${process.env.NEXTAUTH_URL}/typebots/${typebot.id}/results`,
|
||||||
|
omit(answers, 'submittedAt')
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default withSentry(handler)
|
export default withSentry(handler)
|
||||||
@@ -31,7 +31,7 @@ test('should send an email', async ({ page }) => {
|
|||||||
await page.goto(`/${typebotId}-public`)
|
await page.goto(`/${typebotId}-public`)
|
||||||
await typebotViewer(page).locator('text=Send email').click()
|
await typebotViewer(page).locator('text=Send email').click()
|
||||||
const response = await page.waitForResponse((resp) =>
|
const response = await page.waitForResponse((resp) =>
|
||||||
resp.request().url().includes(`/api/integrations/email`)
|
resp.request().url().includes(`integrations/email`)
|
||||||
)
|
)
|
||||||
const { previewUrl } = await response.json()
|
const { previewUrl } = await response.json()
|
||||||
await page.goto(previewUrl)
|
await page.goto(previewUrl)
|
||||||
|
|||||||
@@ -275,7 +275,15 @@ const executeWebhook = async (
|
|||||||
|
|
||||||
const sendEmail = async (
|
const sendEmail = async (
|
||||||
block: SendEmailBlock,
|
block: SendEmailBlock,
|
||||||
{ variables, apiHost, isPreview, onNewLog, resultId }: IntegrationContext
|
{
|
||||||
|
variables,
|
||||||
|
apiHost,
|
||||||
|
isPreview,
|
||||||
|
onNewLog,
|
||||||
|
resultId,
|
||||||
|
typebotId,
|
||||||
|
resultValues,
|
||||||
|
}: IntegrationContext
|
||||||
) => {
|
) => {
|
||||||
if (isPreview) {
|
if (isPreview) {
|
||||||
onNewLog({
|
onNewLog({
|
||||||
@@ -288,7 +296,7 @@ const sendEmail = async (
|
|||||||
const { options } = block
|
const { options } = block
|
||||||
const replyTo = parseVariables(variables)(options.replyTo)
|
const replyTo = parseVariables(variables)(options.replyTo)
|
||||||
const { error } = await sendRequest({
|
const { error } = await sendRequest({
|
||||||
url: `${apiHost}/api/integrations/email?resultId=${resultId}`,
|
url: `${apiHost}/api/typebots/${typebotId}/integrations/email?resultId=${resultId}`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
credentialsId: options.credentialsId,
|
credentialsId: options.credentialsId,
|
||||||
@@ -298,6 +306,9 @@ const sendEmail = async (
|
|||||||
cc: (options.cc ?? []).map(parseVariables(variables)),
|
cc: (options.cc ?? []).map(parseVariables(variables)),
|
||||||
bcc: (options.bcc ?? []).map(parseVariables(variables)),
|
bcc: (options.bcc ?? []).map(parseVariables(variables)),
|
||||||
replyTo: replyTo !== '' ? replyTo : undefined,
|
replyTo: replyTo !== '' ? replyTo : undefined,
|
||||||
|
isCustomBody: options.isCustomBody,
|
||||||
|
isBodyCode: options.isBodyCode,
|
||||||
|
resultValues,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
onNewLog(
|
onNewLog(
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { IntegrationBlockType, blockBaseSchema } from '../shared'
|
|||||||
|
|
||||||
export const sendEmailOptionsSchema = z.object({
|
export const sendEmailOptionsSchema = z.object({
|
||||||
credentialsId: z.string(),
|
credentialsId: z.string(),
|
||||||
|
isCustomBody: z.boolean().optional(),
|
||||||
|
isBodyCode: z.boolean().optional(),
|
||||||
recipients: z.array(z.string()),
|
recipients: z.array(z.string()),
|
||||||
subject: z.string().optional(),
|
subject: z.string().optional(),
|
||||||
body: z.string().optional(),
|
body: z.string().optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user