source: subversion/trunk/plugins/managesieve/lib/Net/Sieve.php @ 3501

Last change on this file since 3501 was 3501, checked in by alec, 3 years ago
  • Net_Sieve-1.2.1: Fix DIGEST-MD5 and dovecot (#1486640)
File size: 37.6 KB
Line 
1<?php
2/**
3 * This file contains the Net_Sieve class.
4 *
5 * PHP version 4
6 *
7 * +-----------------------------------------------------------------------+
8 * | All rights reserved.                                                  |
9 * |                                                                       |
10 * | Redistribution and use in source and binary forms, with or without    |
11 * | modification, are permitted provided that the following conditions    |
12 * | are met:                                                              |
13 * |                                                                       |
14 * | o Redistributions of source code must retain the above copyright      |
15 * |   notice, this list of conditions and the following disclaimer.       |
16 * | o Redistributions in binary form must reproduce the above copyright   |
17 * |   notice, this list of conditions and the following disclaimer in the |
18 * |   documentation and/or other materials provided with the distribution.|
19 * |                                                                       |
20 * | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   |
21 * | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     |
22 * | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
23 * | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  |
24 * | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
25 * | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      |
26 * | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
27 * | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
28 * | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   |
29 * | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
30 * | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  |
31 * +-----------------------------------------------------------------------+
32 *
33 * @category  Networking
34 * @package   Net_Sieve
35 * @author    Richard Heyes <richard@phpguru.org>
36 * @author    Damian Fernandez Sosa <damlists@cnba.uba.ar>
37 * @author    Anish Mistry <amistry@am-productions.biz>
38 * @author    Jan Schneider <jan@horde.org>
39 * @copyright 2002-2003 Richard Heyes
40 * @copyright 2006-2008 Anish Mistry
41 * @license   http://www.opensource.org/licenses/bsd-license.php BSD
42 * @version   SVN: $Id: Sieve.php 298158 2010-04-19 09:43:30Z yunosh $
43 * @link      http://pear.php.net/package/Net_Sieve
44 */
45
46require_once 'PEAR.php';
47require_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 */
59define('NET_SIEVE_STATE_DISCONNECTED', 1, true);
60
61/**
62 * Authorisation state
63 * @const NET_SIEVE_STATE_AUTHORISATION
64 */
65define('NET_SIEVE_STATE_AUTHORISATION', 2, true);
66
67/**
68 * Transaction state
69 * @const NET_SIEVE_STATE_TRANSACTION
70 */
71define('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.2.1
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 */
93class 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     */
208    function Net_Sieve($user = null, $pass  = null, $host = 'localhost',
209        $port = 2000, $logintype = '', $euser = '', $debug = false,
210        $bypassAuth = false, $useTLS = true, $options = null
211    ) {
212        $this->_state             = NET_SIEVE_STATE_DISCONNECTED;
213        $this->_data['user']      = $user;
214        $this->_data['pass']      = $pass;
215        $this->_data['host']      = $host;
216        $this->_data['port']      = $port;
217        $this->_data['logintype'] = $logintype;
218        $this->_data['euser']     = $euser;
219        $this->_sock              = new Net_Socket();
220        $this->_debug             = $debug;
221        $this->_bypassAuth        = $bypassAuth;
222        $this->_useTLS            = $useTLS;
223        $this->_options           = $options;
224
225        /* Try to include the Auth_SASL package.  If the package is not
226         * available, we disable the authentication methods that depend upon
227         * it. */
228        if ((@include_once 'Auth/SASL.php') === false) {
229            $this->_debug('Auth_SASL not present');
230            foreach ($this->supportedSASLAuthMethods as $SASLMethod) {
231                $pos = array_search($SASLMethod, $this->supportedAuthMethods);
232                $this->_debug('Disabling method ' . $SASLMethod);
233                unset($this->supportedAuthMethods[$pos]);
234            }
235        }
236
237        if (strlen($user) && strlen($pass)) {
238            $this->_error = $this->_handleConnectAndLogin();
239        }
240    }
241
242    /**
243     * Returns any error that may have been generated in the constructor.
244     *
245     * @return boolean|PEAR_Error  False if no error, PEAR_Error otherwise.
246     */
247    function getError()
248    {
249        return PEAR::isError($this->_error) ? $this->_error : false;
250    }
251
252    /**
253     * Sets the debug state and handler function.
254     *
255     * @param boolean $debug   Whether to enable debugging.
256     * @param string  $handler A custom debug handler. Must be a valid callback.
257     *
258     * @return void
259     */
260    function setDebug($debug = true, $handler = null)
261    {
262        $this->_debug = $debug;
263        $this->_debug_handler = $handler;
264    }
265
266    /**
267     * Connects to the server and logs in.
268     *
269     * @return boolean  True on success, PEAR_Error on failure.
270     */
271    function _handleConnectAndLogin()
272    {
273        if (PEAR::isError($res = $this->connect($this->_data['host'], $this->_data['port'], $this->_options, $this->_useTLS))) {
274            return $res;
275        }
276        if ($this->_bypassAuth === false) {
277            if (PEAR::isError($res = $this->login($this->_data['user'], $this->_data['pass'], $this->_data['logintype'], $this->_data['euser'], $this->_bypassAuth))) {
278                return $res;
279            }
280        }
281        return true;
282    }
283
284    /**
285     * Handles connecting to the server and checks the response validity.
286     *
287     * @param string  $host    Hostname of server.
288     * @param string  $port    Port of server.
289     * @param array   $options List of options to pass to
290     *                         stream_context_create().
291     * @param boolean $useTLS  Use TLS if available.
292     *
293     * @return boolean  True on success, PEAR_Error otherwise.
294     */
295    function connect($host, $port, $options = null, $useTLS = true)
296    {
297        if (NET_SIEVE_STATE_DISCONNECTED != $this->_state) {
298            return PEAR::raiseError('Not currently in DISCONNECTED state', 1);
299        }
300
301        if (PEAR::isError($res = $this->_sock->connect($host, $port, false, 5, $options))) {
302            return $res;
303        }
304
305        if ($this->_bypassAuth) {
306            $this->_state = NET_SIEVE_STATE_TRANSACTION;
307        } else {
308            $this->_state = NET_SIEVE_STATE_AUTHORISATION;
309            if (PEAR::isError($res = $this->_doCmd())) {
310                return $res;
311            }
312        }
313
314        // Explicitly ask for the capabilities in case the connection is
315        // picked up from an existing connection.
316        if (PEAR::isError($res = $this->_cmdCapability())) {
317            return PEAR::raiseError(
318                'Failed to connect, server said: ' . $res->getMessage(), 2
319            );
320        }
321
322        // Check if we can enable TLS via STARTTLS.
323        if ($useTLS && !empty($this->_capability['starttls'])
324            && function_exists('stream_socket_enable_crypto')
325        ) {
326            if (PEAR::isError($res = $this->_startTLS())) {
327                return $res;
328            }
329        }
330
331        return true;
332    }
333
334    /**
335     * Disconnect from the Sieve server.
336     *
337     * @param boolean $sendLogoutCMD Whether to send LOGOUT command before
338     *                               disconnecting.
339     *
340     * @return boolean  True on success, PEAR_Error otherwise.
341     */
342    function disconnect($sendLogoutCMD = true)
343    {
344        return $this->_cmdLogout($sendLogoutCMD);
345    }
346
347    /**
348     * Logs into server.
349     *
350     * @param string  $user       Login username.
351     * @param string  $pass       Login password.
352     * @param string  $logintype  Type of login method to use.
353     * @param string  $euser      Effective UID (perform on behalf of $euser).
354     * @param boolean $bypassAuth Do not perform authentication.
355     *
356     * @return boolean  True on success, PEAR_Error otherwise.
357     */
358    function login($user, $pass, $logintype = null, $euser = '', $bypassAuth = false)
359    {
360        if (NET_SIEVE_STATE_AUTHORISATION != $this->_state) {
361            return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
362        }
363
364        if (!$bypassAuth ) {
365            if (PEAR::isError($res = $this->_cmdAuthenticate($user, $pass, $logintype, $euser))) {
366                return $res;
367            }
368        }
369        $this->_state = NET_SIEVE_STATE_TRANSACTION;
370
371        return true;
372    }
373
374    /**
375     * Returns an indexed array of scripts currently on the server.
376     *
377     * @return array  Indexed array of scriptnames.
378     */
379    function listScripts()
380    {
381        if (is_array($scripts = $this->_cmdListScripts())) {
382            $this->_active = $scripts[1];
383            return $scripts[0];
384        } else {
385            return $scripts;
386        }
387    }
388
389    /**
390     * Returns the active script.
391     *
392     * @return string  The active scriptname.
393     */
394    function getActive()
395    {
396        if (!empty($this->_active)) {
397            return $this->_active;
398        }
399        if (is_array($scripts = $this->_cmdListScripts())) {
400            $this->_active = $scripts[1];
401            return $scripts[1];
402        }
403    }
404
405    /**
406     * Sets the active script.
407     *
408     * @param string $scriptname The name of the script to be set as active.
409     *
410     * @return boolean  True on success, PEAR_Error on failure.
411     */
412    function setActive($scriptname)
413    {
414        return $this->_cmdSetActive($scriptname);
415    }
416
417    /**
418     * Retrieves a script.
419     *
420     * @param string $scriptname The name of the script to be retrieved.
421     *
422     * @return string  The script on success, PEAR_Error on failure.
423    */
424    function getScript($scriptname)
425    {
426        return $this->_cmdGetScript($scriptname);
427    }
428
429    /**
430     * Adds a script to the server.
431     *
432     * @param string  $scriptname Name of the script.
433     * @param string  $script     The script content.
434     * @param boolean $makeactive Whether to make this the active script.
435     *
436     * @return boolean  True on success, PEAR_Error on failure.
437     */
438    function installScript($scriptname, $script, $makeactive = false)
439    {
440        if (PEAR::isError($res = $this->_cmdPutScript($scriptname, $script))) {
441            return $res;
442        }
443        if ($makeactive) {
444            return $this->_cmdSetActive($scriptname);
445        }
446        return true;
447    }
448
449    /**
450     * Removes a script from the server.
451     *
452     * @param string $scriptname Name of the script.
453     *
454     * @return boolean  True on success, PEAR_Error on failure.
455     */
456    function removeScript($scriptname)
457    {
458        return $this->_cmdDeleteScript($scriptname);
459    }
460
461    /**
462     * Checks if the server has space to store the script by the server.
463     *
464     * @param string  $scriptname The name of the script to mark as active.
465     * @param integer $size       The size of the script.
466     *
467     * @return boolean|PEAR_Error  True if there is space, PEAR_Error otherwise.
468     *
469     * @todo Rename to hasSpace()
470     */
471    function haveSpace($scriptname, $size)
472    {
473        if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
474            return PEAR::raiseError('Not currently in TRANSACTION state', 1);
475        }
476        if (PEAR::isError($res = $this->_doCmd(sprintf('HAVESPACE "%s" %d', $scriptname, $size)))) {
477            return $res;
478        }
479        return true;
480    }
481
482    /**
483     * Returns the list of extensions the server supports.
484     *
485     * @return array  List of extensions or PEAR_Error on failure.
486     */
487    function getExtensions()
488    {
489        if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
490            return PEAR::raiseError('Not currently connected', 7);
491        }
492        return $this->_capability['extensions'];
493    }
494
495    /**
496     * Returns whether the server supports an extension.
497     *
498     * @param string $extension The extension to check.
499     *
500     * @return boolean  Whether the extension is supported or PEAR_Error on
501     *                  failure.
502     */
503    function hasExtension($extension)
504    {
505        if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
506            return PEAR::raiseError('Not currently connected', 7);
507        }
508
509        $extension = trim($this->_toUpper($extension));
510        if (is_array($this->_capability['extensions'])) {
511            foreach ($this->_capability['extensions'] as $ext) {
512                if ($ext == $extension) {
513                    return true;
514                }
515            }
516        }
517
518        return false;
519    }
520
521    /**
522     * Returns the list of authentication methods the server supports.
523     *
524     * @return array  List of authentication methods or PEAR_Error on failure.
525     */
526    function getAuthMechs()
527    {
528        if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
529            return PEAR::raiseError('Not currently connected', 7);
530        }
531        return $this->_capability['sasl'];
532    }
533
534    /**
535     * Returns whether the server supports an authentication method.
536     *
537     * @param string $method The method to check.
538     *
539     * @return boolean  Whether the method is supported or PEAR_Error on
540     *                  failure.
541     */
542    function hasAuthMech($method)
543    {
544        if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
545            return PEAR::raiseError('Not currently connected', 7);
546        }
547
548        $method = trim($this->_toUpper($method));
549        if (is_array($this->_capability['sasl'])) {
550            foreach ($this->_capability['sasl'] as $sasl) {
551                if ($sasl == $method) {
552                    return true;
553                }
554            }
555        }
556
557        return false;
558    }
559
560    /**
561     * Handles the authentication using any known method.
562     *
563     * @param string $uid        The userid to authenticate as.
564     * @param string $pwd        The password to authenticate with.
565     * @param string $userMethod The method to use. If empty, the class chooses
566     *                           the best (strongest) available method.
567     * @param string $euser      The effective uid to authenticate as.
568     *
569     * @return void
570     */
571    function _cmdAuthenticate($uid, $pwd, $userMethod = null, $euser = '')
572    {
573        if (PEAR::isError($method = $this->_getBestAuthMethod($userMethod))) {
574            return $method;
575        }
576        switch ($method) {
577        case 'DIGEST-MD5':
578            return $this->_authDigestMD5($uid, $pwd, $euser);
579        case 'CRAM-MD5':
580            $result = $this->_authCRAMMD5($uid, $pwd, $euser);
581            break;
582        case 'LOGIN':
583            $result = $this->_authLOGIN($uid, $pwd, $euser);
584            break;
585        case 'PLAIN':
586            $result = $this->_authPLAIN($uid, $pwd, $euser);
587            break;
588        case 'EXTERNAL':
589            $result = $this->_authEXTERNAL($uid, $pwd, $euser);
590            break;
591        default :
592            $result = PEAR::raiseError(
593                $method . ' is not a supported authentication method'
594            );
595            break;
596        }
597
598        if (PEAR::isError($res = $this->_doCmd())) {
599            return $res;
600        }
601
602        return $result;
603    }
604
605    /**
606     * Authenticates the user using the PLAIN method.
607     *
608     * @param string $user  The userid to authenticate as.
609     * @param string $pass  The password to authenticate with.
610     * @param string $euser The effective uid to authenticate as.
611     *
612     * @return void
613     */
614    function _authPLAIN($user, $pass, $euser)
615    {
616        return $this->_sendCmd(
617            sprintf(
618                'AUTHENTICATE "PLAIN" "%s"',
619                base64_encode($euser . chr(0) . $user . chr(0) . $pass)
620            )
621        );
622    }
623
624    /**
625     * Authenticates the user using the LOGIN method.
626     *
627     * @param string $user  The userid to authenticate as.
628     * @param string $pass  The password to authenticate with.
629     * @param string $euser The effective uid to authenticate as.
630     *
631     * @return void
632     */
633    function _authLOGIN($user, $pass, $euser)
634    {
635        if (PEAR::isError($result = $this->_sendCmd('AUTHENTICATE "LOGIN"'))) {
636            return $result;
637        }
638        if (PEAR::isError($result = $this->_doCmd('"' . base64_encode($user) . '"'))) {
639            return $result;
640        }
641        return $this->_doCmd('"' . base64_encode($pass) . '"');
642    }
643
644    /**
645     * Authenticates the user using the CRAM-MD5 method.
646     *
647     * @param string $user  The userid to authenticate as.
648     * @param string $pass  The password to authenticate with.
649     * @param string $euser The effective uid to authenticate as.
650     *
651     * @return void
652     */
653    function _authCRAMMD5($user, $pass, $euser)
654    {
655        if (PEAR::isError($challenge = $this->_doCmd('AUTHENTICATE "CRAM-MD5"', true))) {
656            return $challenge;
657        }
658
659        $challenge = base64_decode(trim($challenge));
660        $cram = Auth_SASL::factory('crammd5');
661        if (PEAR::isError($response = $cram->getResponse($user, $pass, $challenge))) {
662            return $response;
663        }
664
665        return $this->_sendStringResponse(base64_encode($response));
666    }
667
668    /**
669     * Authenticates the user using the DIGEST-MD5 method.
670     *
671     * @param string $user  The userid to authenticate as.
672     * @param string $pass  The password to authenticate with.
673     * @param string $euser The effective uid to authenticate as.
674     *
675     * @return void
676     */
677    function _authDigestMD5($user, $pass, $euser)
678    {
679        if (PEAR::isError($challenge = $this->_doCmd('AUTHENTICATE "DIGEST-MD5"', true))) {
680            return $challenge;
681        }
682
683        $challenge = base64_decode(trim($challenge));
684        $digest = Auth_SASL::factory('digestmd5');
685        // @todo Really 'localhost'?
686        if (PEAR::isError($response = $digest->getResponse($user, $pass, $challenge, 'localhost', 'sieve', $euser))) {
687            return $response;
688        }
689
690        if (PEAR::isError($result = $this->_sendStringResponse(base64_encode($response)))) {
691            return $result;
692        }
693        if (PEAR::isError($result = $this->_doCmd('', true))) {
694            return $result;
695        }
696        if ($this->_toUpper(substr($result, 0, 2)) == 'OK') {
697            return;
698        }
699
700        /* We don't use the protocol's third step because SIEVE doesn't allow
701         * subsequent authentication, so we just silently ignore it. */
702        if (PEAR::isError($result = $this->_sendStringResponse(''))) {
703            return $result;
704        }
705
706        return $this->_doCmd();
707    }
708
709    /**
710     * Authenticates the user using the EXTERNAL method.
711     *
712     * @param string $user  The userid to authenticate as.
713     * @param string $pass  The password to authenticate with.
714     * @param string $euser The effective uid to authenticate as.
715     *
716     * @return void
717     *
718     * @since  1.1.7
719     */
720    function _authEXTERNAL($user, $pass, $euser)
721    {
722        $cmd = sprintf(
723            'AUTHENTICATE "EXTERNAL" "%s"',
724            base64_encode(strlen($euser) ? $euser : $user)
725        );
726        return $this->_sendCmd($cmd);
727    }
728
729    /**
730     * Removes a script from the server.
731     *
732     * @param string $scriptname Name of the script to delete.
733     *
734     * @return boolean  True on success, PEAR_Error otherwise.
735     */
736    function _cmdDeleteScript($scriptname)
737    {
738        if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
739            return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
740        }
741        if (PEAR::isError($res = $this->_doCmd(sprintf('DELETESCRIPT "%s"', $scriptname)))) {
742            return $res;
743        }
744        return true;
745    }
746
747    /**
748     * Retrieves the contents of the named script.
749     *
750     * @param string $scriptname Name of the script to retrieve.
751     *
752     * @return string  The script if successful, PEAR_Error otherwise.
753     */
754    function _cmdGetScript($scriptname)
755    {
756        if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
757            return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
758        }
759
760        if (PEAR::isError($res = $this->_doCmd(sprintf('GETSCRIPT "%s"', $scriptname)))) {
761            return $res;
762        }
763
764        return preg_replace('/{[0-9]+}\r\n/', '', $res);
765    }
766
767    /**
768     * Sets the active script, i.e. the one that gets run on new mail by the
769     * server.
770     *
771     * @param string $scriptname The name of the script to mark as active.
772     *
773     * @return boolean  True on success, PEAR_Error otherwise.
774    */
775    function _cmdSetActive($scriptname)
776    {
777        if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
778            return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
779        }
780        if (PEAR::isError($res = $this->_doCmd(sprintf('SETACTIVE "%s"', $scriptname)))) {
781            return $res;
782        }
783        $this->_activeScript = $scriptname;
784        return true;
785    }
786
787    /**
788     * Returns the list of scripts on the server.
789     *
790     * @return array  An array with the list of scripts in the first element
791     *                and the active script in the second element on success,
792     *                PEAR_Error otherwise.
793     */
794    function _cmdListScripts()
795    {
796        if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
797            return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
798        }
799
800        if (PEAR::isError($res = $this->_doCmd('LISTSCRIPTS'))) {
801            return $res;
802        }
803
804        $scripts = array();
805        $activescript = null;
806        $res = explode("\r\n", $res);
807        foreach ($res as $value) {
808            if (preg_match('/^"(.*)"( ACTIVE)?$/i', $value, $matches)) {
809                $scripts[] = $matches[1];
810                if (!empty($matches[2])) {
811                    $activescript = $matches[1];
812                }
813            }
814        }
815
816        return array($scripts, $activescript);
817    }
818
819    /**
820     * Adds a script to the server.
821     *
822     * @param string $scriptname Name of the new script.
823     * @param string $scriptdata The new script.
824     *
825     * @return boolean  True on success, PEAR_Error otherwise.
826     */
827    function _cmdPutScript($scriptname, $scriptdata)
828    {
829        if (NET_SIEVE_STATE_TRANSACTION != $this->_state) {
830            return PEAR::raiseError('Not currently in AUTHORISATION state', 1);
831        }
832
833        $stringLength = $this->_getLineLength($scriptdata);
834
835        if (PEAR::isError($res = $this->_doCmd(sprintf("PUTSCRIPT \"%s\" {%d+}\r\n%s", $scriptname, $stringLength, $scriptdata)))) {
836            return $res;
837        }
838
839        return true;
840    }
841
842    /**
843     * Logs out of the server and terminates the connection.
844     *
845     * @param boolean $sendLogoutCMD Whether to send LOGOUT command before
846     *                               disconnecting.
847     *
848     * @return boolean  True on success, PEAR_Error otherwise.
849     */
850    function _cmdLogout($sendLogoutCMD = true)
851    {
852        if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
853            return PEAR::raiseError('Not currently connected', 1);
854        }
855
856        if ($sendLogoutCMD) {
857            if (PEAR::isError($res = $this->_doCmd('LOGOUT'))) {
858                return $res;
859            }
860        }
861
862        $this->_sock->disconnect();
863        $this->_state = NET_SIEVE_STATE_DISCONNECTED;
864
865        return true;
866    }
867
868    /**
869     * Sends the CAPABILITY command
870     *
871     * @return boolean  True on success, PEAR_Error otherwise.
872     */
873    function _cmdCapability()
874    {
875        if (NET_SIEVE_STATE_DISCONNECTED == $this->_state) {
876            return PEAR::raiseError('Not currently connected', 1);
877        }
878        if (PEAR::isError($res = $this->_doCmd('CAPABILITY'))) {
879            return $res;
880        }
881        $this->_parseCapability($res);
882        return true;
883    }
884
885    /**
886     * Parses the response from the CAPABILITY command and stores the result
887     * in $_capability.
888     *
889     * @param string $data The response from the capability command.
890     *
891     * @return void
892     */
893    function _parseCapability($data)
894    {
895        // Clear the cached capabilities.
896        $this->_capability = array('sasl' => array(),
897                                   'extensions' => array());
898
899        $data = preg_split('/\r?\n/', $this->_toUpper($data), -1, PREG_SPLIT_NO_EMPTY);
900
901        for ($i = 0; $i < count($data); $i++) {
902            if (!preg_match('/^"([A-Z]+)"( "(.*)")?$/', $data[$i], $matches)) {
903                continue;
904            }
905            switch ($matches[1]) {
906            case 'IMPLEMENTATION':
907                $this->_capability['implementation'] = $matches[3];
908                break;
909
910            case 'SASL':
911                $this->_capability['sasl'] = preg_split('/\s+/', $matches[3]);
912                break;
913
914            case 'SIEVE':
915                $this->_capability['extensions'] = preg_split('/\s+/', $matches[3]);
916                break;
917
918            case 'STARTTLS':
919                $this->_capability['starttls'] = true;
920                break;
921            }
922        }
923    }
924
925    /**
926     * Sends a command to the server
927     *
928     * @param string $cmd The command to send.
929     *
930     * @return void
931     */
932    function _sendCmd($cmd)
933    {
934        $status = $this->_sock->getStatus();
935        if (PEAR::isError($status) || $status['eof']) {
936            return PEAR::raiseError('Failed to write to socket: connection lost');
937        }
938        if (PEAR::isError($error = $this->_sock->write($cmd . "\r\n"))) {
939            return PEAR::raiseError(
940                'Failed to write to socket: ' . $error->getMessage()
941            );
942        }
943        $this->_debug("C: $cmd");
944    }
945
946    /**
947     * Sends a string response to the server.
948     *
949     * @param string $str The string to send.
950     *
951     * @return void
952     */
953    function _sendStringResponse($str)
954    {
955        return $this->_sendCmd('{' . $this->_getLineLength($str) . "+}\r\n" . $str);
956    }
957
958    /**
959     * Receives a single line from the server.
960     *
961     * @return string  The server response line.
962     */
963    function _recvLn()
964    {
965        if (PEAR::isError($lastline = $this->_sock->gets(8192))) {
966            return PEAR::raiseError(
967                'Failed to read from socket: ' . $lastline->getMessage()
968            );
969        }
970
971        $lastline = rtrim($lastline);
972        $this->_debug("S: $lastline");
973
974        if ($lastline === '') {
975            return PEAR::raiseError('Failed to read from socket');
976        }
977
978        return $lastline;
979    }
980
981    /**
982     * Send a command and retrieves a response from the server.
983     *
984     * @param string $cmd   The command to send.
985     * @param boolean $auth Whether this is an authentication command.
986     *
987     * @return string|PEAR_Error  Reponse string if an OK response, PEAR_Error
988     *                            if a NO response.
989     */
990    function _doCmd($cmd = '', $auth = false)
991    {
992        $referralCount = 0;
993        while ($referralCount < $this->_maxReferralCount) {
994            if (strlen($cmd)) {
995                if (PEAR::isError($error = $this->_sendCmd($cmd))) {
996                    return $error;
997                }
998            }
999
1000            $response = '';
1001            while (true) {
1002                if (PEAR::isError($line = $this->_recvLn())) {
1003                    return $line;
1004                }
1005                $uc_line = $this->_toUpper($line);
1006
1007                if ('OK' == substr($uc_line, 0, 2)) {
1008                    $response .= $line;
1009                    return rtrim($response);
1010                }
1011
1012                if ('NO' == substr($uc_line, 0, 2)) {
1013                    // Check for string literal error message.
1014                    if (preg_match('/^no {([0-9]+)\+?}/i', $line, $matches)) {
1015                        $line .= str_replace(
1016                            "\r\n", ' ', $this->_sock->read($matches[1] + 2)
1017                        );
1018                        $this->_debug("S: $line");
1019                    }
1020                    return PEAR::raiseError(trim($response . substr($line, 2)), 3);
1021                }
1022
1023                if ('BYE' == substr($uc_line, 0, 3)) {
1024                    if (PEAR::isError($error = $this->disconnect(false))) {
1025                        return PEAR::raiseError(
1026                            'Cannot handle BYE, the error was: '
1027                            . $error->getMessage(),
1028                            4
1029                        );
1030                    }
1031                    // Check for referral, then follow it.  Otherwise, carp an
1032                    // error.
1033                    if (preg_match('/^bye \(referral "(sieve:\/\/)?([^"]+)/i', $line, $matches)) {
1034                        // Replace the old host with the referral host
1035                        // preserving any protocol prefix.
1036                        $this->_data['host'] = preg_replace(
1037                            '/\w+(?!(\w|\:\/\/)).*/', $matches[2],
1038                            $this->_data['host']
1039                        );
1040                        if (PEAR::isError($error = $this->_handleConnectAndLogin())) {
1041                            return PEAR::raiseError(
1042                                'Cannot follow referral to '
1043                                . $this->_data['host'] . ', the error was: '
1044                                . $error->getMessage(),
1045                                5
1046                            );
1047                        }
1048                        break;
1049                    }
1050                    return PEAR::raiseError(trim($response . $line), 6);
1051                }
1052
1053                if (preg_match('/^{([0-9]+)\+?}/i', $line, $matches)) {
1054                    // Matches String Responses.
1055                    $str_size = $matches[1] + 2;
1056                    $line = '';
1057                    $line_length = 0;
1058                    while ($line_length < $str_size) {
1059                        $line .= $this->_sock->read($str_size - $line_length);
1060                        $line_length = $this->_getLineLength($line);
1061                    }
1062                    $this->_debug("S: $line");
1063
1064                    if (!$auth) {
1065                        // Receive the pending OK only if we aren't
1066                        // authenticating since string responses during
1067                        // authentication don't need an OK.
1068                        $this->_recvLn();
1069                    }
1070                    return $line;
1071                }
1072
1073                if ($auth) {
1074                    // String responses during authentication don't need an
1075                    // OK.
1076                    $response .= $line;
1077                    return rtrim($response);
1078                }
1079
1080                $response .= $line . "\r\n";
1081                $referralCount++;
1082            }
1083        }
1084
1085        return PEAR::raiseError('Max referral count (' . $referralCount . ') reached. Cyrus murder loop error?', 7);
1086    }
1087
1088    /**
1089     * Returns the name of the best authentication method that the server
1090     * has advertised.
1091     *
1092     * @param string $userMethod Only consider this method as available.
1093     *
1094     * @return string  The name of the best supported authentication method or
1095     *                 a PEAR_Error object on failure.
1096     */
1097    function _getBestAuthMethod($userMethod = null)
1098    {
1099        if (!isset($this->_capability['sasl'])) {
1100            return PEAR::raiseError('This server doesn\'t support any authentication methods. SASL problem?');
1101        }
1102
1103        $serverMethods = $this->_capability['sasl'];
1104
1105        if ($userMethod) {
1106            $methods = array($userMethod);
1107        } else {
1108            $methods = $this->supportedAuthMethods;
1109        }
1110
1111        if (!$methods || !$serverMethods) {
1112            return PEAR::raiseError(
1113                'This server doesn\'t support any authentication methods.'
1114            );
1115        }
1116
1117        foreach ($methods as $method) {
1118            if (in_array($method, $serverMethods)) {
1119                return $method;
1120            }
1121        }
1122
1123        return PEAR::raiseError(
1124            'No supported authentication method found. The server supports these methods: '
1125            . implode(',', $serverMethods)
1126            . ', but we only support: '
1127            . implode(',', $this->supportedAuthMethods)
1128        );
1129    }
1130
1131    /**
1132     * Starts a TLS connection.
1133     *
1134     * @return boolean  True on success, PEAR_Error on failure.
1135     */
1136    function _startTLS()
1137    {
1138        if (PEAR::isError($res = $this->_doCmd('STARTTLS'))) {
1139            return $res;
1140        }
1141
1142        if (!stream_socket_enable_crypto($this->_sock->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
1143            return PEAR::raiseError('Failed to establish TLS connection', 2);
1144        }
1145
1146        $this->_debug('STARTTLS negotiation successful');
1147
1148        // The server should be sending a CAPABILITY response after
1149        // negotiating TLS. Read it, and ignore if it doesn't.
1150        $this->_doCmd();
1151
1152        // RFC says we need to query the server capabilities again now that we
1153        // are under encryption.
1154        if (PEAR::isError($res = $this->_cmdCapability())) {
1155            return PEAR::raiseError(
1156                'Failed to connect, server said: ' . $res->getMessage(), 2
1157            );
1158        }
1159
1160        return true;
1161    }
1162
1163    /**
1164     * Returns the length of a string.
1165     *
1166     * @param string $string A string.
1167     *
1168     * @return integer  The length of the string.
1169     */
1170    function _getLineLength($string)
1171    {
1172        if (extension_loaded('mbstring')) {
1173            return mb_strlen($string, 'latin1');
1174        } else {
1175            return strlen($string);
1176        }
1177    }
1178
1179    /**
1180     * Locale independant strtoupper() implementation.
1181     *
1182     * @param string $string The string to convert to lowercase.
1183     *
1184     * @return string  The lowercased string, based on ASCII encoding.
1185     */
1186    function _toUpper($string)
1187    {
1188        $language = setlocale(LC_CTYPE, 0);
1189        setlocale(LC_CTYPE, 'C');
1190        $string = strtoupper($string);
1191        setlocale(LC_CTYPE, $language);
1192        return $string;
1193    }
1194
1195    /**
1196     * Write debug text to the current debug output handler.
1197     *
1198     * @param string $message Debug message text.
1199     *
1200     * @return void
1201     */
1202    function _debug($message)
1203    {
1204        if ($this->_debug) {
1205            if ($this->_debug_handler) {
1206                call_user_func_array($this->_debug_handler, array(&$this, $message));
1207            } else {
1208                echo "$message\n";
1209            }
1210        }
1211    }
1212}
Note: See TracBrowser for help on using the repository browser.