carlos / platform Public

mail: the verification walk, with SES as the only authority

All three axes must converge: an identity verified for sending whose MAIL
FROM has not converged still sends, from the wrong envelope domain, failing
DMARC. Verification never does its own DNS lookup — a wildcard at a parent
can synthesise an answer for an absent name and turn pending into a false
verified. Status is recomputed rather than latched, so a domain can recover.
Paul Campbell 6603dda30a7d17830af0491ee3b5d5c8651fd7ec parent e84f0c5
2 files changed, +180 −0
  • internal/mail/walk.go +51 −0
  • internal/mail/walk_verify_test.go +129 −0
diff --git a/internal/mail/walk.go b/internal/mail/walk.go
index f1635a93b..a887fa8d0 100644
--- a/internal/mail/walk.go
+++ b/internal/mail/walk.go
@@ -155,3 +155,54 @@ func EnsureDomain(ctx context.Context, s release.Store, p Provisioner, w dnsrec.
// ConfigSetName is the per-account SES configuration set that carries the
// event destination feeding bounce, complaint and send counts (spec §9).
func ConfigSetName(acct string) string { return "carlos-" + acct }
+
+// VerifyDomain re-reads SES's verdict for one (domain, region) and records
+// it.
+//
+// SES'S STATUS IS THE ONLY AUTHORITY. This function deliberately does NOT
+// do a DNS lookup to decide whether the records are live: a wildcard record
+// at a parent name can synthesise an answer for a name that is genuinely
+// absent, and a poller trusting its own lookup would flip a domain to
+// verified while SES still refuses to sign with it. Asking SES costs a round
+// trip and cannot be fooled that way.
+//
+// The status is recomputed from scratch each call rather than latched, so a
+// domain that SES later reports FAILED leaves verified, and one that
+// recovers leaves failed.
+func VerifyDomain(ctx context.Context, s release.Store, p Provisioner,
+ acct, domain, region string, now time.Time) (*DomainRecord, error) {
+
+ rec, etag, err := GetDomain(ctx, s, acct, domain)
+ if err != nil {
+ return nil, err
+ }
+ if rec == nil {
+ return nil, fmt.Errorf("mail: no domain record for %s", domain)
+ }
+ st, ok := rec.Regions[region]
+ if !ok {
+ return nil, fmt.Errorf("mail: domain %s has no identity in %s", domain, region)
+ }
+
+ got, err := p.IdentityStatus(ctx, region, domain)
+ if err != nil {
+ return nil, fmt.Errorf("mail: identity status for %s in %s: %w", domain, region, err)
+ }
+
+ switch {
+ case got.Verified():
+ st.Status = StatusVerified
+ case got.DKIM == "FAILED" || got.MailFrom == "FAILED":
+ // Terminal at SES until DNS changes. Recording it is what stops a
+ // member waiting on a spinner that will never resolve.
+ st.Status = StatusFailed
+ default:
+ st.Status = StatusPending
+ }
+ st.CheckedAt = now
+
+ if _, err := PutDomain(ctx, s, acct, rec, etag); err != nil {
+ return nil, err
+ }
+ return rec, nil
+}
diff --git a/internal/mail/walk_verify_test.go b/internal/mail/walk_verify_test.go
new file mode 100644
index 000000000..df9ec54af
--- /dev/null
+++ b/internal/mail/walk_verify_test.go
@@ -0,0 +1,129 @@
+package mail
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/dnsrec"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+func seedDomain(t *testing.T, s release.Store, p *MemProvisioner) time.Time {
+ t.Helper()
+ now := time.Date(2026, 8, 19, 12, 0, 0, 0, time.UTC)
+ if _, _, err := EnsureDomain(context.Background(), s, p, dnsrec.NewMemWriter(),
+ testDeployment(), "bag", "seapointish", "seapointish.bag.oncarlos.com", now); err != nil {
+ t.Fatalf("seed: %v", err)
+ }
+ return now
+}
+
+// All three axes must converge. An identity verified for SENDING whose
+// MAIL FROM has not converged still sends — from the wrong envelope domain,
+// failing DMARC alignment — so partial success must NOT read as verified.
+func TestVerifyRequiresEveryAxis(t *testing.T) {
+ ctx := context.Background()
+ s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
+ now := seedDomain(t, s, p)
+
+ p.SetIdentityStatus("eu-west-1", "seapointish.bag.oncarlos.com",
+ Status{DKIM: "SUCCESS", MailFrom: "PENDING", Sending: "SUCCESS"})
+ rec, err := VerifyDomain(ctx, s, p, "bag", "seapointish.bag.oncarlos.com", "eu-west-1", now)
+ if err != nil {
+ t.Fatalf("VerifyDomain: %v", err)
+ }
+ if rec.Regions["eu-west-1"].Status != StatusPending {
+ t.Fatal("a domain with an unconverged MAIL FROM was marked verified")
+ }
+
+ p.SetIdentityStatus("eu-west-1", "seapointish.bag.oncarlos.com",
+ Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"})
+ rec, err = VerifyDomain(ctx, s, p, "bag", "seapointish.bag.oncarlos.com", "eu-west-1", now)
+ if err != nil {
+ t.Fatalf("VerifyDomain: %v", err)
+ }
+ if rec.Regions["eu-west-1"].Status != StatusVerified {
+ t.Fatalf("status = %q, want verified", rec.Regions["eu-west-1"].Status)
+ }
+}
+
+// SES reports a permanent DKIM failure as FAILED. That is a terminal state
+// the member must see, not a pending one they wait on forever.
+func TestVerifyRecordsAPermanentFailure(t *testing.T) {
+ ctx := context.Background()
+ s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
+ now := seedDomain(t, s, p)
+
+ p.SetIdentityStatus("eu-west-1", "seapointish.bag.oncarlos.com",
+ Status{DKIM: "FAILED", MailFrom: "PENDING", Sending: "PENDING"})
+ rec, err := VerifyDomain(ctx, s, p, "bag", "seapointish.bag.oncarlos.com", "eu-west-1", now)
+ if err != nil {
+ t.Fatalf("VerifyDomain: %v", err)
+ }
+ if rec.Regions["eu-west-1"].Status != StatusFailed {
+ t.Fatalf("status = %q, want failed", rec.Regions["eu-west-1"].Status)
+ }
+}
+
+// A domain that recovers must be able to leave the failed state — SES can
+// report FAILED and later succeed once DNS is fixed.
+func TestVerifyCanRecoverFromFailed(t *testing.T) {
+ ctx := context.Background()
+ s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
+ now := seedDomain(t, s, p)
+
+ p.SetIdentityStatus("eu-west-1", "seapointish.bag.oncarlos.com",
+ Status{DKIM: "FAILED", MailFrom: "PENDING", Sending: "PENDING"})
+ VerifyDomain(ctx, s, p, "bag", "seapointish.bag.oncarlos.com", "eu-west-1", now)
+ p.SetIdentityStatus("eu-west-1", "seapointish.bag.oncarlos.com",
+ Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"})
+ rec, err := VerifyDomain(ctx, s, p, "bag", "seapointish.bag.oncarlos.com", "eu-west-1", now)
+ if err != nil {
+ t.Fatalf("VerifyDomain: %v", err)
+ }
+ if rec.Regions["eu-west-1"].Status != StatusVerified {
+ t.Fatal("a recovered domain stayed failed")
+ }
+}
+
+// A domain must not stay verified once SES regresses — status is
+// recomputed from scratch every call, never latched on a prior verified
+// reading. Without this test, a VerifyDomain that short-circuits on an
+// already-verified region ("nothing to do, it converged") would pass every
+// other test in this file while silently leaving a de-verified domain
+// reading verified forever.
+func TestVerifyLeavesVerifiedWhenSESRegresses(t *testing.T) {
+ ctx := context.Background()
+ s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
+ now := seedDomain(t, s, p)
+
+ p.SetIdentityStatus("eu-west-1", "seapointish.bag.oncarlos.com",
+ Status{DKIM: "SUCCESS", MailFrom: "SUCCESS", Sending: "SUCCESS"})
+ rec, err := VerifyDomain(ctx, s, p, "bag", "seapointish.bag.oncarlos.com", "eu-west-1", now)
+ if err != nil {
+ t.Fatalf("VerifyDomain: %v", err)
+ }
+ if rec.Regions["eu-west-1"].Status != StatusVerified {
+ t.Fatalf("status = %q, want verified", rec.Regions["eu-west-1"].Status)
+ }
+
+ p.SetIdentityStatus("eu-west-1", "seapointish.bag.oncarlos.com",
+ Status{DKIM: "FAILED", MailFrom: "SUCCESS", Sending: "SUCCESS"})
+ rec, err = VerifyDomain(ctx, s, p, "bag", "seapointish.bag.oncarlos.com", "eu-west-1", now)
+ if err != nil {
+ t.Fatalf("VerifyDomain: %v", err)
+ }
+ if rec.Regions["eu-west-1"].Status != StatusFailed {
+ t.Fatalf("status = %q, want failed — a previously verified domain must not stay latched verified once SES regresses", rec.Regions["eu-west-1"].Status)
+ }
+}
+
+func TestVerifyRefusesAnUnknownRegion(t *testing.T) {
+ ctx := context.Background()
+ s, p := release.NewDirStore(t.TempDir()), NewMemProvisioner("1")
+ now := seedDomain(t, s, p)
+ if _, err := VerifyDomain(ctx, s, p, "bag", "seapointish.bag.oncarlos.com", "ap-south-1", now); err == nil {
+ t.Fatal("verifying a region the domain has no identity in returned success")
+ }
+}