source: subversion/branches/devel-api/program/include/rcube_imap.php @ 2358

Last change on this file since 2358 was 2358, checked in by thomasb, 4 years ago

Add hooks to fetch and display additional headers + use html wrapper functions

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