import * as z from "zod" import { defineAction } from "../../automation/actions" import { getBrevoApi, parseBrevoResponse, } from "@automate.ax/integration-contracts/brevo" const BREVO_ACCOUNT = "brevo" const DOMAIN_SCHEMA = z .string() .trim() .toLowerCase() .regex(/^(?:[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?\.)+[a-z]{2,63}$/) const DATE_RANGE_SCHEMA = z .object({ endDate: z.iso.date().optional(), startDate: z.iso.date().optional(), }) .refine(({ endDate, startDate }) => Boolean(endDate) === Boolean(startDate), { message: "startDate and endDate must be provided together.", }) const REPORT_METRICS_SCHEMA = z.object({ blocked: z.number().int().nonnegative(), clicks: z.number().int().nonnegative(), delivered: z.number().int().nonnegative(), hardBounces: z.number().int().nonnegative(), invalid: z.number().int().nonnegative(), opens: z.number().int().nonnegative(), requests: z.number().int().nonnegative(), softBounces: z.number().int().nonnegative(), spamReports: z.number().int().nonnegative(), uniqueClicks: z.number().int().nonnegative(), uniqueOpens: z.number().int().nonnegative(), unsubscribed: z.number().int().nonnegative(), }) const OPTIONAL_REPORT_METRICS_SCHEMA = REPORT_METRICS_SCHEMA.partial() const REPORT_FILTER_SCHEMA = z .object({ days: z.number().int().min(1).max(90).optional(), endDate: z.iso.date().optional(), startDate: z.iso.date().optional(), tag: z.string().min(1).optional(), }) .refine( ({ days, endDate, startDate }) => days ? startDate === undefined && endDate === undefined : Boolean(startDate) === Boolean(endDate), { message: "Use days or provide both startDate and endDate, but not both.", }, ) const SCHEDULED_EMAIL_SCHEMA = z.object({ createdAt: z.string(), scheduledAt: z.string(), status: z.enum(["inProgress", "queued", "processed", "error"]), }) const SCHEDULED_EMAIL_IDENTIFIER_SCHEMA = z.union([ z.uuid(), z.string().regex(/^<[^<>\s]+@[^<>\s]+>$/, "Provide a batch or message ID."), ]) /** Lists contacts blocked from transactional email. */ export const listBrevoBlockedContacts = defineAction( "List Brevo blocked contacts", ) .describe("Lists blocked or unsubscribed transactional email contacts.") .account(BREVO_ACCOUNT) .input( DATE_RANGE_SCHEMA.extend({ limit: z.number().int().min(1).max(100).prefault(50), offset: z.number().int().nonnegative().prefault(0), senders: z.email().array().min(1).optional(), sort: z.enum(["asc", "desc"]).prefault("desc"), }), ) .output( z.object({ contacts: z .object({ blockedAt: z.string(), email: z.email(), reason: z.object({ code: z .enum([ "unsubscribedViaMA", "unsubscribedViaEmail", "adminBlocked", "unsubscribedViaApi", "hardBounce", "contactFlaggedAsSpam", ]) .optional(), message: z.string().optional(), }), senderEmail: z.email().nullable(), }) .array(), count: z.number().int().nonnegative(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await parseBrevoResponse( await getBrevoApi(account.secret).request("smtp/blockedContacts", { query: input, }), z.object({ contacts: z .object({ blockedAt: z.string(), email: z.email(), reason: z.object({ code: z .enum([ "unsubscribedViaMA", "unsubscribedViaEmail", "adminBlocked", "unsubscribedViaApi", "hardBounce", "contactFlaggedAsSpam", ]) .optional(), message: z.string().optional(), }), senderEmail: z.email().nullable(), }) .array() .optional(), count: z.number().int().nonnegative().optional(), }), ) return { contacts: result.contacts ?? [], count: result.count ?? 0 } }) /** Removes a contact from Brevo’s transactional email blocklist. */ export const unblockBrevoContact = defineAction("Unblock Brevo contact") .describe("Removes an email address from the transactional blocklist.") .account(BREVO_ACCOUNT) .input(z.object({ email: z.email() })) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getBrevoApi(account.secret).request( `smtp/blockedContacts/${encodeURIComponent(input.email)}`, { method: "DELETE" }, ) }) /** Lists domains blocked from transactional email. */ export const listBrevoBlockedDomains = defineAction( "List Brevo blocked domains", ) .describe("Lists domains blocked from transactional email delivery.") .account(BREVO_ACCOUNT) .output(z.object({ domains: DOMAIN_SCHEMA.array() })) .retry({ replaySafety: "safe" }) .handler( async ({ account }) => await parseBrevoResponse( await getBrevoApi(account.secret).request("smtp/blockedDomains"), z.object({ domains: DOMAIN_SCHEMA.array() }), ), ) /** Blocks a domain from transactional email delivery. */ export const blockBrevoEmailDomain = defineAction("Block Brevo email domain") .describe("Adds a domain to the transactional email blocklist.") .account(BREVO_ACCOUNT) .input(z.object({ domain: DOMAIN_SCHEMA })) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getBrevoApi(account.secret).request("smtp/blockedDomains", { body: JSON.stringify(input), method: "POST", }) }) /** Removes a domain from the transactional email blocklist. */ export const unblockBrevoEmailDomain = defineAction( "Unblock Brevo email domain", ) .describe("Removes a domain from the transactional email blocklist.") .account(BREVO_ACCOUNT) .input(z.object({ domain: DOMAIN_SCHEMA })) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getBrevoApi(account.secret).request( `smtp/blockedDomains/${encodeURIComponent(input.domain)}`, { method: "DELETE" }, ) }) /** Deletes selected or all transactional hard-bounce records. */ export const deleteBrevoHardBounces = defineAction("Delete Brevo hard bounces") .describe("Deletes hard-bounce records using an explicit deletion scope.") .account(BREVO_ACCOUNT) .input( z.union([ z.object({ all: z.literal(true) }), z.object({ contactEmail: z.email() }), z.object({ endDate: z.iso.date(), startDate: z.iso.date() }), ]), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getBrevoApi(account.secret).request("smtp/deleteHardbounces", { body: JSON.stringify("all" in input ? {} : input), method: "POST", }) }) /** Retrieves one scheduled email or a scheduled batch. */ export const getBrevoScheduledEmail = defineAction("Get Brevo scheduled email") .describe("Retrieves a scheduled email by message ID or batch ID.") .account(BREVO_ACCOUNT) .input( DATE_RANGE_SCHEMA.extend({ identifier: SCHEDULED_EMAIL_IDENTIFIER_SCHEMA, limit: z.number().int().min(1).max(500).prefault(50), offset: z.number().int().nonnegative().prefault(0), sort: z.enum(["asc", "desc"]).prefault("desc"), status: z.enum(["inProgress", "queued", "processed"]).optional(), }), ) .output( z.union([ SCHEDULED_EMAIL_SCHEMA, z.object({ batches: SCHEDULED_EMAIL_SCHEMA.array(), count: z.number().int().nonnegative(), }), ]), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { identifier, ...query } = input const result = await parseBrevoResponse( await getBrevoApi(account.secret).request( `smtp/emailStatus/${encodeURIComponent(identifier)}`, { query }, ), z.union([ SCHEDULED_EMAIL_SCHEMA, z.object({ batches: SCHEDULED_EMAIL_SCHEMA.array().optional(), count: z.number().int().nonnegative().optional(), }), ]), ) return "createdAt" in result ? result : { batches: result.batches ?? [], count: result.count ?? 0 } }) /** Cancels one scheduled email or batch. */ export const cancelBrevoScheduledEmail = defineAction( "Cancel Brevo scheduled email", ) .describe("Cancels a scheduled email or batch before processing.") .account(BREVO_ACCOUNT) .input(z.object({ identifier: SCHEDULED_EMAIL_IDENTIFIER_SCHEMA })) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getBrevoApi(account.secret).request( `smtp/email/${encodeURIComponent(input.identifier)}`, { method: "DELETE" }, ) }) /** Lists transactional email logs. */ export const listBrevoEmailLogs = defineAction("List Brevo email logs") .describe( "Lists transactional email records by recipient, template, or message.", ) .account(BREVO_ACCOUNT) .input( DATE_RANGE_SCHEMA.extend({ email: z.email().optional(), limit: z.number().int().min(1).max(500).prefault(50), messageId: z.string().min(1).optional(), offset: z.number().int().nonnegative().prefault(0), sort: z.enum(["asc", "desc"]).prefault("desc"), templateId: z.number().int().positive().optional(), }).refine( ({ email, messageId, templateId }) => email || messageId || templateId, { message: "Provide email, messageId, or templateId.", }, ), ) .output( z.object({ count: z.number().int().nonnegative(), emails: z .object({ date: z.string(), email: z.email(), from: z.email().optional(), messageId: z.string(), subject: z.string(), tags: z.string().array().optional(), templateId: z.number().int().optional(), uuid: z.uuid(), }) .array(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await parseBrevoResponse( await getBrevoApi(account.secret).request("smtp/emails", { query: input, }), z.object({ count: z.number().int().nonnegative().optional(), transactionalEmails: z .object({ date: z.string(), email: z.email(), from: z.email().optional(), messageId: z.string(), subject: z.string(), tags: z.string().array().optional(), templateId: z.number().int().optional(), uuid: z.uuid(), }) .array() .optional(), }), ) return { count: result.count ?? 0, emails: result.transactionalEmails ?? [], } }) /** Deletes transactional email logs by message ID or recipient. */ export const deleteBrevoEmailLogs = defineAction("Delete Brevo email logs") .describe("Deletes transactional email logs by message ID or recipient.") .account(BREVO_ACCOUNT) .input( DATE_RANGE_SCHEMA.extend({ identifier: z.union([z.email(), z.string().min(1)]), }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { endDate, identifier, startDate } = input await getBrevoApi(account.secret).request( `smtp/log/${encodeURIComponent(identifier)}`, { method: "DELETE", query: { from_date: startDate, to_date: endDate }, }, ) }) /** Retrieves aggregate transactional email statistics. */ export const getBrevoAggregateEmailReport = defineAction( "Get Brevo aggregate email report", ) .describe("Retrieves aggregate transactional email delivery statistics.") .account(BREVO_ACCOUNT) .input(REPORT_FILTER_SCHEMA) .output( OPTIONAL_REPORT_METRICS_SCHEMA.extend({ range: z.string().optional() }), ) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await parseBrevoResponse( await getBrevoApi(account.secret).request( "smtp/statistics/aggregatedReport", { query: input }, ), OPTIONAL_REPORT_METRICS_SCHEMA.extend({ range: z.string().optional() }), ), ) /** Retrieves daily transactional email statistics. */ export const getBrevoEmailReport = defineAction("Get Brevo email report") .describe("Retrieves daily transactional email delivery statistics.") .account(BREVO_ACCOUNT) .input( REPORT_FILTER_SCHEMA.safeExtend({ days: z.number().int().min(1).max(30).optional(), limit: z.number().int().min(1).max(500).prefault(50), offset: z.number().int().nonnegative().prefault(0), sort: z.enum(["asc", "desc"]).prefault("desc"), }), ) .output( z.object({ reports: REPORT_METRICS_SCHEMA.extend({ date: z.string() }).array(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { reports } = await parseBrevoResponse( await getBrevoApi(account.secret).request("smtp/statistics/reports", { query: input, }), z.object({ reports: REPORT_METRICS_SCHEMA.extend({ date: z.string() }) .array() .optional(), }), ) return { reports: reports ?? [] } }) /** Renders a transactional template with contact or explicit parameters. */ export const previewBrevoEmailTemplate = defineAction( "Preview Brevo email template", ) .describe("Renders a transactional template with resolved variables.") .account(BREVO_ACCOUNT) .input( z .object({ email: z.email().optional(), params: z.record(z.string(), z.json()).optional(), templateId: z.union([z.number().int().positive(), z.string().min(1)]), }) .refine(({ email, params }) => email || params, { message: "Provide email or params.", }), ) .output( z.object({ fromEmail: z.email().optional(), fromName: z.string().optional(), html: z.string().optional(), previewText: z.string().optional(), subject: z.string().optional(), usedFeedNames: z.string().array().optional(), }), ) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await parseBrevoResponse( await getBrevoApi(account.secret).request("smtp/template/preview", { body: JSON.stringify(input), method: "POST", }), z.object({ fromEmail: z.email().optional(), fromName: z.string().optional(), html: z.string().optional(), previewText: z.string().optional(), subject: z.string().optional(), usedFeedNames: z.string().array().optional(), }), ), ) /** Deletes a transactional email template. */ export const deleteBrevoEmailTemplate = defineAction( "Delete Brevo email template", ) .describe("Permanently deletes a transactional email template.") .account(BREVO_ACCOUNT) .input(z.object({ templateId: z.number().int().positive() })) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getBrevoApi(account.secret).request( `smtp/templates/${input.templateId}`, { method: "DELETE" }, ) }) /** Sends a transactional template to explicit recipients or the test list. */ export const sendBrevoTestEmailTemplate = defineAction( "Send Brevo test email template", ) .describe( "Sends a transactional template to recipients or the Brevo test list.", ) .account(BREVO_ACCOUNT) .input( z.object({ recipients: z.email().array().min(1).optional(), templateId: z.number().int().positive(), }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getBrevoApi(account.secret).request( `smtp/templates/${input.templateId}/sendTest`, { body: JSON.stringify({ emailTo: input.recipients }), method: "POST", }, ) })