package email import ( "sync" "time" ) // slidingLimiter is an in-process email send budget (per key). // Not shared across API replicas; RATE_LIMIT_REPLICAS does not divide this limiter. // Multi-replica hard caps need edge/WAF (or a future shared store). type slidingLimiter struct { mu sync.Mutex window time.Duration limit int hits map[string][]time.Time lastGC time.Time } func newSlidingLimiter(limit int, window time.Duration) *slidingLimiter { if limit <= 0 { limit = 30 } if window <= 0 { window = time.Minute } return &slidingLimiter{ window: window, limit: limit, hits: make(map[string][]time.Time), lastGC: time.Now(), } } func (l *slidingLimiter) allow(key string) bool { now := time.Now() cutoff := now.Add(-l.window) l.mu.Lock() defer l.mu.Unlock() if now.Sub(l.lastGC) > l.window { for k, ts := range l.hits { kept := ts[:0] for _, t := range ts { if t.After(cutoff) { kept = append(kept, t) } } if len(kept) == 0 { delete(l.hits, k) } else { l.hits[k] = kept } } l.lastGC = now } ts := l.hits[key] kept := ts[:0] for _, t := range ts { if t.After(cutoff) { kept = append(kept, t) } } if len(kept) >= l.limit { l.hits[key] = kept return false } l.hits[key] = append(kept, now) return true }