1<?php 2 3/** 4 * IMAP libs 5 * @package modules 6 * @subpackage imap 7 */ 8 9require_once('hm-imap-base.php'); 10require_once('hm-imap-parser.php'); 11require_once('hm-imap-cache.php'); 12require_once('hm-imap-bodystructure.php'); 13require_once('hm-jmap.php'); 14 15/** 16 * IMAP connection manager 17 * @subpackage imap/lib 18 */ 19class Hm_IMAP_List { 20 21 use Hm_Server_List; 22 23 public static $use_cache = true; 24 25 public static function service_connect($id, $server, $user, $pass, $cache=false) { 26 if (array_key_exists('type', $server) && $server['type'] == 'jmap') { 27 self::$server_list[$id]['object'] = new Hm_JMAP(); 28 } 29 else { 30 self::$server_list[$id]['object'] = new Hm_IMAP(); 31 } 32 if (self::$use_cache && $cache && is_array($cache)) { 33 self::$server_list[$id]['object']->load_cache($cache, 'array'); 34 } 35 $config = array( 36 'server' => $server['server'], 37 'port' => $server['port'], 38 'tls' => $server['tls'], 39 'type' => array_key_exists('type', $server) ? $server['type'] : 'imap', 40 'username' => $user, 41 'password' => $pass, 42 'use_cache' => self::$use_cache 43 ); 44 if (array_key_exists('auth', $server)) { 45 $config['auth'] = $server['auth']; 46 } 47 return self::$server_list[$id]['object']->connect($config); 48 } 49 50 public static function get_cache($hm_cache, $id) { 51 if (!self::$use_cache) { 52 return false; 53 } 54 $res = $hm_cache->get('imap'.$id); 55 return $res; 56 } 57} 58 59/* for testing */ 60if (!class_exists('Hm_IMAP')) { 61 62/** 63 * public interface to IMAP commands 64 * @subpackage imap/lib 65 */ 66class Hm_IMAP extends Hm_IMAP_Cache { 67 68 /* config */ 69 70 /* maximum characters to read in from a request */ 71 public $max_read = false; 72 73 /* SSL connection knobs */ 74 public $verify_peer_name = false; 75 public $verify_peer = false; 76 77 /* IMAP server IP address or hostname */ 78 public $server = '127.0.0.1'; 79 80 /* IP port to connect to. Standard port is 143, TLS is 993 */ 81 public $port = 143; 82 83 /* enable TLS when connecting to the IMAP server */ 84 public $tls = false; 85 86 /* don't change the account state in any way */ 87 public $read_only = false; 88 89 /* convert folder names to utf7 */ 90 public $utf7_folders = true; 91 92 /* defaults to LOGIN, CRAM-MD5 also supported but experimental */ 93 public $auth = false; 94 95 /* search character set to use. can be US-ASCII, UTF-8, or '' */ 96 public $search_charset = ''; 97 98 /* sort responses can _probably_ be parsed quickly. This is non-conformant however */ 99 public $sort_speedup = true; 100 101 /* use built in caching. strongly recommended */ 102 public $use_cache = true; 103 104 /* limit LIST/LSUB responses to this many characters */ 105 public $folder_max = 50000; 106 107 /* number of commands and responses to keep in memory. */ 108 public $max_history = 1000; 109 110 /* default IMAP folder delimiter. Only used if NAMESPACE is not supported */ 111 public $default_delimiter = '/'; 112 113 /* defailt IMAP mailbox prefix. Only used if NAMESPACE is not supported */ 114 public $default_prefix = ''; 115 116 /* list of supported IMAP extensions to ignore */ 117 public $blacklisted_extensions = array(); 118 119 /* maximum number of IMAP commands to cache */ 120 public $cache_limit = 100; 121 122 /* query the server for it's CAPABILITY response */ 123 public $no_caps = false; 124 125 /* server type */ 126 public $server_type = 'IMAP'; 127 128 /* IMAP ID client information */ 129 public $app_name = 'Hm_IMAP'; 130 public $app_version = '3.0'; 131 public $app_vendor = 'Hastymail Development Group'; 132 public $app_support_url = 'http://hastymail.org/contact_us/'; 133 134 /* connect error info */ 135 public $con_error_msg = ''; 136 public $con_error_num = 0; 137 138 /* holds information about the currently selected mailbox */ 139 public $selected_mailbox = false; 140 141 /* special folders defined by the IMAP SPECIAL-USE extension */ 142 public $special_use_mailboxes = array( 143 '\All' => false, 144 '\Archive' => false, 145 '\Drafts' => false, 146 '\Flagged' => false, 147 '\Junk' => false, 148 '\Sent' => false, 149 '\Trash' => false 150 ); 151 152 /* holds the current IMAP connection state */ 153 private $state = 'disconnected'; 154 155 /* used for message part content streaming */ 156 private $stream_size = 0; 157 158 /* current selected mailbox status */ 159 public $folder_state = false; 160 161 /** 162 * constructor 163 */ 164 public function __construct() { 165 } 166 167 /* ------------------ CONNECT/AUTH ------------------------------------- */ 168 169 /** 170 * connect to the imap server 171 * @param array $config list of configuration options for this connections 172 * @return bool true on connection sucess 173 */ 174 public function connect($config) { 175 if (isset($config['username']) && isset($config['password'])) { 176 $this->commands = array(); 177 $this->debug = array(); 178 $this->capability = false; 179 $this->responses = array(); 180 $this->current_command = false; 181 $this->apply_config($config); 182 if ($this->tls) { 183 $this->server = 'tls://'.$this->server; 184 } 185 else { 186 $this->server = 'tcp://'.$this->server; 187 } 188 $this->debug[] = 'Connecting to '.$this->server.' on port '.$this->port; 189 $ctx = stream_context_create(); 190 191 stream_context_set_option($ctx, 'ssl', 'verify_peer_name', $this->verify_peer_name); 192 stream_context_set_option($ctx, 'ssl', 'verify_peer', $this->verify_peer); 193 194 $timeout = 10; 195 $this->handle = Hm_Functions::stream_socket_client($this->server, $this->port, $errorno, $errorstr, $timeout, STREAM_CLIENT_CONNECT, $ctx); 196 if (is_resource($this->handle)) { 197 $this->debug[] = 'Successfully opened port to the IMAP server'; 198 $this->state = 'connected'; 199 return $this->authenticate($config['username'], $config['password']); 200 } 201 else { 202 $this->debug[] = 'Could not connect to the IMAP server'; 203 $this->debug[] = 'fsockopen errors #'.$errorno.'. '.$errorstr; 204 $this->con_error_msg = $errorstr; 205 $this->con_error_num = $errorno; 206 return false; 207 } 208 } 209 else { 210 $this->debug[] = 'username and password must be set in the connect() config argument'; 211 return false; 212 } 213 } 214 215 /** 216 * close the IMAP connection 217 * @return void 218 */ 219 public function disconnect() { 220 $command = "LOGOUT\r\n"; 221 $this->state = 'disconnected'; 222 $this->selected_mailbox = false; 223 $this->send_command($command); 224 $result = $this->get_response(); 225 if (is_resource($this->handle)) { 226 fclose($this->handle); 227 } 228 } 229 230 /** 231 * authenticate the username/password 232 * @param string $username IMAP login name 233 * @param string $password IMAP password 234 * @return bool true on sucessful login 235 */ 236 public function authenticate($username, $password) { 237 $this->get_capability(); 238 if (!$this->tls) { 239 $this->starttls(); 240 } 241 switch (strtolower($this->auth)) { 242 243 case 'cram-md5': 244 $this->banner = $this->fgets(1024); 245 $cram1 = 'AUTHENTICATE CRAM-MD5'."\r\n"; 246 $this->send_command($cram1); 247 $response = $this->get_response(); 248 $challenge = base64_decode(substr(trim($response[0]), 1)); 249 $pass = str_repeat(chr(0x00), (64-strlen($password))); 250 $ipad = str_repeat(chr(0x36), 64); 251 $opad = str_repeat(chr(0x5c), 64); 252 $digest = bin2hex(pack("H*", md5(($pass ^ $opad).pack("H*", md5(($pass ^ $ipad).$challenge))))); 253 $challenge_response = base64_encode($username.' '.$digest); 254 fputs($this->handle, $challenge_response."\r\n"); 255 break; 256 case 'xoauth2': 257 $challenge = 'user='.$username.chr(1).'auth=Bearer '.$password.chr(1).chr(1); 258 $command = 'AUTHENTICATE XOAUTH2 '.base64_encode($challenge)."\r\n"; 259 $this->send_command($command); 260 break; 261 default: 262 $login = 'LOGIN "'.str_replace(array('\\', '"'), array('\\\\', '\"'), $username).'" "'.str_replace(array('\\', '"'), array('\\\\', '\"'), $password). "\"\r\n"; 263 $this->send_command($login); 264 break; 265 } 266 $res = $this->get_response(); 267 $authed = false; 268 if (is_array($res) && !empty($res)) { 269 $response = array_pop($res); 270 if (!$this->auth) { 271 if (isset($res[1])) { 272 $this->banner = $res[1]; 273 } 274 if (isset($res[0])) { 275 $this->banner = $res[0]; 276 } 277 } 278 if (stristr($response, 'A'.$this->command_count.' OK')) { 279 $authed = true; 280 $this->state = 'authenticated'; 281 } 282 elseif (strtolower($this->auth) == 'xoauth2' && preg_match("/^\+ ([a-zA-Z0-9=]+)$/", $response, $matches)) { 283 $this->send_command("\r\n", true); 284 $this->get_response(); 285 } 286 } 287 if ($authed) { 288 $this->debug[] = 'Logged in successfully as '.$username; 289 $this->get_capability(); 290 $this->enable(); 291 //$this->enable_compression(); 292 } 293 else { 294 $this->debug[] = 'Log in for '.$username.' FAILED'; 295 } 296 return $authed; 297 } 298 299 /** 300 * attempt starttls 301 * @return void 302 */ 303 public function starttls() { 304 if ($this->is_supported('STARTTLS')) { 305 $command = "STARTTLS\r\n"; 306 $this->send_command($command); 307 $response = $this->get_response(); 308 if (!empty($response)) { 309 $end = array_pop($response); 310 if (substr($end, 0, strlen('A'.$this->command_count.' OK')) == 'A'.$this->command_count.' OK') { 311 Hm_Functions::stream_socket_enable_crypto($this->handle, get_tls_stream_type()); 312 } 313 else { 314 $this->debug[] = 'Unexpected results from STARTTLS: '.implode(' ', $response); 315 } 316 } 317 else { 318 $this->debug[] = 'No response from STARTTLS command'; 319 } 320 } 321 } 322 323 /* ------------------ UNSELECTED STATE COMMANDS ------------------------ */ 324 325 /** 326 * fetch IMAP server capability response 327 * @return string capability response 328 */ 329 public function get_capability() { 330 if (!$this->no_caps) { 331 $command = "CAPABILITY\r\n"; 332 $this->send_command($command); 333 $response = $this->get_response(); 334 foreach ($response as $line) { 335 if (stristr($line, '* CAPABILITY')) { 336 $this->capability = $line; 337 break; 338 } 339 } 340 $this->debug['CAPS'] = $this->capability; 341 $this->parse_extensions_from_capability(); 342 } 343 return $this->capability; 344 } 345 346 /** 347 * special version of LIST to return just special use mailboxes 348 * @param string $type type of special folder to return (sent, all, trash, flagged, junk) 349 * @return array list of special use folders 350 */ 351 public function get_special_use_mailboxes($type=false) { 352 $folders = array(); 353 $types = array('trash', 'sent', 'flagged', 'all', 'junk'); 354 $command = 'LIST (SPECIAL-USE) "" "*"'."\r\n"; 355 $this->send_command($command); 356 $res = $this->get_response(false, true); 357 foreach ($res as $row) { 358 foreach ($row as $atom) { 359 if (in_array(strtolower(substr($atom, 1)), $types, true)) { 360 $folder = array_pop($row); 361 $name = strtolower(substr($atom, 1)); 362 if ($type && $type == $name) { 363 return array($name => $folder); 364 } 365 $folders[$name] = $folder; 366 break; 367 } 368 } 369 } 370 return $folders; 371 } 372 373 /** 374 * get a list of mailbox folders 375 * @param bool $lsub flag to limit results to subscribed folders only 376 * @return array associative array of folder details 377 */ 378 public function get_mailbox_list($lsub=false, $mailbox='', $keyword='*') { 379 /* defaults */ 380 $folders = array(); 381 $excluded = array(); 382 $parents = array(); 383 $delim = false; 384 $inbox = false; 385 $commands = $this->build_list_commands($lsub, $mailbox, $keyword); 386 $cache_command = implode('', array_map(function($v) { return $v[0]; }, $commands)).(string)$mailbox.(string)$keyword; 387 $cache = $this->check_cache($cache_command); 388 if ($cache !== false) { 389 return $cache; 390 } 391 392 foreach($commands as $vals) { 393 $command = $vals[0]; 394 $namespace = $vals[1]; 395 396 $this->send_command($command); 397 $result = $this->get_response($this->folder_max, true); 398 399 /* loop through the "parsed" response. Each iteration is one folder */ 400 foreach ($result as $vals) { 401 402 if (in_array('STATUS', $vals)) { 403 $status_values = $this->parse_status_response(array($vals)); 404 $this->check_mailbox_state_change($status_values); 405 continue; 406 } 407 /* break at the end of the list */ 408 if (!isset($vals[0]) || $vals[0] == 'A'.$this->command_count) { 409 continue; 410 } 411 412 /* defaults */ 413 $flags = false; 414 $flag = false; 415 $delim_flag = false; 416 $parent = ''; 417 $base_name = ''; 418 $folder_parts = array(); 419 $no_select = false; 420 $can_have_kids = true; 421 $has_kids = false; 422 $marked = false; 423 $folder_sort_by = 'ARRIVAL'; 424 $check_for_new = false; 425 426 /* full folder name, includes an absolute path of parent folders */ 427 $folder = $this->utf7_decode($vals[(count($vals) - 1)]); 428 429 /* sometimes LIST responses have dupes */ 430 if (isset($folders[$folder]) || !$folder) { 431 continue; 432 } 433 434 /* folder flags */ 435 foreach ($vals as $v) { 436 if ($v == '(') { 437 $flag = true; 438 } 439 elseif ($v == ')') { 440 $flag = false; 441 $delim_flag = true; 442 } 443 else { 444 if ($flag) { 445 $flags .= ' '.$v; 446 } 447 if ($delim_flag && !$delim) { 448 $delim = $v; 449 $delim_flag = false; 450 } 451 } 452 } 453 454 /* get each folder name part of the complete hierarchy */ 455 $folder_parts = array(); 456 if ($delim && strstr($folder, $delim)) { 457 $temp_parts = explode($delim, $folder); 458 foreach ($temp_parts as $g) { 459 if (trim($g)) { 460 $folder_parts[] = $g; 461 } 462 } 463 } 464 else { 465 $folder_parts[] = $folder; 466 } 467 468 /* get the basename part of the folder name. For a folder named "inbox.sent.march" 469 * with a delimiter of "." the basename would be "march" */ 470 $base_name = $folder_parts[(count($folder_parts) - 1)]; 471 472 /* determine the parent folder basename if it exists */ 473 if (isset($folder_parts[(count($folder_parts) - 2)])) { 474 $parent = implode($delim, array_slice($folder_parts, 0, -1)); 475 if ($parent.$delim == $namespace) { 476 $parent = ''; 477 } 478 } 479 480 /* special use mailbox extension */ 481 if ($this->is_supported('SPECIAL-USE')) { 482 $special = false; 483 foreach ($this->special_use_mailboxes as $name => $value) { 484 if (stristr($flags, $name)) { 485 $special = $name; 486 } 487 } 488 if ($special) { 489 $this->special_use_mailboxes[$special] = $folder; 490 } 491 } 492 493 /* build properties from the flags string */ 494 if (stristr($flags, 'marked')) { 495 $marked = true; 496 } 497 if (stristr($flags, 'noinferiors')) { 498 $can_have_kids = false; 499 } 500 if (($folder == $namespace && $namespace) || stristr($flags, 'hashchildren') || stristr($flags, 'haschildren')) { 501 $has_kids = true; 502 } 503 if ($folder != 'INBOX' && $folder != $namespace && stristr($flags, 'noselect')) { 504 $no_select = true; 505 } 506 507 /* store the results in the big folder list struct */ 508 if (strtolower($folder) == 'inbox') { 509 $inbox = true; 510 } 511 $folders[$folder] = array('parent' => $parent, 'delim' => $delim, 'name' => $folder, 512 'name_parts' => $folder_parts, 'basename' => $base_name, 513 'realname' => $folder, 'namespace' => $namespace, 'marked' => $marked, 514 'noselect' => $no_select, 'can_have_kids' => $can_have_kids, 515 'has_kids' => $has_kids); 516 517 /* store a parent list used below */ 518 if ($parent && !in_array($parent, $parents)) { 519 $parents[$parent][] = $folders[$folder]; 520 } 521 } 522 } 523 524 /* ALL account need an inbox. If we did not find one manually add it to the results */ 525 if (!$inbox && !$mailbox ) { 526 $folders = array_merge(array('INBOX' => array( 527 'name' => 'INBOX', 'basename' => 'INBOX', 'realname' => 'INBOX', 'noselect' => false, 528 'parent' => false, 'has_kids' => false, 'name_parts' => array(), 'delim' => $delim)), $folders); 529 } 530 531 /* sort and return the list */ 532 uksort($folders, array($this, 'fsort')); 533 return $this->cache_return_val($folders, $cache_command); 534 } 535 536 /** 537 * Sort a folder list with the inbox at the top 538 */ 539 function fsort($a, $b) { 540 if (strtolower($a) == 'inbox') { 541 return -1; 542 } 543 if (strtolower($b) == 'inbox') { 544 return 1; 545 } 546 return strcasecmp($a, $b); 547 } 548 549 /** 550 * get IMAP folder namespaces 551 * @return array list of available namespace details 552 */ 553 public function get_namespaces() { 554 if (!$this->is_supported('NAMESPACE')) { 555 return array(array( 556 'prefix' => $this->default_prefix, 557 'delim' => $this->default_delimiter, 558 'class' => 'personal' 559 )); 560 } 561 $data = array(); 562 $command = "NAMESPACE\r\n"; 563 $cache = $this->check_cache($command); 564 if ($cache !== false) { 565 return $cache; 566 } 567 $this->send_command("NAMESPACE\r\n"); 568 $res = $this->get_response(); 569 $this->namespace_count = 0; 570 $status = $this->check_response($res); 571 if ($status) { 572 if (preg_match("/\* namespace (\(.+\)|NIL) (\(.+\)|NIL) (\(.+\)|NIL)/i", $res[0], $matches)) { 573 $classes = array(1 => 'personal', 2 => 'other_users', 3 => 'shared'); 574 foreach ($classes as $i => $v) { 575 if (trim(strtoupper($matches[$i])) == 'NIL') { 576 continue; 577 } 578 $list = str_replace(') (', '),(', substr($matches[$i], 1, -1)); 579 $prefix = ''; 580 $delim = ''; 581 foreach (explode(',', $list) as $val) { 582 $val = trim($val, ")(\r\n "); 583 if (strlen($val) == 1) { 584 $delim = $val; 585 $prefix = ''; 586 } 587 else { 588 $delim = substr($val, -1); 589 $prefix = trim(substr($val, 0, -1)); 590 } 591 $this->namespace_count++; 592 $data[] = array('delim' => $delim, 'prefix' => $prefix, 'class' => $v); 593 } 594 } 595 } 596 return $this->cache_return_val($data, $command); 597 } 598 return $data; 599 } 600 601 /** 602 * select a mailbox 603 * @param string $mailbox the mailbox to attempt to select 604 */ 605 public function select_mailbox($mailbox) { 606 if (isset($this->selected_mailbox['name']) && $this->selected_mailbox['name'] == $mailbox) { 607 return $this->poll(); 608 } 609 $this->folder_state = $this->get_mailbox_status($mailbox); 610 $box = $this->utf7_encode(str_replace('"', '\"', $mailbox)); 611 if (!$this->is_clean($box, 'mailbox')) { 612 return false; 613 } 614 if (!$this->read_only) { 615 $command = "SELECT \"$box\""; 616 } 617 else { 618 $command = "EXAMINE \"$box\""; 619 } 620 if ($this->is_supported('QRESYNC')) { 621 $command .= $this->build_qresync_params(); 622 } 623 elseif ($this->is_supported('CONDSTORE')) { 624 $command .= ' (CONDSTORE)'; 625 } 626 $cached_state = $this->check_cache($command); 627 $this->send_command($command."\r\n"); 628 $res = $this->get_response(false, true); 629 $status = $this->check_response($res, true); 630 $result = array(); 631 if ($status) { 632 list($qresync, $attributes) = $this->parse_untagged_responses($res); 633 if (!$qresync) { 634 $this->check_mailbox_state_change($attributes, $cached_state, $mailbox); 635 } 636 else { 637 $this->debug[] = sprintf('Cache bust avoided on %s with QRESYNC!', $this->selected_mailbox['name']); 638 } 639 $result = array( 640 'selected' => $status, 641 'uidvalidity' => $attributes['uidvalidity'], 642 'exists' => $attributes['exists'], 643 'first_unseen' => $attributes['unseen'], 644 'uidnext' => $attributes['uidnext'], 645 'flags' => $attributes['flags'], 646 'permanentflags' => $attributes['pflags'], 647 'recent' => $attributes['recent'], 648 'nomodseq' => $attributes['nomodseq'], 649 'modseq' => $attributes['modseq'], 650 ); 651 $this->state = 'selected'; 652 $this->selected_mailbox = array('name' => $box, 'detail' => $result); 653 return $this->cache_return_val($result, $command); 654 655 } 656 return $result; 657 } 658 659 /** 660 * issue IMAP status command on a mailbox 661 * @param string $mailbox IMAP mailbox to check 662 * @param array $args list of properties to fetch 663 * @return array list of attribute values discovered 664 */ 665 public function get_mailbox_status($mailbox, $args=array('UNSEEN', 'UIDVALIDITY', 'UIDNEXT', 'MESSAGES', 'RECENT')) { 666 $command = 'STATUS "'.$this->utf7_encode($mailbox).'" ('.implode(' ', $args).")\r\n"; 667 $this->send_command($command); 668 $attributes = array(); 669 $response = $this->get_response(false, true); 670 if ($this->check_response($response, true)) { 671 $attributes = $this->parse_status_response($response); 672 $this->check_mailbox_state_change($attributes); 673 } 674 return $attributes; 675 } 676 677 /* ------------------ SELECTED STATE COMMANDS -------------------------- */ 678 679 /** 680 * use IMAP NOOP to poll for untagged server messages 681 * @return bool 682 */ 683 public function poll() { 684 $command = "NOOP\r\n"; 685 $this->send_command($command); 686 $res = $this->get_response(false, true); 687 if ($this->check_response($res, true)) { 688 list($qresync, $attributes) = $this->parse_untagged_responses($res); 689 if (!$qresync) { 690 $this->check_mailbox_state_change($attributes); 691 } 692 else { 693 $this->debug[] = sprintf('Cache bust avoided on %s with QRESYNC!', $this->selected_mailbox['name']); 694 } 695 return true; 696 } 697 return false; 698 } 699 700 /** 701 * return a header list for the supplied message uids 702 * @todo refactor. abstract header line continuation parsing for re-use 703 * @param mixed $uids an array of uids or a valid IMAP sequence set as a string 704 * @param bool $raw flag to disable decoding header values 705 * @return array list of headers and values for the specified uids 706 */ 707 public function get_message_list($uids, $raw=false) { 708 if (is_array($uids)) { 709 sort($uids); 710 $sorted_string = implode(',', $uids); 711 } 712 else { 713 $sorted_string = $uids; 714 } 715 if (!$this->is_clean($sorted_string, 'uid_list')) { 716 return array(); 717 } 718 $command = 'UID FETCH '.$sorted_string.' (FLAGS INTERNALDATE RFC822.SIZE '; 719 if ($this->is_supported( 'X-GM-EXT-1' )) { 720 $command .= 'X-GM-MSGID X-GM-THRID X-GM-LABELS '; 721 } 722 $command .= "BODY.PEEK[HEADER.FIELDS (SUBJECT X-AUTO-BCC FROM DATE CONTENT-TYPE X-PRIORITY TO LIST-ARCHIVE REFERENCES MESSAGE-ID)])\r\n"; 723 $cache_command = $command.(string)$raw; 724 $cache = $this->check_cache($cache_command); 725 if ($cache !== false) { 726 return $cache; 727 } 728 $this->send_command($command); 729 $res = $this->get_response(false, true); 730 $status = $this->check_response($res, true); 731 $tags = array('X-GM-MSGID' => 'google_msg_id', 'X-GM-THRID' => 'google_thread_id', 'X-GM-LABELS' => 'google_labels', 'UID' => 'uid', 'FLAGS' => 'flags', 'RFC822.SIZE' => 'size', 'INTERNALDATE' => 'internal_date'); 732 $junk = array('X-AUTO-BCC', 'MESSAGE-ID', 'REFERENCES', 'LIST-ARCHIVE', 'SUBJECT', 'FROM', 'CONTENT-TYPE', 'TO', '(', ')', ']', 'X-PRIORITY', 'DATE'); 733 $flds = array('x-auto-bcc' => 'x_auto_bcc', 'message-id' => 'message_id', 'references' => 'references', 'list-archive' => 'list_archive', 'date' => 'date', 'from' => 'from', 'to' => 'to', 'subject' => 'subject', 'content-type' => 'content_type', 'x-priority' => 'x_priority'); 734 $headers = array(); 735 foreach ($res as $n => $vals) { 736 if (isset($vals[0]) && $vals[0] == '*') { 737 $uid = 0; 738 $size = 0; 739 $subject = ''; 740 $list_archive = ''; 741 $from = ''; 742 $references = ''; 743 $date = ''; 744 $message_id = ''; 745 $x_priority = 0; 746 $content_type = ''; 747 $to = ''; 748 $flags = ''; 749 $internal_date = ''; 750 $google_msg_id = ''; 751 $google_thread_id = ''; 752 $google_labels = ''; 753 $x_auto_bcc = ''; 754 $count = count($vals); 755 for ($i=0;$i<$count;$i++) { 756 if ($vals[$i] == 'BODY[HEADER.FIELDS') { 757 $i++; 758 while(isset($vals[$i]) && in_array(strtoupper($vals[$i]), $junk)) { 759 $i++; 760 } 761 $last_header = false; 762 $lines = explode("\r\n", $vals[$i]); 763 foreach ($lines as $line) { 764 $header = strtolower(substr($line, 0, strpos($line, ':'))); 765 if (!$header || (!isset($flds[$header]) && $last_header)) { 766 ${$flds[$last_header]} .= str_replace("\t", " ", $line); 767 } 768 elseif (isset($flds[$header])) { 769 ${$flds[$header]} = substr($line, (strpos($line, ':') + 1)); 770 $last_header = $header; 771 } 772 } 773 } 774 elseif (isset($tags[strtoupper($vals[$i])])) { 775 if (isset($vals[($i + 1)])) { 776 if (($tags[strtoupper($vals[$i])] == 'flags' || $tags[strtoupper($vals[$i])] == 'google_labels' ) && $vals[$i + 1] == '(') { 777 $n = 2; 778 while (isset($vals[$i + $n]) && $vals[$i + $n] != ')') { 779 ${$tags[strtoupper($vals[$i])]} .= $vals[$i + $n]; 780 $n++; 781 } 782 $i += $n; 783 } 784 else { 785 ${$tags[strtoupper($vals[$i])]} = $vals[($i + 1)]; 786 $i++; 787 } 788 } 789 } 790 } 791 if ($uid) { 792 $cset = ''; 793 if (stristr($content_type, 'charset=')) { 794 if (preg_match("/charset\=([^\s;]+)/", $content_type, $matches)) { 795 $cset = trim(strtolower(str_replace(array('"', "'"), '', $matches[1]))); 796 } 797 } 798 $headers[(string) $uid] = array('uid' => $uid, 'flags' => $flags, 'internal_date' => $internal_date, 'size' => $size, 799 'date' => $date, 'from' => $from, 'to' => $to, 'subject' => $subject, 'content-type' => $content_type, 800 'timestamp' => time(), 'charset' => $cset, 'x-priority' => $x_priority, 'google_msg_id' => $google_msg_id, 801 'google_thread_id' => $google_thread_id, 'google_labels' => $google_labels, 'list_archive' => $list_archive, 802 'references' => $references, 'message_id' => $message_id, 'x_auto_bcc' => $x_auto_bcc); 803 804 if ($raw) { 805 $headers[$uid] = array_map('trim', $headers[$uid]); 806 } 807 else { 808 $headers[$uid] = array_map(array($this, 'decode_fld'), $headers[$uid]); 809 } 810 811 } 812 } 813 } 814 if ($status) { 815 return $this->cache_return_val($headers, $cache_command); 816 } 817 else { 818 return $headers; 819 } 820 } 821 822 /** 823 * get the IMAP BODYSTRUCTURE of a message 824 * @param int $uid IMAP UID of the message 825 * @return array message structure represented as a nested array 826 */ 827 public function get_message_structure($uid) { 828 $result = $this->get_raw_bodystructure($uid); 829 if (count($result) == 0) { 830 return $result; 831 } 832 $struct = $this->parse_bodystructure_response($result); 833 return $struct; 834 } 835 836 /** 837 * get the raw IMAP BODYSTRUCTURE response 838 * @param int $uid IMAP UID of the message 839 * @return array low-level parsed message structure 840 */ 841 private function get_raw_bodystructure($uid) { 842 if (!$this->is_clean($uid, 'uid')) { 843 return array(); 844 } 845 $part_num = 1; 846 $struct = array(); 847 $command = "UID FETCH $uid BODYSTRUCTURE\r\n"; 848 $cache = $this->check_cache($command); 849 if ($cache !== false) { 850 return $cache; 851 } 852 $this->send_command($command); 853 $result = $this->get_response(false, true); 854 while (isset($result[0][0]) && isset($result[0][1]) && $result[0][0] == '*' && strtoupper($result[0][1]) == 'OK') { 855 array_shift($result); 856 } 857 $status = $this->check_response($result, true); 858 if (!isset($result[0][4])) { 859 $status = false; 860 } 861 if ($status) { 862 return $this->cache_return_val($result, $command); 863 } 864 return $result; 865 } 866 867 /** 868 * New BODYSTRUCTURE parsing routine 869 * @param array $result low-level IMAP response 870 * @return array 871 */ 872 private function parse_bodystructure_response($result) { 873 $response = array(); 874 if (array_key_exists(6, $result[0]) && strtoupper($result[0][6]) == 'MODSEQ') { 875 $response = array_slice($result[0], 11, -1); 876 } 877 elseif (array_key_exists(4, $result[0]) && strtoupper($result[0][4]) == 'UID') { 878 $response = array_slice($result[0], 7, -1); 879 } 880 else { 881 $response = array_slice($result[0], 5, -1); 882 } 883 884 $this->struct_object = new Hm_IMAP_Struct($response, $this); 885 $struct = $this->struct_object->data(); 886 return $struct; 887 } 888 889 /** 890 * get content for a message part 891 * @param int $uid a single IMAP message UID 892 * @param string $message_part the IMAP message part number 893 * @param bool $raw flag to enabled fetching the entire message as text 894 * @param int $max maximum read length to allow. 895 * @param mixed $struct a message part structure array for decoding and 896 * charset conversion. bool true for auto discovery 897 * @return string message content 898 */ 899 public function get_message_content($uid, $message_part, $max=false, $struct=true) { 900 $message_part = preg_replace("/^0\.{1}/", '', $message_part); 901 if (!$this->is_clean($uid, 'uid')) { 902 return ''; 903 } 904 if ($message_part == 0) { 905 $command = "UID FETCH $uid BODY[]\r\n"; 906 } 907 else { 908 if (!$this->is_clean($message_part, 'msg_part')) { 909 return ''; 910 } 911 $command = "UID FETCH $uid BODY[$message_part]\r\n"; 912 } 913 $cache_command = $command.(string)$max; 914 if ($struct) { 915 $cache_command .= '1'; 916 } 917 $cache = $this->check_cache($cache_command); 918 if ($cache !== false) { 919 return $cache; 920 } 921 $this->send_command($command); 922 $result = $this->get_response($max, true); 923 $status = $this->check_response($result, true); 924 $res = ''; 925 foreach ($result as $vals) { 926 if ($vals[0] != '*') { 927 continue; 928 } 929 $search = true; 930 foreach ($vals as $v) { 931 if ($v != ']' && !$search) { 932 if ($v == 'NIL') { 933 $res = ''; 934 break 2; 935 } 936 $res = trim(preg_replace("/\s*\)$/", '', $v)); 937 break 2; 938 } 939 if (stristr(strtoupper($v), 'BODY')) { 940 $search = false; 941 } 942 } 943 } 944 if ($struct === true) { 945 $full_struct = $this->get_message_structure($uid); 946 $part_struct = $this->search_bodystructure( $full_struct, array('imap_part_number' => $message_part)); 947 if (isset($part_struct[$message_part])) { 948 $struct = $part_struct[$message_part]; 949 } 950 } 951 if (is_array($struct)) { 952 if (isset($struct['encoding']) && $struct['encoding']) { 953 if (strtolower($struct['encoding']) == 'quoted-printable') { 954 $res = quoted_printable_decode($res); 955 } 956 elseif (strtolower($struct['encoding']) == 'base64') { 957 $res = base64_decode($res); 958 } 959 } 960 if (isset($struct['attributes']['charset']) && $struct['attributes']['charset']) { 961 if ($struct['attributes']['charset'] != 'us-ascii') { 962 $res = mb_convert_encoding($res, 'UTF-8', $struct['attributes']['charset']); 963 } 964 } 965 } 966 if ($status) { 967 return $this->cache_return_val($res, $cache_command); 968 } 969 return $res; 970 } 971 972 /** 973 * use IMAP SEARCH or ESEARCH 974 * @param string $target message types to search. can be ALL, UNSEEN, ANSWERED, etc 975 * @param mixed $uids an array of uids or a valid IMAP sequence set as a string (or false for ALL) 976 * @param string $fld optional field to search 977 * @param string $term optional search term 978 * @param bool $exclude_deleted extra argument to exclude messages with the deleted flag 979 * @param bool $exclude_auto_bcc don't include auto-bcc'ed messages 980 * @param bool $only_auto_bcc only include auto-bcc'ed messages 981 * @return array list of IMAP message UIDs that match the search 982 */ 983 public function search($target='ALL', $uids=false, $terms=array(), $esearch=array(), $exclude_deleted=true, $exclude_auto_bcc=true, $only_auto_bcc=false) { 984 if (!$this->is_clean($this->search_charset, 'charset')) { 985 return array(); 986 } 987 if (is_array($target)) { 988 foreach ($target as $val) { 989 if (!$this->is_clean($val, 'keyword')) { 990 return array(); 991 } 992 } 993 $target = implode(' ', $target); 994 } 995 elseif (!$this->is_clean($target, 'keyword')) { 996 return array(); 997 } 998 if (!empty($terms)) { 999 foreach ($terms as $vals) { 1000 if (!$this->is_clean($vals[0], 'search_str') || !$this->is_clean($vals[1], 'search_str')) { 1001 return array(); 1002 } 1003 } 1004 } 1005 if (!empty($uids)) { 1006 if (is_array($uids)) { 1007 $uids = implode(',', $uids); 1008 } 1009 if (!$this->is_clean($uids, 'uid_list')) { 1010 return array(); 1011 } 1012 $uids = 'UID '.$uids; 1013 } 1014 else { 1015 $uids = 'ALL'; 1016 } 1017 if ($this->search_charset) { 1018 $charset = 'CHARSET '.strtoupper($this->search_charset).' '; 1019 } 1020 else { 1021 $charset = ''; 1022 } 1023 if (!empty($terms)) { 1024 $flds = array(); 1025 foreach ($terms as $vals) { 1026 if (substr($vals[1], 0, 4) == 'NOT ') { 1027 $flds[] = 'NOT '.$vals[0].' "'.str_replace('"', '\"', substr($vals[1], 4)).'"'; 1028 } 1029 else { 1030 $flds[] = $vals[0].' "'.str_replace('"', '\"', $vals[1]).'"'; 1031 } 1032 } 1033 $fld = ' '.implode(' ', $flds); 1034 } 1035 else { 1036 $fld = ''; 1037 } 1038 if ($exclude_deleted) { 1039 $fld .= ' NOT DELETED'; 1040 } 1041 if ($only_auto_bcc) { 1042 $fld .= ' HEADER X-Auto-Bcc cypht'; 1043 } 1044 if (!strstr($this->server, 'yahoo') && $exclude_auto_bcc) { 1045 $fld .= ' NOT HEADER X-Auto-Bcc cypht'; 1046 } 1047 $esearch_enabled = false; 1048 $command = 'UID SEARCH '; 1049 if (!empty($esearch) && $this->is_supported('ESEARCH')) { 1050 $valid = array_filter($esearch, function($v) { return in_array($v, array('MIN', 'MAX', 'COUNT', 'ALL')); }); 1051 if (!empty($valid)) { 1052 $esearch_enabled = true; 1053 $command .= 'RETURN ('.implode(' ', $valid).') '; 1054 } 1055 } 1056 $cache_command = $command.$charset.'('.$target.') '.$uids.$fld."\r\n"; 1057 $cache = $this->check_cache($cache_command); 1058 if ($cache !== false) { 1059 return $cache; 1060 } 1061 $command .= $charset.'('.$target.') '.$uids.$fld."\r\n"; 1062 $this->send_command($command); 1063 $result = $this->get_response(false, true); 1064 $status = $this->check_response($result, true); 1065 $res = array(); 1066 $esearch_res = array(); 1067 if ($status) { 1068 array_pop($result); 1069 foreach ($result as $vals) { 1070 if (in_array('ESEARCH', $vals)) { 1071 $esearch_res = $this->parse_esearch_response($vals); 1072 continue; 1073 } 1074 elseif (in_array('SEARCH', $vals)) { 1075 foreach ($vals as $v) { 1076 if (ctype_digit((string) $v)) { 1077 $res[] = $v; 1078 } 1079 } 1080 } 1081 } 1082 if ($esearch_enabled) { 1083 $res = $esearch_res; 1084 } 1085 return $this->cache_return_val($res, $cache_command); 1086 } 1087 return $res; 1088 } 1089 1090 /** 1091 * get the headers for the selected message 1092 * @param int $uid IMAP message UID 1093 * @param string $message_part IMAP message part number 1094 * @return array associate array of message headers 1095 */ 1096 public function get_message_headers($uid, $message_part=false, $raw=false) { 1097 if (!$this->is_clean($uid, 'uid')) { 1098 return array(); 1099 } 1100 if ($message_part == 1 || !$message_part) { 1101 $command = "UID FETCH $uid (FLAGS BODY[HEADER])\r\n"; 1102 } 1103 else { 1104 if (!$this->is_clean($message_part, 'msg_part')) { 1105 return array(); 1106 } 1107 $command = "UID FETCH $uid (FLAGS BODY[$message_part.HEADER])\r\n"; 1108 } 1109 $cache_command = $command.(string)$raw; 1110 $cache = $this->check_cache($cache_command); 1111 if ($cache !== false) { 1112 return $cache; 1113 } 1114 $this->send_command($command); 1115 $result = $this->get_response(false, true); 1116 $status = $this->check_response($result, true); 1117 $headers = array(); 1118 $flags = array(); 1119 if ($status) { 1120 foreach ($result as $vals) { 1121 if ($vals[0] != '*') { 1122 continue; 1123 } 1124 $search = true; 1125 $flag_search = false; 1126 foreach ($vals as $v) { 1127 if ($flag_search) { 1128 if ($v == ')') { 1129 $flag_search = false; 1130 } 1131 elseif ($v == '(') { 1132 continue; 1133 } 1134 else { 1135 $flags[] = $v; 1136 } 1137 } 1138 elseif ($v != ']' && !$search) { 1139 $v = preg_replace("/(?!\r)\n/", "\r\n", $v); 1140 $parts = explode("\r\n", $v); 1141 if (is_array($parts) && !empty($parts)) { 1142 $i = 0; 1143 foreach ($parts as $line) { 1144 $split = strpos($line, ':'); 1145 if (preg_match("/^from /i", $line)) { 1146 continue; 1147 } 1148 if (isset($headers[$i]) && trim($line) && ($line[0] == "\t" || $line[0] == ' ')) { 1149 $headers[$i][1] .= str_replace("\t", " ", $line); 1150 } 1151 elseif ($split) { 1152 $i++; 1153 $last = substr($line, 0, $split); 1154 $headers[$i] = array($last, trim(substr($line, ($split + 1)))); 1155 } 1156 } 1157 } 1158 break; 1159 } 1160 if (stristr(strtoupper($v), 'BODY')) { 1161 $search = false; 1162 } 1163 elseif (stristr(strtoupper($v), 'FLAGS')) { 1164 $flag_search = true; 1165 } 1166 } 1167 } 1168 if (!empty($flags)) { 1169 $headers[] = array('Flags', implode(' ', $flags)); 1170 } 1171 } 1172 $results = array(); 1173 foreach ($headers as $vals) { 1174 if (!$raw) { 1175 $vals[1] = $this->decode_fld($vals[1]); 1176 } 1177 $results[$vals[0]] = $vals[1]; 1178 } 1179 if ($status) { 1180 return $this->cache_return_val($results, $cache_command); 1181 } 1182 return $results; 1183 } 1184 1185 /** 1186 * start streaming a message part. returns the number of characters in the message 1187 * @param int $uid IMAP message UID 1188 * @param string $message_part IMAP message part number 1189 * @return int the size of the message queued up to stream 1190 */ 1191 public function start_message_stream($uid, $message_part) { 1192 if (!$this->is_clean($uid, 'uid')) { 1193 return false; 1194 } 1195 if ($message_part == 0) { 1196 $command = "UID FETCH $uid BODY[]\r\n"; 1197 } 1198 else { 1199 if (!$this->is_clean($message_part, 'msg_part')) { 1200 return false; 1201 } 1202 $command = "UID FETCH $uid BODY[$message_part]\r\n"; 1203 } 1204 $this->send_command($command); 1205 $result = $this->fgets(1024); 1206 $size = false; 1207 if (preg_match("/\{(\d+)\}\r\n/", $result, $matches)) { 1208 $size = $matches[1]; 1209 $this->stream_size = $size; 1210 $this->current_stream_size = 0; 1211 } 1212 return $size; 1213 } 1214 1215 /** 1216 * read a line from a message stream. Called until it returns 1217 * false will "stream" a message part content one line at a time. 1218 * useful for avoiding memory consumption when dealing with large 1219 * attachments 1220 * @param int $size chunk size to read using fgets 1221 * @return string chunk of the streamed message 1222 */ 1223 public function read_stream_line($size=1024) { 1224 if ($this->stream_size) { 1225 $res = $this->fgets(1024); 1226 while(substr($res, -2) != "\r\n") { 1227 $res .= $this->fgets($size); 1228 } 1229 if ($res && $this->check_response(array($res), false, false)) { 1230 $res = false; 1231 } 1232 if ($res) { 1233 $this->current_stream_size += strlen($res); 1234 } 1235 if ($this->current_stream_size >= $this->stream_size) { 1236 $this->stream_size = 0; 1237 } 1238 } 1239 else { 1240 $res = false; 1241 } 1242 return $res; 1243 } 1244 1245 /** 1246 * use FETCH to sort a list of uids when SORT is not available 1247 * @param string $sort the sort field 1248 * @param bool $reverse flag to reverse the results 1249 * @param string $filter IMAP message type (UNSEEN, ANSWERED, DELETED, etc) 1250 * @param string $uid_str IMAP sequence set string or false 1251 * @return array list of UIDs in the sort order 1252 */ 1253 public function sort_by_fetch($sort, $reverse, $filter, $uid_str=false) { 1254 if (!$this->is_clean($sort, 'keyword')) { 1255 return false; 1256 } 1257 if ($uid_str) { 1258 $command1 = 'UID FETCH '.$uid_str.' (FLAGS '; 1259 } 1260 else { 1261 $command1 = 'UID FETCH 1:* (FLAGS '; 1262 } 1263 switch ($sort) { 1264 case 'DATE': 1265 $command2 = "BODY.PEEK[HEADER.FIELDS (DATE)])\r\n"; 1266 $key = "BODY[HEADER.FIELDS"; 1267 break; 1268 case 'SIZE': 1269 $command2 = "RFC822.SIZE)\r\n"; 1270 $key = "RFC822.SIZE"; 1271 break; 1272 case 'TO': 1273 $command2 = "BODY.PEEK[HEADER.FIELDS (TO)])\r\n"; 1274 $key = "BODY[HEADER.FIELDS"; 1275 break; 1276 case 'CC': 1277 $command2 = "BODY.PEEK[HEADER.FIELDS (CC)])\r\n"; 1278 $key = "BODY[HEADER.FIELDS"; 1279 break; 1280 case 'FROM': 1281 $command2 = "BODY.PEEK[HEADER.FIELDS (FROM)])\r\n"; 1282 $key = "BODY[HEADER.FIELDS"; 1283 break; 1284 case 'SUBJECT': 1285 $command2 = "BODY.PEEK[HEADER.FIELDS (SUBJECT)])\r\n"; 1286 $key = "BODY[HEADER.FIELDS"; 1287 break; 1288 case 'ARRIVAL': 1289 default: 1290 $command2 = "INTERNALDATE)\r\n"; 1291 $key = "INTERNALDATE"; 1292 break; 1293 } 1294 $command = $command1.$command2; 1295 $cache_command = $command.(string)$reverse; 1296 $cache = $this->check_cache($cache_command); 1297 if ($cache !== false) { 1298 return $cache; 1299 } 1300 $this->send_command($command); 1301 $res = $this->get_response(false, true); 1302 $status = $this->check_response($res, true); 1303 $uids = array(); 1304 $sort_keys = array(); 1305 foreach ($res as $vals) { 1306 if (!isset($vals[0]) || $vals[0] != '*') { 1307 continue; 1308 } 1309 $uid = 0; 1310 $sort_key = 0; 1311 $body = false; 1312 foreach ($vals as $i => $v) { 1313 if ($body) { 1314 if ($v == ']' && isset($vals[$i + 1])) { 1315 if ($command2 == "BODY.PEEK[HEADER.FIELDS (DATE)]\r\n") { 1316 $sort_key = strtotime(trim(substr($vals[$i + 1], 5))); 1317 } 1318 else { 1319 $sort_key = $vals[$i + 1]; 1320 } 1321 $body = false; 1322 } 1323 } 1324 if (strtoupper($v) == 'FLAGS') { 1325 $index = $i + 2; 1326 $flag_string = ''; 1327 while (isset($vals[$index]) && $vals[$index] != ')') { 1328 $flag_string .= $vals[$index]; 1329 $index++; 1330 } 1331 if ($filter && $filter != 'ALL' && !$this->flag_match($filter, $flag_string)) { 1332 continue 2; 1333 } 1334 } 1335 if (strtoupper($v) == 'UID') { 1336 if (isset($vals[($i + 1)])) { 1337 $uid = $vals[$i + 1]; 1338 } 1339 } 1340 if ($key == strtoupper($v)) { 1341 if (substr($key, 0, 4) == 'BODY') { 1342 $body = 1; 1343 } 1344 elseif (isset($vals[($i + 1)])) { 1345 if ($key == "INTERNALDATE") { 1346 $sort_key = strtotime($vals[$i + 1]); 1347 } 1348 else { 1349 $sort_key = $vals[$i + 1]; 1350 } 1351 } 1352 } 1353 } 1354 if ($sort_key && $uid) { 1355 $sort_keys[$uid] = $sort_key; 1356 $uids[] = $uid; 1357 } 1358 } 1359 if (count($sort_keys) != count($uids)) { 1360 if (count($sort_keys) < count($uids)) { 1361 foreach ($uids as $v) { 1362 if (!isset($sort_keys[$v])) { 1363 $sort_keys[$v] = false; 1364 } 1365 } 1366 } 1367 } 1368 natcasesort($sort_keys); 1369 $uids = array_keys($sort_keys); 1370 if ($reverse) { 1371 $uids = array_reverse($uids); 1372 } 1373 if ($status) { 1374 return $this->cache_return_val($uids, $cache_command); 1375 } 1376 return $uids; 1377 } 1378 1379 /* ------------------ WRITE COMMANDS ----------------------------------- */ 1380 1381 /** 1382 * delete an existing mailbox 1383 * @param string $mailbox IMAP mailbox name to delete 1384 * 1385 * @return bool tru if the mailbox was deleted 1386 */ 1387 public function delete_mailbox($mailbox) { 1388 if (!$this->is_clean($mailbox, 'mailbox')) { 1389 return false; 1390 } 1391 if ($this->read_only) { 1392 $this->debug[] = 'Delete mailbox not permitted in read only mode'; 1393 return false; 1394 } 1395 $command = 'DELETE "'.str_replace('"', '\"', $this->utf7_encode($mailbox))."\"\r\n"; 1396 $this->send_command($command); 1397 $result = $this->get_response(false); 1398 $status = $this->check_response($result, false); 1399 if ($status) { 1400 return true; 1401 } 1402 else { 1403 $this->debug[] = str_replace('A'.$this->command_count, '', $result[0]); 1404 return false; 1405 } 1406 } 1407 1408 /** 1409 * rename and existing mailbox 1410 * @param string $mailbox IMAP mailbox to rename 1411 * @param string $new_mailbox new name for the mailbox 1412 * @return bool true if the rename operation worked 1413 */ 1414 public function rename_mailbox($mailbox, $new_mailbox) { 1415 if (!$this->is_clean($mailbox, 'mailbox') || !$this->is_clean($new_mailbox, 'mailbox')) { 1416 return false; 1417 } 1418 if ($this->read_only) { 1419 $this->debug[] = 'Rename mailbox not permitted in read only mode'; 1420 return false; 1421 } 1422 $command = 'RENAME "'.$this->utf7_encode($mailbox).'" "'.$this->utf7_encode($new_mailbox).'"'."\r\n"; 1423 $this->send_command($command); 1424 $result = $this->get_response(false); 1425 $status = $this->check_response($result, false); 1426 if ($status) { 1427 return true; 1428 } 1429 else { 1430 $this->debug[] = str_replace('A'.$this->command_count, '', $result[0]); 1431 return false; 1432 } 1433 } 1434 1435 /** 1436 * create a new mailbox 1437 * @param string $mailbox IMAP mailbox name 1438 * @return bool true if the mailbox was created 1439 */ 1440 public function create_mailbox($mailbox) { 1441 if (!$this->is_clean($mailbox, 'mailbox')) { 1442 return false; 1443 } 1444 if ($this->read_only) { 1445 $this->debug[] = 'Create mailbox not permitted in read only mode'; 1446 return false; 1447 } 1448 $command = 'CREATE "'.$this->utf7_encode($mailbox).'"'."\r\n"; 1449 $this->send_command($command); 1450 $result = $this->get_response(false); 1451 $status = $this->check_response($result, false); 1452 if ($status) { 1453 return true; 1454 } 1455 else { 1456 $this->debug[] = str_replace('A'.$this->command_count, '', $result[0]); 1457 return false; 1458 } 1459 } 1460 1461 /** 1462 * perform an IMAP action on a message 1463 * @param string $action action to perform, can be one of READ, UNREAD, FLAG, 1464 * UNFLAG, ANSWERED, DELETE, UNDELETE, EXPUNGE, or COPY 1465 * @param mixed $uids an array of uids or a valid IMAP sequence set as a string 1466 * @param string $mailbox destination IMAP mailbox name for operations the require one 1467 * @param string $keyword optional custom keyword flag 1468 */ 1469 public function message_action($action, $uids, $mailbox=false, $keyword=false) { 1470 $status = false; 1471 $command = false; 1472 $uid_strings = array(); 1473 if (is_array($uids)) { 1474 if (count($uids) > 1000) { 1475 while (count($uids) > 1000) { 1476 $uid_strings[] = implode(',', array_splice($uids, 0, 1000)); 1477 } 1478 if (count($uids)) { 1479 $uid_strings[] = implode(',', $uids); 1480 } 1481 } 1482 else { 1483 $uid_strings[] = implode(',', $uids); 1484 } 1485 } 1486 else { 1487 $uid_strings[] = $uids; 1488 } 1489 foreach ($uid_strings as $uid_string) { 1490 if ($uid_string) { 1491 if (!$this->is_clean($uid_string, 'uid_list')) { 1492 return false; 1493 } 1494 } 1495 switch ($action) { 1496 case 'READ': 1497 $command = "UID STORE $uid_string +FLAGS (\Seen)\r\n"; 1498 break; 1499 case 'FLAG': 1500 $command = "UID STORE $uid_string +FLAGS (\Flagged)\r\n"; 1501 break; 1502 case 'UNFLAG': 1503 $command = "UID STORE $uid_string -FLAGS (\Flagged)\r\n"; 1504 break; 1505 case 'ANSWERED': 1506 $command = "UID STORE $uid_string +FLAGS (\Answered)\r\n"; 1507 break; 1508 case 'UNREAD': 1509 $command = "UID STORE $uid_string -FLAGS (\Seen)\r\n"; 1510 break; 1511 case 'DELETE': 1512 $command = "UID STORE $uid_string +FLAGS (\Deleted)\r\n"; 1513 break; 1514 case 'UNDELETE': 1515 $command = "UID STORE $uid_string -FLAGS (\Deleted)\r\n"; 1516 break; 1517 case 'CUSTOM': 1518 /* TODO: check permanentflags of the selected mailbox to 1519 * make sure custom keywords are supported */ 1520 if ($keyword && $this->is_clean($keyword, 'mailbox')) { 1521 $command = "UID STORE $uid_string +FLAGS ($keyword)\r\n"; 1522 } 1523 break; 1524 case 'EXPUNGE': 1525 $command = "EXPUNGE\r\n"; 1526 break; 1527 case 'COPY': 1528 if (!$this->is_clean($mailbox, 'mailbox')) { 1529 return false; 1530 } 1531 $command = "UID COPY $uid_string \"".$this->utf7_encode($mailbox)."\"\r\n"; 1532 break; 1533 case 'MOVE': 1534 if (!$this->is_clean($mailbox, 'mailbox')) { 1535 return false; 1536 } 1537 if ($this->is_supported('MOVE')) { 1538 $command = "UID MOVE $uid_string \"".$this->utf7_encode($mailbox)."\"\r\n"; 1539 } 1540 else { 1541 if ($this->message_action('COPY', $uids, $mailbox, $keyword)) { 1542 if ($this->message_action('DELETE', $uids, $mailbox, $keyword)) { 1543 $command = "EXPUNGE\r\n"; 1544 } 1545 } 1546 } 1547 break; 1548 } 1549 if ($command) { 1550 $this->send_command($command); 1551 $res = $this->get_response(); 1552 $status = $this->check_response($res); 1553 } 1554 if ($status) { 1555 if (is_array($this->selected_mailbox)) { 1556 $this->bust_cache($this->selected_mailbox['name']); 1557 } 1558 if ($mailbox) { 1559 $this->bust_cache($mailbox); 1560 } 1561 } 1562 } 1563 return $status; 1564 } 1565 1566 /** 1567 * start writing a message to a folder with IMAP APPEND 1568 * @param string $mailbox IMAP mailbox name 1569 * @param int $size size of the message to be written 1570 * @param bool $seen flag to mark the message seen 1571 * $return bool true on success 1572 */ 1573 public function append_start($mailbox, $size, $seen=true) { 1574 if (!$this->is_clean($mailbox, 'mailbox') || !$this->is_clean($size, 'uid')) { 1575 return false; 1576 } 1577 if ($seen) { 1578 $command = 'APPEND "'.$this->utf7_encode($mailbox).'" (\Seen) {'.$size."}\r\n"; 1579 } 1580 else { 1581 $command = 'APPEND "'.$this->utf7_encode($mailbox).'" () {'.$size."}\r\n"; 1582 } 1583 $this->send_command($command); 1584 $result = $this->fgets(); 1585 if (substr($result, 0, 1) == '+') { 1586 return true; 1587 } 1588 else { 1589 return false; 1590 } 1591 } 1592 1593 /** 1594 * write a line to an active IMAP APPEND operation 1595 * @param string $string line to write 1596 * @return int length written 1597 */ 1598 public function append_feed($string) { 1599 return fputs($this->handle, $string); 1600 } 1601 1602 /** 1603 * finish an IMAP APPEND operation 1604 * @return bool true on success 1605 */ 1606 public function append_end() { 1607 $result = $this->get_response(false, true); 1608 return $this->check_response($result, true); 1609 } 1610 1611 /* ------------------ HELPERS ------------------------------------------ */ 1612 1613 /** 1614 * convert a sequence string to an array 1615 * @param string $sequence an IMAP sequence string 1616 * 1617 * @return $array list of ids 1618 */ 1619 public function convert_sequence_to_array($sequence) { 1620 $res = array(); 1621 foreach (explode(',', $sequence) as $atom) { 1622 if (strstr($atom, ':')) { 1623 $markers = explode(':', $atom); 1624 if (ctype_digit($markers[0]) && ctype_digit($markers[1])) { 1625 $res = array_merge($res, range($markers[0], $markers[1])); 1626 } 1627 } 1628 elseif (ctype_digit($atom)) { 1629 $res[] = $atom; 1630 } 1631 } 1632 return array_unique($res); 1633 } 1634 1635 /** 1636 * convert an array into a sequence string 1637 * @param array $array list of ids 1638 * 1639 * @return string an IMAP sequence string 1640 */ 1641 public function convert_array_to_sequence($array) { 1642 $res = ''; 1643 $seq = false; 1644 $max = count($array) - 1; 1645 foreach ($array as $index => $value) { 1646 if (!isset($array[$index - 1])) { 1647 $res .= $value; 1648 } 1649 elseif ($seq) { 1650 $last_val = $array[$index - 1]; 1651 if ($index == $max) { 1652 $res .= $value; 1653 break; 1654 } 1655 elseif ($last_val == $value - 1) { 1656 continue; 1657 } 1658 else { 1659 $res .= $last_val.','.$value; 1660 $seq = false; 1661 } 1662 1663 } 1664 else { 1665 $last_val = $array[$index - 1]; 1666 if ($last_val == $value - 1) { 1667 $seq = true; 1668 $res .= ':'; 1669 } 1670 else { 1671 $res .= ','.$value; 1672 } 1673 } 1674 } 1675 return $res; 1676 } 1677 1678 /** 1679 * decode mail fields to human readable text 1680 * @param string $string field to decode 1681 * @return string decoded field 1682 */ 1683 public function decode_fld($string) { 1684 return decode_fld($string); 1685 } 1686 1687 /** 1688 * check if an IMAP extension is supported by the server 1689 * @param string $extension name of an extension 1690 * @return bool true if the extension is supported 1691 */ 1692 public function is_supported( $extension ) { 1693 return in_array(strtolower($extension), array_diff($this->supported_extensions, $this->blacklisted_extensions)); 1694 } 1695 1696 /** 1697 * returns current IMAP state 1698 * @return string one of: 1699 * disconnected = no IMAP server TCP connection 1700 * connected = an IMAP server TCP connection exists 1701 * authenticated = successfully authenticated to the IMAP server 1702 * selected = a mailbox has been selected 1703 */ 1704 public function get_state() { 1705 return $this->state; 1706 } 1707 1708 /** 1709 * output IMAP session debug info 1710 * @param bool $full flag to enable full IMAP response display 1711 * @param bool $return flag to return the debug results instead of printing them 1712 * @param bool $list flag to return array 1713 * @return void/string 1714 */ 1715 public function show_debug($full=false, $return=false, $list=false) { 1716 if ($list) { 1717 if ($full) { 1718 return array( 1719 'debug' => $this->debug, 1720 'commands' => $this->commands, 1721 'responses' => $this->responses 1722 ); 1723 } 1724 else { 1725 return array_merge($this->debug, $this->commands); 1726 } 1727 } 1728 $res = sprintf("\nDebug %s\n", print_r(array_merge($this->debug, $this->commands), true)); 1729 if ($full) { 1730 $res .= sprintf("Response %s", print_r($this->responses, true)); 1731 } 1732 if (!$return) { 1733 echo $res; 1734 } 1735 return $res; 1736 } 1737 1738 /** 1739 * search a nested BODYSTRUCTURE response for a specific part 1740 * @param array $struct the structure to search 1741 * @param string $search_term the search term 1742 * @param array $search_flds list of fields to search for the term 1743 * @return array array of all matching parts from the message 1744 */ 1745 public function search_bodystructure($struct, $search_flds, $all=true, $res=array()) { 1746 return $this->struct_object->recursive_search($struct, $search_flds, $all, $res); 1747 } 1748 1749 /* ------------------ EXTENSIONS --------------------------------------- */ 1750 1751 /** 1752 * use the IMAP GETQUOTA command to fetch quota information 1753 * @param string $quota_root named quota root to fetch 1754 * @return array list of quota details 1755 */ 1756 public function get_quota($quota_root='') { 1757 $quotas = array(); 1758 if ($this->is_supported('QUOTA')) { 1759 $command = 'GETQUOTA "'.$quota_root."\"\r\n"; 1760 $this->send_command($command); 1761 $res = $this->get_response(false, true); 1762 if ($this->check_response($res, true)) { 1763 foreach($res as $vals) { 1764 list($name, $max, $current) = $this->parse_quota_response($vals); 1765 if ($max) { 1766 $quotas[] = array('name' => $name, 'max' => $max, 'current' => $current); 1767 } 1768 } 1769 } 1770 } 1771 return $quotas; 1772 } 1773 1774 /** 1775 * use the IMAP GETQUOTAROOT command to fetch quota information about a mailbox 1776 * @param string $mailbox IMAP folder to check 1777 * @return array list of quota details 1778 */ 1779 public function get_quota_root($mailbox) { 1780 $quotas = array(); 1781 if ($this->is_supported('QUOTA') && $this->is_clean($mailbox, 'mailbox')) { 1782 $command = 'GETQUOTAROOT "'. $this->utf7_encode($mailbox).'"'."\r\n"; 1783 $this->send_command($command); 1784 $res = $this->get_response(false, true); 1785 if ($this->check_response($res, true)) { 1786 foreach($res as $vals) { 1787 list($name, $max, $current) = $this->parse_quota_response($vals); 1788 if ($max) { 1789 $quotas[] = array('name' => $name, 'max' => $max, 'current' => $current); 1790 } 1791 } 1792 } 1793 } 1794 return $quotas; 1795 } 1796 1797 /** 1798 * use the ENABLE extension to tell the IMAP server what extensions we support 1799 * @return array list of supported extensions that can be enabled 1800 */ 1801 public function enable() { 1802 $extensions = array(); 1803 if ($this->is_supported('ENABLE')) { 1804 $supported = array_diff($this->declared_extensions, $this->blacklisted_extensions); 1805 if ($this->is_supported('QRESYNC')) { 1806 $extension_string = implode(' ', array_filter($supported, function($val) { return $val != 'CONDSTORE'; })); 1807 } 1808 else { 1809 $extension_string = implode(' ', $supported); 1810 } 1811 if (!$extension_string) { 1812 return array(); 1813 } 1814 $command = 'ENABLE '.$extension_string."\r\n"; 1815 $this->send_command($command); 1816 $res = $this->get_response(false, true); 1817 if ($this->check_response($res, true)) { 1818 foreach($res as $vals) { 1819 if (in_array('ENABLED', $vals)) { 1820 $extensions[] = $this->get_adjacent_response_value($vals, -1, 'ENABLED'); 1821 } 1822 } 1823 } 1824 $this->enabled_extensions = $extensions; 1825 $this->debug[] = sprintf("Enabled extensions: ".implode(', ', $extensions)); 1826 } 1827 return $extensions; 1828 } 1829 1830 /** 1831 * unselect the selected mailbox 1832 * @return bool true on success 1833 */ 1834 public function unselect_mailbox() { 1835 $this->send_command("UNSELECT\r\n"); 1836 $res = $this->get_response(false, true); 1837 $status = $this->check_response($res, true); 1838 if ($status) { 1839 $this->selected_mailbox = false; 1840 } 1841 return $status; 1842 } 1843 1844 /** 1845 * use the ID extension 1846 * @return array list of server properties on success 1847 */ 1848 public function id() { 1849 $server_id = array(); 1850 if ($this->is_supported('ID')) { 1851 $params = array( 1852 'name' => $this->app_name, 1853 'version' => $this->app_version, 1854 'vendor' => $this->app_vendor, 1855 'support-url' => $this->app_support_url, 1856 ); 1857 $param_parts = array(); 1858 foreach ($params as $name => $value) { 1859 $param_parts[] = '"'.$name.'" "'.$value.'"'; 1860 } 1861 if (!empty($param_parts)) { 1862 $command = 'ID ('.implode(' ', $param_parts).")\r\n"; 1863 $this->send_command($command); 1864 $result = $this->get_response(false, true); 1865 if ($this->check_response($result, true)) { 1866 foreach ($result as $vals) { 1867 if (in_array('name', $vals)) { 1868 $server_id['name'] = $this->get_adjacent_response_value($vals, -1, 'name'); 1869 } 1870 if (in_array('vendor', $vals)) { 1871 $server_id['vendor'] = $this->get_adjacent_response_value($vals, -1, 'vendor'); 1872 } 1873 if (in_array('version', $vals)) { 1874 $server_id['version'] = $this->get_adjacent_response_value($vals, -1, 'version'); 1875 } 1876 if (in_array('support-url', $vals)) { 1877 $server_id['support-url'] = $this->get_adjacent_response_value($vals, -1, 'support-url'); 1878 } 1879 if (in_array('remote-host', $vals)) { 1880 $server_id['remote-host'] = $this->get_adjacent_response_value($vals, -1, 'remote-host'); 1881 } 1882 } 1883 $this->server_id = $server_id; 1884 $res = true; 1885 } 1886 } 1887 } 1888 return $server_id; 1889 } 1890 1891 /** 1892 * use the SORT extension to get a sorted UID list 1893 * @param string $sort sort order. can be one of ARRIVAL, DATE, CC, TO, SUBJECT, FROM, or SIZE 1894 * @param bool $reverse flag to reverse the sort order 1895 * @param string $filter can be one of ALL, SEEN, UNSEEN, ANSWERED, UNANSWERED, DELETED, UNDELETED, FLAGGED, or UNFLAGGED 1896 * @return array list of IMAP message UIDs 1897 */ 1898 public function get_message_sort_order($sort='ARRIVAL', $reverse=true, $filter='ALL', $esort=array()) { 1899 if (!$this->is_clean($sort, 'keyword') || !$this->is_clean($filter, 'keyword') || !$this->is_supported('SORT')) { 1900 return false; 1901 } 1902 $esort_enabled = false; 1903 $esort_res = array(); 1904 $command = 'UID SORT '; 1905 if (!empty($esort) && $this->is_supported('ESORT')) { 1906 $valid = array_filter($esort, function($v) { return in_array($v, array('MIN', 'MAX', 'COUNT', 'ALL')); }); 1907 if (!empty($valid)) { 1908 $esort_enabled = true; 1909 $command .= 'RETURN ('.implode(' ', $valid).') '; 1910 } 1911 } 1912 $command .= '('.$sort.') US-ASCII '.$filter."\r\n"; 1913 $cache_command = $command.(string)$reverse; 1914 $cache = $this->check_cache($cache_command); 1915 if ($cache !== false) { 1916 return $cache; 1917 } 1918 $this->send_command($command); 1919 if ($this->sort_speedup) { 1920 $speedup = true; 1921 } 1922 else { 1923 $speedup = false; 1924 } 1925 $res = $this->get_response(false, true, 8192, $speedup); 1926 $status = $this->check_response($res, true); 1927 $uids = array(); 1928 foreach ($res as $vals) { 1929 if ($vals[0] == '*' && strtoupper($vals[1]) == 'ESEARCH') { 1930 $esort_res = $this->parse_esearch_response($vals); 1931 } 1932 if ($vals[0] == '*' && strtoupper($vals[1]) == 'SORT') { 1933 array_shift($vals); 1934 array_shift($vals); 1935 $uids = array_merge($uids, $vals); 1936 } 1937 else { 1938 if (ctype_digit((string) $vals[0])) { 1939 $uids = array_merge($uids, $vals); 1940 } 1941 } 1942 } 1943 if ($reverse) { 1944 $uids = array_reverse($uids); 1945 } 1946 if ($esort_enabled) { 1947 $uids = $esort_res; 1948 } 1949 if ($status) { 1950 return $this->cache_return_val($uids, $cache_command); 1951 } 1952 return $uids; 1953 } 1954 1955 /** 1956 * search using the Google X-GM-RAW IMAP extension 1957 * @param string $start_str formatted search string like "has:attachment in:unread" 1958 * @return array list of IMAP UIDs that match the search 1959 */ 1960 public function google_search($search_str) { 1961 $uids = array(); 1962 if ($this->is_supported('X-GM-EXT-1')) { 1963 $search_str = str_replace('"', '', $search_str); 1964 if ($this->is_clean($search_str, 'search_str')) { 1965 $command = "UID SEARCH X-GM-RAW \"".$search_str."\"\r\n"; 1966 $this->send_command($command); 1967 $res = $this->get_response(false, true); 1968 $uids = array(); 1969 foreach ($res as $vals) { 1970 foreach ($vals as $v) { 1971 if (ctype_digit((string) $v)) { 1972 $uids[] = $v; 1973 } 1974 } 1975 } 1976 } 1977 } 1978 return $uids; 1979 } 1980 1981 /** 1982 * attempt enable IMAP COMPRESS extension 1983 * @todo: currently does not work ... 1984 * @return void 1985 */ 1986 public function enable_compression() { 1987 if ($this->is_supported('COMPRESS=DEFLATE')) { 1988 $this->send_command("COMPRESS DEFLATE\r\n"); 1989 $res = $this->get_response(false, true); 1990 if ($this->check_response($res, true)) { 1991 $params = array('level' => 6, 'window' => 15, 'memory' => 9); 1992 stream_filter_prepend($this->handle, 'zlib.inflate', STREAM_FILTER_READ); 1993 stream_filter_append($this->handle, 'zlib.deflate', STREAM_FILTER_WRITE, $params); 1994 $this->debug[] = 'DEFLATE compression extension activated'; 1995 return true; 1996 } 1997 } 1998 return false; 1999 } 2000 2001 /* ------------------ HIGH LEVEL --------------------------------------- */ 2002 2003 /** 2004 * return the formatted message content of the first part that matches the supplied MIME type 2005 * @param int $uid IMAP UID value for the message 2006 * @param string $type Primary MIME type like "text" 2007 * @param string $subtype Secondary MIME type like "plain" 2008 * @return string formatted message content, bool false if no matching part is found 2009 */ 2010 public function get_first_message_part($uid, $type, $subtype=false, $struct=false) { 2011 if (!$subtype) { 2012 $flds = array('type' => $type); 2013 } 2014 else { 2015 $flds = array('type' => $type, 'subtype' => $subtype); 2016 } 2017 if (!$struct) { 2018 $struct = $this->get_message_structure($uid); 2019 } 2020 $matches = $this->search_bodystructure($struct, $flds, false); 2021 if (!empty($matches)) { 2022 2023 $subset = array_slice(array_keys($matches), 0, 1); 2024 $msg_part_num = $subset[0]; 2025 $struct = array_slice($matches, 0, 1); 2026 2027 if (isset($struct[$msg_part_num])) { 2028 $struct = $struct[$msg_part_num]; 2029 } 2030 elseif (isset($struct[0])) { 2031 $struct = $struct[0]; 2032 } 2033 2034 return array($msg_part_num, $this->get_message_content($uid, $msg_part_num, false, $struct)); 2035 } 2036 return array(false, false); 2037 } 2038 2039 /** 2040 * return a list of headers and UIDs for a page of a mailbox 2041 * @param string $mailbox the mailbox to access 2042 * @param string $sort sort order. can be one of ARRIVAL, DATE, CC, TO, SUBJECT, FROM, or SIZE 2043 * @param string $filter type of messages to include (UNSEEN, ANSWERED, ALL, etc) 2044 * @param int $limit max number of messages to return 2045 * @param int $offset offset from the first message in the list 2046 * @param string $keyword optional keyword to filter the results by 2047 * @return array list of headers 2048 */ 2049 2050 public function get_mailbox_page($mailbox, $sort, $rev, $filter, $offset=0, $limit=0, $keyword=false) { 2051 $result = array(); 2052 2053 /* select the mailbox if need be */ 2054 if (!$this->selected_mailbox || $this->selected_mailbox['name'] != $mailbox) { 2055 $this->select_mailbox($mailbox); 2056 } 2057 2058 /* use the SORT extension if we can */ 2059 if ($this->is_supported( 'SORT' )) { 2060 $uids = $this->get_message_sort_order($sort, $rev, $filter); 2061 } 2062 2063 /* fall back to using FETCH and manually sorting */ 2064 else { 2065 $uids = $this->sort_by_fetch($sort, $rev, $filter); 2066 } 2067 if ($keyword) { 2068 $uids = $this->search($filter, $uids, array(array('TEXT', $keyword))); 2069 } 2070 $total = count($uids); 2071 2072 /* reduce to one page */ 2073 if ($limit) { 2074 $uids = array_slice($uids, $offset, $limit, true); 2075 } 2076 2077 /* get the headers and build a result array by UID */ 2078 if (!empty($uids)) { 2079 $headers = $this->get_message_list($uids); 2080 foreach($uids as $uid) { 2081 if (isset($headers[$uid])) { 2082 $result[$uid] = $headers[$uid]; 2083 } 2084 } 2085 } 2086 return array($total, $result); 2087 } 2088 2089 /** 2090 * return all the folders contained at a hierarchy level, and if possible, if they have sub-folders 2091 * @param string $level mailbox name or empty string for the top level 2092 * @return array list of matching folders 2093 */ 2094 public function get_folder_list_by_level($level='') { 2095 $result = array(); 2096 $folders = $this->get_mailbox_list(false, $level, '%'); 2097 foreach ($folders as $name => $folder) { 2098 $result[$name] = array( 2099 'delim' => $folder['delim'], 2100 'basename' => $folder['basename'], 2101 'children' => $folder['has_kids'], 2102 'noselect' => $folder['noselect'], 2103 'id' => bin2hex($folder['basename']), 2104 'name_parts' => $folder['name_parts'], 2105 ); 2106 } 2107 return $result; 2108 } 2109} 2110 2111} 2112 2113 2114