1<?php
2
3namespace GO\Base\Mail;
4
5
6use go\core\ErrorHandler;
7
8class Imap extends ImapBodyStruct {
9
10	const SORT_NAME='NAME';
11	const SORT_FROM='FROM';
12	const SORT_TO='TO';
13	const SORT_DATE='DATE';
14	const SORT_ARRIVAL='ARRIVAL';
15	const SORT_SUBJECT='SUBJECT';
16	const SORT_SIZE='SIZE';
17
18	var $handle=false;
19
20	var $ssl=false;
21	var $server='';
22	var $port=143;
23	var $username='';
24	var $password='';
25
26	var $starttls=false;
27
28	var $auth='plain';
29
30	var $selected_mailbox=false;
31
32	var $touched_folders =array();
33
34	var $delimiter=false;
35
36	var $sort_count = 0;
37
38	var $gmail_server = false;
39
40	var $permittedFlags = false;
41
42
43	public $ignoreInvalidCertificates = false;
44
45	public static $systemFlags = array(
46		'Seen',
47		'Answered',
48		'Flagged',
49		'Deleted',
50		'Draft',
51		'Recent'
52	);
53
54	public function __construct(){
55
56	}
57
58	public function __destruct() {
59		$this->disconnect();
60	}
61
62	public function checkConnection(){
63		if(!is_resource($this->handle)){
64			return $this->connect(
65							$this->server,
66							$this->port,
67							$this->username,
68							$this->password,
69							$this->ssl,
70							$this->starttls,
71							$this->auth);
72		}else
73		{
74			return true;
75		}
76	}
77
78	/**
79	 * Connects to the IMAP server and authenticates the user
80	 *
81	 * @param <type> $server
82	 * @param <type> $port
83	 * @param <type> $username
84	 * @param <type> $password
85	 * @param <type> $ssl
86	 * @param <type> $starttls
87	 * @return <type>
88	 */
89
90	public function connect($server, $port, $username, $password, $ssl=false, $starttls=false, $auth='plain') {
91
92		\GO::debug("imap::connect($server, $port, $username, ***, $ssl, $starttls)");
93
94		//cache DNS in session. Seems to be faster with gmail somehow.
95//		if(empty(\GO::session()->values['imap'][$server]))
96//		{
97//			\GO::session()->values['imap'][$server]=gethostbyname($server);
98//		}
99
100		if(empty($password)){
101			throw new ImapAuthenticationFailedException('Authententication failed for user '.$username.' on IMAP server '.$this->server);
102		}
103
104		$this->ssl = $ssl;
105		$this->starttls = $starttls;
106		$this->auth = strtolower($auth);
107
108		$this->server=$server;
109		$this->port=$port;
110		$this->username=$username;
111		$this->password=$password;
112
113//		$server = $this->ssl ? 'ssl://'.$this->server : $this->server;
114
115
116//		$this->handle = fsockopen($server, $this->port, $errorno, $errorstr, 10);
117//		if (!is_resource($this->handle)) {
118//			throw new \Exception('Failed to open socket #'.$errorno.'. '.$errorstr);
119//		}
120
121		$context_options = array();
122		if($this->ignoreInvalidCertificates) {
123			$context_options = array('ssl' => array(
124					"verify_peer"=>false,
125					"verify_peer_name"=>false
126			));
127		}
128		$streamContext = stream_context_create($context_options);
129
130		$errorno = null;
131		$errorstr = null;
132		$remote = $this->ssl ? 'ssl://' : '';
133		$remote .=  $this->server.":".$this->port;
134
135		$this->handle = stream_socket_client($remote, $errorno, $errorstr, 10, STREAM_CLIENT_CONNECT, $streamContext);
136		if (!is_resource($this->handle)) {
137			throw new \Exception('Failed to open socket #'.$errorno.'. '.$errorstr);
138		}
139
140		$authed = $this->authenticate($username, $password);
141
142		if(!$authed)
143			return false;
144
145//		just testing for gmail
146//		$this->send_command("ENABLE UTF8=ACCEPT\r\n");
147
148
149
150		return true;
151	}
152
153	/**
154	 * Disconnect from the IMAP server
155	 *
156	 * @return <type>
157	 */
158
159	public function disconnect() {
160		if (is_resource($this->handle)) {
161			$command = "LOGOUT\r\n";
162			$this->send_command($command);
163			$this->state = 'disconnected';
164			$result = $this->get_response();
165			$this->check_response($result);
166			fclose($this->handle);
167
168			foreach($this->errors as $error){
169				error_log("IMAP error: ".$error);
170			}
171
172			$this->handle=false;
173			$this->selected_mailbox=false;
174
175			return true;
176		}else {
177			return false;
178		}
179	}
180
181	/**
182	 * Handles authentication. You can optionally set
183	 * $this->starttls or $this->auth to CRAM-MD5
184	 *
185	 * @param <type> $username
186	 * @param <type> $pass
187	 * @return <type>
188	 */
189
190	private function authenticate($username, $pass) {
191
192		if ($this->starttls) {
193
194			$command = "STARTTLS\r\n";
195			$this->send_command($command);
196			$response = $this->get_response();
197			if (!empty($response)) {
198				$end = array_pop($response);
199				if (substr($end, 0, strlen('A'.$this->command_count.' OK')) == 'A'.$this->command_count.' OK') {
200					if(!stream_socket_enable_crypto($this->handle, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
201						throw new \Exception("Failed to enable TLS on socket");
202					}
203				}else
204				{
205					throw new \Exception("Failed to enable TLS: ".$end);
206				}
207			}
208		}
209		switch (strtolower($this->auth)) {
210			case 'cram-md5':
211				$this->banner = fgets($this->handle, 1024);
212				$cram1 = 'A'.$this->command_number().' AUTHENTICATE CRAM-MD5'."\r\n";
213				fputs ($this->handle, $cram1);
214				$this->commands[trim($cram1)] = \GO\Base\Util\Date::getmicrotime();
215				$response = fgets($this->handle, 1024);
216				$this->responses[] = $response;
217				$challenge = base64_decode(substr(trim($response), 1));
218				$pass .= str_repeat(chr(0x00), (64-strlen($pass)));
219				$ipad = str_repeat(chr(0x36), 64);
220				$opad = str_repeat(chr(0x5c), 64);
221				$digest = bin2hex(pack("H*", md5(($pass ^ $opad).pack("H*", md5(($pass ^ $ipad).$challenge)))));
222				$challenge_response = base64_encode($username.' '.$digest);
223				$this->commands[trim($challenge_response)] = \GO\Base\Util\Date::getmicrotime();
224				fputs($this->handle, $challenge_response."\r\n");
225				break;
226			default:
227				$login = 'A'.$this->command_number().' LOGIN "'.$this->_escape( $username).'" "'.$this->_escape( $pass). "\"\r\n";
228				$this->commands[trim(str_replace($pass, 'xxxx', $login))] = \GO\Base\Util\Date::getmicrotime();
229				fputs($this->handle, $login);
230				break;
231		}
232		$res = $this->get_response();
233
234		$authed = false;
235		if (is_array($res) && !empty($res)) {
236			$response = array_pop($res);
237
238			//Sometimes an extra empty line comes along
239			if(!$response && count($res)==2)
240				$response = array_pop($res);
241
242			$this->short_responses[$response] = \GO\Base\Util\Date::getmicrotime();
243			if (!$this->auth) {
244				if (isset($res[1])) {
245					$this->banner = $res[1];
246				}
247				if (isset($res[0])) {
248					$this->banner = $res[0];
249				}
250			}
251			if (stristr($response, 'A'.$this->command_count.' OK')) {
252				$authed = true;
253				$this->state = 'authed';
254
255
256				//some imap servers like dovecot respond with the capability after login.
257				//Set this in the session so we don't need to do an extra capability command.
258				if(($startpos = strpos($response, 'CAPABILITY'))!==false){
259					\GO::debug("Use capability from login");
260					$endpos=  strpos($response, ']', $startpos);
261					if($endpos){
262						$capability = substr($response, $startpos, $endpos-$startpos);
263						\GO::session()->values['GO_IMAP'][$this->server]['imap_capability']=$capability;
264					}
265
266				}
267			}else
268			{
269//				if(!\GO::config()->debug)
270//					$this->errors[]=$response;
271
272				throw new ImapAuthenticationFailedException('Authententication failed for user '.$username.' on IMAP server '.$this->server."\n\n".$response);
273
274			}
275		}
276		return $authed;
277	}
278
279	private function _escape($str){
280		return str_replace(array('\\','"'), array('\\\\','\"'), $str);
281	}
282	private function _unescape($str){
283		return str_replace(array('\\\\','\"'), array('\\','"'), $str);
284	}
285	/**
286	 * Get's the capabilities of the IMAP server. Useful to determine if the
287	 * IMAP server supports server side sorting.
288	 *
289	 * @return <type>
290	 */
291
292	public function get_capability() {
293		//Cache capability in the session so this command is not used repeatedly
294		if (isset(\GO::session()->values['GO_IMAP'][$this->server]['imap_capability'])) {
295			$this->capability=\GO::session()->values['GO_IMAP'][$this->server]['imap_capability'];
296		}else {
297			if(!isset($this->capability)){
298				$command = "CAPABILITY\r\n";
299				$this->send_command($command);
300				$response = $this->get_response();
301				$this->capability = implode(' ', $response);
302			}
303			$this->capability = \GO::session()->values['GO_IMAP'][$this->server]['imap_capability'] = implode(' ', $response);
304		}
305		return $this->capability;
306	}
307
308	/**
309	 * Check if the IMAP server has a particular capability.
310	 * eg. QUOTA, ACL, LIST-EXTENDED etc.
311	 *
312	 * @param StringHelper $str
313	 * @return boolean
314	 */
315	public function has_capability($str){
316		$has = stripos($this->get_capability(), $str)!==false;
317
318		if(isset(\GO::session()->values['imap_disable_capabilites_'.$this->server])){
319			if(!isset(\GO::config()->disable_imap_capabilities))
320				\GO::config()->disable_imap_capabilities='';
321
322			\GO::config()->disable_imap_capabilities.=" ".\GO::session()->values['imap_disable_capabilites_'.$this->server];
323		}
324
325		//We stumbled upon a dovecot server that crashed when sending a command
326		//using LIST-EXTENDED. With this option we can workaround that issue.
327		if($has && stripos(\GO::config()->disable_imap_capabilities, $str)!==false)
328			$has=false;
329
330		return $has;
331	}
332
333
334	public function get_acl($mailbox){
335
336		$mailbox = $this->utf7_encode($this->_escape( $mailbox));
337		$this->clean($mailbox, 'mailbox');
338
339		$command = "GETACL \"$mailbox\"\r\n";
340		$this->send_command($command);
341		$response = $this->get_response(false, true);
342
343		$ret = array();
344
345		foreach($response as $line)
346		{
347			if($line[0]=='*' && $line[1]=='ACL' && count($line)>3){
348				for($i=3,$max=count($line);$i<$max;$i+=2){
349					$ret[]=array('identifier'=>$line[$i],'permissions'=>$line[$i+1]);
350				}
351			}
352		}
353
354		return $ret;
355	}
356
357	public function set_acl($mailbox, $identifier, $permissions){
358
359		$mailbox = $this->utf7_encode($this->_escape( $mailbox));
360		$this->clean($mailbox, 'mailbox');
361
362		$command = "SETACL \"$mailbox\" $identifier $permissions\r\n";
363		//throw new \Exception($command);
364		$this->send_command($command);
365
366		$response = $this->get_response();
367
368		return $this->check_response($response);
369	}
370
371	public function delete_acl($mailbox, $identifier){
372		$mailbox = $this->utf7_encode($this->_escape( $mailbox));
373		$this->clean($mailbox, 'mailbox');
374
375		$command = "DELETEACL \"$mailbox\" $identifier\r\n";
376		$this->send_command($command);
377		$response = $this->get_response();
378		return $this->check_response($response);
379	}
380
381	/**
382		* Get the delimiter that is used to delimit Mailbox names
383		*
384		* @access public
385		* @return mixed The delimiter or false on failure
386		*/
387
388	public function get_mailbox_delimiter() {
389		if(!$this->delimiter){
390			if(isset(\GO::session()->values['imap_delimiter'][$this->server])){
391				$this->delimiter=\GO::session()->values['imap_delimiter'][$this->server];
392			}else
393			{
394				$this->get_folders();
395//				$cmd = 'LIST "" ""'."\r\n";
396//				$this->send_command($cmd);
397//				$result = $this->get_response(false, true);
398//				var_dump($result);
399//				throw new \Exception("test");
400			}
401		}
402		return $this->delimiter;
403	}
404
405	private function set_mailbox_delimiter($delimiter) {
406		$this->delimiter=\GO::session()->values['imap_delimiter'][$this->server]=$delimiter;
407	}
408
409
410	private $_subscribedFoldersCache;
411
412	private function _isSubscribed($mailboxName, $flags){
413
414		if(strtoupper($mailboxName)=="INBOX"){
415			return true;
416			//returning subscribed flag with list-extended doesn't work with public folders.
417			//that's why we disabled this code and use LSUB to determine the subscribtions more reliably.
418//		}elseif($this->has_capability("LIST-EXTENDED")){
419//			return stristr($flags, 'subscribed');
420		}else
421		{
422			if(!isset($this->_subscribedFoldersCache[$this->server.$this->username])){
423				$this->_subscribedFoldersCache[$this->server.$this->username] = $this->list_folders(true, false, '', '*');
424
425//				\GO::debug(array_keys($this->_subscribedFoldersCache));
426			}
427			return isset($this->_subscribedFoldersCache[$this->server.$this->username][$mailboxName]);
428		}
429	}
430
431	public function list_folders($listSubscribed=true, $withStatus=false, $namespace='', $pattern='*', $isRoot=false){
432
433		\GO::debug("list_folders($listSubscribed, $withStatus, $namespace, $pattern)");
434		//$delim = false;
435
436		//unset($this->_subscribedFoldersCache);
437
438//		$listStatus = $this->has_capability('LIST-STATUS');
439
440		$listCmd = $listSubscribed ? 'LSUB' : 'LIST';
441
442//		if($listSubscribed && $this->has_capability("LIST-EXTENDED"))
443////		$listCmd = "LIST (SUBSCRIBED)";
444//			$listCmd = "LIST";
445
446
447		$cmd = $listCmd.' "'.$this->addslashes($this->utf7_encode($namespace)).'" "'.$this->addslashes($this->utf7_encode($pattern)).'"';
448
449//		if($listSubscribed && $this->has_capability("LIST-EXTENDED"))
450//			$listCmd = 'LIST';
451
452//		if($listStatus && $withStatus){
453//			$cmd .= ' RETURN (CHILDREN SUBSCRIBED STATUS (MESSAGES UNSEEN))';
454//		}
455
456		if($this->has_capability("LIST-EXTENDED") && !$listSubscribed){
457				$cmd .= ' RETURN (CHILDREN';
458
459				if($withStatus){
460					$cmd .= ' STATUS (MESSAGES UNSEEN)';
461				}
462
463			$cmd .= ')';
464		}
465
466//		\GO::debug($cmd);
467
468		$cmd .= "\r\n";
469
470		$this->send_command($cmd);
471		$result = $this->get_response(false, true);
472
473		if(!$this->check_response($result, true, false) && $this->has_capability("LIST-EXTENDED")){
474
475			//some servers pretend to support list-extended but fail on the commands.
476			//work around by disabling support and try again.
477			\GO::session()->values['imap_disable_capabilites_'.$this->server]='LIST-EXTENDED';
478
479			return $this->list_folders($listSubscribed, $withStatus, $namespace, $pattern, $isRoot);
480		}
481//		\GO::debug($result);
482
483		$delim=false;
484
485		$folders = array();
486		foreach ($result as $vals) {
487			if (!isset($vals[0])) {
488				continue;
489			}
490			if ($vals[0] == 'A'.$this->command_count) {
491				continue;
492			}
493
494			if($vals[1]==$listCmd){
495				$flags = false;
496				//$count = count($vals);
497				$folder = "";//$vals[($count - 1)];
498				$flag = false;
499				$delim_flag = false;
500				$delim=false;
501				$parent = '';
502				$no_select = false;
503				$can_have_kids = true;
504				$has_no_kids=false;
505				$has_kids = false;
506				$marked = false;
507				//$subscribed=$listSubscribed;
508
509				foreach ($vals as $v) {
510					if ($v == '(') {
511						$flag = true;
512					}
513					elseif ($v == ')') {
514						$flag = false;
515						$delim_flag = true;
516					}
517					else {
518						if ($flag) {
519							$flags .= ' '.$v;
520						}
521						if ($delim_flag && !$delim) {
522							$delim = $this->_unescape($v);
523							$delim_flag = false;
524						}elseif($delim){
525								$folder .= $v;
526						}
527					}
528				}
529
530				if(strtoupper($folder)=='INBOX')
531					$folder='INBOX'; //fix lowercase or mixed case inbox strings
532
533				if($folder=='dovecot')
534					continue;
535
536				if (!$this->delimiter) {
537					$this->set_mailbox_delimiter($delim);
538				}
539
540
541				//in some case the mailserver return the mailbox twice when it has subfolders:
542				//R: * LIST ( ) / Drafts
543				//R: * LIST ( ) / Folder3
544				//R: * LIST ( ) / Trash
545				//R: * LIST ( ) / Sent
546				//R: * LIST ( ) / Folder2
547				//R: * LIST ( ) / INBOX
548				//R: * LIST ( ) / INBOX/
549				//R: * LIST ( ) / Test &- test/
550				//R: * LIST ( ) / Test &- test
551
552				//We trim the delimiter of the folder to fix that.
553				$folder = trim($folder, $this->delimiter);
554
555
556
557				if (stristr($flags, 'marked')) {
558					$marked = true;
559				}
560				if (!stristr($flags, 'noinferiors')) {
561					$can_have_kids = false;
562				}
563				if (stristr($flags, 'haschildren')) {
564					$has_kids = true;
565				}
566
567				if (stristr($flags, 'hasnochildren')) {
568					$has_no_kids = true;
569				}
570
571
572				$subscribed = $listSubscribed || $this->_isSubscribed($folder, $flags);
573
574				$nonexistent = stristr($flags, 'NonExistent');
575
576				if ($folder != 'INBOX' && (stristr($flags, 'noselect') || $nonexistent)) {
577					$no_select = true;
578				}
579
580
581
582				if (!isset($folders[$folder]) && $folder) {
583					$folder = $this->_unescape($folder);
584					$folders[$folder] = array(
585									'delimiter' => $delim,
586									'name' => $this->utf7_decode($folder),
587									'marked' => $marked,
588									'noselect' => $no_select,
589									'nonexistent' => $nonexistent,
590									'noinferiors' => $can_have_kids,
591									'haschildren' => $has_kids,
592									'hasnochildren' => $has_no_kids,
593									'subscribed'=>$subscribed
594					);
595				}
596			}else
597			{
598				$lastProp=false;
599				foreach ($vals as $v) {
600					if ($v == '(') {
601						$flag = true;
602					}
603					elseif ($v == ')') {
604						break;
605					}
606					else {
607						if($lastProp=='MESSAGES'){
608							$folders[$folder]['messages']=intval($v);
609						}elseif($lastProp=='UNSEEN'){
610							$folders[$folder]['unseen']=intval($v);
611						}
612					}
613
614					$lastProp=$v;
615				}
616			}
617		}
618
619//		if($namespace=="" && $pattern=="%" && $listSubscribed && !isset($folders['INBOX'])){
620//			//inbox is not subscribed. Let's fix that/
621//			if(!$this->subscribe('INBOX'))
622//				throw new \Exception("Could not subscribe to INBOX folder!");
623//			return $this->list_folders($listSubscribed, $withStatus, $namespace, $pattern);
624//		}
625
626			//sometimes shared folders like "Other user.shared" are in the folder list
627		//but there's no "Other user" parent folder. We create a dummy folder here.
628		if(!isset($folders['INBOX']) && $isRoot){
629			$folders["INBOX"]=array(
630						'delimiter' => $delim,
631						'name' => 'INBOX',
632						'marked' => true,
633						'nonexistent'=>false,
634						'noselect' => false,
635						'haschildren'=>false,
636						'hasnochildren'=>true,
637						'noinferiors' => false,
638						'subscribed'=>true);
639		}
640
641		if($withStatus){
642			//no support for list status. Get the status for each folder
643			//with seperate status calls
644			foreach($folders as $name=>$folder){
645				if(!isset($folders[$name]['unseen'])){
646					if($folders[$name]['nonexistent'] || $folders[$name]['noselect']){
647						$folders[$name]['messages']=0;
648						$folders[$name]['unseen']=0;
649					}else
650					{
651						$status = $this->get_status($folder["name"]);
652						if(!$status) {
653							go()->warn("Could not get status for folder '" . $folder['name'] . "'");
654						} else{
655							$folders[$name]['messages']=$status['messages'];
656							$folders[$name]['unseen']=$status['unseen'];
657						}
658
659					}
660				}
661			}
662		}
663
664		\GO\Base\Util\ArrayUtil::caseInsensitiveSort($folders);
665
666		\GO::debug($folders);
667
668		return $folders;
669	}
670
671	/**
672	 * Get the namespaces that are available on the mailserver.
673	 *
674	 * @return array
675	 */
676	public function get_namespaces(){
677		// Array with the namespaces that are found.
678		$nss = array();
679
680		if($this->has_capability('NAMESPACE')){
681			//IMAP ccommand
682
683			$command = "NAMESPACE\r\n";
684			$this->send_command($command);
685			$result = $this->get_response(false, true);
686
687			$namespaceCmdFound=false;
688
689			$insideNamespace=false;
690
691			$namespace = array('name'=>null, 'delimiter'=>null);
692
693			foreach ($result as $vals) {
694				foreach ($vals as $val) {
695					if (!$namespaceCmdFound && strtoupper($val) == 'NAMESPACE') {
696						$namespaceCmdFound = true;
697					} else {
698						switch (strtoupper($val)) {
699
700							case '(':
701								$insideNamespace = true;
702								break;
703
704							case ')':
705								$insideNamespace = false;
706
707								if(isset($namespace['name'])){
708									$namespace['name']=$this->utf7_decode(trim($namespace['name'], $namespace['delimiter']));
709									$nss[] = $namespace;
710									$namespace = array('name' => null, 'delimiter' => null);
711								}
712								break;
713
714							default:
715								if ($insideNamespace) {
716									if (!isset($namespace['name'])) {
717										$namespace['name'] = $val;
718									} else {
719										$namespace['delimiter'] = $val;
720									}
721								}
722								break;
723						}
724					}
725				}
726			}
727
728			return $nss;
729		}else
730		{
731			return array(array('name'=>'','delimiter'=>$this->get_mailbox_delimiter()));
732		}
733	}
734
735
736	/**
737	 * Get's the mailboxes
738	 *
739	 * @param <type> $namespace
740	 * @param <type> $subscribed
741	 * @return <type>
742	 */
743
744	public function get_folders($namespace='', $subscribed=false, $pattern='*') {
745
746		$this->get_capability();
747
748		if ($subscribed) {
749			$imap_command = 'LSUB';
750		}
751		else {
752			$imap_command = 'LIST';
753		}
754		$excluded = array();
755		$parents = array();
756		$delim = false;
757
758		$command = $imap_command.' "'.$namespace."\" \"$pattern\"\r\n";
759		$this->send_command($command);
760		$result = $this->get_response(false, true);
761		$folders = array();
762		foreach ($result as $vals) {
763			if (!isset($vals[0])) {
764				continue;
765			}
766			if ($vals[0] == 'A'.$this->command_count) {
767				continue;
768			}
769			$flags = false;
770			$count = count($vals);
771			$folder = $this->utf7_decode($vals[($count - 1)]);
772			$flag = false;
773			$delim_flag = false;
774			$parent = '';
775			$folder_parts = array();
776			$no_select = false;
777			$can_have_kids = false;
778			$has_kids = false;
779			$marked = false;
780			$hidden = false;
781
782			foreach ($vals as $v) {
783				if ($v == '(') {
784					$flag = true;
785				}
786				elseif ($v == ')') {
787					$flag = false;
788					$delim_flag = true;
789				}
790				else {
791					if ($flag) {
792						$flags .= ' '.$v;
793					}
794					if ($delim_flag && !$delim) {
795						$delim = $v;
796						$delim_flag = false;
797					}
798				}
799			}
800
801			if (!$this->delimiter) {
802				$this->set_mailbox_delimiter($delim);
803			}
804
805			if (stristr($flags, 'marked')) {
806				$marked = true;
807			}
808			if (!stristr($flags, 'noinferiors')) {
809				$can_have_kids = true;
810			}
811			if (($folder == $namespace && $namespace) || stristr($flags, 'haschildren')) {
812				$has_kids = true;
813			}
814			if ($folder != 'INBOX' && $folder != $namespace && stristr($flags, 'noselect')) {
815				$no_select = true;
816			}
817
818			if (!isset($folders[$folder]) && $folder) {
819				$folders[$folder] = array(
820								'delimiter' => $delim,
821								'name' => $this->utf7_decode($folder),
822								'marked' => $marked,
823								'noselect' => $no_select,
824								'can_have_children' => $can_have_kids,
825								'has_children' => $has_kids
826				);
827			}
828		}
829
830
831
832
833
834		//sometimes shared folders like "Other user.shared" are in the folder list
835		//but there's no "Other user" parent folder. We create a dummy folder here.
836
837		foreach($folders as $name=>$folder){
838			$pos = strrpos($name, $delim);
839
840			if($pos){
841				$parent = substr($name,0,$pos);
842				if(!isset($folders[$parent]))
843				{
844					$folders[$parent]=array(
845								'delimiter' => $delim,
846								'name' => $parent,
847								'marked' => true,
848								'noselect' => true,
849								'can_have_children' => true,
850								'has_children' => true);
851				}
852			}
853
854			$last_folder = $name;
855		}
856
857		//\GO::debug($folders);
858
859		ksort($folders);
860
861		return $folders;
862	}
863
864
865	/**
866	 * Before getting message a mailbox must be selected
867	 *
868	 * @param <type> $mailbox_name
869	 * @return <type>
870	 *
871	 */
872
873	public function select_mailbox($mailbox_name='INBOX') {
874
875		//\GO::debug($this->selected_mailbox);
876
877		if($this->selected_mailbox && $this->selected_mailbox['name']==$mailbox_name)
878			return true;
879
880		if(!in_array($mailbox_name, $this->touched_folders))
881			$this->touched_folders[]=$mailbox_name;
882
883
884		$box = $this->utf7_encode($mailbox_name);
885		$this->clean($box, 'mailbox');
886
887		\GO::debug("Selecting IMAP mailbox $box");
888
889		$command = "SELECT \"$box\"\r\n";
890
891		$this->send_command($command);
892		$res = $this->get_response(false, true);
893		$status = $this->check_response($res, true);
894
895		if(!$status)
896			return false;
897
898		$highestmodseq=false;
899		$uidvalidity = 0;
900		$exists = 0;
901		$uidnext = 0;
902		$flags = array();
903		$pflags = array();
904		foreach ($res as $vals) {
905			if (in_array('UIDNEXT', $vals)) {
906				foreach ($vals as $i => $v) {
907					if (intval($v) && isset($vals[($i - 1)]) && $vals[($i - 1)] == 'UIDNEXT') {
908						$uidnext = $v;
909					}
910				}
911			}
912//			This is only the first unseen uid not very useful
913//			if (in_array('UNSEEN', $vals)) {
914//				foreach ($vals as $i => $v) {
915//					if (intval($v) && isset($vals[($i - 1)]) && $vals[($i - 1)] == 'UNSEEN') {
916//						$unseen = $v;
917//					}
918//				}
919//			}
920			if (in_array('UIDVALIDITY', $vals)) {
921				foreach ($vals as $i => $v) {
922					if (intval($v) && isset($vals[($i - 1)]) && $vals[($i - 1)] == 'UIDVALIDITY') {
923						$uidvalidity = $v;
924					}
925				}
926			}
927
928			if (in_array('HIGHESTMODSEQ', $vals)) {
929				foreach ($vals as $i => $v) {
930					if (intval($v) && isset($vals[($i - 1)]) && $vals[($i - 1)] == 'HIGHESTMODSEQ') {
931						$highestmodseq = $v;
932					}
933				}
934			}
935			if (in_array('PERMANENTFLAGS', $vals)) {
936				$collect_flags = false;
937				foreach ($vals as $i => $v) {
938					if ($v == ')') {
939						$collect_flags = false;
940					}
941					if ($collect_flags) {
942						$pflags[] = $v;
943					}
944					if ($v == '(') {
945						$collect_flags = true;
946					}
947				}
948
949				if (implode(' ', array_slice($vals, -2)) == 'Flags permitted.') {
950					$this->permittedFlags = true;
951				}
952			}
953			if (in_array('FLAGS', $vals)) {
954				$collect_flags = false;
955				foreach ($vals as $i => $v) {
956					if ($v == ')') {
957						$collect_flags = false;
958					}
959					if ($collect_flags) {
960						$flags[] = $v;
961					}
962					if ($v == '(') {
963						$collect_flags = true;
964					}
965				}
966			}
967			if (in_array('EXISTS', $vals)) {
968				foreach ($vals as $i => $v) {
969					if (intval($v) && isset($vals[($i + 1)]) && $vals[($i + 1)] == 'EXISTS') {
970						$exists = $v;
971					}
972				}
973			}
974		}
975
976		$mailbox=array();
977		$mailbox['name']=$mailbox_name;
978		$mailbox['uidnext'] = $uidnext;
979		$mailbox['uidvalidity'] = $uidvalidity;
980		$mailbox['highestmodseq'] = $highestmodseq;
981		$mailbox['messages'] = $exists;
982		$mailbox['flags'] = $flags;
983		$mailbox['permanentflags'] = $pflags;
984
985		$this->selected_mailbox=$mailbox;
986
987		return $mailbox;
988	}
989
990	/**
991	 * Get's the number and UID's of unseen messages of a mailbox
992	 *
993	 * @param <type> $folder
994	 * @return <type>
995	 */
996
997	private $_unseen;
998
999	public function get_unseen($mailbox=false, $nocache=false) {
1000
1001		if(!$mailbox)
1002			$mailbox = $this->selected_mailbox['name'];
1003
1004		if(isset($this->_unseen[$mailbox])){
1005			return $this->_unseen[$mailbox];
1006		}
1007
1008		if($mailbox){
1009			if(!$this->select_mailbox($mailbox)){
1010				return false;
1011			}
1012		}
1013
1014//		\GO::debug(\GO::session()->values['GO_IMAP'][$this->server][$mailbox]);
1015//		\GO::debug($this->selected_mailbox['uidvalidity']);
1016//		\GO::debug($this->selected_mailbox['highestmodseq']);
1017//		//get from session cache
1018//		if(isset(\GO::session()->values['GO_IMAP'][$this->server][$mailbox]) && !empty(\GO::session()->values['GO_IMAP'][$this->server][$mailbox]['highestmodseq'])){
1019//			if(\GO::session()->values['GO_IMAP'][$this->server][$mailbox]['uidvalidity']==$this->selected_mailbox['uidvalidity'] && \GO::session()->values['GO_IMAP'][$this->server][$mailbox]['highestmodseq']==$this->selected_mailbox['highestmodseq']){
1020//
1021//				\GO::debug("Returning unseen from cache");
1022//
1023//
1024//				return \GO::session()->values['GO_IMAP'][$this->server][$mailbox];
1025//			}
1026//		}
1027//
1028//		\GO::debug("Getting unseen");
1029
1030		#some servers don't seem to support brackets
1031		#$command = "UID SEARCH (UNSEEN) ALL\r\n";
1032
1033		$command = "UID SEARCH UNSEEN ALL\r\n";
1034
1035		$this->send_command($command);
1036		$res = $this->get_response(false, true);
1037		$status = $this->check_response($res, true);
1038		$unseen = 0;
1039		$uids = array();
1040		if ($status) {
1041			array_pop($res);
1042			foreach ($res as $vals) {
1043				foreach ($vals as $v) {
1044
1045					if (is_numeric($v)) {
1046						$unseen++;
1047						$uids[] = $v;
1048					}
1049				}
1050			}
1051		}
1052
1053		$this->selected_mailbox['unseen']=$unseen;
1054
1055
1056//		$this->_unseen[$mailbox]=\GO::session()->values['GO_IMAP'][$this->server][$mailbox]=array('count'=>$unseen, 'uids'=>$uids, 'uidvalidity'=>$this->selected_mailbox['uidvalidity'], 'highestmodseq'=>$this->selected_mailbox['highestmodseq']);
1057		$this->_unseen[$mailbox]=array('count'=>$unseen, 'uids'=>$uids);
1058
1059
1060		return $this->_unseen[$mailbox];
1061	}
1062
1063
1064	/**
1065	 * Returns a sorted list of mailbox UID's
1066	 *
1067	 * @param <type> $sort
1068	 * @param <type> $reverse
1069	 * @param <type> $filter
1070	 * @return <type>
1071	 */
1072	public function sort_mailbox($sort='ARRIVAL', $reverse=false, $filter='ALL') {
1073
1074		if(empty($filter)){
1075			$filter = 'ALL';
1076		}
1077
1078		if(!$this->selected_mailbox)
1079			throw new \Exception('No mailbox selected');
1080
1081		$this->get_capability();
1082
1083		if (($sort == 'THREAD_R' || $sort == 'THREAD_O')) {
1084			if ($sort == 'THREAD_O') {
1085				if (stristr($this->capability, 'ORDEREDSUBJECT')) {
1086					$ret =  $this->thread_sort($sort, $filter);
1087					$this->sort_count = $ret['total'];
1088					return $ret;
1089				}
1090				else {
1091					$uids=$this->server_side_sort('ARRIVAL', false, $filter);
1092					$this->sort_count = count($uids);
1093					return $uids;
1094				}
1095			}
1096			if ($sort == 'THREAD_R') {
1097				if (stristr($this->capability, 'THREAD')) {
1098					$ret = $this->thread_sort($sort, $filter);
1099					$this->sort_count = $ret['total'];
1100					return $ret;
1101				}
1102				else {
1103					$uids=$this->server_side_sort('ARRIVAL', false, $filter);
1104					$this->sort_count = count($uids);
1105					return $uids;
1106				}
1107			}
1108		}
1109		elseif (stristr($this->capability, 'SORT')) {
1110			$uids=$this->server_side_sort($sort, $reverse, $filter);
1111			if($uids === false) {
1112			  throw new \Exception("Sort error: " . $this->last_error());
1113      }
1114			$this->sort_count = count($uids); // <-- BAD
1115			return $uids;
1116		}
1117		else {
1118			$uids=$this->client_side_sort($sort, $reverse, $filter);
1119      if($uids === false) {
1120        throw new \Exception("Sort error: " . $this->last_error());
1121      }
1122
1123			$this->sort_count = count($uids);
1124			return $uids;
1125		}
1126	}
1127
1128	private function server_side_sort($sort, $reverse, $filter, $forceAscii=false) {
1129		\GO::debug("server_side_sort($sort, $reverse, $filter)");
1130
1131		$this->clean($sort, 'keyword');
1132		//$this->clean($filter, 'keyword');
1133
1134		$charset = $forceAscii || !\GO\Base\Util\StringHelper::isUtf8($filter) ? 'US-ASCII' : 'UTF-8';
1135
1136		$command = 'UID SORT ('.$sort.') '.$charset.' '.trim($filter)."\r\n";
1137
1138		$this->send_command($command);
1139		/*if ($this->disable_sort_speedup) {
1140			$speedup = false;
1141		}
1142		else {*/
1143		$speedup = true;
1144		//}
1145		$res = $this->get_response(false, true, 8192, $speedup);
1146		$status = $this->check_response($res, true);
1147		if(!$status && stripos($this->last_error(), 'utf')){
1148			return $this->server_side_sort($sort, $reverse, $filter, true);
1149		}
1150		$uids = array();
1151		foreach ($res as $vals) {
1152			if ($vals[0] == '*' && strtoupper($vals[1]) == 'SORT') {
1153				array_shift($vals);
1154				array_shift($vals);
1155				$uids = array_merge($uids, $vals);
1156			}
1157			else {
1158				if (preg_match("/^(\d)+$/", $vals[0])) {
1159					$uids = array_merge($uids, $vals);
1160				}
1161			}
1162		}
1163		unset($res);
1164		if ($reverse) {
1165			$uids = array_reverse($uids);
1166		}
1167		return $status ? $uids : false;
1168	}
1169
1170	/**
1171	 * Search
1172	 *
1173	 * @param <type> $terms
1174	 * @param <type> $sort
1175	 * @param <type> $reverse
1176	 * @return array uiids
1177	 */
1178	public function search($terms) {
1179		//$this->clean($this->search_charset, 'charset');
1180		$this->clean($terms, 'search_str');
1181
1182		/*
1183		 * Sending charset along doesn't work on iMailserver.
1184		 * Without seems to work on different servers.
1185		 */
1186		$charset = '';
1187		//$charset =  'CHARSET UTF-8 ';
1188
1189
1190		$command = 'UID SEARCH '.$charset.trim($terms)."\r\n";
1191		$this->send_command($command);
1192		$result = $this->get_response(false, true);
1193		$status = $this->check_response($result, true);
1194		$res = array();
1195		if ($status) {
1196			array_pop($result);
1197			foreach ($result as $vals) {
1198				foreach ($vals as $v) {
1199					if (preg_match("/^\d+$/", $v)) {
1200						$res[] = $v;
1201					}
1202				}
1203			}
1204		}
1205		return $res;
1206	}
1207
1208
1209	/* use the FETCH command to manually sort the mailbox */
1210	private function client_side_sort($sort, $reverse, $filter='ALL') {
1211
1212		// Check if the imap_sort_on_date flag is set. Usually this can be set when
1213		// the mailserver is a Microsoft Exchange server that
1214		// does NOT support Server Side Sort
1215
1216		if (!\GO::config()->imap_sort_on_date) {
1217			\GO::debug("imap::Config::imap_sort_on_date(false)");
1218			if ($sort == 'DATE' || $sort == 'R_DATE') {
1219				$sort = 'ARRIVAL';
1220			}
1221		} else {
1222			\GO::debug("imap::Config::imap_sort_on_date(true)");
1223		}
1224
1225		\GO::debug("imap::client_side_sort($sort, $reverse, $filter)");
1226
1227		$uid_string='1:*';
1228		if(!empty($filter) && $filter !='ALL'){
1229			$uids = $this->search($filter);
1230			if(!count($uids)){
1231				return array();
1232			}else
1233			{
1234				$uid_string=implode(',', $uids);
1235			}
1236		}
1237
1238		$this->clean($sort, 'keyword');
1239		$command1 = 'UID FETCH '.$uid_string.' ';
1240		switch ($sort) {
1241//		Doesn't work on some servers. Use internal date for these.
1242//		Enabled because we have added GO::config()->imap_sort_on_date functionality
1243			case 'DATE':
1244			case 'R_DATE':
1245				$command2 = "BODY.PEEK[HEADER.FIELDS (DATE)]";
1246				$key = "BODY[HEADER.FIELDS";
1247				break;
1248			case 'SIZE':
1249//		END
1250
1251			case 'R_SIZE':
1252				$command2 = "RFC822.SIZE";
1253				$key = "RFC822.SIZE";
1254				break;
1255			case 'ARRIVAL':
1256				$command2 = "INTERNALDATE";
1257				$key = "INTERNALDATE";
1258				break;
1259			case 'R_ARRIVAL':
1260				$command2 = "INTERNALDATE";
1261				$key = "INTERNALDATE";
1262				break;
1263			case 'FROM':
1264			case 'R_FROM':
1265				$command2 = "BODY.PEEK[HEADER.FIELDS (FROM)]";
1266				$key = "BODY[HEADER.FIELDS";
1267				break;
1268			case 'SUBJECT':
1269			case 'R_SUBJECT':
1270				$command2 = "BODY.PEEK[HEADER.FIELDS (SUBJECT)]";
1271				$key = "BODY[HEADER.FIELDS";
1272				break;
1273			default:
1274				$command2 = "INTERNALDATE";
1275				$key = "INTERNALDATE";
1276				break;
1277		}
1278		$command = $command1.'('.$command2.")\r\n";
1279
1280		$this->send_command($command);
1281		$res = $this->get_response(false, true);
1282		$status = $this->check_response($res, true);
1283		$uids = array();
1284		$sort_keys = array();
1285		foreach ($res as $vals) {
1286			if (!isset($vals[0]) || $vals[0] != '*') {
1287				continue;
1288			}
1289			$uid = 0;
1290			$sort_key = 0;
1291			$body = false;
1292			foreach ($vals as $i => $v) {
1293				if ($body) {
1294					if ($v == ']' && isset($vals[$i + 1])) {
1295						if ($command2 == "BODY.PEEK[HEADER.FIELDS (DATE)]") {
1296							$sort_key = strtotime(trim(substr($vals[$i + 1], 5)));
1297						}
1298						else {
1299							$sort_key = $vals[$i + 1];
1300						}
1301						$body = false;
1302					}
1303				}
1304				if (strtoupper($v) == 'UID') {
1305					if (isset($vals[($i + 1)])) {
1306						$uid = $vals[$i + 1];
1307						$uids[] = $uid;
1308					}
1309				}
1310				if ($key == strtoupper($v)) {
1311					if (substr($key, 0, 4) == 'BODY') {
1312						$body = 1;
1313					}
1314					elseif (isset($vals[($i + 1)])) {
1315						if ($key == "INTERNALDATE") {
1316							$sort_key = strtotime($vals[$i + 1]);
1317						}
1318						else {
1319							$sort_key = $vals[$i + 1];
1320						}
1321					}
1322				}
1323			}
1324			if ($sort_key && $uid) {
1325				$sort_keys[$uid] = $sort_key;
1326			}
1327		}
1328
1329		if (count($sort_keys) != count($uids)) {
1330			//echo 'BUG: Client side sort array mismatch';
1331			//exit;
1332		}
1333		unset($res);
1334		natcasesort($sort_keys);
1335		$uids = array_keys($sort_keys);
1336		if ($reverse) {
1337			$uids = array_reverse($uids);
1338		}
1339		return $status ? $uids : false;
1340	}
1341	/* use the THREAD extension to get the sorted UID list and thread data */
1342	private function thread_sort($sort ,$filter) {
1343		$this->clean($filter, 'keyword');
1344		if (substr($sort, 7) == 'R') {
1345			$method = 'REFERENCES';
1346		}
1347		else {
1348			$method = 'ORDEREDSUBJECT';
1349		}
1350		$command = 'UID THREAD '.$method.' US-ASCII '.$filter."\r\n";
1351		$this->send_command($command);
1352		$res = $this->get_response();
1353		$status = $this->check_response($res);
1354		$uid_string = '';
1355		foreach ($res as $val) {
1356			if (strtoupper(substr($val, 0, 8)) == '* THREAD') {
1357				$uid_string .= ' '.substr($val, 8);
1358			}
1359		}
1360		unset($res);
1361		$uids = array();
1362		$thread_data = array();
1363		$uid_string = str_replace(array(' )', ' ) ', ')', ' (', ' ( ', '( '), array(')', ')', ')', '(', '(', '('), $uid_string);
1364		$branches = array();
1365		$level = 0;
1366		$thread = 0;
1367		$last_id = 0;
1368		$offset = 0;
1369		$parents = array();
1370		while($uid_string) {
1371			switch ($uid_string[0]) {
1372				case ' ':
1373					$level++;
1374					$offset++;
1375					$parents[$level] = $last_id;
1376					$uid_string = substr($uid_string, 1);
1377					break;
1378				case '(':
1379					$level++;
1380					if ($level == 2) {
1381						$parents[$level] = $thread;
1382					}
1383					$uid_string = substr($uid_string, 1);
1384					break;
1385				case ')':
1386					$uid_string = substr($uid_string, 1);
1387					if ($offset) {
1388						$level -= $offset;
1389						$offset = 0;
1390					}
1391					$level--;
1392					break;
1393				default:
1394					if (preg_match("/^(\d+)/", $uid_string, $matches)) {
1395						if ($level == 1) {
1396							$thread = $matches[1];
1397							$parents = array(1 => 0);
1398						}
1399						if (!isset($parents[$level])) {
1400							if (isset($parents[$level - 1])) {
1401								$parents[$level] = $parents[$level - 1];
1402							}
1403							else {
1404								$parents[$level] = 0;
1405							}
1406						}
1407						$thread_data[$thread][$matches[1]] = array('parent' => $parents[$level], 'level' => $level, 'thread' => $thread);
1408						$parents[$level] = $thread;
1409						$last_id = $matches[1];
1410						$uid_string = substr($uid_string, strlen($matches[1]));
1411					}
1412					else {
1413						echo 'BUG'.$uid_string."\r\n";
1414						;
1415						$uid_string = substr($uid_string, 1);
1416					}
1417			}
1418		}
1419		$thread_data = array_reverse($thread_data);
1420		$new_thread_data = array();
1421		$threads = array();
1422		foreach ($thread_data as $vals) {
1423			foreach ($vals as $i => $v) {
1424				$uids[] = $i;
1425				if ($v['parent'] && isset($new_thread_data[$v['parent']])) {
1426					if (isset($new_thread_data[$v['thread']]['reply_count'])) {
1427						$new_thread_data[$v['thread']]['reply_count']++;
1428					}
1429					else {
1430						$new_thread_data[$v['thread']]['reply_count'] = 1;
1431					}
1432				}
1433				else {
1434					$threads[] = $i;
1435				}
1436				$new_thread_data[$i] = $v;
1437			}
1438		}
1439		return array('uids' => $uids, 'total' => count($uids), 'thread_data' => $new_thread_data,
1440						'sort' => $sort, 'filter' => $filter, 'timestamp' => time(), 'threads' => $threads);
1441
1442	}
1443
1444
1445	/**
1446	 * Get's message headers of a single message:
1447	 *
1448	 * $message=array(
1449					'to'=>'',
1450					'cc'=>'',
1451					'bcc'=>'',
1452					'from'=>'',
1453					'subject'=>'',
1454					'uid'=>'',
1455					'size'=>'',
1456					'internal_date'=>'',
1457					'date'=>'',
1458					'udate'=>'',
1459					'internal_udate'=>'',
1460					'x-priority'=>3,
1461					'reply-to'=>'',
1462					'content-type'=>'',
1463					'disposition-notification-to'=>'',
1464					'content-transfer-encoding'=>'',
1465					'charset'=>'',
1466					'seen'=>0,
1467					'flagged'=>0,
1468					'answered'=>0,
1469					'forwarded'=>0
1470				);
1471	 *
1472	 * @param <type> $uid
1473	 * @return <type>
1474	 */
1475
1476	public function get_message_header($uid, $full_data=false){
1477		$headers = $this->get_message_headers(array($uid), $full_data);
1478		if(isset($headers[$uid])){
1479			return $headers[$uid];
1480		}else
1481		{
1482			return false;
1483		}
1484	}
1485
1486
1487	/**
1488	 * Get's message headers from an UID range:
1489	 *
1490	 * $message=array(
1491					'to'=>'',
1492					'cc'=>'',
1493					'bcc'=>'',
1494					'from'=>'',
1495					'subject'=>'',
1496					'uid'=>'',
1497					'size'=>'',
1498					'internal_date'=>'',
1499					'date'=>'',
1500					'udate'=>'',
1501					'internal_udate'=>'',
1502					'x-priority'=>3,
1503					'reply-to'=>'',
1504					'content-type'=>'',
1505					'disposition-notification-to'=>'',
1506					'content-transfer-encoding'=>'',
1507				 'charset'=>'',
1508					'seen'=>0,
1509					'flagged'=>0,
1510					'answered'=>0,
1511					'forwarded'=>0
1512				);
1513	 *
1514	 * @param <type> $uids
1515	 * @return <type>
1516	 */
1517	public function get_message_headers($uids, $full_data=false) {
1518
1519		if(empty($uids))
1520			return array();
1521
1522		$sorted_string = implode(',', $uids);
1523		$this->clean($sorted_string, 'uid_list');
1524
1525		$flags_string = 'FLAGS';
1526		if ($this->server == 'imap.gmail.com') {
1527			$this->gmail_server = true;
1528			$flags_string = 'X-GM-LABELS FLAGS';
1529		}
1530
1531		$command = 'UID FETCH '.$sorted_string.' (' . $flags_string . ' INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (SUBJECT FROM '.
1532						"DATE CONTENT-TYPE X-PRIORITY TO CC";
1533
1534		if($full_data)
1535			$command .= " BCC REPLY-TO DISPOSITION-NOTIFICATION-TO CONTENT-TRANSFER-ENCODING MESSAGE-ID";
1536
1537		$command .= ")])\r\n";
1538
1539		$this->send_command($command);
1540		$res = $this->get_response(false, true);
1541
1542		$status = $this->check_response($res, true);
1543		$tags = array('UID' => 'uid', 'FLAGS' => 'flags', 'X-GM-LABELS' => 'flags', 'RFC822.SIZE' => 'size', 'INTERNALDATE' => 'internal_date');
1544		$junk = array('SUBJECT', 'FROM', 'CONTENT-TYPE', 'TO', 'CC','BCC', '(', ')', ']', 'X-PRIORITY', 'DATE','REPLY-TO','DISPOSITION-NOTIFICATION-TO','CONTENT-TRANSFER-ENCODING', 'MESSAGE-ID');
1545		//$flds = array('uid','flags','size','internal_date','answered','seen','','reply-to', 'content-type','x-priority','disposition-notification-to');
1546		$headers = array();
1547		foreach ($res as $n => $vals) {
1548			if (isset($vals[0]) && $vals[0] == '*') {
1549				$message=array(
1550					'to'=>'',
1551					'cc'=>'',
1552					'bcc'=>'',
1553					'from'=>'',
1554					'subject'=>'',
1555					'uid'=>'',
1556					'size'=>'',
1557					'internal_date'=>'',
1558					'date'=>'',
1559					'udate'=>'',
1560					'internal_udate'=>'',
1561					'x_priority'=>3,
1562					'reply_to'=>'',
1563					'message_id'=>'',
1564					'content_type'=>'',
1565					'content_type_attributes'=>array(),
1566					'disposition_notification_to'=>'',
1567					'content_transfer_encoding'=>'',
1568					'charset'=>'',
1569					'seen'=>0,
1570					'flagged'=>0,
1571					'answered'=>0,
1572					'forwarded'=>0,
1573					'has_attachments'=>0,
1574					'labels'=>array(),
1575					'deleted'=>0,
1576				);
1577
1578				$count = count($vals);
1579				for ($i=0;$i<$count;$i++) {
1580					if ($vals[$i] == 'BODY[HEADER.FIELDS') {
1581						$i++;
1582						while(isset($vals[$i]) && in_array($vals[$i], $junk)) {
1583							$i++;
1584						}
1585
1586						$header = str_replace("\r\n", "\n", $vals[$i]);
1587						$header = preg_replace("/\n\s/", " ", $header);
1588
1589						$lines = explode("\n", $header);
1590
1591						foreach ($lines as $line) {
1592							if(!empty($line)) {
1593								$header = trim(strtolower(substr($line, 0, strpos($line, ':'))));
1594								$header = str_replace('-','_',$header);
1595
1596								if (!$header && !empty($last_header)) {
1597									$message[$last_header] .= "\n".trim($line);
1598								}else {
1599									if(isset($message[$header])){
1600										$message[$header] = trim(substr($line, (strpos($line, ':') + 1)));
1601										$last_header = $header;
1602									}
1603								}
1604							}
1605						}
1606					}
1607					elseif (isset($tags[strtoupper($vals[$i])])) {
1608						if (isset($vals[($i + 1)])) {
1609							if ($tags[strtoupper($vals[$i])] == 'flags' && $vals[$i + 1] == '(') {
1610								$n = 2;
1611								while (isset($vals[$i + $n]) && $vals[$i + $n] != ')') {
1612									$prop = str_replace('-','_',strtolower(substr($vals[$i + $n],1)));
1613									//\GO::debug($prop);
1614									if(isset($message[$prop])) {
1615										$message[$prop]=true;
1616									} else {
1617										$message['labels'][] = strtolower($vals[$i + $n]);
1618									}
1619
1620									$n++;
1621								}
1622								$i += $n;
1623							}
1624							else {
1625								$prop = $tags[strtoupper($vals[$i])];
1626
1627								if(isset($message[$prop]))
1628										$message[$prop] = trim($vals[($i + 1)]);
1629								$i++;
1630							}
1631						}
1632					}
1633				}
1634				if ($message['uid']) {
1635					if(isset($message['content_type'])) {
1636						$message['content_type']=strtolower($message['content_type']);
1637						if (strpos($message['content_type'], 'charset=')!==false) {
1638							if (preg_match("/charset\=([^\s]+)/", $message['content_type'], $matches)) {
1639								$message['charset'] = trim(str_replace(array('"', "'", ';'), '', $matches[1]));
1640							}
1641						}
1642						if(preg_match("/([^\/]*\/[^;]*)(.*)/", $message['content_type'], $matches)){
1643							$message['content_type']=$matches[1];
1644							$atts = trim($matches[2], ' ;');
1645							$atts=explode(';', $atts);
1646
1647							for($i=0;$i<count($atts);$i++){
1648								$keyvalue=explode('=', $atts[$i]);
1649								if(isset($keyvalue[1]) && $keyvalue[0]!='boundary')
1650									$message['content_type_attributes'][trim($keyvalue[0])]=trim($keyvalue[1],' "');
1651							}
1652
1653							//$message['content-type-attributes']=$atts;
1654						}
1655					}
1656
1657					//sometimes headers contain some extra stuff between ()
1658					$message['date']=preg_replace('/\([^\)]*\)/','', $message['date']);
1659
1660					$message['udate']=strtotime($message['date']);
1661					$message['internal_udate']=strtotime($message['internal_date']);
1662					if(empty($message['udate']))
1663						$message['udate']=$message['internal_udate'];
1664
1665					$message['subject']=$this->mime_header_decode($message['subject']);
1666					$message['from']=$this->mime_header_decode($message['from']);
1667					$message['to']=$this->mime_header_decode($message['to']);
1668					$message['reply_to']=$this->mime_header_decode($message['reply_to']);
1669					$message['disposition_notification_to']=$this->mime_header_decode($message['disposition_notification_to']);
1670
1671					//remove non ascii stuff. Incredimail likes iso encoded chars too :(
1672					if(isset($message['message_id'])) {
1673						$message['message_id']= preg_replace('/[[:^print:]]/', '', $message['message_id']);
1674					}
1675
1676					if(isset($message['cc']))
1677						$message['cc']=$this->mime_header_decode($message['cc']);
1678
1679					if(isset($message['bcc']))
1680						$message['bcc']=$this->mime_header_decode($message['bcc']);
1681
1682					preg_match("'([^/]*)/([^ ;\n\t]*)'i", $message['content_type'], $ct);
1683
1684					if (isset($ct[2]) && $ct[1] != 'text' && $ct[2] != 'alternative' && $ct[2] != 'related')
1685					{
1686						$message["has_attachments"] = 1;
1687					}
1688
1689					$headers[$message['uid']] = $message;
1690
1691					//$message['priority']=intval($message['x-priority']);
1692
1693
1694				}
1695			}
1696		}
1697		$final_headers = array();
1698		foreach ($uids as $v) {
1699			if (isset($headers[$v])) {
1700				$final_headers[$v] = $headers[$v];
1701			}
1702		}
1703
1704		//\GO::debug($final_headers);
1705		return $final_headers;
1706	}
1707
1708
1709	public function get_flags($uidRange = '1:*') {
1710		$command = 'UID FETCH '.$uidRange.' (FLAGS INTERNALDATE)'."\r\n";
1711
1712		$this->send_command($command);
1713		$res = $this->get_response(false, false);
1714
1715		$status = $this->check_response($res, false);
1716		if(!$status) {
1717			return false;
1718		}
1719
1720		//remove status response
1721		array_pop($res);
1722
1723		$data = [];
1724
1725		foreach($res as $message) {
1726			//UID 17 FLAGS ( \Flagged \Seen ) INTERNALDATE 24-May-2018 13:02:43 +0000
1727
1728			//or different order!
1729			// l * 2 FETCH ( UID 2 INTERNALDATE 30-Jan-2020 11:20:06 +0000 FLAGS ( \Seen ) )
1730
1731			if(preg_match('/UID ([0-9]+)/', $message, $uidMatches)) {
1732				$uid = (int) $uidMatches[1];
1733			} else{
1734				return false;
1735			}
1736
1737			if(preg_match('/FLAGS \((.*)\)/U', $message, $flagMatches)) {
1738				$flags = array_map('trim', explode(' ', trim($flagMatches[1])));
1739			}else{
1740				return false;
1741			}
1742
1743			if(preg_match('/INTERNALDATE ([^\s\)]+ [^\s\)]+ [^\s\)]+)/', $message, $dateMatches)) {
1744				$date = $dateMatches[1];
1745			}else{
1746				return false;
1747			}
1748
1749			$data[] = [
1750				'uid' => $uid,
1751				'flags' => $flags,
1752				'date' => $date
1753			];
1754
1755		}
1756
1757		return $data;
1758	}
1759
1760
1761	public function get_message_headers_set($start, $limit, $sort_field , $reverse=false, $query='ALL')
1762	{
1763		\GO::debug("get_message_headers_set($start, $limit, $sort_field , $reverse, $query)");
1764
1765		if($query=='ALL' || $query==""){
1766			$unseen = $this->get_unseen($this->selected_mailbox['name']);
1767
1768			$key = 'sort_cache_'.$this->selected_mailbox['name'].'_'.$this->server.'_'.$sort_field;
1769			$key .= $reverse ? '_1' : '_0';
1770
1771			$unseenCheck = $unseen['count'].':'.$this->selected_mailbox['messages'];
1772			if(!empty($this->selected_mailbox['uidnext']))
1773				$unseenCheck .= ':'.$this->selected_mailbox['uidnext'];
1774
1775			\GO::debug($unseenCheck);
1776			//var_dump($unseenCheck);
1777			if(isset(\GO::session()->values['emailmod'][$key]['unseen']) && \GO::session()->values['emailmod'][$key]['unseen']==$unseenCheck){
1778					//throw new \Exception("From cache");
1779				\GO::debug("IMAP sort from session cache");
1780				$uids = \GO::session()->values['emailmod'][$key]['uids'];
1781				$this->sort_count=count($uids);
1782			}else
1783			{
1784				\GO::debug("IMAP sort from server");
1785				\GO::session()->values['emailmod'][$key]['unseen']=$unseenCheck;
1786				$uids = \GO::session()->values['emailmod'][$key]['uids'] = $this->sort_mailbox($sort_field, $reverse, $query);
1787			}
1788		}else
1789		{
1790			$uids = $this->sort_mailbox($sort_field, $reverse, $query);
1791		}
1792
1793		\GO::debug("Count uids: ".count($uids));
1794
1795		if(!is_array($uids))
1796			return array();
1797
1798		if($limit>0)
1799			$uids=array_slice($uids,$start, $limit);
1800
1801		$chunks = array_chunk($uids, 1000);
1802
1803		$headers = array();
1804		while($subset = array_shift($chunks)){
1805			$headers = array_merge($headers, $this->get_message_headers($subset, true));
1806		}
1807
1808		return $headers;
1809	}
1810
1811
1812	/**
1813		* Check if the given mailbox root is valid and return it with the correct delimiter
1814		*
1815		* @param $mbroot The Mailbox root. (eg. INBOX/)
1816		* @access public
1817		* @return mixed Mailbox root with delimiter or false on failure
1818		*/
1819
1820	function check_mbroot($mbroot) {
1821		$mbroot = trim($mbroot);
1822
1823		if(empty($mbroot))
1824			return "";
1825
1826		$list = $this->get_folders('', false,'%');
1827//		\GO::debug($list);
1828//		throw new \Exception($mbroot);
1829		if (is_array($list)) {
1830			while ($folder = array_shift($list)) {
1831				if (!$this->delimiter && strlen($folder['delimiter']) > 0) {
1832					$this->set_mailbox_delimiter($folder['delimiter']);
1833
1834					if (substr($mbroot, -1) == $this->delimiter) {
1835						$mbroot = substr($mbroot, 0, -1);
1836					}
1837				}
1838
1839				if ($folder['name'] == $mbroot) {
1840					return $mbroot.$this->delimiter;
1841				}
1842			}
1843		}
1844		return '';
1845	}
1846
1847
1848	/**
1849	 * Get's an array with two keys. usage and limit in bytes.
1850	 *
1851	 * @return <type>
1852	 */
1853	public function get_quota() {
1854
1855		if(!$this->has_capability("QUOTA"))
1856			return false;
1857
1858		$command = "GETQUOTAROOT \"INBOX\"\r\n";
1859
1860		$this->send_command($command);
1861		$res = $this->get_response();
1862		$status = $this->check_response($res);
1863		if($status){
1864			foreach($res as $response){
1865				if(strpos($response, 'STORAGE')!==false){
1866					$parts = explode(" ", $response);
1867					$storage_part = array_search("STORAGE", $parts);
1868					if ($storage_part>0){
1869						return array(
1870							'usage'=>intval($parts[$storage_part+1]),
1871							'limit'=>intval($parts[$storage_part+2]));
1872					}
1873				}
1874			}
1875		}
1876		return false;
1877	}
1878
1879	/**
1880	 * Get the structure of a message
1881	 *
1882	 * @param <type> $uid
1883	 * @return <type>
1884	 */
1885	public function get_message_structure($uid) {
1886		$this->clean($uid, 'uid');
1887		$part_num = 1;
1888		$struct = array();
1889		$command = "UID FETCH $uid BODYSTRUCTURE\r\n";
1890		$this->send_command($command);
1891		$result = $this->get_response(false, true);
1892
1893		while (isset($result[0][0]) && isset($result[0][1]) && $result[0][0] == '*' && strtoupper($result[0][1]) == 'OK') {
1894			array_shift($result);
1895		}
1896		$status = $this->check_response($result, true);
1897		array_pop($result);
1898
1899		$r = [];
1900		while($line = array_shift($result)) {
1901			$r = array_merge($r, $line);
1902		}
1903
1904		$response = array();
1905		if (!isset($r[4])) {
1906			$status = false;
1907		}
1908		if ($status) {
1909			if (strtoupper($r[4]) == 'UID') {
1910				$response = array_slice($r, 7, -1);
1911			}
1912			else {
1913				$response = array_slice($r, 5, -1);
1914			}
1915			$response = $this->split_toplevel_result($response);
1916			if (count($response) > 1) {
1917				$struct = $this->parse_multi_part($response, 1, 1);
1918			}
1919			else {
1920				$struct[1] = $this->parse_single_part($response);
1921			}
1922		}
1923
1924		return $struct;
1925	}
1926
1927	/**
1928	 * Find's the first message part in a structure returned from
1929	 * get_message_structure that matches the parameters given.
1930	 *
1931	 * Useful to find the first text/plain or text/html for example to find the
1932	 * message body.
1933	 *
1934	 * @param <type> $struct
1935	 * @param <type> $number
1936	 * @param <type> $type
1937	 * @param <type> $subtype
1938	 * @return <type>
1939	 */
1940
1941	public function find_message_parts($struct, $number, $type='text', $subtype=false, $parts=array()) {
1942		if (!is_array($struct) || empty($struct)) {
1943			return $parts;
1944		}
1945		foreach ($struct as $id => $vals) {
1946			if ($number && $id == $number) {
1947				$vals['number'] = $id;
1948				$parts[] = $vals;
1949			}
1950			elseif (!$number && isset($vals['type']) && $vals['type'] == $type) {
1951				if ($subtype) {
1952					if ($subtype == $vals['subtype']) {
1953						$vals['number'] = $id;
1954						$parts[] = $vals;
1955					}
1956				}
1957				else {
1958					$vals['number'] = $id;
1959					$parts[] = $vals;
1960				}
1961			}
1962			if (empty($res) && isset($vals['subs'])) {
1963				$this->find_message_parts($vals['subs'], $number, $type, $subtype, $parts);
1964			}
1965		}
1966		return $parts;
1967	}
1968
1969
1970
1971
1972	public function has_alternative_body($struct){
1973
1974		//\GO::debug($struct);
1975
1976		if (!is_array($struct) || empty($struct)) {
1977			return false;
1978		}
1979
1980		if(isset($struct['type']) && $struct['type']=='message' && strtolower($struct['subtype'])=='alternative'){
1981			return true;
1982		}
1983
1984		foreach ($struct as $id => $vals) {
1985			if(isset($vals['type']) && $vals['type']=='message' && isset($struct['subtype']) && strtolower($struct['subtype'])=='alternative'){
1986				return true;
1987			}elseif (isset($vals['subs']) && (!isset($vals['subtype']) || $vals['subtype']!='rfc822')){
1988				if($this->has_alternative_body($vals['subs'])){
1989					return true;
1990				}
1991			}
1992		}
1993
1994		return false;
1995	}
1996
1997
1998	/**
1999	 * Find's the first message part in a structure returned from
2000	 * get_message_structure that matches the parameters given.
2001	 *
2002	 * Useful to find the first text/plain or text/html for example to find the
2003	 * message body.
2004	 *
2005	 * @param <type> $struct
2006	 * @param <type> $number
2007	 * @param <type> $type
2008	 * @param <type> $subtype
2009	 * @return <type>
2010	 */
2011
2012	public function find_body_parts($struct, $type='text', $subtype='html', &$parts=array('text_found'=>false, 'parts'=>array())) {
2013
2014		if (!is_array($struct) || empty($struct)) {
2015			return $parts;
2016		}
2017
2018		$imgs  =array('jpg','jpeg','gif','png','bmp');
2019		foreach ($struct as $id => $vals) {
2020
2021			//\GO::debug($vals);
2022			if(is_array($vals)){
2023				if (isset($vals['type'])){
2024
2025					$vals['number'] = $id;
2026					//\GO::debug($vals);
2027
2028					if ($vals['type'] == $type && $subtype == $vals['subtype'] && strtolower($vals['disposition'])!='attachment' && empty($vals['name'])) {
2029
2030						$parts['text_found']=true;
2031						$parts['parts'][] = $vals;
2032
2033					}elseif($vals['type']=='image' && in_array($vals['subtype'], $imgs) && $vals['disposition']=='inline' && empty($vals['id']))
2034					{
2035						//\GO::debug($vals);
2036						//work around ugly stuff. Some mails contain stuff with type image/gif but it's actually an html file.
2037						//so we double check if the image has a filename that it has a valid image extension
2038						$file = empty($vals['name']) ? false : new \GO\Base\Fs\File($vals['name']);
2039						if(!$file || $file->isImage()){
2040
2041							//an inline image without ID. We'll display in the part order. Apple
2042							//mail sends mail like this.
2043							$parts['parts'][]=$vals;
2044						}
2045					}
2046				}
2047
2048				//don't decent into message/RFC822 files. Sometimes they come nested in the body from the IMAP server.
2049				if (isset($vals['subs']) && (!isset($vals['subtype']) || $vals['subtype']!='rfc822')){
2050
2051//					$text_found_at_this_level = $parts['text_found'];
2052					$this->find_body_parts($vals['subs'], $type, $subtype, $parts);
2053
2054					//
2055
2056					/*
2057					 * If we found body parts for example 1.1 and 1.2 doesn't include a text
2058					 * attachment with number 2 like in this sample structure
2059					 *
2060					 * array (
2061	  1 =>
2062	  array (
2063		'subs' =>
2064		array (
2065		  '1.1' =>
2066		  array (
2067			'type' => 'text',
2068			'subtype' => 'plain',
2069			'charset' => 'iso-8859-1',
2070			'format' => 'flowed',
2071			'id' => false,
2072			'description' => false,
2073			'encoding' => '8bit',
2074			'size' => '279',
2075			'lines' => '15',
2076			'md5' => false,
2077			'disposition' => false,
2078			'language' => false,
2079			'location' => false,
2080			'name' => false,
2081			'filename' => false,
2082		  ),
2083		  '1.2' =>
2084		  array (
2085			'subs' =>
2086			array (
2087			  '1.2.1' =>
2088			  array (
2089				'type' => 'text',
2090				'subtype' => 'html',
2091				'charset' => 'iso-8859-1',
2092				'id' => false,
2093				'description' => false,
2094				'encoding' => '7bit',
2095				'size' => '1028',
2096				'lines' => '28',
2097				'md5' => false,
2098				'disposition' => false,
2099				'language' => false,
2100				'location' => false,
2101				'name' => false,
2102				'filename' => false,
2103			  ),
2104			  '1.2.2' =>
2105			  array (
2106				'type' => 'image',
2107				'subtype' => 'jpeg',
2108				'name' => 'gass-sign.jpg',
2109				'id' => '<part1.04070803.02030505@gassinstallasjon.no>',
2110				'description' => false,
2111				'encoding' => 'base64',
2112				'size' => '19818',
2113				'md5' => false,
2114				'disposition' => 'inline',
2115				'language' => '(',
2116				'location' => 'filename',
2117				'filename' => false,
2118				'charset' => false,
2119				'lines' => false,
2120			  ),
2121			  'type' => 'message',
2122			  'subtype' => 'related',
2123			),
2124		  ),
2125		  'type' => 'message',
2126		  'subtype' => 'alternative',
2127		),
2128	  ),
2129	  2 =>
2130	  array (
2131		'type' => 'text',
2132		'subtype' => 'plain',
2133		'name' => 'gass-skriver.txt',
2134		'charset' => 'us-ascii',
2135		'id' => false,
2136		'description' => false,
2137		'encoding' => 'base64',
2138		'size' => '32',
2139		'lines' => '0',
2140		'md5' => false,
2141		'disposition' => 'inline',
2142		'language' => '(',
2143		'location' => 'filename',
2144		'filename' => false,
2145	  ),
2146	  'type' => 'message',
2147	  'subtype' => 'mixed',
2148	)
2149					 */
2150//					if(!$text_found_at_this_level && $parts['text_found'])
2151//						break;
2152				}
2153			}
2154		}
2155		return $parts;
2156	}
2157
2158	/**
2159	 * Find all attachment parts from a structure returned by get_message_structure
2160	 *
2161	 * @param <type> $struct
2162	 * @param <type> $skip_ids Skip thise ID's
2163	 * @param <type> $attachments
2164	 * @return <type>
2165	 */
2166
2167	public function find_message_attachments($struct, $skip_ids=array(), $attachments=array()) {
2168		if (!is_array($struct) || empty($struct)) {
2169			return $attachments;
2170		}
2171
2172		foreach ($struct as $id => $vals) {
2173			//if(!is_array($vals) || in_array($id, $skip_ids))
2174			if(!is_array($vals))
2175				continue;
2176//var_dump($vals);
2177			// Strict must be true as 2.1 == 2.10 if false
2178			if(isset($vals['type']) && !in_array($id, $skip_ids, true)){
2179				$vals['number'] = $id;
2180
2181				//sometimes NIL is returned from Dovecot?!?
2182				if($vals['id']=='NIL')
2183					$vals['id']='';
2184
2185				$attachments[]=$vals;
2186			}elseif(isset($vals['subs'])) {
2187				$attachments = $this->find_message_attachments($vals['subs'],$skip_ids,	$attachments);
2188			}
2189		}
2190		return $attachments;
2191	}
2192
2193	/**
2194	 * Decodes a message part.
2195	 *
2196	 * @param <type> $str
2197	 * @param <type> $encoding Can be base64 or quoted-printable
2198	 * @param <type> $charset If this is given then the part will be converted to UTF-8 and illegal characters will be stripped.
2199	 * @return <type>
2200	 */
2201
2202	public function decode_message_part($str, $encoding, $charset=false) {
2203
2204		switch(strtolower($encoding)) {
2205			case 'base64':
2206				$str = base64_decode($str);
2207				break;
2208			case 'quoted-printable':
2209				$str =  quoted_printable_decode($str);
2210				break;
2211		}
2212
2213		if($charset){
2214
2215			//some clients don't send the charset.
2216			if($charset=='us-ascii')
2217				$charset = 'windows-1252';
2218
2219			$str = \GO\Base\Util\StringHelper::clean_utf8($str, $charset);
2220			if($charset != 'utf-8') {
2221				$str = str_replace($charset, 'utf-8', $str);
2222			}
2223		}
2224		return $str;
2225	}
2226
2227	/**
2228	 * Decode an uuencoded attachment
2229	 *
2230	 * @param int $uid
2231	 * @param int $part_no
2232	 * @param boolean $peek
2233	 * @param type $fp
2234	 * @return type
2235	 * @throws \Exception
2236	 */
2237	private function _uudecode($uid, $part_no, $peek, $fp) {
2238		$regex = "/(begin ([0-7]{1,3}) (.+))\n/";
2239
2240		$body = $this->get_message_part($uid, $part_no, $peek);
2241
2242		if (preg_match($regex, $body, $matches, PREG_OFFSET_CAPTURE)) {
2243
2244			$offset = $matches[3][1] + strlen($matches[3][0]) + 1;
2245
2246			$endpos = strpos($body, 'end', $offset) - $offset - 1;
2247
2248
2249			if(!$endpos){
2250				throw new \Exception("Invalid UUEncoded attachment in uid: ".$uid);
2251			}
2252
2253			if(!isset($startPosAtts))
2254				$startPosAtts= $matches[0][1];
2255
2256			$att = str_replace(array("\r"), "", substr($body, $offset, $endpos));
2257
2258			$data = convert_uudecode($att);
2259
2260			if(!$fp){
2261				return $data;
2262			}else{
2263				fputs($fp, $data);
2264			}
2265		}
2266	}
2267
2268	/**
2269	 * Get's a message part and returned in binary form or UTF-8 charset.
2270	 *
2271	 * @param int $uid
2272	 * @param StringHelper $part_no
2273	 * @param stirng $encoding
2274	 * @param StringHelper $charset
2275	 * @param boolean $peek
2276	 * @return StringHelper
2277	 */
2278
2279	public function get_message_part_decoded($uid, $part_no, $encoding, $charset=false, $peek=false, $cutofflength=false, $fp=false) {
2280		\GO::debug("get_message_part_decoded($uid, $part_no, $encoding, $charset)");
2281
2282
2283		if($encoding == 'uuencode') {
2284			return $this->_uudecode($uid, $part_no, $peek, $fp);
2285		}
2286
2287		$str = '';
2288		$this->get_message_part_start($uid, $part_no, $peek);
2289
2290
2291		$leftOver='';
2292
2293		while ($line = $this->get_message_part_line()) {
2294
2295			switch (strtolower($encoding)) {
2296				case 'base64':
2297					$line = trim($leftOver.$line);
2298					$leftOver = "";
2299
2300					if(strlen($line) % 4 == 0){
2301
2302						if(!$fp){
2303							$str .= base64_decode($line);
2304						}  else {
2305							fputs($fp, base64_decode($line));
2306						}
2307					}else{
2308
2309						$buffer = "";
2310						while(strlen($line)>4){
2311							$buffer .= substr($line, 0, 4);
2312							$line = substr($line, 4);
2313						}
2314
2315						if(!$fp){
2316							$str .= base64_decode($buffer);
2317						}  else {
2318							fputs($fp, base64_decode($buffer));
2319						}
2320
2321						if(strlen($line)){
2322							$leftOver = $line;
2323						}
2324					}
2325					break;
2326				case 'quoted-printable':
2327					if(!$fp){
2328						$str .= quoted_printable_decode($line);
2329					}else{
2330						fputs($fp, quoted_printable_decode($line));
2331					}
2332					break;
2333				default:
2334					if(!$fp){
2335						$str .= $line;
2336					}else{
2337						fputs($fp, $line);
2338					}
2339					break;
2340			}
2341
2342			if($cutofflength && strlen($line)>$cutofflength){
2343				break;
2344			}
2345		}
2346
2347		if(!empty($leftOver))
2348		{
2349			\GO::debug($leftOver);
2350
2351			if(!$fp){
2352				$str .= base64_decode($leftOver);
2353			}  else {
2354				fputs($fp, base64_decode($leftOver));
2355			}
2356		}
2357
2358
2359		if($charset){
2360
2361			//some clients don't send the charset.
2362			if($charset=='us-ascii') {
2363				$charset = $this->findCharsetInHtmlBody($str);
2364			}
2365
2366			$str = \GO\Base\Util\StringHelper::clean_utf8($str, $charset);
2367			if($charset != 'utf-8') {
2368				$str = str_replace($charset, 'utf-8', $str);
2369			}
2370		}
2371
2372
2373		return $fp ? true : $str;
2374
2375
2376//		return $this->decode_message_part(
2377//						$this->get_message_part($uid, $part_no, $peek, $cutofflength),
2378//						$encoding,
2379//						$charset
2380//		);
2381	}
2382
2383	private function findCharsetInHtmlBody($body) {
2384// var_dump($body);
2385		if(preg_match('/<meta.*charset=([^"\'\b]+)/i', $body, $matches)) {
2386			return $matches[1];
2387		}
2388
2389		return 'windows-1252';
2390	}
2391
2392
2393	/**
2394	 * Get the full body of a message part. Obtain the partnumbers with get_message_structure.
2395	 *
2396	 * @param <type> $uid
2397	 * @param <type> $message_part omit if you want the full message
2398	 * @param <type> $raw
2399	 * @param <type> $max
2400	 * @return <type>
2401	 */
2402	public function get_message_part($uid, $message_part=0, $peek=false, $max=false, &$maxReached=false) {
2403//		$this->clean($uid, 'uid');
2404//
2405//		$peek_str = $peek ? '.PEEK' : '';
2406//
2407//		if (empty($message_part)) {
2408//			$command = "UID FETCH $uid BODY".$peek_str."[]\r\n";
2409//		}
2410//		else {
2411//			//$this->clean($message_part, 'msg_part');
2412//			$command = "UID FETCH $uid BODY".$peek_str."[$message_part]\r\n";
2413//		}
2414//		$this->send_command($command);
2415//
2416//		$result = $this->get_response($max, true);
2417//
2418//		$status = $this->check_response($result, true, false);
2419//
2420//		$res = '';
2421//		foreach ($result as $vals) {
2422//			if ($vals[0] != '*') {
2423//				continue;
2424//			}
2425//			$search = true;
2426//			foreach ($vals as $v) {
2427//				if ($v != ']' && !$search) {
2428//					$res = trim(preg_replace("/\s*\)$/", '', $v));
2429//					break 2;
2430//				}
2431//				if (stristr(strtoupper($v), 'BODY')) {
2432//					$search = false;
2433//				}
2434//			}
2435//		}
2436//		return $res;
2437
2438		$str = '';
2439		$this->get_message_part_start($uid,$message_part, $peek);
2440		while ($line = $this->get_message_part_line()) {
2441			$str .= $line;
2442		}
2443		return $str;
2444	}
2445
2446	/**
2447	 * Start getting a message part for reading it line by line
2448	 *
2449	 * @param <type> $uid
2450	 * @param <type> $message_part
2451	 * @return <type>
2452	 */
2453	public function get_message_part_start($uid, $message_part=0, $peek=false) {
2454
2455		$this->readFullLiteral = false;
2456		$this->clean($uid, 'uid');
2457
2458		$peek_str = $peek ? '.PEEK' : '';
2459
2460		if (empty($message_part)) {
2461			$command = "UID FETCH $uid BODY".$peek_str."[]\r\n";
2462		}
2463		else {
2464			//$this->clean($message_part, 'msg_part');
2465			$command = "UID FETCH $uid BODY".$peek_str."[$message_part]\r\n";
2466		}
2467		$this->send_command($command);
2468		$result = fgets($this->handle);
2469
2470		$size = false;
2471		if (preg_match("/\{(\d+)\}\r\n/", $result, $matches)) {
2472			$size = $matches[1];
2473		}
2474
2475//		if(!$size)
2476//			return false;
2477
2478		$this->message_part_size=$size;
2479		$this->message_part_read=0;
2480
2481//		\GO::debug("Part size: ".$size);
2482		return $size;
2483	}
2484
2485 private $readFullLiteral = false;
2486	/**
2487	 * Read message part line. get_message_part_start must be called first
2488	 *
2489	 * @return <type>
2490	 */
2491	public function get_message_part_line() {
2492
2493		$line=false;
2494		$leftOver = $this->message_part_size-$this->message_part_read;
2495		if($leftOver>0){
2496
2497			//reading exact length doesn't work if the last char is just one char somehow.
2498			//we cut the left over later with substr.
2499			$blockSize = 1024;//$leftOver>1024 ? 1024 : $leftOver;
2500			$line = fgets($this->handle,$blockSize);
2501			$this->message_part_read+=strlen($line);
2502		}
2503
2504		if ($this->message_part_size < $this->message_part_read) {
2505
2506			$line = substr($line, 0, ($this->message_part_read-$this->message_part_size)*-1);
2507		}
2508
2509		if($line===false){
2510
2511			if($this->readFullLiteral) {
2512				//don't attempt to read response after already have done that because it will hang for a long time
2513				$this->readFullLiteral = true;
2514				return false;
2515			}
2516
2517			//read and check left over response.
2518			$response = $this->get_response(false, true);
2519			if(!$this->check_response($response, true)) {
2520				return false;
2521			}
2522			//for some imap servers that don't return the attachment size. It will read the entire attachment into memory :(
2523			if(isset($response[0][6]) && substr($response[0][6], 0, 4) == 'BODY' && !empty($response[0][8])) {
2524				$line = $response[0][8];
2525				$this->readFullLiteral = true;
2526			}
2527
2528		}
2529		return $line;
2530	}
2531
2532	public function save_to_file($uid, $path, $imap_part_id=-1, $encoding='', $peek=false){
2533
2534		$fp = fopen($path, 'w+');
2535
2536		if(!$fp)
2537			return false;
2538
2539		/*
2540		 * Somehow fetching a message with an empty message part which should fetch it
2541		 * all doesn't work. (http://tools.ietf.org/html/rfc3501#section-6.4.5)
2542		 *
2543		 * That's why I first fetch the header and then the text.
2544		 */
2545		if($imap_part_id==-1){
2546			$header = $this->get_message_part($uid, 'HEADER', $peek)."\r\n\r\n";
2547
2548			if(empty($header))
2549				return false;
2550
2551			if(!fputs($fp, $header))
2552				return false;
2553
2554			$imap_part_id='TEXT';
2555		}
2556
2557
2558		$this->get_message_part_decoded($uid, $imap_part_id, $encoding, false, $peek, false, $fp);
2559
2560//		$size = $this->get_message_part_start($uid,$imap_part_id, $peek);
2561//
2562//		if(!$size)
2563//			return false;
2564//
2565//		while($line = $this->get_message_part_line()){
2566//			switch(strtolower($encoding)) {
2567//				case 'base64':
2568//					$line=base64_decode($line);
2569//					break;
2570//				case 'quoted-printable':
2571//					$line= quoted_printable_decode($line);
2572//					break;
2573//			}
2574//
2575//			if($line != "" && !fputs($fp, $line))
2576//				return false;
2577//		}
2578
2579		fclose($fp);
2580
2581		return true;
2582	}
2583
2584	/**
2585	 * Runs $command multiple times, with $uids split up in chunks of 500 UIDs
2586	 * for each run of $command.
2587	 * @param StringHelper $command IMAP command
2588	 * @param array $uids Array of UIDs
2589	 * @param boolean $trackErrors passed as third argument to $this->check_response()
2590	 * @return boolean
2591	 */
2592	private function _runInChunks($command, $uids, $trackErrors=true){
2593		$status=false;
2594		$uid_strings = array();
2595		if (empty($uids))
2596			return true;
2597
2598		if (count($uids) > 500) {
2599			while (count($uids) > 500) {
2600				$uid_strings[] = implode(',', array_splice($uids, 0, 2));
2601			}
2602			if (count($uids)) {
2603				$uid_strings[] = implode(',', $uids);
2604			}
2605		}
2606		else {
2607			$uid_strings[] = implode(',', $uids);
2608		}
2609
2610		foreach ($uid_strings as $uid_string) {
2611			if ($uid_string) {
2612				$this->clean($uid_string, 'uid_list');
2613			}
2614			$theCommand = sprintf($command,$uid_string);
2615			$this->send_command($theCommand);
2616			$res = $this->get_response();
2617			$status = $this->check_response($res, false, $trackErrors);
2618			if (!$status) {
2619				return $status;
2620			}
2621		}
2622
2623		return $status;
2624	}
2625
2626	/**
2627	 * Set or clear flags of an UID range. Flags can be:
2628	 *
2629	 * \Seen
2630	 * \Answered
2631	 * \Flagged
2632	 * \Deleted
2633	 * $Forwarded
2634	 *
2635	 * @param array $uids
2636	 * @param string $flags
2637	 * @param boolean $clear
2638	 * @return boolean
2639	 */
2640	public function set_message_flag($uids, $flags, $clear=false) {
2641		$status=false;
2642
2643		//TODO parhaps we can manage X-GM-LABEL too (but only what we can read is type like \\Starred)
2644
2645		if($clear)
2646			$command = "UID STORE %s -FLAGS.SILENT ($flags)\r\n";
2647		else
2648			$command = "UID STORE %s +FLAGS.SILENT ($flags)\r\n";
2649
2650		$status = $this->_runInChunks($command,$uids,false);
2651		return $status;
2652	}
2653
2654	/**
2655	 * Copy a message from the currently selected mailbox to another mailbox
2656	 *
2657	 * @param <type> $uids
2658	 * @param <type> $mailbox
2659	 * @return <type>
2660	 */
2661	public function copy($uids, $mailbox) {
2662
2663		if(empty($mailbox))
2664			$mailbox='INBOX';
2665
2666		$this->clean($mailbox, 'mailbox');
2667
2668		$uid_string = implode(',',$uids);
2669
2670		$command = "UID COPY %s \"".$this->utf7_encode($mailbox)."\"\r\n";
2671		$status = $this->_runInChunks($command, $uids);
2672		return $status;
2673	}
2674
2675	/**
2676	 * Move a message from the currently selected mailbox to another mailbox
2677	 *
2678	 * @param <type> $uids
2679	 * @param <type> $mailbox
2680	 * @param <type> $expunge
2681	 * @return <type>
2682	 */
2683	public function move($uids, $mailbox, $expunge=true) {
2684
2685		if(empty($mailbox))
2686			$mailbox='INBOX';
2687
2688		if(!in_array($mailbox, $this->touched_folders)) {
2689			$this->touched_folders[]=$mailbox;
2690		}
2691
2692		if(!$this->copy($uids, $mailbox))
2693			return false;
2694
2695		return $this->delete($uids, $expunge);
2696	}
2697
2698	/**
2699	 * Delete messages from the currently selected mailbox
2700	 *
2701	 * @param <type> $uids
2702	 * @param <type> $expunge
2703	 * @return <type>
2704	 */
2705	public function delete($uids, $expunge=true) {
2706		$status = $this->set_message_flag($uids, '\Deleted \Seen');
2707		if(!$status)
2708			return false;
2709
2710		return !$expunge || $this->expunge();
2711	}
2712
2713	/**
2714	 * Expunge the mailbox. It will remove all the messages marked with the
2715	 *  \Deleted flag.
2716	 *
2717	 * @return <type>
2718	 */
2719	public function expunge() {
2720		$this->send_command("EXPUNGE\r\n");
2721		$res = $this->get_response();
2722		return $this->check_response($res);
2723	}
2724
2725	private function addslashes($mailbox){
2726
2727		// For mailserver with \ as folder delimiter
2728		if($this->delimiter == '\\') {
2729			return str_replace('"', '\"', $mailbox);
2730		}
2731
2732		return $this->_escape( $mailbox);
2733	}
2734
2735	/**
2736	 * Removes a mailbox
2737	 *
2738	 * @param <type> $mailbox
2739	 * @return <type>
2740	 */
2741	public function delete_folder($mailbox) {
2742		$this->clean($mailbox, 'mailbox');
2743
2744		$success = $this->unsubscribe($mailbox);
2745
2746		$command = 'DELETE "'.$this->addslashes($this->utf7_encode($mailbox))."\"\r\n";
2747		$this->send_command($command);
2748		$result = $this->get_response(false);
2749		return $success;
2750	}
2751
2752	public function get_folder_tree($mailbox) {
2753		$this->clean($mailbox, 'mailbox');
2754		$delim = $this->get_mailbox_delimiter();
2755		return $this->get_folders($mailbox.$delim,true);
2756	}
2757
2758	/**
2759	 * Rename a mailbox
2760	 *
2761	 * @param <type> $mailbox
2762	 * @param <type> $new_mailbox
2763	 * @return <type>
2764	 */
2765	public function rename_folder($mailbox, $new_mailbox) {
2766		$this->clean($mailbox, 'mailbox');
2767		$this->clean($new_mailbox, 'mailbox');
2768
2769		$delim = $this->get_mailbox_delimiter();
2770
2771		$children = $this->get_folders($mailbox.$delim);
2772
2773		//\GO::debug($children);
2774		//throw new \Exception('test');
2775
2776		$command = 'RENAME "'.$this->addslashes($this->utf7_encode($mailbox)).'" "'.
2777						$this->addslashes($this->utf7_encode($new_mailbox)).'"'."\r\n";
2778//		throw new \Exception($command);
2779//		\GO::debug($command);
2780
2781		$this->send_command($command);
2782		$result = $this->get_response(false);
2783
2784		$status = $this->check_response($result, false);
2785
2786		if($status && $this->unsubscribe($mailbox) && $this->subscribe($new_mailbox)){
2787
2788			foreach($children as $old_child) {
2789				if($old_child['name']!=$mailbox){
2790				 $old_child = $old_child['name'];
2791				 $pos = strpos($old_child, $mailbox);
2792				 $new_child = substr_replace($old_child, $new_mailbox, $pos, strlen($mailbox));
2793
2794				 $this->unsubscribe($old_child);
2795				 $this->subscribe($new_child);
2796				}
2797			}
2798			return true;
2799		}else
2800		{
2801			return false;
2802		}
2803	}
2804
2805	/**
2806	 * Create a new mailbox
2807	 *
2808	 * @param <type> $mailbox
2809	 * @param <type> $subscribe
2810	 * @return <type>
2811	 */
2812	public function create_folder($mailbox, $subscribe=true) {
2813		$this->clean($mailbox, 'mailbox');
2814
2815		$command = 'CREATE "'.$this->addslashes($this->utf7_encode($mailbox)).'"'."\r\n";
2816
2817		$this->send_command($command);
2818		$result = $this->get_response(false);
2819
2820		$status = $this->check_response($result, false);
2821
2822		if(!$status)
2823			return false;
2824
2825		return !$subscribe || $this->subscribe($mailbox);
2826	}
2827
2828
2829	/**
2830	 * Subscribe to a mailbox
2831	 *
2832	 * @param <type> $mailbox
2833	 * @return <type>
2834	 */
2835	public function subscribe($mailbox){
2836		$command = 'SUBSCRIBE "'.$this->addslashes($this->utf7_encode($mailbox)).'"'."\r\n";
2837		$this->send_command($command);
2838		$result = $this->get_response(false, true);
2839		return $this->check_response($result, true);
2840	}
2841
2842	/**
2843	 * Unsubscribe a mailbox
2844	 *
2845	 * @param <type> $mailbox
2846	 * @return <type>
2847	 */
2848	public function unsubscribe($mailbox){
2849		$command = 'UNSUBSCRIBE "'.$this->addslashes($this->utf7_encode($mailbox)).'"'."\r\n";
2850		$this->send_command($command);
2851		$result = $this->get_response(false, true);
2852		return $this->check_response($result, true);
2853	}
2854
2855	/**
2856	 * Get the next UID for the selected mailbox
2857	 *
2858	 * @return StringHelper the next UID on the IMAP server
2859	 */
2860
2861	public function get_uidnext(){
2862
2863		if(empty($this->selected_mailbox['uidnext'])){
2864			$command = 'STATUS "'.$this->addslashes($this->utf7_encode($this->selected_mailbox['name'])).'" (UIDNEXT)'."\r\n";
2865			$this->send_command($command);
2866			$result = $this->get_response(false, true);
2867
2868			$vals = array_shift($result);
2869			if($vals){
2870				foreach ($vals as $i => $v) {
2871					if (intval($v) && isset($vals[($i - 1)]) && $vals[($i - 1)] == 'UIDNEXT') {
2872						$this->selected_mailbox['uidnext'] = $v;
2873					}
2874				}
2875			}
2876		}
2877
2878		return $this->selected_mailbox['uidnext'];
2879	}
2880
2881	/**
2882	 * Get unseen and messages in an array. eg:
2883	 *
2884	 * array('messages'=>2, 'unseen'=>1);
2885	 *
2886	 * @param StringHelper $mailbox
2887	 * @return array
2888	 */
2889	public function get_status($mailbox){
2890		$command = 'STATUS "'.$this->addslashes($this->utf7_encode($mailbox)).'" (MESSAGES UNSEEN)'."\r\n";
2891		$this->send_command($command);
2892		$result = $this->get_response(false, true);
2893
2894		if($result[0][1] === 'NO'){
2895			return false;
2896		}
2897
2898		$vals = array_shift($result);
2899
2900		$status = array('unseen'=>0, 'messages'=>0);
2901
2902		$lastProp=false;
2903		foreach ($vals as $v) {
2904			if ($v == '(') {
2905				$flag = true;
2906			}
2907			elseif ($v == ')') {
2908				break;
2909			}
2910			else {
2911				if($lastProp=='MESSAGES'){
2912					$status['messages']=intval($v);
2913				}elseif($lastProp=='UNSEEN'){
2914					$status['unseen']=intval($v);
2915				}
2916			}
2917
2918			$lastProp=$v;
2919		}
2920
2921		return $status;
2922	}
2923
2924	/**
2925	 * End's a line by line append operation
2926	 *
2927	 * @return <type>
2928	 */
2929	public function append_end() {
2930		$result = $this->get_response(false, true);
2931		return  $this->check_response($result, true);
2932		/*if($status){
2933			return !empty($this->selected_mailbox['uidnext']) ? $this->selected_mailbox['uidnext'] : true;
2934		}*/
2935	}
2936
2937	/**
2938	 * Feed data when after append_start is called to start an append operation
2939	 *
2940	 * @param <type> $string
2941	 * @return <type>
2942	 */
2943	public function append_feed($string) {
2944		return fwrite($this->handle, $string);
2945	}
2946
2947	/**
2948	 * Start an append operation. Data can be fed line by line with append_feed
2949	 * after this function is called.
2950	 *
2951	 * @param <type> $mailbox
2952	 * @param <type> $size
2953	 * @param <type> $flags
2954	 * @return <type>
2955	 */
2956	public function append_start($mailbox, $size, $flags = "") {
2957		//Select mailbox first so we can predict the UID.
2958		$this->select_mailbox($mailbox);
2959
2960		$this->clean($mailbox, 'mailbox');
2961		$this->clean($size, 'uid');
2962		$command = 'APPEND "'.$this->utf7_encode($mailbox).'" ('.$flags.') {'.$size."}\r\n";
2963		$this->send_command($command);
2964		$result = fgets($this->handle);
2965		if (substr($result, 0, 1) == '+') {
2966			return true;
2967		}
2968		else {
2969			return false;
2970		}
2971	}
2972
2973	/**
2974	 * Append a message to a mailbox
2975	 *
2976	 * @param StringHelper $mailbox
2977	 * @param StringHelper|\Swift_Message $data
2978	 * @param StringHelper $flags See set_message_flag
2979	 * @return boolean
2980	 */
2981	public function append_message($mailbox, $data, $flags=""){
2982
2983
2984		if($data instanceof \Swift_Message){
2985
2986			$tmpfile = \GO\Base\Fs\File::tempFile();
2987
2988			$is = new \Swift_ByteStream_FileByteStream($tmpfile->path(), true);
2989			$data->toByteStream($is);
2990
2991			unset($data);
2992			unset($is);
2993
2994
2995			if(!$this->append_start($mailbox, $tmpfile->size(), $flags))
2996				return false;
2997
2998			$fp = fopen($tmpfile->path(), 'r');
2999
3000			while($line = fgets($fp, 1024)){
3001				if(!$this->append_feed($line))
3002					return false;
3003			}
3004
3005			fclose($fp);
3006			$tmpfile->delete();
3007		}else
3008		{
3009			if(!$this->append_start($mailbox, strlen($data), $flags))
3010				return false;
3011
3012			if(!$this->append_feed($data))
3013				return false;
3014		}
3015
3016		$this->append_feed("\r\n");
3017
3018		return $this->append_end();
3019	}
3020
3021
3022	/**
3023	 * Extract uuencoded attachment from a text/plain body. Some mail clients
3024	 * embed attachments in the text body. This function will take them out and
3025	 * retrn them in an array.
3026	 *
3027	 * @param <type> $body
3028	 * @return <type>
3029	 *
3030	 */
3031	public function extract_uuencoded_attachments(&$body)
3032	{
3033		$body = str_replace("\r", '', $body);
3034		$regex = "/(begin ([0-7]{3}) (.+))\n(.+)\nend/Us";
3035
3036		preg_match_all($regex, $body, $matches);
3037
3038		$attachments = array();
3039
3040		for ($i = 0; $i < count($matches[3]); $i++) {
3041				$boundary	= $matches[1][$i];
3042				$fileperm	= $matches[2][$i];
3043				$filename	= $matches[3][$i];
3044
3045				$size = strlen($matches[4][$i]);
3046
3047				$mime = File::get_mime($matches[3][$i]);
3048				$ct = explode('/', $mime);
3049				$attachments[]=array(
3050					'boundary'=>$matches[1][$i],
3051					'permissions'=>$matches[2][$i],
3052					'name'=>$matches[3][$i],
3053					'data'=>$matches[4][$i],
3054					'disposition'=>'ATTACHMENT',
3055					'encoding'=>'',
3056					'type'=>$ct[0],
3057					'subtype'=>$ct[1],
3058					'size'=>$size,
3059					'human_size'=>Number::format_size($size)
3060				);
3061		}
3062
3063    //remove it from the body.
3064    $body = preg_replace($regex, "", $body);
3065    //\GO::debug($body);
3066
3067		return $attachments;
3068	}
3069}
3070