# Ephemera — Transactional email from a domain on AWS SES

> Self-executing Markdown. One binding of the *send-transactional-email* intent — siblings:
> [`email.resend.md`](./email.resend.md), [`email.cloudflare.md`](./email.cloudflare.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) — SES's Easy
> DKIM tokens become CNAMEs in the **Cloudflare zone** (`<CF_ZONE_ID>`). **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. SES is regional and async (DNS + identity verification take minutes; production access is a
review). Verbs: `verify` / `apply` / `teardown`.

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

## Intent

Send transactional email (a website **contact form**) from `contact@example.com`. **SES is the
cost-and-scale binding:** ~$0.10 per 1,000 emails, battle-tested, deep AWS integration (SNS bounce/complaint
events, dedicated IPs, configuration sets). Easy DKIM authenticates the domain (DKIM alignment alone
satisfies DMARC). The friction is honest and one-time: identity verification **and** the sandbox→production
gate — a deliverability/anti-abuse safeguard, not a defect. For high volume or AWS-native shops, SES is
frequently the right call.

**Shared acceptance contract** (every binding must pass):
1. a test email **sends** (SES returns a MessageId)
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 | Region | `us-west-2` / `us-east-1` / … | `us-west-2` | `AWS_REGION` | all SES calls |
| 2 | Sender address | free-text | `contact@example.com` | `SENDER` | the `from` address |
| 3 | Test recipient | free-text | `you@example.com` | `TEST_TO` | §5 send |
| 4 | Production access | `request` / `sandbox-only` | `request` | `PROD_ACCESS` | §4 — the 🔴 gate |

```yaml
resolved_inputs:
  region:      us-west-2
  sender:      contact@example.com
  test_to:     you@example.com
  prod_access: request          # 'request' = lift the sandbox; 'sandbox-only' = verified recipients only
```

## Live State

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

| key          | value |
|--------------|-------|
| AWS_REGION   | `us-west-2` |
| CF_ZONE_ID   | `<CF_ZONE_ID>` |
| SENDER       | `contact@example.com` |
| DKIM_TOKENS  | `—` (3, from §1) |

| ✔ check                  | expected                              | observed | result |
|--------------------------|---------------------------------------|----------|--------|
| identity created          | `example.com` with Easy DKIM      | —        | — |
| 3 DKIM CNAMEs in CF zone  | UPSERTed                                | —        | — |
| identity verified         | DKIM status `SUCCESS`, sending verified | —        | — |
| **production access**     | sandbox lifted (or recipient verified)  | —        | — |
| test delivered            | MessageId + lands in inbox              | —        | — |
| auth results             | dkim=pass spf=pass dmarc=pass          | —        | — |

## 0. Variables

```bash
export AWS_REGION="us-west-2" CF_ZONE_ID="<CF_ZONE_ID>"
export SENDER="contact@example.com" TEST_TO="you@example.com" PROD_ACCESS="request"
# token in $CLOUDFLARE_API_TOKEN (for the DNS step). SES uses your normal AWS session creds (no IAM created).
```

## Dependency frontier

```
create identity (Easy DKIM) ─> add 3 DKIM CNAMEs to CF zone (Requires zone) ─> verify ⏳ ─┐
                                                                                          ├─> 🔴 production access ⏳ ─> 🟡 send ─> ✔ auth-results
DMARC (CF zone) ──────────────────────────────────────────────────────────────────────────┘
```
Non-negotiable edges: **DKIM CNAMEs must be in the zone before verification succeeds**; **in sandbox you
can only send to verified recipients** — real visitors need §4's production access. DKIM alignment gives
DMARC pass without a custom MAIL FROM.

## 1. Create the domain identity (Easy DKIM)  🟢

```bash
aws sesv2 create-email-identity --region "$AWS_REGION" --email-identity example.com >/dev/null
# read the 3 DKIM tokens → CNAMEs
aws sesv2 get-email-identity --region "$AWS_REGION" --email-identity example.com \
  --query 'DkimAttributes.Tokens' --output text   # token1 token2 token3
```
> → Live State: DKIM_TOKENS. Each token `T` becomes:
> `T._domainkey.example.com  CNAME  T.dkim.amazonses.com`

## 2. Add the DKIM CNAMEs to the Cloudflare zone  🟡  *(Requires `domain.cloudflare.md`)*

```bash
for T in $(aws sesv2 get-email-identity --region "$AWS_REGION" --email-identity example.com --query 'DkimAttributes.Tokens' --output text); do
  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\":\"CNAME\",\"name\":\"${T}._domainkey.example.com\",\"content\":\"${T}.dkim.amazonses.com\",\"proxied\":false}" \
    && echo "added ${T}._domainkey" || echo "skip/exists ${T}"
done
```

## 3. Wait for verification  🟡⏳

```bash
for i in $(seq 1 12); do
  S="$(aws sesv2 get-email-identity --region "$AWS_REGION" --email-identity example.com --query 'DkimAttributes.Status' --output text)"
  echo "dkim: $S"; [ "$S" = "SUCCESS" ] && break; sleep 15
done
aws sesv2 get-email-identity --region "$AWS_REGION" --email-identity example.com --query 'VerifiedForSendingStatus' --output text   # true
```

## 4. Production access — lift the sandbox  🔴 GATE  *(skip if `PROD_ACCESS=sandbox-only`)*

> 🔴 **The "invisible ceiling," made an explicit gate.** In sandbox SES only sends to **verified** addresses
> (~200/day, 1/sec) — a real contact form silently drops every unverified visitor. Production access is a
> one-time review (~24h). This is the README's denial-by-default surfaced *before* it bites under load.

```bash
if [ "$PROD_ACCESS" = "request" ]; then
  aws sesv2 put-account-details --region "$AWS_REGION" \
    --production-access-enabled --mail-type TRANSACTIONAL \
    --website-url "https://example.com" \
    --use-case-description "Website contact form — transactional notifications to the site owner." \
    --additional-contact-email-addresses "$TEST_TO" --contact-language EN
  # ⏳ review (~24h). Poll:
  aws sesv2 get-account --region "$AWS_REGION" --query 'ProductionAccessEnabled' --output text   # true when granted
else
  # sandbox-only: verify the test recipient so you can send to it now
  aws sesv2 create-email-identity --region "$AWS_REGION" --email-identity "$TEST_TO" >/dev/null
  echo "Verify ${TEST_TO}: click the SES verification email before §5."
fi
```
> → Live State: record approver/timestamp when granted.

## 5. DMARC (shared; idempotent) + send a test  🟡

```bash
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 "dmarc exists"

aws sesv2 send-email --region "$AWS_REGION" \
  --from-email-address "$SENDER" \
  --destination "ToAddresses=${TEST_TO}" \
  --content 'Simple={Subject={Data="Ephemera SES test",Charset="UTF-8"},Body={Text={Data="It works — AWS SES.",Charset="UTF-8"}}}' \
  --query 'MessageId' --output text
```

## 6. Acceptance verify  ✔

```bash
echo "Open the email at ${TEST_TO} → 'Show original' → expect:  dkim=pass  spf=pass  dmarc=pass"
aws sesv2 get-account --region "$AWS_REGION" --query '{Prod:ProductionAccessEnabled,Quota:SendQuota.Max24HourSend}' --output json
```
> → Live State: status: live; fill verify rows.

## Teardown  💥

```bash
aws sesv2 delete-email-identity --region "$AWS_REGION" --email-identity example.com
# remove the 3 DKIM CNAMEs this binding added to the CF zone (list by name then delete); leave shared DMARC.
# production access can't be un-requested (it's an account property) — harmless to leave.
```
```bash
# ✔ verify
aws sesv2 get-email-identity --region "$AWS_REGION" --email-identity example.com 2>&1 | grep -q NotFound && echo "identity gone"
```
> → Live State: status: gone.

## Portability ledger

Lives in [`email.resend.md`](./email.resend.md). SES's row: **cheapest at scale, deepest AWS integration**,
trade is the verification + sandbox-approval friction (a one-time tax for durable low cost).

## Deliberately not included

- **SNS bounce/complaint handling + configuration sets** — production hardening; add once live.
- **Custom MAIL FROM (SPF alignment)** — DKIM alignment already passes DMARC; add MAIL FROM for strict SPF.
- **Dedicated IP / marketing** — high-volume / bulk concerns, out of scope.
- **The contact-form app** — the consumer of this capability.
