UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

153 lines (152 loc) 5.26 kB
import { $env, $hook, $inject, $module, Alepha, AlephaError, z } from "alepha"; import { AlephaEmail, EmailError, EmailProvider } from "alepha/email"; import { $logger } from "alepha/logger"; //#region ../../src/email/cloudflare/providers/CloudflareEmailProvider.ts /** * Default Cloudflare Email Sending binding name. * * Matches the convention used by other Alepha Cloudflare providers * (e.g. `KV_CACHE` for {@link CloudflareKVProvider}). */ const SEND_EMAIL_DEFAULT_BINDING = "SEND_EMAIL"; /** * Environment variables for Cloudflare email configuration. * * `EMAIL_FROM` is `optional` at the schema level so the provider can be * registered (and constructed) in non-Workers contexts — Node `yarn * start`, build-time introspection, etc. — without forcing every dev to * set it. `send()` re-checks at call time and throws a clear error if * it's missing then. */ const envSchema = z.object({ EMAIL_FROM: z.text({ description: "Default sender (a verified sender address). Accepts a bare address or an RFC 5322 display-name form, e.g. `Lore <noreply@lore.alepha.dev>`." }).optional() }); /** * Email provider using Cloudflare's Email Sending API via a Workers binding. * * Requires the Workers Paid plan and a verified sender address on the * `EMAIL_FROM` domain. * * **Required Cloudflare binding:** * - `SEND_EMAIL` — an Email Sending binding in wrangler configuration * * Configuration is provided via environment variables: * - `EMAIL_FROM`: Default sender email address * * @example * ```toml * # wrangler.toml * [[send_email]] * binding = "SEND_EMAIL" * ``` * * @example * ```typescript * // app.ts * import { AlephaEmailCloudflare } from "alepha/email/cloudflare"; * * const app = Alepha.create().with(AlephaEmailCloudflare); * ``` */ var CloudflareEmailProvider = class { alepha = $inject(Alepha); env = $env(envSchema); log = $logger(); binding; onStart = $hook({ on: "start", handler: () => { const cloudflareEnv = this.alepha.get("cloudflare.env"); if (!cloudflareEnv) { this.log.warn("Cloudflare Email Sending inert: 'cloudflare.env' not set (not running on Workers)."); return; } const binding = cloudflareEnv[SEND_EMAIL_DEFAULT_BINDING]; if (!binding) { this.log.warn(`Cloudflare Email Sending inert: binding '${SEND_EMAIL_DEFAULT_BINDING}' not found in Workers environment.`); return; } this.binding = binding; this.log.info("Cloudflare Email Sending OK"); } }); async send(options) { const { to, subject, body } = options; if (!this.env.EMAIL_FROM) throw new EmailError("Cannot send email via Cloudflare: EMAIL_FROM env var is not set."); this.log.info("Sending email via Cloudflare", { to, subject }); const message = { to: Array.isArray(to) ? to : [to], from: this.resolveFrom(this.env.EMAIL_FROM), subject, html: body }; try { const result = await this.getBinding().send(message); if (result?.status === "bounced") throw new EmailError(`Cloudflare email bounced (id=${result.id ?? "unknown"})`); this.log.info("Email sent successfully via Cloudflare", { to, subject, id: result?.id, status: result?.status }); } catch (error) { if (error instanceof EmailError) throw error; const message = error?.status === 429 ? `Cloudflare email rate limit hit (429): ${error instanceof Error ? error.message : String(error)}` : `Failed to send email via Cloudflare: ${error instanceof Error ? error.message : String(error)}`; this.log.error(message, { to, subject }); throw new EmailError(message, error instanceof Error ? error : void 0); } } /** * Map the configured `EMAIL_FROM` to Cloudflare's `from` shape. An RFC 5322 * display-name form — `Name <addr@host>` (the name may be quoted) — is split * into `{ email, name }` so Cloudflare renders the sender's display name * (e.g. `Lore <noreply@lore.alepha.dev>` → "Lore"). A bare address is passed * through unchanged. * * The object key is `email` (not `address`) — that is the field the * Cloudflare Email Service Workers API expects. Sending `{ address, name }` * makes the API reject the message with an opaque "internal error". */ resolveFrom(value) { const match = value.match(/^\s*(.*?)\s*<([^>]+)>\s*$/); if (!match) return value.trim(); const name = match[1].replace(/^"|"$/g, "").trim(); const email = match[2].trim(); return name ? { email, name } : { email }; } getBinding() { if (!this.binding) throw new AlephaError("Cloudflare Email binding not initialized. Call start() first."); return this.binding; } }; //#endregion //#region ../../src/email/cloudflare/index.ts /** * Plugin for Alepha Email that sends through Cloudflare's Email Sending API * via a Workers binding. * * @see {@link CloudflareEmailProvider} * @module alepha.email.cloudflare */ const AlephaEmailCloudflare = $module({ name: "alepha.email.cloudflare", services: [CloudflareEmailProvider], register: (alepha) => { if (alepha.isServerless()) alepha.with({ optional: true, provide: EmailProvider, use: CloudflareEmailProvider }); return alepha.with(AlephaEmail); } }); //#endregion export { AlephaEmailCloudflare, CloudflareEmailProvider, SEND_EMAIL_DEFAULT_BINDING }; //# sourceMappingURL=index.js.map