carlos / platform Public

mail: the identity record, account-scoped with a region map

Account-scoped so two apps sharing a custom domain cannot both create the
SES identity; the apps set is what stops one app's teardown revoking a
sibling's verified domain. Regions is a map rather than a field because
every SES dimension that matters is regional, and a scalar would need a
fleet-wide migration the day a second region appears.
Paul Campbell e7c1b7e9124447e54eba28aa7b26f27ee146b5a0 parent 1c03a19
2 files changed, +281 −0
  • internal/mail/domain.go +178 −0
  • internal/mail/domain_test.go +103 −0
diff --git a/internal/mail/domain.go b/internal/mail/domain.go
new file mode 100644
index 000000000..f74cd3f7b
--- /dev/null
+++ b/internal/mail/domain.go
@@ -0,0 +1,178 @@
+// The identity record (spec §6a): accounts/<sqid>/email/domains/<domain>.json.
+//
+// ACCOUNT-scoped, not app-scoped, and the reason is narrow: two apps in one
+// account may legitimately share one custom domain, and only one of them may
+// create the SES identity and write its DNS. A double-create is the failure
+// this split exists to prevent. In the common case there is exactly one such
+// record per app, auto-created, and nobody looks at it.
+package mail
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "regexp"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// The closed status set. Only StatusVerified puts an identity's ARN into a
+// credential's policy, so a pending domain grants nothing.
+const (
+ StatusPending = "pending"
+ StatusVerified = "verified"
+ StatusFailed = "failed"
+)
+
+// KindPlatform means CARLOS owns the zone and publishes DNS itself;
+// KindCustom means the customer does. It is the one bit that decides who
+// publishes (spec §7).
+const (
+ KindPlatform = "platform"
+ KindCustom = "custom"
+)
+
+// RegionState is one region's identity. Everything in here is regional:
+// SES issues a distinct identity, distinct DKIM tokens and a distinct
+// MAIL FROM MX target per region (spec §8).
+type RegionState struct {
+ IdentityARN string `json:"identity_arn"`
+ DKIMTokens []string `json:"dkim_tokens"`
+ MailFrom string `json:"mail_from"`
+ ConfigSet string `json:"config_set"`
+ Status string `json:"status"`
+ CheckedAt time.Time `json:"checked_at"`
+}
+
+// DomainRecord is one sending domain.
+//
+// Regions is a MAP rather than a scalar field. Every SES dimension that
+// matters is regional, so a scalar would have to be migrated across the
+// fleet the first time a second region appears. Keying costs nothing today
+// and is the difference between adding a region and a migration.
+type DomainRecord struct {
+ Schema int `json:"schema"`
+ Domain string `json:"domain"`
+ Kind string `json:"kind"`
+ Apps []string `json:"apps"`
+ Regions map[string]*RegionState `json:"regions"`
+ CreatedAt time.Time `json:"created_at"`
+}
+
+func DomainsPrefix(acct string) string { return "accounts/" + acct + "/email/domains/" }
+
+func DomainKey(acct, domain string) string { return DomainsPrefix(acct) + domain + ".json" }
+
+// A conservative LDH hostname: lowercase labels of alphanumerics and inner
+// hyphens, at least two labels. Deliberately stricter than DNS allows,
+// because this string becomes a bucket key AND an IAM policy resource, and
+// the only safe place to refuse a traversal or an injection is the one
+// gate every caller passes through.
+var domainRe = regexp.MustCompile(`^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$`)
+
+func ValidDomain(d string) bool {
+ return len(d) <= 253 && domainRe.MatchString(d)
+}
+
+func GetDomain(ctx context.Context, s release.Store, acct, domain string) (*DomainRecord, string, error) {
+ if !release.ValidAccount(acct) {
+ return nil, "", fmt.Errorf("mail: invalid account %q", acct)
+ }
+ if !ValidDomain(domain) {
+ return nil, "", fmt.Errorf("mail: invalid domain %q", domain)
+ }
+ raw, etag, err := s.Get(ctx, DomainKey(acct, domain))
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, "", nil
+ }
+ return nil, "", err
+ }
+ var rec DomainRecord
+ if err := json.Unmarshal(raw, &rec); err != nil {
+ return nil, "", fmt.Errorf("mail: %s: %w", DomainKey(acct, domain), err)
+ }
+ return &rec, etag, nil
+}
+
+func PutDomain(ctx context.Context, s release.Store, acct string, rec *DomainRecord, etag string) (string, error) {
+ if !ValidDomain(rec.Domain) {
+ return "", fmt.Errorf("mail: invalid domain %q", rec.Domain)
+ }
+ rec.Schema = Schema
+ body, err := json.MarshalIndent(rec, "", " ")
+ if err != nil {
+ return "", err
+ }
+ return s.Put(ctx, DomainKey(acct, rec.Domain), append(body, '\n'), etag)
+}
+
+// ListDomains reads every identity record for an account. A key that fails
+// to parse is skipped rather than failing the listing: one malformed record
+// must not hide an account's other domains from the console.
+func ListDomains(ctx context.Context, s release.Store, acct string) ([]*DomainRecord, error) {
+ keys, err := s.List(ctx, DomainsPrefix(acct))
+ if err != nil {
+ return nil, err
+ }
+ var out []*DomainRecord
+ for _, k := range keys {
+ if !strings.HasSuffix(k, ".json") {
+ continue
+ }
+ raw, _, err := s.Get(ctx, k)
+ if err != nil {
+ continue
+ }
+ var rec DomainRecord
+ if json.Unmarshal(raw, &rec) != nil {
+ continue
+ }
+ out = append(out, &rec)
+ }
+ sort.Slice(out, func(i, j int) bool { return out[i].Domain < out[j].Domain })
+ return out, nil
+}
+
+// VerifiedARNs is every identity ARN this domain contributes to a
+// credential's policy. Only verified regions count.
+func (r *DomainRecord) VerifiedARNs() []string {
+ var out []string
+ for _, st := range r.Regions {
+ if st.Status == StatusVerified && st.IdentityARN != "" {
+ out = append(out, st.IdentityARN)
+ }
+ }
+ sort.Strings(out)
+ return out
+}
+
+// AddApp records an app as a referrer. Reports whether it changed anything.
+func (r *DomainRecord) AddApp(app string) bool {
+ for _, a := range r.Apps {
+ if a == app {
+ return false
+ }
+ }
+ r.Apps = append(r.Apps, app)
+ sort.Strings(r.Apps)
+ return true
+}
+
+// RemoveApp drops a referrer. The SES identity and its DNS are deleted only
+// when the set empties — without this, one app's teardown would silently
+// revoke a sibling app's verified domain.
+func (r *DomainRecord) RemoveApp(app string) bool {
+ for i, a := range r.Apps {
+ if a == app {
+ r.Apps = append(r.Apps[:i], r.Apps[i+1:]...)
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/mail/domain_test.go b/internal/mail/domain_test.go
new file mode 100644
index 000000000..4d9354537
--- /dev/null
+++ b/internal/mail/domain_test.go
@@ -0,0 +1,103 @@
+package mail
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func TestDomainKeyIsAccountScoped(t *testing.T) {
+ got := DomainKey("bag", "seapointish.bag.oncarlos.com")
+ want := "accounts/bag/email/domains/seapointish.bag.oncarlos.com.json"
+ if got != want {
+ t.Fatalf("DomainKey = %q, want %q", got, want)
+ }
+}
+
+func TestValidDomainRefusesWhatWouldEscapeTheKey(t *testing.T) {
+ for _, ok := range []string{"seapointish.bag.oncarlos.com", "example.com", "a-b.example.co.uk"} {
+ if !ValidDomain(ok) {
+ t.Fatalf("ValidDomain(%q) = false, want true", ok)
+ }
+ }
+ // A domain becomes a bucket KEY and an IAM policy RESOURCE. Anything
+ // that could traverse or inject must be refused here, at the only
+ // place every caller passes through.
+ for _, bad := range []string{"", ".", "../etc", "a/b.com", "UPPER.com", "-lead.com", "trail-.com", "a..b.com"} {
+ if ValidDomain(bad) {
+ t.Fatalf("ValidDomain(%q) = true, want false", bad)
+ }
+ }
+}
+
+// Regions is a MAP, and this test is the reason: a domain verified in two
+// regions holds two independent identities, ARNs and DKIM token sets, and
+// a scalar region field would have to be migrated the day a second region
+// appears (spec §6a, §8).
+func TestRegionsAreIndependent(t *testing.T) {
+ ctx, s := context.Background(), release.NewDirStore(t.TempDir())
+ now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
+ rec := &DomainRecord{
+ Domain: "seapointish.bag.oncarlos.com",
+ Kind: KindPlatform,
+ Apps: []string{"seapointish"},
+ Regions: map[string]*RegionState{
+ "eu-west-1": {IdentityARN: "arn:aws:ses:eu-west-1:1:identity/seapointish.bag.oncarlos.com",
+ DKIMTokens: []string{"a", "b", "c"},
+ MailFrom: "mail-eu-west-1.seapointish.bag.oncarlos.com",
+ Status: StatusVerified, CheckedAt: now},
+ "us-east-1": {IdentityARN: "arn:aws:ses:us-east-1:1:identity/seapointish.bag.oncarlos.com",
+ DKIMTokens: []string{"d", "e", "f"},
+ MailFrom: "mail-us-east-1.seapointish.bag.oncarlos.com",
+ Status: StatusPending, CheckedAt: now},
+ },
+ CreatedAt: now,
+ }
+ if _, err := PutDomain(ctx, s, "bag", rec, ""); err != nil {
+ t.Fatalf("PutDomain: %v", err)
+ }
+ out, _, err := GetDomain(ctx, s, "bag", "seapointish.bag.oncarlos.com")
+ if err != nil {
+ t.Fatalf("GetDomain: %v", err)
+ }
+ if len(out.Regions) != 2 {
+ t.Fatalf("regions = %d, want 2", len(out.Regions))
+ }
+ if out.Regions["us-east-1"].DKIMTokens[0] != "d" {
+ t.Fatal("per-region DKIM tokens did not round-trip independently")
+ }
+}
+
+// Only a VERIFIED region contributes an ARN, so a pending domain grants
+// nothing (spec §6a).
+func TestVerifiedARNsSkipsPendingAndFailed(t *testing.T) {
+ rec := &DomainRecord{Regions: map[string]*RegionState{
+ "eu-west-1": {IdentityARN: "arn-ok", Status: StatusVerified},
+ "us-east-1": {IdentityARN: "arn-pending", Status: StatusPending},
+ "ap-south-1": {IdentityARN: "arn-failed", Status: StatusFailed},
+ }}
+ got := rec.VerifiedARNs()
+ if len(got) != 1 || got[0] != "arn-ok" {
+ t.Fatalf("VerifiedARNs = %v, want [arn-ok]", got)
+ }
+}
+
+// The apps set is what makes account-scoping worth its cost: teardown of
+// one app must never revoke a sibling's verified domain (spec §6a).
+func TestAppsSetGuardsSiblingTeardown(t *testing.T) {
+ rec := &DomainRecord{Apps: []string{"seapointish"}}
+ if !rec.AddApp("marketing") {
+ t.Fatal("AddApp returned false for a new app")
+ }
+ if rec.AddApp("marketing") {
+ t.Fatal("AddApp returned true for an app already present")
+ }
+ if !rec.RemoveApp("seapointish") {
+ t.Fatal("RemoveApp returned false for a present app")
+ }
+ if len(rec.Apps) != 1 || rec.Apps[0] != "marketing" {
+ t.Fatalf("Apps = %v, want [marketing] — the sibling must survive", rec.Apps)
+ }
+}