Examples
Examples and checks
Section titled “Examples and checks”Cloudflare Durable Object
Section titled “Cloudflare Durable Object”Construct one Carrier per Durable Object instance, using that object’s own storage and an Email Service binding. Forward the object’s alarm:
import { DurableObject } from "cloudflare:workers";import type { SendEmail } from "@cloudflare/workers-types";import type { Submission } from "@fungi.computer/carrier";import { createCloudflareCarrier } from "@fungi.computer/carrier/cloudflare";
type Env = { EMAIL: SendEmail };
export class Mailbox extends DurableObject<Env> { private readonly carrier;
constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.carrier = createCloudflareCarrier({ storage: ctx.storage, email: env.EMAIL, }); }
async send(email: Submission) { return (await this.carrier).send(email); }
async get(id: string) { return (await this.carrier).get(id); }
async alarm() { await (await this.carrier).alarm(); }}Configure a SQLite Durable Object namespace and an Email Service binding in the
host application. An application with a bound MAILBOX namespace can then
submit and inspect mail:
const mailbox = env.MAILBOX.getByName("team-example");const receipt = await mailbox.send({ id: "welcome:user-123:v1", from: { email: "hello@example.com", name: "Example" }, to: ["alice@example.net", "bob@example.net"], cc: ["host@example.net"], bcc: ["archive@example.net"], subject: "Welcome", text: "Your account is ready.", headers: { "List-Unsubscribe": "<https://example.com/unsubscribe/token>" },});const current = await mailbox.get(receipt.id);send() needs at least one recipient across To, CC and BCC. To is optional. At
least one of text or html is required. Display names and visible To and CC
roles pass to the provider unchanged. BCC uses the provider’s native BCC field.
Receipts expose all recipient addresses, BCC included, to the trusted caller, so
do not forward whole receipts to recipients or other viewers. Optional provider
headers are part of the immutable submission. A replay with different headers
conflicts just like a replay with different content.
Another provider
Section titled “Another provider”To use a different provider with Durable Object SQLite and alarms, pass
transport instead of email. Exactly one of the two is required:
const carrier = await createCloudflareCarrier({ storage: ctx.storage, transport: sendWithConfiguredProvider,});Operator inspection
Section titled “Operator inspection”const inspection = createDeliveryInspection(ctx.storage.sql);const emails = inspection.email({ limit: 32 });const receipts = inspection.post({ view: "receipts", limit: 32 });const dead = inspection.post({ view: "deadLetters", limit: 32 });Checks
Section titled “Checks”Unit tests exercise the native transport with controlled I/O, Post ordering,
rollback, duplicate admission and recovery over local SQLite, and operator
inspection. Cloudflare runtime tests exercise SQLite, alarms, eviction,
interrupted dispatch and the D1 Post adapter without a live email binding. The
PostgreSQL proof starts an isolated PostgreSQL 16 instance over a private Unix
socket and always stops it. Install PostgreSQL 16, or set
BOTANICAL_TEST_POSTGRES_ROOT to its extracted root. The D1 and PostgreSQL
proofs cover producer, envelope and job rollback and commit, replay conflicts,
restart, overlapping workers and fenced lease expiry. From the package
directory:
pnpm testNo email is sent by these fixtures.