php-imap icon indicating copy to clipboard operation
php-imap copied to clipboard

nextLine() hangs (infinite loop)

Open sergiy-petrov opened this issue 3 years ago • 12 comments

Describe the bug When i try to search all folders using $client->getFolders() method the script just gets into infinite loop. I tried to debug it by myself and find out that when reading next char of the stream always returning an empty string.

This happens right after i received ' BYE Connection closed.' After closing connection i expect to receive 'logout' or something like that, but instead the script just hangs infinitely.

Used config

$cm = new ClientManager(['options' => [
            'debug' => true,
        ]]);

        $client = $cm->make([
            'host'           => 'outlook.office365.com',
            'port'           => 993,
            'encryption'     => 'ssl',
            'validate_cert'  => true,
            'protocol'       => 'imap',
            'username'       => $username,
            'password'       => $token,
            'authentication' => 'oauth',
        ]);

Code to Reproduce The troubling code section which produces the reported bug.

\Webklex\PHPIMAP\Connection\Protocols\ImapProtocol::nextLine()

    public function nextLine(): string {
        $line = "";
        while (($next_char = fread($this->stream, 1)) !== false && $next_char !== "\n") {
           dump($next_char); // i added this by myself to debug - always return empty string (screenshot below)
            $line .= $next_char; // always return empty string ''
        }
        if ($line === "") {
            throw new RuntimeException('empty response');
        }
        if ($this->debug) echo "<< ".$line."\n";
        return $line . "\n";
    }

Expected behavior

Hard to say what is expected result, but i assume this should be handled somehow.

Screenshots image

Desktop / Server (please complete the following information):

  • OS: [Ubuntu]
  • PHP: [8.0.23] (cli)
  • Version [v4.0.2]
  • Provider [Outlook]

sergiy-petrov avatar Sep 30 '22 15:09 sergiy-petrov

I would personally use stream_get_line() as it will be more efficient and just check the resulting $line===false for an empty response as a $line="" indicates an empty line (stream_get_line doesn't include the cr)

AzzaAzza69 avatar Oct 11 '22 07:10 AzzaAzza69

I have the same issue, but only from time to time. I pinpointed the infitnite loop to the while loop in the ImapProtocol::nextLine() method as @sergiy-petrov described. I'm provisionally doing a check to see if there are other problematic chars:

    public function nextLine(): string {
        $line = "";
        $beginningTime = time();
        while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["","\n"])) {
            $line .= $next_char;
            if (time() > $beginningTime + 30) {
                throw new RuntimeException('reading line for longer than 30s (last char was "'.$next_char.'"): '.$line);
            }
        }
        if ($line === "") {
            throw new RuntimeException('empty response');
        }
        if ($this->debug) echo "<< ".$line."\n";
        return $line . "\n";
    }

thin-k-design avatar Nov 04 '22 10:11 thin-k-design

@thin-k-design yeah for me it also happens from time to time

sergiy-petrov avatar Nov 04 '22 18:11 sergiy-petrov

I have the same issue, our script sometimes timeouts while reading next line. Since this occurs quite randomly for us, we added a && !feof($this->stream) to the while condition, and we'll see if that makes the trick.

laurent-rizer avatar Nov 17 '22 11:11 laurent-rizer

@laurent-rizer not sure if your solution will work, fread() and feof() should behave the same, I think. changing while (($next_char = fread($this->stream, 1)) !== false && $next_char !== "\n") { to while (($next_char = fread($this->stream, 1)) !== false && !in_array($next_char, ["","\n"])) { has worked for me. Since then I haven't had any more issues.

See my pull request

thin-k-design avatar Nov 17 '22 11:11 thin-k-design

@thin-k-design thx, I'll see if feof does change anything in the next days and if not I'll test your solution

laurent-rizer avatar Nov 17 '22 12:11 laurent-rizer

I got the next problem, I got again an endless loop, but this time over 500.000 times a PHP Warning was written to the logs: "fread(): SSL: An existing connection was forcibly closed by the remote host.".

I am wondering, wouldn't it be easier just to use fgets? Are there any downsides I am not aware of?

    public function nextLine(): string {
        $line = fgets($this->stream);
        if ($line === false) {
            throw new RuntimeException('no response');
        }
        if ($this->debug) echo "<< ".$line;
        return $line;
    }

thin-k-design avatar Nov 21 '22 11:11 thin-k-design

@thin-k-design I went for the following which partially resolves the issue:

    public function nextLine(): string {
        $line = "";
        while (($next_char = fread($this->stream, 1)) !== false && $next_char !== "\n" && !feof($this->stream)) {
            $line .= $next_char;
        }
        if (
            $line === "" &&
            ($next_char === false || feof($this->stream))
        ) {
            throw new RuntimeException('empty response');
        }
        if ($this->debug) echo "<< ".$line."\n";
        return $line . "\n";
    }

I said partially since we now from time to time get the following error: Warning: fwrite(): SSL operation failed with code 1. OpenSSL Error messages: error:1420C0CF:SSL routines:ssl_write_internal:protocol is shutdown

For us this seems to always happen after a ImapProtocol::logout() I need to dig deeper in our code, but I wonder if we don't try to re-connect after an not properly closed pre-existing connection.

laurent-rizer avatar Nov 23 '22 14:11 laurent-rizer

Concerning fgets that was the case a few versions before

laurent-rizer avatar Nov 23 '22 14:11 laurent-rizer

Concerning fgets that was the case a few versions before

Do you know what the problem was? I have changed my code to use fgets and seems to work pretty fine (in my case mostly Microsoft and Gmail mailboxes).

thin-k-design avatar Nov 23 '22 15:11 thin-k-design

I have no clue Blame (https://github.com/Webklex/php-imap/blame/master/src/Connection/Protocols/ImapProtocol.php) gives the following commit: https://github.com/Webklex/php-imap/commit/f0fcdb728ed8285789e7a3d0cca96f7f93a30ec5

Which doesn't gives more information than "IMAP Connection debugging improved"

laurent-rizer avatar Nov 24 '22 14:11 laurent-rizer

If the change was really only for debugging purposes, we should revert to fgets(). There is no advantage (as I understand it) in reading character after character for debugging. fgets() is much simpler and the while loop is buggy. @Webklex?

thin-k-design avatar Nov 24 '22 15:11 thin-k-design