]> git.donarmstrong.com Git - roundcube.git/blob - plugins/managesieve/lib/Net/Sieve.php
Imported Debian patch 0.5.1+dfsg-7
[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         if (PEAR::isError($res = $this->_doCmd(sprintf('HAVESPACE "%s" %d', $scriptname, $size)))) {
479             return $res;
480         }
481         return true;
482     }
483
484     /**
485      * Returns the list of extensions the server supports.
486      *
487      * @return array  List of extensions or PEAR_Error on failure.
488      */
489     function getExtensions()
490     {
491         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
492             return PEAR::raiseError('Not currently connected', 7);
493         }
494         return $this->_capability['extensions'];
495     }
496
497     /**
498      * Returns whether the server supports an extension.
499      *
500      * @param string $extension The extension to check.
501      *
502      * @return boolean  Whether the extension is supported or PEAR_Error on
503      *                  failure.
504      */
505     function hasExtension($extension)
506     {
507         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
508             return PEAR::raiseError('Not currently connected', 7);
509         }
510
511         $extension = trim($this->_toUpper($extension));
512         if (is_array($this->_capability['extensions'])) {
513             foreach ($this->_capability['extensions'] as $ext) {
514                 if ($ext == $extension) {
515                     return true;
516                 }
517             }
518         }
519
520         return false;
521     }
522
523     /**
524      * Returns the list of authentication methods the server supports.
525      *
526      * @return array  List of authentication methods or PEAR_Error on failure.
527      */
528     function getAuthMechs()
529     {
530         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
531             return PEAR::raiseError('Not currently connected', 7);
532         }
533         return $this->_capability['sasl'];
534     }
535
536     /**
537      * Returns whether the server supports an authentication method.
538      *
539      * @param string $method The method to check.
540      *
541      * @return boolean  Whether the method is supported or PEAR_Error on
542      *                  failure.
543      */
544     function hasAuthMech($method)
545     {
546         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
547             return PEAR::raiseError('Not currently connected', 7);
548         }
549
550         $method = trim($this->_toUpper($method));
551         if (is_array($this->_capability['sasl'])) {
552             foreach ($this->_capability['sasl'] as $sasl) {
553                 if ($sasl == $method) {
554                     return true;
555                 }
556             }
557         }
558
559         return false;
560     }
561
562     /**
563      * Handles the authentication using any known method.
564      *
565      * @param string $uid        The userid to authenticate as.
566      * @param string $pwd        The password to authenticate with.
567      * @param string $userMethod The method to use. If empty, the class chooses
568      *                           the best (strongest) available method.
569      * @param string $euser      The effective uid to authenticate as.
570      *
571      * @return void
572      */
573     function _cmdAuthenticate($uid, $pwd, $userMethod = null, $euser = '')
574     {
575         if (PEAR::isError($method = $this->_getBestAuthMethod($userMethod))) {
576             return $method;
577         }
578         switch ($method) {
579         case 'DIGEST-MD5':
580             return $this->_authDigestMD5($uid, $pwd, $euser);
581         case 'CRAM-MD5':
582             $result = $this->_authCRAMMD5($uid, $pwd, $euser);
583             break;
584         case 'LOGIN':
585             $result = $this->_authLOGIN($uid, $pwd, $euser);
586             break;
587         case 'PLAIN':
588             $result = $this->_authPLAIN($uid, $pwd, $euser);
589             break;
590         case 'EXTERNAL':
591             $result = $this->_authEXTERNAL($uid, $pwd, $euser);
592             break;
593         default :
594             $result = PEAR::raiseError(
595                 $method . ' is not a supported authentication method'
596             );
597             break;
598         }
599
600         if (PEAR::isError($res = $this->_doCmd())) {
601             return $res;
602         }
603
604         return $result;
605     }
606
607     /**
608      * Authenticates the user using the PLAIN method.
609      *
610      * @param string $user  The userid to authenticate as.
611      * @param string $pass  The password to authenticate with.
612      * @param string $euser The effective uid to authenticate as.
613      *
614      * @return void
615      */
616     function _authPLAIN($user, $pass, $euser)
617     {
618         return $this->_sendCmd(
619             sprintf(
620                 'AUTHENTICATE "PLAIN" "%s"',
621                 base64_encode($euser . chr(0) . $user . chr(0) . $pass)
622             )
623         );
624     }
625
626     /**
627      * Authenticates the user using the LOGIN method.
628      *
629      * @param string $user  The userid to authenticate as.
630      * @param string $pass  The password to authenticate with.
631      * @param string $euser The effective uid to authenticate as.
632      *
633      * @return void
634      */
635     function _authLOGIN($user, $pass, $euser)
636     {
637         if (PEAR::isError($result = $this->_sendCmd('AUTHENTICATE "LOGIN"'))) {
638             return $result;
639         }
640         if (PEAR::isError($result = $this->_doCmd('"' . base64_encode($user) . '"', true))) {
641             return $result;
642         }
643         return $this->_doCmd('"' . base64_encode($pass) . '"', true);
644     }
645
646     /**
647      * Authenticates the user using the CRAM-MD5 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.
652      *
653      * @return void
654      */
655     function _authCRAMMD5($user, $pass, $euser)
656     {
657         if (PEAR::isError($challenge = $this->_doCmd('AUTHENTICATE "CRAM-MD5"', true))) {
658             return $challenge;
659         }
660
661         $challenge = base64_decode(trim($challenge));
662         $cram = Auth_SASL::factory('crammd5');
663         if (PEAR::isError($response = $cram->getResponse($user, $pass, $challenge))) {
664             return $response;
665         }
666
667         return $this->_sendStringResponse(base64_encode($response));
668     }
669
670     /**
671      * Authenticates the user using the DIGEST-MD5 method.
672      *
673      * @param string $user  The userid to authenticate as.
674      * @param string $pass  The password to authenticate with.
675      * @param string $euser The effective uid to authenticate as.
676      *
677      * @return void
678      */
679     function _authDigestMD5($user, $pass, $euser)
680     {
681         if (PEAR::isError($challenge = $this->_doCmd('AUTHENTICATE "DIGEST-MD5"', true))) {
682             return $challenge;
683         }
684
685         $challenge = base64_decode(trim($challenge));
686         $digest = Auth_SASL::factory('digestmd5');
687         // @todo Really 'localhost'?
688         if (PEAR::isError($response = $digest->getResponse($user, $pass, $challenge, 'localhost', 'sieve', $euser))) {
689             return $response;
690         }
691
692         if (PEAR::isError($result = $this->_sendStringResponse(base64_encode($response)))) {
693             return $result;
694         }
695         if (PEAR::isError($result = $this->_doCmd('', true))) {
696             return $result;
697         }
698         if ($this->_toUpper(substr($result, 0, 2)) == 'OK') {
699             return;
700         }
701
702         /* We don't use the protocol's third step because SIEVE doesn't allow
703          * subsequent authentication, so we just silently ignore it. */
704         if (PEAR::isError($result = $this->_sendStringResponse(''))) {
705             return $result;
706         }
707
708         return $this->_doCmd();
709     }
710
711     /**
712      * Authenticates the user using the EXTERNAL method.
713      *
714      * @param string $user  The userid to authenticate as.
715      * @param string $pass  The password to authenticate with.
716      * @param string $euser The effective uid to authenticate as.
717      *
718      * @return void
719      *
720      * @since  1.1.7
721      */
722     function _authEXTERNAL($user, $pass, $euser)
723     {
724         $cmd = sprintf(
725             'AUTHENTICATE "EXTERNAL" "%s"',
726             base64_encode(strlen($euser) ? $euser : $user)
727         );
728         return $this->_sendCmd($cmd);
729     }
730
731     /**
732      * Removes a script from the server.
733      *
734      * @param string $scriptname Name of the script to delete.
735      *
736      * @return boolean  True on success, PEAR_Error otherwise.
737      */
738     function _cmdDeleteScript($scriptname)
739     {
740         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
741             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
742         }
743         if (PEAR::isError($res = $this->_doCmd(sprintf('DELETESCRIPT "%s"', $scriptname)))) {
744             return $res;
745         }
746         return true;
747     }
748
749     /**
750      * Retrieves the contents of the named script.
751      *
752      * @param string $scriptname Name of the script to retrieve.
753      *
754      * @return string  The script if successful, PEAR_Error otherwise.
755      */
756     function _cmdGetScript($scriptname)
757     {
758         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
759             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
760         }
761
762         if (PEAR::isError($res = $this->_doCmd(sprintf('GETSCRIPT "%s"', $scriptname)))) {
763             return $res;
764         }
765
766         return preg_replace('/{[0-9]+}\r\n/', '', $res);
767     }
768
769     /**
770      * Sets the active script, i.e. the one that gets run on new mail by the
771      * server.
772      *
773      * @param string $scriptname The name of the script to mark as active.
774      *
775      * @return boolean  True on success, PEAR_Error otherwise.
776     */
777     function _cmdSetActive($scriptname)
778     {
779         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
780             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
781         }
782         if (PEAR::isError($res = $this->_doCmd(sprintf('SETACTIVE "%s"', $scriptname)))) {
783             return $res;
784         }
785         $this->_activeScript = $scriptname;
786         return true;
787     }
788
789     /**
790      * Returns the list of scripts on the server.
791      *
792      * @return array  An array with the list of scripts in the first element
793      *                and the active script in the second element on success,
794      *                PEAR_Error otherwise.
795      */
796     function _cmdListScripts()
797     {
798         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
799             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
800         }
801
802         if (PEAR::isError($res = $this->_doCmd('LISTSCRIPTS'))) {
803             return $res;
804         }
805
806         $scripts = array();
807         $activescript = null;
808         $res = explode("\r\n", $res);
809         foreach ($res as $value) {
810             if (preg_match('/^"(.*)"( ACTIVE)?$/i', $value, $matches)) {
811                 $scripts[] = $matches[1];
812                 if (!empty($matches[2])) {
813                     $activescript = $matches[1];
814                 }
815             }
816         }
817
818         return array($scripts, $activescript);
819     }
820
821     /**
822      * Adds a script to the server.
823      *
824      * @param string $scriptname Name of the new script.
825      * @param string $scriptdata The new script.
826      *
827      * @return boolean  True on success, PEAR_Error otherwise.
828      */
829     function _cmdPutScript($scriptname, $scriptdata)
830     {
831         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
832             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
833         }
834
835         $stringLength = $this->_getLineLength($scriptdata);
836
837         if (PEAR::isError($res = $this->_doCmd(sprintf("PUTSCRIPT \"%s\" {%d+}\r\n%s", $scriptname, $stringLength, $scriptdata)))) {
838             return $res;
839         }
840
841         return true;
842     }
843
844     /**
845      * Logs out of the server and terminates the connection.
846      *
847      * @param boolean $sendLogoutCMD Whether to send LOGOUT command before
848      *                               disconnecting.
849      *
850      * @return boolean  True on success, PEAR_Error otherwise.
851      */
852     function _cmdLogout($sendLogoutCMD = true)
853     {
854         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
855             return PEAR::raiseError('Not currently connected', 1);
856         }
857
858         if ($sendLogoutCMD) {
859             if (PEAR::isError($res = $this->_doCmd('LOGOUT'))) {
860                 return $res;
861             }
862         }
863
864         $this->_sock->disconnect();
865         $this->_state = NET_SIEVE_STATE_DISCONNECTED;
866
867         return true;
868     }
869
870     /**
871      * Sends the CAPABILITY command
872      *
873      * @return boolean  True on success, PEAR_Error otherwise.
874      */
875     function _cmdCapability()
876     {
877         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
878             return PEAR::raiseError('Not currently connected', 1);
879         }
880         if (PEAR::isError($res = $this->_doCmd('CAPABILITY'))) {
881             return $res;
882         }
883         $this->_parseCapability($res);
884         return true;
885     }
886
887     /**
888      * Parses the response from the CAPABILITY command and stores the result
889      * in $_capability.
890      *
891      * @param string $data The response from the capability command.
892      *
893      * @return void
894      */
895     function _parseCapability($data)
896     {
897         // Clear the cached capabilities.
898         $this->_capability = array('sasl' => array(),
899                                    'extensions' => array());
900
901         $data = preg_split('/\r?\n/', $this->_toUpper($data), -1, PREG_SPLIT_NO_EMPTY);
902
903         for ($i = 0; $i < count($data); $i++) {
904             if (!preg_match('/^"([A-Z]+)"( "(.*)")?$/', $data[$i], $matches)) {
905                 continue;
906             }
907             switch ($matches[1]) {
908             case 'IMPLEMENTATION':
909                 $this->_capability['implementation'] = $matches[3];
910                 break;
911
912             case 'SASL':
913                 $this->_capability['sasl'] = preg_split('/\s+/', $matches[3]);
914                 break;
915
916             case 'SIEVE':
917                 $this->_capability['extensions'] = preg_split('/\s+/', $matches[3]);
918                 break;
919
920             case 'STARTTLS':
921                 $this->_capability['starttls'] = true;
922                 break;
923             }
924         }
925     }
926
927     /**
928      * Sends a command to the server
929      *
930      * @param string $cmd The command to send.
931      *
932      * @return void
933      */
934     function _sendCmd($cmd)
935     {
936         $status = $this->_sock->getStatus();
937         if (PEAR::isError($status) || $status['eof']) {
938             return PEAR::raiseError('Failed to write to socket: connection lost');
939         }
940         if (PEAR::isError($error = $this->_sock->write($cmd . "\r\n"))) {
941             return PEAR::raiseError(
942                 'Failed to write to socket: ' . $error->getMessage()
943             );
944         }
945         $this->_debug("C: $cmd");
946     }
947
948     /**
949      * Sends a string response to the server.
950      *
951      * @param string $str The string to send.
952      *
953      * @return void
954      */
955     function _sendStringResponse($str)
956     {
957         return $this->_sendCmd('{' . $this->_getLineLength($str) . "+}\r\n" . $str);
958     }
959
960     /**
961      * Receives a single line from the server.
962      *
963      * @return string  The server response line.
964      */
965     function _recvLn()
966     {
967         if (PEAR::isError($lastline = $this->_sock->gets(8192))) {
968             return PEAR::raiseError(
969                 'Failed to read from socket: ' . $lastline->getMessage()
970             );
971         }
972
973         $lastline = rtrim($lastline);
974         $this->_debug("S: $lastline");
975
976         if ($lastline === '') {
977             return PEAR::raiseError('Failed to read from socket');
978         }
979
980         return $lastline;
981     }
982
983     /**
984      * Send a command and retrieves a response from the server.
985      *
986      * @param string $cmd   The command to send.
987      * @param boolean $auth Whether this is an authentication command.
988      *
989      * @return string|PEAR_Error  Reponse string if an OK response, PEAR_Error
990      *                            if a NO response.
991      */
992     function _doCmd($cmd = '', $auth = false)
993     {
994         $referralCount = 0;
995         while ($referralCount < $this->_maxReferralCount) {
996             if (strlen($cmd)) {
997                 if (PEAR::isError($error = $this->_sendCmd($cmd))) {
998                     return $error;
999                 }
1000             }
1001
1002             $response = '';
1003             while (true) {
1004                 if (PEAR::isError($line = $this->_recvLn())) {
1005                     return $line;
1006                 }
1007                 $uc_line = $this->_toUpper($line);
1008
1009                 if ('OK' == substr($uc_line, 0, 2)) {
1010                     $response .= $line;
1011                     return rtrim($response);
1012                 }
1013
1014                 if ('NO' == substr($uc_line, 0, 2)) {
1015                     // Check for string literal error message.
1016                     if (preg_match('/^no {([0-9]+)\+?}/i', $line, $matches)) {
1017                         $line .= str_replace(
1018                             "\r\n", ' ', $this->_sock->read($matches[1] + 2)
1019                         );
1020                         $this->_debug("S: $line");
1021                     }
1022                     return PEAR::raiseError(trim($response . substr($line, 2)), 3);
1023                 }
1024
1025                 if ('BYE' == substr($uc_line, 0, 3)) {
1026                     if (PEAR::isError($error = $this->disconnect(false))) {
1027                         return PEAR::raiseError(
1028                             'Cannot handle BYE, the error was: '
1029                             . $error->getMessage(),
1030                             4
1031                         );
1032                     }
1033                     // Check for referral, then follow it.  Otherwise, carp an
1034                     // error.
1035                     if (preg_match('/^bye \(referral "(sieve:\/\/)?([^"]+)/i', $line, $matches)) {
1036                         // Replace the old host with the referral host
1037                         // preserving any protocol prefix.
1038                         $this->_data['host'] = preg_replace(
1039                             '/\w+(?!(\w|\:\/\/)).*/', $matches[2],
1040                             $this->_data['host']
1041                         );
1042                         if (PEAR::isError($error = $this->_handleConnectAndLogin())) {
1043                             return PEAR::raiseError(
1044                                 'Cannot follow referral to '
1045                                 . $this->_data['host'] . ', the error was: '
1046                                 . $error->getMessage(),
1047                                 5
1048                             );
1049                         }
1050                         break;
1051                     }
1052                     return PEAR::raiseError(trim($response . $line), 6);
1053                 }
1054
1055                 if (preg_match('/^{([0-9]+)\+?}/i', $line, $matches)) {
1056                     // Matches String Responses.
1057                     $str_size = $matches[1] + 2;
1058                     $line = '';
1059                     $line_length = 0;
1060                     while ($line_length < $str_size) {
1061                         $line .= $this->_sock->read($str_size - $line_length);
1062                         $line_length = $this->_getLineLength($line);
1063                     }
1064                     $this->_debug("S: $line");
1065
1066                     if (!$auth) {
1067                         // Receive the pending OK only if we aren't
1068                         // authenticating since string responses during
1069                         // authentication don't need an OK.
1070                         $this->_recvLn();
1071                     }
1072                     return $line;
1073                 }
1074
1075                 if ($auth) {
1076                     // String responses during authentication don't need an
1077                     // OK.
1078                     $response .= $line;
1079                     return rtrim($response);
1080                 }
1081
1082                 $response .= $line . "\r\n";
1083                 $referralCount++;
1084             }
1085         }
1086
1087         return PEAR::raiseError('Max referral count (' . $referralCount . ') reached. Cyrus murder loop error?', 7);
1088     }
1089
1090     /**
1091      * Returns the name of the best authentication method that the server
1092      * has advertised.
1093      *
1094      * @param string $userMethod Only consider this method as available.
1095      *
1096      * @return string  The name of the best supported authentication method or
1097      *                 a PEAR_Error object on failure.
1098      */
1099     function _getBestAuthMethod($userMethod = null)
1100     {
1101         if (!isset($this->_capability['sasl'])) {
1102             return PEAR::raiseError('This server doesn\'t support any authentication methods. SASL problem?');
1103         }
1104         if (!$this->_capability['sasl']) {
1105             return PEAR::raiseError('This server doesn\'t support any authentication methods.');
1106         }
1107
1108         if ($userMethod) {
1109             if (in_array($userMethod, $this->_capability['sasl'])) {
1110                 return $userMethod;
1111             }
1112             return PEAR::raiseError(
1113                 sprintf('No supported authentication method found. The server supports these methods: %s, but we want to use: %s',
1114                         implode(', ', $this->_capability['sasl']),
1115                         $userMethod));
1116         }
1117
1118         foreach ($this->supportedAuthMethods as $method) {
1119             if (in_array($method, $this->_capability['sasl'])) {
1120                 return $method;
1121             }
1122         }
1123
1124         return PEAR::raiseError(
1125             sprintf('No supported authentication method found. The server supports these methods: %s, but we only support: %s',
1126                     implode(', ', $this->_capability['sasl']),
1127                     implode(', ', $this->supportedAuthMethods)));
1128     }
1129
1130     /**
1131      * Starts a TLS connection.
1132      *
1133      * @return boolean  True on success, PEAR_Error on failure.
1134      */
1135     function _startTLS()
1136     {
1137         if (PEAR::isError($res = $this->_doCmd('STARTTLS'))) {
1138             return $res;
1139         }
1140
1141         if (!stream_socket_enable_crypto($this->_sock->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
1142             return PEAR::raiseError('Failed to establish TLS connection', 2);
1143         }
1144
1145         $this->_debug('STARTTLS negotiation successful');
1146
1147         // The server should be sending a CAPABILITY response after
1148         // negotiating TLS. Read it, and ignore if it doesn't.
1149         $this->_doCmd();
1150
1151         // RFC says we need to query the server capabilities again now that we
1152         // are under encryption.
1153         if (PEAR::isError($res = $this->_cmdCapability())) {
1154             return PEAR::raiseError(
1155                 'Failed to connect, server said: ' . $res->getMessage(), 2
1156             );
1157         }
1158
1159         return true;
1160     }
1161
1162     /**
1163      * Returns the length of a string.
1164      *
1165      * @param string $string A string.
1166      *
1167      * @return integer  The length of the string.
1168      */
1169     function _getLineLength($string)
1170     {
1171         if (extension_loaded('mbstring')) {
1172             return mb_strlen($string, 'latin1');
1173         } else {
1174             return strlen($string);
1175         }
1176     }
1177
1178     /**
1179      * Locale independant strtoupper() implementation.
1180      *
1181      * @param string $string The string to convert to lowercase.
1182      *
1183      * @return string  The lowercased string, based on ASCII encoding.
1184      */
1185     function _toUpper($string)
1186     {
1187         $language = setlocale(LC_CTYPE, 0);
1188         setlocale(LC_CTYPE, 'C');
1189         $string = strtoupper($string);
1190         setlocale(LC_CTYPE, $language);
1191         return $string;
1192     }
1193
1194     /**
1195      * Write debug text to the current debug output handler.
1196      *
1197      * @param string $message Debug message text.
1198      *
1199      * @return void
1200      */
1201     function _debug($message)
1202     {
1203         if ($this->_debug) {
1204             if ($this->_debug_handler) {
1205                 call_user_func_array($this->_debug_handler, array(&$this, $message));
1206             } else {
1207                 echo "$message\n";
1208             }
1209         }
1210     }
1211 }