alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
153 lines (152 loc) • 5.26 kB
JavaScript
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