6a17c81b0b67d847812861a483b552421853e0aa
[tine20] / tine20 / Zend / Mail / Protocol / Sieve.php
1 <?php
2 /**
3  * Zend Framework
4  *
5  * LICENSE
6  *
7  * This source file is subject to the new BSD license that is bundled
8  * with this package in the file LICENSE.txt.
9  * It is also available through the world-wide-web at this URL:
10  * http://framework.zend.com/license/new-bsd
11  * If you did not receive a copy of the license and are unable to
12  * obtain it through the world-wide-web, please send an email
13  * to license@zend.com so we can send you a copy immediately.
14  * 
15  * @category   Zend
16  * @package    Zend_Mail
17  * @subpackage Protocol
18  * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
19  * @license    http://framework.zend.com/license/new-bsd     New BSD License
20  * @version    $Id$
21  */
22
23
24 /**
25  * see http://tools.ietf.org/html/draft-ietf-sieve-managesieve-09.txt
26  * 
27  * @category   Zend
28  * @package    Zend_Mail
29  * @subpackage Protocol
30  * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
31  * @license    http://framework.zend.com/license/new-bsd     New BSD License
32  */
33 class Zend_Mail_Protocol_Sieve
34 {
35     /**
36      * Default timeout in seconds for initiating session
37      */
38     const TIMEOUT_CONNECTION = 30;
39     
40     /**
41      * socket to Sieve
42      * @var null|resource
43      */
44     protected $_socket;
45     
46     /**
47      * the welcome array when connecting
48      * 
49      * @var array
50      */
51     protected $_welcome = array();
52     
53     /**
54      * sieve implementation
55      * 
56      * @var string
57      */
58     protected $_implementation = '';
59
60     /**
61      * Public constructor
62      *
63      * @param  string      $host  hostname of IP address of Sieve server, if given connect() is called
64      * @param  int|null    $port  port of Sieve server, null for default (2000)
65      * @param  bool|string $ssl   use 'TLS' or false
66      * @throws Zend_Mail_Protocol_Exception
67      */
68     public function __construct($host = '', $port = null, $ssl = false)
69     {
70         if ($host) {
71             $this->connect($host, $port, $ssl);
72         }
73     }
74
75     /**
76      * Public destructor
77      */
78     public function __destruct()
79     {
80         $this->logout();
81     }
82
83     /**
84      * Open connection to Sieve server
85      *
86      * @param  string      $host  hostname of IP address of Sieve server
87      * @param  int|null    $port  of Sieve server, default is 2000
88      * @param  string|bool $ssl   use 'TLS' or false
89      * @return string welcome message
90      * @throws Zend_Mail_Protocol_Exception
91      */
92     public function connect($host, $port = null, $ssl = false)
93     {
94         if ($port === null) {
95             $port = 2000;
96         }
97
98         $errno  =  0;
99         $errstr = '';
100         $this->_socket = @fsockopen($host, $port, $errno, $errstr, self::TIMEOUT_CONNECTION);
101         if (!$this->_socket) {
102             /**
103              * @see Zend_Mail_Protocol_Exception
104              */
105             require_once 'Zend/Mail/Protocol/Exception.php';
106             throw new Zend_Mail_Protocol_Exception('cannot connect to host : ' . $errno . ' : ' . $errstr);
107         }
108
109         $this->_welcome = $this->readResponse();
110         $this->_parseWelcomeArray();
111
112         if ($ssl === 'TLS') {
113             $result = $this->requestAndResponse('STARTTLS');
114             $result = $result && stream_socket_enable_crypto($this->_socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
115             if (!$result) {
116                 /**
117                  * @see Zend_Mail_Protocol_Exception
118                  */
119                 require_once 'Zend/Mail/Protocol/Exception.php';
120                 throw new Zend_Mail_Protocol_Exception('cannot enable TLS');
121             }
122         }
123         
124         return $this->_welcome;
125     }
126     
127     /**
128      * parses the welcome array and sets implementation
129      */
130     protected function _parseWelcomeArray()
131     {
132         foreach ($this->_welcome as $value) {
133             if ($value[0] == 'IMPLEMENTATION') {
134                 $this->_implementation = $value[1];
135             }
136         }
137     }
138     
139     /**
140      * returns implementation string
141      * 
142      * @return string
143      */
144     public function getImplementation()
145     {
146         return $this->_implementation;
147     }
148
149     /**
150      * send a request
151      *
152      * @param  string $command your request command
153      * @param  array  $tokens  additional parameters to command, use escapeString() to prepare
154      * @return null
155      * @throws Zend_Mail_Protocol_Exception
156      */
157     public function sendRequest($command, $tokens = array())
158     {
159         $line = $command;
160
161         foreach ($tokens as $token) {
162             if (is_array($token)) {
163                 if (@fputs($this->_socket, $line . ' ' . $token[0] . "\r\n") === false) {
164                     /**
165                      * @see Zend_Mail_Protocol_Exception
166                      */
167                     require_once 'Zend/Mail/Protocol/Exception.php';
168                     throw new Zend_Mail_Protocol_Exception('cannot write - connection closed?');
169                 }
170                 $line = $token[1];
171             } else {
172                 $line .= ' ' . $token;
173             }
174         }
175         
176         if (@fputs($this->_socket, $line . "\r\n") === false) {
177             /**
178              * @see Zend_Mail_Protocol_Exception
179              */
180             require_once 'Zend/Mail/Protocol/Exception.php';
181             throw new Zend_Mail_Protocol_Exception('cannot write - connection closed?');
182         }
183     }
184     
185     /**
186      * get the next line from socket with error checking, but nothing else
187      *
188      * @return string next line
189      * @throws Zend_Mail_Protocol_Exception
190      */
191     protected function _nextLine()
192     {
193         $line = @fgets($this->_socket);
194         #echo "READ: $line";
195         if ($line === false) {
196             /**
197              * @see Zend_Mail_Protocol_Exception
198              */
199             require_once 'Zend/Mail/Protocol/Exception.php';
200             throw new Zend_Mail_Protocol_Exception('cannot read - connection closed?');
201         }
202
203         return $line;
204     }
205     
206     /**
207      * split a given line in tokens. a token is literal of any form or a list
208      *
209      * @param  string $line line to decode
210      * @return array tokens, literals are returned as string, lists as array
211      * @throws Zend_Mail_Protocol_Exception
212      */
213     protected function _decodeLine($line)
214     {
215         $tokens = array();
216         $stack = array();
217
218         /*
219             We start to decode the response here. The unterstood tokens are:
220                 literal
221                 "literal" or also "lit\\er\"al"
222                 {bytes}<NL>literal
223                 (literals*)
224             All tokens are returned in an array. Literals in braces (the last unterstood
225             token in the list) are returned as an array of tokens. I.e. the following response:
226                 "foo" baz {3}<NL>bar ("f\\\"oo" bar)
227             would be returned as:
228                 array('foo', 'baz', 'bar', array('f\\\"oo', 'bar'));
229                 
230             // TODO: add handling of '[' and ']' to parser for easier handling of response text 
231         */
232         //  replace any trailling <NL> including spaces with a single space
233         $line = rtrim($line) . ' ';
234         while (($pos = strpos($line, ' ')) !== false) {
235             $token = substr($line, 0, $pos);
236             while ($token[0] == '(') {
237                 array_push($stack, $tokens);
238                 $tokens = array();
239                 $token = substr($token, 1);
240             }
241             if ($token[0] == '"') {
242                 if (preg_match('%^"((.|\\\\|\\")*?)" *%', $line, $matches)) {
243                     $tokens[] = $matches[1];
244                     $line = substr($line, strlen($matches[0]));
245                     continue;
246                 }
247             }
248             if ($token[0] == '{') {
249                 $endPos = strpos($token, '}');
250                 $chars = substr($token, 1, $endPos - 1);
251                 // a literal can be {1234+} and {1234}
252                 // see http://tools.ietf.org/html/rfc2244#section-2.6.3
253                 $chars = rtrim($chars, '+');
254                 if (is_numeric($chars)) {
255                     $token = '';
256                     while (strlen($token) < $chars) {
257                         $token .= $this->_nextLine();
258                     }
259                     $line = '';
260                     if (strlen($token) > $chars) {
261                         $line = substr($token, $chars);
262                         $token = substr($token, 0, $chars);
263                     } else {
264                         $line .= $this->_nextLine();
265                     }
266                     $tokens[] = $token;
267                     $line = trim($line);
268                     if(!empty($line)) {
269                         $line = $line . ' ';
270                     }
271                     continue;
272                 }
273             }
274             if ($stack && $token[strlen($token) - 1] == ')') {
275                 // closing braces are not seperated by spaces, so we need to count them
276                 $braces = strlen($token);
277                 $token = rtrim($token, ')');
278                 // only count braces if more than one
279                 $braces -= strlen($token) + 1;
280                 // only add if token had more than just closing braces
281                 if ($token) {
282                     $tokens[] = $token;
283                 }
284                 $token = $tokens;
285                 $tokens = array_pop($stack);
286                 // special handline if more than one closing brace
287                 while ($braces-- > 0) {
288                     $tokens[] = $token;
289                     $token = $tokens;
290                     $tokens = array_pop($stack);
291                 }
292             }
293             $tokens[] = $token;
294             $line = substr($line, $pos + 1);
295         }
296
297         // maybe the server forgot to send some closing braces
298         while ($stack) {
299             $child = $tokens;
300             $tokens = array_pop($stack);
301             $tokens[] = $child;
302         }
303         
304         return $tokens;
305     }
306     
307     /**
308      * read a response "line" (could also be more than one real line if response has {..}<NL>)
309      * and do a simple decode
310      *
311      * @param  array|string  $tokens    decoded tokens are returned by reference, if $dontParse
312      *                                  is true the unparsed line is returned here
313      * @param  bool          $dontParse if true only the unparsed line is returned $tokens
314      */
315     public function readLine(&$tokens = array(), $dontParse = false)
316     {
317         $line = $this->_nextLine();
318         if (!$dontParse) {
319             $tokens = $this->_decodeLine($line);
320         } else {
321             $tokens = $line;
322         }
323     }
324     
325     /**
326      * read a response
327      *
328      * @param  boolean $dontParse not used currently
329      * @return string response
330      * @throws Zend_Mail_Protocol_Exception
331      */
332     public function readResponse($dontParse = false)
333     {
334         $lines = array();
335         
336         while (!feof($this->_socket)) {
337             $this->readLine($tokens, $dontParse);
338             
339             if($tokens[0] == 'OK') {
340                 break;
341             } elseif($tokens[0] == 'NO') {
342                 $message = (is_string($tokens[1])) 
343                     ? $tokens[1] 
344                     : ((is_string($tokens[2])) ? $tokens[2] : 'Could not read response from server: ' . print_r($tokens, TRUE));
345                 throw new Zend_Mail_Protocol_Exception($message);
346             }
347             
348             $lines[] = $tokens;
349         }
350         
351         #var_dump($lines);
352         
353         return $lines;
354     }
355     
356     /**
357      * send a request and get response at once
358      *
359      * @param  string $command   command as in sendRequest()
360      * @param  array  $tokens    parameters as in sendRequest()
361      * @param  bool   $dontParse if true unparsed lines are returned instead of tokens
362      * @return mixed response as in readResponse()
363      * @throws Zend_Mail_Protocol_Exception
364      */
365     public function requestAndResponse($command, $tokens = array(), $dontParse = false)
366     {
367         $this->sendRequest($command, $tokens);
368         $response = $this->readResponse($dontParse);
369
370         return $response;
371     }
372
373     /**
374      * End communication with Sieve server (also closes socket)
375      *
376      * @return null
377      */
378     public function logout()
379     {
380         $result = false;
381         if ($this->_socket) {
382             try {
383                 $result = $this->requestAndResponse('LOGOUT');
384             } catch (Zend_Mail_Protocol_Exception $e) {
385                 // ignoring exception
386             }
387             fclose($this->_socket);
388             $this->_socket = null;
389         }
390         return $result;
391         
392     }
393
394     /**
395      * The HAVESPACE command is used to query the server for available
396      * space.
397      *
398      * @todo test with a Sieve server supporting this command
399      *
400      * @param string    $scriptName     the script name
401      * @param int       $size           the required size
402      */
403     public function haveSpace($scriptName, $size)
404     {
405         $result = $this->requestAndResponse('HAVESPACE', $this->escapeString($scriptName, $size));
406     }
407     
408     /**
409      * Get supported features from Sieve server
410      *
411      * @return array list of capabilities
412      */
413     public function capability()
414     {
415         $lines = $this->requestAndResponse('CAPABILITY');
416         
417         $capabilities = array();
418         
419         foreach ($lines as $line) {
420             if (count($line) === 1) {
421                 $name = $line[0];
422                 $value = '';
423             } else {
424                 list($name, $value) = $line;
425             }
426             
427             $name = strtoupper($name);
428             
429             switch ($name) {
430                 case 'SASL':
431                 case 'SIEVE':
432                 case 'NOTIFY':
433                     $capabilities[$name] = explode(' ', rtrim($value));
434                     break;
435                     
436                 default:
437                     $capabilities[$name] = $value;
438                     break;
439             }
440         }
441
442         return $capabilities;
443     }
444     
445     /**
446      * List scripts the user has on Sieve server
447      *
448      * @return array list of scripts
449      */
450     public function listScripts()
451     {
452         $lines = $this->requestAndResponse('LISTSCRIPTS');
453         
454         $scripts = array();
455         
456         foreach($lines as $scriptData) {
457             #var_dump($scriptData);
458             $scripts[$scriptData[0]] = array(
459                 'name'      => $scriptData[0],
460                 'active'    => isset($scriptData[1]) ? true : false
461             );
462         }
463         
464         return $scripts;
465     }
466     
467     /**
468      * Return the server to the non-authenticated state
469      *
470      * @todo test with a Sieve server supporting this command
471      */
472     public function unAuthenticate()
473     {
474         $this->requestAndResponse('UNAUTHENTICATE');
475     }
476     
477     /**
478      * send noop
479      *
480      * @todo test with a Sieve server supporting this command
481      *  
482      * @param string   $content    a string to echo from Sieve server
483      */
484     public function noop($content)
485     {
486         $lines = $this->requestAndResponse('NOOP', array($this->escapeString($content)));
487     }
488
489     /**
490      * Submit a Sieve script to the Sieve server
491      * 
492      * @param string    $name       the name of the script
493      * @param string    $content    the content of the script
494      */
495     public function putScript($name, $content)
496     {
497         $this->requestAndResponse('PUTSCRIPT', $this->escapeString($name, $content));
498     }
499
500     /**
501      * Verify Sieve script validity without storing the script on the server
502      * 
503      * @todo test with a Sieve server supporting this command
504      * 
505      * @param string    $content    the script to validate
506      */
507     public function checkScript($content)
508     {
509         $this->requestAndResponse('CHECKSCRIPT', $this->escapeString($content));
510     }
511     
512     /**
513      * Rename script on Sieve server
514      * 
515      * @todo test with a Sieve server supporting this command
516      * 
517      * @param string    $oldName    the old name of the script
518      * @param string    $newName    the new name of the script
519      */
520     public function renameScript($oldName, $newName)
521     {
522         $this->requestAndResponse('RENAMESCRIPT', $this->escapeString($oldName, $newName));
523     }
524     
525     /**
526      * Delete script on Sieve server
527      * @param string    $name   the name of the script to delete
528      */
529     public function deleteScript($name)
530     {
531         $this->requestAndResponse('DELETESCRIPT', array($this->escapeString($name)));
532     }
533     
534     /**
535      * Set active script
536      * 
537      * @param string    $name   the name of the script to activate (set to "" to disable any active script)
538      */
539     public function setActive($name)
540     {
541         $this->requestAndResponse('SETACTIVE', array($this->escapeString($name)));
542     }
543     
544     /**
545      * escape one or more literals i.e. for sendRequest
546      *
547      * @param  string|array $string the literal/-s
548      * @return string|array escape literals, literals with newline ar returned
549      *                      as array('{size}', 'string');
550      */
551     public function escapeString($string)
552     {
553         if (func_num_args() < 2) {
554             if (strpos($string, "\n") !== false) {
555                 return array('{' . strlen($string) . '+}', $string);
556             } else {
557                 return '"' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $string) . '"';
558             }
559         }
560         $result = array();
561         foreach (func_get_args() as $string) {
562             $result[] = $this->escapeString($string);
563         }
564         return $result;
565     }
566     
567     /**
568      * Retrieve script from Sieve server
569      *
570      * @param string    $name   the name of the script
571      * @return string the script
572      */
573     public function getScript($name)
574     {
575         $lines = $this->requestAndResponse('GETSCRIPT', array($this->escapeString($name)));
576         
577         $script = implode($lines[0]);
578         
579         return $script;
580     }
581     
582     /**
583      * Login to Sieve server
584      *
585      * @todo currently only plain auth is implemented
586      *
587      * @param  string $username  username
588      * @param  string $password  password
589      * @return void
590      * @throws Zend_Mail_Protocol_Exception
591      */
592     public function authenticate($username, $password)
593     {
594         $token = base64_encode(chr(0) . $username . chr(0) . $password);
595         $result = $this->requestAndResponse('AUTHENTICATE',  $this->escapeString('PLAIN', $token));
596     }
597 }