Back | Home
الـ Path الحالي: /home/picotech/domains/instantly.picotech.app/public_html/public/./../app/.././../../finland.picotech.app/public_html/storage/../vendor/./nikic/../dompdf/../alexandr-mironov/../monolog/../unicodeveloper/../dragonmantank/../telnyx/.././vlucas/.././phenx/../firebase/../maatwebsite/../alexandr-mironov/php8-smpp/src
الملفات الموجودة في هذا الـ Path:
.
..
Address.php
Client.php
Collection.php
DefaultLogger.php
DeliveryReceipt.php
Host.php
HostCollection.php
ItemInterface.php
LoggerAwareInterface.php
LoggerDecorator.php
LoggerInterface.php
Pdu.php
Smpp.php
Sms.php
Tag.php
exceptions
helpers
transport

مشاهدة ملف: Client.php

<?php

declare(strict_types=1);

namespace smpp;

use DateInterval;
use DateTime;
use Exception;
use smpp\exceptions\ClosedTransportException;
use smpp\exceptions\SmppException;
use smpp\exceptions\SmppInvalidArgumentException;
use smpp\exceptions\SocketTransportException;
use smpp\transport\Socket;

/**
 * Class for receiving or sending sms through SMPP protocol.
 * This is a reduced implementation of the SMPP protocol, and as such not all features will or ought to be available.
 * The purpose is to create a lightweight and simplified SMPP client.
 *
 * @author hd@onlinecity.dk, paladin, Alexandr Mironov
 * @see http://en.wikipedia.org/wiki/Short_message_peer-to-peer_protocol - SMPP 3.4 protocol specification
 * Derived from work done by paladin, see: http://sourceforge.net/projects/phpsmppapi/
 *
 * Copyright (C) 2020 Alexandr Mironov
 * Copyright (C) 2011 OnlineCity
 * Copyright (C) 2006 Paladin
 *
 * This library is free software; you can redistribute it and/or modify it under the terms of
 * the GNU Lesser General Public License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * This license can be read at: http://www.opensource.org/licenses/lgpl-2.1.php
 */
class Client
{
    // Available modes
    /** @var string */
    public const MODE_TRANSMITTER = 'transmitter';

    /** @var string */
    public const MODE_TRANSCEIVER = 'transceiver';

    /** @var string */
    public const MODE_RECEIVER = 'receiver';

    /** @var integer Use sar_msg_ref_num and sar_total_segments with 16 bit tags */
    public const CSMS_16BIT_TAGS = 0;

    /** @var integer Use message payload for CSMS */
    public const CSMS_PAYLOAD = 1;

    /** @var integer Embed a UDH in the message with 8-bit reference. */
    public const CSMS_8BIT_UDH = 2;

    // SMPP bind parameters
    /** @var string */
    public static string $systemType = "WWW";

    /** @var int */
    public static int $interfaceVersion = 0x34;

    /** @var int */
    public static int $addrTon = 0;

    /** @var int */
    public static int $addrNPI = 0;

    /** @var string */
    public static string $addressRange = "";

    // ESME transmitter parameters
    /** @var string */
    public static string $smsServiceType = "";

    /** @var int */
    public static int $smsEsmClass = 0x00;

    /** @var int */
    public static int $smsProtocolID = 0x00;

    /** @var int */
    public static int $smsPriorityFlag = 0x00;

    /** @var int */
    public static int $smsRegisteredDeliveryFlag = 0x00;

    /** @var int */
    public static int $smsReplaceIfPresentFlag = 0x00;

    /** @var int */
    public static int $smsSmDefaultMessageID = 0x00;

    /**
     * SMPP v3.4 says octet string are "not necessarily NULL terminated".
     * Switch to toggle this feature
     * @var boolean
     *
     * set NULL terminate octetstrings FALSE as default
     */
    public static bool $smsNullTerminateOctetstrings = false;

    /** @var int */
    public static int $csmsMethod = self::CSMS_16BIT_TAGS;

    /** @var Pdu[] */
    protected array $pduQueue = [];

    // Used for reconnect
    /** @var string */
    protected string $mode;

    /** @var string $login Login of SMPP gateway */
    private string $login = '';

    /** @var string $pass Password of SMPP gateway */
    private string $pass = '';

    /** @var int */
    protected int $sequenceNumber = 1;

    /** @var int */
    protected int $sarMessageReferenceNumber;

    /** @var LoggerDecorator */
    public LoggerDecorator $logger;

    /**
     * Construct the SMPP class
     *
     * @param Socket $transport
     * @param LoggerInterface ...$loggers
     */
    public function __construct(
        public Socket $transport,
        LoggerInterface ...$loggers
    )
    {
        LoggerDecorator::$debug = Socket::$defaultDebug;
        $this->logger = new LoggerDecorator(...$loggers);
    }

    /**
     * Binds the receiver. One object can be bound only as receiver or only as transmitter.
     * @param string $login - ESME system_id
     * @param string $pass - ESME password
     *
     * @return void
     *
     * @throws SmppException
     * @throws ClosedTransportException
     * @throws Exception
     */
    public function bindReceiver(string $login, string $pass): void
    {
        if (!$this->transport->isOpen()) {
            throw new ClosedTransportException();
        }

        $this->logger->info('Binding receiver...');

        $response = $this->bind($login, $pass, Smpp::BIND_RECEIVER);

        $this->logger->info("Binding status  : " . $response->status);

        $this->mode = self::MODE_RECEIVER;
        $this->login = $login;
        $this->pass = $pass;
    }

    /**
     * Binds the transmitter. One object can be bound only as receiver or only as transmitter.
     *
     * @param string $login - ESME system_id
     * @param string $pass - ESME password
     *
     * @return void
     * @throws Exception
     */
    public function bindTransmitter(string $login, string $pass): void
    {
        if (!$this->transport->isOpen()) {
            throw new ClosedTransportException();
        }

        $this->logger->info('Binding transmitter...');

        $response = $this->bind($login, $pass, Smpp::BIND_TRANSMITTER);

        $this->logger->info("Binding status  : " . $response->status);

        $this->mode = self::MODE_TRANSMITTER;
        $this->login = $login;
        $this->pass = $pass;
    }

    /**
     * Bind transceiver, this object bound as receiver and transmitter at same time,
     * only if available in SMPP gateway
     *
     * @param string $login - ESME system_id
     * @param string $pass - ESME password
     *
     * @return void
     * @throws Exception
     */
    public function bindTransceiver(string $login, string $pass): void
    {
        if (!$this->transport->isOpen()) {
            throw new ClosedTransportException();
        }

        $this->logger->info('Binding transciever...');

        $response = $this->bind($login, $pass, Smpp::BIND_TRANSCEIVER);

        $this->logger->info("Binding status  : " . $response->status);

        $this->mode = self::MODE_TRANSCEIVER;
        $this->login = $login;
        $this->pass = $pass;
    }

    /**
     * Closes the session on the SMSC server.
     *
     * @return void
     * @throws Exception
     */
    public function close(): void
    {
        if (!$this->transport->isOpen()) {
            return;
        }

        $this->logger->info('Unbinding...');

        $response = $this->sendCommand(Smpp::UNBIND, "");

        $this->logger->info("Unbind status   : " . $response->status);

        $this->transport->close();
    }

    /**
     * Parse a time string as formatted by SMPP v3.4 section 7.1.
     * Returns an object of either DateTime or DateInterval is returned.
     *
     * @param string $input
     *
     * @return DateTime|DateInterval|null
     *
     * @throws Exception
     */
    public function parseSmppTime(string $input): null|DateTime|DateInterval
    {
        if (
            !preg_match(
                '/^(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{2})(\\d{1})(\\d{2})([R+-])$/',
                $input,
                $matches
            )
        ) {
            return null;
        }

        /**
         * @var int $y
         * @var int $m
         * @var int $d
         * @var int $h
         * @var int $i
         * @var int $s
         * @var int $n
         * @var string $p
         */
        [$whole, $y, $m, $d, $h, $i, $s, $t, $n, $p] = $matches;

        if ($p == 'R') {
            $spec = "P";
            if ($y) {
                $spec .= $y . 'Y';
            }
            if ($m) {
                $spec .= $m . 'M';
            }
            if ($d) {
                $spec .= $d . 'D';
            }
            if ($h || $i || $s) {
                $spec .= 'T';
            }
            if ($h) {
                $spec .= $h . 'H';
            }
            if ($i) {
                $spec .= $i . 'M';
            }
            if ($s) {
                $spec .= $s . 'S';
            }
            return new DateInterval($spec);
        } else {
            $offsetHours = floor($n / 4);
            $offsetMinutes = ($n % 4) * 15;
            // Not Y3K safe
            $time = sprintf(
                "20%02s-%02s-%02sT%02s:%02s:%02s%s%02s:%02s",
                $y,
                $m,
                $d,
                $h,
                $i,
                $s,
                $p,
                $offsetHours,
                $offsetMinutes
            );
            return new DateTime($time);
        }
    }

    /**
     * Query the SMSC about current state/status of a previous sent SMS.
     * You must specify the SMSC assigned message id and source of the sent SMS.
     * Returns an associative array with elements: message_id, final_date, message_state and error_code.
     *    message_state would be one of the SMPP::STATE_* constants. (SMPP v3.4 section 5.2.28)
     *    error_code depends on the telco network, so could be anything.
     *
     * @param string $messageID
     * @param Address $source
     *
     * @return null|array<string, mixed>
     *
     * @throws Exception
     */
    public function queryStatus(string $messageID, Address $source): null|array
    {
        $pduBody = pack(
            'a' . (strlen($messageID) + 1) . 'cca' . (strlen($source->value) + 1),
            $messageID,
            $source->ton,
            $source->npi,
            $source->value
        );

        $reply = $this->sendCommand(Smpp::QUERY_SM, $pduBody);

        if ($reply->status !== Smpp::ESME_ROK) {
            return null;
        }

        // Parse reply
        $posID = strpos($reply->body, "\0", 0);
        $posDate = strpos($reply->body, "\0", $posID + 1);

        if ($posID === false) {
            // todo: replace exception and add message
            throw new Exception();
        }

        $data = [
            'message_id' => substr($reply->body, 0, $posID),
            'final_date' => substr($reply->body, $posID, (int)$posDate - $posID),
        ];
        $data['final_date'] = $data['final_date'] ? $this->parseSmppTime(trim($data['final_date'])) : null;
        /** @var false|array{message_state: mixed, error_code: mixed} $status */
        $status = unpack("cmessage_state/cerror_code", substr($reply->body, $posDate + 1));

        if (!$status) {
            // todo: replace exception and add message
            throw new Exception();
        }

        return array_merge($data, $status);
    }

    /**
     * Read one SMS from SMSC. Can be executed only after bindReceiver() call.
     * This method blocks. Method returns on socket timeout or enquire_link signal from SMSC.
     *
     * @return DeliveryReceipt|Sms|bool
     * @throws Exception
     */
    public function readSMS(): bool|DeliveryReceipt|Sms
    {
        // Check the queue
        $queueLength = count($this->pduQueue);
        for ($i = 0; $i < $queueLength; $i++) {
            $pdu = $this->pduQueue[$i];
            if ($pdu->id === Smpp::DELIVER_SM) {
                //remove response
                array_splice($this->pduQueue, $i, 1);
                return $this->parseSMS($pdu);
            }
        }
        // Read pdu
        do {
            $pdu = $this->readPDU();
            if ($pdu === false) {
                return false;
            } // TSocket v. 0.6.0+ returns false on timeout
            //check for enquire link command
            if ($pdu->id === Smpp::ENQUIRE_LINK) {
                $response = new Pdu(Smpp::ENQUIRE_LINK_RESP, Smpp::ESME_ROK, $pdu->sequence, "\x00");
                $this->sendPDU($response);
            } else if ($pdu->id !== Smpp::DELIVER_SM) { // if this is not the correct PDU add to queue
                array_push($this->pduQueue, $pdu);
            }
        } while ($pdu->id !== Smpp::DELIVER_SM);

        return $this->parseSMS($pdu);
    }

    /**
     * Send one SMS to SMSC. Can be executed only after bindTransmitter() call.
     * $message is always in octets regardless of the data encoding.
     * For correct handling of Concatenated SMS,
     * message must be encoded with GSM 03.38 (data_coding 0x00) or UCS-2BE (0x08).
     * Concatenated SMS'es uses 16-bit reference numbers, which gives 152 GSM 03.38 chars or 66 UCS-2BE chars per CSMS.
     * If we are using 8-bit ref numbers in the UDH for CSMS it's 153 GSM 03.38 chars
     *
     * @param Address $from
     * @param Address $to
     * @param string $message
     * @param Tag[]|null $tags (optional)
     * @param int $dataCoding (optional)
     * @param int $priority (optional)
     * @param null $scheduleDeliveryTime (optional)
     * @param null $validityPeriod (optional)
     *
     * @return bool|string message id
     *
     * @throws Exception
     */
    public function sendSMS(
        Address $from,
        Address $to,
        string $message,
        array $tags = null,
        int $dataCoding = Smpp::DATA_CODING_DEFAULT,
        int $priority = 0x00,
        $scheduleDeliveryTime = null,
        $validityPeriod = null
    ): bool|string
    {
        $messageLength = strlen($message);

        if ($messageLength > 160 && !in_array($dataCoding, [Smpp::DATA_CODING_UCS2, Smpp::DATA_CODING_DEFAULT])) {
            return false;
        }

        switch ($dataCoding) {
            case Smpp::DATA_CODING_UCS2:
                // in octets, 70 UCS-2 chars
                $singleSmsOctetLimit = 140;
                // There are 133 octets available, but this would split the UCS the middle so use 132 instead
                $csmsSplit = 132;
                $message = mb_convert_encoding($message, 'UCS-2');
                //Update message length with current encoding
                $messageLength = mb_strlen($message);
                break;
            case Smpp::DATA_CODING_DEFAULT:
                // we send data in octets, but GSM 03.38 will be packed in septets (7-bit) by SMSC.
                $singleSmsOctetLimit = 160;
                // send 152/153 chars in each SMS (SMSC will format data)
                $csmsSplit = (self::$csmsMethod == self::CSMS_8BIT_UDH) ? 153 : 152;
                break;
            default:
                $singleSmsOctetLimit = 254; // From SMPP standard
                break;
        }

        // Figure out if we need to do CSMS, since it will affect our PDU
        if ($messageLength > $singleSmsOctetLimit) {
            $doCsms = true;
            if (self::$csmsMethod != self::CSMS_PAYLOAD) {
                $parts = $this->splitMessageString($message, $csmsSplit, $dataCoding);
                $shortMessage = reset($parts);
                $csmsReference = $this->getCsmsReference();
            }
        } else {
            $shortMessage = $message;
            $doCsms = false;
        }

        // Deal with CSMS
        if ($doCsms) {
            if (self::$csmsMethod == self::CSMS_PAYLOAD) {
                $payload = new Tag(Tag::MESSAGE_PAYLOAD, $message, $messageLength);
                // todo: replace array to k=>v storage (Collection??), where key is tag id
                $tags[] = $payload;
                return $this->submitShortMessage(
                    $from,
                    $to,
                    null,
                    $tags,
                    $dataCoding,
                    $priority,
                    $scheduleDeliveryTime,
                    $validityPeriod
                );
            } elseif (self::$csmsMethod == self::CSMS_8BIT_UDH) {
                $sequenceNumber = 1;
                foreach ($parts as $part) {
                    $udh = pack(
                        'cccccc',
                        5,
                        0,
                        3,
                        substr((string)$csmsReference, 1, 1),
                        count($parts),
                        $sequenceNumber
                    );
                    $res = $this->submitShortMessage(
                        $from,
                        $to,
                        $udh . $part,
                        $tags,
                        $dataCoding,
                        $priority,
                        $scheduleDeliveryTime,
                        $validityPeriod,
                        (string)(self::$smsEsmClass | 0x40) //todo: check this
                    );
                    $sequenceNumber++;
                }
                return $res;
            } else {
                $sarMessageRefNumber = new Tag(Tag::SAR_MSG_REF_NUM, $csmsReference, 2, 'n');
                $sarTotalSegments = new Tag(Tag::SAR_TOTAL_SEGMENTS, count($parts), 1, 'c');
                $sequenceNumber = 1;
                foreach ($parts as $part) {
                    $sartags = [
                        $sarMessageRefNumber,
                        $sarTotalSegments,
                        new Tag(Tag::SAR_SEGMENT_SEQNUM, $sequenceNumber, 1, 'c')
                    ];
                    $res = $this->submitShortMessage(
                        $from,
                        $to,
                        (string)$part,
                        (empty($tags) ? $sartags : array_merge($tags, $sartags)),
                        $dataCoding,
                        $priority,
                        $scheduleDeliveryTime,
                        $validityPeriod
                    );
                    $sequenceNumber++;
                }
                return $res;
            }
        }

        return $this->submitShortMessage($from, $to, (string)($shortMessage ?? ''), $tags, $dataCoding, $priority);
    }

    /**
     * Perform the actual submit_sm call to send SMS.
     * Implemented as a protected method to allow automatic sms concatenation.
     * Tags must be an array of already packed and encoded TLV-params.
     *
     * @param Address $source
     * @param Address $destination
     * @param string|null $shortMessage
     * @param Tag[]|null $tags
     * @param integer $dataCoding
     * @param integer $priority
     * @param string|null $scheduleDeliveryTime
     * @param string|null $validityPeriod
     * @param string|null $esmClass
     *
     * @return string message id
     *
     * @throws Exception
     */
    protected function submitShortMessage(
        Address $source,
        Address $destination,
        string $shortMessage = null,
        array $tags = null,
        int $dataCoding = Smpp::DATA_CODING_DEFAULT,
        int $priority = 0x00,
        string $scheduleDeliveryTime = null,
        string $validityPeriod = null,
        string $esmClass = null
    ): string
    {
        if (is_null($esmClass)) {
            $esmClass = self::$smsEsmClass;
        }

        $shortMessageLength = strlen($shortMessage);
        // Construct PDU with mandatory fields
        $pdu = pack(
            'a1cca' . (strlen($source->value) + 1)
            . 'cca' . (strlen($destination->value) + 1)
            . 'ccc' . ($scheduleDeliveryTime ? 'a16x' : 'a1') . ($validityPeriod ? 'a16x' : 'a1')
            . 'ccccca' . ($shortMessageLength + (self::$smsNullTerminateOctetstrings ? 1 : 0)),
            self::$smsServiceType,
            $source->ton,
            $source->npi,
            $source->value,
            $destination->ton,
            $destination->npi,
            $destination->value,
            $esmClass,
            self::$smsProtocolID,
            $priority,
            $scheduleDeliveryTime,
            $validityPeriod,
            self::$smsRegisteredDeliveryFlag,
            self::$smsReplaceIfPresentFlag,
            $dataCoding,
            self::$smsSmDefaultMessageID,
            $shortMessageLength, //sm_length
            $shortMessage //short_message
        );

        // Add any tags
        if (!empty($tags)) {
            foreach ($tags as $tag) {
                $pdu .= $tag->getBinary();
            }
        }

        $response = $this->sendCommand(Smpp::SUBMIT_SM, $pdu);
        /** @var array{msgid: string}|false $body */
        $body = unpack("a*msgid", $response->body);
        if (!$body) {
            throw new SmppException('unable to unpack response body:' . $response->body);
        }
        return $body['msgid'];
    }

    /**
     * Get a CSMS reference number for sar_msg_ref_num.
     * Initializes with a random value, and then returns the number in sequence with each call.
     *
     * @return int
     */
    protected function getCsmsReference(): int
    {
        $limit = (self::$csmsMethod == self::CSMS_8BIT_UDH) ? 255 : 65535;
        if (!isset($this->sarMessageReferenceNumber)) {
            $this->sarMessageReferenceNumber = mt_rand(0, $limit);
        }
        $this->sarMessageReferenceNumber++;

        if ($this->sarMessageReferenceNumber > $limit) {
            $this->sarMessageReferenceNumber = 0;
        }
        return $this->sarMessageReferenceNumber;
    }


    /**
     * Split a message into multiple parts, taking the encoding into account.
     * A character represented by an GSM 03.38 escape-sequence shall not be split in the middle.
     * Uses str_split if at all possible, and will examine all split points for escape chars if it's required.
     *
     * @param string $message
     * @param int<1,max> $split
     * @param integer $dataCoding (optional)
     *
     * @return array<int|string>
     */
    protected function splitMessageString(
        string $message,
        int $split,
        int $dataCoding = Smpp::DATA_CODING_DEFAULT
    ): array
    {
        switch ($dataCoding) {
            case Smpp::DATA_CODING_DEFAULT:
                $messageLength = strlen($message);
                // Do we need to do php based split?
                $numParts = floor($messageLength / $split);
                if ($messageLength % $split == 0) {
                    $numParts--;
                }
                $slowSplit = false;

                for ($i = 1; $i <= $numParts; $i++) {
                    if ($message[$i * $split - 1] == "\x1B") {
                        $slowSplit = true;
                        break;
                    }
                }
                if (!$slowSplit) {
                    return str_split($message, $split);
                }

                // Split the message char-by-char
                $parts = [];
                $part = null;
                $n = 0;
                for ($i = 0; $i < $messageLength; $i++) {
                    $c = $message[$i];
                    // reset on $split or if last char is a GSM 03.38 escape char
                    if ($n == $split || ($n == ($split - 1) && $c == "\x1B")) {
                        $parts[] = $part;
                        $n = 0;
                        $part = null;
                    }
                    $part .= $c;
                }
                $parts[] = $part;
                return $parts;
            /**
             * UCS2-BE can just use str_split since we send 132 octets per message,
             * which gives a fine split using UCS2
             */
            case Smpp::DATA_CODING_UCS2:
            default:
                return str_split($message, $split);
        }
    }

    /**
     * Binds the socket and opens the session on SMSC
     *
     * @param string $login - ESME system_id
     * @param string $pass
     * @param int $commandID (todo replace to ENUM in php 8.1)
     *
     * @return Pdu
     *
     * @throws Exception
     */
    protected function bind(string $login, string $pass, int $commandID): Pdu
    {
        // Make PDU body
        $pduBody = pack(
            'a' . (strlen($login) + 1)
            . 'a' . (strlen($pass) + 1)
            . 'a' . (strlen(self::$systemType) + 1)
            . 'CCCa' . (strlen(self::$addressRange) + 1),
            $login,
            $pass,
            self::$systemType,
            self::$interfaceVersion,
            self::$addrTon,
            self::$addrNPI,
            self::$addressRange
        );

        $response = $this->sendCommand($commandID, $pduBody);
        if ($response->status != Smpp::ESME_ROK) {
            throw new SmppException(Smpp::getStatusMessage($response->status), $response->status);
        }

        return $response;
    }

    /**
     * Parse received PDU from SMSC.
     * @param Pdu $pdu - received PDU from SMSC.
     *
     * @return DeliveryReceipt|Sms parsed PDU as array.
     *
     * @throws Exception
     */
    protected function parseSMS(Pdu $pdu): DeliveryReceipt|Sms
    {
        // Check command id
        if ($pdu->id != Smpp::DELIVER_SM) {
            throw new SmppInvalidArgumentException('PDU is not an received SMS');
        }

        // Unpack PDU
        $ar = unpack("C*", $pdu->body);

        if (!$ar) {
            throw new SmppException(''); // todo: update message
        }

        // Read mandatory params
        $serviceType = $this->getString($ar, 6, true);

        //
        $sourceAddrTon = next($ar);
        $sourceAddrNPI = next($ar);
        $sourceAddr = $this->getString($ar, 21);
        $source = new Address($sourceAddr, $sourceAddrTon, $sourceAddrNPI);

        //
        $destinationAddrTon = next($ar);
        $destinationAddrNPI = next($ar);
        $destinationAddr = $this->getString($ar, 21);
        $destination = new Address($destinationAddr, $destinationAddrTon, $destinationAddrNPI);

        $esmClass = next($ar);
        $protocolId = next($ar);
        $priorityFlag = next($ar);
        next($ar); // schedule_delivery_time
        next($ar); // validity_period
        $registeredDelivery = next($ar);
        next($ar); // replace_if_present_flag
        $dataCoding = next($ar);
        next($ar); // sm_default_msg_id
        $sm_length = next($ar);
        $message = $this->getString($ar, $sm_length);

        // Check for optional params, and parse them
        if (current($ar) !== false) {
            $tags = [];
            do {
                $tag = $this->parseTag($ar);
                if ($tag !== false) {
                    $tags[] = $tag;
                }
            } while (current($ar) !== false);
        } else {
            $tags = null;
        }

        if (($esmClass & Smpp::ESM_DELIVER_SMSC_RECEIPT) != 0) {
            $sms = new DeliveryReceipt(
                $pdu->id,
                $pdu->status,
                $pdu->sequence,
                $pdu->body,
                $serviceType,
                $source,
                $destination,
                $esmClass,
                $protocolId,
                $priorityFlag,
                $registeredDelivery,
                $dataCoding,
                $message,
                $tags
            );
            $sms->parseDeliveryReceipt();
        } else {
            $sms = new Sms(
                $pdu->id,
                $pdu->status,
                $pdu->sequence,
                $pdu->body,
                $serviceType,
                $source,
                $destination,
                $esmClass,
                $protocolId,
                $priorityFlag,
                $registeredDelivery,
                $dataCoding,
                $message,
                $tags
            );
        }

        $this->logger->info("Received sms:\n" . print_r($sms, true));

        // Send response of recieving sms
        $response = new Pdu(Smpp::DELIVER_SM_RESP, Smpp::ESME_ROK, $pdu->sequence, "\x00");
        $this->sendPDU($response);
        return $sms;
    }

    /**
     * Send the enquire link command.
     * @return Pdu
     * @throws Exception
     */
    public function enquireLink(): Pdu
    {
        return $this->sendCommand(Smpp::ENQUIRE_LINK, null);
    }

    /**
     * Respond to any enquire link we might have waiting.
     * If will check the queue first and respond to any enquire links we have there.
     * Then it will move on to the transport, and if the first PDU is enquire link respond,
     * otherwise add it to the queue and return.
     *
     */
    public function respondEnquireLink(): void
    {
        // Check the queue first
        $queueLength = count($this->pduQueue);
        for ($i = 0; $i < $queueLength; $i++) {
            $pdu = $this->pduQueue[$i];
            if ($pdu->id == Smpp::ENQUIRE_LINK) {
                //remove response
                array_splice($this->pduQueue, $i, 1);
                $this->sendPDU(new Pdu(Smpp::ENQUIRE_LINK_RESP, Smpp::ESME_ROK, $pdu->sequence, "\x00"));
            }
        }

        // Check the transport for data
        if ($this->transport->hasData()) {
            $pdu = $this->readPDU();
            if ($pdu && $pdu->id == Smpp::ENQUIRE_LINK) {
                $this->sendPDU(new Pdu(Smpp::ENQUIRE_LINK_RESP, Smpp::ESME_ROK, $pdu->sequence, "\x00"));
            } elseif ($pdu) {
                array_push($this->pduQueue, $pdu);
            }
        }
    }

    /**
     * Reconnect to SMSC.
     * This is mostly to deal with the situation were we run out of sequence numbers
     *
     * @throws SmppException|Exception
     */
    protected function reconnect(): void
    {
        $this->close();
        sleep(1);
        $this->transport->open();
        $this->sequenceNumber = 1;

        match ($this->mode) {
            self::MODE_TRANSMITTER => $this->bindTransmitter($this->login, $this->pass),
            self::MODE_RECEIVER => $this->bindReceiver($this->login, $this->pass),
            self::MODE_TRANSCEIVER => $this->bindTransceiver($this->login, $this->pass),
            default => throw new SmppException('Invalid mode: ' . $this->mode)
        };
    }

    /**
     * Sends the PDU command to the SMSC and waits for response.
     * @param int $id - command ID
     * @param ?string $pduBody - PDU body
     * @return Pdu
     *
     * @throws Exception
     */
    protected function sendCommand(int $id, ?string $pduBody): Pdu
    {
        if (!$this->transport->isOpen()) {
            throw new SocketTransportException('Socket is closed');
            //return false;
        }
        $pdu = new Pdu($id, 0, $this->sequenceNumber, $pduBody);
        $this->sendPDU($pdu);
        $response = $this->readPduResponse($this->sequenceNumber, $pdu->id);

        if ($response === false) {
            throw new SmppException('Failed to read reply to command: 0x' . dechex($id));
        }

        if ($response->status != Smpp::ESME_ROK) {
            throw new SmppException(Smpp::getStatusMessage($response->status), $response->status);
        }

        $this->sequenceNumber++;

        // Reached max sequence number, spec does not state what happens now, so we re-connect
        if ($this->sequenceNumber >= 0x7FFFFFFF) {
            $this->reconnect();
        }

        return $response;
    }

    /**
     * Prepares and sends PDU to SMSC.
     * @param Pdu $pdu
     * @throws Exception
     */
    protected function sendPDU(Pdu $pdu): void
    {
        $length = strlen($pdu->body) + 16;
        $header = pack("NNNN", $length, $pdu->id, $pdu->status, $pdu->sequence);

        $this->logger->info("Read PDU         : $length bytes");
        $this->logger->info(' ' . chunk_split(bin2hex($header . $pdu->body), 2, " "));
        $this->logger->info(' command_id      : 0x' . dechex($pdu->id));
        $this->logger->info(' sequence number : ' . $pdu->sequence);

        $this->transport->write($header . $pdu->body, $length);
    }

    /**
     * Waits for SMSC response on specific PDU.
     * If a GENERIC_NACK with a matching sequence number, or null sequence is received instead it's also accepted.
     * Some SMPP servers, ie. logica returns GENERIC_NACK on errors.
     *
     * @param int $sequenceNumber - PDU sequence number
     * @param int $commandID - PDU command ID
     *
     * @return Pdu|false
     * @throws SmppException
     */
    protected function readPduResponse(int $sequenceNumber, int $commandID): Pdu|false
    {
        // Get response cmd id from command ID
        $commandID = $commandID | Smpp::GENERIC_NACK;

        // Check the queue first
        $queueLength = count($this->pduQueue);
        for ($i = 0; $i < $queueLength; $i++) {
            $pdu = $this->pduQueue[$i];
            if (
                ($pdu->sequence == $sequenceNumber && ($pdu->id == $commandID || $pdu->id == Smpp::GENERIC_NACK))
                ||
                ($pdu->sequence == null && $pdu->id == Smpp::GENERIC_NACK)
            ) {
                // remove response pdu from queue
                array_splice($this->pduQueue, $i, 1);
                return $pdu;
            }
        }

        // Read PDUs until the one we are looking for shows up, or a generic nack pdu with matching sequence or null sequence
        do {
            $pdu = $this->readPDU();
            if ($pdu) {
                if (
                    $pdu->sequence == $sequenceNumber
                    && ($pdu->id == $commandID || $pdu->id == Smpp::GENERIC_NACK)
                ) {
                    return $pdu;
                }
                if ($pdu->sequence == null && $pdu->id == Smpp::GENERIC_NACK) {
                    return $pdu;
                }
                array_push($this->pduQueue, $pdu); // unknown PDU push to queue
            }
        } while ($pdu);
        return false;
    }

    /**
     * Reads incoming PDU from SMSC.
     * @return false|Pdu
     */
    protected function readPDU(): Pdu|false
    {
        // Read PDU length
        $bufLength = $this->transport->read(4);
        if (!$bufLength) {
            return false;
        }

        $extract = unpack("Nlength", $bufLength);
        if (!$extract) {
            throw new SmppException('unable to unpack string');
        }
        /**
         * extraction define next variables:
         * @var $length
         */
        extract($extract);

        // Read PDU headers
        $bufHeaders = $this->transport->read(12);
        if (!$bufHeaders) {
            return false;
        }

        $extract = unpack("Ncommand_id/Ncommand_status/Nsequence_number", $bufHeaders);
        if (!$extract) {
            throw new SmppException('unable to unpack string');
        }
        /**
         * @var $command_id
         * @var $command_status
         * @var $sequence_number
         */
        extract($extract);

        if (!isset($command_id, $command_status, $sequence_number, $length)) {
            return false; // todo: maybe replace to exception??
        }

        // Read PDU body
        $bodyLength = $length - 16;
        if ($bodyLength > 0) {
            if (!$body = $this->transport->readAll($bodyLength)) {
                throw new SmppException('Could not read PDU body');
            }
        } else {
            $body = null;
        }

        $this->logger->info("Read PDU         : $length bytes");
        $this->logger->info(' ' . chunk_split(bin2hex($bufLength . $bufHeaders . $body), 2, " "));
        $this->logger->info(" command id      : 0x" . dechex($command_id));
        $this->logger->info(" command status  : 0x" . dechex($command_status) . " " . Smpp::getStatusMessage($command_status));
        $this->logger->info(' sequence number : ' . $sequence_number);

        return new Pdu($command_id, $command_status, $sequence_number, $body);
    }

    /**
     * Reads C style null padded string from the char array.
     * Reads until $maxlen or null byte.
     *
     * @param array<mixed> $ar - input array
     * @param integer $maxLength - maximum length to read.
     * @param boolean $firstRead - is this the first bytes read from array?
     * @return string.
     */
    protected function getString(array &$ar, int $maxLength = 255, bool $firstRead = false): string
    {
        $s = "";
        $i = 0;
        do {
            $c = ($firstRead && $i == 0) ? current($ar) : next($ar);
            if ($c != 0) $s .= chr($c);
            $i++;
        } while ($i < $maxLength && $c != 0);
        return $s;
    }

    /**
     * Read a specific number of octets from the char array.
     * Does not stop at null byte
     *
     * @param array<mixed> $ar - input array
     * @param int $length
     * @return string
     */
    protected function getOctets(array &$ar, int $length): string
    {
        $s = "";
        for ($i = 0; $i < $length; $i++) {
            $c = next($ar);
            if ($c === false) {
                return $s;
            }
            $s .= chr($c);
        }
        return $s;
    }

    /**
     * @param array<mixed> $ar
     * @return false|Tag
     */
    protected function parseTag(array &$ar): false|Tag
    {
        $unpackedData = unpack(
            'nid/nlength',
            pack("C2C2", next($ar), next($ar), next($ar), next($ar))
        );

        if (!$unpackedData) {
            throw new SmppInvalidArgumentException('Could not read tag data');
        }
        /**
         * Extraction create variables:
         * @var $length
         * @var $id
         */
        extract($unpackedData);

        // Sometimes SMSC return an extra null byte at the end
        if (!isset($id, $length) || ($length == 0 && $id == 0)) {
            return false;
        }

        $value = $this->getOctets($ar, $length);
        $tag = new Tag($id, $value, $length);

        $this->logger->info("Parsed tag:");
        $this->logger->info(" id     :0x" . dechex($tag->id));
        $this->logger->info(" length :" . $tag->length);
        $this->logger->info(" value  :" . chunk_split(bin2hex((string)$tag->value), 2, " "));

        return $tag;
    }
}