ultisuite-backend/internal/mail/smtp/circuit.go
R3D347HR4Y 4eadb91a64 Enhance mail API with rate limiting, idempotency, and attachment management
- Added rate limiting for outbound email sends to prevent abuse, implemented in `internal/api/mail/sendguard`.
- Introduced idempotency key support for email sending to avoid duplicate submissions.
- Enhanced attachment handling with new limits and validation in `internal/api/mail/limits`.
- Updated outbox processing to include retry logic and circuit breaker for SMTP failures.
- Improved HTML sanitization for email content to enhance security.
- Added unit tests for new features, ensuring robust functionality and error handling.
- Updated configuration options in `.env.example` for new mail settings.
2026-05-22 17:19:16 +02:00

89 lines
1.7 KiB
Go

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
}