# Ephemera — Transactional email from a domain on Cloudflare (Email Sending)

> Self-executing Markdown. One binding of the *send-transactional-email* intent — siblings:
> [`email.resend.md`](./email.resend.md), [`email.aws.md`](./email.aws.md). The cloud is the source of
> truth; this file is intent + write-back ledger.

> **Requires** `zone(example.com)` from [`domain.cloudflare.md`](./domain.cloudflare.md) — sending
> from the domain needs the zone on Cloudflare (it is: `<CF_ZONE_ID>`); onboarding
> auto-writes SPF + DKIM into that zone. **Provides** `email-sender(example.com)`.

---

## 🤖 Director prompt

Observe before acting; verify each step; stop at 🔴/💥 for human go; write realized values back into Live
State. Cloudflare deploys in seconds; the only gate is the outward-facing first send. **Lowest-friction
binding** — the domain is already on Cloudflare, so DKIM/SPF are added for you.

```
Legend  🟢 create · 🟡 config · 🔴 GATE (human go) · 💥 destructive (human go) · ⏳ wait · ✔ verify
```

## Intent

Send transactional email (e.g. a website **contact form**) from `contact@example.com`, authenticated
so it lands in inboxes. On Cloudflare this is the lowest-friction binding: `wrangler email sending enable`
writes **SPF + DKIM into the same zone** automatically — no cross-provider DNS, no sandbox-approval gate.
This is Email **Sending** (outbound transactional), not Email **Routing** (inbound). Cloudflare's
first-party service; the MailChannels partnership it replaced is retired.

**Shared acceptance contract** (every binding must pass — the honest-equivalence test):
1. a test email **sends** (the provider returns success / queued)
2. it **lands in a real inbox**, not spam
3. **DKIM=pass, SPF=pass, DMARC=aligned** — read the recipient's `Authentication-Results` header

## Provisioning Inputs

| # | Question | Options (closed enum) | Default | Sets | Gates |
|---|----------|-----------------------|---------|------|-------|
| 1 | Sender address | free-text | `contact@example.com` | `SENDER` | the `from` address |
| 2 | Test recipient | free-text | `you@example.com` | `TEST_TO` | §3 verify send |
| 3 | Send path | `cli` / `rest` / `worker-binding` | `cli` | `SEND_MODE` | how §3 sends |

```yaml
resolved_inputs:
  sender:    contact@example.com
  test_to:   you@example.com
  send_mode: cli
```

## Live State

```yaml
status:        not-created      # published template - run it to realize state
last_action:   authored — Cloudflare Email Sending binding
last_verified: —
```

| key           | value |
|---------------|-------|
| ACCOUNT_ID    | `<CF_ACCOUNT_ID>` |
| CF_ZONE_ID    | `<CF_ZONE_ID>` |
| SENDER        | `contact@example.com` |

| ✔ check                  | expected                              | observed | result |
|--------------------------|---------------------------------------|----------|--------|
| domain enabled for send  | listed by `email sending list`        | —        | — |
| SPF + DKIM in zone        | `dns get` shows records present       | —        | — |
| DMARC present            | `_dmarc` TXT resolves                  | —        | — |
| test email delivered     | success/queued + lands in inbox        | —        | — |
| auth results             | dkim=pass spf=pass dmarc=pass          | —        | — |

## 0. Variables

```bash
export ACCOUNT_ID="<CF_ACCOUNT_ID>"
export CF_ZONE_ID="<CF_ZONE_ID>"
export SENDER="contact@example.com" TEST_TO="you@example.com" SEND_MODE="cli"
# token in $CLOUDFLARE_API_TOKEN; `command wrangler` to bypass the local wrapper.
```

## Dependency frontier

```
zone on Cloudflare (Requires) ─> enable sending (auto SPF+DKIM in zone) ─> DMARC ─> 🟡 send test ─> ✔ auth-results
```
Non-negotiable edges: **DKIM/SPF must exist before a send authenticates**; onboarding writes them since the
zone is local. DMARC is independent (one TXT). Teardown reverses.

## 1. Onboard the domain for Email Sending  🟢🟡

```bash
command wrangler email sending enable example.com      # auto-adds SPF (TXT) + DKIM into the CF zone
```
```bash
# ✔ verify — records present + domain enabled
command wrangler email sending dns get example.com     # shows SPF + DKIM records
command wrangler email sending list | grep example.com # enabled
```
> → Live State: domain enabled; SPF+DKIM in zone.

## 2. DMARC record (shared across bindings; idempotent)  🟡

```bash
# UPSERT _dmarc TXT in the CF zone (provider-agnostic; p=none to start, then tighten to quarantine/reject)
curl -fsS -X POST "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records" \
  -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" -H "Content-Type: application/json" \
  --data '{"type":"TXT","name":"_dmarc.example.com","content":"v=DMARC1; p=none; rua=mailto:you@example.com"}' \
  || echo "exists (idempotent)"
```
```bash
# ✔ verify
dig +short TXT _dmarc.example.com   # v=DMARC1; ...
```

## 3. Send a test  🟡 (outward-facing — light gate)

```bash
case "$SEND_MODE" in
  cli)  command wrangler email sending send --from "$SENDER" --to "$TEST_TO" \
          --subject "Ephemera CF email test" --text "It works — Cloudflare Email Sending." ;;
  rest) curl -fsS "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/email/sending/send" \
          -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" -H "Content-Type: application/json" \
          --data "{\"to\":\"${TEST_TO}\",\"from\":{\"address\":\"${SENDER}\",\"name\":\"Cybercussion\"},\"subject\":\"Ephemera CF email test\",\"text\":\"It works.\"}" ;;
  worker-binding) echo "Add  \"send_email\": [{ \"name\": \"EMAIL\" }]  to wrangler.jsonc; the contact-form Worker calls env.EMAIL.send({to,from:{email,name},subject,html,text})." ;;
esac
```
> The **contact form** in production posts to a Worker with the `send_email` binding (`env.EMAIL.send(...)`).

## 4. Acceptance verify  ✔

```bash
# the send returns success/queued (above). Then confirm authentication on the RECEIVED message:
echo "Open the email at ${TEST_TO} → 'Show original' → expect:  dkim=pass  spf=pass  dmarc=pass"
# (programmatic check: query the zone-level emailSendingAdaptiveGroups GraphQL dataset for delivered count)
```
> → Live State: status: live; fill verify rows.

## Teardown  💥

```bash
command wrangler email sending disable example.com   # stop sending; removes the sending records
# DMARC TXT is shared — leave it unless no binding uses the domain (then DELETE the _dmarc record).
```
```bash
# ✔ verify
command wrangler email sending list | grep example.com || echo "sending disabled"
```
> → Live State: status: gone.

## Portability ledger

Lives in [`email.resend.md`](./email.resend.md) — SES vs Resend vs Cloudflare across friction, DX, cost,
and lock-in. Headline for this binding: **lowest friction** because the zone is already on Cloudflare.

## Deliberately not included

- **Inbound (Email Routing)** — receiving/forwarding is a separate intent.
- **Marketing / bulk** — Email Sending is transactional-only; use a campaign platform for newsletters.
- **The contact-form Worker itself** — the consumer; this plan provisions the *capability*.
