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