# Tenant email SES spike

> **For agentic workers:** this is a spike, not a build. Its output is
> four answers written back into the spec, and throwaway AWS resources
> deleted at the end. Do not keep the code.

**Goal:** Answer the four AWS questions the sender design depends on,
against the Tito deployment's real SES account, before any plan is
written for steps 2 and 3.

**Spec:** `docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md` §4

**Tech stack:** `aws` CLI (SES v2, IAM), a throwaway domain the Tito
deployment already controls, Python `smtplib` for the SMTP sends.

## Global constraints

- Region `eu-west-1`, AWS account 665106592142 (the Tito deployment).
- **Never touch `tito.io`, `mail.tito.io`, or any identity Classic
  owns.** The spike's identity is a fresh subdomain created for it.
- `aws --profile` for assumed roles needs `~/.aws/cli/cache`, which the
  sandbox blocks — run those commands with the sandbox off.
- The SES control plane is throttled to one request per second; space
  the calls.
- Everything created here is deleted in step 5, including the IAM user
  and its access key.

## Setup

- [ ] **Create the throwaway identity.** A subdomain of a domain the
  deployment controls, e.g. `spike.<a domain you own>`. Verify DKIM the
  normal way and wait for `GetEmailIdentity` to report
  `DkimAttributes.Status = SUCCESS`. Do **not** set a custom MAIL FROM:
  the spike needs an identity shaped like `tito.io`, which has none.
- [ ] **Create two configuration sets**, `spike-owned` and `spike-other`,
  each with an SNS event destination on a throwaway topic you can read.
- [ ] **Bind `spike-other` as the identity's default configuration set**
  with `PutEmailIdentityConfigurationSetAttributes`. This models
  Classic's set: one CARLOS is not allowed to change.
- [ ] **Create a tenant** `spike-tenant` with `CreateTenant`; record
  `TenantArn`. Associate the identity and `spike-owned` with it using
  `CreateTenantResourceAssociation`. Do **not** associate `spike-other`.
- [ ] **Create an IAM user** with one inline policy, one access key, and
  derive its SMTP password (the repo's `SMTPPassword` is
  `internal/mail/smtppass.go:37`; a small Go program calling it is the
  quickest route). Policy:

```json
{"Version":"2012-10-17","Statement":[{"Effect":"Allow",
 "Action":"ses:SendRawEmail",
 "Resource":["<identity ARN>","<spike-owned config set ARN>"],
 "Condition":{"StringEquals":{"ses:TenantName":"spike-tenant"}}}]}
```

## Question 1: is `ses:TenantName` enforced over SMTP?

- [ ] **Send over SMTP with both headers** — `X-SES-TENANT: spike-tenant`
  and `X-SES-CONFIGURATION-SET: spike-owned`. Record accepted or
  refused, and the exact SMTP error if refused.
- [ ] **Send over SMTP with neither header.** Record the same.
- [ ] **Write down which of the three outcomes happened:**
  1. Both accepted — the key is not populated for SMTP; the tenant
     condition buys nothing and comes out of the policy.
  2. Tagged accepted, untagged refused — the design works as written.
  3. Both refused — SES never populates the key; the condition denies
     everything and must come out of the policy.

## Question 2: does a tenant apply its configuration set on its own?

- [ ] **Send with `X-SES-TENANT` but no `X-SES-CONFIGURATION-SET`.**
  The identity's default is `spike-other`, which is not associated with
  the tenant. Record whether the send is refused (the documented
  behaviour) or accepted.
- [ ] **If accepted, check which topic the events arrived on.** That
  tells you whether events would reach CARLOS or the identity owner.
- [ ] **Send with both headers and confirm the event arrives on
  `spike-owned`'s topic**, carrying `ses:caller-identity` with the IAM
  user's name — §8's attribution depends on that tag being present.

## Question 3: is an associated identity left alone, and are its other senders?

- [ ] **Record `GetEmailIdentity` before and after the association** and
  diff: DKIM tokens, `MailFromAttributes`, and
  `ConfigurationSetName`. Nothing should change.
- [ ] **Send on the identity with no tenant header at all**, using a
  second IAM user whose policy has no tenant condition. This is
  Classic's traffic. Confirm it still sends, and that its events go to
  `spike-other` as before.
- [ ] **Confirm `DeleteEmailIdentity` is refused** while the association
  exists, then re-associate. This is the cross-team contract §7 records:
  Classic loses the ability to delete the identity.

## Question 4: can a tenant be paused directly?

- [ ] **Pause it** with `UpdateReputationEntityCustomerManagedStatus`
  (`ReputationEntityType=RESOURCE`, the tenant ARN,
  `SendingStatus=DISABLED`).
- [ ] **Send with both headers** and confirm the send is refused.
- [ ] **Read `GetTenant`** and record where the paused state appears, so
  §8 knows what status to display.
- [ ] **Re-enable** with `SendingStatus=ENABLED` and confirm sending
  resumes.
- [ ] **Record the tenant's reputation policy** as created — the default
  is Standard, which auto-pauses — so §8 can decide between adopting
  SES's pause and setting the policy to `None`.

## Finish

- [ ] **Write the four answers into the spec** as a new "§4 results"
  subsection: outcome number for question 1, refused-or-accepted for
  question 2, the before/after diff for question 3, and the pause verb
  and status field for question 4. Say plainly what each one changes.
- [ ] **Delete everything**: both IAM users and their keys, the tenant
  and its associations, both configuration sets, the SNS topic, and the
  identity. `ListTenantResources` before deleting the tenant, since an
  identity cannot be deleted while associated.
- [ ] **Commit the spec change** on this branch. No other code lands.
