1<?php 2/** 3 * EGroupware - Mail - worker class 4 * 5 * @link http://www.egroupware.org 6 * @package api 7 * @subpackage amil 8 * @author Stylite AG [info@stylite.de] 9 * @copyright (c) 2013-2016 by Stylite AG <info-AT-stylite.de> 10 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License 11 * @version $Id$ 12 */ 13 14namespace EGroupware\Api; 15 16use Horde_Imap_Client; 17use Horde_Imap_Client_Ids; 18use Horde_Imap_Client_Fetch_Query; 19use Horde_Imap_Client_Data_Fetch; 20use Horde_Mime_Part; 21use Horde_Imap_Client_Search_Query; 22use Horde_Idna; 23use Horde_Imap_Client_DateTime; 24use Horde_Mime_Headers; 25use Horde_Compress; 26use Horde_Mime_Magic; 27use Horde_Mail_Rfc822; 28use Horde_Mail_Rfc822_List; 29use Horde_Mime_Mdn; 30use Horde_Translation; 31use Horde_Translation_Handler_Gettext; 32use EGroupware\Api; 33 34use tidy; 35 36/** 37 * Mail worker class 38 * -provides backend functionality for all classes in Mail 39 * -provides classes that may be used by other apps too 40 * 41 * @link https://github.com/horde/horde/blob/master/imp/lib/Contents.php 42 */ 43class Mail 44{ 45 /** 46 * the current selected user profile 47 * @var int 48 */ 49 var $profileID = 0; 50 51 /** 52 * delimiter - used to separate acc_id from mailbox / folder-tree-structure 53 * 54 * @var string 55 */ 56 const DELIMITER = '::'; 57 58 /** 59 * the current display char set 60 * @var string 61 */ 62 static $displayCharset; 63 static $activeFolderCache; 64 static $folderStatusCache; 65 static $supportsORinQuery; 66 67 /** 68 * Active preferences 69 * 70 * @var array 71 */ 72 var $mailPreferences; 73 74 /** 75 * active html Options 76 * 77 * @var array 78 */ 79 var $htmlOptions; 80 81 /** 82 * Active mimeType 83 * 84 * @var string 85 */ 86 var $activeMimeType; 87 88 /** 89 * Active incomming (IMAP) Server Object 90 * 91 * @var Api\Mail\Imap 92 */ 93 var $icServer; 94 95 /** 96 * Active outgoing (smtp) Server Object 97 * 98 * @var Api\Mail\Smtp 99 */ 100 var $ogServer; 101 102 /** 103 * errorMessage 104 * 105 * @var string $errorMessage 106 */ 107 var $errorMessage; 108 109 /** 110 * switch to enable debug; sometimes debuging is quite handy, to see things. check with the error log to see results 111 * @var boolean 112 */ 113 static $debug = false; //true; 114 static $debugTimes = false; //true; 115 116 /** 117 * static used to hold the mail Config values 118 * @array 119 */ 120 static $mailConfig; 121 122 /** 123 * static used to configure tidy - if tidy is loadable, this config is used with tidy to straighten out html, instead of using purifiers tidy mode 124 * 125 * @array 126 */ 127 static $tidy_config = array('clean'=>false,'output-html'=>true,'join-classes'=>true,'join-styles'=>true,'show-body-only'=>"auto",'word-2000'=>true,'wrap'=>0); 128 129 /** 130 * static used to configure htmLawed, for use with emails 131 * 132 * @array 133 */ 134 static $htmLawed_config = array('comment'=>1, //remove comments 135 'make_tag_strict' => 3, // 3 is a new own config value, to indicate that transformation is to be performed, but don't transform font as size transformation of numeric sizes to keywords alters the intended result too much 136 'keep_bad'=>2, //remove tags but keep element content (4 and 6 keep element content only if text (pcdata) is valid in parent element as per specs, this may lead to textloss if balance is switched on) 137 // we switch the balance off because of some broken html mails contents get removed like (td in table), and let browser deal with it 138 'balance'=>0,//turn off tag-balancing (config['balance']=>0). That will not introduce any security risk; only standards-compliant tag nesting check/filtering will be turned off (basic tag-balance will remain; i.e., there won't be any unclosed tag, etc., after filtering) 139 'direct_list_nest' => 1, 140 'allow_for_inline' => array('table','div','li','p'),//block elements allowed for nesting when only inline is allowed; Example span does not allow block elements as table; table is the only element tested so far 141 // tidy eats away even some wanted whitespace, so we switch it off; 142 // we used it for its compacting and beautifying capabilities, which resulted in better html for further processing 143 'tidy'=>0, 144 'elements' => "* -script -meta -object", 145 'deny_attribute' => 'on*', 146 'schemes'=>'href: file, ftp, http, https, mailto, phone, tel; src: cid, data, file, ftp, http, https; *:file, http, https, cid, src', 147 'hook_tag' =>"hl_email_tag_transform", 148 ); 149 150 /** 151 * static used define abbrevations for common access rights 152 * 153 * @array 154 */ 155 static $aclShortCuts = array('' => array('label'=>'none','title'=>'The user has no rights whatsoever.'), 156 'lrs' => array('label'=>'readable','title'=>'Allows a user to read the contents of the mailbox.'), 157 'lprs' => array('label'=>'post','title'=>'Allows a user to read the mailbox and post to it through the delivery system by sending mail to the submission address of the mailbox.'), 158 'ilprs' => array('label'=>'append','title'=>'Allows a user to read the mailbox and append messages to it, either via IMAP or through the delivery system.'), 159 'cdilprsw' => array('label'=>'write','title'=>'Allows a user to read the maibox, post to it, append messages to it, and delete messages or the mailbox itself. The only right not given is the right to change the ACL of the mailbox.'), 160 'acdilprsw' => array('label'=>'all','title'=>'The user has all possible rights on the mailbox. This is usually granted to users only on the mailboxes they own.'), 161 'custom' => array('label'=>'custom','title'=>'User defined combination of rights for the ACL'), 162 ); 163 164 /** 165 * Folders that get automatic created AND get translated to the users language 166 * their creation is also controlled by users mailpreferences. if set to none / dont use folder 167 * the folder will not be automatically created. This is controlled in Mail->getFolderObjects 168 * so changing names here, must include a change of keywords there as well. Since these 169 * foldernames are subject to translation, keep that in mind too, if you change names here. 170 * lang('Drafts'), lang('Templates'), lang('Sent'), lang('Trash'), lang('Junk'), lang('Outbox') 171 * ActiveSync: 172 * Outbox is needed by Nokia Clients to be able to send Mails 173 * @var array 174 */ 175 static $autoFolders = array('Drafts', 'Templates', 'Sent', 'Trash', 'Junk', 'Outbox'); 176 177 /** 178 * Array to cache the specialUseFolders, if existing 179 * @var array 180 */ 181 static $specialUseFolders; 182 183 /** 184 * Hold instances by profileID for getInstance() singleton 185 * 186 * @var array 187 */ 188 private static $instances = array(); 189 private static $profileDefunct = array(); 190 191 /** 192 * Singleton for Mail 193 * 194 * @param boolean $_restoreSession = true 195 * @param int $_profileID = 0 196 * @param boolean $_validate = true - flag wether the profileid should be validated or not, if validation is true, you may receive a profile 197 * not matching the input profileID, if we can not find a profile matching the given ID 198 * @param mixed boolean/object $_icServerObject - if object, return instance with object set as icServer 199 * immediately, if boolean === true use oldImapServer in constructor 200 * @param boolean $_reuseCache = null if null it is set to the value of $_restoreSession 201 * @return Mail 202 */ 203 public static function getInstance($_restoreSession=true, &$_profileID=0, $_validate=true, $_oldImapServerObject=false, $_reuseCache=null) 204 { 205 //$_restoreSession=false; 206 if (is_null($_reuseCache)) $_reuseCache = $_restoreSession; 207 //error_log(__METHOD__.' ('.__LINE__.') '.' RestoreSession:'.$_restoreSession.' ProfileId:'.$_profileID.'/'.Mail\Account::get_default_acc_id().' for user:'.$GLOBALS['egw_info']['user']['account_lid'].' called from:'.function_backtrace()); 208 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($_oldImapServerObject)); 209 self::$profileDefunct = Cache::getCache(Cache::INSTANCE,'email','profileDefunct'.trim($GLOBALS['egw_info']['user']['account_id']),null,array(),5*1); 210 if (isset(self::$profileDefunct[$_profileID]) && strlen(self::$profileDefunct[$_profileID])) 211 { 212 throw new Exception(__METHOD__." failed to instanciate Mail for Profile #$_profileID Reason:".self::$profileDefunct[$_profileID]); 213 } 214 if ($_oldImapServerObject instanceof Mail\Imap) 215 { 216 if (!is_object(self::$instances[$_profileID])) 217 { 218 self::$instances[$_profileID] = new Mail('utf-8',false,$_profileID,false,$_reuseCache); 219 } 220 self::$instances[$_profileID]->icServer = $_oldImapServerObject; 221 self::$instances[$_profileID]->accountid= $_oldImapServerObject->ImapServerId; 222 self::$instances[$_profileID]->profileID= $_oldImapServerObject->ImapServerId; 223 self::$instances[$_profileID]->mailPreferences = $GLOBALS['egw_info']['user']['preferences']['mail']; 224 self::$instances[$_profileID]->htmlOptions = self::$instances[$_profileID]->mailPreferences['htmlOptions']; 225 return self::$instances[$_profileID]; 226 } 227 if ($_profileID == 0) 228 { 229 if (isset($GLOBALS['egw_info']['user']['preferences']['mail']['ActiveProfileID']) && !empty($GLOBALS['egw_info']['user']['preferences']['mail']['ActiveProfileID'])) 230 { 231 $profileID = (int)$GLOBALS['egw_info']['user']['preferences']['mail']['ActiveProfileID']; 232 } 233 else 234 { 235 $profileID = Mail\Account::get_default_acc_id(); 236 } 237 if ($profileID!=$_profileID) $_restoreSession==false; 238 $_profileID=$profileID; 239 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.' called with profileID==0 using '.$profileID.' instead->'.function_backtrace()); 240 } 241 // no validation or restoreSession for old ImapServer Object, just fetch it and return it 242 if ($_oldImapServerObject===true) 243 { 244 return new Mail('utf-8',false,$_profileID,true,$_reuseCache); 245 } 246 if ($_profileID != 0 && $_validate) 247 { 248 $profileID = self::validateProfileID($_profileID); 249 if ($profileID != $_profileID) 250 { 251 if (self::$debug) 252 { 253 error_log(__METHOD__.' ('.__LINE__.') '.' Validation of profile with ID:'.$_profileID.' failed. Using '.$profileID.' instead.'); 254 error_log(__METHOD__.' ('.__LINE__.') '.' # Instance='.$GLOBALS['egw_info']['user']['domain'].', User='.$GLOBALS['egw_info']['user']['account_lid']); 255 } 256 $_profileID = $profileID; 257 //$GLOBALS['egw']->preferences->add('mail','ActiveProfileID',$_profileID,'user'); 258 // save prefs 259 //$GLOBALS['egw']->preferences->save_repository(true); 260 } 261 //Cache::setSession('mail','activeProfileID',$_profileID); 262 } 263 //error_log(__METHOD__.' ('.__LINE__.') '.' RestoreSession:'.$_restoreSession.' ProfileId:'.$_profileID.' called from:'.function_backtrace()); 264 if ($_profileID && (!isset(self::$instances[$_profileID]) || $_restoreSession===false)) 265 { 266 self::$instances[$_profileID] = new Mail('utf-8',$_restoreSession,$_profileID,false,$_reuseCache); 267 } 268 else 269 { 270 //refresh objects 271 try 272 { 273 self::$instances[$_profileID]->icServer = Mail\Account::read($_profileID)->imapServer(); 274 self::$instances[$_profileID]->ogServer = Mail\Account::read($_profileID)->smtpServer(); 275 // TODO: merge mailprefs into userprefs, for easy treatment 276 self::$instances[$_profileID]->mailPreferences = $GLOBALS['egw_info']['user']['preferences']['mail']; 277 self::$instances[$_profileID]->htmlOptions = self::$instances[$_profileID]->mailPreferences['htmlOptions']; 278 } catch (\Exception $e) 279 { 280 $newprofileID = Mail\Account::get_default_acc_id(); 281 // try loading the default profile for the user 282 error_log(__METHOD__.' ('.__LINE__.') '." Loading the Profile for ProfileID ".$_profileID.' failed for icServer; '.$e->getMessage().' Trigger new instance for Default-Profile '.$newprofileID.'. called from:'.function_backtrace()); 283 if ($newprofileID) 284 { 285 self::$instances[$newprofileID] = new Mail('utf-8',false,$newprofileID,false,$_reuseCache); 286 $_profileID = $newprofileID; 287 } 288 else 289 { 290 throw new Exception(__METHOD__." failed to load the Profile for ProfileID for $_profileID with error:".$e->getMessage().($e->details?', '.$e->details:'')); 291 } 292 } 293 self::storeActiveProfileIDToPref(self::$instances[$_profileID]->icServer, $_profileID, $_validate ); 294 } 295 self::$instances[$_profileID]->profileID = $_profileID; 296 if (!isset(self::$instances[$_profileID]->idna2)) self::$instances[$_profileID]->idna2 = new Horde_Idna; 297 //if ($_profileID==0); error_log(__METHOD__.' ('.__LINE__.') '.' RestoreSession:'.$_restoreSession.' ProfileId:'.$_profileID); 298 if (is_null(self::$mailConfig)) self::$mailConfig = Config::read('mail'); 299 return self::$instances[$_profileID]; 300 } 301 302 /** 303 * This method tries to fix alias address lacking domain part 304 * by trying to add domain part extracted from given reference address 305 * 306 * @param string $refrence email address to be used for domain extraction 307 * @param string $address alias address 308 * 309 * @return string returns alias address with appended default domain 310 */ 311 public static function fixInvalidAliasAddress($refrence, $address) 312 { 313 $parts = explode('@', $refrence); 314 if (!strpos($address,'@') && !empty($parts[1])) $address .= '@'.$parts[1]; 315 return $address; 316 } 317 318 /** 319 * store given ProfileID to Session and pref 320 * 321 * @param int $_profileID = 0 322 * @param boolean $_testConnection = 0 323 * @return mixed $_profileID or false on failed ConnectionTest 324 */ 325 public static function storeActiveProfileIDToPref($_icServerObject, $_profileID=0, $_testConnection=true) 326 { 327 if (isset($GLOBALS['egw_info']['user']['preferences']['mail']['ActiveProfileID']) && !empty($GLOBALS['egw_info']['user']['preferences']['mail']['ActiveProfileID'])) 328 { 329 $oldProfileID = (int)$GLOBALS['egw_info']['user']['preferences']['mail']['ActiveProfileID']; 330 } 331 if ($_testConnection) 332 { 333 try 334 { 335 $_icServerObject->getCurrentMailbox(); 336 } 337 catch (\Exception $e) 338 { 339 if ($_profileID != Mail\Account::get_default_acc_id()) $_profileID = Mail\Account::get_default_acc_id(); 340 error_log(__METHOD__.__LINE__.' '.$e->getMessage()); 341 return false; 342 } 343 } 344 if ($oldProfileID != $_profileID) 345 { 346 if ($oldProfileID && $_profileID==0) $_profileID = $oldProfileID; 347 $GLOBALS['egw']->preferences->add('mail','ActiveProfileID',$_profileID,'user'); 348 // save prefs 349 $GLOBALS['egw']->preferences->save_repository(true); 350 $GLOBALS['egw_info']['user']['preferences']['mail']['ActiveProfileID'] = $_profileID; 351 Cache::setSession('mail','activeProfileID',$_profileID); 352 } 353 return $_profileID; 354 } 355 356 /** 357 * Validate given account acc_id to make sure account is valid for current user 358 * 359 * Validation checks: 360 * - non-empty imap-host 361 * - non-empty imap-username 362 * 363 * @param int $_acc_id = 0 364 * @return int validated acc_id -> either acc_id given, or first valid one 365 */ 366 public static function validateProfileID($_acc_id=0) 367 { 368 if ($_acc_id) 369 { 370 try { 371 $account = Mail\Account::read($_acc_id); 372 if ($account->is_imap()) 373 { 374 return $_acc_id; 375 } 376 if (self::$debug) error_log(__METHOD__."($_acc_id) account NOT valid, no imap-host!"); 377 } 378 catch (\Exception $e) { 379 unset($e); 380 if (self::$debug) error_log(__METHOD__."($_acc_id) account NOT found!"); 381 } 382 } 383 // no account specified or specified account not found or not valid 384 // --> search existing account for first valid one and return that 385 foreach(Mail\Account::search($only_current_user=true, 'acc_imap_host') as $acc_id => $imap_host) 386 { 387 if (!empty($imap_host) && ($account = Mail\Account::read($acc_id)) && $account->is_imap()) 388 { 389 if (self::$debug && $_acc_id) error_log(__METHOD__."($_acc_id) using $acc_id instead"); 390 return $acc_id; 391 } 392 } 393 if (self::$debug) error_log(__METHOD__."($_acc_id) NO valid account found!"); 394 return 0; 395 } 396 397 398 /** 399 * Private constructor, use Mail::getInstance() instead 400 * 401 * @param string $_displayCharset = 'utf-8' 402 * @param boolean $_restoreSession = true 403 * @param int $_profileID = 0 if not nummeric, we assume we only want an empty class object 404 * @param boolean $_oldImapServerObject = false 405 * @param boolean $_reuseCache = null if null it is set to the value of $_restoreSession 406 */ 407 private function __construct($_displayCharset='utf-8',$_restoreSession=true, $_profileID=0, $_oldImapServerObject=false, $_reuseCache=null) 408 { 409 if (is_null($_reuseCache)) $_reuseCache = $_restoreSession; 410 if (!empty($_displayCharset)) self::$displayCharset = $_displayCharset; 411 // not nummeric, we assume we only want an empty class object 412 if (!is_numeric($_profileID)) return; 413 if ($_restoreSession) 414 { 415 //error_log(__METHOD__." Session restore ".function_backtrace()); 416 $this->restoreSessionData(); 417 $lv_mailbox = $this->sessionData['mailbox']; 418 $firstMessage = $this->sessionData['previewMessage']; 419 } 420 else 421 { 422 $this->restoreSessionData(); 423 $lv_mailbox = $this->sessionData['mailbox']; 424 $firstMessage = $this->sessionData['previewMessage']; 425 $this->sessionData = array(); 426 } 427 if (!$_reuseCache) $this->forcePrefReload($_profileID,!$_reuseCache); 428 try 429 { 430 $this->profileID = self::validateProfileID($_profileID); 431 $this->accountid = $GLOBALS['egw_info']['user']['account_id']; 432 433 //error_log(__METHOD__.' ('.__LINE__.') '." ProfileID ".$this->profileID.' called from:'.function_backtrace()); 434 $acc = Mail\Account::read($this->profileID); 435 } 436 catch (\Exception $e) 437 { 438 throw new Exception(__METHOD__." failed to instanciate Mail for $_profileID / ".$this->profileID." with error:".$e->getMessage()); 439 } 440 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($acc->imapServer())); 441 $this->icServer = ($_oldImapServerObject?$acc->oldImapServer():$acc->imapServer()); 442 $this->ogServer = $acc->smtpServer(); 443 // TODO: merge mailprefs into userprefs, for easy treatment 444 $this->mailPreferences = $GLOBALS['egw_info']['user']['preferences']['mail']; 445 $this->htmlOptions = $this->mailPreferences['htmlOptions']; 446 if (isset($this->icServer->ImapServerId) && !empty($this->icServer->ImapServerId)) 447 { 448 $_profileID = $this->profileID = $GLOBALS['egw_info']['user']['preferences']['mail']['ActiveProfileID'] = $this->icServer->ImapServerId; 449 } 450 451 if (is_null(self::$mailConfig)) self::$mailConfig = Config::read('mail'); 452 } 453 454 /** 455 * forceEAProfileLoad 456 * used to force the load of a specific emailadmin profile; we assume administrative use only (as of now) 457 * @param int $_profile_id 458 * @return object instance of Mail (by reference) 459 */ 460 public static function &forceEAProfileLoad($_profile_id) 461 { 462 self::unsetCachedObjects($_profile_id); 463 $mail = self::getInstance(false, $_profile_id,false); 464 //_debug_array( $_profile_id); 465 $mail->icServer = Mail\Account::read($_profile_id)->imapServer(); 466 $mail->ogServer = Mail\Account::read($_profile_id)->smtpServer(); 467 return $mail; 468 } 469 470 /** 471 * trigger the force of the reload of the SessionData by resetting the session to an empty array 472 * @param int $_profile_id 473 * @param boolean $_resetFolderObjects 474 */ 475 public static function forcePrefReload($_profile_id=null,$_resetFolderObjects=true) 476 { 477 // unset the mail_preferences session object, to force the reload/rebuild 478 Cache::setSession('mail','mail_preferences',serialize(array())); 479 Cache::setSession('emailadmin','session_data',serialize(array())); 480 if ($_resetFolderObjects) self::resetFolderObjectCache($_profile_id); 481 } 482 483 /** 484 * restore the SessionData 485 */ 486 function restoreSessionData() 487 { 488 $this->sessionData = array(); 489 self::$activeFolderCache = Cache::getCache(Cache::INSTANCE,'email','activeMailbox'.trim($GLOBALS['egw_info']['user']['account_id']),null,array(),60*60*10); 490 if (is_array(self::$activeFolderCache[$this->profileID])) 491 { 492 foreach (self::$activeFolderCache[$this->profileID] as $key => $value) 493 { 494 $this->sessionData[$key] = $value; 495 } 496 } 497 } 498 499 /** 500 * saveSessionData saves session data 501 */ 502 function saveSessionData() 503 { 504 //error_log(__METHOD__.' ('.__LINE__.') '.array2string(array_keys($this->sessionData))); 505 foreach ($this->sessionData as $key => $value) 506 { 507 if (!is_array(self::$activeFolderCache) && empty(self::$activeFolderCache[$this->profileID])) 508 { 509 self::$activeFolderCache = array($this->profileID => array($key => $value)); 510 } 511 else if(empty(self::$activeFolderCache[$this->profileID])) 512 { 513 self::$activeFolderCache += array($this->profileID => array($key => $value)); 514 } 515 else 516 { 517 self::$activeFolderCache[$this->profileID] = array_merge(self::$activeFolderCache[$this->profileID], array($key => $value)); 518 } 519 } 520 521 if (isset(self::$activeFolderCache) && is_array(self::$activeFolderCache)) 522 { 523 Cache::setCache(Cache::INSTANCE,'email','activeMailbox'.trim($GLOBALS['egw_info']['user']['account_id']),self::$activeFolderCache, 60*60*10); 524 } 525 // no need to block session any longer 526 $GLOBALS['egw']->session->commit_session(); 527 } 528 529 /** 530 * unset certain CachedObjects for the given profile id, unsets the profile for default ID=0 as well 531 * 532 * 1) icServerIMAP_connectionError 533 * 2) icServerSIEVE_connectionError 534 * 3) INSTANCE OF MAIL_BO 535 * 4) HierarchyDelimiter 536 * 5) VacationNotice 537 * 538 * @param int $_profileID = null default profile of user as returned by getUserDefaultProfileID 539 * @return void 540 */ 541 static function unsetCachedObjects($_profileID=null) 542 { 543 if (is_null($_profileID)) $_profileID = Mail\Account::get_default_acc_id(); 544 if (is_array($_profileID) && $_profileID['account_id']) $account_id = $_profileID['account_id']; 545 //error_log(__METHOD__.__LINE__.' called with ProfileID:'.array2string($_profileID).' from '.function_backtrace()); 546 if (!is_array($_profileID) && (is_numeric($_profileID) || !(stripos($_profileID,'tracker_')===false))) 547 { 548 self::resetConnectionErrorCache($_profileID); 549 $rawHeadersCache = Cache::getCache(Cache::INSTANCE,'email','rawHeadersCache'.trim($account_id),$callback=null,$callback_params=array(),$expiration=60*60*1); 550 if (isset($rawHeadersCache[$_profileID])) 551 { 552 unset($rawHeadersCache[$_profileID]); 553 Cache::setCache(Cache::INSTANCE,'email','rawHeadersCache'.trim($account_id),$rawHeadersCache, $expiration=60*60*1); 554 } 555 $HierarchyDelimiterCache = Cache::getCache(Cache::INSTANCE,'email','HierarchyDelimiter'.trim($account_id),$callback=null,$callback_params=array(),$expiration=60*60*24*5); 556 if (isset($HierarchyDelimiterCache[$_profileID])) 557 { 558 unset($HierarchyDelimiterCache[$_profileID]); 559 Cache::setCache(Cache::INSTANCE,'email','HierarchyDelimiter'.trim($account_id),$HierarchyDelimiterCache, $expiration=60*60*24*5); 560 } 561 //reset folderObject cache, to trigger reload 562 self::resetFolderObjectCache($_profileID); 563 //reset counter of deleted messages per folder 564 $eMailListContainsDeletedMessages = Cache::getCache(Cache::INSTANCE,'email','eMailListContainsDeletedMessages'.trim($account_id),$callback=null,$callback_params=array(),$expiration=60*60*1); 565 if (isset($eMailListContainsDeletedMessages[$_profileID])) 566 { 567 unset($eMailListContainsDeletedMessages[$_profileID]); 568 Cache::setCache(Cache::INSTANCE,'email','eMailListContainsDeletedMessages'.trim($account_id),$eMailListContainsDeletedMessages, $expiration=60*60*1); 569 } 570 $vacationCached = Cache::getCache(Cache::INSTANCE, 'email', 'vacationNotice'.trim($account_id),$callback=null,$callback_params=array(),$expiration=60*60*24*1); 571 if (isset($vacationCached[$_profileID])) 572 { 573 unset($vacationCached[$_profileID]); 574 Cache::setCache(Cache::INSTANCE,'email','vacationNotice'.trim($account_id),$vacationCached, $expiration=60*60*24*1); 575 } 576 577 if (isset(self::$instances[$_profileID])) unset(self::$instances[$_profileID]); 578 } 579 if (is_array($_profileID) && $_profileID['location'] == 'clear_cache') 580 { 581 // called via hook 582 foreach($GLOBALS['egw']->accounts->search(array('type' => 'accounts','order' => 'account_lid')) as $account) 583 { 584 //error_log(__METHOD__.__LINE__.array2string($account)); 585 $account_id = $account['account_id']; 586 $_profileID = null; 587 self::resetConnectionErrorCache($_profileID,$account_id); 588 self::resetFolderObjectCache($_profileID,$account_id); 589 Cache::setCache(Cache::INSTANCE,'email','rawHeadersCache'.trim($account_id),array(), 60*60*1); 590 Cache::setCache(Cache::INSTANCE,'email','HierarchyDelimiter'.trim($account_id),array(), 60*60*24*5); 591 Cache::setCache(Cache::INSTANCE,'email','eMailListContainsDeletedMessages'.trim($account_id),array(), 60*60*1); 592 Cache::setCache(Cache::INSTANCE,'email','vacationNotice'.trim($account_id),array(), 60*60*24*1); 593 } 594 } 595 } 596 597 /** 598 * resets the various cache objects where connection error Objects may be cached 599 * 600 * @param int $_ImapServerId the profileID to look for 601 * @param int $account_id the egw account to look for 602 */ 603 static function resetConnectionErrorCache($_ImapServerId=null,$account_id=null) 604 { 605 //error_log(__METHOD__.' ('.__LINE__.') '.' for Profile:'.array2string($_ImapServerId) .' for user:'.trim($account_id)); 606 if (is_null($account_id)) $account_id = $GLOBALS['egw_info']['user']['account_id']; 607 if (is_array($_ImapServerId)) 608 { 609 // called via hook 610 $account_id = $_ImapServerId['account_id']; 611 unset($_ImapServerId); 612 $_ImapServerId = null; 613 } 614 if (is_null($_ImapServerId)) 615 { 616 $isConError = array(); 617 $waitOnFailure = array(); 618 } 619 else 620 { 621 $isConError = Cache::getCache(Cache::INSTANCE,'email','icServerSIEVE_connectionError'.trim($account_id)); 622 if (isset($isConError[$_ImapServerId])) 623 { 624 unset($isConError[$_ImapServerId]); 625 } 626 $waitOnFailure = Cache::getCache(Cache::INSTANCE,'email','ActiveSyncWaitOnFailure'.trim($account_id),null,array(),60*60*2); 627 if (isset($waitOnFailure[$_ImapServerId])) 628 { 629 unset($waitOnFailure[$_ImapServerId]); 630 } 631 } 632 Cache::setCache(Cache::INSTANCE,'email','icServerSIEVE_connectionError'.trim($account_id),$isConError,60*15); 633 Cache::setCache(Cache::INSTANCE,'email','ActiveSyncWaitOnFailure'.trim($account_id),$waitOnFailure,60*60*2); 634 } 635 636 /** 637 * resets the various cache objects where Folder Objects may be cached 638 * 639 * @param int $_ImapServerId the profileID to look for 640 * @param int $account_id the egw account to look for 641 */ 642 static function resetFolderObjectCache($_ImapServerId=null,$account_id=null) 643 { 644 //error_log(__METHOD__.' ('.__LINE__.') '.' called for Profile:'.array2string($_ImapServerId).'->'.function_backtrace()); 645 if (is_null($account_id)) $account_id = $GLOBALS['egw_info']['user']['account_id']; 646 // on [location] => verify_settings we coud either use [prefs] => Array([ActiveProfileID] => 9, .. as $_ImapServerId 647 // or treat it as not given. we try that path 648 if (is_null($_ImapServerId)||is_array($_ImapServerId)) 649 { 650 $folders2return = array(); 651 $folderInfo = array(); 652 $folderBasicInfo = array(); 653 $_specialUseFolders = array(); 654 } 655 else 656 { 657 $folders2return = Cache::getCache(Cache::INSTANCE,'email','folderObjects'.trim($account_id),null,array(),60*60*1); 658 if (!empty($folders2return) && isset($folders2return[$_ImapServerId])) 659 { 660 unset($folders2return[$_ImapServerId]); 661 } 662 $folderInfo = Cache::getCache(Cache::INSTANCE,'email','icServerFolderExistsInfo'.trim($account_id),null,array(),60*60*5); 663 if (!empty($folderInfo) && isset($folderInfo[$_ImapServerId])) 664 { 665 unset($folderInfo[$_ImapServerId]); 666 } 667 /* 668 $lastFolderUsedForMove = Cache::getCache(Cache::INSTANCE,'email','lastFolderUsedForMove'.trim($account_id),null,array(),$expiration=60*60*1); 669 if (isset($lastFolderUsedForMove[$_ImapServerId])) 670 { 671 unset($lastFolderUsedForMove[$_ImapServerId]); 672 } 673 */ 674 $folderBasicInfo = Cache::getCache(Cache::INSTANCE,'email','folderBasicInfo'.trim($account_id),null,array(),60*60*1); 675 if (!empty($folderBasicInfo) && isset($folderBasicInfo[$_ImapServerId])) 676 { 677 unset($folderBasicInfo[$_ImapServerId]); 678 } 679 $_specialUseFolders = Cache::getCache(Cache::INSTANCE,'email','specialUseFolders'.trim($account_id),null,array(),60*60*12); 680 if (!empty($_specialUseFolders) && isset($_specialUseFolders[$_ImapServerId])) 681 { 682 unset($_specialUseFolders[$_ImapServerId]); 683 self::$specialUseFolders=null; 684 } 685 } 686 Cache::setCache(Cache::INSTANCE,'email','folderObjects'.trim($account_id),$folders2return, 60*60*1); 687 Cache::setCache(Cache::INSTANCE,'email','icServerFolderExistsInfo'.trim($account_id),$folderInfo,60*60*5); 688 //Cache::setCache(Cache::INSTANCE,'email','lastFolderUsedForMove'.trim($account_id),$lastFolderUsedForMove,$expiration=60*60*1); 689 Cache::setCache(Cache::INSTANCE,'email','folderBasicInfo'.trim($account_id),$folderBasicInfo,60*60*1); 690 Cache::setCache(Cache::INSTANCE,'email','specialUseFolders'.trim($account_id),$_specialUseFolders,60*60*12); 691 } 692 693 /** 694 * checks if the imap server supports a given capability 695 * 696 * @param string $_capability the name of the capability to check for 697 * @return bool 698 */ 699 function hasCapability($_capability) 700 { 701 $rv = $this->icServer->hasCapability(strtoupper($_capability)); 702 //error_log(__METHOD__.' ('.__LINE__.') '." $_capability:".array2string($rv)); 703 return $rv; 704 } 705 706 /** 707 * getUserEMailAddresses - function to gather the emailadresses connected to the current mail-account 708 * @param string $_profileID the ID of the mailaccount to check for identities, if null current mail-account is used 709 * @return array - array(email=>realname) 710 */ 711 function getUserEMailAddresses($_profileID=null) 712 { 713 $acc = Mail\Account::read((!empty($_profileID)?$_profileID:$this->profileID)); 714 //error_log(__METHOD__.' ('.__LINE__.') '.':'.array2string($acc)); 715 $identities = Mail\Account::identities($acc); 716 717 $userEMailAdresses = array($acc['ident_email']=>$acc['ident_realname']); 718 719 foreach($identities as $ik => $ident) { 720 //error_log(__METHOD__.' ('.__LINE__.') '.':'.$ik.'->'.array2string($ident)); 721 $identity = Mail\Account::read_identity($ik); 722 if (!empty($identity['ident_email']) && !isset($userEMailAdresses[$identity['ident_email']])) $userEMailAdresses[$identity['ident_email']] = $identity['ident_realname']; 723 } 724 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($userEMailAdresses)); 725 return $userEMailAdresses; 726 } 727 728 /** 729 * getAllIdentities - function to gather the identities connected to the current user 730 * @param string/int $_accountToSearch = null if set search accounts for user specified 731 * @param boolean $resolve_placeholders wether or not resolve possible placeholders in identities 732 * @return array - array(email=>realname) 733 */ 734 static function getAllIdentities($_accountToSearch=null,$resolve_placeholders=false) 735 { 736 $userEMailAdresses = array(); 737 foreach(Mail\Account::search($only_current_user=($_accountToSearch?$_accountToSearch:true), $just_name=true) as $acc_id => $identity_name) 738 { 739 $acc = Mail\Account::read($acc_id,($_accountToSearch?$_accountToSearch:null)); 740 if (!$resolve_placeholders) $userEMailAdresses[$acc['ident_id']] = array('acc_id'=>$acc_id,'ident_id'=>$acc['ident_id'],'ident_email'=>$acc['ident_email'],'ident_org'=>$acc['ident_org'],'ident_realname'=>$acc['ident_realname'],'ident_signature'=>$acc['ident_signature'],'ident_name'=>$acc['ident_name']); 741 742 foreach(Mail\Account::identities($acc) as $ik => $ident) { 743 //error_log(__METHOD__.' ('.__LINE__.') '.':'.$ik.'->'.array2string($ident)); 744 $identity = Mail\Account::read_identity($ik,$resolve_placeholders); 745 //error_log(__METHOD__.' ('.__LINE__.') '.':'.$ik.'->'.array2string($identity)); 746 if (!isset($userEMailAdresses[$identity['ident_id']])) $userEMailAdresses[$identity['ident_id']] = array('acc_id'=>$acc_id,'ident_id'=>$identity['ident_id'],'ident_email'=>$identity['ident_email'],'ident_org'=>$identity['ident_org'],'ident_realname'=>$identity['ident_realname'],'ident_signature'=>$identity['ident_signature'],'ident_name'=>$identity['ident_name']); 747 } 748 } 749 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($userEMailAdresses)); 750 return $userEMailAdresses; 751 } 752 753 /** 754 * Get all identities of given mailaccount 755 * 756 * @param int|Mail\Account $account account-object or acc_id 757 * @return array - array(email=>realname) 758 */ 759 function getAccountIdentities($account) 760 { 761 if (!$account instanceof Mail\Account) 762 { 763 $account = Mail\Account::read($account); 764 } 765 $userEMailAdresses = array(); 766 foreach(Mail\Account::identities($account, true, 'params') as $ik => $ident) { 767 //error_log(__METHOD__.' ('.__LINE__.') '.':'.$ik.'->'.array2string($ident)); 768 $identity = Mail\Account::read_identity($ik,true,null,$account); 769 //error_log(__METHOD__.' ('.__LINE__.') '.':'.$ik.'->'.array2string($identity)); 770 // standardIdentity has ident_id==acc_id (as it is done within account->identities) 771 if (empty($identity['ident_id'])) $identity['ident_id'] = $identity['acc_id']; 772 if (!isset($userEMailAdresses[$identity['ident_id']])) 773 { 774 $userEMailAdresses[$identity['ident_id']] = array('acc_id'=>$identity['acc_id'], 775 'ident_id'=>$identity['ident_id'], 776 'ident_email'=>$identity['ident_email'], 777 'ident_org'=>$identity['ident_org'], 778 'ident_realname'=>$identity['ident_realname'], 779 'ident_signature'=>$identity['ident_signature'], 780 'ident_name'=>$identity['ident_name']); 781 } 782 } 783 784 return $userEMailAdresses; 785 } 786 787 /** 788 * Function to gather the default identitiy connected to the current mailaccount 789 * 790 * @return int - id of the identity 791 */ 792 function getDefaultIdentity() 793 { 794 // retrieve the signature accociated with the identity 795 $_accountData=array(); 796 $id = $this->getIdentitiesWithAccounts($_accountData); 797 foreach(Mail\Account::identities($_accountData[$this->profileID] ? 798 $this->profileID : $_accountData[$id],false,'ident_id') as $accountData) 799 { 800 return $accountData; 801 } 802 } 803 804 /** 805 * getIdentitiesWithAccounts 806 * 807 * @param array reference to pass all identities back 808 * @return int the default Identity (active) or 0 809 */ 810 function getIdentitiesWithAccounts(&$identities) 811 { 812 // account select box 813 $selectedID = $this->profileID; 814 $allAccountData = Mail\Account::search($only_current_user=true, false, null); 815 if ($allAccountData) { 816 $rememberFirst=$selectedFound=null; 817 foreach ($allAccountData as $tmpkey => $icServers) 818 { 819 if (is_null($rememberFirst)) $rememberFirst = $tmpkey; 820 if ($tmpkey == $selectedID) $selectedFound=true; 821 //error_log(__METHOD__.' ('.__LINE__.') '.' Key:'.$tmpkey.'->'.array2string($icServers->acc_imap_host)); 822 $host = $icServers->acc_imap_host; 823 if (empty($host)) continue; 824 $identities[$icServers->acc_id] = $icServers['ident_realname'].' '.$icServers['ident_org'].' <'.$icServers['ident_email'].'>'; 825 //error_log(__METHOD__.' ('.__LINE__.') '.' Key:'.$tmpkey.'->'.array2string($identities[$icServers->acc_id])); 826 } 827 } 828 return ($selectedFound?$selectedID:$rememberFirst); 829 } 830 831 /** 832 * construct the string representing an Identity passed by $identity 833 * 834 * @var array/object $identity, identity object that holds realname, organization, emailaddress and signatureid 835 * @var boolean $fullString full or false=NamePart only is returned 836 * @return string - constructed of identity object data as defined in mailConfig 837 */ 838 static function generateIdentityString($identity, $fullString=true) 839 { 840 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($identity)); 841 //if (is_null(self::$mailConfig)) self::$mailConfig = Config::read('mail'); 842 // not set? -> use default, means full display of all available data 843 //if (!isset(self::$mailConfig['how2displayIdentities'])) self::$mailConfig['how2displayIdentities']=''; 844 $how2displayIdentities = ''; 845 switch ($how2displayIdentities) 846 { 847 case 'email'; 848 //$retData = str_replace('@',' ',$identity->emailAddress).($fullString===true?' <'.$identity->emailAddress.'>':''); 849 $retData = $identity['ident_email'].($fullString===true?' <'.$identity['ident_email'].'>':''); 850 break; 851 case 'nameNemail'; 852 $retData = (!empty($identity['ident_realname'])?$identity['ident_realname']:substr_replace($identity['ident_email'],'',strpos($identity['ident_email'],'@'))).($fullString===true?' <'.$identity['ident_email'].'>':''); 853 break; 854 case 'orgNemail'; 855 $retData = (!empty($identity['ident_org'])?$identity['ident_org']:substr_replace($identity['ident_email'],'',0,strpos($identity['ident_email'],'@')+1)).($fullString===true?' <'.$identity['ident_email'].'>':''); 856 break; 857 default: 858 $retData = $identity['ident_realname'].(!empty($identity['ident_org'])?' '.$identity['ident_org']:'').($fullString===true?' <'.$identity['ident_email'].'>':''); 859 } 860 return $retData; 861 } 862 863 /** 864 * closes a connection on the active Server ($this->icServer) 865 * 866 * @return void 867 */ 868 function closeConnection() 869 { 870 //if ($icServer->_connected) error_log(__METHOD__.' ('.__LINE__.') '.' disconnect from Server'); 871 //error_log(__METHOD__."() ".function_backtrace()); 872 $this->icServer->disconnect(); 873 } 874 875 /** 876 * reopens a connection for the active Server ($this->icServer), and selects the folder given 877 * 878 * @param string $_foldername folder to open/select 879 * @return void 880 */ 881 function reopen($_foldername) 882 { 883 if (self::$debugTimes) $starttime = microtime (true); 884 885 //error_log(__METHOD__.' ('.__LINE__.') '."('$_foldername') ".function_backtrace()); 886 // TODO: trying to reduce traffic to the IMAP Server here, introduces problems with fetching the bodies of 887 // eMails when not in "current-Folder" (folder that is selected by UI) 888 static $folderOpened; 889 //if (empty($folderOpened) || $folderOpened!=$_foldername) 890 //{ 891 //error_log( __METHOD__.' ('.__LINE__.') '." $_foldername ".function_backtrace()); 892 //error_log(__METHOD__.' ('.__LINE__.') '.' Connected with icServer for Profile:'.$this->profileID.'?'.print_r($this->icServer->_connected,true)); 893 if ($this->folderIsSelectable($_foldername)) { 894 $this->icServer->openMailbox($_foldername); 895 } 896 $folderOpened = $_foldername; 897 //} 898 if (self::$debugTimes) self::logRunTimes($starttime,null,'Folder:'.$_foldername,__METHOD__.' ('.__LINE__.') '); 899 } 900 901 902 /** 903 * openConnection 904 * 905 * @param int $_icServerID = 0 906 * @throws Horde_Imap_Client_Exception on connection error or authentication failure 907 * @throws InvalidArgumentException on missing credentials 908 */ 909 function openConnection($_icServerID=0) 910 { 911 //error_log( "-------------------------->open connection ".function_backtrace()); 912 //error_log(__METHOD__.' ('.__LINE__.') '.' ->'.array2string($this->icServer)); 913 if (self::$debugTimes) $starttime = microtime (true); 914 $mailbox=null; 915 try 916 { 917 if(isset($this->sessionData['mailbox'])&&$this->folderExists($this->sessionData['mailbox'])) $mailbox = $this->sessionData['mailbox']; 918 if (empty($mailbox)) $mailbox = $this->icServer->getCurrentMailbox(); 919/* 920 if (isset(Mail\Imap::$supports_keywords[$_icServerID])) 921 { 922 $this->icServer->openMailbox($mailbox); 923 } 924 else 925 { 926 $this->icServer->examineMailbox($mailbox); 927 } 928*/ 929 // the above should detect if there is a known information about supporting KEYWORDS 930 // but does not work as expected :-( 931 $this->icServer->examineMailbox($mailbox); 932 //error_log(__METHOD__." using existing Connection ProfileID:".$_icServerID.' Status:'.print_r($this->icServer->_connected,true)); 933 //error_log(__METHOD__.' ('.__LINE__.') '."->open connection for Server with profileID:".$_icServerID.function_backtrace()); 934 935 //make sure we are working with the correct hierarchyDelimiter on the current connection, calling getHierarchyDelimiter with false to reset the cache 936 $this->getHierarchyDelimiter(false); 937 self::$specialUseFolders = $this->getSpecialUseFolders(); 938 } 939 catch (\Exception $e) 940 { 941 error_log(__METHOD__.' ('.__LINE__.') '."->open connection for Server with profileID:".$_icServerID." trying to examine ($mailbox) failed!".$e->getMessage()); 942 throw new Exception(__METHOD__." failed to ".__METHOD__." on Profile to $_icServerID while trying to examine $mailbox:".$e->getMessage()); 943 } 944 if (self::$debugTimes) self::logRunTimes($starttime,null,'ProfileID:'.$_icServerID,__METHOD__.' ('.__LINE__.') '); 945 } 946 947 /** 948 * getQuotaRoot 949 * return the qouta of the users INBOX 950 * 951 * @return mixed array/boolean 952 */ 953 function getQuotaRoot() 954 { 955 static $quota; 956 if (isset($quota)) return $quota; 957 if (isset(self::$profileDefunct[$this->profileID]) && strlen(self::$profileDefunct[$this->profileID])) 958 { 959 // something is wrong. Do not proceed. either no folder or profile is marked as defunct for this request 960 return false; 961 } 962 try 963 { 964 $this->icServer->getCurrentMailbox(); 965 if(!$this->icServer->hasCapability('QUOTA')) { 966 $quota = false; 967 return false; 968 } 969 $quota = $this->icServer->getStorageQuotaRoot('INBOX'); 970 } 971 catch (Exception $e) 972 { 973 //error_log(__METHOD__.array2string($e)); 974 //error_log(__METHOD__." failed to fetch quota on ".$this->profileID.' Reason:'.$e->getMessage().($e->details?', '.$e->details:'')/*.function_backtrace()*/); 975 if ($e->getCode()==102) 976 { 977 self::$profileDefunct[$this->profileID]=$e->getMessage().($e->details?', '.$e->details:''); 978 Cache::setCache(Cache::INSTANCE,'email','profileDefunct'.trim($GLOBALS['egw_info']['user']['account_id']),self::$profileDefunct, $expiration=5*1); 979 throw new Exception(__METHOD__." failed to fetch quota on ".$this->profileID.' Reason:'.$e->getMessage().($e->details?', '.$e->details:'')); 980 } 981 } 982 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($quota)); 983 if(is_array($quota)) { 984 $quota = array( 985 'usage' => $quota['USED'], 986 'limit' => $quota['QMAX'], 987 ); 988 } else { 989 $quota = false; 990 } 991 return $quota; 992 } 993 994 /** 995 * getTimeOut 996 * 997 * @param string _use decide if the use is for IMAP or SIEVE, by now only the default differs 998 * 999 * @return int - timeout (either set or default 20/10) 1000 */ 1001 static function getTimeOut($_use='IMAP') 1002 { 1003 $timeout = $GLOBALS['egw_info']['user']['preferences']['mail']['connectionTimeout']; 1004 if (empty($timeout)) $timeout = ($_use=='SIEVE'?10:20); // this is the default value 1005 return $timeout; 1006 } 1007 1008 /** 1009 * Fetch the namespace from icServer 1010 * 1011 * An IMAPServer may present several namespaces under each key: 1012 * so we return an array of namespacearrays for our needs 1013 * 1014 * @return array array(prefix_present=>mixed (bool/string) ,prefix=>string,delimiter=>string,type=>string (personal|others|shared)) 1015 */ 1016 function _getNameSpaces() 1017 { 1018 static $nameSpace = null; 1019 $foldersNameSpace = array(); 1020 $delimiter = $this->getHierarchyDelimiter(); 1021 // TODO: cache by $this->icServer->ImapServerId 1022 if (is_null($nameSpace)) $nameSpace = $this->icServer->getNameSpaceArray(); 1023 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($nameSpace)); 1024 if (is_array($nameSpace)) { 1025 foreach($nameSpace as $type => $singleNameSpaceArray) 1026 { 1027 foreach ($singleNameSpaceArray as $singleNameSpace) 1028 { 1029 $_foldersNameSpace = array(); 1030 if($type == 'personal' && $singleNameSpace['name'] == '#mh/' && ($this->folderExists('Mail')||$this->folderExists('INBOX'))) 1031 { 1032 $_foldersNameSpace['prefix_present'] = 'forced'; 1033 // uw-imap server with mailbox prefix or dovecot maybe 1034 $_foldersNameSpace['prefix'] = ($this->folderExists('Mail')?'Mail':(!empty($singleNameSpace['name'])?$singleNameSpace['name']:'')); 1035 } 1036 elseif($type == 'personal' && ($singleNameSpace['name'] == '#mh/') && $this->folderExists('mail')) 1037 { 1038 $_foldersNameSpace['prefix_present'] = 'forced'; 1039 // uw-imap server with mailbox prefix or dovecot maybe 1040 $_foldersNameSpace['prefix'] = 'mail'; 1041 } else { 1042 $_foldersNameSpace['prefix_present'] = !empty($singleNameSpace['name']); 1043 $_foldersNameSpace['prefix'] = $singleNameSpace['name']; 1044 } 1045 $_foldersNameSpace['delimiter'] = ($singleNameSpace['delimiter']?$singleNameSpace['delimiter']:$delimiter); 1046 $_foldersNameSpace['type'] = $type; 1047 $foldersNameSpace[] =$_foldersNameSpace; 1048 } 1049 //echo "############## $type->".print_r($foldersNameSpace[$type],true)." ###################<br>"; 1050 } 1051 } 1052 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($foldersNameSpace)); 1053 return $foldersNameSpace; 1054 } 1055 1056 /** 1057 * Wrapper to extract the folder prefix from folder compared to given namespace array 1058 * 1059 * @param array $nameSpace 1060 * @paam string $_folderName 1061 * @return string the prefix (may be an empty string) 1062 */ 1063 function getFolderPrefixFromNamespace($nameSpace, $folderName) 1064 { 1065 foreach($nameSpace as &$singleNameSpace) 1066 { 1067 //if (substr($singleNameSpace['prefix'],0,strlen($folderName))==$folderName) return $singleNameSpace['prefix']; 1068 if (substr($folderName,0,strlen($singleNameSpace['prefix']))==$singleNameSpace['prefix']) return $singleNameSpace['prefix']; 1069 } 1070 return ""; 1071 } 1072 1073 /** 1074 * getHierarchyDelimiter 1075 * 1076 * @var boolean $_useCache 1077 * @return string the hierarchyDelimiter 1078 */ 1079 function getHierarchyDelimiter($_useCache=true) 1080 { 1081 static $HierarchyDelimiter = null; 1082 if (is_null($HierarchyDelimiter)) $HierarchyDelimiter = Cache::getCache(Cache::INSTANCE,'email','HierarchyDelimiter'.trim($GLOBALS['egw_info']['user']['account_id']),null,array(),60*60*24*5); 1083 if ($_useCache===false) unset($HierarchyDelimiter[$this->icServer->ImapServerId]); 1084 if (isset($HierarchyDelimiter[$this->icServer->ImapServerId])&&!empty($HierarchyDelimiter[$this->icServer->ImapServerId])) 1085 { 1086 return $HierarchyDelimiter[$this->icServer->ImapServerId]; 1087 } 1088 $HierarchyDelimiter[$this->icServer->ImapServerId] = '/'; 1089 try 1090 { 1091 $this->icServer->getCurrentMailbox(); 1092 $HierarchyDelimiter[$this->icServer->ImapServerId] = $this->icServer->getDelimiter(); 1093 } 1094 catch(\Exception $e) 1095 { 1096 if ($e->getCode()==102) 1097 { 1098 self::$profileDefunct[$this->profileID]=$e->getMessage().($e->details?', '.$e->details:''); 1099 Cache::setCache(Cache::INSTANCE,'email','profileDefunct'.trim($GLOBALS['egw_info']['user']['account_id']),self::$profileDefunct, $expiration=5*1); 1100 } 1101 unset($e); 1102 $HierarchyDelimiter[$this->icServer->ImapServerId] = '/'; 1103 } 1104 Cache::setCache(Cache::INSTANCE,'email','HierarchyDelimiter'.trim($GLOBALS['egw_info']['user']['account_id']),$HierarchyDelimiter, 60*60*24*5); 1105 return $HierarchyDelimiter[$this->icServer->ImapServerId]; 1106 } 1107 1108 /** 1109 * getSpecialUseFolders 1110 * @ToDo: could as well be static, when icServer is passed 1111 * @return mixed null/array 1112 */ 1113 function getSpecialUseFolders() 1114 { 1115 //error_log(__METHOD__.' ('.__LINE__.') '.':'.$this->icServer->ImapServerId.' Connected:'.$this->icServer->_connected); 1116 static $_specialUseFolders = null; 1117 if (is_null($_specialUseFolders)||empty($_specialUseFolders)) $_specialUseFolders = Cache::getCache(Cache::INSTANCE,'email','specialUseFolders'.trim($GLOBALS['egw_info']['user']['account_id']),null,array(),60*60*24*5); 1118 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($this->icServer->acc_folder_trash)); 1119 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($this->icServer->acc_folder_sent)); 1120 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($this->icServer->acc_folder_draft)); 1121 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($this->icServer->acc_folder_template)); 1122 self::$specialUseFolders = $_specialUseFolders[$this->icServer->ImapServerId]; 1123 if (isset($_specialUseFolders[$this->icServer->ImapServerId]) && !empty($_specialUseFolders[$this->icServer->ImapServerId])) 1124 return $_specialUseFolders[$this->icServer->ImapServerId]; 1125 $_specialUseFolders[$this->icServer->ImapServerId]=array(); 1126 //if (!empty($this->icServer->acc_folder_trash) && !isset($_specialUseFolders[$this->icServer->ImapServerId][$this->icServer->acc_folder_trash])) 1127 $_specialUseFolders[$this->icServer->ImapServerId][$this->icServer->acc_folder_trash]='Trash'; 1128 //if (!empty($this->icServer->acc_folder_draft) && !isset($_specialUseFolders[$this->icServer->ImapServerId][$this->icServer->acc_folder_draft])) 1129 $_specialUseFolders[$this->icServer->ImapServerId][$this->icServer->acc_folder_draft]='Drafts'; 1130 //if (!empty($this->icServer->acc_folder_sent) && !isset($_specialUseFolders[$this->icServer->ImapServerId][$this->icServer->acc_folder_sent])) 1131 $_specialUseFolders[$this->icServer->ImapServerId][$this->icServer->acc_folder_sent]='Sent'; 1132 //if (!empty($this->icServer->acc_folder_template) && !isset($_specialUseFolders[$this->icServer->ImapServerId][$this->icServer->acc_folder_template])) 1133 $_specialUseFolders[$this->icServer->ImapServerId][$this->icServer->acc_folder_template]='Templates'; 1134 $_specialUseFolders[$this->icServer->ImapServerId][$this->icServer->acc_folder_junk]='Junk'; 1135 $_specialUseFolders[$this->icServer->ImapServerId][$this->icServer->acc_folder_archive]='Archive'; 1136 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($_specialUseFolders));//.'<->'.array2string($this->icServer)); 1137 self::$specialUseFolders = $_specialUseFolders[$this->icServer->ImapServerId]; 1138 Cache::setCache(Cache::INSTANCE,'email','specialUseFolders'.trim($GLOBALS['egw_info']['user']['account_id']),$_specialUseFolders, 60*60*24*5); 1139 return $_specialUseFolders[$this->icServer->ImapServerId]; 1140 } 1141 1142 /** 1143 * get IMAP folder status regarding NoSelect 1144 * 1145 * @param foldertoselect string the foldername 1146 * 1147 * @return boolean true or false regarding the noselect attribute 1148 */ 1149 function folderIsSelectable($folderToSelect) 1150 { 1151 $retval = true; 1152 if($folderToSelect && ($folderStatus = $this->getFolderStatus($folderToSelect,false,true))) { 1153 if (!empty($folderStatus['attributes']) && stripos(array2string($folderStatus['attributes']),'noselect')!==false) 1154 { 1155 $retval = false; 1156 } 1157 } 1158 return $retval; 1159 } 1160 1161 /** 1162 * get IMAP folder status, wrapper to store results within a single request 1163 * 1164 * returns an array information about the imap folder 1165 * 1166 * @param folderName string the foldername 1167 * @param ignoreStatusCache bool ignore the cache used for counters 1168 * 1169 * @return array 1170 * 1171 * @throws Exception 1172 */ 1173 function _getStatus($folderName,$ignoreStatusCache=false) 1174 { 1175 static $folderStatus = null; 1176 if (!$ignoreStatusCache && isset($folderStatus[$this->icServer->ImapServerId][$folderName])) 1177 { 1178 //error_log(__METHOD__.' ('.__LINE__.') '.' Using cache for status on Server:'.$this->icServer->ImapServerId.' for folder:'.$folderName.'->'.array2string($folderStatus[$this->icServer->ImapServerId][$folderName])); 1179 return $folderStatus[$this->icServer->ImapServerId][$folderName]; 1180 } 1181 try 1182 { 1183 $folderStatus[$this->icServer->ImapServerId][$folderName] = $this->icServer->getStatus($folderName,$ignoreStatusCache); 1184 } 1185 catch (\Exception $e) 1186 { 1187 throw new Exception(__METHOD__.' ('.__LINE__.') '." failed for $folderName with error:".$e->getMessage().($e->details?', '.$e->details:'')); 1188 } 1189 return $folderStatus[$this->icServer->ImapServerId][$folderName]; 1190 } 1191 1192 /** 1193 * get IMAP folder status 1194 * 1195 * returns an array information about the imap folder, may be used as wrapper to retrieve results from cache 1196 * 1197 * @param _folderName string the foldername 1198 * @param ignoreStatusCache bool ignore the cache used for counters 1199 * @param basicInfoOnly bool retrieve only names and stuff returned by getMailboxes 1200 * @param fetchSubscribedInfo bool fetch Subscribed Info on folder 1201 * @return array|false 1202 */ 1203 function getFolderStatus($_folderName,$ignoreStatusCache=false,$basicInfoOnly=false,$fetchSubscribedInfo=true) 1204 { 1205 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '." called with:$_folderName,$ignoreStatusCache,$basicInfoOnly"); 1206 if (!is_string($_folderName) || empty($_folderName)||(isset(self::$profileDefunct[$this->profileID]) && strlen(self::$profileDefunct[$this->profileID]))) 1207 { 1208 // something is wrong. Do not proceed. either no folder or profile is marked as defunct for this request 1209 return false; 1210 } 1211 static $folderInfoCache = null; // reduce traffic on single request 1212 static $folderBasicInfo = null; 1213 if (isset($folderBasicInfo[$this->profileID])) 1214 { 1215 $folderInfoCache = $folderBasicInfo[$this->profileID]; 1216 } 1217 if (isset($folderInfoCache[$_folderName]) && $ignoreStatusCache==false && $basicInfoOnly) return $folderInfoCache[$_folderName]; 1218 $retValue = array(); 1219 1220 //error_log(__METHOD__.' ('.__LINE__.') '.$_folderName.' '.array2string(array_keys($folderInfoCache))); 1221 // does the folder exist??? 1222 if (is_null($folderInfoCache) || !isset($folderInfoCache[$_folderName])) 1223 { 1224 try 1225 { 1226 $ret = $this->icServer->getMailboxes($_folderName, 1, true); 1227 } 1228 catch (\Exception $e) 1229 { 1230 //error_log(__METHOD__.array2string($e)); 1231 //error_log(__METHOD__." failed to fetch Mailbox $_folderName on ".$this->profileID.' Reason:'.$e->getMessage().($e->details?', '.$e->details:'')/*.function_backtrace()*/); 1232 self::$profileDefunct[$this->profileID]=$e->getMessage().($e->details?', '.$e->details:''); 1233 Cache::setCache(Cache::INSTANCE,'email','profileDefunct'.trim($GLOBALS['egw_info']['user']['account_id']),self::$profileDefunct, $expiration=5*1); 1234 throw new Exception(__METHOD__." failed to fetch Mailbox $_folderName on ".$this->profileID.' Reason:'.$e->getMessage().($e->details?', '.$e->details:'')); 1235 } 1236 //error_log(__METHOD__.' ('.__LINE__.') '.$_folderName.' '.array2string($ret)); 1237 if (is_array($ret)) 1238 { 1239 $retkeys = array_keys($ret); 1240 if ($retkeys[0]==$_folderName) $folderInfoCache[$_folderName] = $ret[$retkeys[0]]; 1241 } 1242 else 1243 { 1244 $folderInfoCache[$_folderName]=false; 1245 } 1246 } 1247 $folderInfo = $folderInfoCache[$_folderName]; 1248 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($folderInfo).'->'.function_backtrace()); 1249 if($ignoreStatusCache||!$folderInfo|| !is_array($folderInfo)) { 1250 try 1251 { 1252 $folderInfo = $this->_getStatus($_folderName,$ignoreStatusCache); 1253 } 1254 catch (\Exception $e) 1255 { 1256 //error_log(__METHOD__.array2string($e)); 1257 error_log(__METHOD__." failed to fetch status for $_folderName on ".$this->profileID.' Reason:'.$e->getMessage().($e->details?', '.$e->details:'')/*.function_backtrace()*/); 1258 self::$profileDefunct[$this->profileID]=$e->getMessage().($e->details?', '.$e->details:''); 1259 Cache::setCache(Cache::INSTANCE,'email','profileDefunct'.trim($GLOBALS['egw_info']['user']['account_id']),self::$profileDefunct, $expiration=5*1); 1260 //throw new Exception(__METHOD__." failed to fetch status for $_folderName on ".$this->profileID.' Reason:'.$e->getMessage().($e->details?', '.$e->details:'')); 1261 $folderInfo=null; 1262 } 1263 if (!is_array($folderInfo)) 1264 { 1265 return false; 1266 } 1267 } 1268 #if(!is_array($folderInfo)) { 1269 # return false; 1270 #} 1271 $retValue['delimiter'] = (isset($folderInfo['HIERACHY_DELIMITER']) && $folderInfo['HIERACHY_DELIMITER']?$folderInfo['HIERACHY_DELIMITER']:$folderInfo['delimiter']); 1272 $retValue['attributes'] = (isset($folderInfo['ATTRIBUTES']) && $folderInfo['ATTRIBUTES']?$folderInfo['ATTRIBUTES']:$folderInfo['attributes']); 1273 $retValue['subscribed'] = $folderInfo['SUBSCRIBED'] ?? $folderInfo['subscribed'] ?? false; 1274 $shortNameParts = explode($retValue['delimiter'], $_folderName); 1275 $retValue['shortName'] = array_pop($shortNameParts); 1276 $retValue['displayName'] = $_folderName; 1277 $retValue['shortDisplayName'] = $retValue['shortName']; 1278 if(strtoupper($retValue['shortName']) == 'INBOX') { 1279 $retValue['displayName'] = lang('INBOX'); 1280 $retValue['shortDisplayName'] = lang('INBOX'); 1281 } 1282 // translate the automatic Folders (Sent, Drafts, ...) like the INBOX 1283 elseif (in_array($retValue['shortName'],self::$autoFolders)) 1284 { 1285 $retValue['displayName'] = $retValue['shortDisplayName'] = lang($retValue['shortName']); 1286 } 1287 if ($folderInfo) $folderBasicInfo[$this->profileID][$_folderName]=$retValue; 1288 //error_log(__METHOD__.' ('.__LINE__.') '.' '.$_folderName.array2string($retValue['attributes'])); 1289 if ($basicInfoOnly || (isset($retValue['attributes']) && stripos(array2string($retValue['attributes']),'noselect')!==false)) 1290 { 1291 return $retValue; 1292 } 1293 // fetch all in one go for one request, instead of querying them one by one 1294 // cache it for a minute 60*60*1 1295 // this should reduce communication to the imap server 1296 static $subscribedFolders = null; 1297 static $nameSpace = null; 1298 static $prefix = null; 1299 if (is_null($nameSpace) || empty($nameSpace[$this->profileID])) $nameSpace[$this->profileID] = $this->_getNameSpaces(); 1300 if (!empty($nameSpace[$this->profileID])) 1301 { 1302 $nsNoPersonal=array(); 1303 foreach($nameSpace[$this->profileID] as &$ns) 1304 { 1305 if ($ns['type']!='personal') $nsNoPersonal[]=$ns; 1306 } 1307 $nameSpace[$this->profileID]=$nsNoPersonal; 1308 } 1309 if (is_null($prefix) || empty($prefix[$this->profileID]) || empty($prefix[$this->profileID][$_folderName])) $prefix[$this->profileID][$_folderName] = $this->getFolderPrefixFromNamespace($nameSpace[$this->profileID], $_folderName); 1310 1311 if ($fetchSubscribedInfo && is_null($subscribedFolders) || empty($subscribedFolders[$this->profileID])) 1312 { 1313 $subscribedFolders[$this->profileID] = $this->icServer->listSubscribedMailboxes(); 1314 } 1315 1316 if($fetchSubscribedInfo && is_array($subscribedFolders[$this->profileID]) && in_array($_folderName,$subscribedFolders[$this->profileID])) { 1317 $retValue['subscribed'] = true; 1318 } 1319 1320 try 1321 { 1322 //$folderStatus = $this->_getStatus($_folderName,$ignoreStatusCache); 1323 $folderStatus = $this->getMailBoxCounters($_folderName,false); 1324 $retValue['messages'] = $folderStatus['MESSAGES']; 1325 $retValue['recent'] = $folderStatus['RECENT']; 1326 $retValue['uidnext'] = $folderStatus['UIDNEXT']; 1327 $retValue['uidvalidity'] = $folderStatus['UIDVALIDITY']; 1328 $retValue['unseen'] = $folderStatus['UNSEEN']; 1329 if (//$retValue['unseen']==0 && 1330 (isset($this->mailPreferences['trustServersUnseenInfo']) && // some servers dont serve the UNSEEN information 1331 $this->mailPreferences['trustServersUnseenInfo']==false) || 1332 (isset($this->mailPreferences['trustServersUnseenInfo']) && 1333 $this->mailPreferences['trustServersUnseenInfo']==2 && 1334 $prefix[$this->profileID][$_folderName] != '' && stripos($_folderName,$prefix[$this->profileID][$_folderName]) !== false) 1335 ) 1336 { 1337 //error_log(__METHOD__." returned folderStatus for Folder $_folderName:".print_r($prefix,true).' TS:'.$this->mailPreferences['trustServersUnseenInfo']); 1338 // we filter for the combined status of unseen and undeleted, as this is what we show in list 1339 try 1340 { 1341 $byUid=true; 1342 $_reverse=1; 1343 $sortResult = $this->getSortedList($_folderName, $_sort=0, $_reverse, array('status'=>array('UNSEEN','UNDELETED')),$byUid,false); 1344 $retValue['unseen'] = $sortResult['count']; 1345 } 1346 catch (\Exception $ee) 1347 { 1348 if (self::$debug) error_log(__METHOD__." could not fetch/calculate unseen counter for $_folderName Reason:'".$ee->getMessage()."' but requested."); 1349 } 1350 } 1351 } 1352 catch (\Exception $e) 1353 { 1354 if (self::$debug) error_log(__METHOD__." returned folderStatus for Folder $_folderName:".print_r($e->getMessage(),true)); 1355 } 1356 1357 return $retValue; 1358 } 1359 1360 /** 1361 * Convert Horde_Mime_Headers object to an associative array like Horde_Mime_Array::toArray() 1362 * 1363 * Catches Horde_Idna_Exception and returns raw header instead eg. for invalid domains like "test@-domain.com". 1364 * 1365 * @param Horde_Mime_Headers $headers 1366 * @return array 1367 */ 1368 protected static function headers2array(Horde_Mime_Headers $headers) 1369 { 1370 try { 1371 $arr = $headers->toArray(); 1372 } 1373 catch(\Horde_Idna_Exception $e) { 1374 $arr = array(); 1375 foreach($headers as $header) 1376 { 1377 try { 1378 $val = $header->sendEncode(); 1379 } catch (\Horde_Idna_Exception $e) { 1380 $val = (array)$header->value; 1381 } 1382 $arr[$header->name] = count($val) == 1 ? reset($val) : $val; 1383 } 1384 } 1385 return $arr; 1386 } 1387 1388 /** 1389 * getHeaders 1390 * 1391 * this function is a wrapper function for getSortedList and populates the resultList thereof with headerdata 1392 * 1393 * @param string $_folderName 1394 * @param int $_startMessage 1395 * @param int $_numberOfMessages number of messages to return 1396 * @param array $_sort sort by criteria 1397 * @param boolean $_reverse reverse sorting of the result array (may be switched, as it is passed to getSortedList by reference) 1398 * @param array $_filter filter to apply to getSortedList 1399 * @param mixed $_thisUIDOnly = null, if given fetch the headers of this uid only (either one, or array of uids) 1400 * @param boolean $_cacheResult = true try touse the cache of getSortedList 1401 * @param mixed $_fetchPreviews = false (boolean/int) fetch part of the body of the messages requested (if integer the number is assumed to be the number of chars to be returned for preview; if set to true 300 chars are returned (when available)) 1402 * @return array result as array(header=>array,total=>int,first=>int,last=>int) 1403 */ 1404 function getHeaders($_folderName, $_startMessage, $_numberOfMessages, $_sort, $_reverse, $_filter, $_thisUIDOnly=null, $_cacheResult=true, $_fetchPreviews=false) 1405 { 1406 //self::$debug=true; 1407 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.function_backtrace()); 1408 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '."$_folderName,$_startMessage, $_numberOfMessages, $_sort, $_reverse, ".array2string($_filter).", $_thisUIDOnly"); 1409 $reverse = (bool)$_reverse; 1410 // get the list of messages to fetch 1411 $this->reopen($_folderName); 1412 //$currentFolder = $this->icServer->getCurrentMailbox(); 1413 //if ($currentFolder != $_folderName); $this->icServer->openMailbox($_folderName); 1414 $rByUid = true; // try searching by uid. this var will be passed by reference to getSortedList, and may be set to false, if UID retrieval fails 1415 #print "<pre>"; 1416 #$this->icServer->setDebug(true); 1417 $total=0; 1418 if ($_thisUIDOnly === null) 1419 { 1420 if (($_startMessage || $_numberOfMessages) && !isset($_filter['range'])) 1421 { 1422 // this will not work we must calculate the range we want to retieve as e.g.: 0:20 retirieves the first 20 mails and sorts them 1423 // if sort capability is applied to the range fetched, not sort first and fetch the range afterwards 1424 //$start = $_startMessage-1; 1425 //$end = $_startMessage-1+$_numberOfMessages; 1426 //$_filter['range'] ="$start:$end"; 1427 //$_filter['range'] ="$_startMessage:*"; 1428 } 1429 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '."$_folderName, $_sort, $reverse, ".array2string($_filter).", $rByUid"); 1430 if (self::$debug||self::$debugTimes) $starttime = microtime (true); 1431 //see this example below for a 12 week datefilter (since) 1432 //$_filter = array('status'=>array('UNDELETED'),'type'=>"SINCE",'string'=> date("d-M-Y", $starttime-(3600*24*7*12))); 1433 $_sortResult = $this->getSortedList($_folderName, $_sort, $reverse, $_filter, $rByUid, $_cacheResult); 1434 $sortResult = $_sortResult['match']->ids; 1435 //$modseq = $_sortResult['modseq']; 1436 //error_log(__METHOD__.' ('.__LINE__.') '.'Modsequence:'.$modseq); 1437 if (self::$debug||self::$debugTimes) self::logRunTimes($starttime,null,' call getSortedList for Folder:'.$_folderName.' Filter:'.array2string($_filter).' Ids:'.array2string($_thisUIDOnly),__METHOD__.' ('.__LINE__.') '); 1438 1439 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.array2string($sortResult)); 1440 #$this->icServer->setDebug(false); 1441 #print "</pre>"; 1442 // nothing found 1443 if(!is_array($sortResult) || empty($sortResult)) { 1444 $retValue = array(); 1445 $retValue['info']['total'] = 0; 1446 $retValue['info']['first'] = 0; 1447 $retValue['info']['last'] = 0; 1448 return $retValue; 1449 } 1450 1451 $total = $_sortResult['count']; 1452 #_debug_array($sortResult); 1453 #_debug_array(array_slice($sortResult, -5, -2)); 1454 //error_log("REVERSE: $reverse"); 1455 if($reverse === true) { 1456 if ($_startMessage<=$total) 1457 { 1458 $startMessage = $_startMessage-1; 1459 } 1460 else 1461 { 1462 //error_log(__METHOD__.' ('.__LINE__.') '.' Start:'.$_startMessage.' NumberOfMessages:'.$_numberOfMessages.' Total:'.$total); 1463 if ($_startMessage+$_numberOfMessages>$total) 1464 { 1465 $numberOfMessages = $total%$_numberOfMessages; 1466 //$numberOfMessages = abs($_startMessage-$total-1); 1467 if ($numberOfMessages>0 && $numberOfMessages<=$_numberOfMessages) $_numberOfMessages = $numberOfMessages; 1468 //error_log(__METHOD__.' ('.__LINE__.') '.' Start:'.$_startMessage.' NumberOfMessages:'.$_numberOfMessages.' Total:'.$total); 1469 } 1470 $startMessage=($total-$_numberOfMessages)-1; 1471 //$retValue['info']['first'] = $startMessage; 1472 //$retValue['info']['last'] = $total; 1473 1474 } 1475 if ($startMessage+$_numberOfMessages>$total) 1476 { 1477 $_numberOfMessages = $_numberOfMessages-($total-($startMessage+$_numberOfMessages)); 1478 //$retValue['info']['first'] = $startMessage; 1479 //$retValue['info']['last'] = $total; 1480 } 1481 if($startMessage > 0) { 1482 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.' StartMessage:'.(-($_numberOfMessages+$startMessage)).', '.-$startMessage.' Number of Messages:'.count($sortResult)); 1483 $sortResult = array_slice($sortResult, -($_numberOfMessages+$startMessage), -$startMessage); 1484 } else { 1485 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.' StartMessage:'.(-($_numberOfMessages+($_startMessage-1))).', AllTheRest, Number of Messages:'.count($sortResult)); 1486 $sortResult = array_slice($sortResult, -($_numberOfMessages+($_startMessage-1))); 1487 } 1488 $sortResult = array_reverse($sortResult); 1489 } else { 1490 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.' StartMessage:'.($_startMessage-1).', '.$_numberOfMessages.' Number of Messages:'.count($sortResult)); 1491 $sortResult = array_slice($sortResult, $_startMessage-1, $_numberOfMessages); 1492 } 1493 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.array2string($sortResult)); 1494 } 1495 else 1496 { 1497 $sortResult = (is_array($_thisUIDOnly) ? $_thisUIDOnly:(array)$_thisUIDOnly); 1498 } 1499 1500 1501 // fetch the data for the selected messages 1502 if (self::$debug||self::$debugTimes) $starttime = microtime(true); 1503 try 1504 { 1505 $uidsToFetch = new Horde_Imap_Client_Ids(); 1506 $uidsToFetch->add($sortResult); 1507 1508 $fquery = new Horde_Imap_Client_Fetch_Query(); 1509 1510 // Pre-cache the headers we want, 'fetchHeaders' is a label into the cache 1511 $fquery->headers('fetchHeaders',array( 1512 'DISPOSITION-NOTIFICATION-TO','RETURN-RECEIPT-TO','X-CONFIRM-READING-TO', 1513 'DATE','SUBJECT','FROM','TO','CC','REPLY-TO', 1514 'X-PRIORITY' 1515 ),array( 1516 // Cache headers, we'll look at them below 1517 'cache' => true,//$_cacheResult, 1518 // Set peek so messages are not flagged as read 1519 'peek' => true 1520 )); 1521 $fquery->size(); 1522 $fquery->structure(); 1523 $fquery->flags(); 1524 $fquery->imapDate();// needed to ensure getImapDate fetches the internaldate, not the current time 1525 // if $_fetchPreviews is activated fetch part of the messages too 1526 if ($_fetchPreviews) $fquery->fullText(array('peek'=>true,'length'=>((int)$_fetchPreviews<5000?5000:$_fetchPreviews),'start'=>0)); 1527 $headersNew = $this->icServer->fetch($_folderName, $fquery, array( 1528 'ids' => $uidsToFetch, 1529 )); 1530 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($headersNew->ids())); 1531 } 1532 catch (\Exception $e) 1533 { 1534 $headersNew = array(); 1535 $sortResult = array(); 1536 } 1537 if (self::$debug||self::$debugTimes) 1538 { 1539 self::logRunTimes($starttime,null,'HordeFetch: for Folder:'.$_folderName.' Filter:'.array2string($_filter),__METHOD__.' ('.__LINE__.') '); 1540 if (self::$debug) 1541 { 1542 $queryString = implode(',', $sortResult); 1543 error_log(__METHOD__.' ('.__LINE__.') '.' Query:'.$queryString.' Result:'.array2string($headersNew)); 1544 } 1545 } 1546 1547 $cnt = 0; 1548 1549 foreach((array)$sortResult as $uid) { 1550 $sortOrder[$uid] = $cnt++; 1551 } 1552 1553 $count = 0; 1554 if (is_object($headersNew)) { 1555 if (self::$debug||self::$debugTimes) $starttime = microtime(true); 1556 foreach($headersNew->ids() as $id) { 1557 $_headerObject = $headersNew->get($id); 1558 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($_headerObject)); 1559 $headerObject = array(); 1560 $bodyPreview = null; 1561 $uid = $headerObject['UID']= ($_headerObject->getUid()?$_headerObject->getUid():$id); 1562 $headerObject['MSG_NUM'] = $_headerObject->getSeq(); 1563 $headerObject['SIZE'] = $_headerObject->getSize(); 1564 $headerObject['INTERNALDATE'] = $_headerObject->getImapDate(); 1565 1566 // Get already cached headers, 'fetchHeaders' is a label matchimg above 1567 $headerForPrio = self::headers2array($_headerObject->getHeaders('fetchHeaders',Horde_Imap_Client_Data_Fetch::HEADER_PARSE)); 1568 // Try to fetch header with key='' as some servers might have no fetchHeaders index. e.g. yandex.com 1569 if (empty($headerForPrio)) $headerForPrio = self::headers2array($_headerObject->getHeaders('',Horde_Imap_Client_Data_Fetch::HEADER_PARSE)); 1570 //fetch the fullMsg part if all conditions match to be available in case $_headerObject->getHeaders returns 1571 //nothing worthwhile (as it does for googlemail accounts, when preview is switched on 1572 if ($_fetchPreviews) 1573 { 1574 // on enabled preview $bodyPreview is needed lateron. fetched here, for fallback-reasons 1575 // in case of failed Header-Retrieval 1576 $bodyPreview = $_headerObject->getFullMsg(); 1577 if (empty($headerForPrio)||(is_array($headerForPrio)&&count($headerForPrio)===1&&$headerForPrio[''])) 1578 { 1579 $length = strpos($bodyPreview, Horde_Mime_Part::RFC_EOL.Horde_Mime_Part::RFC_EOL); 1580 if ($length===false) $length = strlen($bodyPreview); 1581 $headerForPrio = self::headers2array(Horde_Mime_Headers::parseHeaders(substr($bodyPreview, 0,$length))); 1582 } 1583 } 1584 $headerForPrio = array_change_key_case($headerForPrio, CASE_UPPER); 1585 if (self::$debug) { 1586 error_log(__METHOD__.' ('.__LINE__.') '.array2string($_headerObject).'UID:'.$_headerObject->getUid().' Size:'.$_headerObject->getSize().' Date:'.$_headerObject->getImapDate().'/'.DateTime::to($_headerObject->getImapDate(),'Y-m-d H:i:s')); 1587 error_log(__METHOD__.' ('.__LINE__.') '.array2string($headerForPrio)); 1588 } 1589 // message deleted from server but cache still reporting its existence ; may happen on QRESYNC with No permanent modsequences 1590 if (empty($headerForPrio)) 1591 { 1592 $total--; 1593 continue; 1594 } 1595 if ( isset($headerForPrio['DISPOSITION-NOTIFICATION-TO']) ) { 1596 $headerObject['DISPOSITION-NOTIFICATION-TO'] = self::decode_header(trim($headerForPrio['DISPOSITION-NOTIFICATION-TO'])); 1597 } else if ( isset($headerForPrio['RETURN-RECEIPT-TO']) ) { 1598 $headerObject['DISPOSITION-NOTIFICATION-TO'] = self::decode_header(trim($headerForPrio['RETURN-RECEIPT-TO'])); 1599 } else if ( isset($headerForPrio['X-CONFIRM-READING-TO']) ) { 1600 $headerObject['DISPOSITION-NOTIFICATION-TO'] = self::decode_header(trim($headerForPrio['X-CONFIRM-READING-TO'])); 1601 } /*else $sent_not = "";*/ 1602 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($headerObject)); 1603 $headerObject['DATE'] = $headerForPrio['DATE']; 1604 $headerObject['SUBJECT'] = (is_array($headerForPrio['SUBJECT'])?$headerForPrio['SUBJECT'][0]:$headerForPrio['SUBJECT']); 1605 $headerObject['FROM'] = (array)($headerForPrio['FROM']?$headerForPrio['FROM']:($headerForPrio['REPLY-TO']?$headerForPrio['REPLY-TO']:$headerForPrio['RETURN-PATH'])); 1606 $headerObject['TO'] = (array)$headerForPrio['TO']; 1607 $headerObject['CC'] = isset($headerForPrio['CC'])?(array)$headerForPrio['CC']:array(); 1608 $headerObject['REPLY-TO'] = isset($headerForPrio['REPLY-TO'])?(array)$headerForPrio['REPLY-TO']:array(); 1609 $headerObject['PRIORITY'] = isset($headerForPrio['X-PRIORITY'])?$headerForPrio['X-PRIORITY']:null; 1610 foreach (array('FROM','TO','CC','REPLY-TO') as $key) 1611 { 1612 $address = array(); 1613 foreach ($headerObject[$key] as $k => $ad) 1614 { 1615 //the commented section below IS a simplified version of the section "make sure ..." 1616 /* 1617 if (stripos($ad,'@')===false) 1618 { 1619 $remember=$k; 1620 } 1621 else 1622 { 1623 $address[] = (!is_null($remember)?$headerObject[$key][$remember].' ':'').$ad; 1624 $remember=null; 1625 } 1626 */ 1627 // make sure addresses are real emailaddresses one by one in the array as expected 1628 $rfcAddr = self::parseAddressList($ad); // does some fixing of known problems too 1629 foreach ($rfcAddr as $_rfcAddr) 1630 { 1631 if (!$_rfcAddr->valid) continue; // skip. not a valid address 1632 $address[] = imap_rfc822_write_address($_rfcAddr->mailbox,$_rfcAddr->host,$_rfcAddr->personal); 1633 } 1634 } 1635 $headerObject[$key] = $address; 1636 } 1637 $headerObject['FLAGS'] = $_headerObject->getFlags(); 1638 $headerObject['BODYPREVIEW']=null; 1639 // this section fetches part of the message-body (if enabled) for some kind of preview 1640 // if we fail to succeed, we fall back to the retrieval of the message-body with 1641 // fetchPartContents (see below, when we iterate over the structure to determine the 1642 // existance (and the details) for attachments) 1643 if ($_fetchPreviews) 1644 { 1645 // $bodyPreview is populated at the beginning of the loop, as it may be 1646 // needed to parse the Headers of the Message 1647 if (empty($bodyPreview)) $bodyPreview = $_headerObject->getFullMsg(); 1648 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($bodyPreview)); 1649 $base = Horde_Mime_Part::parseMessage($bodyPreview); 1650 foreach($base->partIterator() as $part) 1651 { 1652 //error_log(__METHOD__.__LINE__.'Part:'.$part->getPrimaryType()); 1653 if (empty($headerObject['BODYPREVIEW'])&&$part->getPrimaryType()== 'text') 1654 { 1655 $charset = $part->getContentTypeParameter('charset'); 1656 $buffer = Mail\Html::convertHTMLToText($part->toString(array( 1657 'encode' => Horde_Mime_Part::ENCODE_BINARY, // otherwise we cant recode charset 1658 )), $charset, 'utf-8'); 1659 $headerObject['BODYPREVIEW']=trim(str_replace(array("\r\n","\r","\n"),' ',mb_substr(Translation::convert_jsonsafe($buffer),0,((int)$_fetchPreviews<300?300:$_fetchPreviews)))); 1660 } elseif (empty($headerObject['BODYPREVIEW'])&&$part->getPrimaryType()== 'multipart') 1661 { 1662 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($part)); 1663 } 1664 } 1665 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($headerObject['BODYPREVIEW'])); 1666 } 1667 $mailStructureObject = $_headerObject->getStructure(); 1668 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($headerObject)); 1669 //error_log(__METHOD__.' ('.__LINE__.') '.' MimeMap:'.array2string($mailStructureObject->contentTypeMap())); 1670 //foreach ($_headerObject->getStructure()->getParts() as $p => $part) 1671 $headerObject['ATTACHMENTS']=null; 1672 $skipParts=array(); 1673 $messageMimeType=''; 1674 foreach ($mailStructureObject->contentTypeMap() as $mime_id => $mime_type) 1675 { 1676 if ($mime_id==0 || $messageMimeType==='') $messageMimeType = $mime_type; 1677 $part = $mailStructureObject->getPart($mime_id); 1678 $partdisposition = $part->getDisposition(); 1679 $partPrimaryType = $part->getPrimaryType(); 1680 // this section fetches the body for the purpose of previewing a few lines 1681 // drawback here it is talking to the mailserver for each mail thus consuming 1682 // more time than expected; so we call this section only when there is no 1683 // bodypreview could be found (multipart/....) 1684 if ($_fetchPreviews && empty($headerObject['BODYPREVIEW'])&&($partPrimaryType == 'text') && 1685 ((intval($mime_id) === 1) || !$mime_id) && 1686 ($partdisposition !== 'attachment')) { 1687 $_structure=$part; 1688 $this->fetchPartContents($uid, $_structure, false,true); 1689 $headerObject['BODYPREVIEW']=trim(str_replace(array("\r\n","\r","\n"),' ',mb_substr(Mail\Html::convertHTMLToText($_structure->getContents()),0,((int)$_fetchPreviews<300?300:$_fetchPreviews)))); 1690 $charSet = $part->getCharset(); 1691 // check if client set a wrong charset and content is utf-8 --> use utf-8 1692 if (strtolower($charSet) !='utf-8' && preg_match('//u', $headerObject['BODYPREVIEW'])) 1693 { 1694 $charSet = 'UTF-8'; 1695 } 1696 // add line breaks to $bodyParts 1697 //error_log(__METHOD__.' ('.__LINE__.') '.' Charset:'.$bodyParts[$i]['charSet'].'->'.$bodyParts[$i]['body']); 1698 $headerObject['BODYPREVIEW'] = Translation::convert_jsonsafe($headerObject['BODYPREVIEW'], $charSet); 1699 //error_log(__METHOD__.__LINE__.$headerObject['BODYPREVIEW']); 1700 } 1701 //error_log(__METHOD__.' ('.__LINE__.') '.' Uid:'.$uid.'->'.$mime_id.' Disp:'.$partdisposition.' Type:'.$partPrimaryType); 1702 $cid = $part->getContentId(); 1703 if (empty($partdisposition) && $partPrimaryType != 'multipart' && $partPrimaryType != 'text') 1704 { 1705 // the presence of an cid does not necessarily indicate its inline. it may lack the needed 1706 // link to show the image. Considering this: we "list" everything that matches the above criteria 1707 // as attachment in order to not loose/miss information on our data 1708 $partdisposition='attachment';//($partPrimaryType == 'image'&&!empty($cid)?'inline':'attachment'); 1709 } 1710 if ($mime_type=='message/rfc822') 1711 { 1712 //error_log(__METHOD__.' ('.__LINE__.') '.' Uid:'.$uid.'->'.$mime_id.':'.array2string($part->contentTypeMap())); 1713 foreach($part->contentTypeMap() as $sub_id => $sub_type) { if ($sub_id != $mime_id) $skipParts[$sub_id] = $sub_type;} 1714 } 1715 //error_log(__METHOD__.' ('.__LINE__.') '.' Uid:'.$uid.'->'.$mime_id.' Disp:'.$partdisposition.' Type:'.$partPrimaryType.' Skip:'.array2string($skipParts)); 1716 if (array_key_exists($mime_id,$skipParts)) continue; 1717 if ($partdisposition=='attachment' || 1718 ($partdisposition=='inline'&&$partPrimaryType == 'image'&&$mime_type=='image/tiff') || // as we are not able to display tiffs 1719 ($partdisposition=='inline'&&$partPrimaryType == 'image'&&empty($cid)) || 1720 ($partdisposition=='inline' && $partPrimaryType != 'image' && $partPrimaryType != 'multipart' && $partPrimaryType != 'text')) 1721 { 1722 $headerObject['ATTACHMENTS'][$mime_id]=$part->getAllDispositionParameters(); 1723 $headerObject['ATTACHMENTS'][$mime_id]['mimeType']=$mime_type; 1724 $headerObject['ATTACHMENTS'][$mime_id]['uid']=$uid; 1725 $headerObject['ATTACHMENTS'][$mime_id]['cid'] = $cid; 1726 $headerObject['ATTACHMENTS'][$mime_id]['partID']=$mime_id; 1727 if (!isset($headerObject['ATTACHMENTS'][$mime_id]['name'])) 1728 { 1729 $headerObject['ATTACHMENTS'][$mime_id]['name']= $part->getName() ? $part->getName() : 1730 ($mime_type == "message/rfc822" ? lang('forwarded message') : lang('attachment')); 1731 } 1732 if (!strcasecmp($headerObject['ATTACHMENTS'][$mime_id]['name'],'winmail.dat') || 1733 $headerObject['ATTACHMENTS'][$mime_id]['mimeType']=='application/ms-tnef') 1734 { 1735 $headerObject['ATTACHMENTS'][$mime_id]['is_winmail'] = true; 1736 } 1737 //error_log(__METHOD__.' ('.__LINE__.') '.' PartDisposition:'.$mime_id.'->'.array2string($part->getName())); 1738 //error_log(__METHOD__.' ('.__LINE__.') '.' PartDisposition:'.$mime_id.'->'.array2string($part->getAllDispositionParameters())); 1739 //error_log(__METHOD__.' ('.__LINE__.') '.' Attachment:'.$mime_id.'->'.array2string($headerObject['ATTACHMENTS'][$mime_id])); 1740 } 1741 } 1742 //error_log(__METHOD__.' ('.__LINE__.') '.' FindBody (plain):'.array2string($mailStructureObject->findBody('plain'))); 1743 //error_log(__METHOD__.' ('.__LINE__.') '.' FindBody (html):'.array2string($mailStructureObject->findBody('html'))); 1744 //if($count == 0) error_log(__METHOD__.array2string($headerObject)); 1745 if (empty($headerObject['UID'])) continue; 1746 //$uid = ($rByUid ? $headerObject['UID'] : $headerObject['MSG_NUM']); 1747 // make dates like "Mon, 23 Apr 2007 10:11:06 UT" working with strtotime 1748 if(substr($headerObject['DATE'],-2) === 'UT') { 1749 $headerObject['DATE'] .= 'C'; 1750 } 1751 if(substr($headerObject['INTERNALDATE'],-2) === 'UT') { 1752 $headerObject['INTERNALDATE'] .= 'C'; 1753 } 1754 //error_log(__METHOD__.' ('.__LINE__.') '.' '.$headerObject['SUBJECT'].'->'.$headerObject['DATE'].'<->'.$headerObject['INTERNALDATE'] .'#'); 1755 //error_log(__METHOD__.' ('.__LINE__.') '.' '.$this->decode_subject($headerObject['SUBJECT']).'->'.$headerObject['DATE']); 1756 if (isset($headerObject['ATTACHMENTS']) && count($headerObject['ATTACHMENTS'])) foreach ($headerObject['ATTACHMENTS'] as &$a) { $retValue['header'][$sortOrder[$uid]]['attachments'][]=$a;} 1757 $retValue['header'][$sortOrder[$uid]]['subject'] = $this->decode_subject($headerObject['SUBJECT']); 1758 $retValue['header'][$sortOrder[$uid]]['size'] = $headerObject['SIZE']; 1759 $retValue['header'][$sortOrder[$uid]]['date'] = self::_strtotime(($headerObject['DATE']&&!($headerObject['DATE']=='NIL')?$headerObject['DATE']:$headerObject['INTERNALDATE']),'ts',true); 1760 $retValue['header'][$sortOrder[$uid]]['internaldate']= self::_strtotime($headerObject['INTERNALDATE'],'ts',true); 1761 $retValue['header'][$sortOrder[$uid]]['mimetype'] = $messageMimeType; 1762 $retValue['header'][$sortOrder[$uid]]['id'] = $headerObject['MSG_NUM']; 1763 $retValue['header'][$sortOrder[$uid]]['uid'] = $headerObject['UID']; 1764 $retValue['header'][$sortOrder[$uid]]['bodypreview'] = $headerObject['BODYPREVIEW']; 1765 $retValue['header'][$sortOrder[$uid]]['priority'] = ($headerObject['PRIORITY']?$headerObject['PRIORITY']:3); 1766 $retValue['header'][$sortOrder[$uid]]['smimeType'] = Mail\Smime::getSmimeType($mailStructureObject); 1767 //error_log(__METHOD__.' ('.__LINE__.') '.' '.array2string($retValue['header'][$sortOrder[$uid]])); 1768 if (isset($headerObject['DISPOSITION-NOTIFICATION-TO'])) $retValue['header'][$sortOrder[$uid]]['disposition-notification-to'] = $headerObject['DISPOSITION-NOTIFICATION-TO']; 1769 if (is_array($headerObject['FLAGS'])) { 1770 $retValue['header'][$sortOrder[$uid]] = array_merge($retValue['header'][$sortOrder[$uid]],self::prepareFlagsArray($headerObject)); 1771 } 1772 //error_log(__METHOD__.' ('.__LINE__.') '.$headerObject['SUBJECT'].'->'.array2string($_headerObject->getEnvelope()->__get('from'))); 1773 if(is_array($headerObject['FROM']) && $headerObject['FROM'][0]) { 1774 $retValue['header'][$sortOrder[$uid]]['sender_address'] = self::decode_header($headerObject['FROM'][0],true); 1775 if (count($headerObject['FROM'])>1) 1776 { 1777 $ki=0; 1778 foreach($headerObject['FROM'] as $k => $add) 1779 { 1780 if ($k==0) continue; 1781 $retValue['header'][$sortOrder[$uid]]['additional_from_addresses'][$ki] = self::decode_header($add,true); 1782 $ki++; 1783 } 1784 } 1785 } 1786 if(is_array($headerObject['REPLY-TO']) && $headerObject['REPLY-TO'][0]) { 1787 $retValue['header'][$sortOrder[$uid]]['reply_to_address'] = self::decode_header($headerObject['REPLY-TO'][0],true); 1788 } 1789 if(is_array($headerObject['TO']) && $headerObject['TO'][0]) { 1790 $retValue['header'][$sortOrder[$uid]]['to_address'] = self::decode_header($headerObject['TO'][0],true); 1791 if (count($headerObject['TO'])>1) 1792 { 1793 $ki=0; 1794 foreach($headerObject['TO'] as $k => $add) 1795 { 1796 if ($k==0) continue; 1797 //error_log(__METHOD__.' ('.__LINE__.') '."-> $k:".array2string($add)); 1798 $retValue['header'][$sortOrder[$uid]]['additional_to_addresses'][$ki] = self::decode_header($add,true); 1799 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($retValue['header'][$sortOrder[$uid]]['additional_to_addresses'][$ki])); 1800 $ki++; 1801 } 1802 } 1803 } 1804 if(is_array($headerObject['CC']) && count($headerObject['CC'])>0) { 1805 $ki=0; 1806 foreach($headerObject['CC'] as $k => $add) 1807 { 1808 //error_log(__METHOD__.' ('.__LINE__.') '."-> $k:".array2string($add)); 1809 $retValue['header'][$sortOrder[$uid]]['cc_addresses'][$ki] = self::decode_header($add,true); 1810 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($retValue['header'][$sortOrder[$uid]]['additional_to_addresses'][$ki])); 1811 $ki++; 1812 } 1813 } 1814 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($retValue['header'][$sortOrder[$uid]])); 1815 1816 $count++; 1817 } 1818 if (self::$debug||self::$debugTimes) self::logRunTimes($starttime,null,' fetching Headers and stuff for Folder:'.$_folderName,__METHOD__.' ('.__LINE__.') '); 1819 //self::$debug=false; 1820 // sort the messages to the requested displayorder 1821 if(is_array($retValue['header'])) { 1822 $countMessages = $total; 1823 if (isset($_filter['range'])) $countMessages = self::$folderStatusCache[$this->profileID][$_folderName]['messages']; 1824 ksort($retValue['header']); 1825 $retValue['info']['total'] = $total; 1826 //if ($_startMessage>$total) $_startMessage = $total-($count-1); 1827 $retValue['info']['first'] = $_startMessage; 1828 $retValue['info']['last'] = $_startMessage + $count - 1 ; 1829 return $retValue; 1830 } else { 1831 $retValue = array(); 1832 $retValue['info']['total'] = 0; 1833 $retValue['info']['first'] = 0; 1834 $retValue['info']['last'] = 0; 1835 return $retValue; 1836 } 1837 } else { 1838 if ($headersNew == null && empty($_thisUIDOnly)) error_log(__METHOD__." -> retrieval of Message Details to Query $queryString failed: ".print_r($headersNew,TRUE)); 1839 $retValue = array(); 1840 $retValue['info']['total'] = 0; 1841 $retValue['info']['first'] = 0; 1842 $retValue['info']['last'] = 0; 1843 return $retValue; 1844 } 1845 } 1846 1847 /** 1848 * static function prepareFlagsArray 1849 * prepare headerObject to return some standardized array to tell which flags are set for a message 1850 * @param array $headerObject - array to process, a full return array from icServer->getSummary 1851 * @return array array of flags 1852 */ 1853 static function prepareFlagsArray($headerObject) 1854 { 1855 if (is_array($headerObject['FLAGS'])) $headerFlags = array_map('strtolower',$headerObject['FLAGS']); 1856 $retValue = array(); 1857 $retValue['recent'] = in_array('\\recent', $headerFlags); 1858 $retValue['flagged'] = in_array('\\flagged', $headerFlags); 1859 $retValue['answered'] = in_array('\\answered', $headerFlags); 1860 $retValue['forwarded'] = in_array('$forwarded', $headerFlags); 1861 $retValue['deleted'] = in_array('\\deleted', $headerFlags); 1862 $retValue['seen'] = in_array('\\seen', $headerFlags); 1863 $retValue['draft'] = in_array('\\draft', $headerFlags); 1864 $retValue['mdnsent'] = in_array('$mdnsent', $headerFlags)||in_array('mdnsent', $headerFlags); 1865 $retValue['mdnnotsent'] = in_array('$mdnnotsent', $headerFlags)||in_array('mdnnotsent', $headerFlags); 1866 $retValue['label1'] = in_array('$label1', $headerFlags); 1867 $retValue['label2'] = in_array('$label2', $headerFlags); 1868 $retValue['label3'] = in_array('$label3', $headerFlags); 1869 $retValue['label4'] = in_array('$label4', $headerFlags); 1870 $retValue['label5'] = in_array('$label5', $headerFlags); 1871 //error_log(__METHOD__.' ('.__LINE__.') '.$headerObject['SUBJECT'].':'.array2string($retValue)); 1872 return $retValue; 1873 } 1874 1875 /** 1876 * fetches a sorted list of messages from the imap server 1877 * private function 1878 * 1879 * @todo implement sort based on Net_IMAP 1880 * @param string $_folderName the name of the folder in which the messages get searched 1881 * @param integer $_sort the primary sort key 1882 * @param bool $_reverse sort the messages ascending or descending 1883 * @param array $_filter the search filter 1884 * @param bool $resultByUid if set to true, the result is to be returned by uid, if the server does not reply 1885 * on a query for uids, the result may be returned by IDs only, this will be indicated by this param 1886 * @param bool $setSession if set to true the session will be populated with the result of the query 1887 * @return mixed bool/array false or array of ids 1888 */ 1889 function getSortedList($_folderName, $_sort, &$_reverse, $_filter, &$resultByUid=true, $setSession=true) 1890 { 1891 static $cachedFolderStatus = null; 1892 // in the past we needed examineMailbox to figure out if the server with the serverID support keywords 1893 // this information is filled/provided by examineMailbox; but caching within one request seems o.k. 1894 if (is_null($cachedFolderStatus) || !isset($cachedFolderStatus[$this->profileID][$_folderName]) ) 1895 { 1896 $folderStatus = $cachedFolderStatus[$this->profileID][$_folderName] = $this->icServer->examineMailbox($_folderName); 1897 } 1898 else 1899 { 1900 $folderStatus = $cachedFolderStatus[$this->profileID][$_folderName]; 1901 } 1902 //error_log(__METHOD__.' ('.__LINE__.') '.' F:'.$_folderName.' S:'.array2string($folderStatus)); 1903 //error_log(__METHOD__.' ('.__LINE__.') '.' Filter:'.array2string($_filter)); 1904 $try2useCache = true; 1905 static $eMailListContainsDeletedMessages = null; 1906 if (is_null($eMailListContainsDeletedMessages)) $eMailListContainsDeletedMessages = Cache::getCache(Cache::INSTANCE,'email','eMailListContainsDeletedMessages'.trim($GLOBALS['egw_info']['user']['account_id']),null,array(),60*60*1); 1907 // this indicates, that there is no Filter set, and the returned set/subset should not contain DELETED Messages, nor filtered for UNDELETED 1908 if ($setSession==true && ((strpos(array2string($_filter), 'UNDELETED') === false && strpos(array2string($_filter), 'DELETED') === false))) 1909 { 1910 if (self::$debugTimes) $starttime = microtime(true); 1911 if (is_null($eMailListContainsDeletedMessages) || empty($eMailListContainsDeletedMessages[$this->profileID]) || empty($eMailListContainsDeletedMessages[$this->profileID][$_folderName])) $eMailListContainsDeletedMessages = Cache::getCache(Cache::INSTANCE,'email','eMailListContainsDeletedMessages'.trim($GLOBALS['egw_info']['user']['account_id']),null,array(),60*60*1); 1912 $five=true; 1913 $dReverse=1; 1914 $deletedMessages = $this->getSortedList($_folderName, 0, $dReverse, array('status'=>array('DELETED')),$five,false); 1915 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') Found DeletedMessages:'.array2string($eMailListContainsDeletedMessages)); 1916 $eMailListContainsDeletedMessages[$this->profileID][$_folderName] =$deletedMessages['count']; 1917 Cache::setCache(Cache::INSTANCE,'email','eMailListContainsDeletedMessages'.trim($GLOBALS['egw_info']['user']['account_id']),$eMailListContainsDeletedMessages, 60*60*1); 1918 if (self::$debugTimes) self::logRunTimes($starttime,null,'setting eMailListContainsDeletedMessages for Profile:'.$this->profileID.' Folder:'.$_folderName.' to '.$eMailListContainsDeletedMessages[$this->profileID][$_folderName],__METHOD__.' ('.__LINE__.') '); //error_log(__METHOD__.' ('.__LINE__.') '.' Profile:'.$this->profileID.' Folder:'.$_folderName.' -> EXISTS/SessStat:'.array2string($folderStatus['MESSAGES']).'/'.self::$folderStatusCache[$this->profileID][$_folderName]['messages'].' ListContDelMsg/SessDeleted:'.$eMailListContainsDeletedMessages[$this->profileID][$_folderName].'/'.self::$folderStatusCache[$this->profileID][$_folderName]['deleted']); 1919 } 1920 $try2useCache = false; 1921 //self::$supportsORinQuery[$this->profileID]=true; 1922 if (is_null(self::$supportsORinQuery) || !isset(self::$supportsORinQuery[$this->profileID])) 1923 { 1924 self::$supportsORinQuery = Cache::getCache(Cache::INSTANCE,'email','supportsORinQuery'.trim($GLOBALS['egw_info']['user']['account_id']),null,array(),60*60*10); 1925 if (!isset(self::$supportsORinQuery[$this->profileID])) self::$supportsORinQuery[$this->profileID]=true; 1926 } 1927 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($_filter).' SupportsOrInQuery:'.self::$supportsORinQuery[$this->profileID]); 1928 $filter = $this->createIMAPFilter($_folderName, $_filter,self::$supportsORinQuery[$this->profileID]); 1929 if (self::$debug) 1930 { 1931 $query_str = $filter->build(); 1932 error_log(__METHOD__.' ('.__LINE__.') '.' '.$query_str['query']); 1933 } 1934 //_debug_array($filter); 1935 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($filter).'#'.array2string($this->icServer->capability())); 1936 if($this->icServer->hasCapability('SORT')) { 1937 // when using an orQuery and we sort by date. sort seems to fail on certain servers => ZIMBRA with Horde_Imap_Client 1938 // thus we translate the search request from date to Horde_Imap_Client::SORT_SEQUENCE (which should be the same, if 1939 // there is no messing with the dates) 1940 //if (self::$supportsORinQuery[$this->profileID]&&$_sort=='date'&&$_filter['type']=='quick'&&!empty($_filter['string']))$_sort='INTERNALDATE'; 1941 if (self::$debug) error_log(__METHOD__." Mailserver has SORT Capability, SortBy: ".array2string($_sort)." Reverse: $_reverse"); 1942 $sortOrder = $this->_getSortString($_sort, $_reverse); 1943 if ($_reverse && in_array(Horde_Imap_Client::SORT_REVERSE,$sortOrder)) $_reverse=false; // as we reversed the result already 1944 if (self::$debug) error_log(__METHOD__." Mailserver runs SORT: SortBy:".array2string($_sort)."->".array2string($sortOrder)." Filter: ".array2string($filter)); 1945 try 1946 { 1947 $sortResult = $this->icServer->search($_folderName, $filter, array( 1948 'sort' => $sortOrder,)); 1949 1950 // Attempt another search without sorting filter if first try failed with 1951 // no result, as may some servers do not coupe well with sort option 1952 // eventhough they claim to support SORT capability. 1953 if (!isset($sortResult['count'])) $sortResult = $this->icServer->search($_folderName, $filter); 1954 1955 // if there is an Error, we assume that the server is not capable of sorting 1956 } 1957 catch(\Exception $e) 1958 { 1959 //error_log(__METHOD__.'('.__LINE__.'):'.$e->getMessage()); 1960 $resultByUid = false; 1961 $sortOrder = array(Horde_Imap_Client::SORT_SEQUENCE); 1962 if ($_reverse) array_unshift($sortOrder,Horde_Imap_Client::SORT_REVERSE); 1963 try 1964 { 1965 $sortResult = $this->icServer->search($_folderName, $filter, array( 1966 'sort' => $sortOrder)); 1967 } 1968 catch(\Exception $e) 1969 { 1970 error_log(__METHOD__.'('.__LINE__.'):'.$e->getMessage()); 1971 $sortResult = self::$folderStatusCache[$this->profileID][$_folderName]['sortResult']; 1972 } 1973 } 1974 if (self::$debug) error_log(__METHOD__.print_r($sortResult,true)); 1975 } else { 1976 if (self::$debug) error_log(__METHOD__." Mailserver has NO SORT Capability"); 1977 //$sortOrder = array(Horde_Imap_Client::SORT_SEQUENCE); 1978 //if ($_reverse) array_unshift($sortOrder,Horde_Imap_Client::SORT_REVERSE); 1979 try 1980 { 1981 $sortResult = $this->icServer->search($_folderName, $filter, array()/*array( 1982 'sort' => $sortOrder)*/); 1983 } 1984 catch(\Exception $e) 1985 { 1986 //error_log(__METHOD__.'('.__LINE__.'):'.$e->getMessage()); 1987 // possible error OR Query. But Horde gives no detailed Info :-( 1988 self::$supportsORinQuery[$this->profileID]=false; 1989 Cache::setCache(Cache::INSTANCE,'email','supportsORinQuery'.trim($GLOBALS['egw_info']['user']['account_id']),self::$supportsORinQuery,60*60*10); 1990 if (self::$debug) error_log(__METHOD__.__LINE__." Mailserver seems to have NO OR Capability for Search:".$sortResult->message); 1991 $filter = $this->createIMAPFilter($_folderName, $_filter, self::$supportsORinQuery[$this->profileID]); 1992 try 1993 { 1994 $sortResult = $this->icServer->search($_folderName, $filter, array()/*array( 1995 'sort' => $sortOrder)*/); 1996 } 1997 catch(\Exception $e) 1998 { 1999 } 2000 } 2001 if(is_array($sortResult['match'])) { 2002 // not sure that this is going so succeed as $sortResult['match'] is a hordeObject 2003 sort($sortResult['match'], SORT_NUMERIC); 2004 } 2005 if (self::$debug) error_log(__METHOD__." using Filter:".print_r($filter,true)." ->".print_r($sortResult,true)); 2006 } 2007 if ($setSession) 2008 { 2009 self::$folderStatusCache[$this->profileID][$_folderName]['uidValidity'] = $folderStatus['UIDVALIDITY']; 2010 self::$folderStatusCache[$this->profileID][$_folderName]['messages'] = $folderStatus['MESSAGES']; 2011 self::$folderStatusCache[$this->profileID][$_folderName]['deleted'] = $eMailListContainsDeletedMessages[$this->profileID][$_folderName]; 2012 self::$folderStatusCache[$this->profileID][$_folderName]['uidnext'] = $folderStatus['UIDNEXT']; 2013 self::$folderStatusCache[$this->profileID][$_folderName]['filter'] = $_filter; 2014 self::$folderStatusCache[$this->profileID][$_folderName]['sortResult'] = $sortResult; 2015 self::$folderStatusCache[$this->profileID][$_folderName]['sort'] = $_sort; 2016 } 2017 //error_log(__METHOD__." using Filter:".print_r($filter,true)." ->".print_r($sortResult,true)); 2018 //_debug_array($sortResult['match']->ids); 2019 return $sortResult; 2020 } 2021 2022 /** 2023 * convert the sort value from the gui(integer) into a string 2024 * 2025 * @param mixed _sort the integer sort order / or a valid and handeled SORTSTRING (right now: UID/ARRIVAL/INTERNALDATE (->ARRIVAL)) 2026 * @param bool _reverse wether to add REVERSE to the Sort String or not 2027 * @return the sort sequence for horde search 2028 */ 2029 function _getSortString($_sort, $_reverse=false) 2030 { 2031 $_reverse=false; 2032 if (is_numeric($_sort)) 2033 { 2034 switch($_sort) { 2035 case 2: 2036 $retValue = array(Horde_Imap_Client::SORT_FROM); 2037 break; 2038 case 4: 2039 $retValue = array(Horde_Imap_Client::SORT_TO); 2040 break; 2041 case 3: 2042 $retValue = array(Horde_Imap_Client::SORT_SUBJECT); 2043 break; 2044 case 6: 2045 $retValue = array(Horde_Imap_Client::SORT_SIZE); 2046 break; 2047 case 0: 2048 default: 2049 $retValue = array(Horde_Imap_Client::SORT_DATE); 2050 //$retValue = 'ARRIVAL'; 2051 break; 2052 } 2053 } 2054 else 2055 { 2056 switch(strtoupper($_sort)) { 2057 case 'FROMADDRESS': 2058 $retValue = array(Horde_Imap_Client::SORT_FROM); 2059 break; 2060 case 'TOADDRESS': 2061 $retValue = array(Horde_Imap_Client::SORT_TO); 2062 break; 2063 case 'SUBJECT': 2064 $retValue = array(Horde_Imap_Client::SORT_SUBJECT); 2065 break; 2066 case 'SIZE': 2067 $retValue = array(Horde_Imap_Client::SORT_SIZE); 2068 break; 2069 case 'ARRIVAL': 2070 $retValue = array(Horde_Imap_Client::SORT_ARRIVAL); 2071 break; 2072 case 'UID': // should be equivalent to INTERNALDATE, which is ARRIVAL, which should be highest (latest) uid should be newest date 2073 case 'INTERNALDATE': 2074 $retValue = array(Horde_Imap_Client::SORT_SEQUENCE); 2075 break; 2076 case 'DATE': 2077 default: 2078 $retValue = array(Horde_Imap_Client::SORT_DATE); 2079 break; 2080 } 2081 } 2082 if ($_reverse) array_unshift($retValue,Horde_Imap_Client::SORT_REVERSE); 2083 //error_log(__METHOD__.' ('.__LINE__.') '.' '.($_reverse?'REVERSE ':'').$_sort.'->'.$retValue); 2084 return $retValue; 2085 } 2086 2087 /** 2088 * this function creates an IMAP filter from the criterias given 2089 * 2090 * @param string $_folder used to determine the search to TO or FROM on QUICK Search wether it is a send-folder or not 2091 * @param array $_criterias contains the search/filter criteria 2092 * @param boolean $_supportsOrInQuery wether to use the OR Query on QuickSearch 2093 * @return Horde_Imap_Client_Search_Query the IMAP filter 2094 */ 2095 function createIMAPFilter($_folder, $_criterias, $_supportsOrInQuery=true) 2096 { 2097 $imapFilter = new Horde_Imap_Client_Search_Query(); 2098 $imapFilter->charset('UTF-8'); 2099 2100 //_debug_array($_criterias); 2101 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.' Criterias:'.(!is_array($_criterias)?" none -> returning":array2string($_criterias))); 2102 if((!is_array($_criterias) || $_criterias['status']=='any') && 2103 (!isset($_criterias['string']) || empty($_criterias['string'])) && 2104 (!isset($_criterias['range'])|| empty($_criterias['range']) || 2105 ( !empty($_criterias['range'])&& ($_criterias['range']!='BETWEEN' && empty($_criterias['date'])|| 2106 ($_criterias['range']=='BETWEEN' && empty($_criterias['since'])&& empty($_criterias['before'])))))) 2107 { 2108 //error_log(__METHOD__.' ('.__LINE__.') returning early Criterias:'.print_r($_criterias, true)); 2109 $imapFilter->flag('DELETED', $set=false); 2110 return $imapFilter; 2111 } 2112 $queryValid = false; 2113 // statusQuery MUST be placed first, as search for subject/mailbody and such is 2114 // depending on charset. flagSearch is not BUT messes the charset if called afterwards 2115 $statusQueryValid = false; 2116 foreach((array)$_criterias['status'] as $k => $criteria) { 2117 $imapStatusFilter = new Horde_Imap_Client_Search_Query(); 2118 $imapStatusFilter->charset('UTF-8'); 2119 $criteria = strtoupper($criteria); 2120 switch ($criteria) { 2121 case 'ANSWERED': 2122 case 'DELETED': 2123 case 'FLAGGED': 2124 case 'RECENT': 2125 case 'SEEN': 2126 $imapStatusFilter->flag($criteria, $set=true); 2127 $queryValid = $statusQueryValid =true; 2128 break; 2129 case 'READ': 2130 $imapStatusFilter->flag('SEEN', $set=true); 2131 $queryValid = $statusQueryValid =true; 2132 break; 2133 case 'LABEL1': 2134 case 'KEYWORD1': 2135 case 'LABEL2': 2136 case 'KEYWORD2': 2137 case 'LABEL3': 2138 case 'KEYWORD3': 2139 case 'LABEL4': 2140 case 'KEYWORD4': 2141 case 'LABEL5': 2142 case 'KEYWORD5': 2143 $imapStatusFilter->flag(str_ireplace('KEYWORD','$LABEL',$criteria), $set=true); 2144 $queryValid = $statusQueryValid =true; 2145 break; 2146 case 'NEW': 2147 $imapStatusFilter->flag('RECENT', $set=true); 2148 $imapStatusFilter->flag('SEEN', $set=false); 2149 $queryValid = $statusQueryValid =true; 2150 break; 2151 case 'OLD': 2152 $imapStatusFilter->flag('RECENT', $set=false); 2153 $queryValid = $statusQueryValid =true; 2154 break; 2155// operate only on system flags 2156// $systemflags = array( 2157// 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'RECENT', 'SEEN' 2158// ); 2159 case 'UNANSWERED': 2160 $imapStatusFilter->flag('ANSWERED', $set=false); 2161 $queryValid = $statusQueryValid =true; 2162 break; 2163 case 'UNDELETED': 2164 $imapFilter->flag('DELETED', $set=false); 2165 $queryValid = true; 2166 break; 2167 case 'UNFLAGGED': 2168 $imapStatusFilter->flag('FLAGGED', $set=false); 2169 $queryValid = $statusQueryValid =true; 2170 break; 2171 case 'UNREAD': 2172 case 'UNSEEN': 2173 $imapStatusFilter->flag('SEEN', $set=false); 2174 $queryValid = $statusQueryValid =true; 2175 break; 2176 case 'UNLABEL1': 2177 case 'UNKEYWORD1': 2178 case 'UNLABEL2': 2179 case 'UNKEYWORD2': 2180 case 'UNLABEL3': 2181 case 'UNKEYWORD3': 2182 case 'UNLABEL4': 2183 case 'UNKEYWORD4': 2184 case 'UNLABEL5': 2185 case 'UNKEYWORD5': 2186 $imapStatusFilter->flag(str_ireplace(array('UNKEYWORD','UNLABEL'),'$LABEL',$criteria), $set=false); 2187 $queryValid = $statusQueryValid =true; 2188 break; 2189 default: 2190 $statusQueryValid = false; 2191 } 2192 if ($statusQueryValid) 2193 { 2194 $imapFilter->andSearch($imapStatusFilter); 2195 } 2196 } 2197 2198 2199 //error_log(__METHOD__.' ('.__LINE__.') '.print_r($_criterias, true)); 2200 $imapSearchFilter = new Horde_Imap_Client_Search_Query(); 2201 $imapSearchFilter->charset('UTF-8'); 2202 2203 if(!empty($_criterias['string'])) { 2204 $criteria = strtoupper($_criterias['type']); 2205 switch ($criteria) { 2206 case 'BYDATE': 2207 case 'QUICK': 2208 case 'QUICKWITHCC': 2209 $imapSearchFilter->headerText('SUBJECT', $_criterias['string'], $not=false); 2210 //$imapSearchFilter->charset('UTF-8'); 2211 $imapFilter2 = new Horde_Imap_Client_Search_Query(); 2212 $imapFilter2->charset('UTF-8'); 2213 if($this->isSentFolder($_folder)) { 2214 $imapFilter2->headerText('TO', $_criterias['string'], $not=false); 2215 } else { 2216 $imapFilter2->headerText('FROM', $_criterias['string'], $not=false); 2217 } 2218 if ($_supportsOrInQuery) 2219 { 2220 $imapSearchFilter->orSearch($imapFilter2); 2221 } 2222 else 2223 { 2224 $imapSearchFilter->andSearch($imapFilter2); 2225 } 2226 if ($_supportsOrInQuery && $criteria=='QUICKWITHCC') 2227 { 2228 $imapFilter3 = new Horde_Imap_Client_Search_Query(); 2229 $imapFilter3->charset('UTF-8'); 2230 $imapFilter3->headerText('CC', $_criterias['string'], $not=false); 2231 $imapSearchFilter->orSearch($imapFilter3); 2232 } 2233 $queryValid = true; 2234 break; 2235 case 'LARGER': 2236 case 'SMALLER': 2237 if (strlen(trim($_criterias['string'])) != strlen((float) trim($_criterias['string']))) 2238 { 2239 //examine string to evaluate size 2240 $unit = strtoupper(trim(substr(trim($_criterias['string']),strlen((float) trim($_criterias['string']))))); 2241 $multipleBy = array('KB'=>1024,'K'=>1024, 2242 'MB'=>1024*1000,'M'=>1024*1000, 2243 'GB'=>1024*1000*1000,'G'=>1024*1000*1000, 2244 'TB'=>1024*1000*1000*1000,'T'=>1024*1000*1000*1000); 2245 $numberinBytes=(float)$_criterias['string']; 2246 if (isset($multipleBy[$unit])) $numberinBytes=(float)$_criterias['string']*$multipleBy[$unit]; 2247 //error_log(__METHOD__.__LINE__.'#'.$_criterias['string'].'->'.(float)$_criterias['string'].'#'.$unit.' ='.$numberinBytes); 2248 $_criterias['string']=$numberinBytes; 2249 } 2250 $imapSearchFilter->size( $_criterias['string'], ($criteria=='LARGER'?true:false), $not=false); 2251 //$imapSearchFilter->charset('UTF-8'); 2252 $queryValid = true; 2253 break; 2254 case 'FROM': 2255 case 'TO': 2256 case 'CC': 2257 case 'BCC': 2258 case 'SUBJECT': 2259 $imapSearchFilter->headerText($criteria, $_criterias['string'], $not=false); 2260 //$imapSearchFilter->charset('UTF-8'); 2261 $queryValid = true; 2262 break; 2263 case 'BODY': 2264 case 'TEXT': 2265 $imapSearchFilter->text($_criterias['string'],($criteria=='BODY'?true:false), $not=false); 2266 //$imapSearchFilter->charset('UTF-8'); 2267 $queryValid = true; 2268 break; 2269 case 'SINCE': 2270 $imapSearchFilter->dateSearch(new DateTime($_criterias['string']), Horde_Imap_Client_Search_Query::DATE_SINCE, $header=true, $not=false); 2271 $queryValid = true; 2272 break; 2273 case 'BEFORE': 2274 $imapSearchFilter->dateSearch(new DateTime($_criterias['string']), Horde_Imap_Client_Search_Query::DATE_BEFORE, $header=true, $not=false); 2275 $queryValid = true; 2276 break; 2277 case 'ON': 2278 $imapSearchFilter->dateSearch(new DateTime($_criterias['string']), Horde_Imap_Client_Search_Query::DATE_ON, $header=true, $not=false); 2279 $queryValid = true; 2280 break; 2281 } 2282 } 2283 if ($statusQueryValid && !$queryValid) $queryValid=true; 2284 if ($queryValid) $imapFilter->andSearch($imapSearchFilter); 2285 2286 if (isset($_criterias['range']) && !empty($_criterias['range'])) 2287 { 2288 $rangeValid = false; 2289 $imapRangeFilter = new Horde_Imap_Client_Search_Query(); 2290 $imapRangeFilter->charset('UTF-8'); 2291 $criteria = strtoupper($_criterias['range']); 2292 if ($_criterias['range'] == "BETWEEN" && isset($_criterias['since']) && isset($_criterias['before']) && $_criterias['since']==$_criterias['before']) 2293 { 2294 $_criterias['date']=$_criterias['since']; 2295 unset($_criterias['since']); 2296 unset($_criterias['before']); 2297 $criteria=$_criterias['range']='ON'; 2298 } 2299 switch ($criteria) { 2300 case 'BETWEEN': 2301 //try to be smart about missing 2302 //enddate 2303 if ($_criterias['since']) 2304 { 2305 $imapRangeFilter->dateSearch(new DateTime($_criterias['since']), Horde_Imap_Client_Search_Query::DATE_SINCE, $header=true, $not=false); 2306 $rangeValid = true; 2307 } 2308 //startdate 2309 if ($_criterias['before']) 2310 { 2311 $imapRangeFilter2 = new Horde_Imap_Client_Search_Query(); 2312 $imapRangeFilter2->charset('UTF-8'); 2313 //our before (startdate) is inklusive, as we work with "d-M-Y", we must add a day 2314 $_criterias['before'] = date("d-M-Y",DateTime::to($_criterias['before'],'ts')+(3600*24)); 2315 $imapRangeFilter2->dateSearch(new DateTime($_criterias['before']), Horde_Imap_Client_Search_Query::DATE_BEFORE, $header=true, $not=false); 2316 $imapRangeFilter->andSearch($imapRangeFilter2); 2317 $rangeValid = true; 2318 } 2319 break; 2320 case 'SINCE'://enddate 2321 $imapRangeFilter->dateSearch(new DateTime(($_criterias['since']?$_criterias['since']:$_criterias['date'])), Horde_Imap_Client_Search_Query::DATE_SINCE, $header=true, $not=false); 2322 $rangeValid = true; 2323 break; 2324 case 'BEFORE'://startdate 2325 //our before (startdate) is inklusive, as we work with "d-M-Y", we must add a day 2326 $_criterias['before'] = date("d-M-Y",DateTime::to(($_criterias['before']?$_criterias['before']:$_criterias['date']),'ts')+(3600*24)); 2327 $imapRangeFilter->dateSearch(new DateTime($_criterias['before']), Horde_Imap_Client_Search_Query::DATE_BEFORE, $header=true, $not=false); 2328 $rangeValid = true; 2329 break; 2330 case 'ON': 2331 $imapRangeFilter->dateSearch(new DateTime($_criterias['date']), Horde_Imap_Client_Search_Query::DATE_ON, $header=true, $not=false); 2332 $rangeValid = true; 2333 break; 2334 } 2335 if ($rangeValid && !$queryValid) $queryValid=true; 2336 if ($rangeValid) $imapFilter->andSearch($imapRangeFilter); 2337 } 2338 if (self::$debug) 2339 { 2340 //$imapFilter->charset('UTF-8'); 2341 $query_str = $imapFilter->build(); 2342 //error_log(__METHOD__.' ('.__LINE__.') '.' '.$query_str['query'].' created by Criterias:'.(!is_array($_criterias)?" none -> returning":array2string($_criterias))); 2343 } 2344 if($queryValid==false) { 2345 $imapFilter->flag('DELETED', $set=false); 2346 return $imapFilter; 2347 } else { 2348 return $imapFilter; 2349 } 2350 } 2351 2352 /** 2353 * decode header (or envelope information) 2354 * if array given, note that only values will be converted 2355 * @param mixed $_string input to be converted, if array call decode_header recursively on each value 2356 * @param boolean|string $_tryIDNConversion (true/false AND 'FORCE'): try IDN Conversion on domainparts of emailADRESSES 2357 * @return mixed - based on the input type 2358 */ 2359 static function decode_header($_string, $_tryIDNConversion=false) 2360 { 2361 if (is_array($_string)) 2362 { 2363 foreach($_string as $k=>$v) 2364 { 2365 $_string[$k] = self::decode_header($v, $_tryIDNConversion); 2366 } 2367 return $_string; 2368 } 2369 else 2370 { 2371 $_string = Mail\Html::decodeMailHeader($_string,self::$displayCharset); 2372 $test = @json_encode($_string); 2373 //error_log(__METHOD__.__LINE__.' ->'.strlen($singleBodyPart['body']).' Error:'.json_last_error().'<- BodyPart:#'.$test.'#'); 2374 if (($test=="null" || $test === false || is_null($test)) && strlen($_string)>0) 2375 { 2376 // try to fix broken utf8 2377 $x = utf8_encode($_string); 2378 $test = @json_encode($x); 2379 if (($test=="null" || $test === false || is_null($test)) && strlen($_string)>0) 2380 { 2381 // this should not be needed, unless something fails with charset detection/ wrong charset passed 2382 $_string = (function_exists('mb_convert_encoding')?mb_convert_encoding($_string,'UTF-8','UTF-8'):(function_exists('iconv')?@iconv("UTF-8","UTF-8//IGNORE",$_string):$_string)); 2383 } 2384 else 2385 { 2386 $_string = $x; 2387 } 2388 } 2389 2390 if ($_tryIDNConversion===true && stripos($_string,'@')!==false) 2391 { 2392 $rfcAddr = self::parseAddressList($_string); 2393 $stringA = array(); 2394 foreach ($rfcAddr as $_rfcAddr) 2395 { 2396 if (!$_rfcAddr->valid) 2397 { 2398 $stringA = array(); 2399 break; // skip idna conversion if we encounter an error here 2400 } 2401 try { 2402 $stringA[] = imap_rfc822_write_address($_rfcAddr->mailbox,Horde_Idna::decode($_rfcAddr->host),$_rfcAddr->personal); 2403 } 2404 // if Idna conversation fails, leave address unchanged 2405 catch(\Exception $e) { 2406 unset($e); 2407 $stringA[] = imap_rfc822_write_address($_rfcAddr->mailbox, $_rfcAddr->host, $_rfcAddr->personal); 2408 } 2409 } 2410 if (!empty($stringA)) $_string = implode(',',$stringA); 2411 } 2412 if ($_tryIDNConversion==='FORCE') 2413 { 2414 //error_log(__METHOD__.' ('.__LINE__.') '.'->'.$_string.'='.Horde_Idna::decode($_string)); 2415 $_string = Horde_Idna::decode($_string); 2416 } 2417 return $_string; 2418 } 2419 } 2420 2421 /** 2422 * decode subject 2423 * if array given, note that only values will be converted 2424 * @param mixed $_string input to be converted, if array call decode_header recursively on each value 2425 * @param boolean $decode try decoding 2426 * @return mixed - based on the input type 2427 */ 2428 function decode_subject($_string,$decode=true) 2429 { 2430 #$string = $_string; 2431 if($_string=='NIL') 2432 { 2433 return 'No Subject'; 2434 } 2435 if ($decode) $_string = self::decode_header($_string); 2436 // make sure its utf-8 2437 $test = @json_encode($_string); 2438 if (($test=="null" || $test === false || is_null($test)) && strlen($_string)>0) 2439 { 2440 $_string = utf8_encode($_string); 2441 } 2442 return $_string; 2443 2444 } 2445 2446 /** 2447 * decodeEntityFolderName - remove html entities 2448 * @param string _folderName the foldername 2449 * @return string the converted string 2450 */ 2451 function decodeEntityFolderName($_folderName) 2452 { 2453 return html_entity_decode($_folderName, ENT_QUOTES, self::$displayCharset); 2454 } 2455 2456 /** 2457 * convert a mailboxname from utf7-imap to displaycharset 2458 * 2459 * @param string _folderName the foldername 2460 * @return string the converted string 2461 */ 2462 function encodeFolderName($_folderName) 2463 { 2464 return Translation::convert($_folderName, 'UTF7-IMAP', self::$displayCharset); 2465 } 2466 2467 /** 2468 * convert the foldername from display charset to UTF-7 2469 * 2470 * @param string _parent the parent foldername 2471 * @return ISO-8859-1 / UTF7-IMAP encoded string 2472 */ 2473 function _encodeFolderName($_folderName) { 2474 return Translation::convert($_folderName, self::$displayCharset, 'ISO-8859-1'); 2475 #return Translation::convert($_folderName, self::$displayCharset, 'UTF7-IMAP'); 2476 } 2477 2478 /** 2479 * create a new folder under given parent folder 2480 * 2481 * @param string _parent the parent foldername 2482 * @param string _folderName the new foldername 2483 * @param string _error pass possible error back to caller 2484 * 2485 * @return mixed name of the newly created folder or false on error 2486 */ 2487 function createFolder($_parent, $_folderName, &$_error) 2488 { 2489 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '."->"."$_parent, $_folderName called from:".function_backtrace()); 2490 $parent = $_parent;//$this->_encodeFolderName($_parent); 2491 $folderName = $_folderName;//$this->_encodeFolderName($_folderName); 2492 2493 if(empty($parent)) { 2494 $newFolderName = $folderName; 2495 } else { 2496 $HierarchyDelimiter = $this->getHierarchyDelimiter(); 2497 $newFolderName = $parent . $HierarchyDelimiter . $folderName; 2498 } 2499 if (empty($newFolderName)) return false; 2500 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.'->'.$newFolderName); 2501 if ($this->folderExists($newFolderName,true)) 2502 { 2503 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '." Folder $newFolderName already exists."); 2504 return $newFolderName; 2505 } 2506 try 2507 { 2508 $opts = array(); 2509 // if new created folder is a specal-use-folder, mark it as such, so other clients know to use it too 2510 if (isset(self::$specialUseFolders[$newFolderName])) 2511 { 2512 $opts['special_use'] = self::$specialUseFolders[$newFolderName]; 2513 } 2514 $this->icServer->createMailbox($newFolderName, $opts); 2515 } 2516 catch (\Exception $e) 2517 { 2518 $_error = lang('Could not create Folder %1 Reason: %2',$newFolderName,$e->getMessage()); 2519 error_log(__METHOD__.' ('.__LINE__.') '.' create Folder '.$newFolderName.'->'.$e->getMessage().' ('.$e->details.') Namespace:'.array2string($this->icServer->getNameSpaces()).function_backtrace()); 2520 return false; 2521 } 2522 try 2523 { 2524 $this->icServer->subscribeMailbox($newFolderName); 2525 } 2526 catch (\Exception $e) 2527 { 2528 error_log(__METHOD__.' ('.__LINE__.') '.' subscribe to new folder '.$newFolderName.'->'.$e->getMessage().' ('.$e->details); 2529 return false; 2530 } 2531 2532 return $newFolderName; 2533 } 2534 2535 /** 2536 * rename a folder 2537 * 2538 * @param string _oldFolderName the old foldername 2539 * @param string _parent the parent foldername 2540 * @param string _folderName the new foldername 2541 * 2542 * @return mixed name of the newly created folder or false on error 2543 * @throws Exception 2544 */ 2545 function renameFolder($_oldFolderName, $_parent, $_folderName) 2546 { 2547 $oldFolderName = $_oldFolderName;//$this->_encodeFolderName($_oldFolderName); 2548 $parent = $_parent;//$this->_encodeFolderName($_parent); 2549 $folderName = $_folderName;//$this->_encodeFolderName($_folderName); 2550 2551 if(empty($parent)) { 2552 $newFolderName = $folderName; 2553 } else { 2554 $HierarchyDelimiter = $this->getHierarchyDelimiter(); 2555 $newFolderName = $parent . $HierarchyDelimiter . $folderName; 2556 } 2557 if (self::$debug) error_log("create folder: $newFolderName"); 2558 try 2559 { 2560 $this->icServer->renameMailbox($oldFolderName, $newFolderName); 2561 } 2562 catch (\Exception $e) 2563 { 2564 throw new Exception(__METHOD__." failed for $oldFolderName (rename to: $newFolderName) with error:".$e->getMessage());; 2565 } 2566 // clear FolderExistsInfoCache 2567 Cache::setCache(Cache::INSTANCE,'email','icServerFolderExistsInfo'.trim($GLOBALS['egw_info']['user']['account_id']),$folderInfo,60*60*5); 2568 2569 return $newFolderName; 2570 2571 } 2572 2573 /** 2574 * delete an existing folder 2575 * 2576 * @param string _folderName the name of the folder to be deleted 2577 * 2578 * @return bool true on success, PEAR Error on failure 2579 * @throws Exception 2580 */ 2581 function deleteFolder($_folderName) 2582 { 2583 //$folderName = $this->_encodeFolderName($_folderName); 2584 try 2585 { 2586 $this->icServer->subscribeMailbox($_folderName,false); 2587 $this->icServer->deleteMailbox($_folderName); 2588 } 2589 catch (\Exception $e) 2590 { 2591 throw new Exception("Deleting Folder $_folderName failed! Error:".$e->getMessage());; 2592 } 2593 // clear FolderExistsInfoCache 2594 Cache::setCache(Cache::INSTANCE,'email','icServerFolderExistsInfo'.trim($GLOBALS['egw_info']['user']['account_id']),$folderInfo,60*60*5); 2595 2596 return true; 2597 } 2598 2599 /** 2600 * fetchUnSubscribedFolders: get unsubscribed IMAP folder list 2601 * 2602 * returns an array of unsubscribed IMAP folder names. 2603 * 2604 * @return array with folder names. eg.: 1 => INBOX/TEST 2605 */ 2606 function fetchUnSubscribedFolders() 2607 { 2608 $unSubscribedMailboxes = $this->icServer->listUnSubscribedMailboxes(); 2609 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($unSubscribedMailboxes)); 2610 return $unSubscribedMailboxes; 2611 } 2612 2613 /** 2614 * get IMAP folder objects 2615 * 2616 * returns an array of IMAP folder objects. Put INBOX folder in first 2617 * position. Preserves the folder seperator for later use. The returned 2618 * array is indexed using the foldername. Use cachedObjects when retrieving subscribedFolders 2619 * 2620 * @param boolean _subscribedOnly get subscribed or all folders 2621 * @param boolean _getCounters get get messages counters 2622 * @param boolean _alwaysGetDefaultFolders this triggers to ignore the possible notavailableautofolders - preference 2623 * as activeSync needs all folders like sent, trash, drafts, templates and outbox - if not present devices may crash 2624 * -> autoFolders should be created if needed / accessed (if possible and configured) 2625 * @param boolean _useCacheIfPossible - if set to false cache will be ignored and reinitialized 2626 * 2627 * @return array with folder objects. eg.: INBOX => {inbox object} 2628 */ 2629 function getFolderObjects($_subscribedOnly=false, $_getCounters=false, $_alwaysGetDefaultFolders=false,$_useCacheIfPossible=true) 2630 { 2631 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.' ServerID:'.$this->icServer->ImapServerId.", subscribedOnly:$_subscribedOnly, getCounters:$_getCounters, alwaysGetDefaultFolders:$_alwaysGetDefaultFolders, _useCacheIfPossible:$_useCacheIfPossible"); 2632 if (self::$debugTimes) $starttime = microtime (true); 2633 static $folders2return; 2634 //$_subscribedOnly=false; 2635 // always use static on single request if info is available; 2636 // so if you require subscribed/unsubscribed results on a single request you MUST 2637 // set $_useCacheIfPossible to false ! 2638 if ($_useCacheIfPossible && isset($folders2return[$this->icServer->ImapServerId]) && !empty($folders2return[$this->icServer->ImapServerId])) 2639 { 2640 if (self::$debugTimes) self::logRunTimes($starttime,null,'using static',__METHOD__.' ('.__LINE__.') '); 2641 return $folders2return[$this->icServer->ImapServerId]; 2642 } 2643 2644 if ($_subscribedOnly && $_getCounters===false) 2645 { 2646 if (is_null($folders2return)) $folders2return = Cache::getCache(Cache::INSTANCE,'email','folderObjects'.trim($GLOBALS['egw_info']['user']['account_id']),null,array(),60*60*1); 2647 if ($_useCacheIfPossible && isset($folders2return[$this->icServer->ImapServerId]) && !empty($folders2return[$this->icServer->ImapServerId])) 2648 { 2649 //error_log(__METHOD__.' ('.__LINE__.') '.' using Cached folderObjects'.array2string($folders2return[$this->icServer->ImapServerId])); 2650 if (self::$debugTimes) self::logRunTimes($starttime,null,'from Cache',__METHOD__.' ('.__LINE__.') '); 2651 return $folders2return[$this->icServer->ImapServerId]; 2652 } 2653 } 2654 // use $folderBasicInfo for holding attributes and other basic folderinfo $folderBasicInfo[$this->icServer->ImapServerId] 2655 static $folderBasicInfo; 2656 if (is_null($folderBasicInfo)||!isset($folderBasicInfo[$this->icServer->ImapServerId])) $folderBasicInfo = Cache::getCache(Cache::INSTANCE,'email','folderBasicInfo'.trim($GLOBALS['egw_info']['user']['account_id']),null,array(),60*60*1); 2657 //error_log(__METHOD__.' ('.__LINE__.') '.array2string(array_keys($folderBasicInfo[$this->icServer->ImapServerId]))); 2658 2659 $delimiter = $this->getHierarchyDelimiter(); 2660 2661 $inboxData = new \stdClass; 2662 $inboxData->name = 'INBOX'; 2663 $inboxData->folderName = 'INBOX'; 2664 $inboxData->displayName = lang('INBOX'); 2665 $inboxData->delimiter = $delimiter; 2666 $inboxData->shortFolderName = 'INBOX'; 2667 $inboxData->shortDisplayName = lang('INBOX'); 2668 $inboxData->subscribed = true; 2669 if($_getCounters == true) { 2670 $inboxData->counter = $this->getMailBoxCounters('INBOX'); 2671 } 2672 // force unsubscribed by preference showAllFoldersInFolderPane 2673 if ($_subscribedOnly == true && 2674 isset($this->mailPreferences['showAllFoldersInFolderPane']) && 2675 $this->mailPreferences['showAllFoldersInFolderPane']==1) 2676 { 2677 $_subscribedOnly = false; 2678 } 2679 $inboxFolderObject = array('INBOX' => $inboxData); 2680 2681 //$nameSpace = $this->icServer->getNameSpaces(); 2682 $nameSpace = $this->_getNameSpaces(); 2683 $fetchedAllInOneGo = false; 2684 $subscribedFoldersForCache = $foldersNameSpace = array(); 2685 //error_log(__METHOD__.__LINE__.array2string($nameSpace)); 2686 if (is_array($nameSpace)) 2687 { 2688 foreach($nameSpace as $k => $singleNameSpace) { 2689 $type = $singleNameSpace['type']; 2690 // the following line (assumption that for the same namespace the delimiter should be equal) may be wrong 2691 $foldersNameSpace[$type]['delimiter'] = $singleNameSpace['delimiter']; 2692 2693 if(is_array($singleNameSpace)&&$fetchedAllInOneGo==false) { 2694 // fetch and sort the subscribed folders 2695 // we alway fetch the subscribed, as this provides the only way to tell 2696 // if a folder is subscribed or not 2697 if ($_subscribedOnly == true) 2698 { 2699 try 2700 { 2701 $subscribedMailboxes = $this->icServer->listSubscribedMailboxes('',0,true); 2702 if (!empty($subscribedMailboxes)) 2703 { 2704 $fetchedAllInOneGo = true; 2705 } 2706 else 2707 { 2708 $subscribedMailboxes = $this->icServer->listSubscribedMailboxes($singleNameSpace['prefix'],0,true); 2709 } 2710 } 2711 catch(Exception $e) 2712 { 2713 continue; 2714 } 2715 //echo "subscribedMailboxes";_debug_array($subscribedMailboxes); 2716 $subscribedFoldersPerNS = (!empty($subscribedMailboxes)?array_keys($subscribedMailboxes):array()); 2717 //if (is_array($foldersNameSpace[$type]['subscribed'])) sort($foldersNameSpace[$type]['subscribed']); 2718 //_debug_array($foldersNameSpace); 2719 //error_log(__METHOD__.__LINE__.array2string($singleNameSpace).':#:'.array2string($subscribedFoldersPerNS)); 2720 if (!empty($subscribedFoldersPerNS) && !empty($subscribedMailboxes)) 2721 { 2722 //error_log(__METHOD__.' ('.__LINE__.') '." $type / subscribed:". array2string($subscribedMailboxes)); 2723 foreach ($subscribedMailboxes as $k => $finfo) 2724 { 2725 //error_log(__METHOD__.__LINE__.$k.':#:'.array2string($finfo)); 2726 $subscribedFoldersForCache[$this->icServer->ImapServerId][$k]= 2727 $folderBasicInfo[$this->icServer->ImapServerId][$k]=array( 2728 'MAILBOX'=>$finfo['MAILBOX'], 2729 'ATTRIBUTES'=>$finfo['ATTRIBUTES'], 2730 'delimiter'=>$finfo['delimiter'],//lowercase for some reason??? 2731 'SUBSCRIBED'=>$finfo['SUBSCRIBED'],//seeded by getMailboxes 2732 ); 2733 if (empty($foldersNameSpace[$type]['subscribed']) || !in_array($k,$foldersNameSpace[$type]['subscribed'])) 2734 { 2735 $foldersNameSpace[$type]['subscribed'][] = $k; 2736 } 2737 if (empty($foldersNameSpace[$type]['all']) || !in_array($k,$foldersNameSpace[$type]['all'])) 2738 { 2739 $foldersNameSpace[$type]['all'][] = $k; 2740 } 2741 } 2742 } 2743 //error_log(__METHOD__.' ('.__LINE__.') '.' '.$type.'->'.array2string($foldersNameSpace[$type]['subscribed'])); 2744 if (!is_array($foldersNameSpace[$type]['all'])) $foldersNameSpace[$type]['all'] = array(); 2745 if ($_subscribedOnly == true && !empty($foldersNameSpace[$type]['subscribed'])) { 2746 continue; 2747 } 2748 2749 } 2750 2751 // fetch and sort all folders 2752 //echo $type.'->'.$singleNameSpace['prefix'].'->'.($type=='shared'?0:2)."<br>"; 2753 try 2754 { 2755 // calling with 2 lists all mailboxes on that level with fetches all 2756 // we switch to all, to avoid further calls for subsequent levels 2757 // that may produce problems, when encountering recursions probably 2758 // horde is handling that, so we do not; keep that in mind! 2759 //$allMailboxesExt = $this->icServer->getMailboxes($singleNameSpace['prefix'],2,true); 2760 $allMailboxesExt = $this->icServer->getMailboxes($singleNameSpace['prefix'],0,true); 2761 } 2762 catch (\Exception $e) 2763 { 2764 error_log(__METHOD__.' ('.__LINE__.') '.' Failed to retrieve all Boxes:'.$e->getMessage()); 2765 $allMailboxesExt = array(); 2766 } 2767 if (!is_array($allMailboxesExt)) 2768 { 2769 //error_log(__METHOD__.' ('.__LINE__.') '.' Expected Array but got:'.array2string($allMailboxesExt). 'Type:'.$type.' Prefix:'.$singleNameSpace['prefix']); 2770 continue; 2771 //$allMailboxesExt=array(); 2772 } 2773 2774 //error_log(__METHOD__.' ('.__LINE__.') '.' '.$type.'->'.array2string($allMailboxesExt)); 2775 foreach ($allMailboxesExt as $mbx) { 2776 if (!isset($folderBasicInfo[$this->icServer->ImapServerId][$mbx['MAILBOX']])) 2777 { 2778 $folderBasicInfo[$this->icServer->ImapServerId][$mbx['MAILBOX']]=array( 2779 'MAILBOX'=>$mbx['MAILBOX'], 2780 'ATTRIBUTES'=>$mbx['ATTRIBUTES'], 2781 'delimiter'=>$mbx['delimiter'],//lowercase for some reason??? 2782 'SUBSCRIBED'=>$mbx['SUBSCRIBED'],//seeded by getMailboxes 2783 ); 2784 if ($mbx['SUBSCRIBED'] && !isset($subscribedFoldersForCache[$this->icServer->ImapServerId][$mbx['MAILBOX']])) 2785 { 2786 $subscribedFoldersForCache[$this->icServer->ImapServerId][$mbx['MAILBOX']] = $folderBasicInfo[$this->icServer->ImapServerId][$mbx['MAILBOX']]; 2787 } 2788 } 2789 if ($mbx['SUBSCRIBED'] && (empty($foldersNameSpace[$type]['subscribed']) || !in_array($mbx['MAILBOX'],$foldersNameSpace[$type]['subscribed']))) 2790 { 2791 $foldersNameSpace[$type]['subscribed'][] = $mbx['MAILBOX']; 2792 } 2793 //echo __METHOD__;_debug_array($mbx); 2794 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($mbx)); 2795 if (isset($allMailBoxesExtSorted[$mbx['MAILBOX']])|| 2796 isset($allMailBoxesExtSorted[$mbx['MAILBOX'].$foldersNameSpace[$type]['delimiter']])|| 2797 (substr($mbx['MAILBOX'],-1)==$foldersNameSpace[$type]['delimiter'] && isset($allMailBoxesExtSorted[substr($mbx['MAILBOX'],0,-1)])) 2798 ) continue; 2799 2800 //echo '#'.$mbx['MAILBOX'].':'.array2string($mbx)."#<br>"; 2801 $allMailBoxesExtSorted[$mbx['MAILBOX']] = $mbx; 2802 } 2803 if (is_array($allMailBoxesExtSorted)) ksort($allMailBoxesExtSorted); 2804 //_debug_array(array_keys($allMailBoxesExtSorted)); 2805 $allMailboxes = array(); 2806 foreach ((array)$allMailBoxesExtSorted as $mbx) { 2807 if (!in_array($mbx['MAILBOX'],$allMailboxes)) $allMailboxes[] = $mbx['MAILBOX']; 2808 //echo "Result:";_debug_array($allMailboxes); 2809 } 2810 $foldersNameSpace[$type]['all'] = $allMailboxes; 2811 if (is_array($foldersNameSpace[$type]['all'])) sort($foldersNameSpace[$type]['all']); 2812 } 2813 } 2814 } 2815 //subscribed folders may be used in getFolderStatus 2816 Cache::setCache(Cache::INSTANCE,'email','subscribedFolders'.trim($GLOBALS['egw_info']['user']['account_id']),$subscribedFoldersForCache,$expiration=60*60*1); 2817 //echo "<br>FolderNameSpace To Process:";_debug_array($foldersNameSpace); 2818 $autoFolderObjects = $folders = array(); 2819 $autofolder_exists = array(); 2820 foreach( array('personal', 'others', 'shared') as $type) { 2821 if(isset($foldersNameSpace[$type])) { 2822 if($_subscribedOnly) { 2823 if( !empty($foldersNameSpace[$type]['subscribed']) ) $listOfFolders = $foldersNameSpace[$type]['subscribed']; 2824 } else { 2825 if( !empty($foldersNameSpace[$type]['all'])) $listOfFolders = $foldersNameSpace[$type]['all']; 2826 } 2827 foreach((array)$listOfFolders as $folderName) { 2828 //echo "<br>FolderToCheck:$folderName<br>"; 2829 //error_log(__METHOD__.__LINE__.'#Delimiter:'.$delimiter.':#'.$folderName); 2830 if ($_subscribedOnly && empty($foldersNameSpace[$type]['all'])) continue;//when subscribedonly, we fetch all folders in one go. 2831 if($_subscribedOnly && !(in_array($folderName, $foldersNameSpace[$type]['all'])||in_array($folderName.$foldersNameSpace[$type]['delimiter'], $foldersNameSpace[$type]['all']))) { 2832 #echo "$folderName failed to be here <br>"; 2833 continue; 2834 } 2835 if (isset($folders[$folderName])) continue; 2836 if (isset($autoFolderObjects[$folderName])) continue; 2837 if (empty($delimiter)||$delimiter != $foldersNameSpace[$type]['delimiter']) $delimiter = $foldersNameSpace[$type]['delimiter']; 2838 $folderParts = explode($delimiter, $folderName); 2839 $shortName = array_pop($folderParts); 2840 2841 $folderObject = new \stdClass; 2842 $folderObject->delimiter = $delimiter; 2843 $folderObject->folderName = $folderName; 2844 $folderObject->shortFolderName = $shortName; 2845 if(!$_subscribedOnly) { 2846 #echo $folderName."->".$type."<br>"; 2847 #_debug_array($foldersNameSpace[$type]['subscribed']); 2848 $folderObject->subscribed = in_array($folderName, (array)$foldersNameSpace[$type]['subscribed']); 2849 } 2850 2851 if($_getCounters == true) { 2852 //error_log(__METHOD__.' ('.__LINE__.') '.' getCounter forFolder:'.$folderName); 2853 $folderObject->counter = $this->getMailBoxCounters($folderName); 2854 } 2855 if(strtoupper($folderName) == 'INBOX') { 2856 $folderName = 'INBOX'; 2857 $folderObject->folderName = 'INBOX'; 2858 $folderObject->shortFolderName = 'INBOX'; 2859 $folderObject->displayName = lang('INBOX'); 2860 $folderObject->shortDisplayName = lang('INBOX'); 2861 $folderObject->subscribed = true; 2862 // translate the automatic Folders (Sent, Drafts, ...) like the INBOX 2863 } elseif (in_array($shortName,self::$autoFolders)) { 2864 $tmpfolderparts = explode($delimiter,$folderObject->folderName); 2865 array_pop($tmpfolderparts); 2866 $folderObject->displayName = implode($delimiter,$tmpfolderparts).$delimiter.lang($shortName); 2867 $folderObject->shortDisplayName = lang($shortName); 2868 unset($tmpfolderparts); 2869 } else { 2870 $folderObject->displayName = $folderObject->folderName; 2871 $folderObject->shortDisplayName = $shortName; 2872 } 2873 //$folderName = $folderName; 2874 if (in_array($shortName,self::$autoFolders)&&self::searchValueInFolderObjects($shortName,$autoFolderObjects)===false) { 2875 $autoFolderObjects[$folderName] = $folderObject; 2876 } else { 2877 $folders[$folderName] = $folderObject; 2878 } 2879 //error_log(__METHOD__.' ('.__LINE__.') '.':'.$folderObject->folderName); 2880 if (!isset(self::$specialUseFolders)) $this->getSpecialUseFolders (); 2881 if (isset(self::$specialUseFolders[$folderName])) 2882 { 2883 $autofolder_exists[$folderName] = self::$specialUseFolders[$folderName]; 2884 } 2885 } 2886 } 2887 } 2888 if (is_array($autoFolderObjects) && !empty($autoFolderObjects)) { 2889 uasort($autoFolderObjects,array($this,"sortByAutoFolderPos")); 2890 } 2891 // check if some standard folders are missing and need to be created 2892 if (count($autofolder_exists) < count(self::$autoFolders) && $this->check_create_autofolders($autofolder_exists)) 2893 { 2894 // if new folders have been created, re-read folders ignoring the cache 2895 return $this->getFolderObjects($_subscribedOnly, $_getCounters, $_alwaysGetDefaultFolders, false); // false = do NOT use cache 2896 } 2897 if (is_array($folders)) uasort($folders,array($this,"sortByDisplayName")); 2898 //$folders2return = array_merge($autoFolderObjects,$folders); 2899 //_debug_array($folders2return); #exit; 2900 $folders2return[$this->icServer->ImapServerId] = array_merge((array)$inboxFolderObject,(array)$autoFolderObjects,(array)$folders); 2901 if (($_subscribedOnly && $_getCounters===false) || 2902 ($_subscribedOnly == false && $_getCounters===false && 2903 isset($this->mailPreferences['showAllFoldersInFolderPane']) && 2904 $this->mailPreferences['showAllFoldersInFolderPane']==1)) 2905 { 2906 Cache::setCache(Cache::INSTANCE,'email','folderObjects'.trim($GLOBALS['egw_info']['user']['account_id']),$folders2return,$expiration=60*60*1); 2907 } 2908 Cache::setCache(Cache::INSTANCE,'email','folderBasicInfo'.trim($GLOBALS['egw_info']['user']['account_id']),$folderBasicInfo,$expiration=60*60*1); 2909 if (self::$debugTimes) self::logRunTimes($starttime,null,function_backtrace(),__METHOD__.' ('.__LINE__.') '); 2910 return $folders2return[$this->icServer->ImapServerId]; 2911 } 2912 2913 /** 2914 * Get IMAP folders for a mailbox 2915 * 2916 * @param string $_nodePath = null folder name to fetch from IMAP, 2917 * null means all folders 2918 * @param boolean $_onlyTopLevel if set to true only top level objects 2919 * will be return and nodePath would be ignored 2920 * @param int $_search = 2 search restriction in given mailbox 2921 * 0:All folders recursively from the $_nodePath 2922 * 1:Only folder of specified $_nodePath 2923 * 2:All folders of $_nodePath in the same heirachy level 2924 * 2925 * @param boolean $_subscribedOnly = false Command to fetch only the subscribed folders 2926 * @param boolean $_getCounter = false Command to fetch mailbox counter 2927 * 2928 * @return array arrays of folders 2929 */ 2930 function getFolderArrays ($_nodePath = null, $_onlyTopLevel = false, $_search= 2, $_subscribedOnly = false, $_getCounter = false) 2931 { 2932 // delimiter 2933 $delimiter = $this->getHierarchyDelimiter(); 2934 2935 $folders = $nameSpace = array(); 2936 $nameSpaceTmp = $this->_getNameSpaces(); 2937 foreach($nameSpaceTmp as $k => $singleNameSpace) { 2938 $nameSpace[$singleNameSpace['type']]=$singleNameSpace; 2939 } 2940 unset($nameSpaceTmp); 2941 2942 //error_log(__METHOD__.__LINE__.array2string($nameSpace)); 2943 // Get special use folders 2944 if (!isset(self::$specialUseFolders)) $this->getSpecialUseFolders (); // Set self::$specialUseFolders 2945 // topLevelQueries generally ignore the $_search param. Except for Config::examineNamespace 2946 if ($_onlyTopLevel) // top level leaves 2947 { 2948 // Get top mailboxes of icServer 2949 $topFolders = $this->icServer->getMailboxes("", 2, true); 2950 // Trigger examination of namespace to retrieve 2951 // folders located in other and shared; needed only for some servers 2952 if (is_null(self::$mailConfig)) self::$mailConfig = Config::read('mail'); 2953 if (self::$mailConfig['examineNamespace']) 2954 { 2955 $prefixes=array(); 2956 if (is_array($nameSpace)) 2957 { 2958 foreach($nameSpace as $k => $singleNameSpace) { 2959 $type = $singleNameSpace['type']; 2960 2961 if(is_array($singleNameSpace) && $singleNameSpace['prefix']){ 2962 $prefixes[$type] = $singleNameSpace['prefix']; 2963 //regard extra care for nameSpacequeries when configured AND respect $_search 2964 $result = $this->icServer->getMailboxes($singleNameSpace['prefix'], $_search==0?0:2, true); 2965 if (is_array($result)) 2966 { 2967 ksort($result); 2968 $topFolders = array_merge($topFolders,$result); 2969 } 2970 } 2971 } 2972 } 2973 } 2974 2975 $autofolders = array(); 2976 2977 foreach(self::$specialUseFolders as $path => $folder) 2978 { 2979 if ($this->folderExists($path)) 2980 { 2981 $autofolders[$folder] = $folder; 2982 } 2983 } 2984 // Check if the special use folders are there, otherwise try to create them 2985 if (count($autofolders) < count(self::$autoFolders) && $this->check_create_autofolders ($autofolders)) 2986 { 2987 return $this->getFolderArrays ($_nodePath, $_onlyTopLevel, $_search, $_subscribedOnly, $_getCounter); 2988 } 2989 2990 // now process topFolders for next level 2991 foreach ($topFolders as &$node) 2992 { 2993 $pattern = "/\\".$delimiter."/"; 2994 $reference = preg_replace($pattern, '', $node['MAILBOX']); 2995 if(!empty($prefixes)) 2996 { 2997 $reference = ''; 2998 $tmpArray = explode($delimiter,$node['MAILBOX']); 2999 foreach($tmpArray as $p) 3000 { 3001 $reference = empty($reference)?$p:$reference.$delimiter.$p; 3002 } 3003 } 3004 $mainFolder = $subFolders = array(); 3005 3006 if ($_subscribedOnly) 3007 { 3008 $mainFolder = $this->icServer->listSubscribedMailboxes($reference, 1, true); 3009 $subFolders = $this->icServer->listSubscribedMailboxes($node['MAILBOX'].$node['delimiter'], $_search, true); 3010 } 3011 else 3012 { 3013 $mainFolder = $this->icServer->getMailboxes($reference, 1, true); 3014 $subFolders = $this->icServer->getMailboxes($node['MAILBOX'].$node['delimiter'], $_search, true); 3015 } 3016 3017 if (is_array($mainFolder['INBOX'])) 3018 { 3019 // Array container of auto folders 3020 $aFolders = array(); 3021 3022 // Array container of non auto folders 3023 $nFolders = array(); 3024 3025 foreach ((array)$subFolders as $path => $folder) 3026 { 3027 $folderInfo = self::pathToFolderData($folder['MAILBOX'], $folder['delimiter']); 3028 if (in_array(trim($folderInfo['name']), $autofolders) || in_array(trim($folderInfo['name']), self::$autoFolders)) 3029 { 3030 $aFolders [$path] = $folder; 3031 } 3032 else 3033 { 3034 $nFolders [$path] = $folder; 3035 } 3036 } 3037 if (is_array($aFolders)) uasort ($aFolders, array($this,'sortByAutofolder')); 3038 //ksort($aFolders); 3039 3040 // Sort none auto folders base on mailbox name 3041 uasort($nFolders,array($this,'sortByMailbox')); 3042 3043 $subFolders = array_merge($aFolders,$nFolders); 3044 } 3045 else 3046 { 3047 if (is_array($subFolders)) ksort($subFolders); 3048 } 3049 $folders = array_merge($folders,(array)$mainFolder, (array)$subFolders); 3050 } 3051 } 3052 elseif ($_nodePath) // single node 3053 { 3054 switch ($_search) 3055 { 3056 // Including children 3057 case 0: 3058 case 2: 3059 $path = $_nodePath.''.$delimiter; 3060 break; 3061 // Node itself 3062 // shouldn't contain next level delimiter 3063 case 1: 3064 $path = $_nodePath; 3065 break; 3066 } 3067 if ($_subscribedOnly) 3068 { 3069 $folders = $this->icServer->listSubscribedMailboxes($path, $_search, true); 3070 } 3071 else 3072 { 3073 $folders = $this->icServer->getMailboxes($path, $_search, true); 3074 } 3075 3076 uasort($folders,array($this,'sortByMailbox'));//ksort($folders); 3077 } 3078 elseif(!$_nodePath) // all 3079 { 3080 if ($_subscribedOnly) 3081 { 3082 $folders = $this->icServer->listSubscribedMailboxes('', 0, true); 3083 } 3084 else 3085 { 3086 $folders = $this->icServer->getMailboxes('', 0, true); 3087 } 3088 } 3089 // only sort (autofolders, shared, others ...) when retrieving all folders or toplevelquery 3090 if ($_onlyTopLevel || !$_nodePath) 3091 { 3092 // SORTING FOLDERS 3093 //self::$debugTimes=true; 3094 if (self::$debugTimes) $starttime = microtime (true); 3095 // Merge of all auto folders and specialusefolders 3096 $autoFoldersTmp = array_unique((array_merge(self::$autoFolders, array_values(self::$specialUseFolders)))); 3097 uasort($folders,array($this,'sortByMailbox'));//ksort($folders); 3098 $tmpFolders = $folders; 3099 $inboxFolderObject=$inboxSubFolderObjects=$autoFolderObjects=$typeFolderObject=$mySpecialUseFolders=array(); 3100 $googleMailFolderObject=$googleAutoFolderObjects=$googleSubFolderObjects=array(); 3101 $isGoogleMail=false; 3102 foreach($autoFoldersTmp as $afk=>$aF) 3103 { 3104 if (!isset($mySpecialUseFolders[$aF]) && $aF) $mySpecialUseFolders[$aF]=$this->getFolderByType($aF,false); 3105 //error_log($afk.':'.$aF.'->'.$mySpecialUseFolders[$aF]); 3106 } 3107 //error_log(array2string($mySpecialUseFolders)); 3108 foreach ($tmpFolders as $k => $f) { 3109 $sorted=false; 3110 if (strtoupper(substr($k,0,5))=='INBOX') { 3111 if (strtoupper($k)=='INBOX') { 3112 //error_log(__METHOD__.__LINE__.':'.strtoupper(substr($k,0,5)).':'.$k); 3113 $inboxFolderObject[$k]=$f; 3114 unset($folders[$k]); 3115 $sorted=true; 3116 } else { 3117 $isAutoFolder=false; 3118 foreach($autoFoldersTmp as $afk=>$aF) 3119 { 3120 //error_log(__METHOD__.__LINE__.$k.':'.$aF.'->'.$mySpecialUseFolders[$aF]); 3121 if($aF && strlen($mySpecialUseFolders[$aF])&&/*strlen($k)>=strlen($mySpecialUseFolders[$aF])&&*/ 3122 ($mySpecialUseFolders[$aF]==$k || substr($k,0,strlen($mySpecialUseFolders[$aF].$delimiter))==$mySpecialUseFolders[$aF].$delimiter || //k may be child of an autofolder 3123 stristr($mySpecialUseFolders[$aF],$k.$delimiter)!==false)) // k is parent of an autofolder 3124 { 3125 //error_log(__METHOD__.__LINE__.$k.'->'.$mySpecialUseFolders[$aF]); 3126 $isAutoFolder=true; 3127 $autoFolderObjects[$k]=$f; 3128 break; 3129 } 3130 } 3131 if ($isAutoFolder==false) $inboxSubFolderObjects[$k]=$f; 3132 unset($folders[$k]); 3133 $sorted=true; 3134 } 3135 } elseif (strtoupper(substr($k,0,13))=='[GOOGLE MAIL]') { 3136 $isGoogleMail=true; 3137 if (strtoupper($k)=='[GOOGLE MAIL]') { 3138 $googleMailFolderObject[$k]=$f; 3139 unset($folders[$k]); 3140 $sorted=true; 3141 } else { 3142 $isAutoFolder=false; 3143 foreach($autoFoldersTmp as $afk=>$aF) 3144 { 3145 //error_log($k.':'.$aF.'->'.$mySpecialUseFolders[$aF]); 3146 if($aF && strlen($mySpecialUseFolders[$aF])&&/*strlen($k)>=strlen($mySpecialUseFolders[$aF])&&*/ 3147 ($mySpecialUseFolders[$aF]==$k || substr($k,0,strlen($mySpecialUseFolders[$aF].$delimiter))==$mySpecialUseFolders[$aF].$delimiter|| //k may be child of an autofolder 3148 stristr($mySpecialUseFolders[$aF],$k.$delimiter)!==false)) // k is parent of an autofolder 3149 { 3150 //error_log($k.'->'.$mySpecialUseFolders[$aF]); 3151 $isAutoFolder=true; 3152 $googleAutoFolderObjects[$k]=$f; 3153 break; 3154 } 3155 } 3156 if ($isAutoFolder==false) $googleSubFolderObjects[$k]=$f; 3157 unset($folders[$k]); 3158 $sorted=true; 3159 } 3160 } else { 3161 $isAutoFolder=false; 3162 foreach($autoFoldersTmp as $afk=>$aF) 3163 { 3164 //error_log($k.':'.$aF.'->'.$mySpecialUseFolders[$aF]); 3165 if($aF && strlen($mySpecialUseFolders[$aF])&&/*strlen($k)>=strlen($mySpecialUseFolders[$aF])&&*/ 3166 ($mySpecialUseFolders[$aF]==$k || substr($k,0,strlen($mySpecialUseFolders[$aF].$delimiter))==$mySpecialUseFolders[$aF].$delimiter|| //k may be child of an autofolder 3167 stristr($mySpecialUseFolders[$aF],$k.$delimiter)!==false)) // k is parent of an autofolder 3168 { 3169 //error_log($k.'->'.$mySpecialUseFolders[$aF]); 3170 $isAutoFolder=true; 3171 $autoFolderObjects[$k]=$f; 3172 unset($folders[$k]); 3173 $sorted=true; 3174 break; 3175 } 3176 } 3177 } 3178 3179 if ($sorted==false) 3180 { 3181 foreach(array('others','shared') as $type) 3182 { 3183 if ($nameSpace[$type]['prefix_present']&&$nameSpace[$type]['prefix']) 3184 { 3185 if (substr($k,0,strlen($nameSpace[$type]['prefix']))==$nameSpace[$type]['prefix']|| 3186 substr($k,0,strlen($nameSpace[$type]['prefix'])-strlen($nameSpace[$type]['delimiter']))==substr($nameSpace[$type]['prefix'],0,strlen($nameSpace[$type]['delimiter'])*-1)) { 3187 //error_log(__METHOD__.__LINE__.':'.substr($k,0,strlen($nameSpace[$type]['prefix'])).':'.$k); 3188 $typeFolderObject[$type][$k]=$f; 3189 unset($folders[$k]); 3190 } 3191 } 3192 } 3193 } 3194 } 3195 //error_log(__METHOD__.__LINE__.array2string($autoFolderObjects)); 3196 // avoid calling sortByAutoFolder as it is not regarding subfolders 3197 $autoFolderObjectsTmp = $autoFolderObjects; 3198 unset($autoFolderObjects); 3199 uasort($autoFolderObjectsTmp, array($this,'sortByMailbox')); 3200 foreach($autoFoldersTmp as $afk=>$aF) 3201 { 3202 foreach($autoFolderObjectsTmp as $k => $f) 3203 { 3204 if($aF && ($mySpecialUseFolders[$aF]==$k || 3205 substr($k,0,strlen($mySpecialUseFolders[$aF].$delimiter))==$mySpecialUseFolders[$aF].$delimiter || 3206 stristr($mySpecialUseFolders[$aF],$k.$delimiter)!==false)) 3207 { 3208 $autoFolderObjects[$k]=$f; 3209 } 3210 } 3211 } 3212 //error_log(__METHOD__.__LINE__.array2string($autoFolderObjects)); 3213 if (!$isGoogleMail) { 3214 $folders = array_merge($inboxFolderObject,$autoFolderObjects,(array)$inboxSubFolderObjects,(array)$folders,(array)$typeFolderObject['others'],(array)$typeFolderObject['shared']); 3215 } else { 3216 // avoid calling sortByAutoFolder as it is not regarding subfolders 3217 $gAutoFolderObjectsTmp = $googleAutoFolderObjects; 3218 unset($googleAutoFolderObjects); 3219 uasort($gAutoFolderObjectsTmp, array($this,'sortByMailbox')); 3220 foreach($autoFoldersTmp as $afk=>$aF) 3221 { 3222 foreach($gAutoFolderObjectsTmp as $k => $f) 3223 { 3224 if($aF && ($mySpecialUseFolders[$aF]==$k || substr($k,0,strlen($mySpecialUseFolders[$aF].$delimiter))==$mySpecialUseFolders[$aF].$delimiter)) 3225 { 3226 $googleAutoFolderObjects[$k]=$f; 3227 } 3228 } 3229 } 3230 $folders = array_merge($inboxFolderObject,$autoFolderObjects,(array)$folders,(array)$googleMailFolderObject,$googleAutoFolderObjects,$googleSubFolderObjects,(array)$typeFolderObject['others'],(array)$typeFolderObject['shared']); 3231 } 3232 if (self::$debugTimes) self::logRunTimes($starttime,null,function_backtrace(),__METHOD__.' ('.__LINE__.') Sorting:'); 3233 //self::$debugTimes=false; 3234 } 3235 // Get counter information and add them to each fetched folders array 3236 // TODO: do not fetch counters for user .... as in shared / others 3237 if ($_getCounter) 3238 { 3239 foreach ($folders as &$folder) 3240 { 3241 $folder['counter'] = $this->icServer->getMailboxCounters($folder['MAILBOX']); 3242 } 3243 } 3244 return $folders; 3245 } 3246 3247 3248 /** 3249 * Check if all automatic folders exist and create them if not 3250 * 3251 * @param array $autofolders_exists existing folders, no need to check their existance again 3252 * @return int number of new folders created 3253 */ 3254 function check_create_autofolders(array $autofolders_exists=array()) 3255 { 3256 $num_created = 0; 3257 foreach(self::$autoFolders as $folder) 3258 { 3259 $created = false; 3260 if (!in_array($folder, $autofolders_exists) && $this->_getSpecialUseFolder($folder, true, $created) && 3261 $created && $folder != 'Outbox') 3262 { 3263 $num_created++; 3264 } 3265 } 3266 return $num_created; 3267 } 3268 3269 /** 3270 * search Value In FolderObjects 3271 * 3272 * Helper function to search for a specific value within the foldertree objects 3273 * @param string $needle 3274 * @param array $haystack array of folderobjects 3275 * @return MIXED false or key 3276 */ 3277 static function searchValueInFolderObjects($needle, $haystack) 3278 { 3279 $rv = false; 3280 foreach ($haystack as $k => $v) 3281 { 3282 foreach($v as &$sv) {if (trim($sv)==trim($needle)) return $k;} 3283 } 3284 return $rv; 3285 } 3286 3287 /** 3288 * sortByMailbox 3289 * 3290 * Helper function to sort folders array by mailbox 3291 * @param array $a 3292 * @param array $b array of folders 3293 * @return int expect values (0, 1 or -1) 3294 */ 3295 function sortByMailbox($a,$b) 3296 { 3297 return strcasecmp($a['MAILBOX'],$b['MAILBOX']); 3298 } 3299 3300 /** 3301 * Get folder data from path 3302 * 3303 * @param string $_path a node path 3304 * @param string $_hDelimiter hierarchy delimiter 3305 * @return array returns an array of data extracted from given node path 3306 */ 3307 static function pathToFolderData ($_path, $_hDelimiter) 3308 { 3309 if (!strpos($_path, self::DELIMITER)) $_path = self::DELIMITER.$_path; 3310 list(,$path) = explode(self::DELIMITER, $_path); 3311 $path_chain = $parts = explode($_hDelimiter, $path); 3312 $name = array_pop($parts); 3313 return array ( 3314 'name' => $name, 3315 'mailbox' => $path, 3316 'parent' => implode($_hDelimiter, $parts), 3317 'text' => $name, 3318 'tooltip' => $name, 3319 'path' => $path_chain 3320 ); 3321 } 3322 3323 /** 3324 * sortByAutoFolder 3325 * 3326 * Helper function to sort folder-objects by auto Folder Position 3327 * @param array $_a 3328 * @param array $_b 3329 * @return int expect values (0, 1 or -1) 3330 */ 3331 function sortByAutoFolder($_a, $_b) 3332 { 3333 // 0, 1 und -1 3334 $a = self::pathToFolderData($_a['MAILBOX'], $_a['delimiter']); 3335 $b = self::pathToFolderData($_b['MAILBOX'], $_b['delimiter']); 3336 $pos1 = array_search(trim($a['name']),self::$autoFolders); 3337 $pos2 = array_search(trim($b['name']),self::$autoFolders); 3338 if ($pos1 == $pos2) return 0; 3339 return ($pos1 < $pos2) ? -1 : 1; 3340 } 3341 3342 /** 3343 * sortByDisplayName 3344 * 3345 * Helper function to sort folder-objects by displayname 3346 * @param object $a 3347 * @param object $b array of folderobjects 3348 * @return int expect values (0, 1 or -1) 3349 */ 3350 function sortByDisplayName($a,$b) 3351 { 3352 // 0, 1 und -1 3353 return strcasecmp($a->displayName,$b->displayName); 3354 } 3355 3356 /** 3357 * sortByAutoFolderPos 3358 * 3359 * Helper function to sort folder-objects by auto Folder Position 3360 * @param object $a 3361 * @param object $b array of folderobjects 3362 * @return int expect values (0, 1 or -1) 3363 */ 3364 function sortByAutoFolderPos($a,$b) 3365 { 3366 // 0, 1 und -1 3367 $pos1 = array_search(trim($a->shortFolderName),self::$autoFolders); 3368 $pos2 = array_search(trim($b->shortFolderName),self::$autoFolders); 3369 if ($pos1 == $pos2) return 0; 3370 return ($pos1 < $pos2) ? -1 : 1; 3371 } 3372 3373 /** 3374 * getMailBoxCounters 3375 * 3376 * function to retrieve the counters for a given folder 3377 * @param string $folderName 3378 * @param boolean $_returnObject return the counters as object rather than an array 3379 * @return mixed false or array of counters array(MESSAGES,UNSEEN,RECENT,UIDNEXT,UIDVALIDITY) or object 3380 */ 3381 function getMailBoxCounters($folderName,$_returnObject=true) 3382 { 3383 try 3384 { 3385 $folderStatus = $this->icServer->getMailboxCounters($folderName); 3386 //error_log(__METHOD__.' ('.__LINE__.') '.$folderName.": FolderStatus:".array2string($folderStatus).function_backtrace()); 3387 } 3388 catch (\Exception $e) 3389 { 3390 if (self::$debug) error_log(__METHOD__." returned FolderStatus for Folder $folderName:".$e->getMessage()); 3391 return false; 3392 } 3393 if(is_array($folderStatus)) { 3394 if ($_returnObject===false) return $folderStatus; 3395 $status = new \stdClass; 3396 $status->messages = $folderStatus['MESSAGES']; 3397 $status->unseen = $folderStatus['UNSEEN']; 3398 $status->recent = $folderStatus['RECENT']; 3399 $status->uidnext = $folderStatus['UIDNEXT']; 3400 $status->uidvalidity = $folderStatus['UIDVALIDITY']; 3401 3402 return $status; 3403 } 3404 return false; 3405 } 3406 3407 /** 3408 * getMailBoxesRecursive 3409 * 3410 * function to retrieve mailboxes recursively from given mailbox 3411 * @param string $_mailbox 3412 * @param string $delimiter 3413 * @param string $prefix 3414 * @param string $reclevel 0, counter to keep track of the current recursionlevel 3415 * @return array of mailboxes 3416 */ 3417 function getMailBoxesRecursive($_mailbox, $delimiter, $prefix, $reclevel=0) 3418 { 3419 #echo __METHOD__." retrieve SubFolders for $_mailbox$delimiter <br>"; 3420 $maxreclevel=25; 3421 if ($reclevel > $maxreclevel) { 3422 error_log( __METHOD__." Recursion Level Exeeded ($reclevel) while looking up $_mailbox$delimiter "); 3423 return array(); 3424 } 3425 $reclevel++; 3426 // clean up double delimiters 3427 $_mailbox = preg_replace('~'.($delimiter == '.' ? "\\".$delimiter:$delimiter).'+~s',$delimiter,$_mailbox); 3428 //get that mailbox in question 3429 $mbx = $this->icServer->getMailboxes($_mailbox,1,true); 3430 $mbxkeys = array_keys($mbx); 3431 #_debug_array($mbx); 3432//error_log(__METHOD__.' ('.__LINE__.') '.' Delimiter:'.array2string($delimiter)); 3433//error_log(__METHOD__.' ('.__LINE__.') '.array2string($mbx)); 3434 // Example: Array([INBOX/GaGa] => Array([MAILBOX] => INBOX/GaGa[ATTRIBUTES] => Array([0] => \\unmarked)[delimiter] => /)) 3435 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"]))) { 3436 // if there are children fetch them 3437 //echo $mbx[$mbxkeys[0]]['MAILBOX']."<br>"; 3438 3439 $buff = $this->icServer->getMailboxes($mbx[$mbxkeys[0]]['MAILBOX'].($mbx[$mbxkeys[0]]['MAILBOX'] == $prefix ? '':$delimiter),2,false); 3440 //$buff = $this->icServer->getMailboxes($mbx[$mbxkeys[0]]['MAILBOX'],2,false); 3441 //_debug_array($buff); 3442 $allMailboxes = array(); 3443 foreach ($buff as $mbxname) { 3444//error_log(__METHOD__.' ('.__LINE__.') '.array2string($mbxname)); 3445 $mbxname = preg_replace('~'.($delimiter == '.' ? "\\".$delimiter:$delimiter).'+~s',$delimiter,$mbxname['MAILBOX']); 3446 #echo "About to recur in level $reclevel:".$mbxname."<br>"; 3447 if ( $mbxname != $mbx[$mbxkeys[0]]['MAILBOX'] && $mbxname != $prefix && $mbxname != $mbx[$mbxkeys[0]]['MAILBOX'].$delimiter) 3448 { 3449 $allMailboxes = array_merge($allMailboxes, self::getMailBoxesRecursive($mbxname, $delimiter, $prefix, $reclevel)); 3450 } 3451 } 3452 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']; 3453 return $allMailboxes; 3454 } else { 3455 return array($_mailbox); 3456 } 3457 } 3458 3459 /** 3460 * _getSpecialUseFolder 3461 * abstraction layer for getDraftFolder, getTemplateFolder, getTrashFolder and getSentFolder 3462 * @param string $_type the type to fetch (Drafts|Template|Trash|Sent) 3463 * @param boolean $_checkexistance trigger check for existance 3464 * @param boolean& $created =null on return true: if folder was just created, false if not 3465 * @return mixed string or false 3466 */ 3467 function _getSpecialUseFolder($_type, $_checkexistance=TRUE, &$created=null) 3468 { 3469 static $types = array( 3470 'Drafts' => array('profileKey'=>'acc_folder_draft','autoFolderName'=>'Drafts'), 3471 'Template' => array('profileKey'=>'acc_folder_template','autoFolderName'=>'Templates'), 3472 'Trash' => array('profileKey'=>'acc_folder_trash','autoFolderName'=>'Trash'), 3473 'Sent' => array('profileKey'=>'acc_folder_sent','autoFolderName'=>'Sent'), 3474 'Junk' => array('profileKey'=>'acc_folder_junk','autoFolderName'=>'Junk'), 3475 'Outbox' => array('profileKey'=>'acc_folder_outbox','autoFolderName'=>'Outbox'), 3476 'Archive' => array('profileKey'=>'acc_folder_archive','autoFolderName'=>'Archive'), 3477 ); 3478 if ($_type == 'Templates') $_type = 'Template'; // for some reason self::$autofolders uses 'Templates'! 3479 $created = false; 3480 if (!isset($types[$_type])) 3481 { 3482 error_log(__METHOD__.' ('.__LINE__.') '.' '.$_type.' not supported for '.__METHOD__); 3483 return false; 3484 } 3485 if (is_null(self::$specialUseFolders) || empty(self::$specialUseFolders)) self::$specialUseFolders = $this->getSpecialUseFolders(); 3486 3487 //highest precedence 3488 try 3489 { 3490 $_folderName = $this->icServer->{$types[$_type]['profileKey']}; 3491 } 3492 catch (\Exception $e) 3493 { 3494 // we know that outbox is not supported, but we use this here, as we autocreate expected SpecialUseFolders in this function 3495 if ($_type != 'Outbox') error_log(__METHOD__.' ('.__LINE__.') '.' Failed to retrieve Folder'.$_folderName." for ".array2string($types[$_type]).":".$e->getMessage()); 3496 $_folderName = false; 3497 } 3498 // do not try to autocreate configured Archive-Folder. Return false if configured folder does not exist 3499 if ($_type == 'Archive') { 3500 if ($_folderName && $_checkexistance && strtolower($_folderName) !='none' && !$this->folderExists($_folderName,true)) { 3501 return false; 3502 } else { 3503 return $_folderName; 3504 } 3505 3506 } 3507 // does the folder exist??? (is configured/preset, but non-existent) 3508 if ($_folderName && $_checkexistance && strtolower($_folderName) !='none' && !$this->folderExists($_folderName,true)) { 3509 try 3510 { 3511 $error = null; 3512 if (($_folderName = $this->createFolder('', $_folderName, $error))) $created = true; 3513 if ($error) error_log(__METHOD__.' ('.__LINE__.') '.' Failed to create Folder '.$_folderName." for $_type:".$error); 3514 } 3515 catch(Exception $e) 3516 { 3517 error_log(__METHOD__.' ('.__LINE__.') '.' Failed to create Folder '.$_folderName." for $_type:".$e->getMessage().':'.function_backtrace()); 3518 $_folderName = false; 3519 } 3520 } 3521 // not sure yet if false is the correct behavior on none 3522 if ($_folderName =='none') return 'none' ; //false; 3523 //no (valid) folder found yet; try specialUseFolders 3524 if (empty($_folderName) && is_array(self::$specialUseFolders) && ($f = array_search($_type,self::$specialUseFolders))) $_folderName = $f; 3525 //no specialUseFolder; try some Defaults 3526 if (empty($_folderName) && isset($types[$_type])) 3527 { 3528 $nameSpace = $this->_getNameSpaces(); 3529 $prefix=''; 3530 foreach ($nameSpace as $nSp) 3531 { 3532 if ($nSp['type']=='personal') 3533 { 3534 //error_log(__METHOD__.__LINE__.array2string($nSp)); 3535 $prefix = $nSp['prefix']; 3536 break; 3537 } 3538 } 3539 if ($this->folderExists($prefix.$types[$_type]['autoFolderName'],true)) 3540 { 3541 $_folderName = $prefix.$types[$_type]['autoFolderName']; 3542 } 3543 else 3544 { 3545 try 3546 { 3547 $error = null; 3548 $this->createFolder('', $prefix.$types[$_type]['autoFolderName'],$error); 3549 $_folderName = $prefix.$types[$_type]['autoFolderName']; 3550 if ($error) error_log(__METHOD__.' ('.__LINE__.') '.' Failed to create Folder '.$_folderName." for $_type:".$error); 3551 } 3552 catch(Exception $e) 3553 { 3554 error_log(__METHOD__.' ('.__LINE__.') '.' Failed to create Folder '.$_folderName." for $_type:".$e->getMessage()); 3555 $_folderName = false; 3556 } 3557 } 3558 } 3559 return $_folderName; 3560 } 3561 3562 /** 3563 * getFolderByType wrapper for _getSpecialUseFolder Type as param 3564 * @param string $type foldertype to look for 3565 * @param boolean $_checkexistance trigger check for existance 3566 * @return mixed string or false 3567 */ 3568 function getFolderByType($type, $_checkexistance=false) 3569 { 3570 return $this->_getSpecialUseFolder($type, $_checkexistance); 3571 } 3572 3573 /** 3574 * getJunkFolder wrapper for _getSpecialUseFolder Type Junk 3575 * @param boolean $_checkexistance trigger check for existance 3576 * @return mixed string or false 3577 */ 3578 function getJunkFolder($_checkexistance=TRUE) 3579 { 3580 return $this->_getSpecialUseFolder('Junk', $_checkexistance); 3581 } 3582 3583 /** 3584 * getDraftFolder wrapper for _getSpecialUseFolder Type Drafts 3585 * @param boolean $_checkexistance trigger check for existance 3586 * @return mixed string or false 3587 */ 3588 function getDraftFolder($_checkexistance=TRUE) 3589 { 3590 return $this->_getSpecialUseFolder('Drafts', $_checkexistance); 3591 } 3592 3593 /** 3594 * getTemplateFolder wrapper for _getSpecialUseFolder Type Template 3595 * @param boolean $_checkexistance trigger check for existance 3596 * @return mixed string or false 3597 */ 3598 function getTemplateFolder($_checkexistance=TRUE) 3599 { 3600 return $this->_getSpecialUseFolder('Template', $_checkexistance); 3601 } 3602 3603 /** 3604 * getTrashFolder wrapper for _getSpecialUseFolder Type Trash 3605 * @param boolean $_checkexistance trigger check for existance 3606 * @return mixed string or false 3607 */ 3608 function getTrashFolder($_checkexistance=TRUE) 3609 { 3610 return $this->_getSpecialUseFolder('Trash', $_checkexistance); 3611 } 3612 3613 /** 3614 * getSentFolder wrapper for _getSpecialUseFolder Type Sent 3615 * @param boolean $_checkexistance trigger check for existance 3616 * @return mixed string or false 3617 */ 3618 function getSentFolder($_checkexistance=TRUE) 3619 { 3620 return $this->_getSpecialUseFolder('Sent', $_checkexistance); 3621 } 3622 3623 /** 3624 * getOutboxFolder wrapper for _getSpecialUseFolder Type Outbox 3625 * @param boolean $_checkexistance trigger check for existance 3626 * @return mixed string or false 3627 */ 3628 function getOutboxFolder($_checkexistance=TRUE) 3629 { 3630 return $this->_getSpecialUseFolder('Outbox', $_checkexistance); 3631 } 3632 3633 /** 3634 * getArchiveFolder wrapper for _getSpecialUseFolder Type Archive 3635 * @param boolean $_checkexistance trigger check for existance . We do no autocreation for configured Archive folder 3636 * @return mixed string or false 3637 */ 3638 function getArchiveFolder($_checkexistance=TRUE) 3639 { 3640 return $this->_getSpecialUseFolder('Archive', $_checkexistance); 3641 } 3642 3643 /** 3644 * isSentFolder is the given folder the sent folder or at least a subfolder of it 3645 * @param string $_folderName folder to perform the check on 3646 * @param boolean $_checkexistance trigger check for existance 3647 * @param boolean $_exactMatch make the check more strict. return false if folder is subfolder only 3648 * @return boolean 3649 */ 3650 function isSentFolder($_folderName, $_checkexistance=TRUE, $_exactMatch=false) 3651 { 3652 $sentFolder = $this->getSentFolder($_checkexistance); 3653 if(empty($sentFolder)) { 3654 return false; 3655 } 3656 // does the folder exist??? 3657 if ($_checkexistance && !$this->folderExists($_folderName)) { 3658 return false; 3659 } 3660 3661 if ($_exactMatch) 3662 { 3663 if(false !== stripos($_folderName, $sentFolder)&& strlen($_folderName)==strlen($sentFolder)) { 3664 return true; 3665 } else { 3666 return false; 3667 } 3668 } else { 3669 if(false !== stripos($_folderName, $sentFolder)) { 3670 return true; 3671 } else { 3672 return false; 3673 } 3674 } 3675 } 3676 3677 /** 3678 * checks if the Outbox folder exists and is part of the foldername to be checked 3679 * @param string $_folderName folder to perform the check on 3680 * @param boolean $_checkexistance trigger check for existance 3681 * @param boolean $_exactMatch make the check more strict. return false if folder is subfolder only 3682 * @return boolean 3683 */ 3684 function isOutbox($_folderName, $_checkexistance=TRUE, $_exactMatch=false) 3685 { 3686 if (stripos($_folderName, 'Outbox')===false) { 3687 return false; 3688 } 3689 // does the folder exist??? 3690 if ($_checkexistance && $GLOBALS['egw_info']['user']['apps']['activesync'] && !$this->folderExists($_folderName)) { 3691 $outboxFolder = $this->getOutboxFolder($_checkexistance); 3692 if ($_exactMatch) 3693 { 3694 if(false !== stripos($_folderName, $outboxFolder)&& strlen($_folderName)==strlen($outboxFolder)) { 3695 return true; 3696 } else { 3697 return false; 3698 } 3699 } else { 3700 if(false !== stripos($_folderName, $outboxFolder)) { 3701 return true; 3702 } else { 3703 return false; 3704 } 3705 } 3706 } 3707 return true; 3708 } 3709 3710 /** 3711 * isDraftFolder is the given folder the sent folder or at least a subfolder of it 3712 * @param string $_folderName folder to perform the check on 3713 * @param boolean $_checkexistance trigger check for existance 3714 * @param boolean $_exactMatch make the check more strict. return false if folder is subfolder only 3715 * @return boolean 3716 */ 3717 function isDraftFolder($_folderName, $_checkexistance=TRUE, $_exactMatch=false) 3718 { 3719 $draftFolder = $this->getDraftFolder($_checkexistance); 3720 if(empty($draftFolder)) { 3721 return false; 3722 } 3723 // does the folder exist??? 3724 if ($_checkexistance && !$this->folderExists($_folderName)) { 3725 return false; 3726 } 3727 if (is_a($_folderName,"Horde_Imap_Client_Mailbox")) $_folderName = $_folderName->utf8; 3728 if ($_exactMatch) 3729 { 3730 if(false !== stripos($_folderName, $draftFolder)&& strlen($_folderName)==strlen($draftFolder)) { 3731 return true; 3732 } else { 3733 return false; 3734 } 3735 } else { 3736 if(false !== stripos($_folderName, $draftFolder)) { 3737 return true; 3738 } else { 3739 return false; 3740 } 3741 } 3742 } 3743 3744 /** 3745 * isTrashFolder is the given folder the sent folder or at least a subfolder of it 3746 * @param string $_folderName folder to perform the check on 3747 * @param boolean $_checkexistance trigger check for existance 3748 * @param boolean $_exactMatch make the check more strict. return false if folder is subfolder only 3749 * @return boolean 3750 */ 3751 function isTrashFolder($_folderName, $_checkexistance=TRUE, $_exactMatch=false) 3752 { 3753 $trashFolder = $this->getTrashFolder($_checkexistance); 3754 if(empty($trashFolder)) { 3755 return false; 3756 } 3757 // does the folder exist??? 3758 if ($_checkexistance && !$this->folderExists($_folderName)) { 3759 return false; 3760 } 3761 3762 if ($_exactMatch) 3763 { 3764 if(false !== stripos($_folderName, $trashFolder)&& strlen($_folderName)==strlen($trashFolder)) { 3765 return true; 3766 } else { 3767 return false; 3768 } 3769 } else { 3770 if(false !== stripos($_folderName, $trashFolder)) { 3771 return true; 3772 } else { 3773 return false; 3774 } 3775 } 3776 } 3777 3778 /** 3779 * isTemplateFolder is the given folder the sent folder or at least a subfolder of it 3780 * @param string $_folderName folder to perform the check on 3781 * @param boolean $_checkexistance trigger check for existance 3782 * @param boolean $_exactMatch make the check more strict. return false if folder is subfolder only 3783 * @return boolean 3784 */ 3785 function isTemplateFolder($_folderName, $_checkexistance=TRUE, $_exactMatch=false) 3786 { 3787 $templateFolder = $this->getTemplateFolder($_checkexistance); 3788 if(empty($templateFolder)) { 3789 return false; 3790 } 3791 // does the folder exist??? 3792 if ($_checkexistance && !$this->folderExists($_folderName)) { 3793 return false; 3794 } 3795 if ($_exactMatch) 3796 { 3797 if(false !== stripos($_folderName, $templateFolder)&& strlen($_folderName)==strlen($templateFolder)) { 3798 return true; 3799 } else { 3800 return false; 3801 } 3802 } else { 3803 if(false !== stripos($_folderName, $templateFolder)) { 3804 return true; 3805 } else { 3806 return false; 3807 } 3808 } 3809 } 3810 3811 /** 3812 * folderExists checks for existance of a given folder 3813 * @param string $_folder folder to perform the check on 3814 * @param boolean $_forceCheck trigger check for existance on icServer 3815 * @return mixed string or false 3816 */ 3817 function folderExists($_folder, $_forceCheck=false) 3818 { 3819 static $folderInfo; 3820 $forceCheck = $_forceCheck; 3821 if (empty($_folder)) 3822 { 3823 // this error is more or less without significance, unless we force the check 3824 if ($_forceCheck===true) error_log(__METHOD__.' ('.__LINE__.') '.' Called with empty Folder:'.$_folder.function_backtrace()); 3825 return false; 3826 } 3827 // when check is not enforced , we assume a folder represented as Horde_Imap_Client_Mailbox as existing folder 3828 if (is_a($_folder,"Horde_Imap_Client_Mailbox")&&$_forceCheck===false) return true; 3829 if (is_a($_folder,"Horde_Imap_Client_Mailbox")) $_folder = $_folder->utf8; 3830 // reduce traffic within the Instance per User; Expire every 5 hours 3831 //error_log(__METHOD__.' ('.__LINE__.') '.' Called with Folder:'.$_folder.function_backtrace()); 3832 if (is_null($folderInfo)) $folderInfo = Cache::getCache(Cache::INSTANCE,'email','icServerFolderExistsInfo'.trim($GLOBALS['egw_info']['user']['account_id']),null,array(),$expiration=60*60*5); 3833 //error_log(__METHOD__.' ('.__LINE__.') '.'Cached Info on Folder:'.$_folder.' for Profile:'.$this->profileID.($forceCheck?'(forcedCheck)':'').':'.array2string($folderInfo)); 3834 if (!empty($folderInfo) && isset($folderInfo[$this->profileID]) && isset($folderInfo[$this->profileID][$_folder]) && $forceCheck===false) 3835 { 3836 //error_log(__METHOD__.' ('.__LINE__.') '.' Using cached Info on Folder:'.$_folder.' for Profile:'.$this->profileID); 3837 return $folderInfo[$this->profileID][$_folder]; 3838 } 3839 else 3840 { 3841 if ($forceCheck === false) 3842 { 3843 //error_log(__METHOD__.' ('.__LINE__.') '.' No cached Info on Folder:'.$_folder.' for Profile:'.$this->profileID.' FolderExistsInfoCache:'.array2string($folderInfo[$this->profileID])); 3844 $forceCheck = true; // try to force the check, in case there is no connection, we may need that 3845 } 3846 } 3847 3848 // does the folder exist??? 3849 //error_log(__METHOD__."->Connected?".$this->icServer->_connected.", ".$_folder.", ".($forceCheck?' forceCheck activated':'dont check on server')); 3850 if ( $forceCheck || empty($folderInfo) || !isset($folderInfo[$this->profileID]) || !isset($folderInfo[$this->profileID][$_folder])) { 3851 //error_log(__METHOD__."->NotConnected and forceCheck with profile:".$this->profileID); 3852 //return false; 3853 //try to connect 3854 $folderInfo[$this->profileID] = array(); 3855 } 3856 try 3857 { 3858 $folderInfo[$this->profileID][$_folder] = $this->icServer->mailboxExist($_folder); 3859 } 3860 catch (\Exception $e) 3861 { 3862 error_log(__METHOD__.__LINE__.$e->getMessage().($e->details?', '.$e->details:'')); 3863 self::$profileDefunct[$this->profileID]=$e->getMessage().($e->details?', '.$e->details:''); 3864 $folderInfo[$this->profileID][$_folder] = false; 3865 } 3866 //error_log(__METHOD__.' ('.__LINE__.') '.' Folder Exists:'.$folderInfo[$this->profileID][$_folder].function_backtrace()); 3867 3868 if(!empty($folderInfo) && isset($folderInfo[$this->profileID][$_folder]) && 3869 $folderInfo[$this->profileID][$_folder] !== true) 3870 { 3871 $folderInfo[$this->profileID][$_folder] = false; // set to false, whatever it was (to have a valid returnvalue for the static return) 3872 } 3873 Cache::setCache(Cache::INSTANCE,'email','icServerFolderExistsInfo'.trim($GLOBALS['egw_info']['user']['account_id']),$folderInfo,$expiration=60*60*5); 3874 return (!empty($folderInfo) && isset($folderInfo[$this->profileID][$_folder]) ? $folderInfo[$this->profileID][$_folder] : false); 3875 } 3876 3877 /** 3878 * remove any messages which are marked as deleted or 3879 * remove any messages from the trashfolder 3880 * 3881 * @param string _folderName the foldername 3882 * @return nothing 3883 */ 3884 function compressFolder($_folderName = false) 3885 { 3886 $folderName = ($_folderName ? $_folderName : $this->sessionData['mailbox']); 3887 $deleteOptions = $GLOBALS['egw_info']['user']['preferences']['mail']['deleteOptions']; 3888 $trashFolder = $this->getTrashFolder(); 3889 3890 $this->icServer->openMailbox($folderName); 3891 3892 if(strtolower($folderName) == strtolower($trashFolder) && $deleteOptions == "move_to_trash") { 3893 $this->deleteMessages('all',$folderName,'remove_immediately'); 3894 } else { 3895 $this->icServer->expunge($folderName); 3896 } 3897 } 3898 3899 /** 3900 * delete a Message 3901 * 3902 * @param mixed array/string _messageUID array of ids to flag, or 'all' 3903 * @param string _folder foldername 3904 * @param string _forceDeleteMethod - "no", or deleteMethod like 'move_to_trash',"mark_as_deleted","remove_immediately" 3905 * 3906 * @return bool true, as we do not handle return values yet 3907 * @throws Exception 3908 */ 3909 function deleteMessages($_messageUID, $_folder=NULL, $_forceDeleteMethod='no') 3910 { 3911 //error_log(__METHOD__.' ('.__LINE__.') '.'->'.array2string($_messageUID).','.array2string($_folder).', '.$_forceDeleteMethod); 3912 $oldMailbox = ''; 3913 if (is_null($_folder) || empty($_folder)) $_folder = $this->sessionData['mailbox']; 3914 if (empty($_messageUID)) 3915 { 3916 if (self::$debug) error_log(__METHOD__." no messages Message(s): ".implode(',',$_messageUID)); 3917 return false; 3918 } 3919 elseif ($_messageUID==='all') 3920 { 3921 $_messageUID= null; 3922 } 3923 else 3924 { 3925 $uidsToDelete = new Horde_Imap_Client_Ids(); 3926 if (!(is_object($_messageUID) || is_array($_messageUID))) $_messageUID = (array)$_messageUID; 3927 $uidsToDelete->add($_messageUID); 3928 } 3929 $deleteOptions = $_forceDeleteMethod; // use forceDeleteMethod if not "no", or unknown method 3930 if ($_forceDeleteMethod === 'no' || !in_array($_forceDeleteMethod,array('move_to_trash',"mark_as_deleted","remove_immediately"))) $deleteOptions = ($this->mailPreferences['deleteOptions']?$this->mailPreferences['deleteOptions']:"mark_as_deleted"); 3931 //error_log(__METHOD__.' ('.__LINE__.') '.'->'.array2string($_messageUID).','.$_folder.'/'.$this->sessionData['mailbox'].' Option:'.$deleteOptions); 3932 $trashFolder = $this->getTrashFolder(); 3933 $draftFolder = $this->getDraftFolder(); //$GLOBALS['egw_info']['user']['preferences']['mail']['draftFolder']; 3934 $templateFolder = $this->getTemplateFolder(); //$GLOBALS['egw_info']['user']['preferences']['mail']['templateFolder']; 3935 if((strtolower($_folder) == strtolower($trashFolder) && $deleteOptions == "move_to_trash")) { 3936 $deleteOptions = "remove_immediately"; 3937 } 3938 if($this->icServer->getCurrentMailbox() != $_folder) { 3939 $oldMailbox = $this->icServer->getCurrentMailbox(); 3940 $this->icServer->openMailbox($_folder); 3941 } 3942 //error_log(__METHOD__.' ('.__LINE__.') '.'->'.array2string($_messageUID).','.$_folder.'/'.$this->sessionData['mailbox'].' Option:'.$deleteOptions); 3943 $updateCache = false; 3944 switch($deleteOptions) { 3945 case "move_to_trash": 3946 //error_log(__METHOD__.' ('.__LINE__.') '); 3947 $updateCache = true; 3948 if(!empty($trashFolder)) { 3949 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.implode(' : ', $_messageUID)); 3950 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '."$trashFolder <= $_folder / ". $this->sessionData['mailbox']); 3951 // copy messages 3952 try 3953 { 3954 $this->icServer->copy($_folder, $trashFolder, array('ids'=>$uidsToDelete,'move'=>true)); 3955 } 3956 catch (\Exception $e) 3957 { 3958 throw new Exception("Failed to move Messages (".array2string($uidsToDelete).") from Folder $_folder to $trashFolder Error:".$e->getMessage()); 3959 } 3960 } 3961 break; 3962 3963 case "mark_as_deleted": 3964 //error_log(__METHOD__.' ('.__LINE__.') '); 3965 // mark messages as deleted 3966 if (is_null($_messageUID)) $_messageUID='all'; 3967 foreach((array)$_messageUID as $key =>$uid) 3968 { 3969 //flag messages, that are flagged for deletion as seen too 3970 $this->flagMessages('read', $uid, $_folder); 3971 $flags = $this->getFlags($uid); 3972 $this->flagMessages('delete', $uid, $_folder); 3973 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($flags)); 3974 if (strpos( array2string($flags),'Deleted')!==false) $undelete[] = $uid; 3975 unset($flags); 3976 } 3977 foreach((array)$undelete as $key =>$uid) 3978 { 3979 $this->flagMessages('undelete', $uid, $_folder); 3980 } 3981 break; 3982 3983 case "remove_immediately": 3984 //error_log(__METHOD__.' ('.__LINE__.') '); 3985 $updateCache = true; 3986 if (is_null($_messageUID)) $_messageUID='all'; 3987 if (is_object($_messageUID)) 3988 { 3989 $this->flagMessages('delete', $_messageUID, $_folder); 3990 } 3991 else 3992 { 3993 foreach((array)$_messageUID as $key =>$uid) 3994 { 3995 //flag messages, that are flagged for deletion as seen too 3996 $this->flagMessages('delete', $uid, $_folder); 3997 } 3998 } 3999 $examineMailbox = $this->icServer->examineMailbox($_folder); 4000 // examine the folder and if there are messages then try to delete the messages finaly 4001 if (is_array($examineMailbox) && $examineMailbox['MESSAGES'] > 0) $this->icServer->expunge($_folder); 4002 break; 4003 } 4004 if($oldMailbox != '') { 4005 $this->icServer->openMailbox($oldMailbox); 4006 } 4007 4008 return true; 4009 } 4010 4011 /** 4012 * get flags for a Message 4013 * 4014 * @param mixed string _messageUID array of id to retrieve the flags for 4015 * 4016 * @return null/array flags 4017 */ 4018 function getFlags ($_messageUID) { 4019 try 4020 { 4021 $uidsToFetch = new Horde_Imap_Client_Ids(); 4022 if (!(is_object($_messageUID) || is_array($_messageUID))) $_messageUID = (array)$_messageUID; 4023 $uidsToFetch->add($_messageUID); 4024 $_folderName = $this->icServer->getCurrentMailbox(); 4025 $fquery = new Horde_Imap_Client_Fetch_Query(); 4026 $fquery->flags(); 4027 $headersNew = $this->icServer->fetch($_folderName, $fquery, array( 4028 'ids' => $uidsToFetch, 4029 )); 4030 if (is_object($headersNew)) { 4031 foreach($headersNew->ids() as $id) { 4032 $_headerObject = $headersNew->get($id); 4033 $flags = $_headerObject->getFlags(); 4034 } 4035 } 4036 } 4037 catch (\Exception $e) 4038 { 4039 error_log(__METHOD__.' ('.__LINE__.') '."Failed to fetch flags for ".array2string($_messageUID)." Error:".$e->getMessage()); 4040 return null; 4041 //throw new Exception("Failed to fetch flags for ".array2string($_messageUID)" Error:".$e->getMessage()); 4042 } 4043 return $flags; 4044 } 4045 4046 /** 4047 * get and parse the flags response for the Notifyflag for a Message 4048 * 4049 * @param string _messageUID array of id to retrieve the flags for 4050 * @param array flags - to avoid additional server call 4051 * 4052 * @return null/boolean 4053 */ 4054 function getNotifyFlags ($_messageUID, $flags=null) 4055 { 4056 if (self::$debug) error_log(__METHOD__.$_messageUID.' Flags:'.array2string($flags)); 4057 try 4058 { 4059 if($flags===null) $flags = $this->getFlags($_messageUID); 4060 } 4061 catch (\Exception $e) 4062 { 4063 return null; 4064 } 4065 4066 if ( stripos( array2string($flags),'MDNSent')!==false) 4067 return true; 4068 4069 if ( stripos( array2string($flags),'MDNnotSent')!==false) 4070 return false; 4071 4072 return null; 4073 } 4074 4075 /** 4076 * flag a Message 4077 * 4078 * @param string _flag (readable name) 4079 * @param mixed array/string _messageUID array of ids to flag, or 'all' 4080 * @param string _folder foldername 4081 * 4082 * @todo handle handle icserver->setFlags returnValue 4083 * 4084 * @return bool true, as we do not handle icserver->setFlags returnValue 4085 */ 4086 function flagMessages($_flag, $_messageUID,$_folder=NULL) 4087 { 4088 //error_log(__METHOD__.' ('.__LINE__.') '.'->' .$_flag." ".array2string($_messageUID).",$_folder /".$this->sessionData['mailbox']); 4089 if (empty($_messageUID)) 4090 { 4091 if (self::$debug) error_log(__METHOD__." no messages Message(s): ".implode(',',$_messageUID)); 4092 return false; 4093 } 4094 $this->icServer->openMailbox(($_folder?$_folder:$this->sessionData['mailbox'])); 4095 $folder = $this->icServer->getCurrentMailbox(); 4096 if (is_array($_messageUID)&& count($_messageUID)>50) 4097 { 4098 $count = $this->getMailBoxCounters($folder,true); 4099 if ($count->messages == count($_messageUID)) $_messageUID='all'; 4100 } 4101 4102 if ($_messageUID==='all') 4103 { 4104 $messageUIDs = array('all'); 4105 } 4106 else 4107 { 4108 if (!(is_object($_messageUID) || is_array($_messageUID))) $_messageUID = (array)$_messageUID; 4109 $messageUIDs = array_chunk($_messageUID,50,true); 4110 } 4111 try 4112 { 4113 foreach($messageUIDs as &$uids) 4114 { 4115 if ($uids==='all') 4116 { 4117 $uidsToModify=null; 4118 } 4119 else 4120 { 4121 $uidsToModify = new Horde_Imap_Client_Ids(); 4122 $uidsToModify->add($uids); 4123 } 4124 switch($_flag) { 4125 case "delete": 4126 $ret = $this->icServer->store($folder, array('add'=>array('\\Deleted'), 'ids'=> $uidsToModify)); 4127 break; 4128 case "undelete": 4129 $ret = $this->icServer->store($folder, array('remove'=>array('\\Deleted'), 'ids'=> $uidsToModify)); 4130 break; 4131 case "flagged": 4132 $ret = $this->icServer->store($folder, array('add'=>array('\\Flagged'), 'ids'=> $uidsToModify)); 4133 break; 4134 case "read": 4135 case "seen": 4136 $ret = $this->icServer->store($folder, array('add'=>array('\\Seen'), 'ids'=> $uidsToModify)); 4137 break; 4138 case "forwarded": 4139 $ret = $this->icServer->store($folder, array('add'=>array('$Forwarded'), 'ids'=> $uidsToModify)); 4140 case "answered": 4141 $ret = $this->icServer->store($folder, array('add'=>array('\\Answered'), 'ids'=> $uidsToModify)); 4142 break; 4143 case "unflagged": 4144 $ret = $this->icServer->store($folder, array('remove'=>array('\\Flagged'), 'ids'=> $uidsToModify)); 4145 break; 4146 case "unread": 4147 case "unseen": 4148 $ret = $this->icServer->store($folder, array('remove'=>array('\\Seen','\\Answered','$Forwarded'), 'ids'=> $uidsToModify)); 4149 break; 4150 case "mdnsent": 4151 $ret = $this->icServer->store($folder, array('add'=>array('MDNSent'), 'ids'=> $uidsToModify)); 4152 break; 4153 case "mdnnotsent": 4154 $ret = $this->icServer->store($folder, array('add'=>array('MDNnotSent'), 'ids'=> $uidsToModify)); 4155 break; 4156 case "label1": 4157 case "labelone": 4158 $ret = $this->icServer->store($folder, array('add'=>array('$label1'), 'ids'=> $uidsToModify)); 4159 break; 4160 case "unlabel1": 4161 case "unlabelone": 4162 $ret = $this->icServer->store($folder, array('remove'=>array('$label1'), 'ids'=> $uidsToModify)); 4163 break; 4164 case "label2": 4165 case "labeltwo": 4166 $ret = $this->icServer->store($folder, array('add'=>array('$label2'), 'ids'=> $uidsToModify)); 4167 break; 4168 case "unlabel2": 4169 case "unlabeltwo": 4170 $ret = $this->icServer->store($folder, array('remove'=>array('$label2'), 'ids'=> $uidsToModify)); 4171 break; 4172 case "label3": 4173 case "labelthree": 4174 $ret = $this->icServer->store($folder, array('add'=>array('$label3'), 'ids'=> $uidsToModify)); 4175 break; 4176 case "unlabel3": 4177 case "unlabelthree": 4178 $ret = $this->icServer->store($folder, array('remove'=>array('$label3'), 'ids'=> $uidsToModify)); 4179 break; 4180 case "label4": 4181 case "labelfour": 4182 $ret = $this->icServer->store($folder, array('add'=>array('$label4'), 'ids'=> $uidsToModify)); 4183 break; 4184 case "unlabel4": 4185 case "unlabelfour": 4186 $ret = $this->icServer->store($folder, array('remove'=>array('$label4'), 'ids'=> $uidsToModify)); 4187 break; 4188 case "label5": 4189 case "labelfive": 4190 $ret = $this->icServer->store($folder, array('add'=>array('$label5'), 'ids'=> $uidsToModify)); 4191 break; 4192 case "unlabel5": 4193 case "unlabelfive": 4194 $ret = $this->icServer->store($folder, array('remove'=>array('$label5'), 'ids'=> $uidsToModify)); 4195 break; 4196 case "unlabel": 4197 $ret = $this->icServer->store($folder, array('remove'=>array('$label1'), 'ids'=> $uidsToModify)); 4198 $ret = $this->icServer->store($folder, array('remove'=>array('$label2'), 'ids'=> $uidsToModify)); 4199 $ret = $this->icServer->store($folder, array('remove'=>array('$label3'), 'ids'=> $uidsToModify)); 4200 $ret = $this->icServer->store($folder, array('remove'=>array('$label4'), 'ids'=> $uidsToModify)); 4201 $ret = $this->icServer->store($folder, array('remove'=>array('$label5'), 'ids'=> $uidsToModify)); 4202 break; 4203 } 4204 } 4205 } 4206 catch(Exception $e) 4207 { 4208 error_log(__METHOD__.__LINE__.' Error, could not flag messages in folder '.$folder.' Reason:'.$e->getMessage()); 4209 } 4210 if ($folder instanceof Horde_Imap_Client_Mailbox) $_folder = $folder->utf8; 4211 //error_log(__METHOD__.__LINE__.'#'.$this->icServer->ImapServerId.'#'.array2string($_folder).'#'); 4212 self::$folderStatusCache[$this->icServer->ImapServerId][(!empty($_folder)?$_folder: $this->sessionData['mailbox'])]['uidValidity'] = 0; 4213 4214 //error_log(__METHOD__.' ('.__LINE__.') '.'->' .$_flag." ".array2string($_messageUID).",".($_folder?$_folder:$this->sessionData['mailbox'])); 4215 return true; // as we do not catch/examine setFlags returnValue 4216 } 4217 4218 /** 4219 * move Message(s) 4220 * 4221 * @param string _foldername target folder 4222 * @param mixed array/string _messageUID array of ids to flag, or 'all' 4223 * @param boolean $deleteAfterMove - decides if a mail is moved (true) or copied (false) 4224 * @param string $currentFolder 4225 * @param boolean $returnUIDs - control wether or not the action called should return the new uids 4226 * caveat: not all servers do support that 4227 * @param int $_sourceProfileID - source profile ID, should be handed over, if not $this->icServer->ImapServerId is used 4228 * @param int $_targetProfileID - target profile ID, should only be handed over when target server is different from source 4229 * 4230 * @return mixed/bool true,false or new uid 4231 * @throws Exception 4232 */ 4233 function moveMessages($_foldername, $_messageUID, $deleteAfterMove=true, $currentFolder = Null, $returnUIDs = false, $_sourceProfileID = Null, $_targetProfileID = Null) 4234 { 4235 $source = Mail\Account::read(($_sourceProfileID?$_sourceProfileID:$this->icServer->ImapServerId))->imapServer(); 4236 //$deleteOptions = $GLOBALS['egw_info']["user"]["preferences"]["mail"]["deleteOptions"]; 4237 if (empty($_messageUID)) 4238 { 4239 if (self::$debug) error_log(__METHOD__." no Message(s): ".implode(',',$_messageUID)); 4240 return false; 4241 } 4242 elseif ($_messageUID==='all') 4243 { 4244 //error_log(__METHOD__." all Message(s): ".implode(',',$_messageUID)); 4245 $uidsToMove= null; 4246 } 4247 else 4248 { 4249 //error_log(__METHOD__." Message(s): ".implode(',',$_messageUID)); 4250 $uidsToMove = new Horde_Imap_Client_Ids(); 4251 if (!(is_object($_messageUID) || is_array($_messageUID))) $_messageUID = (array)$_messageUID; 4252 $uidsToMove->add($_messageUID); 4253 } 4254 $sourceFolder = (!empty($currentFolder)?$currentFolder: $this->sessionData['mailbox']); 4255 //error_log(__METHOD__.__LINE__."$_targetProfileID !== ".array2string($source->ImapServerId)); 4256 if (!is_null($_targetProfileID) && $_targetProfileID !== $source->ImapServerId) 4257 { 4258 $sourceFolder = $source->getMailbox($sourceFolder); 4259 $source->openMailbox($sourceFolder); 4260 $uidsToFetch = new Horde_Imap_Client_Ids(); 4261 $uidsToFetch->add($_messageUID); 4262 $fquery = new Horde_Imap_Client_Fetch_Query(); 4263 $fquery->flags(); 4264 $fquery->headerText(array('peek'=>true)); 4265 $fquery->fullText(array('peek'=>true)); 4266 $fquery->imapDate(); 4267 $headersNew = $source->fetch($sourceFolder, $fquery, array( 4268 'ids' => $uidsToFetch, 4269 )); 4270 4271 //error_log(__METHOD__.' ('.__LINE__.') '.' Sourceserver:'.$source->ImapServerId.' mailheaders:'.array2string($headersNew)); 4272 4273 if (is_object($headersNew)) { 4274 $c=0; 4275 $retUid = new Horde_Imap_Client_Ids(); 4276 // we copy chunks of 5 to avoid too much memory and/or server stress 4277 // some servers seem not to allow/support the appendig of multiple messages. so we are down to one 4278 foreach($headersNew as &$_headerObject) { 4279 $c++; 4280 $flags = $_headerObject->getFlags(); //unseen status seems to be lost when retrieving the full message 4281 $date = $_headerObject->getImapDate(); 4282 $currentDate = new Horde_Imap_Client_DateTime(); 4283 // if the internal Date of the message equals the current date; try using the header date 4284 if ($date==$currentDate) 4285 { 4286 $headerForPrio = array_change_key_case($_headerObject->getHeaderText(0,Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->toArray(), CASE_UPPER); 4287 //error_log(__METHOD__.__LINE__.'#'.array2string($date).'#'.array2string($currentDate).'#'.$headerForPrio['DATE']); 4288 $date = new Horde_Imap_Client_DateTime($headerForPrio['DATE']); 4289 //error_log(__METHOD__.__LINE__.'#'.array2string($date).'#'.array2string($currentDate).'#'); 4290 } 4291 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($_headerObject))); 4292 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($flags)); 4293 $body = $_headerObject->getFullMsg(); 4294 $dataNflags[] = array('data'=>$body, 'flags'=>$flags, 'internaldate'=>$date); 4295 if ($c==1) 4296 { 4297 $target = Mail\Account::read($_targetProfileID)->imapServer(); 4298 //error_log(__METHOD__.' ('.__LINE__.') '.' Sourceserver:'.$source->ImapServerId.' TargetServer:'.$_targetProfileID.' TargetFolderObject:'.array2string($_foldername)); 4299 $foldername = $target->getMailbox($_foldername); 4300 // make sure the target folder is open and ready 4301 $target->openMailbox($foldername); 4302 $ret = $target->append($foldername,$dataNflags); 4303 $retUid->add($ret); 4304 unset($dataNflags); 4305 // sleep 500 miliseconds; AS some sERVERs seem not to be capable of the load this is 4306 // inflicting in them. they "reply" with an unspecific IMAP Error 4307 time_nanosleep(0,500000); 4308 $c=0; 4309 } 4310 } 4311 if (isset($dataNflags)) 4312 { 4313 $target = Mail\Account::read($_targetProfileID)->imapServer(); 4314 //error_log(__METHOD__.' ('.__LINE__.') '.' Sourceserver:'.$source->ImapServerId.' TargetServer:'.$_targetProfileID.' TargetFolderObject:'.array2string($foldername)); 4315 $foldername = $target->getMailbox($_foldername); 4316 // make sure the target folder is open and ready 4317 $target->openMailbox($foldername); 4318 $ret = $target->append($foldername,$dataNflags); 4319 $retUid->add($ret); 4320 unset($dataNflags); 4321 } 4322 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($retUid)); 4323 // make sure we are back to source 4324 $source->openMailbox($sourceFolder); 4325 if ($deleteAfterMove) 4326 { 4327 $remember = $this->icServer; 4328 $this->icServer = $source; 4329 $this->deleteMessages($_messageUID, $sourceFolder, $_forceDeleteMethod='remove_immediately'); 4330 $this->icServer = $remember; 4331 } 4332 } 4333 } 4334 else 4335 { 4336 try 4337 { 4338 $retUid = $source->copy($sourceFolder, $_foldername, array('ids'=>$uidsToMove,'move'=>$deleteAfterMove)); 4339 } 4340 catch (exception $e) 4341 { 4342 error_log(__METHOD__.' ('.__LINE__.') '."Copying to Folder $_foldername failed! Error:".$e->getMessage()); 4343 throw new Exception("Copying to Folder $_foldername failed! Error:".$e->getMessage()); 4344 } 4345 } 4346 4347 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($retUid)); 4348 return ($returnUIDs ? $retUid : true); 4349 } 4350 4351 /** 4352 * Parse dates, also handle wrong or unrecognized timezones, falling back to current time 4353 * 4354 * @param string $_date to be parsed/formatted 4355 * @param string $format ='' if none is passed, use user prefs 4356 * @return string returns the date as it is parseable by strtotime, or current timestamp if everything fails 4357 */ 4358 static function _strtotime($_date='', $format='', $convert2usertime=false) 4359 { 4360 try { 4361 $date = new DateTime($_date); // parse date & time including timezone (throws exception, if not parsable) 4362 if ($convert2usertime) $date->setUser(); // convert to user-time 4363 $date2return = $date->format($format); 4364 } 4365 catch(\Exception $e) 4366 { 4367 unset($e); // not used 4368 4369 // remove last space-separated part and retry 4370 $parts = explode(' ',$_date); 4371 // try only 10 times to prevent of causing error by reaching 4372 // maximum function nesting level. 4373 if (count($parts) > 1 && count($parts)<10) 4374 { 4375 array_pop($parts); 4376 $date2return = self::_strtotime(implode(' ', $parts), $format, $convert2usertime); 4377 } 4378 else // not last part, use current time 4379 { 4380 $date2return = DateTime::to('now', $format); 4381 } 4382 } 4383 return $date2return; 4384 } 4385 4386 /** 4387 * htmlentities 4388 * helperfunction to cope with wrong encoding in strings 4389 * @param string $_string input to be converted 4390 * @param mixed $_charset false or string -> Target charset, if false Mail displayCharset will be used 4391 * @return string 4392 */ 4393 static function htmlentities($_string, $_charset=false) 4394 { 4395 //setting the charset (if not given) 4396 if ($_charset===false) $_charset = self::$displayCharset; 4397 $string = @htmlentities($_string, ENT_QUOTES, $_charset, false); 4398 if (empty($string) && !empty($_string)) $string = @htmlentities(Translation::convert($_string,Translation::detect_encoding($_string),$_charset),ENT_QUOTES | ENT_IGNORE,$_charset, false); 4399 return $string; 4400 } 4401 4402 /** 4403 * clean a message from elements regarded as potentially harmful 4404 * param string/reference $_html is the text to be processed 4405 * return nothing 4406 */ 4407 static function getCleanHTML(&$_html) 4408 { 4409 // remove CRLF and TAB as it is of no use in HTML. 4410 // but they matter in <pre>, so we rather don't 4411 //$_html = str_replace("\r\n",' ',$_html); 4412 //$_html = str_replace("\t",' ',$_html); 4413 //error_log(__METHOD__.__LINE__.':'.$_html); 4414 //repair doubleencoded ampersands, and some stuff htmLawed stumbles upon with balancing switched on 4415 $_html = str_replace(array('&amp;','<DIV><BR></DIV>',"<DIV> </DIV>",'<div> </div>','</td></font>','<br><td>','<tr></tr>','<o:p></o:p>','<o:p>','</o:p>'), 4416 array('&', '<BR>', '<BR>', '<BR>', '</font></td>','<td>', '', '', '', ''),$_html); 4417 //$_html = str_replace(array('&amp;'),array('&'),$_html); 4418 if (stripos($_html,'style')!==false) Mail\Html::replaceTagsCompletley($_html,'style'); // clean out empty or pagewide style definitions / left over tags 4419 if (stripos($_html,'head')!==false) Mail\Html::replaceTagsCompletley($_html,'head'); // Strip out stuff in head 4420 //if (stripos($_html,'![if')!==false && stripos($_html,'<![endif]>')!==false) Mail\Html::replaceTagsCompletley($_html,'!\[if','<!\[endif\]>',false); // Strip out stuff in ifs 4421 //if (stripos($_html,'!--[if')!==false && stripos($_html,'<![endif]-->')!==false) Mail\Html::replaceTagsCompletley($_html,'!--\[if','<!\[endif\]-->',false); // Strip out stuff in ifs 4422 //error_log(__METHOD__.' ('.__LINE__.') '.$_html); 4423 4424 if (get_magic_quotes_gpc() === 1) $_html = stripslashes($_html); 4425 // Strip out doctype in head, as htmlLawed cannot handle it TODO: Consider extracting it and adding it afterwards 4426 if (stripos($_html,'!doctype')!==false) Mail\Html::replaceTagsCompletley($_html,'!doctype'); 4427 if (stripos($_html,'?xml:namespace')!==false) Mail\Html::replaceTagsCompletley($_html,'\?xml:namespace','/>',false); 4428 if (stripos($_html,'?xml version')!==false) Mail\Html::replaceTagsCompletley($_html,'\?xml version','\?>',false); 4429 if (strpos($_html,'!CURSOR')!==false) Mail\Html::replaceTagsCompletley($_html,'!CURSOR'); 4430 // htmLawed filter only the 'body' 4431 //preg_match('`(<htm.+?<body[^>]*>)(.+?)(</body>.*?</html>)`ims', $_html, $matches); 4432 //if ($matches[2]) 4433 //{ 4434 // $hasOther = true; 4435 // $_html = $matches[2]; 4436 //} 4437 // purify got switched to htmLawed 4438 // some testcode to test purifying / htmlawed 4439 //$_html = "<BLOCKQUOTE>hi <div> there </div> kram <br> </blockquote>".$_html; 4440 $_html = Html\HtmLawed::purify($_html,self::$htmLawed_config,array(),true); 4441 //if ($hasOther) $_html = $matches[1]. $_html. $matches[3]; 4442 // clean out comments , should not be needed as purify should do the job. 4443 $search = array( 4444 '@url\(http:\/\/[^\)].*?\)@si', // url calls e.g. in style definitions 4445 '@<!--[\s\S]*?[ \t\n\r]*-->@', // Strip multi-line comments including CDATA 4446 ); 4447 $_html = preg_replace($search,"",$_html); 4448 // remove non printable chars 4449 $_html = preg_replace('/([\000-\011])/','',$_html); 4450 //error_log(__METHOD__.':'.__LINE__.':'.$_html); 4451 } 4452 4453 /** 4454 * Header and Bodystructure stuff 4455 */ 4456 4457 /** 4458 * getMimePartCharset - fetches the charset mimepart if it exists 4459 * @param $_mimePartObject structure object 4460 * @return mixed mimepart or false if no CHARSET is found, the missing charset has to be handled somewhere else, 4461 * as we cannot safely assume any charset as we did earlier 4462 */ 4463 function getMimePartCharset($_mimePartObject) 4464 { 4465 //$charSet = 'iso-8859-1';//self::$displayCharset; //'iso-8859-1'; // self::displayCharset seems to be asmarter fallback than iso-8859-1 4466 $CharsetFound=false; 4467 //echo "#".$_mimePartObject->encoding.'#<br>'; 4468 if(is_array($_mimePartObject->parameters)) { 4469 if(isset($_mimePartObject->parameters['CHARSET'])) { 4470 $charSet = $_mimePartObject->parameters['CHARSET']; 4471 $CharsetFound=true; 4472 } 4473 } 4474 // this one is dirty, but until I find something that does the trick of detecting the encoding, .... 4475 //if ($CharsetFound == false && $_mimePartObject->encoding == "QUOTED-PRINTABLE") $charSet = 'iso-8859-1'; //assume quoted-printable to be ISO 4476 //if ($CharsetFound == false && $_mimePartObject->encoding == "BASE64") $charSet = 'utf-8'; // assume BASE64 to be UTF8 4477 return ($CharsetFound ? $charSet : $CharsetFound); 4478 } 4479 4480 /** 4481 * decodeMimePart - fetches the charset mimepart if it exists 4482 * @param string $_mimeMessage - the message to be decoded 4483 * @param string $_encoding - the encoding used BASE64 and QUOTED-PRINTABLE is supported 4484 * @param string $_charset - not used 4485 * @return string decoded mimePart 4486 */ 4487 function decodeMimePart($_mimeMessage, $_encoding, $_charset = '') 4488 { 4489 // decode the part 4490 if (self::$debug) error_log(__METHOD__."() with $_encoding and $_charset:".print_r($_mimeMessage,true)); 4491 switch (strtoupper($_encoding)) 4492 { 4493 case 'BASE64': 4494 // use imap_base64 to decode, not any longer, as it is strict, and fails if it encounters invalid chars 4495 return base64_decode($_mimeMessage); 4496 4497 case 'QUOTED-PRINTABLE': 4498 // use imap_qprint to decode 4499 return quoted_printable_decode($_mimeMessage); 4500 4501 case 'WEDONTKNOWTHEENCODING': 4502 // try base64 4503 $r = base64_decode($_mimeMessage); 4504 if (json_encode($r)) 4505 { 4506 return $r; 4507 } 4508 //we do not know the encoding, so we do not decode 4509 default: 4510 // it is either not encoded or we don't know about it 4511 return $_mimeMessage; 4512 } 4513 } 4514 4515 /** 4516 * get part of the message, if its stucture is indicating its of multipart alternative style 4517 * a wrapper for multipartmixed 4518 * @param string/int $_uid the messageuid, 4519 * @param Horde_Mime_Part $_structure structure for parsing 4520 * @param string $_htmlMode how to display a message, html, plain text, ... 4521 * @param boolean $_preserveSeen flag to preserve the seenflag by using body.peek 4522 * @param Horde_Mime_Part& $partCalendar =null on return text/calendar part, if one was contained or false 4523 * @return array containing the desired part 4524 */ 4525 function getMultipartAlternative($_uid, Horde_Mime_Part $_structure, $_htmlMode, $_preserveSeen = false, &$partCalendar=null) 4526 { 4527 // a multipart/alternative has exactly 2 parts (text and html OR text and something else) 4528 // sometimes there are 3 parts, when there is an ics/ical attached/included-> we want to show that 4529 // as attachment AND as abstracted ical information (we use our notification style here). 4530 $partText = $partCalendar = $partHTML = null; 4531 if (self::$debug) _debug_array(array("METHOD"=>__METHOD__,"LINE"=>__LINE__,"STRUCTURE"=>$_structure)); 4532 //error_log(__METHOD__.' ('.__LINE__.') '); 4533 $ignore_first_part = true; 4534 foreach($_structure->contentTypeMap() as $mime_id => $mime_type) 4535 { 4536 //error_log(__METHOD__."($_uid, ".$_structure->getMimeId().") $mime_id: $mime_type"." ignoreFirstPart:".$ignore_first_part); 4537 if (self::$debug) echo __METHOD__."($_uid, partID=".$_structure->getMimeId().") $mime_id: $mime_type<br>"; 4538 4539 if ($ignore_first_part) 4540 { 4541 $ignore_first_part = false; 4542 continue; // ignore multipart/alternative itself 4543 } 4544 4545 $mimePart = $_structure->getPart($mime_id); 4546 4547 switch($mimePart->getPrimaryType()) 4548 { 4549 case 'text': 4550 switch($mimePart->getSubType()) 4551 { 4552 case 'plain': 4553 if ($mimePart->getBytes() > 0) $partText = $mimePart; 4554 break; 4555 4556 case 'html': 4557 if ($mimePart->getBytes() > 0) $partHTML = $mimePart; 4558 break; 4559 4560 case 'calendar': 4561 if ($mimePart->getBytes() > 0) $partCalendar = $mimePart; 4562 break; 4563 } 4564 break; 4565 4566 case 'multipart': 4567 switch($mimePart->getSubType()) 4568 { 4569 case 'related': 4570 case 'mixed': 4571 if (count($mimePart->getParts()) > 1) 4572 { 4573 // in a multipart alternative we treat the multipart/related as html part 4574 if (self::$debug) error_log(__METHOD__." process MULTIPART/".$mimePart->getSubType()." with array as subparts"); 4575 $partHTML = $mimePart; 4576 break 3; // GET OUT OF LOOP, will be processed according to type 4577 } 4578 break; 4579 case 'alternative': 4580 if (count($mimePart->getParts()) > 1) 4581 { 4582 //cascading multipartAlternative structure, assuming only the first one is to be used 4583 return $this->getMultipartAlternative($_uid, $mimePart, $_htmlMode, $_preserveSeen); 4584 } 4585 } 4586 } 4587 } 4588 4589 switch($_htmlMode) 4590 { 4591 case 'html_only': 4592 case 'always_display': 4593 if ($partHTML) 4594 { 4595 switch($partHTML->getSubType()) 4596 { 4597 case 'related': 4598 return $this->getMultipartRelated($_uid, $partHTML, $_htmlMode, $_preserveSeen); 4599 4600 case 'mixed': 4601 return $this->getMultipartMixed($_uid, $partHTML, $_htmlMode, $_preserveSeen); 4602 4603 default: 4604 return $this->getTextPart($_uid, $partHTML, $_htmlMode, $_preserveSeen); 4605 } 4606 } 4607 elseif ($partText && $_htmlMode=='always_display') 4608 { 4609 return $this->getTextPart($_uid, $partText, $_htmlMode, $_preserveSeen); 4610 } 4611 break; 4612 4613 case 'only_if_no_text': 4614 if ($partText) 4615 { 4616 return $this->getTextPart($_uid, $partText, $_htmlMode, $_preserveSeen); 4617 } 4618 if ($partHTML) 4619 { 4620 if ($partHTML->getPrimaryType()) 4621 { 4622 return $this->getMultipartRelated($_uid, $partHTML, $_htmlMode, $_preserveSeen); 4623 } 4624 return $this->getTextPart($_uid, $partHTML, 'always_display', $_preserveSeen); 4625 } 4626 break; 4627 4628 default: 4629 if ($partText) 4630 { 4631 return $this->getTextPart($_uid, $partText, $_htmlMode, $_preserveSeen); 4632 } 4633 $bodyPart = array( 4634 'body' => lang("no plain text part found"), 4635 'mimeType' => 'text/plain', 4636 'charSet' => self::$displayCharset, 4637 ); 4638 break; 4639 } 4640 return $bodyPart; 4641 } 4642 4643 /** 4644 * Get part of the message, if its stucture is indicating its of multipart mixed style 4645 * 4646 * @param int $_uid the messageuid, 4647 * @param Horde_Mime_Part $_structure = '' if given use structure for parsing 4648 * @param string $_htmlMode how to display a message, html, plain text, ... 4649 * @param boolean $_preserveSeen flag to preserve the seenflag by using body.peek 4650 * @param array& $skipParts - passed by reference to have control/knowledge which parts are already fetched 4651 * @param Horde_Mime_Part& $partCalendar =null on return text/calendar part, if one was contained or false 4652 * @return array containing the desired part 4653 */ 4654 function getMultipartMixed($_uid, Horde_Mime_Part $_structure, $_htmlMode, $_preserveSeen = false, &$skipParts=array(), &$partCalendar=null) 4655 { 4656 if (self::$debug) echo __METHOD__."$_uid, $_htmlMode<br>"; 4657 $bodyPart = array(); 4658 if (self::$debug) _debug_array($_structure); 4659 4660 $ignore_first_part = true; 4661 //$skipParts = array(); 4662 //error_log(__METHOD__.__LINE__.array2string($_structure->contentTypeMap())); 4663 foreach($_structure->contentTypeMap() as $mime_id => $mime_type) 4664 { 4665 //error_log(__METHOD__."($_uid, ".$_structure->getMimeId().") $mime_id: $mime_type"); 4666 if (self::$debug) echo __METHOD__."($_uid, partID=".$_structure->getMimeId().") $mime_id: $mime_type<br>"; 4667 if ($ignore_first_part) 4668 { 4669 $ignore_first_part = false; 4670 //error_log(__METHOD__."($_uid, ".$_structure->getMimeId().") SKIPPED FirstPart $mime_id: $mime_type"); 4671 continue; // ignore multipart/mixed itself 4672 } 4673 if (array_key_exists($mime_id,$skipParts)) 4674 { 4675 //error_log(__METHOD__."($_uid, ".$_structure->getMimeId().") SKIPPED $mime_id: $mime_type"); 4676 continue; 4677 } 4678 4679 $part = $_structure->getPart($mime_id); 4680 4681 switch($part->getPrimaryType()) 4682 { 4683 case 'multipart': 4684 if ($part->getDisposition() == 'attachment') continue 2; // +1 for switch 4685 switch($part->getSubType()) 4686 { 4687 case 'alternative': 4688 return array($this->getMultipartAlternative($_uid, $part, $_htmlMode, $_preserveSeen, $partCalendar)); 4689 4690 case 'mixed': 4691 case 'signed': 4692 $bodyPart = array_merge($bodyPart, $this->getMultipartMixed($_uid, $part, $_htmlMode, $_preserveSeen, $skipParts, $partCalendar)); 4693 break; 4694 4695 case 'related': 4696 $bodyPart = array_merge($bodyPart, $this->getMultipartRelated($_uid, $part, $_htmlMode, $_preserveSeen, $partCalendar, $skipParts)); 4697 break; 4698 } 4699 break; 4700 case 'application': 4701 switch($part->getSubType()) 4702 { 4703 case 'pgp-encrypted': 4704 if (($part = $_structure->getPart($mime_id+1)) && 4705 $part->getType() == 'application/octet-stream') 4706 { 4707 $this->fetchPartContents($_uid, $part); 4708 $skipParts[$mime_id]=$mime_type; 4709 $skipParts[$mime_id+1]=$part->getType(); 4710 $bodyPart[] = array( 4711 'body' => $part->getContents(array( 4712 'stream' => false, 4713 )), 4714 'mimeType' => 'text/plain', 4715 'charSet' => $_structure->getCharset(), 4716 ); 4717 } 4718 break; 4719 } 4720 break; 4721 4722 case 'text': 4723 switch($part->getSubType()) 4724 { 4725 case 'plain': 4726 case 'html': 4727 case 'calendar': // inline ics/ical files 4728 if($part->getDisposition() != 'attachment') 4729 { 4730 $bodyPart[] = $this->getTextPart($_uid, $part, $_htmlMode, $_preserveSeen); 4731 $skipParts[$mime_id]=$mime_type; 4732 } 4733 //error_log(__METHOD__.' ('.__LINE__.') '.' ->'.$part->type."/".$part->subType.' -> BodyPart:'.array2string($bodyPart[count($bodyPart)-1])); 4734 break; 4735 } 4736 break; 4737 4738 case 'message': 4739 //skip attachments 4740 if($part->getSubType() == 'delivery-status' && $part->getDisposition() != 'attachment') 4741 { 4742 $bodyPart[] = $this->getTextPart($_uid, $part, $_htmlMode, $_preserveSeen); 4743 $skipParts[$mime_id]=$mime_type; 4744 } 4745 // do not descend into attached Messages 4746 if($part->getSubType() == 'rfc822' || $part->getDisposition() == 'attachment') 4747 { 4748 $skipParts[$mime_id.'.0'] = $mime_type; 4749 foreach($part->contentTypeMap() as $sub_id => $sub_type){ $skipParts[$sub_id] = $sub_type;} 4750 //error_log(__METHOD__.' ('.__LINE__.') '.' Uid:'.$_uid.' Part:'.$mime_id.':'.array2string($skipParts)); 4751 //break 2; 4752 } 4753 break; 4754 4755 default: 4756 // do nothing 4757 // the part is a attachment 4758 } 4759 } 4760 return $bodyPart; 4761 } 4762 4763 /** 4764 * getMultipartRelated 4765 * get part of the message, if its stucture is indicating its of multipart related style 4766 * a wrapper for multipartmixed 4767 * @param string/int $_uid the messageuid, 4768 * @param Horde_Mime_Part $_structure if given use structure for parsing 4769 * @param string $_htmlMode how to display a message, html, plain text, ... 4770 * @param boolean $_preserveSeen flag to preserve the seenflag by using body.peek 4771 * @param Horde_Mime_Part& $partCalendar =null on return text/calendar part, if one was contained or false 4772 * @param array& $skipParts - passed by reference to have control/knowledge which parts are already fetched 4773 * @return array containing the desired part 4774 */ 4775 function getMultipartRelated($_uid, Horde_Mime_Part $_structure, $_htmlMode, $_preserveSeen=false, &$partCalendar=null, &$skipParts=array()) 4776 { 4777 return $this->getMultipartMixed($_uid, $_structure, $_htmlMode, $_preserveSeen, $skipParts, $partCalendar); 4778 } 4779 4780 /** 4781 * Fetch a body part 4782 * 4783 * @param int $_uid 4784 * @param string $_partID = null 4785 * @param string $_folder = null 4786 * @param boolean $_preserveSeen = false 4787 * @param boolean $_stream = false true return a stream, false return string 4788 * @param string &$_encoding = null on return: transfer encoding of returned part 4789 * @param boolean $_tryDecodingServerside = true; wether to try to fetch Data with BINARY instead of BODY 4790 * @return string|resource 4791 */ 4792 function getBodyPart($_uid, $_partID=null, $_folder=null, $_preserveSeen=false, $_stream=false, &$_encoding=null, $_tryDecodingServerside=true) 4793 { 4794 if (self::$debug) error_log( __METHOD__.__LINE__."(".array2string($_uid).", $_partID, $_folder, $_preserveSeen, $_stream, $_encoding, $_tryDecodingServerside)"); 4795 4796 if (empty($_folder)) 4797 { 4798 $_folder = (isset($this->sessionData['mailbox'])&&$this->sessionData['mailbox']? $this->sessionData['mailbox'] : $this->icServer->getCurrentMailbox()); 4799 } 4800 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($_folder).'/'.$this->icServer->getCurrentMailbox().'/'. $this->sessionData['mailbox']); 4801 // querying contents of body part 4802 $uidsToFetch = new Horde_Imap_Client_Ids(); 4803 if (!(is_object($_uid) || is_array($_uid))) $_uid = (array)$_uid; 4804 $uidsToFetch->add($_uid); 4805 4806 $fquery = new Horde_Imap_Client_Fetch_Query(); 4807 $fetchParams = array( 4808 'peek' => $_preserveSeen, 4809 'decode' => true, // try decode on server, does NOT neccessary work 4810 ); 4811 if ($_tryDecodingServerside===false)// || ($_tryDecodingServerside&&$this->isDraftFolder($_folder))) 4812 { 4813 $_tryDecodingServerside=false; 4814 $fetchParams = array( 4815 'peek' => $_preserveSeen, 4816 ); 4817 } 4818 $fquery->bodyPart($_partID, $fetchParams); 4819 4820 $part = $this->icServer->fetch($_folder, $fquery, array( 4821 'ids' => $uidsToFetch, 4822 ))->first(); 4823 $partToReturn = null; 4824 if ($part) 4825 { 4826 $_encoding = $part->getBodyPartDecode($_partID); 4827 //error_log(__METHOD__.__LINE__.':'.$_encoding.'#'); 4828 $partToReturn = $part->getBodyPart($_partID, $_stream); 4829 //error_log(__METHOD__.__LINE__.':'.$partToReturn.'#'); 4830 } 4831 // if we get an empty result, server may have trouble fetching data with UID FETCH $_uid (BINARY.PEEK[$_partID]) 4832 // thus we trigger a second go with UID FETCH $_uid (BODY.PEEK[$_partID]) 4833 if (empty($partToReturn)&&$_tryDecodingServerside===true) 4834 { 4835 error_log(__METHOD__.__LINE__.' failed to fetch bodyPart in BINARY. Try BODY'); 4836 $partToReturn = $this->getBodyPart($_uid, $_partID, $_folder, $_preserveSeen, $_stream, $_encoding, false); 4837 } 4838 return ($partToReturn?$partToReturn:null); 4839 } 4840 4841 /** 4842 * Get Body from message 4843 * 4844 * @param int $_uid the messageuid 4845 * @param Horde_Mime_Part $_structure = null if given use structure for parsing 4846 * @param string $_htmlMode how to display a message: 'html_only', 'always_display', 'only_if_no_text' or '' 4847 * @param boolean $_preserveSeen flag to preserve the seenflag by using body.peek 4848 * @param boolean $_stream = false true return a stream, false return string 4849 * @return array containing the desired text part, mimeType and charset 4850 */ 4851 function getTextPart($_uid, Horde_Mime_Part $_structure, $_htmlMode='', $_preserveSeen=false, $_stream=false) 4852 { 4853 //error_log(__METHOD__.' ('.__LINE__.') '.'->'.$_uid.':'.array2string($_structure).' '.function_backtrace()); 4854 $bodyPart = array(); 4855 if (self::$debug) _debug_array(array($_structure,function_backtrace())); 4856 4857 if($_structure->getSubType() == 'html' && !in_array($_htmlMode, array('html_only', 'always_display', 'only_if_no_text'))) 4858 { 4859 $bodyPart = array( 4860 'error' => 1, 4861 'body' => lang("displaying html messages is disabled"), 4862 'mimeType' => 'text/html', 4863 'charSet' => self::$displayCharset, 4864 ); 4865 } 4866 elseif ($_structure->getSubType() == 'plain' && $_htmlMode == 'html_only') 4867 { 4868 $bodyPart = array( 4869 'error' => 1, 4870 'body' => lang("displaying plain messages is disabled"), 4871 'mimeType' => 'text/plain', // make sure we do not return mimeType text/html 4872 'charSet' => self::$displayCharset, 4873 ); 4874 } 4875 else 4876 { 4877 // some Servers append PropertyFile___ ; strip that here for display 4878 // RB: not sure what this is: preg_replace('/PropertyFile___$/','',$this->decodeMimePart($mimePartBody, $_structure->encoding, $this->getMimePartCharset($_structure))), 4879 4880 // Should not try to fetch if the content is already there (e.g. Smime encrypted message) 4881 if (empty($_structure->getContents())) $this->fetchPartContents($_uid, $_structure, $_stream, $_preserveSeen); 4882 4883 $bodyPart = array( 4884 'body' => $_structure->getContents(array( 4885 'stream' => $_stream, 4886 )), 4887 'mimeType' => $_structure->getType() == 'text/html' ? 'text/html' : 'text/plain', 4888 'charSet' => $_structure->getCharset(), 4889 ); 4890 } 4891 return $bodyPart; 4892 } 4893 4894 /** 4895 * Get Body of message 4896 * 4897 * @param int $_uid the messageuid, 4898 * @param string $_htmlOptions how to display a message, html, plain text, ... 4899 * @param string $_partID = null the partID, may be omitted 4900 * @param Horde_Mime_Part $_structure = null if given use structure for parsing 4901 * @param boolean $_preserveSeen flag to preserve the seenflag by using body.peek 4902 * @param string $_folder folder to work on 4903 * @param Horde_Mime_part& $calendar_part =null on return calendar-part or null, if there is none 4904 * @return array containing the message body, mimeType and charset 4905 */ 4906 function getMessageBody($_uid, $_htmlOptions='', $_partID=null, Horde_Mime_Part $_structure=null, $_preserveSeen = false, $_folder = '', &$calendar_part=null) 4907 { 4908 if (self::$debug) echo __METHOD__."$_uid, $_htmlOptions, $_partID<br>"; 4909 if($_htmlOptions != '') { 4910 $this->htmlOptions = $_htmlOptions; 4911 } 4912 if (empty($_folder)) 4913 { 4914 $_folder = $this->sessionData['mailbox']; 4915 } 4916 if (empty($this->sessionData['mailbox']) && !empty($_folder)) 4917 { 4918 $this->sessionData['mailbox'] = $_folder; 4919 } 4920 4921 if (!isset($_structure)) 4922 { 4923 $_structure = $this->getStructure($_uid, $_partID, $_folder, $_preserveSeen); 4924 } 4925 if (!is_object($_structure)) 4926 { 4927 return array( 4928 array( 4929 'error' => 1, 4930 'body' => 'Error: Could not fetch structure on mail:'.$_uid." as $_htmlOptions". 'for Mailprofile'.$this->icServer->ImapServerId.' User:'.$GLOBALS['egw_info']['user']['account_lid'], 4931 'mimeType' => 'text/plain', 4932 'charSet' => self::$displayCharset, 4933 ) 4934 ); 4935 } 4936 if (!empty($_partID)) 4937 { 4938 $_structure->contentTypeMap(); 4939 $_structure = $_structure->getPart($_partID); 4940 //_debug_array($_structure->getMimeId()); exit; 4941 } 4942 4943 switch($_structure->getPrimaryType()) 4944 { 4945 case 'application': 4946 return array( 4947 array( 4948 'body' => '', 4949 'mimeType' => 'text/plain', 4950 'charSet' => 'iso-8859-1', 4951 ) 4952 ); 4953 4954 case 'multipart': 4955 switch($_structure->getSubType()) 4956 { 4957 case 'alternative': 4958 $bodyParts = array($this->getMultipartAlternative($_uid, $_structure, $this->htmlOptions, $_preserveSeen, $calendar_part)); 4959 break; 4960 4961 case 'nil': // multipart with no Alternative 4962 case 'mixed': 4963 case 'report': 4964 case 'signed': 4965 case 'encrypted': 4966 $skipParts = array(); 4967 $bodyParts = $this->getMultipartMixed($_uid, $_structure, $this->htmlOptions, $_preserveSeen, $skipParts, $calendar_part); 4968 break; 4969 4970 case 'related': 4971 $bodyParts = $this->getMultipartRelated($_uid, $_structure, $this->htmlOptions, $_preserveSeen, $calendar_part); 4972 break; 4973 } 4974 return self::normalizeBodyParts($bodyParts); 4975 4976 case 'video': 4977 case 'audio': // some servers send audiofiles and imagesfiles directly, without any stuff surround it 4978 case 'image': // they are displayed as Attachment NOT INLINE 4979 return array( 4980 array( 4981 'body' => '', 4982 'mimeType' => $_structure->getSubType(), 4983 ), 4984 ); 4985 4986 case 'text': 4987 $bodyPart = array(); 4988 if ($_structure->getDisposition() != 'attachment') 4989 { 4990 switch($_structure->getSubType()) 4991 { 4992 case 'calendar': 4993 $calendar_part = $_structure; 4994 // fall through in case user has no calendar rights 4995 case 'html': 4996 case 'plain': 4997 default: 4998 $bodyPart = array($this->getTextPart($_uid, $_structure, $this->htmlOptions, $_preserveSeen)); 4999 } 5000 } else { 5001 // what if the structure->disposition is attachment ,... 5002 } 5003 return self::normalizeBodyParts($bodyPart); 5004 5005 case 'attachment': 5006 case 'message': 5007 switch($_structure->getSubType()) 5008 { 5009 case 'rfc822': 5010 $newStructure = $_structure->getParts(); 5011 if (self::$debug) {echo __METHOD__." Message -> RFC -> NewStructure:"; _debug_array($newStructure[0]);} 5012 return self::normalizeBodyParts($this->getMessageBody($_uid, $_htmlOptions, $newStructure[0]->getMimeId(), $newStructure[0], $_preserveSeen, $_folder)); 5013 } 5014 break; 5015 5016 default: 5017 if (self::$debug) _debug_array($_structure); 5018 return array( 5019 array( 5020 'body' => lang('The mimeparser can not parse this message.').$_structure->getType(), 5021 'mimeType' => 'text/plain', 5022 'charSet' => self::$displayCharset, 5023 ) 5024 ); 5025 } 5026 } 5027 5028 /** 5029 * normalizeBodyParts - function to gather and normalize all body Information 5030 * as we may recieve a bodyParts structure from within getMessageBody nested deeper than expected 5031 * so this is used to normalize the output, so we are able to rely on our expectation 5032 * @param _bodyParts - Body Array 5033 * @return array - a normalized Bodyarray 5034 */ 5035 static function normalizeBodyParts($_bodyParts) 5036 { 5037 if (is_array($_bodyParts)) 5038 { 5039 foreach($_bodyParts as $singleBodyPart) 5040 { 5041 if (!isset($singleBodyPart['body'])) { 5042 $buff = self::normalizeBodyParts($singleBodyPart); 5043 foreach ((array)$buff as $val) { $body2return[] = $val;} 5044 continue; 5045 } 5046 $body2return[] = $singleBodyPart; 5047 } 5048 } 5049 else 5050 { 5051 $body2return = $_bodyParts; 5052 } 5053 return $body2return; 5054 } 5055 5056 /** 5057 * getdisplayableBody - creates the bodypart of the email as textual representation 5058 * @param object $mailClass the mailClass object to be used 5059 * @param array $bodyParts with the bodyparts 5060 * @param boolean $preserveHTML switch to preserve HTML 5061 * @param boolean $useTidy switch to use tidy 5062 * @return string a preformatted string with the mails converted to text 5063 */ 5064 static function &getdisplayableBody(&$mailClass, $bodyParts, $preserveHTML = false, $useTidy = true) 5065 { 5066 $message=''; 5067 for($i=0, $cnt=count($bodyParts); $i < $cnt; $i++) 5068 { 5069 if (!isset($bodyParts[$i]['body'])) { 5070 $bodyParts[$i]['body'] = self::getdisplayableBody($mailClass, $bodyParts[$i], $preserveHTML, $useTidy); 5071 $message .= empty($bodyParts[$i]['body'])?'':$bodyParts[$i]['body']; 5072 continue; 5073 } 5074 if (isset($bodyParts[$i]['error'])) continue; 5075 if (empty($bodyParts[$i]['body'])) continue; 5076 // some characterreplacements, as they fail to translate 5077 $sar = array( 5078 '@(\x84|\x93|\x94)@', 5079 '@(\x96|\x97|\x1a)@', 5080 '@(\x82|\x91|\x92)@', 5081 '@(\x85)@', 5082 '@(\x86)@', 5083 '@(\x99)@', 5084 '@(\xae)@', 5085 ); 5086 $rar = array( 5087 '"', 5088 '-', 5089 '\'', 5090 '...', 5091 '&', 5092 '(TM)', 5093 '(R)', 5094 ); 5095 5096 if(($bodyParts[$i]['mimeType'] == 'text/html' || $bodyParts[$i]['mimeType'] == 'text/plain') && 5097 strtoupper($bodyParts[$i]['charSet']) != 'UTF-8') 5098 { 5099 $bodyParts[$i]['body'] = preg_replace($sar,$rar,$bodyParts[$i]['body']); 5100 } 5101 5102 if ($bodyParts[$i]['charSet']===false) $bodyParts[$i]['charSet'] = Translation::detect_encoding($bodyParts[$i]['body']); 5103 // add line breaks to $bodyParts 5104 //error_log(__METHOD__.' ('.__LINE__.') '.' Charset:'.$bodyParts[$i]['charSet'].'->'.$bodyParts[$i]['body']); 5105 $newBody = Translation::convert_jsonsafe($bodyParts[$i]['body'], $bodyParts[$i]['charSet']); 5106 //error_log(__METHOD__.' ('.__LINE__.') '.' MimeType:'.$bodyParts[$i]['mimeType'].'->'.$newBody); 5107 $mailClass->activeMimeType = 'text/plain'; 5108 if ($bodyParts[$i]['mimeType'] == 'text/html') { 5109 $mailClass->activeMimeType = $bodyParts[$i]['mimeType']; 5110 if (!$preserveHTML) 5111 { 5112 $alreadyHtmlLawed=false; 5113 // as Translation::convert reduces \r\n to \n and purifier eats \n -> peplace it with a single space 5114 $newBody = str_replace("\n"," ",$newBody); 5115 // convert HTML to text, as we dont want HTML in infologs 5116 if ($useTidy && extension_loaded('tidy')) 5117 { 5118 $tidy = new tidy(); 5119 $cleaned = $tidy->repairString($newBody, self::$tidy_config,'utf8'); 5120 // Found errors. Strip it all so there's some output 5121 if($tidy->getStatus() == 2) 5122 { 5123 error_log(__METHOD__.' ('.__LINE__.') '.' ->'.$tidy->errorBuffer); 5124 } 5125 else 5126 { 5127 $newBody = $cleaned; 5128 } 5129 if (!$preserveHTML) 5130 { 5131 // filter only the 'body', as we only want that part, if we throw away the html 5132 $matches = array(); 5133 preg_match('`(<htm.+?<body[^>]*>)(.+?)(</body>.*?</html>)`ims', $newBody, $matches); 5134 if ($matches[2]) 5135 { 5136 $hasOther = true; 5137 $newBody = $matches[2]; 5138 } 5139 } 5140 } 5141 else 5142 { 5143 // htmLawed filter only the 'body' 5144 $matches = array(); 5145 preg_match('`(<htm.+?<body[^>]*>)(.+?)(</body>.*?</html>)`ims', $newBody, $matches); 5146 if ($matches[2]) 5147 { 5148 $hasOther = true; 5149 $newBody = $matches[2]; 5150 } 5151 $htmLawed = new Html\HtmLawed(); 5152 // the next line should not be needed, but produces better results on HTML 2 Text conversion, 5153 // as we switched off HTMLaweds tidy functionality 5154 $newBody = str_replace(array('&amp;','<DIV><BR></DIV>',"<DIV> </DIV>",'<div> </div>'),array('&','<BR>','<BR>','<BR>'),$newBody); 5155 $newBody = $htmLawed->run($newBody,self::$htmLawed_config); 5156 if ($hasOther && $preserveHTML) $newBody = $matches[1]. $newBody. $matches[3]; 5157 $alreadyHtmlLawed=true; 5158 } 5159 //error_log(__METHOD__.' ('.__LINE__.') '.' after purify:'.$newBody); 5160 if ($preserveHTML==false) $newBody = Mail\Html::convertHTMLToText($newBody,self::$displayCharset,true,true); 5161 //error_log(__METHOD__.' ('.__LINE__.') '.' after convertHTMLToText:'.$newBody); 5162 if ($preserveHTML==false) $newBody = nl2br($newBody); // we need this, as htmLawed removes \r\n 5163 /*if (!$alreadyHtmlLawed) */ $mailClass->getCleanHTML($newBody); // remove stuff we regard as unwanted 5164 if ($preserveHTML==false) $newBody = str_replace("<br />","\r\n",$newBody); 5165 //error_log(__METHOD__.' ('.__LINE__.') '.' after getClean:'.$newBody); 5166 } 5167 $message .= $newBody; 5168 continue; 5169 } 5170 //error_log(__METHOD__.' ('.__LINE__.') '.' Body(after specialchars):'.$newBody); 5171 //use Mail\Html::convertHTMLToText instead of strip_tags, (even message is plain text) as strip_tags eats away too much 5172 //$newBody = strip_tags($newBody); //we need to fix broken tags (or just stuff like "<800 USD/p" ) 5173 $newBody = Mail\Html::convertHTMLToText($newBody,self::$displayCharset,false,false); 5174 //error_log(__METHOD__.' ('.__LINE__.') '.' Body(after strip tags):'.$newBody); 5175 $newBody = htmlspecialchars_decode($newBody,ENT_QUOTES); 5176 //error_log(__METHOD__.' ('.__LINE__.') '.' Body (after hmlspc_decode):'.$newBody); 5177 $message .= $newBody; 5178 //continue; 5179 } 5180 return $message; 5181 } 5182 5183 static function wordwrap($str, $cols, $cut, $dontbreaklinesstartingwith=false) 5184 { 5185 $lines = explode("\n", $str); 5186 $newStr = ''; 5187 foreach($lines as $line) 5188 { 5189 // replace tabs by 8 space chars, or any tab only counts one char 5190 //$line = str_replace("\t"," ",$line); 5191 //$newStr .= wordwrap($line, $cols, $cut); 5192 $allowedLength = $cols-strlen($cut); 5193 //dont try to break lines with links, chance is we mess up the text is way too big 5194 if (strlen($line) > $allowedLength && stripos($line,'href=')===false && 5195 ($dontbreaklinesstartingwith==false || 5196 ($dontbreaklinesstartingwith && 5197 strlen($dontbreaklinesstartingwith)>=1 && 5198 substr($line,0,strlen($dontbreaklinesstartingwith)) != $dontbreaklinesstartingwith 5199 ) 5200 ) 5201 ) 5202 { 5203 $s=explode(" ", $line); 5204 $line = ""; 5205 $linecnt = 0; 5206 foreach ($s as &$v) { 5207 $cnt = strlen($v); 5208 // only break long words within the wordboundaries, 5209 // but it may destroy links, so we check for href and dont do it if we find one 5210 // we check for any html within the word, because we do not want to break html by accident 5211 if($cnt > $allowedLength && stripos($v,'href=')===false && stripos($v,'onclick=')===false && $cnt == strlen(html_entity_decode($v))) 5212 { 5213 $v=wordwrap($v, $allowedLength, $cut, true); 5214 } 5215 // the rest should be broken at the start of the new word that exceeds the limit 5216 if ($linecnt+$cnt > $allowedLength) { 5217 $v=$cut.$v; 5218 #$linecnt = 0; 5219 $linecnt =strlen($v)-strlen($cut); 5220 } else { 5221 $linecnt += $cnt; 5222 } 5223 if (strlen($v)) $line .= (strlen($line) ? " " : "").$v; 5224 } 5225 } 5226 $newStr .= $line . "\n"; 5227 } 5228 return $newStr; 5229 } 5230 5231 /** 5232 * getMessageEnvelope 5233 * get parsed headers from message 5234 * @param string/int $_uid the messageuid, 5235 * @param string/int $_partID = '' , the partID, may be omitted 5236 * @param boolean $decode flag to do the decoding on the fly 5237 * @param string $_folder folder to work on 5238 * @param boolean $_useHeaderInsteadOfEnvelope - force getMessageHeader method to be used for fetching Envelope Information 5239 * @return array the message header 5240 */ 5241 function getMessageEnvelope($_uid, $_partID = '',$decode=false, $_folder='', $_useHeaderInsteadOfEnvelope=false) 5242 { 5243 //error_log(__METHOD__.' ('.__LINE__.') '.":$_uid,$_partID,$decode,$_folder".function_backtrace()); 5244 if (empty($_folder)) $_folder = ($this->sessionData['mailbox']? $this->sessionData['mailbox'] : $this->icServer->getCurrentMailbox()); 5245 //error_log(__METHOD__.' ('.__LINE__.') '.":$_uid,$_partID,$decode,$_folder"); 5246 if((empty($_partID)||$_partID=='null')&&$_useHeaderInsteadOfEnvelope===false) { 5247 $uidsToFetch = new Horde_Imap_Client_Ids(); 5248 if (!(is_object($_uid) || is_array($_uid))) $_uid = (array)$_uid; 5249 $uidsToFetch->add($_uid); 5250 5251 $fquery = new Horde_Imap_Client_Fetch_Query(); 5252 $envFields = new Horde_Mime_Headers(); 5253 $fquery->envelope(); 5254 $fquery->size(); 5255 $headersNew = $this->icServer->fetch($_folder, $fquery, array( 5256 'ids' => $uidsToFetch, 5257 )); 5258 if (is_object($headersNew)) { 5259 foreach($headersNew as &$_headerObject) { 5260 $env = $_headerObject->getEnvelope(); 5261 //_debug_array($envFields->singleFields()); 5262 $singleFields = $envFields->singleFields(); 5263 foreach ($singleFields as &$v) 5264 { 5265 switch ($v) 5266 { 5267 case 'to': 5268 case 'reply-to': 5269 case 'from': 5270 case 'cc': 5271 case 'bcc': 5272 case 'sender': 5273 //error_log(__METHOD__.' ('.__LINE__.') '.$v.'->'.array2string($env->$v->addresses)); 5274 $envelope[$v]=$env->$v->addresses; 5275 $address = array(); 5276 if (!is_array($envelope[$v])) break; 5277 foreach ($envelope[$v] as $k => $ad) 5278 { 5279 if (stripos($ad,'@')===false) 5280 { 5281 $remember=$k; 5282 } 5283 else 5284 { 5285 $address[] = (!is_null($remember)?$envelope[$v][$remember].' ':'').$ad; 5286 $remember=null; 5287 } 5288 } 5289 $envelope[$v] = $address; 5290 break; 5291 case 'date': 5292 $envelope[$v]=DateTime::to($env->$v); 5293 break; 5294 default: 5295 $envelope[$v]=$env->$v; 5296 } 5297 } 5298 $envelope['size']=$_headerObject->getSize(); 5299 } 5300 } 5301 $envelope = array_change_key_case($envelope,CASE_UPPER); 5302 //if ($decode) _debug_array($envelope); 5303 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($envelope)); 5304 if ($decode) 5305 { 5306 foreach ($envelope as $key => $rvV) 5307 { 5308 //try idn conversion only on 'FROM', 'TO', 'CC', 'BCC', 'SENDER', 'REPLY-TO' 5309 $envelope[$key]=self::decode_header($rvV,in_array($key,array('FROM', 'TO', 'CC', 'BCC', 'SENDER', 'REPLY-TO'))); 5310 } 5311 } 5312 return $envelope; 5313 } else { 5314 5315 $headers = $this->getMessageHeader($_uid, $_partID, true,true,$_folder); 5316 5317 //error_log(__METHOD__.' ('.__LINE__.') '.':'.array2string($headers)); 5318 //_debug_array($headers); 5319 $newData = array( 5320 'DATE' => $headers['DATE'], 5321 'SUBJECT' => ($decode ? self::decode_header($headers['SUBJECT']):$headers['SUBJECT']), 5322 'MESSAGE_ID' => $headers['MESSAGE-ID'] 5323 ); 5324 if (isset($headers['IN-REPLY-TO'])) $newData['IN-REPLY-TO'] = $headers['IN-REPLY-TO']; 5325 if (isset($headers['REFERENCES'])) $newData['REFERENCES'] = $headers['REFERENCES']; 5326 if (isset($headers['THREAD-TOPIC'])) $newData['THREAD-TOPIC'] = $headers['THREAD-TOPIC']; 5327 if (isset($headers['THREAD-INDEX'])) $newData['THREAD-INDEX'] = $headers['THREAD-INDEX']; 5328 if (isset($headers['LIST-ID'])) $newData['LIST-ID'] = $headers['LIST-ID']; 5329 if (isset($headers['SIZE'])) $newData['SIZE'] = $headers['SIZE']; 5330 //_debug_array($newData); 5331 $recepientList = array('FROM', 'TO', 'CC', 'BCC', 'SENDER', 'REPLY-TO'); 5332 foreach($recepientList as $recepientType) { 5333 if(isset($headers[$recepientType])) { 5334 if ($decode) $headers[$recepientType] = self::decode_header($headers[$recepientType],true); 5335 //error_log(__METHOD__.__LINE__." ".$recepientType."->".array2string($headers[$recepientType])); 5336 foreach(self::parseAddressList($headers[$recepientType]) as $singleAddress) { 5337 $addressData = array( 5338 'PERSONAL_NAME' => $singleAddress->personal ? $singleAddress->personal : 'NIL', 5339 'AT_DOMAIN_LIST' => $singleAddress->adl ? $singleAddress->adl : 'NIL', 5340 'MAILBOX_NAME' => $singleAddress->mailbox ? $singleAddress->mailbox : 'NIL', 5341 'HOST_NAME' => $singleAddress->host ? $singleAddress->host : 'NIL', 5342 'EMAIL' => $singleAddress->host ? $singleAddress->mailbox.'@'.$singleAddress->host : $singleAddress->mailbox, 5343 ); 5344 if($addressData['PERSONAL_NAME'] != 'NIL') { 5345 $addressData['RFC822_EMAIL'] = imap_rfc822_write_address($singleAddress->mailbox, $singleAddress->host, $singleAddress->personal); 5346 } else { 5347 $addressData['RFC822_EMAIL'] = 'NIL'; 5348 } 5349 $newData[$recepientType][] = ($addressData['RFC822_EMAIL']!='NIL'?$addressData['RFC822_EMAIL']:$addressData['EMAIL']);//$addressData; 5350 } 5351 } else { 5352 if($recepientType == 'SENDER' || $recepientType == 'REPLY-TO') { 5353 $newData[$recepientType] = $newData['FROM']; 5354 } else { 5355 $newData[$recepientType] = array(); 5356 } 5357 } 5358 } 5359 //if ($decode) _debug_array($newData); 5360 return $newData; 5361 } 5362 } 5363 5364 /** 5365 * Get parsed headers from message 5366 * 5367 * @param string/int $_uid the messageuid, 5368 * @param string/int $_partID ='' , the partID, may be omitted 5369 * @param boolean|string $decode flag to do the decoding on the fly or "object" 5370 * @param boolean $preserveUnSeen flag to preserve the seen flag where applicable 5371 * @param string $_folder folder to work on 5372 * @return array|Horde_Mime_Headers message header as array or object 5373 */ 5374 function getMessageHeader($_uid, $_partID = '',$decode=false, $preserveUnSeen=false, $_folder='') 5375 { 5376 //error_log(__METHOD__.' ('.__LINE__.') '.':'.$_uid.', '.$_partID.', '.$decode.', '.$preserveUnSeen.', '.$_folder); 5377 if (empty($_folder)) $_folder = ($this->sessionData['mailbox']? $this->sessionData['mailbox'] : $this->icServer->getCurrentMailbox()); 5378 $uidsToFetch = new Horde_Imap_Client_Ids(); 5379 if (!(is_object($_uid) || is_array($_uid))) $_uid = (array)$_uid; 5380 $uidsToFetch->add($_uid); 5381 5382 $fquery = new Horde_Imap_Client_Fetch_Query(); 5383 if ($_partID != '') 5384 { 5385 $fquery->headerText(array('id'=>$_partID,'peek'=>$preserveUnSeen)); 5386 $fquery->structure(); 5387 } 5388 else 5389 { 5390 $fquery->headerText(array('peek'=>$preserveUnSeen)); 5391 } 5392 $fquery->size(); 5393 5394 $headersNew = $this->icServer->fetch($_folder, $fquery, array( 5395 'ids' => $uidsToFetch, 5396 )); 5397 if (is_object($headersNew)) { 5398 foreach($headersNew as $_fetchObject) 5399 { 5400 $headers = $_fetchObject->getHeaderText(0,Horde_Imap_Client_Data_Fetch::HEADER_PARSE); 5401 if ($_partID != '') 5402 { 5403 $mailStructureObject = $_fetchObject->getStructure(); 5404 foreach ($mailStructureObject->contentTypeMap() as $mime_id => $mime_type) 5405 { 5406 if ($mime_id==$_partID) 5407 { 5408 //error_log(__METHOD__.' ('.__LINE__.') '."$mime_id == $_partID".array2string($_headerObject->getHeaderText($mime_id,Horde_Imap_Client_Data_Fetch::HEADER_PARSE)->toArray())); 5409 $headers = $_fetchObject->getHeaderText($mime_id,Horde_Imap_Client_Data_Fetch::HEADER_PARSE); 5410 break; 5411 } 5412 } 5413 } 5414 $size = $_fetchObject->getSize(); 5415 //error_log(__METHOD__.__LINE__.'#'.$size); 5416 } 5417 if ($decode === 'object') 5418 { 5419 if (is_object($headers)) $headers->setUserAgent('EGroupware API '.$GLOBALS['egw_info']['server']['versions']['phpgwapi']); 5420 return $headers; 5421 } 5422 $retValue = is_object($headers) ? $headers->toArray():array(); 5423 if ($size) $retValue['size'] = $size; 5424 } 5425 $retValue = array_change_key_case($retValue,CASE_UPPER); 5426 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($retValue)); 5427 // if SUBJECT is an array, use thelast one, as we assume something with the unfolding for the subject did not work 5428 if (is_array($retValue['SUBJECT'])) 5429 { 5430 $retValue['SUBJECT'] = $retValue['SUBJECT'][count($retValue['SUBJECT'])-1]; 5431 } 5432 //error_log(__METHOD__.' ('.__LINE__.') '.':'.array2string($decode ? self::decode_header($retValue,true):$retValue)); 5433 if ($decode) 5434 { 5435 foreach ($retValue as $key => $rvV) 5436 { 5437 //try idn conversion only on 'FROM', 'TO', 'CC', 'BCC', 'SENDER', 'REPLY-TO' 5438 $retValue[$key]=self::decode_header($rvV,in_array($key,array('FROM', 'TO', 'CC', 'BCC', 'SENDER', 'REPLY-TO'))); 5439 } 5440 } 5441 return $retValue; 5442 } 5443 5444 /** 5445 * getMessageRawHeader 5446 * get messages raw header data 5447 * @param string/int $_uid the messageuid, 5448 * @param string/int $_partID = '' , the partID, may be omitted 5449 * @param string $_folder folder to work on 5450 * @return string the message header 5451 */ 5452 function getMessageRawHeader($_uid, $_partID = '', $_folder = '') 5453 { 5454 static $rawHeaders; 5455 if (empty($_folder)) $_folder = ($this->sessionData['mailbox']? $this->sessionData['mailbox'] : $this->icServer->getCurrentMailbox()); 5456 //error_log(__METHOD__.' ('.__LINE__.') '." Try Using Cache for raw Header $_uid, $_partID in Folder $_folder"); 5457 5458 if (is_null($rawHeaders)||!is_array($rawHeaders)) $rawHeaders = Cache::getCache(Cache::INSTANCE,'email','rawHeadersCache'.trim($GLOBALS['egw_info']['user']['account_id']),null,array(),60*60*1); 5459 if (isset($rawHeaders[$this->icServer->ImapServerId][(string)$_folder][$_uid][(empty($_partID)?'NIL':$_partID)])) 5460 { 5461 //error_log(__METHOD__.' ('.__LINE__.') '." Using Cache for raw Header $_uid, $_partID in Folder $_folder"); 5462 return $rawHeaders[$this->icServer->ImapServerId][(string)$_folder][$_uid][(empty($_partID)?'NIL':$_partID)]; 5463 } 5464 $uidsToFetch = new Horde_Imap_Client_Ids(); 5465 $uid = $_uid; 5466 if (!(is_object($_uid) || is_array($_uid))) $uid = (array)$_uid; 5467 $uidsToFetch->add($uid); 5468 5469 $fquery = new Horde_Imap_Client_Fetch_Query(); 5470 if ($_partID != '') 5471 { 5472 $fquery->headerText(array('id'=>$_partID,'peek'=>true)); 5473 $fquery->structure(); 5474 } 5475 else 5476 { 5477 $fquery->headerText(array('peek'=>true)); 5478 } 5479 $headersNew = $this->icServer->fetch($_folder, $fquery, array( 5480 'ids' => $uidsToFetch, 5481 )); 5482 if (is_object($headersNew)) { 5483 foreach($headersNew as &$_headerObject) { 5484 $retValue = $_headerObject->getHeaderText(); 5485 if ($_partID != '') 5486 { 5487 $mailStructureObject = $_headerObject->getStructure(); 5488 foreach ($mailStructureObject->contentTypeMap() as $mime_id => $mime_type) 5489 { 5490 if ($mime_id==$_partID) 5491 { 5492 $retValue = $_headerObject->getHeaderText($mime_id); 5493 } 5494 } 5495 } 5496 } 5497 } 5498 $rawHeaders[$this->icServer->ImapServerId][(string)$_folder][$_uid][(empty($_partID)?'NIL':$_partID)]=$retValue; 5499 Cache::setCache(Cache::INSTANCE,'email','rawHeadersCache'.trim($GLOBALS['egw_info']['user']['account_id']),$rawHeaders,60*60*1); 5500 return $retValue; 5501 } 5502 5503 /** 5504 * getStyles - extracts the styles from the given bodyparts 5505 * @param array $_bodyParts with the bodyparts 5506 * @return string a preformatted string with the mails converted to text 5507 */ 5508 static function &getStyles($_bodyParts) 5509 { 5510 $style = $ret = ''; 5511 if (empty($_bodyParts)) return $ret; 5512 foreach((array)$_bodyParts as $singleBodyPart) { 5513 if (!isset($singleBodyPart['body'])) { 5514 $singleBodyPart['body'] = self::getStyles($singleBodyPart); 5515 $style .= $singleBodyPart['body']; 5516 continue; 5517 } 5518 5519 if ($singleBodyPart['charSet']===false) $singleBodyPart['charSet'] = Translation::detect_encoding($singleBodyPart['body']); 5520 $singleBodyPart['body'] = Translation::convert( 5521 $singleBodyPart['body'], 5522 strtolower($singleBodyPart['charSet']) 5523 ); 5524 $ct = 0; 5525 $newStyle=array(); 5526 if (stripos($singleBodyPart['body'],'<style')!==false) $ct = preg_match_all('#<style(?:\s.*)?>(.+)</style>#isU', $singleBodyPart['body'], $newStyle); 5527 if ($ct>0) 5528 { 5529 //error_log(__METHOD__.' ('.__LINE__.') '.'#'.$ct.'#'.array2string($newStyle)); 5530 $style2buffer = implode('',$newStyle[0]); 5531 } 5532 if ($style2buffer && strtoupper(self::$displayCharset) == 'UTF-8') 5533 { 5534 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($style2buffer)); 5535 $test = json_encode($style2buffer); 5536 //error_log(__METHOD__.' ('.__LINE__.') '.'#'.$test.'# ->'.strlen($style2buffer).' Error:'.json_last_error()); 5537 //if (json_last_error() != JSON_ERROR_NONE && strlen($style2buffer)>0) 5538 if ($test=="null" && strlen($style2buffer)>0) 5539 { 5540 // this should not be needed, unless something fails with charset detection/ wrong charset passed 5541 error_log(__METHOD__.' ('.__LINE__.') '.' Found Invalid sequence for utf-8 in CSS:'.$style2buffer.' Charset Reported:'.$singleBodyPart['charSet'].' Carset Detected:'.Translation::detect_encoding($style2buffer)); 5542 $style2buffer = utf8_encode($style2buffer); 5543 } 5544 } 5545 $style .= $style2buffer; 5546 } 5547 // clean out comments and stuff 5548 $search = array( 5549 '@url\(http:\/\/[^\)].*?\)@si', // url calls e.g. in style definitions 5550// '@<!--[\s\S]*?[ \t\n\r]*-->@', // Strip multi-line comments including CDATA 5551// '@<!--[\s\S]*?[ \t\n\r]*--@', // Strip broken multi-line comments including CDATA 5552 ); 5553 $style = preg_replace($search,"",$style); 5554 5555 // CSS Security 5556 // http://code.google.com/p/browsersec/wiki/Part1#Cascading_stylesheets 5557 $css = preg_replace('/(javascript|expression|-moz-binding)/i','',$style); 5558 if (stripos($css,'script')!==false) Mail\Html::replaceTagsCompletley($css,'script'); // Strip out script that may be included 5559 // we need this, as styledefinitions are enclosed with curly brackets; and template stuff tries to replace everything between curly brackets that is having no horizontal whitespace 5560 // as the comments as <!-- styledefinition --> in stylesheet are outdated, and ck-editor does not understand it, we remove it 5561 $css = str_replace(array(':','<!--','-->'),array(': ','',''),$css); 5562 //error_log(__METHOD__.' ('.__LINE__.') '.$css); 5563 5564 // check if the outlook style fix is there then set the initial line-height, since the fix is setting it to line-height:0 5565 // which breaks all tr lines in the content. 5566 if (preg_match('/Outlook 2016 Height Fix/i', $css)) $css .='<style>tr {line-height: initial} </style>'; 5567 5568 // TODO: we may have to strip urls and maybe comments and ifs 5569 return $css; 5570 } 5571 5572 /** 5573 * getMessageRawBody 5574 * get the message raw body 5575 * @param string/int $_uid the messageuid, 5576 * @param string/int $_partID = '' , the partID, may be omitted 5577 * @param string $_folder folder to work on 5578 * @param boolean $_stream =false true: return a stream, false: return string, stream suppresses any caching 5579 * @return string the message body 5580 */ 5581 function getMessageRawBody($_uid, $_partID = '', $_folder='', $_stream=false) 5582 { 5583 //TODO: caching einbauen static! 5584 static $rawBody; 5585 if (is_null($rawBody)) $rawBody = array(); 5586 if (empty($_folder)) $_folder = ($this->sessionData['mailbox']? $this->sessionData['mailbox'] : $this->icServer->getCurrentMailbox()); 5587 if (!$_stream && isset($rawBody[$this->icServer->ImapServerId][$_folder][$_uid][(empty($_partID)?'NIL':$_partID)])) 5588 { 5589 //error_log(__METHOD__.' ('.__LINE__.') '." Using Cache for raw Body $_uid, $_partID in Folder $_folder"); 5590 return $rawBody[$this->icServer->ImapServerId][$_folder][$_uid][(empty($_partID)?'NIL':$_partID)]; 5591 } 5592 5593 $uidsToFetch = new Horde_Imap_Client_Ids(); 5594 $uid = $_uid; 5595 if (!(is_object($_uid) || is_array($_uid))) $uid = (array)$_uid; 5596 $uidsToFetch->add($uid); 5597 5598 $fquery = new Horde_Imap_Client_Fetch_Query(); 5599 $fquery->fullText(array('peek'=>true)); 5600 if ($_partID != '') 5601 { 5602 $fquery->structure(); 5603 $fquery->bodyPart($_partID,array('peek'=>true)); 5604 } 5605 $headersNew = $this->icServer->fetch($_folder, $fquery, array( 5606 'ids' => $uidsToFetch, 5607 )); 5608 if (is_object($headersNew)) { 5609 foreach($headersNew as &$_headerObject) { 5610 $body = $_headerObject->getFullMsg($_stream); 5611 if ($_partID != '') 5612 { 5613 $mailStructureObject = $_headerObject->getStructure(); 5614 //_debug_array($mailStructureObject->contentTypeMap()); 5615 foreach ($mailStructureObject->contentTypeMap() as $mime_id => $mime_type) 5616 { 5617 if ($mime_id==$_partID) 5618 { 5619 $body = $_headerObject->getBodyPart($mime_id, $_stream); 5620 } 5621 } 5622 } 5623 } 5624 } 5625 if (!$_stream) 5626 { 5627 //error_log(__METHOD__.' ('.__LINE__.') '."[{$this->icServer->ImapServerId}][$_folder][$_uid][".(empty($_partID)?'NIL':$_partID)."]"); 5628 $rawBody[$this->icServer->ImapServerId][$_folder][$_uid][(empty($_partID)?'NIL':$_partID)] = $body; 5629 } 5630 return $body; 5631 } 5632 5633 /** 5634 * Get structure of a mail or part of a mail 5635 * 5636 * @param int $_uid 5637 * @param string $_partID = null 5638 * @param string $_folder = null 5639 * @param boolean $_preserveSeen = false flag to preserve the seenflag by using body.peek 5640 * @param Horde_Imap_Client_Fetch_Query $fquery=null default query just structure 5641 * @return Horde_Mime_Part 5642 */ 5643 function getStructure($_uid, $_partID=null, $_folder=null, $_preserveSeen=false) 5644 { 5645 if (self::$debug) error_log( __METHOD__.' ('.__LINE__.') '.":$_uid, $_partID"); 5646 5647 if (empty($_folder)) 5648 { 5649 $_folder = ($this->sessionData['mailbox']? $this->sessionData['mailbox'] : $this->icServer->getCurrentMailbox()); 5650 } 5651 $uidsToFetch = new Horde_Imap_Client_Ids(); 5652 5653 if (!(is_object($_uid) || is_array($_uid))) 5654 { 5655 $_uid = (array)$_uid; 5656 } 5657 5658 $uidsToFetch->add($_uid); 5659 5660 try 5661 { 5662 $_fquery = new Horde_Imap_Client_Fetch_Query(); 5663 // not sure why Klaus add these, seem not necessary 5664 // $fquery->envelope(); 5665 // $fquery->size(); 5666 $_fquery->structure(); 5667 if ($_partID) $_fquery->bodyPart($_partID, array('peek' => $_preserveSeen)); 5668 5669 $mail = $this->icServer->fetch($_folder, $_fquery, array( 5670 'ids' => $uidsToFetch, 5671 ))->first(); 5672 if (is_object($mail)) 5673 { 5674 $structure = $mail->getStructure(); 5675 $isSmime = Mail\Smime::isSmime(($mimeType = $structure->getType())) || Mail\Smime::isSmime(($protocol=$structure->getContentTypeParameter('protocol'))); 5676 if ($isSmime && !class_exists('mail_zpush', false)) 5677 { 5678 return $this->resolveSmimeMessage($structure, array( 5679 'uid' => $_uid, 5680 'mailbox' => $_folder, 5681 'mimeType' => Mail\Smime::isSmime($protocol) ? $protocol: $mimeType 5682 )); 5683 } 5684 return $mail->getStructure(); 5685 } 5686 else 5687 { 5688 return null; 5689 } 5690 } 5691 catch (Mail\Smime\PassphraseMissing $e) 5692 { 5693 // re-throw the exception to be caught on UI 5694 throw $e; 5695 } 5696 catch (Exception $e) 5697 { 5698 error_log(__METHOD__.' ('.__LINE__.') '.' Could not fetch structure on mail:'.$_uid.' Serverprofile->'.$this->icServer->ImapServerId.' Message:'.$e->getMessage().' Stack:'.function_backtrace()); 5699 return null; 5700 } 5701 } 5702 5703 /** 5704 * Parse the structure for attachments 5705 * 5706 * Returns not the attachments itself, but an array of information about the attachment 5707 * 5708 * @param int $_uid the messageuid, 5709 * @param string $_partID = null , the partID, may be omitted 5710 * @param Horde_Mime_Part $_structure = null if given use structure for parsing 5711 * @param boolean $fetchEmbeddedImages = true, 5712 * @param boolean $fetchTextCalendar = false, 5713 * @param boolean $resolveTNEF = true 5714 * @param string $_folder folder to work on 5715 * @return array an array of information about the attachment: array of array(name, size, mimeType, partID, encoding) 5716 */ 5717 function getMessageAttachments($_uid, $_partID=null, Horde_Mime_Part $_structure=null, $fetchEmbeddedImages=true, $fetchTextCalendar=false, $resolveTNEF=true, $_folder='') 5718 { 5719 if (self::$debug) error_log( __METHOD__.":$_uid, $_partID"); 5720 if (empty($_folder)) $_folder = ($this->sessionData['mailbox']? $this->sessionData['mailbox'] : $this->icServer->getCurrentMailbox()); 5721 $attachments = array(); 5722 if (!isset($_structure)) 5723 { 5724 $_structure = $this->getStructure($_uid, $_partID,$_folder,true); 5725 //error_log(__METHOD__.' ('.__LINE__.') '.':'.print_r($_structure->contentTypeMap(),true)); 5726 } 5727 if (!$_structure || !$_structure->contentTypeMap()) return array(); 5728 if (!empty($_partID)) $_structure = $_structure->getPart($_partID); 5729 $skipParts = array(); 5730 $tnefParts = array(); 5731 $skip = 0; 5732 foreach($_structure->contentTypeMap() as $mime_id => $mime_type) 5733 { 5734 // skip multipart/encrypted incl. its two sub-parts, as we show 2. sub-part as body to be decrypted client-side 5735 if ($mime_type == 'multipart/encrypted') 5736 { 5737 $skip = 2; 5738 continue; 5739 } 5740 elseif($skip) 5741 { 5742 $skip--; 5743 continue; 5744 } 5745 $part = $_structure->getPart($mime_id); 5746 //error_log(__METHOD__.' ('.__LINE__.') '.':'.' Uid:'.$uid.' Part:'.$_partID.'->'.array2string($part->getMimeId())); 5747 //error_log(__METHOD__.' ('.__LINE__.') '.':'.' Uid:'.$uid.' Part:'.$_partID.'->'.$part->getPrimaryType().'/'.$part->getSubType().'->'.$part->getDisposition()); 5748 //error_log(__METHOD__.' ('.__LINE__.') '.':'.' Uid:'.$uid.' Part:'.$_partID.'->'.array2string($part->getAllDispositionParameters())); 5749 //error_log(__METHOD__.' ('.__LINE__.') '.':'.' Uid:'.$uid.' Part:'.$_partID.'->'.array2string($part->getAllContentTypeParameters())); 5750 $partDisposition = $part->getDisposition(); 5751 $partPrimaryType = $part->getPrimaryType(); 5752 // we only want to retrieve the attachments of the current mail, not those of possible 5753 // attached mails 5754 if ($mime_type=='message/rfc822' && $_partID!=$mime_id) 5755 { 5756 //error_log(__METHOD__.' ('.__LINE__.') '.' Uid:'.$uid.'->'.$mime_id.':'.array2string($part->contentTypeMap())); 5757 foreach($part->contentTypeMap() as $sub_id => $sub_type) {if ($sub_id != $mime_id) $skipParts[$sub_id] = $sub_type;} 5758 } 5759 if (empty($partDisposition) && $partPrimaryType != 'multipart' && $partPrimaryType != 'text') 5760 { 5761 // the absence of an partDisposition does not necessarily indicate there is no attachment. it may be an 5762 // attachment with no link to show the attachment inline. 5763 // Considering this: we "list" everything that matches the above criteria 5764 // as attachment in order to not loose/miss information on our data 5765 $partDisposition='attachment'; 5766 } 5767 //error_log(__METHOD__.' ('.__LINE__.') '.' Uid:'.$uid.' Part:'.$_partID.'->'.$mime_id.':'.array2string($skipParts)); 5768 if (array_key_exists($mime_id,$skipParts)) continue; 5769 5770 if ($partDisposition == 'attachment' || 5771 (($partDisposition == 'inline' || empty($partDisposition)) && $partPrimaryType == 'image' && $part->getContentId()=='') || 5772 (($partDisposition == 'inline' || empty($partDisposition)) && $partPrimaryType != 'image' && $partPrimaryType != 'text' && $partPrimaryType != 'multipart') || 5773 ($mime_type=='image/tiff') || //always fetch. even if $fetchEmbeddedImages is false. as we cannot display tiffs 5774 ($fetchEmbeddedImages && ($partDisposition == 'inline' || empty($partDisposition)) && $partPrimaryType == 'image') || 5775 ($fetchTextCalendar && $partPrimaryType == 'text' && $part->getSubType() == 'calendar')) 5776 { 5777 // if type is message/rfc822 and _partID is given, and MimeID equals partID 5778 // we attempt to fetch "ourselves" 5779 if ($_partID==$part->getMimeId() && $part->getPrimaryType()=='message') continue; 5780 $attachment = $part->getAllDispositionParameters(); 5781 $attachment['disposition'] = $part->getDisposition(); 5782 $attachment['mimeType'] = $mime_type; 5783 $attachment['uid'] = $_uid; 5784 $attachment['partID'] = $mime_id; 5785 if (!isset($attachment['name'])||empty($attachment['name'])) $attachment['name'] = $part->getName() ? $part->getName() : ($mime_type == "message/rfc822" ? lang('forwarded message') : lang('attachment'));; 5786 if ($fetchTextCalendar) 5787 { 5788 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($part->getAllContentTypeParameters())); 5789 $method = $part->getContentTypeParameter('method'); 5790 if ($method) $attachment['method'] = $method; 5791 if (!isset($attachment['name'])) $attachment['name'] = 'event.ics'; 5792 } 5793 $attachment['size'] = $part->getBytes(); 5794 if (($cid = $part->getContentId())) $attachment['cid'] = $cid; 5795 if (empty($attachment['name'])) $attachment['name'] = (isset($attachment['cid'])&&!empty($attachment['cid'])?$attachment['cid']:lang("unknown").'_Uid'.$_uid.'_Part'.$mime_id).'.'.MimeMagic::mime2ext($mime_type); 5796 //error_log(__METHOD__.' ('.__LINE__.') '.' Uid:'.$uid.' Part:'.$_partID.'->'.$mime_id.':'.array2string($attachment)); 5797 //typical winmail.dat attachment is 5798 //Array([size] => 1462762[filename] => winmail.dat[mimeType] => application/ms-tnef[uid] => 100[partID] => 2[name] => winmail.dat) 5799 if ($resolveTNEF && ($attachment['mimeType']=='application/ms-tnef' || !strcasecmp($attachment['name'],'winmail.dat'))) 5800 { 5801 $tnefParts[] = $attachment; 5802 } 5803 else 5804 { 5805 $attachments[] = $attachment; 5806 } 5807 } 5808 } 5809 if ($resolveTNEF && !empty($tnefParts)) 5810 { 5811 //error_log(__METHOD__.__LINE__.array2string($tnefParts)); 5812 foreach ($tnefParts as $k => $tnp) 5813 { 5814 $tnefResolved=false; 5815 $tnef_data = $this->getAttachment($tnp['uid'],$tnp['partID'],$k,false); 5816 $myTnef = $this->tnef_decoder($tnef_data['attachment']); 5817 //error_log(__METHOD__.__LINE__.array2string($myTnef->getParts())); 5818 // Note: MimeId starts with 0, almost always, we cannot use that as winmail_id 5819 // we need to build Something that meets the needs 5820 if ($myTnef) 5821 { 5822 foreach($myTnef->getParts() as $mime_id => $part) 5823 { 5824 $tnefResolved=true; 5825 $attachment = $part->getAllDispositionParameters(); 5826 $attachment['disposition'] = $part->getDisposition(); 5827 $attachment['mimeType'] = $part->getType(); 5828 $attachment['uid'] = $tnp['uid']; 5829 $attachment['partID'] = $tnp['partID']; 5830 $attachment['is_winmail'] = $tnp['uid'].'@'.$tnp['partID'].'@'.$mime_id; 5831 if (!isset($attachment['name'])||empty($attachment['name'])) $attachment['name'] = $part->getName(); 5832 $attachment['size'] = $part->getBytes(); 5833 if (($cid = $part->getContentId())) $attachment['cid'] = $cid; 5834 if (empty($attachment['name'])) $attachment['name'] = (isset($attachment['cid'])&&!empty($attachment['cid'])?$attachment['cid']:lang("unknown").'_Uid'.$_uid.'_Part'.$mime_id).'.'.MimeMagic::mime2ext($attachment['mimeType']); 5835 $attachments[] = $attachment; 5836 } 5837 } 5838 if ($tnefResolved===false) $attachments[]=$tnp; 5839 } 5840 } 5841 //error_log(__METHOD__.__LINE__.array2string($attachments)); 5842 return $attachments; 5843 } 5844 5845 /** 5846 * Decode TNEF type attachment into Multipart/mixed attachment 5847 * 5848 * @param MIME object $data Mime part object 5849 * 5850 * @return boolean|Horde_Mime_part Multipart/Mixed part decoded attachments | 5851 * return false if there's no attachments or failure 5852 */ 5853 public function tnef_decoder( $data ) 5854 { 5855 foreach(array('Horde_Compress', 'Horde_Icalendar', 'Horde_Mapi') as $class) 5856 { 5857 if (!class_exists($class)) 5858 { 5859 error_log(__METHOD__."() missing required PEAR package $class --> aborting"); 5860 return false; 5861 } 5862 } 5863 $parts_obj = new Horde_Mime_part; 5864 $parts_obj->setType('multipart/mixed'); 5865 5866 $tnef_object = Horde_Compress::factory('tnef'); 5867 try 5868 { 5869 $tnef_data = $tnef_object->decompress($data); 5870 } 5871 catch (Horde_Exception $ex) 5872 { 5873 error_log(__METHOD__."() ".$ex->getMessage().' --> aborting'); 5874 _egw_log_exception($ex); 5875 return false; 5876 } 5877 if (is_array($tnef_data)) 5878 { 5879 foreach ($tnef_data as &$data) 5880 { 5881 $tmp_part = new Horde_Mime_part; 5882 5883 $tmp_part->setName($data['name']); 5884 $tmp_part->setContents($data['stream']); 5885 $tmp_part->setDescription($data['name']); 5886 5887 $type = $data['type'] . '/' . $data['subtype']; 5888 if (in_array($type, array('application/octet-stream', 'application/base64'))) 5889 { 5890 $type = Horde_Mime_Magic::filenameToMIME($data['name']); 5891 } 5892 $tmp_part->setType($type); 5893 //error_log(__METHOD__.__LINE__.array2string($tmp_part)); 5894 $parts_obj->addPart($tmp_part); 5895 } 5896 $parts_obj->buildMimeIds(); 5897 return $parts_obj; 5898 } 5899 return false; 5900 } 5901 5902 /** 5903 * Get attachment data as string, to be used with Link::(get|set)_data() 5904 * 5905 * @param int $acc_id 5906 * @param string $_mailbox 5907 * @param int $_uid 5908 * @param string $_partID 5909 * @param int $_winmail_nr 5910 * @return resource stream with attachment content 5911 */ 5912 public static function getAttachmentAccount($acc_id, $_mailbox, $_uid, $_partID, $_winmail_nr) 5913 { 5914 $bo = self::getInstance(false, $acc_id); 5915 5916 $attachment = $bo->getAttachment($_uid, $_partID, $_winmail_nr, false, true, $_mailbox); 5917 5918 return $attachment['attachment']; 5919 } 5920 5921 /** 5922 * Retrieve tnef attachments 5923 * 5924 * @param int $_uid the uid of the message 5925 * @param string $_partID the id of the part, which holds the attachment 5926 * @param boolean $_stream =false flag to indicate if the attachment is to be fetched or returned as filepointer 5927 * @param string $_folder =null folder to use if not current folder 5928 * 5929 * @return array returns an array of all resolved embeded attachments from winmail.dat 5930 */ 5931 function getTnefAttachments ($_uid, $_partID, $_stream=false, $_folder=null) 5932 { 5933 $tnef_data = $this->getAttachment($_uid, $_partID,0,false, false , $_folder); 5934 $tnef_parts = $this->tnef_decoder($tnef_data['attachment']); 5935 $attachments = array(); 5936 if ($tnef_parts) 5937 { 5938 foreach($tnef_parts->getParts() as $mime_id => $part) 5939 { 5940 5941 $attachment = $part->getAllDispositionParameters(); 5942 $attachment['mimeType'] = $part->getType(); 5943 if (!isset($attachment['filename'])||empty($attachment['filename'])) $attachment['filename'] = $part->getName(); 5944 if (($cid = $part->getContentId())) $attachment['cid'] = $cid; 5945 if (empty($attachment['filename'])) 5946 { 5947 $attachment['filename'] = (isset($attachment['cid'])&&!empty($attachment['cid'])? 5948 $attachment['cid']:lang("unknown").'_Uid'.$_uid.'_Part'.$mime_id).'.'.MimeMagic::mime2ext($attachment['mimeType']); 5949 } 5950 5951 $attachment['attachment'] = $part->getContents(array('stream'=>$_stream)); 5952 5953 $attachments[$_uid.'@'.$_partID.'@'.$mime_id] = $attachment; 5954 } 5955 } 5956 if (!is_array($attachments)) return false; 5957 return $attachments; 5958 } 5959 5960 /** 5961 * Retrieve a attachment 5962 * 5963 * @param int $_uid the uid of the message 5964 * @param string $_partID the id of the part, which holds the attachment 5965 * @param int $_winmail_nr = 0 winmail.dat attachment nr. 5966 * @param boolean $_returnPart =true flag to indicate if the attachment is to be returned as horde mime part object 5967 * @param boolean $_stream =false flag to indicate if the attachment is to be fetched or returned as filepointer 5968 * @param string $_folder =null folder to use if not current folder 5969 * 5970 * @return array 5971 */ 5972 function getAttachment($_uid, $_partID, $_winmail_nr=0, $_returnPart=true, $_stream=false, $_folder=null) 5973 { 5974 //error_log(__METHOD__.__LINE__."Uid:$_uid, PartId:$_partID, WinMailNr:$_winmail_nr, ReturnPart:$_returnPart, Stream:$_stream, Folder:$_folder".function_backtrace()); 5975 if (!isset($_folder)) $_folder = ($this->sessionData['mailbox']? $this->sessionData['mailbox'] : $this->icServer->getCurrentMailbox()); 5976 5977 $uidsToFetch = new Horde_Imap_Client_Ids(); 5978 if (!(is_object($_uid) || is_array($_uid))) $_uid = (array)$_uid; 5979 $uidsToFetch->add($_uid); 5980 5981 $fquery = new Horde_Imap_Client_Fetch_Query(); 5982 $fquery->structure(); 5983 $fquery->bodyPart($_partID, array('peek'=>true)); 5984 $headersNew = $this->icServer->fetch($_folder, $fquery, array( 5985 'ids' => $uidsToFetch, 5986 )); 5987 if (is_object($headersNew)) { 5988 foreach($headersNew as $id=>$_headerObject) { 5989 $body = $_headerObject->getFullMsg(); 5990 if ($_partID != '') 5991 { 5992 $mailStructureObject = $_headerObject->getStructure(); 5993 if (!class_exists('mail_zpush', false) && (Mail\Smime::isSmime(($mimeType = $mailStructureObject->getType())) || 5994 Mail\Smime::isSmime(($protocol=$mailStructureObject->getContentTypeParameter('protocol'))))) 5995 { 5996 $mailStructureObject = $this->resolveSmimeMessage($mailStructureObject, array( 5997 'uid' => $_uid, 5998 'mailbox' => $_folder, 5999 'mimeType' => Mail\Smime::isSmime($protocol) ? $protocol : $mimeType 6000 6001 )); 6002 } 6003 $mailStructureObject->contentTypeMap(); 6004 $part = $mailStructureObject->getPart($_partID); 6005 $partDisposition = ($part?$part->getDisposition():'failed'); 6006 if ($partDisposition=='failed') 6007 { 6008 error_log(__METHOD__.'('.__LINE__.'):'.array2string($_uid).','.$_partID.' ID:'.$id.' HObject:'.array2string($_headerObject).' StructureObject:'.array2string($mailStructureObject->contentTypeMap()).'->'.function_backtrace()); 6009 } 6010 // if $partDisposition is empty, we assume attachment, and hope that the function 6011 // itself is only triggered to fetch attachments 6012 if (empty($partDisposition)) $partDisposition='attachment'; 6013 if ($part && ($partDisposition=='attachment' || $partDisposition=='inline' || ($part->getPrimaryType() == 'text' && $part->getSubType() == 'calendar'))) 6014 { 6015 //$headerObject=$part->getAllDispositionParameters();//not used anywhere around here 6016 $structure_mime = $part->getType(); 6017 $filename = $part->getName(); 6018 $charset = $part->getContentTypeParameter('charset'); 6019 //$structure_bytes = $part->getBytes(); $structure_partID=$part->getMimeId(); error_log(__METHOD__.__LINE__." fetchPartContents(".array2string($_uid).", $structure_partID, $_stream, $_preserveSeen,$structure_mime)" ); 6020 if (empty($part->getContents())) $this->fetchPartContents($_uid, $part, $_stream, $_preserveSeen=true,$structure_mime); 6021 if ($_returnPart) return $part; 6022 } 6023 } 6024 } 6025 } 6026 $ext = MimeMagic::mime2ext($structure_mime); 6027 if ($ext && stripos($filename,'.')===false && stripos($filename,$ext)===false) $filename = trim($filename).'.'.$ext; 6028 if (!$part) 6029 { 6030 throw new Exception\WrongParameter("Error: Could not fetch attachment for Uid=".array2string($_uid).", PartId=$_partID, WinMailNr=$_winmail_nr, folder=$_folder"); 6031 } 6032 $attachmentData = array( 6033 'type' => $structure_mime, 6034 'charset' => $charset, 6035 'filename' => $filename, 6036 'attachment' => $part->getContents(array( 6037 // tnef_decode needs strings not a stream 6038 'stream' => $_stream && !($filename == 'winmail.dat' && $_winmail_nr) 6039 )), 6040 ); 6041 6042 // try guessing the mimetype, if we get the application/octet-stream 6043 if (strtolower($attachmentData['type']) == 'application/octet-stream') $attachmentData['type'] = MimeMagic::filename2mime($attachmentData['filename']); 6044 # if the attachment holds a winmail number and is a winmail.dat then we have to handle that. 6045 if ( $filename == 'winmail.dat' && $_winmail_nr) 6046 { 6047 //by now _uid is of type array 6048 $tnefResolved=false; 6049 $wantedPart=$_uid[0].'@'.$_partID; 6050 $myTnef = $this->tnef_decoder($attachmentData['attachment']); 6051 //error_log(__METHOD__.__LINE__.array2string($myTnef->getParts())); 6052 // Note: MimeId starts with 0, almost always, we cannot use that as winmail_id 6053 // we need to build Something that meets the needs 6054 if ($myTnef) 6055 { 6056 foreach($myTnef->getParts() as $mime_id => $part) 6057 { 6058 $tnefResolved=true; 6059 $attachment = $part->getAllDispositionParameters(); 6060 $attachment['mimeType'] = $part->getType(); 6061 //error_log(__METHOD__.__LINE__.'#'.$mime_id.'#'.$filename.'#'.array2string($attachment)); 6062 //error_log(__METHOD__.__LINE__." $_winmail_nr == $wantedPart@$mime_id"); 6063 if ($_winmail_nr == $wantedPart.'@'.$mime_id) 6064 { 6065 //error_log(__METHOD__.__LINE__.'#'.$structure_mime.'#'.$filename.'#'.array2string($attachment)); 6066 if (!isset($attachment['filename'])||empty($attachment['filename'])) $attachment['filename'] = $part->getName(); 6067 if (($cid = $part->getContentId())) $attachment['cid'] = $cid; 6068 if (empty($attachment['filename'])) $attachment['filename'] = (isset($attachment['cid'])&&!empty($attachment['cid'])?$attachment['cid']:lang("unknown").'_Uid'.$_uid.'_Part'.$mime_id).'.'.MimeMagic::mime2ext($attachment['mimeType']); 6069 $wmattach = $attachment; 6070 $wmattach['attachment'] = $part->getContents(array('stream'=>$_stream)); 6071 6072 } 6073 } 6074 } 6075 if ($tnefResolved) 6076 { 6077 $ext = MimeMagic::mime2ext($wmattach['mimeType']); 6078 if ($ext && stripos($wmattach['filename'],'.')===false && stripos($wmattach['filename'],$ext)===false) $wmattach['filename'] = trim($wmattach['filename']).'.'.$ext; 6079 $attachmentData = array( 6080 'type' => $wmattach['mimeType'], 6081 'filename' => $wmattach['filename'], 6082 'attachment' => $wmattach['attachment'], 6083 ); 6084 } 6085 } 6086 return $attachmentData; 6087 } 6088 6089 /** 6090 * Fetch a specific attachment from a message by it's cid 6091 * 6092 * this function is based on a on "Building A PHP-Based Mail Client" 6093 * http://www.devshed.com 6094 * 6095 * @param string|int $_uid 6096 * @param string $_cid 6097 * @param string $_part 6098 * @param boolean $_stream = null null do NOT fetch content, use fetchPartContents later 6099 * true: 6100 * @return Horde_Mime_Part|false false on error / not found 6101 */ 6102 function getAttachmentByCID($_uid, $_cid, $_part, $_stream=null) 6103 { 6104 // some static variables to avoid fetching the same mail multiple times 6105 static $uid=null, $part=null, $structure=null; 6106 //error_log(__METHOD__.' ('.__LINE__.') '.":$_uid, $_cid, $_part"); 6107 6108 if(empty($_cid)) return false; 6109 6110 if ($_uid != $uid || $_part != $part) 6111 { 6112 $structure = $this->getStructure($uid=$_uid, $part=$_part); 6113 } 6114 if (!$structure) return false; 6115 6116 /** @var Horde_Mime_Part */ 6117 $attachment = null; 6118 foreach($structure->contentTypeMap() as $mime_id => $mime_type) 6119 { 6120 $part = $structure->getPart($mime_id); 6121 6122 if ($part->getPrimaryType() == 'image' && 6123 (($cid = $part->getContentId()) && 6124 // RB: seem a bit fague to search for inclusion in both ways 6125 (strpos($cid, $_cid) !== false || strpos($_cid, $cid) !== false)) || 6126 (($name = $part->getName()) && 6127 (strpos($name, $_cid) !== false || strpos($_cid, $name) !== false))) 6128 { 6129 // if we have a direct match, dont search any further 6130 if ($cid == $_cid) 6131 { 6132 $attachment = $part; 6133 } 6134 // everything else we only consider after we checked all 6135 if (!isset($attachment)) $attachment = $part; 6136 // do we want content fetched, can be done later, if not needed 6137 if (isset($_stream)) 6138 { 6139 $this->fetchPartContents($_uid, $attachment, $_stream); 6140 } 6141 if (isset($attachment)) break; 6142 } 6143 } 6144 // set name as filename, if not set 6145 if ($attachment && !$attachment->getDispositionParameter('filename')) 6146 { 6147 $attachment->setDispositionParameter('filename', $attachment->getName()); 6148 } 6149 // guess type, if not set 6150 if ($attachment && $attachment->getType() == 'application/octet-stream') 6151 { 6152 $attachment->setType(MimeMagic::filename2mime($attachment->getDispositionParameter('filename'))); 6153 } 6154 //error_log(__METHOD__."($_uid, '$_cid', '$_part') returning ".array2string($attachment)); 6155 return $attachment; 6156 } 6157 6158 /** 6159 * Fetch and add contents to a part 6160 * 6161 * To get contents you use $part->getContents(); 6162 * 6163 * @param int $_uid 6164 * @param Horde_Mime_Part $part 6165 * @param boolean $_stream = false true return a stream, false a string 6166 * @param boolean $_preserveSeen flag to preserve the seenflag by using body.peek 6167 * @param string $_mimetype to decide wether to try to fetch part as binary or not 6168 * @return Horde_Mime_Part 6169 */ 6170 public function fetchPartContents($_uid, Horde_Mime_Part $part=null, $_stream=false, $_preserveSeen=false, $_mimetype=null) 6171 { 6172 if (is_null($part)) return null;//new Horde_Mime_Part; 6173 $encoding = null; 6174 $fetchAsBinary = true; 6175 if ($_mimetype && strtolower($_mimetype)=='message/rfc822') $fetchAsBinary = false; 6176 // we need to set content on structure to decode transfer encoding 6177 $part->setContents( 6178 $this->getBodyPart($_uid, $part->getMimeId(), null, $_preserveSeen, $_stream, $encoding, $fetchAsBinary), 6179 array('encoding' => (!$fetchAsBinary&&!$encoding?'8bit':$encoding))); 6180 6181 return $part; 6182 } 6183 6184 /** 6185 * save a message in folder 6186 * throws exception on failure 6187 * @todo set flags again 6188 * 6189 * @param string _folderName the foldername 6190 * @param string|resource _header header part of message or resource with hole message 6191 * @param string _body body part of message, only used if _header is NO resource 6192 * @param string _flags = '\\Recent'the imap flags to set for the saved message 6193 * 6194 * @return the id of the message appended or exception 6195 * @throws Exception\WrongUserinput 6196 */ 6197 function appendMessage($_folderName, $_header, $_body, $_flags='\\Recent') 6198 { 6199 if (!is_resource($_header)) 6200 { 6201 if (stripos($_header,'message-id:')===false) 6202 { 6203 $_header = 'Message-ID: <'.self::getRandomString().'@localhost>'."\n".$_header; 6204 } 6205 //error_log(__METHOD__.' ('.__LINE__.') '."$_folderName, $_header, $_body, $_flags"); 6206 $_header = ltrim(str_replace("\n","\r\n",$_header)); 6207 $_header .= str_replace("\n","\r\n",$_body); 6208 } 6209 // the recent flag is the default enforced here ; as we assume the _flags is always set, 6210 // we default it to hordes default (Recent) (, other wise we should not pass the parameter 6211 // for flags at all) 6212 if (empty($_flags)) $_flags = '\\Recent'; 6213 //if (!is_array($_flags) && stripos($_flags,',')!==false) $_flags=explode(',',$_flags); 6214 //if (!is_array($_flags)) $_flags = (array) $_flags; 6215 try 6216 { 6217 $dataNflags = array(); 6218 // both methods below are valid for appending a message to a mailbox. 6219 // the commented version fails in retrieving the uid of the created message if the server 6220 // is not returning the uid upon creation, as the method in append for detecting the uid 6221 // expects data to be a string. this string is parsed for message-id, and the mailbox 6222 // searched for the message-id then returning the uid found 6223 //$dataNflags[] = array('data'=>array(array('t'=>'text','v'=>"$header"."$body")), 'flags'=>array($_flags)); 6224 $dataNflags[] = array('data' => $_header, 'flags'=>array($_flags)); 6225 $messageid = $this->icServer->append($_folderName,$dataNflags); 6226 } 6227 catch (\Exception $e) 6228 { 6229 if (self::$debug) error_log("Could not append Message: ".$e->getMessage()); 6230 throw new Exception\WrongUserinput(lang("Could not append Message:").' '.$e->getMessage().': '.$e->details); 6231 //return false; 6232 } 6233 //error_log(__METHOD__.' ('.__LINE__.') '.' appended UID:'.$messageid); 6234 //$messageid = true; // for debug reasons only 6235 if ($messageid === true || empty($messageid)) // try to figure out the message uid 6236 { 6237 $list = $this->getHeaders($_folderName, $_startMessage=1, 1, 'INTERNALDATE', true, array(),null, false); 6238 if ($list) 6239 { 6240 if (self::$debug) error_log(__METHOD__.' ('.__LINE__.') '.' MessageUid:'.$messageid.' but found:'.array2string($list)); 6241 $messageid = $list['header'][0]['uid']; 6242 } 6243 } 6244 return $messageid; 6245 } 6246 6247 /** 6248 * Get a random string of 32 chars 6249 * 6250 * @return string 6251 */ 6252 static function getRandomString() 6253 { 6254 return Auth::randomstring(32); 6255 } 6256 6257 /** 6258 * functions to allow access to mails through other apps to fetch content 6259 * used in infolog, tracker 6260 */ 6261 6262 /** 6263 * get_mailcontent - fetches the actual mailcontent, and returns it as well defined array 6264 * @param object mailClass the mailClassobject to be used 6265 * @param uid the uid of the email to be processed 6266 * @param partid the partid of the email 6267 * @param mailbox the mailbox, that holds the message 6268 * @param preserveHTML flag to pass through to getdisplayableBody, null for both text and HTML 6269 * @param addHeaderSection flag to be able to supress headersection 6270 * @param includeAttachments flag to be able to supress possible attachments 6271 * @return array/bool with 'mailaddress'=>$mailaddress, 6272 * 'subject'=>$subject, 6273 * 'message'=>$message, 6274 * 'attachments'=>$attachments, 6275 * 'headers'=>$headers,; boolean false on failure 6276 */ 6277 static function get_mailcontent(&$mailClass,$uid,$partid='',$mailbox='', $preserveHTML = false, $addHeaderSection=true, $includeAttachments=true) 6278 { 6279 //echo __METHOD__." called for $uid,$partid <br>"; 6280 $headers = $mailClass->getMessageHeader($uid,$partid,true,false,$mailbox); 6281 if (empty($headers)) return false; 6282 // dont force retrieval of the textpart, let mailClass preferences decide 6283 $bodyParts = $mailClass->getMessageBody($uid,($preserveHTML?'always_display':'only_if_no_text'),$partid,null,false,$mailbox); 6284 if(is_null($preserveHTML)) 6285 { 6286 $html = static::getdisplayablebody( 6287 $mailClass, 6288 $mailClass->getMessageBody($uid,'always_display',$partid,null,false,$mailbox), 6289 true 6290 ); 6291 6292 } 6293 // if we do not want HTML but there is no TextRepresentation with the message itself, try converting 6294 if ( !$preserveHTML && $bodyParts[0]['mimeType']=='text/html') 6295 { 6296 foreach($bodyParts as $i => $part) 6297 { 6298 if ($bodyParts[$i]['mimeType']=='text/html') 6299 { 6300 $bodyParts[$i]['body'] = Mail\Html::convertHTMLToText($bodyParts[$i]['body'],$bodyParts[$i]['charSet'],true,$stripalltags=true); 6301 $bodyParts[$i]['mimeType']='text/plain'; 6302 } 6303 } 6304 } 6305 //error_log(array2string($bodyParts)); 6306 $attachments = $includeAttachments?$mailClass->getMessageAttachments($uid,$partid,null,true,false,true,$mailbox):array(); 6307 6308 if ($mailClass->isSentFolder($mailbox)) $mailaddress = $headers['TO']; 6309 elseif (isset($headers['FROM'])) $mailaddress = $headers['FROM']; 6310 elseif (isset($headers['SENDER'])) $mailaddress = $headers['SENDER']; 6311 if (isset($headers['CC'])) $mailaddress .= ','.$headers['CC']; 6312 //_debug_array(array($headers,$mailaddress)); 6313 $subject = $headers['SUBJECT']; 6314 6315 $message = self::getdisplayableBody($mailClass, $bodyParts, $preserveHTML); 6316 if ($preserveHTML && $mailClass->activeMimeType == 'text/plain') $message = '<pre>'.$message.'</pre>'; 6317 $headdata = ($addHeaderSection ? self::createHeaderInfoSection($headers, '',$preserveHTML) : ''); 6318 $message = $headdata.$message; 6319 //echo __METHOD__.'<br>'; 6320 //_debug_array($attachments); 6321 if (is_array($attachments)) 6322 { 6323 // For dealing with multiple files of the same name 6324 $dupe_count = $file_list = array(); 6325 6326 foreach ($attachments as $num => $attachment) 6327 { 6328 if ($attachment['mimeType'] == 'MESSAGE/RFC822') 6329 { 6330 //_debug_array($mailClass->getMessageHeader($uid, $attachment['partID'])); 6331 //_debug_array($mailClass->getMessageBody($uid,'', $attachment['partID'])); 6332 //_debug_array($mailClass->getMessageAttachments($uid, $attachment['partID'])); 6333 $mailcontent = self::get_mailcontent($mailClass,$uid,$attachment['partID'],$mailbox); 6334 $headdata =''; 6335 if ($mailcontent['headers']) 6336 { 6337 $headdata = self::createHeaderInfoSection($mailcontent['headers'],'',$preserveHTML); 6338 } 6339 if ($mailcontent['message']) 6340 { 6341 $tempname =tempnam($GLOBALS['egw_info']['server']['temp_dir'],$GLOBALS['egw_info']['flags']['currentapp']."_"); 6342 $attachedMessages[] = array( 6343 'type' => 'TEXT/PLAIN', 6344 'name' => $mailcontent['subject'].'.txt', 6345 'tmp_name' => $tempname, 6346 ); 6347 $tmpfile = fopen($tempname,'w'); 6348 fwrite($tmpfile,$headdata.$mailcontent['message']); 6349 fclose($tmpfile); 6350 } 6351 foreach($mailcontent['attachments'] as &$tmpval) 6352 { 6353 $attachedMessages[] = $tmpval; 6354 } 6355 unset($attachments[$num]); 6356 } 6357 else 6358 { 6359 $attachments[$num] = array_merge($attachments[$num],$mailClass->getAttachment($uid, $attachment['partID'],0,false,false)); 6360 6361 if (empty($attachments[$num]['attachment'])&&$attachments[$num]['cid']) 6362 { 6363 $c = $mailClass->getAttachmentByCID($uid, $attachment['cid'], $attachment['partID'],true); 6364 $attachments[$num]['attachment'] = $c->getContents(); 6365 } 6366 // no attempt to convert, if we dont know about the charset 6367 if (isset($attachments[$num]['charset'])&&!empty($attachments[$num]['charset'])) { 6368 // we do not try guessing the charset, if it is not set 6369 //if ($attachments[$num]['charset']===false) $attachments[$num]['charset'] = Translation::detect_encoding($attachments[$num]['attachment']); 6370 Translation::convert($attachments[$num]['attachment'],$attachments[$num]['charset']); 6371 } 6372 if(in_array($attachments[$num]['name'], $file_list)) 6373 { 6374 $dupe_count[$attachments[$num]['name']]++; 6375 $attachments[$num]['name'] = pathinfo($attachments[$num]['name'], PATHINFO_FILENAME) . 6376 ' ('.($dupe_count[$attachments[$num]['name']] + 1).')' . '.' . 6377 pathinfo($attachments[$num]['name'], PATHINFO_EXTENSION); 6378 } 6379 $attachments[$num]['type'] = $attachments[$num]['mimeType']; 6380 $attachments[$num]['tmp_name'] = tempnam($GLOBALS['egw_info']['server']['temp_dir'],$GLOBALS['egw_info']['flags']['currentapp']."_"); 6381 $tmpfile = fopen($attachments[$num]['tmp_name'],'w'); 6382 fwrite($tmpfile,$attachments[$num]['attachment']); 6383 fclose($tmpfile); 6384 $file_list[] = $attachments[$num]['name']; 6385 unset($attachments[$num]['attachment']); 6386 } 6387 } 6388 if (is_array($attachedMessages)) $attachments = array_merge($attachments,$attachedMessages); 6389 } 6390 $return = array( 6391 'mailaddress'=>$mailaddress, 6392 'subject'=>$subject, 6393 'message'=>$message, 6394 'attachments'=>$attachments, 6395 'headers'=>$headers, 6396 ); 6397 if($html) 6398 { 6399 $return['html_message'] = $html; 6400 } 6401 6402 return $return; 6403 } 6404 6405 /** 6406 * getStandardIdentityForProfile 6407 * get either the first identity out of the given identities or the one matching the profile_id 6408 * @param object/array $_identities identity iterator object or array with identities from Mail\Account 6409 * @param integer $_profile_id the acc_id/profileID the identity with the matching key is the standard one 6410 * @return array the identity 6411 */ 6412 static function getStandardIdentityForProfile($_identities, $_profile_id) 6413 { 6414 $c = 0; 6415 // use the standardIdentity 6416 foreach($_identities as $key => $acc) { 6417 if ($c==0) $identity = $acc; 6418 //error_log(__METHOD__.__LINE__." $key == $_profile_id "); 6419 if ($key==$_profile_id) $identity = $acc; 6420 $c++; 6421 } 6422 return $identity; 6423 } 6424 /** 6425 * createHeaderInfoSection - creates a textual headersection from headerobject 6426 * @param array header headerarray may contain SUBJECT,FROM,SENDER,TO,CC,BCC,DATE,PRIORITY,IMPORTANCE 6427 * @param string headline Text tom use for headline, if SUPPRESS, supress headline and footerline 6428 * @param bool createHTML do it with HTML breaks 6429 * @return string a preformatted string with the information of the header worked into it 6430 */ 6431 static function createHeaderInfoSection($header,$headline='', $createHTML = false) 6432 { 6433 $headdata = null; 6434 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($header).function_backtrace()); 6435 if ($header['SUBJECT']) $headdata = lang('subject').': '.$header['SUBJECT'].($createHTML?"<br />":"\n"); 6436 if ($header['FROM']) $headdata .= lang('from').': '.self::convertAddressArrayToString($header['FROM'], $createHTML).($createHTML?"<br />":"\n"); 6437 if ($header['SENDER']) $headdata .= lang('sender').': '.self::convertAddressArrayToString($header['SENDER'], $createHTML).($createHTML?"<br />":"\n"); 6438 if ($header['TO']) $headdata .= lang('to').': '.self::convertAddressArrayToString($header['TO'], $createHTML).($createHTML?"<br />":"\n"); 6439 if ($header['CC']) $headdata .= lang('cc').': '.self::convertAddressArrayToString($header['CC'], $createHTML).($createHTML?"<br />":"\n"); 6440 if ($header['BCC']) $headdata .= lang('bcc').': '.self::convertAddressArrayToString($header['BCC'], $createHTML).($createHTML?"<br />":"\n"); 6441 if ($header['DATE']) $headdata .= lang('date').': '.$header['DATE'].($createHTML?"<br />":"\n"); 6442 if ($header['PRIORITY'] && $header['PRIORITY'] != 'normal') $headdata .= lang('priority').': '.$header['PRIORITY'].($createHTML?"<br />":"\n"); 6443 if ($header['IMPORTANCE'] && $header['IMPORTANCE'] !='normal') $headdata .= lang('importance').': '.$header['IMPORTANCE'].($createHTML?"<br />":"\n"); 6444 //if ($mailcontent['headers']['ORGANIZATION']) $headdata .= lang('organization').': '.$mailcontent['headers']['ORGANIZATION']."\ 6445 if (!empty($headdata)) 6446 { 6447 if (!empty($headline) && $headline != 'SUPPRESS') $headdata = "---------------------------- $headline ----------------------------".($createHTML?"<br />":"\n").$headdata; 6448 if (empty($headline)) $headdata = ($headline != 'SUPPRESS'?"--------------------------------------------------------".($createHTML?"<br />":"\n"):'').$headdata; 6449 $headdata .= ($headline != 'SUPPRESS'?"--------------------------------------------------------".($createHTML?"<br />":"\n"):''); 6450 } 6451 else 6452 { 6453 $headdata = ($headline != 'SUPPRESS'?"--------------------------------------------------------".($createHTML?"<br />":"\n"):''); 6454 } 6455 return $headdata; 6456 } 6457 6458 /** 6459 * Make the provided filename safe to store in the VFS 6460 * 6461 * Some characters found in subjects that cause problems if we try to put 6462 * them as filenames (Windows) so we remove any characters that might result 6463 * in additional directories, or issues on Windows. 6464 * 6465 * Under Windows the characters < > ? " : | \ / * are not allowed. 6466 * % causes problems with VFS UI 6467 * 6468 * 4-byte unicode is also unwanted, as our current MySQL collation can store it 6469 * 6470 * We also dont want empty filenames, using lang('empty') instead. 6471 * 6472 * @param string $filename 6473 * @return Cleaned filename, with problematic characters replaced with ' '. 6474 */ 6475 static function clean_subject_for_filename($filename) 6476 { 6477 static $filter_pattern = '$[\f\n\t\x0b\:*#?<>%"\|/\x{10000}-\x{10FFFF}\\\\]$u'; 6478 $file = substr(trim(preg_replace($filter_pattern, ' ', $filename)), 0, 200); 6479 if (empty($file)) $file = lang('empty'); 6480 return $file; 6481 } 6482 6483 /** 6484 * adaptSubjectForImport - strips subject from unwanted Characters, and does some normalization 6485 * to meet expectations 6486 * @param string $subject string to process 6487 * @return string 6488 */ 6489 static function adaptSubjectForImport($subject) 6490 { 6491 $subject = str_replace('$$','__',($subject?$subject:lang('(no subject)'))); 6492 $subject = str_ireplace(array('[FWD]','[',']','{','}','<','>'),array('Fwd:',' ',' ',' ',' ',' ',' '),trim($subject)); 6493 return $subject; 6494 } 6495 6496 /** 6497 * convertAddressArrayToString - converts an mail envelope Address Array To String 6498 * @param array $rfcAddressArray an addressarray as provided by mail retieved via egw_pear.... 6499 * @return string a comma separated string with the mailaddress(es) converted to text 6500 */ 6501 static function convertAddressArrayToString($rfcAddressArray) 6502 { 6503 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($rfcAddressArray)); 6504 $returnAddr =''; 6505 if (is_array($rfcAddressArray)) 6506 { 6507 foreach((array)$rfcAddressArray as $addressData) { 6508 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($addressData)); 6509 if($addressData['MAILBOX_NAME'] == 'NIL') { 6510 continue; 6511 } 6512 if(strtolower($addressData['MAILBOX_NAME']) == 'undisclosed-recipients') { 6513 continue; 6514 } 6515 if ($addressData['RFC822_EMAIL']) 6516 { 6517 $addressObjectA = self::parseAddressList($addressData['RFC822_EMAIL']); 6518 } 6519 else 6520 { 6521 $emailaddress = ($addressData['PERSONAL_NAME']?$addressData['PERSONAL_NAME'].' <'.$addressData['EMAIL'].'>':$addressData['EMAIL']); 6522 $addressObjectA = self::parseAddressList($emailaddress); 6523 } 6524 $addressObject = $addressObjectA[0]; 6525 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($addressObject)); 6526 if (!$addressObject->valid) continue; 6527 //$mb =(string)$addressObject->mailbox; 6528 //$h = (string)$addressObject->host; 6529 //$p = (string)$addressObject->personal; 6530 $returnAddr .= (strlen($returnAddr)>0?',':''); 6531 //error_log(__METHOD__.' ('.__LINE__.') '.$p.' <'.$mb.'@'.$h.'>'); 6532 try { 6533 $buff = imap_rfc822_write_address($addressObject->mailbox, Horde_Idna::decode($addressObject->host), $addressObject->personal); 6534 } 6535 // if Idna conversation fails, leave address unchanged 6536 catch (\Exception $e) { 6537 unset($e); 6538 $buff = imap_rfc822_write_address($addressObject->mailbox, $addressObject->host, $addressObject->personal); 6539 } 6540 $returnAddr .= str_replace(array('<','>','"\'','\'"'),array('[',']','"','"'),$buff); 6541 //error_log(__METHOD__.' ('.__LINE__.') '.' Address: '.$returnAddr); 6542 } 6543 } 6544 else 6545 { 6546 // do not mess with strings, return them untouched /* ToDo: validate string as Address */ 6547 $rfcAddressArray = self::decode_header($rfcAddressArray,true); 6548 $rfcAddressArray = str_replace(array('<','>','"\'','\'"'),array('[',']','"','"'),$rfcAddressArray); 6549 if (is_string($rfcAddressArray)) return $rfcAddressArray; 6550 } 6551 return $returnAddr; 6552 } 6553 6554 /** 6555 * Merges a given content with contact data 6556 * 6557 * @param string $content 6558 * @param array $ids array with contact id(s) 6559 * @param string &$err error-message on error 6560 * @return string/boolean merged content or false on error 6561 */ 6562 static function merge($content,$ids,$mimetype='') 6563 { 6564 $mergeobj = new Contacts\Merge(); 6565 6566 if (empty($mimetype)) $mimetype = (strlen(strip_tags($content)) == strlen($content) ?'text/plain':'text/html'); 6567 $err = ''; 6568 $rv = $mergeobj->merge_string($content,$ids,$err, $mimetype, array(), self::$displayCharset); 6569 if (empty($rv) && !empty($content) && !empty($err)) $rv = $content; 6570 if (!empty($err) && !empty($content) && !empty($ids)) error_log(__METHOD__.' ('.__LINE__.') '.' Merge failed for Ids:'.array2string($ids).' ContentType:'.$mimetype.' Content:'.$content.' Reason:'.array2string($err)); 6571 return $rv; 6572 } 6573 6574 /** 6575 * Returns a string showing the size of the message/attachment 6576 * 6577 * @param integer $bytes 6578 * @return string formatted string 6579 */ 6580 static function show_readable_size($bytes) 6581 { 6582 $bytes /= 1024; 6583 $type = 'k'; 6584 6585 if ($bytes / 1024 > 1) 6586 { 6587 $bytes /= 1024; 6588 $type = 'M'; 6589 6590 if ($bytes / 1024 > 1) 6591 { 6592 $bytes *= 10; 6593 settype($bytes, 'integer'); 6594 $bytes /= 10; 6595 $bytes /= 1024; 6596 $type = 'G'; 6597 } 6598 6599 } 6600 6601 if ($bytes < 10) 6602 { 6603 $bytes *= 10; 6604 settype($bytes, 'integer'); 6605 $bytes /= 10; 6606 } 6607 else 6608 settype($bytes, 'integer'); 6609 6610 return $bytes . ' ' . $type ; 6611 } 6612 6613 static function detect_qp(&$sting) { 6614 $needle = '/(=[0-9][A-F])|(=[A-F][0-9])|(=[A-F][A-F])|(=[0-9][0-9])/'; 6615 return preg_match("$needle",$string); 6616 } 6617 6618 /** 6619 * logRunTimes 6620 * logs to the error log all parameters given; output only if self::$debugTimes is true 6621 * 6622 * @param int $_starttime starttime of the action measured based on microtime(true) 6623 * @param int $_endtime endtime of the action measured, if not given microtime(true) is used 6624 * @param string $_message message to output details or params, whatever seems neccesary 6625 * @param string $_methodNline - Information where the log was taken 6626 * @return void 6627 */ 6628 static function logRunTimes($_starttime,$_endtime=null,$_message='',$_methodNline='') 6629 { 6630 if (is_null($_endtime)) $_endtime = microtime(true); 6631 $usagetime = microtime(true) - $_starttime; 6632 if (self::$debugTimes) error_log($_methodNline.' took:'.number_format($usagetime,5).'(s) '.($_message?'Details:'.$_message:'')); 6633 } 6634 6635 /** 6636 * check if formdata meets basic restrictions (in tmp dir, or vfs, mimetype, etc.) 6637 * 6638 * @param array $_formData passed by reference Array with information of name, type, file and size, mimetype may be adapted 6639 * @param string $IDtoAddToFileName id to enrich the returned tmpfilename 6640 * @param string $reqMimeType /(default message/rfc822, if set to false, mimetype check will not be performed 6641 * @return mixed $fullPathtoFile or exception 6642 * 6643 * @throws Exception\WrongUserinput 6644 */ 6645 static function checkFileBasics(&$_formData, $IDtoAddToFileName='', $reqMimeType='message/rfc822') 6646 { 6647 if (parse_url($_formData['file'],PHP_URL_SCHEME) == 'egw-data') return $_formData['file']; 6648 6649 //error_log(__METHOD__.__FILE__.array2string($_formData).' Id:'.$IDtoAddToFileName.' ReqMimeType:'.$reqMimeType); 6650 $importfailed = $tmpFileName = false; 6651 // ignore empty files, but allow to share vfs directories (which can have 0 size) 6652 if ($_formData['size'] == 0 && parse_url($_formData['file'], PHP_URL_SCHEME) != 'vfs' && is_dir($_formData['file'])) 6653 { 6654 $importfailed = true; 6655 $alert_msg .= lang("Empty file %1 ignored.", $_formData['name']); 6656 } 6657 elseif (parse_url($_formData['file'],PHP_URL_SCHEME) == 'vfs' || is_uploaded_file($_formData['file']) || 6658 realpath(dirname($_formData['file'])) == realpath($GLOBALS['egw_info']['server']['temp_dir'])) 6659 { 6660 // ensure existance of eGW temp dir 6661 // note: this is different from apache temp dir, 6662 // and different from any other temp file location set in php.ini 6663 if (!file_exists($GLOBALS['egw_info']['server']['temp_dir'])) 6664 { 6665 @mkdir($GLOBALS['egw_info']['server']['temp_dir'],0700); 6666 } 6667 6668 // if we were NOT able to create this temp directory, then make an ERROR report 6669 if (!file_exists($GLOBALS['egw_info']['server']['temp_dir'])) 6670 { 6671 $alert_msg .= 'Error:'.'<br>' 6672 .'Server is unable to access EGroupware tmp directory'.'<br>' 6673 .$GLOBALS['egw_info']['server']['temp_dir'].'<br>' 6674 .'Please check your configuration'.'<br>' 6675 .'<br>'; 6676 } 6677 6678 // sometimes PHP is very clue-less about MIME types, and gives NO file_type 6679 // rfc default for unknown MIME type is: 6680 if ($reqMimeType == 'message/rfc822') 6681 { 6682 $mime_type_default = 'message/rfc'; 6683 } 6684 else 6685 { 6686 $mime_type_default = $reqMimeType; 6687 } 6688 // check the mimetype by extension. as browsers seem to report crap 6689 // maybe its application/octet-stream -> this may mean that we could not determine the type 6690 // so we check for the suffix too 6691 // trust vfs mime-types, trust the mimetype if it contains a method 6692 if ((substr($_formData['file'],0,6) !== 'vfs://' || $_formData['type'] == 'application/octet-stream') && stripos($_formData['type'],'method=')===false) 6693 { 6694 $buff = explode('.',$_formData['name']); 6695 $suffix = ''; 6696 if (is_array($buff)) $suffix = array_pop($buff); // take the last extension to check with ext2mime 6697 if (!empty($suffix)) $sfxMimeType = MimeMagic::ext2mime($suffix); 6698 if (!empty($suffix) && !empty($sfxMimeType) && 6699 (strlen(trim($_formData['type']))==0 || (strtolower(trim($_formData['type'])) != $sfxMimeType))) 6700 { 6701 error_log(__METHOD__.' ('.__LINE__.') '.' Data:'.array2string($_formData)); 6702 error_log(__METHOD__.' ('.__LINE__.') '.' Form reported Mimetype:'.$_formData['type'].' but seems to be:'.$sfxMimeType); 6703 $_formData['type'] = $sfxMimeType; 6704 } 6705 } 6706 if (trim($_formData['type']) == '') 6707 { 6708 $_formData['type'] = 'application/octet-stream'; 6709 } 6710 // if reqMimeType is set to false do not test for that 6711 if ($reqMimeType) 6712 { 6713 // so if PHP did not pass any file_type info, then substitute the rfc default value 6714 if (substr(strtolower(trim($_formData['type'])),0,strlen($mime_type_default)) != $mime_type_default) 6715 { 6716 if (!(strtolower(trim($_formData['type'])) == "application/octet-stream" && $sfxMimeType == $reqMimeType)) 6717 { 6718 //error_log("Message rejected, no message/rfc. Is:".$_formData['type']); 6719 $importfailed = true; 6720 $alert_msg .= lang("File rejected, no %2. Is:%1",$_formData['type'],$reqMimeType); 6721 } 6722 if ((strtolower(trim($_formData['type'])) != $reqMimeType && $sfxMimeType == $reqMimeType)) 6723 { 6724 $_formData['type'] = MimeMagic::ext2mime($suffix); 6725 } 6726 } 6727 } 6728 // as FreeBSD seems to have problems with the generated temp names we append some more random stuff 6729 $randomString = chr(rand(65,90)).chr(rand(48,57)).chr(rand(65,90)).chr(rand(48,57)).chr(rand(65,90)); 6730 $tmpFileName = $GLOBALS['egw_info']['user']['account_id']. 6731 trim($IDtoAddToFileName).basename($_formData['file']).'_'.$randomString; 6732 6733 if (parse_url($_formData['file'],PHP_URL_SCHEME) == 'vfs') 6734 { 6735 $tmpFileName = $_formData['file']; // no need to store it somewhere 6736 } 6737 elseif (is_uploaded_file($_formData['file'])) 6738 { 6739 move_uploaded_file($_formData['file'], $GLOBALS['egw_info']['server']['temp_dir'].'/'.$tmpFileName); // requirement for safe_mode! 6740 } 6741 else 6742 { 6743 rename($_formData['file'], $GLOBALS['egw_info']['server']['temp_dir'].'/'.$tmpFileName); 6744 } 6745 } else { 6746 //error_log("Import of message ".$_formData['file']." failes to meet basic restrictions"); 6747 $importfailed = true; 6748 $alert_msg .= lang("Processing of file %1 failed. Failed to meet basic restrictions.",$_formData['name']); 6749 } 6750 if ($importfailed == true) 6751 { 6752 throw new Exception\WrongUserinput($alert_msg); 6753 } 6754 else 6755 { 6756 if (parse_url($tmpFileName,PHP_URL_SCHEME) == 'vfs') 6757 { 6758 Vfs::load_wrapper('vfs'); 6759 } 6760 return $tmpFileName; 6761 } 6762 } 6763 6764 /** 6765 * Parses a html text for images, and adds them as inline attachment 6766 * 6767 * Images can be data-urls, own VFS webdav.php urls or absolute path. 6768 * 6769 * @param Mailer $_mailObject instance of the Mailer Object to be used 6770 * @param string $_html2parse the html to parse and to be altered, if conditions meet 6771 * @param $mail_bo mail bo object 6772 * @return array|null return inline images stored as tmp file in vfs as array of attachments otherwise null 6773 */ 6774 static function processURL2InlineImages(Mailer $_mailObject, &$_html2parse, $mail_bo) 6775 { 6776 //error_log(__METHOD__."()"); 6777 $imageC = 0; 6778 $images = null; 6779 if (preg_match_all("/(src|background)=\"(.*)\"/Ui", $_html2parse, $images) && isset($images[2])) 6780 { 6781 foreach($images[2] as $i => $url) 6782 { 6783 //$isData = false; 6784 $basedir = $data = ''; 6785 $needTempFile = true; 6786 6787 try 6788 { 6789 // do not change urls for absolute images (thanks to corvuscorax) 6790 if (substr($url, 0, 5) !== 'data:') 6791 { 6792 $filename = basename($url); // need to resolve all sort of url 6793 if (($directory = dirname($url)) == '.') $directory = ''; 6794 $ext = pathinfo($filename, PATHINFO_EXTENSION); 6795 $mimeType = MimeMagic::ext2mime($ext); 6796 if ( strlen($directory) > 1 && substr($directory,-1) != '/') { $directory .= '/'; } 6797 $myUrl = $directory.$filename; 6798 if ($myUrl[0]=='/') // local path -> we only allow path's that are available via http/https (or vfs) 6799 { 6800 $basedir = Framework::getUrl('/'); 6801 } 6802 // use vfs instead of url containing webdav.php 6803 // ToDo: we should test if the webdav url is of our own scope, as we cannot handle foreign 6804 // webdav.php urls as vfs 6805 if (strpos($myUrl,'/webdav.php') !== false) // we have a webdav link, so we build a vfs/sqlfs link of it. 6806 { 6807 Vfs::load_wrapper('vfs'); 6808 list(,$myUrl) = explode('/webdav.php',$myUrl,2); 6809 $basedir = 'vfs://default'; 6810 $needTempFile = false; 6811 } 6812 6813 // If it is an inline image url, we need to fetch the actuall attachment 6814 // content and later on to be able to store its content as temp file 6815 if (strpos($myUrl, '/index.php?menuaction=mail.mail_ui.displayImage') !== false && $mail_bo) 6816 { 6817 $URI_params = array(); 6818 // Strips the url and store it into a temp for further procss 6819 $tmp_url = html_entity_decode($myUrl); 6820 6821 parse_str(parse_url($tmp_url, PHP_URL_QUERY),$URI_params); 6822 if ($URI_params['mailbox'] && $URI_params['uid'] && $URI_params['cid']) 6823 { 6824 $mail_bo->reopen(base64_decode($URI_params['mailbox'])); 6825 $attachment = $mail_bo->getAttachmentByCID(base64_decode($URI_params['uid']), base64_decode($URI_params['cid']),base64_decode($URI_params['partID']),true); 6826 $mail_bo->closeConnection(); 6827 if ($attachment) 6828 { 6829 $data = $attachment->getContents(); 6830 $mimeType = $attachment->getType(); 6831 $filename = $attachment->getDispositionParameter('filename'); 6832 } 6833 } 6834 } 6835 6836 if ( strlen($basedir) > 1 && substr($basedir,-1) != '/' && $myUrl[0]!='/') { $basedir .= '/'; } 6837 if ($needTempFile && !$attachment && substr($myUrl,0,4) !== "http") $data = file_get_contents($basedir.urldecode($myUrl)); 6838 } 6839 if (substr($url,0,strlen('data:'))=='data:') 6840 { 6841 //error_log(__METHOD__.' ('.__LINE__.') '.' -> '.$i.': '.array2string($images[$i])); 6842 // we only support base64 encoded data 6843 $tmp = substr($url,strlen('data:')); 6844 list($mimeType,$data_base64) = explode(';base64,',$tmp); 6845 $data = base64_decode($data_base64); 6846 // FF currently does NOT add any mime-type 6847 if (strtolower(substr($mimeType, 0, 6)) != 'image/') 6848 { 6849 $mimeType = MimeMagic::analyze_data($data); 6850 } 6851 list($what,$exactly) = explode('/',$mimeType); 6852 $needTempFile = true; 6853 $filename = ($what?$what:'data').$imageC++.'.'.$exactly; 6854 } 6855 if ($data || $needTempFile === false) 6856 { 6857 if ($needTempFile) 6858 { 6859 $attachment_file =tempnam($GLOBALS['egw_info']['server']['temp_dir'],$GLOBALS['egw_info']['flags']['currentapp']."_"); 6860 $tmpfile = fopen($attachment_file,'w'); 6861 fwrite($tmpfile,$data); 6862 fclose($tmpfile); 6863 } 6864 else 6865 { 6866 $attachment_file = $basedir.urldecode($myUrl); 6867 } 6868 // we use $attachment_file as base for cid instead of filename, as it may be image.png 6869 // (or similar) in all cases (when cut&paste). This may lead to more attached files, in case 6870 // we use the same image multiple times, but, if we do this, we should try to detect that 6871 // on upload. filename itself is not sufficient to determine the sameness of images 6872 $cid = 'cid:' . md5($attachment_file); 6873 if ($_mailObject->AddEmbeddedImage($attachment_file, substr($cid, 4), urldecode($filename), $mimeType) !== null) 6874 { 6875 //$_html2parse = preg_replace("/".$images[1][$i]."=\"".preg_quote($url, '/')."\"/Ui", $images[1][$i]."=\"".$cid."\"", $_html2parse); 6876 $_html2parse = str_replace($images[0][$i], $images[1][$i].'="'.$cid.'"', $_html2parse); 6877 } 6878 } 6879 } 6880 catch(\Exception $e) 6881 { 6882 // Something went wrong with this attachment. Skip it. 6883 error_log("Error adding inline attachment. " . $e->getMessage()); 6884 error_log($e->getTraceAsString()); 6885 } 6886 $attachments [] = array( 6887 'name' => $filename, 6888 'type' => $mimeType, 6889 'file' => $attachment_file, 6890 'tmp_name' => $attachment_file 6891 ); 6892 } 6893 return is_array($attachments) ? $attachments : null; 6894 } 6895 } 6896 6897 /** 6898 * importMessageToMergeAndSend 6899 * 6900 * @param Storage\Merge Storage\Merge bo_merge object 6901 * @param string $document the full filename 6902 * @param array $SendAndMergeTocontacts array of contact ids 6903 * @param string&|false $_folder (passed by reference) will set the folder used. must be set with a folder, but will hold modifications if 6904 * folder is modified. Set to false to not keep the message. 6905 * @param string& $importID ID for the imported message, used by attachments to identify them unambiguously 6906 * @return mixed array of messages with success and failed messages or exception 6907 */ 6908 function importMessageToMergeAndSend(Storage\Merge $bo_merge, $document, $SendAndMergeTocontacts, &$_folder, &$importID='') 6909 { 6910 $importfailed = false; 6911 $processStats = array('success'=>array(),'failed'=>array()); 6912 if (empty($SendAndMergeTocontacts)) 6913 { 6914 $importfailed = true; 6915 $alert_msg .= lang("Import of message %1 failed. No Contacts to merge and send to specified.", ''); 6916 } 6917 6918 // check if formdata meets basic restrictions (in tmp dir, or vfs, mimetype, etc.) 6919 /* as the file is provided by Storage\Merge, we do not check 6920 try 6921 { 6922 $tmpFileName = Mail::checkFileBasics($_formData,$importID); 6923 } 6924 catch (\Exception\WrongUserinput $e) 6925 { 6926 $importfailed = true; 6927 $alert_msg .= $e->getMessage(); 6928 } 6929 */ 6930 $tmpFileName = $document; 6931 // ----------------------------------------------------------------------- 6932 if ($importfailed === false) 6933 { 6934 $mailObject = new Mailer($this->profileID); 6935 try 6936 { 6937 $this->parseFileIntoMailObject($mailObject, $tmpFileName); 6938 } 6939 catch (Exception\AssertionFailed $e) 6940 { 6941 $importfailed = true; 6942 $alert_msg .= $e->getMessage(); 6943 } 6944 6945 //_debug_array($Body); 6946 $this->openConnection(); 6947 if (empty($_folder) && $_folder !== FALSE) 6948 { 6949 $_folder = $this->getSentFolder(); 6950 } 6951 $delimiter = $this->getHierarchyDelimiter(); 6952 if($_folder=='INBOX'.$delimiter) $_folder='INBOX'; 6953 if ($importfailed === false) 6954 { 6955 $Subject = $mailObject->getHeader('Subject'); 6956 //error_log(__METHOD__.' ('.__LINE__.') '.' Subject:'.$Subject); 6957 $Body = ($text_body = $mailObject->findBody('plain')) ? $text_body->getContents() : null; 6958 //error_log(__METHOD__.' ('.__LINE__.') '.' Body:'.$Body); 6959 //error_log(__METHOD__.' ('.__LINE__.') '.' BodyContentType:'.$mailObject->BodyContentType); 6960 $AltBody = ($html_body = $mailObject->findBody('html')) ? $html_body->getContents() : null; 6961 //error_log(__METHOD__.' ('.__LINE__.') '.' AltBody:'.$AltBody); 6962 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($mailObject->GetReplyTo())); 6963 6964 if(!$Body && !$AltBody) 6965 { 6966 throw new Exception\NotFound('No mail body in template "'.$document.'"'); 6967 } 6968 6969 // Fetch ReplyTo - Address if existing to check if we are to replace it 6970 $replyTo = $mailObject->getReplyTo(); 6971 if (isset($replyTo['replace@import.action'])) 6972 { 6973 $mailObject->clearReplyTos(); 6974 $activeMailProfiles = $this->mail->getAccountIdentities($this->profileID); 6975 $activeMailProfile = self::getStandardIdentityForProfile($activeMailProfiles,$this->profileID); 6976 6977 $mailObject->addReplyTo(Horde_Idna::encode($activeMailProfile['ident_email']),Mail::generateIdentityString($activeMailProfile,false)); 6978 } 6979 if(count($SendAndMergeTocontacts) > 1) 6980 { 6981 foreach(Mailer::$type2header as $type => $h) 6982 { 6983 $header = $mailObject->getHeader(Mailer::$type2header[$type]); 6984 if(is_array($header)) $header = implode(', ',$header); 6985 $headers[$type] = $header; 6986 } 6987 } 6988 foreach ($SendAndMergeTocontacts as $k => $val) 6989 { 6990 $errorInfo = $email = ''; 6991 $sendOK = $openComposeWindow = $openAsDraft = null; 6992 //error_log(__METHOD__.' ('.__LINE__.') '.' Id To Merge:'.$val); 6993 if (/*$GLOBALS['egw_info']['flags']['currentapp'] == 'addressbook' &&*/ 6994 (count($SendAndMergeTocontacts) > 1 || $_folder === FALSE) && $val && 6995 (is_numeric($val) || $GLOBALS['egw']->accounts->name2id($val))) // do the merge 6996 { 6997 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($mailObject)); 6998 6999 // Parse destinations for placeholders 7000 foreach(Mailer::$type2header as $type => $h) 7001 { 7002 //error_log('ID ' . $val . ' ' .$type . ': ' . $mailObject->getHeader(Mailer::$type2header[$type]) . ' -> ' .$bo_merge->merge_string($mailObject->getHeader(Mailer::$type2header[$type]),$val,$e,'text/plain',array(),self::$displayCharset)); 7003 $merged = $bo_merge->merge_string($headers[$type],$val,$e,'text/plain',array(),self::$displayCharset); 7004 $mailObject->clearAddresses($type); 7005 $mailObject->addAddress($merged,'',$type); 7006 if($type == 'to') 7007 { 7008 $email = $merged; 7009 } 7010 } 7011 7012 // No addresses from placeholders? Treat it as just a contact ID 7013 if (!$email) 7014 { 7015 $contact = $bo_merge->contacts->read($val); 7016 //error_log(__METHOD__.' ('.__LINE__.') '.' ID:'.$val.' Data:'.array2string($contact)); 7017 $email = ($contact['email'] ? $contact['email'] : $contact['email_home']); 7018 $nfn = ($contact['n_fn'] ? $contact['n_fn'] : $contact['n_given'].' '.$contact['n_family']); 7019 if($email) 7020 { 7021 $mailObject->addAddress(Horde_Idna::encode($email), $nfn); 7022 } 7023 } 7024 7025 $activeMailProfiles = $this->getAccountIdentities($this->profileID); 7026 $activeMailProfile = self::getStandardIdentityForProfile($activeMailProfiles,$this->profileID); 7027 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($activeMailProfile)); 7028 $mailObject->setFrom($activeMailProfile['ident_email'], 7029 self::generateIdentityString($activeMailProfile,false)); 7030 7031 $mailObject->removeHeader('Message-ID'); 7032 $mailObject->removeHeader('Date'); 7033 $mailObject->clearCustomHeaders(); 7034 $mailObject->addHeader('Subject', $bo_merge->merge_string($Subject, $val, $e, 'text/plain', array(), self::$displayCharset)); 7035 //error_log(__METHOD__.' ('.__LINE__.') '.' ContentType:'.$mailObject->BodyContentType); 7036 if($text_body) $text_body->setContents($bo_merge->merge_string($Body, $val, $e, 'text/plain', array(), self::$displayCharset),array('encoding'=>Horde_Mime_Part::DEFAULT_ENCODING)); 7037 //error_log(__METHOD__.' ('.__LINE__.') '.' Result:'.$mailObject->Body.' error:'.array2string($e)); 7038 if($html_body) $html_body->setContents($bo_merge->merge_string($AltBody, $val, $e, 'text/html', array(), self::$displayCharset),array('encoding'=>Horde_Mime_Part::DEFAULT_ENCODING)); 7039 7040 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($mailObject)); 7041 // set a higher timeout for big messages 7042 @set_time_limit(120); 7043 $sendOK = true; 7044 try { 7045 $mailObject->send(); 7046 $message_id = $mailObject->getHeader('Message-ID'); 7047 if($_folder) 7048 { 7049 $id = $this->appendMessage($_folder, $mailObject->getRaw(), ''); 7050 $importID = $id->current(); 7051 } 7052 } 7053 catch(Exception $e) { 7054 $sendOK = false; 7055 $errorInfo = $e->getMessage(); 7056 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($errorInfo)); 7057 } 7058 } 7059 elseif (!$k) // 1. entry, further entries will fail for apps other then addressbook 7060 { 7061 $openAsDraft = true; 7062 $mailObject->removeHeader('Message-ID'); 7063 $mailObject->removeHeader('Date'); 7064 $mailObject->clearCustomHeaders(); 7065 7066 // Parse destinations for placeholders 7067 foreach(Mailer::$type2header as $type => $h) 7068 { 7069 $header = $mailObject->getHeader(Mailer::$type2header[$type]); 7070 if(is_array($header)) $header = implode(', ',$header); 7071 $mailObject->clearAddresses($type); 7072 $merged = $bo_merge->merge_string($header,$val,$e,'text/plain',array(),self::$displayCharset); 7073 //error_log($type . ': ' . $mailObject->getHeader(Mailer::$type2header[$type]) . ' -> ' .$merged); 7074 $mailObject->addAddress(trim($merged,'"'),'',$type); 7075 } 7076 $mailObject->forceBccHeader(); 7077 7078 // No addresses from placeholders? Treat it as just a contact ID 7079 if (count($mailObject->getAddresses('to',true)) == 0 && 7080 is_numeric($val) || $GLOBALS['egw']->accounts->name2id($val)) // do the merge 7081 { 7082 $contact = $bo_merge->contacts->read($val); 7083 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($contact)); 7084 $email = ($contact['email'] ? $contact['email'] : $contact['email_home']); 7085 $nfn = ($contact['n_fn'] ? $contact['n_fn'] : $contact['n_given'].' '.$contact['n_family']); 7086 if($email) 7087 { 7088 $mailObject->addAddress(Horde_Idna::encode($email), $nfn); 7089 } 7090 } 7091 $mailObject->addHeader('Subject', $bo_merge->merge_string($Subject, $val, $e, 'text/plain', array(), self::$displayCharset)); 7092 //error_log(__METHOD__.' ('.__LINE__.') '.' ContentType:'.$mailObject->BodyContentType); 7093 if (!empty($Body)) $text_body->setContents($bo_merge->merge_string($Body, $val, $e, 'text/plain', array(), self::$displayCharset),array('encoding'=>Horde_Mime_Part::DEFAULT_ENCODING)); 7094 //error_log(__METHOD__.' ('.__LINE__.') '.' Result:'.$mailObject->Body.' error:'.array2string($e)); 7095 if (!empty($AltBody)) $html_body->setContents($bo_merge->merge_string($AltBody, $val, $e, 'text/html', array(), self::$displayCharset),array('encoding'=>Horde_Mime_Part::DEFAULT_ENCODING)); 7096 if(!$_folder !== false) 7097 { 7098 $_folder = $this->getDraftFolder(); 7099 } 7100 } 7101 if ($sendOK || $openAsDraft) 7102 { 7103 if ($openAsDraft) 7104 { 7105 if($this->folderExists($_folder,true)) 7106 { 7107 if($this->isSentFolder($_folder)) 7108 { 7109 $flags = '\\Seen'; 7110 } elseif($this->isDraftFolder($_folder)) { 7111 $flags = '\\Draft'; 7112 } else { 7113 $flags = ''; 7114 } 7115 $savefailed = false; 7116 try 7117 { 7118 $messageUid =$this->appendMessage($_folder, 7119 $mailObject->getRaw(), 7120 null, 7121 $flags); 7122 } 7123 catch (\Exception\WrongUserinput $e) 7124 { 7125 $savefailed = true; 7126 $alert_msg .= lang("Save of message %1 failed. Could not save message to folder %2 due to: %3",$Subject,$_folder,$e->getMessage()); 7127 } 7128 // no send, save successful, and message_uid present 7129 if ($savefailed===false && $messageUid && is_null($sendOK)) 7130 { 7131 $importID = $messageUid; 7132 $openComposeWindow = true; 7133 } 7134 } 7135 else if ($_folder !== FALSE) 7136 { 7137 $savefailed = true; 7138 $alert_msg .= lang("Saving of message %1 failed. Destination Folder %2 does not exist.",$Subject,$_folder); 7139 } 7140 } 7141 if ($sendOK) 7142 { 7143 $processStats['success'][$val] = 'Send succeeded to '.$nfn.'<'.$email.'>'.($savefailed?' but failed to store to Folder:'.$_folder:''); 7144 } 7145 else 7146 { 7147 if (!$openComposeWindow) $processStats['failed'][$val] = $errorInfo?$errorInfo:'Send failed to '.$nfn.'<'.$email.'> See error_log for details'; 7148 } 7149 } 7150 if (!is_null($sendOK) && $sendOK===false && is_null($openComposeWindow)) 7151 { 7152 $processStats['failed'][$val] = $errorInfo?$errorInfo:'Send failed to '.$nfn.'<'.$email.'> See error_log for details'; 7153 } 7154 } 7155 } 7156 unset($mailObject); 7157 } 7158 // set the url to open when refreshing 7159 if ($importfailed == true) 7160 { 7161 throw new Exception\WrongUserinput($alert_msg); 7162 } 7163 else 7164 { 7165 //error_log(__METHOD__.' ('.__LINE__.') '.array2string($processStats)); 7166 return $processStats; 7167 } 7168 } 7169 7170 /** 7171 * functions to allow the parsing of message/rfc files 7172 * used in felamimail to import mails, or parsev a message from file enrich it with addressdata (merge) and send it right away. 7173 */ 7174 7175 /** 7176 * Parses a message/rfc mail from file to the mailobject 7177 * 7178 * @param object $mailer instance of the SMTP Mailer Object 7179 * @param string $tmpFileName string that points/leads to the file to be imported 7180 * @throws Exception\NotFound if $fle is not found 7181 */ 7182 function parseFileIntoMailObject(Mailer $mailer, $tmpFileName) 7183 { 7184 switch (parse_url($tmpFileName, PHP_URL_SCHEME)) 7185 { 7186 case 'vfs': 7187 break; 7188 case 'egw-data': 7189 $message = ($host = parse_url($tmpFileName, PHP_URL_HOST)) ? Link::get_data($host, true) : false; 7190 break; 7191 default: 7192 $tmpFileName = $GLOBALS['egw_info']['server']['temp_dir'].'/'.basename($tmpFileName); 7193 break; 7194 } 7195 if (!isset($message)) $message = fopen($tmpFileName, 'r'); 7196 7197 if (!$message) 7198 { 7199 throw new Exception\NotFound("File '$tmpFileName' not found!"); 7200 } 7201 $this->parseRawMessageIntoMailObject($mailer, $message); 7202 7203 fclose($message); 7204 } 7205 7206 /** 7207 * Check and fix headers of raw message for headers with line width 7208 * more than 998 chars per line, as none folding long headers might 7209 * break the mail content. RFC 2822 (2.2.3 Long Header fields) 7210 * https://www.ietf.org/rfc/rfc2822.txt 7211 * 7212 * @param string|resource $message 7213 * @return string 7214 */ 7215 static private function _checkAndfixLongHeaderFields($message) 7216 { 7217 $eol = Horde_Mime_Part::RFC_EOL.Horde_Mime_Part::RFC_EOL; 7218 $needsFix = false; 7219 if (is_resource($message)) 7220 { 7221 fseek($message, 0, SEEK_SET); 7222 $m = ''; 7223 while (!feof($message)) { 7224 $m .= fread($message, 8192); 7225 } 7226 $message = $m; 7227 } 7228 7229 if (is_string($message)) 7230 { 7231 $start = substr($message,0, strpos($message, $eol)); 7232 $body = substr($message, strlen($start)); 7233 $hlength = strpos($start, $eol) ? strpos($start, $eol) : strlen($start); 7234 $headers = Horde_Mime_Headers::parseHeaders(substr($start, 0,$hlength)); 7235 foreach($headers->toArray() as $header => $value) 7236 { 7237 $needsReplacement = false; 7238 foreach((array)$value as $val) 7239 { 7240 if (strlen($val)+ strlen($header) > 900) 7241 { 7242 $needsReplacement = $needsFix = true; 7243 } 7244 } 7245 if ($needsReplacement) { 7246 $headers->removeHeader($header); 7247 $headers->addHeader($header, $value); 7248 } 7249 } 7250 } 7251 return $needsFix ? ($headers->toString(array('canonical'=>true)).$body) : $message; 7252 } 7253 7254 /** 7255 * Parses a message/rfc mail from file to the mailobject 7256 * 7257 * @param Mailer $mailer instance of SMTP Mailer object 7258 * @param string|ressource|Horde_Mime_Part $message string or resource containing the RawMessage / object Mail_mimeDecoded message (part)) 7259 * @param boolean $force8bitOnPrimaryPart (default false. force transferEncoding and charset to 8bit/utf8 if we have a textpart as primaryPart) 7260 * @throws Exception\WrongParameter when the required Horde_Mail_Part not found 7261 */ 7262 function parseRawMessageIntoMailObject(Mailer $mailer, $message, $force8bitOnPrimaryPart=false) 7263 { 7264 if (is_string($message) || is_resource($message)) 7265 { 7266 // Check and fix long header fields 7267 $message = self::_checkAndfixLongHeaderFields($message); 7268 7269 // Default charset to utf-8, not us-ascii which Horde chooses 7270 Horde_Mime_Part::$defaultCharset = 'utf-8'; 7271 7272 $structure = Horde_Mime_Part::parseMessage($message); 7273 //error_log(__METHOD__.__LINE__.'#'.$structure->getPrimaryType().'#'); 7274 if ($force8bitOnPrimaryPart&&$structure->getPrimaryType()=='text') 7275 { 7276 $structure->setTransferEncoding('8bit'); 7277 $structure->setCharset('utf-8'); 7278 } 7279 $mailer->setBasePart($structure); 7280 //error_log(__METHOD__.__LINE__.':'.array2string($structure)); 7281 7282 // unfortunately parseMessage does NOT return parsed headers (we assume header is shorter then 8k) 7283 // *** increase the header size limit to 32k to make sure most of the mails even with huge headers are 7284 // covered. TODO: Not sure if we even need to cut of the header parts and not just passing the whole 7285 // message to be parsed in order to get all headers, it needs more invetigation. 7286 $start = is_string($message) ? substr($message, 0, 32768) : 7287 (fseek($message, 0, SEEK_SET) == -1 ? '' : fread($message, 32768)); 7288 7289 $length = strpos($start, Horde_Mime_Part::RFC_EOL.Horde_Mime_Part::RFC_EOL); 7290 if ($length===false) $length = strlen($start); 7291 $headers = Horde_Mime_Headers::parseHeaders(substr($start, 0,$length)); 7292 7293 foreach($headers->toArray(array('nowrap' => true)) as $header => $value) 7294 { 7295 foreach((array)$value as $n => $val) 7296 { 7297 $overwrite = !$n; 7298 switch($header) 7299 { 7300 case 'Content-Transfer-Encoding': 7301 //as we parse the message and this sets the part with a Content-Transfer-Encoding, we 7302 //should not overwrite it with the header-values of the source-message as the encoding 7303 //may be altered when retrieving the message e.g. from server 7304 //error_log(__METHOD__.__LINE__.':'.$header.'->'.$val.'<->'.$mailer->getHeader('Content-Transfer-Encoding')); 7305 break; 7306 case 'Bcc': 7307 case 'bcc': 7308 //error_log(__METHOD__.__LINE__.':'.$header.'->'.$val); 7309 $mailer->addBcc($val); 7310 break; 7311 default: 7312 //error_log(__METHOD__.__LINE__.':'.$header.'->'.$val); 7313 $mailer->addHeader($header, $val, $overwrite); 7314 //error_log(__METHOD__.__LINE__.':'.'getHeader('.$header.')'.array2string($mailer->getHeader($header))); 7315 } 7316 } 7317 } 7318 } 7319 elseif (is_a($message, 'Horde_Mime_Part')) 7320 { 7321 $mailer->setBasePart($message); 7322 } 7323 else 7324 { 7325 if (($type = gettype($message)) == 'object') $type = get_class ($message); 7326 throw new Exception\WrongParameter('Wrong parameter type for message: '.$type); 7327 } 7328 } 7329 7330 /** 7331 * Parse an address-list 7332 * 7333 * Replaces imap_rfc822_parse_adrlist, which fails for utf-8, if not our replacement in common_functions is used! 7334 * 7335 * @param string $addresses 7336 * @param string $default_domain 7337 * @return Horde_Mail_Rfc822_List iteratable Horde_Mail_Rfc822_Address objects with attributes mailbox, host, personal and valid 7338 */ 7339 public static function parseAddressList($addresses, $default_domain=null) 7340 { 7341 $rfc822 = new Horde_Mail_Rfc822(); 7342 $ret = $rfc822->parseAddressList($addresses, $default_domain ? array('default_domain' => $default_domain) : array()); 7343 //error_log(__METHOD__.__LINE__.'#'.array2string($addresses).'#'.array2string($ret).'#'.$ret->count().'#'.$ret->count.function_backtrace()); 7344 if ((empty($ret) || $ret->count()==0)&& is_string($addresses) && strlen($addresses)>0) 7345 { 7346 $matches = array(); 7347 preg_match_all("/[\w\.,-.,_.,0-9.]+@[\w\.,-.,_.,0-9.]+/",$addresses,$matches); 7348 //error_log(__METHOD__.__LINE__.array2string($matches)); 7349 foreach ($matches[0] as &$match) {$match = trim($match,', ');} 7350 $addresses = implode(',',$matches[0]); 7351 //error_log(__METHOD__.__LINE__.array2string($addresses)); 7352 $ret = $rfc822->parseAddressList($addresses, $default_domain ? array('default_domain' => $default_domain) : array()); 7353 //error_log(__METHOD__.__LINE__.'#'.array2string($addresses).'#'.array2string($ret).'#'.$ret->count().'#'.$ret->count); 7354 } 7355 $previousFailed=false; 7356 $ret2 = new Horde_Mail_Rfc822_List(); 7357 // handle known problems on emailaddresses 7358 foreach($ret as $i => $adr) 7359 { 7360 //mailaddresses enclosed in single quotes like 'me@you.com' show up as 'me as mailbox and you.com' as host 7361 if ($adr->mailbox && stripos($adr->mailbox,"'")== 0 && 7362 $adr->host && stripos($adr->host,"'")== (strlen($adr->host) -1)) 7363 { 7364 $adr->mailbox = str_replace("'","",$adr->mailbox); 7365 $adr->host = str_replace("'","",$adr->host); 7366 } 7367 7368 7369 // try to strip extra quoting or slashes from personal part 7370 $adr->personal = stripslashes($adr->personal); 7371 if ($adr->personal && (stripos($adr->personal, '"') == 0 && 7372 substr($adr->personal, -1) == '"') || 7373 (substr($adr->personal, -2) == '""')) 7374 { 7375 $adr->personal = str_replace('"', "", $adr->personal); 7376 } 7377 7378 7379 // no mailbox or host part as 'Xr\xc3\xa4hlyz, User <mailboxpart1.mailboxpart2@yourhost.com>' is parsed as 2 addresses separated by ',' 7380 //#'Xr\xc3\xa4hlyz, User <mailboxpart1.mailboxpart2@yourhost.com>' 7381 //#Horde_Mail_Rfc822_List Object([_data:protected] => Array( 7382 //[0] => Horde_Mail_Rfc822_Address Object([comment] => Array()[mailbox] => Xr\xc3\xa4hlyz[_host:protected] => [_personal:protected] => ) 7383 //[1] => Horde_Mail_Rfc822_Address Object([comment] => Array()[mailbox] => mailboxpart1.mailboxpart2[_host:protected] => youthost.com[_personal:protected] => User))[_filter:protected] => Array()[_ptr:protected] => )#2#, 7384 if (strlen($adr->mailbox)==0||strlen($adr->host)==0) 7385 { 7386 $remember = ($adr->mailbox?$adr->mailbox:($adr->host?$adr->host:'')); 7387 $previousFailed=true; 7388 //error_log(__METHOD__.__LINE__."('$addresses', $default_domain) parsed $i: mailbox=$adr->mailbox, host=$adr->host, personal=$adr->personal"); 7389 } 7390 else 7391 { 7392 if ($previousFailed && $remember) $adr->personal = $remember. ' ' . $adr->personal; 7393 $remember = ''; 7394 $previousFailed=false; 7395 //error_log(__METHOD__.__LINE__."('$addresses', $default_domain) parsed $i: mailbox=$adr->mailbox, host=$adr->host, personal=$adr->personal"); 7396 $ret2->add($adr); 7397 } 7398 } 7399 //error_log(__METHOD__.__LINE__.'#'.array2string($addresses).'#'.array2string($ret2).'#'.$ret2->count().'#'.$ret2->count); 7400 return $ret2; 7401 } 7402 7403 /** 7404 * Send a read notification 7405 * 7406 * @param string $uid 7407 * @param string $_folder 7408 * @return boolean 7409 */ 7410 function sendMDN($uid,$_folder) 7411 { 7412 $acc = Mail\Account::read($this->profileID); 7413 $identity = Mail\Account::read_identity($acc['ident_id'], true, null, $acc); 7414 if (self::$debug) error_log(__METHOD__.__LINE__.array2string($identity)); 7415 $headers = $this->getMessageHeader($uid, '', 'object', true, $_folder); 7416 7417 // Override Horde's translation with our own 7418 Horde_Translation::setHandler('Horde_Mime', new Horde_Translation_Handler_Gettext('Horde_Mime', EGW_SERVER_ROOT.'/api/lang/locale')); 7419 Preferences::setlocale(); 7420 7421 $mdn = new Horde_Mime_Mdn($headers); 7422 $mdn->generate(true, true, 'displayed', php_uname('n'), $acc->smtpTransport(), array( 7423 'charset' => 'utf-8', 7424 'from_addr' => self::generateIdentityString($identity), 7425 )); 7426 7427 return true; 7428 } 7429 7430 /** 7431 * Hook stuff 7432 */ 7433 7434 /** 7435 * hook to add account 7436 * 7437 * this function is a wrapper function for emailadmin 7438 * 7439 * @param _hookValues contains the hook values as array 7440 * @return nothing 7441 */ 7442 function addAccount($_hookValues) 7443 { 7444 error_log(__METHOD__.' ('.__LINE__.') '.' NOT DONE YET!' . ' hookValue = '. $_hookValues); 7445 7446 } 7447 7448 /** 7449 * hook to delete account 7450 * 7451 * this function is a wrapper function for emailadmin 7452 * 7453 * @param _hookValues contains the hook values as array 7454 * @return nothing 7455 */ 7456 function deleteAccount($_hookValues) 7457 { 7458 error_log(__METHOD__.' ('.__LINE__.') '.' NOT DONE YET!' . ' hookValue = '. $_hookValues); 7459 7460 } 7461 7462 /** 7463 * hook to update account 7464 * 7465 * this function is a wrapper function for emailadmin 7466 * 7467 * @param _hookValues contains the hook values as array 7468 * @return nothing 7469 */ 7470 function updateAccount($_hookValues) 7471 { 7472 error_log(__METHOD__.' ('.__LINE__.') '.' NOT DONE YET!' . ' hookValue = '. $_hookValues); 7473 7474 } 7475 7476 /** 7477 * This function gets array of email addresses in RFC822 format 7478 * and tries to normalize the addresses into only email addresses. 7479 * 7480 * @param array $_addresses Addresses 7481 */ 7482 static function stripRFC822Addresses ($_addresses) 7483 { 7484 $matches = array(); 7485 foreach ($_addresses as &$address) 7486 { 7487 preg_match("/<([^\'\" <>]+)>$/", $address, $matches); 7488 if ($matches[1]) $address = $matches[1]; 7489 } 7490 return $_addresses; 7491 } 7492 7493 7494 7495 /** 7496 * Resolve certificate and encrypted message from smime attachment 7497 * 7498 * @param Horde_Mime_Part $_mime_part 7499 * @param array $_params 7500 * params = array ( 7501 * mimeType => (string) // message mime type 7502 * uid => (string) // message uid 7503 * mailbox => (string) // the mailbox where message is stored 7504 * passphrase => (string) // smime private key passphrase 7505 * ) 7506 * 7507 * @return Horde_Mime_Part returns a resolved mime part 7508 * @throws PassphraseMissing if private key passphrase is not provided 7509 * @throws Horde_Crypt_Exception if decryption fails 7510 */ 7511 function resolveSmimeMessage(Horde_Mime_Part $_mime_part, $_params) 7512 { 7513 // default params 7514 $params = array_merge(array( 7515 'passphrase' => '' 7516 ), $_params); 7517 7518 $metadata = array ( 7519 'mimeType' => $params['mimeType']?$params['mimeType']:$_mime_part->getType() 7520 ); 7521 $this->smime = new Mail\Smime; 7522 $message = $this->getMessageRawBody($params['uid'], null, $params['mailbox']); 7523 if (!Mail\Smime::isSmimeSignatureOnly(Mail\Smime::getSmimeType($_mime_part))) 7524 { 7525 try{ 7526 $message = $this->_decryptSmimeBody($message, $params['passphrase'] !='' ? 7527 $params['passphrase'] : Api\Cache::getSession('mail', 'smime_passphrase')); 7528 } 7529 catch(\Horde_Crypt_Exception $e) 7530 { 7531 throw new Mail\Smime\PassphraseMissing(lang('Could not decrypt '. 7532 'S/MIME data. This message may not be encrypted by your '. 7533 'public key and not being able to find corresponding private key.')); 7534 } 7535 $metadata['encrypted'] = true; 7536 } 7537 7538 try { 7539 $cert = $this->smime->verifySignature($message); 7540 } catch (\Exception $ex) { 7541 // passphrase is required to decrypt the message 7542 if (isset($message['password_required'])) 7543 { 7544 throw new Mail\Smime\PassphraseMissing($message['msg']); 7545 } 7546 // verifivation failure either message has been tempered, 7547 // signature is not valid or message has not ben signed 7548 // but encrypted only. 7549 else 7550 { 7551 $metadata['verify'] = false; 7552 $metadata['signed'] = true; 7553 $metadata['msg'] = $ex->getMessage(); 7554 } 7555 } 7556 7557 if ($cert) // signed message, it might be encrypted too 7558 { 7559 $envelope = $this->getMessageEnvelope($params['uid'], '', false, $params['mailbox']); 7560 $from = $this->stripRFC822Addresses($envelope['FROM']); 7561 $message_parts = $this->smime->extractSignedContents($message); 7562 $cert_email = strtolower($cert->email); 7563 //$f = $message_parts->_headers->getHeader('from'); 7564 $metadata = array_merge ($metadata, array ( 7565 'verify' => $cert->verify, 7566 'cert' => $cert->cert, 7567 'certDetails' => $this->smime->parseCert($cert->cert), 7568 'msg' => $cert->msg, 7569 'certHtml' => $this->smime->certToHTML($cert->cert), 7570 'email' => $cert_email, 7571 'signed' => true 7572 )); 7573 // check for email address if both signer email address and 7574 // email address of sender are the same. It also takes subjectAltName emails into account. 7575 if (is_array($from) && strcasecmp($from[0], $cert_email) != 0 7576 && stripos($metadata['certDetails']['extensions']['subjectAltName'],$from[0]) === false) 7577 { 7578 $metadata['unknownemail'] = true; 7579 $metadata['msg'] .= ' '.lang('Email address of signer is different from the email address of sender!'); 7580 } 7581 7582 $AB_bo = new \addressbook_bo(); 7583 $certkey = $AB_bo->get_smime_keys($cert_email); 7584 if (!is_array($certkey) || strcasecmp(trim($certkey[$cert_email]), trim($cert->cert)) != 0) $metadata['addtocontact'] = true; 7585 } 7586 else // only encrypted message 7587 { 7588 $message_parts = Horde_Mime_Part::parseMessage($message, array('forcemime' => true)); 7589 } 7590 $message_parts->setMetadata('X-EGroupware-Smime', $metadata); 7591 return $message_parts; 7592 } 7593 7594 /** 7595 * decrypt given smime encrypted message 7596 * 7597 * @param string $_message 7598 * @param string $_passphrase 7599 * @return array|string return 7600 * @throws Horde_Crypt_Exception 7601 */ 7602 private function _decryptSmimeBody ($_message, $_passphrase = '') 7603 { 7604 $AB_bo = new \addressbook_bo(); 7605 $acc_smime = Mail\Smime::get_acc_smime($this->profileID, $_passphrase); 7606 $certkey = $AB_bo->get_smime_keys($acc_smime['acc_smime_username']); 7607 if (!$this->smime->verifyPassphrase($acc_smime['pkey'], $_passphrase)) 7608 { 7609 return array ( 7610 'password_required' => true, 7611 'msg' => 'Authentication failure!' 7612 ); 7613 } 7614 7615 $params = array ( 7616 'type' => 'message', 7617 'pubkey' => $certkey[strtolower($acc_smime['acc_smime_username'])], 7618 'privkey' => $acc_smime['pkey'], 7619 'passphrase'=> $_passphrase 7620 ); 7621 return $this->smime->decrypt($_message, $params); 7622 } 7623} 7624