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