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