/home/ivoiecob/email.hirewise-va.com/vendor/afterlogic/mailso/lib/MailSo/Smtp/SmtpClient.php
<?php
/**
 * This code is licensed under AGPLv3 license or Afterlogic Software License
 * if commercial version of the product was purchased.
 * For full statements of the licenses see LICENSE-AFTERLOGIC and LICENSE-AGPL3 files.
 */

namespace MailSo\Smtp;

/**
 * @license https://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
 * @license https://afterlogic.com/products/common-licensing Afterlogic Software License
 * @copyright Copyright (c) 2019, Afterlogic Corp.
 *
 * @category MailSo
 * @package Smtp
 */
class SmtpClient extends \MailSo\Net\NetClient
{
    /**
     * @var bool
     */
    private $bHelo;

    /**
     * @var bool
     */
    private $bRcpt;

    /**
     * @var bool
     */
    private $bMail;

    /**
     * @var bool
     */
    private $bData;

    /**
     * @var array
     */
    private $aAuthTypes;

    /**
     * @var array
     */
    private $aCapa;

    /**
     * @var int
     */
    private $iSizeCapaValue;

    /**
     * @var int
     */
    private $iRequestTime;

    /**
     * @var array
     */
    private $aResults;

    /**
     * @var bool
     */
    public $__USE_SINGLE_LINE_AUTH_PLAIN_COMMAND = false;

    /**
     * @access protected
     */
    protected function __construct()
    {
        parent::__construct();

        $this->aAuthTypes = array();

        $this->iRequestTime = 0;
        $this->iSizeCapaValue = 0;
        $this->aResults = array();
        $this->aCapa = array();

        $this->bHelo = false;
        $this->bRcpt = false;
        $this->bMail = false;
        $this->bData = false;
    }

    /**
     * @return \MailSo\Smtp\SmtpClient
     */
    public static function NewInstance()
    {
        return new self();
    }

    /**
     * @return bool
     */
    public function IsSupported($sCapa)
    {
        return in_array(strtoupper($sCapa), $this->aCapa);
    }

    /**
     * @return bool
     */
    public function IsAuthSupported($sAuth)
    {
        return in_array(strtoupper($sAuth), $this->aAuthTypes);
    }

    /**
     * @return bool
     */
    public function HasSupportedAuth()
    {
        return $this->IsAuthSupported('PLAIN') || $this->IsAuthSupported('LOGIN');
    }

    /**
     * @return string
     */
    public static function EhloHelper()
    {
        $sEhloHost = empty($_SERVER['SERVER_NAME']) ? '' : \trim($_SERVER['SERVER_NAME']);
        if (empty($sEhloHost)) {
            $sEhloHost = empty($_SERVER['HTTP_HOST']) ? '' : \trim($_SERVER['HTTP_HOST']);
        }

        if (empty($sEhloHost)) {
            $sEhloHost = \function_exists('gethostname') ? \gethostname() : 'localhost';
        }

        $sEhloHost = \trim(\preg_replace('/:\d+$/', '', \trim($sEhloHost)));

        if (\preg_match('/^\d+\.\d+\.\d+\.\d+$/', $sEhloHost)) {
            $sEhloHost = '['.$sEhloHost.']';
        }

        return empty($sEhloHost) ? 'localhost' : $sEhloHost;
    }

    /**
     * @param string $sServerName
     * @param int $iPort = 25
     * @param string $sEhloHost = '[127.0.0.1]'
     * @param int $iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT
     * @param bool $bVerifySsl = false
     *
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Base\Exceptions\InvalidArgumentException
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\ResponseException
     */
    public function Connect(
        $sServerName,
        $iPort = 25,
        $sEhloHost = '[127.0.0.1]',
        $iSecurityType = \MailSo\Net\Enumerations\ConnectionSecurityType::AUTO_DETECT,
        $bVerifySsl = false
    ) {
        $this->iRequestTime = microtime(true);

        parent::Connect($sServerName, $iPort, $iSecurityType, $bVerifySsl);
        $this->validateResponse(220);

        $this->preLoginStartTLSAndEhloProcess($sEhloHost);

        return $this;
    }

    /**
     * @param string $sLogin
     * @param string $sPassword
     *
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Base\Exceptions\InvalidArgumentException
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    public function Login($sLogin, $sPassword)
    {
        $sLogin = \MailSo\Base\Utils::IdnToAscii($sLogin);

        if ($this->IsAuthSupported('LOGIN')) {
            try {
                $this->sendRequestWithCheck('AUTH', 334, 'LOGIN');
            } catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) {
                $this->writeLogException(
                    new \MailSo\Smtp\Exceptions\LoginBadMethodException(
                        $oException->GetResponses(),
                        $oException->getMessage(),
                        0,
                        $oException
                    ),
                    \MailSo\Log\Enumerations\Type::NOTICE,
                    true
                );
            }

            try {
                $this->sendRequestWithCheck(\base64_encode($sLogin), 334, '');
                $this->sendRequestWithCheck(\base64_encode($sPassword), 235, '', true);
            } catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) {
                $this->writeLogException(
                    new \MailSo\Smtp\Exceptions\LoginBadCredentialsException(
                        $oException->GetResponses(),
                        $oException->getMessage(),
                        0,
                        $oException
                    ),
                    \MailSo\Log\Enumerations\Type::NOTICE,
                    true
                );
            }
        } elseif ($this->IsAuthSupported('PLAIN')) {
            if ($this->__USE_SINGLE_LINE_AUTH_PLAIN_COMMAND) {
                try {
                    $this->sendRequestWithCheck('AUTH', 235, 'PLAIN '.\base64_encode("\0".$sLogin."\0".$sPassword), true);
                } catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) {
                    $this->writeLogException(
                        new \MailSo\Smtp\Exceptions\LoginBadCredentialsException(
                            $oException->GetResponses(),
                            $oException->getMessage(),
                            0,
                            $oException
                        ),
                        \MailSo\Log\Enumerations\Type::NOTICE,
                        true
                    );
                }
            } else {
                try {
                    $this->sendRequestWithCheck('AUTH', 334, 'PLAIN');
                } catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) {
                    $this->writeLogException(
                        new \MailSo\Smtp\Exceptions\LoginBadMethodException(
                            $oException->GetResponses(),
                            $oException->getMessage(),
                            0,
                            $oException
                        ),
                        \MailSo\Log\Enumerations\Type::NOTICE,
                        true
                    );
                }

                try {
                    $this->sendRequestWithCheck(\base64_encode("\0".$sLogin."\0".$sPassword), 235, '', true);
                } catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) {
                    $this->writeLogException(
                        new \MailSo\Smtp\Exceptions\LoginBadCredentialsException(
                            $oException->GetResponses(),
                            $oException->getMessage(),
                            0,
                            $oException
                        ),
                        \MailSo\Log\Enumerations\Type::NOTICE,
                        true
                    );
                }
            }
        } else {
            $this->writeLogException(
                new \MailSo\Smtp\Exceptions\LoginBadMethodException(),
                \MailSo\Log\Enumerations\Type::NOTICE,
                true
            );
        }

        return $this;
    }

    /**
     * @param string $sXOAuth2Token
     *
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Base\Exceptions\InvalidArgumentException
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    public function LoginWithXOauth2($sXOAuth2Token)
    {
        if ($this->IsAuthSupported('XOAUTH2')) {
            try {
                $this->sendRequestWithCheck('AUTH', 235, 'XOAUTH2 '.trim($sXOAuth2Token));
            } catch (\MailSo\Smtp\Exceptions\NegativeResponseException $oException) {
                $this->writeLogException(
                    new \MailSo\Smtp\Exceptions\LoginBadCredentialsException(
                        $oException->GetResponses(),
                        $oException->getMessage(),
                        0,
                        $oException
                    ),
                    \MailSo\Log\Enumerations\Type::NOTICE,
                    true
                );
            }
        } else {
            $this->writeLogException(
                new \MailSo\Smtp\Exceptions\LoginBadMethodException(),
                \MailSo\Log\Enumerations\Type::NOTICE,
                true
            );
        }

        return $this;
    }

    /**
     * @param string $sFrom
     * @param string $sSizeIfSupported = ''
     *
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    public function MailFrom($sFrom, $sSizeIfSupported = '')
    {
        $sFrom = \MailSo\Base\Utils::IdnToAscii($sFrom, true);
        $sCmd = 'FROM:<'.$sFrom.'>';

        $sSizeIfSupported = (string) $sSizeIfSupported;
        if (0 < \strlen($sSizeIfSupported) && \is_numeric($sSizeIfSupported) && $this->IsSupported('SIZE')) {
            $sCmd .= ' SIZE='.$sSizeIfSupported;
        }

        $this->sendRequestWithCheck('MAIL', 250, $sCmd);

        $this->bMail = true;
        $this->bRcpt = false;
        $this->bData = false;

        return $this;
    }

    /**
     * @param string $sTo
     *
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    public function Rcpt($sTo)
    {
        if (!$this->bMail) {
            $this->writeLogException(
                new Exceptions\RuntimeException('No sender reverse path has been supplied'),
                \MailSo\Log\Enumerations\Type::ERROR,
                true
            );
        }

        $sTo = \MailSo\Base\Utils::IdnToAscii($sTo, true);
        $this->sendRequestWithCheck('RCPT', array(250, 251), 'TO:<'.$sTo.'>');

        $this->bRcpt = true;

        return $this;
    }

    /**
     * @param string $sTo
     *
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    public function MailTo($sTo)
    {
        return $this->Rcpt($sTo);
    }

    /**
     * @param string $sData
     *
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    public function Data($sData)
    {
        if (!\MailSo\Base\Validator::NotEmptyString($sData, true)) {
            throw new \MailSo\Base\Exceptions\InvalidArgumentException();
        }

        $rDataStream = \MailSo\Base\ResourceRegistry::CreateMemoryResourceFromString($sData);
        unset($sData);
        $this->DataWithStream($rDataStream);
        \MailSo\Base\ResourceRegistry::CloseMemoryResource($rDataStream);

        return $this;
    }

    /**
     * @param resource $rDataStream
     *
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Base\Exceptions\InvalidArgumentException
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    public function DataWithStream($rDataStream)
    {
        if (!\is_resource($rDataStream)) {
            throw new \MailSo\Base\Exceptions\InvalidArgumentException();
        }

        if (!$this->bRcpt) {
            $this->writeLogException(
                new Exceptions\RuntimeException('No recipient forward path has been supplied'),
                \MailSo\Log\Enumerations\Type::ERROR,
                true
            );
        }

        $this->sendRequestWithCheck('DATA', 354);

        $this->writeLog('Message data.', \MailSo\Log\Enumerations\Type::NOTE);

        $this->bRunningCallback = true;

        while (strlen($sData = \fread($rDataStream, 65536)) > 0) {
            $sLineToEnd = \fgets($rDataStream);
            if ($sLineToEnd) {
                $sData .= $sLineToEnd;
            }
            $this->sendData($sData, false);

            \MailSo\Base\Utils::ResetTimeLimit();
        }

        $this->sendRequestWithCheck('.', 250);

        $this->bRunningCallback = false;

        $this->bData = true;

        return $this;
    }

    /**
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    public function Rset()
    {
        $this->sendRequestWithCheck('RSET', array(250, 220));

        $this->bMail = false;
        $this->bRcpt = false;
        $this->bData = false;

        return $this;
    }

    /**
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    public function Vrfy($sUser)
    {
        $this->sendRequestWithCheck('VRFY', array(250, 251, 252), $sUser);

        return $this;
    }

    /**
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    public function Noop()
    {
        $this->sendRequestWithCheck('NOOP', 250);

        return $this;
    }

    /**
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    public function Logout()
    {
        if ($this->IsConnected()) {
            $this->sendRequestWithCheck('QUIT', 221);
        }

        $this->bHelo = false;
        $this->bMail = false;
        $this->bRcpt = false;
        $this->bData = false;

        return $this;
    }

    /**
     * @param string $sEhloHost
     *
     * @return void
     */
    private function preLoginStartTLSAndEhloProcess($sEhloHost)
    {
        if ($this->bHelo) {
            $this->writeLogException(
                new Exceptions\RuntimeException('Cannot issue EHLO/HELO to existing session'),
                \MailSo\Log\Enumerations\Type::ERROR,
                true
            );
        }

        $this->ehloOrHelo($sEhloHost);

        if (\MailSo\Net\Enumerations\ConnectionSecurityType::UseStartTLS(
            $this->IsSupported('STARTTLS'),
            $this->iSecurityType,
            $this->HasSupportedAuth()
        )) {
            $this->sendRequestWithCheck('STARTTLS', 220);
            $this->EnableCrypto();

            $this->ehloOrHelo($sEhloHost);
        } elseif (\MailSo\Net\Enumerations\ConnectionSecurityType::STARTTLS === $this->iSecurityType) {
            $this->writeLogException(
                new \MailSo\Net\Exceptions\SocketUnsuppoterdSecureConnectionException('STARTTLS is not supported'),
                \MailSo\Log\Enumerations\Type::ERROR,
                true
            );
        }

        $this->bHelo = true;
    }

    protected function quoteData(&$sData)
    {
        $sData = preg_replace('/^\./m', '..', $sData);
    }

    protected function sendData($sRaw, $bWriteToLog = true, $sFakeRaw = '')
    {
        if ($this->bUnreadBuffer) {
            $this->writeLogException(
                new \MailSo\Net\Exceptions\SocketUnreadBufferException(),
                \MailSo\Log\Enumerations\Type::ERROR,
                true
            );
        }

        $bFake = 0 < \strlen($sFakeRaw);

        if ($this->oLogger && $this->oLogger->IsShowSecter()) {
            $bFake = false;
        }

        $this->quoteData($sRaw);

        $mResult = @\fwrite($this->rConnect, $sRaw);
        if (false === $mResult) {
            $this->IsConnected(true);

            $this->writeLogException(
                new \MailSo\Net\Exceptions\SocketWriteException(),
                \MailSo\Log\Enumerations\Type::ERROR,
                true
            );
        } else {
            \MailSo\Base\Loader::IncStatistic('NetWrite', $mResult);

            if ($bWriteToLog) {
                $this->writeLogWithCrlf('> '.($bFake ? $sFakeRaw : $sRaw), //.' ['.$iWriteSize.']',
                    $bFake ? \MailSo\Log\Enumerations\Type::SECURE : \MailSo\Log\Enumerations\Type::INFO);
            }
        }
    }

    /**
     * @param string $sCommand
     * @param string $sAddToCommand = ''
     * @param bool $bSecureLog = false
     *
     * @return void
     *
     * @throws \MailSo\Base\Exceptions\InvalidArgumentException
     * @throws \MailSo\Net\Exceptions\Exception
     */
    private function sendRequest($sCommand, $sAddToCommand = '', $bSecureLog = false)
    {
        if (!\MailSo\Base\Validator::NotEmptyString($sCommand, true)) {
            $this->writeLogException(
                new \MailSo\Base\Exceptions\InvalidArgumentException(),
                \MailSo\Log\Enumerations\Type::ERROR,
                true
            );
        }

        $this->IsConnected(true);

        $sCommand = \trim($sCommand);
        $sRealCommand = $sCommand.(0 === \strlen($sAddToCommand) ? '' : ' '.$sAddToCommand);

        $sFakeCommand = ($bSecureLog) ? '********' : '';

        $this->iRequestTime = \microtime(true);
        $this->sendRaw($sRealCommand, true, $sFakeCommand);

        return $this;
    }

    /**
     * @param string $sCommand
     * @param int|array $mExpectCode
     * @param string $sAddToCommand = ''
     * @param bool $bSecureLog = false
     *
     * @return void
     *
     * @throws \MailSo\Base\Exceptions\InvalidArgumentException
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    private function sendRequestWithCheck($sCommand, $mExpectCode, $sAddToCommand = '', $bSecureLog = false)
    {
        $this->sendRequest($sCommand, $sAddToCommand, $bSecureLog);
        $this->validateResponse($mExpectCode);
    }

    /**
     * @param string $sHost
     *
     * @return void
     */
    private function ehloOrHelo($sHost)
    {
        try {
            $this->ehlo($sHost);
        } catch (\Exception $oException) {
            try {
                $this->helo($sHost);
            } catch (\Exception $oException) {
                throw $oException;
            }
        }

        return $this;
    }

    /**
     * @param string $sHost
     *
     * @return void
     *
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    private function ehlo($sHost)
    {
        $this->sendRequestWithCheck('EHLO', 250, $sHost);

        foreach ($this->aResults as $sLine) {
            $aMatch = array();
            if (\preg_match('/[\d]+[ \-](.+)$/', $sLine, $aMatch) && isset($aMatch[1]) && 0 < \strlen($aMatch[1])) {
                $sLine = \trim($aMatch[1]);
                $aLine = \preg_split('/[ =]/', $sLine, 2);
                if (\is_array($aLine) && 0 < \count($aLine) && !empty($aLine[0])) {
                    $sCapa = \strtoupper($aLine[0]);
                    if (('AUTH' === $sCapa || 'SIZE' === $sCapa) && !empty($aLine[1])) {
                        $sSubLine = \trim(\strtoupper($aLine[1]));
                        if (0 < \strlen($sSubLine)) {
                            if ('AUTH' === $sCapa) {
                                $this->aAuthTypes = \explode(' ', $sSubLine);
                            } elseif ('SIZE' === $sCapa && \is_numeric($sSubLine)) {
                                $this->iSizeCapaValue = (int) $sSubLine;
                            }
                        }
                    }

                    $this->aCapa[] = $sCapa;
                }
            }
        }
    }

    /**
     * @param string $sHost
     *
     * @return void
     *
     * @throws \MailSo\Net\Exceptions\Exception
     * @throws \MailSo\Smtp\Exceptions\Exception
     */
    private function helo($sHost)
    {
        $this->sendRequestWithCheck('HELO', 250, $sHost);
        $this->aAuthTypes = array();
        $this->iSizeCapaValue = 0;
        $this->aCapa = array();
    }

    /**
     * @param int|array $mExpectCode
     *
     * @return void
     *
     * @throws \MailSo\Smtp\Exceptions\ResponseException
     */
    private function validateResponse($mExpectCode)
    {
        if (!\is_array($mExpectCode)) {
            $mExpectCode = array((int) $mExpectCode);
        } else {
            $mExpectCode = \array_map('intval', $mExpectCode);
        }

        $aParts = array('', '', '');
        $this->aResults = array();
        do {
            $this->getNextBuffer();
            $aParts = \preg_split('/([\s-]+)/', $this->sResponseBuffer, 2, PREG_SPLIT_DELIM_CAPTURE);

            if (\is_array($aParts) && 3 === \count($aParts) && \is_numeric($aParts[0])) {
                if ('-' !== trim($aParts[1]) && !\in_array((int) $aParts[0], $mExpectCode)) {
                    if ((int)$aParts[0] === 550) {
                        $this->writeLogException(
                            new Exceptions\MailboxUnavailableException($this->aResults, \trim(
                                (0 < \count($this->aResults) ? \implode("\r\n", $this->aResults)."\r\n" : '').
                                $this->sResponseBuffer
                            )),
                            \MailSo\Log\Enumerations\Type::ERROR,
                            true
                        );
                    } else {
                        $this->writeLogException(
                            new Exceptions\NegativeResponseException($this->aResults, \trim(
                                (0 < \count($this->aResults) ? \implode("\r\n", $this->aResults)."\r\n" : '').
                                $this->sResponseBuffer
                            )),
                            \MailSo\Log\Enumerations\Type::ERROR,
                            true
                        );
                    }
                }
            } else {
                $this->writeLogException(
                    new Exceptions\ResponseException($this->aResults, \trim(
                        (0 < \count($this->aResults) ? \implode("\r\n", $this->aResults)."\r\n" : '').
                        $this->sResponseBuffer
                    )),
                    \MailSo\Log\Enumerations\Type::ERROR,
                    true
                );
            }

            $this->aResults[] = $this->sResponseBuffer;
        } while ('-' === \trim($aParts[1]));

        $this->writeLog(
            (microtime(true) - $this->iRequestTime),
            \MailSo\Log\Enumerations\Type::TIME
        );
    }

    /**
     * @return string
     */
    protected function getLogName()
    {
        return 'SMTP';
    }

    /**
     * @param \MailSo\Log\Logger $oLogger
     *
     * @return \MailSo\Smtp\SmtpClient
     *
     * @throws \MailSo\Base\Exceptions\InvalidArgumentException
     */
    public function SetLogger($oLogger)
    {
        parent::SetLogger($oLogger);

        return $this;
    }
}