Aleksander Machniak
2013-10-06 4daaa09438bc05f9d5d6cf339cc0b60b511057e9
commit | author | age
48e9c1 1 <?php
T 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
4daaa0 42  * @version   SVN: $Id$
48e9c1 43  * @link      http://pear.php.net/package/Net_Sieve
T 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
4daaa0 86  * @version   Release: 1.3.2
48e9c1 87  * @link      http://pear.php.net/package/Net_Sieve
4daaa0 88  * @link      http://tools.ietf.org/html/rfc5228 RFC 5228 (Sieve: An Email
48e9c1 89  *            Filtering Language)
4daaa0 90  * @link      http://tools.ietf.org/html/rfc5804 RFC 5804 A Protocol for
AM 91  *            Remotely Managing Sieve Scripts
48e9c1 92  */
T 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         $this->_data['host'] = $host;
300         $this->_data['port'] = $port;
301         $this->_useTLS       = $useTLS;
4daaa0 302         if (is_array($options)) {
48e9c1 303             $this->_options = array_merge($this->_options, $options);
T 304         }
305
306         if (NET_SIEVE_STATE_DISCONNECTED != $this->_state) {
307             return PEAR::raiseError('Not currently in DISCONNECTED state', 1);
308         }
309
310         if (PEAR::isError($res = $this->_sock->connect($host, $port, false, 5, $options))) {
311             return $res;
312         }
313
314         if ($this->_bypassAuth) {
315             $this->_state = NET_SIEVE_STATE_TRANSACTION;
316         } else {
317             $this->_state = NET_SIEVE_STATE_AUTHORISATION;
318             if (PEAR::isError($res = $this->_doCmd())) {
319                 return $res;
320             }
321         }
322
323         // Explicitly ask for the capabilities in case the connection is
324         // picked up from an existing connection.
325         if (PEAR::isError($res = $this->_cmdCapability())) {
326             return PEAR::raiseError(
327                 'Failed to connect, server said: ' . $res->getMessage(), 2
328             );
329         }
330
331         // Check if we can enable TLS via STARTTLS.
332         if ($useTLS && !empty($this->_capability['starttls'])
333             && function_exists('stream_socket_enable_crypto')
334         ) {
335             if (PEAR::isError($res = $this->_startTLS())) {
336                 return $res;
337             }
338         }
339
340         return true;
341     }
342
343     /**
344      * Disconnect from the Sieve server.
345      *
346      * @param boolean $sendLogoutCMD Whether to send LOGOUT command before
347      *                               disconnecting.
348      *
349      * @return boolean  True on success, PEAR_Error otherwise.
350      */
351     function disconnect($sendLogoutCMD = true)
352     {
353         return $this->_cmdLogout($sendLogoutCMD);
354     }
355
356     /**
357      * Logs into server.
358      *
359      * @param string  $user       Login username.
360      * @param string  $pass       Login password.
361      * @param string  $logintype  Type of login method to use.
362      * @param string  $euser      Effective UID (perform on behalf of $euser).
363      * @param boolean $bypassAuth Do not perform authentication.
364      *
365      * @return boolean  True on success, PEAR_Error otherwise.
366      */
367     function login($user, $pass, $logintype = null, $euser = '', $bypassAuth = false)
368     {
369         $this->_data['user']      = $user;
370         $this->_data['pass']      = $pass;
371         $this->_data['logintype'] = $logintype;
372         $this->_data['euser']     = $euser;
373         $this->_bypassAuth        = $bypassAuth;
374
375         if (NET_SIEVE_STATE_AUTHORISATION != $this->_state) {
376             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
377         }
378
379         if (!$bypassAuth ) {
380             if (PEAR::isError($res = $this->_cmdAuthenticate($user, $pass, $logintype, $euser))) {
381                 return $res;
382             }
383         }
384         $this->_state = NET_SIEVE_STATE_TRANSACTION;
385
386         return true;
387     }
388
389     /**
390      * Returns an indexed array of scripts currently on the server.
391      *
392      * @return array  Indexed array of scriptnames.
393      */
394     function listScripts()
395     {
396         if (is_array($scripts = $this->_cmdListScripts())) {
397             $this->_active = $scripts[1];
398             return $scripts[0];
399         } else {
400             return $scripts;
401         }
402     }
403
404     /**
405      * Returns the active script.
406      *
407      * @return string  The active scriptname.
408      */
409     function getActive()
410     {
411         if (!empty($this->_active)) {
412             return $this->_active;
413         }
414         if (is_array($scripts = $this->_cmdListScripts())) {
415             $this->_active = $scripts[1];
416             return $scripts[1];
417         }
418     }
419
420     /**
421      * Sets the active script.
422      *
423      * @param string $scriptname The name of the script to be set as active.
424      *
425      * @return boolean  True on success, PEAR_Error on failure.
426      */
427     function setActive($scriptname)
428     {
429         return $this->_cmdSetActive($scriptname);
430     }
431
432     /**
433      * Retrieves a script.
434      *
435      * @param string $scriptname The name of the script to be retrieved.
436      *
437      * @return string  The script on success, PEAR_Error on failure.
438     */
439     function getScript($scriptname)
440     {
441         return $this->_cmdGetScript($scriptname);
442     }
443
444     /**
445      * Adds a script to the server.
446      *
447      * @param string  $scriptname Name of the script.
448      * @param string  $script     The script content.
449      * @param boolean $makeactive Whether to make this the active script.
450      *
451      * @return boolean  True on success, PEAR_Error on failure.
452      */
453     function installScript($scriptname, $script, $makeactive = false)
454     {
455         if (PEAR::isError($res = $this->_cmdPutScript($scriptname, $script))) {
456             return $res;
457         }
458         if ($makeactive) {
459             return $this->_cmdSetActive($scriptname);
460         }
461         return true;
462     }
463
464     /**
465      * Removes a script from the server.
466      *
467      * @param string $scriptname Name of the script.
468      *
469      * @return boolean  True on success, PEAR_Error on failure.
470      */
471     function removeScript($scriptname)
472     {
473         return $this->_cmdDeleteScript($scriptname);
474     }
475
476     /**
477      * Checks if the server has space to store the script by the server.
478      *
479      * @param string  $scriptname The name of the script to mark as active.
480      * @param integer $size       The size of the script.
481      *
482      * @return boolean|PEAR_Error  True if there is space, PEAR_Error otherwise.
483      *
484      * @todo Rename to hasSpace()
485      */
486     function haveSpace($scriptname, $size)
487     {
488         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
489             return PEAR::raiseError('Not currently in TRANSACTION state', 1);
490         }
491
4daaa0 492         if (PEAR::isError($res = $this->_doCmd(sprintf('HAVESPACE %s %d', $this->_escape($scriptname), $size)))) {
48e9c1 493             return $res;
T 494         }
495         return true;
496     }
497
498     /**
499      * Returns the list of extensions the server supports.
500      *
501      * @return array  List of extensions or PEAR_Error on failure.
502      */
503     function getExtensions()
504     {
505         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
506             return PEAR::raiseError('Not currently connected', 7);
507         }
508         return $this->_capability['extensions'];
509     }
510
511     /**
512      * Returns whether the server supports an extension.
513      *
514      * @param string $extension The extension to check.
515      *
516      * @return boolean  Whether the extension is supported or PEAR_Error on
517      *                  failure.
518      */
519     function hasExtension($extension)
520     {
521         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
522             return PEAR::raiseError('Not currently connected', 7);
523         }
524
525         $extension = trim($this->_toUpper($extension));
526         if (is_array($this->_capability['extensions'])) {
527             foreach ($this->_capability['extensions'] as $ext) {
528                 if ($ext == $extension) {
529                     return true;
530                 }
531             }
532         }
533
534         return false;
535     }
536
537     /**
538      * Returns the list of authentication methods the server supports.
539      *
540      * @return array  List of authentication methods or PEAR_Error on failure.
541      */
542     function getAuthMechs()
543     {
544         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
545             return PEAR::raiseError('Not currently connected', 7);
546         }
547         return $this->_capability['sasl'];
548     }
549
550     /**
551      * Returns whether the server supports an authentication method.
552      *
553      * @param string $method The method to check.
554      *
555      * @return boolean  Whether the method is supported or PEAR_Error on
556      *                  failure.
557      */
558     function hasAuthMech($method)
559     {
560         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
561             return PEAR::raiseError('Not currently connected', 7);
562         }
563
564         $method = trim($this->_toUpper($method));
565         if (is_array($this->_capability['sasl'])) {
566             foreach ($this->_capability['sasl'] as $sasl) {
567                 if ($sasl == $method) {
568                     return true;
569                 }
570             }
571         }
572
573         return false;
574     }
575
576     /**
577      * Handles the authentication using any known method.
578      *
579      * @param string $uid        The userid to authenticate as.
580      * @param string $pwd        The password to authenticate with.
581      * @param string $userMethod The method to use. If empty, the class chooses
582      *                           the best (strongest) available method.
583      * @param string $euser      The effective uid to authenticate as.
584      *
585      * @return void
586      */
587     function _cmdAuthenticate($uid, $pwd, $userMethod = null, $euser = '')
588     {
589         if (PEAR::isError($method = $this->_getBestAuthMethod($userMethod))) {
590             return $method;
591         }
592         switch ($method) {
593         case 'DIGEST-MD5':
594             return $this->_authDigestMD5($uid, $pwd, $euser);
595         case 'CRAM-MD5':
596             $result = $this->_authCRAMMD5($uid, $pwd, $euser);
597             break;
598         case 'LOGIN':
599             $result = $this->_authLOGIN($uid, $pwd, $euser);
600             break;
601         case 'PLAIN':
602             $result = $this->_authPLAIN($uid, $pwd, $euser);
603             break;
604         case 'EXTERNAL':
605             $result = $this->_authEXTERNAL($uid, $pwd, $euser);
606             break;
607         default :
608             $result = PEAR::raiseError(
609                 $method . ' is not a supported authentication method'
610             );
611             break;
612         }
613
614         if (PEAR::isError($res = $this->_doCmd())) {
615             return $res;
4daaa0 616         }
AM 617
618         // Query the server capabilities again now that we are authenticated.
619         if (PEAR::isError($res = $this->_cmdCapability())) {
620             return PEAR::raiseError(
621                 'Failed to connect, server said: ' . $res->getMessage(), 2
622             );
48e9c1 623         }
T 624
625         return $result;
626     }
627
628     /**
629      * Authenticates the user using the PLAIN 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 _authPLAIN($user, $pass, $euser)
638     {
639         return $this->_sendCmd(
640             sprintf(
641                 'AUTHENTICATE "PLAIN" "%s"',
642                 base64_encode($euser . chr(0) . $user . chr(0) . $pass)
643             )
644         );
645     }
646
647     /**
648      * Authenticates the user using the LOGIN method.
649      *
650      * @param string $user  The userid to authenticate as.
651      * @param string $pass  The password to authenticate with.
652      * @param string $euser The effective uid to authenticate as.
653      *
654      * @return void
655      */
656     function _authLOGIN($user, $pass, $euser)
657     {
658         if (PEAR::isError($result = $this->_sendCmd('AUTHENTICATE "LOGIN"'))) {
659             return $result;
660         }
661         if (PEAR::isError($result = $this->_doCmd('"' . base64_encode($user) . '"', true))) {
662             return $result;
663         }
664         return $this->_doCmd('"' . base64_encode($pass) . '"', true);
665     }
666
667     /**
668      * Authenticates the user using the CRAM-MD5 method.
669      *
670      * @param string $user  The userid to authenticate as.
671      * @param string $pass  The password to authenticate with.
672      * @param string $euser The effective uid to authenticate as.
673      *
674      * @return void
675      */
676     function _authCRAMMD5($user, $pass, $euser)
677     {
678         if (PEAR::isError($challenge = $this->_doCmd('AUTHENTICATE "CRAM-MD5"', true))) {
679             return $challenge;
680         }
681
682         $challenge = base64_decode(trim($challenge));
683         $cram = Auth_SASL::factory('crammd5');
684         if (PEAR::isError($response = $cram->getResponse($user, $pass, $challenge))) {
685             return $response;
686         }
687
688         return $this->_sendStringResponse(base64_encode($response));
689     }
690
691     /**
692      * Authenticates the user using the DIGEST-MD5 method.
693      *
694      * @param string $user  The userid to authenticate as.
695      * @param string $pass  The password to authenticate with.
696      * @param string $euser The effective uid to authenticate as.
697      *
698      * @return void
699      */
700     function _authDigestMD5($user, $pass, $euser)
701     {
702         if (PEAR::isError($challenge = $this->_doCmd('AUTHENTICATE "DIGEST-MD5"', true))) {
703             return $challenge;
704         }
705
706         $challenge = base64_decode(trim($challenge));
707         $digest = Auth_SASL::factory('digestmd5');
708         // @todo Really 'localhost'?
709         if (PEAR::isError($response = $digest->getResponse($user, $pass, $challenge, 'localhost', 'sieve', $euser))) {
710             return $response;
711         }
712
713         if (PEAR::isError($result = $this->_sendStringResponse(base64_encode($response)))) {
714             return $result;
715         }
716         if (PEAR::isError($result = $this->_doCmd('', true))) {
717             return $result;
718         }
719         if ($this->_toUpper(substr($result, 0, 2)) == 'OK') {
720             return;
721         }
722
723         /* We don't use the protocol's third step because SIEVE doesn't allow
724          * subsequent authentication, so we just silently ignore it. */
725         if (PEAR::isError($result = $this->_sendStringResponse(''))) {
726             return $result;
727         }
728
729         return $this->_doCmd();
730     }
731
732     /**
733      * Authenticates the user using the EXTERNAL method.
734      *
735      * @param string $user  The userid to authenticate as.
736      * @param string $pass  The password to authenticate with.
737      * @param string $euser The effective uid to authenticate as.
738      *
739      * @return void
740      *
741      * @since  1.1.7
742      */
743     function _authEXTERNAL($user, $pass, $euser)
744     {
745         $cmd = sprintf(
746             'AUTHENTICATE "EXTERNAL" "%s"',
747             base64_encode(strlen($euser) ? $euser : $user)
748         );
749         return $this->_sendCmd($cmd);
750     }
751
752     /**
753      * Removes a script from the server.
754      *
755      * @param string $scriptname Name of the script to delete.
756      *
757      * @return boolean  True on success, PEAR_Error otherwise.
758      */
759     function _cmdDeleteScript($scriptname)
760     {
761         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
762             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
763         }
764
4daaa0 765         if (PEAR::isError($res = $this->_doCmd(sprintf('DELETESCRIPT %s', $this->_escape($scriptname))))) {
48e9c1 766             return $res;
T 767         }
768         return true;
769     }
770
771     /**
772      * Retrieves the contents of the named script.
773      *
774      * @param string $scriptname Name of the script to retrieve.
775      *
776      * @return string  The script if successful, PEAR_Error otherwise.
777      */
778     function _cmdGetScript($scriptname)
779     {
780         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
781             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
782         }
783
4daaa0 784         if (PEAR::isError($res = $this->_doCmd(sprintf('GETSCRIPT %s', $this->_escape($scriptname))))) {
48e9c1 785             return $res;
T 786         }
787
788         return preg_replace('/^{[0-9]+}\r\n/', '', $res);
789     }
790
791     /**
792      * Sets the active script, i.e. the one that gets run on new mail by the
793      * server.
794      *
795      * @param string $scriptname The name of the script to mark as active.
796      *
797      * @return boolean  True on success, PEAR_Error otherwise.
798     */
799     function _cmdSetActive($scriptname)
800     {
801         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
802             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
803         }
804
4daaa0 805         if (PEAR::isError($res = $this->_doCmd(sprintf('SETACTIVE %s', $this->_escape($scriptname))))) {
48e9c1 806             return $res;
T 807         }
808
809         $this->_activeScript = $scriptname;
810         return true;
811     }
812
813     /**
814      * Returns the list of scripts on the server.
815      *
816      * @return array  An array with the list of scripts in the first element
817      *                and the active script in the second element on success,
818      *                PEAR_Error otherwise.
819      */
820     function _cmdListScripts()
821     {
822         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
823             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
824         }
825
826         if (PEAR::isError($res = $this->_doCmd('LISTSCRIPTS'))) {
827             return $res;
828         }
829
830         $scripts = array();
831         $activescript = null;
832         $res = explode("\r\n", $res);
833         foreach ($res as $value) {
834             if (preg_match('/^"(.*)"( ACTIVE)?$/i', $value, $matches)) {
835                 $script_name = stripslashes($matches[1]);
836                 $scripts[] = $script_name;
837                 if (!empty($matches[2])) {
838                     $activescript = $script_name;
839                 }
840             }
841         }
842
843         return array($scripts, $activescript);
844     }
845
846     /**
847      * Adds a script to the server.
848      *
849      * @param string $scriptname Name of the new script.
850      * @param string $scriptdata The new script.
851      *
852      * @return boolean  True on success, PEAR_Error otherwise.
853      */
854     function _cmdPutScript($scriptname, $scriptdata)
855     {
856         if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
857             return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
858         }
859
860         $stringLength = $this->_getLineLength($scriptdata);
861         $command      = sprintf("PUTSCRIPT %s {%d+}\r\n%s",
4daaa0 862                                 $this->_escape($scriptname),
AM 863                                 $stringLength,
864                                 $scriptdata);
48e9c1 865         if (PEAR::isError($res = $this->_doCmd($command))) {
T 866             return $res;
867         }
868
869         return true;
870     }
871
872     /**
873      * Logs out of the server and terminates the connection.
874      *
875      * @param boolean $sendLogoutCMD Whether to send LOGOUT command before
876      *                               disconnecting.
877      *
878      * @return boolean  True on success, PEAR_Error otherwise.
879      */
880     function _cmdLogout($sendLogoutCMD = true)
881     {
882         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
883             return PEAR::raiseError('Not currently connected', 1);
884         }
885
886         if ($sendLogoutCMD) {
887             if (PEAR::isError($res = $this->_doCmd('LOGOUT'))) {
888                 return $res;
889             }
890         }
891
892         $this->_sock->disconnect();
893         $this->_state = NET_SIEVE_STATE_DISCONNECTED;
894
895         return true;
896     }
897
898     /**
899      * Sends the CAPABILITY command
900      *
901      * @return boolean  True on success, PEAR_Error otherwise.
902      */
903     function _cmdCapability()
904     {
905         if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
906             return PEAR::raiseError('Not currently connected', 1);
907         }
908         if (PEAR::isError($res = $this->_doCmd('CAPABILITY'))) {
909             return $res;
910         }
911         $this->_parseCapability($res);
912         return true;
913     }
914
915     /**
916      * Parses the response from the CAPABILITY command and stores the result
917      * in $_capability.
918      *
919      * @param string $data The response from the capability command.
920      *
921      * @return void
922      */
923     function _parseCapability($data)
924     {
925         // Clear the cached capabilities.
926         $this->_capability = array('sasl' => array(),
927                                    'extensions' => array());
928
929         $data = preg_split('/\r?\n/', $this->_toUpper($data), -1, PREG_SPLIT_NO_EMPTY);
930
931         for ($i = 0; $i < count($data); $i++) {
932             if (!preg_match('/^"([A-Z]+)"( "(.*)")?$/', $data[$i], $matches)) {
933                 continue;
934             }
935             switch ($matches[1]) {
936             case 'IMPLEMENTATION':
937                 $this->_capability['implementation'] = $matches[3];
938                 break;
939
940             case 'SASL':
941                 $this->_capability['sasl'] = preg_split('/\s+/', $matches[3]);
942                 break;
943
944             case 'SIEVE':
945                 $this->_capability['extensions'] = preg_split('/\s+/', $matches[3]);
946                 break;
947
948             case 'STARTTLS':
949                 $this->_capability['starttls'] = true;
950                 break;
951             }
952         }
953     }
954
955     /**
956      * Sends a command to the server
957      *
958      * @param string $cmd The command to send.
959      *
960      * @return void
961      */
962     function _sendCmd($cmd)
963     {
964         $status = $this->_sock->getStatus();
965         if (PEAR::isError($status) || $status['eof']) {
966             return PEAR::raiseError('Failed to write to socket: connection lost');
967         }
968         if (PEAR::isError($error = $this->_sock->write($cmd . "\r\n"))) {
969             return PEAR::raiseError(
970                 'Failed to write to socket: ' . $error->getMessage()
971             );
972         }
973         $this->_debug("C: $cmd");
974     }
975
976     /**
977      * Sends a string response to the server.
978      *
979      * @param string $str The string to send.
980      *
981      * @return void
982      */
983     function _sendStringResponse($str)
984     {
985         return $this->_sendCmd('{' . $this->_getLineLength($str) . "+}\r\n" . $str);
986     }
987
988     /**
989      * Receives a single line from the server.
990      *
991      * @return string  The server response line.
992      */
993     function _recvLn()
994     {
995         if (PEAR::isError($lastline = $this->_sock->gets(8192))) {
996             return PEAR::raiseError(
997                 'Failed to read from socket: ' . $lastline->getMessage()
998             );
999         }
1000
1001         $lastline = rtrim($lastline);
1002         $this->_debug("S: $lastline");
1003
1004         if ($lastline === '') {
1005             return PEAR::raiseError('Failed to read from socket');
1006         }
1007
1008         return $lastline;
1009     }
1010
1011     /**
4daaa0 1012      * Receives a number of bytes from the server.
48e9c1 1013      *
4daaa0 1014      * @param integer $length  Number of bytes to read.
48e9c1 1015      *
T 1016      * @return string  The server response.
1017      */
1018     function _recvBytes($length)
1019     {
1020         $response = '';
1021         $response_length = 0;
1022         while ($response_length < $length) {
1023             $response .= $this->_sock->read($length - $response_length);
1024             $response_length = $this->_getLineLength($response);
1025         }
4daaa0 1026         $this->_debug('S: ' . rtrim($response));
48e9c1 1027         return $response;
T 1028     }
1029
1030     /**
1031      * Send a command and retrieves a response from the server.
1032      *
1033      * @param string $cmd   The command to send.
1034      * @param boolean $auth Whether this is an authentication command.
1035      *
1036      * @return string|PEAR_Error  Reponse string if an OK response, PEAR_Error
1037      *                            if a NO response.
1038      */
1039     function _doCmd($cmd = '', $auth = false)
1040     {
1041         $referralCount = 0;
1042         while ($referralCount < $this->_maxReferralCount) {
1043             if (strlen($cmd)) {
1044                 if (PEAR::isError($error = $this->_sendCmd($cmd))) {
1045                     return $error;
1046                 }
1047             }
1048
1049             $response = '';
1050             while (true) {
1051                 if (PEAR::isError($line = $this->_recvLn())) {
1052                     return $line;
1053                 }
1054                 $uc_line = $this->_toUpper($line);
1055
1056                 if ('OK' == substr($uc_line, 0, 2)) {
1057                     $response .= $line;
1058                     return rtrim($response);
1059                 }
1060
1061                 if ('NO' == substr($uc_line, 0, 2)) {
1062                     // Check for string literal error message.
4daaa0 1063                     if (preg_match('/{([0-9]+)}$/', $line, $matches)) {
48e9c1 1064                         $line = substr($line, 0, -(strlen($matches[1])+2))
T 1065                             . str_replace(
1066                                 "\r\n", ' ', $this->_recvBytes($matches[1] + 2)
1067                             );
1068                     }
1069                     return PEAR::raiseError(trim($response . substr($line, 2)), 3);
1070                 }
1071
1072                 if ('BYE' == substr($uc_line, 0, 3)) {
1073                     if (PEAR::isError($error = $this->disconnect(false))) {
1074                         return PEAR::raiseError(
1075                             'Cannot handle BYE, the error was: '
1076                             . $error->getMessage(),
1077                             4
1078                         );
1079                     }
1080                     // Check for referral, then follow it.  Otherwise, carp an
1081                     // error.
1082                     if (preg_match('/^bye \(referral "(sieve:\/\/)?([^"]+)/i', $line, $matches)) {
1083                         // Replace the old host with the referral host
1084                         // preserving any protocol prefix.
1085                         $this->_data['host'] = preg_replace(
1086                             '/\w+(?!(\w|\:\/\/)).*/', $matches[2],
1087                             $this->_data['host']
1088                         );
1089                         if (PEAR::isError($error = $this->_handleConnectAndLogin())) {
1090                             return PEAR::raiseError(
1091                                 'Cannot follow referral to '
1092                                 . $this->_data['host'] . ', the error was: '
1093                                 . $error->getMessage(),
1094                                 5
1095                             );
1096                         }
1097                         break;
1098                     }
1099                     return PEAR::raiseError(trim($response . $line), 6);
1100                 }
1101
4daaa0 1102                 if (preg_match('/^{([0-9]+)}/', $line, $matches)) {
48e9c1 1103                     // Matches literal string responses.
T 1104                     $line = $this->_recvBytes($matches[1] + 2);
1105                     if (!$auth) {
1106                         // Receive the pending OK only if we aren't
1107                         // authenticating since string responses during
1108                         // authentication don't need an OK.
1109                         $this->_recvLn();
1110                     }
1111                     return $line;
1112                 }
1113
1114                 if ($auth) {
1115                     // String responses during authentication don't need an
1116                     // OK.
1117                     $response .= $line;
1118                     return rtrim($response);
1119                 }
1120
1121                 $response .= $line . "\r\n";
1122                 $referralCount++;
1123             }
1124         }
1125
1126         return PEAR::raiseError('Max referral count (' . $referralCount . ') reached. Cyrus murder loop error?', 7);
1127     }
1128
1129     /**
1130      * Returns the name of the best authentication method that the server
1131      * has advertised.
1132      *
1133      * @param string $userMethod Only consider this method as available.
1134      *
1135      * @return string  The name of the best supported authentication method or
1136      *                 a PEAR_Error object on failure.
1137      */
1138     function _getBestAuthMethod($userMethod = null)
1139     {
1140         if (!isset($this->_capability['sasl'])) {
1141             return PEAR::raiseError('This server doesn\'t support any authentication methods. SASL problem?');
1142         }
1143         if (!$this->_capability['sasl']) {
1144             return PEAR::raiseError('This server doesn\'t support any authentication methods.');
1145         }
1146
1147         if ($userMethod) {
1148             if (in_array($userMethod, $this->_capability['sasl'])) {
1149                 return $userMethod;
1150             }
1151             return PEAR::raiseError(
1152                 sprintf('No supported authentication method found. The server supports these methods: %s, but we want to use: %s',
1153                         implode(', ', $this->_capability['sasl']),
1154                         $userMethod));
1155         }
1156
1157         foreach ($this->supportedAuthMethods as $method) {
1158             if (in_array($method, $this->_capability['sasl'])) {
1159                 return $method;
1160             }
1161         }
1162
1163         return PEAR::raiseError(
1164             sprintf('No supported authentication method found. The server supports these methods: %s, but we only support: %s',
1165                     implode(', ', $this->_capability['sasl']),
1166                     implode(', ', $this->supportedAuthMethods)));
1167     }
1168
1169     /**
1170      * Starts a TLS connection.
1171      *
1172      * @return boolean  True on success, PEAR_Error on failure.
1173      */
1174     function _startTLS()
1175     {
1176         if (PEAR::isError($res = $this->_doCmd('STARTTLS'))) {
1177             return $res;
1178         }
1179
1180         if (!stream_socket_enable_crypto($this->_sock->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
1181             return PEAR::raiseError('Failed to establish TLS connection', 2);
1182         }
1183
1184         $this->_debug('STARTTLS negotiation successful');
1185
1186         // The server should be sending a CAPABILITY response after
1187         // negotiating TLS. Read it, and ignore if it doesn't.
4daaa0 1188         // Unfortunately old Cyrus versions are broken and don't send a
AM 1189         // CAPABILITY response, thus we would wait here forever. Parse the
1190         // Cyrus version and work around this broken behavior.
1191         if (!preg_match('/^CYRUS TIMSIEVED V([0-9.]+)/', $this->_capability['implementation'], $matches) ||
1192             version_compare($matches[1], '2.3.10', '>=')) {
48e9c1 1193             $this->_doCmd();
T 1194         }
1195
4daaa0 1196         // Query the server capabilities again now that we are under
AM 1197         // encryption.
48e9c1 1198         if (PEAR::isError($res = $this->_cmdCapability())) {
T 1199             return PEAR::raiseError(
1200                 'Failed to connect, server said: ' . $res->getMessage(), 2
1201             );
1202         }
1203
1204         return true;
1205     }
1206
1207     /**
1208      * Returns the length of a string.
1209      *
1210      * @param string $string A string.
1211      *
1212      * @return integer  The length of the string.
1213      */
1214     function _getLineLength($string)
1215     {
1216         if (extension_loaded('mbstring')) {
1217             return mb_strlen($string, 'latin1');
1218         } else {
1219             return strlen($string);
1220         }
1221     }
1222
1223     /**
1224      * Locale independant strtoupper() implementation.
1225      *
1226      * @param string $string The string to convert to lowercase.
1227      *
1228      * @return string  The lowercased string, based on ASCII encoding.
1229      */
1230     function _toUpper($string)
1231     {
1232         $language = setlocale(LC_CTYPE, 0);
1233         setlocale(LC_CTYPE, 'C');
1234         $string = strtoupper($string);
1235         setlocale(LC_CTYPE, $language);
1236         return $string;
1237     }
1238
1239     /**
4daaa0 1240      * Converts strings into RFC's quoted-string or literal-c2s form.
48e9c1 1241      *
4daaa0 1242      * @param string $string  The string to convert.
48e9c1 1243      *
4daaa0 1244      * @return string  Result string.
48e9c1 1245      */
T 1246     function _escape($string)
1247     {
4daaa0 1248         // Some implementations don't allow UTF-8 characters in quoted-string,
AM 1249         // use literal-c2s.
48e9c1 1250         if (preg_match('/[^\x01-\x09\x0B-\x0C\x0E-\x7F]/', $string)) {
T 1251             return sprintf("{%d+}\r\n%s", $this->_getLineLength($string), $string);
1252         }
1253
1254         return '"' . addcslashes($string, '\\"') . '"';
1255     }
1256
1257     /**
1258      * Write debug text to the current debug output handler.
1259      *
1260      * @param string $message Debug message text.
1261      *
1262      * @return void
1263      */
1264     function _debug($message)
1265     {
1266         if ($this->_debug) {
1267             if ($this->_debug_handler) {
1268                 call_user_func_array($this->_debug_handler, array(&$this, $message));
1269             } else {
1270                 echo "$message\n";
1271             }
1272         }
1273     }
1274 }