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

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