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;amp;','<DIV><BR></DIV>',"<DIV>&nbsp;</DIV>",'<div>&nbsp;</div>','</td></font>','<br><td>','<tr></tr>','<o:p></o:p>','<o:p>','</o:p>'),
4416							 array('&amp;',    '<BR>',           '<BR>',             '<BR>',             '</font></td>','<td>',    '',         '',           '',  ''),$_html);
4417		//$_html = str_replace(array('&amp;amp;'),array('&amp;'),$_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;amp;','<DIV><BR></DIV>',"<DIV>&nbsp;</DIV>",'<div>&nbsp;</div>'),array('&amp;','<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