source: github/program/include/rcube_imap.php @ be7d3b6

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