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