* @author Damian Fernandez Sosa * @author Anish Mistry * @author Jan Schneider * @license http://www.horde.org/licenses/bsd BSD */ namespace Horde; use Auth_SASL; use Horde\Socket\Client; use Horde\ManageSieve\Exception; /** * This class implements the ManageSieve protocol (RFC 5804). * * @package ManageSieve * @author Richard Heyes * @author Damian Fernandez Sosa * @author Anish Mistry * @author Jan Schneider * @copyright 2002-2003 Richard Heyes * @copyright 2006-2008 Anish Mistry * @copyright 2009-2017 Horde LLC * @license http://www.horde.org/licenses/bsd BSD * @link http://tools.ietf.org/html/rfc5804 RFC 5804 A Protocol for * Remotely Managing Sieve Scripts */ class ManageSieve { /** * Client is disconnected. */ const STATE_DISCONNECTED = 1; /** * Client is connected but not authenticated. */ const STATE_NON_AUTHENTICATED = 2; /** * Client is authenticated. */ const STATE_AUTHENTICATED = 3; /** * Authentication with the best available method. */ const AUTH_AUTOMATIC = 0; /** * DIGEST-MD5 authentication. */ const AUTH_DIGESTMD5 = 'DIGEST-MD5'; /** * CRAM-MD5 authentication. */ const AUTH_CRAMMD5 = 'CRAM-MD5'; /** * LOGIN authentication. */ const AUTH_LOGIN = 'LOGIN'; /** * PLAIN authentication. */ const AUTH_PLAIN = 'PLAIN'; /** * EXTERNAL authentication. */ const AUTH_EXTERNAL = 'EXTERNAL'; /** * The authentication methods this class supports. * * Can be overwritten if having problems with certain methods. * * @var array */ public $supportedAuthMethods = array( self::AUTH_DIGESTMD5, self::AUTH_CRAMMD5, self::AUTH_EXTERNAL, self::AUTH_PLAIN, self::AUTH_LOGIN, ); /** * SASL authentication methods that require Auth_SASL. * * @var array */ public $supportedSASLAuthMethods = array( self::AUTH_DIGESTMD5, self::AUTH_CRAMMD5, ); /** * The socket client. * * @var \Horde\Socket\Client */ protected $_sock; /** * Parameters and connection information. * * @var array */ protected $_params; /** * Current state of the connection. * * One of the STATE_* constants. * * @var integer */ protected $_state = self::STATE_DISCONNECTED; /** * Logging handler. * * @var string|array */ protected $_logger; /** * Maximum number of referral loops * * @var array */ protected $_maxReferralCount = 15; /** * Constructor. * * If username and password are provided connects to the server and logs * in too. * * @param array $params A hash of connection parameters: * - host: Hostname of server (DEFAULT: localhost). Optionally prefixed * with protocol scheme. * - port: Port of server (DEFAULT: 4190). * - user: Login username (optional). * - password: Login password (optional). * - authmethod: Type of login to perform (see $supportedAuthMethods) * (DEFAULT: AUTH_AUTOMATIC). * - euser: Effective user. If authenticating as an administrator, login * as this user. * - bypassauth: Skip the authentication phase. Useful if passing an * already open socket. * - secure: Security layer requested. One of: * - true: (TLS if available/necessary) [DEFAULT] * - false: (No encryption) * - 'ssl': (Auto-detect SSL version) * - 'sslv2': (Force SSL version 3) * - 'sslv3': (Force SSL version 2) * - 'tls': (TLS; started via protocol-level negotation over * unencrypted channel) * - 'tlsv1': (TLS version 1.x connection) * - context: Additional options for stream_context_create(). * - logger: A log handler, must implement debug(). * * @throws \Horde\ManageSieve\Exception */ public function __construct($params = array()) { $this->_params = array_merge( array( 'authmethod' => self::AUTH_AUTOMATIC, 'bypassauth' => false, 'context' => array(), 'euser' => null, 'host' => 'localhost', 'logger' => null, 'password' => '', 'port' => 4190, 'secure' => true, 'timeout' => 5, 'user' => '', ), $params ); /* Try to include the Auth_SASL package. If the package is not * available, we disable the authentication methods that depend upon * it. */ if (!class_exists('Auth_SASL')) { $this->_debug('Auth_SASL not present'); $this->supportedAuthMethods = array_diff( $this->supportedAuthMethods, $this->supportedSASLAuthMethods ); } if ($this->_params['logger']) { $this->setLogger($this->_params['logger']); } if (strlen($this->_params['user']) && strlen($this->_params['password'])) { $this->_handleConnectAndLogin(); } } /** * Passes a logger for debug logging. * * @param object $logger A log handler, must implement debug(). */ public function setLogger($logger) { $this->_logger = $logger; } /** * Connects to the server and logs in. * * @throws \Horde\ManageSieve\Exception */ protected function _handleConnectAndLogin() { $this->connect( $this->_params['host'], $this->_params['port'], $this->_params['context'], $this->_params['secure'] ); if (!$this->_params['bypassauth']) { $this->login( $this->_params['user'], $this->_params['password'], $this->_params['authmethod'], $this->_params['euser'] ); } } /** * Handles connecting to the server and checks the response validity. * * Defaults from the constructor are used for missing parameters. * * @param string $host Hostname of server. * @param string $port Port of server. * @param array $context List of options to pass to * stream_context_create(). * @param boolean $secure Security layer requested. @see __construct(). * * @throws \Horde\ManageSieve\Exception */ public function connect( $host = null, $port = null, $context = null, $secure = null ) { if (isset($host)) { $this->_params['host'] = $host; } if (isset($port)) { $this->_params['port'] = $port; } if (isset($context)) { $this->_params['context'] = array_replace_recursive( $this->_params['context'], $context ); } if (isset($secure)) { $this->_params['secure'] = $secure; } if (self::STATE_DISCONNECTED != $this->_state) { throw new Exception\NotDisconnected(); } try { $this->_sock = new Client( $this->_params['host'], $this->_params['port'], $this->_params['timeout'], $this->_params['secure'], $this->_params['context'] ); } catch (Client\Exception $e) { throw new Exception\ConnectionFailed($e); } if ($this->_params['bypassauth']) { $this->_state = self::STATE_AUTHENTICATED; } else { $this->_state = self::STATE_NON_AUTHENTICATED; $this->_doCmd(); } // Explicitly ask for the capabilities in case the connection is // picked up from an existing connection. try { $this->_cmdCapability(); } catch (Exception $e) { throw new Exception\ConnectionFailed($e); } // Check if we can enable TLS via STARTTLS. if ($this->_params['secure'] === 'tls' || ($this->_params['secure'] === true && !empty($this->_capability['starttls']))) { $this->_doCmd('STARTTLS'); if (!$this->_sock->startTls()) { throw new Exception('Failed to establish TLS connection'); } // The server should be sending a CAPABILITY response after // negotiating TLS. Read it, and ignore if it doesn't. // Unfortunately old Cyrus versions are broken and don't send a // CAPABILITY response, thus we would wait here forever. Parse the // Cyrus version and work around this broken behavior. if (!preg_match('/^CYRUS TIMSIEVED V([0-9.]+)/', $this->_capability['implementation'], $matches) || version_compare($matches[1], '2.3.10', '>=')) { $this->_doCmd(); } // Query the server capabilities again now that we are under // encryption. try { $this->_cmdCapability(); } catch (Exception $e) { throw new Exception\ConnectionFailed($e); } } } /** * Disconnect from the Sieve server. * * @param boolean $sendLogoutCMD Whether to send LOGOUT command before * disconnecting. * * @throws \Horde\ManageSieve\Exception */ public function disconnect($sendLogoutCMD = true) { $this->_cmdLogout($sendLogoutCMD); } /** * Logs into server. * * Defaults from the constructor are used for missing parameters. * * @param string $user Login username. * @param string $password Login password. * @param string $authmethod Type of login method to use. * @param string $euser Effective UID (perform on behalf of $euser). * * @throws \Horde\ManageSieve\Exception */ public function login( $user = null, $password = null, $authmethod = null, $euser = null ) { if (isset($user)) { $this->_params['user'] = $user; } if (isset($password)) { $this->_params['password'] = $password; } if (isset($authmethod)) { $this->_params['authmethod'] = $authmethod; } if (isset($euser)) { $this->_params['euser'] = $euser; } $this->_checkConnected(); if (self::STATE_AUTHENTICATED == $this->_state) { throw new Exception('Already authenticated'); } $this->_cmdAuthenticate( $this->_params['user'], $this->_params['password'], $this->_params['authmethod'], $this->_params['euser'] ); $this->_state = self::STATE_AUTHENTICATED; } /** * Returns an indexed array of scripts currently on the server. * * @return array Indexed array of scriptnames. */ public function listScripts() { if (is_array($scripts = $this->_cmdListScripts())) { return $scripts[0]; } else { return $scripts; } } /** * Returns the active script. * * @return string The active scriptname. */ public function getActive() { if (is_array($scripts = $this->_cmdListScripts())) { return $scripts[1]; } } /** * Sets the active script. * * @param string $scriptname The name of the script to be set as active. * * @throws \Horde\ManageSieve\Exception */ public function setActive($scriptname) { $this->_cmdSetActive($scriptname); } /** * Retrieves a script. * * @param string $scriptname The name of the script to be retrieved. * * @throws \Horde\ManageSieve\Exception * @return string The script. */ public function getScript($scriptname) { return $this->_cmdGetScript($scriptname); } /** * Adds a script to the server. * * @param string $scriptname Name of the script. * @param string $script The script content. * @param boolean $makeactive Whether to make this the active script. * * @throws \Horde\ManageSieve\Exception */ public function installScript($scriptname, $script, $makeactive = false) { $this->_cmdPutScript($scriptname, $script); if ($makeactive) { $this->_cmdSetActive($scriptname); } } /** * Removes a script from the server. * * @param string $scriptname Name of the script. * * @throws \Horde\ManageSieve\Exception */ public function removeScript($scriptname) { $this->_cmdDeleteScript($scriptname); } /** * Checks if the server has space to store the script by the server. * * @param string $scriptname The name of the script to mark as active. * @param integer $size The size of the script. * * @throws \Horde\ManageSieve\Exception * @return boolean True if there is space. */ public function hasSpace($scriptname, $size) { $this->_checkAuthenticated(); try { $this->_doCmd( sprintf('HAVESPACE %s %d', $this->_escape($scriptname), $size) ); } catch (Exception $e) { return false; } return true; } /** * Returns the list of extensions the server supports. * * @throws \Horde\ManageSieve\Exception * @return array List of extensions. */ public function getExtensions() { $this->_checkConnected(); return $this->_capability['extensions']; } /** * Returns whether the server supports an extension. * * @param string $extension The extension to check. * * @throws \Horde\ManageSieve\Exception * @return boolean Whether the extension is supported. */ public function hasExtension($extension) { $this->_checkConnected(); $extension = trim(\Horde_String::upper($extension)); if (is_array($this->_capability['extensions'])) { foreach ($this->_capability['extensions'] as $ext) { if ($ext == $extension) { return true; } } } return false; } /** * Returns the list of authentication methods the server supports. * * @throws \Horde\ManageSieve\Exception * @return array List of authentication methods. */ public function getAuthMechs() { $this->_checkConnected(); return $this->_capability['sasl']; } /** * Returns whether the server supports an authentication method. * * @param string $method The method to check. * * @throws \Horde\ManageSieve\Exception * @return boolean Whether the method is supported. */ public function hasAuthMech($method) { $this->_checkConnected(); $method = trim(\Horde_String::upper($method)); if (is_array($this->_capability['sasl'])) { foreach ($this->_capability['sasl'] as $sasl) { if ($sasl == $method) { return true; } } } return false; } /** * Handles the authentication using any known method. * * @param string $uid The userid to authenticate as. * @param string $pwd The password to authenticate with. * @param string $authmethod The method to use. If empty, the class chooses * the best (strongest) available method. * @param string $euser The effective uid to authenticate as. * * @throws \Horde\ManageSieve\Exception */ protected function _cmdAuthenticate( $uid, $pwd, $authmethod = null, $euser = '' ) { $method = $this->_getBestAuthMethod($authmethod); switch ($method) { case self::AUTH_DIGESTMD5: $this->_authDigestMD5($uid, $pwd, $euser); return; case self::AUTH_CRAMMD5: $this->_authCRAMMD5($uid, $pwd, $euser); break; case self::AUTH_LOGIN: $this->_authLOGIN($uid, $pwd, $euser); break; case self::AUTH_PLAIN: $this->_authPLAIN($uid, $pwd, $euser); break; case self::AUTH_EXTERNAL: $this->_authEXTERNAL($uid, $pwd, $euser); break; default : throw new Exception( $method . ' is not a supported authentication method' ); break; } $this->_doCmd(); // Query the server capabilities again now that we are authenticated. try { $this->_cmdCapability(); } catch (Exception $e) { throw new Exception\ConnectionFailed($e); } } /** * Authenticates the user using the PLAIN method. * * @param string $user The userid to authenticate as. * @param string $pass The password to authenticate with. * @param string $euser The effective uid to authenticate as. * * @throws \Horde\ManageSieve\Exception */ protected function _authPLAIN($user, $pass, $euser) { return $this->_sendCmd( sprintf( 'AUTHENTICATE "PLAIN" "%s"', base64_encode($euser . chr(0) . $user . chr(0) . $pass) ) ); } /** * Authenticates the user using the LOGIN method. * * @param string $user The userid to authenticate as. * @param string $pass The password to authenticate with. * @param string $euser The effective uid to authenticate as. Not used. * * @throws \Horde\ManageSieve\Exception */ protected function _authLOGIN($user, $pass, $euser) { $this->_sendCmd('AUTHENTICATE "LOGIN"'); $this->_doCmd('"' . base64_encode($user) . '"', true); $this->_doCmd('"' . base64_encode($pass) . '"', true); } /** * Authenticates the user using the CRAM-MD5 method. * * @param string $user The userid to authenticate as. * @param string $pass The password to authenticate with. * @param string $euser The effective uid to authenticate as. Not used. * * @throws \Horde\ManageSieve\Exception */ protected function _authCRAMMD5($user, $pass, $euser) { $challenge = $this->_doCmd('AUTHENTICATE "CRAM-MD5"', true); $challenge = base64_decode(trim($challenge)); $cram = Auth_SASL::factory('crammd5'); $response = $cram->getResponse($user, $pass, $challenge); if (is_a($response, 'PEAR_Error')) { throw new Exception($response); } $this->_sendStringResponse(base64_encode($response)); } /** * Authenticates the user using the DIGEST-MD5 method. * * @param string $user The userid to authenticate as. * @param string $pass The password to authenticate with. * @param string $euser The effective uid to authenticate as. * * @throws \Horde\ManageSieve\Exception */ protected function _authDigestMD5($user, $pass, $euser) { $challenge = $this->_doCmd('AUTHENTICATE "DIGEST-MD5"', true); $challenge = base64_decode(trim($challenge)); $digest = Auth_SASL::factory('digestmd5'); // @todo Really 'localhost'? $response = $digest->getResponse( $user, $pass, $challenge, 'localhost', 'sieve', $euser ); if (is_a($response, 'PEAR_Error')) { throw new Exception($response); } $this->_sendStringResponse(base64_encode($response)); $this->_doCmd('', true); if (\Horde_String::upper(substr($result, 0, 2)) == 'OK') { return; } /* We don't use the protocol's third step because SIEVE doesn't allow * subsequent authentication, so we just silently ignore it. */ $this->_sendStringResponse(''); $this->_doCmd(); } /** * Authenticates the user using the EXTERNAL method. * * @param string $user The userid to authenticate as. * @param string $pass The password to authenticate with. * @param string $euser The effective uid to authenticate as. * * @throws \Horde\ManageSieve\Exception */ protected function _authEXTERNAL($user, $pass, $euser) { $cmd = sprintf( 'AUTHENTICATE "EXTERNAL" "%s"', base64_encode(strlen($euser) ? $euser : $user) ); return $this->_sendCmd($cmd); } /** * Removes a script from the server. * * @param string $scriptname Name of the script to delete. * * @throws \Horde\ManageSieve\Exception */ protected function _cmdDeleteScript($scriptname) { $this->_checkAuthenticated(); $this->_doCmd(sprintf('DELETESCRIPT %s', $this->_escape($scriptname))); } /** * Retrieves the contents of the named script. * * @param string $scriptname Name of the script to retrieve. * * @throws \Horde\ManageSieve\Exception * @return string The script. */ protected function _cmdGetScript($scriptname) { $this->_checkAuthenticated(); $result = $this->_doCmd( sprintf('GETSCRIPT %s', $this->_escape($scriptname)) ); return preg_replace('/^{[0-9]+}\r\n/', '', $result); } /** * Sets the active script, i.e. the one that gets run on new mail by the * server. * * @param string $scriptname The name of the script to mark as active. * * @throws \Horde\ManageSieve\Exception */ protected function _cmdSetActive($scriptname) { $this->_checkAuthenticated(); $this->_doCmd(sprintf('SETACTIVE %s', $this->_escape($scriptname))); } /** * Returns the list of scripts on the server. * * @throws \Horde\ManageSieve\Exception * @return array An array with the list of scripts in the first element * and the active script in the second element. */ protected function _cmdListScripts() { $this->_checkAuthenticated(); $result = $this->_doCmd('LISTSCRIPTS'); $scripts = array(); $activescript = null; $result = explode("\r\n", $result); foreach ($result as $value) { if (preg_match('/^"(.*)"( ACTIVE)?$/i', $value, $matches)) { $script_name = stripslashes($matches[1]); $scripts[] = $script_name; if (!empty($matches[2])) { $activescript = $script_name; } } } return array($scripts, $activescript); } /** * Adds a script to the server. * * @param string $scriptname Name of the new script. * @param string $scriptdata The new script. * * @throws \Horde\ManageSieve\Exception */ protected function _cmdPutScript($scriptname, $scriptdata) { $this->_checkAuthenticated(); $command = sprintf( "PUTSCRIPT %s {%d+}\r\n%s", $this->_escape($scriptname), strlen($scriptdata), $scriptdata ); $this->_doCmd($command); } /** * Logs out of the server and terminates the connection. * * @param boolean $sendLogoutCMD Whether to send LOGOUT command before * disconnecting. * * @throws \Horde\ManageSieve\Exception */ protected function _cmdLogout($sendLogoutCMD = true) { $this->_checkConnected(); if ($sendLogoutCMD) { $this->_doCmd('LOGOUT'); } $this->_sock->close(); $this->_state = self::STATE_DISCONNECTED; } /** * Sends the CAPABILITY command * * @throws \Horde\ManageSieve\Exception */ protected function _cmdCapability() { $this->_checkConnected(); $result = $this->_doCmd('CAPABILITY'); $this->_parseCapability($result); } /** * Parses the response from the CAPABILITY command and stores the result * in $_capability. * * @param string $data The response from the capability command. */ protected function _parseCapability($data) { // Clear the cached capabilities. $this->_capability = array( 'sasl' => array(), 'extensions' => array() ); $data = preg_split( '/\r?\n/', \Horde_String::upper($data), -1, PREG_SPLIT_NO_EMPTY ); for ($i = 0; $i < count($data); $i++) { if (!preg_match('/^"([A-Z]+)"( "(.*)")?$/', $data[$i], $matches)) { continue; } switch ($matches[1]) { case 'IMPLEMENTATION': $this->_capability['implementation'] = $matches[3]; break; case 'SASL': $this->_capability['sasl'] = preg_split('/\s+/', $matches[3]); break; case 'SIEVE': $this->_capability['extensions'] = preg_split('/\s+/', $matches[3]); break; case 'STARTTLS': $this->_capability['starttls'] = true; break; } } } /** * Sends a command to the server * * @param string $cmd The command to send. */ protected function _sendCmd($cmd) { $status = $this->_sock->getStatus(); if ($status['eof']) { throw new Exception('Failed to write to socket: connection lost'); } $this->_sock->write($cmd . "\r\n"); $this->_debug("C: $cmd"); } /** * Sends a string response to the server. * * @param string $str The string to send. */ protected function _sendStringResponse($str) { return $this->_sendCmd('{' . strlen($str) . "+}\r\n" . $str); } /** * Receives a single line from the server. * * @return string The server response line. */ protected function _recvLn() { $lastline = rtrim($this->_sock->gets(8192)); $this->_debug("S: $lastline"); if ($lastline === '') { throw new Exception('Failed to read from socket'); } return $lastline; } /** * Receives a number of bytes from the server. * * @param integer $length Number of bytes to read. * * @return string The server response. */ protected function _recvBytes($length) { $response = ''; $response_length = 0; while ($response_length < $length) { $response .= $this->_sock->read($length - $response_length); $response_length = strlen($response); } $this->_debug('S: ' . rtrim($response)); return $response; } /** * Send a command and retrieves a response from the server. * * @param string $cmd The command to send. * @param boolean $auth Whether this is an authentication command. * * @throws \Horde\ManageSieve\Exception if a NO response. * @return string Reponse string if an OK response. * */ protected function _doCmd($cmd = '', $auth = false) { $referralCount = 0; while ($referralCount < $this->_maxReferralCount) { if (strlen($cmd)) { $this->_sendCmd($cmd); } $response = ''; while (true) { $line = $this->_recvLn(); if (preg_match('/^(OK|NO)/i', $line, $tag)) { // Check for string literal message. // DBMail has some broken versions that send the trailing // plus even though it's disallowed. if (preg_match('/{([0-9]+)\+?}$/', $line, $matches)) { $line = substr($line, 0, -(strlen($matches[1]) + 2)) . str_replace( "\r\n", ' ', $this->_recvBytes($matches[1] + 2) ); } if ('OK' == \Horde_String::upper($tag[1])) { $response .= $line; return rtrim($response); } throw new Exception(trim($response . substr($line, 2)), 3); } if (preg_match('/^BYE/i', $line)) { try { $this->disconnect(false); } catch (Exception $e) { throw new Exception( 'Cannot handle BYE, the error was: ' . $e->getMessage(), 4 ); } // Check for referral, then follow it. Otherwise, carp an // error. if (preg_match('/^bye \(referral "(sieve:\/\/)?([^"]+)/i', $line, $matches)) { // Replace the old host with the referral host // preserving any protocol prefix. $this->_params['host'] = preg_replace( '/\w+(?!(\w|\:\/\/)).*/', $matches[2], $this->_params['host'] ); try { $this->_handleConnectAndLogin(); } catch (Exception $e) { throw new Exception\Referral( 'Cannot follow referral to ' . $this->_params['host'] . ', the error was: ' . $e->getMessage() ); } break; } throw new Exception(trim($response . $line), 6); } if (preg_match('/^{([0-9]+)}/', $line, $matches)) { // Matches literal string responses. $line = $this->_recvBytes($matches[1] + 2); if (!$auth) { // Receive the pending OK only if we aren't // authenticating since string responses during // authentication don't need an OK. $this->_recvLn(); } return $line; } if ($auth) { // String responses during authentication don't need an // OK. $response .= $line; return rtrim($response); } $response .= $line . "\r\n"; $referralCount++; } } throw new Exception\Referral('Max referral count (' . $referralCount . ') reached.'); } /** * Returns the name of the best authentication method that the server * has advertised. * * @param string $authmethod Only consider this method as available. * * @throws \Horde\ManageSieve\Exception * @return string The name of the best supported authentication method. */ protected function _getBestAuthMethod($authmethod = null) { if (!isset($this->_capability['sasl'])) { throw new Exception( 'This server doesn\'t support any authentication methods. SASL problem?' ); } if (!$this->_capability['sasl']) { throw new Exception( 'This server doesn\'t support any authentication methods.' ); } if ($authmethod) { if (in_array($authmethod, $this->_capability['sasl'])) { return $authmethod; } throw new Exception( sprintf( 'No supported authentication method found. The server supports these methods: %s, but we want to use: %s', implode(', ', $this->_capability['sasl']), $authmethod ) ); } foreach ($this->supportedAuthMethods as $method) { if (in_array($method, $this->_capability['sasl'])) { return $method; } } throw new Exception( sprintf( 'No supported authentication method found. The server supports these methods: %s, but we only support: %s', implode(', ', $this->_capability['sasl']), implode(', ', $this->supportedAuthMethods) ) ); } /** * Asserts that the client is in disconnected state. * * @throws \Horde\ManageSieve\Exception */ protected function _checkConnected() { if (self::STATE_DISCONNECTED == $this->_state) { throw new Exception\NotConnected(); } } /** * Asserts that the client is in authenticated state. * * @throws \Horde\ManageSieve\Exception */ protected function _checkAuthenticated() { if (self::STATE_AUTHENTICATED != $this->_state) { throw new Exception\NotAuthenticated(); } } /** * Converts strings into RFC's quoted-string or literal-c2s form. * * @param string $string The string to convert. * * @return string Result string. */ protected function _escape($string) { // Some implementations don't allow UTF-8 characters in quoted-string, // use literal-c2s. if (preg_match('/[^\x01-\x09\x0B-\x0C\x0E-\x7F]/', $string)) { return sprintf("{%d+}\r\n%s", strlen($string), $string); } return '"' . addcslashes($string, '\\"') . '"'; } /** * Write debug text to the current log handler. * * @param string $message Debug message text. */ protected function _debug($message) { if ($this->_logger) { $this->_logger->debug($message); } } }