Bugfix: SieveClient quoted string parsing

Redesign NetClient::getNextBuffer() to return string
This commit is contained in:
the-djmaze 2022-12-19 10:39:50 +01:00
parent 2a86ab5753
commit 8b65b6ee7c
4 changed files with 94 additions and 152 deletions

View file

@ -87,39 +87,31 @@ trait ResponseParser
$sAtomBuilder = $bTreatAsAtom ? '' : null;
$aList = array();
if ($bRoot)
{
if ($bRoot) {
$aList =& $oImapResponse->ResponseList;
}
while (true)
{
if ($this->bNeedNext)
{
/**
* $this->sResponseBuffer is a single fgets() that ends with \r\n
*/
$sResponseBuffer = '';
while (true) {
if ($this->bNeedNext) {
$iPos = 0;
$this->getNextBuffer();
$sResponseBuffer = $this->getNextBuffer();
$this->iResponseBufParsedPos = $iPos;
$this->bNeedNext = false;
}
$sChar = null;
if ($bIsGotoDefault)
{
if ($bIsGotoDefault) {
$bIsGotoDefault = false;
}
else
{
$iBufferEndIndex = \strlen($this->sResponseBuffer) - 3;
} else {
$iBufferEndIndex = \strlen($sResponseBuffer) - 3;
if ($iPos > $iBufferEndIndex)
{
if ($iPos > $iBufferEndIndex) {
break;
}
$sChar = $this->sResponseBuffer[$iPos];
$sChar = $sResponseBuffer[$iPos];
}
switch ($sChar)
@ -167,13 +159,13 @@ trait ResponseParser
continue 2;
case '~': // literal8
if ('{' !== $this->sResponseBuffer[++$iPos]) {
if ('{' !== $sResponseBuffer[++$iPos]) {
break;
}
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);
$iLength = \strspn($sResponseBuffer, '0123456789', $iPos + 1);
if ($iLength && "}\r\n" === \substr($sResponseBuffer, $iPos + 1 + $iLength, 3)) {
$iLiteralLen = (int) \substr($sResponseBuffer, $iPos + 1, $iLength);
$iPos += 4 + $iLength;
if ($this->partialResponseLiteralCallbacks($sParentToken, $sPreviousAtomUpperCase, $iLiteralLen)) {
@ -217,13 +209,13 @@ trait ResponseParser
$iPos = $iBufferEndIndex;
break;
}
$iLength = \strcspn($this->sResponseBuffer, "\r\n\\\"", $iOffset);
$sSpecial = $this->sResponseBuffer[$iOffset + $iLength];
$iLength = \strcspn($sResponseBuffer, "\r\n\\\"", $iOffset);
$sSpecial = $sResponseBuffer[$iOffset + $iLength];
switch ($sSpecial)
{
case '\\':
// Is escaped character \ or "?
if (!\in_array($this->sResponseBuffer[$iOffset + $iLength + 1], ['\\','"'])) {
if (!\in_array($sResponseBuffer[$iOffset + $iLength + 1], ['\\','"'])) {
// No, not allowed in quoted string
break 2;
}
@ -232,9 +224,9 @@ trait ResponseParser
case '"':
if ($bTreatAsAtom) {
$sAtomBuilder .= \stripslashes(\substr($this->sResponseBuffer, $iPos, $iOffset + $iLength - $iPos + 1));
$sAtomBuilder .= \stripslashes(\substr($sResponseBuffer, $iPos, $iOffset + $iLength - $iPos + 1));
} else {
$aList[] = \stripslashes(\substr($this->sResponseBuffer, $iPos + 1, $iOffset + $iLength - $iPos - 1));
$aList[] = \stripslashes(\substr($sResponseBuffer, $iPos + 1, $iOffset + $iLength - $iPos - 1));
}
$iPos = $iOffset + $iLength + 1;
break 2;
@ -242,7 +234,7 @@ trait ResponseParser
default:
case "\r":
case "\n":
\SnappyMail\Log::notice('IMAP', 'Invalid char in quoted string: "' . \substr($this->sResponseBuffer, $iPos, $iOffset + $iLength - $iPos) . '"');
\SnappyMail\Log::notice('IMAP', 'Invalid char in quoted string: "' . \substr($sResponseBuffer, $iPos, $iOffset + $iLength - $iPos) . '"');
// Not allowed in quoted string
break 2;
}
@ -253,29 +245,26 @@ trait ResponseParser
default:
$iCharBlockStartPos = $iPos;
if ($bRoot && $oImapResponse->IsStatusResponse)
{
if ($bRoot && $oImapResponse->IsStatusResponse) {
$iPos = $iBufferEndIndex;
if ($iPos > $iCharBlockStartPos) {
$iCharBlockStartPos += \strspn($this->sResponseBuffer, ' ', $iCharBlockStartPos, $iPos - $iCharBlockStartPos);
$iCharBlockStartPos += \strspn($sResponseBuffer, ' ', $iCharBlockStartPos, $iPos - $iCharBlockStartPos);
}
}
while ($iPos <= $iBufferEndIndex)
{
$sCharDef = $this->sResponseBuffer[$iPos];
while ($iPos <= $iBufferEndIndex) {
$sCharDef = $sResponseBuffer[$iPos];
switch (true)
{
case $bRoot && ('[' === $sCharDef || ']' === $sCharDef) && static::skipSquareBracketParse($oImapResponse):
++$iPos;
break;
case '[' === $sCharDef:
if (null === $sAtomBuilder)
{
if (null === $sAtomBuilder) {
$sAtomBuilder = '';
}
$sAtomBuilder .= \substr($this->sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos + 1);
$sAtomBuilder .= \substr($sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos + 1);
$this->iResponseBufParsedPos = ++$iPos;
@ -301,33 +290,25 @@ trait ResponseParser
}
}
if ($iPos > $iCharBlockStartPos || null !== $sAtomBuilder)
{
$sLastCharBlock = \substr($this->sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos);
if (null === $sAtomBuilder)
{
if ($iPos > $iCharBlockStartPos || null !== $sAtomBuilder) {
$sLastCharBlock = \substr($sResponseBuffer, $iCharBlockStartPos, $iPos - $iCharBlockStartPos);
if (null === $sAtomBuilder) {
$aList[] = 'NIL' === $sLastCharBlock ? null : $sLastCharBlock;
$sPreviousAtomUpperCase = \strtoupper($sLastCharBlock);
}
else
{
} else {
$sAtomBuilder .= $sLastCharBlock;
if (!$bTreatAsAtom)
{
if (!$bTreatAsAtom) {
$aList[] = $sAtomBuilder;
$sPreviousAtomUpperCase = \strtoupper($sAtomBuilder);
$sAtomBuilder = null;
}
}
if ($bRoot)
{
if (!isset($oImapResponse->Tag) && 1 === \count($aList))
{
if ($bRoot) {
if (!isset($oImapResponse->Tag) && 1 === \count($aList)) {
$oImapResponse->setTag($aList[0]);
if ($this->getCurrentTag() === $oImapResponse->Tag)
{
if ($this->getCurrentTag() === $oImapResponse->Tag) {
$oImapResponse->ResponseType = ResponseType::TAGGED;
}
}
@ -372,8 +353,7 @@ trait ResponseParser
$iLiteralSize = \strlen($sLiteral);
if ($iLiteralLen !== $iLiteralSize) {
$this->writeLog('Literal stream read warning "read '.$iLiteralSize.' of '.
$iLiteralLen.'" bytes', \LOG_WARNING);
$this->writeLog('Literal stream read warning "read '.$iLiteralSize.' of '.$iLiteralLen.'" bytes', \LOG_WARNING);
}
return $sLiteral;
}

View file

@ -28,8 +28,6 @@ abstract class NetClient
private string $sConnectedHost = '';
protected string $sResponseBuffer = '';
private bool $ssl = false;
private int $iConnectTimeOut = 10;
@ -260,25 +258,25 @@ abstract class NetClient
* @throws \MailSo\Net\Exceptions\SocketConnectionDoesNotAvailableException
* @throws \MailSo\Net\Exceptions\SocketReadException
*/
protected function getNextBuffer(?int $iReadLen = null) : void
protected function getNextBuffer(?int $iReadLen = null) : ?string
{
if (null === $iReadLen) {
$this->sResponseBuffer = \fgets($this->rConnect);
$sResponseBuffer = \fgets($this->rConnect);
} else {
$this->sResponseBuffer = '';
$sResponseBuffer = '';
$iRead = $iReadLen;
while (0 < $iRead) {
$sAddRead = \fread($this->rConnect, $iRead);
if (false === $sAddRead) {
$this->sResponseBuffer = false;
$sResponseBuffer = false;
break;
}
$this->sResponseBuffer .= $sAddRead;
$sResponseBuffer .= $sAddRead;
$iRead -= \strlen($sAddRead);
}
}
if (false === $this->sResponseBuffer) {
if (false === $sResponseBuffer) {
$this->IsConnected(true);
$this->bUnreadBuffer = true;
$aSocketStatus = \stream_get_meta_data($this->rConnect);
@ -288,16 +286,19 @@ abstract class NetClient
$this->writeLog('Stream Meta: '.\print_r($aSocketStatus, true), \LOG_ERR);
$this->writeLogException(new Exceptions\SocketReadException, \LOG_ERR);
}
} else {
$iReadBytes = \strlen($this->sResponseBuffer);
// $iReadLen && $this->writeLog('Received '.$iReadBytes.'/'.$iReadLen.' bytes.');
$iLimit = 5000; // 5KB
if ($iLimit < $iReadBytes) {
$this->writeLogWithCrlf('[cutted:'.$iReadBytes.'] < '.\substr($this->sResponseBuffer, 0, $iLimit).'...');
} else {
$this->writeLogWithCrlf('< '.$this->sResponseBuffer);
}
return null;
}
$iReadBytes = \strlen($sResponseBuffer);
// $iReadLen && $this->writeLog('Received '.$iReadBytes.'/'.$iReadLen.' bytes.');
$iLimit = 5000; // 5KB
if ($iLimit < $iReadBytes) {
$this->writeLogWithCrlf('[cutted:'.$iReadBytes.'] < '.\substr($sResponseBuffer, 0, $iLimit).'...');
} else {
$this->writeLogWithCrlf('< '.$sResponseBuffer);
}
return $sResponseBuffer;
}
abstract function getLogName() : string;

View file

@ -32,21 +32,11 @@ class SieveClient extends \MailSo\Net\NetClient
return isset($this->aCapa[\strtoupper($sCapa)]);
}
public function IsModuleSupported(string $sModule) : bool
{
return $this->hasCapability('SIEVE') && \in_array(\strtolower(\trim($sModule)), $this->aModules);
}
public function Modules() : array
{
return $this->aModules;
}
public function IsAuthSupported(string $sAuth) : bool
{
return $this->hasCapability('SASL') && \in_array(\strtoupper($sAuth), $this->aAuth);
}
/**
* @throws \InvalidArgumentException
* @throws \MailSo\RuntimeException
@ -58,7 +48,6 @@ class SieveClient extends \MailSo\Net\NetClient
parent::Connect($oSettings);
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$this->parseStartupResponse($aResponse);
if (ConnectionSecurityType::STARTTLS === $this->Settings->type
@ -73,7 +62,6 @@ class SieveClient extends \MailSo\Net\NetClient
$this->sendRequestWithCheck('STARTTLS');
$this->EnableCrypto();
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$this->parseStartupResponse($aResponse);
} else {
$this->writeLogException(
@ -98,10 +86,12 @@ class SieveClient extends \MailSo\Net\NetClient
}
$type = '';
foreach ($oSettings->SASLMechanisms as $sasl_type) {
if ($this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) {
$type = $sasl_type;
break;
if ($this->hasCapability('SASL')) {
foreach ($oSettings->SASLMechanisms as $sasl_type) {
if (\in_array(\strtoupper($sasl_type), $this->aAuth) && \SnappyMail\SASL::isSupported($sasl_type)) {
$type = $sasl_type;
break;
}
}
}
if (!$type) {
@ -143,7 +133,6 @@ class SieveClient extends \MailSo\Net\NetClient
}
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$this->parseStartupResponse($aResponse);
$bAuth = true;
}
@ -160,7 +149,6 @@ class SieveClient extends \MailSo\Net\NetClient
$this->sendRaw($sPassword);
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$this->parseStartupResponse($aResponse);
$bAuth = true;
}
@ -201,12 +189,11 @@ class SieveClient extends \MailSo\Net\NetClient
{
$this->sendRequest('LISTSCRIPTS');
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$aResult = array();
foreach ($aResponse as $sLine) {
$aTokens = $this->parseLine($sLine);
if ($aTokens) {
$aResult[$aTokens[0]] = 'ACTIVE' === substr($sLine, -6);
$aResult[$aTokens[0]] = 'ACTIVE' === \substr($sLine, -6);
}
}
@ -222,7 +209,6 @@ class SieveClient extends \MailSo\Net\NetClient
{
$this->sendRequest('CAPABILITY');
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$this->parseStartupResponse($aResponse);
return $this->aCapa;
@ -247,9 +233,9 @@ class SieveClient extends \MailSo\Net\NetClient
*/
public function GetScript(string $sScriptName) : string
{
$sScriptName = \addcslashes($sScriptName, '"\\');
$this->sendRequest('GETSCRIPT "'.$sScriptName.'"');
$aResponse = $this->parseResponse();
$this->validateResponse($aResponse);
$sScript = '';
if (\count($aResponse)) {
@ -274,6 +260,7 @@ class SieveClient extends \MailSo\Net\NetClient
*/
public function PutScript(string $sScriptName, string $sScriptSource) : self
{
$sScriptName = \addcslashes($sScriptName, '"\\');
$sScriptSource = \preg_replace('/\r?\n/', "\r\n", $sScriptSource);
$this->sendRequest('PUTSCRIPT "'.$sScriptName.'" {'.\strlen($sScriptSource).'+}');
$this->sendRequestWithCheck($sScriptSource);
@ -302,6 +289,7 @@ class SieveClient extends \MailSo\Net\NetClient
*/
public function SetActiveScript(string $sScriptName) : self
{
$sScriptName = \addcslashes($sScriptName, '"\\');
$this->sendRequestWithCheck('SETACTIVE "'.$sScriptName.'"');
return $this;
@ -314,6 +302,7 @@ class SieveClient extends \MailSo\Net\NetClient
*/
public function DeleteScript(string $sScriptName) : self
{
$sScriptName = \addcslashes($sScriptName, '"\\');
$this->sendRequestWithCheck('DELETESCRIPT "'.$sScriptName.'"');
return $this;
@ -326,6 +315,8 @@ class SieveClient extends \MailSo\Net\NetClient
*/
public function RenameScript(string $sOldName, string $sNewName) : self
{
$sOldName = \addcslashes($sOldName, '"\\');
$sNewName = \addcslashes($sNewName, '"\\');
$this->sendRequestWithCheck('RENAMESCRIPT "'.$sOldName.'" "'.$sNewName.'"');
return $this;
@ -333,26 +324,13 @@ class SieveClient extends \MailSo\Net\NetClient
private function parseLine(string $sLine) : ?array
{
if (false === $sLine || null === $sLine || \in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) {
return null;
}
$iStart = -1;
$iIndex = 0;
$aResult = array();
for ($iPos = 0; $iPos < \strlen($sLine); ++$iPos) {
if ('"' === $sLine[$iPos] && '\\' !== $sLine[$iPos]) {
if (-1 === $iStart) {
$iStart = $iPos;
} else {
$aResult[$iIndex++] = \substr($sLine, $iStart + 1, $iPos - $iStart - 1);
$iStart = -1;
}
if (!\in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) {
$aResult = array();
if (\preg_match_all('/(?:(?:"((?:\\\\"|[^"])*)"))/', $sLine, $aResult)) {
return \array_map('stripcslashes', $aResult[1]);
}
}
return isset($aResult[0]) ? $aResult : null;
return null;
}
/**
@ -364,10 +342,7 @@ class SieveClient extends \MailSo\Net\NetClient
{
foreach ($aResponse as $sLine) {
$aTokens = $this->parseLine($sLine);
if (false === $aTokens || !isset($aTokens[0]) ||
\in_array(\substr($sLine, 0, 2), array('OK', 'NO')))
{
if (empty($aTokens[0]) || \in_array(\substr($sLine, 0, 2), array('OK', 'NO'))) {
continue;
}
@ -411,7 +386,7 @@ class SieveClient extends \MailSo\Net\NetClient
private function sendRequestWithCheck(string $sRequest) : void
{
$this->sendRequest($sRequest);
$this->validateResponse($this->parseResponse());
$this->parseResponse();
}
private function parseResponse() : array
@ -419,22 +394,21 @@ class SieveClient extends \MailSo\Net\NetClient
$aResult = array();
do
{
$this->getNextBuffer();
$sLine = $this->sResponseBuffer;
if (false === $sLine) {
$sResponseBuffer = $this->getNextBuffer();
if (null === $sResponseBuffer) {
break;
}
$bEnd = \in_array(\substr($sLine, 0, 2), array('OK', 'NO'));
$bEnd = \in_array(\substr($sResponseBuffer, 0, 2), array('OK', 'NO'));
// convertEndOfLine
$sLine = \trim($sLine);
$sLine = \trim($sResponseBuffer);
if ('}' === \substr($sLine, -1)) {
$iPos = \strrpos($sLine, '{');
if (false !== $iPos) {
$iLen = \intval(\substr($sLine, $iPos + 1, -1));
if (0 < $iLen) {
$this->getNextBuffer($iLen);
if (\strlen($this->sResponseBuffer) === $iLen) {
$sLine = \trim(\substr_replace($sLine, $this->sResponseBuffer, $iPos));
$sResponseBuffer = $this->getNextBuffer($iLen);
if (\strlen($sResponseBuffer) === $iLen) {
$sLine = \trim(\substr_replace($sLine, $sResponseBuffer, $iPos));
}
}
}
@ -446,17 +420,11 @@ class SieveClient extends \MailSo\Net\NetClient
}
while (true);
return $aResult;
}
/**
* @throws \MailSo\Sieve\Exceptions\NegativeResponseException
*/
private function validateResponse(array $aResponse)
{
if (!$aResponse || 'OK' !== \substr($aResponse[\count($aResponse) - 1], 0, 2)) {
$this->writeLogException(new \MailSo\Sieve\Exceptions\NegativeResponseException($aResponse), \LOG_WARNING);
if (!$aResult || 'OK' !== \substr($aResult[\array_key_last($aResult)], 0, 2)) {
$this->writeLogException(new \MailSo\Sieve\Exceptions\NegativeResponseException($aResult), \LOG_WARNING);
}
return $aResult;
}
public function getLogName() : string

View file

@ -41,11 +41,6 @@ class SmtpClient extends \MailSo\Net\NetClient
return \in_array(\strtoupper($sCapa), $this->aCapa);
}
public function IsAuthSupported(string $sAuth) : bool
{
return \in_array(\strtoupper($sAuth), $this->aAuthTypes);
}
public function maxSize() : int
{
return $this->iSizeCapaValue;
@ -118,7 +113,7 @@ class SmtpClient extends \MailSo\Net\NetClient
$type = '';
foreach ($oSettings->SASLMechanisms as $sasl_type) {
if ($this->IsAuthSupported($sasl_type) && \SnappyMail\SASL::isSupported($sasl_type)) {
if (\in_array(\strtoupper($sasl_type), $this->aAuthTypes) && \SnappyMail\SASL::isSupported($sasl_type)) {
$type = $sasl_type;
break;
}
@ -486,18 +481,16 @@ class SmtpClient extends \MailSo\Net\NetClient
*/
private function validateResponse($mExpectCode, string $sErrorPrefix = '') : void
{
if (!\is_array($mExpectCode)) {
$mExpectCode = array((int) $mExpectCode);
} else {
$mExpectCode = \array_map('intval', $mExpectCode);
}
$mExpectCode = \is_array($mExpectCode)
? \array_map('intval', $mExpectCode)
: array((int) $mExpectCode);
$aParts = array('', '', '');
$this->aResults = array();
do
{
$this->getNextBuffer();
$aParts = \preg_split('/([\s\-]+)/', $this->sResponseBuffer, 2, PREG_SPLIT_DELIM_CAPTURE);
$sResponse = $this->getNextBuffer();
$aParts = \preg_split('/([\s\-]+)/', $sResponse, 2, PREG_SPLIT_DELIM_CAPTURE);
if (3 === \count($aParts) && \is_numeric($aParts[0])) {
if ('-' !== \substr($aParts[1], 0, 1) && !\in_array((int) $aParts[0], $mExpectCode)) {
@ -505,17 +498,17 @@ class SmtpClient extends \MailSo\Net\NetClient
new Exceptions\NegativeResponseException($this->aResults,
('' === $sErrorPrefix ? '' : $sErrorPrefix.': ').\trim(
(\count($this->aResults) ? \implode("\r\n", $this->aResults)."\r\n" : '').
$this->sResponseBuffer)), \LOG_ERR);
$sResponse)), \LOG_ERR);
}
} else {
$this->writeLogException(
new Exceptions\ResponseException($this->aResults,
('' === $sErrorPrefix ? '' : $sErrorPrefix.': ').\trim(
(\count($this->aResults) ? \implode("\r\n", $this->aResults)."\r\n" : '').
$this->sResponseBuffer)), \LOG_ERR);
$sResponse)), \LOG_ERR);
}
$this->aResults[] = $this->sResponseBuffer;
$this->aResults[] = $sResponse;
}
while ('-' === \substr($aParts[1], 0, 1));
}