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

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