path($owner, $ruleId, $recipient, $sender); if (!is_file($path)) { return true; } $data = json_decode((string)file_get_contents($path), true); $last = is_array($data) ? (int)($data['last_sent_at'] ?? 0) : 0; return ($this->time() - $last) >= ($repeatMinutes * 60); } public function record(string $owner, string $ruleId, string $recipient, string $sender): void { $path = $this->path($owner, $ruleId, $recipient, $sender); $dir = dirname($path); if (!is_dir($dir)) { mkdir($dir, 0700, true); } file_put_contents($path, json_encode(['last_sent_at' => $this->time()], JSON_PRETTY_PRINT), LOCK_EX); @chmod($path, 0600); } private function path(string $owner, string $ruleId, string $recipient, string $sender): string { if (!preg_match('/^[A-Za-z0-9._-]+$/', $owner)) { throw new InvalidArgumentException('Invalid owner'); } $hash = hash('sha256', implode('|', [$owner, $ruleId, strtolower($recipient), strtolower($sender)])); return rtrim($this->baseDir, '/') . '/replies/' . $owner . '/' . $hash . '.json'; } private function time(): int { return $this->now ?? time(); } }