source: subversion/trunk/roundcubemail/program/include/rcube_imap.inc @ 291

Last change on this file since 291 was 291, checked in by thomasb, 7 years ago

Message sorting: added patch by Eric; fixed check-for-recent confusion

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 57.3 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_imap.inc                                        |
6 |                                                                       |
7 | This file is part of the RoundCube Webmail client                     |
8 | Copyright (C) 2005, 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/utf7.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    RoundCube Webmail
38 * @author     Thomas Bruederli <roundcube@gmail.com>
39 * @version    1.30
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_folders = array('inbox', 'drafts', 'sent', 'junk', 'trash');
56  var $cache = array();
57  var $cache_keys = array(); 
58  var $cache_changes = array();
59  var $uid_id_map = array();
60  var $msg_headers = array();
61  var $capabilities = array();
62  var $skip_deleted = FALSE;
63  var $debug_level = 1;
64
65
66  /**
67   * Object constructor
68   *
69   * @param  object  Database connection
70   */
71  function __construct($db_conn)
72    {
73    $this->db = $db_conn;
74    }
75
76
77  /**
78   * PHP 4 object constructor
79   *
80   * @see  rcube_imap::__construct
81   */
82  function rcube_imap($db_conn)
83    {
84    $this->__construct($db_conn);
85    }
86
87
88  /**
89   * Connect to an IMAP server
90   *
91   * @param  string   Host to connect
92   * @param  string   Username for IMAP account
93   * @param  string   Password for IMAP account
94   * @param  number   Port to connect to
95   * @param  boolean  Use SSL connection
96   * @return boolean  TRUE on success, FALSE on failure
97   * @access public
98   */
99  function connect($host, $user, $pass, $port=143, $use_ssl=FALSE)
100    {
101    global $ICL_SSL, $ICL_PORT, $IMAP_USE_INTERNAL_DATE;
102   
103    // check for Open-SSL support in PHP build
104    if ($use_ssl && in_array('openssl', get_loaded_extensions()))
105      $ICL_SSL = TRUE;
106    else if ($use_ssl)
107      {
108      raise_error(array('code' => 403, 'type' => 'imap', 'file' => __FILE__,
109                        'message' => 'Open SSL not available;'), TRUE, FALSE);
110      $port = 143;
111      }
112
113    $ICL_PORT = $port;
114    $IMAP_USE_INTERNAL_DATE = false;
115   
116    $this->conn = iil_Connect($host, $user, $pass, array('imap' => 'check'));
117    $this->host = $host;
118    $this->user = $user;
119    $this->pass = $pass;
120    $this->port = $port;
121    $this->ssl = $use_ssl;
122   
123    // print trace mesages
124    if ($this->conn && ($this->debug_level & 8))
125      console($this->conn->message);
126   
127    // write error log
128    else if (!$this->conn && $GLOBALS['iil_error'])
129      {
130      raise_error(array('code' => 403,
131                       'type' => 'imap',
132                       'message' => $GLOBALS['iil_error']), TRUE, FALSE);
133      }
134
135    // get account namespace
136    if ($this->conn)
137      {
138      $this->_parse_capability($this->conn->capability);
139      iil_C_NameSpace($this->conn);
140     
141      if (!empty($this->conn->delimiter))
142        $this->delimiter = $this->conn->delimiter;
143      if (!empty($this->conn->rootdir))
144        {
145        $this->set_rootdir($this->conn->rootdir);
146        $this->root_ns = ereg_replace('[\.\/]$', '', $this->conn->rootdir);
147        }
148      }
149
150    return $this->conn ? TRUE : FALSE;
151    }
152
153
154  /**
155   * Close IMAP connection
156   * Usually done on script shutdown
157   *
158   * @access public
159   */
160  function close()
161    {   
162    if ($this->conn)
163      iil_Close($this->conn);
164    }
165
166
167  /**
168   * Close IMAP connection and re-connect
169   * This is used to avoid some strange socket errors when talking to Courier IMAP
170   *
171   * @access public
172   */
173  function reconnect()
174    {
175    $this->close();
176    $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
177    }
178
179
180  /**
181   * Set a root folder for the IMAP connection.
182   *
183   * Only folders within this root folder will be displayed
184   * and all folder paths will be translated using this folder name
185   *
186   * @param  string   Root folder
187   * @access public
188   */
189  function set_rootdir($root)
190    {
191    if (ereg('[\.\/]$', $root)) //(substr($root, -1, 1)==='/')
192      $root = substr($root, 0, -1);
193
194    $this->root_dir = $root;
195   
196    if (empty($this->delimiter))
197      $this->get_hierarchy_delimiter();
198    }
199
200
201  /**
202   * This list of folders will be listed above all other folders
203   *
204   * @param  array  Indexed list of folder names
205   * @access public
206   */
207  function set_default_mailboxes($arr)
208    {
209    if (is_array($arr))
210      {
211      $this->default_folders = array();
212     
213      // add mailbox names lower case
214      foreach ($arr as $mbox_row)
215        $this->default_folders[] = strtolower($mbox_row);
216     
217      // add inbox if not included
218      if (!in_array('inbox', $this->default_folders))
219        array_unshift($arr, 'inbox');
220      }
221    }
222
223
224  /**
225   * Set internal mailbox reference.
226   *
227   * All operations will be perfomed on this mailbox/folder
228   *
229   * @param  string  Mailbox/Folder name
230   * @access public
231   */
232  function set_mailbox($new_mbox)
233    {
234    $mailbox = $this->_mod_mailbox($new_mbox);
235
236    if ($this->mailbox == $mailbox)
237      return;
238
239    $this->mailbox = $mailbox;
240
241    // clear messagecount cache for this mailbox
242    $this->_clear_messagecount($mailbox);
243    }
244
245
246  /**
247   * Set internal list page
248   *
249   * @param  number  Page number to list
250   * @access public
251   */
252  function set_page($page)
253    {
254    $this->list_page = (int)$page;
255    }
256
257
258  /**
259   * Set internal page size
260   *
261   * @param  number  Number of messages to display on one page
262   * @access public
263   */
264  function set_pagesize($size)
265    {
266    $this->page_size = (int)$size;
267    }
268
269
270  /**
271   * Returns the currently used mailbox name
272   *
273   * @return  string Name of the mailbox/folder
274   * @access  public
275   */
276  function get_mailbox_name()
277    {
278    return $this->conn ? $this->_mod_mailbox($this->mailbox, 'out') : '';
279    }
280
281
282  /**
283   * Returns the IMAP server's capability
284   *
285   * @param   string  Capability name
286   * @return  mixed   Capability value or TRUE if supported, FALSE if not
287   * @access  public
288   */
289  function get_capability($cap)
290    {
291    $cap = strtoupper($cap);
292    return $this->capabilities[$cap];
293    }
294
295
296  /**
297   * Returns the delimiter that is used by the IMAP server for folder separation
298   *
299   * @return  string  Delimiter string
300   * @access  public
301   */
302  function get_hierarchy_delimiter()
303    {
304    if ($this->conn && empty($this->delimiter))
305      $this->delimiter = iil_C_GetHierarchyDelimiter($this->conn);
306
307    if (empty($this->delimiter))
308      $this->delimiter = '/';
309
310    return $this->delimiter;
311    }
312
313
314  /**
315   * Public method for mailbox listing.
316   *
317   * Converts mailbox name with root dir first
318   *
319   * @param   string  Optional root folder
320   * @param   string  Optional filter for mailbox listing
321   * @return  array   List of mailboxes/folders
322   * @access  public
323   */
324  function list_mailboxes($root='', $filter='*')
325    {
326    $a_out = array();
327    $a_mboxes = $this->_list_mailboxes($root, $filter);
328
329    foreach ($a_mboxes as $mbox_row)
330      {
331      $name = $this->_mod_mailbox($mbox_row, 'out');
332      if (strlen($name))
333        $a_out[] = $name;
334      }
335
336    // sort mailboxes
337    $a_out = $this->_sort_mailbox_list($a_out);
338
339    return $a_out;
340    }
341
342
343  /**
344   * Private method for mailbox listing
345   *
346   * @return  array   List of mailboxes/folders
347   * @access  private
348   * @see     rcube_imap::list_mailboxes
349   */
350  function _list_mailboxes($root='', $filter='*')
351    {
352    $a_defaults = $a_out = array();
353   
354    // get cached folder list   
355    $a_mboxes = $this->get_cache('mailboxes');
356    if (is_array($a_mboxes))
357      return $a_mboxes;
358
359    // retrieve list of folders from IMAP server
360    $a_folders = iil_C_ListSubscribed($this->conn, $this->_mod_mailbox($root), $filter);
361   
362    if (!is_array($a_folders) || !sizeof($a_folders))
363      $a_folders = array();
364
365    // create Default folders if they do not exist
366    global $CONFIG;
367    foreach ($CONFIG['default_imap_folders'] as $folder)
368      {
369      if (!in_array_nocase($folder, $a_folders))
370        {
371        $this->create_mailbox($folder, TRUE);
372        $this->subscribe($folder);
373        }
374      }
375
376    $a_folders = iil_C_ListSubscribed($this->conn, $this->_mod_mailbox($root), $filter);
377    $a_mailbox_cache = array();
378
379    // write mailboxlist to cache
380    $this->update_cache('mailboxes', $a_folders);
381   
382    return $a_folders;
383    }
384
385
386  /**
387   * Get message count for a specific mailbox
388   *
389   * @param   string   Mailbox/folder name
390   * @param   string   Mode for count [ALL|UNSEEN|RECENT]
391   * @param   boolean  Force reading from server and update cache
392   * @return  number   Number of messages
393   * @access  public   
394   */
395  function messagecount($mbox_name='', $mode='ALL', $force=FALSE)
396    {
397    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
398    return $this->_messagecount($mailbox, $mode, $force);
399    }
400
401
402  /**
403   * Private method for getting nr of messages
404   *
405   * @access  private
406   * @see     rcube_imap::messagecount
407   */
408  function _messagecount($mailbox='', $mode='ALL', $force=FALSE)
409    {
410    $a_mailbox_cache = FALSE;
411    $mode = strtoupper($mode);
412
413    if (empty($mailbox))
414      $mailbox = $this->mailbox;
415
416    $a_mailbox_cache = $this->get_cache('messagecount');
417   
418    // return cached value
419    if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
420      return $a_mailbox_cache[$mailbox][$mode];
421
422    // RECENT count is fetched abit different     
423    if ($mode == 'RECENT')
424       $count = iil_C_CheckForRecent($this->conn, $mailbox);
425
426    // use SEARCH for message counting
427    else if ($this->skip_deleted)
428      {
429      $search_str = "ALL UNDELETED";
430
431      // get message count and store in cache
432      if ($mode == 'UNSEEN')
433        $search_str .= " UNSEEN";
434
435      // get message count using SEARCH
436      // not very performant but more precise (using UNDELETED)
437      $count = 0;
438      $index = $this->_search_index($mailbox, $search_str);
439      if (is_array($index))
440        {
441        $str = implode(",", $index);
442        if (!empty($str))
443          $count = count($index);
444        }
445      }
446    else
447      {
448      if ($mode == 'UNSEEN')
449        $count = iil_C_CountUnseen($this->conn, $mailbox);
450      else
451        $count = iil_C_CountMessages($this->conn, $mailbox);
452      }
453
454    if (!is_array($a_mailbox_cache[$mailbox]))
455      $a_mailbox_cache[$mailbox] = array();
456     
457    $a_mailbox_cache[$mailbox][$mode] = (int)$count;
458
459    // write back to cache
460    $this->update_cache('messagecount', $a_mailbox_cache);
461
462    return (int)$count;
463    }
464
465
466  /**
467   * Public method for listing headers
468   * convert mailbox name with root dir first
469   *
470   * @param   string   Mailbox/folder name
471   * @param   number   Current page to list
472   * @param   string   Header field to sort by
473   * @param   string   Sort order [ASC|DESC]
474   * @return  array    Indexed array with message header objects
475   * @access  public   
476   */
477  function list_headers($mbox_name='', $page=NULL, $sort_field=NULL, $sort_order=NULL)
478    {
479    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
480    return $this->_list_headers($mailbox, $page, $sort_field, $sort_order);
481    }
482
483
484  /**
485   * Private method for listing message headers
486   *
487   * @access  private
488   * @see     rcube_imap::list_headers
489   */
490  function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=FALSE)
491    {
492    if (!strlen($mailbox))
493      return array();
494     
495    if ($sort_field!=NULL)
496      $this->sort_field = $sort_field;
497    if ($sort_order!=NULL)
498      $this->sort_order = strtoupper($sort_order);
499
500    $max = $this->_messagecount($mailbox);
501    $start_msg = ($this->list_page-1) * $this->page_size;
502
503    list($begin, $end) = $this->_get_message_range($max, $page);
504
505        // mailbox is empty
506    if ($begin >= $end)
507      return array();
508
509    $headers_sorted = FALSE;
510    $cache_key = $mailbox.'.msg';
511    $cache_status = $this->check_cache_status($mailbox, $cache_key);
512
513    // cache is OK, we can get all messages from local cache
514    if ($cache_status>0)
515      {
516      $a_msg_headers = $this->get_message_cache($cache_key, $start_msg, $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
517      $headers_sorted = TRUE;
518      }
519    // cache is dirty, sync it
520    else if ($this->caching_enabled && $cache_status==-1 && !$recursive)
521      {
522      $this->sync_header_index($mailbox);
523      return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, TRUE);
524      }
525    else
526      {
527      // retrieve headers from IMAP
528      if ($this->get_capability('sort') && ($msg_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')))
529        {       
530        $msgs = $msg_index[$begin];
531        for ($i=$begin+1; $i < $end; $i++)
532          $msgs = $msgs.','.$msg_index[$i];
533        }
534      else
535        {
536        $msgs = sprintf("%d:%d", $begin+1, $end);
537
538        $i = 0;
539        for ($msg_seqnum = $begin; $msg_seqnum <= $end; $msg_seqnum++)
540          $msg_index[$i++] = $msg_seqnum;
541        }
542
543      // use this class for message sorting
544      $sorter = new rcube_header_sorter();
545      $sorter->set_sequence_numbers($msg_index);
546
547      // fetch reuested headers from server
548      $a_msg_headers = array();
549      $deleted_count = $this->_fetch_headers($mailbox, $msgs, $a_msg_headers, $cache_key);
550
551      // delete cached messages with a higher index than $max
552      $this->clear_message_cache($cache_key, $max);
553
554
555      // kick child process to sync cache
556      // ...
557
558      }
559
560
561    // return empty array if no messages found
562        if (!is_array($a_msg_headers) || empty($a_msg_headers))
563                return array();
564
565
566    // if not already sorted
567    if (!$headers_sorted)
568      {
569      $sorter->sort_headers($a_msg_headers);
570
571      if ($this->sort_order == 'DESC')
572        $a_msg_headers = array_reverse($a_msg_headers);
573      }
574
575    return array_values($a_msg_headers);
576    }
577
578
579
580  /**
581   * Public method for listing a specific set of headers
582   * convert mailbox name with root dir first
583   *
584   * @param   string   Mailbox/folder name
585   * @param   array    List of message ids to list
586   * @param   number   Current page to list
587   * @param   string   Header field to sort by
588   * @param   string   Sort order [ASC|DESC]
589   * @return  array    Indexed array with message header objects
590   * @access  public   
591   */
592  function list_header_set($mbox_name='', $msgs, $page=NULL, $sort_field=NULL, $sort_order=NULL)
593    {
594    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
595    return $this->_list_header_set($mailbox, $msgs, $page, $sort_field, $sort_order);   
596    }
597   
598
599  /**
600   * Private method for listing a set of message headers
601   *
602   * @access  private
603   * @see     rcube_imap::list_header_set
604   */
605  function _list_header_set($mailbox, $msgs, $page=NULL, $sort_field=NULL, $sort_order=NULL)
606    {
607    // also accept a comma-separated list of message ids
608    if (is_string($msgs))
609      $msgs = split(',', $msgs);
610     
611    if (!strlen($mailbox) || empty($msgs))
612      return array();
613
614    if ($sort_field!=NULL)
615      $this->sort_field = $sort_field;
616    if ($sort_order!=NULL)
617      $this->sort_order = strtoupper($sort_order);
618
619    $max = count($msgs);
620    $start_msg = ($this->list_page-1) * $this->page_size;
621
622    // fetch reuested headers from server
623    $a_msg_headers = array();
624    $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
625
626    // return empty array if no messages found
627        if (!is_array($a_msg_headers) || empty($a_msg_headers))
628                return array();
629
630    // if not already sorted
631    $a_msg_headers = iil_SortHeaders($a_msg_headers, $this->sort_field, $this->sort_order);
632
633        // only return the requested part of the set
634        return array_slice(array_values($a_msg_headers), $start_msg, min($max-$start_msg, $this->page_size));
635    }
636
637
638  /**
639   * Helper function to get first and last index of the requested set
640   *
641   * @param  number  message count
642   * @param  mixed   page number to show, or string 'all'
643   * @return array   array with two values: first index, last index
644   * @access private
645   */
646  function _get_message_range($max, $page)
647    {
648    $start_msg = ($this->list_page-1) * $this->page_size;
649   
650    if ($page=='all')
651      {
652      $begin = 0;
653      $end = $max;
654      }
655    else if ($this->sort_order=='DESC')
656      {
657      $begin = $max - $this->page_size - $start_msg;
658      $end =   $max - $start_msg;
659      }
660    else
661      {
662      $begin = $start_msg;
663      $end   = $start_msg + $this->page_size;
664      }
665
666    if ($begin < 0) $begin = 0;
667    if ($end < 0) $end = $max;
668    if ($end > $max) $end = $max;
669   
670    return array($begin, $end);
671    }
672   
673   
674
675  /**
676   * Fetches message headers
677   * Used for loop
678   *
679   * @param  string  Mailbox name
680   * @param  string  Message index to fetch
681   * @param  array   Reference to message headers array
682   * @param  array   Array with cache index
683   * @return number  Number of deleted messages
684   * @access private
685   */
686  function _fetch_headers($mailbox, $msgs, &$a_msg_headers, $cache_key)
687    {
688    // cache is incomplete
689    $cache_index = $this->get_message_cache_index($cache_key);
690   
691    // fetch reuested headers from server
692    $a_header_index = iil_C_FetchHeaders($this->conn, $mailbox, $msgs);
693    $deleted_count = 0;
694   
695    if (!empty($a_header_index))
696      {
697      foreach ($a_header_index as $i => $headers)
698        {
699        if ($headers->deleted && $this->skip_deleted)
700          {
701          // delete from cache
702          if ($cache_index[$headers->id] && $cache_index[$headers->id] == $headers->uid)
703            $this->remove_message_cache($cache_key, $headers->id);
704
705          $deleted_count++;
706          continue;
707          }
708
709        // add message to cache
710        if ($this->caching_enabled && $cache_index[$headers->id] != $headers->uid)
711          $this->add_message_cache($cache_key, $headers->id, $headers);
712
713        $a_msg_headers[$headers->uid] = $headers;
714        }
715      }
716       
717    return $deleted_count;
718    }
719   
720 
721  // return sorted array of message UIDs
722  function message_index($mbox_name='', $sort_field=NULL, $sort_order=NULL)
723    {
724    if ($sort_field!=NULL)
725      $this->sort_field = $sort_field;
726    if ($sort_order!=NULL)
727      $this->sort_order = strtoupper($sort_order);
728
729    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
730    $key = "$mbox:".$this->sort_field.":".$this->sort_order.".msgi";
731
732    // have stored it in RAM
733    if (isset($this->cache[$key]))
734      return $this->cache[$key];
735
736    // check local cache
737    $cache_key = $mailbox.'.msg';
738    $cache_status = $this->check_cache_status($mailbox, $cache_key);
739
740    // cache is OK
741    if ($cache_status>0)
742      {
743      $a_index = $this->get_message_cache_index($cache_key, TRUE, $this->sort_field, $this->sort_order);
744      return array_values($a_index);
745      }
746
747
748    // fetch complete message index
749    $msg_count = $this->_messagecount($mailbox);
750    if ($this->get_capability('sort') && ($a_index = iil_C_Sort($this->conn, $mailbox, $this->sort_field, '', TRUE)))
751      {
752      if ($this->sort_order == 'DESC')
753        $a_index = array_reverse($a_index);
754
755      $this->cache[$key] = $a_index;
756
757      }
758    else
759      {
760      $a_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:$msg_count", $this->sort_field);
761      $a_uids = iil_C_FetchUIDs($this->conn, $mailbox);
762   
763      if ($this->sort_order=="ASC")
764        asort($a_index);
765      else if ($this->sort_order=="DESC")
766        arsort($a_index);
767       
768      $i = 0;
769      $this->cache[$key] = array();
770      foreach ($a_index as $index => $value)
771        $this->cache[$key][$i++] = $a_uids[$index];
772      }
773
774    return $this->cache[$key];
775    }
776
777
778  function sync_header_index($mailbox)
779    {
780    $cache_key = $mailbox.'.msg';
781    $cache_index = $this->get_message_cache_index($cache_key);
782    $msg_count = $this->_messagecount($mailbox);
783
784    // fetch complete message index
785    $a_message_index = iil_C_FetchHeaderIndex($this->conn, $mailbox, "1:$msg_count", 'UID');
786       
787    foreach ($a_message_index as $id => $uid)
788      {
789      // message in cache at correct position
790      if ($cache_index[$id] == $uid)
791        {
792// console("$id / $uid: OK");
793        unset($cache_index[$id]);
794        continue;
795        }
796       
797      // message in cache but in wrong position
798      if (in_array((string)$uid, $cache_index, TRUE))
799        {
800// console("$id / $uid: Moved");
801        unset($cache_index[$id]);       
802        }
803     
804      // other message at this position
805      if (isset($cache_index[$id]))
806        {
807// console("$id / $uid: Delete");
808        $this->remove_message_cache($cache_key, $id);
809        unset($cache_index[$id]);
810        }
811       
812
813// console("$id / $uid: Add");
814
815      // fetch complete headers and add to cache
816      $headers = iil_C_FetchHeader($this->conn, $mailbox, $id);
817      $this->add_message_cache($cache_key, $headers->id, $headers);
818      }
819
820    // those ids that are still in cache_index have been deleted     
821    if (!empty($cache_index))
822      {
823      foreach ($cache_index as $id => $uid)
824        $this->remove_message_cache($cache_key, $id);
825      }
826    }
827
828
829  /**
830   * Invoke search request to IMAP server
831   *
832   * @param  string  mailbox name to search in
833   * @param  string  search criteria (ALL, TO, FROM, SUBJECT, etc)
834   * @param  string  search string
835   * @return array   search results as list of message ids
836   * @access public
837   */
838  function search($mbox_name='', $criteria='ALL', $str=NULL)
839    {
840    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
841    if ($str && $criteria)
842      {
843      $criteria = 'CHARSET UTF-8 '.$criteria.' "'.UTF7EncodeString($str).'"';
844      return $this->_search_index($mailbox, $criteria);
845      }
846    else
847      return $this->_search_index($mailbox, $criteria);
848    }   
849
850
851  /**
852   * Private search method
853   *
854   * @return array   search results as list of message ids
855   * @access private
856   * @see rcube_imap::search()
857   */
858  function _search_index($mailbox, $criteria='ALL')
859    {
860    $a_messages = iil_C_Search($this->conn, $mailbox, $criteria);
861    // clean message list (there might be some empty entries)
862    if (is_array($a_messages))
863      {
864      foreach ($a_messages as $i => $val)
865        if (empty($val))
866          unset($a_messages[$i]);
867      }
868       
869    return $a_messages;
870    }
871
872
873  function get_headers($id, $mbox_name=NULL, $is_uid=TRUE)
874    {
875    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
876
877    // get cached headers
878    if ($is_uid && ($headers = $this->get_cached_message($mailbox.'.msg', $id)))
879      return $headers;
880
881    $msg_id = $is_uid ? $this->_uid2id($id) : $id;
882    $headers = iil_C_FetchHeader($this->conn, $mailbox, $msg_id);
883
884    // write headers cache
885    if ($headers)
886      $this->add_message_cache($mailbox.'.msg', $msg_id, $headers);
887
888    return $headers;
889    }
890
891
892  function get_body($uid, $part=1)
893    {
894    if (!($msg_id = $this->_uid2id($uid)))
895      return FALSE;
896
897        $structure_str = iil_C_FetchStructureString($this->conn, $this->mailbox, $msg_id);
898        $structure = iml_GetRawStructureArray($structure_str);
899    $body = iil_C_FetchPartBody($this->conn, $this->mailbox, $msg_id, $part);
900
901    $encoding = iml_GetPartEncodingCode($structure, $part);
902   
903    if ($encoding==3) $body = $this->mime_decode($body, 'base64');
904    else if ($encoding==4) $body = $this->mime_decode($body, 'quoted-printable');
905
906    return $body;
907    }
908
909
910  function get_raw_body($uid)
911    {
912    if (!($msg_id = $this->_uid2id($uid)))
913      return FALSE;
914
915        $body = iil_C_FetchPartHeader($this->conn, $this->mailbox, $msg_id, NULL);
916        $body .= iil_C_HandlePartBody($this->conn, $this->mailbox, $msg_id, NULL, 1);
917
918    return $body;   
919    }
920
921
922  // set message flag to one or several messages
923  // possible flags are: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT
924  function set_flag($uids, $flag)
925    {
926    $flag = strtoupper($flag);
927    $msg_ids = array();
928    if (!is_array($uids))
929      $uids = explode(',',$uids);
930     
931    foreach ($uids as $uid) {
932      $msg_ids[$uid] = $this->_uid2id($uid);
933    }
934     
935    if ($flag=='UNDELETED')
936      $result = iil_C_Undelete($this->conn, $this->mailbox, join(',', array_values($msg_ids)));
937    else if ($flag=='UNSEEN')
938      $result = iil_C_Unseen($this->conn, $this->mailbox, join(',', array_values($msg_ids)));
939    else
940      $result = iil_C_Flag($this->conn, $this->mailbox, join(',', array_values($msg_ids)), $flag);
941
942    // reload message headers if cached
943    $cache_key = $this->mailbox.'.msg';
944    if ($this->caching_enabled)
945      {
946      foreach ($msg_ids as $uid => $id)
947        {
948        if ($cached_headers = $this->get_cached_message($cache_key, $uid))
949          {
950          $this->remove_message_cache($cache_key, $id);
951          //$this->get_headers($uid);
952          }
953        }
954
955      // close and re-open connection
956      // this prevents connection problems with Courier
957      $this->reconnect();
958      }
959
960    // set nr of messages that were flaged
961    $count = count($msg_ids);
962
963    // clear message count cache
964    if ($result && $flag=='SEEN')
965      $this->_set_messagecount($this->mailbox, 'UNSEEN', $count*(-1));
966    else if ($result && $flag=='UNSEEN')
967      $this->_set_messagecount($this->mailbox, 'UNSEEN', $count);
968    else if ($result && $flag=='DELETED')
969      $this->_set_messagecount($this->mailbox, 'ALL', $count*(-1));
970
971    return $result;
972    }
973
974
975  // append a mail message (source) to a specific mailbox
976  function save_message($mbox_name, &$message)
977    {
978    $mbox_name = stripslashes($mbox_name);
979    $mailbox = $this->_mod_mailbox($mbox_name);
980
981    // make sure mailbox exists
982    if (in_array($mailbox, $this->_list_mailboxes()))
983      $saved = iil_C_Append($this->conn, $mailbox, $message);
984
985    if ($saved)
986      {
987      // increase messagecount of the target mailbox
988      $this->_set_messagecount($mailbox, 'ALL', 1);
989      }
990         
991    return $saved;
992    }
993
994
995  // move a message from one mailbox to another
996  function move_message($uids, $to_mbox, $from_mbox='')
997    {
998    $to_mbox = stripslashes($to_mbox);
999    $from_mbox = stripslashes($from_mbox);
1000    $to_mbox = $this->_mod_mailbox($to_mbox);
1001    $from_mbox = $from_mbox ? $this->_mod_mailbox($from_mbox) : $this->mailbox;
1002
1003    // make sure mailbox exists
1004    if (!in_array($to_mbox, $this->_list_mailboxes()))
1005      {
1006      if (in_array(strtolower($to_mbox), $this->default_folders))
1007        $this->create_mailbox($to_mbox, TRUE);
1008      else
1009        return FALSE;
1010      }
1011
1012    // convert the list of uids to array
1013    $a_uids = is_string($uids) ? explode(',', $uids) : (is_array($uids) ? $uids : NULL);
1014   
1015    // exit if no message uids are specified
1016    if (!is_array($a_uids))
1017      return false;
1018
1019    // convert uids to message ids
1020    $a_mids = array();
1021    foreach ($a_uids as $uid)
1022      $a_mids[] = $this->_uid2id($uid, $from_mbox);
1023
1024    $moved = iil_C_Move($this->conn, join(',', $a_mids), $from_mbox, $to_mbox);
1025   
1026    // send expunge command in order to have the moved message
1027    // really deleted from the source mailbox
1028    if ($moved)
1029      {
1030      $this->_expunge($from_mbox, FALSE);
1031      $this->_clear_messagecount($from_mbox);
1032      $this->_clear_messagecount($to_mbox);
1033      }
1034
1035    // update cached message headers
1036    $cache_key = $from_mbox.'.msg';
1037    if ($moved && ($a_cache_index = $this->get_message_cache_index($cache_key)))
1038      {
1039      $start_index = 100000;
1040      foreach ($a_uids as $uid)
1041        {
1042        if(($index = array_search($uid, $a_cache_index)) !== FALSE)
1043          $start_index = min($index, $start_index);
1044        }
1045
1046      // clear cache from the lowest index on
1047      $this->clear_message_cache($cache_key, $start_index);
1048      }
1049
1050    return $moved;
1051    }
1052
1053
1054  // mark messages as deleted and expunge mailbox
1055  function delete_message($uids, $mbox_name='')
1056    {
1057    $mbox_name = stripslashes($mbox_name);
1058    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
1059
1060    // convert the list of uids to array
1061    $a_uids = is_string($uids) ? explode(',', $uids) : (is_array($uids) ? $uids : NULL);
1062   
1063    // exit if no message uids are specified
1064    if (!is_array($a_uids))
1065      return false;
1066
1067
1068    // convert uids to message ids
1069    $a_mids = array();
1070    foreach ($a_uids as $uid)
1071      $a_mids[] = $this->_uid2id($uid, $mailbox);
1072       
1073    $deleted = iil_C_Delete($this->conn, $mailbox, join(',', $a_mids));
1074   
1075    // send expunge command in order to have the deleted message
1076    // really deleted from the mailbox
1077    if ($deleted)
1078      {
1079      $this->_expunge($mailbox, FALSE);
1080      $this->_clear_messagecount($mailbox);
1081      }
1082
1083    // remove deleted messages from cache
1084    $cache_key = $mailbox.'.msg';
1085    if ($deleted && ($a_cache_index = $this->get_message_cache_index($cache_key)))
1086      {
1087      $start_index = 100000;
1088      foreach ($a_uids as $uid)
1089        {
1090        $index = array_search($uid, $a_cache_index);
1091        $start_index = min($index, $start_index);
1092        }
1093
1094      // clear cache from the lowest index on
1095      $this->clear_message_cache($cache_key, $start_index);
1096      }
1097
1098    return $deleted;
1099    }
1100
1101
1102  // clear all messages in a specific mailbox
1103  function clear_mailbox($mbox_name=NULL)
1104    {
1105    $mbox_name = stripslashes($mbox_name);
1106    $mailbox = !empty($mbox_name) ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
1107    $msg_count = $this->_messagecount($mailbox, 'ALL');
1108   
1109    if ($msg_count>0)
1110      {
1111      $cleared = iil_C_ClearFolder($this->conn, $mailbox);
1112     
1113      // make sure the message count cache is cleared as well
1114      if ($cleared)
1115        {
1116        $this->clear_message_cache($mailbox.'.msg');     
1117        $a_mailbox_cache = $this->get_cache('messagecount');
1118        unset($a_mailbox_cache[$mailbox]);
1119        $this->update_cache('messagecount', $a_mailbox_cache);
1120        }
1121       
1122      return $cleared;
1123      }
1124    else
1125      return 0;
1126    }
1127
1128
1129  // send IMAP expunge command and clear cache
1130  function expunge($mbox_name='', $clear_cache=TRUE)
1131    {
1132    $mbox_name = stripslashes($mbox_name);
1133    $mailbox = $mbox_name ? $this->_mod_mailbox($mbox_name) : $this->mailbox;
1134    return $this->_expunge($mailbox, $clear_cache);
1135    }
1136
1137
1138  // send IMAP expunge command and clear cache
1139  function _expunge($mailbox, $clear_cache=TRUE)
1140    {
1141    $result = iil_C_Expunge($this->conn, $mailbox);
1142
1143    if ($result>=0 && $clear_cache)
1144      {
1145      //$this->clear_message_cache($mailbox.'.msg');
1146      $this->_clear_messagecount($mailbox);
1147      }
1148     
1149    return $result;
1150    }
1151
1152
1153  /* --------------------------------
1154   *        folder managment
1155   * --------------------------------*/
1156
1157
1158  // return an array with all folders available in IMAP server
1159  function list_unsubscribed($root='')
1160    {
1161    static $sa_unsubscribed;
1162   
1163    if (is_array($sa_unsubscribed))
1164      return $sa_unsubscribed;
1165     
1166    // retrieve list of folders from IMAP server
1167    $a_mboxes = iil_C_ListMailboxes($this->conn, $this->_mod_mailbox($root), '*');
1168
1169    // modify names with root dir
1170    foreach ($a_mboxes as $mbox_name)
1171      {
1172      $name = $this->_mod_mailbox($mbox_name, 'out');
1173      if (strlen($name))
1174        $a_folders[] = $name;
1175      }
1176
1177    // filter folders and sort them
1178    $sa_unsubscribed = $this->_sort_mailbox_list($a_folders);
1179    return $sa_unsubscribed;
1180    }
1181
1182
1183  /**
1184   * Get quota
1185   * added by Nuny
1186   */
1187  function get_quota()
1188    {
1189    if ($this->get_capability('QUOTA'))
1190      {
1191      $result = iil_C_GetQuota($this->conn);
1192      if ($result["total"])
1193        return sprintf("%.2fMB / %.2fMB (%.0f%%)", $result["used"] / 1000.0, $result["total"] / 1000.0, $result["percent"]);       
1194      }
1195
1196    return FALSE;
1197    }
1198
1199
1200  // subscribe to a specific mailbox(es)
1201  function subscribe($mbox_name, $mode='subscribe')
1202    {
1203    if (is_array($mbox_name))
1204      $a_mboxes = $mbox_name;
1205    else if (is_string($mbox_name) && strlen($mbox_name))
1206      $a_mboxes = explode(',', $mbox_name);
1207   
1208    // let this common function do the main work
1209    return $this->_change_subscription($a_mboxes, 'subscribe');
1210    }
1211
1212
1213  // unsubscribe mailboxes
1214  function unsubscribe($mbox_name)
1215    {
1216    if (is_array($mbox_name))
1217      $a_mboxes = $mbox_name;
1218    else if (is_string($mbox_name) && strlen($mbox_name))
1219      $a_mboxes = explode(',', $mbox_name);
1220
1221    // let this common function do the main work
1222    return $this->_change_subscription($a_mboxes, 'unsubscribe');
1223    }
1224
1225
1226  // create a new mailbox on the server and register it in local cache
1227  function create_mailbox($name, $subscribe=FALSE)
1228    {
1229    $result = FALSE;
1230   
1231    // replace backslashes
1232    $name = preg_replace('/[\\\]+/', '-', $name);
1233
1234    $name_enc = UTF7EncodeString($name);
1235
1236    // reduce mailbox name to 100 chars
1237    $name_enc = substr($name_enc, 0, 100);
1238
1239    $abs_name = $this->_mod_mailbox($name_enc);
1240    $a_mailbox_cache = $this->get_cache('mailboxes');
1241       
1242    if (strlen($abs_name) && (!is_array($a_mailbox_cache) || !in_array($abs_name, $a_mailbox_cache)))
1243      $result = iil_C_CreateFolder($this->conn, $abs_name);
1244
1245    // update mailboxlist cache
1246    if ($result && $subscribe)
1247      $this->subscribe($name_enc);
1248
1249    return $result ? $name : FALSE;
1250    }
1251
1252
1253  // set a new name to an existing mailbox
1254  function rename_mailbox($mbox_name, $new_name)
1255    {
1256    $result = FALSE;
1257
1258    // replace backslashes
1259    $name = preg_replace('/[\\\]+/', '-', $new_name);
1260       
1261    // encode mailbox name and reduce it to 100 chars
1262    $name_enc = substr(UTF7EncodeString($new_name), 0, 100);
1263
1264    // make absolute path
1265    $mailbox = $this->_mod_mailbox($mbox_name);
1266    $abs_name = $this->_mod_mailbox($name_enc);
1267   
1268    if (strlen($abs_name))
1269      $result = iil_C_RenameFolder($this->conn, $mailbox, $abs_name);
1270   
1271    // clear cache
1272    if ($result)
1273      {
1274      $this->clear_message_cache($mailbox.'.msg');
1275      $this->clear_cache('mailboxes');
1276      }
1277
1278    return $result ? $name : FALSE;
1279    }
1280
1281
1282  // remove mailboxes from server
1283  function delete_mailbox($mbox_name)
1284    {
1285    $deleted = FALSE;
1286
1287    if (is_array($mbox_name))
1288      $a_mboxes = $mbox_name;
1289    else if (is_string($mbox_name) && strlen($mbox_name))
1290      $a_mboxes = explode(',', $mbox_name);
1291
1292    if (is_array($a_mboxes))
1293      foreach ($a_mboxes as $mbox_name)
1294        {
1295        $mailbox = $this->_mod_mailbox($mbox_name);
1296
1297        // unsubscribe mailbox before deleting
1298        iil_C_UnSubscribe($this->conn, $mailbox);
1299       
1300        // send delete command to server
1301        $result = iil_C_DeleteFolder($this->conn, $mailbox);
1302        if ($result>=0)
1303          $deleted = TRUE;
1304        }
1305
1306    // clear mailboxlist cache
1307    if ($deleted)
1308      {
1309      $this->clear_message_cache($mailbox.'.msg');
1310      $this->clear_cache('mailboxes');
1311      }
1312
1313    return $deleted;
1314    }
1315
1316
1317
1318
1319  /* --------------------------------
1320   *   internal caching methods
1321   * --------------------------------*/
1322
1323
1324  function set_caching($set)
1325    {
1326    if ($set && is_object($this->db))
1327      $this->caching_enabled = TRUE;
1328    else
1329      $this->caching_enabled = FALSE;
1330    }
1331
1332
1333  function get_cache($key)
1334    {
1335    // read cache
1336    if (!isset($this->cache[$key]) && $this->caching_enabled)
1337      {
1338      $cache_data = $this->_read_cache_record('IMAP.'.$key);
1339      $this->cache[$key] = strlen($cache_data) ? unserialize($cache_data) : FALSE;
1340      }
1341   
1342    return $this->cache[$key];
1343    }
1344
1345
1346  function update_cache($key, $data)
1347    {
1348    $this->cache[$key] = $data;
1349    $this->cache_changed = TRUE;
1350    $this->cache_changes[$key] = TRUE;
1351    }
1352
1353
1354  function write_cache()
1355    {
1356    if ($this->caching_enabled && $this->cache_changed)
1357      {
1358      foreach ($this->cache as $key => $data)
1359        {
1360        if ($this->cache_changes[$key])
1361          $this->_write_cache_record('IMAP.'.$key, serialize($data));
1362        }
1363      }   
1364    }
1365
1366
1367  function clear_cache($key=NULL)
1368    {
1369    if ($key===NULL)
1370      {
1371      foreach ($this->cache as $key => $data)
1372        $this->_clear_cache_record('IMAP.'.$key);
1373
1374      $this->cache = array();
1375      $this->cache_changed = FALSE;
1376      $this->cache_changes = array();
1377      }
1378    else
1379      {
1380      $this->_clear_cache_record('IMAP.'.$key);
1381      $this->cache_changes[$key] = FALSE;
1382      unset($this->cache[$key]);
1383      }
1384    }
1385
1386
1387
1388  function _read_cache_record($key)
1389    {
1390    $cache_data = FALSE;
1391   
1392    if ($this->db)
1393      {
1394      // get cached data from DB
1395      $sql_result = $this->db->query(
1396        "SELECT cache_id, data
1397         FROM ".get_table_name('cache')."
1398         WHERE  user_id=?
1399         AND    cache_key=?",
1400        $_SESSION['user_id'],
1401        $key);
1402
1403      if ($sql_arr = $this->db->fetch_assoc($sql_result))
1404        {
1405        $cache_data = $sql_arr['data'];
1406        $this->cache_keys[$key] = $sql_arr['cache_id'];
1407        }
1408      }
1409
1410    return $cache_data;   
1411    }
1412   
1413
1414  function _write_cache_record($key, $data)
1415    {
1416    if (!$this->db)
1417      return FALSE;
1418
1419    // check if we already have a cache entry for this key
1420    if (!isset($this->cache_keys[$key]))
1421      {
1422      $sql_result = $this->db->query(
1423        "SELECT cache_id
1424         FROM ".get_table_name('cache')."
1425         WHERE  user_id=?
1426         AND    cache_key=?",
1427        $_SESSION['user_id'],
1428        $key);
1429                                     
1430      if ($sql_arr = $this->db->fetch_assoc($sql_result))
1431        $this->cache_keys[$key] = $sql_arr['cache_id'];
1432      else
1433        $this->cache_keys[$key] = FALSE;
1434      }
1435
1436    // update existing cache record
1437    if ($this->cache_keys[$key])
1438      {
1439      $this->db->query(
1440        "UPDATE ".get_table_name('cache')."
1441         SET    created=now(),
1442                data=?
1443         WHERE  user_id=?
1444         AND    cache_key=?",
1445        $data,
1446        $_SESSION['user_id'],
1447        $key);
1448      }
1449    // add new cache record
1450    else
1451      {
1452      $this->db->query(
1453        "INSERT INTO ".get_table_name('cache')."
1454         (created, user_id, cache_key, data)
1455         VALUES (now(), ?, ?, ?)",
1456        $_SESSION['user_id'],
1457        $key,
1458        $data);
1459      }
1460    }
1461
1462
1463  function _clear_cache_record($key)
1464    {
1465    $this->db->query(
1466      "DELETE FROM ".get_table_name('cache')."
1467       WHERE  user_id=?
1468       AND    cache_key=?",
1469      $_SESSION['user_id'],
1470      $key);
1471    }
1472
1473
1474
1475  /* --------------------------------
1476   *   message caching methods
1477   * --------------------------------*/
1478   
1479
1480  // checks if the cache is up-to-date
1481  // return: -3 = off, -2 = incomplete, -1 = dirty
1482  function check_cache_status($mailbox, $cache_key)
1483    {
1484    if (!$this->caching_enabled)
1485      return -3;
1486
1487    $cache_index = $this->get_message_cache_index($cache_key, TRUE);
1488    $msg_count = $this->_messagecount($mailbox);
1489    $cache_count = count($cache_index);
1490
1491    // console("Cache check: $msg_count !== ".count($cache_index));
1492
1493    if ($cache_count==$msg_count)
1494      {
1495      // get highest index
1496      $header = iil_C_FetchHeader($this->conn, $mailbox, "$msg_count");
1497      $cache_uid = array_pop($cache_index);
1498     
1499      // uids of highest message matches -> cache seems OK
1500      if ($cache_uid == $header->uid)
1501        return 1;
1502
1503      // cache is dirty
1504      return -1;
1505      }
1506    // if cache count differs less than 10% report as dirty
1507    else if (abs($msg_count - $cache_count) < $msg_count/10)
1508      return -1;
1509    else
1510      return -2;
1511    }
1512
1513
1514
1515  function get_message_cache($key, $from, $to, $sort_field, $sort_order)
1516    {
1517    $cache_key = "$key:$from:$to:$sort_field:$sort_order";
1518    $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
1519   
1520    if (!in_array($sort_field, $db_header_fields))
1521      $sort_field = 'idx';
1522   
1523    if ($this->caching_enabled && !isset($this->cache[$cache_key]))
1524      {
1525      $this->cache[$cache_key] = array();
1526      $sql_result = $this->db->limitquery(
1527        "SELECT idx, uid, headers
1528         FROM ".get_table_name('messages')."
1529         WHERE  user_id=?
1530         AND    cache_key=?
1531         ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".
1532         strtoupper($sort_order),
1533        $from,
1534        $to-$from,
1535        $_SESSION['user_id'],
1536        $key);
1537
1538      while ($sql_arr = $this->db->fetch_assoc($sql_result))
1539        {
1540        $uid = $sql_arr['uid'];
1541        $this->cache[$cache_key][$uid] = unserialize($sql_arr['headers']);
1542        }
1543      }
1544     
1545    return $this->cache[$cache_key];
1546    }
1547
1548
1549  function get_cached_message($key, $uid, $body=FALSE)
1550    {
1551    if (!$this->caching_enabled)
1552      return FALSE;
1553
1554    $internal_key = '__single_msg';
1555    if ($this->caching_enabled && (!isset($this->cache[$internal_key][$uid]) || $body))
1556      {
1557      $sql_select = "idx, uid, headers";
1558      if ($body)
1559        $sql_select .= ", body";
1560     
1561      $sql_result = $this->db->query(
1562        "SELECT $sql_select
1563         FROM ".get_table_name('messages')."
1564         WHERE  user_id=?
1565         AND    cache_key=?
1566         AND    uid=?",
1567        $_SESSION['user_id'],
1568        $key,
1569        $uid);
1570     
1571      if ($sql_arr = $this->db->fetch_assoc($sql_result))
1572        {
1573        $headers = unserialize($sql_arr['headers']);
1574        if (is_object($headers) && !empty($sql_arr['body']))
1575          $headers->body = $sql_arr['body'];
1576
1577        $this->cache[$internal_key][$uid] = $headers;
1578        }
1579      }
1580
1581    return $this->cache[$internal_key][$uid];
1582    }
1583
1584   
1585  function get_message_cache_index($key, $force=FALSE, $sort_col='idx', $sort_order='ASC')
1586    {
1587    static $sa_message_index = array();
1588   
1589    // empty key -> empty array
1590    if (empty($key))
1591      return array();
1592   
1593    if (!empty($sa_message_index[$key]) && !$force)
1594      return $sa_message_index[$key];
1595   
1596    $sa_message_index[$key] = array();
1597    $sql_result = $this->db->query(
1598      "SELECT idx, uid
1599       FROM ".get_table_name('messages')."
1600       WHERE  user_id=?
1601       AND    cache_key=?
1602       ORDER BY ".$this->db->quote_identifier($sort_col)." ".$sort_order,
1603      $_SESSION['user_id'],
1604      $key);
1605
1606    while ($sql_arr = $this->db->fetch_assoc($sql_result))
1607      $sa_message_index[$key][$sql_arr['idx']] = $sql_arr['uid'];
1608     
1609    return $sa_message_index[$key];
1610    }
1611
1612
1613  function add_message_cache($key, $index, $headers)
1614    {
1615    if (!$key || !is_object($headers) || empty($headers->uid))
1616      return;
1617
1618    $this->db->query(
1619      "INSERT INTO ".get_table_name('messages')."
1620       (user_id, del, cache_key, created, idx, uid, subject, ".$this->db->quoteIdentifier('from').", ".$this->db->quoteIdentifier('to').", cc, date, size, headers)
1621       VALUES (?, 0, ?, now(), ?, ?, ?, ?, ?, ?, ".$this->db->fromunixtime($headers->timestamp).", ?, ?)",
1622      $_SESSION['user_id'],
1623      $key,
1624      $index,
1625      $headers->uid,
1626      (string)substr($this->decode_header($headers->subject, TRUE), 0, 128),
1627      (string)substr($this->decode_header($headers->from, TRUE), 0, 128),
1628      (string)substr($this->decode_header($headers->to, TRUE), 0, 128),
1629      (string)substr($this->decode_header($headers->cc, TRUE), 0, 128),
1630      (int)$headers->size,
1631      serialize($headers));
1632    }
1633   
1634   
1635  function remove_message_cache($key, $index)
1636    {
1637    $this->db->query(
1638      "DELETE FROM ".get_table_name('messages')."
1639       WHERE  user_id=?
1640       AND    cache_key=?
1641       AND    idx=?",
1642      $_SESSION['user_id'],
1643      $key,
1644      $index);
1645    }
1646
1647
1648  function clear_message_cache($key, $start_index=1)
1649    {
1650    $this->db->query(
1651      "DELETE FROM ".get_table_name('messages')."
1652       WHERE  user_id=?
1653       AND    cache_key=?
1654       AND    idx>=?",
1655      $_SESSION['user_id'],
1656      $key,
1657      $start_index);
1658    }
1659
1660
1661
1662
1663  /* --------------------------------
1664   *   encoding/decoding methods
1665   * --------------------------------*/
1666
1667 
1668  function decode_address_list($input, $max=NULL)
1669    {
1670    $a = $this->_parse_address_list($input);
1671    $out = array();
1672   
1673    if (!is_array($a))
1674      return $out;
1675
1676    $c = count($a);
1677    $j = 0;
1678
1679    foreach ($a as $val)
1680      {
1681      $j++;
1682      $address = $val['address'];
1683      $name = preg_replace(array('/^[\'"]/', '/[\'"]$/'), '', trim($val['name']));
1684      $string = $name!==$address ? sprintf('%s <%s>', strpos($name, ',')!==FALSE ? '"'.$name.'"' : $name, $address) : $address;
1685     
1686      $out[$j] = array('name' => $name,
1687                       'mailto' => $address,
1688                       'string' => $string);
1689             
1690      if ($max && $j==$max)
1691        break;
1692      }
1693   
1694    return $out;
1695    }
1696
1697
1698  function decode_header($input, $remove_quotes=FALSE)
1699    {
1700    $str = $this->decode_mime_string((string)$input);
1701    if ($str{0}=='"' && $remove_quotes)
1702      {
1703      $str = str_replace('"', '', $str);
1704      }
1705   
1706    return $str;
1707    }
1708
1709
1710  /**
1711   * Decode a mime-encoded string to internal charset
1712   *
1713   * @access static
1714   */
1715  function decode_mime_string($input, $recursive=false)
1716    {
1717    $out = '';
1718
1719    $pos = strpos($input, '=?');
1720    if ($pos !== false)
1721      {
1722      $out = substr($input, 0, $pos);
1723 
1724      $end_cs_pos = strpos($input, "?", $pos+2);
1725      $end_en_pos = strpos($input, "?", $end_cs_pos+1);
1726      $end_pos = strpos($input, "?=", $end_en_pos+1);
1727 
1728      $encstr = substr($input, $pos+2, ($end_pos-$pos-2));
1729      $rest = substr($input, $end_pos+2);
1730
1731      $out .= rcube_imap::_decode_mime_string_part($encstr);
1732      $out .= rcube_imap::decode_mime_string($rest);
1733
1734      return $out;
1735      }
1736     
1737    // no encoding information, defaults to what is specified in the class header
1738    return rcube_charset_convert($input, 'ISO-8859-1');
1739    }
1740
1741
1742  /**
1743   * Decode a part of a mime-encoded string
1744   *
1745   * @access static
1746   */
1747  function _decode_mime_string_part($str)
1748    {
1749    $a = explode('?', $str);
1750    $count = count($a);
1751
1752    // should be in format "charset?encoding?base64_string"
1753    if ($count >= 3)
1754      {
1755      for ($i=2; $i<$count; $i++)
1756        $rest.=$a[$i];
1757
1758      if (($a[1]=="B")||($a[1]=="b"))
1759        $rest = base64_decode($rest);
1760      else if (($a[1]=="Q")||($a[1]=="q"))
1761        {
1762        $rest = str_replace("_", " ", $rest);
1763        $rest = quoted_printable_decode($rest);
1764        }
1765
1766      return rcube_charset_convert($rest, $a[0]);
1767      }
1768    else
1769      return $str;    // we dont' know what to do with this 
1770    }
1771
1772
1773  function mime_decode($input, $encoding='7bit')
1774    {
1775    switch (strtolower($encoding))
1776      {
1777      case '7bit':
1778        return $input;
1779        break;
1780     
1781      case 'quoted-printable':
1782        return quoted_printable_decode($input);
1783        break;
1784     
1785      case 'base64':
1786        return base64_decode($input);
1787        break;
1788     
1789      default:
1790        return $input;
1791      }
1792    }
1793
1794
1795  function mime_encode($input, $encoding='7bit')
1796    {
1797    switch ($encoding)
1798      {
1799      case 'quoted-printable':
1800        return quoted_printable_encode($input);
1801        break;
1802
1803      case 'base64':
1804        return base64_encode($input);
1805        break;
1806
1807      default:
1808        return $input;
1809      }
1810    }
1811
1812
1813  // convert body chars according to the ctype_parameters
1814  function charset_decode($body, $ctype_param)
1815    {
1816    if (is_array($ctype_param) && !empty($ctype_param['charset']))
1817      return rcube_charset_convert($body, $ctype_param['charset']);
1818
1819    // defaults to what is specified in the class header
1820    return rcube_charset_convert($body,  'ISO-8859-1');
1821    }
1822
1823
1824
1825
1826  /* --------------------------------
1827   *         private methods
1828   * --------------------------------*/
1829
1830
1831  function _mod_mailbox($mbox_name, $mode='in')
1832    {
1833    if ((!empty($this->root_ns) && $this->root_ns == $mbox_name) || ($mbox_name == 'INBOX' && $mode == 'in'))
1834      return $mbox_name;
1835
1836    if (!empty($this->root_dir) && $mode=='in')
1837      $mbox_name = $this->root_dir.$this->delimiter.$mbox_name;
1838    else if (strlen($this->root_dir) && $mode=='out')
1839      $mbox_name = substr($mbox_name, strlen($this->root_dir)+1);
1840
1841    return $mbox_name;
1842    }
1843
1844
1845  // sort mailboxes first by default folders and then in alphabethical order
1846  function _sort_mailbox_list($a_folders)
1847    {
1848    $a_out = $a_defaults = array();
1849
1850    // find default folders and skip folders starting with '.'
1851    foreach($a_folders as $i => $folder)
1852      {
1853      if ($folder{0}=='.')
1854        continue;
1855       
1856      if (($p = array_search(strtolower($folder), $this->default_folders))!==FALSE)
1857        $a_defaults[$p] = $folder;
1858      else
1859        $a_out[] = $folder;
1860      }
1861
1862    sort($a_out);
1863    ksort($a_defaults);
1864   
1865    return array_merge($a_defaults, $a_out);
1866    }
1867
1868  function get_id($uid, $mbox_name=NULL)
1869    {
1870      return $this->_uid2id($uid, $mbox_name);
1871    }
1872 
1873  function get_uid($id,$mbox_name=NULL)
1874    {
1875      return $this->_id2uid($id, $mbox_name);
1876    }
1877
1878  function _uid2id($uid, $mbox_name=NULL)
1879    {
1880    if (!$mbox_name)
1881      $mbox_name = $this->mailbox;
1882     
1883    if (!isset($this->uid_id_map[$mbox_name][$uid]))
1884      $this->uid_id_map[$mbox_name][$uid] = iil_C_UID2ID($this->conn, $mbox_name, $uid);
1885
1886    return $this->uid_id_map[$mbox_name][$uid];
1887    }
1888
1889  function _id2uid($id, $mbox_name=NULL)
1890    {
1891    if (!$mbox_name)
1892      $mbox_name = $this->mailbox;
1893     
1894    return iil_C_ID2UID($this->conn, $mbox_name, $id);
1895    }
1896
1897
1898  // parse string or array of server capabilities and put them in internal array
1899  function _parse_capability($caps)
1900    {
1901    if (!is_array($caps))
1902      $cap_arr = explode(' ', $caps);
1903    else
1904      $cap_arr = $caps;
1905   
1906    foreach ($cap_arr as $cap)
1907      {
1908      if ($cap=='CAPABILITY')
1909        continue;
1910
1911      if (strpos($cap, '=')>0)
1912        {
1913        list($key, $value) = explode('=', $cap);
1914        if (!is_array($this->capabilities[$key]))
1915          $this->capabilities[$key] = array();
1916         
1917        $this->capabilities[$key][] = $value;
1918        }
1919      else
1920        $this->capabilities[$cap] = TRUE;
1921      }
1922    }
1923
1924
1925  // subscribe/unsubscribe a list of mailboxes and update local cache
1926  function _change_subscription($a_mboxes, $mode)
1927    {
1928    $updated = FALSE;
1929   
1930    if (is_array($a_mboxes))
1931      foreach ($a_mboxes as $i => $mbox_name)
1932        {
1933        $mailbox = $this->_mod_mailbox($mbox_name);
1934        $a_mboxes[$i] = $mailbox;
1935
1936        if ($mode=='subscribe')
1937          $result = iil_C_Subscribe($this->conn, $mailbox);
1938        else if ($mode=='unsubscribe')
1939          $result = iil_C_UnSubscribe($this->conn, $mailbox);
1940
1941        if ($result>=0)
1942          $updated = TRUE;
1943        }
1944       
1945    // get cached mailbox list   
1946    if ($updated)
1947      {
1948      $a_mailbox_cache = $this->get_cache('mailboxes');
1949      if (!is_array($a_mailbox_cache))
1950        return $updated;
1951
1952      // modify cached list
1953      if ($mode=='subscribe')
1954        $a_mailbox_cache = array_merge($a_mailbox_cache, $a_mboxes);
1955      else if ($mode=='unsubscribe')
1956        $a_mailbox_cache = array_diff($a_mailbox_cache, $a_mboxes);
1957       
1958      // write mailboxlist to cache
1959      $this->update_cache('mailboxes', $this->_sort_mailbox_list($a_mailbox_cache));
1960      }
1961
1962    return $updated;
1963    }
1964
1965
1966  // increde/decrese messagecount for a specific mailbox
1967  function _set_messagecount($mbox_name, $mode, $increment)
1968    {
1969    $a_mailbox_cache = FALSE;
1970    $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
1971    $mode = strtoupper($mode);
1972
1973    $a_mailbox_cache = $this->get_cache('messagecount');
1974   
1975    if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
1976      return FALSE;
1977   
1978    // add incremental value to messagecount
1979    $a_mailbox_cache[$mailbox][$mode] += $increment;
1980   
1981    // there's something wrong, delete from cache
1982    if ($a_mailbox_cache[$mailbox][$mode] < 0)
1983      unset($a_mailbox_cache[$mailbox][$mode]);
1984
1985    // write back to cache
1986    $this->update_cache('messagecount', $a_mailbox_cache);
1987   
1988    return TRUE;
1989    }
1990
1991
1992  // remove messagecount of a specific mailbox from cache
1993  function _clear_messagecount($mbox_name='')
1994    {
1995    $a_mailbox_cache = FALSE;
1996    $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
1997
1998    $a_mailbox_cache = $this->get_cache('messagecount');
1999
2000    if (is_array($a_mailbox_cache[$mailbox]))
2001      {
2002      unset($a_mailbox_cache[$mailbox]);
2003      $this->update_cache('messagecount', $a_mailbox_cache);
2004      }
2005    }
2006
2007
2008  function _parse_address_list($str)
2009    {
2010    $a = $this->_explode_quoted_string(',', $str);
2011    $result = array();
2012   
2013    foreach ($a as $key => $val)
2014      {
2015      $val = str_replace("\"<", "\" <", $val);
2016      $sub_a = $this->_explode_quoted_string(' ', $this->decode_header($val));
2017      $result[$key]['name'] = '';
2018
2019      foreach ($sub_a as $k => $v)
2020        {
2021        if ((strpos($v, '@') > 0) && (strpos($v, '.') > 0))
2022          $result[$key]['address'] = str_replace('<', '', str_replace('>', '', $v));
2023        else
2024          $result[$key]['name'] .= (empty($result[$key]['name'])?'':' ').str_replace("\"",'',stripslashes($v));
2025        }
2026       
2027      if (empty($result[$key]['name']))
2028        $result[$key]['name'] = $result[$key]['address'];       
2029      }
2030   
2031    return $result;
2032    }
2033
2034
2035  function _explode_quoted_string($delimiter, $string)
2036    {
2037    $quotes = explode("\"", $string);
2038    foreach ($quotes as $key => $val)
2039      if (($key % 2) == 1)
2040        $quotes[$key] = str_replace($delimiter, "_!@!_", $quotes[$key]);
2041       
2042    $string = implode("\"", $quotes);
2043
2044    $result = explode($delimiter, $string);
2045    foreach ($result as $key => $val)
2046      $result[$key] = str_replace("_!@!_", $delimiter, $result[$key]);
2047   
2048    return $result;
2049    }
2050  }
2051
2052
2053
2054/**
2055 * rcube_header_sorter
2056 *
2057 * Class for sorting an array of iilBasicHeader objects in a predetermined order.
2058 *
2059 * @author Eric Stadtherr
2060 */
2061class rcube_header_sorter
2062{
2063   var $sequence_numbers = array();
2064   
2065   /**
2066    * set the predetermined sort order.
2067    *
2068    * @param array $seqnums numerically indexed array of IMAP message sequence numbers
2069    */
2070   function set_sequence_numbers($seqnums)
2071   {
2072      $this->sequence_numbers = $seqnums;
2073   }
2074 
2075   /**
2076    * sort the array of header objects
2077    *
2078    * @param array $headers array of iilBasicHeader objects indexed by UID
2079    */
2080   function sort_headers(&$headers)
2081   {
2082      /*
2083       * uksort would work if the keys were the sequence number, but unfortunately
2084       * the keys are the UIDs.  We'll use uasort instead and dereference the value
2085       * to get the sequence number (in the "id" field).
2086       *
2087       * uksort($headers, array($this, "compare_seqnums"));
2088       */
2089       uasort($headers, array($this, "compare_seqnums"));
2090   }
2091 
2092   /**
2093    * get the position of a message sequence number in my sequence_numbers array
2094    *
2095    * @param integer $seqnum message sequence number contained in sequence_numbers 
2096    */
2097   function position_of($seqnum)
2098   {
2099      $c = count($this->sequence_numbers);
2100      for ($pos = 0; $pos <= $c; $pos++)
2101      {
2102         if ($this->sequence_numbers[$pos] == $seqnum)
2103            return $pos;
2104      }
2105      return -1;
2106   }
2107 
2108   /**
2109    * Sort method called by uasort()
2110    */
2111   function compare_seqnums($a, $b)
2112   {
2113      // First get the sequence number from the header object (the 'id' field).
2114      $seqa = $a->id;
2115      $seqb = $b->id;
2116     
2117      // then find each sequence number in my ordered list
2118      $posa = $this->position_of($seqa);
2119      $posb = $this->position_of($seqb);
2120     
2121      // return the relative position as the comparison value
2122      $ret = $posa - $posb;
2123      return $ret;
2124   }
2125}
2126
2127
2128/**
2129 * Add quoted-printable encoding to a given string
2130 *
2131 * @param string  $input      string to encode
2132 * @param int     $line_max   add new line after this number of characters
2133 * @param boolena $space_conf true if spaces should be converted into =20
2134 * @return encoded string
2135 */
2136function quoted_printable_encode($input, $line_max=76, $space_conv=false)
2137  {
2138  $hex = array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F');
2139  $lines = preg_split("/(?:\r\n|\r|\n)/", $input);
2140  $eol = "\r\n";
2141  $escape = "=";
2142  $output = "";
2143
2144  while( list(, $line) = each($lines))
2145    {
2146    //$line = rtrim($line); // remove trailing white space -> no =20\r\n necessary
2147    $linlen = strlen($line);
2148    $newline = "";
2149    for($i = 0; $i < $linlen; $i++)
2150      {
2151      $c = substr( $line, $i, 1 );
2152      $dec = ord( $c );
2153      if ( ( $i == 0 ) && ( $dec == 46 ) ) // convert first point in the line into =2E
2154        {
2155        $c = "=2E";
2156        }
2157      if ( $dec == 32 )
2158        {
2159        if ( $i == ( $linlen - 1 ) ) // convert space at eol only
2160          {
2161          $c = "=20";
2162          }
2163        else if ( $space_conv )
2164          {
2165          $c = "=20";
2166          }
2167        }
2168      else if ( ($dec == 61) || ($dec < 32 ) || ($dec > 126) )  // always encode "\t", which is *not* required
2169        {
2170        $h2 = floor($dec/16);
2171        $h1 = floor($dec%16);
2172        $c = $escape.$hex["$h2"].$hex["$h1"];
2173        }
2174         
2175      if ( (strlen($newline) + strlen($c)) >= $line_max )  // CRLF is not counted
2176        {
2177        $output .= $newline.$escape.$eol; // soft line break; " =\r\n" is okay
2178        $newline = "";
2179        // check if newline first character will be point or not
2180        if ( $dec == 46 )
2181          {
2182          $c = "=2E";
2183          }
2184        }
2185      $newline .= $c;
2186      } // end of for
2187    $output .= $newline.$eol;
2188    } // end of while
2189
2190  return trim($output);
2191  }
2192
2193?>
Note: See TracBrowser for help on using the repository browser.