- 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.
89 lines
1.7 KiB
Go
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
|
|
}
|