source: github/program/include/rcube_imap.inc @ c007e6a

HEADcourier-fixdev-browser-capabilitiespdorelease-0.6release-0.7release-0.8
Last change on this file since c007e6a was c007e6a, checked in by thomascube <thomas@…>, 6 years ago

Fix bug with case-sensitive folder names (#1484245)

  • Property mode set to 100644
File size: 76.8 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_imap.inc                                        |
6 |                                                                       |
7 | This file is part of the RoundCube Webmail client                     |
8 | Copyright (C) 2005-2006, RoundCube Dev. - Switzerland                 |
9 | Licensed under the GNU GPL                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   IMAP wrapper that implements the Iloha IMAP Library (IIL)           |
13 |   See http://ilohamail.org/ for details                               |
14 |                                                                       |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17 +-----------------------------------------------------------------------+
18
19 $Id$
20
21*/
22
23
24/*
25 * Obtain classes from the Iloha IMAP library
26 */
27require_once('lib/imap.inc');
28require_once('lib/mime.inc');
29
30
31/**
32 * Interface class for accessing an IMAP server
33 *
34 * This is a wrapper that implements the Iloha IMAP Library (IIL)
35 *
36 * @package    Mail
37 * @author     Thomas Bruederli <roundcube@gmail.com>
38 * @version    1.39
39 * @link       http://ilohamail.org
40 */
41class rcube_imap
42{
43  var $db;
44  var $conn;
45  var $root_ns = '';
46  var $root_dir = '';
47  var $mailbox = 'INBOX';
48  var $list_page = 1;
49  var $page_size = 10;
50  var $sort_field = 'date';
51  var $sort_order = 'DESC';
52  var $delimiter = NULL;
53  var $caching_enabled = FALSE;
54  var $default_folders = array('INBOX');
55  var $default_folders_lc = array('inbox');
56  var $cache = array();
57  var $cache_keys = array(); 
58  var $cache_changes = array();
59  var $uid_id_map = array();
60  var $msg_headers = array();
61  var $capabilities = array();
62  var $skip_deleted = FALSE;
63  var $search_set = NULL;
64  var $search_subject = '';
65  var $search_string = '';
66  var $search_charset = '';
67  var $debug_level = 1;
68  var $error_code = 0;
69
70
71  /**
72   * Object constructor
73   *
74   * @param object DB Database connection
75   */
76  function __construct($db_conn)
77    {
78    $this->db = $db_conn;
79    }
80
81
82  /**
83   * PHP 4 object constructor
84   *
85   * @see  rcube_imap::__construct
86   */
87  function rcube_imap($db_conn)
88    {
89    $this->__construct($db_conn);
90    }
91
92
93  /**
94   * Connect to an IMAP server
95   *
96   * @param  string   Host to connect
97   * @param  string   Username for IMAP account
98   * @param  string   Password for IMAP account
99   * @param  number   Port to connect to
100   * @param  boolean  Use SSL connection
101   * @return boolean  TRUE on success, FALSE on failure
102   * @access public
103   */
104  function connect($host, $user, $pass, $port=143, $use_ssl=FALSE)
105    {
106    global $ICL_SSL, $ICL_PORT, $IMAP_USE_INTERNAL_DATE;
107   
108    // check for Open-SSL support in PHP build
109    if ($use_ssl && in_array('openssl', get_loaded_extensions()))
110      $ICL_SSL = TRUE;
111    else if ($use_ssl)
112      {
113      raise_error(array('code' => 403, 'type' => 'imap', 'file' => __FILE__,
114                        'message' => 'Open SSL not available;'), TRUE, FALSE);
115      $port = 143;
116      }
117
118    $ICL_PORT = $port;
119    $IMAP_USE_INTERNAL_DATE = false;
120   
121    $this->conn = iil_Connect($host, $user, $pass, array('imap' => 'check'));
122    $this->host = $host;
123    $this->user = $user;
124    $this->pass = $pass;
125    $this->port = $port;
126    $this->ssl = $use_ssl;
127   
128    // print trace mesages
129    if ($this->conn && ($this->debug_level & 8))
130      console($this->conn->message);
131   
132    // write error log
133    else if (!$this->conn && $GLOBALS['iil_error'])
134      {
135      $this->error_code = $GLOBALS['iil_errornum'];
136      raise_error(array('code' => 403,
137                       'type' => 'imap',
138                       'message' => $GLOBALS['iil_error']), TRUE, FALSE);
139      }
140
141    // get server properties
142    if ($this->conn)
143      {
144      $this->_parse_capability($this->conn->capability);
145     
146      if (!empty($this->conn->delimiter))
147        $this->delimiter = $this->conn->delimiter;
148      if (!empty($this->conn->rootdir))
149        {
150        $this->set_rootdir($this->conn->rootdir);
151        $this->root_ns = ereg_replace('[\.\/]$', '', $this->conn->rootdir);
152        }
153      }
154
155    return $this->conn ? TRUE : FALSE;
156    }
157
158
159  /**
160   * Close IMAP connection
161   * Usually done on script shutdown
162   *
163   * @access public
164   */
165  function close()
166    {   
167    if ($this->conn)
168      iil_Close($this->conn);
169    }
170
171
172  /**
173   * Close IMAP connection and re-connect
174   * This is used to avoid some strange socket errors when talking to Courier IMAP
175   *
176   * @access public
177   */
178  function reconnect()
179    {
180    $this->close();
181    $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
182    }
183
184
185  /**
186   * Set a root folder for the IMAP connection.
187   *
188   * Only folders within this root folder will be displayed
189   * and all folder paths will be translated using this folder name
190   *
191   * @param  string   Root folder
192   * @access public
193   */
194  function set_rootdir($root)
195    {
196    if (ereg('[\.\/]$', $root)) //(substr($root, -1, 1)==='/')
197      $root = substr($root, 0, -1);
198
199    $this->root_dir = $root;
200   
201    if (empty($this->delimiter))
202      $this->get_hierarchy_delimiter();
203    }
204
205
206  /**
207   * This list of folders will be listed above all other folders
208   *
209   * @param  array  Indexed list of folder names
210   * @access public
211   */
212  function set_default_mailboxes($arr)
213    {
214    if (is_array($arr))
215      {
216      $this->default_folders = $arr;
217      $this->default_folders_lc = array();
218
219      // add inbox if not included
220      if (!in_array_nocase('INBOX', $this->default_folders))
221        array_unshift($this->default_folders, 'INBOX');
222
223      // create a second list with lower cased names
224      foreach ($this->default_folders as $mbox)
225        $this->default_folders_lc[] = strtolower($mbox);
226      }
227    }
228
229
230  /**
231   * Set internal mailbox reference.
232   *
233   * All operations will be perfomed on this mailbox/folder
234   *
235   * @param  string  Mailbox/Folder name
236   * @access public
237   */
238  function set_mailbox($new_mbox)
239    {
240    $mailbox = $this->_mod_mailbox($new_mbox);
241
242    if ($this->mailbox == $mailbox)
243      return;
244
245    $this->mailbox = $mailbox;
246
247    // clear messagecount cache for this mailbox
248    $this->_clear_messagecount($mailbox);
249    }
250
251
252  /**
253   * Set internal list page
254   *
255   * @param  number  Page number to list
256   * @access public
257   */
258  function set_page($page)
259    {
260    $this->list_page = (int)$page;
261    }
262
263
264  /**
265   * Set internal page size
266   *
267   * @param  number  Number of messages to display on one page
268   * @access public
269   */
270  function set_pagesize($size)
271    {
272    $this->page_size = (int)$size;
273    }
274   
275
276  /**
277   * Save a set of message ids for future message listing methods
278   *
279   * @param  array  List of IMAP fields to search in
280   * @param  string Search string
281   * @param  array  List of message ids or NULL if empty
282   */
283  function set_search_set($subject, $str=null, $msgs=null, $charset=null)
284    {
285    if (is_array($subject) && $str == null && $msgs == null)
286      list($subject, $str, $msgs, $charset) = $subject;
287    if ($msgs != null && !is_array($msgs))
288      $msgs = split(',', $msgs);
289     
290    $this->search_subject = $subject;
291    $this->search_string = $str;
292    $this->search_set = (array)$msgs;
293    $this->search_charset = $charset;
294    }
295
296
297  /**
298   * Return the saved search set as hash array
299   * @return array Search set
300   */
301  function get_search_set()
302    {
303    return array($this->search_subject, $this->search_string, $this->search_set, $this->search_charset);
304    }
305
306
307  /**
308   * Returns the currently used mailbox name
309   *
310   * @return  string Name of the mailbox/folder
311   * @access  public
312   */
313  function get_mailbox_name()
314    {
315    return $this->conn ? $this->_mod_mailbox($this->mailbox, 'out') : '';
316    }
317
318
319  /**
320   * Returns the IMAP server's capability
321   *
322   * @param   string  Capability name
323   * @return  mixed   Capability value or TRUE if supported, FALSE if not
324   * @access  public
325   */
326  function get_capability($cap)
327    {
328    $cap = strtoupper($cap);
329    return $this->capabilities[$cap];
330    }
331
332
333  /**
334   * Returns the delimiter that is used by the IMAP server for folder separation
335   *
336   * @return  string  Delimiter string
337   * @access  public
338   */
339  function get_hierarchy_delimiter()
340    {
341    if ($this->conn && empty($this->delimiter))
342      $this->delimiter = iil_C_GetHierarchyDelimiter($this->conn);
343
344    if (empty($this->delimiter))
345      $this->delimiter = '/';
346
347    return $this->delimiter;
348    }
349
350
351  /**
352   * Public method for mailbox listing.
353   *
354   * Converts mailbox name with root dir first
355   *
356   * @param   string  Optional root folder
357   * @param   string  Optional filter for mailbox listing
358   * @return  array   List of mailboxes/folders
359   * @access  public
360   */
361  function list_mailboxes($root='', $filter='*')
362    {
363    $a_out = array();
364    $a_mboxes = $this->_list_mailboxes($root, $filter);
365
366    foreach ($a_mboxes as $mbox_row)
367      {
368      $name = $this->_mod_mailbox($mbox_row, 'out');
369      if (strlen($name))
370        $a_out[] = $name;
371      }
372
373    // INBOX should always be available
374    if (!in_array_nocase('INBOX', $a_out))
375      array_unshift($a_out, 'INBOX');
376
377    // sort mailboxes
378    $a_out = $this->_sort_mailbox_list($a_out);
379
380    return $a_out;
381    }
382
383
384  /**
385   * Private method for mailbox listing
386   *
387   * @return  array   List of mailboxes/folders
388   * @see     rcube_imap::list_mailboxes()
389   * @access  private
390   */
391  function _list_mailboxes($root='', $filter='*')
392    {
393    $a_defaults = $a_out = array();
394   
395    // get cached folder list   
396    $a_mboxes = $this->get_cache('mailboxes');
397    if (is_array($a_mboxes))
398      return $a_mboxes;
399
400    // retrieve list of folders from IMAP server
401    $a_folders = iil_C_ListSubscribed($this->conn, $this->_mod_mailbox($root), $filter);
402   
403    if (!is_array($a_folders) || !sizeof($a_folders))
404      $a_folders = array();
405
406    // write mailboxlist to cache
407    $this->update_cache('mailboxes', $a_folders);
408   
409    return $a_folders;
410    }
411
412
413  /**
414   * Get message count for a specific mailbox
415   *
416   * @param   string   Mailbox/folder name
417   * @param   string   Mode for count [ALL|UNSEEN|RECENT]
418   * @param   boolean  Force reading from server and update cache
419   * @return  int      Number of messages
420   * @access  public
421   */
422  function messagecount($mbox_name='', $mode='ALL', $force=FALSE)
423    {
424    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
425    return $this->_messagecount($mailbox, $mode, $force);
426    }
427
428
429  /**
430   * Private method for getting nr of messages
431   *
432   * @access  private
433   * @see     rcube_imap::messagecount()
434   */
435  function _messagecount($mailbox='', $mode='ALL', $force=FALSE)
436    {
437    $a_mailbox_cache = FALSE;
438    $mode = strtoupper($mode);
439
440    if (empty($mailbox))
441      $mailbox = $this->mailbox;
442     
443    // count search set
444    if ($this->search_string && $mailbox == $this->mailbox && $mode == 'ALL')
445      return count((array)$this->search_set);
446
447    $a_mailbox_cache = $this->get_cache('messagecount');
448   
449    // return cached value
450    if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
451      return $a_mailbox_cache[$mailbox][$mode];
452
453    // RECENT count is fetched abit different     
454    if ($mode == 'RECENT')
455       $count = iil_C_CheckForRecent($this->conn, $mailbox);
456
457    // use SEARCH for message counting
458    else if ($this->skip_deleted)
459      {
460      $search_str = "ALL UNDELETED";
461
462      // get message count and store in cache
463      if ($mode == 'UNSEEN')
464        $search_str .= " UNSEEN";
465
466      // get message count using SEARCH
467      // not very performant but more precise (using UNDELETED)
468      $count = 0;
469      $index = $this->_search_index($mailbox, $search_str);
470      if (is_array($index))
471        {
472        $str = implode(",", $index);
473        if (!empty($str))
474          $count = count($index);
475        }
476      }
477    else
478      {
479      if ($mode == 'UNSEEN')
480        $count = iil_C_CountUnseen($this->conn, $mailbox);
481      else
482        $count = iil_C_CountMessages($this->conn, $mailbox);
483      }
484
485    if (!is_array($a_mailbox_cache[$mailbox]))
486      $a_mailbox_cache[$mailbox] = array();
487     
488    $a_mailbox_cache[$mailbox][$mode] = (int)$count;
489
490    // write back to cache
491    $this->update_cache('messagecount', $a_mailbox_cache);
492
493    return (int)$count;
494    }
495
496
497  /**
498   * Public method for listing headers
499   * convert mailbox name with root dir first
500   *
501   * @param   string   Mailbox/folder name
502   * @param   int      Current page to list
503   * @param   string   Header field to sort by
504   * @param   string   Sort order [ASC|DESC]
505   * @return  array    Indexed array with message header objects
506   * @access  public   
507   */
508  function list_headers($mbox_name='', $page=NULL, $sort_field=NULL, $sort_order=NULL)
509    {
510    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
511    return $this->_list_headers($mailbox, $page, $sort_field, $sort_order);
512    }
513
514
515  /**
516   * Private method for listing message headers
517   *
518   * @access  private
519   * @see     rcube_imap::list_headers
520   */
521  function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=FALSE)
522    {
523    if (!strlen($mailbox))
524      return array();
525
526    // use saved message set
527    if ($this->search_string && $mailbox == $this->mailbox)
528      return $this->_list_header_set($mailbox, $this->search_set, $page, $sort_field, $sort_order);
529
530    if ($sort_field!=NULL)
531      $this->sort_field = $sort_field;
532    if ($sort_order!=NULL)
533      $this->sort_order = strtoupper($sort_order);
534
535    $max = $this->_messagecount($mailbox);
536    $start_msg = ($this->list_page-1) * $this->page_size;
537
538    list($begin, $end) = $this->_get_message_range($max, $page);
539
540    // mailbox is empty
541    if ($begin >= $end)
542      return array();
543     
544    $headers_sorted = FALSE;
545    $cache_key = $mailbox.'.msg';
546    $cache_status = $this->check_cache_status($mailbox, $cache_key);
547
548    // cache is OK, we can get all messages from local cache
549    if ($cache_status>0)
550      {
551      $a_msg_headers = $this->get_message_cache($cache_key, $start_msg, $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
552      $headers_sorted = TRUE;
553      }
554    // cache is dirty, sync it
555    else if ($this->caching_enabled && $cache_status==-1 && !$recursive)
556      {
557      $this->sync_header_index($mailbox);
558      return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, TRUE);
559      }
560    else
561      {
562      // retrieve headers from IMAP
563      if ($this->get_capability('sort') && ($msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
564        {       
565        $msgs = $msg_index[$begin];
566        for ($i=$begin+1; $i < $end; $i++)
567          $msgs = $msgs.','.$msg_index[$i];
568        }
569      else
570        {
571        $msgs = sprintf("%d:%d", $begin+1, $end);
572
573        $i = 0;
574        for ($msg_seqnum = $begin; $msg_seqnum <= $end; $msg_seqnum++)
575          $msg_index[$i++] = $msg_seqnum;
576        }
577
578      // use this class for message sorting
579      $sorter = new rcube_header_sorter();
580      $sorter->set_sequence_numbers($msg_index);
581
582      // fetch reuested headers from server
583      $a_msg_headers = array();
584      $deleted_count = $this->_fetch_headers($mailbox, $msgs, $a_msg_headers, $cache_key);
585
586      // delete cached messages with a higher index than $max+1
587      // Changed $max to $max+1 to fix this bug : #1484295
588      $this->clear_message_cache($cache_key, $max + 1);
589
590
591      // kick child process to sync cache
592      // ...
593
594      }
595
596
597    // return empty array if no messages found
598        if (!is_array($a_msg_headers) || empty($a_msg_headers))
599                return array();
600
601
602    // if not already sorted
603    if (!$headers_sorted)
604      {
605      $sorter->sort_headers($a_msg_headers);
606
607      if ($this->sort_order == 'DESC')
608        $a_msg_headers = array_reverse($a_msg_headers);
609      }
610
611    return array_values($a_msg_headers);
612    }
613
614
615
616  /**
617   * Public method for listing a specific set of headers
618   * convert mailbox name with root dir first
619   *
620   * @param   string   Mailbox/folder name
621   * @param   array    List of message ids to list
622   * @param   int      Current page to list
623   * @param   string   Header field to sort by
624   * @param   string   Sort order [ASC|DESC]
625   * @return  array    Indexed array with message header objects
626   * @access  public   
627   */
628  function list_header_set($mbox_name='', $msgs, $page=NULL, $sort_field=NULL, $sort_order=NULL)
629    {
630    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
631    return $this->_list_header_set($mailbox, $msgs, $page, $sort_field, $sort_order);   
632    }
633   
634
635  /**
636   * Private method for listing a set of message headers
637   *
638   * @access  private
639   * @see     rcube_imap::list_header_set()
640   */
641  function _list_header_set($mailbox, $msgs, $page=NULL, $sort_field=NULL, $sort_order=NULL)
642    {
643    // also accept a comma-separated list of message ids
644    if (is_string($msgs))
645      $msgs = split(',', $msgs);
646     
647    if (!strlen($mailbox) || empty($msgs))
648      return array();
649
650    if ($sort_field!=NULL)
651      $this->sort_field = $sort_field;
652    if ($sort_order!=NULL)
653      $this->sort_order = strtoupper($sort_order);
654
655    $max = count($msgs);
656    $start_msg = ($this->list_page-1) * $this->page_size;
657
658    // fetch reuested headers from server
659    $a_msg_headers = array();
660    $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
661
662    // return empty array if no messages found
663    if (!is_array($a_msg_headers) || empty($a_msg_headers))
664      return array();
665
666    // if not already sorted
667    $a_msg_headers = iil_SortHeaders($a_msg_headers, $this->sort_field, $this->sort_order);
668
669    // only return the requested part of the set
670    return array_slice(array_values($a_msg_headers), $start_msg, min($max-$start_msg, $this->page_size));
671    }
672
673
674  /**
675   * Helper function to get first and last index of the requested set
676   *
677   * @param  int     message count
678   * @param  mixed   page number to show, or string 'all'
679   * @return array   array with two values: first index, last index
680   * @access private
681   */
682  function _get_message_range($max, $page)
683    {
684    $start_msg = ($this->list_page-1) * $this->page_size;
685   
686    if ($page=='all')
687      {
688      $begin = 0;
689      $end = $max;
690      }
691    else if ($this->sort_order=='DESC')
692      {
693      $begin = $max - $this->page_size - $start_msg;
694      $end =   $max - $start_msg;
695      }
696    else
697      {
698      $begin = $start_msg;
699      $end   = $start_msg + $this->page_size;
700      }
701
702    if ($begin < 0) $begin = 0;
703    if ($end < 0) $end = $max;
704    if ($end > $max) $end = $max;
705   
706    return array($begin, $end);
707    }
708   
709   
710
711  /**
712   * Fetches message headers
713   * Used for loop
714   *
715   * @param  string  Mailbox name
716   * @param  string  Message index to fetch
717   * @param  array   Reference to message headers array
718   * @param  array   Array with cache index
719   * @return int     Number of deleted messages
720   * @access private
721   */
722  function _fetch_headers($mailbox, $msgs, &$a_msg_headers, $cache_key)
723    {
724    // cache is incomplete
725    $cache_index = $this->get_message_cache_index($cache_key);
726   
727    // fetch reuested headers from server
728    $a_header_index = iil_C_FetchHeaders($this->conn, $mailbox, $msgs);
729    $deleted_count = 0;
730   
731    if (!empty($a_header_index))
732      {
733      foreach ($a_header_index as $i => $headers)
734        {
735        if ($headers->deleted && $this->skip_deleted)
736          {
737          // delete from cache
738          if ($cache_index[$headers->id] && $cache_index[$headers->id] == $headers->uid)
739            $this->remove_message_cache($cache_key, $headers->id);
740
741          $deleted_count++;
742          continue;
743          }
744
745        // add message to cache
746        if ($this->caching_enabled && $cache_index[$headers->id] != $headers->uid)
747          $this->add_message_cache($cache_key, $headers->id, $headers);
748
749        $a_msg_headers[$headers->uid] = $headers;
750        }
751      }
752       
753    return $deleted_count;
754    }
755   
756 
757  /**
758   * Return sorted array of message UIDs
759   *
760   * @param string Mailbox to get index from
761   * @param string Sort column
762   * @param string Sort order [ASC, DESC]
763   * @return array Indexed array with message ids
764   */
765  function message_index($mbox_name='', $sort_field=NULL, $sort_order=NULL)
766    {
767    if ($sort_field!=NULL)
768      $this->sort_field = $sort_field;
769    if ($sort_order!=NULL)
770      $this->sort_order = strtoupper($sort_order);
771
772    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
773    $key = "$mbox:".$this->sort_field.":".$this->sort_order.".msgi";
774
775    // have stored it in RAM
776    if (isset($this->cache[$key]))
777      return $this->cache[$key];
778
779    // check local cache
780    $cache_key = $mailbox.'.msg';
781    $cache_status = $this->check_cache_status($mailbox, $cache_key);
782
783    // cache is OK
784    if ($cache_status>0)
785      {
786      $a_index = $this->get_message_cache_index($cache_key, TRUE, $this->sort_field, $this->sort_order);
787      return array_values($a_index);
788      }
789
790
791    // fetch complete message index
792    $msg_count = $this->_messagecount($mailbox);
793    if ($this->get_capability('sort') && ($a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, '', TRUE)))
794      {
795      if ($this->sort_order == 'DESC')
796        $a_index = array_reverse($a_index);
797
798      $this->cache[$key] = $a_index;
799
800      }
801    else
802      {
803      $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:$msg_count", $this->sort_field);
804      $a_uids = iil_C_FetchUIDs($this->conn, $mailbox);
805   
806      if ($this->sort_order=="ASC")
807        asort($a_index);
808      else if ($this->sort_order=="DESC")
809        arsort($a_index);
810       
811      $i = 0;
812      $this->cache[$key] = array();
813      foreach ($a_index as $index => $value)
814        $this->cache[$key][$i++] = $a_uids[$index];
815      }
816
817    return $this->cache[$key];
818    }
819
820
821  /**
822   * @access private
823   */
824  function sync_header_index($mailbox)
825    {
826    $cache_key = $mailbox.'.msg';
827    $cache_index = $this->get_message_cache_index($cache_key);
828    $msg_count = $this->_messagecount($mailbox);
829
830    // fetch complete message index
831    $a_message_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:$msg_count", 'UID');
832       
833    foreach ($a_message_index as $id => $uid)
834      {
835      // message in cache at correct position
836      if ($cache_index[$id] == $uid)
837        {
838        unset($cache_index[$id]);
839        continue;
840        }
841       
842      // message in cache but in wrong position
843      if (in_array((string)$uid, $cache_index, TRUE))
844        {
845        unset($cache_index[$id]);       
846        }
847     
848      // other message at this position
849      if (isset($cache_index[$id]))
850        {
851        $this->remove_message_cache($cache_key, $id);
852        unset($cache_index[$id]);
853        }
854       
855
856      // fetch complete headers and add to cache
857      $headers = iil_C_FetchHeader($this->conn, $mailbox, $id);
858      $this->add_message_cache($cache_key, $headers->id, $headers);
859      }
860
861    // those ids that are still in cache_index have been deleted     
862    if (!empty($cache_index))
863      {
864      foreach ($cache_index as $id => $uid)
865        $this->remove_message_cache($cache_key, $id);
866      }
867    }
868
869
870  /**
871   * Invoke search request to IMAP server
872   *
873   * @param  string  mailbox name to search in
874   * @param  string  search criteria (ALL, TO, FROM, SUBJECT, etc)
875   * @param  string  search string
876   * @return array   search results as list of message ids
877   * @access public
878   */
879  function search($mbox_name='', $criteria='ALL', $str=NULL, $charset=NULL)
880    {
881    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
882
883    // have an array of criterias => execute multiple searches
884    if (is_array($criteria) && $str)
885      {
886      $results = array();
887      foreach ($criteria as $crit)
888        if ($search_result = $this->search($mbox_name, $crit, $str, $charset))
889          $results = array_merge($results, $search_result);
890     
891      $results = array_unique($results);
892      $this->set_search_set($criteria, $str, $results, $charset);
893      return $results;
894      }
895    else if ($str && $criteria)
896      {
897      $search = (!empty($charset) ? "CHARSET $charset " : '') . sprintf("%s {%d}\r\n%s", $criteria, strlen($str), $str);
898      $results = $this->_search_index($mailbox, $search);
899
900      // try search with ISO charset (should be supported by server)
901      if (empty($results) && !empty($charset) && $charset!='ISO-8859-1')
902        $results = $this->search($mbox_name, $criteria, rcube_charset_convert($str, $charset, 'ISO-8859-1'), 'ISO-8859-1');
903     
904      $this->set_search_set($criteria, $str, $results, $charset);
905      return $results;
906      }
907    else
908      return $this->_search_index($mailbox, $criteria);
909    }   
910
911
912  /**
913   * Private search method
914   *
915   * @return array   search results as list of message ids
916   * @access private
917   * @see rcube_imap::search()
918   */
919  function _search_index($mailbox, $criteria='ALL')
920    {
921    $a_messages = iil_C_Search($this->conn, $mailbox, $criteria);
922    // clean message list (there might be some empty entries)
923    if (is_array($a_messages))
924      {
925      foreach ($a_messages as $i => $val)
926        if (empty($val))
927          unset($a_messages[$i]);
928      }
929       
930    return $a_messages;
931    }
932   
933 
934  /**
935   * Refresh saved search set
936   *
937   * @return array Current search set
938   */
939  function refresh_search()
940    {
941    if (!empty($this->search_subject) && !empty($this->search_string))
942      $this->search_set = $this->search('', $this->search_subject, $this->search_string, $this->search_charset);
943     
944    return $this->get_search_set();
945    }
946
947
948  /**
949   * Return message headers object of a specific message
950   *
951   * @param int     Message ID
952   * @param string  Mailbox to read from
953   * @param boolean True if $id is the message UID
954   * @return object Message headers representation
955   */
956  function get_headers($id, $mbox_name=NULL, $is_uid=TRUE)
957    {
958    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
959    $uid = $is_uid ? $id : $this->_id2uid($id);
960
961    // get cached headers
962    if ($uid && ($headers = &$this->get_cached_message($mailbox.'.msg', $uid)))
963      return $headers;
964
965    $headers = iil_C_FetchHeader($this->conn, $mailbox, $id, $is_uid);
966
967    // write headers cache
968    if ($headers)
969      {
970      if ($headers->uid && $headers->id)
971        $this->uid_id_map[$mailbox][$headers->uid] = $headers->id;
972
973      $this->add_message_cache($mailbox.'.msg', $headers->id, $headers);
974      }
975
976    return $headers;
977    }
978
979
980  /**
981   * Fetch body structure from the IMAP server and build
982   * an object structure similar to the one generated by PEAR::Mail_mimeDecode
983   *
984   * @param int Message UID to fetch
985   * @return object stdClass Message part tree or False on failure
986   */
987  function &get_structure($uid)
988    {
989    $cache_key = $this->mailbox.'.msg';
990    $headers = &$this->get_cached_message($cache_key, $uid, true);
991
992    // return cached message structure
993    if (is_object($headers) && is_object($headers->structure))
994      return $headers->structure;
995   
996    // resolve message sequence number
997    if (!($msg_id = $this->_uid2id($uid)))
998      return FALSE;
999
1000    $structure_str = iil_C_FetchStructureString($this->conn, $this->mailbox, $msg_id);
1001    $structure = iml_GetRawStructureArray($structure_str);
1002    $struct = false;
1003
1004    // parse structure and add headers
1005    if (!empty($structure))
1006      {
1007      $this->_msg_id = $msg_id;
1008      $headers = $this->get_headers($uid);
1009     
1010      $struct = &$this->_structure_part($structure);
1011      $struct->headers = get_object_vars($headers);
1012
1013      // don't trust given content-type
1014      if (empty($struct->parts) && !empty($struct->headers['ctype']))
1015        {
1016        $struct->mime_id = '1';
1017        $struct->mimetype = strtolower($struct->headers['ctype']);
1018        list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
1019        }
1020
1021      // write structure to cache
1022      if ($this->caching_enabled)
1023        $this->add_message_cache($cache_key, $msg_id, $headers, $struct);
1024      }
1025     
1026    return $struct;
1027    }
1028
1029 
1030  /**
1031   * Build message part object
1032   *
1033   * @access private
1034   */
1035  function &_structure_part($part, $count=0, $parent='')
1036    {
1037    $struct = new rcube_message_part;
1038    $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
1039   
1040    // multipart
1041    if (is_array($part[0]))
1042      {
1043      $struct->ctype_primary = 'multipart';
1044     
1045      // find first non-array entry
1046      for ($i=1; count($part); $i++)
1047        if (!is_array($part[$i]))
1048          {
1049          $struct->ctype_secondary = strtolower($part[$i]);
1050          break;
1051          }
1052         
1053      $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
1054
1055      $struct->parts = array();
1056      for ($i=0, $count=0; $i<count($part); $i++)
1057        if (is_array($part[$i]) && count($part[$i]) > 5)
1058          $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id);
1059         
1060      return $struct;
1061      }
1062   
1063   
1064    // regular part
1065    $struct->ctype_primary = strtolower($part[0]);
1066    $struct->ctype_secondary = strtolower($part[1]);
1067    $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
1068
1069    // read content type parameters
1070    if (is_array($part[2]))
1071      {
1072      $struct->ctype_parameters = array();
1073      for ($i=0; $i<count($part[2]); $i+=2)
1074        $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
1075       
1076      if (isset($struct->ctype_parameters['charset']))
1077        $struct->charset = $struct->ctype_parameters['charset'];
1078      }
1079   
1080    // read content encoding
1081    if (!empty($part[5]) && $part[5]!='NIL')
1082      {
1083      $struct->encoding = strtolower($part[5]);
1084      $struct->headers['content-transfer-encoding'] = $struct->encoding;
1085      }
1086   
1087    // get part size
1088    if (!empty($part[6]) && $part[6]!='NIL')
1089      $struct->size = intval($part[6]);
1090
1091    // read part disposition
1092    $di = count($part) - 2;
1093    if ((is_array($part[$di]) && count($part[$di]) == 2 && is_array($part[$di][1])) ||
1094        (is_array($part[--$di]) && count($part[$di]) == 2))
1095      {
1096      $struct->disposition = strtolower($part[$di][0]);
1097
1098      if (is_array($part[$di][1]))
1099        for ($n=0; $n<count($part[$di][1]); $n+=2)
1100          $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
1101      }
1102     
1103    // get child parts
1104    if (is_array($part[8]) && $di != 8)
1105      {
1106      $struct->parts = array();
1107      for ($i=0, $count=0; $i<count($part[8]); $i++)
1108        if (is_array($part[8][$i]) && count($part[8][$i]) > 5)
1109          $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
1110      }
1111
1112    // get part ID
1113    if (!empty($part[3]) && $part[3]!='NIL')
1114      {
1115      $struct->content_id = $part[3];
1116      $struct->headers['content-id'] = $part[3];
1117   
1118      if (empty($struct->disposition))
1119        $struct->disposition = 'inline';
1120      }
1121
1122    // fetch message headers if message/rfc822
1123    if ($struct->ctype_primary=='message')
1124      {
1125      $headers = iil_C_FetchPartBody($this->conn, $this->mailbox, $this->_msg_id, $struct->mime_id.'.HEADER');
1126      $struct->headers = $this->_parse_headers($headers);
1127     
1128      if (is_array($part[8]) && empty($struct->parts))
1129        $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
1130      }
1131     
1132    // normalize filename property
1133    if (!empty($struct->d_parameters['filename']))
1134      $struct->filename = $this->decode_mime_string($struct->d_parameters['filename']);
1135    else if (!empty($struct->ctype_parameters['name']))
1136      $struct->filename = $this->decode_mime_string($struct->ctype_parameters['name']);
1137    else if (!empty($struct->headers['content-description']))
1138      $struct->filename = $this->decode_mime_string($struct->headers['content-description']);
1139     
1140    return $struct;
1141    }
1142   
1143 
1144  /**
1145   * Return a flat array with references to all parts, indexed by part numbers
1146   *
1147   * @param object rcube_message_part Message body structure
1148   * @return Array with part number -> object pairs
1149   */
1150  function get_mime_numbers(&$structure)
1151    {
1152    $a_parts = array();
1153    $this->_get_part_numbers($structure, $a_parts);
1154    return $a_parts;
1155    }
1156 
1157 
1158  /**
1159   * Helper method for recursive calls
1160   *
1161   * @access private
1162   */
1163  function _get_part_numbers(&$part, &$a_parts)
1164    {
1165    if ($part->mime_id)
1166      $a_parts[$part->mime_id] = &$part;
1167     
1168    if (is_array($part->parts))
1169      for ($i=0; $i<count($part->parts); $i++)
1170        $this->_get_part_numbers($part->parts[$i], $a_parts);
1171    }
1172 
1173
1174  /**
1175   * Fetch message body of a specific message from the server
1176   *
1177   * @param  int    Message UID
1178   * @param  string Part number
1179   * @param  object rcube_message_part Part object created by get_structure()
1180   * @param  mixed  True to print part, ressource to write part contents in
1181   * @return string Message/part body if not printed
1182   */
1183  function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL)
1184    {
1185    if (!($msg_id = $this->_uid2id($uid)))
1186      return FALSE;
1187   
1188    // get part encoding if not provided
1189    if (!is_object($o_part))
1190      {
1191      $structure_str = iil_C_FetchStructureString($this->conn, $this->mailbox, $msg_id);
1192      $structure = iml_GetRawStructureArray($structure_str);
1193      $part_type = iml_GetPartTypeCode($structure, $part);
1194      $o_part = new rcube_message_part;
1195      $o_part->ctype_primary = $part_type==0 ? 'text' : ($part_type==2 ? 'message' : 'other');
1196      $o_part->encoding = strtolower(iml_GetPartEncodingString($structure, $part));
1197      $o_part->charset = iml_GetPartCharset($structure, $part);
1198      }
1199     
1200    // TODO: Add caching for message parts
1201
1202    if ($print)
1203      {
1204      $mode = $o_part->encoding == 'base64' ? 3 : ($o_part->encoding == 'quoted-printable' ? 1 : 2);
1205      $body = iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, $part, $mode);
1206     
1207      // we have to decode the part manually before printing
1208      if ($mode == 1)
1209        {
1210        echo $this->mime_decode($body, $o_part->encoding);
1211        $body = true;
1212        }
1213      }
1214    else
1215      {
1216      $body = iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, $part, 1);
1217
1218      // decode part body
1219      if ($o_part->encoding)
1220        $body = $this->mime_decode($body, $o_part->encoding);
1221
1222      // convert charset (if text or message part)
1223      if ($o_part->ctype_primary=='text' || $o_part->ctype_primary=='message')
1224        {
1225        // assume ISO-8859-1 if no charset specified
1226        if (empty($o_part->charset))
1227          $o_part->charset = 'ISO-8859-1';
1228
1229        $body = rcube_charset_convert($body, $o_part->charset);
1230        }
1231      }
1232
1233    return $body;
1234    }
1235
1236
1237  /**
1238   * Fetch message body of a specific message from the server
1239   *
1240   * @param  int    Message UID
1241   * @return string Message/part body
1242   * @see    rcube_imap::get_message_part()
1243   */
1244  function &get_body($uid, $part=1)
1245    {
1246    return $this->get_message_part($uid, $part);
1247    }
1248
1249
1250  /**
1251   * Returns the whole message source as string
1252   *
1253   * @param int  Message UID
1254   * @return string Message source string
1255   */
1256  function &get_raw_body($uid)
1257    {
1258    if (!($msg_id = $this->_uid2id($uid)))
1259      return FALSE;
1260
1261    $body = iil_C_FetchPartHeader($this->conn, $this->mailbox, $msg_id, NULL);
1262    $body .= iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, NULL, 1);
1263
1264    return $body;   
1265    }
1266   
1267
1268  /**
1269   * Sends the whole message source to stdout
1270   *
1271   * @param int  Message UID
1272   */
1273  function print_raw_body($uid)
1274    {
1275    if (!($msg_id = $this->_uid2id($uid)))
1276      return FALSE;
1277
1278    print iil_C_FetchPartHeader($this->conn, $this->mailbox, $msg_id, NULL);
1279    flush();
1280    iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, NULL, 2);
1281    }
1282
1283
1284  /**
1285   * Set message flag to one or several messages
1286   *
1287   * @param mixed  Message UIDs as array or as comma-separated string
1288   * @param string Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT
1289   * @return boolean True on success, False on failure
1290   */
1291  function set_flag($uids, $flag)
1292    {
1293    $flag = strtoupper($flag);
1294    $msg_ids = array();
1295    if (!is_array($uids))
1296      $uids = explode(',',$uids);
1297     
1298    foreach ($uids as $uid) {
1299      $msg_ids[$uid] = $this->_uid2id($uid);
1300    }
1301     
1302    if ($flag=='UNDELETED')
1303      $result = iil_C_Undelete($this->conn, $this->mailbox, join(',', array_values($msg_ids)));
1304    else if ($flag=='UNSEEN')
1305      $result = iil_C_Unseen($this->conn, $this->mailbox, join(',', array_values($msg_ids)));
1306    else
1307      $result = iil_C_Flag($this->conn, $this->mailbox, join(',', array_values($msg_ids)), $flag);
1308
1309    // reload message headers if cached
1310    $cache_key = $this->mailbox.'.msg';
1311    if ($this->caching_enabled)
1312      {
1313      foreach ($msg_ids as $uid => $id)
1314        {
1315        if ($cached_headers = $this->get_cached_message($cache_key, $uid))
1316          {
1317          $this->remove_message_cache($cache_key, $id);
1318          //$this->get_headers($uid);
1319          }
1320        }
1321
1322      // close and re-open connection
1323      // this prevents connection problems with Courier
1324      $this->reconnect();
1325      }
1326
1327    // set nr of messages that were flaged
1328    $count = count($msg_ids);
1329
1330    // clear message count cache
1331    if ($result && $flag=='SEEN')
1332      $this->_set_messagecount($this->mailbox, 'UNSEEN', $count*(-1));
1333    else if ($result && $flag=='UNSEEN')
1334      $this->_set_messagecount($this->mailbox, 'UNSEEN', $count);
1335    else if ($result && $flag=='DELETED')
1336      $this->_set_messagecount($this->mailbox, 'ALL', $count*(-1));
1337
1338    return $result;
1339    }
1340
1341
1342  /**
1343   * Append a mail message (source) to a specific mailbox
1344   *
1345   * @param string Target mailbox
1346   * @param string Message source
1347   * @return boolean True on success, False on error
1348   */
1349  function save_message($mbox_name, &$message)
1350    {
1351    $mbox_name = stripslashes($mbox_name);
1352    $mailbox = $this->_mod_mailbox($mbox_name);
1353
1354    // make sure mailbox exists
1355    if (in_array($mailbox, $this->_list_mailboxes()))
1356      $saved = iil_C_Append($this->conn, $mailbox, $message);
1357
1358    if ($saved)
1359      {
1360      // increase messagecount of the target mailbox
1361      $this->_set_messagecount($mailbox, 'ALL', 1);
1362      }
1363         
1364    return $saved;
1365    }
1366
1367
1368  /**
1369   * Move a message from one mailbox to another
1370   *
1371   * @param string List of UIDs to move, separated by comma
1372   * @param string Target mailbox
1373   * @param string Source mailbox
1374   * @return boolean True on success, False on error
1375   */
1376  function move_message($uids, $to_mbox, $from_mbox='')
1377    {
1378    $to_mbox = stripslashes($to_mbox);
1379    $from_mbox = stripslashes($from_mbox);
1380    $to_mbox = $this->_mod_mailbox($to_mbox);
1381    $from_mbox = $from_mbox ? $this->_mod_mailbox($from_mbox) : $this->mailbox;
1382
1383    // make sure mailbox exists
1384    if (!in_array($to_mbox, $this->_list_mailboxes()))
1385      {
1386      if (in_array($to_mbox, $this->default_folders))
1387        $this->create_mailbox($to_mbox, TRUE);
1388      else
1389        return FALSE;
1390      }
1391
1392    // convert the list of uids to array
1393    $a_uids = is_string($uids) ? explode(',', $uids) : (is_array($uids) ? $uids : NULL);
1394   
1395    // exit if no message uids are specified
1396    if (!is_array($a_uids))
1397      return false;
1398
1399    // convert uids to message ids
1400    $a_mids = array();
1401    foreach ($a_uids as $uid)
1402      $a_mids[] = $this->_uid2id($uid, $from_mbox);
1403
1404    $iil_move = iil_C_Move($this->conn, join(',', $a_mids), $from_mbox, $to_mbox);
1405    $moved = !($iil_move === false || $iil_move < 0);
1406   
1407    // send expunge command in order to have the moved message
1408    // really deleted from the source mailbox
1409    if ($moved)
1410      {
1411      $this->_expunge($from_mbox, FALSE);
1412      $this->_clear_messagecount($from_mbox);
1413      $this->_clear_messagecount($to_mbox);
1414      }
1415     
1416    // remove message ids from search set
1417    if ($moved && $this->search_set && $from_mbox == $this->mailbox)
1418      $this->search_set = array_diff($this->search_set, $a_mids);
1419
1420    // update cached message headers
1421    $cache_key = $from_mbox.'.msg';
1422    if ($moved && ($a_cache_index = $this->get_message_cache_index($cache_key)))
1423      {
1424      $start_index = 100000;
1425      foreach ($a_uids as $uid)
1426        {
1427        if (($index = array_search($uid, $a_cache_index)) !== FALSE)
1428          $start_index = min($index, $start_index);
1429        }
1430
1431      // clear cache from the lowest index on
1432      $this->clear_message_cache($cache_key, $start_index);
1433      }
1434
1435    return $moved;
1436    }
1437
1438
1439  /**
1440   * Mark messages as deleted and expunge mailbox
1441   *
1442   * @param string List of UIDs to move, separated by comma
1443   * @param string Source mailbox
1444   * @return boolean True on success, False on error
1445   */
1446  function delete_message($uids, $mbox_name='')
1447    {
1448    $mbox_name = stripslashes($mbox_name);
1449    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
1450
1451    // convert the list of uids to array
1452    $a_uids = is_string($uids) ? explode(',', $uids) : (is_array($uids) ? $uids : NULL);
1453   
1454    // exit if no message uids are specified
1455    if (!is_array($a_uids))
1456      return false;
1457
1458
1459    // convert uids to message ids
1460    $a_mids = array();
1461    foreach ($a_uids as $uid)
1462      $a_mids[] = $this->_uid2id($uid, $mailbox);
1463       
1464    $deleted = iil_C_Delete($this->conn, $mailbox, join(',', $a_mids));
1465   
1466    // send expunge command in order to have the deleted message
1467    // really deleted from the mailbox
1468    if ($deleted)
1469      {
1470      $this->_expunge($mailbox, FALSE);
1471      $this->_clear_messagecount($mailbox);
1472      }
1473
1474    // remove message ids from search set
1475    if ($moved && $this->search_set && $mailbox == $this->mailbox)
1476      $this->search_set = array_diff($this->search_set, $a_mids);
1477
1478    // remove deleted messages from cache
1479    $cache_key = $mailbox.'.msg';
1480    if ($deleted && ($a_cache_index = $this->get_message_cache_index($cache_key)))
1481      {
1482      $start_index = 100000;
1483      foreach ($a_uids as $uid)
1484        {
1485        if (($index = array_search($uid, $a_cache_index)) !== FALSE)
1486          $start_index = min($index, $start_index);
1487        }
1488
1489      // clear cache from the lowest index on
1490      $this->clear_message_cache($cache_key, $start_index);
1491      }
1492
1493    return $deleted;
1494    }
1495
1496
1497  /**
1498   * Clear all messages in a specific mailbox
1499   *
1500   * @param string Mailbox name
1501   * @return int Above 0 on success
1502   */
1503  function clear_mailbox($mbox_name=NULL)
1504    {
1505    $mbox_name = stripslashes($mbox_name);
1506    $mailbox = !empty($mbox_name) ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
1507    $msg_count = $this->_messagecount($mailbox, 'ALL');
1508   
1509    if ($msg_count>0)
1510      {
1511      $cleared = iil_C_ClearFolder($this->conn, $mailbox);
1512     
1513      // make sure the message count cache is cleared as well
1514      if ($cleared)
1515        {
1516        $this->clear_message_cache($mailbox.'.msg');     
1517        $a_mailbox_cache = $this->get_cache('messagecount');
1518        unset($a_mailbox_cache[$mailbox]);
1519        $this->update_cache('messagecount', $a_mailbox_cache);
1520        }
1521       
1522      return $cleared;
1523      }
1524    else
1525      return 0;
1526    }
1527
1528
1529  /**
1530   * Send IMAP expunge command and clear cache
1531   *
1532   * @param string Mailbox name
1533   * @param boolean False if cache should not be cleared
1534   * @return boolean True on success
1535   */
1536  function expunge($mbox_name='', $clear_cache=TRUE)
1537    {
1538    $mbox_name = stripslashes($mbox_name);
1539    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
1540    return $this->_expunge($mailbox, $clear_cache);
1541    }
1542
1543
1544  /**
1545   * Send IMAP expunge command and clear cache
1546   *
1547   * @see rcube_imap::expunge()
1548   * @access private
1549   */
1550  function _expunge($mailbox, $clear_cache=TRUE)
1551    {
1552    $result = iil_C_Expunge($this->conn, $mailbox);
1553
1554    if ($result>=0 && $clear_cache)
1555      {
1556      $this->clear_message_cache($mailbox.'.msg');
1557      $this->_clear_messagecount($mailbox);
1558      }
1559     
1560    return $result;
1561    }
1562
1563
1564  /* --------------------------------
1565   *        folder managment
1566   * --------------------------------*/
1567
1568
1569  /**
1570   * Get a list of all folders available on the IMAP server
1571   *
1572   * @param string IMAP root dir
1573   * @return array Indexed array with folder names
1574   */
1575  function list_unsubscribed($root='')
1576    {
1577    static $sa_unsubscribed;
1578   
1579    if (is_array($sa_unsubscribed))
1580      return $sa_unsubscribed;
1581     
1582    // retrieve list of folders from IMAP server
1583    $a_mboxes = iil_C_ListMailboxes($this->conn, $this->_mod_mailbox($root), '*');
1584
1585    // modify names with root dir
1586    foreach ($a_mboxes as $mbox_name)
1587      {
1588      $name = $this->_mod_mailbox($mbox_name, 'out');
1589      if (strlen($name))
1590        $a_folders[] = $name;
1591      }
1592
1593    // filter folders and sort them
1594    $sa_unsubscribed = $this->_sort_mailbox_list($a_folders);
1595    return $sa_unsubscribed;
1596    }
1597
1598
1599  /**
1600   * Get mailbox quota information
1601   * added by Nuny
1602   *
1603   * @return mixed Quota info or False if not supported
1604   */
1605  function get_quota()
1606    {
1607    if ($this->get_capability('QUOTA'))
1608      return iil_C_GetQuota($this->conn);
1609       
1610    return FALSE;
1611    }
1612
1613
1614  /**
1615   * Subscribe to a specific mailbox(es)
1616   *
1617   * @param string Mailbox name(s)
1618   * @return boolean True on success
1619   */
1620  function subscribe($mbox_name)
1621    {
1622    if (is_array($mbox_name))
1623      $a_mboxes = $mbox_name;
1624    else if (is_string($mbox_name) && strlen($mbox_name))
1625      $a_mboxes = explode(',', $mbox_name);
1626   
1627    // let this common function do the main work
1628    return $this->_change_subscription($a_mboxes, 'subscribe');
1629    }
1630
1631
1632  /**
1633   * Unsubscribe mailboxes
1634   *
1635   * @param string Mailbox name(s)
1636   * @return boolean True on success
1637   */
1638  function unsubscribe($mbox_name)
1639    {
1640    if (is_array($mbox_name))
1641      $a_mboxes = $mbox_name;
1642    else if (is_string($mbox_name) && strlen($mbox_name))
1643      $a_mboxes = explode(',', $mbox_name);
1644
1645    // let this common function do the main work
1646    return $this->_change_subscription($a_mboxes, 'unsubscribe');
1647    }
1648
1649
1650  /**
1651   * Create a new mailbox on the server and register it in local cache
1652   *
1653   * @param string  New mailbox name (as utf-7 string)
1654   * @param boolean True if the new mailbox should be subscribed
1655   * @param string  Name of the created mailbox, false on error
1656   */
1657  function create_mailbox($name, $subscribe=FALSE)
1658    {
1659    $result = FALSE;
1660   
1661    // replace backslashes
1662    $name = preg_replace('/[\\\]+/', '-', $name);
1663
1664    // reduce mailbox name to 100 chars
1665    $name = substr($name, 0, 100);
1666
1667    $abs_name = $this->_mod_mailbox($name);
1668    $a_mailbox_cache = $this->get_cache('mailboxes');
1669
1670    if (strlen($abs_name) && (!is_array($a_mailbox_cache) || !in_array($abs_name, $a_mailbox_cache)))
1671      $result = iil_C_CreateFolder($this->conn, $abs_name);
1672
1673    // try to subscribe it
1674    if ($result && $subscribe)
1675      $this->subscribe($name);
1676
1677    return $result ? $name : FALSE;
1678    }
1679
1680
1681  /**
1682   * Set a new name to an existing mailbox
1683   *
1684   * @param string Mailbox to rename (as utf-7 string)
1685   * @param string New mailbox name (as utf-7 string)
1686   * @return string Name of the renames mailbox, False on error
1687   */
1688  function rename_mailbox($mbox_name, $new_name)
1689    {
1690    $result = FALSE;
1691
1692    // replace backslashes
1693    $name = preg_replace('/[\\\]+/', '-', $new_name);
1694       
1695    // encode mailbox name and reduce it to 100 chars
1696    $name = substr($new_name, 0, 100);
1697
1698    // make absolute path
1699    $mailbox = $this->_mod_mailbox($mbox_name);
1700    $abs_name = $this->_mod_mailbox($name);
1701   
1702    // check if mailbox is subscribed
1703    $a_subscribed = $this->_list_mailboxes();
1704    $subscribed = in_array($mailbox, $a_subscribed);
1705   
1706    // unsubscribe folder
1707    if ($subscribed)
1708      iil_C_UnSubscribe($this->conn, $mailbox);
1709
1710    if (strlen($abs_name))
1711      $result = iil_C_RenameFolder($this->conn, $mailbox, $abs_name);
1712
1713    if ($result)
1714      {
1715      $delm = $this->get_hierarchy_delimiter();
1716     
1717      // check if mailbox children are subscribed
1718      foreach ($a_subscribed as $c_subscribed)
1719        if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed))
1720          {
1721          iil_C_UnSubscribe($this->conn, $c_subscribed);
1722          iil_C_Subscribe($this->conn, preg_replace('/^'.preg_quote($mailbox, '/').'/', $abs_name, $c_subscribed));
1723          }
1724
1725      // clear cache
1726      $this->clear_message_cache($mailbox.'.msg');
1727      $this->clear_cache('mailboxes');     
1728      }
1729
1730    // try to subscribe it
1731    if ($result && $subscribed)
1732      iil_C_Subscribe($this->conn, $abs_name);
1733
1734    return $result ? $name : FALSE;
1735    }
1736
1737
1738  /**
1739   * Remove mailboxes from server
1740   *
1741   * @param string Mailbox name
1742   * @return boolean True on success
1743   */
1744  function delete_mailbox($mbox_name)
1745    {
1746    $deleted = FALSE;
1747
1748    if (is_array($mbox_name))
1749      $a_mboxes = $mbox_name;
1750    else if (is_string($mbox_name) && strlen($mbox_name))
1751      $a_mboxes = explode(',', $mbox_name);
1752
1753    $all_mboxes = iil_C_ListMailboxes($this->conn, $this->_mod_mailbox($root), '*');
1754
1755    if (is_array($a_mboxes))
1756      foreach ($a_mboxes as $mbox_name)
1757        {
1758        $mailbox = $this->_mod_mailbox($mbox_name);
1759
1760        // unsubscribe mailbox before deleting
1761        iil_C_UnSubscribe($this->conn, $mailbox);
1762
1763        // send delete command to server
1764        $result = iil_C_DeleteFolder($this->conn, $mailbox);
1765        if ($result>=0)
1766          $deleted = TRUE;
1767
1768        foreach ($all_mboxes as $c_mbox)
1769          if (preg_match('/^'.preg_quote($mailbox.$this->delimiter).'/', $c_mbox))
1770            {
1771            iil_C_UnSubscribe($this->conn, $c_mbox);
1772            $result = iil_C_DeleteFolder($this->conn, $c_mbox);
1773            if ($result>=0)
1774              $deleted = TRUE;
1775            }
1776        }
1777
1778    // clear mailboxlist cache
1779    if ($deleted)
1780      {
1781      $this->clear_message_cache($mailbox.'.msg');
1782      $this->clear_cache('mailboxes');
1783      }
1784
1785    return $deleted;
1786    }
1787
1788
1789  /**
1790   * Create all folders specified as default
1791   */
1792  function create_default_folders()
1793    {
1794    $a_folders = iil_C_ListMailboxes($this->conn, $this->_mod_mailbox(''), '*');
1795    $a_subscribed = iil_C_ListSubscribed($this->conn, $this->_mod_mailbox(''), '*');
1796   
1797    // create default folders if they do not exist
1798    foreach ($this->default_folders as $folder)
1799      {
1800      $abs_name = $this->_mod_mailbox($folder);
1801      if (!in_array_nocase($abs_name, $a_folders))
1802        $this->create_mailbox($folder, TRUE);
1803      else if (!in_array_nocase($abs_name, $a_subscribed))
1804        $this->subscribe($folder);
1805      }
1806    }
1807
1808
1809
1810  /* --------------------------------
1811   *   internal caching methods
1812   * --------------------------------*/
1813
1814  /**
1815   * @access private
1816   */
1817  function set_caching($set)
1818    {
1819    if ($set && is_object($this->db))
1820      $this->caching_enabled = TRUE;
1821    else
1822      $this->caching_enabled = FALSE;
1823    }
1824
1825  /**
1826   * @access private
1827   */
1828  function get_cache($key)
1829    {
1830    // read cache
1831    if (!isset($this->cache[$key]) && $this->caching_enabled)
1832      {
1833      $cache_data = $this->_read_cache_record('IMAP.'.$key);
1834      $this->cache[$key] = strlen($cache_data) ? unserialize($cache_data) : FALSE;
1835      }
1836   
1837    return $this->cache[$key];
1838    }
1839
1840  /**
1841   * @access private
1842   */
1843  function update_cache($key, $data)
1844    {
1845    $this->cache[$key] = $data;
1846    $this->cache_changed = TRUE;
1847    $this->cache_changes[$key] = TRUE;
1848    }
1849
1850  /**
1851   * @access private
1852   */
1853  function write_cache()
1854    {
1855    if ($this->caching_enabled && $this->cache_changed)
1856      {
1857      foreach ($this->cache as $key => $data)
1858        {
1859        if ($this->cache_changes[$key])
1860          $this->_write_cache_record('IMAP.'.$key, serialize($data));
1861        }
1862      }   
1863    }
1864
1865  /**
1866   * @access private
1867   */
1868  function clear_cache($key=NULL)
1869    {
1870    if ($key===NULL)
1871      {
1872      foreach ($this->cache as $key => $data)
1873        $this->_clear_cache_record('IMAP.'.$key);
1874
1875      $this->cache = array();
1876      $this->cache_changed = FALSE;
1877      $this->cache_changes = array();
1878      }
1879    else
1880      {
1881      $this->_clear_cache_record('IMAP.'.$key);
1882      $this->cache_changes[$key] = FALSE;
1883      unset($this->cache[$key]);
1884      }
1885    }
1886
1887  /**
1888   * @access private
1889   */
1890  function _read_cache_record($key)
1891    {
1892    $cache_data = FALSE;
1893   
1894    if ($this->db)
1895      {
1896      // get cached data from DB
1897      $sql_result = $this->db->query(
1898        "SELECT cache_id, data
1899         FROM ".get_table_name('cache')."
1900         WHERE  user_id=?
1901         AND    cache_key=?",
1902        $_SESSION['user_id'],
1903        $key);
1904
1905      if ($sql_arr = $this->db->fetch_assoc($sql_result))
1906        {
1907        $cache_data = $sql_arr['data'];
1908        $this->cache_keys[$key] = $sql_arr['cache_id'];
1909        }
1910      }
1911
1912    return $cache_data;
1913    }
1914
1915  /**
1916   * @access private
1917   */
1918  function _write_cache_record($key, $data)
1919    {
1920    if (!$this->db)
1921      return FALSE;
1922
1923    // check if we already have a cache entry for this key
1924    if (!isset($this->cache_keys[$key]))
1925      {
1926      $sql_result = $this->db->query(
1927        "SELECT cache_id
1928         FROM ".get_table_name('cache')."
1929         WHERE  user_id=?
1930         AND    cache_key=?",
1931        $_SESSION['user_id'],
1932        $key);
1933                                     
1934      if ($sql_arr = $this->db->fetch_assoc($sql_result))
1935        $this->cache_keys[$key] = $sql_arr['cache_id'];
1936      else
1937        $this->cache_keys[$key] = FALSE;
1938      }
1939
1940    // update existing cache record
1941    if ($this->cache_keys[$key])
1942      {
1943      $this->db->query(
1944        "UPDATE ".get_table_name('cache')."
1945         SET    created=".$this->db->now().",
1946                data=?
1947         WHERE  user_id=?
1948         AND    cache_key=?",
1949        $data,
1950        $_SESSION['user_id'],
1951        $key);
1952      }
1953    // add new cache record
1954    else
1955      {
1956      $this->db->query(
1957        "INSERT INTO ".get_table_name('cache')."
1958         (created, user_id, cache_key, data)
1959         VALUES (".$this->db->now().", ?, ?, ?)",
1960        $_SESSION['user_id'],
1961        $key,
1962        $data);
1963      }
1964    }
1965
1966  /**
1967   * @access private
1968   */
1969  function _clear_cache_record($key)
1970    {
1971    $this->db->query(
1972      "DELETE FROM ".get_table_name('cache')."
1973       WHERE  user_id=?
1974       AND    cache_key=?",
1975      $_SESSION['user_id'],
1976      $key);
1977    }
1978
1979
1980
1981  /* --------------------------------
1982   *   message caching methods
1983   * --------------------------------*/
1984   
1985
1986  /**
1987   * Checks if the cache is up-to-date
1988   *
1989   * @param string Mailbox name
1990   * @param string Internal cache key
1991   * @return int -3 = off, -2 = incomplete, -1 = dirty
1992   */
1993  function check_cache_status($mailbox, $cache_key)
1994    {
1995    if (!$this->caching_enabled)
1996      return -3;
1997
1998    $cache_index = $this->get_message_cache_index($cache_key, TRUE);
1999    $msg_count = $this->_messagecount($mailbox);
2000    $cache_count = count($cache_index);
2001
2002    // console("Cache check: $msg_count !== ".count($cache_index));
2003
2004    if ($cache_count==$msg_count)
2005      {
2006      // get highest index
2007      $header = iil_C_FetchHeader($this->conn, $mailbox, "$msg_count");
2008      $cache_uid = array_pop($cache_index);
2009     
2010      // uids of highest message matches -> cache seems OK
2011      if ($cache_uid == $header->uid)
2012        return 1;
2013
2014      // cache is dirty
2015      return -1;
2016      }
2017    // if cache count differs less than 10% report as dirty
2018    else if (abs($msg_count - $cache_count) < $msg_count/10)
2019      return -1;
2020    else
2021      return -2;
2022    }
2023
2024  /**
2025   * @access private
2026   */
2027  function get_message_cache($key, $from, $to, $sort_field, $sort_order)
2028    {
2029    $cache_key = "$key:$from:$to:$sort_field:$sort_order";
2030    $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
2031   
2032    if (!in_array($sort_field, $db_header_fields))
2033      $sort_field = 'idx';
2034   
2035    if ($this->caching_enabled && !isset($this->cache[$cache_key]))
2036      {
2037      $this->cache[$cache_key] = array();
2038      $sql_result = $this->db->limitquery(
2039        "SELECT idx, uid, headers
2040         FROM ".get_table_name('messages')."
2041         WHERE  user_id=?
2042         AND    cache_key=?
2043         ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".
2044         strtoupper($sort_order),
2045        $from,
2046        $to-$from,
2047        $_SESSION['user_id'],
2048        $key);
2049
2050      while ($sql_arr = $this->db->fetch_assoc($sql_result))
2051        {
2052        $uid = $sql_arr['uid'];
2053        $this->cache[$cache_key][$uid] = unserialize($sql_arr['headers']);
2054       
2055        // featch headers if unserialize failed
2056        if (empty($this->cache[$cache_key][$uid]))
2057          $this->cache[$cache_key][$uid] = iil_C_FetchHeader($this->conn, preg_replace('/.msg$/', '', $key), $uid, true);
2058        }
2059      }
2060     
2061    return $this->cache[$cache_key];
2062    }
2063
2064  /**
2065   * @access private
2066   */
2067  function &get_cached_message($key, $uid, $struct=false)
2068    {
2069    $internal_key = '__single_msg';
2070   
2071    if ($this->caching_enabled && (!isset($this->cache[$internal_key][$uid]) ||
2072        ($struct && empty($this->cache[$internal_key][$uid]->structure))))
2073      {
2074      $sql_select = "idx, uid, headers" . ($struct ? ", structure" : '');
2075      $sql_result = $this->db->query(
2076        "SELECT $sql_select
2077         FROM ".get_table_name('messages')."
2078         WHERE  user_id=?
2079         AND    cache_key=?
2080         AND    uid=?",
2081        $_SESSION['user_id'],
2082        $key,
2083        $uid);
2084
2085      if ($sql_arr = $this->db->fetch_assoc($sql_result))
2086        {
2087        $this->cache[$internal_key][$uid] = unserialize($sql_arr['headers']);
2088        if (is_object($this->cache[$internal_key][$uid]) && !empty($sql_arr['structure']))
2089          $this->cache[$internal_key][$uid]->structure = unserialize($sql_arr['structure']);
2090        }
2091      }
2092
2093    return $this->cache[$internal_key][$uid];
2094    }
2095
2096  /**
2097   * @access private
2098   */ 
2099  function get_message_cache_index($key, $force=FALSE, $sort_col='idx', $sort_order='ASC')
2100    {
2101    static $sa_message_index = array();
2102   
2103    // empty key -> empty array
2104    if (!$this->caching_enabled || empty($key))
2105      return array();
2106   
2107    if (!empty($sa_message_index[$key]) && !$force)
2108      return $sa_message_index[$key];
2109   
2110    $sa_message_index[$key] = array();
2111    $sql_result = $this->db->query(
2112      "SELECT idx, uid
2113       FROM ".get_table_name('messages')."
2114       WHERE  user_id=?
2115       AND    cache_key=?
2116       ORDER BY ".$this->db->quote_identifier($sort_col)." ".$sort_order,
2117      $_SESSION['user_id'],
2118      $key);
2119
2120    while ($sql_arr = $this->db->fetch_assoc($sql_result))
2121      $sa_message_index[$key][$sql_arr['idx']] = $sql_arr['uid'];
2122     
2123    return $sa_message_index[$key];
2124    }
2125
2126  /**
2127   * @access private
2128   */
2129  function add_message_cache($key, $index, $headers, $struct=null)
2130    {
2131    if (empty($key) || !is_object($headers) || empty($headers->uid))
2132        return;
2133   
2134    // add to internal (fast) cache
2135    $this->cache['__single_msg'][$headers->uid] = $headers;
2136    $this->cache['__single_msg'][$headers->uid]->structure = $struct;
2137   
2138    // no further caching
2139    if (!$this->caching_enabled)
2140      return;
2141   
2142    // check for an existing record (probly headers are cached but structure not)
2143    $sql_result = $this->db->query(
2144        "SELECT message_id
2145         FROM ".get_table_name('messages')."
2146         WHERE  user_id=?
2147         AND    cache_key=?
2148         AND    uid=?
2149         AND    del<>1",
2150        $_SESSION['user_id'],
2151        $key,
2152        $headers->uid);
2153
2154    // update cache record
2155    if ($sql_arr = $this->db->fetch_assoc($sql_result))
2156      {
2157      $this->db->query(
2158        "UPDATE ".get_table_name('messages')."
2159         SET   idx=?, headers=?, structure=?
2160         WHERE message_id=?",
2161        $index,
2162        serialize($headers),
2163        is_object($struct) ? serialize($struct) : NULL,
2164        $sql_arr['message_id']
2165        );
2166      }
2167    else  // insert new record
2168      {
2169      $this->db->query(
2170        "INSERT INTO ".get_table_name('messages')."
2171         (user_id, del, cache_key, created, idx, uid, subject, ".$this->db->quoteIdentifier('from').", ".$this->db->quoteIdentifier('to').", cc, date, size, headers, structure)
2172         VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".$this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
2173        $_SESSION['user_id'],
2174        $key,
2175        $index,
2176        $headers->uid,
2177        (string)substr($this->decode_header($headers->subject, TRUE), 0, 128),
2178        (string)substr($this->decode_header($headers->from, TRUE), 0, 128),
2179        (string)substr($this->decode_header($headers->to, TRUE), 0, 128),
2180        (string)substr($this->decode_header($headers->cc, TRUE), 0, 128),
2181        (int)$headers->size,
2182        serialize($headers),
2183        is_object($struct) ? serialize($struct) : NULL
2184        );
2185      }
2186    }
2187   
2188  /**
2189   * @access private
2190   */
2191  function remove_message_cache($key, $index)
2192    {
2193    $this->db->query(
2194      "DELETE FROM ".get_table_name('messages')."
2195       WHERE  user_id=?
2196       AND    cache_key=?
2197       AND    idx=?",
2198      $_SESSION['user_id'],
2199      $key,
2200      $index);
2201    }
2202
2203  /**
2204   * @access private
2205   */
2206  function clear_message_cache($key, $start_index=1)
2207    {
2208    $this->db->query(
2209      "DELETE FROM ".get_table_name('messages')."
2210       WHERE  user_id=?
2211       AND    cache_key=?
2212       AND    idx>=?",
2213      $_SESSION['user_id'],
2214      $key,
2215      $start_index);
2216    }
2217
2218
2219
2220
2221  /* --------------------------------
2222   *   encoding/decoding methods
2223   * --------------------------------*/
2224
2225  /**
2226   * Split an address list into a structured array list
2227   *
2228   * @param string  Input string
2229   * @param int     List only this number of addresses
2230   * @param boolean Decode address strings
2231   * @return array  Indexed list of addresses
2232   */
2233  function decode_address_list($input, $max=null, $decode=true)
2234    {
2235    $a = $this->_parse_address_list($input, $decode);
2236    $out = array();
2237   
2238    if (!is_array($a))
2239      return $out;
2240
2241    $c = count($a);
2242    $j = 0;
2243
2244    foreach ($a as $val)
2245      {
2246      $j++;
2247      $address = $val['address'];
2248      $name = preg_replace(array('/^[\'"]/', '/[\'"]$/'), '', trim($val['name']));
2249      if ($name && $address && $name != $address)
2250        $string = sprintf('%s <%s>', strpos($name, ',')!==FALSE ? '"'.$name.'"' : $name, $address);
2251      else if ($address)
2252        $string = $address;
2253      else if ($name)
2254        $string = $name;
2255     
2256      $out[$j] = array('name' => $name,
2257                       'mailto' => $address,
2258                       'string' => $string);
2259             
2260      if ($max && $j==$max)
2261        break;
2262      }
2263   
2264    return $out;
2265    }
2266
2267
2268  /**
2269   * Decode a message header value
2270   *
2271   * @param string  Header value
2272   * @param boolean Remove quotes if necessary
2273   * @return string Decoded string
2274   */
2275  function decode_header($input, $remove_quotes=FALSE)
2276    {
2277    $str = $this->decode_mime_string((string)$input);
2278    if ($str{0}=='"' && $remove_quotes)
2279      $str = str_replace('"', '', $str);
2280   
2281    return $str;
2282    }
2283
2284
2285  /**
2286   * Decode a mime-encoded string to internal charset
2287   *
2288   * @param string  Header value
2289   * @param string  Fallback charset if none specified
2290   * @return string Decoded string
2291   * @static
2292   */
2293  function decode_mime_string($input, $fallback=null)
2294    {
2295    $out = '';
2296
2297    $pos = strpos($input, '=?');
2298    if ($pos !== false)
2299      {
2300      $out = substr($input, 0, $pos);
2301 
2302      $end_cs_pos = strpos($input, "?", $pos+2);
2303      $end_en_pos = strpos($input, "?", $end_cs_pos+1);
2304      $end_pos = strpos($input, "?=", $end_en_pos+1);
2305 
2306      $encstr = substr($input, $pos+2, ($end_pos-$pos-2));
2307      $rest = substr($input, $end_pos+2);
2308
2309      $out .= rcube_imap::_decode_mime_string_part($encstr);
2310      $out .= rcube_imap::decode_mime_string($rest, $fallback);
2311
2312      return $out;
2313      }
2314     
2315    // no encoding information, use fallback
2316    return rcube_charset_convert($input, !empty($fallback) ? $fallback : 'ISO-8859-1');
2317    }
2318
2319
2320  /**
2321   * Decode a part of a mime-encoded string
2322   *
2323   * @access private
2324   */
2325  function _decode_mime_string_part($str)
2326    {
2327    $a = explode('?', $str);
2328    $count = count($a);
2329
2330    // should be in format "charset?encoding?base64_string"
2331    if ($count >= 3)
2332      {
2333      for ($i=2; $i<$count; $i++)
2334        $rest.=$a[$i];
2335
2336      if (($a[1]=="B")||($a[1]=="b"))
2337        $rest = base64_decode($rest);
2338      else if (($a[1]=="Q")||($a[1]=="q"))
2339        {
2340        $rest = str_replace("_", " ", $rest);
2341        $rest = quoted_printable_decode($rest);
2342        }
2343
2344      return rcube_charset_convert($rest, $a[0]);
2345      }
2346    else
2347      return $str;    // we dont' know what to do with this 
2348    }
2349
2350
2351  /**
2352   * Decode a mime part
2353   *
2354   * @param string Input string
2355   * @param string Part encoding
2356   * @return string Decoded string
2357   * @access private
2358   */
2359  function mime_decode($input, $encoding='7bit')
2360    {
2361    switch (strtolower($encoding))
2362      {
2363      case '7bit':
2364        return $input;
2365        break;
2366     
2367      case 'quoted-printable':
2368        return quoted_printable_decode($input);
2369        break;
2370     
2371      case 'base64':
2372        return base64_decode($input);
2373        break;
2374     
2375      default:
2376        return $input;
2377      }
2378    }
2379
2380
2381  /**
2382   * Convert body charset to UTF-8 according to the ctype_parameters
2383   *
2384   * @param string Part body to decode
2385   * @param string Charset to convert from
2386   * @return string Content converted to internal charset
2387   */
2388  function charset_decode($body, $ctype_param)
2389    {
2390    if (is_array($ctype_param) && !empty($ctype_param['charset']))
2391      return rcube_charset_convert($body, $ctype_param['charset']);
2392
2393    // defaults to what is specified in the class header
2394    return rcube_charset_convert($body,  'ISO-8859-1');
2395    }
2396
2397
2398  /**
2399   * Translate UID to message ID
2400   *
2401   * @param int    Message UID
2402   * @param string Mailbox name
2403   * @return int   Message ID
2404   */
2405  function get_id($uid, $mbox_name=NULL)
2406    {
2407      $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
2408      return $this->_uid2id($uid, $mailbox);
2409    }
2410
2411
2412  /**
2413   * Translate message number to UID
2414   *
2415   * @param int    Message ID
2416   * @param string Mailbox name
2417   * @return int   Message UID
2418   */
2419  function get_uid($id,$mbox_name=NULL)
2420    {
2421      $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
2422      return $this->_id2uid($id, $mailbox);
2423    }
2424
2425
2426
2427  /* --------------------------------
2428   *         private methods
2429   * --------------------------------*/
2430
2431
2432  /**
2433   * @access private
2434   */
2435  function _mod_mailbox($mbox_name, $mode='in')
2436    {
2437    if ((!empty($this->root_ns) && $this->root_ns == $mbox_name) || $mbox_name == 'INBOX')
2438      return $mbox_name;
2439
2440    if (!empty($this->root_dir) && $mode=='in')
2441      $mbox_name = $this->root_dir.$this->delimiter.$mbox_name;
2442    else if (strlen($this->root_dir) && $mode=='out')
2443      $mbox_name = substr($mbox_name, strlen($this->root_dir)+1);
2444
2445    return $mbox_name;
2446    }
2447
2448
2449  /**
2450   * Sort mailboxes first by default folders and then in alphabethical order
2451   * @access private
2452   */
2453  function _sort_mailbox_list($a_folders)
2454    {
2455    $a_out = $a_defaults = array();
2456
2457    // find default folders and skip folders starting with '.'
2458    foreach($a_folders as $i => $folder)
2459      {
2460      if ($folder{0}=='.')
2461        continue;
2462
2463      if (($p = array_search(strtolower($folder), $this->default_folders_lc)) !== false && !$a_defaults[$p])
2464        $a_defaults[$p] = $folder;
2465      else
2466        $a_out[] = $folder;
2467      }
2468
2469    natcasesort($a_out);
2470    ksort($a_defaults);
2471   
2472    return array_merge($a_defaults, $a_out);
2473    }
2474
2475  /**
2476   * @access private
2477   */
2478  function _uid2id($uid, $mbox_name=NULL)
2479    {
2480    if (!$mbox_name)
2481      $mbox_name = $this->mailbox;
2482     
2483    if (!isset($this->uid_id_map[$mbox_name][$uid]))
2484      $this->uid_id_map[$mbox_name][$uid] = iil_C_UID2ID($this->conn, $mbox_name, $uid);
2485
2486    return $this->uid_id_map[$mbox_name][$uid];
2487    }
2488
2489  /**
2490   * @access private
2491   */
2492  function _id2uid($id, $mbox_name=NULL)
2493    {
2494    if (!$mbox_name)
2495      $mbox_name = $this->mailbox;
2496     
2497    $index = array_flip((array)$this->uid_id_map[$mbox_name]);
2498    if (isset($index[$id]))
2499      $uid = $index[$id];
2500    else
2501      {
2502      $uid = iil_C_ID2UID($this->conn, $mbox_name, $id);
2503      $this->uid_id_map[$mbox_name][$uid] = $id;
2504      }
2505   
2506    return $uid;
2507    }
2508
2509
2510  /**
2511   * Parse string or array of server capabilities and put them in internal array
2512   * @access private
2513   */
2514  function _parse_capability($caps)
2515    {
2516    if (!is_array($caps))
2517      $cap_arr = explode(' ', $caps);
2518    else
2519      $cap_arr = $caps;
2520   
2521    foreach ($cap_arr as $cap)
2522      {
2523      if ($cap=='CAPABILITY')
2524        continue;
2525
2526      if (strpos($cap, '=')>0)
2527        {
2528        list($key, $value) = explode('=', $cap);
2529        if (!is_array($this->capabilities[$key]))
2530          $this->capabilities[$key] = array();
2531         
2532        $this->capabilities[$key][] = $value;
2533        }
2534      else
2535        $this->capabilities[$cap] = TRUE;
2536      }
2537    }
2538
2539
2540  /**
2541   * Subscribe/unsubscribe a list of mailboxes and update local cache
2542   * @access private
2543   */
2544  function _change_subscription($a_mboxes, $mode)
2545    {
2546    $updated = FALSE;
2547   
2548    if (is_array($a_mboxes))
2549      foreach ($a_mboxes as $i => $mbox_name)
2550        {
2551        $mailbox = $this->_mod_mailbox($mbox_name);
2552        $a_mboxes[$i] = $mailbox;
2553
2554        if ($mode=='subscribe')
2555          $result = iil_C_Subscribe($this->conn, $mailbox);
2556        else if ($mode=='unsubscribe')
2557          $result = iil_C_UnSubscribe($this->conn, $mailbox);
2558
2559        if ($result>=0)
2560          $updated = TRUE;
2561        }
2562       
2563    // get cached mailbox list   
2564    if ($updated)
2565      {
2566      $a_mailbox_cache = $this->get_cache('mailboxes');
2567      if (!is_array($a_mailbox_cache))
2568        return $updated;
2569
2570      // modify cached list
2571      if ($mode=='subscribe')
2572        $a_mailbox_cache = array_merge($a_mailbox_cache, $a_mboxes);
2573      else if ($mode=='unsubscribe')
2574        $a_mailbox_cache = array_diff($a_mailbox_cache, $a_mboxes);
2575       
2576      // write mailboxlist to cache
2577      $this->update_cache('mailboxes', $this->_sort_mailbox_list($a_mailbox_cache));
2578      }
2579
2580    return $updated;
2581    }
2582
2583
2584  /**
2585   * Increde/decrese messagecount for a specific mailbox
2586   * @access private
2587   */
2588  function _set_messagecount($mbox_name, $mode, $increment)
2589    {
2590    $a_mailbox_cache = FALSE;
2591    $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
2592    $mode = strtoupper($mode);
2593
2594    $a_mailbox_cache = $this->get_cache('messagecount');
2595   
2596    if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
2597      return FALSE;
2598   
2599    // add incremental value to messagecount
2600    $a_mailbox_cache[$mailbox][$mode] += $increment;
2601   
2602    // there's something wrong, delete from cache
2603    if ($a_mailbox_cache[$mailbox][$mode] < 0)
2604      unset($a_mailbox_cache[$mailbox][$mode]);
2605
2606    // write back to cache
2607    $this->update_cache('messagecount', $a_mailbox_cache);
2608   
2609    return TRUE;
2610    }
2611
2612
2613  /**
2614   * Remove messagecount of a specific mailbox from cache
2615   * @access private
2616   */
2617  function _clear_messagecount($mbox_name='')
2618    {
2619    $a_mailbox_cache = FALSE;
2620    $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
2621
2622    $a_mailbox_cache = $this->get_cache('messagecount');
2623
2624    if (is_array($a_mailbox_cache[$mailbox]))
2625      {
2626      unset($a_mailbox_cache[$mailbox]);
2627      $this->update_cache('messagecount', $a_mailbox_cache);
2628      }
2629    }
2630
2631
2632  /**
2633   * Split RFC822 header string into an associative array
2634   * @access private
2635   */
2636  function _parse_headers($headers)
2637    {
2638    $a_headers = array();
2639    $lines = explode("\n", $headers);
2640    $c = count($lines);
2641    for ($i=0; $i<$c; $i++)
2642      {
2643      if ($p = strpos($lines[$i], ': '))
2644        {
2645        $field = strtolower(substr($lines[$i], 0, $p));
2646        $value = trim(substr($lines[$i], $p+1));
2647        if (!empty($value))
2648          $a_headers[$field] = $value;
2649        }
2650      }
2651   
2652    return $a_headers;
2653    }
2654
2655
2656  /**
2657   * @access private
2658   */
2659  function _parse_address_list($str, $decode=true)
2660    {
2661    // remove any newlines and carriage returns before
2662    $a = $this->_explode_quoted_string('[,;]', preg_replace( "/[\r\n]/", " ", $str));
2663    $result = array();
2664   
2665    foreach ($a as $key => $val)
2666      {
2667      $val = preg_replace("/([\"\w])</", "$1 <", $val);
2668      $sub_a = $this->_explode_quoted_string(' ', $decode ? $this->decode_header($val) : $val);
2669      $result[$key]['name'] = '';
2670
2671      foreach ($sub_a as $k => $v)
2672        {
2673        if (strpos($v, '@') > 0)
2674          $result[$key]['address'] = str_replace('<', '', str_replace('>', '', $v));
2675        else
2676          $result[$key]['name'] .= (empty($result[$key]['name'])?'':' ').str_replace("\"",'',stripslashes($v));
2677        }
2678       
2679      if (empty($result[$key]['name']))
2680        $result[$key]['name'] = $result[$key]['address'];       
2681      }
2682   
2683    return $result;
2684    }
2685
2686
2687  /**
2688   * @access private
2689   */
2690  function _explode_quoted_string($delimiter, $string)
2691    {
2692    $result = array();
2693    $strlen = strlen($string);
2694    for ($q=$p=$i=0; $i < $strlen; $i++)
2695    {
2696      if ($string{$i} == "\"" && $string{$i-1} != "\\")
2697        $q = $q ? false : true;
2698      else if (!$q && preg_match("/$delimiter/", $string{$i}))
2699      {
2700        $result[] = substr($string, $p, $i - $p);
2701        $p = $i + 1;
2702      }
2703    }
2704   
2705    $result[] = substr($string, $p);
2706    return $result;
2707    }
2708
2709}  // end class rcube_imap
2710
2711
2712/**
2713 * Class representing a message part
2714 *
2715 * @package Mail
2716 */
2717class rcube_message_part
2718{
2719  var $mime_id = '';
2720  var $ctype_primary = 'text';
2721  var $ctype_secondary = 'plain';
2722  var $mimetype = 'text/plain';
2723  var $disposition = '';
2724  var $filename = '';
2725  var $encoding = '8bit';
2726  var $charset = '';
2727  var $size = 0;
2728  var $headers = array();
2729  var $d_parameters = array();
2730  var $ctype_parameters = array();
2731
2732}
2733
2734
2735/**
2736 * Class for sorting an array of iilBasicHeader objects in a predetermined order.
2737 *
2738 * @package Mail
2739 * @author Eric Stadtherr
2740 */
2741class rcube_header_sorter
2742{
2743   var $sequence_numbers = array();
2744   
2745   /**
2746    * Set the predetermined sort order.
2747    *
2748    * @param array Numerically indexed array of IMAP message sequence numbers
2749    */
2750   function set_sequence_numbers($seqnums)
2751   {
2752      $this->sequence_numbers = $seqnums;
2753   }
2754 
2755   /**
2756    * Sort the array of header objects
2757    *
2758    * @param array Array of iilBasicHeader objects indexed by UID
2759    */
2760   function sort_headers(&$headers)
2761   {
2762      /*
2763       * uksort would work if the keys were the sequence number, but unfortunately
2764       * the keys are the UIDs.  We'll use uasort instead and dereference the value
2765       * to get the sequence number (in the "id" field).
2766       *
2767       * uksort($headers, array($this, "compare_seqnums"));
2768       */
2769       uasort($headers, array($this, "compare_seqnums"));
2770   }
2771 
2772   /**
2773    * Get the position of a message sequence number in my sequence_numbers array
2774    *
2775    * @param int Message sequence number contained in sequence_numbers
2776    * @return int Position, -1 if not found
2777    */
2778   function position_of($seqnum)
2779   {
2780      $c = count($this->sequence_numbers);
2781      for ($pos = 0; $pos <= $c; $pos++)
2782      {
2783         if ($this->sequence_numbers[$pos] == $seqnum)
2784            return $pos;
2785      }
2786      return -1;
2787   }
2788 
2789   /**
2790    * Sort method called by uasort()
2791    */
2792   function compare_seqnums($a, $b)
2793   {
2794      // First get the sequence number from the header object (the 'id' field).
2795      $seqa = $a->id;
2796      $seqb = $b->id;
2797     
2798      // then find each sequence number in my ordered list
2799      $posa = $this->position_of($seqa);
2800      $posb = $this->position_of($seqb);
2801     
2802      // return the relative position as the comparison value
2803      $ret = $posa - $posb;
2804      return $ret;
2805   }
2806}
2807
2808
2809/**
2810 * Add quoted-printable encoding to a given string
2811 *
2812 * @param string   String to encode
2813 * @param int      Add new line after this number of characters
2814 * @param boolean  True if spaces should be converted into =20
2815 * @return string Encoded string
2816 */
2817function quoted_printable_encode($input, $line_max=76, $space_conv=false)
2818  {
2819  $hex = array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F');
2820  $lines = preg_split("/(?:\r\n|\r|\n)/", $input);
2821  $eol = "\r\n";
2822  $escape = "=";
2823  $output = "";
2824
2825  while( list(, $line) = each($lines))
2826    {
2827    //$line = rtrim($line); // remove trailing white space -> no =20\r\n necessary
2828    $linlen = strlen($line);
2829    $newline = "";
2830    for($i = 0; $i < $linlen; $i++)
2831      {
2832      $c = substr( $line, $i, 1 );
2833      $dec = ord( $c );
2834      if ( ( $i == 0 ) && ( $dec == 46 ) ) // convert first point in the line into =2E
2835        {
2836        $c = "=2E";
2837        }
2838      if ( $dec == 32 )
2839        {
2840        if ( $i == ( $linlen - 1 ) ) // convert space at eol only
2841          {
2842          $c = "=20";
2843          }
2844        else if ( $space_conv )
2845          {
2846          $c = "=20";
2847          }
2848        }
2849      else if ( ($dec == 61) || ($dec < 32 ) || ($dec > 126) )  // always encode "\t", which is *not* required
2850        {
2851        $h2 = floor($dec/16);
2852        $h1 = floor($dec%16);
2853        $c = $escape.$hex["$h2"].$hex["$h1"];
2854        }
2855         
2856      if ( (strlen($newline) + strlen($c)) >= $line_max )  // CRLF is not counted
2857        {
2858        $output .= $newline.$escape.$eol; // soft line break; " =\r\n" is okay
2859        $newline = "";
2860        // check if newline first character will be point or not
2861        if ( $dec == 46 )
2862          {
2863          $c = "=2E";
2864          }
2865        }
2866      $newline .= $c;
2867      } // end of for
2868    $output .= $newline.$eol;
2869    } // end of while
2870
2871  return trim($output);
2872  }
2873
2874
2875?>
Note: See TracBrowser for help on using the repository browser.