// The backend seam (spec §5): everything the console needs a backend to do, // and nothing an app ever calls. // // Every method is idempotent — "already exists" and "already gone" are both // successes — because every walk that drives one must be re-runnable after a // half-failure. That is not a style preference; it is the only reason a // crashed provisioning run can be repaired by running it again rather than // by hand. package mail import ( "context" "errors" ) // Identity is what EnsureIdentity answers: the ARN a policy names and the // DKIM tokens whose CNAMEs must be published. The tokens are STABLE for the // life of the identity — re-ensuring returns the same three, because fresh // ones would orphan DNS already published (and, for a custom domain, DNS // somebody else published by hand). type Identity struct { ARN string DKIMTokens []string // Created is true when this call made the identity, false when it was // already there. The walk records it, because an identity CARLOS did not // create is not CARLOS's to delete (discussion #216). Created bool } // ErrIdentityExists is EnsureIdentity's refusal, with adopt false, of a // domain that already has an identity. Nothing about the identity has been // read or changed when it is returned. var ErrIdentityExists = errors.New("mail: identity already exists") // Status is SES's own verdict on an identity. All three must read // "SUCCESS" before the identity is usable; a domain that is verified for // sending but whose MAIL FROM has not converged still sends, from the // wrong envelope domain, and fails DMARC alignment. type Status struct { DKIM string MailFrom string Sending string // ConfigSet is the configuration set SES currently has BOUND to this // identity, empty for none. It is read here because // GetEmailIdentity already returns it and this call already happens on // every verify — which makes drift detection free (issue #272). // // The alternative was reconciling on every status read, which would put // a GetEmailIdentity per domain on a path that today answers from the // bucket alone, and would make a working read fail whenever AWS is // unreachable. Reading it where the call is already being made costs // nothing and catches the same drift a beat later. ConfigSet string } // Verified reports whether every axis has converged. func (s Status) Verified() bool { return s.DKIM == "SUCCESS" && s.MailFrom == "SUCCESS" && s.Sending == "SUCCESS" } type Provisioner interface { // EnsureTenant creates the account's SES tenant if absent. Additive: // a backend without tenant support returns nil and the walks still // isolate correctly, because isolation is the IAM policy (spec §4). EnsureTenant(ctx context.Context, name string) error // EnsureConfigSet creates the event destination that feeds bounce and // complaint ingestion (spec §9). EnsureConfigSet(ctx context.Context, region, name, snsTopicARN string) error // SetIdentityConfigSet binds an identity to the configuration set that // carries SES's event destination (spec §9; C1, review follow-up). // EnsureConfigSet alone creates the SET — it does not touch any // identity. SES only emits events (send, bounce, complaint) for a // message sent WITH a configuration set, so an identity that never // gets this call generates NO events, ever, no matter how many // messages it sends: every count stays zero forever, the daily cap // never enforces, and the automatic abuse pause never fires. Called // once per (region, domain) — idempotent, like every verb here. SetIdentityConfigSet(ctx context.Context, region, domain, configSet string) error // AssociateTenantResource associates an identity, a configuration set or // a template with a tenant. Associating does not modify the resource and // one resource may belong to many tenants, so this is safe to call on an // identity another product owns. It has exactly one side effect on the // owner: an identity cannot be deleted while an association exists, // which is why DisassociateTenantResource has to run before // DeleteIdentity. Idempotent, like every verb here. AssociateTenantResource(ctx context.Context, tenant, resourceARN string) error // DisassociateTenantResource removes one association. "Already gone" is // success. DisassociateTenantResource(ctx context.Context, tenant, resourceARN string) error // EnsureIdentity creates the domain identity, enables Easy DKIM and // sets the custom MAIL FROM subdomain, all idempotently. // // adopt says whether an identity that already exists may be taken over: // its DKIM tokens read back and its MAIL FROM rewritten. With adopt // false, an existing identity is refused with ErrIdentityExists and left // exactly as it was — the walk passes false unless the identity is one // this account already holds or has proved it controls (#216). EnsureIdentity(ctx context.Context, region, domain, mailFrom string, adopt bool) (Identity, error) IdentityStatus(ctx context.Context, region, domain string) (Status, error) DeleteIdentity(ctx context.Context, region, domain string) error // EnsureUser creates the principal if absent and writes its inline // policy wholesale. It MUST refuse an empty grant rather than create a // principal with no policy. EnsureUser(ctx context.Context, name, path string, identityARNs, configSetARNs, domains []string) (arn string, err error) MintKey(ctx context.Context, user string) (id, secret string, err error) // SetKeyActive is the pause (spec §9): instant, reversible, and it // needs no policy rewrite. SetKeyActive(ctx context.Context, user, keyID string, active bool) error DeleteKey(ctx context.Context, user, keyID string) error // ReapKeys deletes every access key of user NOT named in keep. A key // minted before a failed delivery is recorded nowhere, so only // list-and-delete can find it. ReapKeys(ctx context.Context, user string, keep []string) error DeleteUser(ctx context.Context, user string) error }