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