1<?php
2/**
3 * sieve-php.lib.php
4 *
5 * $Id: managesieve.lib.php 1030 2009-05-25 08:19:28Z avel $
6 *
7 * Copyright 2001-2003 Dan Ellis <danellis@rushmore.com>
8 *
9 * This program is released under the GNU Public License.  See the enclosed
10 * file COPYING for license information. If you did not receive this file, see
11 * http://www.fsf.org/copyleft/gpl.html.
12 *
13 * You should have received a copy of the GNU Public License along with this
14 * package; if not, write to the Free Software Foundation, Inc., 59 Temple
15 * Place - Suite 330, Boston, MA 02111-1307, USA.
16 *
17 * See CHANGES for updates since last release
18 *
19 * @author Dan Ellis, Alexandros Vellis
20 * @package sieve-php
21 * @copyright Copyright 2002-2003, Dan Ellis, All Rights Reserved.
22 * @version 0.1.0
23 */
24
25/**
26 * Constants
27 */
28define ("F_NO", 0);
29define ("F_OK", 1);
30define ("F_DATA", 2);
31define ("F_HEAD", 3);
32
33define ("EC_NOT_LOGGED_IN", 0);
34define ("EC_QUOTA", 10);
35define ("EC_NOSCRIPTS", 20);
36define ("EC_UNKNOWN", 255);
37
38/**
39 * SIEVE class - A Class that implements MANAGESIEVE in PHP4|5.
40 *
41 * This program provides a handy interface into the Cyrus timsieved server
42 * under php4.  It is tested with Sieve server included in Cyrus 2.0, but it
43 * has been upgraded (not tested) to work with older Sieve server versions.
44 *
45 * All functions will return either true or false and will fill in
46 * $sieve->error with a defined error code like EC_QUOTA, raw server errors in
47 * $sieve->error_raw, and successful responses in $sieve->responses.
48 *
49 * NOTE: a major change since version (0.0.5) is the inclusion of a standard
50 * method to retrieve  server responses.  All functions will return either true
51 * or false and will fill in $sieve->error with a defined error code like
52 * EC_QUOTA, raw server errors in $sieve->error_raw, and successful responses
53 * in $sieve->responses.
54 *
55 * Usage is pretty simple.  The basics is login, do what you need and logout.
56 * There are two sample files (which suck) test.php and testsieve.php.
57 * test.php allows you to create/delete/view scripts and testsieve.php is a
58 * very basic sieve server test.
59 *
60 * Please let us know of any bugs, problems or ideas at sieve-php development
61 * list:  sieve-php-devel@lists.sourceforge.net. A web interface to subscribe
62 * to this list is available at:
63 * https://lists.sourceforge.net/mailman/listinfo/sieve-php-devel
64 *
65 * @author Dan Ellis
66 * @example simple_example.php A simple example that shows usage of sieve-php
67 * class.
68 * @example vacationset-sieve.php A more elaborate example of vacation script
69 * handling.
70 * @version 0.1.0
71 * @package sieve-php
72 * @todo Maybe add the NOOP function.
73 * @todo Have timing mechanism when port problems arise.
74 * @todo Provide better error diagnostics.
75 */
76class sieve {
77  var $host;
78  var $port;
79  var $user;
80  var $pass;
81  /**
82   * a comma seperated list of allowed auth types, in order of preference
83   */
84  var $auth_types;
85
86  /**
87   * options
88   */
89  var $broken_tls;
90
91  /**
92   * type of authentication attempted
93   */
94  var $auth_in_use;
95
96  /**
97   * @var boolean Force disabling of STARTTLS for clients that do not want/need
98   * it. */
99  var $disabletls = false;
100
101  var $line;
102  var $fp;
103  var $retval;
104  var $tmpfile;
105  var $fh;
106  var $len;
107  var $script;
108
109  var $loggedin;
110  var $capabilities;
111  var $error;
112  var $error_raw;
113  var $responses;
114
115  // lastcmd is for referral processing
116  var $lastcmd;
117  var $reftok;
118  var $refsv;
119
120
121  //maybe we should add an errorlvl that the user will pass to new sieve = sieve(,,,,E_WARN)
122  //so we can decide how to handle certain errors?!?
123
124  /**
125   * get response
126   * @todo Test Cyrus version 2.2 vs version 2.1 style referrals parsing
127   * @todo Perhaps do referrals like in function sieve_get_capability()
128   */
129  function get_response()
130  {
131    if($this->loggedin == false or feof($this->fp)){
132        $this->error = EC_NOT_LOGGED_IN;
133        $this->error_raw = "You are not logged in.";
134        return false;
135    }
136
137    unset($this->response);
138    unset($this->error);
139    unset($this->error_raw);
140
141    $this->line=fgets($this->fp,1024);
142    $this->token = split(" ", $this->line, 2);
143
144    if($this->token[0] == "NO"){
145        /* we need to try and extract the error code from here.  There are two possibilites: one, that it will take the form of:
146           NO ("yyyyy") "zzzzzzz" or, two, NO {yyyyy} "zzzzzzzzzzz" */
147        $this->x = 0;
148        list($this->ltoken, $this->mtoken, $this->rtoken) = split(" ", $this->line." ", 3);
149        if($this->mtoken[0] == "{"){
150            while($this->mtoken[$this->x] != "}" or $this->err_len < 1){
151                $this->err_len = substr($this->mtoken, 1, $this->x);
152                $this->x++;
153            }
154            //print "<br>Trying to receive $this->err_len bytes for result<br>";
155            $this->line = fgets($this->fp,$this->err_len);
156            $this->error_raw[]=substr($this->line, 0, strlen($this->line) -2);    //we want to be nice and strip crlf's
157            $this->err_recv = strlen($this->line);
158
159            while($this->err_recv < $this->err_len-1){
160                //print "<br>Trying to receive ".($this->err_len-$this->err_recv)." bytes for result<br>";
161                $this->line = fgets($this->fp, ($this->err_len-$this->err_recv));
162                $this->error_raw[]=substr($this->line, 0, strlen($this->line) -2);    //we want to be nice and strip crlf's
163                $this->err_recv += strlen($this->line);
164            } /* end while */
165            $this->line = fgets($this->fp, 1024);    //we need to grab the last crlf, i think.  this may be a bug...
166            $this->error=EC_UNKNOWN;
167
168        } /* end if */
169        elseif($this->mtoken[0] == "("){
170            switch($this->mtoken){
171                case "(\"QUOTA\")":
172                    $this->error = EC_QUOTA;
173                    $this->error_raw=$this->rtoken;
174                    break;
175                default:
176                    $this->error = EC_UNKNOWN;
177                    $this->error_raw=$this->rtoken;
178                    break;
179            } /* end switch */
180        } /* end elseif */
181        else{
182            $this->error = EC_UNKNOWN;
183            $this->error_raw = $this->line;
184        }
185        return false;
186
187    } /* end if */
188    elseif(substr($this->token[0],0,2) == "OK"){
189         return true;
190    } /* end elseif */
191    elseif($this->token[0][0] == "{"){
192
193        /* Unable wild assumption:  that the only function that gets here is the get_script(), doesn't really matter though */
194
195        /* the first line is the len field {xx}, which we don't care about at this point */
196        $this->line = fgets($this->fp,1024);
197        while(substr($this->line,0,2) != "OK" and substr($this->line,0,2) != "NO"){
198            $this->response[]=$this->line;
199            $this->line = fgets($this->fp, 1024);
200        }
201        if(substr($this->line,0,2) == "OK")
202            return true;
203        else
204            return false;
205    } /* end elseif */
206    elseif($this->token[0][0] == "\""){
207
208        /* I'm going under the _assumption_ that the only function that will get here is the listscripts().
209           I could very well be mistaken here, if I am, this part needs some rework */
210
211        $this->found_script=false;
212
213        while(substr($this->line,0,2) != "OK" and substr($this->line,0,2) != "NO"){
214            $this->found_script=true;
215            list($this->ltoken, $this->rtoken) = explode(" ", $this->line." ",2);
216        //hmmm, a bug in php, if there is no space on explode line, a warning is generated...
217
218            if(strcmp(rtrim($this->rtoken), "ACTIVE")==0){
219                $this->response["ACTIVE"] = substr(rtrim($this->ltoken),1,-1);
220            }
221            else
222                $this->response[] = substr(rtrim($this->ltoken),1,-1);
223            $this->line = fgets($this->fp, 1024);
224        } /* end while */
225
226        return true;
227
228    } /* end elseif */
229    elseif(strstr($this->token[1], '(REFERRAL "' ) ){
230        /* process a referral, retry the lastcmd, return the results.  this is
231           sort of messy, really I should probably try to use parse_for_quotes
232           but the problem is I still have the ( )'s to deal with.  This is
233           atleast true for timsieved as it sits in 2.1.16, if someone has a
234           BYE (REFERRAL ...) example for later timsieved please forward it to
235           me and I'll code it in proper-like! - mloftis@wgops.com */
236        $this->reftok = split(" ", $this->token[1], 3);
237        $this->refsv = substr($this->reftok[1], 0, -2);
238        $this->refsv = substr($this->refsv, 1);
239
240        /* TODO - perform more testing */
241        if(strstr($this->capabilities['implementation'], 'v2.1')) {
242            /* Cyrus 2.1 - Style referrals */
243            $this->host = $this->refsv;
244        } else {
245            /* Cyrus 2.2 - Style referrals */
246            $tmp = array_reverse( explode( '/', $this->refsv ) );
247            $this->host = $tmp[0];
248        }
249        $this->loggedin = false;
250        /* flush buffers or anything?  probably not, and the remote has already closed it's
251           end by now!  */
252        fclose($this->fp);
253
254        if( sieve::sieve_login() ) {
255            fputs($this->fp, $this->lastcmd);
256            return sieve::get_response();
257        } /* end good case happy ending */
258        else{
259            /* what to do?  login failed, should we punt and die? or log back into the referrer?
260               i'm electing to retn EC_UNKNOWN for now and set the error string. */
261            $this->loggedin = false;
262            fclose($this->fp);
263            $this->error = EC_UNKNOWN;
264            $this->error_raw = 'UNABLE TO FOLLOW REFERRAL - ' . $this->line;
265            return false;
266        } /* end else of the unhappy ending */
267
268        /* should never make it here! */
269
270    } /* end elseif */
271    else{
272            $this->error = EC_UNKNOWN;
273            $this->error_raw = $this->line;
274        print '<b><i>UNKNOWN ERROR (Please report this line to <a
275        href="mailto:sieve-php-devel@lists.sourceforge.net">sieve-php-devel
276        Mailing List</a> to include in future releases):
277        '.$this->line.'</i></b><br>';
278
279            return false;
280    } /* end else */
281  } /* end get_response() */
282
283  /**
284   * Initialization of the SIEVE class.
285   *
286   * It will return
287   * false if it fails, true if all is well.  This also loads some arrays up
288   * with some handy information:
289   *
290   * @param $host string hostname to connect to. Usually the IMAP server where
291   * a SIEVE daemon, such as timsieved, is listening.
292   *
293   * @param $port string Numeric port to connect to. SIEVE daemons usually
294   * listen to port 2000.
295   *
296   * @param $user string is the  user identity for which the SIEVE scripts
297   * will be managed (also know as authcid).
298   *
299   * @param $pass string password to use for authentication
300   *
301   * @param $auth string is a super-user or proxy-user that has ACL rights to
302   * login on behalf of the $auth (also know as authzid).
303   *
304   * @param $auth_types string a string containing all the allowed
305   * authentication types allowed in order of preference, seperated by spaces.
306   * (ex.  "PLAIN DIGEST-MD5 CRAM-MD5"  The method the library will try first
307   * is PLAIN.) The default for this value is PLAIN.
308   *
309   * Note: $user, if included, is the account name (and $pass will be the
310   * password) of an administrator account that can act on behalf of the user.
311   * If you are using Cyrus, you must make sure that the admin account has
312   * rights to admin the user.  This is to allow admins to edit/view users
313   * scripts without having to know the user's password.  Very handy.
314   */
315  function sieve($host, $port, $user, $pass, $auth="", $auth_types='PLAIN') {
316    $this->host=$host;
317    $this->port=$port;
318    $this->user=$user;
319    $this->pass=$pass;
320    if(!strcmp($auth, ""))        /* If there is no auth user, we deem the user itself to be the auth'd user */
321        $this->auth = $this->user;
322    else
323        $this->auth = $auth;
324    $this->auth_types=$auth_types;    /* Allowed authentication types */
325
326    $this->broken_tls = false;
327
328    $this->fp=0;
329    $this->line="";
330    $this->retval="";
331    $this->tmpfile="";
332    $this->fh=0;
333    $this->len=0;
334    $this->capabilities="";
335    $this->loggedin=false;
336    $this->error= "";
337    $this->error_raw="";
338  }
339
340   /**
341    * Tokenize a line of input by quote marks and return them as an array
342    *
343    * @param $string string Input line to parse for quotes
344    * @return array Array of broken by quotes parts of original string
345    */
346  function parse_for_quotes($string) {
347
348      $start = -1;
349      $index = 0;
350
351      for($ptr = 0; $ptr < strlen($string); $ptr++){
352          if($string[$ptr] == '"' and $string[$ptr] != '\\'){
353              if($start == -1){
354                  $start = $ptr;
355              } /* end if */
356              else{
357                  $token[$index++] = substr($string, $start + 1, $ptr - $start - 1);
358                  $found = true;
359                  $start = -1;
360              } /* end else */
361
362          } /* end if */
363
364      } /* end for */
365
366      if(isset($token))
367          return $token;
368      else
369          return false;
370  } /* end function */
371
372  /**
373   * Parser for status responses.
374   *
375   * This should probably be replaced by a smarter parser.
376   *
377   * @param $string string Input that contains status responses.
378   * @todo remove this function and dependencies
379   */
380  function status($string) {
381
382      /*  Need to remove this and all dependencies from the class */
383
384      switch (substr($string, 0,2)){
385          case "NO":
386              return F_NO;        //there should be some function to extract the error code from this line
387                    //NO ("quota") "You are oly allowed x number of scripts"
388              break;
389          case "OK":
390              return F_OK;
391              break;
392          default:
393              switch ($string[0]){
394                  case "{":
395                      //do parse here for curly braces - maybe modify
396                      //parse_for_quotes to handle any parse delimiter?
397                      return F_HEAD;
398                      break;
399                  default:
400                      return F_DATA;
401                      break;
402              }
403        }
404  }
405
406  /**
407   * Attemp to log in to the sieve server.
408   *
409   * It will return false if it fails, true if all is well.  This also loads
410   * some arrays up with some handy information:
411   *
412   * capabilities["implementation"] contains the sieve version information
413   *
414   * capabilities["auth"] contains the supported authentication modes by the
415   * SIEVE server.
416   *
417   * capabilities["modules"] contains the built in modules like "reject",
418   * "redirect", etc.
419   *
420   * capabilities["starttls"] , if is set and equal to true, will show that the
421   * server supports the STARTTLS extension.
422   *
423   * capabilities["unknown"] contains miscellaneous/extraneous header info sieve
424   * may have sent
425   *
426   * @return boolean
427   */
428  function sieve_login() {
429    $this->fp=@fsockopen($this->host,$this->port, $errno, $errstr);
430    if($this->fp == false) {
431        $this->error = $errno. ' '.$errstr;
432        return false;
433    }
434
435    $this->line=fgets($this->fp,1024);
436
437    //Hack for older versions of Sieve Server.  They do not respond with the Cyrus v2+ standard
438    //response.  They repsond as follows: "Cyrus timsieved v1.0.0" "SASL={PLAIN,........}"
439    //So, if we see IMPLEMENTATION in the first line, then we are done.
440
441    if(ereg("IMPLEMENTATION",$this->line))
442    {
443      //we're on the Cyrus V2 or Cyrus V3 sieve server
444      while(sieve::status($this->line) == F_DATA){
445          $this->item = sieve::parse_for_quotes($this->line);
446
447          if(strcmp($this->item[0], "IMPLEMENTATION") == 0)
448              $this->capabilities["implementation"] = $this->item[1];
449
450          elseif(strcmp($this->item[0], "SIEVE") == 0 or strcmp($this->item[0], "SASL") == 0){
451
452              if(strcmp($this->item[0], "SIEVE") == 0) {
453                  $this->cap_type="modules";
454              } else {
455                  $this->cap_type="auth";
456              }
457              $this->modules = split(" ", $this->item[1]);
458              if(is_array($this->modules)){
459                  foreach($this->modules as $this->module)
460                      $this->capabilities[$this->cap_type][$this->module]=true;
461              } /* end if */
462              elseif(is_string($this->modules))
463                  $this->capabilites[$this->cap_type][$this->modules]=true;
464          }
465          elseif(strcmp($this->item[0], "STARTTLS") == 0) {
466              $this->capabilities['starttls'] = true;
467
468          }
469      else{
470              $this->capabilities["unknown"][]=$this->line;
471          }
472      $this->line=fgets($this->fp,1024);
473
474       }// end while
475    }
476    else
477    {
478        //we're on the older Cyrus V1. server
479        //this version does not support module reporting.  We only have auth types.
480        $this->cap_type="auth";
481
482        //break apart at the "Cyrus timsieve...." "SASL={......}"
483        $this->item = sieve::parse_for_quotes($this->line);
484
485        $this->capabilities["implementation"] = $this->item[0];
486
487        //we should have "SASL={..........}" now.  Break out the {xx,yyy,zzzz}
488        $this->modules = substr($this->item[1], strpos($this->item[1], "{"),strlen($this->item[1])-1);
489
490        //then split again at the ", " stuff.
491        $this->modules = split($this->modules, ", ");
492
493        //fill up our $this->modules property
494        if(is_array($this->modules)){
495            foreach($this->modules as $this->module)
496                $this->capabilities[$this->cap_type][$this->module]=true;
497        } /* end if */
498        elseif(is_string($this->modules))
499            $this->capabilites[$this->cap_type][$this->module]=true;
500
501    // set broken_tls. Older cyrus servers do not respond with
502    // capabilities after STARTTLS
503    $broken_tls = true;
504    }
505
506    if(sieve::status($this->line) == F_NO){        //here we should do some returning of error codes?
507        $this->error=EC_UNKNOWN;
508        $this->error_raw = "Server not allowing connections.";
509        return false;
510    }
511
512    /* decision login to decide what type of authentication to use... */
513
514    /* If we allow STARTTLS, use it */
515    if(isset($this->capabilities['starttls']) && $this->capabilities['starttls'] === true &&
516      function_exists('stream_socket_enable_crypto') === true && !$this->disabletls ) {
517        fputs($this->fp,"STARTTLS\r\n");
518        $starttls_response = $this->line=fgets($this->fp,1024);
519        if(stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT) == false) {
520            $this->error=EC_UNKNOWN;
521            $this->error_raw = "Failed to establish TLS connection.";
522            return false;
523        } else {
524            $this->loggedin = true;
525            // RFC says that we get an unsolicited capability response after TLS negotiation. Older Cyrus
526            // did not do this. If the server has old/broken TLS we need to send a CAPABILITY command,
527            // otherwise we just parse the unsolicited capability response.
528            if ( $this->broken_tls )
529                $this->sieve_get_capability();
530            else
531                $this->sieve_read_capability_response();
532            $this->loggedin = false;
533        }
534    }
535
536    /* Loop through each allowed authentication type and see if the server allows the type */
537    foreach(explode(" ", $this->auth_types) as $auth_type) {
538        if ($this->capabilities["auth"][$auth_type]) {
539            /* We found an auth type that is allowed. */
540            $this->auth_in_use = $auth_type;
541        }
542    }
543
544    /* call our authentication program */
545    return sieve::authenticate();
546  }
547
548  /**
549   * Log out of the sieve server.
550   *
551   * @return boolean Always returns true at this point.
552   */
553  function sieve_logout() {
554    if($this->loggedin==false)
555        return false;
556
557    fputs($this->fp,"LOGOUT\r\n");
558    fclose($this->fp);
559    $this->loggedin=false;
560    return true;
561  }
562
563  /**
564   * Send the script contained in $script to the server.
565   *
566   * It will return any error results it finds (in $sieve->error and
567   * $sieve->error_raw), and return true if it is successfully sent.  The
568   * function does _not_ automatically make the script the active script.
569   *
570   * @param $scriptname string The name of the SIEVE script.
571   * @param $script The script to be uploaded.
572   * @return boolean Returns true if script has been successfully uploaded.
573   */
574  function sieve_sendscript($scriptname, $script) {
575    if($this->loggedin==false)
576        return false;
577    $this->script=stripslashes($script);
578    $len=strlen($this->script);
579
580    $this->lastcmd = 'PUTSCRIPT "'.$scriptname.'" {'.$len.'+}'."\r\n".$this->script."\r\n";
581    fputs($this->fp, $this->lastcmd);
582    return sieve::get_response();
583
584  }
585
586  /**
587   * Check if there is enough space for a script to be uploaded.
588   *
589   * This function returns true or false based on whether the sieve server will
590   * allow your script to be sent and your quota has not been exceeded.  This
591   * function does not currently work due to a believed bug in timsieved.  It
592   * could be my code too.
593   *
594   * It appears the timsieved does not honor the NUMBER type.  see lex.c in
595   * timsieved src.  don't expect this function to work yet.  I might have
596   * messed something up here, too.
597   *
598   * @param $scriptname string The name of the SIEVE script.
599   * @param $scriptsize integer The size of the SIEVE script.
600   * @return boolean
601   * @todo Does not work; bug fix and test.
602   */
603  function sieve_havespace($scriptname, $scriptsize)   {
604    if($this->loggedin==false)
605        return false;
606
607    $this->lastcmd = "HAVESPACE \"$scriptname\" $scriptsize\r\n";
608    fputs($this->fp, $this->lastcmd);
609    return sieve::get_response();
610  }
611
612  /**
613   * Set the script active on the sieve server.
614   *
615   * @param $scriptname string The name of the SIEVE script.
616   * @return boolean
617   */
618  function sieve_setactivescript($scriptname)   {
619    if($this->loggedin==false)
620        return false;
621
622        $this->lastcmd = "SETACTIVE \"$scriptname\"\r\n";
623    fputs($this->fp, $this->lastcmd);
624    return sieve::get_response();
625
626  }
627
628  /**
629   * Return the contents of the requested script.
630   *
631   * If you want to display the script, you will need to change all CrLf to
632   * '.'.
633   *
634   * @param $scriptname string The name of the SIEVE script.
635   * @return arr SIEVE script data.
636   */
637  function sieve_getscript($scriptname) {
638    unset($this->script);
639    if($this->loggedin==false)
640        return false;
641
642    $this->lastcmd = "GETSCRIPT \"$scriptname\"\r\n";
643    fputs($this->fp, $this->lastcmd);
644    return sieve::get_response();
645  }
646
647  /**
648   * Attempt to delete the script requested.
649   *
650   * If the script is currently active, the server will not have any active
651   * script after the deletion.
652   *
653   * @param $scriptname string The name of the SIEVE script.
654   * @return mixed
655   */
656  function sieve_deletescript($scriptname)   {
657    if($this->loggedin==false)
658        return false;
659
660    // If there is an active script, "inactivate" it first.
661    $this->sieve_listscripts();
662    if ($this->response['ACTIVE'] === $scriptname)
663        $this->sieve_setactivescript("");
664
665    $this->lastcmd = "DELETESCRIPT \"$scriptname\"\r\n";
666    fputs($this->fp, $this->lastcmd);
667
668    return sieve::get_response();
669  }
670
671
672  /**
673   * List available scripts on the SIEVE server.
674   *
675   * This function returns true or false.  $sieve->response will be filled
676   * with the names of the scripts found.  If a script is active, the
677   * $sieve->response["ACTIVE"] will contain the name of the active script.
678   *
679   * @return boolean
680   */
681  function sieve_listscripts() {
682       $this->lastcmd = "LISTSCRIPTS\r\n";
683     fputs($this->fp, $this->lastcmd);
684     sieve::get_response();        //should always return true, even if there are no scripts...
685     if(isset($this->found_script) and $this->found_script)
686         return true;
687     else{
688         $this->error=EC_NOSCRIPTS;    //sieve::getresponse has no way of telling wether a script was found...
689         $this->error_raw="No scripts found for this account.";
690         return false;
691     }
692   }
693
694
695  /**
696   * Check availability of connection to the SIEVE server.
697   *
698   * This function returns true or false based on whether the connection to the
699   * sieve server is still alive.
700   *
701   * @return boolean
702   */
703  function sieve_alive()   {
704      if(!isset($this->fp) or $this->fp==0){
705          $this->error = EC_NOT_LOGGED_IN;
706          return false;
707      }
708      elseif(feof($this->fp)){
709          $this->error = EC_NOT_LOGGED_IN;
710          return false;
711      }
712      else
713          return true;
714  }
715
716   /**
717   * Neil Darlow - 2009/03/26
718   * Changes to Sieve servers to be compliant with draft-managesieve-09.txt
719   * break authenticate() which cannot handle the capability strings received
720   * after AUTHENTICATE under a TLS encrypted session. We ignore capability
721   * strings for each SASL mechanism.
722   */
723  function ignore_capabilities()
724  {
725    if ($this->capabilities['starttls']) do {
726      $line = fgets($this->fp, 1024);
727    } while (strncmp($line, 'OK', 2));
728  }
729
730  /**
731   * Perform SASL authentication to SIEVE server.
732   *
733   * Attempts to authenticate to SIEVE, using some SASL authentication method
734   * such as PLAIN or DIGEST-MD5.
735   *
736   */
737  function authenticate() {
738
739    switch ($this->auth_in_use) {
740
741        case "PLAIN":
742            $auth=base64_encode($this->user."\0".$this->auth."\0".$this->pass);
743
744            $this->len=strlen($auth);
745            fputs($this->fp, 'AUTHENTICATE "PLAIN" {' . $this->len . '+}' . "\r\n");
746            $this->ignore_capabilities();
747            fputs($this->fp, "$auth\r\n");
748
749            $this->line=fgets($this->fp,1024);
750            while(sieve::status($this->line) == F_DATA)
751               $this->line=fgets($this->fp,1024);
752
753             if(sieve::status($this->line) == F_NO)
754               return false;
755             $this->loggedin=true;
756               return true;
757        break;
758
759        case "DIGEST-MD5":
760         // SASL DIGEST-MD5 support works with timsieved 1.1.0
761         // follows rfc2831 for generating the $response to $challenge
762         fputs($this->fp, "AUTHENTICATE \"DIGEST-MD5\"\r\n");
763         $this->ignore_capabilities();
764         // $clen is length of server challenge, we ignore it.
765         $clen = fgets($this->fp, 1024);
766         // read for 2048, rfc2831 max length allowed
767         $challenge = fgets($this->fp, 2048);
768         // vars used when building $response_value and $response
769         $cnonce = base64_encode(bin2hex(hmac_md5(microtime())));
770         $ncount = "00000001";
771         $qop_value = "auth";
772         $digest_uri_value = "sieve/$this->host";
773         // decode the challenge string
774         $result = decode_challenge($challenge);
775         // verify server supports qop=auth
776         $qop = explode(",",$result['qop']);
777         if (!in_array($qop_value, $qop)) {
778            // rfc2831: client MUST fail if no qop methods supported
779            return false;
780         }
781         // build the $response_value
782         $string_a1 = utf8_encode($this->user).":";
783         $string_a1 .= utf8_encode($result['realm']).":";
784         $string_a1 .= utf8_encode($this->pass);
785         $string_a1 = hmac_md5($string_a1);
786         $A1 = $string_a1.":".$result['nonce'].":".$cnonce.":".utf8_encode($this->auth);
787         $A1 = bin2hex(hmac_md5($A1));
788         $A2 = bin2hex(hmac_md5("AUTHENTICATE:$digest_uri_value"));
789         $string_response = $result['nonce'].":".$ncount.":".$cnonce.":".$qop_value;
790         $response_value = bin2hex(hmac_md5($A1.":".$string_response.":".$A2));
791         // build the challenge $response
792         $reply = "charset=utf-8,username=\"".$this->user."\",realm=\"".$result['realm']."\",";
793         $reply .= "nonce=\"".$result['nonce']."\",nc=$ncount,cnonce=\"$cnonce\",";
794         $reply .= "digest-uri=\"$digest_uri_value\",response=$response_value,";
795         $reply .= "qop=$qop_value,authzid=\"".utf8_encode($this->auth)."\"";
796         $response = base64_encode($reply);
797         fputs($this->fp, "\"$response\"\r\n");
798
799             $this->line = fgets($this->fp, 1024);
800             while(sieve::status($this->line) == F_DATA)
801                $this->line = fgets($this->fp,1024);
802
803             if(sieve::status($this->line) == F_NO)
804               return false;
805             $this->loggedin = TRUE;
806               return TRUE;
807             break;
808
809        case "CRAM-MD5":
810           // SASL CRAM-MD5 support works with timsieved 1.1.0
811         // follows rfc2195 for generating the $response to $challenge
812         // CRAM-MD5 does not support proxy of $auth by $user
813         // requires php mhash extension
814         fputs($this->fp, "AUTHENTICATE \"CRAM-MD5\"\r\n");
815         $this->ignore_capabilities();
816         // $clen is the length of the challenge line the server gives us
817         $clen = fgets($this->fp, 1024);
818         // read for 1024, should be long enough?
819         $challenge = fgets($this->fp, 1024);
820         // build a response to the challenge
821         $hash = bin2hex(hmac_md5(base64_decode($challenge), $this->pass));
822         $response = base64_encode($this->user." ".$hash);
823         // respond to the challenge string
824         fputs($this->fp, "\"$response\"\r\n");
825
826             $this->line = fgets($this->fp, 1024);
827             while(sieve::status($this->line) == F_DATA)
828                $this->line = fgets($this->fp,1024);
829
830             if(sieve::status($this->line) == F_NO)
831               return false;
832             $this->loggedin = TRUE;
833               return TRUE;
834             break;
835
836    case "LOGIN":
837          $login=base64_encode($this->user);
838          $pass=base64_encode($this->pass);
839
840          fputs($this->fp, "AUTHENTICATE \"LOGIN\"\r\n");
841          $this->ignore_capabilities();
842          fputs($this->fp, "{".strlen($login)."+}\r\n");
843          fputs($this->fp, "$login\r\n");
844          fputs($this->fp, "{".strlen($pass)."+}\r\n");
845          fputs($this->fp, "$pass\r\n");
846
847         $this->line=fgets($this->fp,1024);
848          while(sieve::status($this->line) == F_HEAD ||
849                sieve::status($this->line) == F_DATA)
850              $this->line=fgets($this->fp,1024);
851
852          if(sieve::status($this->line) == F_NO)
853              return false;
854          $this->loggedin=true;
855          return true;
856          break;
857
858        default:
859            return false;
860            break;
861
862    }//end switch
863  }
864
865  /**
866   * Read incoming capability response.
867   *
868   * @return array
869   */
870  function sieve_read_capability_response() {
871    $this->line=fgets($this->fp,1024);
872
873    $tmp = array();
874    if(preg_match('|^BYE \(REFERRAL "(sieve://)?([^/"]+)"\)|', $this->line, $tmp ) ){
875        $this->host = $tmp[2];
876        $this->loggedin = false;
877        fclose($this->fp);
878
879        if( sieve::sieve_login() ) {
880            return $this->sieve_get_capability();
881        } else {
882            $this->loggedin = false;
883            fclose($this->fp);
884            $this->error = EC_UNKNOWN;
885            $this->error_raw = 'UNABLE TO FOLLOW REFERRAL - ' . $this->line;
886            return false;
887        }
888    }
889
890    while(sieve::status($this->line) == F_DATA){
891       $this->item = sieve::parse_for_quotes($this->line);
892
893       if(strcmp($this->item[0], "IMPLEMENTATION") == 0) {
894           $this->capabilities["implementation"] = $this->item[1];
895
896       } elseif(strcmp($this->item[0], "SIEVE") == 0 or strcmp($this->item[0], "SASL") == 0){
897
898              $cap_type = '';
899              if(strcmp($this->item[0], "SIEVE") == 0) {
900                  $cap_type="modules";
901              } else {
902                  $cap_type="auth";
903              }
904
905              $this->modules = split(' ', $this->item[1]);
906              if(is_array($this->modules)){
907                  foreach($this->modules as $m) {
908                      $this->capabilities[$cap_type][strtolower($m)]=true;
909                  }
910              } elseif(is_string($this->modules)) {
911                  $this->capabilites[$cap_type][strtolower($this->modules)]=true;
912              }
913          } else {
914              $this->capabilities["unknown"][]=$this->line;
915          }
916      $this->line=fgets($this->fp,1024);
917
918    }// end while
919    return $this->capabilities['modules'];
920  }
921
922  /**
923   * Return an array of available capabilities.
924   *
925   * @return array
926   */
927  function sieve_get_capability() {
928    if($this->loggedin==false)
929        return false;
930    fputs($this->fp, "CAPABILITY\r\n");
931
932    return $this->sieve_read_capability_response();
933  }
934}
935
936
937/**
938 * The following functions are support functions and might be handy to the
939 * sieve class.
940 */
941
942if(!function_exists('hmac_md5')) {
943
944/**
945 * Creates a HMAC digest that can be used for auth purposes.
946 * See RFCs 2104, 2617, 2831
947 * Uses mhash() extension if available
948 *
949 * Squirrelmail has this function in functions/auth.php, and it might have been
950 * included already. However, it helps remove the dependancy on mhash.so PHP
951 * extension, for some sites. If mhash.so _is_ available, it is used for its
952 * speed.
953 *
954 * This function is Copyright (c) 1999-2003 The SquirrelMail Project Team
955 * Licensed under the GNU GPL. For full terms see the file COPYING.
956 *
957 * @param string $data Data to apply hash function to.
958 * @param string $key Optional key, which, if supplied, will be used to
959 * calculate data's HMAC.
960 * @return string HMAC Digest string
961 */
962function hmac_md5($data, $key='') {
963    // See RFCs 2104, 2617, 2831
964    // Uses mhash() extension if available
965    if (extension_loaded('mhash')) {
966      if ($key== '') {
967        $mhash=mhash(MHASH_MD5,$data);
968      } else {
969        $mhash=mhash(MHASH_MD5,$data,$key);
970      }
971      return $mhash;
972    }
973    if (!$key) {
974         return pack('H*',md5($data));
975    }
976    $key = str_pad($key,64,chr(0x00));
977    if (strlen($key) > 64) {
978        $key = pack("H*",md5($key));
979    }
980    $k_ipad =  $key ^ str_repeat(chr(0x36), 64) ;
981    $k_opad =  $key ^ str_repeat(chr(0x5c), 64) ;
982    /* Heh, let's get recursive. */
983    $hmac=hmac_md5($k_opad . pack("H*",md5($k_ipad . $data)) );
984    return $hmac;
985}
986}
987
988/**
989 * A hack to decode the challenge from timsieved 1.1.0.
990 *
991 * This function may not work with other versions and most certainly won't work
992 * with other DIGEST-MD5 implentations
993 *
994 * @param $input string Challenge supplied by timsieved.
995 */
996function decode_challenge ($input) {
997    $input = base64_decode($input);
998    preg_match("/nonce=\"(.*)\"/U",$input, $matches);
999    $resp['nonce'] = $matches[1];
1000    preg_match("/realm=\"(.*)\"/U",$input, $matches);
1001    $resp['realm'] = $matches[1];
1002    preg_match("/qop=\"(.*)\"/U",$input, $matches);
1003    $resp['qop'] = $matches[1];
1004    return $resp;
1005}
1006
1007