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
64 changes: 62 additions & 2 deletions src/Connection/ImapTokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use DirectoryTree\ImapEngine\Connection\Tokens\ListClose;
use DirectoryTree\ImapEngine\Connection\Tokens\ListOpen;
use DirectoryTree\ImapEngine\Connection\Tokens\Literal;
use DirectoryTree\ImapEngine\Connection\Tokens\Number;
use DirectoryTree\ImapEngine\Connection\Tokens\QuotedString;
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeClose;
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeOpen;
Expand Down Expand Up @@ -120,8 +121,8 @@ public function nextToken(): ?Token
return $this->readLiteral();
}

// Otherwise, parse an atom.
return $this->readAtom();
// Otherwise, parse a number or atom.
return $this->readNumberOrAtom();
}

/**
Expand Down Expand Up @@ -300,6 +301,64 @@ protected function readLiteral(): Literal
return new Literal($literal);
}

/**
* Reads a number or atom token.
*/
protected function readNumberOrAtom(): Token
{
$position = $this->position;

// First char must be a digit to even consider a number.
if (! ctype_digit($this->buffer[$position] ?? '')) {
return $this->readAtom();
}

// Walk forward to find the end of the digit run.
while (ctype_digit($this->buffer[$position] ?? '')) {
$position++;

$this->ensureBuffer($position - $this->position + 1);
}

$next = $this->buffer[$position] ?? null;

// If next is EOF or a delimiter, it's a Number.
if ($next === null || $this->isDelimiter($next)) {
return $this->readNumber();
}

// Otherwise it's an Atom.
return $this->readAtom();
}

/**
* Reads a number token.
*
* A number consists of one or more digit characters and represents a numeric value.
*/
protected function readNumber(): Number
{
$start = $this->position;

while (true) {
$this->ensureBuffer(1);

$char = $this->currentChar();

if ($char === null) {
break;
}

if (! ctype_digit($char)) {
break;
}

$this->advance();
}

return new Number(substr($this->buffer, $start, $this->position - $start));
}

/**
* Reads an atom token.
*
Expand All @@ -311,6 +370,7 @@ protected function readAtom(): Atom

while (true) {
$this->ensureBuffer(1);

$char = $this->currentChar();

if ($char === null) {
Expand Down
5 changes: 5 additions & 0 deletions src/Connection/Tokens/Number.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace DirectoryTree\ImapEngine\Connection\Tokens;

class Number extends Token {}
4 changes: 2 additions & 2 deletions src/MessageQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder;
use DirectoryTree\ImapEngine\Connection\Responses\MessageResponseParser;
use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse;
use DirectoryTree\ImapEngine\Connection\Tokens\Atom;
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier;
use DirectoryTree\ImapEngine\Enums\ImapFlag;
use DirectoryTree\ImapEngine\Exceptions\ImapCommandException;
Expand Down Expand Up @@ -305,7 +305,7 @@ protected function search(): Collection
]);

return new Collection(array_map(
fn (Atom $token) => $token->value,
fn (Token $token) => $token->value,
$response->tokensAfter(2)
));
}
Expand Down
25 changes: 24 additions & 1 deletion tests/Unit/Connection/ImapTokenizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use DirectoryTree\ImapEngine\Connection\Tokens\ListClose;
use DirectoryTree\ImapEngine\Connection\Tokens\ListOpen;
use DirectoryTree\ImapEngine\Connection\Tokens\Literal;
use DirectoryTree\ImapEngine\Connection\Tokens\Number;
use DirectoryTree\ImapEngine\Connection\Tokens\QuotedString;
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeClose;
use DirectoryTree\ImapEngine\Connection\Tokens\ResponseCodeOpen;
Expand Down Expand Up @@ -257,7 +258,7 @@
expect($token->value)->toBe('UIDNEXT');

$token = $tokenizer->nextToken();
expect($token)->toBeInstanceOf(Atom::class);
expect($token)->toBeInstanceOf(Number::class);
expect($token->value)->toBe('1000');

$token = $tokenizer->nextToken();
Expand Down Expand Up @@ -334,6 +335,28 @@
],
]);

test('tokenizer handles edge cases', function (string $feed, Token ...$expectedTokens) {
$stream = new FakeStream;
$stream->open();

$stream->feed($feed);

$tokenizer = new ImapTokenizer($stream);

foreach ($expectedTokens as $expectedToken) {
$actualToken = $tokenizer->nextToken();

expect($actualToken)->toBeInstanceOf(get_class($expectedToken));
expect($actualToken->value)->toBe($expectedToken->value);
}
})->with([
['UID 48273)', new Atom('UID'), new Number('48273'), new ListClose(')')],
['* 23 EXISTS', new Atom('*'), new Number('23'), new Atom('EXISTS')],
['OK (0.002 secs)', new Atom('OK'), new ListOpen('('), new Atom('0.002'), new Atom('secs'), new ListClose(')')],
['OK 404NotFound', new Atom('OK'), new Atom('404NotFound')],
['A1 OK [UIDNEXT 1000]', new Atom('A1'), new Atom('OK'), new ResponseCodeOpen('['), new Atom('UIDNEXT'), new Number('1000'), new ResponseCodeClose(']')],
]);

test('all tokens implement the token interface', function (string $data) {
$stream = new FakeStream;
$stream->open();
Expand Down