Aleksander Machniak
2013-03-15 a02c77c584906f629d382409e76f0df4d2cfaf01
program/js/app.js
@@ -3,8 +3,8 @@
 | Roundcube Webmail Client Script                                       |
 |                                                                       |
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
 | Copyright (C) 2011, Kolab Systems AG                                  |
 | Copyright (C) 2005-2013, The Roundcube Dev Team                       |
 | Copyright (C) 2011-2013, Kolab Systems AG                             |
 |                                                                       |
 | Licensed under the GNU General Public License version 3 or            |
 | any later version with exceptions for skins & plugins.                |
@@ -179,7 +179,8 @@
    }
    // enable general commands
    this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref', 'compose', 'undo', 'about', 'switch-task', true);
    this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref',
      'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-save', true);
    if (this.env.permaurl)
      this.enable_command('permaurl', 'extwin', true);
@@ -205,12 +206,13 @@
          this.message_list.addEventListener('dragend', function(e){ p.drag_end(e); });
          this.message_list.addEventListener('expandcollapse', function(e){ p.msglist_expand(e); });
          this.message_list.addEventListener('column_replace', function(e){ p.msglist_set_coltypes(e); });
          this.message_list.addEventListener('listupdate', function(e){ p.triggerEvent('listupdate', e); });
          document.onmouseup = function(e){ return p.doc_mouse_up(e); };
          this.gui_objects.messagelist.parentNode.onmousedown = function(e){ return p.click_on_list(e); };
          this.message_list.init();
          this.enable_command('toggle_status', 'toggle_flag', 'menu-open', 'menu-save', 'sort', true);
          this.enable_command('toggle_status', 'toggle_flag', 'sort', true);
          // load messages
          this.command('list');
@@ -219,15 +221,15 @@
        if (this.gui_objects.qsearchbox) {
          if (this.env.search_text != null)
            this.gui_objects.qsearchbox.value = this.env.search_text;
          $(this.gui_objects.qsearchbox).focusin(function() { rcmail.message_list.blur(); });
          $(this.gui_objects.qsearchbox).focusin(function() { rcmail.message_list && rcmail.message_list.blur(); });
        }
        this.set_button_titles();
        this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list',
          'moveto', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource',
          'print', 'load-attachment', 'show-headers', 'hide-headers', 'download',
          'forward', 'forward-inline', 'forward-attachment'];
          'print', 'load-attachment', 'download-attachment', 'show-headers', 'hide-headers', 'download',
          'forward', 'forward-inline', 'forward-attachment', 'change-format'];
        if (this.env.action == 'show' || this.env.action == 'preview') {
          this.enable_command(this.env.message_commands, this.env.uid);
@@ -251,7 +253,7 @@
          }
        }
        else if (this.env.action == 'compose') {
          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'extwin'];
          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'search', 'reset-search', 'extwin'];
          if (this.env.drafts_mailbox)
            this.env.compose_commands.push('savedraft')
@@ -459,8 +461,22 @@
      this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]);
    // map implicit containers
    if (this.gui_objects.folderlist)
    if (this.gui_objects.folderlist) {
      this.gui_containers.foldertray = $(this.gui_objects.folderlist);
      // init treelist widget
      if (window.rcube_treelist_widget) {
        this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, {
          id_prefix: 'rcmli',
          id_encode: this.html_identifier_encode,
          id_decode: this.html_identifier_decode,
          check_droptarget: function(node){ return !node.virtual && ref.check_droptarget(node.id) }
        });
        this.treelist.addEventListener('collapse', function(node){ ref.folder_collapsed(node) });
        this.treelist.addEventListener('expand', function(node){ ref.folder_collapsed(node) });
        this.treelist.addEventListener('select', function(node){ ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }) });
      }
    }
    // activate html5 file drop feature (if browser supports it and if configured)
    if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) {
@@ -509,7 +525,7 @@
      return false;
    // let the browser handle this click (shift/ctrl usually opens the link in a new window/tab)
    if ((obj && obj.href && String(obj.href).indexOf(location.href) < 0) && rcube_event.get_modifier(event)) {
    if ((obj && obj.href && String(obj.href).indexOf('#') < 0) && rcube_event.get_modifier(event)) {
      return true;
    }
@@ -580,18 +596,35 @@
      case 'extwin':
        if (this.env.action == 'compose') {
          var prevstate = this.env.compose_extwin;
          $("input[name='_action']", this.gui_objects.messageform).val('compose');
          this.gui_objects.messageform.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 });
          this.gui_objects.messageform.target = this.open_window('', 1150, 900);
          this.gui_objects.messageform.submit();
          var form = this.gui_objects.messageform;
          $("input[name='_action']", form).val('compose');
          form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 });
          form.target = this.open_window('', 1100, 900);
          form.submit();
        }
        else {
          this.open_window(this.env.permaurl, 1000, 1200);
          this.open_window(this.env.permaurl, 900, 900);
        }
        break;
      case 'change-format':
        url = this.env.permaurl + '&_format=' + props;
        if (this.env.action == 'preview')
          url = url.replace(/_action=show/, '_action=preview') + '&_framed=1';
        if (this.env.extwin)
          url += '&_extwin=1';
        location.href = url;
        break;
      case 'menu-open':
        if (props && props.menu == 'attachmentmenu') {
          var mimetype = this.env.attachments[props.id];
          this.enable_command('open-attachment', mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0);
        }
      case 'menu-save':
        this.triggerEvent(command, {props:props});
        return false;
@@ -756,7 +789,7 @@
      case 'moveto':
        if (this.task == 'mail')
          this.move_messages(props);
        else if (this.task == 'addressbook' && this.drag_active)
        else if (this.task == 'addressbook')
          this.copy_contact(null, props);
        break;
@@ -817,15 +850,16 @@
        break;
      case 'load-attachment':
        var qstring = '_mbox='+urlencode(this.env.mailbox)+'&_uid='+this.env.uid+'&_part='+props.part;
      case 'open-attachment':
      case 'download-attachment':
        var qstring = '_mbox='+urlencode(this.env.mailbox)+'&_uid='+this.env.uid+'&_part='+props,
          mimetype = this.env.attachments[props];
        // open attachment in frame if it's of a supported mimetype
        if (this.env.uid && props.mimetype && this.env.mimetypes && $.inArray(props.mimetype, $.map(this.env.mimetypes, function(v,k){ return v })) >= 0) {
          if (props.mimetype == 'text/html')
            qstring += '&_safe=1';
          this.attachment_win = window.open(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1', 'rcubemailattachment');
          if (this.attachment_win) {
            setTimeout(function(){ ref.attachment_win.focus(); }, 10);
        if (command != 'download-attachment' && mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0) {
          var attachment_win = window.open(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1', 'rcubemailattachment'+this.env.uid+props);
          if (attachment_win) {
            setTimeout(function(){ attachment_win.focus(); }, 10);
            break;
          }
        }
@@ -866,7 +900,7 @@
      case 'nextmessage':
        if (this.env.next_uid)
          this.show_message(this.env.next_uid, false, this.env.action=='preview');
          this.show_message(this.env.next_uid, false, this.env.action == 'preview');
        break;
      case 'lastmessage':
@@ -1052,8 +1086,13 @@
        this.reset_qsearch();
        this.select_all_mode = false;
        if (s && this.env.mailbox)
        if (s && this.env.action == 'compose') {
          if (this.contact_list)
            this.list_contacts_clear();
        }
        else if (s && this.env.mailbox) {
          this.list_mailbox(this.env.mailbox, 1);
        }
        else if (s && this.task == 'addressbook') {
          if (this.env.source == '') {
            for (n in this.env.address_sources) break;
@@ -1087,6 +1126,12 @@
      case 'export':
        if (this.contact_list.rowcount > 0) {
          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request });
        }
        break;
      case 'export-selected':
        if (this.contact_list.rowcount > 0) {
          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') });
        }
        break;
@@ -1201,7 +1246,7 @@
    if (!url)
      url = this.env.comm_path;
    return url.replace(/_task=[a-z]+/, '_task='+task);
    return url.replace(/_task=[a-z0-9_-]+/i, '_task='+task);
  };
  this.reload = function(delay)
@@ -1254,11 +1299,12 @@
  this.html_identifier = function(str, encode)
  {
    str = String(str);
    if (encode)
      return Base64.encode(str).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
    else
      return str.replace(this.identifier_expr, '_');
    return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_');
  };
  this.html_identifier_encode = function(str)
  {
    return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
  };
  this.html_identifier_decode = function(str)
@@ -1311,29 +1357,9 @@
    if (this.preview_read_timer)
      clearTimeout(this.preview_read_timer);
    // save folderlist and folders location/sizes for droptarget calculation in drag_move()
    if (this.gui_objects.folderlist && model) {
      this.initialBodyScrollTop = bw.ie ? 0 : window.pageYOffset;
      this.initialListScrollTop = this.gui_objects.folderlist.parentNode.scrollTop;
      var k, li, height,
        list = $(this.gui_objects.folderlist);
        pos = list.offset();
      this.env.folderlist_coords = { x1:pos.left, y1:pos.top, x2:pos.left + list.width(), y2:pos.top + list.height() };
      this.env.folder_coords = [];
      for (k in model) {
        if (li = this.get_folder_li(k)) {
          // only visible folders
          if (height = li.firstChild.offsetHeight) {
            pos = $(li.firstChild).offset();
            this.env.folder_coords[k] = { x1:pos.left, y1:pos.top,
              x2:pos.left + li.firstChild.offsetWidth, y2:pos.top + height, on:0 };
          }
        }
      }
    }
    // prepare treelist widget for dragging interactions
    if (this.treelist)
      this.treelist.drag_start();
  };
  this.drag_end = function(e)
@@ -1341,87 +1367,28 @@
    this.drag_active = false;
    this.env.last_folder_target = null;
    if (this.folder_auto_timer) {
      clearTimeout(this.folder_auto_timer);
      this.folder_auto_timer = null;
      this.folder_auto_expand = null;
    }
    // over the folders
    if (this.gui_objects.folderlist && this.env.folder_coords) {
      for (var k in this.env.folder_coords) {
        if (this.env.folder_coords[k].on)
          $(this.get_folder_li(k)).removeClass('droptarget');
      }
    }
    if (this.treelist)
      this.treelist.drag_end();
  };
  this.drag_move = function(e)
  {
    if (this.gui_objects.folderlist && this.env.folder_coords) {
      var k, li, div, check, oldclass,
    if (this.gui_objects.folderlist) {
      var drag_target, oldclass,
        layerclass = 'draglayernormal',
        mouse = rcube_event.get_mouse_pos(e),
        pos = this.env.folderlist_coords,
        // offsets to compensate for scrolling while dragging a message
        boffset = bw.ie ? -document.documentElement.scrollTop : this.initialBodyScrollTop,
        moffset = this.initialListScrollTop-this.gui_objects.folderlist.parentNode.scrollTop;
        mouse = rcube_event.get_mouse_pos(e);
      if (this.contact_list && this.contact_list.draglayer)
        oldclass = this.contact_list.draglayer.attr('class');
      mouse.y += -moffset-boffset;
      // if mouse pointer is outside of folderlist
      if (mouse.x < pos.x1 || mouse.x >= pos.x2 || mouse.y < pos.y1 || mouse.y >= pos.y2) {
        if (this.env.last_folder_target) {
          $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget');
          this.env.folder_coords[this.env.last_folder_target].on = 0;
          this.env.last_folder_target = null;
        }
        if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
          this.contact_list.draglayer.attr('class', layerclass);
        return;
      // mouse intersects a valid drop target on the treelist
      if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) {
        this.env.last_folder_target = drag_target;
        layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal');
      }
      // over the folders
      for (k in this.env.folder_coords) {
        pos = this.env.folder_coords[k];
        if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.y >= pos.y1 && mouse.y < pos.y2) {
          if (check = this.check_droptarget(k)) {
            li = this.get_folder_li(k);
            div = $(li.getElementsByTagName('div')[0]);
            // if the folder is collapsed, expand it after 1sec and restart the drag & drop process.
            if (div.hasClass('collapsed')) {
              if (this.folder_auto_timer)
                clearTimeout(this.folder_auto_timer);
              this.folder_auto_expand = this.env.mailboxes[k].id;
              this.folder_auto_timer = setTimeout(function() {
                rcmail.command('collapse-folder', rcmail.folder_auto_expand);
                rcmail.drag_start(null);
              }, 1000);
            }
            else if (this.folder_auto_timer) {
              clearTimeout(this.folder_auto_timer);
              this.folder_auto_timer = null;
              this.folder_auto_expand = null;
            }
            $(li).addClass('droptarget');
            this.env.folder_coords[k].on = 1;
            this.env.last_folder_target = k;
            layerclass = 'draglayer' + (check > 1 ? 'copy' : 'normal');
          }
          // Clear target, otherwise drag end will trigger move into last valid droptarget
          else
            this.env.last_folder_target = null;
        }
        else if (pos.on) {
          $(this.get_folder_li(k)).removeClass('droptarget');
          this.env.folder_coords[k].on = 0;
        }
      else {
        // Clear target, otherwise drag end will trigger move into last valid droptarget
        this.env.last_folder_target = null;
      }
      if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
@@ -1431,40 +1398,33 @@
  this.collapse_folder = function(name)
  {
    var li = this.get_folder_li(name, '', true),
      div = $('div:first', li),
      ul = $('ul:first', li);
    if (this.treelist)
      this.treelist.toggle(name);
  };
    if (div.hasClass('collapsed')) {
      ul.show();
      div.removeClass('collapsed').addClass('expanded');
      var reg = new RegExp('&'+urlencode(name)+'&');
      this.env.collapsed_folders = this.env.collapsed_folders.replace(reg, '');
    }
    else if (div.hasClass('expanded')) {
      ul.hide();
      div.removeClass('expanded').addClass('collapsed');
      this.env.collapsed_folders = this.env.collapsed_folders+'&'+urlencode(name)+'&';
  this.folder_collapsed = function(node)
  {
    var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders';
    if (node.collapsed) {
      this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&';
      // select the folder if one of its childs is currently selected
      // don't select if it's virtual (#1488346)
      if (this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !$(li).hasClass('virtual'))
      if (this.env.mailbox && this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !node.virtual)
        this.command('list', name);
    }
    else
      return;
    // Work around a bug in IE6 and IE7, see #1485309
    if (bw.ie6 || bw.ie7) {
      var siblings = li.nextSibling ? li.nextSibling.getElementsByTagName('ul') : null;
      if (siblings && siblings.length && (li = siblings[0]) && li.style && li.style.display != 'none') {
        li.style.display = 'none';
        li.style.display = '';
      }
    else {
      var reg = new RegExp('&'+urlencode(node.id)+'&');
      this.env[prefname] = this.env[prefname].replace(reg, '');
    }
    this.command('save-pref', { name: 'collapsed_folders', value: this.env.collapsed_folders });
    this.set_unread_count_display(name, false);
    if (!this.drag_active) {
      this.command('save-pref', { name: prefname, value: this.env[prefname] });
      if (this.env.unread_counts)
        this.set_unread_count_display(node.id, false);
    }
  };
  this.doc_mouse_up = function(e)
@@ -1489,9 +1449,9 @@
    if (this.drag_active && model && this.env.last_folder_target) {
      var target = model[this.env.last_folder_target];
      $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget');
      this.env.last_folder_target = null;
      list.draglayer.hide();
      this.drag_end(e);
      if (!this.drag_menu(e, target))
        this.command('moveto', target);
@@ -1675,11 +1635,10 @@
    var w = Math.min(width, screen.width - 10),
      h = Math.min(height, screen.height - 100),
      l = (screen.width - w) / 2 + (screen.left || 0),
      t = Math.max(0, (screen.height - h) / 2 + (screen.top || 0) - 20);
    var wname = 'rcmextwin' + new Date().getTime(),
      extwin = window.open(url + '&_extwin=1', wname, 'width='+w+',height='+h+',top='+t+',left='+l+',resizable=yes,toolbar=no,status=no');
    extwin.moveTo(l,t);
      t = Math.max(0, (screen.height - h) / 2 + (screen.top || 0) - 20),
      wname = 'rcmextwin' + new Date().getTime(),
      extwin = window.open(url + '&_extwin=1', wname,
        'width='+w+',height='+h+',top='+t+',left='+l+',resizable=yes,toolbar=no,status=no,location=no');
    // write loading... message to empty windows
    if (!url && extwin.document) {
@@ -1687,7 +1646,9 @@
    }
    // focus window, delayed to bring to front
    window.setTimeout(function(){ extwin.focus(); }, 10);
    window.setTimeout(function() { extwin.focus(); }, 10);
    // position window with setTimeout for Chrome (#1488931)
    window.setTimeout(function() { extwin.moveTo(l,t); }, bw.chrome ? 100 : 10);
    return wname;
  };
@@ -3034,10 +2995,10 @@
      input_message = $("[name='_message']").get(0),
      html_mode = $("input[name='_is_html']").val() == '1',
      ac_fields = ['cc', 'bcc', 'replyto', 'followupto'],
      ac_props;
      ac_props, opener_rc = this.opener();
    // close compose step in opener
    if (window.opener && opener.rcmail && opener.rcmail.env.action == 'compose') {
    if (opener_rc && opener_rc.env.action == 'compose') {
      setTimeout(function(){ opener.history.back(); }, 100);
      this.env.opened_extwin = true;
    }
@@ -3109,6 +3070,13 @@
    form._draft.value = draft ? '1' : '';
    form.action = this.add_url(form.action, '_unlock', msgid);
    form.action = this.add_url(form.action, '_lang', lang);
    // register timer to notify about connection timeout
    this.submit_timer = setTimeout(function(){
      ref.set_busy(false, null, msgid);
      ref.display_message(ref.get_label('requesttimedout'), 'error');
    }, this.env.request_timeout * 1000);
    form.submit();
  };
@@ -3648,7 +3616,8 @@
      // reset vars
      this.env.current_page = 1;
      r = this.http_request('search', url, lock);
      var action = this.env.action == 'compose' && this.contact_list ? 'search-contacts' : 'search';
      r = this.http_request(action, url, lock);
      this.env.qsearch = {lock: lock, request: r};
    }
@@ -3707,9 +3676,11 @@
  {
    this.display_message(msg, type);
    if (this.env.extwin && window.opener && opener.rcmail) {
    if (this.env.extwin) {
      var opener_rc = this.opener();
      this.lock_form(this.gui_objects.messageform);
      opener.rcmail.display_message(msg, type);
      if (opener_rc)
        opener_rc.display_message(msg, type);
      setTimeout(function(){ window.close() }, 1000);
    }
    else {
@@ -4115,6 +4086,7 @@
    // thend we can enable the group-remove-selected command
    this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0);
    this.enable_command('compose', this.env.group || list.selection.length > 0);
    this.enable_command('export-selected', list.selection.length > 0);
    this.enable_command('edit', id && writable);
    this.enable_command('delete', list.selection.length && writable);
@@ -4141,10 +4113,10 @@
    if (this.env.search_id)
      folder = 'S'+this.env.search_id;
    else
    else if (!this.env.search_request)
      folder = group ? 'G'+src+group : src;
    this.select_folder(folder);
    this.select_folder(folder, '', true);
    this.env.source = src;
    this.env.group = group;
@@ -4194,7 +4166,7 @@
    this.env.source = src;
    this.env.group = group;
    // also send search request to get the right messages
    // also send search request to get the right records
    if (this.env.search_request)
      url._search = this.env.search_request;
@@ -4219,12 +4191,10 @@
      target = win;
      this.show_contentframe(true);
      // load dummy content
      if (!cid) {
        // unselect selected row(s)
      // load dummy content, unselect selected row(s)
      if (!cid)
        this.contact_list.clear_selection();
        this.enable_command('delete', 'compose', false);
      }
      this.enable_command('delete', 'compose', 'export-selected', cid);
    }
    else if (framed)
      return false;
@@ -4277,7 +4247,7 @@
        this.group_member_change('add', cid, dest, to.id);
      else {
        var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
          post_data = {_cid: cid, _source: source, _to: dest, _togid: to.id, _gid: group};
          post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group};
        this.http_post('copy', post_data, lock);
      }
@@ -4285,7 +4255,7 @@
    // target is an addressbook
    else if (to.id != source) {
      var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
        post_data = {_cid: cid, _source: source, _to: to.id, _gid: group};
        post_data = {_cid: cid, _source: this.env.source, _to: to.id, _gid: group};
      this.http_post('copy', post_data, lock);
    }
@@ -4427,6 +4397,11 @@
    }
    $("input[type='text']:visible").first().focus();
    // Submit search form on Enter
    if (this.env.action == 'search')
      $(this.gui_objects.editform).append($('<input type="submit">').hide())
        .submit(function() { $('input.mainaction').click(); return false; });
  };
  this.group_create = function()
@@ -4445,7 +4420,7 @@
      this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
      this.env.group_renaming = true;
      var link, li = this.get_folder_li(this.env.source+this.env.group, 'rcmliG');
      var link, li = this.get_folder_li('G'+this.env.source+this.env.group,'',true);
      if (li && (link = li.firstChild)) {
        $(link).hide().before(this.name_input);
      }
@@ -4465,11 +4440,8 @@
  // callback from server upon group-delete command
  this.remove_group_item = function(prop)
  {
    var li, key = 'G'+prop.source+prop.id;
    if ((li = this.get_folder_li(key))) {
      this.triggerEvent('group_delete', { source:prop.source, id:prop.id, li:li });
      li.parentNode.removeChild(li);
    var key = 'G'+prop.source+prop.id;
    if (this.treelist.remove(key)) {
      delete this.env.contactfolders[key];
      delete this.env.contactgroups[key];
    }
@@ -4488,8 +4460,11 @@
      this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
      this.name_input_li = $('<li>').addClass(type).append(this.name_input);
      var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : this.get_folder_li(this.env.source);
      this.name_input_li.insertAfter(li);
      var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : $('ul.groups li:last', this.get_folder_li(this.env.source,'',true));
      if (li.length)
        this.name_input_li.insertAfter(li);
      else
        this.name_input_li.appendTo(type == 'contactsearch' ? this.gui_objects.folderlist : $('ul.groups', this.get_folder_li(this.env.source,'',true)));
    }
    this.name_input.select().focus();
@@ -4573,14 +4548,12 @@
      link = $('<a>').attr('href', '#')
        .attr('rel', prop.source+':'+prop.id)
        .click(function() { return rcmail.command('listgroup', prop, this); })
        .html(prop.name),
      li = $('<li>').attr({id: 'rcmli'+this.html_identifier(key), 'class': 'contactgroup'})
        .append(link);
        .html(prop.name);
    this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
    this.add_contact_group_row(prop, li);
    this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, true);
    this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:li[0] });
    this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key) });
  };
  // callback for renaming a contact group
@@ -4589,15 +4562,13 @@
    this.reset_add_input();
    var key = 'G'+prop.source+prop.id,
      li = this.get_folder_li(key),
      link;
      newnode = {};
    // group ID has changed, replace link node and identifiers
    if (li && prop.newid) {
    if (prop.newid) {
      var newkey = 'G'+prop.source+prop.newid,
        newprop = $.extend({}, prop);;
        newprop = $.extend({}, prop);
      li.id = 'rcmli' + this.html_identifier(newkey);
      this.env.contactfolders[newkey] = this.env.contactfolders[key];
      this.env.contactfolders[newkey].id = prop.newid;
      this.env.group = prop.newid;
@@ -4608,45 +4579,22 @@
      newprop.id = prop.newid;
      newprop.type = 'group';
      link = $('<a>').attr('href', '#')
      newnode.id = newkey;
      newnode.html = $('<a>').attr('href', '#')
        .attr('rel', prop.source+':'+prop.newid)
        .click(function() { return rcmail.command('listgroup', newprop, this); })
        .html(prop.name);
      $(li).children().replaceWith(link);
    }
    // update displayed group name
    else if (li && (link = li.firstChild) && link.tagName.toLowerCase() == 'a')
      link.innerHTML = prop.name;
    this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
    this.add_contact_group_row(prop, $(li), true);
    this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:li[0], newid:prop.newid });
  };
  // add contact group row to the list, with sorting
  this.add_contact_group_row = function(prop, li, reloc)
  {
    var row, name = prop.name.toUpperCase(),
      sibling = this.get_folder_li(prop.source),
      prefix = 'rcmliG' + this.html_identifier(prop.source);
    // When renaming groups, we need to remove it from DOM and insert it in the proper place
    if (reloc) {
      row = li.clone(true);
      li.remove();
    else {
      $(this.treelist.get_item(key)).children().first().html(prop.name);
      this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
    }
    else
      row = li;
    $('li[id^="'+prefix+'"]', this.gui_objects.folderlist).each(function(i, elem) {
      if (name >= $(this).text().toUpperCase())
        sibling = elem;
      else
        return false;
    });
    // update list node and re-sort it
    this.treelist.update(key, newnode, true);
    row.insertAfter(sibling);
    this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key), newid:prop.newid });
  };
  this.update_group_commands = function()
@@ -4878,45 +4826,14 @@
        .attr('rel', id)
        .click(function() { return rcmail.command('listsearch', id, this); })
        .html(name),
      li = $('<li>').attr({id: 'rcmli' + this.html_identifier(key), 'class': 'contactsearch'})
        .append(link),
      prop = {name:name, id:id, li:li[0]};
      prop = { name:name, id:id };
    this.add_saved_search_row(prop, li);
    this.select_folder('S'+id);
    this.treelist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch');
    this.select_folder(key,'',true);
    this.enable_command('search-delete', true);
    this.env.search_id = id;
    this.triggerEvent('abook_search_insert', prop);
  };
  // add saved search row to the list, with sorting
  this.add_saved_search_row = function(prop, li, reloc)
  {
    var row, sibling, name = prop.name.toUpperCase();
    // When renaming groups, we need to remove it from DOM and insert it in the proper place
    if (reloc) {
      row = li.clone(true);
      li.remove();
    }
    else
      row = li;
    $('li[class~="contactsearch"]', this.gui_objects.folderlist).each(function(i, elem) {
      if (!sibling)
        sibling = this.previousSibling;
      if (name >= $(this).text().toUpperCase())
        sibling = elem;
      else
        return false;
    });
    if (sibling)
      row.insertAfter(sibling);
    else
      row.appendTo(this.gui_objects.folderlist);
  };
  // creates an input for saved search name
@@ -4937,10 +4854,8 @@
  this.remove_search_item = function(id)
  {
    var li, key = 'S'+id;
    if ((li = this.get_folder_li(key))) {
    if (this.treelist.remove(key)) {
      this.triggerEvent('search_delete', { id:id, li:li });
      li.parentNode.removeChild(li);
    }
    this.env.search_id = null;
@@ -4959,7 +4874,7 @@
    }
    this.reset_qsearch();
    this.select_folder('S'+id);
    this.select_folder('S'+id, '', true);
    // reset vars
    this.env.current_page = 1;
@@ -5674,14 +5589,15 @@
    if (!this.gui_objects.message)
      return;
    var k, n, i, msg, m = this.messages;
    var k, n, i, o, m = this.messages;
    // Hide message by object, don't use for 'loading'!
    if (typeof obj === 'object') {
      $(obj)[fade?'fadeOut':'hide']();
      msg = $(obj).data('key');
      if (this.messages[msg])
        delete this.messages[msg];
      o = $(obj);
      k = o.data('key');
      this.hide_message_object(o, fade);
      if (m[k])
        delete m[k];
    }
    // Hide message by id
    else {
@@ -5691,7 +5607,7 @@
            m[k].elements.splice(n, 1);
            // hide DOM element if last instance is removed
            if (!m[k].elements.length) {
              m[k].obj[fade?'fadeOut':'hide']();
              this.hide_message_object(m[k].obj, fade);
              delete m[k];
            }
            // set pending action label for 'loading' message
@@ -5701,15 +5617,24 @@
                  delete m[k].labels[i];
                }
                else {
                  msg = m[k].labels[i].msg;
                  o = m[k].labels[i].msg;
                  m[k].obj.html(o);
                }
                m[k].obj.html(msg);
              }
            }
          }
        }
      }
    }
  };
  // hide message object and remove from the DOM
  this.hide_message_object = function(o, fade)
  {
    if (fade)
      o.fadeOut(600, function() {$(this).remove(); });
    else
      o.hide().remove();
  };
  // remove all messages immediately
@@ -5724,7 +5649,7 @@
    for (k in m)
      for (n in m[k].elements)
        if (m[k].obj)
          m[k].obj.hide();
          this.hide_message_object(m[k].obj);
    this.messages = {};
  };
@@ -5765,7 +5690,10 @@
  // mark a mailbox as selected and set environment variable
  this.select_folder = function(name, prefix, encode)
  {
    if (this.gui_objects.folderlist) {
    if (this.treelist) {
      this.treelist.select(name);
    }
    else if (this.gui_objects.folderlist) {
      var current_li, target_li;
      if ((current_li = $('li.selected', this.gui_objects.folderlist))) {
@@ -6053,9 +5981,9 @@
    var base = this.env.comm_path, k, param = {};
    // overwrite task name
    if (query._action.match(/([a-z]+)\/([a-z0-9-_.]+)/)) {
    if (query._action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) {
      query._action = RegExp.$2;
      base = base.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
      base = base.replace(/\_task=[a-z0-9_-]+/, '_task='+RegExp.$1);
    }
    // remove undefined values
@@ -6238,6 +6166,7 @@
          this.enable_command('compose', (uid && this.contact_list.rows[uid]));
          this.enable_command('delete', 'edit', writable);
          this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
          this.enable_command('export-selected', false);
        }
      case 'moveto':
@@ -6328,12 +6257,29 @@
    // redirect to url specified in location header if not empty
    var location_url = request.getResponseHeader("Location");
    if (location_url)
    if (location_url && this.env.action != 'compose')  // don't redirect on compose screen, contents might get lost (#1488926)
      this.redirect(location_url);
    // 403 Forbidden response (CSRF prevention) - reload the page.
    // In case there's a new valid session it will be used, otherwise
    // login form will be presented (#1488960).
    if (request.status == 403) {
      (this.is_framed() ? parent : window).location.reload();
      return;
    }
    // re-send keep-alive requests after 30 seconds
    if (action == 'keep-alive')
      setTimeout(function(){ ref.keep_alive(); ref.start_keepalive(); }, 30000);
  };
  // callback when an iframe finished loading
  this.iframe_loaded = function(unlock)
  {
    this.set_busy(false, null, unlock);
    if (this.submit_timer)
      clearTimeout(this.submit_timer);
  };
  // post the given form to a hidden iframe
@@ -6574,6 +6520,17 @@
  /*********            helper methods            *********/
  /********************************************************/
  // get window.opener.rcmail if available
  this.opener = function()
  {
    // catch Error: Permission denied to access property rcmail
    try {
      if (window.opener && !opener.closed && opener.rcmail)
        return opener.rcmail;
    }
    catch (e) {}
  };
  // check if we're in show mode or if we have a unique selection
  // and return the message uid
  this.get_single_uid = function()