1import * as emailRouting from "@distilled.cloud/cloudflare/email-routing";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import * as Provider from "../../Provider.ts";
5import { Resource } from "../../Resource.ts";
6import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
7import type { Providers } from "../Providers.ts";
8import { resolveZoneId, type Reference } from "../Zone/index.ts";
9import { listAllZones } from "../Zone/lookup.ts";
10
11type RoutingAttributes = Routing["Attributes"];
12
13const toAttributes = (
14 zoneId: string,
15 result: emailRouting.GetEmailRoutingResponse,
16): RoutingAttributes => ({
17 routingId: result.id,
18 zoneId,
19 name: result.name,
20 enabled: result.enabled,
21 status: (result.status ?? undefined) as RoutingStatus | undefined,
22});
23
24export type RoutingStatus =
25 | "ready"
26 | "unconfigured"
27 | "misconfigured"
28 | "misconfigured/locked"
29 | "unlocked";
30
31export type RoutingProps = {
32 /**
33 * Zone to enable email routing on. Accepts a zone id, a zone name
34 * (`example.com`), or a `{ zoneId, name? }` object.
35 */
36 zone: Reference;
37 /**
38 * Whether to enable Email Routing on the zone.
39 *
40 * @default true
41 */
42 enabled?: boolean;
43};
44
45export type Routing = Resource<
46 "Cloudflare.Email.Routing",
47 RoutingProps,
48 {
49 routingId: string;
50 zoneId: string;
51 name: string;
52 enabled: boolean;
53 status: RoutingStatus | undefined;
54 },
55 never,
56 Providers
57>;
58
59/**
60 * Enables Cloudflare Email Routing on a zone. This is the prerequisite for
61 * receiving mail at any address on the domain and for sending email from a
62 * Worker via `send_email` bindings.
63 * ### Enabling Email Routing
64 * **Example:** Enable on a zone you own
65 * ```typescript
66 * const routing = yield* Cloudflare.Email.Routing("Routing", {
67 * zone: "example.com",
68 * });
69 * ```
70 *
71 * @resource
72 * @product Email
73 * @category Email
74 */
75export const Routing = Resource<Routing>("Cloudflare.Email.Routing", {
76 aliases: ["Cloudflare.EmailRouting"],
77});
78
79const resolve = Effect.fn(function* (zone: Reference) {
80 const { accountId } = yield* yield* CloudflareEnvironment;
81 return yield* resolveZoneId({
82 accountId,
83 zone,
84 hostname: typeof zone === "string" ? zone : (zone.name ?? ""),
85 });
86});
87
88export const RoutingProvider = () =>
89 Provider.succeed(Routing, {
90 nuke: { singleton: true },
91 stables: ["zoneId", "routingId"],
92 list: Effect.fn(function* () {
93 const { accountId } = yield* yield* CloudflareEnvironment;
94 // Email Routing settings are a per-zone singleton — no account-wide
95 // enumeration API. Enumerate every zone in the account and read the
96 // settings in each (every zone has one).
97 const allZones = yield* listAllZones(accountId);
98 const rows = yield* Effect.forEach(
99 allZones.map((zone) => zone.id),
100 (zoneId) =>
101 emailRouting.getEmailRouting({ zoneId }).pipe(
102 Effect.map((result) => toAttributes(zoneId, result)),
103 // Plan-gated or partial zones reject the route; skip them.
104 Effect.catchTag("InvalidRoute", () => Effect.succeed(undefined)),
105 ),
106 { concurrency: 10 },
107 );
108 return rows.filter((row): row is RoutingAttributes => row !== undefined);
109 }),
110 diff: Effect.fn(function* ({ news, output }) {
111 if (!output) return undefined;
112 if (!isResolved(news)) return undefined;
113 const zoneId = yield* resolve(news.zone);
114 if (zoneId !== output.zoneId) {
115 return { action: "replace" } as const;
116 }
117 if ((news.enabled ?? true) !== output.enabled) {
118 return { action: "update" } as const;
119 }
120 return undefined;
121 }),
122 read: Effect.fn(function* ({ output }) {
123 if (!output?.zoneId) return undefined;
124 const result = yield* emailRouting.getEmailRouting({
125 zoneId: output.zoneId,
126 });
127 return {
128 routingId: result.id,
129 zoneId: output.zoneId,
130 name: result.name,
131 enabled: result.enabled,
132 status: (result.status ?? undefined) as RoutingStatus | undefined,
133 };
134 }),
135 reconcile: Effect.fn(function* ({ news, output }) {
136 const zoneId = output?.zoneId ?? (yield* resolve(news.zone));
137 const desired = news.enabled ?? true;
138
139 if (desired) {
140 const result = yield* emailRouting.enableEmailRouting({ zoneId });
141 return {
142 routingId: result.id,
143 zoneId,
144 name: result.name,
145 enabled: result.enabled,
146 status: (result.status ?? undefined) as RoutingStatus | undefined,
147 };
148 } else {
149 const result = yield* emailRouting.disableEmailRouting({ zoneId });
150 return {
151 routingId: result.id,
152 zoneId,
153 name: result.name,
154 enabled: result.enabled,
155 status: (result.status ?? undefined) as RoutingStatus | undefined,
156 };
157 }
158 }),
159 delete: Effect.fn(function* ({ output }) {
160 yield* emailRouting
161 .disableEmailRouting({ zoneId: output.zoneId })
162 .pipe(Effect.catch(() => Effect.void));
163 }),
164 });