alecpl
2011-07-01 02e079f9170539a639a9f42248829faeb7c17ae3
program/js/app.js
@@ -70,9 +70,12 @@
  };
  // add a localized label to the client environment
  this.add_label = function(key, value)
  this.add_label = function(p, value)
  {
    this.labels[key] = value;
    if (typeof p == 'string')
      this.labels[p] = value;
    else if (typeof p == 'object')
      $.extend(this.labels, p);
  };
  // add a button to the button list
@@ -161,7 +164,7 @@
    }
    // enable general commands
    this.enable_command('logout', 'mail', 'addressbook', 'settings', true);
    this.enable_command('logout', 'mail', 'addressbook', 'settings', 'save-pref', true);
    if (this.env.permaurl)
      this.enable_command('permaurl', true);
@@ -277,7 +280,6 @@
        break;
      case 'addressbook':
        if (this.gui_objects.folderlist)
          this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups);
@@ -302,14 +304,11 @@
          if (this.gui_objects.qsearchbox) {
            $(this.gui_objects.qsearchbox).focusin(function() { rcmail.contact_list.blur(); });
          }
          this.enable_command('group-create', this.env.address_sources[this.env.source].groups);
        }
        this.set_page_buttons();
        if (this.env.address_sources && this.env.address_sources[this.env.source] && !this.env.address_sources[this.env.source].readonly) {
          this.enable_command('add', 'import', true);
          this.enable_command('group-create', this.env.address_sources[this.env.source].groups);
        }
        if (this.env.cid) {
          this.enable_command('show', 'edit', true);
@@ -324,11 +323,12 @@
          }
        }
        if ((this.env.action == 'add' || this.env.action == 'edit') && this.gui_objects.editform) {
        if (this.gui_objects.editform) {
          this.enable_command('save', true);
          this.init_contact_form();
          if (this.env.action == 'add' || this.env.action == 'edit')
              this.init_contact_form();
        }
        else if (this.gui_objects.qsearchbox) {
        if (this.gui_objects.qsearchbox) {
          this.enable_command('search', 'reset-search', 'moveto', true);
          $(this.gui_objects.qsearchbox).select();
        }
@@ -336,7 +336,8 @@
        if (this.contact_list && this.contact_list.rowcount > 0)
          this.enable_command('export', true);
        this.enable_command('list', 'listgroup', true);
        this.enable_command('add', 'import', this.env.writable_source);
        this.enable_command('list', 'listgroup', 'advanced-search', true);
        break;
@@ -528,12 +529,12 @@
          if (this.env.trash_mailbox)
            this.set_alttext('delete', this.env.mailbox != this.env.trash_mailbox ? 'movemessagetotrash' : 'deletemessage');
        }
        else if (this.task=='addressbook') {
        else if (this.task == 'addressbook') {
          if (!this.env.search_request || (props != this.env.source))
            this.reset_qsearch();
          this.list_contacts(props);
          this.enable_command('add', 'import', (this.env.address_sources && !this.env.address_sources[this.env.source].readonly));
          this.enable_command('add', 'import', this.env.writable_source);
        }
        break;
@@ -585,7 +586,7 @@
      // common commands used in multiple tasks
      case 'show':
        if (this.task=='mail') {
        if (this.task == 'mail') {
          var uid = this.get_single_uid();
          if (uid && (!this.env.uid || uid != this.env.uid)) {
            if (this.env.mailbox == this.env.drafts_mailbox)
@@ -594,17 +595,17 @@
              this.show_message(uid);
          }
        }
        else if (this.task=='addressbook') {
        else if (this.task == 'addressbook') {
          var cid = props ? props : this.get_single_cid();
          if (cid && !(this.env.action=='show' && cid==this.env.cid))
          if (cid && !(this.env.action == 'show' && cid == this.env.cid))
            this.load_contact(cid, 'show');
        }
        break;
      case 'add':
        if (this.task=='addressbook')
        if (this.task == 'addressbook')
          this.load_contact(0, 'add');
        else if (this.task=='settings') {
        else if (this.task == 'settings') {
          this.identity_list.clear_selection();
          this.load_identity(0, 'add-identity');
        }
@@ -623,27 +624,29 @@
        break;
      case 'save':
        if (this.gui_objects.editform) {
          var input_pagesize = $("input[name='_pagesize']");
          var input_name  = $("input[name='_name']");
          var input_email = $("input[name='_email']");
        var input, form = this.gui_objects.editform;
        if (form) {
          // adv. search
          if (this.env.action == 'search') {
          }
          // user prefs
          if (input_pagesize.length && isNaN(parseInt(input_pagesize.val()))) {
          else if ((input = $("input[name='_pagesize']", form)) && input.length && isNaN(parseInt(input.val()))) {
            alert(this.get_label('nopagesizewarning'));
            input_pagesize.focus();
            input.focus();
            break;
          }
          // contacts/identities
          else {
            if (input_name.length && input_name.val() == '') {
            if ((input = $("input[name='_name']", form)) &&input.length && input.val() == '') {
              alert(this.get_label('nonamewarning'));
              input_name.focus();
              input.focus();
              break;
            }
            else if (this.task == 'settings' && input_email.length && (this.env.identities_level % 2) == 0 && !rcube_check_email(input_email.val())) {
            else if (this.task == 'settings' && (this.env.identities_level % 2) == 0  &&
              (input = $("input[name='_email']", form)) && input.length&& !rcube_check_email(input.val())
            ) {
              alert(this.get_label('noemailwarning'));
              input_email.focus();
              input.focus();
              break;
            }
@@ -651,7 +654,7 @@
            $('input.placeholder').each(function(){ if (this.value == this._placeholder) this.value = ''; });
          }
          this.gui_objects.editform.submit();
          form.submit();
        }
        break;
@@ -890,12 +893,14 @@
        self.clearTimeout(this.save_timer);
        // all checks passed, send message
        var form = this.gui_objects.messageform,
        var lang = this.spellcheck_lang(),
          form = this.gui_objects.messageform,
          msgid = this.set_busy(true, 'sendingmessage');
        form.target = 'savetarget';
        form._draft.value = '';
        form.action = this.add_url(form.action, '_unlock', msgid);
        form.action = this.add_url(form.action, '_lang', lang);
        form.submit();
        // clear timeout (sending could take longer)
@@ -931,10 +936,13 @@
      case 'forward-attachment':
      case 'forward':
        var uid;
        if (uid = this.get_single_uid())
          this.goto_url('compose', '_forward_uid='+uid+'&_mbox='+urlencode(this.env.mailbox)
            + (command == 'forward-attachment' ? '&_attachment=1' : ''), true);
        var uid, url;
        if (uid = this.get_single_uid()) {
          url = '_forward_uid='+uid+'&_mbox='+urlencode(this.env.mailbox);
          if (command == 'forward-attachment' || (!props && this.env.forward_attachment))
            url += '&_attachment=1';
          this.goto_url('compose', url, true);
        }
        break;
      case 'print':
@@ -980,8 +988,14 @@
        if (s && this.env.mailbox)
          this.list_mailbox(this.env.mailbox);
        else if (s && this.task == 'addressbook')
        else if (s && this.task == 'addressbook') {
          if (this.env.source == '') {
            for (var n in this.env.address_sources) break;
            this.env.source = n;
            this.env.group = '';
          }
          this.list_contacts(this.env.source, this.env.group);
        }
        break;
      case 'listgroup':
@@ -1166,6 +1180,18 @@
  this.is_framed = function()
  {
    return (this.env.framed && parent.rcmail && parent.rcmail != this && parent.rcmail.command);
  };
  this.save_pref = function(prop)
  {
    var request = {'_name': prop.name, '_value': prop.value};
    if (prop.session)
      request['_session'] = prop.session;
    if (prop.env)
      this.env[prop.env] = prop.value;
    this.http_post('save-pref', request);
  };
@@ -1360,7 +1386,7 @@
      }
    }
    this.http_post('save-pref', '_name=collapsed_folders&_value='+urlencode(this.env.collapsed_folders));
    this.command('save-pref', { name: 'collapsed_folders', value: this.env.collapsed_folders });
    this.set_unread_count_display(id, false);
  };
@@ -1534,7 +1560,7 @@
    if ((found = $.inArray('subject', this.env.coltypes)) >= 0)
      this.set_env('subject_col', found);
    this.http_post('save-pref', { '_name':'list_cols', '_value':this.env.coltypes, '_session':'list_attrib/columns' });
    this.command('save-pref', { name: 'list_cols', value: this.env.coltypes, session: 'list_attrib/columns' });
  };
  this.check_droptarget = function(id)
@@ -2586,12 +2612,13 @@
  // set class to read/unread
  this.toggle_read_status = function(flag, a_uids)
  {
    // mark all message rows as read/unread
    for (var i=0; i<a_uids.length; i++)
      this.set_message(a_uids[i], 'unread', (flag=='unread' ? true : false));
    var url = '_uid='+this.uids_to_list(a_uids)+'&_flag='+flag,
    var i, len = a_uids.length,
      url = '_uid='+this.uids_to_list(a_uids)+'&_flag='+flag,
      lock = this.display_message(this.get_label('markingmessage'), 'loading');
    // mark all message rows as read/unread
    for (i=0; i<len; i++)
      this.set_message(a_uids[i], 'unread', (flag=='unread' ? true : false));
    // also send search request to get the right messages
    if (this.env.search_request)
@@ -2599,19 +2626,20 @@
    this.http_post('mark', url, lock);
    for (var i=0; i<a_uids.length; i++)
    for (i=0; i<len; i++)
      this.update_thread_root(a_uids[i], flag);
  };
  // set image to flagged or unflagged
  this.toggle_flagged_status = function(flag, a_uids)
  {
    // mark all message rows as flagged/unflagged
    for (var i=0; i<a_uids.length; i++)
      this.set_message(a_uids[i], 'flagged', (flag=='flagged' ? true : false));
    var url = '_uid='+this.uids_to_list(a_uids)+'&_flag='+flag,
    var i, len = a_uids.length,
      url = '_uid='+this.uids_to_list(a_uids)+'&_flag='+flag,
      lock = this.display_message(this.get_label('markingmessage'), 'loading');
    // mark all message rows as flagged/unflagged
    for (i=0; i<len; i++)
      this.set_message(a_uids[i], 'flagged', (flag=='flagged' ? true : false));
    // also send search request to get the right messages
    if (this.env.search_request)
@@ -2623,9 +2651,11 @@
  // mark all message rows as deleted/undeleted
  this.toggle_delete_status = function(a_uids)
  {
    var rows = this.message_list ? this.message_list.rows : [];
    var len = a_uids.length,
      i, uid, all_deleted = true,
      rows = this.message_list ? this.message_list.rows : [];
    if (a_uids.length==1) {
    if (len == 1) {
      if (!rows.length || (rows[a_uids[0]] && !rows[a_uids[0]].deleted))
        this.flag_as_deleted(a_uids);
      else
@@ -2634,8 +2664,7 @@
      return true;
    }
    var uid, all_deleted = true;
    for (var i=0, len=a_uids.length; i<len; i++) {
    for (i=0; i<len; i++) {
      uid = a_uids[i];
      if (rows[uid] && !rows[uid].deleted) {
        all_deleted = false;
@@ -2653,11 +2682,12 @@
  this.flag_as_undeleted = function(a_uids)
  {
    for (var i=0, len=a_uids.length; i<len; i++)
      this.set_message(a_uids[i], 'deleted', false);
    var url = '_uid='+this.uids_to_list(a_uids)+'&_flag=undelete',
    var i, len=a_uids.length,
      url = '_uid='+this.uids_to_list(a_uids)+'&_flag=undelete',
      lock = this.display_message(this.get_label('markingmessage'), 'loading');
    for (i=0; i<len; i++)
      this.set_message(a_uids[i], 'deleted', false);
    // also send search request to get the right messages
    if (this.env.search_request)
@@ -2724,13 +2754,13 @@
  // argument should be a coma-separated list of uids
  this.flag_deleted_as_read = function(uids)
  {
    var icn_src, uid,
      rows = this.message_list ? this.message_list.rows : [],
      str = String(uids),
      a_uids = str.split(',');
    var icn_src, uid, i, len,
      rows = this.message_list ? this.message_list.rows : [];
    for (var i=0; i<a_uids.length; i++) {
      uid = a_uids[i];
    uids = String(uids).split(',');
    for (i=0, len=uids.length; i<len; i++) {
      uid = uids[i];
      if (rows[uid])
        this.set_message(uid, 'unread', false);
    }
@@ -2837,7 +2867,7 @@
      this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length);
      // add signature according to selected identity
      // if we have HTML editor, signature is added in callback
      if (input_from.attr('type') == 'select-one' && $("input[name='_draft_saveid']").val() == '') {
      if (input_from.prop('type') == 'select-one' && $("input[name='_draft_saveid']").val() == '') {
        this.change_identity(input_from[0]);
      }
    }
@@ -2876,7 +2906,7 @@
      input_message = $("[name='_message']");
    // check sender (if have no identities)
    if (input_from.attr('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
    if (input_from.prop('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
      alert(this.get_label('nosenderwarning'));
      input_from.focus();
      return false;
@@ -2988,6 +3018,36 @@
    this.spellcheck_ready = (s == 'ready' || s == 'no_error_found');
    this.enable_command('spellcheck', this.spellcheck_ready);
  };
  // get selected language
  this.spellcheck_lang = function()
  {
    var ed;
    if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)) && ed.plugins.spellchecker) {
      return ed.plugins.spellchecker.selectedLang;
    }
    else if (this.env.spellcheck) {
      return GOOGIE_CUR_LANG;
    }
  };
  // resume spellchecking, highlight provided mispellings without new ajax request
  this.spellcheck_resume = function(ishtml, data)
  {
    if (ishtml) {
      var ed = tinyMCE.get(this.env.composebody);
        sp = ed.plugins.spellchecker;
      sp.active = 1;
      sp._markWords(data);
      ed.nodeChanged();
    }
    else {
      var sp = this.env.spellcheck;
      sp.prepare(false, true);
      sp.processData(data);
    }
  }
  this.set_draft_id = function(id)
  {
@@ -3285,21 +3345,23 @@
  this.qsearch = function(value)
  {
    if (value != '') {
      var addurl = '';
      var n, addurl = '', mods_arr = [],
        mods = this.env.search_mods,
        mbox = this.env.mailbox,
        lock = this.set_busy(true, 'searching');
      if (this.message_list) {
        this.clear_message_list();
        if (this.env.search_mods) {
          var mods = this.env.search_mods[this.env.mailbox] ? this.env.search_mods[this.env.mailbox] : this.env.search_mods['*'];
          if (mods) {
            var head_arr = [];
            for (var n in mods)
              head_arr.push(n);
            addurl += '&_headers='+head_arr.join(',');
          }
        }
        if (mods)
          mods = mods[mbox] ? mods[mbox] : mods['*'];
      } else if (this.contact_list) {
        this.contact_list.clear(true);
        this.show_contentframe(false);
        this.list_contacts_clear();
      }
      if (mods) {
        for (n in mods)
          mods_arr.push(n);
        addurl += '&_headers='+mods_arr.join(',');
      }
      if (this.gui_objects.search_filter)
@@ -3307,9 +3369,8 @@
      // reset vars
      this.env.current_page = 1;
      var lock = this.set_busy(true, 'searching');
      this.http_request('search', '_q='+urlencode(value)
        + (this.env.mailbox ? '&_mbox='+urlencode(this.env.mailbox) : '')
        + (mbox ? '&_mbox='+urlencode(mbox) : '')
        + (this.env.source ? '&_source='+urlencode(this.env.source) : '')
        + (this.env.group ? '&_gid='+urlencode(this.env.group) : '')
        + (addurl ? addurl : ''), lock);
@@ -3596,15 +3657,34 @@
    if (this.preview_timer)
      clearTimeout(this.preview_timer);
    var id, frame, ref = this;
    var n, id, sid, ref = this, writable = false,
      source = this.env.source ? this.env.address_sources[this.env.source] : null;
    if (id = list.get_single_selection())
      this.preview_timer = window.setTimeout(function(){ ref.load_contact(id, 'show'); }, 200);
    else if (this.env.contentframe)
      this.show_contentframe(false);
    // no source = search result, we'll need to detect if any of
    // selected contacts are in writable addressbook to enable edit/delete
    if (list.selection.length) {
      if (!source) {
        for (n in list.selection) {
          sid = String(list.selection[n]).replace(/^[^-]+-/, '');
          if (sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly) {
            writable = true;
            break;
          }
        }
      }
      else {
        writable = !source.readonly;
      }
    }
    this.enable_command('compose', list.selection.length > 0);
    this.enable_command('edit', (id && this.env.address_sources && !this.env.address_sources[this.env.source].readonly) ? true : false);
    this.enable_command('delete', list.selection.length && this.env.address_sources && !this.env.address_sources[this.env.source].readonly);
    this.enable_command('edit', id && writable);
    this.enable_command('delete', list.selection.length && writable);
    return false;
  };
@@ -3660,9 +3740,7 @@
  this.list_contacts_remote = function(src, group, page)
  {
    // clear message list first
    this.contact_list.clear(true);
    this.show_contentframe(false);
    this.enable_command('delete', 'compose', false);
    this.list_contacts_clear();
    // send request to server
    var url = (src ? '_source='+urlencode(src) : '') + (page ? (src?'&':'') + '_page='+page : ''),
@@ -3679,6 +3757,13 @@
      url += '&_search='+this.env.search_request;
    this.http_request('list', url, lock);
  };
  this.list_contacts_clear = function()
  {
    this.contact_list.clear(true);
    this.show_contentframe(false);
    this.enable_command('delete', 'compose', false);
  };
  // load contact record
@@ -3737,12 +3822,12 @@
    if (!(selection.length || this.env.cid) || !confirm(this.get_label('deletecontactconfirm')))
      return;
    var id, a_cids = [], qs = '';
    var id, n, a_cids = [], qs = '';
    if (this.env.cid)
      a_cids.push(this.env.cid);
    else {
      for (var n=0; n<selection.length; n++) {
      for (n=0; n<selection.length; n++) {
        id = selection[n];
        a_cids.push(id);
        this.contact_list.remove_row(id, (n == selection.length-1));
@@ -3757,7 +3842,7 @@
      qs += '&_gid='+urlencode(this.env.group);
    // also send search request to get the right records from the next page
    if (this.env.search_request)
    if (this.env.search_request)
      qs += '&_search='+this.env.search_request;
    // send request to server
@@ -4001,7 +4086,6 @@
    this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:li[0], newid:prop.newid });
  };
  this.init_edit_field = function(col, elem)
  {
    if (!elem)
@@ -4023,7 +4107,7 @@
    else {
      var lastelem = $('.ff_'+col),
        appendcontainer = $('#contactsection'+section+' .contactcontroller'+col);
      if (!appendcontainer.length)
        appendcontainer = $('<fieldset>').addClass('contactfieldgroup contactcontroller'+col).insertAfter($('#contactsection'+section+' .contactfieldgroup').last());
@@ -4032,7 +4116,7 @@
          row = $('<div>').addClass('row'),
          cell = $('<div>').addClass('contactfieldcontent data'),
          label = $('<div>').addClass('contactfieldlabel label');
        if (colprop.subtypes_select)
          label.html(colprop.subtypes_select);
        else
@@ -4048,14 +4132,27 @@
          this.init_edit_field(col, input);
        }
        else if (colprop.type == 'composite') {
          var childcol, cp, first;
          for (childcol in colprop.childs) {
          var childcol, cp, first, templ, cols = [], suffices = [];
          // read template for composite field order
          if ((templ = this.env[col+'_template'])) {
            for (var j=0; j < templ.length; j++) {
              cols.push(templ[j][1]);
              suffices.push(templ[j][2]);
            }
          }
          else {  // list fields according to appearance in colprop
            for (childcol in colprop.childs)
              cols.push(childcol);
          }
          for (var i=0; i < cols.length; i++) {
            childcol = cols[i];
            cp = colprop.childs[childcol];
            input = $('<input>')
              .addClass('ff_'+childcol)
              .attr({type: 'text', name: '_'+childcol+name_suffix, size: cp.size})
              .attr({ type: 'text', name: '_'+childcol+name_suffix, size: cp.size })
              .appendTo(cell);
            cell.append(" ");
            cell.append(suffices[i] || " ");
            this.init_edit_field(childcol, input);
            if (!first) first = input;
          }
@@ -4066,7 +4163,7 @@
            .addClass('ff_'+col)
            .attr('name', '_'+col+name_suffix)
            .appendTo(cell);
          var options = input.attr('options');
          options[options.length] = new Option('---', '');
          if (colprop.options)
@@ -4080,10 +4177,10 @@
            .html(this.env.delbutton)
            .click(function(){ ref.delete_edit_field(this); return false })
            .appendTo(cell);
          row.append(label).append(cell).appendTo(appendcontainer.show());
          input.first().focus();
          // disable option if limit reached
          if (!colprop.count) colprop.count = 0;
          if (++colprop.count == colprop.limit && colprop.limit)
@@ -4099,7 +4196,7 @@
      colprop = this.env.coltypes[col],
      fieldset = $(elem).parents('fieldset.contactfieldgroup'),
      addmenu = fieldset.parent().find('select.addfieldmenu');
    // just clear input but don't hide the last field
    if (--colprop.count <= 0 && colprop.visible)
      $(elem).parent().children('input').val('').blur();
@@ -4109,7 +4206,7 @@
      if (!fieldset.children('div.row').length)
        fieldset.hide();
    }
    // enable option in add-field selector or insert it if necessary
    if (addmenu.length) {
      var option = addmenu.children('option[value="'+col+'"]');
@@ -4159,25 +4256,36 @@
    this.enable_command('delete-photo', this.env.coltypes.photo && id != '-del-');
  };
  // load advanced search page
  this.advanced_search = function()
  {
    var add_url = '&_form=1', target = window;
    if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
      add_url += '&_framed=1';
      target = window.frames[this.env.contentframe];
      this.contact_list.clear_selection();
    }
    this.location_href(this.env.comm_path+'&_action=search'+add_url, target);
    return true;
  };
  // unselect directory/group
  this.unselect_directory = function()
  {
    if (this.env.address_sources.length > 1 || this.env.group != '') {
      this.select_folder('', (this.env.group ? 'G'+this.env.source+this.env.group : this.env.source));
      this.env.group = '';
      this.env.source = '';
    }
  };
  /*********************************************************/
  /*********        user settings methods          *********/
  /*********************************************************/
  this.init_subscription_list = function()
  {
    var p = this;
    this.subscription_list = new rcube_list_widget(this.gui_objects.subscriptionlist,
      {multiselect:false, draggable:true, keyboard:false, toggleselect:true});
    this.subscription_list.addEventListener('select', function(o){ p.subscription_select(o); });
    this.subscription_list.addEventListener('dragstart', function(o){ p.drag_active = true; });
    this.subscription_list.addEventListener('dragend', function(o){ p.subscription_move_folder(o); });
    this.subscription_list.row_init = function (row) {
      row.obj.onmouseover = function() { p.focus_subscription(row.id); };
      row.obj.onmouseout = function() { p.unfocus_subscription(row.id); };
    };
    this.subscription_list.init();
  };
  // preferences section select and load options frame
  this.section_select = function(list)
@@ -4241,6 +4349,26 @@
    this.goto_url('delete-identity', '_iid='+id+'&_token='+this.env.request_token, true);
    return true;
  };
  /*********************************************************/
  /*********        folder manager methods         *********/
  /*********************************************************/
  this.init_subscription_list = function()
  {
    var p = this;
    this.subscription_list = new rcube_list_widget(this.gui_objects.subscriptionlist,
      {multiselect:false, draggable:true, keyboard:false, toggleselect:true});
    this.subscription_list.addEventListener('select', function(o){ p.subscription_select(o); });
    this.subscription_list.addEventListener('dragstart', function(o){ p.drag_active = true; });
    this.subscription_list.addEventListener('dragend', function(o){ p.subscription_move_folder(o); });
    this.subscription_list.row_init = function (row) {
      row.obj.onmouseover = function() { p.focus_subscription(row.id); };
      row.obj.onmouseout = function() { p.unfocus_subscription(row.id); };
    };
    this.subscription_list.init();
  };
  this.focus_subscription = function(id)
@@ -4332,90 +4460,185 @@
    }
  };
  // add a new folder to the subscription list by cloning a folder row
  this.add_folder_row = function(name, display_name, replace, before)
  // Add folder row to the table and initialize it
  this.add_folder_row = function (name, display_name, protected, subscribed, skip_init, class_name)
  {
    if (!this.gui_objects.subscriptionlist)
      return false;
    // find not protected folder
    var refid;
    for (var rid in this.env.subscriptionrows) {
      if (this.env.subscriptionrows[rid]!=null && !this.env.subscriptionrows[rid][2]) {
        refid = rid;
        break;
      }
    }
    var refrow, form,
    var row, n, i, tmp, folders, len, list = [], slist = [],
      tbody = this.gui_objects.subscriptionlist.tBodies[0],
      id = 'rcmrow'+(tbody.childNodes.length+1),
      selection = this.subscription_list.get_single_selection();
      refrow = $('tr', tbody).get(0),
      id = 'rcmrow'+((new Date).getTime());
    if (replace && replace.id) {
      id = replace.id;
      refid = replace.id;
    }
    if (!id || !refid || !(refrow = document.getElementById(refid))) {
    if (!refrow) {
      // Refresh page if we don't have a table row to clone
      this.goto_url('folders');
      return false;
    }
    // clone a table row if there are existing rows
    var row = this.clone_table_row(refrow);
    row.id = id;
    row = $(refrow).clone(true);
    if (before && (before = this.get_folder_row_id(before)))
      tbody.insertBefore(row, document.getElementById(before));
    else
      tbody.appendChild(row);
    if (replace)
      tbody.removeChild(replace);
    // add to folder/row-ID map
    this.env.subscriptionrows[row.id] = [name, display_name, 0];
    // set ID, reset css class
    row.attr('id', id);
    row.attr('class', class_name);
    // set folder name
    row.cells[0].innerHTML = display_name;
    row.find('td:first').html(display_name);
    if (!replace) {
      // set messages count to zero
      row.cells[1].innerHTML = '*';
    // update subscription checkbox
    $('input[name="_subscribed[]"]', row).val(name)
      .prop({checked: subscribed ? true : false, disabled: protected ? true : false});
      // update subscription checkbox
      $('input[name="_subscribed[]"]', row).val(name).prop('checked', true);
    // add to folder/row-ID map
    this.env.subscriptionrows[id] = [name, display_name, 0];
    // sort folders, to find a place where to insert the row
    folders = this.env.subscriptionrows;
    for (n in folders) {
      // protected folder
      if (folders[n][2]) {
        slist.push(folders[n][0]);
        tmp = folders[n][0]+this.env.delimiter;
      }
      // protected folder's child
      else if (tmp && folders[n][0].indexOf(tmp) == 0)
        slist.push(folders[n][0]);
      // other
      else {
        list.push(folders[n][0]);
        tmp = null;
      }
    }
    list.sort();
    // make sure protected folders (and their subs) are on top
    list = slist.concat(list);
    // find folder position after sorting
    for (n=0, len=list.length; n<len; n++) {
      if (list[n] == name)
        break;
    }
    this.init_subscription_list();
    if (selection && document.getElementById('rcmrow'+selection))
      this.subscription_list.select_row(selection);
    // add row to the table
    if (n && n < len)
      $('#'+this.get_folder_row_id(list[n-1])).after(row);
    else
      row.appendTo(tbody);
    if (document.getElementById(id).scrollIntoView)
      document.getElementById(id).scrollIntoView();
    // update list widget
    this.subscription_list.clear_selection();
    if (!skip_init)
      this.init_subscription_list();
    row = row.get(0);
    if (row.scrollIntoView)
      row.scrollIntoView();
    return row;
  };
  // replace an existing table row with a new folder line
  this.replace_folder_row = function(oldfolder, newfolder, display_name, before)
  // replace an existing table row with a new folder line (with subfolders)
  this.replace_folder_row = function(oldfolder, newfolder, display_name, protected, class_name)
  {
    var id = this.get_folder_row_id(oldfolder),
      row = document.getElementById(id);
    if (!this.gui_objects.subscriptionlist)
      return false;
    // replace an existing table row (if found)
    this.add_folder_row(newfolder, display_name, row, before);
    var i, n, len, name, dispname, oldrow, tmprow, row, level,
      tbody = this.gui_objects.subscriptionlist.tBodies[0],
      folders = this.env.subscriptionrows,
      id = this.get_folder_row_id(oldfolder),
      regex = new RegExp('^'+RegExp.escape(oldfolder)),
      subscribed = $('input[name="_subscribed[]"]', $('#'+id)).prop('checked'),
      // find subfolders of renamed folder
      list = this.get_subfolders(oldfolder);
    // replace an existing table row
    this._remove_folder_row(id);
    row = $(this.add_folder_row(newfolder, display_name, protected, subscribed, true, class_name));
    // detect tree depth change
    if (len = list.length) {
      level = (oldfolder.split(this.env.delimiter)).length - (newfolder.split(this.env.delimiter)).length;
    }
    // move subfolders to the new branch
    for (n=0; n<len; n++) {
      id = list[n];
      name = this.env.subscriptionrows[id][0];
      dispname = this.env.subscriptionrows[id][1];
      oldrow = $('#'+id);
      tmprow = oldrow.clone(true);
      oldrow.remove();
      row.after(tmprow);
      row = tmprow;
      // update folder index
      name = name.replace(regex, newfolder);
      $('input[name="_subscribed[]"]', row).val(name);
      this.env.subscriptionrows[id][0] = name;
      // update the name if level is changed
      if (level != 0) {
        if (level > 0) {
          for (i=level; i>0; i--)
            dispname = dispname.replace(/^&nbsp;&nbsp;&nbsp;&nbsp;/, '');
        }
        else {
          for (i=level; i<0; i++)
            dispname = '&nbsp;&nbsp;&nbsp;&nbsp;' + dispname;
        }
        row.find('td:first').html(dispname);
        this.env.subscriptionrows[id][1] = dispname;
      }
    }
    // update list widget
    this.init_subscription_list();
  };
  // remove the table row of a specific mailbox from the table
  // (the row will not be removed, just hidden)
  this.remove_folder_row = function(folder)
  this.remove_folder_row = function(folder, subs)
  {
    var row, id = this.get_folder_row_id(folder);
    var n, len, list = [], id = this.get_folder_row_id(folder);
    if (id && (row = document.getElementById(id)))
      row.style.display = 'none';
    // get subfolders if any
    if (subs)
      list = this.get_subfolders(folder);
    // remove old row
    this._remove_folder_row(id);
    // remove subfolders
    for (n=0, len=list.length; n<len; n++)
      this._remove_folder_row(list[n]);
  };
  this._remove_folder_row = function(id)
  {
    this.subscription_list.remove_row(id.replace(/^rcmrow/, ''));
    $('#'+id).remove();
    delete this.env.subscriptionrows[id];
  }
  this.get_subfolders = function(folder)
  {
    var name, list = [],
      regex = new RegExp('^'+RegExp.escape(folder)+RegExp.escape(this.env.delimiter)),
      row = $('#'+this.get_folder_row_id(folder)).get(0);
    while (row = row.nextSibling) {
      if (row.id) {
        name = this.env.subscriptionrows[row.id][0];
        if (regex.test(name)) {
          list.push(row.id);
        }
        else
          break;
      }
    }
    return list;
  }
  this.subscribe = function(folder)
  {
@@ -4436,33 +4659,12 @@
  // helper method to find a specific mailbox row ID
  this.get_folder_row_id = function(folder)
  {
    for (var id in this.env.subscriptionrows)
      if (this.env.subscriptionrows[id] && this.env.subscriptionrows[id][0] == folder)
    var id, folders = this.env.subscriptionrows;
    for (id in folders)
      if (folders[id] && folders[id][0] == folder)
        break;
    return id;
  };
  // duplicate a specific table row
  this.clone_table_row = function(row)
  {
    var cell, td,
      new_row = document.createElement('tr');
    for (var n=0; n<row.cells.length; n++) {
      cell = row.cells[n];
      td = document.createElement('td');
      if (cell.className)
        td.className = cell.className;
      if (cell.align)
        td.setAttribute('align', cell.align);
      td.innerHTML = cell.innerHTML;
      new_row.appendChild(td);
    }
    return new_row;
  };
  // when user select a folder in manager
@@ -5002,16 +5204,6 @@
    }
  };
  // notifies that a new message(s) has hit the mailbox
  this.new_message_focus = function()
  {
    // focus main window
    if (this.env.framed && window.parent)
      window.parent.focus();
    else
      window.focus();
  };
  this.toggle_prefer_html = function(checkbox)
  {
    var elem;
@@ -5310,9 +5502,20 @@
    switch (response.action) {
      case 'delete':
        if (this.task == 'addressbook') {
          var uid = this.contact_list.get_selection();
          var sid, uid = this.contact_list.get_selection(), writable = false;
          if (uid && this.contact_list.rows[uid]) {
            // search results, get source ID from record ID
            if (this.env.source == '') {
              sid = String(uid).replace(/^[^-]+-/, '');
              writable = sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly;
            }
            else {
              writable = !this.env.address_sources[this.env.source].readonly;
            }
          }
          this.enable_command('compose', (uid && this.contact_list.rows[uid]));
          this.enable_command('delete', 'edit', (uid && this.contact_list.rows[uid] && this.env.address_sources && !this.env.address_sources[this.env.source].readonly));
          this.enable_command('delete', 'edit', writable);
          this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
        }
@@ -5361,10 +5564,9 @@
          this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
          if (response.action == 'list' || response.action == 'search') {
            this.enable_command('group-create',
              (this.env.address_sources[this.env.source].groups && !this.env.address_sources[this.env.source].readonly));
            this.enable_command('group-rename', 'group-delete',
              (this.env.address_sources[this.env.source].groups && this.env.group && !this.env.address_sources[this.env.source].readonly));
            var source = this.env.source != '' ? this.env.address_sources[this.env.source] : null;
            this.enable_command('group-create', (source && source.groups && !source.readonly));
            this.enable_command('group-rename', 'group-delete', (source && source.groups && this.env.group && !source.readonly));
            this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
          }
        }
@@ -5417,12 +5619,13 @@
    form.target = frame_name;
    form.action = this.url(action, { _id:this.env.compose_id||'', _uploadid:ts });
    form.setAttribute('method', 'POST');
    form.setAttribute('enctype', 'multipart/form-data');
    form.submit();
    return frame_name;
  };
  // starts interval for keep-alive/check-recent signal
  this.start_keepalive = function()
  {