Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 62 additions & 49 deletions src/Connection/ImapQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use BackedEnum;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use DateTimeInterface;
use DirectoryTree\ImapEngine\Enums\ImapSearchKey;
use DirectoryTree\ImapEngine\Support\Str;
Expand Down Expand Up @@ -177,23 +178,29 @@ 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)
));
}

/**
* Add a where "SINCE" clause to the query.
*/
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)
));
}

/**
* Add a where "BEFORE" clause to the query.
*/
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)
));
}

/**
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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'].'"';
}

Expand Down
15 changes: 15 additions & 0 deletions src/Connection/RawQueryValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace DirectoryTree\ImapEngine\Connection;

use Stringable;

class RawQueryValue
{
/**
* Constructor.
*/
public function __construct(
public readonly Stringable|string $value
) {}
}
3 changes: 2 additions & 1 deletion src/HasParsedMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace DirectoryTree\ImapEngine;

use Carbon\Carbon;
use Carbon\CarbonInterface;
use DirectoryTree\ImapEngine\Exceptions\RuntimeException;
use GuzzleHttp\Psr7\Utils;
use ZBateson\MailMimeParser\Header\HeaderConsts;
Expand All @@ -24,7 +25,7 @@ trait HasParsedMessage
/**
* Get the message date and time.
*/
public function date(): ?Carbon
public function date(): ?CarbonInterface
{
if ($date = $this->header(HeaderConsts::DATE)?->getDateTime()) {
return Carbon::instance($date);
Expand Down
3 changes: 2 additions & 1 deletion src/Idle.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 2 additions & 2 deletions src/MessageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down
26 changes: 26 additions & 0 deletions tests/Unit/Connection/ImapQueryBuilderTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?php

use Carbon\Carbon;
use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder;
use DirectoryTree\ImapEngine\Connection\RawQueryValue;
use DirectoryTree\ImapEngine\Enums\ImapSearchKey;

test('returns an empty string if no conditions are provided', function () {
Expand Down Expand Up @@ -194,3 +196,27 @@ function (ImapQueryBuilder $q) {

expect($builder->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');
});