import { DefaultBotNotificationEmail, render } from "indite-js/emails"; import { AnswerInSessionState, ChatLog, SendEmailBlock, SessionState, SmtpCredentials, BotInSession, Variable, } from "indite-js/schemas"; import { createTransport } from "nodemailer"; import Mail from "nodemailer/lib/mailer"; import { byId, isDefined, isEmpty, isNotDefined, omit } from "indite-js/lib"; import { decrypt } from "indite-js/lib/api/encryption/decrypt"; import { defaultFrom, defaultTransportOptions } from "./constants"; import { findUniqueVariableValue } from "indite-js/variables/findUniqueVariableValue"; import { env } from "indite-js/env"; import { ExecuteIntegrationResponse } from "../../../types"; import prisma from "indite-js/lib/prisma"; import { parseVariables } from "indite-js/variables/parseVariables"; import { defaultSendEmailOptions } from "indite-js/schemas/features/blocks/integrations/sendEmail/constants"; import { parseAnswers } from "indite-js/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 { bot, resultId, answers } = state.botsQueue[0]; const isPreview = !resultId; if (isPreview) return { outgoingEdgeId: block.outgoingEdgeId, logs: [ { status: "info", description: "Emails are not sent in preview mode", }, ], }; const bodyUniqueVariable = findUniqueVariableValue(bot.variables)( options?.body ); const body = bodyUniqueVariable ? stringifyUniqueVariableValueAsHtml(bodyUniqueVariable) : parseVariables(bot.variables, { isInsideHtml: !options?.isBodyCode })( options?.body ?? "" ); if (!options?.recipients) return { outgoingEdgeId: block.outgoingEdgeId, logs }; try { const sendEmailLogs = await sendEmail({ bot, answers, credentialsId: options.credentialsId ?? defaultSendEmailOptions.credentialsId, recipients: options.recipients.map(parseVariables(bot.variables)), subject: options.subject ? parseVariables(bot.variables)(options?.subject) : undefined, body, cc: options.cc ? options.cc.map(parseVariables(bot.variables)) : undefined, bcc: options.bcc ? options.bcc.map(parseVariables(bot.variables)) : undefined, replyTo: options.replyTo ? parseVariables(bot.variables)(options.replyTo) : undefined, fileUrls: getFileUrls(bot.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 ({ bot, 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; bot: BotInSession; 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, bot, 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, bot, answersInSession, }: { bot: BotInSession; 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: bot.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("
"); };