1import * as Effect from "effect/Effect";
2import { ProviderModePolicy } from "../../ProviderMode.ts";
3
4type SendEmailTypeId = typeof SendEmailTypeId;
5const SendEmailTypeId = "Cloudflare.Email.SendEmail" as const;
6
7export type SendEmailProps = {
8 /**
9 * Restrict the Worker to send to a single verified destination address.
10 *
11 * Mutually exclusive with `allowedDestinationAddresses`. The destination
12 * must be a verified address on the account (see {@link Address}).
13 */
14 destinationAddress?: string;
15 /**
16 * Restrict the Worker to send to one of these verified destination addresses.
17 *
18 * Mutually exclusive with `destinationAddress`.
19 */
20 allowedDestinationAddresses?: string[];
21 /**
22 * Restrict the Worker to send from one of these sender addresses.
23 *
24 * The sender domain must have Email Routing configured (see
25 * {@link Routing}) and the addresses must be verified.
26 */
27 allowedSenderAddresses?: string[];
28};
29
30/**
31 * A Cloudflare Workers `send_email` binding descriptor.
32 *
33 * `SendEmail` is a Worker-only binding — it does not create any cloud-side
34 * resource. The descriptor names the binding and records optional
35 * destination/sender restrictions; the actual `send_email` entry is attached
36 * to the Worker via {@link SendBinding}.
37 *
38 * Under `alchemy dev` the binding is lowered onto the local email simulator:
39 * `send()` validates the message and persists it as a `.eml` under
40 * `.alchemy/local/email` instead of delivering mail. Pipe the descriptor
41 * through `Alchemy.remote()` to send through the live Cloudflare Email
42 * service in dev — the same opt-out aspect resources use.
43 *
44 *
45 * ### Binding to a Worker
46 * **Example:** Send to any verified destination
47 * ```typescript
48 * const Email = Cloudflare.Email.SendEmail("Email");
49 *
50 * // in the Worker effect:
51 * const email = yield* Cloudflare.Email.Send(Email);
52 * yield* email.send({
53 * from: "noreply@example.com",
54 * to: "user@example.com",
55 * subject: "Hello",
56 * text: "Hi from Alchemy",
57 * });
58 * ```
59 *
60 * **Example:** Restrict the sender address
61 * ```typescript
62 * const Ops = Cloudflare.Email.SendEmail("OpsEmail", {
63 * allowedSenderAddresses: ["noreply@example.com"],
64 * destinationAddress: "ops@example.com",
65 * });
66 * ```
67 *
68 * ### Local development
69 * **Example:** Send real mail from the dev loop
70 * ```typescript
71 * // Default: `send()` lands in the local simulator under `alchemy dev`.
72 * // Alchemy.remote() opts into the live Email service instead:
73 * const Email = Cloudflare.Email.SendEmail("Email", {
74 * allowedSenderAddresses: ["noreply@example.com"],
75 * }).pipe(Alchemy.remote());
76 * ```
77 *
78 * @resource
79 */
80export type SendEmail = SendEmailProps & {
81 kind: SendEmailTypeId;
82 name: string;
83 /**
84 * Alchemy-internal: opt-out of local emulation in `alchemy dev` — the
85 * `Alchemy.remote()` decoration captured at registration time. Registered
86 * on the host Worker via the binding-data `devRemote` channel, never on
87 * the wire binding itself.
88 */
89 devRemote?: boolean;
90};
91
92export const isSendEmail = (value: unknown): value is SendEmail =>
93 typeof value === "object" &&
94 value !== null &&
95 "kind" in value &&
96 (value as SendEmail).kind === SendEmailTypeId;
97
98export const SendEmail: (
99 id: string,
100 props?: SendEmailProps,
101) => Effect.Effect<SendEmail> = Effect.fn(function* (
102 id: string,
103 props?: SendEmailProps,
104) {
105 // Capture the `Alchemy.remote()` decoration the same way resources do —
106 // at registration time, from the ambient ProviderModePolicy reference.
107 // `send_email` has no cloud-side resource (and thus no provider mode to
108 // stamp), so the captured flag rides on the descriptor instead.
109 const devRemote = (yield* ProviderModePolicy) === true || undefined;
110 return {
111 kind: SendEmailTypeId,
112 name: id,
113 destinationAddress: props?.destinationAddress,
114 allowedDestinationAddresses: props?.allowedDestinationAddresses,
115 allowedSenderAddresses: props?.allowedSenderAddresses,
116 devRemote,
117 } satisfies SendEmail;
118});