source: trunk/library/Net/Sieve.php @ 5565

Revision 5565, 40.1 KB checked in by airton, 12 years ago (diff)

Ticket #2088 - Melhorias no editor de regras de filtro de mensagens do Expresso - Adicionando um arquivo que faltou

  • Property svn:executable set to *
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$
43 * @link      http://pear.php.net/package/Net_Sieve
44 */
45
46require_once $_SESSION['rootPath'].'/library/PEAR/PEAR.php';
47require_once $_SESSION['rootPath'].'/library/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.3.2
87 * @link      http://pear.php.net/package/Net_Sieve
88 * @link      http://tools.ietf.org/html/rfc5228 RFC 5228 (Sieve: An Email
89 *            Filtering Language)
90 * @link      http://tools.ietf.org/html/rfc5804 RFC 5804 A Protocol for
91 *            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     * @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 (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        if (PEAR::isError($res = $this->_doCmd(sprintf('HAVESPACE %s %d', $this->_escape($scriptname), $size)))) {
493            return $res;
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;
616        }
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            );
623        }
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
765        if (PEAR::isError($res = $this->_doCmd(sprintf('DELETESCRIPT %s', $this->_escape($scriptname))))) {
766            return $res;
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
784        if (PEAR::isError($res = $this->_doCmd(sprintf('GETSCRIPT %s', $this->_escape($scriptname))))) {
785            return $res;
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
805        if (PEAR::isError($res = $this->_doCmd(sprintf('SETACTIVE %s', $this->_escape($scriptname))))) {
806            return $res;
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",
862                                $this->_escape($scriptname),
863                                $stringLength,
864                                $scriptdata);
865        if (PEAR::isError($res = $this->_doCmd($command))) {
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    /**
1012     * Receives a number of bytes from the server.
1013     *
1014     * @param integer $length  Number of bytes to read.
1015     *
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        }
1026        $this->_debug('S: ' . rtrim($response));
1027        return $response;
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.
1063                    if (preg_match('/{([0-9]+)}$/', $line, $matches)) {
1064                        $line = substr($line, 0, -(strlen($matches[1])+2))
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
1102                if (preg_match('/^{([0-9]+)}/', $line, $matches)) {
1103                    // Matches literal string responses.
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.
1188        // Unfortunately old Cyrus versions are broken and don't send a
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', '>=')) {
1193            $this->_doCmd();
1194        }
1195
1196        // Query the server capabilities again now that we are under
1197        // encryption.
1198        if (PEAR::isError($res = $this->_cmdCapability())) {
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    /**
1240     * Converts strings into RFC's quoted-string or literal-c2s form.
1241     *
1242     * @param string $string  The string to convert.
1243     *
1244     * @return string  Result string.
1245     */
1246    function _escape($string)
1247    {
1248        // Some implementations don't allow UTF-8 characters in quoted-string,
1249        // use literal-c2s.
1250        if (preg_match('/[^\x01-\x09\x0B-\x0C\x0E-\x7F]/', $string)) {
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}
Note: See TracBrowser for help on using the repository browser.