import { DefaultBotNotificationEmail, render } from '@typebot.io/emails' import { AnswerInSessionState, ChatLog, SendEmailBlock, SessionState, SmtpCredentials, TypebotInSession, Variable, } from '@typebot.io/schemas' import { createTransport } from 'nodemailer' import Mail from 'nodemailer/lib/mailer' import { byId, isDefined, isEmpty, isNotDefined, omit } from '@typebot.io/lib' import { decrypt } from '@typebot.io/lib/api/encryption/decrypt' import { defaultFrom, defaultTransportOptions } from './constants' import { findUniqueVariableValue } from '@typebot.io/variables/findUniqueVariableValue' import { env } from '@typebot.io/env' import { ExecuteIntegrationResponse } from '../../../types' import prisma from '@typebot.io/lib/prisma' import { parseVariables } from '@typebot.io/variables/parseVariables' import { defaultSendEmailOptions } from '@typebot.io/schemas/features/blocks/integrations/sendEmail/constants' import { parseAnswers } from '@typebot.io/lib/results/parseAnswers' export const sendEmailSuccessDescription = 'Email successfully sent' export const sendEmailErrorDescription = 'Email not sent' export const executeSendEmailBlock = async ( state: SessionState, block: SendEmailBlock ): Promise => { const logs: ChatLog[] = [] 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 ?? '' ) if (!options?.recipients) return { outgoingEdgeId: block.outgoingEdgeId, logs } try { const sendEmailLogs = await sendEmail({ typebot, answers, credentialsId: options.credentialsId ?? defaultSendEmailOptions.credentialsId, recipients: options.recipients.map(parseVariables(typebot.variables)), subject: options.subject ? parseVariables(typebot.variables)(options?.subject) : undefined, body, cc: options.cc ? options.cc.map(parseVariables(typebot.variables)) : undefined, bcc: options.bcc ? options.bcc.map(parseVariables(typebot.variables)) : undefined, 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 } } const sendEmail = async ({ typebot, answers, credentialsId, recipients, body, subject, cc, bcc, replyTo, isBodyCode, isCustomBody, fileUrls, }: { credentialsId: string recipients: string[] body: string | undefined subject: string | undefined cc: string[] | undefined bcc: string[] | undefined replyTo: string | undefined isBodyCode: boolean | undefined isCustomBody: boolean | undefined typebot: TypebotInSession answers: AnswerInSessionState[] fileUrls?: string | string[] }): Promise => { const logs: ChatLog[] = [] 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, }) if (!emailBody) { logs.push({ status: 'error', description: sendEmailErrorDescription, details: { error: 'No email body found', transportConfig, recipients, subject, cc, bcc, replyTo, emailBody, }, }) return logs } 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, ...emailBody, } try { await transporter.sendMail(email) logs.push({ status: 'success', description: sendEmailSuccessDescription, details: { transportConfig: { ...transportConfig, auth: { user: transportConfig.auth.user, pass: '******' }, }, email, }, }) } catch (err) { logs.push({ status: 'error', description: sendEmailErrorDescription, details: { error: err instanceof Error ? err.toString() : err, transportConfig: { ...transportConfig, auth: { user: transportConfig.auth.user, pass: '******' }, }, email, }, }) } return logs } const getEmailInfo = async ( credentialsId: string ): Promise => { 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'] } const getEmailBody = async ({ body, isCustomBody, isBodyCode, typebot, answersInSession, }: { typebot: TypebotInSession answersInSession: AnswerInSessionState[] } & Pick< NonNullable, '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: typebot.variables, answers: answersInSession, }) return { html: render( ).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, '
') return value.map(stringifyUniqueVariableValueAsHtml).join('
') }