sendingpolicy

package
v1.8.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 3, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package sendingpolicy owns the authoritative sending-protection runtime policy: the typed payload, its closed enums and numeric invariants, its RFC 8785 canonical form, and the compare-and-swap activation that advances it.

Nothing here enforces anything. Task 2 establishes the authority that later slices read; every mode in the generation-zero policy is disabled, and the module is deliberately inert until a reviewed activation says otherwise.

Index

Constants

View Source
const (
	EnvFeedbackHMACKeys      = "E2A_SENDING_FEEDBACK_HMAC_KEYS"
	EnvOperatorRecipientsMap = "E2A_SENDING_PROTECTION_OPERATOR_EMAILS"
)

Environment variable names, fixed by the B1a prewire: the ops assemblers render exactly these keys, and docker-compose passes them through to the server environment.

View Source
const (
	ReasonAccountPaused       = "account_paused"
	ReasonAccountDailyBudget  = "account_daily_budget_exhausted"
	ReasonAccountSharedBudget = "account_shared_daily_budget_exhausted"
	ReasonGlobalAllBudget     = "global_all_budget_exhausted"
	ReasonGlobalProbation     = "global_probation_budget_exhausted"
	ReasonGlobalCritical      = "global_critical_budget_exhausted"
	ReasonGlobalViolation     = "global_violation_budget_exhausted"
	ReasonTenantNotReady      = "ses_tenant_not_ready"
	ReasonRecipientSuperseded = "notice_recipient_superseded"
	ReasonAccountDeleted      = "account_deleted"
	// ReasonSourceUnavailable means the durable source row this operation was
	// derived from is gone, so there is nothing left to send.
	ReasonSourceUnavailable = "source_unavailable"
	// ReasonNoticeSettled means the notice delivery already reached a terminal
	// state; re-sending it would duplicate a logical notice.
	ReasonNoticeSettled = "notice_already_settled"
	// ReasonTenantUnnamed means the policy requires a tenant header but the
	// account has no tenant name to send.
	ReasonTenantUnnamed = "ses_tenant_unnamed"
	// ReasonClassChanged means the message's reputation class stopped matching
	// the immutable class its operation was derived from, so the operation no
	// longer describes the send.
	ReasonClassChanged = "reputation_class_changed"
)

Reason codes for a hold. These are machine-readable and appear in metrics and lifecycle events, so they are part of the operator contract even though no public API surfaces them in this slice.

View Source
const ContractLevel = 0

ContractLevel is the compiled provider-call closure contract this binary advertises. It stays 0 until Task 12's closure guard proves every SES path requires authorization; Task 12 is the only change allowed to raise it.

View Source
const SchemaVersion = 1

SchemaVersion is the only policy schema this binary understands. A stored policy carrying any other version is rejected before it can reach a provider authorization decision — an older binary must never silently reinterpret a newer operator's payload.

View Source
const SystemPolicySubject = "e2a-system"

SystemPolicySubject is the fixed policy subject for operational and public-feedback mail. It is a sentinel reference rather than a real account row: no customer state may authorize or block a pause notice, and there is no users row to pause. Phase 7 binds it to the e2a-system SES tenant.

Variables

View Source
var (
	// ErrAttemptStale means the reference names an attempt that is no longer
	// the operation's current one, or one whose state forbids the requested
	// transition. It is always zero writes to the ledger and zero provider
	// calls.
	ErrAttemptStale = errors.New("sendingpolicy: attempt is stale")
	// ErrProviderCallStarted means the attempt already opened a socket, so its
	// capacity cannot be given back. Retrying needs a new ordinal, not this one.
	ErrProviderCallStarted = errors.New("sendingpolicy: provider call already started")
	// ErrAuthorizationInvalid means a redemption failed its final recheck. The
	// attempt is invalidated and a strictly greater ordinal must be allocated
	// before any provider call.
	ErrAuthorizationInvalid = errors.New("sendingpolicy: provider authorization is no longer valid")
	// ErrEnvelopeUnavailable means the operation's authorized envelope could
	// not be resolved from durable state or the caller's reference.
	ErrEnvelopeUnavailable = errors.New("sendingpolicy: authorized envelope is unavailable")
)

Sentinel errors for the authorization surface.

View Source
var (
	// ErrSourceUnavailable means the referenced durable source row is absent,
	// deleted, or not of the shape its constructor promised. It is always
	// fail-closed: no operation is created, so no provider call can follow.
	ErrSourceUnavailable = errors.New("sendingpolicy: notification source is unavailable")
	// ErrAudienceNotAllowed means a notice event was asked for an audience its
	// kind forbids — the global guardrail has no owner to blame.
	ErrAudienceNotAllowed = errors.New("sendingpolicy: audience is not allowed for this notice")
	// ErrNoticeSettled means the notice delivery already reached a terminal
	// state, so there is nothing left to send.
	ErrNoticeSettled = errors.New("sendingpolicy: notice delivery is already settled")
)

Sentinel errors for the preparation surface.

View Source
var (
	// ErrStaleGeneration means the stored policy generation moved between the
	// operator reading it and this write. Zero rows were written.
	ErrStaleGeneration = errors.New("sendingpolicy: stale policy generation")
	// ErrStaleAttestation means the attestation revision or its four-field
	// hash did not match what the caller expected. Zero rows were written.
	ErrStaleAttestation = errors.New("sendingpolicy: stale runtime attestation")
	// ErrAttestationCommitUnchanged means Commit returned an error and the
	// mandatory reread proved the reviewed prior revision/hash are still
	// current. The same forward request is safe to retry if still intended.
	ErrAttestationCommitUnchanged = errors.New("sendingpolicy: attestation commit left state unchanged")
	// ErrAttestationCommitUnknown means Commit returned an error and the
	// mandatory reread could not prove either the exact requested next state or
	// the unchanged prior state. The caller must inspect and fence/restore.
	ErrAttestationCommitUnknown = errors.New("sendingpolicy: attestation commit outcome is unknown")
	// ErrPolicyHashMismatch means the reviewed hash does not describe the
	// policy actually being submitted.
	ErrPolicyHashMismatch = errors.New("sendingpolicy: policy hash mismatch")
	// ErrRegistryConflict means an operator-recipient version already exists
	// with a different key identity or commitment. Append-only history is
	// never rewritten, so this is always zero writes.
	ErrRegistryConflict = errors.New("sendingpolicy: operator recipient registry conflict")
	// ErrOperatorRecipientUnavailable means the policy-selected logical
	// version is absent from either this process's immutable secret map or the
	// permanent registry. Policy activation must fail before writing anything.
	ErrOperatorRecipientUnavailable = errors.New("sendingpolicy: selected operator recipient is unavailable")
	// ErrBillingContractTooLow means a budget-enforcement transition was
	// attempted without both active and rollback billing images attested at
	// sending-protection contract level 1 or higher.
	ErrBillingContractTooLow = errors.New("sendingpolicy: billing contract is too low for budget enforcement")
	// ErrInvalidBillingDigest means an attestation did not name a canonical
	// immutable sha256 image digest. Empty is reserved for contract level 0.
	ErrInvalidBillingDigest = errors.New("sendingpolicy: invalid billing image digest")
	// ErrAlreadyGrandfathered means the one-shot ramp-grandfathering marker
	// already exists. The retry is a documented no-op: it can never widen the
	// grandfathered set, so it performs zero writes.
	ErrAlreadyGrandfathered = errors.New("sendingpolicy: sending domains were already grandfathered")
)

Sentinel errors. Callers distinguish "you lost a race, re-read and decide" from "your request was malformed", because the correct operator response to each is different: one is a retry, the other is a fix.

View Source
var ErrEnvelopeMismatch = errors.New("sendingpolicy: envelope does not match the authorized recipients")

ErrEnvelopeMismatch means the envelope a caller is about to submit is not the envelope that was authorized. It is returned before any redemption or network I/O, because a mismatch here is either a bug or an attempt to reuse one authorization for different recipients.

Functions

func AttestationHash

func AttestationHash(a RuntimeAttestation) (string, error)

AttestationHash is the lowercase SHA-256 an operator passes back as -expected-attestation-sha256. It covers only the four state fields, never the revision: the revision is compared separately and explicitly.

func CanonicalBytes

func CanonicalBytes(p RuntimePolicy) ([]byte, error)

CanonicalBytes exposes the canonical form for storage and operator readback.

func Hash

func Hash(p RuntimePolicy) (string, error)

Hash returns the lowercase hex SHA-256 of the canonical form. This is the value an operator reviews and then passes back as -expected-policy-sha256, so it must be derived from exactly the bytes that get stored.

func HashBytes

func HashBytes(canonical []byte) string

HashBytes returns the lowercase hex SHA-256 of already-canonical bytes. The store uses it to re-derive a stored row's hash without re-canonicalizing, so a row whose bytes drifted from its recorded hash is detected rather than silently re-blessed.

Types

type AcceptanceDecision added in v1.8.8

type AcceptanceDecision string

AcceptanceDecision is what an API acceptance surface learns from PrepareExternalTx: whether this account may still queue outbound mail at all. It deliberately carries no budget verdict — budgets are decided immediately before the provider call, not at acceptance, so that a queued message is held rather than rejected when the account runs out of daily capacity.

const (
	// AcceptanceAccept means the send may be durably queued.
	AcceptanceAccept AcceptanceDecision = "accept"
	// AcceptanceSendingPaused means the account is paused; the caller rejects
	// the request rather than queueing mail that can never leave.
	AcceptanceSendingPaused AcceptanceDecision = "sending_paused"
)

type ActivationRequest

type ActivationRequest struct {
	ExpectedGeneration int64
	Policy             RuntimePolicy
	Actor              string
	Reason             string

	// GrandfatherCurrentSendingDomains performs the one-shot phase-3 snapshot
	// in the same transaction as the policy CAS: insert the singleton marker,
	// lock the domains table against concurrent sender transitions, and flip
	// every currently sending-verified, ramp-inactive domain to exempt. A
	// second attempt fails with ErrAlreadyGrandfathered and writes nothing.
	GrandfatherCurrentSendingDomains bool
}

ActivationRequest is one reviewed policy change.

type AttemptRef added in v1.8.8

type AttemptRef struct {
	// contains filtered or unexported fields
}

AttemptRef names one durable submission attempt: the module-allocated ordinal on a provider operation. River's own job.Attempt is deliberately not used — River retries and provider submissions are different clocks, and conflating them is how one logical message ends up making several unaccounted SES calls.

func (AttemptRef) Attempt added in v1.8.8

func (a AttemptRef) Attempt() int

Attempt exposes the durable submission ordinal, for logging.

func (AttemptRef) IsZero added in v1.8.8

func (a AttemptRef) IsZero() bool

IsZero reports an unset reference.

func (AttemptRef) OperationID added in v1.8.8

func (a AttemptRef) OperationID() string

OperationID exposes the operation this attempt belongs to, for logging.

type Audience added in v1.8.8

type Audience string

Audience is the closed recipient class of a protection notice. Owner mail goes to the affected customer; operator mail goes to the version of the operator mailbox map that the runtime policy currently selects.

const (
	AudienceOwner    Audience = "owner"
	AudienceOperator Audience = "operator"
)

type Capabilities

type Capabilities struct {
	SendingProtectionContract int               `json:"sending_protection_contract"`
	RuntimePolicySource       string            `json:"runtime_policy_source"`
	OperatorCommitments       map[string]string `json:"operator_notice_recipient_commitments"`
}

Capabilities is the operator-facing readback printed by -print-capabilities and compared across blue/green slots by the ops deploy gate. It carries commitments only: no addresses, no key material, no customer data.

func BuildCapabilities

func BuildCapabilities(source PolicySource, secrets Secrets) Capabilities

BuildCapabilities assembles the readback. A missing operator map yields an empty commitments object rather than an error: the self-host disabled mode legitimately has none, and the deploy gate treats absence as absence.

type Decision added in v1.8.8

type Decision struct {
	Allow    bool
	Reason   string
	RetryAt  time.Time
	Terminal bool
}

Decision is allow-or-hold. A hold carries the earliest time a retry could plausibly succeed — for a daily budget that is the next UTC midnight, which lets the worker snooze rather than spin.

Terminal separates "come back later" from "this can never proceed". Without it every hold reads as retryable, and a worker faced with an operation that is permanently void — its account deleted, its notice already sent, its reputation class no longer the one it was derived from — would snooze on it forever instead of failing the message once. A terminal hold carries no RetryAt because there is no time at which the answer changes.

type Gate added in v1.8.8

type Gate interface {
	PrepareExternalTx(ctx context.Context, tx pgx.Tx, messageID string) (AcceptanceDecision, OperationRef, error)
	PrepareNotificationTx(context.Context, pgx.Tx, NotificationRef) (OperationRef, error)
	PrepareProtectionNoticeTx(context.Context, pgx.Tx, ProtectionNoticeRef) (OperationRef, error)
	PreparePublicFeedback(context.Context, PublicFeedbackRef) (OperationRef, error)
	Reserve(context.Context, OperationRef) (Decision, AttemptRef, error)
	ConsumeAttempt(context.Context, AttemptRef) (Decision, *ProviderAuthorization, error)
	RedeemProviderCall(context.Context, ProviderAuthorization) error
	DeferAttempt(context.Context, AttemptRef) error
	CancelAttempt(context.Context, AttemptRef) error
	SettleProvider(context.Context, ProviderSettlement) error
}

Gate is the provider-authorization surface: the only way anything in this codebase is permitted to hand a message to SES.

The shape is deliberate. A caller cannot ask "am I allowed?" and then act on the answer later, because an allow is not a boolean — it is a single-use ProviderAuthorization bound to one durable attempt, which the SMTP adapter must redeem immediately before it opens a socket. That removes the entire class of bug where a decision goes stale between the check and the call: a pause, a plan downgrade, a policy change, a midnight rollover, or a competing worker all invalidate the token rather than being raced.

func NewGate added in v1.8.8

func NewGate(pool *pgxpool.Pool, secrets Secrets, source PolicySource, configPolicy RuntimePolicy) Gate

NewGate binds the module to the deployment's policy authority and returns it as the narrow provider-authorization role.

The source is a constructor argument rather than a runtime lookup because it decides where authority lives, and that must not be able to change under a decision in flight. A hosted deployment reads the audited database singleton; a self-host reads the config file it already validated at startup.

type GrandfatherResult

type GrandfatherResult struct {
	DomainsExempted int64
}

GrandfatherResult reports what the one-shot snapshot did.

type Keyring

type Keyring struct {
	// contains filtered or unexported fields
}

Keyring is the immutable HMAC keyring loaded from E2A_SENDING_FEEDBACK_HMAC_KEYS. One version signs new material; every version stays available to verify feedback that was signed before a rotation.

func LoadKeyring

func LoadKeyring(raw string) (*Keyring, error)

LoadKeyring parses and validates the keyring secret. Every failure is fatal at startup: a deployment that cannot sign feedback correlations must not come up believing it can.

func (*Keyring) ActiveVersion

func (k *Keyring) ActiveVersion() int

ActiveVersion reports which key new material is signed under.

func (*Keyring) Sign

func (k *Keyring) Sign(msg []byte) (version int, mac []byte)

Sign returns the HMAC-SHA256 of msg under the active key, with the version it used so a verifier can select the same key after a rotation.

func (*Keyring) Verify

func (k *Keyring) Verify(version int, msg, mac []byte) bool

Verify checks msg against mac under a specific version. An unknown version is a failure, never a fallback to the active key: accepting material signed by a key this process does not hold would defeat the correlation entirely.

func (*Keyring) Versions

func (k *Keyring) Versions() []int

Versions lists every loaded version in ascending order. Used by the deploy gate to prove one slot's keyring is a superset of the other's.

type Mode

type Mode string

Mode is a three-state rollout control. The three states are independent per control: a policy may run the budget in shadow while the detector is still disabled, and vice versa.

const (
	// ModeDisabled computes nothing and blocks nothing.
	ModeDisabled Mode = "disabled"
	// ModeShadow computes the decision and records it, but never denies.
	ModeShadow Mode = "shadow"
	// ModeEnforce computes the decision and acts on it.
	ModeEnforce Mode = "enforce"
)

type Module

type Module struct {
	// contains filtered or unexported fields
}

Module is the single concrete owner of sending-protection policy state. Later slices expose narrow role interfaces (Gate, FeedbackProcessor, Admin) backed by this same object; the Postgres store stays private because there is only ever one adapter.

func NewModule

func NewModule(pool *pgxpool.Pool, secrets Secrets) *Module

NewModule binds the module to a pool and the immutable trust roots parsed at startup. It performs no I/O: a server that never reads the policy (self-host on config source) must not pay for a query.

func (*Module) ActivatePolicy

func (m *Module) ActivatePolicy(ctx context.Context, req ActivationRequest) (PolicySnapshot, error)

ActivatePolicy advances the policy by exactly one generation, under a compare-and-swap on the generation the operator reviewed.

Everything happens in one transaction holding the singleton's row lock, so a concurrent activation either waits and then loses on generation, or wins and makes this one lose. There is no path that writes the policy without also writing its audit event.

func (*Module) AttestRuntime

AttestRuntime compare-and-swaps the billing attestation on both the expected revision and the canonical hash of the four fields it replaces.

Requiring both is what defeats an ABA retry: a delayed writer whose four expected fields happen to match a later state is still rejected, because the revision advanced past it. A higher revision is always stale, never a candidate for replacement.

func (*Module) CancelAttempt added in v1.8.8

func (m *Module) CancelAttempt(ctx context.Context, ref AttemptRef) error

CancelAttempt gives back both ledgers for a terminal local cancellation such as a suppression match.

func (*Module) ConsumeAttempt added in v1.8.8

func (m *Module) ConsumeAttempt(ctx context.Context, ref AttemptRef) (Decision, *ProviderAuthorization, error)

ConsumeAttempt is the final pre-I/O authorization: the one place where the account's live state, the current policy generation, today's UTC date, and every applicable pool are checked together under lock.

It is a full re-evaluation, not a confirmation of what Reserve decided. Everything Reserve saw may have changed: the policy can have been activated, the plan downgraded, the account paused, the day rolled over, a control armed or disarmed. Re-deriving from scratch — including releasing units Reserve took on scopes that no longer apply — is what makes a policy change between the two calls impossible to slip past.

func (*Module) DeferAttempt added in v1.8.8

func (m *Module) DeferAttempt(ctx context.Context, ref AttemptRef) error

DeferAttempt gives back only the sending budget for a rate deferral.

The ramp reservation is deliberately retained: a message deferred by the per-agent rate limiter has not been rejected by the provider and has not used a ramp day, so releasing its ramp claim would let the same message re-qualify a stage it already qualified.

func (*Module) InspectAttestation

func (m *Module) InspectAttestation(ctx context.Context) (RuntimeAttestation, error)

InspectAttestation reads the current runtime attestation.

func (*Module) InspectPolicy

func (m *Module) InspectPolicy(ctx context.Context) (PolicySnapshot, error)

InspectPolicy reads the current policy without locking. It is the readback half of every operator command and the dry-run of an activation.

func (*Module) PrepareExternalTx added in v1.8.8

func (m *Module) PrepareExternalTx(ctx context.Context, tx pgx.Tx, messageID string) (AcceptanceDecision, OperationRef, error)

PrepareExternalTx derives the provider operation for an accepted outbound customer message, inside the same transaction that durably inserts it.

It returns an acceptance verdict rather than a budget verdict on purpose. Budgets are decided immediately before the provider call, so a customer who has used today's allowance still gets their message queued and sent after midnight; only a paused account is refused at the door, because queueing mail that can never leave is worse than saying no.

func (*Module) PrepareNotificationTx added in v1.8.8

func (m *Module) PrepareNotificationTx(ctx context.Context, tx pgx.Tx, ref NotificationRef) (OperationRef, error)

PrepareNotificationTx derives the provider operation for platform mail a customer's own action triggered.

These are attributed to and budgeted against the triggering customer, not the platform, because a customer controls how much of this mail exists: every held message is an approval email and every failing webhook is a health warning. Their From identity is platform-owned, so they also carry the shared reputation class and the stricter shared-domain cap that comes with it.

func (*Module) PrepareProtectionNoticeTx added in v1.8.8

func (m *Module) PrepareProtectionNoticeTx(ctx context.Context, tx pgx.Tx, ref ProtectionNoticeRef) (OperationRef, error)

PrepareProtectionNoticeTx allocates or resumes the one stable operation for a committed notice event and audience.

Stability is the point. A pause notice may be retried for days; every physical retry must be a greater submission ordinal on the SAME operation, so that the ledger can prove at most one socket per ordinal and so a retry can never mint a second logical notice. Deliberately no recipient is resolved here: the owner address is whatever it is at final authorization, and binding it now would mail a retired address after a legitimate account edit.

func (*Module) PreparePublicFeedback added in v1.8.8

func (m *Module) PreparePublicFeedback(ctx context.Context, ref PublicFeedbackRef) (OperationRef, error)

PreparePublicFeedback derives the operation for one /api/feedback fan-out.

The unauthenticated endpoint has no account to charge, but its mail leaves through the same provider and damages the same reputation, so it consumes the platform and probation pools. Neither the recipient set nor the purpose comes from the request: the submission ID is server-minted and the envelope is configuration, which is what stops the form from becoming an open relay.

func (*Module) RedeemProviderCall added in v1.8.8

func (m *Module) RedeemProviderCall(ctx context.Context, auth ProviderAuthorization) error

RedeemProviderCall consumes the single-use authorization immediately before the socket opens.

It exists because ConsumeAttempt's transaction has committed by the time the adapter runs, and everything it checked can have changed in the meantime. Re-proving the whole chain here — policy, owner, delivery, operation, ordinal, nonce, recipient selector — costs one short transaction and closes the window in which an owner edit, a policy rotation, a supersession, or a mixed-slot secret rotation could mail a retired address.

func (*Module) RegisterOperatorRecipients

func (m *Module) RegisterOperatorRecipients(ctx context.Context, actor, reason string) (inserted []int, err error)

RegisterOperatorRecipients inserts registry rows for versions that are not yet recorded, and refuses any disagreement with history.

The table's triggers already make UPDATE and DELETE impossible, so the only way to be wrong here is to insert a row that contradicts an existing one. That is checked explicitly and fails the whole transaction, because a partial registration would leave the operator unable to tell which versions are trustworthy.

func (*Module) Reserve added in v1.8.8

func (m *Module) Reserve(ctx context.Context, ref OperationRef) (Decision, AttemptRef, error)

Reserve allocates this operation's current durable submission attempt and, as an optimization, tries to take its capacity early.

It is explicitly not authority to submit. Its value is that a worker learns about an exhausted budget before it does the expensive work of composing and signing a message, and that the durable ordinal is allocated exactly once per provider opportunity. That ordinal allocation is the load-bearing half: after a crash, timeout, ambiguous SMTP result, or ordinary River retry, the next execution sees a confirmed row and allocates N+1 rather than reusing an ordinal that may already have reached the network.

func (*Module) SettleProvider added in v1.8.8

func (m *Module) SettleProvider(ctx context.Context, settlement ProviderSettlement) error

SettleProvider records an authoritative provider outcome.

It changes only the custom-domain ramp ledger, never the sending budget: the budget was spent when the attempt was authorized, and SES rejecting a message does not give back the reputation exposure of having asked. Idempotent by construction, because both the synchronous success branch and the delayed delivery-feedback finalizer call it for the same attempt.

type NotificationRef added in v1.8.8

type NotificationRef struct {
	// contains filtered or unexported fields
}

NotificationRef names one supported notification source row. It has no exported constructor taking a purpose: the two constructors below are the only way to make one, which is what stops a caller from labelling its own mail as operational to escape a budget.

func NewHITLNotificationRef added in v1.8.8

func NewHITLNotificationRef(messageID string) NotificationRef

NewHITLNotificationRef references a pending outbound message whose approval request is being sent. PrepareNotificationTx locks the owning agent and then the message, and requires the message to still be outbound and still awaiting review before deriving anything from it.

func NewWebhookHealthNotificationRef added in v1.8.8

func NewWebhookHealthNotificationRef(webhookID string) NotificationRef

NewWebhookHealthNotificationRef references a webhook whose health episode is being reported to its owner.

type NotificationSource added in v1.8.8

type NotificationSource string

NotificationSource is the closed set of durable rows that may produce a customer notification.

const (
	// NotificationHITLMessage is a pending message awaiting human approval.
	NotificationHITLMessage NotificationSource = "hitl_message"
	// NotificationWebhookHealth is a webhook warning/disabled episode.
	NotificationWebhookHealth NotificationSource = "webhook_health"
)

type OperationRef added in v1.8.8

type OperationRef struct {
	// contains filtered or unexported fields
}

OperationRef names one durable provider operation. Purpose, attribution, and shared-reputation class are captured here for the caller's convenience, but they are advisory: every Gate method reloads the row under lock and uses the stored values, so a forged or stale ref grants exactly no authority.

func (OperationRef) ID added in v1.8.8

func (r OperationRef) ID() string

ID exposes the opaque operation identifier for logging and for the River argument round-trip. It is not a capability.

func (OperationRef) IsZero added in v1.8.8

func (r OperationRef) IsZero() bool

IsZero reports an unset reference.

func (OperationRef) MarshalJSON added in v1.8.8

func (r OperationRef) MarshalJSON() ([]byte, error)

MarshalJSON writes the versioned wire form.

func (OperationRef) Purpose added in v1.8.8

func (r OperationRef) Purpose() Purpose

Purpose exposes the derived purpose for metrics. Advisory, as above.

func (*OperationRef) UnmarshalJSON added in v1.8.8

func (r *OperationRef) UnmarshalJSON(raw []byte) error

UnmarshalJSON reads the versioned wire form and yields a reference carrying only an ID. The absent purpose/attribution fields are what force every Gate method to reload from the database instead of trusting deserialized state.

type OperatorRecipients

type OperatorRecipients struct {
	// contains filtered or unexported fields
}

OperatorRecipients is the immutable versioned operator mailbox map loaded from E2A_SENDING_PROTECTION_OPERATOR_EMAILS.

The addresses are secret deployment configuration, not part of the account model, so this type deliberately exposes commitments freely and mailboxes narrowly: capabilities and registry rows carry only the HMAC commitment, and only the notice sender ever resolves an actual address.

func LoadOperatorRecipients

func LoadOperatorRecipients(raw string) (*OperatorRecipients, error)

LoadOperatorRecipients parses and validates the operator map, deriving the key identity and per-version commitments that the registry and the capability readback compare against.

func (*OperatorRecipients) Commitment

func (o *OperatorRecipients) Commitment(version int) (string, bool)

Commitment returns the lowercase HMAC-SHA256 binding a version to its mailbox. This is what the append-only registry stores and what the capability readback advertises.

func (*OperatorRecipients) Commitments

func (o *OperatorRecipients) Commitments() map[string]string

Commitments returns a defensive copy of every version's commitment, keyed by canonical decimal version for the capability payload.

func (*OperatorRecipients) KeyID

func (o *OperatorRecipients) KeyID() string

KeyID is the lowercase HMAC-SHA256 of the commitment key over a fixed label. It identifies the key across slots and registry rows without revealing it.

func (*OperatorRecipients) Mailbox

func (o *OperatorRecipients) Mailbox(version int) (string, bool)

Mailbox resolves the address for a version. Only the notice sender calls it; nothing else in the system needs the plaintext.

func (*OperatorRecipients) Versions

func (o *OperatorRecipients) Versions() []int

Versions lists every configured version in ascending order.

type PolicySnapshot

type PolicySnapshot struct {
	Generation    int64
	SchemaVersion int
	Policy        RuntimePolicy
	PolicySHA256  string
	ActivatedAt   time.Time
	ActivatedBy   string
}

PolicySnapshot is the stored policy plus the metadata an operator needs to express the next compare-and-swap.

type PolicySource

type PolicySource string

PolicySource selects where the runtime policy is read from. Self-hosted deployments stay on the config file; the hosted deployment selects the database so that activation is an audited CAS rather than a redeploy.

const (
	// PolicySourceConfig is the default for every deployment, including
	// self-host, and is what a binary uses before B15 flips the hosted service.
	PolicySourceConfig PolicySource = "config"
	// PolicySourceDatabase reads the singleton policy row.
	PolicySourceDatabase PolicySource = "database"
)

func SourceFromConfig

func SourceFromConfig(cfg *config.Config) (PolicySource, error)

SourceFromConfig returns the validated policy source. An absent value is the config source, matching config.Validate: a deployment that never mentions sending protection stays on the file it already has.

type ProtectionNoticeRef added in v1.8.8

type ProtectionNoticeRef struct {
	// contains filtered or unexported fields
}

ProtectionNoticeRef names one already-committed notice event and audience. The event row must exist: notices are enqueued by the transaction that detects the violation, and the drain worker only ever resumes them.

func NewProtectionNoticeRef added in v1.8.8

func NewProtectionNoticeRef(eventID string, audience Audience) ProtectionNoticeRef

NewProtectionNoticeRef references one notice event/audience pair.

type ProviderAuthorization added in v1.8.8

type ProviderAuthorization struct {
	// contains filtered or unexported fields
}

ProviderAuthorization is the single-use permission to make exactly one SES call for exactly one durable attempt. It cannot be constructed outside this package, cannot be widened, and is worthless without the durable nonce that RedeemProviderCall consumes.

func (ProviderAuthorization) Attempt added in v1.8.8

func (a ProviderAuthorization) Attempt() AttemptRef

Attempt exposes the durable attempt this token authorizes, for logging.

func (ProviderAuthorization) AuthorizedRecipients added in v1.8.8

func (a ProviderAuthorization) AuthorizedRecipients() []string

AuthorizedRecipients returns a defensive copy of the exact final envelope.

Only the protection notifier uses it: that path is the one caller that does not already know its recipient, because the address is resolved under lock at final authorization and deliberately never persisted in plaintext. Every other caller composed its own envelope and must not re-derive one here.

func (ProviderAuthorization) IsZero added in v1.8.8

func (a ProviderAuthorization) IsZero() bool

IsZero reports an unset authorization.

func (ProviderAuthorization) Purpose added in v1.8.8

func (a ProviderAuthorization) Purpose() Purpose

Purpose exposes the derived purpose, for metrics.

func (ProviderAuthorization) ValidateEnvelope added in v1.8.8

func (a ProviderAuthorization) ValidateEnvelope(recipients []string) (ProviderHeaders, error)

ValidateEnvelope proves the caller's actual envelope is the authorized one and returns the provider header values derived from the token.

THE CONTRACT: submit exactly AuthorizedRecipients(). Ordering and letter case are free — those are presentation. The COUNT is not: the envelope must carry one entry per distinct mailbox, because the adapter issues one RCPT TO per entry and the budget charged one unit per distinct mailbox.

So a caller must collapse its own To/Cc/Bcc overlap before submitting. That is a real constraint on the SMTP adapter — a reply-all naming the same mailbox in To and Cc is an ordinary message, and reassembling the raw header lists would be rejected here. Handing back AuthorizedRecipients() is not a workaround for that; it is the intended call, and it is the only envelope this token was ever priced for.

The alternative — silently deduplicating whatever arrives — is what makes an envelope of fifty case-variant spellings of one mailbox pass as "the same recipient" while SES receives fifty RCPT TO commands. That is a 50x reputation amplifier on one unit of budget, which is precisely the quantity this module exists to bound.

type ProviderHeaders added in v1.8.8

type ProviderHeaders struct {
	AttemptCorrelationID string
	TenantRequired       bool
	TenantName           string
}

ProviderHeaders is what the SMTP adapter is allowed to learn from a token. The adapter derives its provider-owned headers only from these values and never accepts them as separate parameters, which is what makes header smuggling a compile-time impossibility rather than a review question.

type ProviderSettlement added in v1.8.8

type ProviderSettlement struct {
	Attempt AttemptRef
	Outcome SettlementOutcome
}

ProviderSettlement pairs an attempt with its authoritative outcome.

type PublicFeedbackRef added in v1.8.8

type PublicFeedbackRef struct {
	// contains filtered or unexported fields
}

PublicFeedbackRef names one server-generated /api/feedback submission and the complete fixed recipient set configured for it. Request data reaches neither field: the ID is minted by the handler and the recipients come from configuration, so a submitter cannot add, replace, or redirect a recipient.

func NewPublicFeedbackRef added in v1.8.8

func NewPublicFeedbackRef(submissionID string, recipients []string) PublicFeedbackRef

NewPublicFeedbackRef references one submission and its configured envelope.

type Purpose added in v1.8.8

type Purpose string

Purpose is the closed set of reasons e2a may hand a message to SES. It is derived by a server-side constructor from a durable source row and persisted on the provider operation; no caller, River argument, or MIME header can select or change it. The values match the CHECK constraint on sending_provider_operations.purpose.

const (
	// PurposeCustomerMessage is mail a customer's agent composed.
	PurposeCustomerMessage Purpose = "customer_message"
	// PurposeCustomerNotification is platform mail a customer's own action
	// triggered — HITL approval requests and webhook-health warnings. It is
	// attributed to and budgeted against that customer precisely because a
	// customer can cause an unbounded amount of it.
	PurposeCustomerNotification Purpose = "customer_notification"
	// PurposeCriticalOperational is a pause notice. It must survive an abuse
	// wave that has exhausted every customer pool, so it draws on its own.
	PurposeCriticalOperational Purpose = "critical_operational"
	// PurposeViolationOperational is a budget-violation or global-guardrail
	// notice. Separate from critical so limit-driven mail — which an attacker
	// can provoke — cannot starve pause notices.
	PurposeViolationOperational Purpose = "violation_operational"
	// PurposePublicFeedback is the unauthenticated /api/feedback fan-out to a
	// fixed configured recipient set. It has no customer to attribute to but
	// still shares provider reputation, so it consumes the global pools.
	PurposePublicFeedback Purpose = "public_feedback_notification"
	// PurposeTrustedSystem is first-party prober/conformance traffic from
	// system and internal accounts. Unbudgeted by design: it is not
	// customer-triggerable, so its compromise is a credential incident rather
	// than an abuse-policy question.
	PurposeTrustedSystem Purpose = "trusted_system"
)

type RuntimeAttestation

type RuntimeAttestation struct {
	Revision                int64
	ActiveBillingDigest     string
	ActiveBillingContract   int
	RollbackBillingDigest   string
	RollbackBillingContract int
	UpdatedAt               time.Time
	UpdatedBy               string
}

RuntimeAttestation records which billing images the deployment has verified, so a policy activation cannot outrun the artifacts that support it.

type RuntimeAttestationRequest

type RuntimeAttestationRequest struct {
	ExpectedRevision int64
	ExpectedSHA256   string

	ActiveBillingDigest     string
	ActiveBillingContract   int
	RollbackBillingDigest   string
	RollbackBillingContract int

	Actor  string
	Reason string
}

RuntimeAttestationRequest is a CAS over both the revision and the canonical hash of the four fields being replaced.

type RuntimePolicy

type RuntimePolicy struct {
	AllCustomerGlobalDailyRecipients int              `json:"all_customer_global_daily_recipients"`
	BounceMinOutcomes                int              `json:"bounce_min_outcomes"`
	BouncePauseBasisPoints           int              `json:"bounce_pause_basis_points"`
	BudgetHoldMaxDays                int              `json:"budget_hold_max_days"`
	BudgetMode                       Mode             `json:"budget_mode"`
	ComplaintPauseBasisPoints        int              `json:"complaint_pause_basis_points"`
	CriticalOperationalDailyRecip    int              `json:"critical_operational_daily_recipients"`
	DailyUnlimitedPlanCodes          []string         `json:"daily_unlimited_plan_codes"`
	DefaultAccountDailyRecipients    int              `json:"default_account_daily_recipients"`
	DetectorIntervalSeconds          int              `json:"detector_interval_seconds"`
	DetectorMode                     Mode             `json:"detector_mode"`
	DetectorWindowDays               int              `json:"detector_window_days"`
	OperatorNoticeRecipientVersion   int              `json:"operator_notice_recipient_version"`
	ProbationGlobalDailyRecipients   int              `json:"probation_global_daily_recipients"`
	RampDays                         int              `json:"ramp_days"`
	RampEnabled                      bool             `json:"ramp_enabled"`
	RampStartDaily                   int              `json:"ramp_start_daily"`
	RampTargetDaily                  int              `json:"ramp_target_daily"`
	SendingControlAuditRetentionDays int              `json:"sending_control_audit_retention_days"`
	SendingFeedbackPostAcctRetention int              `json:"sending_feedback_post_account_retention_days"`
	SharedDomainAccountDailyRecip    int              `json:"shared_domain_account_daily_recipients"`
	SharedReputationBounceMinOutcome int              `json:"shared_reputation_bounce_min_outcomes"`
	TenantHeaderCanaryAccountIDs     []string         `json:"tenant_header_canary_account_ids"`
	TenantHeaderMode                 TenantHeaderMode `json:"tenant_header_mode"`
	TenantProvisioningMode           ToggleMode       `json:"tenant_provisioning_mode"`
	TenantSuppressionSyncMode        ToggleMode       `json:"tenant_suppression_sync_mode"`
	ViolationOperationalDailyRecip   int              `json:"violation_operational_daily_recipients"`
}

RuntimePolicy is the whole sending-protection policy as one immutable typed value. Field names and JSON keys are load-bearing: the canonical form of this struct is hashed, reviewed by a human, and then required by hash at activation, so renaming a key is a policy-breaking change.

func DisabledPolicy

func DisabledPolicy() RuntimePolicy

DisabledPolicy returns the generation-zero policy: every control off, every numeric bound at its documented default. Migration 112 seeds exactly this value, so canonicalizing it must reproduce the hash that migration recorded. The keyring and registry tests use that equality as their anchor fixture.

func FromConfig

func FromConfig(cfg *config.Config) (RuntimePolicy, error)

FromConfig assembles the typed runtime policy from the validated config the server itself would use.

The schedule fields come from the `sending_ramp` block and the rest from `sending_protection`, because sending_ramp stays the custom-domain ramp SSOT and duplicating its numbers into a second block is how the two drift apart. The result is validated here, so an operator command and the server agree on what the config means before either acts on it.

func ParsePolicy

func ParsePolicy(raw []byte) (RuntimePolicy, error)

ParsePolicy decodes and validates a stored or configured policy. Unknown fields are rejected: a payload written by a newer binary carrying a control this one does not implement must fail closed, not be silently ignored.

func (RuntimePolicy) AllControlsDisabled

func (p RuntimePolicy) AllControlsDisabled() bool

AllControlsDisabled reports whether every sending-protection control is off.

RampEnabled is deliberately not part of this: the custom-domain ramp predates sending protection and self-hosts already run it without the new secrets. What the secrets protect -- provider authorization signing and operator notices -- only activates with the controls below.

func (RuntimePolicy) Validate

func (p RuntimePolicy) Validate() error

Validate enforces every closed enum and numeric invariant. It is called on the config-parsed policy at startup and again on any policy read from the database, because a row written by a newer binary must not be trusted just because it parsed.

type Scope added in v1.8.8

type Scope string

Scope is a budget counter dimension. The values match the CHECK constraint on sending_budget_counters.scope.

const (
	ScopeGlobalAll          Scope = "global_all"
	ScopeGlobalProbation    Scope = "global_probation"
	ScopeAccountDaily       Scope = "account_daily"
	ScopeAccountSharedDaily Scope = "account_shared_daily"
	ScopeGlobalCritical     Scope = "global_critical"
	ScopeGlobalViolation    Scope = "global_violation"
)

type Secrets

type Secrets struct {
	Keyring    *Keyring
	Recipients *OperatorRecipients
}

Secrets bundles the two immutable trust roots a running server holds.

func LoadSecretsFromEnv

func LoadSecretsFromEnv(source PolicySource, policy RuntimePolicy) (Secrets, error)

LoadSecretsFromEnv loads and validates both B1a-prewired secrets.

Presence rules follow the plan exactly: a config-source deployment whose policy keeps every control disabled may omit them (self-host compatibility -- the secrets guard mechanisms that are not running). Database source, or any enabled control, requires both; and a value that is PRESENT must always be valid regardless of mode, because a malformed secret discovered at activation time is far worse than one discovered at boot. Errors are redacted by construction -- see keyring.go.

type SettlementOutcome added in v1.8.8

type SettlementOutcome string

SettlementOutcome is the closed set of authoritative provider results that move the ramp ledger. Retryable and ambiguous results are deliberately absent: they leave the reservation standing, because a message that might have been delivered must not release ramp capacity.

const (
	// SettlementProviderAccepted means SES took responsibility for the message.
	SettlementProviderAccepted SettlementOutcome = "provider_accepted"
	// SettlementProviderPermanentlyRejected means SES definitively refused it.
	SettlementProviderPermanentlyRejected SettlementOutcome = "provider_permanently_rejected"
)

type TenantHeaderMode

type TenantHeaderMode string

TenantHeaderMode controls the SES tenant header. Unlike Mode, its middle state is a canary over an explicit account list rather than a shadow computation — a header is either sent or not; there is nothing to simulate.

const (
	TenantHeaderDisabled TenantHeaderMode = "disabled"
	TenantHeaderCanary   TenantHeaderMode = "canary"
	TenantHeaderEnforce  TenantHeaderMode = "enforce"
)

type TenantMode added in v1.8.8

type TenantMode string

TenantMode is the closed tenant-header state carried by an authorization. It is resolved under the final account-control lock, so a job that was enqueued before a tenant flip still submits with the post-flip header.

const (
	// TenantModeNone means no X-SES-TENANT header. This is every deployment
	// before phase 7 and every self-host.
	TenantModeNone TenantMode = "none"
	// TenantModeRequired means the exact named tenant must be sent.
	TenantModeRequired TenantMode = "required"
)

type ToggleMode

type ToggleMode string

ToggleMode is a two-state control for operations that have no meaningful shadow: provisioning either creates tenants or it does not, and suppression sync either writes to the provider or it does not.

const (
	ToggleDisabled ToggleMode = "disabled"
	ToggleEnforce  ToggleMode = "enforce"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL