carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1# Tenant email SES spike
2
3> **For agentic workers:** this is a spike, not a build. Its output is
4> four answers written back into the spec, and throwaway AWS resources
5> deleted at the end. Do not keep the code.
6
7**Goal:** Answer the four AWS questions the sender design depends on,
8against the Tito deployment's real SES account, before any plan is
9written for steps 2 and 3.
10
11**Spec:** `docs/superpowers/specs/2026-09-15-tenant-email-senders-design.md` §4
12
13**Tech stack:** `aws` CLI (SES v2, IAM), a throwaway domain the Tito
14deployment already controls, Python `smtplib` for the SMTP sends.
15
16## Global constraints
17
18- Region `eu-west-1`, AWS account 665106592142 (the Tito deployment).
19- **Never touch `tito.io`, `mail.tito.io`, or any identity Classic
20 owns.** The spike's identity is a fresh subdomain created for it.
21- `aws --profile` for assumed roles needs `~/.aws/cli/cache`, which the
22 sandbox blocks — run those commands with the sandbox off.
23- The SES control plane is throttled to one request per second; space
24 the calls.
25- Everything created here is deleted in step 5, including the IAM user
26 and its access key.
27
28## Setup
29
30- [ ] **Create the throwaway identity.** A subdomain of a domain the
31 deployment controls, e.g. `spike.<a domain you own>`. Verify DKIM the
32 normal way and wait for `GetEmailIdentity` to report
33 `DkimAttributes.Status = SUCCESS`. Do **not** set a custom MAIL FROM:
34 the spike needs an identity shaped like `tito.io`, which has none.
35- [ ] **Create two configuration sets**, `spike-owned` and `spike-other`,
36 each with an SNS event destination on a throwaway topic you can read.
37- [ ] **Bind `spike-other` as the identity's default configuration set**
38 with `PutEmailIdentityConfigurationSetAttributes`. This models
39 Classic's set: one CARLOS is not allowed to change.
40- [ ] **Create a tenant** `spike-tenant` with `CreateTenant`; record
41 `TenantArn`. Associate the identity and `spike-owned` with it using
42 `CreateTenantResourceAssociation`. Do **not** associate `spike-other`.
43- [ ] **Create an IAM user** with one inline policy, one access key, and
44 derive its SMTP password (the repo's `SMTPPassword` is
45 `internal/mail/smtppass.go:37`; a small Go program calling it is the
46 quickest route). Policy:
47
48```json
49{"Version":"2012-10-17","Statement":[{"Effect":"Allow",
50 "Action":"ses:SendRawEmail",
51 "Resource":["<identity ARN>","<spike-owned config set ARN>"],
52 "Condition":{"StringEquals":{"ses:TenantName":"spike-tenant"}}}]}
53```
54
55## Question 1: is `ses:TenantName` enforced over SMTP?
56
57- [ ] **Send over SMTP with both headers** — `X-SES-TENANT: spike-tenant`
58 and `X-SES-CONFIGURATION-SET: spike-owned`. Record accepted or
59 refused, and the exact SMTP error if refused.
60- [ ] **Send over SMTP with neither header.** Record the same.
61- [ ] **Write down which of the three outcomes happened:**
62 1. Both accepted — the key is not populated for SMTP; the tenant
63 condition buys nothing and comes out of the policy.
64 2. Tagged accepted, untagged refused — the design works as written.
65 3. Both refused — SES never populates the key; the condition denies
66 everything and must come out of the policy.
67
68## Question 2: does a tenant apply its configuration set on its own?
69
70- [ ] **Send with `X-SES-TENANT` but no `X-SES-CONFIGURATION-SET`.**
71 The identity's default is `spike-other`, which is not associated with
72 the tenant. Record whether the send is refused (the documented
73 behaviour) or accepted.
74- [ ] **If accepted, check which topic the events arrived on.** That
75 tells you whether events would reach CARLOS or the identity owner.
76- [ ] **Send with both headers and confirm the event arrives on
77 `spike-owned`'s topic**, carrying `ses:caller-identity` with the IAM
78 user's name — §8's attribution depends on that tag being present.
79
80## Question 3: is an associated identity left alone, and are its other senders?
81
82- [ ] **Record `GetEmailIdentity` before and after the association** and
83 diff: DKIM tokens, `MailFromAttributes`, and
84 `ConfigurationSetName`. Nothing should change.
85- [ ] **Send on the identity with no tenant header at all**, using a
86 second IAM user whose policy has no tenant condition. This is
87 Classic's traffic. Confirm it still sends, and that its events go to
88 `spike-other` as before.
89- [ ] **Confirm `DeleteEmailIdentity` is refused** while the association
90 exists, then re-associate. This is the cross-team contract §7 records:
91 Classic loses the ability to delete the identity.
92
93## Question 4: can a tenant be paused directly?
94
95- [ ] **Pause it** with `UpdateReputationEntityCustomerManagedStatus`
96 (`ReputationEntityType=RESOURCE`, the tenant ARN,
97 `SendingStatus=DISABLED`).
98- [ ] **Send with both headers** and confirm the send is refused.
99- [ ] **Read `GetTenant`** and record where the paused state appears, so
100 §8 knows what status to display.
101- [ ] **Re-enable** with `SendingStatus=ENABLED` and confirm sending
102 resumes.
103- [ ] **Record the tenant's reputation policy** as created — the default
104 is Standard, which auto-pauses — so §8 can decide between adopting
105 SES's pause and setting the policy to `None`.
106
107## Finish
108
109- [ ] **Write the four answers into the spec** as a new "§4 results"
110 subsection: outcome number for question 1, refused-or-accepted for
111 question 2, the before/after diff for question 3, and the pause verb
112 and status field for question 4. Say plainly what each one changes.
113- [ ] **Delete everything**: both IAM users and their keys, the tenant
114 and its associations, both configuration sets, the SNS topic, and the
115 identity. `ListTenantResources` before deleting the tenant, since an
116 identity cannot be deleted while associated.
117- [ ] **Commit the spec change** on this branch. No other code lands.
118