1 <?php
3 /*
4 * $Id: class_sieve.inc,v 1.1 2005/02/21 09:33:01 cajus Exp $
5 *
6 * Copyright 2001 Dan Ellis <danellis@rushmore.com>
7 *
8 * See the enclosed file COPYING for license information (GPL). If you
9 * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
10 */
12 // TODO before next release: remove ::status() and dependencies
15 define ("F_NO", 0);
16 define ("F_OK", 1);
17 define ("F_DATA", 2);
18 define ("F_HEAD", 3);
20 define ("EC_NOT_LOGGED_IN", 0);
21 define ("EC_QUOTA", 10);
22 define ("EC_NOSCRIPTS", 20);
23 define ("EC_UNKNOWN", 255);
24 /*
26 SIEVE-PHP.LIB VERSION 0.0.8
28 (C) 2001 Dan Ellis.
30 PLEASE READ THE README FILE FOR MORE INFORMATION.
32 Basically, this is the first re-release. Things are much better than before.
34 Notes:
35 This program/libary has bugs.
36 . This was quickly hacked out, so please let me know what is wrong and if you feel ambitious submit
37 a patch :).
39 Todo:
40 . Provide better error diagnostics. (mostly done with ver 0.0.5)
41 . Allow other auth mechanisms besides plain (in progress)
42 . Have timing mechanism when port problems arise. (not done yet)
43 . Maybe add the NOOP function. (not done yet)
44 . Other top secret stuff.... (some done, believe me?)
46 Dan Ellis (danellis@rushmore.com)
48 This program is released under the GNU Public License.
50 You should have received a copy of the GNU Public
51 License along with this package; if not, write to the
52 Free Software Foundation, Inc., 59 Temple Place - Suite 330,
53 Boston, MA 02111-1307, USA.
55 See CHANGES for updates since last release
57 Contributers of patches:
58 Atif Ghaffar
59 Andrew Sterling Hanenkamp <sterling@hanenkamp.com>
60 */
63 class sieve
64 {
65 var $host;
66 var $port;
67 var $user;
68 var $pass;
69 var $auth_types; /* a comma seperated list of allowed auth types, in order of preference */
70 var $auth_in_use; /* type of authentication attempted */
72 var $line;
73 var $fp;
74 var $retval;
75 var $tmpfile;
76 var $fh;
77 var $len;
78 var $script;
80 var $loggedin;
81 var $capabilities;
82 var $error;
83 var $error_raw;
84 var $responses;
85 var $options = "";
87 //maybe we should add an errorlvl that the user will pass to new sieve = sieve(,,,,E_WARN)
88 //so we can decide how to handle certain errors?!?
90 //also add a connection type, like PLAIN, MD5, etc...
93 function get_response()
94 {
95 if($this->loggedin == false or feof($this->fp)){
96 $this->error = EC_NOT_LOGGED_IN;
97 $this->error_raw = "You are not logged in.";
98 return false;
99 }
101 unset($this->response);
102 unset($this->error);
103 unset($this->error_raw);
105 $this->line=fgets($this->fp,1024);
106 $this->token = explode(" ", $this->line, 2);
108 if($this->token[0] == "NO"){
109 /* we need to try and extract the error code from here. There are two possibilites: one, that it will take the form of:
110 NO ("yyyyy") "zzzzzzz" or, two, NO {yyyyy} "zzzzzzzzzzz" */
111 $this->x = 0;
112 list($this->ltoken, $this->mtoken, $this->rtoken) = explode(" ", $this->line." ", 3);
113 if($this->mtoken[0] == "{"){
114 while($this->mtoken[$this->x] != "}" or $this->err_len < 1){
115 $this->err_len = substr($this->mtoken, 1, $this->x);
116 $this->x++;
117 }
118 #print "<br>Trying to receive $this->err_len bytes for result<br>";
119 $this->line = fgets($this->fp,$this->err_len);
120 $this->error_raw[]=substr($this->line, 0, strlen($this->line) -2); //we want to be nice and strip crlf's
121 $this->err_recv = strlen($this->line);
123 /* Ensure max loop of 1000, to keep the ui from freezing */
124 $max = 1000;
125 $cur = 0;
127 while($this->err_recv < ($this->err_len) && ($cur < $max)){
129 $cur ++;
130 $this->line = fgets($this->fp,4096);
131 $this->err_recv += strlen($this->line);
132 $this->error_raw[]=preg_replace("/\r\n/","",$this->line); //we want to be nice and strip crlf'
133 }
134 } /* end if */
135 elseif($this->mtoken[0] == "("){
136 switch($this->mtoken){
137 case "(\"QUOTA\")":
138 $this->error = EC_QUOTA;
139 $this->error_raw=$this->rtoken;
140 break;
141 default:
142 $this->error = EC_UNKNOWN;
143 $this->error_raw=$this->rtoken;
144 break;
145 } /* end switch */
146 } /* end elseif */
147 else{
148 $this->error = EC_UNKNOWN;
149 $this->error_raw = $this->line;
150 }
151 return false;
153 } /* end if */
154 elseif(substr($this->token[0],0,-2) == "OK"){
155 return true;
156 } /* end elseif */
157 elseif($this->token[0][0] == "{"){
159 /* Unable wild assumption: that the only function that gets here is the get_script(), doesn't really matter though */
161 /* the first line is the len field {xx}, which we don't care about at this point */
162 $this->line = fgets($this->fp,1024);
163 while(substr($this->line,0,2) != "OK" and substr($this->line,0,2) != "NO"){
164 $this->response[]=$this->line;
165 $this->line = fgets($this->fp, 1024);
166 }
167 if(substr($this->line,0,2) == "OK")
168 return true;
169 else
170 return false;
171 } /* end elseif */
172 elseif($this->token[0][0] == "\""){
174 /* I'm going under the _assumption_ that the only function that will get here is the listscripts().
175 I could very well be mistaken here, if I am, this part needs some rework */
177 $this->found_script=false;
179 while(substr($this->line,0,2) != "OK" and substr($this->line,0,2) != "NO"){
180 $this->found_script=true;
181 list($this->ltoken, $this->rtoken) = explode(" ", $this->line." ",2);
182 //hmmm, a bug in php, if there is no space on explode line, a warning is generated...
184 if(strcmp(rtrim($this->rtoken), "ACTIVE")==0){
185 $this->response["ACTIVE"] = substr(rtrim($this->ltoken),1,-1);
186 }
187 else
188 $this->response[] = substr(rtrim($this->ltoken),1,-1);
189 $this->line = fgets($this->fp, 1024);
190 } /* end while */
192 return true;
194 } /* end elseif */
195 else{
196 $this->error = EC_UNKNOWN;
197 $this->error_raw = $this->line;
198 print "<b><i>UNKNOWN ERROR (Please report this line to danellis@rushmore.com to include in future releases): $this->line</i></b><br>";
199 return false;
200 } /* end else */
201 } /* end get_response() */
203 function sieve($host, $port, $user, $pass, $auth="",$options ="", $auth_types="PLAIN DIGEST-MD5")
204 {
205 $this->host=$host;
206 $this->port=$port;
207 $this->user=$user;
208 $this->pass=$pass;
209 if(!strcmp($auth, "")) /* If there is no auth user, we deem the user itself to be the auth'd user */
210 $this->auth = $this->user;
211 else
212 $this->auth = $auth;
213 $this->auth_types=$auth_types; /* Allowed authentication types */
214 $this->fp=0;
215 $this->line="";
216 $this->retval="";
217 $this->tmpfile="";
218 $this->fh=0;
219 $this->len=0;
220 $this->capabilities="";
221 $this->loggedin=false;
222 $this->error= "";
223 $this->error_raw="";
224 $this->options = $options;
226 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__, $this->error_raw,
227 "<b>SIEVE: Host: $host:$port. Options: $options - Auth Types: $auth_types </b>");
228 }
230 function parse_for_quotes($string)
231 {
232 /* This function tokenizes a line of input by quote marks and returns them as an array */
234 $start = -1;
235 $index = 0;
237 for($ptr = 0; $ptr < strlen($string); $ptr++){
238 if($string[$ptr] == '"' and $string[$ptr] != '\\'){
239 if($start == -1){
240 $start = $ptr;
241 } /* end if */
242 else{
243 $token[$index++] = substr($string, $start + 1, $ptr - $start - 1);
244 $found = true;
245 $start = -1;
246 } /* end else */
248 } /* end if */
250 } /* end for */
252 if(isset($token))
253 return $token;
254 else
255 return false;
256 } /* end function */
258 function status($string)
259 {
260 //this should probably be replaced by a smarter parser.
262 /* Need to remove this and all dependencies from the class */
264 switch (substr($string, 0,2)){
265 case "NO":
266 return F_NO; //there should be some function to extract the error code from this line
267 //NO ("quota") "You are oly allowed x number of scripts"
268 break;
269 case "OK":
270 return F_OK;
271 break;
272 default:
273 switch ($string[0]){
274 case "{":
275 //do parse here for {}'s maybe modify parse_for_quotes to handle any parse delimiter?
276 return F_HEAD;
277 break;
278 default:
279 return F_DATA;
280 break;
281 } /* end switch */
282 } /* end switch */
283 } /* end status() */
285 function sieve_login()
286 {
288 $this->fp=@fsockopen($this->host,$this->port,$err_id,$err_str);
289 if($this->fp == false){
291 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,$this->error_raw,
292 "<b>SIEVE: Socket connection failed. (".$this->host.":".$this->port.")</b>");
294 $this->error_raw = $err_str;
295 return false;
296 }
298 $this->line=fgets($this->fp,1024);
300 //Hack for older versions of Sieve Server. They do not respond with the Cyrus v2. standard
301 //response. They repsond as follows: "Cyrus timsieved v1.0.0" "SASL={PLAIN,........}"
302 //So, if we see IMLEMENTATION in the first line, then we are done.
304 if(strstr($this->line, "IMPLEMENTATION"))
305 {
306 //we're on the Cyrus V2 sieve server
307 while(sieve::status($this->line) == F_DATA){
309 $this->item = sieve::parse_for_quotes($this->line);
311 if(strcmp($this->item[0], "IMPLEMENTATION") == 0)
312 $this->capabilities["implementation"] = $this->item[1];
314 elseif(strcmp($this->item[0], "SIEVE") == 0 or strcmp($this->item[0], "SASL") == 0){
316 if(strcmp($this->item[0], "SIEVE") == 0){
317 $this->cap_type="modules";
318 }else{
319 $this->cap_type="auth";
320 }
322 $this->modules = explode(" ", $this->item[1]);
323 if(is_array($this->modules)){
324 foreach($this->modules as $this->module)
325 $this->capabilities[$this->cap_type][$this->module]=true;
326 } /* end if */
327 elseif(is_string($this->modules))
328 $this->capabilites[$this->cap_type][$this->modules]=true;
329 }
330 else{
331 $this->capabilities["unknown"][]=$this->line;
332 }
333 $this->line=fgets($this->fp,1024);
335 }// end while
336 }
337 else
338 {
339 //we're on the older Cyrus V1. server
340 //this version does not support module reporting. We only have auth types.
341 $this->cap_type="auth";
343 //break apart at the "Cyrus timsieve...." "SASL={......}"
344 $this->item = sieve::parse_for_quotes($this->line);
346 $this->capabilities["implementation"] = $this->item[0];
348 //we should have "SASL={..........}" now. Break out the {xx,yyy,zzzz}
349 $this->modules = substr($this->item[1], strpos($this->item[1], "{"),strlen($this->item[1])-1);
351 //then split again at the ", " stuff.
352 $this->modules = explode(", ", $this->modules);
354 //fill up our $this->modules property
355 if(is_array($this->modules)){
356 foreach($this->modules as $this->module)
357 $this->capabilities[$this->cap_type][$this->module]=true;
358 } /* end if */
359 elseif(is_string($this->modules))
360 $this->capabilites[$this->cap_type][$this->module]=true;
361 }
366 if(sieve::status($this->line) == F_NO){ //here we should do some returning of error codes?
367 $this->error=EC_UNKNOWN;
369 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,$this->error_raw,
370 "<b>SIEVE: Unknown sieve response, giving up.</b>");
372 $this->error_raw = "Server not allowing connections.";
373 return false;
374 }
376 /* Loop through each allowed authentication type and see if the server allows the type */
377 foreach(explode(" ",$this->auth_types) as $auth_type)
378 {
379 if (isset($this->capabilities["auth"][$auth_type]))
380 {
381 /* We found an auth type that is allowed. */
382 $this->auth_in_use = $auth_type;
383 break;
384 }
385 }
387 /* Sometimes we do not get any authentification methods, assume PLAIN here.
388 * #TODO Maybe there is a better solution for this?
389 */
390 if($this->auth_in_use == ""){
391 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,$this->error_raw,
392 "<b>SIEVE: No authentification mechanisms received, try using PLAIN!.</b>");
393 $this->auth_in_use = "PLAIN";
394 }
396 /* Fill error message if no auth types are present */
397 if (!isset($this->capabilities["auth"])){
398 $this->error=EC_UNKNOWN;
399 $this->error_raw = "No authentication methods found - please check your sieve setup for missing sasl modules";
400 return false;
401 }
403 /* DEBUG */
404 $imp = "";
405 if(isset($this->capabilities['implementation'])){
406 $imp = $this->capabilities['implementation'];
407 }
408 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,$imp,
409 "<b>SIEVE: Socket connection established. </b>");
411 /* call our authentication program */
412 return sieve::authenticate();
414 }
416 function sieve_logout()
417 {
418 if($this->loggedin==false)
419 return false;
421 fputs($this->fp,"LOGOUT\r\n");
422 fclose($this->fp);
424 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,"",
425 "<b>SIEVE: Logout!</b>");
427 $this->loggedin=false;
428 return true;
429 }
431 function sieve_sendscript($scriptname, $script)
432 {
433 if($this->loggedin==false)
434 return false;
435 $this->script=stripslashes($script);
436 $len=strlen($this->script);
437 fputs($this->fp, "PUTSCRIPT \"$scriptname\" {".$len."+}\r\n");
438 fputs($this->fp, "$this->script\r\n");
440 if(sieve::get_response()){
441 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,"",
442 "<b>SIEVE: Script '".$scriptname."' successfully send!</b>");
443 return(TRUE);
444 }else{
445 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,trim($this->error_raw),
446 "<b>SIEVE: Script couldn't be send!</b>");
447 return(FALSE);
448 }
449 }
451 //it appears the timsieved does not honor the NUMBER type. see lex.c in timsieved src.
452 //don't expect this function to work yet. I might have messed something up here, too.
453 function sieve_havespace($scriptname, $scriptsize)
454 {
455 if($this->loggedin==false)
456 return false;
457 fputs($this->fp, "HAVESPACE \"$scriptname\" $scriptsize\r\n");
458 return sieve::get_response();
460 }
462 function sieve_setactivescript($scriptname)
463 {
464 if($this->loggedin==false)
465 return false;
467 fputs($this->fp, "SETACTIVE \"$scriptname\"\r\n");
469 if(sieve::get_response()){
470 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,"",
471 "<b>SIEVE: Set active script '".$scriptname."' was successful!</b>");
472 return(TRUE);
473 }else{
474 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,trim($this->error_raw),
475 "<b>SIEVE: Set active script '".$scriptname."' failed!</b>");
476 return(FALSE);
477 }
478 }
480 function sieve_getscript($scriptname)
481 {
482 unset($this->script);
483 if($this->loggedin==false)
484 return false;
486 fputs($this->fp, "GETSCRIPT \"$scriptname\"\r\n");
488 if(sieve::get_response()){
489 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,"",
490 "<b>SIEVE: Get script '".$scriptname."' was successful!</b>");
491 return(TRUE);
492 }else{
493 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,trim($this->error_raw),
494 "<b>SIEVE: Get script '".$scriptname."' failed!</b>");
495 return(FALSE);
496 }
497 }
500 function sieve_deletescript($scriptname)
501 {
502 if($this->loggedin==false)
503 return false;
505 fputs($this->fp, "DELETESCRIPT \"$scriptname\"\r\n");
507 if(sieve::get_response()){
508 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,"",
509 "<b>SIEVE: Delete script '".$scriptname."' was successful!</b>");
510 return(TRUE);
511 }else{
512 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,trim($this->error_raw),
513 "<b>SIEVE: Delete script '".$scriptname."' failed!</b>");
514 return(FALSE);
515 }
516 }
519 function sieve_listscripts()
520 {
521 fputs($this->fp, "LISTSCRIPTS\r\n");
522 sieve::get_response(); //should always return true, even if there are no scripts...
523 if(isset($this->found_script) and $this->found_script){
524 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__, implode($this->response,", "),
525 "<b>SIEVE: List scripts was successful!</b>");
526 return true;
527 }else{
528 $this->error=EC_NOSCRIPTS; //sieve::getresponse has no way of telling wether a script was found...
529 $this->error_raw="No scripts found for this account.";
530 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__, $this->error_raw,
531 "<b>SIEVE: List scripts returned no scripts</b>");
532 return false;
533 }
534 }
536 function sieve_alive()
537 {
538 if(!isset($this->fp) or $this->fp==0){
539 $this->error = EC_NOT_LOGGED_IN;
540 return false;
541 }
542 elseif(feof($this->fp)){
543 $this->error = EC_NOT_LOGGED_IN;
544 return false;
545 }
546 else
547 return true;
548 }
551 function authenticate()
552 {
554 /* Check if a TLS bases connection is required
555 */
556 if($this->options == "tls"){
558 /* Check if PHP supports TLS encryption for sockets
559 */
560 if(function_exists("stream_socket_enable_crypto")){
562 /* Initiate TLS and get response
563 */
564 if (@ fputs ($this->fp, "STARTTLS\r\n") === false) {
565 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,"couldn't send data to socket.",
566 "<b>SIEVE: TLS couldn't be initiated. </b>");
567 $this->error_raw = "fputs() returned 'false'"; return (false);
568 }
569 @ $linha = fgets ($this->fp, 1024);
570 if ($linha === false) {
571 $this->error_raw = "fgets() returned 'false'";
572 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,"couldn't read data from socket.",
573 "<b>SIEVE: TLS couldn't be initiated. </b>");
574 return (false);
575 }
576 if (! preg_match ("/\\s*OK\\s/i", $linha)) {
577 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,"server returned '".trim ($linha)."' instead of 'OK'",
578 "<b>SIEVE: TLS couldn't be initiated. </b>");
579 $this->error_raw = "expected an 'OK', but server replied: '" . trim ($linha) . "'";
580 return (false);
581 }
582 if (! @ stream_socket_enable_crypto ($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
583 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,
584 "The socket doesn't seem to support STREAM_CRYPTO_METHOD_TLS_CLIENT",
585 "<b>SIEVE: TLS couldn't be initiated. </b>");
586 $this->error_raw = "stream_socket_enable_crypto() returned 'false'";
587 return (false);
588 }
589 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,"",
590 "<b>SIEVE: TLS successfully initiated. </b>");
591 }
592 }
594 switch ($this->auth_in_use) {
596 case "PLAIN":
597 {
598 $auth=base64_encode("$this->user\0$this->auth\0$this->pass");
600 $this->len=strlen($auth);
601 fputs($this->fp, "AUTHENTICATE \"PLAIN\" {".$this->len."+}\r\n");
602 fputs($this->fp, "$auth\r\n");
604 $this->line=fgets($this->fp,1024);
605 while(sieve::status($this->line) == F_DATA)
606 $this->line=fgets($this->fp,1024);
608 if(sieve::status($this->line) == F_NO){
609 $this->error_raw = $this->line;
610 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,trim($this->error_raw),
611 "<b>SIEVE: Authentication for '".$this->user."-".$this->auth_in_use."' failed.</b>");
613 return false;
614 }
616 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,"",
617 "<b>SIEVE: Authentication for '".$this->user."-".$this->auth_in_use."' was successful.</b>");
618 $this->loggedin=true;
619 return true;
620 }break;
622 default:
623 {
624 @DEBUG (DEBUG_MAIL, __LINE__, __FUNCTION__, __FILE__,"Unsupported authentication method '".$this->auth_in_use."'!",
625 "<b>SIEVE: Authentication for '".$this->user."' with type '".$this->auth_in_use."' failed.</b>");
627 $this->error_raw = "Unsupported authentication method '".$this->auth_in_use."'!";
628 return false;
629 } break;
631 }//end switch
634 }//end authenticate()
637 }
640 // vim:tabstop=2:expandtab:shiftwidth=2:filetype=php:syntax:ruler:
641 ?>