// Package mail provides runtime mail-delivery adapters for the auth/session // service. package mail import ( "context" "errors" "fmt" "sync" "galaxy/authsession/internal/ports" ) var errForcedFailure = errors.New("stub mail sender: forced failure") // StubMode identifies the deterministic outcome used by StubSender for one // delivery attempt. type StubMode string const ( // StubModeSent reports that the adapter accepts delivery and returns the // stable sent outcome expected by the auth flow. StubModeSent StubMode = "sent" // StubModeSuppressed reports that the adapter intentionally suppresses // outward delivery while still returning a successful suppressed outcome. StubModeSuppressed StubMode = "suppressed" // StubModeFailed reports that the adapter returns an explicit delivery // failure instead of a successful outcome. StubModeFailed StubMode = "failed" ) // IsKnown reports whether mode is one of the supported stub delivery modes. func (mode StubMode) IsKnown() bool { switch mode { case StubModeSent, StubModeSuppressed, StubModeFailed: return true default: return false } } // StubStep overrides the default stub behavior for one queued delivery // attempt. type StubStep struct { // Mode selects the delivery behavior for this queued step. Mode StubMode // Err optionally overrides the failure returned when Mode is StubModeFailed. Err error } // Validate reports whether step contains one supported queued behavior. func (step StubStep) Validate() error { if !step.Mode.IsKnown() { return fmt.Errorf("stub mail step mode %q is unsupported", step.Mode) } return nil } // Attempt records one validated delivery request handled by StubSender, // including the auth challenge-derived idempotency key. type Attempt struct { // Input stores the validated cleartext mail-delivery request exactly as it // was passed into SendLoginCode. Input ports.SendLoginCodeInput // Mode stores the resolved stub mode after queued overrides were applied. Mode StubMode } // StubSender is a deterministic runtime MailSender implementation intended // for development, local integration, and explicit stub-based tests. // // The zero value is ready to use and defaults to StubModeSent. type StubSender struct { // DefaultMode controls the fallback behavior when Script is empty. The zero // value is treated as StubModeSent so the zero-value sender is usable // without extra configuration. DefaultMode StubMode // DefaultError optionally overrides the failure returned when DefaultMode // resolves to StubModeFailed. DefaultError error // Script stores queued one-shot overrides consumed in FIFO order before the // default behavior is used. Script []StubStep mu sync.Mutex attempts []Attempt } // SendLoginCode records one validated delivery request and returns the // deterministic stub outcome selected by the queued script or the default // mode. func (s *StubSender) SendLoginCode(ctx context.Context, input ports.SendLoginCodeInput) (ports.SendLoginCodeResult, error) { if ctx == nil { return ports.SendLoginCodeResult{}, errors.New("stub mail sender: nil context") } if err := ctx.Err(); err != nil { return ports.SendLoginCodeResult{}, err } if err := input.Validate(); err != nil { return ports.SendLoginCodeResult{}, err } s.mu.Lock() defer s.mu.Unlock() mode, errOverride, err := s.resolveNextStepLocked() if err != nil { return ports.SendLoginCodeResult{}, err } s.attempts = append(s.attempts, Attempt{ Input: input, Mode: mode, }) switch mode { case StubModeSent: return ports.SendLoginCodeResult{Outcome: ports.SendLoginCodeOutcomeSent}, nil case StubModeSuppressed: return ports.SendLoginCodeResult{Outcome: ports.SendLoginCodeOutcomeSuppressed}, nil case StubModeFailed: if errOverride != nil { return ports.SendLoginCodeResult{}, errOverride } return ports.SendLoginCodeResult{}, errForcedFailure default: return ports.SendLoginCodeResult{}, fmt.Errorf("stub mail sender: unsupported resolved mode %q", mode) } } // RecordedAttempts returns a stable defensive copy of every validated delivery // attempt handled by the stub. func (s *StubSender) RecordedAttempts() []Attempt { s.mu.Lock() defer s.mu.Unlock() return append([]Attempt(nil), s.attempts...) } func (s *StubSender) resolveNextStepLocked() (StubMode, error, error) { if len(s.Script) > 0 { step := s.Script[0] s.Script = append([]StubStep(nil), s.Script[1:]...) if err := step.Validate(); err != nil { return "", nil, fmt.Errorf("stub mail sender: %w", err) } if step.Mode == StubModeFailed { if step.Err != nil { return step.Mode, step.Err, nil } return step.Mode, errForcedFailure, nil } return step.Mode, nil, nil } mode := s.DefaultMode if mode == "" { mode = StubModeSent } if !mode.IsKnown() { return "", nil, fmt.Errorf("stub mail sender: default mode %q is unsupported", mode) } if mode == StubModeFailed { if s.DefaultError != nil { return mode, s.DefaultError, nil } return mode, errForcedFailure, nil } return mode, nil, nil } var _ ports.MailSender = (*StubSender)(nil)