1<?php
2/**
3 * EGroupware Api: IMAP support using Horde_Imap_Client
4 *
5 * @link http://www.stylite.de
6 * @package api
7 * @subpackage mail
8 * @author Ralf Becker <rb@stylite.de>
9 * @author Stylite AG <info@stylite.de>
10 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
11 * @version $Id$
12 */
13
14namespace EGroupware\Api\Mail;
15
16use EGroupware\Api;
17
18use EGroupware\SwoolePush\Tokens;
19use Horde_Imap_Client;
20use Horde_Imap_Client_Socket;
21use Horde_Imap_Client_Cache_Backend_Cache;
22use Horde_Imap_Client_Mailbox_List;
23
24/**
25 * This class holds all information about the imap connection.
26 * This is the base class for all other imap classes.
27 *
28 * Also proxies Sieve calls to Mail\Sieve (eg. it behaves like the former felamimail bosieve),
29 * to allow IMAP plugins to also manage Sieve connection.
30 *
31 * @property-read integer $ImapServerId acc_id of mail account (alias for acc_id)
32 * @property-read boolean $enableSieve sieve enabled (alias for acc_sieve_enabled)
33 * @property-read int $acc_id id
34 * @property-read string $acc_name description / display name
35 * @property-read string $acc_imap_host imap hostname
36 * @property-read int $acc_imap_ssl 0=none, 1=starttls, 2=tls, 3=ssl, &8=validate certificate
37 * @property-read int $acc_imap_port imap port, default 143 or for ssl 993
38 * @property-read string $acc_imap_username
39 * @property-read string $acc_imap_password
40 * @property-read boolean $acc_sieve_enabled sieve enabled
41 * @property-read string $acc_sieve_host possible sieve hostname, default imap_host
42 * @property-read int $acc_sieve_ssl 0=none, 1=starttls, 2=tls, 3=ssl, &8=validate certificate
43 * @property-read int $acc_sieve_port sieve port, default 4190, old non-ssl port 2000 or ssl 5190
44 * @property-read string $acc_folder_sent sent folder
45 * @property-read string $acc_folder_trash trash folder
46 * @property-read string $acc_folder_draft draft folder
47 * @property-read string $acc_folder_template template folder
48 * @property-read string $acc_folder_junk junk/spam folder
49 * @property-read string $acc_imap_type imap class to use, default Imap
50 * @property-read string $acc_imap_logintype how to construct login-name standard, vmailmgr, admin, uidNumber
51 * @property-read string $acc_domain domain name
52 * @property-read boolean $acc_imap_administration enable administration
53 * @property-read string $acc_imap_admin_username
54 * @property-read string $acc_imap_admin_password
55 * @property-read boolean $acc_further_identities are non-admin users allowed to create further identities
56 * @property-read boolean $acc_user_editable are non-admin users allowed to edit this account, if it is for them
57 * @property-read array $params parameters passed to constructor (all above as array)
58 * @property-read boolean|int|string $isAdminConnection admin connection if true or account_id or imap username
59 */
60class Imap extends Horde_Imap_Client_Socket implements Imap\PushIface
61{
62	/**
63	 * Default parameters for Horde_Imap_Client constructor
64	 *
65	 * @var array
66	 */
67	static public $default_params = array(
68		//'debug' => '/tmp/imap.log', // uncomment to log communitcation with IMAP server
69		//'debug_literal' => true,    // uncomment to log mail contents returned by IMAP server
70		'cache' => true,              // default caching via Cache / Api\Cache
71	);
72
73	/**
74	 * Label shown in EMailAdmin
75	 */
76	const DESCRIPTION = 'standard IMAP server';
77
78	/**
79	 * Capabilities of this class (pipe-separated): default, sieve, admin, logintypeemail
80	 */
81	const CAPABILITIES = 'default|sieve';
82
83	/**
84	 * does the server with the serverID support keywords
85	 * this information is filled/provided by examineMailbox
86	 *
87	 * init_static references this to a session-variable, so it persists
88	 *
89	 * @var array of boolean for each known serverID
90	 */
91	static $supports_keywords;
92
93	/**
94	 * is the mbstring extension available
95	 *
96	 * @var boolean
97	 */
98	protected $mbAvailable;
99
100	/**
101	 * Login type: 'uid', 'vmailmgr', 'uidNumber', 'email'
102	 *
103	 * @var string
104	 */
105	protected $imapLoginType;
106
107	/**
108	 * a debug switch
109	 */
110	public $debug = false;
111
112	/**
113	 * Sieve available
114	 *
115	 * @var boolean
116	 */
117	protected $enableSieve = false;
118
119	/**
120	 * Connection is an admin connection
121	 *
122	 * @var boolean|int|string $isAdminConnection admin connection if true or account_id or imap username
123	 */
124	protected $isAdminConnection = false;
125
126	/**
127	 * Domain name
128	 *
129	 * @var string
130	 */
131	protected $domainName;
132
133	/**
134	 * Parameters passed to constructor from Account
135	 *
136	 * @var array
137	 */
138	protected $params = array();
139
140	/**
141	 * Construtor
142	 *
143	 * @param array
144	 * @param bool|int|string $_adminConnection create admin connection if true or account_id or imap username
145	 * @param int $_timeout =null timeout in secs, if none given fmail pref or default of 20 is used
146	 * @return void
147	 */
148	function __construct(array $params, $_adminConnection=false, $_timeout=null)
149	{
150		if (function_exists('mb_convert_encoding'))
151		{
152			$this->mbAvailable = true;
153		}
154		$this->params = $params;
155		$this->isAdminConnection = $_adminConnection;
156		$this->enableSieve = (boolean)$this->params['acc_sieve_enabled'];
157		$this->loginType = $this->params['acc_imap_logintype'];
158		$this->domainName = $this->params['acc_domain'];
159
160		if (is_null($_timeout)) $_timeout = $this->params['acc_imap_timeout']?$this->params['acc_imap_timeout']:self::getTimeOut ();
161
162		// Horde use locale for translation of error messages
163		// need to set LC_CTYPE for charachter classification (eg. Umlauts)
164		Api\Preferences::setlocale(LC_CTYPE);
165		Api\Preferences::setlocale(LC_MESSAGES);
166
167		// some plugins need extra measures to switch to an admin connection (eg. Dovecot constructs a special admin user name)
168		$username = $_adminConnection;
169		if (!is_bool($username) && is_numeric($username))
170		{
171			$username = $this->getMailBoxUserName($username);
172		}
173		if ($_adminConnection) $this->adminConnection($username);
174		$parent_params = array(
175			'username' => $this->params[$_adminConnection ? 'acc_imap_admin_username' : 'acc_imap_username'],
176			'password' => $this->params[$_adminConnection ? 'acc_imap_admin_password' : 'acc_imap_password'],
177			'hostspec' => $this->params['acc_imap_host'],
178			'port' => $this->params['acc_imap_port'],
179			'secure' => Account::ssl2secure($this->params['acc_imap_ssl']),
180			'timeout' => $_timeout,
181		)+self::$default_params;
182
183		if ($parent_params['cache'] === true)
184		{
185			$parent_params['cache'] = array(
186				'backend' => new Horde_Imap_Client_Cache_Backend_Cache(array(
187					'cacheob' => new Cache(),
188				)),
189			);
190		}
191		// uncomment to enable imap log for a single user
192		//if ($GLOBALS['egw_info']['user']['account_lid'] === 'username') $parent_params['debug'] = '/var/lib/egroupware/'.$_SERVER['HTTP_HOST'].'/imap.log';
193
194		// switch to allow to disable some capabilites known to be troublesome
195		switch (strtolower(trim($this->params['acc_imap_host'])))
196		{
197			case 'imap.yandex.ru':
198			case 'imap.yandex.com':
199				// imap.yandex.com - reports BINARY (server side decoding) but does not decode but
200				// returns undecoded bodyParts AND reports an encoding for the returned part.
201				// expected behavior would be: if server side decoding succeeds , horde should
202				// either report 7bit or 8bit when calling getBodyPartDecode. if it fails or BINARY
203				// is not supported NULL is expected on getBodyPartDecode
204				// yandex.com does not succeed in decoding but getBodyPartDecode is reported as 7bit/8bit
205				// as we have no way to tell this apart we ignore BINARY this affects
206				// Horde_Imap_Client_Fetch_Query::bodyPart for its fetch parameter decode=true is ignored
207				// (other functionality depending on BINARY is, of cause, affected too)
208				$parent_params['capability_ignore']= array_merge((array)$parent_params['capability_ignore'],array('BINARY'));
209				break;
210		}
211		parent::__construct($parent_params);
212	}
213
214	/**
215	 * Ensure we use an admin connection
216	 *
217	 * Plugins can overwrite it to eg. construct a special admin user name
218	 *
219	 * @param string $_username =true create an admin connection for given user or $this->acc_imap_username
220	 */
221	function adminConnection($_username=true)
222	{
223		if ($this->isAdminConnection !== $_username)
224		{
225			$this->logout();
226
227			$this->__construct($this->params, $_username);
228			$this->acc_imap_username = $_username;
229		}
230	}
231
232	/**
233	 * Check admin credientials and connection (if supported)
234	 *
235	 * @param string $_username =null create an admin connection for given user or $this->acc_imap_username
236	 * @throws Horde_IMAP_Client_Exception
237	 */
238	public function checkAdminConnection($_username=true)
239	{
240		if ($this->acc_imap_administration)
241		{
242			$this->adminConnection($_username);
243			$this->login();
244		}
245	}
246
247	/**
248	 * Methods to run on successful login
249	 *
250	 * @var array
251	 */
252	protected $run_on_login=array();
253
254	/**
255	 * Run given function on successful login
256	 *
257	 * @param callable $func
258	 * @param array $params =array()
259	 */
260	public function runOnLogin($func, array $params=array())
261	{
262		$this->run_on_login[] = array($func, $params);
263	}
264
265	/**
266	 * Login to the IMAP server.
267	 *
268	 * @throws Horde_Imap_Client_Exception
269	 */
270	public function login()
271	{
272		parent::login();
273
274		foreach($this->run_on_login as $key => $data)
275		{
276			call_user_func_array($data[0], $data[1]);
277
278			unset($this->run_on_login[$key]);
279		}
280	}
281
282	/**
283	 * Allow read access to former public attributes
284	 *
285	 * @param type $name
286	 * @return mixed null for an unknown attribute
287	 */
288	public function __get($name)
289	{
290		switch($name)
291		{
292			case 'acc_imap_administration':
293				return !empty($this->params['acc_imap_admin_username']);
294
295			case 'acc_id':	// to not get an exception, if account is not yet stored, just return null
296			case 'ImapServerId':
297				return $this->params['acc_id'];
298
299			case 'enableSieve':
300				return (boolean)$this->params['acc_sieve_enabled'];
301
302			default:
303				// allow readonly access to all class attributes
304				if (isset($this->$name))
305				{
306					return $this->$name;
307				}
308				if (array_key_exists($name,$this->params))
309				{
310					return $this->params[$name];
311				}
312				if ($this->getParam($name))
313				{
314					return $this->getParam($name);
315				}
316				// calling Horde_Imap_Client's __get() method available since 2.24.1
317				return is_callable('parent::__get') ? parent::__get($name) : null;
318		}
319	}
320
321	/**
322	 * opens a connection to a imap server
323	 *
324	 * @param bool $_adminConnection create admin connection if true
325	 * @param int $_timeout =null timeout in secs, if none given fmail pref or default of 20 is used
326	 * @deprecated allready called by constructor automatic, parameters must be passed to constructor!
327	 * @throws Api\Exception\WrongParameter
328	 */
329	function openConnection($_adminConnection=false, $_timeout=null)
330	{
331		unset($_timeout);	// not used
332		if ($_adminConnection !== $this->params['adminConnection'])
333		{
334			throw new Api\Exception\WrongParameter('need to set parameters on calling Account->imapServer()!');
335		}
336	}
337
338	/**
339	 * getTimeOut
340	 *
341	 * @param string _use decide if the use is for IMAP or SIEVE, by now only the default differs
342	 * @return int - timeout (either set or default 20/10)
343	 */
344	static function getTimeOut($_use='IMAP')
345	{
346		$timeout = $GLOBALS['egw_info']['user']['preferences']['mail']['connectionTimeout'];
347		if (empty($timeout) || !($timeout > 0)) $timeout = $_use == 'SIEVE' ? 10 : 20; // this is the default value
348		return $timeout;
349	}
350
351	/**
352	 * Return description for EMailAdmin
353	 *
354	 * @return string
355	 */
356	public static function description()
357	{
358		return static::DESCRIPTION;
359	}
360
361	/**
362	 * adds a account on the imap server
363	 *
364	 * @param array $_hookValues
365	 * @return bool true on success, false on failure
366	 */
367	function addAccount($_hookValues)
368	{
369		unset($_hookValues);	// not used
370		return true;
371	}
372
373	/**
374	 * updates a account on the imap server
375	 *
376	 * @param array $_hookValues
377	 * @return bool true on success, false on failure
378	 */
379	function updateAccount($_hookValues)
380	{
381		unset($_hookValues);	// not used
382		return true;
383	}
384
385	/**
386	 * deletes a account on the imap server
387	 *
388	 * @param array $_hookValues
389	 * @return bool true on success, false on failure
390	 */
391	function deleteAccount($_hookValues)
392	{
393		unset($_hookValues);	// not used
394		return true;
395	}
396
397	function disconnect()
398	{
399
400	}
401
402	/**
403	 * converts a foldername from current system charset to UTF7
404	 *
405	 * @param string $_folderName
406	 * @return string the encoded foldername
407	 */
408	function encodeFolderName($_folderName)
409	{
410		if($this->mbAvailable) {
411			return mb_convert_encoding($_folderName, "UTF7-IMAP", Api\Translation::charset());
412		}
413
414		// if not
415		// we can encode only from ISO 8859-1
416		return imap_utf7_encode($_folderName);
417	}
418
419	/**
420	 * getMailbox
421	 *
422	 * @param string $mailbox
423	 * @return mixed mailbox object/string (string if not found by listMailboxes but existing)
424	 */
425	function getMailbox($mailbox)
426	{
427		$mailboxes = $this->listMailboxes($mailbox,Horde_Imap_Client::MBOX_ALL);
428		if (empty($mailboxes)) $mailboxes = $this->listMailboxes($mailbox,Horde_Imap_Client::MBOX_UNSUBSCRIBED);
429		//error_log(__METHOD__.__LINE__.'->'.$mailbox.'/'.array2string($mailboxes));
430		$mboxes = new Horde_Imap_Client_Mailbox_List($mailboxes);
431		//_debug_array($mboxes->count());
432		foreach ($mboxes->getIterator() as $k =>$box)
433		{
434			//error_log(__METHOD__.__LINE__.'->'.$k);
435			if ($k!='user' && $k != '' && $k==$mailbox) return $box['mailbox']; //_debug_array(array($k => $client->status($k)));
436		}
437		return ($this->mailboxExist($mailbox)?$mailbox:false);
438	}
439
440	/**
441	 * mailboxExists
442	 *
443	 * @param string $mailbox
444	 * @return boolean
445	 */
446	function mailboxExist($mailbox)
447	{
448		try
449		{
450			//error_log(__METHOD__.__LINE__.':'.$mailbox);
451			$currentMailbox = $this->currentMailbox();
452		}
453		catch(\Exception $e)
454		{
455			//error_log(__METHOD__.__LINE__.' failed detecting currentMailbox:'.$currentMailbox.':'.$e->getMessage());
456			$currentMailbox=null;
457			unset($e);
458		}
459		try
460		{
461			//error_log(__METHOD__.__LINE__.':'.$mailbox);
462			$this->openMailbox($mailbox);
463			$returnvalue=true;
464		}
465		catch(\Exception $e)
466		{
467			//error_log(__METHOD__.__LINE__.' failed opening:'.$mailbox.':'.$e->getMessage().' Called by:'.function_backtrace());
468			unset($e);
469			$returnvalue=false;
470		}
471		if (!empty($currentMailbox) && $currentMailbox['mailbox'] != $mailbox)
472		{
473			try
474			{
475				//error_log(__METHOD__.__LINE__.':'.$currentMailbox .'<->'.$mailbox);
476				$this->openMailbox($currentMailbox['mailbox']);
477			}
478			catch(\Exception $e)
479			{
480				//error_log(__METHOD__.__LINE__.' failed reopening:'.$currentMailbox.':'.$e->getMessage());
481				unset($e);
482			}
483		}
484		return $returnvalue;
485	}
486
487	/**
488	 * getSpecialUseFolders
489	 *
490	 * @return current mailbox, or if none check on INBOX, and return upon existance
491	 */
492	function getCurrentMailbox()
493	{
494		try
495		{
496			$mailbox = $this->currentMailbox();
497		}
498		catch(\Exception $e)
499		{
500			error_log(__METHOD__.' ('.__LINE__.') failed fetching currentMailbox:'.$e->getMessage());
501			//throw new egw_exception(__METHOD__.' ('.__LINE__.") failed to ".__METHOD__." :".$e->getMessage());
502			unset($e);
503		}
504		if (!empty($mailbox)) return $mailbox['mailbox'];
505		if (empty($mailbox) && $this->mailboxExist('INBOX')) return 'INBOX';
506		return null;
507	}
508
509	/**
510	 * getSpecialUseFolders
511	 *
512	 * @return array with special use folders
513	 */
514	function getSpecialUseFolders()
515	{
516		$mailboxes = $this->getMailboxes('',0,true);
517		$suF = array();
518		foreach ($mailboxes as $box)
519		{
520			if ($box['MAILBOX']!='user' && $box['MAILBOX'] != '')
521			{
522				//error_log(__METHOD__.__LINE__.$k.'->'.array2string($box));
523				if (isset($box['ATTRIBUTES'])&&!empty($box['ATTRIBUTES'])&&
524					stripos(strtolower(array2string($box['ATTRIBUTES'])),'\noselect')=== false&&
525					stripos(strtolower(array2string($box['ATTRIBUTES'])),'\nonexistent')=== false)
526				{
527					$suF[$box['MAILBOX']] = $box;
528				}
529			}
530		}
531		return $suF;
532	}
533
534	/**
535	 * getMailboxCounters
536	 *
537	 * @param array/string $mailbox
538	 * @return array with counters
539	 */
540	function getMailboxCounters($mailbox)
541	{
542		try
543		{
544			$status = $this->status($mailbox);
545			foreach ($status as $key => $v)
546			{
547				$_status[strtoupper($key)]=$v;
548			}
549			return $_status;
550		}
551		catch (\Exception $e)
552		{
553			unset($e);
554			return false;
555		}
556	}
557
558	/**
559	 * Attribute returned for Horde_Imap_Client::MBOX_ALL_SUBSCRIBED if mailbox is subscribed
560	 */
561	const SUBSCRIBED_ATTRIBUTE = '\\subscribed';
562
563	/**
564	 * getStatus
565	 *
566	 * @param string $mailbox
567	 * @param ignoreStatusCache bool ignore the cache used for counters
568	 * @return array with counters
569	 */
570	function getStatus($mailbox, $ignoreStatusCache=false)
571	{
572		$mailboxes = $this->listMailboxes($mailbox,Horde_Imap_Client::MBOX_ALL_SUBSCRIBED, array(
573				'attributes'=>true,
574				'children'=>true, //child info
575				'delimiter'=>true,
576				'special_use'=>true,
577			));
578
579		$flags = Horde_Imap_Client::STATUS_ALL;
580		if ($ignoreStatusCache) $flags |= Horde_Imap_Client::STATUS_FORCE_REFRESH;
581
582		$mboxes = new Horde_Imap_Client_Mailbox_List($mailboxes);
583		//error_log(__METHOD__.__LINE__.array2string($mboxes->count()));
584		foreach ($mboxes->getIterator() as $k =>$box)
585		{
586			if ($k!='user' && $k != '' && $k==$mailbox)
587			{
588				if (stripos(array2string($box['attributes']),'\noselect')=== false)
589				{
590					$status = $this->status($k, $flags);
591					foreach ($status as $key => $v)
592					{
593						$_status[strtoupper($key)]=$v;
594					}
595					$_status['HIERACHY_DELIMITER'] = $_status['delimiter'] = ($box['delimiter']?$box['delimiter']:$this->getDelimiter('personal'));
596					$_status['ATTRIBUTES'] = $box['attributes'];
597					$_status['SUBSCRIBED'] = in_array(self::SUBSCRIBED_ATTRIBUTE, $box['attributes']);
598					//error_log(__METHOD__.__LINE__.$k.'->'.array2string($_status));
599					return $_status;
600				}
601				else
602				{
603					return false;
604				}
605			}
606		}
607		return false;
608	}
609
610	/**
611	 * Returns an array containing the names of the selected mailboxes
612	 *
613	 * @param   string  $reference          base mailbox to start the search (default is current mailbox)
614	 * @param   string  $restriction_search false or 0 means return all mailboxes
615	 *                                      true or 1 return only the mailbox that contains that exact name
616	 *                                      2 return all mailboxes in that hierarchy level
617	 * @param   string  $returnAttributes   true means return an assoc array containing mailbox names and mailbox attributes
618	 *                                      false - the default - means return an array of mailboxes with only selected attributes like delimiter
619	 *
620	 * @return  mixed   array of mailboxes
621	 */
622	function getMailboxes($reference = ''  , $restriction_search = 0, $returnAttributes = false)
623	{
624		if ( is_bool($restriction_search) ){
625			$restriction_search = (int) $restriction_search;
626		}
627		$mailbox = '';
628		if ( is_int( $restriction_search ) ){
629			switch ( $restriction_search ) {
630			case 0:
631				$searchstring = $reference."*";
632				break;
633			case 1:
634				$mailbox = $searchstring = $reference;
635				//$reference = '%';
636				break;
637			case 2:
638				$searchstring = $reference."%";
639				break;
640			}
641		}else{
642			if ( is_string( $restriction_search ) ){
643				$mailbox = $searchstring = $restriction_search;
644			}
645		}
646		//error_log(__METHOD__.__LINE__.array2string($mailbox));
647		//if (is_array($mailbox))error_log(__METHOD__.__LINE__.function_backtrace());
648		$options = array(
649				'attributes'=>true,
650				'children'=>true, //child info
651				'delimiter'=>true,
652				'special_use'=>true,
653				'sort'=>true,
654			);
655		if ($returnAttributes==false)
656		{
657			unset($options['attributes']);
658			unset($options['children']);
659			unset($options['special_use']);
660		}
661		// use Horde_Imap_Client::MBOX_ALL_SUBSCRIBED to get all mailboxes in a single imap command
662		// unfortunatly this fails for some Cyrus servers ...
663		$need_cyrus_workaround = Api\Cache::getInstance(__CLASS__, 'cyrus-workaround-'.$this->acc_imap_host);
664		if (!$need_cyrus_workaround && ($mailboxes = $this->listMailboxes($searchstring,Horde_Imap_Client::MBOX_ALL_SUBSCRIBED, $options)))
665		{
666			//$mboxes = new Horde_Imap_Client_Mailbox_List($mailboxes);
667			//_debug_array($mboxes->count());
668			foreach ((array)$mailboxes as $k => $box)
669			{
670				//error_log(__METHOD__.__LINE__.' Box:'.$k.'->'.array2string($box));
671				$ret[$k] = [
672					'MAILBOX' => $k,
673					'ATTRIBUTES' => $box['attributes'],
674					'delimiter' => $box['delimiter'] ? $box['delimiter'] : $this->getDelimiter('personal'),
675					'SUBSCRIBED' => in_array(self::SUBSCRIBED_ATTRIBUTE, $box['attributes']),
676				];
677			}
678		}
679		else
680		{
681			// remember that server needs the workaround
682			if (!$need_cyrus_workaround) Api\Cache::setInstance(__CLASS__, 'cyrus-workaround-'.$this->acc_imap_host, true);
683
684			$mailboxes = $this->listMailboxes($searchstring, Horde_Imap_Client::MBOX_ALL, $options);
685			//$mboxes = new Horde_Imap_Client_Mailbox_List($mailboxes);
686			//_debug_array($mboxes->count());
687			foreach ((array)$mailboxes as $k => $box)
688			{
689				//error_log(__METHOD__.__LINE__.' Box:'.$k.'->'.array2string($box));
690				$ret[$k] = array('MAILBOX' => $k, 'ATTRIBUTES' => $box['attributes'], 'delimiter' => ($box['delimiter'] ? $box['delimiter'] : $this->getDelimiter('personal')), 'SUBSCRIBED' => true);
691			}
692			// for unknown reasons on ALL, UNSUBSCRIBED are not returned
693			//always fetch unsubscribed, think about only fetching it when $options['attributes'] is set
694			//but then allMailboxes are not all, ....
695			//if (!empty($mailbox) && !isset($ret[$mailbox]))
696			{
697				$unsub_mailboxes = $this->listMailboxes($searchstring, Horde_Imap_Client::MBOX_UNSUBSCRIBED, $options);
698				//$mboxes = new Horde_Imap_Client_Mailbox_List($mailboxes);
699				//_debug_array($mboxes->count());
700				//error_log(__METHOD__.__LINE__.' '.$mailbox.':'.count((array)$mailboxes).'->'.function_backtrace());
701				foreach ((array)$unsub_mailboxes as $k => $box)
702				{
703					//error_log(__METHOD__.__LINE__.' Box:'.$k.' already In?'.array_key_exists($k,$boxexists).'->'.array2string($box));
704					if (!array_key_exists($k, $ret))
705					{
706						$ret[$k] = array('MAILBOX' => $k, 'ATTRIBUTES' => $box['attributes'], 'delimiter' => ($box['delimiter'] ? $box['delimiter'] : $this->getDelimiter('personal')), 'SUBSCRIBED' => false);
707					}
708					else
709					{
710						$ret[$k]['SUBSCRIBED'] = false;
711					}
712				}
713			}
714		}
715		return $ret;
716	}
717
718	/**
719	 * Returns an array containing the names of the subscribed selected mailboxes
720	 *
721	 * @param   string  $reference          base mailbox to start the search
722	 * @param   string  $restriction_search false or 0 means return all mailboxes
723	 *                                      true or 1 return only the mailbox that contains that exact name
724	 *                                      2 return all mailboxes in that hierarchy level
725	 * @param   string  $returnAttributes   true means return an assoc array containing mailbox names and mailbox attributes
726	 *                                      false - the default - means return an array of mailboxes with only selected attributes like delimiter
727	 *
728	 * @return  mixed   array of mailboxes
729	 */
730	function listSubscribedMailboxes($reference = ''  , $restriction_search = 0, $returnAttributes = false)
731	{
732		if ( is_bool($restriction_search) ){
733			$restriction_search = (int) $restriction_search;
734		}
735		$mailbox = '';
736		if ( is_int( $restriction_search ) ){
737			switch ( $restriction_search ) {
738			case 0:
739				$searchstring = $reference."*";
740				break;
741			case 1:
742				$mailbox = $searchstring = $reference;
743				//$reference = '%';
744				break;
745			case 2:
746				$searchstring = $reference."%";
747				break;
748			}
749		}else{
750			if ( is_string( $restriction_search ) ){
751				$mailbox = $searchstring = $restriction_search;
752			}
753		}
754		//error_log(__METHOD__.__LINE__.$mailbox);
755		$options = array(
756				'attributes'=>true,
757				'children'=>true, //child info
758				'delimiter'=>true,
759				'special_use'=>true,
760				'sort'=>true,
761			);
762		if ($returnAttributes==false)
763		{
764			unset($options['attributes']);
765			unset($options['children']);
766			unset($options['special_use']);
767		}
768		$mailboxes = $this->listMailboxes($searchstring,Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS, $options);
769		//$mboxes = new Horde_Imap_Client_Mailbox_List($mailboxes);
770		//_debug_array($mboxes->count());
771		foreach ((array)$mailboxes as $k =>$box)
772		{
773			//error_log(__METHOD__.__LINE__.' Searched for:'.$mailbox.' got Box:'.$k.'->'.array2string($box).function_backtrace());
774			if ($returnAttributes==false)
775			{
776				$ret[]=$k;
777			}
778			else
779			{
780				$ret[$k]=array('MAILBOX'=>$k,'ATTRIBUTES'=>$box['attributes'],'delimiter'=>($box['delimiter']?$box['delimiter']:$this->getDelimiter('personal')),'SUBSCRIBED'=>true);
781			}
782		}
783		return $ret;
784	}
785
786	/**
787	 * Returns an array containing the names of the selected unsubscribed mailboxes
788	 *
789	 * @param   string  $reference          base mailbox to start the search
790	 * @param   string  $restriction_search false or 0 means return all mailboxes
791	 *                                      true or 1 return only the mailbox that contains that exact name
792	 *                                      2 return all mailboxes in that hierarchy level
793	 *
794	 * @return  mixed   array of mailboxes
795	 */
796	function listUnSubscribedMailboxes($reference = ''  , $restriction_search = 0)
797	{
798		if ( is_bool($restriction_search) ){
799			$restriction_search = (int) $restriction_search;
800		}
801
802		if ( is_int( $restriction_search ) ){
803			switch ( $restriction_search ) {
804			case 0:
805				$mailbox = $reference."*";
806				break;
807			case 1:
808				$mailbox = $reference;
809				$reference = '%';
810				break;
811			case 2:
812				$mailbox = "%";
813				break;
814			}
815		}else{
816			if ( is_string( $restriction_search ) ){
817				$mailbox = $restriction_search;
818			}
819		}
820		//error_log(__METHOD__.__LINE__.$mailbox);
821		$options = array(
822			'sort'=>true,
823			//'flat'=>true,
824		);
825		$mailboxes = $this->listMailboxes($mailbox,Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS, $options);
826		foreach ($mailboxes as $box)
827		{
828			//error_log(__METHOD__.__LINE__.' Box:'.$k.'->'.array2string($box['mailbox']->utf8));
829			$sret[]=$box['mailbox']->utf8;
830		}
831		$unsubscribed = $this->listMailboxes($mailbox,Horde_Imap_Client::MBOX_UNSUBSCRIBED, $options);
832		foreach ($unsubscribed as $box)
833		{
834			//error_log(__METHOD__.__LINE__.' Box:'.$k.'->'.array2string($box['mailbox']->utf8));
835			if (!in_array($box['mailbox']->utf8,$sret) && $box['mailbox']->utf8!='INBOX') $ret[]=$box['mailbox']->utf8;
836		}
837		return $ret;
838	}
839
840	/**
841	 * examineMailbox
842	 *
843	 * @param string $mailbox
844	 * @param int $flags =null default Horde_Imap_Client::STATUS_ALL | Horde_Imap_Client::STATUS_FLAGS | Horde_Imap_Client::STATUS_PERMFLAGS
845	 * @return array of counters for mailbox
846	 */
847	function examineMailbox($mailbox, $flags=null)
848	{
849		if ($mailbox=='') return false;
850		$mailboxes = $this->listMailboxes($mailbox);
851
852		if (is_null($flags)) $flags = Horde_Imap_Client::STATUS_ALL | Horde_Imap_Client::STATUS_FLAGS | Horde_Imap_Client::STATUS_PERMFLAGS;
853
854		$mboxes = new Horde_Imap_Client_Mailbox_List($mailboxes);
855		//_debug_array($mboxes->count());
856		foreach ($mboxes->getIterator() as $k => $box)
857		{
858			//error_log(__METHOD__.__LINE__.array2string($box));
859			unset($box);
860			if ($k!='user' && $k != '' && $k==$mailbox)
861			{
862				$status = $this->status($k, $flags);
863				//error_log(__METHOD__.__LINE__.array2string($status));
864				foreach ($status as $key => $v)
865				{
866					$_status[strtoupper($key)]=$v;
867				}
868				if ($flags & (Horde_Imap_Client::STATUS_FLAGS|Horde_Imap_Client::STATUS_PERMFLAGS))
869				{
870					self::$supports_keywords[$this->ImapServerId] = stripos(implode('', $status['flags']), '$label') !== false ||
871						in_array('\\*', $status['permflags']);	// arbitrary keyswords also allow keywords
872				}
873				return $_status;
874			}
875		}
876		return false;
877	}
878
879	/**
880	 * returns the supported capabilities of the imap server
881	 * return false if the imap server does not support capabilities
882	 *
883	 * @deprecated use capability()
884	 * @return array the supported capabilites
885	 */
886	function getCapabilities()
887	{
888		$cap = $this->capability();
889		foreach ($cap as $c => $v)
890		{
891			if (is_array($v))
892			{
893				foreach ($v as $v)
894				{
895					$cap[$c.'='.$v] = true;
896				}
897			}
898		}
899		return $cap;
900	}
901
902	/**
903	 * Query a single capability
904	 *
905	 * @param string $capability
906	 * @return boolean
907	 */
908	function hasCapability($capability)
909	{
910		if ($capability=='SUPPORTS_KEYWORDS')
911		{
912			// if pseudo-flag is not set, call examineMailbox now to set it (no STATUS_ALL = counters necessary)
913			if (!isset(self::$supports_keywords[$this->ImapServerId]))
914			{
915				try
916				{
917					$this->examineMailbox('INBOX', Horde_Imap_Client::STATUS_FLAGS|Horde_Imap_Client::STATUS_PERMFLAGS);
918				}
919				catch (\Exception $e)
920				{
921					error_log(__METHOD__.__LINE__.' (examineServer for detection) '.$capability.'->'.array2string(self::$supports_keywords).' failed '.function_backtrace());
922					self::$supports_keywords[$this->ImapServerId]=false;
923				}
924			}
925			//error_log(__METHOD__.__LINE__.' '.$capability.'->'.array2string(self::$supports_keywords).' '.function_backtrace());
926			return self::$supports_keywords[$this->ImapServerId];
927		}
928		try
929		{
930			$cap = $this->capability();
931		}
932		catch (\Exception $e)
933		{
934			if ($this->debug) error_log(__METHOD__.__LINE__.' error querying for capability:'.$capability.' ->'.$e->getMessage());
935			return false;
936		}
937		if (!is_array($cap))
938		{
939			error_log(__METHOD__.__LINE__.' error querying for capability:'.$capability.' Expected array but got->'.array2string($cap));
940			return false;
941		}
942		foreach ($cap as $c => $v)
943		{
944			if (is_array($v))
945			{
946				foreach ($v as $v)
947				{
948					$cap[$c.'='.$v] = true;
949				}
950			}
951		}
952		//error_log(__METHOD__.__LINE__.$capability.'->'.array2string($cap));
953		if (isset($cap[$capability]) && $cap[$capability])
954		{
955			return true;
956		}
957		else
958		{
959			return false;
960		}
961	}
962
963	/**
964	 * getFolderPrefixFromNamespace, wrapper to extract the folder prefix from folder compared to given namespace array
965	 *
966	 * @var array $_nameSpace
967	 * @var string $_folderName
968	 * @return string the prefix (may be an empty string)
969	 */
970	static function getFolderPrefixFromNamespace($_nameSpace, $_folderName)
971	{
972		foreach($_nameSpace as &$singleNameSpace)
973		{
974			if (substr($_folderName,0,strlen($singleNameSpace['prefix'])) == $singleNameSpace['prefix']) return $singleNameSpace['prefix'];
975		}
976		return "";
977	}
978
979	/**
980	 * getMailBoxesRecursive
981	 *
982	 * function to retrieve mailboxes recursively from given mailbox
983	 * @param string $_mailbox
984	 * @param string $delimiter
985	 * @param string $prefix
986	 * @param string $reclevel = 0, counter to keep track of the current recursionlevel
987	 * @return array of mailboxes
988	 */
989	function getMailBoxesRecursive($_mailbox, $delimiter, $prefix, $reclevel=0)
990	{
991		if ($reclevel > 25) {
992			error_log( __METHOD__." Recursion Level Exeeded ($reclevel) while looking up $_mailbox$delimiter ");
993			return array();
994		}
995		$reclevel++;
996		// clean up double delimiters
997		$mailbox = preg_replace('~'.($delimiter == '.' ? "\\".$delimiter:$delimiter).'+~s',$delimiter,$_mailbox);
998		//get that mailbox in question
999		$mbx = $this->getMailboxes($mailbox,1,true);
1000		$mbxkeys = array_keys($mbx);
1001
1002		// Example: Array([INBOX/GaGa] => Array([MAILBOX] => INBOX/GaGa[ATTRIBUTES] => Array([0] => \\unmarked)[delimiter] => /))
1003		if (is_array($mbx[$mbxkeys[0]]["ATTRIBUTES"]) && (in_array('\HasChildren',$mbx[$mbxkeys[0]]["ATTRIBUTES"]) || in_array('\Haschildren',$mbx[$mbxkeys[0]]["ATTRIBUTES"]) || in_array('\haschildren',$mbx[$mbxkeys[0]]["ATTRIBUTES"])))
1004		{
1005			$buff = $this->getMailboxes($mbx[$mbxkeys[0]]['MAILBOX'].($mbx[$mbxkeys[0]]['MAILBOX'] == $prefix ? '':$delimiter),2,false);
1006			$allMailboxes = array();
1007			foreach ($buff as $mbxname) {
1008				$mbxname = preg_replace('~'.($delimiter == '.' ? "\\".$delimiter:$delimiter).'+~s',$delimiter,$mbxname['MAILBOX']);
1009				#echo "About to recur in level $reclevel:".$mbxname."<br>";
1010				if ( $mbxname != $mbx[$mbxkeys[0]]['MAILBOX'] && $mbxname != $prefix  && $mbxname != $mbx[$mbxkeys[0]]['MAILBOX'].$delimiter)
1011				{
1012					$allMailboxes = array_merge($allMailboxes, self::getMailBoxesRecursive($mbxname, $delimiter, $prefix, $reclevel));
1013				}
1014			}
1015			if (!(in_array('\NoSelect',$mbx[$mbxkeys[0]]["ATTRIBUTES"]) || in_array('\Noselect',$mbx[$mbxkeys[0]]["ATTRIBUTES"]) || in_array('\noselect',$mbx[$mbxkeys[0]]["ATTRIBUTES"]))) $allMailboxes[] = $mbx[$mbxkeys[0]]['MAILBOX'];
1016			return $allMailboxes;
1017		}
1018		else
1019		{
1020			return array($mailbox);
1021		}
1022	}
1023
1024	/**
1025	 * getNameSpace, fetch the namespace from icServer
1026	 *
1027	 * 	Note: a IMAPServer may present several namespaces under each key;
1028	 *			so we return an array of namespacearrays for our needs
1029	 *
1030	 * @return array array(prefix_present=>mixed (bool/string) ,prefix=>string,delimiter=>string,type=>string (personal|others|shared))
1031	 */
1032	function getNameSpace()
1033	{
1034		static $nameSpace=null;
1035		$foldersNameSpace = array();
1036		$delimiter = $this->getDelimiter();
1037		if (empty($delimiter)) $delimiter='/';
1038		if (is_null($nameSpace)) $nameSpace = $this->getNameSpaceArray();
1039		if (is_array($nameSpace)) {
1040			foreach($nameSpace as $type => $singleNameSpaceArray)
1041			{
1042				foreach ($singleNameSpaceArray as $singleNameSpace)
1043				{
1044					$_foldersNameSpace = array();
1045					if($type == 'personal' && $singleNameSpace['name'] == '#mh/' && ($this->folderExists('Mail')||$this->folderExists('INBOX')))
1046					{
1047						$_foldersNameSpace['prefix_present'] = 'forced';
1048						// uw-imap server with mailbox prefix or dovecot maybe
1049						$_foldersNameSpace['prefix'] = ($this->folderExists('Mail')?'Mail':(!empty($singleNameSpace['name'])?$singleNameSpace['name']:''));
1050					}
1051					elseif($type == 'personal' && ($singleNameSpace['name'] == '#mh/') && $this->folderExists('mail'))
1052					{
1053						$_foldersNameSpace['prefix_present'] = 'forced';
1054						// uw-imap server with mailbox prefix or dovecot maybe
1055						$_foldersNameSpace['prefix'] = 'mail';
1056					} else {
1057						$_foldersNameSpace['prefix_present'] = !empty($singleNameSpace['name']);
1058						$_foldersNameSpace['prefix'] = $singleNameSpace['name'];
1059					}
1060					$_foldersNameSpace['delimiter'] = ($singleNameSpace['delimiter']?$singleNameSpace['delimiter']:$delimiter);
1061					$_foldersNameSpace['type'] = $type;
1062					$foldersNameSpace[] =$_foldersNameSpace;
1063				}
1064			}
1065		}
1066		return $foldersNameSpace;
1067	}
1068
1069	/**
1070	 * return the delimiter used by the current imap server
1071	 * @param mixed _type (1=personal, 2=user/other, 3=shared)
1072	 * @return string the delimimiter
1073	 */
1074	function getDelimiter($_type=1)
1075	{
1076		switch ($_type)
1077		{
1078			case 'user':
1079			case 'other':
1080			case 2:
1081				$type=2;
1082				break;
1083			case 'shared':
1084			case '':
1085			case 3:
1086				$type=3;
1087				break;
1088			case 'personal':
1089			case 1:
1090			default:
1091				$type=1;
1092		}
1093		$namespaces = $this->getNamespaces();
1094		foreach ($namespaces as $nsp)
1095		{
1096			if ($nsp['type']==$type && $nsp['delimiter']) return $nsp['delimiter'];
1097		}
1098		return "/";
1099	}
1100
1101	/**
1102	 * Check if IMAP server supports group ACL, can be overwritten in extending classes
1103	 *
1104	 * If group ACL is supported getMailBoxUserName and getMailBoxAccountId should be
1105	 * modified too, to return correct values for groups.
1106	 *
1107	 * @return boolean true if group ACL is supported, false if not
1108	 */
1109	function supportsGroupAcl()
1110	{
1111		return false;
1112	}
1113
1114	/**
1115	 * get the effective Username for the Mailbox, as it is depending on the loginType
1116	 *
1117	 * @param string|int $_username account_id or account_lid
1118	 * @return string the effective username to be used to access the Mailbox
1119	 */
1120	function getMailBoxUserName($_username)
1121	{
1122		if (is_numeric($_username))
1123		{
1124			$_username = $GLOBALS['egw']->accounts->id2name($accountID=$_username);
1125		}
1126		else
1127		{
1128			$accountID = $GLOBALS['egw']->accounts->name2id($_username);
1129		}
1130		switch ($this->loginType)
1131		{
1132			case 'email':
1133				$_username = $GLOBALS['egw']->accounts->id2name($accountID,'account_email');
1134				break;
1135
1136			case 'vmailmgr':
1137				$_username .= '@'.$this->domainName;
1138				break;
1139
1140			case 'uidNumber':
1141				$_username = 'u'.$accountID;
1142				break;
1143
1144			default:
1145				if (empty($this->loginType))
1146				{
1147					// try to figure out by params['acc_imap_username']
1148					list($lusername,$domain) = explode('@',$this->params['acc_imap_username'],2);
1149					if (strpos($_username,'@')===false && !empty($domain) && !empty($lusername))
1150					{
1151						$_username = $_username.'@'.$domain;
1152					}
1153				}
1154		}
1155		return strtolower($_username);
1156	}
1157
1158	/**
1159	 * Get account_id from a mailbox username
1160	 *
1161	 * @param string $_username
1162	 * @return int|boolean account_id of user or false if no matching user found
1163	 */
1164	function getMailBoxAccountId($_username)
1165	{
1166		switch ($this->loginType)
1167		{
1168			case 'email':
1169				$account_id = $GLOBALS['egw']->accounts->name2id($_username, 'account_email');
1170				break;
1171
1172			case 'uidNumber':
1173				$account_id = (int)substr($_username, 1);
1174				break;
1175
1176			default:
1177				$account_id = $GLOBALS['egw']->accounts->name2id($_username, 'account_lid');
1178		}
1179		return $account_id;
1180	}
1181
1182	/**
1183	 * Create mailbox string from given mailbox-name and user-name
1184	 *
1185	 * @param string $_folderName=''
1186	 * @return string utf-7 encoded (done in getMailboxName)
1187	 */
1188	function getUserMailboxString($_username, $_folderName='')
1189	{
1190		$nameSpaces = $this->getNameSpaceArray();
1191
1192		if(!isset($nameSpaces['others'])) {
1193			return false;
1194		}
1195
1196		$username = $this->getMailBoxUserName($_username);
1197		if($this->loginType == 'vmailmgr' || $this->loginType == 'uidNumber') {
1198			$username .= '@'. $this->domainName;
1199		}
1200
1201		$mailboxString = $nameSpaces['others'][0]['name'] . $username . (!empty($_folderName) ? ($nameSpaces['others'][0]['delimiter']?$nameSpaces['others'][0]['delimiter']:'/') . $_folderName : '');
1202
1203		return $mailboxString;
1204	}
1205
1206	/**
1207	 * get list of namespaces
1208	 *	Note: a IMAPServer may present several namespaces under each key
1209	 * @return array with keys 'personal', 'shared' and 'others' and value array with values for keys 'name' and 'delimiter'
1210	 */
1211	function getNameSpaceArray()
1212	{
1213		static $types = array(
1214			Horde_Imap_Client::NS_PERSONAL => 'personal',
1215			Horde_Imap_Client::NS_OTHER    => 'others',
1216			Horde_Imap_Client::NS_SHARED   => 'shared'
1217		);
1218		//error_log(__METHOD__.__LINE__.array2string($types));
1219		$result = array();
1220		foreach($this->getNamespaces() as $data)
1221		{
1222			//error_log(__METHOD__.__LINE__.array2string($data));
1223			if (isset($types[$data['type']]))
1224			{
1225				$result[$types[$data['type']]][] = array(
1226					'type' => $types[$data['type']],
1227					'name' => $data['name'],
1228					'prefix' => $data['name'],
1229					'prefix_present' => !empty($data['name']),
1230					'delimiter' => ($data['delimiter']?$data['delimiter']:'/'),
1231				);
1232			}
1233		}
1234		//error_log(__METHOD__."() returning ".array2string($result));
1235		return $result;
1236	}
1237
1238	/**
1239	 * return the quota for the current user
1240	 *
1241	 * @param string $mailboxName
1242	 * @return mixed the quota for the current user -> array with all available Quota Information, or false
1243	 */
1244	function getStorageQuotaRoot($mailboxName)
1245	{
1246		$storageQuota = $this->getQuotaRoot($mailboxName);
1247		foreach ($storageQuota as $qInfo)
1248		{
1249			if ($qInfo['storage'])
1250			{
1251				return array('USED'=>$qInfo['storage']['usage'],'QMAX'=>$qInfo['storage']['limit']);
1252			}
1253		}
1254		return false;
1255	}
1256
1257	/**
1258	 * return the quota for another user
1259	 * used by admin connections only
1260	 *
1261	 * @param string $_username
1262	 * @param string $_what - what to retrieve either limit/QMAX, usage/USED or ALL is supported
1263	 * @return int|array|boolean the quota for specified user (by what) or array with values for "limit" and "usage", or false
1264	 */
1265	function getQuotaByUser($_username, $_what='QMAX')
1266	{
1267		$mailboxName = $this->getUserMailboxString($_username);
1268		$storageQuota = $this->getQuotaRoot($mailboxName);
1269		//error_log(__METHOD__.' Username:'.$_username.' Mailbox:'.$mailboxName.' getQuotaRoot('.$_what.'):'.array2string($storageQuota));
1270
1271		if (is_array($storageQuota) && isset($storageQuota[$mailboxName]) && is_array($storageQuota[$mailboxName]) &&
1272			isset($storageQuota[$mailboxName]['storage']) && is_array($storageQuota[$mailboxName]['storage']))
1273		{
1274			switch($_what)
1275			{
1276				case 'QMAX':
1277					$_what = 'limit';
1278					break;
1279				case 'USED':
1280					$_what = 'usage';
1281				case 'ALL':
1282					return $storageQuota[$mailboxName]['storage'];
1283			}
1284			return isset($storageQuota[$mailboxName]['storage'][$_what]) ? (int)$storageQuota[$mailboxName]['storage'][$_what] : false;
1285		}
1286
1287		return false;
1288	}
1289
1290	/**
1291	 * returns information about a user
1292	 *
1293	 * Only a stub, as admin connection requires, which is only supported for Cyrus
1294	 *
1295	 * @param string $_username
1296	 * @return array userdata
1297	 */
1298	function getUserData($_username)
1299	{
1300		unset($_username);	// not used
1301		return array();
1302	}
1303
1304	/**
1305	 * set userdata
1306	 *
1307	 * @param string $_username username of the user
1308	 * @param int $_quota quota in bytes
1309	 * @return bool true on success, false on failure
1310	 */
1311	function setUserData($_username, $_quota)
1312	{
1313		unset($_username, $_quota);	// not used
1314		return true;
1315	}
1316
1317	/**
1318	 * check if imap server supports given capability
1319	 *
1320	 * @param string $_capability the capability to check for
1321	 * @return bool true if capability is supported, false if not
1322	 */
1323	function supportsCapability($_capability)
1324	{
1325		return $this->hasCapability($_capability);
1326	}
1327
1328	/**
1329	 * Instance of Sieve
1330	 *
1331	 * @var Sieve
1332	 */
1333	private $sieve;
1334
1335	public $scriptName;
1336	public $error;
1337
1338	//public $error;
1339
1340	/**
1341	 * Proxy former felamimail bosieve methods to internal Sieve instance
1342	 *
1343	 * @param string $name
1344	 * @param array $params
1345	 * @throws Api\Exception\WrongParameter
1346	 */
1347	public function __call($name,array $params=null)
1348	{
1349		if ($this->debug) error_log(__METHOD__.'->'.$name.' with params:'.array2string($params));
1350		switch($name)
1351		{
1352			case 'installScript':
1353			case 'getScript':
1354			case 'setActive':
1355			case 'setEmailNotification':
1356			case 'getEmailNotification':
1357			case 'setRules':
1358			case 'getRules':
1359			case 'retrieveRules':
1360			case 'getVacation':
1361			case 'setVacation':
1362				if (is_null($this->sieve))
1363				{
1364					$this->sieve = new Sieve($this);
1365					$this->error =& $this->sieve->error;
1366				}
1367				$ret = call_user_func_array(array($this->sieve,$name),$params);
1368				//error_log(__CLASS__.'->'.$name.'('.array2string($params).') returns '.array2string($ret));
1369				return $ret;
1370		}
1371		throw new Api\Exception\WrongParameter("No method '$name' implemented!");
1372	}
1373
1374	/**
1375	 * Set vacation message for given user
1376	 *
1377	 * @param int|string $_euser nummeric account_id or imap username
1378	 * @param array $_vacation
1379	 * @param string $_scriptName =null
1380	 * @return boolean
1381	 */
1382	public function setVacationUser($_euser, array $_vacation, $_scriptName=null)
1383	{
1384		if ($this->debug) error_log(__METHOD__.' User:'.array2string($_euser).' Scriptname:'.array2string($_scriptName).' VacationMessage:'.array2string($_vacation));
1385
1386		if (is_numeric($_euser))
1387		{
1388			$_euser = $this->getMailBoxUserName($_euser);
1389		}
1390		if (is_null($this->sieve) || $this->isAdminConnection !== $_euser)
1391		{
1392			$this->adminConnection($_euser);
1393			$this->sieve = new Sieve($this, $_euser, $_scriptName);
1394			$this->scriptName =& $this->sieve->scriptName;
1395			$this->error =& $this->sieve->error;
1396		}
1397		$ret = $this->setVacation($_vacation, $_scriptName);
1398
1399		return $ret;
1400	}
1401
1402	/**
1403	 * Get vacation message for given user
1404	 *
1405	 * @param int|string $_euser nummeric account_id or imap username
1406	 * @param string $_scriptName =null
1407	 * @throws Exception on connection error or authentication failure
1408	 * @return array
1409	 */
1410	public function getVacationUser($_euser, $_scriptName=null)
1411	{
1412		if ($this->debug) error_log(__METHOD__.' User:'.array2string($_euser));
1413
1414		if (is_numeric($_euser))
1415		{
1416			$_euser = $this->getMailBoxUserName($_euser);
1417		}
1418		if (is_null($this->sieve) || $this->isAdminConnection !== $_euser)
1419		{
1420			$this->adminConnection($_euser);
1421			$this->sieve = new Sieve($this, $_euser, $_scriptName);
1422			$this->error =& $this->sieve->error;
1423			$this->scriptName =& $this->sieve->scriptName;
1424		}
1425		return $this->sieve->getVacation();
1426	}
1427
1428	/**
1429	 * Return fields or tabs which should be readonly in UI for given imap implementation
1430	 *
1431	 * @return array fieldname => true pairs or 'tabs' => array(tabname => true)
1432	 */
1433	public static function getUIreadonlys()
1434	{
1435		return array();
1436	}
1437
1438	/**
1439	 * @var array IMAP servers supporting push
1440	 */
1441	protected static $hosts_with_push = [];
1442
1443	/**
1444	 * Init static variables
1445	 */
1446	public static function init_static()
1447	{
1448		self::$supports_keywords =& Api\Cache::getSession (__CLASS__, 'supports_keywords');
1449
1450		// hosts from header.inc.php
1451		self::$hosts_with_push = $GLOBALS['egw_info']['server']['imap_hosts_with_push'] ?? [];
1452		// plus hosts from mail site config
1453		$config = Api\Config::read('mail');
1454		foreach(!empty($config['imap_hosts_with_push']) ? preg_split('/[, ]+/', $config['imap_hosts_with_push']) : [] as $host)
1455		{
1456			self::$hosts_with_push[] = $host;
1457		}
1458	}
1459
1460	/**
1461	 * Metadata name to enable push notifications in Dovecot
1462	 */
1463	const METADATA_NAME = '/private/vendor/vendor.dovecot/http-notify';
1464	const METADATA_MAILBOX = '';
1465	const METADATA_PREFIX = 'user=';
1466	const METADATA_SEPARATOR = ';;';
1467
1468	/**
1469	 * Generate token / user-information for push to be stored by Dovecot
1470	 *
1471	 * The user informations has the form "$account_id::$acc_id;$token@$host"
1472	 *
1473	 * @param null $account_id
1474	 * @param string $token =null default push token of instance ($account_id=='0') or user
1475	 * @return string
1476	 * @throws Api\Exception\AssertionFailed
1477	 */
1478	protected function pushToken($account_id=null, $token=null)
1479	{
1480		if (!isset($token)) $token = ((string)$account_id === '0' ? Tokens::instance() : Tokens::user($account_id));
1481
1482		return $GLOBALS['egw_info']['user']['account_id'].'::'.$this->acc_id.';'.
1483			$token . '@' . Api\Header\Http::host();
1484	}
1485
1486	/**
1487	 * Enable push notifictions for current connection and given account_id
1488	 *
1489	 * @param int $account_id =null 0=everyone on the instance
1490	 * @return bool true on success, false on failure
1491	 */
1492	function enablePush($account_id=null)
1493	{
1494		if (!class_exists(Tokens::class))
1495		{
1496			return false;
1497		}
1498		try {
1499			$metadata = ($m = $this->getMetadata(self::METADATA_MAILBOX, [self::METADATA_NAME])[self::METADATA_MAILBOX][self::METADATA_NAME]) ?
1500				explode(self::METADATA_SEPARATOR, substr($m, strlen(self::METADATA_PREFIX))) : [];
1501			$my_token = $this->pushToken($account_id);
1502			$my_token_preg = '/^'.$this->pushToken($account_id, '[^@]+').'$/';
1503			foreach($metadata as $key => $token)
1504			{
1505				// token already registered --> we're done
1506				if ($token === $my_token) return true;
1507
1508				// check old/expired token registered --> remove it
1509				if (preg_match($my_token_preg, $token))
1510				{
1511					unset($metadata[$key]);
1512					break;
1513				}
1514			}
1515			// add my token and send it to Dovecot
1516			$metadata[] = $my_token;
1517			$this->setMetadata(self::METADATA_MAILBOX, [
1518				self::METADATA_NAME => self::METADATA_PREFIX.implode(self::METADATA_SEPARATOR, $metadata),
1519			]);
1520		}
1521		catch (Horde_Imap_Client_Exception $e) {
1522			_egw_log_exception($e);
1523			return false;
1524		}
1525		return true;
1526	}
1527
1528	/**
1529	 * Check if push is available / configured for given server
1530	 *
1531	 * @return bool
1532	 */
1533	function pushAvailable()
1534	{
1535		return self::$hosts_with_push && (in_array($this->acc_imap_host, self::$hosts_with_push) ||
1536			in_array($this->acc_imap_host.':'.$this->acc_imap_port, self::$hosts_with_push));
1537	}
1538}
1539Imap::init_static();
1540