Skip to main content

Crate cratefield_adapter_resend

Crate cratefield_adapter_resend 

Source
Expand description

cratefield-adapter-resend: the Mailer port over the Resend REST API (issue #6). Uses the runtime’s HttpClient port — no reqwest, no vendor SDK — so the same adapter runs on Workers and natively.

Degraded mode. The Resend account has no verified domain yet; until send.<domain> is verified, real sends 403. When the API key is absent the adapter reports SendOutcome::NotConfigured without any network call, so forms keep working.

cratefield-adapter-resend — Mail that goes out.

cratefield-adapter-resend on crates.io cratefield-adapter-resend documentation MIT

§cratefield-adapter-resend

Mailer port over the Resend REST API for the Cratefield harness. Uses the runtime’s HttpClient port — no reqwest, no vendor SDK — so it runs unchanged on Workers (worker::Fetch) and natively.

It also carries the inbound direction: ResendInbound verifies Resend’s email.received webhook with the Svix scheme and parses it into an InboundMessage, ready to hand to a module. The webhook is metadata only — the message body is a separate Received-emails API call the caller makes, not this crate. Configure the signing secret with ResendInbound::new(secret) or RESEND_WEBHOOK_SECRET via ResendInbound::from_env().

§Usage

ⓘ
use std::sync::Arc;
use cratefield_adapter_resend::Resend;
use cratefield_runtime_cloudflare::{FetchClient, WorkersClock};

// With a key:
let mailer = Resend::new(
    Arc::new(FetchClient),
    Arc::new(WorkersClock),
    Some(key),
    "Acme <no-reply@send.example.com>",
    None,
);
// Without a key (degraded mode): send() -> Ok(SendOutcome::NotConfigured),
// no network call. Wire it into the runtime:
let runtime = Cloudflare::new().db("DB").mailer(mailer);

Resend::from_env(http, clock) reads RESEND_API_KEY, MAIL_FROM, MAIL_REPLY_TO from the process environment (native/self-hosted). On Workers, read the secrets from the venture’s Env and call Resend::new. The Clock is what lets the adapter read the HTTP-date form of Retry-After (issue #278); without it a date-form 429 would read as “retry now”.

Error mapping (to cratefield_core::MailError): 401/403 → Unauthorized/DomainNotVerified { domain } (domain parsed from Resend’s message), 422 → Invalid { detail }, 429 → RateLimited { retry_after } (from the Retry-After header), 5xx → Upstream. No error Display ever includes the API key. text is always sent alongside html; the Idempotency-Key header is set from Message::idempotency_key.

§Verify a sending subdomain (not the apex)

The Resend account has no verified domain yet; until it does, real sends 403 with “Domain … is not verified” and this adapter maps that to MailError::DomainNotVerified. Until then, forms run in the degraded NotConfigured/503 mode.

When verifying:

  1. In the Resend dashboard add a subdomain, e.g. send.example.com, not the apex.
  2. Add the DKIM/SPF records Resend shows to the subdomain’s DNS zone.
  3. Wait for “Verified”, then set MAIL_FROM to an address on that subdomain (e.g. Acme <no-reply@send.example.com>).

Why not the apex? The apex domain (e.g. example.com) carries the inbound Email Routing MX records. Adding Resend’s outbound DKIM/SPF to the apex would mix inbound routing and outbound sending policy on the same name and can break delivered mail and future provider moves. A dedicated send. subdomain keeps them isolated, and lets inbound routing stay untouched if the outbound provider ever changes.

§Probing the key

Until a domain is verified, the key is send-only. Probe it with a real send (expect 403 DomainNotVerified, which proves the key itself is valid):

curl -s -X POST https://api.resend.com/emails \
  -H "authorization: Bearer $RESEND_API_KEY" \
  -H "content-type: application/json" \
  -d '{"from":"probe@send.example.com","to":"you@example.com","subject":"probe","text":"probe"}'

A 401 means the key is bad; 403 with the domain message means the key works but the domain is unverified.


MIT. Built in the open for Cratefield, a Factory Zero venture.

Structs§

Resend
Mailer over POST https://api.resend.com/emails.
ResendInbound
Resend’s email.received webhook, over core’s Svix scheme.