* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\MailManager\Daemon; use KTXM\MailManager\Queue\JobStatus; use KTXM\MailManager\Queue\MailQueue; use Psr\Log\LoggerInterface; /** * Mail Queue CLI * * Command-line interface for mail queue management operations. * * Usage: * php mail-queue.php list [--status=pending] * php mail-queue.php retry * php mail-queue.php retry-all --status=failed * php mail-queue.php purge --status=complete --older-than=7d * php mail-queue.php stats * * @since 2025.05.01 */ class MailQueueCli { public function __construct( private MailQueue $queue, private LoggerInterface $logger, ) {} /** * Run CLI command * * @param array $args Command line arguments * * @return int Exit code */ public function run(array $args): int { $command = $args[1] ?? 'help'; return match($command) { 'list' => $this->commandList($args), 'retry' => $this->commandRetry($args), 'retry-all' => $this->commandRetryAll($args), 'purge' => $this->commandPurge($args), 'stats' => $this->commandStats($args), 'help', '--help', '-h' => $this->commandHelp(), default => $this->commandHelp(), }; } /** * List jobs in queue */ private function commandList(array $args): int { $tenantId = $args[2] ?? null; if ($tenantId === null) { echo "Error: tenant ID required\n"; return 1; } $status = $this->parseOption($args, 'status'); $statusEnum = $status !== null ? JobStatus::tryFrom($status) : null; $limit = (int)($this->parseOption($args, 'limit') ?? 100); $jobs = $this->queue->listJobs($tenantId, $statusEnum, $limit); if (empty($jobs)) { echo "No jobs found\n"; return 0; } echo sprintf("%-36s %-12s %-8s %-20s %s\n", 'JOB ID', 'STATUS', 'ATTEMPTS', 'CREATED', 'SUBJECT'); echo str_repeat('-', 100) . "\n"; foreach ($jobs as $job) { $subject = substr($job->message->getSubject(), 0, 30); echo sprintf("%-36s %-12s %-8d %-20s %s\n", $job->id, $job->status->value, $job->attempts, $job->created?->format('Y-m-d H:i:s') ?? '-', $subject ); } return 0; } /** * Retry a specific job */ private function commandRetry(array $args): int { $jobId = $args[2] ?? null; if ($jobId === null) { echo "Error: job ID required\n"; return 1; } if ($this->queue->retry($jobId)) { echo "Job $jobId queued for retry\n"; return 0; } echo "Failed to retry job $jobId (not found or not failed)\n"; return 1; } /** * Retry all failed jobs for a tenant */ private function commandRetryAll(array $args): int { $tenantId = $args[2] ?? null; if ($tenantId === null) { echo "Error: tenant ID required\n"; return 1; } $jobs = $this->queue->listJobs($tenantId, JobStatus::Failed); $retried = 0; foreach ($jobs as $job) { if ($this->queue->retry($job->id)) { $retried++; } } echo "Retried $retried jobs\n"; return 0; } /** * Purge old jobs */ private function commandPurge(array $args): int { $tenantId = $args[2] ?? null; if ($tenantId === null) { echo "Error: tenant ID required\n"; return 1; } $status = $this->parseOption($args, 'status') ?? 'complete'; $statusEnum = JobStatus::tryFrom($status); if ($statusEnum === null) { echo "Error: invalid status '$status'\n"; return 1; } $olderThan = $this->parseOption($args, 'older-than') ?? '7d'; $seconds = $this->parseDuration($olderThan); $purged = $this->queue->purge($tenantId, $statusEnum, $seconds); echo "Purged $purged jobs\n"; return 0; } /** * Show queue statistics */ private function commandStats(array $args): int { $tenantId = $args[2] ?? null; if ($tenantId === null) { echo "Error: tenant ID required\n"; return 1; } $stats = $this->queue->stats($tenantId); echo "Queue Statistics for $tenantId:\n"; echo " Pending: {$stats['pending']}\n"; echo " Processing: {$stats['processing']}\n"; echo " Complete: {$stats['complete']}\n"; echo " Failed: {$stats['failed']}\n"; return 0; } /** * Show help message */ private function commandHelp(): int { echo << [options] Commands: list List jobs in queue --status= Filter by status (pending, processing, complete, failed) --limit= Maximum jobs to show (default: 100) retry Retry a specific failed job retry-all Retry all failed jobs for a tenant purge Purge old jobs --status= Status to purge (default: complete) --older-than= Age threshold (default: 7d, e.g., 1h, 30d) stats Show queue statistics help Show this help message HELP; return 0; } /** * Parse a command line option */ private function parseOption(array $args, string $name): ?string { foreach ($args as $arg) { if (str_starts_with($arg, "--$name=")) { return substr($arg, strlen("--$name=")); } } return null; } /** * Parse a duration string to seconds */ private function parseDuration(string $duration): int { preg_match('/^(\d+)([smhd])?$/', $duration, $matches); $value = (int)($matches[1] ?? 0); $unit = $matches[2] ?? 's'; return match($unit) { 's' => $value, 'm' => $value * 60, 'h' => $value * 3600, 'd' => $value * 86400, default => $value, }; } }