package account import ( "sync" "time" ) // SendLimiter throttles confirm-code sends per recipient address: it enforces a // minimum cooldown between two sends and a cap over a rolling hour. It guards against // email bombing and protects the relay's own quota. State is in-memory (per process, // reset on restart) and keyed by the normalised recipient address, which is adequate // for the single-instance backend. Safe for concurrent use. type SendLimiter struct { mu sync.Mutex cooldown time.Duration perHour int now func() time.Time sends map[string][]time.Time } // NewSendLimiter returns a SendLimiter allowing at most one send per cooldown and at // most perHour sends over any rolling hour, to the same recipient. func NewSendLimiter(cooldown time.Duration, perHour int) *SendLimiter { return &SendLimiter{ cooldown: cooldown, perHour: perHour, now: func() time.Time { return time.Now() }, sends: make(map[string][]time.Time), } } // Allow reports whether a send to key is permitted now, recording the send when it is. // It is denied when the last send was within the cooldown or the rolling-hour cap is // already reached. func (l *SendLimiter) Allow(key string) bool { l.mu.Lock() defer l.mu.Unlock() now := l.now() cutoff := now.Add(-time.Hour) kept := l.sends[key][:0] for _, t := range l.sends[key] { if t.After(cutoff) { kept = append(kept, t) } } if n := len(kept); n > 0 && now.Sub(kept[n-1]) < l.cooldown { l.set(key, kept) return false } if len(kept) >= l.perHour { l.set(key, kept) return false } l.set(key, append(kept, now)) return true } // set stores the retained send times for key, dropping the entry entirely once empty // so the map stays bounded to recipients active within the last hour. func (l *SendLimiter) set(key string, times []time.Time) { if len(times) == 0 { delete(l.sends, key) return } l.sends[key] = times }