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

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