*/ private array $criteria = []; public static function create(): self { return new self(); } public function all(): self { return $this->pushExpression('ALL'); } public function answered(): self { return $this->pushExpression('ANSWERED'); } public function unanswered(): self { return $this->pushExpression('UNANSWERED'); } public function deleted(): self { return $this->pushExpression('DELETED'); } public function undeleted(): self { return $this->pushExpression('UNDELETED'); } public function draft(): self { return $this->pushExpression('DRAFT'); } public function undraft(): self { return $this->pushExpression('UNDRAFT'); } public function flagged(): self { return $this->pushExpression('FLAGGED'); } public function unflagged(): self { return $this->pushExpression('UNFLAGGED'); } public function seen(): self { return $this->pushExpression('SEEN'); } public function unseen(): self { return $this->pushExpression('UNSEEN'); } public function recent(): self { return $this->pushExpression('RECENT'); } public function old(): self { return $this->pushExpression('OLD'); } public function new(): self { return $this->pushExpression('NEW'); } public function from(string $value): self { return $this->pushKeyValue('FROM', $value); } public function to(string $value): self { return $this->pushKeyValue('TO', $value); } public function cc(string $value): self { return $this->pushKeyValue('CC', $value); } public function bcc(string $value): self { return $this->pushKeyValue('BCC', $value); } public function subject(string $value): self { return $this->pushKeyValue('SUBJECT', $value); } public function body(string $value): self { return $this->pushKeyValue('BODY', $value); } public function text(string $value): self { return $this->pushKeyValue('TEXT', $value); } public function keyword(string $value): self { return $this->pushKeyValue('KEYWORD', $value, false); } public function unkeyword(string $value): self { return $this->pushKeyValue('UNKEYWORD', $value, false); } public function before(DateTimeInterface|string $value): self { return $this->pushDate('BEFORE', $value); } public function on(DateTimeInterface|string $value): self { return $this->pushDate('ON', $value); } public function since(DateTimeInterface|string $value): self { return $this->pushDate('SINCE', $value); } public function sentBefore(DateTimeInterface|string $value): self { return $this->pushDate('SENTBEFORE', $value); } public function sentOn(DateTimeInterface|string $value): self { return $this->pushDate('SENTON', $value); } public function sentSince(DateTimeInterface|string $value): self { return $this->pushDate('SENTSINCE', $value); } public function larger(int $value): self { return $this->pushNumber('LARGER', $value); } public function smaller(int $value): self { return $this->pushNumber('SMALLER', $value); } public function uid(int|string|SequenceSet $value): self { return $this->pushExpression('UID ' . $this->formatSequenceSet($value)); } public function sequence(int|string|SequenceSet $value): self { return $this->pushExpression($this->formatSequenceSet($value)); } public function header(string $name, string $value): self { return $this->pushExpression(sprintf( 'HEADER %s %s', $this->formatString($name), $this->formatString($value), )); } public function group(SearchCriteriaBuilder|array|string $criteria): self { $expression = $this->normalizeOperand($criteria, true); if ($expression === '') { return $this; } return $this->pushExpression($expression); } public function not(SearchCriteriaBuilder|array|string $criteria): self { $expression = $this->normalizeOperand($criteria, true); if ($expression === '') { throw new InvalidArgumentException('NOT search criteria cannot be empty.'); } return $this->pushExpression('NOT ' . $expression); } public function or(SearchCriteriaBuilder|array|string $left, SearchCriteriaBuilder|array|string $right): self { $leftExpression = $this->normalizeOperand($left, true); $rightExpression = $this->normalizeOperand($right, true); if ($leftExpression === '' || $rightExpression === '') { throw new InvalidArgumentException('OR search criteria requires two non-empty operands.'); } return $this->pushExpression(sprintf('OR %s %s', $leftExpression, $rightExpression)); } public function raw(string ...$criteria): self { $normalized = []; foreach ($criteria as $criterion) { $criterion = trim($criterion); if ($criterion === '') { continue; } $normalized[] = $criterion; } if ($normalized === []) { return $this; } foreach ($normalized as $expression) { $this->pushExpression($expression); } return $this; } /** * @return list */ public function build(): array { return $this->criteria === [] ? ['ALL'] : $this->criteria; } /** * @return list */ public function toArray(): array { return $this->build(); } private function pushExpression(string $expression): self { $expression = trim($expression); if ($expression !== '') { $this->criteria[] = $expression; } return $this; } private function pushKeyValue(string $key, string $value, bool $quote = true): self { $value = trim($value); if ($value === '') { throw new InvalidArgumentException(sprintf('%s search criteria cannot be empty.', $key)); } return $this->pushExpression(sprintf( '%s %s', $key, $quote ? $this->formatString($value) : $value, )); } private function pushDate(string $key, DateTimeInterface|string $value): self { return $this->pushExpression(sprintf('%s %s', $key, $this->formatDate($value))); } private function pushNumber(string $key, int $value): self { if ($value < 0) { throw new InvalidArgumentException(sprintf('%s search criteria must not be negative.', $key)); } return $this->pushExpression(sprintf('%s %d', $key, $value)); } private function normalizeOperand(SearchCriteriaBuilder|array|string $criteria, bool $wrapComposite): string { if ($criteria instanceof self) { $expressions = $criteria->toArray(); return $this->combineExpressions($expressions, $wrapComposite); } if (is_string($criteria)) { $criteria = trim($criteria); return $criteria; } $normalized = []; foreach ($criteria as $criterion) { $criterion = trim((string) $criterion); if ($criterion === '') { continue; } $normalized[] = $criterion; } return $this->combineExpressions($normalized, $wrapComposite); } /** * @param list $expressions */ private function combineExpressions(array $expressions, bool $wrapComposite): string { $expression = implode(' ', $expressions); if ($expression === '') { return ''; } if (!$wrapComposite || count($expressions) === 1) { return $expression; } return '(' . $expression . ')'; } private function formatDate(DateTimeInterface|string $value): string { if ($value instanceof DateTimeInterface) { return $value->format('d-M-Y'); } $value = trim($value); if ($value === '') { throw new InvalidArgumentException('Search dates cannot be empty.'); } return $value; } private function formatSequenceSet(int|string|SequenceSet $value): string { return match (true) { $value instanceof SequenceSet => $value->toCommand(), is_int($value) => SequenceSet::single($value)->toCommand(), default => SequenceSet::parse($value)->toCommand(), }; } private function formatString(string $value): string { return '"' . addcslashes($value, "\\\"") . '"'; } }