Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore possible untagged lines after IDLE and DONE commands #445

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"ext-libxml": "*",
"ext-zip": "*",
"ext-fileinfo": "*",
"nesbot/carbon": "^2.62.1",
"nesbot/carbon": "^2.62.1|^3.8.0",
"symfony/http-foundation": ">=2.8.0",
"illuminate/pagination": ">=5.0.0"
},
Expand Down
73 changes: 69 additions & 4 deletions src/Connection/Protocols/ImapProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,24 @@ protected function assumedNextLine(Response $response, string $start): bool {
return str_starts_with($this->nextLine($response), $start);
}

/**
* Get the next line and check if it starts with a given string
* The server can send untagged status updates starting with '*' if we are not looking for a status update,
* the untagged lines will be ignored.
*
* @param string $start
*
* @return bool
* @throws RuntimeException
*/
protected function assumedNextLineIgnoreUntagged(Response $response, string $start): bool {
do {
$line = $this->nextLine($response);
} while (!(str_starts_with($start, '*')) && $this->isUntaggedLine($line));

return str_starts_with($line, $start);
}

/**
* Get the next line and split the tag
* @param string|null $tag reference tag
Expand All @@ -154,6 +172,25 @@ protected function nextTaggedLine(Response $response, ?string &$tag): string {
return $line ?? '';
}

/**
* Get the next line and split the tag
* The server can send untagged status updates starting with '*', the untagged lines will be ignored.
*
* @param string|null $tag reference tag
*
* @return string next line
* @throws RuntimeException
*/
protected function nextTaggedLineIgnoreUntagged(Response $response, &$tag): string {
do {
$line = $this->nextLine($response);
} while ($this->isUntaggedLine($line));

list($tag, $line) = explode(' ', $line, 2);

return $line;
}

/**
* Get the next line and check if it contains a given string and split the tag
* @param Response $response
Expand All @@ -167,6 +204,32 @@ protected function assumedNextTaggedLine(Response $response, string $start, &$ta
return str_contains($this->nextTaggedLine($response, $tag), $start);
}

/**
* Get the next line and check if it contains a given string and split the tag
* @param string $start
* @param $tag
*
* @return bool
* @throws RuntimeException
*/
protected function assumedNextTaggedLineIgnoreUntagged(Response $response, string $start, &$tag): bool {
$line = $this->nextTaggedLineIgnoreUntagged($response, $tag);
return strpos($line, $start) !== false;
}

/**
* RFC3501 - 2.2.2
* Data transmitted by the server to the client and status responses
* that do not indicate command completion are prefixed with the token
* "*", and are called untagged responses.
*
* @param string $line
* @return bool
*/
protected function isUntaggedLine(string $line) : bool {
return str_starts_with($line, '* ');
}

/**
* Split a given line in values. A value is literal of any form or a list
* @param Response $response
Expand Down Expand Up @@ -625,10 +688,12 @@ public function examineFolder(string $folder = 'INBOX'): Response {
* @throws RuntimeException
*/
public function fetch(array|string $items, array|int $from, mixed $to = null, int|string $uid = IMAP::ST_UID): Response {
if (is_array($from)) {
if (is_array($from) && count($from) > 1) {
$set = implode(',', $from);
} elseif (is_array($from) && count($from) === 1) {
$set = $from[0] . ':' . $from[0];
} elseif ($to === null) {
$set = $from;
$set = $from . ':' . $from;
} elseif ($to == INF) {
$set = $from . ':*';
} else {
Expand Down Expand Up @@ -1188,7 +1253,7 @@ public function getQuotaRoot(string $quota_root = 'INBOX'): Response {
*/
public function idle() {
$response = $this->sendRequest("IDLE");
if (!$this->assumedNextLine($response, '+ ')) {
if (!$this->assumedNextLineIgnoreUntagged($response, '+ ')) {
throw new RuntimeException('idle failed');
}
}
Expand All @@ -1200,7 +1265,7 @@ public function idle() {
public function done(): bool {
$response = new Response($this->noun, $this->debug);
$this->write($response, "DONE");
if (!$this->assumedNextTaggedLine($response, 'OK', $tags)) {
if (!$this->assumedNextTaggedLineIgnoreUntagged($response, 'OK', $tags)) {
throw new RuntimeException('done failed');
}
return true;
Expand Down
13 changes: 11 additions & 2 deletions src/Folder.php
Original file line number Diff line number Diff line change
Expand Up @@ -451,8 +451,17 @@ public function idle(callable $callback, int $timeout = 300): void {
$sequence = ClientManager::get('options.sequence', IMAP::ST_MSGN);

while (true) {
// This polymorphic call is fine - Protocol::idle() will throw an exception beforehand
$line = $idle_client->getConnection()->nextLine(Response::empty());
try {
// This polymorphic call is fine - Protocol::idle() will throw an exception beforehand
$line = $idle_client->getConnection()->nextLine(Response::empty());
} catch (Exceptions\RuntimeException $e) {
if(strpos($e->getMessage(), "empty response") >= 0 && $idle_client->getConnection()->connected()) {
continue;
}
if(!str_contains($e->getMessage(), "connection closed")) {
throw $e;
}
}

if (($pos = strpos($line, "EXISTS")) !== false) {
$msgn = (int)substr($line, 2, $pos - 2);
Expand Down
4 changes: 2 additions & 2 deletions src/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,7 @@ public function getHTMLBody(): string {
*/
private function parseHeader(): void {
$sequence_id = $this->getSequenceId();
$headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence)->validatedData();
$headers = $this->client->getConnection()->headers([$sequence_id], "RFC822", $this->sequence)->setCanBeEmpty(true)->validatedData();
if (!isset($headers[$sequence_id])) {
throw new MessageHeaderFetchingException("no headers found", 0);
}
Expand Down Expand Up @@ -582,7 +582,7 @@ private function parseFlags(): void {

$sequence_id = $this->getSequenceId();
try {
$flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence)->validatedData();
$flags = $this->client->getConnection()->flags([$sequence_id], $this->sequence)->setCanBeEmpty(true)->validatedData();
} catch (Exceptions\RuntimeException $e) {
throw new MessageFlagException("flag could not be fetched", 0, $e);
}
Expand Down