import * as z from "zod" import { defineAction } from "../../../automation/actions" import { integrationScope as scope } from "../../../automation/integrations" import { apolloAccountOptions, apolloApiKeyAccountOptions, getApolloApi, } from "../lib/api" import { resolveApolloContact } from "../lib/references" import { APOLLO_REFERENCE_SCHEMA } from "../lib/schemas" const APOLLO_PROVIDER_RECIPIENT_SCHEMA = z.looseObject({ contact_id: z.string().nullish(), email: z.string(), raw_name: z.string().nullish(), recipient_type_cd: z.string().nullish(), type: z.string().nullish(), user_id: z.string().nullish(), }) const APOLLO_RECIPIENT_SCHEMA = z.object({ contactId: z.string().optional(), email: z.email(), name: z.string().optional(), type: z.enum(["bcc", "cc", "to"]).optional(), userId: z.string().optional(), }) const APOLLO_PROVIDER_EMAIL_SCHEMA = z.looseObject({ account_id: z.string().nullish(), attachment_ids: z.string().array().nullish(), bcc_emails: z.string().array().nullish(), body_html: z.string().nullish(), body_text: z.string().nullish(), bounce: z.boolean().nullish(), cc_emails: z.string().array().nullish(), click_tracking_enabled: z.boolean().nullish(), completed_at: z.string().nullish(), contact_id: z.string().nullish(), created_at: z.string().nullish(), due_at: z.string().nullish(), email_account_id: z.string().nullish(), emailer_campaign_id: z.string().nullish(), emailer_step_id: z.string().nullish(), enable_tracking: z.boolean().nullish(), failed_at: z.string().nullish(), failure_reason: z.string().nullish(), from_email: z.string().nullish(), from_name: z.string().nullish(), id: z.string(), not_sent_reason: z.string().nullish(), num_clicks: z.number().int().nonnegative().nullish(), num_opens: z.number().int().nonnegative().nullish(), open_tracking_enabled: z.boolean().nullish(), provider_message_id: z.string().nullish(), provider_thread_id: z.string().nullish(), recipients: APOLLO_PROVIDER_RECIPIENT_SCHEMA.array().nullish(), replied: z.boolean().nullish(), spam_blocked: z.boolean().nullish(), status: z.string(), subject: z.string().nullish(), to_email: z.string().nullish(), to_name: z.string().nullish(), type: z.string().nullish(), user_id: z.string().nullish(), }) const APOLLO_EMAIL_SCHEMA = z.object({ accountId: z.string().optional(), attachmentIds: z.string().array(), bccEmails: z.string().array(), bodyHtml: z.string().optional(), bodyText: z.string().optional(), bounced: z.boolean().optional(), ccEmails: z.string().array(), clickTrackingEnabled: z.boolean().optional(), completedAt: z.string().optional(), contactId: z.string().optional(), createdAt: z.string().optional(), dueAt: z.string().optional(), emailAccountId: z.string().optional(), failedAt: z.string().optional(), failureReason: z.string().optional(), fromEmail: z.string().optional(), fromName: z.string().optional(), id: z.string(), notSentReason: z.string().optional(), numClicks: z.number().int().nonnegative().optional(), numOpens: z.number().int().nonnegative().optional(), openTrackingEnabled: z.boolean().optional(), providerMessageId: z.string().optional(), providerThreadId: z.string().optional(), recipients: APOLLO_RECIPIENT_SCHEMA.array(), replied: z.boolean().optional(), sequenceId: z.string().optional(), spamBlocked: z.boolean().optional(), status: z.string(), stepId: z.string().optional(), subject: z.string().optional(), toEmail: z.string().optional(), toName: z.string().optional(), trackingEnabled: z.boolean().optional(), type: z.string().optional(), userId: z.string().optional(), }) const EMAIL_RESPONSE_SCHEMA = z.looseObject({ emailer_message: APOLLO_PROVIDER_EMAIL_SCHEMA, }) const EMAIL_SEARCH_RESPONSE_SCHEMA = z.looseObject({ emailer_messages: APOLLO_PROVIDER_EMAIL_SCHEMA.array().prefault([]), }) const EMAIL_CONTENT_RESPONSE_SCHEMA = z.looseObject({ emailer_messages: z .looseObject({ body: z.string().nullable(), body_format: z.enum(["html", "plain"]), id: z.string(), recipients: APOLLO_PROVIDER_RECIPIENT_SCHEMA.array().prefault([]), sent_at: z.string().nullable(), status: z.string(), subject: z.string().nullable(), }) .array() .prefault([]), }) const EMAIL_CONTENT_SCHEMA = z.object({ body: z.string().nullable(), bodyFormat: z.enum(["html", "plain"]), id: z.string(), recipients: APOLLO_RECIPIENT_SCHEMA.array(), sentAt: z.string().nullable(), status: z.string(), subject: z.string().nullable(), }) const EMAIL_STATUS_RESPONSE_SCHEMA = z.looseObject({ completed_at: z.string().nullish(), failed_at: z.string().nullish(), failure_reason: z.string().nullish(), id: z.string(), message: z.string(), not_sent_reason: z.string().nullish(), retry_after_seconds: z.number().int().nonnegative().nullish(), status: z.string(), }) const EMAIL_STATUS_SCHEMA = z.object({ completedAt: z.string().optional(), failedAt: z.string().optional(), failureReason: z.string().optional(), id: z.string(), message: z.string(), notSentReason: z.string().optional(), retryAfterSeconds: z.number().int().nonnegative().optional(), status: z.string(), }) const EMAIL_STATS_RESPONSE_SCHEMA = z.looseObject({ activities: z.looseObject({}).array().prefault([]), emailer_message: APOLLO_PROVIDER_EMAIL_SCHEMA, }) const EMAIL_STAT_SCHEMA = z.enum([ "bounced", "clicked", "delivered", "demoed", "drafted", "failedOther", "notOpened", "opened", "scheduled", "spamBlocked", "unsubscribed", ]) const REPLY_CLASS_SCHEMA = z.enum([ "alreadyLeftCompanyOrNotRightPerson", "followUpQuestion", "noneOfTheAbove", "notInterested", "outOfOffice", "personReferral", "unsubscribe", "willingToMeet", ]) /** Creates one unsent Apollo email draft. */ export const createApolloEmailDraft = defineAction("Create Apollo email draft") .describe("Creates an unsent email draft for a contact or as a thread reply.") .account( "apollo", apolloAccountOptions( scope.and("emailer_messages_create", "contact_read", "contacts_search"), ), ) .input( z .object({ attachmentIds: z.string().array().optional(), bodyHtml: z.string().optional(), contact: APOLLO_REFERENCE_SCHEMA.optional(), enableTracking: z.boolean().optional(), inResponseToMessageId: z.string().optional(), outreachTaskId: z.string().optional(), recipients: z .object({ contactId: z.string().optional(), email: z.email(), type: z.enum(["bcc", "cc", "to"]), }) .array() .optional(), subject: z.string().optional(), templateId: z.string().optional(), }) .refine( ({ contact, inResponseToMessageId }) => contact !== undefined || inResponseToMessageId !== undefined, "Provide contact or inResponseToMessageId.", ), ) .output(APOLLO_EMAIL_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const api = getApolloApi(account) return toApolloEmail( ( await api.request("emailer_messages", { body: { attachment_ids: input.attachmentIds, body_html: input.bodyHtml, contact_id: input.contact ? await resolveApolloContact(api, input.contact) : undefined, emailer_template_id: input.templateId, enable_tracking: input.enableTracking, in_response_to_emailer_message_id: input.inResponseToMessageId, outreach_task_id: input.outreachTaskId, recipients: input.recipients?.map((recipient) => ({ contact_id: recipient.contactId, email: recipient.email, recipient_type_cd: recipient.type, })), subject: input.subject, }, responseSchema: EMAIL_RESPONSE_SCHEMA, }) ).emailer_message, ) }) /** Queues an Apollo email draft for immediate sending. */ export const sendApolloEmailNow = defineAction("Send Apollo email now") .describe( "Queues a drafted, scheduled, or failed email for immediate sending.", ) .account("apollo", apolloAccountOptions("emailer_messages_send_now")) .input(z.object({ emailId: z.string(), surface: z.string().optional() })) .output(APOLLO_EMAIL_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { return toApolloEmail( ( await getApolloApi(account).request( `emailer_messages/${encodeURIComponent(input.emailId)}/send_now`, { body: { surface: input.surface }, responseSchema: EMAIL_RESPONSE_SCHEMA, }, ) ).emailer_message, ) }) /** Gets content for up to ten completed Apollo outreach emails. */ export const getApolloEmailContent = defineAction("Get Apollo email content") .describe("Gets sent email bodies; unavailable IDs are omitted by Apollo.") .account("apollo", apolloAccountOptions("emailer_messages_get_content")) .input( z.object({ bodyFormat: z.enum(["html", "plain"]).prefault("plain"), emailIds: z.string().array().min(1).max(10), }), ) .output(z.object({ emails: EMAIL_CONTENT_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { return { emails: ( await getApolloApi(account).request("emailer_messages/get_content", { body: { body_format: input.bodyFormat, ids: input.emailIds }, responseSchema: EMAIL_CONTENT_RESPONSE_SCHEMA, }) ).emailer_messages.map((email) => ({ body: email.body, bodyFormat: email.body_format, id: email.id, recipients: email.recipients.map(toApolloRecipient), sentAt: email.sent_at, status: email.status, subject: email.subject, })), } }) /** Gets the asynchronous send status for one Apollo email. */ export const getApolloEmailSendStatus = defineAction( "Get Apollo email send status", ) .describe("Gets delivery, failure, or retry state after sending an email.") .account("apollo", apolloAccountOptions("emailer_messages_email_send_status")) .input(z.object({ emailId: z.string() })) .output(EMAIL_STATUS_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getApolloApi(account).request( "emailer_messages/email_send_status", { body: { id: input.emailId }, responseSchema: EMAIL_STATUS_RESPONSE_SCHEMA, }, ) return { completedAt: result.completed_at ?? undefined, failedAt: result.failed_at ?? undefined, failureReason: result.failure_reason ?? undefined, id: result.id, message: result.message, notSentReason: result.not_sent_reason ?? undefined, retryAfterSeconds: result.retry_after_seconds ?? undefined, status: result.status, } }) /** Gets content and engagement stats for one Apollo sequence email. */ export const getApolloEmailStats = defineAction("Get Apollo email stats") .describe("Gets complete sequence-email details. Requires a master API key.") .account("apollo", apolloApiKeyAccountOptions()) .input(z.object({ emailId: z.string() })) .output(APOLLO_EMAIL_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { return toApolloEmail( ( await getApolloApi(account).request( `emailer_messages/${encodeURIComponent(input.emailId)}/activities`, { responseSchema: EMAIL_STATS_RESPONSE_SCHEMA }, ) ).emailer_message, ) }) /** Searches Apollo outreach emails. */ export const searchApolloEmails = defineAction("Search Apollo emails") .describe("Searches one page of sequence outreach emails.") .account("apollo", apolloAccountOptions("emailer_messages_search")) .input( z.object({ dateMode: z.enum(["completedAt", "dueAt"]).optional(), emailAccountId: z.string().optional(), excludeSequenceIds: z.string().array().optional(), fromDate: z.iso.date().optional(), notSentReasons: z.string().array().optional(), page: z.number().int().min(1).prefault(1), perPage: z.number().int().min(1).max(100).prefault(100), query: z.string().optional(), replyClasses: REPLY_CLASS_SCHEMA.array().optional(), sequenceIds: z.string().array().optional(), statuses: EMAIL_STAT_SCHEMA.array().optional(), toDate: z.iso.date().optional(), userIds: z.string().array().optional(), }), ) .output( z.object({ emails: APOLLO_EMAIL_SCHEMA.array(), hasMore: z.boolean(), page: z.number().int().positive(), perPage: z.number().int().positive(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getApolloApi(account).request( "emailer_messages/search", { query: { "emailer_campaign_ids[]": input.sequenceIds, "emailer_message_date_range[max]": input.toDate, "emailer_message_date_range[min]": input.fromDate, emailer_message_date_range_mode: input.dateMode === "completedAt" ? "completed_at" : input.dateMode === "dueAt" ? "due_at" : undefined, "emailer_message_reply_classes[]": input.replyClasses?.map(toSnakeCase), "emailer_message_stats[]": input.statuses?.map(toSnakeCase), email_account_id_and_aliases: input.emailAccountId, "not_emailer_campaign_ids[]": input.excludeSequenceIds, "not_sent_reason_cds[]": input.notSentReasons, page: input.page, per_page: input.perPage, q_keywords: input.query, "user_ids[]": input.userIds, }, responseSchema: EMAIL_SEARCH_RESPONSE_SCHEMA, }, ) return { emails: result.emailer_messages.map(toApolloEmail), hasMore: result.emailer_messages.length === input.perPage, page: input.page, perPage: input.perPage, } }) /** * Converts one provider email recipient to its public representation. * * @param value - Provider recipient response. */ function toApolloRecipient( value: z.output, ) { return APOLLO_RECIPIENT_SCHEMA.parse({ contactId: value.contact_id ?? undefined, email: value.email, name: value.raw_name ?? undefined, type: value.recipient_type_cd ?? value.type ?? undefined, userId: value.user_id ?? undefined, }) } /** * Converts one provider email to its public representation. * * @param value - Provider email response. */ function toApolloEmail(value: z.output) { return APOLLO_EMAIL_SCHEMA.parse({ accountId: value.account_id ?? undefined, attachmentIds: value.attachment_ids ?? [], bccEmails: value.bcc_emails ?? [], bodyHtml: value.body_html ?? undefined, bodyText: value.body_text ?? undefined, bounced: value.bounce ?? undefined, ccEmails: value.cc_emails ?? [], clickTrackingEnabled: value.click_tracking_enabled ?? undefined, completedAt: value.completed_at ?? undefined, contactId: value.contact_id ?? undefined, createdAt: value.created_at ?? undefined, dueAt: value.due_at ?? undefined, emailAccountId: value.email_account_id ?? undefined, failedAt: value.failed_at ?? undefined, failureReason: value.failure_reason ?? undefined, fromEmail: value.from_email ?? undefined, fromName: value.from_name ?? undefined, id: value.id, notSentReason: value.not_sent_reason ?? undefined, numClicks: value.num_clicks ?? undefined, numOpens: value.num_opens ?? undefined, openTrackingEnabled: value.open_tracking_enabled ?? undefined, providerMessageId: value.provider_message_id ?? undefined, providerThreadId: value.provider_thread_id ?? undefined, recipients: (value.recipients ?? []).map(toApolloRecipient), replied: value.replied ?? undefined, sequenceId: value.emailer_campaign_id ?? undefined, spamBlocked: value.spam_blocked ?? undefined, status: value.status, stepId: value.emailer_step_id ?? undefined, subject: value.subject ?? undefined, toEmail: value.to_email ?? undefined, toName: value.to_name ?? undefined, trackingEnabled: value.enable_tracking ?? undefined, type: value.type ?? undefined, userId: value.user_id ?? undefined, }) } /** * Converts one public enum value to Apollo's provider spelling. * * @param value - Public camelCase value. */ function toSnakeCase(value: string) { return value.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`) }