1
11import { renderMarkdown } from "@factory/shared";
12import { enqueue, QUEUES } from "../../../queues.js";
13import { FatalError } from "./classify.js";
14import { reserveEmailSlot } from "./email-cap.js";
15import { interpolateConfig, resolveUserId } from "./helpers.js";
16import type { ActionExecutor, ActionResult } from "./types.js";
17
18
21export async function sendEmail(
22 cfg: Record<string, unknown>,
23 ctx: import("./types.js").AutomationRunContext,
24 deps: import("./types.js").ExecutorDeps,
25 index: number
26): Promise<ActionResult> {
27 const warnings: string[] = [];
28 const toRaw = (cfg.to as string) ?? "automation_creator";
29 let toEmail: string;
30 if (toRaw === "trigger_user" || toRaw === "automation_creator") {
31 const userId = resolveUserId(toRaw, ctx);
32 const user = await deps.prisma.user.findUnique({ where: { id: userId }, select: { email: true } });
33 if (!user?.email) throw new FatalError("EMAIL_ADDRESS_MISSING", `recipient ${toRaw} has no email address`);
34 toEmail = user.email;
35 } else {
36 toEmail = toRaw;
37 }
38
39 const cc = Array.isArray(cfg.cc) ? (cfg.cc as string[]) : [];
40 const subject = interpolateConfig(cfg.subject as string, ctx.vars, 200, "subject", warnings);
41 const bodyHtml = interpolateConfig(cfg.body as string, ctx.vars, 20_000, "body", warnings);
42
43
44 await reserveEmailSlot(deps.redis, ctx.workspaceId);
45
46 await enqueue(
47 deps.redis,
48 QUEUES.email,
49 { to: toEmail, cc, subject, html: renderMarkdown(bodyHtml), text: bodyHtml, template: "automation" },
50 { jobId: `automation-email:${ctx.runId}:${index}` }
51 );
52
53 return {
54 output: { to: toEmail, cc, subject, slotReserved: true },
55 emittedEvents: [],
56 audit: { action: "automation.email_sent", resourceType: "user", resourceId: toEmail, metadata: { cc } }
57 };
58}
59
60const executor: ActionExecutor = {
61 type: "SEND_EMAIL",
62 execute: (cfg, ctx, deps, index) => sendEmail(cfg, ctx, deps, index ?? 0)
63};
64
65export default executor;
66