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