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