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
18 changes: 17 additions & 1 deletion src/Connection/ImapTokenizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,20 @@ protected function readAtom(): Atom
$this->advance();
}

// If no value was read, we will throw an exception since
// an atom must contain at least one valid character.
if ($value === '') {
if ($char === null) {
throw new ImapStreamException('Unexpected end of stream while reading atom');
}

throw new ImapParserException(sprintf(
'Unexpected byte 0x%02X in response at buffer offset %d',
ord($char),
$this->position
));
}

if (strcasecmp($value, 'NIL') === 0) {
return new Nil($value);
}
Expand Down Expand Up @@ -437,7 +451,9 @@ protected function ensureBuffer(int $length): void
while ((strlen($this->buffer) - $this->position) < $length) {
$data = $this->stream->fgets();

if ($data === false) {
// If the stream did not return any data, we will stop
// filling the buffer until another read is attempted.
if ($data === false || $data === '') {
return;
}

Expand Down
24 changes: 17 additions & 7 deletions tests/Integration/MessagesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,21 @@ function folder(): Folder
test('first or fail', function () {
$folder = folder();

expect(fn () => $folder->messages()->firstOrFail())->toThrow(ItemNotFoundException::class);

$uid = $folder->messages()->append(
new DraftMessage(from: 'foo@example.com', text: 'hello world'),
);

expect($folder->messages()->firstOrFail()->uid())->toBe($uid);
$message = $folder->messages()->firstOrFail();

expect($message->uid())->toBe($uid);
});

test('first or fail throws exception', function () {
$folder = folder();

$folder->messages()->firstOrFail();
})->throws(ItemNotFoundException::class);

test('find', function () {
$folder = folder();

Expand All @@ -93,13 +99,17 @@ function folder(): Folder
),
);

expect($folder->messages()->findOrFail($uid))->toBeInstanceOf(Message::class);
$message = $folder->messages()->findOrFail($uid);

expect(function () use ($folder) {
$folder->messages()->findOrFail(999);
})->toThrow(ItemNotFoundException::class);
expect($message)->toBeInstanceOf(Message::class);
});

test('find or fail throws exception', function () {
$folder = folder();

$folder->messages()->findOrFail(999);
})->throws(ItemNotFoundException::class);

test('get without fetches', function () {
$folder = folder();

Expand Down
68 changes: 68 additions & 0 deletions tests/Unit/Connection/ImapTokenizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,74 @@
$tokenizer->nextToken();
})->throws(ImapParserException::class);

test('tokenizer throws exception for an unexpected byte instead of returning an empty atom', function () {
$stream = new FakeStream;
$stream->open();

$stream->feedRaw("TAG1 OK L\x80GIN completed\r\n");

$tokenizer = new ImapTokenizer($stream);

expect($tokenizer->nextToken()->value)->toBe('TAG1');
expect($tokenizer->nextToken()->value)->toBe('OK');
expect($tokenizer->nextToken()->value)->toBe('L');

$tokenizer->nextToken();
})->throws(ImapParserException::class, 'Unexpected byte 0x80 in response at buffer offset 9');

test('tokenizer throws exception for an unexpected atom delimiter', function (string $delimiter, string $hex) {
$stream = new FakeStream;
$stream->open();

$stream->feedRaw($delimiter);

$tokenizer = new ImapTokenizer($stream);

expect(fn () => $tokenizer->nextToken())->toThrow(
ImapParserException::class,
"Unexpected byte 0x{$hex} in response at buffer offset 0"
);
})->with([
'closing curly brace' => ['}', '7D'],
'closing angle bracket' => ['>', '3E'],
]);

test('tokenizer stops filling the buffer when the stream returns an empty string', function () {
$stream = new class extends FakeStream
{
public int $reads = 0;

public function fgets(): string|false
{
if ($this->reads++ > 1) {
throw new RuntimeException('The tokenizer continued reading after an empty stream read');
}

return '';
}
};

$stream->open();

$tokenizer = new ImapTokenizer($stream);

expect($tokenizer->nextToken())->toBeNull();
expect($stream->reads)->toBe(2);
});

test('tokenizer does not treat zero stream data as empty', function () {
$stream = new FakeStream;
$stream->open();

$stream->feedRaw(['0', "\r\n"]);

$tokenizer = new ImapTokenizer($stream);

$token = $tokenizer->nextToken();

expect($token->value)->toBe('0');
});

test('tokenizer throws exception for unterminated quoted string', function () {
$stream = new FakeStream;
$stream->open();
Expand Down
Loading