source: subversion/trunk/roundcubemail/program/include/rcube_imap.php @ 1444

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