diff --git a/src/Connection/ImapQueryBuilder.php b/src/Connection/ImapQueryBuilder.php index 173be9b..fc25762 100644 --- a/src/Connection/ImapQueryBuilder.php +++ b/src/Connection/ImapQueryBuilder.php @@ -4,6 +4,7 @@ use BackedEnum; use Carbon\Carbon; +use Carbon\CarbonInterface; use DateTimeInterface; use DirectoryTree\ImapEngine\Enums\ImapSearchKey; use DirectoryTree\ImapEngine\Support\Str; @@ -177,7 +178,9 @@ public function keyword(string $value): static */ public function on(mixed $date): static { - return $this->where(ImapSearchKey::On, $this->parseDate($date)); + return $this->where(ImapSearchKey::On, new RawQueryValue( + $this->parseDate($date)->format($this->dateFormat) + )); } /** @@ -185,7 +188,9 @@ public function on(mixed $date): static */ public function since(mixed $date): static { - return $this->where(ImapSearchKey::Since, $this->parseDate($date)); + return $this->where(ImapSearchKey::Since, new RawQueryValue( + $this->parseDate($date)->format($this->dateFormat) + )); } /** @@ -193,7 +198,9 @@ public function since(mixed $date): static */ public function before(mixed $value): static { - return $this->where(ImapSearchKey::Before, $this->parseDate($value)); + return $this->where(ImapSearchKey::Before, new RawQueryValue( + $this->parseDate($value)->format($this->dateFormat) + )); } /** @@ -308,6 +315,34 @@ protected function addBasicCondition(string $boolean, mixed $column, mixed $valu ]; } + /** + * Prepare the where value, escaping it as needed. + */ + protected function prepareWhereValue(mixed $value): RawQueryValue|string|null + { + if (is_null($value)) { + return null; + } + + if ($value instanceof RawQueryValue) { + return $value; + } + + if ($value instanceof BackedEnum) { + $value = $value->value; + } + + if ($value instanceof DateTimeInterface) { + $value = Carbon::instance($value); + } + + if ($value instanceof CarbonInterface) { + $value = $value->format($this->dateFormat); + } + + return Str::escape($value); + } + /** * Add a nested condition group to the query. */ @@ -325,30 +360,15 @@ protected function addNestedCondition(string $boolean, callable $callback): void } /** - * Recursively compile the wheres array into an IMAP-compatible string. + * Attempt to parse a date string into a Carbon instance. */ - protected function compileWheres(array $wheres): string + protected function parseDate(mixed $date): CarbonInterface { - if (empty($wheres)) { - return ''; - } - - // Convert each "where" into a node for later merging. - $exprNodes = array_map(fn ($where) => ( - $this->makeExpressionNode($where) - ), $wheres); - - // Start with the first expression. - $combined = array_shift($exprNodes)['expr']; - - // Merge the rest of the expressions. - foreach ($exprNodes as $node) { - $combined = $this->mergeExpressions( - $combined, $node['expr'], $node['boolean'] - ); + if ($date instanceof CarbonInterface) { + return $date; } - return trim($combined); + return Carbon::parse($date); } /** @@ -384,39 +404,30 @@ protected function mergeExpressions(string $existing, string $next, string $bool } /** - * Prepare the where value, escaping it as needed. + * Recursively compile the wheres array into an IMAP-compatible string. */ - protected function prepareWhereValue(mixed $value): ?string + protected function compileWheres(array $wheres): string { - if (is_null($value)) { - return null; - } - - if ($value instanceof DateTimeInterface) { - $value = Carbon::instance($value); - } - - if ($value instanceof BackedEnum) { - $value = $value->value; + if (empty($wheres)) { + return ''; } - if ($value instanceof Carbon) { - $value = $value->format($this->dateFormat); - } + // Convert each "where" into a node for later merging. + $exprNodes = array_map(fn ($where) => ( + $this->makeExpressionNode($where) + ), $wheres); - return Str::escape($value); - } + // Start with the first expression. + $combined = array_shift($exprNodes)['expr']; - /** - * Attempt to parse a date string into a Carbon instance. - */ - protected function parseDate(mixed $date): Carbon - { - if ($date instanceof Carbon) { - return $date; + // Merge the rest of the expressions. + foreach ($exprNodes as $node) { + $combined = $this->mergeExpressions( + $combined, $node['expr'], $node['boolean'] + ); } - return Carbon::parse($date); + return trim($combined); } /** @@ -426,7 +437,9 @@ protected function compileBasic(array $where): string { $part = strtoupper($where['key']); - if ($where['value']) { + if ($where['value'] instanceof RawQueryValue) { + $part .= ' '.$where['value']->value; + } elseif ($where['value']) { $part .= ' "'.$where['value'].'"'; } diff --git a/src/Connection/RawQueryValue.php b/src/Connection/RawQueryValue.php new file mode 100644 index 0000000..c97e8bd --- /dev/null +++ b/src/Connection/RawQueryValue.php @@ -0,0 +1,15 @@ +header(HeaderConsts::DATE)?->getDateTime()) { return Carbon::instance($date); diff --git a/src/Idle.php b/src/Idle.php index 60593a3..1f545ab 100644 --- a/src/Idle.php +++ b/src/Idle.php @@ -3,6 +3,7 @@ namespace DirectoryTree\ImapEngine; use Carbon\Carbon; +use Carbon\CarbonInterface; use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse; use DirectoryTree\ImapEngine\Exceptions\Exception; use DirectoryTree\ImapEngine\Exceptions\ImapConnectionClosedException; @@ -152,7 +153,7 @@ protected function idle(): Generator /** * Get the next timeout as a Carbon instance. */ - protected function getNextTimeout(): Carbon + protected function getNextTimeout(): CarbonInterface { return Carbon::now()->addSeconds($this->timeout); } diff --git a/src/MessageInterface.php b/src/MessageInterface.php index b9fbba5..9fb1db0 100644 --- a/src/MessageInterface.php +++ b/src/MessageInterface.php @@ -2,7 +2,7 @@ namespace DirectoryTree\ImapEngine; -use Carbon\Carbon; +use Carbon\CarbonInterface; use Stringable; use ZBateson\MailMimeParser\Header\IHeader; use ZBateson\MailMimeParser\Message as MailMimeMessage; @@ -17,7 +17,7 @@ public function uid(): int; /** * Get the message date and time. */ - public function date(): ?Carbon; + public function date(): ?CarbonInterface; /** * Get the message's subject. diff --git a/tests/Unit/Connection/ImapQueryBuilderTest.php b/tests/Unit/Connection/ImapQueryBuilderTest.php index 862baa2..8035f30 100644 --- a/tests/Unit/Connection/ImapQueryBuilderTest.php +++ b/tests/Unit/Connection/ImapQueryBuilderTest.php @@ -1,6 +1,8 @@ toImap())->toBe('SUBJECT "Foo \\"Bar\\"Baz\\\\Zot"'); }); + +test('compiles a SINCE condition with unquoted date', function () { + $builder = new ImapQueryBuilder; + + $builder->since(Carbon::create(2024, 4, 4)); + + expect($builder->toImap())->toBe('SINCE 04-Apr-2024'); +}); + +test('compiles a SINCE condition with quoted date', function () { + $builder = new ImapQueryBuilder; + + $builder->where('since', Carbon::create(2024, 4, 4)); + + expect($builder->toImap())->toBe('SINCE "04-Apr-2024"'); +}); + +test('compiles raw value', function () { + $builder = new ImapQueryBuilder; + + $builder->where('foo', new RawQueryValue('bar')); + + expect($builder->toImap())->toBe('FOO bar'); +});