PageRenderTime 73ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 0ms

/php-pear-Net-Sieve-1.3.2/Net_Sieve-1.3.2/Sieve.php

#
PHP | 1274 lines | 634 code | 124 blank | 516 comment | 126 complexity | d31a3505b95cd7f3ae2d17594e397a70 MD5 | raw file
  1. <?php
  2. /**
  3. * This file contains the Net_Sieve class.
  4. *
  5. * PHP version 4
  6. *
  7. * +-----------------------------------------------------------------------+
  8. * | All rights reserved. |
  9. * | |
  10. * | Redistribution and use in source and binary forms, with or without |
  11. * | modification, are permitted provided that the following conditions |
  12. * | are met: |
  13. * | |
  14. * | o Redistributions of source code must retain the above copyright |
  15. * | notice, this list of conditions and the following disclaimer. |
  16. * | o Redistributions in binary form must reproduce the above copyright |
  17. * | notice, this list of conditions and the following disclaimer in the |
  18. * | documentation and/or other materials provided with the distribution.|
  19. * | |
  20. * | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
  21. * | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
  22. * | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
  23. * | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
  24. * | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
  25. * | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
  26. * | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
  27. * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
  28. * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
  29. * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
  30. * | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
  31. * +-----------------------------------------------------------------------+
  32. *
  33. * @category Networking
  34. * @package Net_Sieve
  35. * @author Richard Heyes <richard@phpguru.org>
  36. * @author Damian Fernandez Sosa <damlists@cnba.uba.ar>
  37. * @author Anish Mistry <amistry@am-productions.biz>
  38. * @author Jan Schneider <jan@horde.org>
  39. * @copyright 2002-2003 Richard Heyes
  40. * @copyright 2006-2008 Anish Mistry
  41. * @license http://www.opensource.org/licenses/bsd-license.php BSD
  42. * @version SVN: $Id$
  43. * @link http://pear.php.net/package/Net_Sieve
  44. */
  45. require_once 'PEAR.php';
  46. require_once 'Net/Socket.php';
  47. /**
  48. * TODO
  49. *
  50. * o supportsAuthMech()
  51. */
  52. /**
  53. * Disconnected state
  54. * @const NET_SIEVE_STATE_DISCONNECTED
  55. */
  56. define('NET_SIEVE_STATE_DISCONNECTED', 1, true);
  57. /**
  58. * Authorisation state
  59. * @const NET_SIEVE_STATE_AUTHORISATION
  60. */
  61. define('NET_SIEVE_STATE_AUTHORISATION', 2, true);
  62. /**
  63. * Transaction state
  64. * @const NET_SIEVE_STATE_TRANSACTION
  65. */
  66. define('NET_SIEVE_STATE_TRANSACTION', 3, true);
  67. /**
  68. * A class for talking to the timsieved server which comes with Cyrus IMAP.
  69. *
  70. * @category Networking
  71. * @package Net_Sieve
  72. * @author Richard Heyes <richard@phpguru.org>
  73. * @author Damian Fernandez Sosa <damlists@cnba.uba.ar>
  74. * @author Anish Mistry <amistry@am-productions.biz>
  75. * @author Jan Schneider <jan@horde.org>
  76. * @copyright 2002-2003 Richard Heyes
  77. * @copyright 2006-2008 Anish Mistry
  78. * @license http://www.opensource.org/licenses/bsd-license.php BSD
  79. * @version Release: 1.3.2
  80. * @link http://pear.php.net/package/Net_Sieve
  81. * @link http://tools.ietf.org/html/rfc5228 RFC 5228 (Sieve: An Email
  82. * Filtering Language)
  83. * @link http://tools.ietf.org/html/rfc5804 RFC 5804 A Protocol for
  84. * Remotely Managing Sieve Scripts
  85. */
  86. class Net_Sieve
  87. {
  88. /**
  89. * The authentication methods this class supports.
  90. *
  91. * Can be overwritten if having problems with certain methods.
  92. *
  93. * @var array
  94. */
  95. var $supportedAuthMethods = array('DIGEST-MD5', 'CRAM-MD5', 'EXTERNAL',
  96. 'PLAIN' , 'LOGIN');
  97. /**
  98. * SASL authentication methods that require Auth_SASL.
  99. *
  100. * @var array
  101. */
  102. var $supportedSASLAuthMethods = array('DIGEST-MD5', 'CRAM-MD5');
  103. /**
  104. * The socket handle.
  105. *
  106. * @var resource
  107. */
  108. var $_sock;
  109. /**
  110. * Parameters and connection information.
  111. *
  112. * @var array
  113. */
  114. var $_data;
  115. /**
  116. * Current state of the connection.
  117. *
  118. * One of the NET_SIEVE_STATE_* constants.
  119. *
  120. * @var integer
  121. */
  122. var $_state;
  123. /**
  124. * Constructor error.
  125. *
  126. * @var PEAR_Error
  127. */
  128. var $_error;
  129. /**
  130. * Whether to enable debugging.
  131. *
  132. * @var boolean
  133. */
  134. var $_debug = false;
  135. /**
  136. * Debug output handler.
  137. *
  138. * This has to be a valid callback.
  139. *
  140. * @var string|array
  141. */
  142. var $_debug_handler = null;
  143. /**
  144. * Whether to pick up an already established connection.
  145. *
  146. * @var boolean
  147. */
  148. var $_bypassAuth = false;
  149. /**
  150. * Whether to use TLS if available.
  151. *
  152. * @var boolean
  153. */
  154. var $_useTLS = true;
  155. /**
  156. * Additional options for stream_context_create().
  157. *
  158. * @var array
  159. */
  160. var $_options = null;
  161. /**
  162. * Maximum number of referral loops
  163. *
  164. * @var array
  165. */
  166. var $_maxReferralCount = 15;
  167. /**
  168. * Constructor.
  169. *
  170. * Sets up the object, connects to the server and logs in. Stores any
  171. * generated error in $this->_error, which can be retrieved using the
  172. * getError() method.
  173. *
  174. * @param string $user Login username.
  175. * @param string $pass Login password.
  176. * @param string $host Hostname of server.
  177. * @param string $port Port of server.
  178. * @param string $logintype Type of login to perform (see
  179. * $supportedAuthMethods).
  180. * @param string $euser Effective user. If authenticating as an
  181. * administrator, login as this user.
  182. * @param boolean $debug Whether to enable debugging (@see setDebug()).
  183. * @param string $bypassAuth Skip the authentication phase. Useful if the
  184. * socket is already open.
  185. * @param boolean $useTLS Use TLS if available.
  186. * @param array $options Additional options for
  187. * stream_context_create().
  188. * @param mixed $handler A callback handler for the debug output.
  189. */
  190. function Net_Sieve($user = null, $pass = null, $host = 'localhost',
  191. $port = 2000, $logintype = '', $euser = '',
  192. $debug = false, $bypassAuth = false, $useTLS = true,
  193. $options = null, $handler = null)
  194. {
  195. $this->_state = NET_SIEVE_STATE_DISCONNECTED;
  196. $this->_data['user'] = $user;
  197. $this->_data['pass'] = $pass;
  198. $this->_data['host'] = $host;
  199. $this->_data['port'] = $port;
  200. $this->_data['logintype'] = $logintype;
  201. $this->_data['euser'] = $euser;
  202. $this->_sock = new Net_Socket();
  203. $this->_bypassAuth = $bypassAuth;
  204. $this->_useTLS = $useTLS;
  205. $this->_options = $options;
  206. $this->setDebug($debug, $handler);
  207. /* Try to include the Auth_SASL package. If the package is not
  208. * available, we disable the authentication methods that depend upon
  209. * it. */
  210. if ((@include_once 'Auth/SASL.php') === false) {
  211. $this->_debug('Auth_SASL not present');
  212. foreach ($this->supportedSASLAuthMethods as $SASLMethod) {
  213. $pos = array_search($SASLMethod, $this->supportedAuthMethods);
  214. $this->_debug('Disabling method ' . $SASLMethod);
  215. unset($this->supportedAuthMethods[$pos]);
  216. }
  217. }
  218. if (strlen($user) && strlen($pass)) {
  219. $this->_error = $this->_handleConnectAndLogin();
  220. }
  221. }
  222. /**
  223. * Returns any error that may have been generated in the constructor.
  224. *
  225. * @return boolean|PEAR_Error False if no error, PEAR_Error otherwise.
  226. */
  227. function getError()
  228. {
  229. return PEAR::isError($this->_error) ? $this->_error : false;
  230. }
  231. /**
  232. * Sets the debug state and handler function.
  233. *
  234. * @param boolean $debug Whether to enable debugging.
  235. * @param string $handler A custom debug handler. Must be a valid callback.
  236. *
  237. * @return void
  238. */
  239. function setDebug($debug = true, $handler = null)
  240. {
  241. $this->_debug = $debug;
  242. $this->_debug_handler = $handler;
  243. }
  244. /**
  245. * Connects to the server and logs in.
  246. *
  247. * @return boolean True on success, PEAR_Error on failure.
  248. */
  249. function _handleConnectAndLogin()
  250. {
  251. if (PEAR::isError($res = $this->connect($this->_data['host'], $this->_data['port'], $this->_options, $this->_useTLS))) {
  252. return $res;
  253. }
  254. if ($this->_bypassAuth === false) {
  255. if (PEAR::isError($res = $this->login($this->_data['user'], $this->_data['pass'], $this->_data['logintype'], $this->_data['euser'], $this->_bypassAuth))) {
  256. return $res;
  257. }
  258. }
  259. return true;
  260. }
  261. /**
  262. * Handles connecting to the server and checks the response validity.
  263. *
  264. * @param string $host Hostname of server.
  265. * @param string $port Port of server.
  266. * @param array $options List of options to pass to
  267. * stream_context_create().
  268. * @param boolean $useTLS Use TLS if available.
  269. *
  270. * @return boolean True on success, PEAR_Error otherwise.
  271. */
  272. function connect($host, $port, $options = null, $useTLS = true)
  273. {
  274. $this->_data['host'] = $host;
  275. $this->_data['port'] = $port;
  276. $this->_useTLS = $useTLS;
  277. if (is_array($options)) {
  278. $this->_options = array_merge($this->_options, $options);
  279. }
  280. if (NET_SIEVE_STATE_DISCONNECTED != $this->_state) {
  281. return PEAR::raiseError('Not currently in DISCONNECTED state', 1);
  282. }
  283. if (PEAR::isError($res = $this->_sock->connect($host, $port, false, 5, $options))) {
  284. return $res;
  285. }
  286. if ($this->_bypassAuth) {
  287. $this->_state = NET_SIEVE_STATE_TRANSACTION;
  288. } else {
  289. $this->_state = NET_SIEVE_STATE_AUTHORISATION;
  290. if (PEAR::isError($res = $this->_doCmd())) {
  291. return $res;
  292. }
  293. }
  294. // Explicitly ask for the capabilities in case the connection is
  295. // picked up from an existing connection.
  296. if (PEAR::isError($res = $this->_cmdCapability())) {
  297. return PEAR::raiseError(
  298. 'Failed to connect, server said: ' . $res->getMessage(), 2
  299. );
  300. }
  301. // Check if we can enable TLS via STARTTLS.
  302. if ($useTLS && !empty($this->_capability['starttls'])
  303. && function_exists('stream_socket_enable_crypto')
  304. ) {
  305. if (PEAR::isError($res = $this->_startTLS())) {
  306. return $res;
  307. }
  308. }
  309. return true;
  310. }
  311. /**
  312. * Disconnect from the Sieve server.
  313. *
  314. * @param boolean $sendLogoutCMD Whether to send LOGOUT command before
  315. * disconnecting.
  316. *
  317. * @return boolean True on success, PEAR_Error otherwise.
  318. */
  319. function disconnect($sendLogoutCMD = true)
  320. {
  321. return $this->_cmdLogout($sendLogoutCMD);
  322. }
  323. /**
  324. * Logs into server.
  325. *
  326. * @param string $user Login username.
  327. * @param string $pass Login password.
  328. * @param string $logintype Type of login method to use.
  329. * @param string $euser Effective UID (perform on behalf of $euser).
  330. * @param boolean $bypassAuth Do not perform authentication.
  331. *
  332. * @return boolean True on success, PEAR_Error otherwise.
  333. */
  334. function login($user, $pass, $logintype = null, $euser = '', $bypassAuth = false)
  335. {
  336. $this->_data['user'] = $user;
  337. $this->_data['pass'] = $pass;
  338. $this->_data['logintype'] = $logintype;
  339. $this->_data['euser'] = $euser;
  340. $this->_bypassAuth = $bypassAuth;
  341. if (NET_SIEVE_STATE_AUTHORISATION != $this->_state) {
  342. return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
  343. }
  344. if (!$bypassAuth ) {
  345. if (PEAR::isError($res = $this->_cmdAuthenticate($user, $pass, $logintype, $euser))) {
  346. return $res;
  347. }
  348. }
  349. $this->_state = NET_SIEVE_STATE_TRANSACTION;
  350. return true;
  351. }
  352. /**
  353. * Returns an indexed array of scripts currently on the server.
  354. *
  355. * @return array Indexed array of scriptnames.
  356. */
  357. function listScripts()
  358. {
  359. if (is_array($scripts = $this->_cmdListScripts())) {
  360. $this->_active = $scripts[1];
  361. return $scripts[0];
  362. } else {
  363. return $scripts;
  364. }
  365. }
  366. /**
  367. * Returns the active script.
  368. *
  369. * @return string The active scriptname.
  370. */
  371. function getActive()
  372. {
  373. if (!empty($this->_active)) {
  374. return $this->_active;
  375. }
  376. if (is_array($scripts = $this->_cmdListScripts())) {
  377. $this->_active = $scripts[1];
  378. return $scripts[1];
  379. }
  380. }
  381. /**
  382. * Sets the active script.
  383. *
  384. * @param string $scriptname The name of the script to be set as active.
  385. *
  386. * @return boolean True on success, PEAR_Error on failure.
  387. */
  388. function setActive($scriptname)
  389. {
  390. return $this->_cmdSetActive($scriptname);
  391. }
  392. /**
  393. * Retrieves a script.
  394. *
  395. * @param string $scriptname The name of the script to be retrieved.
  396. *
  397. * @return string The script on success, PEAR_Error on failure.
  398. */
  399. function getScript($scriptname)
  400. {
  401. return $this->_cmdGetScript($scriptname);
  402. }
  403. /**
  404. * Adds a script to the server.
  405. *
  406. * @param string $scriptname Name of the script.
  407. * @param string $script The script content.
  408. * @param boolean $makeactive Whether to make this the active script.
  409. *
  410. * @return boolean True on success, PEAR_Error on failure.
  411. */
  412. function installScript($scriptname, $script, $makeactive = false)
  413. {
  414. if (PEAR::isError($res = $this->_cmdPutScript($scriptname, $script))) {
  415. return $res;
  416. }
  417. if ($makeactive) {
  418. return $this->_cmdSetActive($scriptname);
  419. }
  420. return true;
  421. }
  422. /**
  423. * Removes a script from the server.
  424. *
  425. * @param string $scriptname Name of the script.
  426. *
  427. * @return boolean True on success, PEAR_Error on failure.
  428. */
  429. function removeScript($scriptname)
  430. {
  431. return $this->_cmdDeleteScript($scriptname);
  432. }
  433. /**
  434. * Checks if the server has space to store the script by the server.
  435. *
  436. * @param string $scriptname The name of the script to mark as active.
  437. * @param integer $size The size of the script.
  438. *
  439. * @return boolean|PEAR_Error True if there is space, PEAR_Error otherwise.
  440. *
  441. * @todo Rename to hasSpace()
  442. */
  443. function haveSpace($scriptname, $size)
  444. {
  445. if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
  446. return PEAR::raiseError('Not currently in TRANSACTION state', 1);
  447. }
  448. if (PEAR::isError($res = $this->_doCmd(sprintf('HAVESPACE %s %d', $this->_escape($scriptname), $size)))) {
  449. return $res;
  450. }
  451. return true;
  452. }
  453. /**
  454. * Returns the list of extensions the server supports.
  455. *
  456. * @return array List of extensions or PEAR_Error on failure.
  457. */
  458. function getExtensions()
  459. {
  460. if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
  461. return PEAR::raiseError('Not currently connected', 7);
  462. }
  463. return $this->_capability['extensions'];
  464. }
  465. /**
  466. * Returns whether the server supports an extension.
  467. *
  468. * @param string $extension The extension to check.
  469. *
  470. * @return boolean Whether the extension is supported or PEAR_Error on
  471. * failure.
  472. */
  473. function hasExtension($extension)
  474. {
  475. if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
  476. return PEAR::raiseError('Not currently connected', 7);
  477. }
  478. $extension = trim($this->_toUpper($extension));
  479. if (is_array($this->_capability['extensions'])) {
  480. foreach ($this->_capability['extensions'] as $ext) {
  481. if ($ext == $extension) {
  482. return true;
  483. }
  484. }
  485. }
  486. return false;
  487. }
  488. /**
  489. * Returns the list of authentication methods the server supports.
  490. *
  491. * @return array List of authentication methods or PEAR_Error on failure.
  492. */
  493. function getAuthMechs()
  494. {
  495. if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
  496. return PEAR::raiseError('Not currently connected', 7);
  497. }
  498. return $this->_capability['sasl'];
  499. }
  500. /**
  501. * Returns whether the server supports an authentication method.
  502. *
  503. * @param string $method The method to check.
  504. *
  505. * @return boolean Whether the method is supported or PEAR_Error on
  506. * failure.
  507. */
  508. function hasAuthMech($method)
  509. {
  510. if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
  511. return PEAR::raiseError('Not currently connected', 7);
  512. }
  513. $method = trim($this->_toUpper($method));
  514. if (is_array($this->_capability['sasl'])) {
  515. foreach ($this->_capability['sasl'] as $sasl) {
  516. if ($sasl == $method) {
  517. return true;
  518. }
  519. }
  520. }
  521. return false;
  522. }
  523. /**
  524. * Handles the authentication using any known method.
  525. *
  526. * @param string $uid The userid to authenticate as.
  527. * @param string $pwd The password to authenticate with.
  528. * @param string $userMethod The method to use. If empty, the class chooses
  529. * the best (strongest) available method.
  530. * @param string $euser The effective uid to authenticate as.
  531. *
  532. * @return void
  533. */
  534. function _cmdAuthenticate($uid, $pwd, $userMethod = null, $euser = '')
  535. {
  536. if (PEAR::isError($method = $this->_getBestAuthMethod($userMethod))) {
  537. return $method;
  538. }
  539. switch ($method) {
  540. case 'DIGEST-MD5':
  541. return $this->_authDigestMD5($uid, $pwd, $euser);
  542. case 'CRAM-MD5':
  543. $result = $this->_authCRAMMD5($uid, $pwd, $euser);
  544. break;
  545. case 'LOGIN':
  546. $result = $this->_authLOGIN($uid, $pwd, $euser);
  547. break;
  548. case 'PLAIN':
  549. $result = $this->_authPLAIN($uid, $pwd, $euser);
  550. break;
  551. case 'EXTERNAL':
  552. $result = $this->_authEXTERNAL($uid, $pwd, $euser);
  553. break;
  554. default :
  555. $result = PEAR::raiseError(
  556. $method . ' is not a supported authentication method'
  557. );
  558. break;
  559. }
  560. if (PEAR::isError($res = $this->_doCmd())) {
  561. return $res;
  562. }
  563. // Query the server capabilities again now that we are authenticated.
  564. if (PEAR::isError($res = $this->_cmdCapability())) {
  565. return PEAR::raiseError(
  566. 'Failed to connect, server said: ' . $res->getMessage(), 2
  567. );
  568. }
  569. return $result;
  570. }
  571. /**
  572. * Authenticates the user using the PLAIN method.
  573. *
  574. * @param string $user The userid to authenticate as.
  575. * @param string $pass The password to authenticate with.
  576. * @param string $euser The effective uid to authenticate as.
  577. *
  578. * @return void
  579. */
  580. function _authPLAIN($user, $pass, $euser)
  581. {
  582. return $this->_sendCmd(
  583. sprintf(
  584. 'AUTHENTICATE "PLAIN" "%s"',
  585. base64_encode($euser . chr(0) . $user . chr(0) . $pass)
  586. )
  587. );
  588. }
  589. /**
  590. * Authenticates the user using the LOGIN method.
  591. *
  592. * @param string $user The userid to authenticate as.
  593. * @param string $pass The password to authenticate with.
  594. * @param string $euser The effective uid to authenticate as.
  595. *
  596. * @return void
  597. */
  598. function _authLOGIN($user, $pass, $euser)
  599. {
  600. if (PEAR::isError($result = $this->_sendCmd('AUTHENTICATE "LOGIN"'))) {
  601. return $result;
  602. }
  603. if (PEAR::isError($result = $this->_doCmd('"' . base64_encode($user) . '"', true))) {
  604. return $result;
  605. }
  606. return $this->_doCmd('"' . base64_encode($pass) . '"', true);
  607. }
  608. /**
  609. * Authenticates the user using the CRAM-MD5 method.
  610. *
  611. * @param string $user The userid to authenticate as.
  612. * @param string $pass The password to authenticate with.
  613. * @param string $euser The effective uid to authenticate as.
  614. *
  615. * @return void
  616. */
  617. function _authCRAMMD5($user, $pass, $euser)
  618. {
  619. if (PEAR::isError($challenge = $this->_doCmd('AUTHENTICATE "CRAM-MD5"', true))) {
  620. return $challenge;
  621. }
  622. $challenge = base64_decode(trim($challenge));
  623. $cram = Auth_SASL::factory('crammd5');
  624. if (PEAR::isError($response = $cram->getResponse($user, $pass, $challenge))) {
  625. return $response;
  626. }
  627. return $this->_sendStringResponse(base64_encode($response));
  628. }
  629. /**
  630. * Authenticates the user using the DIGEST-MD5 method.
  631. *
  632. * @param string $user The userid to authenticate as.
  633. * @param string $pass The password to authenticate with.
  634. * @param string $euser The effective uid to authenticate as.
  635. *
  636. * @return void
  637. */
  638. function _authDigestMD5($user, $pass, $euser)
  639. {
  640. if (PEAR::isError($challenge = $this->_doCmd('AUTHENTICATE "DIGEST-MD5"', true))) {
  641. return $challenge;
  642. }
  643. $challenge = base64_decode(trim($challenge));
  644. $digest = Auth_SASL::factory('digestmd5');
  645. // @todo Really 'localhost'?
  646. if (PEAR::isError($response = $digest->getResponse($user, $pass, $challenge, 'localhost', 'sieve', $euser))) {
  647. return $response;
  648. }
  649. if (PEAR::isError($result = $this->_sendStringResponse(base64_encode($response)))) {
  650. return $result;
  651. }
  652. if (PEAR::isError($result = $this->_doCmd('', true))) {
  653. return $result;
  654. }
  655. if ($this->_toUpper(substr($result, 0, 2)) == 'OK') {
  656. return;
  657. }
  658. /* We don't use the protocol's third step because SIEVE doesn't allow
  659. * subsequent authentication, so we just silently ignore it. */
  660. if (PEAR::isError($result = $this->_sendStringResponse(''))) {
  661. return $result;
  662. }
  663. return $this->_doCmd();
  664. }
  665. /**
  666. * Authenticates the user using the EXTERNAL method.
  667. *
  668. * @param string $user The userid to authenticate as.
  669. * @param string $pass The password to authenticate with.
  670. * @param string $euser The effective uid to authenticate as.
  671. *
  672. * @return void
  673. *
  674. * @since 1.1.7
  675. */
  676. function _authEXTERNAL($user, $pass, $euser)
  677. {
  678. $cmd = sprintf(
  679. 'AUTHENTICATE "EXTERNAL" "%s"',
  680. base64_encode(strlen($euser) ? $euser : $user)
  681. );
  682. return $this->_sendCmd($cmd);
  683. }
  684. /**
  685. * Removes a script from the server.
  686. *
  687. * @param string $scriptname Name of the script to delete.
  688. *
  689. * @return boolean True on success, PEAR_Error otherwise.
  690. */
  691. function _cmdDeleteScript($scriptname)
  692. {
  693. if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
  694. return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
  695. }
  696. if (PEAR::isError($res = $this->_doCmd(sprintf('DELETESCRIPT %s', $this->_escape($scriptname))))) {
  697. return $res;
  698. }
  699. return true;
  700. }
  701. /**
  702. * Retrieves the contents of the named script.
  703. *
  704. * @param string $scriptname Name of the script to retrieve.
  705. *
  706. * @return string The script if successful, PEAR_Error otherwise.
  707. */
  708. function _cmdGetScript($scriptname)
  709. {
  710. if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
  711. return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
  712. }
  713. if (PEAR::isError($res = $this->_doCmd(sprintf('GETSCRIPT %s', $this->_escape($scriptname))))) {
  714. return $res;
  715. }
  716. return preg_replace('/^{[0-9]+}\r\n/', '', $res);
  717. }
  718. /**
  719. * Sets the active script, i.e. the one that gets run on new mail by the
  720. * server.
  721. *
  722. * @param string $scriptname The name of the script to mark as active.
  723. *
  724. * @return boolean True on success, PEAR_Error otherwise.
  725. */
  726. function _cmdSetActive($scriptname)
  727. {
  728. if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
  729. return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
  730. }
  731. if (PEAR::isError($res = $this->_doCmd(sprintf('SETACTIVE %s', $this->_escape($scriptname))))) {
  732. return $res;
  733. }
  734. $this->_activeScript = $scriptname;
  735. return true;
  736. }
  737. /**
  738. * Returns the list of scripts on the server.
  739. *
  740. * @return array An array with the list of scripts in the first element
  741. * and the active script in the second element on success,
  742. * PEAR_Error otherwise.
  743. */
  744. function _cmdListScripts()
  745. {
  746. if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
  747. return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
  748. }
  749. if (PEAR::isError($res = $this->_doCmd('LISTSCRIPTS'))) {
  750. return $res;
  751. }
  752. $scripts = array();
  753. $activescript = null;
  754. $res = explode("\r\n", $res);
  755. foreach ($res as $value) {
  756. if (preg_match('/^"(.*)"( ACTIVE)?$/i', $value, $matches)) {
  757. $script_name = stripslashes($matches[1]);
  758. $scripts[] = $script_name;
  759. if (!empty($matches[2])) {
  760. $activescript = $script_name;
  761. }
  762. }
  763. }
  764. return array($scripts, $activescript);
  765. }
  766. /**
  767. * Adds a script to the server.
  768. *
  769. * @param string $scriptname Name of the new script.
  770. * @param string $scriptdata The new script.
  771. *
  772. * @return boolean True on success, PEAR_Error otherwise.
  773. */
  774. function _cmdPutScript($scriptname, $scriptdata)
  775. {
  776. if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
  777. return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
  778. }
  779. $stringLength = $this->_getLineLength($scriptdata);
  780. $command = sprintf("PUTSCRIPT %s {%d+}\r\n%s",
  781. $this->_escape($scriptname),
  782. $stringLength,
  783. $scriptdata);
  784. if (PEAR::isError($res = $this->_doCmd($command))) {
  785. return $res;
  786. }
  787. return true;
  788. }
  789. /**
  790. * Logs out of the server and terminates the connection.
  791. *
  792. * @param boolean $sendLogoutCMD Whether to send LOGOUT command before
  793. * disconnecting.
  794. *
  795. * @return boolean True on success, PEAR_Error otherwise.
  796. */
  797. function _cmdLogout($sendLogoutCMD = true)
  798. {
  799. if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
  800. return PEAR::raiseError('Not currently connected', 1);
  801. }
  802. if ($sendLogoutCMD) {
  803. if (PEAR::isError($res = $this->_doCmd('LOGOUT'))) {
  804. return $res;
  805. }
  806. }
  807. $this->_sock->disconnect();
  808. $this->_state = NET_SIEVE_STATE_DISCONNECTED;
  809. return true;
  810. }
  811. /**
  812. * Sends the CAPABILITY command
  813. *
  814. * @return boolean True on success, PEAR_Error otherwise.
  815. */
  816. function _cmdCapability()
  817. {
  818. if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
  819. return PEAR::raiseError('Not currently connected', 1);
  820. }
  821. if (PEAR::isError($res = $this->_doCmd('CAPABILITY'))) {
  822. return $res;
  823. }
  824. $this->_parseCapability($res);
  825. return true;
  826. }
  827. /**
  828. * Parses the response from the CAPABILITY command and stores the result
  829. * in $_capability.
  830. *
  831. * @param string $data The response from the capability command.
  832. *
  833. * @return void
  834. */
  835. function _parseCapability($data)
  836. {
  837. // Clear the cached capabilities.
  838. $this->_capability = array('sasl' => array(),
  839. 'extensions' => array());
  840. $data = preg_split('/\r?\n/', $this->_toUpper($data), -1, PREG_SPLIT_NO_EMPTY);
  841. for ($i = 0; $i < count($data); $i++) {
  842. if (!preg_match('/^"([A-Z]+)"( "(.*)")?$/', $data[$i], $matches)) {
  843. continue;
  844. }
  845. switch ($matches[1]) {
  846. case 'IMPLEMENTATION':
  847. $this->_capability['implementation'] = $matches[3];
  848. break;
  849. case 'SASL':
  850. $this->_capability['sasl'] = preg_split('/\s+/', $matches[3]);
  851. break;
  852. case 'SIEVE':
  853. $this->_capability['extensions'] = preg_split('/\s+/', $matches[3]);
  854. break;
  855. case 'STARTTLS':
  856. $this->_capability['starttls'] = true;
  857. break;
  858. }
  859. }
  860. }
  861. /**
  862. * Sends a command to the server
  863. *
  864. * @param string $cmd The command to send.
  865. *
  866. * @return void
  867. */
  868. function _sendCmd($cmd)
  869. {
  870. $status = $this->_sock->getStatus();
  871. if (PEAR::isError($status) || $status['eof']) {
  872. return PEAR::raiseError('Failed to write to socket: connection lost');
  873. }
  874. if (PEAR::isError($error = $this->_sock->write($cmd . "\r\n"))) {
  875. return PEAR::raiseError(
  876. 'Failed to write to socket: ' . $error->getMessage()
  877. );
  878. }
  879. $this->_debug("C: $cmd");
  880. }
  881. /**
  882. * Sends a string response to the server.
  883. *
  884. * @param string $str The string to send.
  885. *
  886. * @return void
  887. */
  888. function _sendStringResponse($str)
  889. {
  890. return $this->_sendCmd('{' . $this->_getLineLength($str) . "+}\r\n" . $str);
  891. }
  892. /**
  893. * Receives a single line from the server.
  894. *
  895. * @return string The server response line.
  896. */
  897. function _recvLn()
  898. {
  899. if (PEAR::isError($lastline = $this->_sock->gets(8192))) {
  900. return PEAR::raiseError(
  901. 'Failed to read from socket: ' . $lastline->getMessage()
  902. );
  903. }
  904. $lastline = rtrim($lastline);
  905. $this->_debug("S: $lastline");
  906. if ($lastline === '') {
  907. return PEAR::raiseError('Failed to read from socket');
  908. }
  909. return $lastline;
  910. }
  911. /**
  912. * Receives a number of bytes from the server.
  913. *
  914. * @param integer $length Number of bytes to read.
  915. *
  916. * @return string The server response.
  917. */
  918. function _recvBytes($length)
  919. {
  920. $response = '';
  921. $response_length = 0;
  922. while ($response_length < $length) {
  923. $response .= $this->_sock->read($length - $response_length);
  924. $response_length = $this->_getLineLength($response);
  925. }
  926. $this->_debug('S: ' . rtrim($response));
  927. return $response;
  928. }
  929. /**
  930. * Send a command and retrieves a response from the server.
  931. *
  932. * @param string $cmd The command to send.
  933. * @param boolean $auth Whether this is an authentication command.
  934. *
  935. * @return string|PEAR_Error Reponse string if an OK response, PEAR_Error
  936. * if a NO response.
  937. */
  938. function _doCmd($cmd = '', $auth = false)
  939. {
  940. $referralCount = 0;
  941. while ($referralCount < $this->_maxReferralCount) {
  942. if (strlen($cmd)) {
  943. if (PEAR::isError($error = $this->_sendCmd($cmd))) {
  944. return $error;
  945. }
  946. }
  947. $response = '';
  948. while (true) {
  949. if (PEAR::isError($line = $this->_recvLn())) {
  950. return $line;
  951. }
  952. $uc_line = $this->_toUpper($line);
  953. if ('OK' == substr($uc_line, 0, 2)) {
  954. $response .= $line;
  955. return rtrim($response);
  956. }
  957. if ('NO' == substr($uc_line, 0, 2)) {
  958. // Check for string literal error message.
  959. if (preg_match('/{([0-9]+)}$/', $line, $matches)) {
  960. $line = substr($line, 0, -(strlen($matches[1])+2))
  961. . str_replace(
  962. "\r\n", ' ', $this->_recvBytes($matches[1] + 2)
  963. );
  964. }
  965. return PEAR::raiseError(trim($response . substr($line, 2)), 3);
  966. }
  967. if ('BYE' == substr($uc_line, 0, 3)) {
  968. if (PEAR::isError($error = $this->disconnect(false))) {
  969. return PEAR::raiseError(
  970. 'Cannot handle BYE, the error was: '
  971. . $error->getMessage(),
  972. 4
  973. );
  974. }
  975. // Check for referral, then follow it. Otherwise, carp an
  976. // error.
  977. if (preg_match('/^bye \(referral "(sieve:\/\/)?([^"]+)/i', $line, $matches)) {
  978. // Replace the old host with the referral host
  979. // preserving any protocol prefix.
  980. $this->_data['host'] = preg_replace(
  981. '/\w+(?!(\w|\:\/\/)).*/', $matches[2],
  982. $this->_data['host']
  983. );
  984. if (PEAR::isError($error = $this->_handleConnectAndLogin())) {
  985. return PEAR::raiseError(
  986. 'Cannot follow referral to '
  987. . $this->_data['host'] . ', the error was: '
  988. . $error->getMessage(),
  989. 5
  990. );
  991. }
  992. break;
  993. }
  994. return PEAR::raiseError(trim($response . $line), 6);
  995. }
  996. if (preg_match('/^{([0-9]+)}/', $line, $matches)) {
  997. // Matches literal string responses.
  998. $line = $this->_recvBytes($matches[1] + 2);
  999. if (!$auth) {
  1000. // Receive the pending OK only if we aren't
  1001. // authenticating since string responses during
  1002. // authentication don't need an OK.
  1003. $this->_recvLn();
  1004. }
  1005. return $line;
  1006. }
  1007. if ($auth) {
  1008. // String responses during authentication don't need an
  1009. // OK.
  1010. $response .= $line;
  1011. return rtrim($response);
  1012. }
  1013. $response .= $line . "\r\n";
  1014. $referralCount++;
  1015. }
  1016. }
  1017. return PEAR::raiseError('Max referral count (' . $referralCount . ') reached. Cyrus murder loop error?', 7);
  1018. }
  1019. /**
  1020. * Returns the name of the best authentication method that the server
  1021. * has advertised.
  1022. *
  1023. * @param string $userMethod Only consider this method as available.
  1024. *
  1025. * @return string The name of the best supported authentication method or
  1026. * a PEAR_Error object on failure.
  1027. */
  1028. function _getBestAuthMethod($userMethod = null)
  1029. {
  1030. if (!isset($this->_capability['sasl'])) {
  1031. return PEAR::raiseError('This server doesn\'t support any authentication methods. SASL problem?');
  1032. }
  1033. if (!$this->_capability['sasl']) {
  1034. return PEAR::raiseError('This server doesn\'t support any authentication methods.');
  1035. }
  1036. if ($userMethod) {
  1037. if (in_array($userMethod, $this->_capability['sasl'])) {
  1038. return $userMethod;
  1039. }
  1040. return PEAR::raiseError(
  1041. sprintf('No supported authentication method found. The server supports these methods: %s, but we want to use: %s',
  1042. implode(', ', $this->_capability['sasl']),
  1043. $userMethod));
  1044. }
  1045. foreach ($this->supportedAuthMethods as $method) {
  1046. if (in_array($method, $this->_capability['sasl'])) {
  1047. return $method;
  1048. }
  1049. }
  1050. return PEAR::raiseError(
  1051. sprintf('No supported authentication method found. The server supports these methods: %s, but we only support: %s',
  1052. implode(', ', $this->_capability['sasl']),
  1053. implode(', ', $this->supportedAuthMethods)));
  1054. }
  1055. /**
  1056. * Starts a TLS connection.
  1057. *
  1058. * @return boolean True on success, PEAR_Error on failure.
  1059. */
  1060. function _startTLS()
  1061. {
  1062. if (PEAR::isError($res = $this->_doCmd('STARTTLS'))) {
  1063. return $res;
  1064. }
  1065. if (!stream_socket_enable_crypto($this->_sock->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
  1066. return PEAR::raiseError('Failed to establish TLS connection', 2);
  1067. }
  1068. $this->_debug('STARTTLS negotiation successful');
  1069. // The server should be sending a CAPABILITY response after
  1070. // negotiating TLS. Read it, and ignore if it doesn't.
  1071. // Unfortunately old Cyrus versions are broken and don't send a
  1072. // CAPABILITY response, thus we would wait here forever. Parse the
  1073. // Cyrus version and work around this broken behavior.
  1074. if (!preg_match('/^CYRUS TIMSIEVED V([0-9.]+)/', $this->_capability['implementation'], $matches) ||
  1075. version_compare($matches[1], '2.3.10', '>=')) {
  1076. $this->_doCmd();
  1077. }
  1078. // Query the server capabilities again now that we are under
  1079. // encryption.
  1080. if (PEAR::isError($res = $this->_cmdCapability())) {
  1081. return PEAR::raiseError(
  1082. 'Failed to connect, server said: ' . $res->getMessage(), 2
  1083. );
  1084. }
  1085. return true;
  1086. }
  1087. /**
  1088. * Returns the length of a string.
  1089. *
  1090. * @param string $string A string.
  1091. *
  1092. * @return integer The length of the string.
  1093. */
  1094. function _getLineLength($string)
  1095. {
  1096. if (extension_loaded('mbstring')) {
  1097. return mb_strlen($string, 'latin1');
  1098. } else {
  1099. return strlen($string);
  1100. }
  1101. }
  1102. /**
  1103. * Locale independant strtoupper() implementation.
  1104. *
  1105. * @param string $string The string to convert to lowercase.
  1106. *
  1107. * @return string The lowercased string, based on ASCII encoding.
  1108. */
  1109. function _toUpper($string)
  1110. {
  1111. $language = setlocale(LC_CTYPE, 0);
  1112. setlocale(LC_CTYPE, 'C');
  1113. $string = strtoupper($string);
  1114. setlocale(LC_CTYPE, $language);
  1115. return $string;
  1116. }
  1117. /**
  1118. * Converts strings into RFC's quoted-string or literal-c2s form.
  1119. *
  1120. * @param string $string The string to convert.
  1121. *
  1122. * @return string Result string.
  1123. */
  1124. function _escape($string)
  1125. {
  1126. // Some implementations don't allow UTF-8 characters in quoted-string,
  1127. // use literal-c2s.
  1128. if (preg_match('/[^\x01-\x09\x0B-\x0C\x0E-\x7F]/', $string)) {
  1129. return sprintf("{%d+}\r\n%s", $this->_getLineLength($string), $string);
  1130. }
  1131. return '"' . addcslashes($string, '\\"') . '"';
  1132. }
  1133. /**
  1134. * Write debug text to the current debug output handler.
  1135. *
  1136. * @param string $message Debug message text.
  1137. *
  1138. * @return void
  1139. */
  1140. function _debug($message)
  1141. {
  1142. if ($this->_debug) {
  1143. if ($this->_debug_handler) {
  1144. call_user_func_array($this->_debug_handler, array(&$this, $message));
  1145. } else {
  1146. echo "$message\n";
  1147. }
  1148. }
  1149. }
  1150. }