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

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