From 66536974fe12a02ca5ffcec4354bf5113282a0cc Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Mon, 07 Apr 2014 03:53:46 -0400
Subject: [PATCH] Merge branch 'dev-multi-folder-search'

---
 program/js/list.js                                 |   69 ++-
 plugins/acl/acl.php                                |    4 
 program/lib/Roundcube/rcube_imap.php               |  108 ++++
 program/lib/Roundcube/rcube_message_header.php     |    9 
 program/steps/mail/copy.inc                        |    6 
 program/steps/mail/move_del.inc                    |   22 
 skins/larry/templates/mail.html                    |   13 
 program/steps/mail/list.inc                        |    4 
 program/lib/Roundcube/rcube_result_multifolder.php |  250 +++++++++++
 program/steps/mail/search.inc                      |   28 
 program/steps/mail/autocomplete.inc                |    4 
 program/steps/mail/func.inc                        |   76 +++
 program/localization/en_US/labels.inc              |    4 
 program/lib/Roundcube/rcube_imap_search.php        |  336 +++++++++++++++
 program/steps/mail/mark.inc                        |   10 
 program/js/app.js                                  |  301 +++++++++----
 skins/larry/mail.css                               |   12 
 skins/larry/ui.js                                  |   32 +
 18 files changed, 1,117 insertions(+), 171 deletions(-)

diff --git a/plugins/acl/acl.php b/plugins/acl/acl.php
index 681ef0e..d7e50f9 100644
--- a/plugins/acl/acl.php
+++ b/plugins/acl/acl.php
@@ -86,7 +86,7 @@
         $this->load_config();
 
         $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
-        $sid    = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+        $reqid  = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC);
         $users  = array();
 
         if ($this->init_ldap()) {
@@ -115,7 +115,7 @@
 
         sort($users, SORT_LOCALE_STRING);
 
-        $this->rc->output->command('ksearch_query_results', $users, $search, $sid);
+        $this->rc->output->command('ksearch_query_results', $users, $search, $reqid);
         $this->rc->output->send();
     }
 
diff --git a/program/js/app.js b/program/js/app.js
index eea72f0..f548ad8 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -31,6 +31,7 @@
   this.onloads = [];
   this.messages = {};
   this.group2expand = {};
+  this.http_request_jobs = {};
 
   // webmail client settings
   this.dblclick_time = 500;
@@ -216,6 +217,7 @@
           this.gui_objects.messagelist.parentNode.onmousedown = function(e){ return p.click_on_list(e); };
 
           this.enable_command('toggle_status', 'toggle_flag', 'sort', true);
+          this.enable_command('set-listmode', this.env.threads && !this.env.search_request);
 
           // load messages
           this.command('list');
@@ -689,7 +691,7 @@
 
       case 'open':
         if (uid = this.get_single_uid()) {
-          obj.href = this.url('show', {_mbox: this.env.mailbox, _uid: uid});
+          obj.href = this.url('show', {_mbox: this.get_message_mailbox(uid), _uid: uid});
           return true;
         }
         break;
@@ -700,8 +702,17 @@
         break;
 
       case 'list':
-        if (props && props != '')
-          this.reset_qsearch();
+        // re-send for the selected folder
+        if (props && props != '' && this.env.search_request) {
+          var oldmbox = this.env.search_scope == 'all' ? '*' : this.env.mailbox;
+          this.env.search_mods[props] = this.env.search_mods[oldmbox];  // copy search mods from active search
+          this.env.mailbox = props;
+          this.env.search_scope = 'base';
+          this.qsearch(this.gui_objects.qsearchbox.value);
+          this.select_folder(this.env.mailbox, '', true);
+          break;
+        }
+
         if (this.env.action == 'compose' && this.env.extwin)
           window.close();
         else if (this.task == 'mail') {
@@ -710,6 +721,10 @@
         }
         else if (this.task == 'addressbook')
           this.list_contacts(props);
+        break;
+
+      case 'set-listmode':
+        this.set_list_options(null, undefined, undefined, props == 'threads' ? 1 : 0);
         break;
 
       case 'sort':
@@ -792,9 +807,9 @@
           this.load_contact(cid, 'edit');
         else if (this.task == 'settings' && props)
           this.load_identity(props, 'edit-identity');
-        else if (this.task == 'mail' && (cid = this.get_single_uid())) {
-          url = { _mbox: this.env.mailbox };
-          url[this.env.mailbox == this.env.drafts_mailbox && props != 'new' ? '_draft_uid' : '_uid'] = cid;
+        else if (this.task == 'mail' && (uid = this.get_single_uid())) {
+          url = { _mbox: this.get_message_mailbox(uid) };
+          url[this.env.mailbox == this.env.drafts_mailbox && props != 'new' ? '_draft_uid' : '_uid'] = uid;
           this.open_compose_step(url);
         }
         break;
@@ -1077,7 +1092,7 @@
       case 'reply-list':
       case 'reply':
         if (uid = this.get_single_uid()) {
-          url = {_reply_uid: uid, _mbox: this.env.mailbox};
+          url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid)};
           if (command == 'reply-all')
             // do reply-list, when list is detected and popup menu wasn't used
             url._all = (!props && this.env.reply_all_mode == 1 && this.commands['reply-list'] ? 'list' : 'all');
@@ -1105,7 +1120,7 @@
           this.gui_objects.messagepartframe.contentWindow.print();
         }
         else if (uid = this.get_single_uid()) {
-          ref.printwin = this.open_window(this.env.comm_path+'&_action=print&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox)+(this.env.safemode ? '&_safe=1' : ''), true, true);
+          ref.printwin = this.open_window(this.env.comm_path+'&_action=print&_uid='+uid+'&_mbox='+urlencode(this.get_message_mailbox(uid))+(this.env.safemode ? '&_safe=1' : ''), true, true);
           if (this.printwin) {
             if (this.env.action != 'show')
               this.mark_message('read', uid);
@@ -1122,8 +1137,9 @@
         if (this.env.action == 'get') {
           location.href = location.href.replace(/_frame=/, '_download=');
         }
-        else if (uid = this.get_single_uid())
-          this.goto_url('viewsource', { _uid: uid, _mbox: this.env.mailbox, _save: 1 });
+        else if (uid = this.get_single_uid()) {
+          this.goto_url('viewsource', { _uid: uid, _mbox: this.get_message_mailbox(uid), _save: 1 });
+        }
         break;
 
       // quicksearch
@@ -1630,7 +1646,7 @@
 
     var uid = list.get_single_selection();
 
-    if (uid && this.env.mailbox == this.env.drafts_mailbox)
+    if (uid && (this.env.messages[uid].mbox || this.env.mailbox) == this.env.drafts_mailbox)
       this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox });
     else if (uid)
       this.show_message(uid, false, false);
@@ -1762,7 +1778,7 @@
   this.init_message_row = function(row)
   {
     var i, fn = {}, self = this, uid = row.uid,
-      status_icon = (this.env.status_col != null ? 'status' : 'msg') + 'icn' + row.uid;
+      status_icon = (this.env.status_col != null ? 'status' : 'msg') + 'icn' + row.id;
 
     if (uid && this.env.messages[uid])
       $.extend(row, this.env.messages[uid]);
@@ -1774,17 +1790,17 @@
 
     // save message icon position too
     if (this.env.status_col != null)
-      row.msgicon = document.getElementById('msgicn'+row.uid);
+      row.msgicon = document.getElementById('msgicn'+row.id);
     else
       row.msgicon = row.icon;
 
     // set eventhandler to flag icon
-    if (this.env.flagged_col != null && (row.flagicon = document.getElementById('flagicn'+row.uid))) {
+    if (this.env.flagged_col != null && (row.flagicon = document.getElementById('flagicn'+row.id))) {
       fn.flagicon = function(e) { self.command('toggle_flag', uid); };
     }
 
     // set event handler to thread expand/collapse icon
-    if (!row.depth && row.has_children && (row.expando = document.getElementById('rcmexpando'+row.uid))) {
+    if (!row.depth && row.has_children && (row.expando = document.getElementById('rcmexpando'+row.id))) {
       fn.expando = function(e) { self.expand_message_row(e, uid); };
     }
 
@@ -1831,6 +1847,7 @@
       selected: this.select_all_mode || this.message_list.in_selection(uid),
       ml: flags.ml?1:0,
       ctype: flags.ctype,
+      mbox: flags.mbox,
       // flags from plugins
       flags: flags.extra_flags
     });
@@ -1845,7 +1862,7 @@
         + (flags.deleted ? ' deleted' : '')
         + (flags.flagged ? ' flagged' : '')
         + (message.selected ? ' selected' : ''),
-      row = { cols:[], style:{}, id:'rcmrow'+uid };
+      row = { cols:[], style:{}, id:'rcmrow'+this.html_identifier(uid,true), uid:uid };
 
     // message status icons
     css_class = 'msgicon';
@@ -1871,7 +1888,7 @@
     if (this.env.threading) {
       if (message.depth) {
         // This assumes that div width is hardcoded to 15px,
-        tree += '<span id="rcmtab' + uid + '" class="branch" style="width:' + (message.depth * 15) + 'px;">&nbsp;&nbsp;</span>';
+        tree += '<span id="rcmtab' + row.id + '" class="branch" style="width:' + (message.depth * 15) + 'px;">&nbsp;&nbsp;</span>';
 
         if ((rows[message.parent_uid] && rows[message.parent_uid].expanded === false)
           || ((this.env.autoexpand_threads == 0 || this.env.autoexpand_threads == 2) &&
@@ -1890,7 +1907,7 @@
           message.expanded = true;
         }
 
-        expando = '<div id="rcmexpando' + uid + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">&nbsp;&nbsp;</div>';
+        expando = '<div id="rcmexpando' + row.id + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">&nbsp;&nbsp;</div>';
         row_class += ' thread' + (message.expanded? ' expanded' : '');
       }
 
@@ -1898,14 +1915,14 @@
         row_class += ' unroot';
     }
 
-    tree += '<span id="msgicn'+uid+'" class="'+css_class+'">&nbsp;</span>';
+    tree += '<span id="msgicn'+row.id+'" class="'+css_class+'">&nbsp;</span>';
     row.className = row_class;
 
     // build subject link
     if (cols.subject) {
       var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show';
       var uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid';
-      cols.subject = '<a href="./?_task=mail&_action='+action+'&_mbox='+urlencode(flags.mbox)+'&'+uid_param+'='+uid+'"'+
+      cols.subject = '<a href="./?_task=mail&_action='+action+'&_mbox='+urlencode(flags.mbox)+'&'+uid_param+'='+urlencode(uid)+'"'+
         ' onclick="return rcube_event.cancel(event)" onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')"><span>'+cols.subject+'</span></a>';
     }
 
@@ -1916,7 +1933,7 @@
 
       if (c == 'flag') {
         css_class = (flags.flagged ? 'flagged' : 'unflagged');
-        html = '<span id="flagicn'+uid+'" class="'+css_class+'">&nbsp;</span>';
+        html = '<span id="flagicn'+row.id+'" class="'+css_class+'">&nbsp;</span>';
       }
       else if (c == 'attachment') {
         if (/application\/|multipart\/(m|signed)/.test(flags.ctype))
@@ -1935,7 +1952,7 @@
           css_class = 'unreadchildren';
         else
           css_class = 'msgicon';
-        html = '<span id="statusicn'+uid+'" class="'+css_class+'">&nbsp;</span>';
+        html = '<span id="statusicn'+row.id+'" class="'+css_class+'">&nbsp;</span>';
       }
       else if (c == 'threads')
         html = expando;
@@ -2033,7 +2050,7 @@
 
     var win, target = window,
       action = preview ? 'preview': 'show',
-      url = '&_action='+action+'&_uid='+id+'&_mbox='+urlencode(this.env.mailbox);
+      url = '&_action='+action+'&_uid='+id+'&_mbox='+urlencode(this.get_message_mailbox(id));
 
     if (preview && (win = this.get_frame_window(this.env.contentframe))) {
       target = win;
@@ -2398,7 +2415,7 @@
     }
 
     if (html)
-      $('#rcmtab'+uid).html(html);
+      $('#rcmtab'+this.html_identifier(uid, true)).html(html);
   };
 
   // update parent in a thread
@@ -2462,14 +2479,14 @@
 
         r.depth--; // move left
         // reset width and clear the content of a tab, icons will be added later
-        $('#rcmtab'+r.uid).width(r.depth * 15).html('');
+        $('#rcmtab'+r.id).width(r.depth * 15).html('');
         if (!r.depth) { // a new root
           count++; // increase roots count
           r.parent_uid = 0;
           if (r.has_children) {
             // replace 'leaf' with 'collapsed'
-            $('#rcmrow'+r.uid+' '+'.leaf:first')
-              .attr('id', 'rcmexpando' + r.uid)
+            $('#'+r.id+' .leaf:first')
+              .attr('id', 'rcmexpando' + r.id)
               .attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed'))
               .bind('mousedown', {uid:r.uid, p:this},
                 function(e) { return e.data.p.expand_message_row(e, e.data.uid); });
@@ -4148,15 +4165,17 @@
       r = this.http_request(action, url, lock);
 
       this.env.qsearch = {lock: lock, request: r};
+      this.enable_command('set-listmode', this.env.threads && (this.env.search_scope || 'base') == 'base');
     }
   };
 
   // build URL params for search
-  this.search_params = function(search, filter)
+  this.search_params = function(search, filter, smods)
   {
     var n, url = {}, mods_arr = [],
       mods = this.env.search_mods,
-      mbox = this.env.mailbox;
+      scope = this.env.search_scope || 'base',
+      mbox = scope == 'all' ? '*' : this.env.mailbox;
 
     if (!filter && this.gui_objects.search_filter)
       filter = this.gui_objects.search_filter.value;
@@ -4170,17 +4189,19 @@
     if (search) {
       url._q = search;
 
-      if (mods && this.message_list)
-        mods = mods[mbox] ? mods[mbox] : mods['*'];
+      if (!smods && mods && this.message_list)
+        smods = mods[mbox] || mods['*'];
 
-      if (mods) {
-        for (n in mods)
+      if (smods) {
+        for (n in smods)
           mods_arr.push(n);
         url._headers = mods_arr.join(',');
       }
     }
 
-    if (mbox)
+    if (scope)
+      url._scope = scope;
+    if (mbox && scope != 'all')
       url._mbox = mbox;
 
     return url;
@@ -4198,6 +4219,8 @@
     this.env.qsearch = null;
     this.env.search_request = null;
     this.env.search_id = null;
+
+    this.enable_command('set-listmode', this.env.threads);
   };
 
   this.sent_successfully = function(type, msg, folders)
@@ -4376,7 +4399,7 @@
       p = inp_value.lastIndexOf(this.env.recipients_separator, cpos-1),
       q = inp_value.substring(p+1, cpos),
       min = this.env.autocomplete_min_length,
-      ac = this.ksearch_data;
+      data = this.ksearch_data;
 
     // trim query string
     q = $.trim(q);
@@ -4403,34 +4426,26 @@
       return;
 
     // ...new search value contains old one and previous search was not finished or its result was empty
-    if (old_value && old_value.length && q.startsWith(old_value) && (!ac || ac.num <= 0) && this.env.contacts && !this.env.contacts.length)
+    if (old_value && old_value.length && q.startsWith(old_value) && (!data || data.num <= 0) && this.env.contacts && !this.env.contacts.length)
       return;
 
-    var i, lock, source, xhr, reqid = new Date().getTime(),
-      post_data = {_search: q, _id: reqid},
-      threads = props && props.threads ? props.threads : 1,
-      sources = props && props.sources ? props.sources : [],
-      action = props && props.action ? props.action : 'mail/autocomplete';
+    var sources = props && props.sources ? props.sources : [''];
+    var reqid = this.multi_thread_http_request({
+      items: sources,
+      threads: props && props.threads ? props.threads : 1,
+      action:  props && props.action ? props.action : 'mail/autocomplete',
+      postdata: { _search:q, _source:'%s' },
+      lock: this.display_message(this.get_label('searching'), 'loading')
+    });
 
-    this.ksearch_data = {id: reqid, sources: sources.slice(), action: action,
-      locks: [], requests: [], num: sources.length};
-
-    for (i=0; i<threads; i++) {
-      source = this.ksearch_data.sources.shift();
-      if (threads > 1 && source === undefined)
-        break;
-
-      post_data._source = source ? source : '';
-      lock = this.display_message(this.get_label('searching'), 'loading');
-      xhr = this.http_post(action, post_data, lock);
-
-      this.ksearch_data.locks.push(lock);
-      this.ksearch_data.requests.push(xhr);
-    }
+    this.ksearch_data = { id:reqid, sources:sources.slice(), num:sources.length };
   };
 
   this.ksearch_query_results = function(results, search, reqid)
   {
+    // trigger multi-thread http response callback
+    this.multi_thread_http_response(results, reqid);
+
     // search stopped in meantime?
     if (!this.ksearch_value)
       return;
@@ -4442,7 +4457,6 @@
     // display search results
     var i, len, ul, li, text, type, init,
       value = this.ksearch_value,
-      data = this.ksearch_data,
       maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;
 
     // create results pane if not present
@@ -4498,27 +4512,8 @@
     if (len)
       this.env.contacts = this.env.contacts.concat(results);
 
-    // run next parallel search
-    if (data.id == reqid) {
-      data.num--;
-      if (maxlen > 0 && data.sources.length) {
-        var lock, xhr, source = data.sources.shift(), post_data;
-        if (source) {
-          post_data = {_search: value, _id: reqid, _source: source};
-          lock = this.display_message(this.get_label('searching'), 'loading');
-          xhr = this.http_post(data.action, post_data, lock);
-
-          this.ksearch_data.locks.push(lock);
-          this.ksearch_data.requests.push(xhr);
-        }
-      }
-      else if (!maxlen) {
-        if (!this.ksearch_msg)
-          this.ksearch_msg = this.display_message(this.get_label('autocompletemore'));
-        // abort pending searches
-        this.ksearch_abort();
-      }
-    }
+    if (this.ksearch_data.id == reqid)
+      this.ksearch_data.num--;
   };
 
   this.ksearch_click = function(node)
@@ -4553,7 +4548,8 @@
   // Clears autocomplete data/requests
   this.ksearch_destroy = function()
   {
-    this.ksearch_abort();
+    if (this.ksearch_data)
+      this.multi_thread_request_abort(this.ksearch_data.id);
 
     if (this.ksearch_info)
       this.hide_message(this.ksearch_info);
@@ -4564,18 +4560,6 @@
     this.ksearch_data = null;
     this.ksearch_info = null;
     this.ksearch_msg = null;
-  }
-
-  // Aborts pending autocomplete requests
-  this.ksearch_abort = function()
-  {
-    var i, len, ac = this.ksearch_data;
-
-    if (!ac)
-      return;
-
-    for (i=0, len=ac.locks.length; i<len; i++)
-      this.abort_request({request: ac.requests[i], lock: ac.locks[i]});
   };
 
 
@@ -6482,8 +6466,10 @@
     if ((n = $.inArray('status', this.env.coltypes)) >= 0)
       this.env.status_col = n;
 
-    if (list)
+    if (list) {
+      list.hide_column('folder', !(this.env.search_request || this.env.search_id) || this.env.search_scope == 'base');
       list.init_header();
+    }
   };
 
   // replace content of row count display
@@ -7112,6 +7098,130 @@
       clearTimeout(this.submit_timer);
   };
 
+  /**
+   Send multi-threaded parallel HTTP requests to the server for a list if items.
+   The string '%' in either a GET query or POST parameters will be replaced with the respective item value.
+   This is the argument object expected: {
+       items: ['foo','bar','gna'],      // list of items to send requests for
+       action: 'task/some-action',      // Roudncube action to call
+       query: { q:'%s' },               // GET query parameters
+       postdata: { source:'%s' },       // POST data (sends a POST request if present)
+       threads: 3,                      // max. number of concurrent requests
+       onresponse: function(data){ },   // Callback function called for every response received from server
+       whendone: function(alldata){ }   // Callback function called when all requests have been sent
+   }
+  */
+  this.multi_thread_http_request = function(prop)
+  {
+    var reqid = new Date().getTime();
+
+    prop.reqid = reqid;
+    prop.running = 0;
+    prop.requests = [];
+    prop.result = [];
+    prop._items = $.extend([], prop.items);  // copy items
+
+    if (!prop.lock)
+      prop.lock = this.display_message(this.get_label('loading'), 'loading');
+
+    // add the request arguments to the jobs pool
+    this.http_request_jobs[reqid] = prop;
+
+    // start n threads
+    var item, threads = prop.threads || 1;
+    for (var i=0; i < threads; i++) {
+      item = prop._items.shift();
+      if (item === undefined)
+        break;
+
+      prop.running++;
+      prop.requests.push(this.multi_thread_send_request(prop, item));
+    }
+
+    return reqid;
+  };
+
+  // helper method to send an HTTP request with the given iterator value
+  this.multi_thread_send_request = function(prop, item)
+  {
+    var postdata, query;
+
+    // replace %s in post data
+    if (prop.postdata) {
+      postdata = {};
+      for (var k in prop.postdata) {
+        postdata[k] = String(prop.postdata[k]).replace('%s', item);
+      }
+      postdata._reqid = prop.reqid;
+    }
+    // replace %s in query
+    else if (typeof prop.query == 'string') {
+      query = prop.query.replace('%s', item);
+      query += '&_reqid=' + prop.reqid;
+    }
+    else if (typeof prop.query == 'object' && prop.query) {
+      query = {};
+      for (var k in prop.query) {
+        query[k] = String(prop.query[k]).replace('%s', item);
+      }
+      query._reqid = prop.reqid;
+    }
+
+    // send HTTP GET or POST request
+    return postdata ? this.http_post(prop.action, postdata) : this.http_request(prop.action, query);
+  };
+
+  // callback function for multi-threaded http responses
+  this.multi_thread_http_response = function(data, reqid)
+  {
+    var prop = this.http_request_jobs[reqid];
+    if (!prop || prop.running <= 0 || prop.cancelled)
+      return;
+
+    prop.running--;
+
+    // trigger response callback
+    if (prop.onresponse && typeof prop.onresponse == 'function') {
+      prop.onresponse(data);
+    }
+
+    prop.result = $.extend(prop.result, data);
+
+    // send next request if prop.items is not yet empty
+    var item = prop._items.shift();
+    if (item !== undefined) {
+      prop.running++;
+      prop.requests.push(this.multi_thread_send_request(prop, item));
+    }
+    // trigger whendone callback and mark this request as done
+    else if (prop.running == 0) {
+      if (prop.whendone && typeof prop.whendone == 'function') {
+        prop.whendone(prop.result);
+      }
+
+      this.set_busy(false, '', prop.lock);
+
+      // remove from this.http_request_jobs pool
+      delete this.http_request_jobs[reqid];
+    }
+  };
+
+  // abort a running multi-thread request with the given identifier
+  this.multi_thread_request_abort = function(reqid)
+  {
+    var prop = this.http_request_jobs[reqid];
+    if (prop) {
+      for (var i=0; prop.running > 0 && i < prop.requests.length; i++) {
+        if (prop.requests[i].abort)
+          prop.requests[i].abort();
+      }
+
+      prop.running = 0;
+      prop.cancelled = true;
+      this.set_busy(false, '', prop.lock);
+    }
+  };
+
   // post the given form to a hidden iframe
   this.async_upload_form = function(form, action, onload)
   {
@@ -7389,6 +7499,13 @@
     return this.env.cid ? this.env.cid : (this.contact_list ? this.contact_list.get_single_selection() : null);
   };
 
+  // get the IMP mailbox of the message with the given UID
+  this.get_message_mailbox = function(uid)
+  {
+    var msg = this.env.messages ? this.env.messages[uid] : {};
+    return msg.mbox || this.env.mailbox;
+  }
+
   // gets cursor position
   this.get_caret_pos = function(obj)
   {
diff --git a/program/js/list.js b/program/js/list.js
index 476edae..9b7779c 100644
--- a/program/js/list.js
+++ b/program/js/list.js
@@ -107,11 +107,11 @@
  */
 init_row: function(row)
 {
+  row.uid = this.get_row_uid(row);
+
   // make references in internal array and set event handlers
-  if (row && String(row.id).match(this.id_regexp)) {
-    var self = this,
-      uid = RegExp.$1;
-    row.uid = uid;
+  if (row && row.uid) {
+    var self = this, uid = row.uid;
     this.rows[uid] = {uid:uid, id:row.id, obj:row};
 
     // set eventhandlers to table row
@@ -299,6 +299,7 @@
     if (row.id) domrow.id = row.id;
     if (row.className) domrow.className = row.className;
     if (row.style) $.extend(domrow.style, row.style);
+    if (row.uid) $(domrow).data('uid', row.uid);
 
     for (var domcell, col, i=0; row.cols && i < row.cols.length; i++) {
       col = row.cols[i];
@@ -386,6 +387,20 @@
       $(this.rows[id].obj).removeClass('selected focused').addClass('unfocused');
     }
   }
+},
+
+
+/**
+ * Set/unset the given column as hidden
+ */
+hide_column: function(col, hide)
+{
+  var method = hide ? 'addClass' : 'removeClass';
+
+  if (this.fixed_header)
+    $(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.fixed_header)[method]('hidden');
+
+  $(this.row_tagname()+' '+this.col_tagname()+'.'+col, this.list)[method]('hidden');
 },
 
 
@@ -583,7 +598,7 @@
     row.expanded = true;
     depth = row.depth;
     new_row = row.obj.nextSibling;
-    this.update_expando(row.uid, true);
+    this.update_expando(row.id, true);
     this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
   }
   else {
@@ -633,7 +648,7 @@
     row.expanded = false;
     depth = row.depth;
     new_row = row.obj.nextSibling;
-    this.update_expando(row.uid);
+    this.update_expando(row.id);
     this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
 
     // don't collapse sub-root tree in multiexpand mode 
@@ -655,7 +670,7 @@
           $(new_row).css('display', 'none');
         if (r.has_children && r.expanded) {
           r.expanded = false;
-          this.update_expando(r.uid, false);
+          this.update_expando(r.id, false);
           this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
         }
       }
@@ -677,7 +692,7 @@
     row.expanded = true;
     depth = row.depth;
     new_row = row.obj.nextSibling;
-    this.update_expando(row.uid, true);
+    this.update_expando(row.id, true);
     this.triggerEvent('expandcollapse', { uid:row.uid, expanded:row.expanded, obj:row.obj });
   }
   else {
@@ -694,7 +709,7 @@
         $(new_row).css('display', '');
         if (r.has_children && !r.expanded) {
           r.expanded = true;
-          this.update_expando(r.uid, true);
+          this.update_expando(r.id, true);
           this.triggerEvent('expandcollapse', { uid:r.uid, expanded:r.expanded, obj:new_row });
         }
       }
@@ -708,13 +723,26 @@
 },
 
 
-update_expando: function(uid, expanded)
+update_expando: function(id, expanded)
 {
-  var expando = document.getElementById('rcmexpando' + uid);
+  var expando = document.getElementById('rcmexpando' + id);
   if (expando)
     expando.className = expanded ? 'expanded' : 'collapsed';
 },
 
+get_row_uid: function(row)
+{
+  if (row && row.uid)
+    return row.uid;
+
+  var uid;
+  if (row && (uid = $(row).data('uid')))
+    row.uid = uid;
+  else if (row && String(row.id).match(this.id_regexp))
+    row.uid = RegExp.$1;
+
+  return row.uid;
+},
 
 /**
  * get first/next/previous/last rows that are not hidden
@@ -750,11 +778,11 @@
 get_first_row: function()
 {
   if (this.rowcount) {
-    var i, len, rows = this.tbody.childNodes;
+    var i, len, uid, rows = this.tbody.childNodes;
 
     for (i=0, len=rows.length-1; i<len; i++)
-      if (rows[i].id && String(rows[i].id).match(this.id_regexp) && this.rows[RegExp.$1] != null)
-        return RegExp.$1;
+      if (rows[i].id && (uid = this.get_row_uid(rows[i])))
+        return uid;
   }
 
   return null;
@@ -763,11 +791,11 @@
 get_last_row: function()
 {
   if (this.rowcount) {
-    var i, rows = this.tbody.childNodes;
+    var i, uid, rows = this.tbody.childNodes;
 
     for (i=rows.length-1; i>=0; i--)
-      if (rows[i].id && String(rows[i].id).match(this.id_regexp) && this.rows[RegExp.$1] != null)
-        return RegExp.$1;
+      if (rows[i].id && (uid = this.get_row_uid(rows[i])))
+        return uid;
   }
 
   return null;
@@ -1261,7 +1289,7 @@
         this.collapse(selected_row);
     }
 
-    this.update_expando(selected_row.uid, selected_row.expanded);
+    this.update_expando(selected_row.id, selected_row.expanded);
 
     return false;
   }
@@ -1340,10 +1368,7 @@
 
     // get selected rows (in display order), don't use this.selection here
     $(this.row_tagname() + '.selected', this.tbody).each(function() {
-      if (!String(this.id).match(self.id_regexp))
-        return;
-
-      var uid = RegExp.$1, row = self.rows[uid];
+      var uid = self.get_row_uid(this), row = self.rows[uid];
 
       if (!row || $.inArray(uid, selection) > -1)
         return;
diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php
index 628338a..f60be62 100644
--- a/program/lib/Roundcube/rcube_imap.php
+++ b/program/lib/Roundcube/rcube_imap.php
@@ -332,6 +332,10 @@
         $this->search_sort_field = $set[3];
         $this->search_sorted     = $set[4];
         $this->search_threads    = is_a($this->search_set, 'rcube_result_thread');
+
+        if (is_a($this->search_set, 'rcube_result_multifolder')) {
+            $this->set_threading(false);
+        }
     }
 
 
@@ -945,6 +949,54 @@
             return array();
         }
 
+        // gather messages from a multi-folder search
+        if ($this->search_set->multi) {
+            $page_size = $this->page_size;
+            $sort_field = $this->sort_field;
+            $search_set = $this->search_set;
+
+            $this->sort_field = null;
+            $this->page_size = 1000;  // fetch up to 1000 matching messages per folder
+            $this->threading = false;
+
+            $a_msg_headers = array();
+            foreach ($search_set->sets as $resultset) {
+                if (!$resultset->is_empty()) {
+                    $this->search_set = $resultset;
+                    $this->search_threads = $resultset instanceof rcube_result_thread;
+                    $a_msg_headers = array_merge($a_msg_headers, $this->list_search_messages($resultset->get_parameters('MAILBOX'), 1));
+                }
+            }
+
+            // do sorting and paging
+            $cnt   = $search_set->count();
+            $from  = ($page-1) * $page_size;
+            $to    = $from + $page_size;
+
+            // sort headers
+            if (!$this->threading && !empty($a_msg_headers)) {
+                $a_msg_headers = $this->conn->sortHeaders($a_msg_headers, $sort_field, $this->sort_order);
+            }
+
+            // store (sorted) message index
+            $search_set->set_message_index($a_msg_headers, $sort_field, $this->sort_order);
+
+            // only return the requested part of the set
+            $slice_length  = min($page_size, $cnt - $from);
+            $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length);
+
+            if ($slice) {
+                $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
+            }
+
+            // restore members
+            $this->sort_field = $sort_field;
+            $this->page_size = $page_size;
+            $this->search_set = $search_set;
+
+            return $a_msg_headers;
+        }
+
         // use saved messages from searching
         if ($this->threading) {
             return $this->list_search_thread_messages($folder, $page, $slice);
@@ -1111,6 +1163,7 @@
         }
 
         foreach ($headers as $h) {
+            $h->folder = $folder;
             $a_msg_headers[$h->uid] = $h;
         }
 
@@ -1234,8 +1287,13 @@
                 return new rcube_result_index($folder, '* SORT');
             }
 
+            if ($this->search_set instanceof rcube_result_multifolder) {
+                $index = $this->search_set;
+                $index->folder = $folder;
+                // TODO: handle changed sorting
+            }
             // search result is an index with the same sorting?
-            if (($this->search_set instanceof rcube_result_index)
+            else if (($this->search_set instanceof rcube_result_index)
                 && ((!$this->sort_field && !$this->search_sorted) ||
                     ($this->search_sorted && $this->search_sort_field == $this->sort_field))
             ) {
@@ -1422,11 +1480,34 @@
             $str = 'ALL';
         }
 
-        if (!strlen($folder)) {
+        if (empty($folder)) {
             $folder = $this->folder;
         }
 
-        $results = $this->search_index($folder, $str, $charset, $sort_field);
+        // multi-folder search
+        if (is_array($folder) && count($folder) > 1 && $str != 'ALL') {
+            new rcube_result_index; // trigger autoloader and make these classes available for threaded context
+            new rcube_result_thread;
+
+            // connect IMAP to have all the required classes and settings loaded
+            $this->check_connection();
+
+            // disable threading
+            $this->threading = false;
+
+            $searcher = new rcube_imap_search($this->options, $this->conn);
+            $results = $searcher->exec(
+                $folder,
+                $str,
+                $charset ? $charset : $this->default_charset,
+                $sort_field && $this->get_capability('SORT') ? $sort_field : null,
+                $this->threading
+            );
+        }
+        else {
+            $folder = is_array($folder) ? $folder[0] : $folder;
+            $results = $this->search_index($folder, $str, $charset, $sort_field);
+        }
 
         $this->set_search_set(array($str, $results, $charset, $sort_field,
             $this->threading || $this->search_sorted ? true : false));
@@ -1500,7 +1581,7 @@
             // but I've seen that Courier doesn't support UTF-8)
             if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
                 $threads = $this->conn->thread($folder, $this->threading,
-                    $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
+                    self::convert_criteria($criteria, $charset), true, 'US-ASCII');
             }
 
             return $threads;
@@ -1514,7 +1595,7 @@
             // but I've seen Courier with disabled UTF-8 support)
             if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
                 $messages = $this->conn->sort($folder, $sort_field,
-                    $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
+                    self::convert_criteria($criteria, $charset), true, 'US-ASCII');
             }
 
             if (!$messages->is_error()) {
@@ -1529,7 +1610,7 @@
         // Error, try with US-ASCII (some servers may support only US-ASCII)
         if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
             $messages = $this->conn->search($folder,
-                $this->convert_criteria($criteria, $charset), true);
+                self::convert_criteria($criteria, $charset), true);
         }
 
         $this->search_sorted = false;
@@ -1547,7 +1628,7 @@
      *
      * @return string  Search string
      */
-    protected function convert_criteria($str, $charset, $dest_charset='US-ASCII')
+    public static function convert_criteria($str, $charset, $dest_charset='US-ASCII')
     {
         // convert strings to US_ASCII
         if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
@@ -1583,6 +1664,7 @@
     public function refresh_search()
     {
         if (!empty($this->search_string)) {
+            // FIXME: make this work with saved multi-folder searches
             $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field);
         }
 
@@ -1603,6 +1685,11 @@
     {
         if (!strlen($folder)) {
             $folder = $this->folder;
+        }
+
+        // decode combined UID-folder identifier
+        if (preg_match('/^\d+-[^,]+$/', $uid)) {
+            list($uid, $folder) = explode('-', $uid);
         }
 
         // get cached headers
@@ -1634,6 +1721,11 @@
     {
         if (!strlen($folder)) {
             $folder = $this->folder;
+        }
+
+        // decode combined UID-folder identifier
+        if (preg_match('/^\d+-[^,]+$/', $uid)) {
+            list($uid, $folder) = explode('-', $uid);
         }
 
         // Check internal cache
@@ -2397,7 +2489,7 @@
                     $this->refresh_search();
                 }
                 else {
-                    $this->search_set->filter(explode(',', $uids));
+                    $this->search_set->filter(explode(',', $uids), $this->folder);
                 }
             }
 
diff --git a/program/lib/Roundcube/rcube_imap_search.php b/program/lib/Roundcube/rcube_imap_search.php
new file mode 100644
index 0000000..c881981
--- /dev/null
+++ b/program/lib/Roundcube/rcube_imap_search.php
@@ -0,0 +1,336 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client                     |
+ |                                                                       |
+ | Copyright (C) 2013, The Roundcube Dev Team                            |
+ | Copyright (C) 2013, Kolab Systems AG                                  |
+ |                                                                       |
+ | Licensed under the GNU General Public License version 3 or            |
+ | any later version with exceptions for skins & plugins.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Execute (multi-threaded) searches in multiple IMAP folders          |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+*/
+
+// create classes defined by the pthreads module if that isn't installed
+if (!defined('PTHREADS_INHERIT_ALL')) {
+    class Worker { }
+    class Stackable { }
+}
+
+/**
+ * Class to control search jobs on multiple IMAP folders.
+ * This implement a simple threads pool using the pthreads extension.
+ *
+ * @package    Framework
+ * @subpackage Storage
+ * @author     Thomas Bruederli <roundcube@gmail.com>
+ */
+class rcube_imap_search
+{
+    public $options = array();
+
+    private $size = 10;
+    private $next = 0;
+    private $workers = array();
+    private $states = array();
+    private $jobs = array();
+    private $conn;
+
+    /**
+     * Default constructor
+     */
+    public function __construct($options, $conn)
+    {
+        $this->options = $options;
+        $this->conn = $conn;
+    }
+
+    /**
+     * Invoke search request to IMAP server
+     *
+     * @param  array   $folders    List of IMAP folders to search in
+     * @param  string  $str        Search criteria
+     * @param  string  $charset    Search charset
+     * @param  string  $sort_field Header field to sort by
+     * @param  boolean $threading  True if threaded listing is active
+     */
+    public function exec($folders, $str, $charset = null, $sort_field = null, $threading=null)
+    {
+        $pthreads = defined('PTHREADS_INHERIT_ALL');
+
+        // start a search job for every folder to search in
+        foreach ($folders as $folder) {
+            $job = new rcube_imap_search_job($folder, $str, $charset, $sort_field, $threading);
+            if ($pthreads && $this->submit($job)) {
+                $this->jobs[] = $job;
+            }
+            else {
+                $job->worker = $this;
+                $job->run();
+                $this->jobs[] = $job;
+            }
+        }
+
+        // wait for all workers to be done
+        $this->shutdown();
+
+        // gather results
+        $results = new rcube_result_multifolder;
+        foreach ($this->jobs as $job) {
+            $results->add($job->get_result());
+        }
+
+        return $results;
+    }
+
+    /**
+     * Assign the given job object to one of the worker threads for execution
+     */
+    public function submit(Stackable $job)
+    {
+        if (count($this->workers) < $this->size) {
+            $id = count($this->workers);
+            $this->workers[$id] = new rcube_imap_search_worker($id, $this->options);
+            $this->workers[$id]->start(PTHREADS_INHERIT_ALL);
+
+            if ($this->workers[$id]->stack($job)) {
+                return $job;
+            }
+            else {
+                // trigger_error(sprintf("Failed to push Stackable onto %s", $id), E_USER_WARNING);
+            }
+        }
+        if (($worker = $this->workers[$this->next])) {
+            $this->next = ($this->next+1) % $this->size;
+            if ($worker->stack($job)) {
+                return $job;
+            }
+            else {
+                // trigger_error(sprintf("Failed to stack onto selected worker %s", $worker->id), E_USER_WARNING);
+            }
+        }
+        else {
+            // trigger_error(sprintf("Failed to select a worker for Stackable"), E_USER_WARNING);
+        }
+
+        return false;
+    }
+
+    /**
+     * Shutdown the pool of threads cleanly, retaining exit status locally
+     */
+    public function shutdown()
+    {
+        foreach ($this->workers as $worker) {
+            $this->states[$worker->getThreadId()] = $worker->shutdown();
+            $worker->close();
+        }
+
+        # console('shutdown', $this->states);
+    }
+    
+    /**
+     * Get connection to the IMAP server
+     * (used for single-thread mode)
+     */
+    public function get_imap()
+    {
+        return $this->conn;
+    }
+}
+
+
+/**
+ * Stackable item to run the search on a specific IMAP folder
+ */
+class rcube_imap_search_job extends Stackable
+{
+    private $folder;
+    private $search;
+    private $charset;
+    private $sort_field;
+    private $threading;
+    private $searchset;
+    private $result;
+    private $pagesize = 100;
+
+    public function __construct($folder, $str, $charset = null, $sort_field = null, $threading=false)
+    {
+        $this->folder = $folder;
+        $this->search = $str;
+        $this->charset = $charset;
+        $this->sort_field = $sort_field;
+        $this->threading = $threading;
+    }
+
+    public function run()
+    {
+        // trigger_error("Start search $this->folder", E_USER_NOTICE);
+        $this->result = $this->search_index();
+        // trigger_error("End search $this->folder: " . $this->result->count(), E_USER_NOTICE);
+    }
+
+    /**
+     * Copy of rcube_imap::search_index()
+     */
+    protected function search_index()
+    {
+        $pthreads = defined('PTHREADS_INHERIT_ALL');
+        $criteria = $this->search;
+        $charset = $this->charset;
+
+        $imap = $this->worker->get_imap();
+
+        if (!$imap->connected()) {
+            trigger_error("No IMAP connection for $this->folder", E_USER_WARNING);
+
+            if ($this->threading) {
+                return new rcube_result_thread();
+            }
+            else {
+                return new rcube_result_index();
+            }
+        }
+
+        if ($this->worker->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
+            $criteria = 'UNDELETED '.$criteria;
+        }
+
+        // unset CHARSET if criteria string is ASCII, this way
+        // SEARCH won't be re-sent after "unsupported charset" response
+        if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
+            $charset = 'US-ASCII';
+        }
+
+        if ($this->threading) {
+            $threads = $imap->thread($this->folder, $this->threading, $criteria, true, $charset);
+
+            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
+            // but I've seen that Courier doesn't support UTF-8)
+            if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
+                $threads = $imap->thread($this->folder, $this->threading,
+                    rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
+            }
+
+            // close IMAP connection again
+            if ($pthreads)
+                $imap->closeConnection();
+
+            return $threads;
+        }
+
+        if ($this->sort_field) {
+            $messages = $imap->sort($this->folder, $this->sort_field, $criteria, true, $charset);
+
+            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
+            // but I've seen Courier with disabled UTF-8 support)
+            if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
+                $messages = $imap->sort($this->folder, $this->sort_field,
+                    rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
+            }
+        }
+
+        if (!$messages || $messages->is_error()) {
+            $messages = $imap->search($this->folder,
+                ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
+
+            // Error, try with US-ASCII (some servers may support only US-ASCII)
+            if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
+                $messages = $imap->search($this->folder,
+                    rcube_imap::convert_criteria($criteria, $charset), true);
+            }
+        }
+
+        // close IMAP connection again
+        if ($pthreads)
+            $imap->closeConnection();
+
+        return $messages;
+    }
+
+    public function get_search_set()
+    {
+        return array(
+            $this->search,
+            $this->result,
+            $this->charset,
+            $this->sort_field,
+            $this->threading,
+        );
+    }
+
+    public function get_result()
+    {
+        return $this->result;
+    }
+}
+
+
+/**
+ * Worker thread to run search jobs while maintaining a common context
+ */
+class rcube_imap_search_worker extends Worker
+{
+    public $id;
+    public $options;
+
+    private $conn;
+    private $counts = 0;
+
+    /**
+     * Default constructor
+     */
+    public function __construct($id, $options)
+    {
+        $this->id = $id;
+        $this->options = $options;
+    }
+
+    /**
+     * Get a dedicated connection to the IMAP server
+     */
+    public function get_imap()
+    {
+        // TODO: make this connection persistent for several jobs
+        // This doesn't seem to work. Socket connections don't survive serialization which is used in pthreads
+
+        $conn = new rcube_imap_generic();
+        # $conn->setDebug(true, function($conn, $message){ trigger_error($message, E_USER_NOTICE); });
+
+        if ($this->options['user'] && $this->options['password']) {
+            $this->options['ident']['command'] = 'search-' . $this->id . 't' . ++$this->counts;
+            $conn->connect($this->options['host'], $this->options['user'], $this->options['password'], $this->options);
+        }
+
+        if ($conn->error)
+            trigger_error($conn->error, E_USER_WARNING);
+
+        return $conn;
+    }
+
+    /**
+     * @override
+     */
+    public function run()
+    {
+        
+    }
+
+    /**
+     * Close IMAP connection
+     */
+    public function close()
+    {
+        if ($this->conn) {
+            $this->conn->close();
+        }
+    }
+}
+
diff --git a/program/lib/Roundcube/rcube_message_header.php b/program/lib/Roundcube/rcube_message_header.php
index 2c5e2b6..2bda930 100644
--- a/program/lib/Roundcube/rcube_message_header.php
+++ b/program/lib/Roundcube/rcube_message_header.php
@@ -167,6 +167,13 @@
     public $mdn_to;
 
     /**
+     * IMAP folder this message is stored in
+     *
+     * @var string
+     */
+    public $folder;
+
+    /**
      * Other message headers
      *
      * @var array
@@ -189,6 +196,8 @@
         'reply-to'  => 'replyto',
         'cc'        => 'cc',
         'bcc'       => 'bcc',
+        'mbox'      => 'folder',
+        'folder'    => 'folder',
         'content-transfer-encoding' => 'encoding',
         'in-reply-to'               => 'in_reply_to',
         'content-type'              => 'ctype',
diff --git a/program/lib/Roundcube/rcube_result_multifolder.php b/program/lib/Roundcube/rcube_result_multifolder.php
new file mode 100644
index 0000000..188fb26
--- /dev/null
+++ b/program/lib/Roundcube/rcube_result_multifolder.php
@@ -0,0 +1,250 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
+ | Copyright (C) 2011, Kolab Systems AG                                  |
+ |                                                                       |
+ | Licensed under the GNU General Public License version 3 or            |
+ | any later version with exceptions for skins & plugins.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   SORT/SEARCH/ESEARCH response handler                                |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com>                        |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Class holding a set of rcube_result_index instances that together form a
+ * result set of a multi-folder search
+ *
+ * @package    Framework
+ * @subpackage Storage
+ */
+class rcube_result_multifolder
+{
+    public $multi = true;
+    public $sets = array();
+    public $folder;
+
+    protected $meta = array();
+    protected $index = array();
+    protected $sorting;
+    protected $order = 'ASC';
+
+
+    /**
+     * Object constructor.
+     */
+    public function __construct()
+    {
+        $this->meta = array('count' => 0);
+    }
+
+
+    /**
+     * Initializes object with SORT command response
+     *
+     * @param string $data IMAP response string
+     */
+    public function add($result)
+    {
+        if ($count = $result->count()) {
+            $this->sets[] = $result;
+            $this->meta['count'] += $count;
+
+            // append UIDs to global index
+            $folder = $result->get_parameters('MAILBOX');
+            $index = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $result->get());
+            $this->index = array_merge($this->index, $index);
+        }
+    }
+
+    /**
+     * Store a global index of (sorted) message UIDs
+     */
+    public function set_message_index($headers, $sort_field, $sort_order)
+    {
+        $this->index = array();
+        foreach ($headers as $header) {
+            $this->index[] = $header->uid . '-' . $header->folder;
+        }
+
+        $this->sorting = $sort_field;
+        $this->order = $sort_order;
+    }
+
+    /**
+     * Checks the result from IMAP command
+     *
+     * @return bool True if the result is an error, False otherwise
+     */
+    public function is_error()
+    {
+        return false;
+    }
+
+
+    /**
+     * Checks if the result is empty
+     *
+     * @return bool True if the result is empty, False otherwise
+     */
+    public function is_empty()
+    {
+        return empty($this->sets) || $this->meta['count'] == 0;
+    }
+
+
+    /**
+     * Returns number of elements in the result
+     *
+     * @return int Number of elements
+     */
+    public function count()
+    {
+        return $this->meta['count'];
+    }
+
+
+    /**
+     * Returns number of elements in the result.
+     * Alias for count() for compatibility with rcube_result_thread
+     *
+     * @return int Number of elements
+     */
+    public function count_messages()
+    {
+        return $this->count();
+    }
+
+
+    /**
+     * Reverts order of elements in the result
+     */
+    public function revert()
+    {
+        $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
+    }
+
+
+    /**
+     * Check if the given message ID exists in the object
+     *
+     * @param int  $msgid     Message ID
+     * @param bool $get_index When enabled element's index will be returned.
+     *                        Elements are indexed starting with 0
+     * @return mixed False if message ID doesn't exist, True if exists or
+     *               index of the element if $get_index=true
+     */
+    public function exists($msgid, $get_index = false)
+    {
+        if (!empty($this->folder)) {
+            $msgid .= '-' . $this->folder;
+        }
+        return array_search($msgid, $this->index);
+    }
+
+
+    /**
+     * Filters data set. Removes elements listed in $ids list.
+     *
+     * @param array $ids List of IDs to remove.
+     * @param string $folder IMAP folder
+     */
+    public function filter($ids = array(), $folder = null)
+    {
+        $this->meta['count'] = 0;
+        foreach ($this->sets as $set) {
+            if ($set->get_parameters('MAILBOX') == $folder) {
+                $set->filter($ids);
+            }
+            $this->meta['count'] += $set->count();
+        }
+    }
+
+    /**
+     * Filters data set. Removes elements not listed in $ids list.
+     *
+     * @param array $ids List of IDs to keep.
+     */
+    public function intersect($ids = array())
+    {
+        // not implemented
+    }
+
+    /**
+     * Return all messages in the result.
+     *
+     * @return array List of message IDs
+     */
+    public function get()
+    {
+        return $this->index;
+    }
+
+
+    /**
+     * Return all messages in the result.
+     *
+     * @return array List of message IDs
+     */
+    public function get_compressed()
+    {
+        return '';
+    }
+
+
+    /**
+     * Return result element at specified index
+     *
+     * @param int|string  $index  Element's index or "FIRST" or "LAST"
+     *
+     * @return int Element value
+     */
+    public function get_element($idx)
+    {
+        switch ($idx) {
+            case 'FIRST': return $this->index[0];
+            case 'LAST':  return end($this->index);
+            default:      return $this->index[$idx];
+        }
+    }
+
+
+    /**
+     * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
+     * or internal data e.g. MAILBOX, ORDER
+     *
+     * @param string $param  Parameter name
+     *
+     * @return array|string Response parameters or parameter value
+     */
+    public function get_parameters($param=null)
+    {
+        $params = array(
+            'SORT' => $this->sorting,
+            'ORDER' => $this->order,
+        );
+
+        if ($param !== null) {
+            return $params[$param];
+        }
+
+        return $params;
+    }
+
+
+    /**
+     * Returns length of internal data representation
+     *
+     * @return int Data length
+     */
+    protected function length()
+    {
+        return $this->count();
+    }
+}
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index 61890a6..05eab67 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -208,6 +208,10 @@
 $labels['body']  = 'Body';
 $labels['type'] = 'Type';
 $labels['namex'] = 'Name';
+$labels['searchscope'] = 'Scope';
+$labels['currentfolder'] = 'Current folder';
+$labels['subfolders'] = 'This and subfolders';
+$labels['allfolders'] = 'All folders';
 
 $labels['openinextwin'] = 'Open in new window';
 $labels['emlsave'] = 'Download (.eml)';
diff --git a/program/steps/mail/autocomplete.inc b/program/steps/mail/autocomplete.inc
index 20cf940..71b337a 100644
--- a/program/steps/mail/autocomplete.inc
+++ b/program/steps/mail/autocomplete.inc
@@ -49,7 +49,7 @@
 $single = (bool) $RCMAIL->config->get('autocomplete_single');
 $search = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC, true);
 $source = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
-$sid    = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+$reqid  = rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC);
 
 if (strlen($source)) {
     $book_types = array($source);
@@ -155,5 +155,5 @@
     }
 }
 
-$OUTPUT->command('ksearch_query_results', $contacts, $search, $sid);
+$OUTPUT->command('ksearch_query_results', $contacts, $search, $reqid);
 $OUTPUT->send();
diff --git a/program/steps/mail/copy.inc b/program/steps/mail/copy.inc
index a392f30..0f7b1a0 100644
--- a/program/steps/mail/copy.inc
+++ b/program/steps/mail/copy.inc
@@ -26,11 +26,11 @@
 
 // move messages
 if (!empty($_POST['_uid']) && strlen($_POST['_target_mbox'])) {
-    $uids   = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
     $target = rcube_utils::get_input_value('_target_mbox', rcube_utils::INPUT_POST, true);
-    $mbox   = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
 
-    $copied = $RCMAIL->storage->copy_message($uids, $target, $mbox);
+    foreach (rcmail_get_uids() as $mbox => $uids) {
+      $copied += (int)$RCMAIL->storage->copy_message($uids, $target, $mbox);
+    }
 
     if (!$copied) {
         // send error message
diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
index 072ee71..066d381 100644
--- a/program/steps/mail/func.inc
+++ b/program/steps/mail/func.inc
@@ -68,6 +68,23 @@
     $OUTPUT->set_env('search_text', $_SESSION['last_text_search']);
 }
 
+// remove mbox part from _uid
+if (($_uid  = get_input_value('_uid', RCUBE_INPUT_GPC)) && preg_match('/^\d+-[^,]+$/', $_uid)) {
+  list($_uid, $mbox) = explode('-', $_uid);
+  if (isset($_GET['_uid']))  $_GET['_uid']  = $_uid;
+  if (isset($_POST['_uid'])) $_POST['_uid'] = $_uid;
+  $_REQUEST['_uid'] = $_uid;
+  unset($_uid);
+
+  // override mbox
+  if (!empty($mbox)) {
+    $_GET['_mbox']  = $mbox;
+    $_POST['_mbox'] = $mbox;
+    $RCMAIL->storage->set_folder(($_SESSION['mbox'] = $mbox));
+  }
+}
+
+
 // set main env variables, labels and page title
 if (empty($RCMAIL->action) || $RCMAIL->action == 'list') {
     // connect to storage server and trigger error on failure
@@ -88,6 +105,9 @@
         }
 
         $OUTPUT->set_env('search_mods', rcmail_search_mods());
+
+        if (!empty($_SESSION['search_scope']))
+            $OUTPUT->set_env('search_scope', $_SESSION['search_scope']);
     }
 
     $threading = (bool) $RCMAIL->storage->get_threading();
@@ -166,6 +186,34 @@
 ));
 
 
+/**
+ * Returns message UID(s) and IMAP folder(s) from GET/POST data
+ *
+ * @return array List of message UIDs per folder
+ */
+function rcmail_get_uids()
+{
+    // message UID (or comma-separated list of IDs) is provided in
+    // the form of <ID>-<MBOX>[,<ID>-<MBOX>]*
+
+    $_uid  = get_input_value('_uid', RCUBE_INPUT_GPC);
+    $_mbox = (string)get_input_value('_mbox', RCUBE_INPUT_GPC);
+
+    if (is_array($uid)) {
+        return $uid;
+    }
+
+    // create a per-folder UIDs array
+    $result = array();
+    foreach (explode(',', $_uid) as $uid) {
+        list($uid, $mbox) = explode('-', $uid, 2);
+        if (empty($mbox))
+            $mbox = $_mbox;
+        $result[$mbox][] = $uid;
+    }
+
+    return $result;
+}
 
 /**
  * Returns default search mods
@@ -315,7 +363,7 @@
 /**
  * return javascript commands to add rows to the message list
  */
-function rcmail_js_message_list($a_headers, $insert_top=FALSE, $a_show_cols=null)
+function rcmail_js_message_list($a_headers, $insert_top=false, $a_show_cols=null)
 {
     global $RCMAIL, $OUTPUT;
 
@@ -334,6 +382,14 @@
         $head_replace = true;
     }
 
+    // add 'folder' column to list on multi-folder searches
+    $search_set = $RCMAIL->storage->get_search_set();
+    $multifolder = $search_set && $search_set[1]->multi;
+    if ($multifolder && !in_array('folder', $a_show_cols)) {
+        $a_show_cols[] = 'folder';
+        $head_replace = true;
+    }
+
     $mbox = $RCMAIL->storage->get_folder();
 
     // make sure 'threads' and 'subject' columns are present
@@ -341,8 +397,6 @@
         array_unshift($a_show_cols, 'subject');
     if (!in_array('threads', $a_show_cols))
         array_unshift($a_show_cols, 'threads');
-
-    $_SESSION['list_attrib']['columns'] = $a_show_cols;
 
     // Make sure there are no duplicated columns (#1486999)
     $a_show_cols = array_unique($a_show_cols);
@@ -364,6 +418,10 @@
 
     $OUTPUT->command('set_message_coltypes', $a_show_cols, $thead, $smart_col);
 
+    if ($multifolder) {
+        $OUTPUT->command('select_folder', '');
+    }
+
     if (empty($a_headers)) {
         return;
     }
@@ -379,6 +437,14 @@
     foreach ($a_headers as $header) {
         if (empty($header))
             continue;
+
+        // make message UIDs unique by appending the folder name
+        if ($multifolder) {
+            $header->uid .= '-'.$header->folder;
+            $header->flags['skip_mbox_check'] = true;
+            if ($header->parent_uid)
+                $header->parent_uid .= '-'.$header->folder;
+        }
 
         $a_msg_cols  = array();
         $a_msg_flags = array();
@@ -398,6 +464,8 @@
                 $cont = show_bytes($header->$col);
             else if ($col == 'date')
                 $cont = $RCMAIL->format_date($header->date);
+            else if ($col == 'folder')
+                $cont = rcube::Q(rcube_charset::convert($header->folder, 'UTF7-IMAP'));
             else
                 $cont = rcube::Q($header->$col);
 
@@ -421,7 +489,7 @@
             $a_msg_flags['prio'] = (int) $header->priority;
 
         $a_msg_flags['ctype'] = rcube::Q($header->ctype);
-        $a_msg_flags['mbox']  = $mbox;
+        $a_msg_flags['mbox']  = $header->folder;
 
         // merge with plugin result (Deprecated, use $header->flags)
         if (!empty($header->list_flags) && is_array($header->list_flags))
diff --git a/program/steps/mail/list.inc b/program/steps/mail/list.inc
index 277564c..18f771d 100644
--- a/program/steps/mail/list.inc
+++ b/program/steps/mail/list.inc
@@ -42,6 +42,7 @@
 
 // is there a set of columns for this request?
 if ($cols = rcube_utils::get_input_value('_cols', rcube_utils::INPUT_GET)) {
+  $_SESSION['list_attrib']['columns'] = $cols;
   if (!in_array('list_cols', $dont_override)) {
     $save_arr['list_cols'] = explode(',', $cols);
   }
@@ -101,7 +102,8 @@
 $OUTPUT->command('set_rowcount', rcmail_get_messagecount_text($count), $mbox_name);
 
 // add message rows
-rcmail_js_message_list($a_headers, FALSE, $cols);
+rcmail_js_message_list($a_headers, false, $cols);
+
 if (isset($a_headers) && count($a_headers)) {
   if ($search_request) {
     $OUTPUT->show_message('searchsuccessful', 'confirmation', array('nr' => $count));
diff --git a/program/steps/mail/mark.inc b/program/steps/mail/mark.inc
index daa8c7e..50243c6 100644
--- a/program/steps/mail/mark.inc
+++ b/program/steps/mail/mark.inc
@@ -36,7 +36,7 @@
     'unflagged' => 'UNFLAGGED',
 );
 
-if (($uids = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST))
+if (($_uids = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST))
     && ($flag = rcube_utils::get_input_value('_flag', rcube_utils::INPUT_POST))
 ) {
     $flag = $a_flags_map[$flag] ? $a_flags_map[$flag] : strtoupper($flag);
@@ -45,10 +45,12 @@
         // count messages before changing anything
         $old_count = $RCMAIL->storage->count(NULL, $threading ? 'THREADS' : 'ALL');
         $old_pages = ceil($old_count / $RCMAIL->storage->get_pagesize());
-        $count     = sizeof(explode(',', $uids));
     }
 
-    $marked = $RCMAIL->storage->set_flag($uids, $flag);
+    foreach (rcmail_get_uids() as $mbox => $uids) {
+        $marked += (int)$RCMAIL->storage->set_flag($uids, $flag, $mbox);
+        $count += count($uids);
+    }
 
     if (!$marked) {
         // send error message
@@ -128,7 +130,7 @@
             }
 
             // add new rows from next page (if any)
-            if ($count && $uids != '*' && ($jump_back || $nextpage_count > 0)) {
+            if ($old_count && $_uids != '*' && ($jump_back || $nextpage_count > 0)) {
                 $a_headers = $RCMAIL->storage->list_messages($mbox, NULL,
                     rcmail_sort_column(), rcmail_sort_order(), $jump_back ? NULL : $count);
 
diff --git a/program/steps/mail/move_del.inc b/program/steps/mail/move_del.inc
index 7564bb8..26c7245 100644
--- a/program/steps/mail/move_del.inc
+++ b/program/steps/mail/move_del.inc
@@ -5,7 +5,7 @@
  | program/steps/mail/move_del.inc                                       |
  |                                                                       |
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2005-2009, The Roundcube Dev Team                       |
+ | Copyright (C) 2005-2013, The Roundcube Dev Team                       |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -32,11 +32,13 @@
 
 // move messages
 if ($RCMAIL->action == 'move' && !empty($_POST['_uid']) && strlen($_POST['_target_mbox'])) {
-    $count  = sizeof(explode(',', ($uids = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST))));
     $target = rcube_utils::get_input_value('_target_mbox', rcube_utils::INPUT_POST, true);
-    $mbox   = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
+    $trash  = $RCMAIL->config->get('trash_mbox');
 
-    $moved = $RCMAIL->storage->move_message($uids, $target, $mbox);
+    foreach (rcmail_get_uids() as $mbox => $uids) {
+        $moved += (int)$RCMAIL->storage->move_message($uids, $target, $mbox);
+        $count += count($uids);
+    }
 
     if (!$moved) {
         // send error message
@@ -47,17 +49,17 @@
         exit;
     }
     else {
-      $OUTPUT->show_message('messagemoved', 'confirmation');
+        $OUTPUT->show_message('messagemoved', 'confirmation');
     }
 
     $addrows = true;
 }
 // delete messages 
 else if ($RCMAIL->action=='delete' && !empty($_POST['_uid'])) {
-    $count = sizeof(explode(',', ($uids = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST))));
-    $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST, true);
-
-    $del = $RCMAIL->storage->delete_message($uids, $mbox);
+    foreach (rcmail_get_uids() as $mbox => $uids) {
+        $del += (int)$RCMAIL->storage->delete_message($uids, $mbox);
+        $count += count($uids);
+    }
 
     if (!$del) {
         // send error message
@@ -68,7 +70,7 @@
         exit;
     }
     else {
-      $OUTPUT->show_message('messagedeleted', 'confirmation');
+        $OUTPUT->show_message('messagedeleted', 'confirmation');
     }
 
     $addrows = true;
diff --git a/program/steps/mail/search.inc b/program/steps/mail/search.inc
index ba8b124..941e68b 100644
--- a/program/steps/mail/search.inc
+++ b/program/steps/mail/search.inc
@@ -21,6 +21,8 @@
 
 $REMOTE_REQUEST = TRUE;
 
+@set_time_limit(170);  // extend default max_execution_time to ~3 minutes
+
 // reset list_page and old search results
 $RCMAIL->storage->set_page(1);
 $RCMAIL->storage->set_search_set(NULL);
@@ -35,6 +37,7 @@
 $mbox    = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GET, true);
 $filter  = rcube_utils::get_input_value('_filter', rcube_utils::INPUT_GET);
 $headers = rcube_utils::get_input_value('_headers', rcube_utils::INPUT_GET);
+$scope   = rcube_utils::get_input_value('_scope', rcube_utils::INPUT_GET);
 $subject = array();
 
 $filter         = trim($filter);
@@ -102,6 +105,16 @@
     foreach ($subject as $sub) {
         $search_str .= ' ' . $sub . ' ' . rcube_imap_generic::escape($search);
     }
+
+    // search all, current or subfolders folders
+    if ($scope == 'all') {
+        $mboxes = $RCMAIL->storage->list_folders_subscribed('', '*', 'mail');
+    }
+    else if ($scope == 'sub') {
+        $mboxes = $RCMAIL->storage->list_folders_subscribed($mbox, '*', 'mail');
+        if ($mbox != 'INBOX' && $mboxes[0] == 'INBOX')
+            array_shift($mboxes);
+    }
 }
 
 $search_str  = trim($search_str);
@@ -109,8 +122,12 @@
 
 // execute IMAP search
 if ($search_str) {
-    $RCMAIL->storage->search($mbox, $search_str, $imap_charset, $sort_column);
+    $RCMAIL->storage->search($mboxes, $search_str, $imap_charset, $sort_column);
 }
+
+// Get the headers
+$result_h = $RCMAIL->storage->list_messages($mbox, 1, $sort_column, rcmail_sort_order());
+$count    = $RCMAIL->storage->count($mbox, $RCMAIL->storage->get_threading() ? 'THREADS' : 'ALL');
 
 // save search results in session
 if (!is_array($_SESSION['search'])) {
@@ -122,15 +139,11 @@
     $_SESSION['last_text_search'] = $str;
 }
 $_SESSION['search_request'] = $search_request;
-
-
-// Get the headers
-$result_h = $RCMAIL->storage->list_messages($mbox, 1, $sort_column, rcmail_sort_order());
-$count    = $RCMAIL->storage->count($mbox, $RCMAIL->storage->get_threading() ? 'THREADS' : 'ALL');
+$_SESSION['search_scope'] = $scope;
 
 // Make sure we got the headers
 if (!empty($result_h)) {
-    rcmail_js_message_list($result_h);
+    rcmail_js_message_list($result_h, false);
     if ($search_str) {
         $OUTPUT->show_message('searchsuccessful', 'confirmation', array('nr' => $RCMAIL->storage->count(NULL, 'ALL')));
     }
@@ -152,6 +165,7 @@
 
 // update message count display
 $OUTPUT->set_env('search_request', $search_str ? $search_request : '');
+$OUTPUT->set_env('threading', $RCMAIL->storage->get_threading());
 $OUTPUT->set_env('messagecount', $count);
 $OUTPUT->set_env('pagecount', ceil($count/$RCMAIL->storage->get_pagesize()));
 $OUTPUT->set_env('exists', $RCMAIL->storage->count($mbox_name, 'EXISTS'));
diff --git a/skins/larry/mail.css b/skins/larry/mail.css
index 74ce43e..34ce789 100644
--- a/skins/larry/mail.css
+++ b/skins/larry/mail.css
@@ -514,6 +514,18 @@
 	width: 155px;
 }
 
+.messagelist tr td.folder {
+	width: 135px;
+}
+
+.messagelist tr td.folder {
+	width: 135px;
+}
+
+.messagelist tr td.hidden {
+	display: none;
+}
+
 .messagelist tr.message {
 /*	background-color: #fff; */
 }
diff --git a/skins/larry/templates/mail.html b/skins/larry/templates/mail.html
index b4e216a..2f6fbe2 100644
--- a/skins/larry/templates/mail.html
+++ b/skins/larry/templates/mail.html
@@ -76,13 +76,8 @@
 <!-- list footer -->
 <div id="messagelistfooter">
 	<div id="listcontrols">
-		<roundcube:if condition="env:threads" />
-			<a href="#list" class="iconbutton listmode" id="maillistmode" title="<roundcube:label name='list' />">List</a>
-			<a href="#threads" class="iconbutton threadmode" id="mailthreadmode" title="<roundcube:label name='threads' />">Threads</a>
-		<roundcube:else />
-			<a href="#list" class="iconbutton listmode selected" title="<roundcube:label name='list' />" onclick="return false">List</a>
-			<a href="#threads" class="iconbutton threadmode disabled" title="<roundcube:label name='threads' />" onclick="return false">Threads</a>
-		<roundcube:endif />
+		<roundcube:button href="#list" command="set-listmode" prop="list" class="iconbutton listmode disabled" classAct="iconbutton listmode" id="maillistmode" title="list" content="List" />
+		<roundcube:button href="#threads" command="set-listmode" prop="threads" class="iconbutton threadmode disabled" classAct="iconbutton threadmode" id="mailthreadmode" title="threads" content="Threads" />
 	</div>
 	
 	<div id="listselectors">
@@ -132,6 +127,10 @@
 		<li><label><input type="checkbox" name="s_mods[]" value="bcc" id="s_mod_bcc" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="bcc" /></span></label></li>
 		<li><label><input type="checkbox" name="s_mods[]" value="body" id="s_mod_body" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="body" /></span></label></li>
 		<li><label><input type="checkbox" name="s_mods[]" value="text" id="s_mod_text" onclick="UI.set_searchmod(this)" /> <span><roundcube:label name="msgtext" /></span></label></li>
+		<li class="separator" id=""><label><roundcube:label name="searchscope" /></label></li>
+		<li><label><input type="radio" name="s_scope" value="base" id="s_scope_base" onclick="UI.set_searchscope(this)" /> <span><roundcube:label name="currentfolder" /></span></label></li>
+		<li><label><input type="radio" name="s_scope" value="sub" id="s_scope_sub" onclick="UI.set_searchscope(this)" /> <span><roundcube:label name="subfolders" /></span></label></li>
+		<li><label><input type="radio" name="s_scope" value="all" id="s_scope_all" onclick="UI.set_searchscope(this)" /> <span><roundcube:label name="allfolders" /></span></label></li>
 	</ul>
 </div>
 
diff --git a/skins/larry/ui.js b/skins/larry/ui.js
index 05bfa70..74aef8d 100644
--- a/skins/larry/ui.js
+++ b/skins/larry/ui.js
@@ -41,6 +41,7 @@
   this.show_popup = show_popup;
   this.add_popup = add_popup;
   this.set_searchmod = set_searchmod;
+  this.set_searchscope = set_searchscope;
   this.show_uploadform = show_uploadform;
   this.show_header_row = show_header_row;
   this.hide_header_row = hide_header_row;
@@ -138,7 +139,8 @@
     if (rcmail.env.task == 'mail') {
       rcmail.addEventListener('menu-open', menu_open)
         .addEventListener('menu-save', menu_save)
-        .addEventListener('responseafterlist', function(e){ switch_view_mode(rcmail.env.threading ? 'thread' : 'list') });
+        .addEventListener('responseafterlist', function(e){ switch_view_mode(rcmail.env.threading ? 'thread' : 'list', true) })
+        .addEventListener('responseaftersearch', function(e){ switch_view_mode(rcmail.env.threading ? 'thread' : 'list', true) });
 
       var dragmenu = $('#dragmessagemenu');
       if (dragmenu.length) {
@@ -729,13 +731,12 @@
   /**
    *
    */
-  function switch_view_mode(mode)
+  function switch_view_mode(mode, force)
   {
-    if (rcmail.env.threading != (mode == 'thread'))
-      rcmail.set_list_options(null, undefined, undefined, mode == 'thread' ? 1 : 0);
-
-    $('#maillistmode, #mailthreadmode').removeClass('selected');
-    $('#mail'+mode+'mode').addClass('selected');
+    if (force || !$('#mail'+mode+'mode').hasClass('disabled')) {
+      $('#maillistmode, #mailthreadmode').removeClass('selected');
+      $('#mail'+mode+'mode').addClass('selected');
+    }
   }
 
 
@@ -761,11 +762,15 @@
         obj = popups['searchmenu'],
         list = $('input:checkbox[name="s_mods[]"]', obj),
         mbox = rcmail.env.mailbox,
-        mods = rcmail.env.search_mods;
+        mods = rcmail.env.search_mods,
+        scope = rcmail.env.search_scope || 'base';
 
       if (rcmail.env.task == 'mail') {
+        if (scope == 'all')
+          mbox = '*';
         mods = mods[mbox] ? mods[mbox] : mods['*'];
         all = 'text';
+        $('#s_scope_'+scope).attr('checked',true);
       }
       else {
         all = '*';
@@ -896,7 +901,11 @@
   {
     var all, m, task = rcmail.env.task,
       mods = rcmail.env.search_mods,
-      mbox = rcmail.env.mailbox;
+      mbox = rcmail.env.mailbox,
+      scope = $('input[name="s_scope"]:checked').val();
+
+    if (scope == 'all')
+      mbox = '*';
 
     if (!mods)
       mods = {};
@@ -937,6 +946,11 @@
     });
   }
 
+  function set_searchscope(elem)
+  {
+    rcmail.env.search_scope = elem.value;
+  }
+
   function push_contactgroup(p)
   {
     // lets the contacts list swipe to the left, nice!

--
Gitblit v1.9.1