source: subversion/trunk/roundcubemail/program/include/rcube_imap.inc @ 882

Last change on this file since 882 was 882, checked in by thomasb, 6 years ago

Show appropriate warning on connection error

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 76.1 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.36
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 ($is_uid)
971        $this->uid_id_map[$mbox_name][$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($msg_id, NULL, FALSE);
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      iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, $part, ($o_part->encoding=='base64'?3:2));
1205      $body = TRUE;
1206      }
1207    else
1208      {
1209      $body = iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, $part, 1);
1210
1211      // decode part body
1212      if ($o_part->encoding=='base64' || $o_part->encoding=='quoted-printable')
1213        $body = $this->mime_decode($body, $o_part->encoding);
1214
1215      // convert charset (if text or message part)
1216      if ($o_part->ctype_primary=='text' || $o_part->ctype_primary=='message')
1217        {
1218        // assume ISO-8859-1 if no charset specified
1219        if (empty($o_part->charset))
1220          $o_part->charset = 'ISO-8859-1';
1221
1222        $body = rcube_charset_convert($body, $o_part->charset);
1223        }
1224      }
1225
1226    return $body;
1227    }
1228
1229
1230  /**
1231   * Fetch message body of a specific message from the server
1232   *
1233   * @param  int    Message UID
1234   * @return string Message/part body
1235   * @see    rcube_imap::get_message_part()
1236   */
1237  function &get_body($uid, $part=1)
1238    {
1239    return $this->get_message_part($uid, $part);
1240    }
1241
1242
1243  /**
1244   * Returns the whole message source as string
1245   *
1246   * @param int  Message UID
1247   * @return string Message source string
1248   */
1249  function &get_raw_body($uid)
1250    {
1251    if (!($msg_id = $this->_uid2id($uid)))
1252      return FALSE;
1253
1254    $body = iil_C_FetchPartHeader($this->conn, $this->mailbox, $msg_id, NULL);
1255    $body .= iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, NULL, 1);
1256
1257    return $body;   
1258    }
1259   
1260
1261  /**
1262   * Sends the whole message source to stdout
1263   *
1264   * @param int  Message UID
1265   */
1266  function print_raw_body($uid)
1267    {
1268    if (!($msg_id = $this->_uid2id($uid)))
1269      return FALSE;
1270
1271    print iil_C_FetchPartHeader($this->conn, $this->mailbox, $msg_id, NULL);
1272    flush();
1273    iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, NULL, 2);
1274    }
1275
1276
1277  /**
1278   * Set message flag to one or several messages
1279   *
1280   * @param mixed  Message UIDs as array or as comma-separated string
1281   * @param string Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT
1282   * @return boolean True on success, False on failure
1283   */
1284  function set_flag($uids, $flag)
1285    {
1286    $flag = strtoupper($flag);
1287    $msg_ids = array();
1288    if (!is_array($uids))
1289      $uids = explode(',',$uids);
1290     
1291    foreach ($uids as $uid) {
1292      $msg_ids[$uid] = $this->_uid2id($uid);
1293    }
1294     
1295    if ($flag=='UNDELETED')
1296      $result = iil_C_Undelete($this->conn, $this->mailbox, join(',', array_values($msg_ids)));
1297    else if ($flag=='UNSEEN')
1298      $result = iil_C_Unseen($this->conn, $this->mailbox, join(',', array_values($msg_ids)));
1299    else
1300      $result = iil_C_Flag($this->conn, $this->mailbox, join(',', array_values($msg_ids)), $flag);
1301
1302    // reload message headers if cached
1303    $cache_key = $this->mailbox.'.msg';
1304    if ($this->caching_enabled)
1305      {
1306      foreach ($msg_ids as $uid => $id)
1307        {
1308        if ($cached_headers = $this->get_cached_message($cache_key, $uid))
1309          {
1310          $this->remove_message_cache($cache_key, $id);
1311          //$this->get_headers($uid);
1312          }
1313        }
1314
1315      // close and re-open connection
1316      // this prevents connection problems with Courier
1317      $this->reconnect();
1318      }
1319
1320    // set nr of messages that were flaged
1321    $count = count($msg_ids);
1322
1323    // clear message count cache
1324    if ($result && $flag=='SEEN')
1325      $this->_set_messagecount($this->mailbox, 'UNSEEN', $count*(-1));
1326    else if ($result && $flag=='UNSEEN')
1327      $this->_set_messagecount($this->mailbox, 'UNSEEN', $count);
1328    else if ($result && $flag=='DELETED')
1329      $this->_set_messagecount($this->mailbox, 'ALL', $count*(-1));
1330
1331    return $result;
1332    }
1333
1334
1335  /**
1336   * Append a mail message (source) to a specific mailbox
1337   *
1338   * @param string Target mailbox
1339   * @param string Message source
1340   * @return boolean True on success, False on error
1341   */
1342  function save_message($mbox_name, &$message)
1343    {
1344    $mbox_name = stripslashes($mbox_name);
1345    $mailbox = $this->_mod_mailbox($mbox_name);
1346
1347    // make sure mailbox exists
1348    if (in_array($mailbox, $this->_list_mailboxes()))
1349      $saved = iil_C_Append($this->conn, $mailbox, $message);
1350
1351    if ($saved)
1352      {
1353      // increase messagecount of the target mailbox
1354      $this->_set_messagecount($mailbox, 'ALL', 1);
1355      }
1356         
1357    return $saved;
1358    }
1359
1360
1361  /**
1362   * Move a message from one mailbox to another
1363   *
1364   * @param string List of UIDs to move, separated by comma
1365   * @param string Target mailbox
1366   * @param string Source mailbox
1367   * @return boolean True on success, False on error
1368   */
1369  function move_message($uids, $to_mbox, $from_mbox='')
1370    {
1371    $to_mbox = stripslashes($to_mbox);
1372    $from_mbox = stripslashes($from_mbox);
1373    $to_mbox = $this->_mod_mailbox($to_mbox);
1374    $from_mbox = $from_mbox ? $this->_mod_mailbox($from_mbox) : $this->mailbox;
1375
1376    // make sure mailbox exists
1377    if (!in_array($to_mbox, $this->_list_mailboxes()))
1378      {
1379      if (in_array($to_mbox, $this->default_folders))
1380        $this->create_mailbox($to_mbox, TRUE);
1381      else
1382        return FALSE;
1383      }
1384
1385    // convert the list of uids to array
1386    $a_uids = is_string($uids) ? explode(',', $uids) : (is_array($uids) ? $uids : NULL);
1387   
1388    // exit if no message uids are specified
1389    if (!is_array($a_uids))
1390      return false;
1391
1392    // convert uids to message ids
1393    $a_mids = array();
1394    foreach ($a_uids as $uid)
1395      $a_mids[] = $this->_uid2id($uid, $from_mbox);
1396
1397    $iil_move = iil_C_Move($this->conn, join(',', $a_mids), $from_mbox, $to_mbox);
1398    $moved = !($iil_move === false || $iil_move < 0);
1399   
1400    // send expunge command in order to have the moved message
1401    // really deleted from the source mailbox
1402    if ($moved)
1403      {
1404      $this->_expunge($from_mbox, FALSE);
1405      $this->_clear_messagecount($from_mbox);
1406      $this->_clear_messagecount($to_mbox);
1407      }
1408     
1409    // remove message ids from search set
1410    if ($moved && $this->search_set && $from_mbox == $this->mailbox)
1411      $this->search_set = array_diff($this->search_set, $a_mids);
1412
1413    // update cached message headers
1414    $cache_key = $from_mbox.'.msg';
1415    if ($moved && ($a_cache_index = $this->get_message_cache_index($cache_key)))
1416      {
1417      $start_index = 100000;
1418      foreach ($a_uids as $uid)
1419        {
1420        if (($index = array_search($uid, $a_cache_index)) !== FALSE)
1421          $start_index = min($index, $start_index);
1422        }
1423
1424      // clear cache from the lowest index on
1425      $this->clear_message_cache($cache_key, $start_index);
1426      }
1427
1428    return $moved;
1429    }
1430
1431
1432  /**
1433   * Mark messages as deleted and expunge mailbox
1434   *
1435   * @param string List of UIDs to move, separated by comma
1436   * @param string Source mailbox
1437   * @return boolean True on success, False on error
1438   */
1439  function delete_message($uids, $mbox_name='')
1440    {
1441    $mbox_name = stripslashes($mbox_name);
1442    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
1443
1444    // convert the list of uids to array
1445    $a_uids = is_string($uids) ? explode(',', $uids) : (is_array($uids) ? $uids : NULL);
1446   
1447    // exit if no message uids are specified
1448    if (!is_array($a_uids))
1449      return false;
1450
1451
1452    // convert uids to message ids
1453    $a_mids = array();
1454    foreach ($a_uids as $uid)
1455      $a_mids[] = $this->_uid2id($uid, $mailbox);
1456       
1457    $deleted = iil_C_Delete($this->conn, $mailbox, join(',', $a_mids));
1458   
1459    // send expunge command in order to have the deleted message
1460    // really deleted from the mailbox
1461    if ($deleted)
1462      {
1463      $this->_expunge($mailbox, FALSE);
1464      $this->_clear_messagecount($mailbox);
1465      }
1466
1467    // remove message ids from search set
1468    if ($moved && $this->search_set && $mailbox == $this->mailbox)
1469      $this->search_set = array_diff($this->search_set, $a_mids);
1470
1471    // remove deleted messages from cache
1472    $cache_key = $mailbox.'.msg';
1473    if ($deleted && ($a_cache_index = $this->get_message_cache_index($cache_key)))
1474      {
1475      $start_index = 100000;
1476      foreach ($a_uids as $uid)
1477        {
1478        if (($index = array_search($uid, $a_cache_index)) !== FALSE)
1479          $start_index = min($index, $start_index);
1480        }
1481
1482      // clear cache from the lowest index on
1483      $this->clear_message_cache($cache_key, $start_index);
1484      }
1485
1486    return $deleted;
1487    }
1488
1489
1490  /**
1491   * Clear all messages in a specific mailbox
1492   *
1493   * @param string Mailbox name
1494   * @return int Above 0 on success
1495   */
1496  function clear_mailbox($mbox_name=NULL)
1497    {
1498    $mbox_name = stripslashes($mbox_name);
1499    $mailbox = !empty($mbox_name) ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
1500    $msg_count = $this->_messagecount($mailbox, 'ALL');
1501   
1502    if ($msg_count>0)
1503      {
1504      $cleared = iil_C_ClearFolder($this->conn, $mailbox);
1505     
1506      // make sure the message count cache is cleared as well
1507      if ($cleared)
1508        {
1509        $this->clear_message_cache($mailbox.'.msg');     
1510        $a_mailbox_cache = $this->get_cache('messagecount');
1511        unset($a_mailbox_cache[$mailbox]);
1512        $this->update_cache('messagecount', $a_mailbox_cache);
1513        }
1514       
1515      return $cleared;
1516      }
1517    else
1518      return 0;
1519    }
1520
1521
1522  /**
1523   * Send IMAP expunge command and clear cache
1524   *
1525   * @param string Mailbox name
1526   * @param boolean False if cache should not be cleared
1527   * @return boolean True on success
1528   */
1529  function expunge($mbox_name='', $clear_cache=TRUE)
1530    {
1531    $mbox_name = stripslashes($mbox_name);
1532    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
1533    return $this->_expunge($mailbox, $clear_cache);
1534    }
1535
1536
1537  /**
1538   * Send IMAP expunge command and clear cache
1539   *
1540   * @see rcube_imap::expunge()
1541   * @access private
1542   */
1543  function _expunge($mailbox, $clear_cache=TRUE)
1544    {
1545    $result = iil_C_Expunge($this->conn, $mailbox);
1546
1547    if ($result>=0 && $clear_cache)
1548      {
1549      $this->clear_message_cache($mailbox.'.msg');
1550      $this->_clear_messagecount($mailbox);
1551      }
1552     
1553    return $result;
1554    }
1555
1556
1557  /* --------------------------------
1558   *        folder managment
1559   * --------------------------------*/
1560
1561
1562  /**
1563   * Get a list of all folders available on the IMAP server
1564   *
1565   * @param string IMAP root dir
1566   * @return array Indexed array with folder names
1567   */
1568  function list_unsubscribed($root='')
1569    {
1570    static $sa_unsubscribed;
1571   
1572    if (is_array($sa_unsubscribed))
1573      return $sa_unsubscribed;
1574     
1575    // retrieve list of folders from IMAP server
1576    $a_mboxes = iil_C_ListMailboxes($this->conn, $this->_mod_mailbox($root), '*');
1577
1578    // modify names with root dir
1579    foreach ($a_mboxes as $mbox_name)
1580      {
1581      $name = $this->_mod_mailbox($mbox_name, 'out');
1582      if (strlen($name))
1583        $a_folders[] = $name;
1584      }
1585
1586    // filter folders and sort them
1587    $sa_unsubscribed = $this->_sort_mailbox_list($a_folders);
1588    return $sa_unsubscribed;
1589    }
1590
1591
1592  /**
1593   * Get mailbox quota information
1594   * added by Nuny
1595   *
1596   * @return mixed Quota info or False if not supported
1597   */
1598  function get_quota()
1599    {
1600    if ($this->get_capability('QUOTA'))
1601      return iil_C_GetQuota($this->conn);
1602       
1603    return FALSE;
1604    }
1605
1606
1607  /**
1608   * Subscribe to a specific mailbox(es)
1609   *
1610   * @param string Mailbox name(s)
1611   * @return boolean True on success
1612   */
1613  function subscribe($mbox_name)
1614    {
1615    if (is_array($mbox_name))
1616      $a_mboxes = $mbox_name;
1617    else if (is_string($mbox_name) && strlen($mbox_name))
1618      $a_mboxes = explode(',', $mbox_name);
1619   
1620    // let this common function do the main work
1621    return $this->_change_subscription($a_mboxes, 'subscribe');
1622    }
1623
1624
1625  /**
1626   * Unsubscribe mailboxes
1627   *
1628   * @param string Mailbox name(s)
1629   * @return boolean True on success
1630   */
1631  function unsubscribe($mbox_name)
1632    {
1633    if (is_array($mbox_name))
1634      $a_mboxes = $mbox_name;
1635    else if (is_string($mbox_name) && strlen($mbox_name))
1636      $a_mboxes = explode(',', $mbox_name);
1637
1638    // let this common function do the main work
1639    return $this->_change_subscription($a_mboxes, 'unsubscribe');
1640    }
1641
1642
1643  /**
1644   * Create a new mailbox on the server and register it in local cache
1645   *
1646   * @param string  New mailbox name (as utf-7 string)
1647   * @param boolean True if the new mailbox should be subscribed
1648   * @param string  Name of the created mailbox, false on error
1649   */
1650  function create_mailbox($name, $subscribe=FALSE)
1651    {
1652    $result = FALSE;
1653   
1654    // replace backslashes
1655    $name = preg_replace('/[\\\]+/', '-', $name);
1656
1657    // reduce mailbox name to 100 chars
1658    $name = substr($name, 0, 100);
1659
1660    $abs_name = $this->_mod_mailbox($name);
1661    $a_mailbox_cache = $this->get_cache('mailboxes');
1662
1663    if (strlen($abs_name) && (!is_array($a_mailbox_cache) || !in_array($abs_name, $a_mailbox_cache)))
1664      $result = iil_C_CreateFolder($this->conn, $abs_name);
1665
1666    // try to subscribe it
1667    if ($result && $subscribe)
1668      $this->subscribe($name);
1669
1670    return $result ? $name : FALSE;
1671    }
1672
1673
1674  /**
1675   * Set a new name to an existing mailbox
1676   *
1677   * @param string Mailbox to rename (as utf-7 string)
1678   * @param string New mailbox name (as utf-7 string)
1679   * @return string Name of the renames mailbox, False on error
1680   */
1681  function rename_mailbox($mbox_name, $new_name)
1682    {
1683    $result = FALSE;
1684
1685    // replace backslashes
1686    $name = preg_replace('/[\\\]+/', '-', $new_name);
1687       
1688    // encode mailbox name and reduce it to 100 chars
1689    $name = substr($new_name, 0, 100);
1690
1691    // make absolute path
1692    $mailbox = $this->_mod_mailbox($mbox_name);
1693    $abs_name = $this->_mod_mailbox($name);
1694   
1695    // check if mailbox is subscribed
1696    $a_subscribed = $this->_list_mailboxes();
1697    $subscribed = in_array($mailbox, $a_subscribed);
1698   
1699    // unsubscribe folder
1700    if ($subscribed)
1701      iil_C_UnSubscribe($this->conn, $mailbox);
1702
1703    if (strlen($abs_name))
1704      $result = iil_C_RenameFolder($this->conn, $mailbox, $abs_name);
1705
1706    // check if mailbox children are subscribed
1707    foreach ($a_subscribed as $c_subscribed)
1708      if (preg_match('/^'.preg_quote($mailbox.$this->delimiter).'/', $c_subscribed))
1709        {
1710        iil_C_UnSubscribe($this->conn, $c_subscribed);
1711        iil_C_Subscribe($this->conn, preg_replace('/^'.preg_quote($mailbox).'/', $abs_name, $c_subscribed));
1712        }
1713
1714    // clear cache
1715    if ($result)
1716      {
1717      $this->clear_message_cache($mailbox.'.msg');
1718      $this->clear_cache('mailboxes');     
1719      }
1720
1721    // try to subscribe it
1722    if ($result && $subscribed)
1723      iil_C_Subscribe($this->conn, $abs_name);
1724
1725    return $result ? $name : FALSE;
1726    }
1727
1728
1729  /**
1730   * Remove mailboxes from server
1731   *
1732   * @param string Mailbox name
1733   * @return boolean True on success
1734   */
1735  function delete_mailbox($mbox_name)
1736    {
1737    $deleted = FALSE;
1738
1739    if (is_array($mbox_name))
1740      $a_mboxes = $mbox_name;
1741    else if (is_string($mbox_name) && strlen($mbox_name))
1742      $a_mboxes = explode(',', $mbox_name);
1743
1744    $all_mboxes = iil_C_ListMailboxes($this->conn, $this->_mod_mailbox($root), '*');
1745
1746    if (is_array($a_mboxes))
1747      foreach ($a_mboxes as $mbox_name)
1748        {
1749        $mailbox = $this->_mod_mailbox($mbox_name);
1750
1751        // unsubscribe mailbox before deleting
1752        iil_C_UnSubscribe($this->conn, $mailbox);
1753
1754        // send delete command to server
1755        $result = iil_C_DeleteFolder($this->conn, $mailbox);
1756        if ($result>=0)
1757          $deleted = TRUE;
1758
1759        foreach ($all_mboxes as $c_mbox)
1760          if (preg_match('/^'.preg_quote($mailbox.$this->delimiter).'/', $c_mbox))
1761            {
1762            iil_C_UnSubscribe($this->conn, $c_mbox);
1763            $result = iil_C_DeleteFolder($this->conn, $c_mbox);
1764            if ($result>=0)
1765              $deleted = TRUE;
1766            }
1767        }
1768
1769    // clear mailboxlist cache
1770    if ($deleted)
1771      {
1772      $this->clear_message_cache($mailbox.'.msg');
1773      $this->clear_cache('mailboxes');
1774      }
1775
1776    return $deleted;
1777    }
1778
1779
1780  /**
1781   * Create all folders specified as default
1782   */
1783  function create_default_folders()
1784    {
1785    $a_folders = iil_C_ListMailboxes($this->conn, $this->_mod_mailbox(''), '*');
1786    $a_subscribed = iil_C_ListSubscribed($this->conn, $this->_mod_mailbox(''), '*');
1787   
1788    // create default folders if they do not exist
1789    foreach ($this->default_folders as $folder)
1790      {
1791      $abs_name = $this->_mod_mailbox($folder);
1792      if (!in_array_nocase($abs_name, $a_folders))
1793        $this->create_mailbox($folder, TRUE);
1794      else if (!in_array_nocase($abs_name, $a_subscribed))
1795        $this->subscribe($folder);
1796      }
1797    }
1798
1799
1800
1801  /* --------------------------------
1802   *   internal caching methods
1803   * --------------------------------*/
1804
1805  /**
1806   * @access private
1807   */
1808  function set_caching($set)
1809    {
1810    if ($set && is_object($this->db))
1811      $this->caching_enabled = TRUE;
1812    else
1813      $this->caching_enabled = FALSE;
1814    }
1815
1816  /**
1817   * @access private
1818   */
1819  function get_cache($key)
1820    {
1821    // read cache
1822    if (!isset($this->cache[$key]) && $this->caching_enabled)
1823      {
1824      $cache_data = $this->_read_cache_record('IMAP.'.$key);
1825      $this->cache[$key] = strlen($cache_data) ? unserialize($cache_data) : FALSE;
1826      }
1827   
1828    return $this->cache[$key];
1829    }
1830
1831  /**
1832   * @access private
1833   */
1834  function update_cache($key, $data)
1835    {
1836    $this->cache[$key] = $data;
1837    $this->cache_changed = TRUE;
1838    $this->cache_changes[$key] = TRUE;
1839    }
1840
1841  /**
1842   * @access private
1843   */
1844  function write_cache()
1845    {
1846    if ($this->caching_enabled && $this->cache_changed)
1847      {
1848      foreach ($this->cache as $key => $data)
1849        {
1850        if ($this->cache_changes[$key])
1851          $this->_write_cache_record('IMAP.'.$key, serialize($data));
1852        }
1853      }   
1854    }
1855
1856  /**
1857   * @access private
1858   */
1859  function clear_cache($key=NULL)
1860    {
1861    if ($key===NULL)
1862      {
1863      foreach ($this->cache as $key => $data)
1864        $this->_clear_cache_record('IMAP.'.$key);
1865
1866      $this->cache = array();
1867      $this->cache_changed = FALSE;
1868      $this->cache_changes = array();
1869      }
1870    else
1871      {
1872      $this->_clear_cache_record('IMAP.'.$key);
1873      $this->cache_changes[$key] = FALSE;
1874      unset($this->cache[$key]);
1875      }
1876    }
1877
1878  /**
1879   * @access private
1880   */
1881  function _read_cache_record($key)
1882    {
1883    $cache_data = FALSE;
1884   
1885    if ($this->db)
1886      {
1887      // get cached data from DB
1888      $sql_result = $this->db->query(
1889        "SELECT cache_id, data
1890         FROM ".get_table_name('cache')."
1891         WHERE  user_id=?
1892         AND    cache_key=?",
1893        $_SESSION['user_id'],
1894        $key);
1895
1896      if ($sql_arr = $this->db->fetch_assoc($sql_result))
1897        {
1898        $cache_data = $sql_arr['data'];
1899        $this->cache_keys[$key] = $sql_arr['cache_id'];
1900        }
1901      }
1902
1903    return $cache_data;
1904    }
1905
1906  /**
1907   * @access private
1908   */
1909  function _write_cache_record($key, $data)
1910    {
1911    if (!$this->db)
1912      return FALSE;
1913
1914    // check if we already have a cache entry for this key
1915    if (!isset($this->cache_keys[$key]))
1916      {
1917      $sql_result = $this->db->query(
1918        "SELECT cache_id
1919         FROM ".get_table_name('cache')."
1920         WHERE  user_id=?
1921         AND    cache_key=?",
1922        $_SESSION['user_id'],
1923        $key);
1924                                     
1925      if ($sql_arr = $this->db->fetch_assoc($sql_result))
1926        $this->cache_keys[$key] = $sql_arr['cache_id'];
1927      else
1928        $this->cache_keys[$key] = FALSE;
1929      }
1930
1931    // update existing cache record
1932    if ($this->cache_keys[$key])
1933      {
1934      $this->db->query(
1935        "UPDATE ".get_table_name('cache')."
1936         SET    created=".$this->db->now().",
1937                data=?
1938         WHERE  user_id=?
1939         AND    cache_key=?",
1940        $data,
1941        $_SESSION['user_id'],
1942        $key);
1943      }
1944    // add new cache record
1945    else
1946      {
1947      $this->db->query(
1948        "INSERT INTO ".get_table_name('cache')."
1949         (created, user_id, cache_key, data)
1950         VALUES (".$this->db->now().", ?, ?, ?)",
1951        $_SESSION['user_id'],
1952        $key,
1953        $data);
1954      }
1955    }
1956
1957  /**
1958   * @access private
1959   */
1960  function _clear_cache_record($key)
1961    {
1962    $this->db->query(
1963      "DELETE FROM ".get_table_name('cache')."
1964       WHERE  user_id=?
1965       AND    cache_key=?",
1966      $_SESSION['user_id'],
1967      $key);
1968    }
1969
1970
1971
1972  /* --------------------------------
1973   *   message caching methods
1974   * --------------------------------*/
1975   
1976
1977  /**
1978   * Checks if the cache is up-to-date
1979   *
1980   * @param string Mailbox name
1981   * @param string Internal cache key
1982   * @return int -3 = off, -2 = incomplete, -1 = dirty
1983   */
1984  function check_cache_status($mailbox, $cache_key)
1985    {
1986    if (!$this->caching_enabled)
1987      return -3;
1988
1989    $cache_index = $this->get_message_cache_index($cache_key, TRUE);
1990    $msg_count = $this->_messagecount($mailbox);
1991    $cache_count = count($cache_index);
1992
1993    // console("Cache check: $msg_count !== ".count($cache_index));
1994
1995    if ($cache_count==$msg_count)
1996      {
1997      // get highest index
1998      $header = iil_C_FetchHeader($this->conn, $mailbox, "$msg_count");
1999      $cache_uid = array_pop($cache_index);
2000     
2001      // uids of highest message matches -> cache seems OK
2002      if ($cache_uid == $header->uid)
2003        return 1;
2004
2005      // cache is dirty
2006      return -1;
2007      }
2008    // if cache count differs less than 10% report as dirty
2009    else if (abs($msg_count - $cache_count) < $msg_count/10)
2010      return -1;
2011    else
2012      return -2;
2013    }
2014
2015  /**
2016   * @access private
2017   */
2018  function get_message_cache($key, $from, $to, $sort_field, $sort_order)
2019    {
2020    $cache_key = "$key:$from:$to:$sort_field:$sort_order";
2021    $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
2022   
2023    if (!in_array($sort_field, $db_header_fields))
2024      $sort_field = 'idx';
2025   
2026    if ($this->caching_enabled && !isset($this->cache[$cache_key]))
2027      {
2028      $this->cache[$cache_key] = array();
2029      $sql_result = $this->db->limitquery(
2030        "SELECT idx, uid, headers
2031         FROM ".get_table_name('messages')."
2032         WHERE  user_id=?
2033         AND    cache_key=?
2034         ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".
2035         strtoupper($sort_order),
2036        $from,
2037        $to-$from,
2038        $_SESSION['user_id'],
2039        $key);
2040
2041      while ($sql_arr = $this->db->fetch_assoc($sql_result))
2042        {
2043        $uid = $sql_arr['uid'];
2044        $this->cache[$cache_key][$uid] = unserialize($sql_arr['headers']);
2045       
2046        // featch headers if unserialize failed
2047        if (empty($this->cache[$cache_key][$uid]))
2048          $this->cache[$cache_key][$uid] = iil_C_FetchHeader($this->conn, preg_replace('/.msg$/', '', $key), $uid, true);
2049        }
2050      }
2051     
2052    return $this->cache[$cache_key];
2053    }
2054
2055  /**
2056   * @access private
2057   */
2058  function &get_cached_message($key, $uid, $struct=false)
2059    {
2060    if (!$this->caching_enabled)
2061      return FALSE;
2062
2063    $internal_key = '__single_msg';
2064    if ($this->caching_enabled && (!isset($this->cache[$internal_key][$uid]) ||
2065        ($struct && empty($this->cache[$internal_key][$uid]->structure))))
2066      {
2067      $sql_select = "idx, uid, headers" . ($struct ? ", structure" : '');
2068      $sql_result = $this->db->query(
2069        "SELECT $sql_select
2070         FROM ".get_table_name('messages')."
2071         WHERE  user_id=?
2072         AND    cache_key=?
2073         AND    uid=?",
2074        $_SESSION['user_id'],
2075        $key,
2076        $uid);
2077
2078      if ($sql_arr = $this->db->fetch_assoc($sql_result))
2079        {
2080        $this->cache[$internal_key][$uid] = unserialize($sql_arr['headers']);
2081        if (is_object($this->cache[$internal_key][$uid]) && !empty($sql_arr['structure']))
2082          $this->cache[$internal_key][$uid]->structure = unserialize($sql_arr['structure']);
2083        }
2084      }
2085
2086    return $this->cache[$internal_key][$uid];
2087    }
2088
2089  /**
2090   * @access private
2091   */ 
2092  function get_message_cache_index($key, $force=FALSE, $sort_col='idx', $sort_order='ASC')
2093    {
2094    static $sa_message_index = array();
2095   
2096    // empty key -> empty array
2097    if (!$this->caching_enabled || empty($key))
2098      return array();
2099   
2100    if (!empty($sa_message_index[$key]) && !$force)
2101      return $sa_message_index[$key];
2102   
2103    $sa_message_index[$key] = array();
2104    $sql_result = $this->db->query(
2105      "SELECT idx, uid
2106       FROM ".get_table_name('messages')."
2107       WHERE  user_id=?
2108       AND    cache_key=?
2109       ORDER BY ".$this->db->quote_identifier($sort_col)." ".$sort_order,
2110      $_SESSION['user_id'],
2111      $key);
2112
2113    while ($sql_arr = $this->db->fetch_assoc($sql_result))
2114      $sa_message_index[$key][$sql_arr['idx']] = $sql_arr['uid'];
2115     
2116    return $sa_message_index[$key];
2117    }
2118
2119  /**
2120   * @access private
2121   */
2122  function add_message_cache($key, $index, $headers, $struct=null)
2123    {
2124    if (!$this->caching_enabled || empty($key) || !is_object($headers) || empty($headers->uid))
2125      return;
2126     
2127    // check for an existing record (probly headers are cached but structure not)
2128    $sql_result = $this->db->query(
2129        "SELECT message_id
2130         FROM ".get_table_name('messages')."
2131         WHERE  user_id=?
2132         AND    cache_key=?
2133         AND    uid=?
2134         AND    del<>1",
2135        $_SESSION['user_id'],
2136        $key,
2137        $headers->uid);
2138
2139    // update cache record
2140    if ($sql_arr = $this->db->fetch_assoc($sql_result))
2141      {
2142      $this->db->query(
2143        "UPDATE ".get_table_name('messages')."
2144         SET   idx=?, headers=?, structure=?
2145         WHERE message_id=?",
2146        $index,
2147        serialize($headers),
2148        is_object($struct) ? serialize($struct) : NULL,
2149        $sql_arr['message_id']
2150        );
2151      }
2152    else  // insert new record
2153      {
2154      $this->db->query(
2155        "INSERT INTO ".get_table_name('messages')."
2156         (user_id, del, cache_key, created, idx, uid, subject, ".$this->db->quoteIdentifier('from').", ".$this->db->quoteIdentifier('to').", cc, date, size, headers, structure)
2157         VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".$this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
2158        $_SESSION['user_id'],
2159        $key,
2160        $index,
2161        $headers->uid,
2162        (string)substr($this->decode_header($headers->subject, TRUE), 0, 128),
2163        (string)substr($this->decode_header($headers->from, TRUE), 0, 128),
2164        (string)substr($this->decode_header($headers->to, TRUE), 0, 128),
2165        (string)substr($this->decode_header($headers->cc, TRUE), 0, 128),
2166        (int)$headers->size,
2167        serialize($headers),
2168        is_object($struct) ? serialize($struct) : NULL
2169        );
2170      }
2171    }
2172   
2173  /**
2174   * @access private
2175   */
2176  function remove_message_cache($key, $index)
2177    {
2178    $this->db->query(
2179      "DELETE FROM ".get_table_name('messages')."
2180       WHERE  user_id=?
2181       AND    cache_key=?
2182       AND    idx=?",
2183      $_SESSION['user_id'],
2184      $key,
2185      $index);
2186    }
2187
2188  /**
2189   * @access private
2190   */
2191  function clear_message_cache($key, $start_index=1)
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      $start_index);
2201    }
2202
2203
2204
2205
2206  /* --------------------------------
2207   *   encoding/decoding methods
2208   * --------------------------------*/
2209
2210  /**
2211   * Split an address list into a structured array list
2212   *
2213   * @param string  Input string
2214   * @param int     List only this number of addresses
2215   * @param boolean Decode address strings
2216   * @return array  Indexed list of addresses
2217   */
2218  function decode_address_list($input, $max=null, $decode=true)
2219    {
2220    $a = $this->_parse_address_list($input, $decode);
2221    $out = array();
2222   
2223    if (!is_array($a))
2224      return $out;
2225
2226    $c = count($a);
2227    $j = 0;
2228
2229    foreach ($a as $val)
2230      {
2231      $j++;
2232      $address = $val['address'];
2233      $name = preg_replace(array('/^[\'"]/', '/[\'"]$/'), '', trim($val['name']));
2234      if ($name && $address && $name != $address)
2235        $string = sprintf('%s <%s>', strpos($name, ',')!==FALSE ? '"'.$name.'"' : $name, $address);
2236      else if ($address)
2237        $string = $address;
2238      else if ($name)
2239        $string = $name;
2240     
2241      $out[$j] = array('name' => $name,
2242                       'mailto' => $address,
2243                       'string' => $string);
2244             
2245      if ($max && $j==$max)
2246        break;
2247      }
2248   
2249    return $out;
2250    }
2251
2252
2253  /**
2254   * Decode a message header value
2255   *
2256   * @param string  Header value
2257   * @param boolean Remove quotes if necessary
2258   * @return string Decoded string
2259   */
2260  function decode_header($input, $remove_quotes=FALSE)
2261    {
2262    $str = $this->decode_mime_string((string)$input);
2263    if ($str{0}=='"' && $remove_quotes)
2264      $str = str_replace('"', '', $str);
2265   
2266    return $str;
2267    }
2268
2269
2270  /**
2271   * Decode a mime-encoded string to internal charset
2272   *
2273   * @param string  Header value
2274   * @param string  Fallback charset if none specified
2275   * @return string Decoded string
2276   * @static
2277   */
2278  function decode_mime_string($input, $fallback=null)
2279    {
2280    $out = '';
2281
2282    $pos = strpos($input, '=?');
2283    if ($pos !== false)
2284      {
2285      $out = substr($input, 0, $pos);
2286 
2287      $end_cs_pos = strpos($input, "?", $pos+2);
2288      $end_en_pos = strpos($input, "?", $end_cs_pos+1);
2289      $end_pos = strpos($input, "?=", $end_en_pos+1);
2290 
2291      $encstr = substr($input, $pos+2, ($end_pos-$pos-2));
2292      $rest = substr($input, $end_pos+2);
2293
2294      $out .= rcube_imap::_decode_mime_string_part($encstr);
2295      $out .= rcube_imap::decode_mime_string($rest, $fallback);
2296
2297      return $out;
2298      }
2299     
2300    // no encoding information, use fallback
2301    return rcube_charset_convert($input, !empty($fallback) ? $fallback : 'ISO-8859-1');
2302    }
2303
2304
2305  /**
2306   * Decode a part of a mime-encoded string
2307   *
2308   * @access private
2309   */
2310  function _decode_mime_string_part($str)
2311    {
2312    $a = explode('?', $str);
2313    $count = count($a);
2314
2315    // should be in format "charset?encoding?base64_string"
2316    if ($count >= 3)
2317      {
2318      for ($i=2; $i<$count; $i++)
2319        $rest.=$a[$i];
2320
2321      if (($a[1]=="B")||($a[1]=="b"))
2322        $rest = base64_decode($rest);
2323      else if (($a[1]=="Q")||($a[1]=="q"))
2324        {
2325        $rest = str_replace("_", " ", $rest);
2326        $rest = quoted_printable_decode($rest);
2327        }
2328
2329      return rcube_charset_convert($rest, $a[0]);
2330      }
2331    else
2332      return $str;    // we dont' know what to do with this 
2333    }
2334
2335
2336  /**
2337   * Decode a mime part
2338   *
2339   * @param string Input string
2340   * @param string Part encoding
2341   * @return string Decoded string
2342   * @access private
2343   */
2344  function mime_decode($input, $encoding='7bit')
2345    {
2346    switch (strtolower($encoding))
2347      {
2348      case '7bit':
2349        return $input;
2350        break;
2351     
2352      case 'quoted-printable':
2353        return quoted_printable_decode($input);
2354        break;
2355     
2356      case 'base64':
2357        return base64_decode($input);
2358        break;
2359     
2360      default:
2361        return $input;
2362      }
2363    }
2364
2365
2366  /**
2367   * Convert body charset to UTF-8 according to the ctype_parameters
2368   *
2369   * @param string Part body to decode
2370   * @param string Charset to convert from
2371   * @return string Content converted to internal charset
2372   */
2373  function charset_decode($body, $ctype_param)
2374    {
2375    if (is_array($ctype_param) && !empty($ctype_param['charset']))
2376      return rcube_charset_convert($body, $ctype_param['charset']);
2377
2378    // defaults to what is specified in the class header
2379    return rcube_charset_convert($body,  'ISO-8859-1');
2380    }
2381
2382
2383  /**
2384   * Translate UID to message ID
2385   *
2386   * @param int    Message UID
2387   * @param string Mailbox name
2388   * @return int   Message ID
2389   */
2390  function get_id($uid, $mbox_name=NULL)
2391    {
2392      $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
2393      return $this->_uid2id($uid, $mailbox);
2394    }
2395
2396
2397  /**
2398   * Translate message number to UID
2399   *
2400   * @param int    Message ID
2401   * @param string Mailbox name
2402   * @return int   Message UID
2403   */
2404  function get_uid($id,$mbox_name=NULL)
2405    {
2406      $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
2407      return $this->_id2uid($id, $mailbox);
2408    }
2409
2410
2411
2412  /* --------------------------------
2413   *         private methods
2414   * --------------------------------*/
2415
2416
2417  /**
2418   * @access private
2419   */
2420  function _mod_mailbox($mbox_name, $mode='in')
2421    {
2422    if ((!empty($this->root_ns) && $this->root_ns == $mbox_name) || $mbox_name == 'INBOX')
2423      return $mbox_name;
2424
2425    if (!empty($this->root_dir) && $mode=='in')
2426      $mbox_name = $this->root_dir.$this->delimiter.$mbox_name;
2427    else if (strlen($this->root_dir) && $mode=='out')
2428      $mbox_name = substr($mbox_name, strlen($this->root_dir)+1);
2429
2430    return $mbox_name;
2431    }
2432
2433
2434  /**
2435   * Sort mailboxes first by default folders and then in alphabethical order
2436   * @access private
2437   */
2438  function _sort_mailbox_list($a_folders)
2439    {
2440    $a_out = $a_defaults = array();
2441
2442    // find default folders and skip folders starting with '.'
2443    foreach($a_folders as $i => $folder)
2444      {
2445      if ($folder{0}=='.')
2446        continue;
2447
2448      if (($p = array_search(strtolower($folder), $this->default_folders_lc))!==FALSE)
2449        $a_defaults[$p] = $folder;
2450      else
2451        $a_out[] = $folder;
2452      }
2453
2454    natcasesort($a_out);
2455    ksort($a_defaults);
2456   
2457    return array_merge($a_defaults, $a_out);
2458    }
2459
2460  /**
2461   * @access private
2462   */
2463  function _uid2id($uid, $mbox_name=NULL)
2464    {
2465    if (!$mbox_name)
2466      $mbox_name = $this->mailbox;
2467     
2468    if (!isset($this->uid_id_map[$mbox_name][$uid]))
2469      $this->uid_id_map[$mbox_name][$uid] = iil_C_UID2ID($this->conn, $mbox_name, $uid);
2470
2471    return $this->uid_id_map[$mbox_name][$uid];
2472    }
2473
2474  /**
2475   * @access private
2476   */
2477  function _id2uid($id, $mbox_name=NULL)
2478    {
2479    if (!$mbox_name)
2480      $mbox_name = $this->mailbox;
2481     
2482    return iil_C_ID2UID($this->conn, $mbox_name, $id);
2483    }
2484
2485
2486  /**
2487   * Parse string or array of server capabilities and put them in internal array
2488   * @access private
2489   */
2490  function _parse_capability($caps)
2491    {
2492    if (!is_array($caps))
2493      $cap_arr = explode(' ', $caps);
2494    else
2495      $cap_arr = $caps;
2496   
2497    foreach ($cap_arr as $cap)
2498      {
2499      if ($cap=='CAPABILITY')
2500        continue;
2501
2502      if (strpos($cap, '=')>0)
2503        {
2504        list($key, $value) = explode('=', $cap);
2505        if (!is_array($this->capabilities[$key]))
2506          $this->capabilities[$key] = array();
2507         
2508        $this->capabilities[$key][] = $value;
2509        }
2510      else
2511        $this->capabilities[$cap] = TRUE;
2512      }
2513    }
2514
2515
2516  /**
2517   * Subscribe/unsubscribe a list of mailboxes and update local cache
2518   * @access private
2519   */
2520  function _change_subscription($a_mboxes, $mode)
2521    {
2522    $updated = FALSE;
2523   
2524    if (is_array($a_mboxes))
2525      foreach ($a_mboxes as $i => $mbox_name)
2526        {
2527        $mailbox = $this->_mod_mailbox($mbox_name);
2528        $a_mboxes[$i] = $mailbox;
2529
2530        if ($mode=='subscribe')
2531          $result = iil_C_Subscribe($this->conn, $mailbox);
2532        else if ($mode=='unsubscribe')
2533          $result = iil_C_UnSubscribe($this->conn, $mailbox);
2534
2535        if ($result>=0)
2536          $updated = TRUE;
2537        }
2538       
2539    // get cached mailbox list   
2540    if ($updated)
2541      {
2542      $a_mailbox_cache = $this->get_cache('mailboxes');
2543      if (!is_array($a_mailbox_cache))
2544        return $updated;
2545
2546      // modify cached list
2547      if ($mode=='subscribe')
2548        $a_mailbox_cache = array_merge($a_mailbox_cache, $a_mboxes);
2549      else if ($mode=='unsubscribe')
2550        $a_mailbox_cache = array_diff($a_mailbox_cache, $a_mboxes);
2551       
2552      // write mailboxlist to cache
2553      $this->update_cache('mailboxes', $this->_sort_mailbox_list($a_mailbox_cache));
2554      }
2555
2556    return $updated;
2557    }
2558
2559
2560  /**
2561   * Increde/decrese messagecount for a specific mailbox
2562   * @access private
2563   */
2564  function _set_messagecount($mbox_name, $mode, $increment)
2565    {
2566    $a_mailbox_cache = FALSE;
2567    $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
2568    $mode = strtoupper($mode);
2569
2570    $a_mailbox_cache = $this->get_cache('messagecount');
2571   
2572    if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
2573      return FALSE;
2574   
2575    // add incremental value to messagecount
2576    $a_mailbox_cache[$mailbox][$mode] += $increment;
2577   
2578    // there's something wrong, delete from cache
2579    if ($a_mailbox_cache[$mailbox][$mode] < 0)
2580      unset($a_mailbox_cache[$mailbox][$mode]);
2581
2582    // write back to cache
2583    $this->update_cache('messagecount', $a_mailbox_cache);
2584   
2585    return TRUE;
2586    }
2587
2588
2589  /**
2590   * Remove messagecount of a specific mailbox from cache
2591   * @access private
2592   */
2593  function _clear_messagecount($mbox_name='')
2594    {
2595    $a_mailbox_cache = FALSE;
2596    $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
2597
2598    $a_mailbox_cache = $this->get_cache('messagecount');
2599
2600    if (is_array($a_mailbox_cache[$mailbox]))
2601      {
2602      unset($a_mailbox_cache[$mailbox]);
2603      $this->update_cache('messagecount', $a_mailbox_cache);
2604      }
2605    }
2606
2607
2608  /**
2609   * Split RFC822 header string into an associative array
2610   * @access private
2611   */
2612  function _parse_headers($headers)
2613    {
2614    $a_headers = array();
2615    $lines = explode("\n", $headers);
2616    $c = count($lines);
2617    for ($i=0; $i<$c; $i++)
2618      {
2619      if ($p = strpos($lines[$i], ': '))
2620        {
2621        $field = strtolower(substr($lines[$i], 0, $p));
2622        $value = trim(substr($lines[$i], $p+1));
2623        if (!empty($value))
2624          $a_headers[$field] = $value;
2625        }
2626      }
2627   
2628    return $a_headers;
2629    }
2630
2631
2632  /**
2633   * @access private
2634   */
2635  function _parse_address_list($str, $decode=true)
2636    {
2637    // remove any newlines and carriage returns before
2638    $a = $this->_explode_quoted_string('[,;]', preg_replace( "/[\r\n]/", " ", $str));
2639    $result = array();
2640   
2641    foreach ($a as $key => $val)
2642      {
2643      $val = preg_replace("/([\"\w])</", "$1 <", $val);
2644      $sub_a = $this->_explode_quoted_string(' ', $decode ? $this->decode_header($val) : $val);
2645      $result[$key]['name'] = '';
2646
2647      foreach ($sub_a as $k => $v)
2648        {
2649        if (strpos($v, '@') > 0)
2650          $result[$key]['address'] = str_replace('<', '', str_replace('>', '', $v));
2651        else
2652          $result[$key]['name'] .= (empty($result[$key]['name'])?'':' ').str_replace("\"",'',stripslashes($v));
2653        }
2654       
2655      if (empty($result[$key]['name']))
2656        $result[$key]['name'] = $result[$key]['address'];       
2657      }
2658   
2659    return $result;
2660    }
2661
2662
2663  /**
2664   * @access private
2665   */
2666  function _explode_quoted_string($delimiter, $string)
2667    {
2668    $result = array();
2669    $strlen = strlen($string);
2670    for ($q=$p=$i=0; $i < $strlen; $i++)
2671    {
2672      if ($string{$i} == "\"" && $string{$i-1} != "\\")
2673        $q = $q ? false : true;
2674      else if (!$q && preg_match("/$delimiter/", $string{$i}))
2675      {
2676        $result[] = substr($string, $p, $i - $p);
2677        $p = $i + 1;
2678      }
2679    }
2680   
2681    $result[] = substr($string, $p);
2682    return $result;
2683    }
2684
2685}  // end class rcube_imap
2686
2687
2688/**
2689 * Class representing a message part
2690 *
2691 * @package Mail
2692 */
2693class rcube_message_part
2694{
2695  var $mime_id = '';
2696  var $ctype_primary = 'text';
2697  var $ctype_secondary = 'plain';
2698  var $mimetype = 'text/plain';
2699  var $disposition = '';
2700  var $filename = '';
2701  var $encoding = '8bit';
2702  var $charset = '';
2703  var $size = 0;
2704  var $headers = array();
2705  var $d_parameters = array();
2706  var $ctype_parameters = array();
2707
2708}
2709
2710
2711/**
2712 * Class for sorting an array of iilBasicHeader objects in a predetermined order.
2713 *
2714 * @package Mail
2715 * @author Eric Stadtherr
2716 */
2717class rcube_header_sorter
2718{
2719   var $sequence_numbers = array();
2720   
2721   /**
2722    * Set the predetermined sort order.
2723    *
2724    * @param array Numerically indexed array of IMAP message sequence numbers
2725    */
2726   function set_sequence_numbers($seqnums)
2727   {
2728      $this->sequence_numbers = $seqnums;
2729   }
2730 
2731   /**
2732    * Sort the array of header objects
2733    *
2734    * @param array Array of iilBasicHeader objects indexed by UID
2735    */
2736   function sort_headers(&$headers)
2737   {
2738      /*
2739       * uksort would work if the keys were the sequence number, but unfortunately
2740       * the keys are the UIDs.  We'll use uasort instead and dereference the value
2741       * to get the sequence number (in the "id" field).
2742       *
2743       * uksort($headers, array($this, "compare_seqnums"));
2744       */
2745       uasort($headers, array($this, "compare_seqnums"));
2746   }
2747 
2748   /**
2749    * Get the position of a message sequence number in my sequence_numbers array
2750    *
2751    * @param int Message sequence number contained in sequence_numbers
2752    * @return int Position, -1 if not found
2753    */
2754   function position_of($seqnum)
2755   {
2756      $c = count($this->sequence_numbers);
2757      for ($pos = 0; $pos <= $c; $pos++)
2758      {
2759         if ($this->sequence_numbers[$pos] == $seqnum)
2760            return $pos;
2761      }
2762      return -1;
2763   }
2764 
2765   /**
2766    * Sort method called by uasort()
2767    */
2768   function compare_seqnums($a, $b)
2769   {
2770      // First get the sequence number from the header object (the 'id' field).
2771      $seqa = $a->id;
2772      $seqb = $b->id;
2773     
2774      // then find each sequence number in my ordered list
2775      $posa = $this->position_of($seqa);
2776      $posb = $this->position_of($seqb);
2777     
2778      // return the relative position as the comparison value
2779      $ret = $posa - $posb;
2780      return $ret;
2781   }
2782}
2783
2784
2785/**
2786 * Add quoted-printable encoding to a given string
2787 *
2788 * @param string   String to encode
2789 * @param int      Add new line after this number of characters
2790 * @param boolean  True if spaces should be converted into =20
2791 * @return string Encoded string
2792 */
2793function quoted_printable_encode($input, $line_max=76, $space_conv=false)
2794  {
2795  $hex = array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F');
2796  $lines = preg_split("/(?:\r\n|\r|\n)/", $input);
2797  $eol = "\r\n";
2798  $escape = "=";
2799  $output = "";
2800
2801  while( list(, $line) = each($lines))
2802    {
2803    //$line = rtrim($line); // remove trailing white space -> no =20\r\n necessary
2804    $linlen = strlen($line);
2805    $newline = "";
2806    for($i = 0; $i < $linlen; $i++)
2807      {
2808      $c = substr( $line, $i, 1 );
2809      $dec = ord( $c );
2810      if ( ( $i == 0 ) && ( $dec == 46 ) ) // convert first point in the line into =2E
2811        {
2812        $c = "=2E";
2813        }
2814      if ( $dec == 32 )
2815        {
2816        if ( $i == ( $linlen - 1 ) ) // convert space at eol only
2817          {
2818          $c = "=20";
2819          }
2820        else if ( $space_conv )
2821          {
2822          $c = "=20";
2823          }
2824        }
2825      else if ( ($dec == 61) || ($dec < 32 ) || ($dec > 126) )  // always encode "\t", which is *not* required
2826        {
2827        $h2 = floor($dec/16);
2828        $h1 = floor($dec%16);
2829        $c = $escape.$hex["$h2"].$hex["$h1"];
2830        }
2831         
2832      if ( (strlen($newline) + strlen($c)) >= $line_max )  // CRLF is not counted
2833        {
2834        $output .= $newline.$escape.$eol; // soft line break; " =\r\n" is okay
2835        $newline = "";
2836        // check if newline first character will be point or not
2837        if ( $dec == 46 )
2838          {
2839          $c = "=2E";
2840          }
2841        }
2842      $newline .= $c;
2843      } // end of for
2844    $output .= $newline.$eol;
2845    } // end of while
2846
2847  return trim($output);
2848  }
2849
2850
2851?>
Note: See TracBrowser for help on using the repository browser.