Improved IMAP response parser and bugfix to handle NIL

This commit is contained in:
djmaze 2021-11-04 15:15:17 +01:00
parent 60b0fb4b35
commit 9f3c0c499c

View file

@ -34,8 +34,10 @@ trait ResponseParser
protected function partialParseResponse() : Response protected function partialParseResponse() : Response
{ {
$this->iResponseBufParsedPos = 0;
$this->bNeedNext = true;
$oResponse = new Response; $oResponse = new Response;
$this->partialParseResponseBranch($oResponse); $this->partialParseResponseBranch($oResponse, false, '', '', true);
if (ResponseType::UNKNOWN === $oResponse->ResponseType) { if (ResponseType::UNKNOWN === $oResponse->ResponseType) {
throw new ResponseNotFoundException; throw new ResponseNotFoundException;
} }
@ -54,58 +56,51 @@ trait ResponseParser
* Status responses MAY include an OPTIONAL "response code". A response * Status responses MAY include an OPTIONAL "response code". A response
* code consists of data inside square brackets in the form of an atom, * code consists of data inside square brackets in the form of an atom,
* possibly followed by a space and arguments. * possibly followed by a space and arguments.
*
* Like:
* * OK [HIGHESTMODSEQ 11102]
* * OK [PERMANENTFLAGS (\Answered $FORWARDED $SENT $SIGNED $TODO \*)]
* TAG1 OK [READ-WRITE]
*/ */
private function skipSquareBracketParse(?Response $oImapResponse) : bool private static function skipSquareBracketParse(Response $oImapResponse) : bool
{ {
return $oImapResponse return !$oImapResponse->IsStatusResponse || 2 < \count($oImapResponse->ResponseList);
&& !($oImapResponse->IsStatusResponse && 2 < \count($oImapResponse->ResponseList));
} }
/** /**
* @return array|string * @return array|string
* @throws \MailSo\Net\Exceptions\Exception * @throws \MailSo\Net\Exceptions\Exception
*/ */
private function partialParseResponseBranch(?Response $oImapResponse, private function partialParseResponseBranch(Response $oImapResponse,
bool $bTreatAsAtom = false, string $sParentToken = '', string $sOpenBracket = '') bool $bTreatAsAtom,
string $sParentToken,
string $sOpenBracket,
bool $bRoot = false)
{ {
if ($oImapResponse) {
$this->iResponseBufParsedPos = 0;
$this->bNeedNext = true;
}
$iPos = $this->iResponseBufParsedPos; $iPos = $this->iResponseBufParsedPos;
$sPreviousAtomUpperCase = null; $sPreviousAtomUpperCase = null;
$sClosingBracket = ')';
$iLiteralLen = 0;
$iBufferEndIndex = 0; $iBufferEndIndex = 0;
$iDebugCount = 0;
$bIsGotoDefault = false; $bIsGotoDefault = false;
$bIsGotoLiteral = false;
$bIsGotoLiteralEnd = false;
$bIsGotoAtomBracket = false;
$bIsGotoNotAtomBracket = false;
$bCountOneInited = false; $bCountOneInited = false;
$bCountTwoInited = false; $bCountTwoInited = false;
$sAtomBuilder = $bTreatAsAtom ? '' : null; $sAtomBuilder = $bTreatAsAtom ? '' : null;
$aList = array(); $aList = array();
if ($oImapResponse) if ($bRoot)
{ {
$aList =& $oImapResponse->ResponseList; $aList =& $oImapResponse->ResponseList;
} }
while (true) while (true)
{ {
if (100000 === ++$iDebugCount)
{
$this->Logger()->Write('PartialParseOver: '.$iDebugCount, \MailSo\Log\Enumerations\Type::ERROR);
}
if ($this->bNeedNext) if ($this->bNeedNext)
{ {
/**
* $this->sResponseBuffer is a single fgets() that ends with \r\n
*/
$iPos = 0; $iPos = 0;
$this->getNextBuffer(); $this->getNextBuffer();
$this->iResponseBufParsedPos = $iPos; $this->iResponseBufParsedPos = $iPos;
@ -115,118 +110,8 @@ trait ResponseParser
$sChar = null; $sChar = null;
if ($bIsGotoDefault) if ($bIsGotoDefault)
{ {
$sChar = 'GOTO_DEFAULT';
$bIsGotoDefault = false; $bIsGotoDefault = false;
} }
else if ($bIsGotoLiteral)
{
$bIsGotoLiteral = false;
$bIsGotoLiteralEnd = true;
if ($this->partialResponseLiteralCallbackCallable(
$sParentToken, null === $sPreviousAtomUpperCase ? '' : \strtoupper($sPreviousAtomUpperCase), $iLiteralLen))
{
if (!$bTreatAsAtom)
{
$aList[] = '';
}
}
else
{
$sLiteral = '';
$iRead = $iLiteralLen;
while (0 < $iRead)
{
$sAddRead = \fread($this->ConnectionResource(), $iRead);
if (false === $sAddRead)
{
$sLiteral = false;
break;
}
$sLiteral .= $sAddRead;
$iRead -= \strlen($sAddRead);
\MailSo\Base\Utils::ResetTimeLimit();
}
if (false !== $sLiteral)
{
$iLiteralSize = \strlen($sLiteral);
if ($iLiteralLen !== $iLiteralSize)
{
$this->writeLog('Literal stream read warning "read '.$iLiteralSize.' of '.
$iLiteralLen.'" bytes', \MailSo\Log\Enumerations\Type::WARNING);
}
if (!$bTreatAsAtom)
{
$aList[] = $sLiteral;
if (\MailSo\Config::$LogSimpleLiterals)
{
$this->writeLog('{'.\strlen($sLiteral).'} '.$sLiteral, \MailSo\Log\Enumerations\Type::INFO);
}
}
}
else
{
$this->writeLog('Can\'t read imap stream', \MailSo\Log\Enumerations\Type::NOTE);
}
unset($sLiteral);
}
continue;
}
else if ($bIsGotoLiteralEnd)
{
$sPreviousAtomUpperCase = null;
$this->bNeedNext = true;
$bIsGotoLiteralEnd = false;
continue;
}
else if ($bIsGotoAtomBracket)
{
if ($bTreatAsAtom)
{
$sAtomBlock = $this->partialParseResponseBranch(null, true,
null === $sPreviousAtomUpperCase ? '' : \strtoupper($sPreviousAtomUpperCase),
$sOpenBracket);
$sAtomBuilder .= $sAtomBlock;
$iPos = $this->iResponseBufParsedPos;
$sAtomBuilder .= $sClosingBracket;
}
$sPreviousAtomUpperCase = null;
$bIsGotoAtomBracket = false;
continue;
}
else if ($bIsGotoNotAtomBracket)
{
$aSubItems = $this->partialParseResponseBranch(null, false,
null === $sPreviousAtomUpperCase ? '' : \strtoupper($sPreviousAtomUpperCase),
$sOpenBracket);
$aList[] = $aSubItems;
$iPos = $this->iResponseBufParsedPos;
$sPreviousAtomUpperCase = null;
if ($oImapResponse && $oImapResponse->IsStatusResponse)
{
$oImapResponse->OptionalResponse = $aSubItems;
$bIsGotoDefault = true;
$bIsGotoNotAtomBracket = false;
continue;
}
$bIsGotoNotAtomBracket = false;
continue;
}
else else
{ {
$iBufferEndIndex = \strlen($this->sResponseBuffer) - 3; $iBufferEndIndex = \strlen($this->sResponseBuffer) - 3;
@ -239,20 +124,19 @@ trait ResponseParser
$sChar = $this->sResponseBuffer[$iPos]; $sChar = $this->sResponseBuffer[$iPos];
} }
switch (true) switch ($sChar)
{ {
case ']' === $sChar: case ']':
if ($this->skipSquareBracketParse($oImapResponse)) { if ($bRoot && static::skipSquareBracketParse($oImapResponse)) {
$bIsGotoDefault = true; $bIsGotoDefault = true;
$bIsGotoNotAtomBracket = false;
break 2; break 2;
} }
case ')' === $sChar: case ')':
++$iPos; ++$iPos;
$sPreviousAtomUpperCase = null; $sPreviousAtomUpperCase = null;
break 2; break 2;
case ' ' === $sChar: case ' ':
if ($bTreatAsAtom) if ($bTreatAsAtom)
{ {
$sAtomBuilder .= ' '; $sAtomBuilder .= ' ';
@ -260,140 +144,118 @@ trait ResponseParser
++$iPos; ++$iPos;
break; break;
case '[' === $sChar: case '[':
if ($this->skipSquareBracketParse($oImapResponse)) { if ($bRoot && static::skipSquareBracketParse($oImapResponse)) {
$bIsGotoDefault = true; $bIsGotoDefault = true;
$bIsGotoNotAtomBracket = false;
break; break;
} }
case '(' === $sChar: case '(':
$sOpenBracket = $sChar; $this->iResponseBufParsedPos = ++$iPos;
$sClosingBracket = '[' === $sChar ? ']' : ')'; $mResult = $this->partialParseResponseBranch($oImapResponse, $bTreatAsAtom,
if ($bTreatAsAtom) null === $sPreviousAtomUpperCase ? '' : \strtoupper($sPreviousAtomUpperCase),
{ $sChar);
$sAtomBuilder .= $sChar; $sPreviousAtomUpperCase = null;
$bIsGotoAtomBracket = true; $iPos = $this->iResponseBufParsedPos;
$this->iResponseBufParsedPos = ++$iPos; if ($bTreatAsAtom) {
$sAtomBuilder .= $sChar . $mResult . ('[' === $sChar ? ']' : ')');
} else {
$aList[] = $mResult;
if ($bRoot && $oImapResponse->IsStatusResponse) {
$oImapResponse->OptionalResponse = $mResult;
$bIsGotoDefault = true;
}
} }
else unset($mResult);
{ continue 2;
$bIsGotoNotAtomBracket = true;
$this->iResponseBufParsedPos = ++$iPos; case '{':
$iLength = \strspn($this->sResponseBuffer, '0123456789', $iPos + 1);
if ($iLength && "}\r\n" === \substr($this->sResponseBuffer, $iPos + 1 + $iLength, 3)) {
$iLiteralLen = (int) \substr($this->sResponseBuffer, $iPos + 1, $iLength);
$iPos += 4 + $iLength;
if ($this->partialResponseLiteralCallbackCallable(
$sParentToken, null === $sPreviousAtomUpperCase ? '' : \strtoupper($sPreviousAtomUpperCase), $iLiteralLen))
{
if (!$bTreatAsAtom) {
$aList[] = '';
}
} else {
$sLiteral = $this->partialResponseLiteral($iLiteralLen);
if (null !== $sLiteral) {
if (!$bTreatAsAtom) {
$aList[] = $sLiteral;
if (\MailSo\Config::$LogSimpleLiterals) {
$this->writeLog('{'.$iLiteralSize.'} '.$sLiteral, \MailSo\Log\Enumerations\Type::INFO);
}
} else {
\error_log('Literal treated as atom and skipped');
}
unset($sLiteral);
} else {
$this->writeLog('Can\'t read imap stream', \MailSo\Log\Enumerations\Type::NOTE);
}
}
$sPreviousAtomUpperCase = null;
$this->bNeedNext = true;
continue 2;
} else {
$iPos = $iBufferEndIndex;
$sPreviousAtomUpperCase = null;
} }
break; break;
case '{' === $sChar: /**
$bIsLiteralParsed = false; * A quoted string is a sequence of zero or more 7-bit characters,
$mLiteralEndPos = \strpos($this->sResponseBuffer, '}', $iPos); * excluding CR and LF, with double quote (<">) characters at each end.
if (false !== $mLiteralEndPos && $mLiteralEndPos > $iPos) */
{ case '"':
$sLiteralLenAsString = \substr($this->sResponseBuffer, $iPos + 1, $mLiteralEndPos - $iPos - 1); $iOffset = $iPos + 1;
if (\is_numeric($sLiteralLenAsString)) while (true) {
{ if ($iOffset > $iBufferEndIndex) {
$iLiteralLen = (int) $sLiteralLenAsString; // need more data
$bIsLiteralParsed = true; $iPos = $iBufferEndIndex;
$iPos = $mLiteralEndPos + 3;
$bIsGotoLiteral = true;
break; break;
} }
} $iLength = \strcspn($this->sResponseBuffer, "\r\n\\\"", $iOffset);
if (!$bIsLiteralParsed) $sSpecial = $this->sResponseBuffer[$iOffset + $iLength];
{ switch ($sSpecial)
$iPos = $iBufferEndIndex; {
case '\\':
// Is escaped character \ or "?
if (!\in_array($this->sResponseBuffer[$iOffset + $iLength + 1], ['\\','"'])) {
// No, not allowed in quoted string
break 2;
}
$iOffset += $iLength + 2;
break;
case '"':
if ($bTreatAsAtom) {
$sAtomBuilder .= \stripslashes(\substr($this->sResponseBuffer, $iPos, $iOffset + $iLength - $iPos + 1));
} else {
$aList[] = \stripslashes(\substr($this->sResponseBuffer, $iPos + 1, $iOffset + $iLength - $iPos - 1));
}
$iPos = $iOffset + $iLength + 1;
break 2;
default:
case "\r":
case "\n":
\error_log('Invalid char in quoted string: "' . \substr($this->sResponseBuffer, $iPos, $iOffset + $iLength - $iPos) . '"');
// Not allowed in quoted string
break 2;
}
} }
$sPreviousAtomUpperCase = null; $sPreviousAtomUpperCase = null;
break; break;
case '"' === $sChar:
$bIsQuotedParsed = false;
while (true)
{
$iClosingPos = $iPos + 1;
if ($iClosingPos > $iBufferEndIndex)
{
break;
}
while (true)
{
$iClosingPos = \strpos($this->sResponseBuffer, '"', $iClosingPos);
if (false === $iClosingPos)
{
break;
}
// TODO
$iClosingPosNext = $iClosingPos + 1;
if (
isset($this->sResponseBuffer[$iClosingPosNext]) &&
' ' !== $this->sResponseBuffer[$iClosingPosNext] &&
"\r" !== $this->sResponseBuffer[$iClosingPosNext] &&
"\n" !== $this->sResponseBuffer[$iClosingPosNext] &&
']' !== $this->sResponseBuffer[$iClosingPosNext] &&
')' !== $this->sResponseBuffer[$iClosingPosNext]
)
{
++$iClosingPos;
continue;
}
$iSlashCount = 0;
while ('\\' === $this->sResponseBuffer[$iClosingPos - $iSlashCount - 1])
{
++$iSlashCount;
}
if ($iSlashCount % 2 == 1)
{
++$iClosingPos;
continue;
}
else
{
break;
}
}
if (false === $iClosingPos)
{
break;
}
else
{
$bIsQuotedParsed = true;
if ($bTreatAsAtom)
{
$sAtomBuilder .= \strtr(
\substr($this->sResponseBuffer, $iPos, $iClosingPos - $iPos + 1),
array('\\\\' => '\\', '\\"' => '"')
);
}
else
{
$aList[] = \strtr(
\substr($this->sResponseBuffer, $iPos + 1, $iClosingPos - $iPos - 1),
array('\\\\' => '\\', '\\"' => '"')
);
}
$iPos = $iClosingPos + 1;
break;
}
}
if (!$bIsQuotedParsed)
{
$iPos = $iBufferEndIndex;
}
$sPreviousAtomUpperCase = null;
break;
case 'GOTO_DEFAULT' === $sChar:
default: default:
$iCharBlockStartPos = $iPos; $iCharBlockStartPos = $iPos;
if ($oImapResponse && $oImapResponse->IsStatusResponse) if ($bRoot && $oImapResponse->IsStatusResponse)
{ {
$iPos = $iBufferEndIndex; $iPos = $iBufferEndIndex;
@ -409,7 +271,7 @@ trait ResponseParser
$sCharDef = $this->sResponseBuffer[$iPos]; $sCharDef = $this->sResponseBuffer[$iPos];
switch (true) switch (true)
{ {
case ('[' === $sCharDef || ']' === $sCharDef) && $this->skipSquareBracketParse($oImapResponse): case $bRoot && ('[' === $sCharDef || ']' === $sCharDef) && static::skipSquareBracketParse($oImapResponse):
++$iPos; ++$iPos;
break; break;
case '[' === $sCharDef: case '[' === $sCharDef:
@ -423,7 +285,7 @@ trait ResponseParser
++$iPos; ++$iPos;
$this->iResponseBufParsedPos = $iPos; $this->iResponseBufParsedPos = $iPos;
$sListBlock = $this->partialParseResponseBranch(null, true, $sListBlock = $this->partialParseResponseBranch($oImapResponse, true,
null === $sPreviousAtomUpperCase ? '' : \strtoupper($sPreviousAtomUpperCase), null === $sPreviousAtomUpperCase ? '' : \strtoupper($sPreviousAtomUpperCase),
'['); '[');
@ -451,7 +313,7 @@ trait ResponseParser
$sLastCharBlock = \substr($this->sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos); $sLastCharBlock = \substr($this->sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos);
if (null === $sAtomBuilder) if (null === $sAtomBuilder)
{ {
$aList[] = $sLastCharBlock; $aList[] = 'NIL' === $sLastCharBlock ? null : $sLastCharBlock;
$sPreviousAtomUpperCase = $sLastCharBlock; $sPreviousAtomUpperCase = $sLastCharBlock;
} }
else else
@ -466,7 +328,7 @@ trait ResponseParser
} }
} }
if ($oImapResponse) if ($bRoot)
{ {
// if (1 === \count($aList)) // if (1 === \count($aList))
if (!$bCountOneInited && 1 === \count($aList)) if (!$bCountOneInited && 1 === \count($aList))
@ -524,12 +386,35 @@ trait ResponseParser
$this->iResponseBufParsedPos = $iPos; $this->iResponseBufParsedPos = $iPos;
if (100000 < $iDebugCount) return $bTreatAsAtom ? $sAtomBuilder : $aList;
{ }
$this->Logger()->Write('PartialParseOverResult: '.$iDebugCount, \MailSo\Log\Enumerations\Type::ERROR);
private function partialResponseLiteral($iLiteralLen) : ?string
{
$sLiteral = '';
$iRead = $iLiteralLen;
while (0 < $iRead) {
$sAddRead = \fread($this->ConnectionResource(), $iRead);
$iBLen = \strlen($sAddRead);
if (!$iBLen) {
$this->writeLog('Literal stream read warning "read '.\strlen($sLiteral).' of '.
$iLiteralLen.'" bytes', \MailSo\Log\Enumerations\Type::WARNING);
return null;
}
$sLiteral .= $sAddRead;
$iRead -= $iBLen;
if ($iRead > 16384) {
// \set_time_limit(10);
\MailSo\Base\Utils::ResetTimeLimit();
}
} }
return $bTreatAsAtom ? $sAtomBuilder : $aList; $iLiteralSize = \strlen($sLiteral);
if ($iLiteralLen !== $iLiteralSize) {
$this->writeLog('Literal stream read warning "read '.$iLiteralSize.' of '.
$iLiteralLen.'" bytes', \MailSo\Log\Enumerations\Type::WARNING);
}
return $sLiteral;
} }
private function partialResponseLiteralCallbackCallable(string $sParent, string $sLiteralAtomUpperCase, int $iLiteralLen) : bool private function partialResponseLiteralCallbackCallable(string $sParent, string $sLiteralAtomUpperCase, int $iLiteralLen) : bool