diff options
| author | Thomas Bruederli <thomas@roundcube.net> | 2014-04-07 09:53:46 +0200 | 
|---|---|---|
| committer | Thomas Bruederli <thomas@roundcube.net> | 2014-04-07 09:53:46 +0200 | 
| commit | 66536974fe12a02ca5ffcec4354bf5113282a0cc (patch) | |
| tree | 034b848ec971008eab1003a2a9a401fb7020a517 /program | |
| parent | c77a8497e7c4b04dd881e55341c779f6fe5ffa34 (diff) | |
| parent | e3857bb6c2a2f9a4331a72b0bd74e0d70ee8a8a1 (diff) | |
Merge branch 'dev-multi-folder-search'
Diffstat (limited to 'program')
| -rw-r--r-- | program/js/app.js | 301 | ||||
| -rw-r--r-- | program/js/list.js | 69 | ||||
| -rw-r--r-- | program/lib/Roundcube/rcube_imap.php | 108 | ||||
| -rw-r--r-- | program/lib/Roundcube/rcube_imap_search.php | 336 | ||||
| -rw-r--r-- | program/lib/Roundcube/rcube_message_header.php | 9 | ||||
| -rw-r--r-- | program/lib/Roundcube/rcube_result_multifolder.php | 250 | ||||
| -rw-r--r-- | program/localization/en_US/labels.inc | 4 | ||||
| -rw-r--r-- | program/steps/mail/autocomplete.inc | 4 | ||||
| -rw-r--r-- | program/steps/mail/copy.inc | 6 | ||||
| -rw-r--r-- | program/steps/mail/func.inc | 76 | ||||
| -rw-r--r-- | program/steps/mail/list.inc | 4 | ||||
| -rw-r--r-- | program/steps/mail/mark.inc | 10 | ||||
| -rw-r--r-- | program/steps/mail/move_del.inc | 22 | ||||
| -rw-r--r-- | program/steps/mail/search.inc | 28 | 
14 files changed, 1074 insertions, 153 deletions
| diff --git a/program/js/app.js b/program/js/app.js index eea72f0ee..f548ad850 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -31,6 +31,7 @@ function rcube_webmail()    this.onloads = [];    this.messages = {};    this.group2expand = {}; +  this.http_request_jobs = {};    // webmail client settings    this.dblclick_time = 500; @@ -216,6 +217,7 @@ function rcube_webmail()            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 @@ function rcube_webmail()        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 @@ function rcube_webmail()          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') { @@ -712,6 +723,10 @@ function rcube_webmail()            this.list_contacts(props);          break; +      case 'set-listmode': +        this.set_list_options(null, undefined, undefined, props == 'threads' ? 1 : 0); +        break; +        case 'sort':          var sort_order = this.env.sort_order,            sort_col = !this.env.disabled_sort_col ? props : this.env.sort_col; @@ -792,9 +807,9 @@ function rcube_webmail()            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 @@ function rcube_webmail()        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 @@ function rcube_webmail()            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 @@ function rcube_webmail()          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 @@ function rcube_webmail()      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 @@ function rcube_webmail()    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 @@ function rcube_webmail()      // 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 @@ function rcube_webmail()        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 @@ function rcube_webmail()          + (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 @@ function rcube_webmail()      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;">  </span>'; +        tree += '<span id="rcmtab' + row.id + '" class="branch" style="width:' + (message.depth * 15) + 'px;">  </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 @@ function rcube_webmail()            message.expanded = true;          } -        expando = '<div id="rcmexpando' + uid + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">  </div>'; +        expando = '<div id="rcmexpando' + row.id + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">  </div>';          row_class += ' thread' + (message.expanded? ' expanded' : '');        } @@ -1898,14 +1915,14 @@ function rcube_webmail()          row_class += ' unroot';      } -    tree += '<span id="msgicn'+uid+'" class="'+css_class+'"> </span>'; +    tree += '<span id="msgicn'+row.id+'" class="'+css_class+'"> </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 @@ function rcube_webmail()        if (c == 'flag') {          css_class = (flags.flagged ? 'flagged' : 'unflagged'); -        html = '<span id="flagicn'+uid+'" class="'+css_class+'"> </span>'; +        html = '<span id="flagicn'+row.id+'" class="'+css_class+'"> </span>';        }        else if (c == 'attachment') {          if (/application\/|multipart\/(m|signed)/.test(flags.ctype)) @@ -1935,7 +1952,7 @@ function rcube_webmail()            css_class = 'unreadchildren';          else            css_class = 'msgicon'; -        html = '<span id="statusicn'+uid+'" class="'+css_class+'"> </span>'; +        html = '<span id="statusicn'+row.id+'" class="'+css_class+'"> </span>';        }        else if (c == 'threads')          html = expando; @@ -2033,7 +2050,7 @@ function rcube_webmail()      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 @@ function rcube_webmail()      }      if (html) -      $('#rcmtab'+uid).html(html); +      $('#rcmtab'+this.html_identifier(uid, true)).html(html);    };    // update parent in a thread @@ -2462,14 +2479,14 @@ function rcube_webmail()          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 @@ function rcube_webmail()        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 @@ function rcube_webmail()      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 @@ function rcube_webmail()      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 @@ function rcube_webmail()        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 @@ function rcube_webmail()        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'; - -    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); +    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.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 @@ function rcube_webmail()      // 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 @@ function rcube_webmail()      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 @@ function rcube_webmail()    // 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 @@ function rcube_webmail()      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 @@ function rcube_webmail()      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 @@ function rcube_webmail()        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 @@ function rcube_webmail()      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 476edae4b..9b7779c7b 100644 --- a/program/js/list.js +++ b/program/js/list.js @@ -107,11 +107,11 @@ init: function()   */  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 @@ insert_row: function(row, before)      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]; @@ -390,6 +391,20 @@ blur: function()  /** + * 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'); +}, + + +/**   * onmousedown-handler of message list column   */  drag_column: function(e, col) @@ -583,7 +598,7 @@ expand: function(row)      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 @@ collapse_all: function(row)      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 @@ collapse_all: function(row)            $(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 @@ expand_all: function(row)      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 @@ expand_all: function(row)          $(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 @@ expand_all: function(row)  }, -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_prev_row: function()  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_first_row: function()  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 @@ use_arrow_key: function(keyCode, mod_key)          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 @@ drag_mouse_move: function(e)      // 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 628338a44..f60be620c 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -332,6 +332,10 @@ class rcube_imap extends rcube_storage          $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 @@ class rcube_imap extends rcube_storage              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 @@ class rcube_imap extends rcube_storage          }          foreach ($headers as $h) { +            $h->folder = $folder;              $a_msg_headers[$h->uid] = $h;          } @@ -1234,8 +1287,13 @@ class rcube_imap extends rcube_storage                  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 @@ class rcube_imap extends rcube_storage              $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 @@ class rcube_imap extends rcube_storage              // 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 @@ class rcube_imap extends rcube_storage              // 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 @@ class rcube_imap extends rcube_storage          // 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 @@ class rcube_imap extends rcube_storage       *       * @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 @@ class rcube_imap extends rcube_storage      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);          } @@ -1605,6 +1687,11 @@ class rcube_imap extends rcube_storage              $folder = $this->folder;          } +        // decode combined UID-folder identifier +        if (preg_match('/^\d+-[^,]+$/', $uid)) { +            list($uid, $folder) = explode('-', $uid); +        } +          // get cached headers          if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {              $headers = $mcache->get_message($folder, $uid); @@ -1636,6 +1723,11 @@ class rcube_imap extends rcube_storage              $folder = $this->folder;          } +        // decode combined UID-folder identifier +        if (preg_match('/^\d+-[^,]+$/', $uid)) { +            list($uid, $folder) = explode('-', $uid); +        } +          // Check internal cache          if (!empty($this->icache['message'])) {              if (($headers = $this->icache['message']) && $headers->uid == $uid) { @@ -2397,7 +2489,7 @@ class rcube_imap extends rcube_storage                      $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 000000000..c88198140 --- /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 2c5e2b6c8..2bda930eb 100644 --- a/program/lib/Roundcube/rcube_message_header.php +++ b/program/lib/Roundcube/rcube_message_header.php @@ -167,6 +167,13 @@ class rcube_message_header      public $mdn_to;      /** +     * IMAP folder this message is stored in +     * +     * @var string +     */ +    public $folder; + +    /**       * Other message headers       *       * @var array @@ -189,6 +196,8 @@ class rcube_message_header          '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 000000000..188fb26f2 --- /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 61890a642..05eab6713 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -208,6 +208,10 @@ $labels['msgtext']  = 'Entire message';  $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 20cf94084..71b337a53 100644 --- a/program/steps/mail/autocomplete.inc +++ b/program/steps/mail/autocomplete.inc @@ -49,7 +49,7 @@ $mode   = (int) $RCMAIL->config->get('addressbook_search_mode');  $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 @@ if (!empty($book_types) && strlen($search)) {      }  } -$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 a392f309f..0f7b1a03a 100644 --- a/program/steps/mail/copy.inc +++ b/program/steps/mail/copy.inc @@ -26,11 +26,11 @@ if (!$OUTPUT->ajax_call) {  // 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 072ee716c..066d38151 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -68,6 +68,23 @@ if (!empty($_REQUEST['_search']) && isset($_SESSION['search'])      $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 @@ if (empty($RCMAIL->action) || $RCMAIL->action == 'list') {          }          $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 @@ $RCMAIL->register_action_map(array(  )); +/** + * 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 @@ function rcmail_message_list($attrib)  /**   * 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 @@ function rcmail_js_message_list($a_headers, $insert_top=FALSE, $a_show_cols=null          $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 @@ -342,8 +398,6 @@ function rcmail_js_message_list($a_headers, $insert_top=FALSE, $a_show_cols=null      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 @@ function rcmail_js_message_list($a_headers, $insert_top=FALSE, $a_show_cols=null      $OUTPUT->command('set_message_coltypes', $a_show_cols, $thead, $smart_col); +    if ($multifolder) { +        $OUTPUT->command('select_folder', ''); +    } +      if (empty($a_headers)) {          return;      } @@ -380,6 +438,14 @@ function rcmail_js_message_list($a_headers, $insert_top=FALSE, $a_show_cols=null          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 @@ function rcmail_js_message_list($a_headers, $insert_top=FALSE, $a_show_cols=null                  $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 @@ function rcmail_js_message_list($a_headers, $insert_top=FALSE, $a_show_cols=null              $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 277564c38..18f771d8b 100644 --- a/program/steps/mail/list.inc +++ b/program/steps/mail/list.inc @@ -42,6 +42,7 @@ if ($sort = rcube_utils::get_input_value('_sort', rcube_utils::INPUT_GET)) {  // 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->set_env('exists', $exists);  $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 daa8c7e54..50243c636 100644 --- a/program/steps/mail/mark.inc +++ b/program/steps/mail/mark.inc @@ -36,7 +36,7 @@ $a_flags_map  = array(      '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 @@ if (($uids = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST))          // 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 @@ if (($uids = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST))              }              // 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 7564bb89d..26c724597 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 @@ $trash  = $RCMAIL->config->get('trash_mbox');  // 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 @@ if ($RCMAIL->action == 'move' && !empty($_POST['_uid']) && strlen($_POST['_targe          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 @@ else if ($RCMAIL->action=='delete' && !empty($_POST['_uid'])) {          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 ba8b124a3..941e68bd9 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 @@ $str     = rcube_utils::get_input_value('_q', rcube_utils::INPUT_GET, true);  $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 @@ if (!empty($subject)) {      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,9 +122,13 @@ $sort_column = rcmail_sort_column();  // 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'])) {      $_SESSION['search'] = array(); @@ -122,15 +139,11 @@ if ($search_str) {      $_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 @@ else {  // 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')); | 
