package smtp import ( "errors" "sync" "time" ) var ErrCircuitOpen = errors.New("smtp circuit open") // CircuitBreaker tracks consecutive SMTP failures per mail account. type CircuitBreaker struct { threshold int cooldown time.Duration mu sync.Mutex accounts map[string]*circuitState } type circuitState struct { failures int openUntil time.Time halfOpenTry bool } func NewCircuitBreaker(threshold int, cooldown time.Duration) *CircuitBreaker { if threshold < 1 { threshold = 5 } if cooldown <= 0 { cooldown = 5 * time.Minute } return &CircuitBreaker{ threshold: threshold, cooldown: cooldown, accounts: make(map[string]*circuitState), } } func (cb *CircuitBreaker) Allow(accountID string) error { cb.mu.Lock() defer cb.mu.Unlock() st := cb.state(accountID) now := time.Now() if st.openUntil.IsZero() || now.After(st.openUntil) { if !st.openUntil.IsZero() { st.halfOpenTry = true } return nil } return ErrCircuitOpen } func (cb *CircuitBreaker) RecordSuccess(accountID string) { cb.mu.Lock() defer cb.mu.Unlock() st := cb.state(accountID) st.failures = 0 st.openUntil = time.Time{} st.halfOpenTry = false } func (cb *CircuitBreaker) RecordFailure(accountID string) { cb.mu.Lock() defer cb.mu.Unlock() st := cb.state(accountID) if st.halfOpenTry { st.halfOpenTry = false st.failures = cb.threshold st.openUntil = time.Now().Add(cb.cooldown) return } st.failures++ if st.failures >= cb.threshold { st.openUntil = time.Now().Add(cb.cooldown) } } func (cb *CircuitBreaker) state(accountID string) *circuitState { st, ok := cb.accounts[accountID] if !ok { st = &circuitState{} cb.accounts[accountID] = st } return st }