alecpl
2011-05-05 4764079c6af092a6dfa18306601e1b33482fb756
program/js/app.js
@@ -3,7 +3,7 @@
 | Roundcube Webmail Client Script                                       |
 |                                                                       |
 | This file is part of the Roundcube Webmail client                     |
 | Copyright (C) 2005-2010, Roundcube Dev, - Switzerland                 |
 | Copyright (C) 2005-2010, The Roundcube Dev Team                       |
 | Licensed under the GNU GPL                                            |
 |                                                                       |
 +-----------------------------------------------------------------------+
@@ -16,7 +16,6 @@
  $Id$
*/
function rcube_webmail()
{
@@ -63,7 +62,7 @@
  // set environment variable(s)
  this.set_env = function(p, value)
  {
    if (p != null && typeof(p) == 'object' && !value)
    if (p != null && typeof p === 'object' && !value)
      for (var n in p)
        this.env[n] = p[n];
    else
@@ -134,6 +133,14 @@
    if (!bw.dom || !bw.xmlhttp_test()) {
      this.goto_url('error', '_code=0x199');
      return;
    }
    // Enable debug console
    if (!window.console || !window.console.log) {
      window.console = new rcube_console();
    }
    else {
      $('#console').hide();
    }
    // find all registered gui containers
@@ -228,7 +235,7 @@
          }
        }
        else if (this.env.action == 'compose') {
          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'toggle-editor'];
          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor'];
          if (this.env.drafts_mailbox)
            this.env.compose_commands.push('savedraft')
@@ -317,9 +324,9 @@
          }
        }
        if ((this.env.action=='add' || this.env.action=='edit') && this.gui_objects.editform) {
        if ((this.env.action == 'add' || this.env.action == 'edit') && this.gui_objects.editform) {
          this.enable_command('save', true);
          $("input[type='text']").first().select();
          this.init_contact_form();
        }
        else if (this.gui_objects.qsearchbox) {
          this.enable_command('search', 'reset-search', 'moveto', true);
@@ -336,14 +343,14 @@
      case 'settings':
        this.enable_command('preferences', 'identities', 'save', 'folders', true);
        if (this.env.action=='identities') {
        if (this.env.action == 'identities') {
          this.enable_command('add', this.env.identities_level < 2);
        }
        else if (this.env.action=='edit-identity' || this.env.action=='add-identity') {
        else if (this.env.action == 'edit-identity' || this.env.action == 'add-identity') {
          this.enable_command('add', this.env.identities_level < 2);
          this.enable_command('save', 'delete', 'edit', 'toggle-editor', true);
        }
        else if (this.env.action=='folders') {
        else if (this.env.action == 'folders') {
          this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', true);
        }
        else if (this.env.action == 'edit-folder' && this.gui_objects.editform) {
@@ -415,9 +422,9 @@
    // execute all foreign onload scripts
    // @deprecated
    for (var i in this.onloads) {
      if (typeof(this.onloads[i]) == 'string')
      if (typeof this.onloads[i] === 'string')
        eval(this.onloads[i]);
      else if (typeof(this.onloads[i]) == 'function')
      else if (typeof this.onloads[i] === 'function')
        this.onloads[i]();
      }
@@ -455,24 +462,24 @@
    }
    // process external commands
    if (typeof this.command_handlers[command] == 'function') {
    if (typeof this.command_handlers[command] === 'function') {
      var ret = this.command_handlers[command](props, obj);
      return ret !== null ? ret : (obj ? false : true);
      return ret !== undefined ? ret : (obj ? false : true);
    }
    else if (typeof this.command_handlers[command] == 'string') {
    else if (typeof this.command_handlers[command] === 'string') {
      var ret = window[this.command_handlers[command]](props, obj);
      return ret !== null ? ret : (obj ? false : true);
      return ret !== undefined ? ret : (obj ? false : true);
    }
    // trigger plugin hooks
    this.triggerEvent('actionbefore', {props:props, action:command});
    var event_ret = this.triggerEvent('before'+command, props);
    if (typeof event_ret != 'undefined') {
    var ret = this.triggerEvent('before'+command, props);
    if (ret !== undefined) {
      // abort if one the handlers returned false
      if (event_ret === false)
      if (ret === false)
        return false;
      else
        props = event_ret;
        props = ret;
    }
    // process internal command
@@ -634,11 +641,14 @@
              input_name.focus();
              break;
            }
            else if (input_email.length && !rcube_check_email(input_email.val())) {
            else if (this.task == 'settings' && input_email.length && (this.env.identities_level % 2) == 0 && !rcube_check_email(input_email.val())) {
              alert(this.get_label('noemailwarning'));
              input_email.focus();
              break;
            }
            // clear empty input fields
            $('input.placeholder').each(function(){ if (this.value == this._placeholder) this.value = ''; });
          }
          this.gui_objects.editform.submit();
@@ -647,13 +657,13 @@
      case 'delete':
        // mail task
        if (this.task=='mail')
        if (this.task == 'mail')
          this.delete_messages();
        // addressbook task
        else if (this.task=='addressbook')
        else if (this.task == 'addressbook')
          this.delete_contacts();
        // user settings task
        else if (this.task=='settings')
        else if (this.task == 'settings')
          this.delete_identity();
        break;
@@ -798,10 +808,10 @@
      case 'compose':
        var url = this.env.comm_path+'&_action=compose';
        if (this.task=='mail') {
        if (this.task == 'mail') {
          url += '&_mbox='+urlencode(this.env.mailbox);
          if (this.env.mailbox==this.env.drafts_mailbox) {
          if (this.env.mailbox == this.env.drafts_mailbox) {
            var uid;
            if (uid = this.get_single_uid())
              url += '&_draft_uid='+uid;
@@ -810,7 +820,7 @@
             url += '&_to='+urlencode(props);
        }
        // modify url if we're in addressbook
        else if (this.task=='addressbook') {
        else if (this.task == 'addressbook') {
          // switch to mail compose step directly
          if (props && props.indexOf('@') > 0) {
            url = this.get_task_url('mail', url);
@@ -819,24 +829,21 @@
          }
          // use contact_id passed as command parameter
          var a_cids = [];
          var n, len, a_cids = [];
          if (props)
            a_cids.push(props);
          // get selected contacts
          else if (this.contact_list) {
            var selection = this.contact_list.get_selection();
            for (var n=0; n<selection.length; n++)
            for (n=0, len=selection.length; n<len; n++)
              a_cids.push(selection[n]);
          }
          if (a_cids.length)
            this.http_request('mailto', '_cid='+urlencode(a_cids.join(','))+'&_source='+urlencode(this.env.source), true);
            this.http_post('mailto', {_cid: a_cids.join(','), _source: this.env.source}, true);
          break;
        }
        // don't know if this is necessary...
        url = url.replace(/&_framed=1/, '');
        this.redirect(url);
        break;
@@ -996,31 +1003,29 @@
      case 'export':
        if (this.contact_list.rowcount > 0) {
          var add_url = (this.env.source ? '_source='+urlencode(this.env.source)+'&' : '');
          if (this.env.search_request)
            add_url += '_search='+this.env.search_request;
          this.goto_url('export', add_url);
          this.goto_url('export', { _source:this.env.source, _gid:this.env.group, _search:this.env.search_request });
        }
        break;
      case 'upload-photo':
        this.upload_contact_photo(props);
        break;
      case 'delete-photo':
        this.replace_contact_photo('-del-');
        break;
      // user settings commands
      case 'preferences':
        this.goto_url('');
        break;
      case 'identities':
        this.goto_url('settings/identities');
        break;
      case 'folders':
        this.goto_url('settings/folders');
        this.goto_url('settings/' + command);
        break;
      // unified command call (command name == function name)
      default:
        var func = command.replace(/-/g, '_');
        if (this[func] && typeof this[func] == 'function')
        if (this[func] && typeof this[func] === 'function')
          this[func](props);
        break;
    }
@@ -1158,7 +1163,7 @@
  this.is_framed = function()
  {
    return (this.env.framed && parent.rcmail);
    return (this.env.framed && parent.rcmail && parent.rcmail != this && parent.rcmail.command);
  };
@@ -1359,22 +1364,20 @@
  this.doc_mouse_up = function(e)
  {
    var model, list, li;
    var model, list, li, id;
    if (this.message_list) {
      if (!rcube_mouse_is_over(e, this.message_list.list.parentNode))
        this.message_list.blur();
    if (list = this.message_list) {
      if (!rcube_mouse_is_over(e, list.list.parentNode))
        list.blur();
      else
        this.message_list.focus();
      list = this.message_list;
        list.focus();
      model = this.env.mailboxes;
    }
    else if (this.contact_list) {
      if (!rcube_mouse_is_over(e, this.contact_list.list.parentNode))
        this.contact_list.blur();
    else if (list = this.contact_list) {
      if (!rcube_mouse_is_over(e, list.list.parentNode))
        list.blur();
      else
        this.contact_list.focus();
      list = this.contact_list;
        list.focus();
      model = this.env.contactfolders;
    }
    else if (this.ksearch_value) {
@@ -1395,8 +1398,8 @@
    // reset 'pressed' buttons
    if (this.buttons_sel) {
      for (var id in this.buttons_sel)
        if (typeof id != 'function')
      for (id in this.buttons_sel)
        if (typeof id !== 'function')
          this.button_out(this.buttons_sel[id], id);
      this.buttons_sel = {};
    }
@@ -1494,8 +1497,6 @@
      this.command('previouspage');
    else if (list.key_pressed == 34)
      this.command('nextpage');
    else
      list.shiftkey = false;
  };
  this.msglist_get_preview = function()
@@ -1682,7 +1683,7 @@
          message.expanded = true;
      }
      else if (message.has_children) {
        if (typeof(message.expanded) == 'undefined' && (this.env.autoexpand_threads == 1 || (this.env.autoexpand_threads == 2 && message.unread_children))) {
        if (message.expanded === undefined && (this.env.autoexpand_threads == 1 || (this.env.autoexpand_threads == 2 && message.unread_children))) {
          message.expanded = true;
        }
      }
@@ -1701,7 +1702,7 @@
      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+'"'+
        ' onclick="return rcube_event.cancel(event)">'+cols.subject+'</a>';
        ' onclick="return rcube_event.cancel(event)" onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')">'+cols.subject+'</a>';
    }
    // add each submitted col
@@ -1770,7 +1771,7 @@
  {
    var update, add_url = '';
    if (typeof sort_col == 'undefined')
    if (sort_col === undefined)
      sort_col = this.env.sort_col;
    if (!sort_order)
      sort_order = this.env.sort_order;
@@ -1838,7 +1839,7 @@
      if (!this.env.frame_lock) {
        (this.is_framed() ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
      }
      target.location.href = this.env.comm_path+url;
      this.location_href(this.env.comm_path+url, target);
      // mark as read and change mbox unread counter
      if (action == 'preview' && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read >= 0) {
@@ -1960,7 +1961,7 @@
    // load message list to target frame/window
    if (mbox) {
      this.set_busy(true, 'loading');
      target.location.href = this.env.comm_path+'&_mbox='+urlencode(mbox)+(page ? '&_page='+page : '')+url;
      this.location_href(this.env.comm_path+'&_mbox='+urlencode(mbox)+(page ? '&_page='+page : '')+url, target);
    }
  };
@@ -2366,7 +2367,7 @@
  // copy selected messages to the specified mailbox
  this.copy_messages = function(mbox)
  {
    if (mbox && typeof mbox == 'object')
    if (mbox && typeof mbox === 'object')
      mbox = mbox.id;
    // exit if current or no mailbox specified or if selection is empty
@@ -2395,7 +2396,7 @@
  // move selected messages to the specified mailbox
  this.move_messages = function(mbox)
  {
    if (mbox && typeof mbox == 'object')
    if (mbox && typeof mbox === 'object')
      mbox = mbox.id;
    // exit if current or no mailbox specified or if selection is empty
@@ -2421,17 +2422,19 @@
  // delete selected messages from the current mailbox
  this.delete_messages = function()
  {
    var selection = this.message_list ? $.merge([], this.message_list.get_selection()) : [];
    var uid, i, len, trash = this.env.trash_mailbox,
      list = this.message_list,
      selection = list ? $.merge([], list.get_selection()) : [];
    // exit if no mailbox specified or if selection is empty
    if (!this.env.uid && !selection.length)
      return;
    // also select childs of collapsed rows
    for (var uid, i=0, len=selection.length; i<len; i++) {
    for (i=0, len=selection.length; i<len; i++) {
      uid = selection[i];
      if (this.message_list.rows[uid].has_children && !this.message_list.rows[uid].expanded)
        this.message_list.select_childs(uid);
      if (list.rows[uid].has_children && !list.rows[uid].expanded)
        list.select_childs(uid);
    }
    // if config is set to flag for deletion
@@ -2440,17 +2443,18 @@
      return false;
    }
    // if there isn't a defined trash mailbox or we are in it
    else if (!this.env.trash_mailbox || this.env.mailbox == this.env.trash_mailbox)
    // @TODO: we should check if defined trash mailbox exists
    else if (!trash || this.env.mailbox == trash)
      this.permanently_remove_messages();
    // if there is a trash mailbox defined and we're not currently in it
    else {
      // if shift was pressed delete it immediately
      if (this.message_list && this.message_list.shiftkey) {
      if (list && list.shiftkey) {
        if (confirm(this.get_label('deletemessagesconfirm')))
          this.permanently_remove_messages();
      }
      else
        this.move_messages(this.env.trash_mailbox);
        this.move_messages(trash);
    }
    return true;
@@ -2886,7 +2890,7 @@
    // check if all files has been uploaded
    for (var key in this.env.attachments) {
      if (typeof this.env.attachments[key] == 'object' && !this.env.attachments[key].complete) {
      if (typeof this.env.attachments[key] === 'object' && !this.env.attachments[key].complete) {
        alert(this.get_label('notuploadedwarning'));
        return false;
      }
@@ -3000,11 +3004,11 @@
  this.compose_field_hash = function(save)
  {
    // check input fields
    var value_to = $("[name='_to']").val();
    var value_cc = $("[name='_cc']").val();
    var value_bcc = $("[name='_bcc']").val();
    var value_subject = $("[name='_subject']").val();
    var str = '';
    var ed, str = '',
      value_to = $("[name='_to']").val(),
      value_cc = $("[name='_cc']").val(),
      value_bcc = $("[name='_bcc']").val(),
      value_subject = $("[name='_subject']").val();
    if (value_to)
      str += value_to+':';
@@ -3015,9 +3019,8 @@
    if (value_subject)
      str += value_subject+':';
    var editor = tinyMCE.get(this.env.composebody);
    if (editor)
      str += editor.getContent();
    if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)))
      str += ed.getContent();
    else
      str += $("[name='_message']").val();
@@ -3167,37 +3170,13 @@
    if (!form)
      return false;
    // get file input fields
    var send = false;
    for (var n=0; n<form.elements.length; n++)
      if (form.elements[n].type=='file' && form.elements[n].value) {
        send = true;
        break;
      }
    // get file input field, count files on capable browser
    var field = $('input[type=file]', form).get(0),
      files = field.files ? field.files.length : field.value ? 1 : 0;
    // create hidden iframe and post upload form
    if (send) {
      var ts = new Date().getTime();
      var frame_name = 'rcmupload'+ts;
      // have to do it this way for IE
      // otherwise the form will be posted to a new window
      if (document.all) {
        var html = '<iframe name="'+frame_name+'" src="program/blank.gif" style="width:0;height:0;visibility:hidden;"></iframe>';
        document.body.insertAdjacentHTML('BeforeEnd',html);
      }
      else { // for standards-compilant browsers
        var frame = document.createElement('iframe');
        frame.name = frame_name;
        frame.style.border = 'none';
        frame.style.width = 0;
        frame.style.height = 0;
        frame.style.visibility = 'hidden';
        document.body.appendChild(frame);
      }
      // handle upload errors, parsing iframe content in onload
      $(frame_name).bind('load', {ts:ts}, function(e) {
    if (files) {
      var frame_name = this.async_upload_form(form, 'upload', function(e) {
        var d, content = '';
        try {
          if (this.contentDocument) {
@@ -3206,7 +3185,7 @@
            d = this.contentWindow.document;
          }
          content = d.childNodes[0].innerHTML;
        } catch (e) {}
        } catch (err) {}
        if (!content.match(/add2attachment/) && (!bw.opera || (rcmail.env.uploadframe && rcmail.env.uploadframe == e.data.ts))) {
          if (!content.match(/display_message/))
@@ -3218,13 +3197,10 @@
          rcmail.env.uploadframe = e.data.ts;
      });
      form.target = frame_name;
      form.action = this.env.comm_path+'&_action=upload&_uploadid='+ts;
      form.setAttribute('enctype', 'multipart/form-data');
      form.submit();
      // display upload indicator and cancel button
      var content = this.get_label('uploading');
      var content = this.get_label('uploading' + (files > 1 ? 'many' : '')),
        ts = frame_name.replace(/^rcmupload/, '');
      if (this.env.loadingicon)
        content = '<img src="'+this.env.loadingicon+'" alt="" />'+content;
      if (this.env.cancelicon)
@@ -3244,8 +3220,7 @@
    if (!this.gui_objects.attachmentlist)
      return false;
    var li = $('<li>').attr('id', name).html(att.html);
    var indicator;
    var indicator, li = $('<li>').attr('id', name).html(att.html);
    // replace indicator's li
    if (upload_id && (indicator = document.getElementById(upload_id))) {
@@ -3272,7 +3247,7 @@
      return false;
    var list = this.gui_objects.attachmentlist.getElementsByTagName("li");
    for (i=0;i<list.length;i++)
    for (i=0; i<list.length; i++)
      if (list[i].id == name)
        this.gui_objects.attachmentlist.removeChild(list[i]);
  };
@@ -3280,7 +3255,7 @@
  this.remove_attachment = function(name)
  {
    if (name && this.env.attachments[name])
      this.http_post('remove-attachment', '_file='+urlencode(name));
      this.http_post('remove-attachment', { _id:this.env.compose_id, _file:name });
    return true;
  };
@@ -3449,13 +3424,13 @@
      end = inp_value.substring(p+this.ksearch_value.length, inp_value.length);
    // insert all members of a group
    if (typeof this.env.contacts[id] == 'object' && this.env.contacts[id].id) {
    if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].id) {
      insert += this.env.contacts[id].name + ', ';
      this.group2expand = $.extend({}, this.env.contacts[id]);
      this.group2expand.input = this.ksearch_input;
      this.http_request('group-expand', '_source='+urlencode(this.env.contacts[id].source)+'&_gid='+urlencode(this.env.contacts[id].id), false);
    }
    else if (typeof this.env.contacts[id] == 'string')
    else if (typeof this.env.contacts[id] === 'string')
      insert = this.env.contacts[id] + ', ';
    this.ksearch_input.value = pre + insert + end;
@@ -3498,15 +3473,14 @@
    if (q == this.ksearch_value)
      return;
    if (q.length < min) {
    if (q.length && q.length < min) {
      if (!this.env.acinfo) {
        var label = this.get_label('autocompletechars');
        label = label.replace('$min', min);
        this.env.acinfo = this.display_message(label);
        this.env.acinfo = this.display_message(
          this.get_label('autocompletechars').replace('$min', min));
      }
      return;
    }
    else if (this.env.acinfo && q.length == min) {
    else if (this.env.acinfo) {
      this.hide_message(this.env.acinfo);
    }
@@ -3554,7 +3528,7 @@
      // add each result line to list
      for (i=0; i < a_results.length; i++) {
        text = typeof a_results[i] == 'object' ? a_results[i].name : a_results[i];
        text = typeof a_results[i] === 'object' ? a_results[i].name : a_results[i];
        li = document.createElement('LI');
        li.innerHTML = text.replace(new RegExp('('+RegExp.escape(s_val)+')', 'ig'), '##$1%%').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/##([^%]+)%%/g, '<b>$1</b>');
        li.onmouseover = function(){ ref.ksearch_select(this); };
@@ -3602,7 +3576,7 @@
    if (this.ksearch_pane)
      this.ksearch_pane.hide();
  };
   };
  /*********************************************************/
@@ -3677,7 +3651,7 @@
      add_url += '&_search='+this.env.search_request;
    this.set_busy(true, 'loading');
    target.location.href = this.env.comm_path + (src ? '&_source='+urlencode(src) : '') + add_url;
    this.location_href(this.env.comm_path + (src ? '&_source='+urlencode(src) : '') + add_url, target);
  };
  // send remote request to load contacts list
@@ -3723,7 +3697,7 @@
        add_url += '&_gid='+urlencode(this.env.group);
      this.set_busy(true);
      target.location.href = this.env.comm_path+'&_action='+action+'&_source='+urlencode(this.env.source)+'&_cid='+urlencode(cid) + add_url;
      this.location_href(this.env.comm_path+'&_action='+action+'&_source='+urlencode(this.env.source)+'&_cid='+urlencode(cid) + add_url, target);
    }
    return true;
  };
@@ -3777,6 +3751,9 @@
        this.show_contentframe(false);
    }
    if (this.env.group)
      qs += '&_gid='+urlencode(this.env.group);
    // also send search request to get the right records from the next page
    if (this.env.search_request) 
      qs += '&_search='+this.env.search_request;
@@ -3790,25 +3767,25 @@
  // update a contact record in the list
  this.update_contact_row = function(cid, cols_arr, newcid)
  {
    var row;
    if (this.contact_list.rows[cid] && (row = this.contact_list.rows[cid].obj)) {
      for (var c=0; c<cols_arr.length; c++)
    var c, row, list = this.contact_list;
    cid = String(cid).replace(this.identifier_expr, '_');
    if (list.rows[cid] && (row = list.rows[cid].obj)) {
      for (c=0; c<cols_arr.length; c++)
        if (row.cells[c])
          $(row.cells[c]).html(cols_arr[c]);
      // cid change
      if (newcid) {
        newcid = String(newcid).replace(this.identifier_expr, '_');
        row.id = 'rcmrow' + newcid;
        this.contact_list.remove_row(cid);
        this.contact_list.init_row(row);
        this.contact_list.selection[0] = newcid;
        list.remove_row(cid);
        list.init_row(row);
        list.selection[0] = newcid;
        row.style.display = '';
      }
      return true;
    }
    return false;
  };
  // add row to contacts list
@@ -3822,7 +3799,7 @@
      even = rowcount%2,
      row = document.createElement('tr');
    row.id = 'rcmrow'+cid;
    row.id = 'rcmrow'+String(cid).replace(this.identifier_expr, '_');
    row.className = 'contact '+(even ? 'even' : 'odd');
    if (this.contact_list.in_selection(cid))
@@ -3839,6 +3816,28 @@
    this.contact_list.insert_row(row);
    this.enable_command('export', (this.contact_list.rowcount > 0));
  };
  this.init_contact_form = function()
  {
    var ref = this, col;
    this.set_photo_actions($('#ff_photo').val());
    for (col in this.env.coltypes)
      this.init_edit_field(col, null);
    $('.contactfieldgroup .row a.deletebutton').click(function() {
      ref.delete_edit_field(this);
      return false;
    });
    $('select.addfieldmenu').change(function(e) {
      ref.insert_edit_field($(this).val(), $(this).attr('rel'), this);
      this.selectedIndex = 0;
    });
    $("input[type='text']:visible").first().focus();
  };
  this.group_create = function()
@@ -3954,9 +3953,10 @@
    this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
    var link = $('<a>').attr('href', '#')
      .attr('rel', prop.source+':'+prop.id)
      .bind('click', function() { return rcmail.command('listgroup', prop, this);})
      .html(prop.name);
    var li = $('<li>').attr('id', 'rcmli'+key)
    var li = $('<li>').attr('id', 'rcmli'+key.replace(this.identifier_expr, '_'))
      .addClass('contactgroup')
      .append(link)
      .insertAfter(this.get_folder_li(prop.source));
@@ -3969,13 +3969,198 @@
  {
    this.reset_add_input();
    var key = 'G'+prop.source+prop.id, link, li = this.get_folder_li(key);
    var key = 'G'+prop.source+prop.id,
      li = this.get_folder_li(key),
      link;
    if (li && (link = li.firstChild) && link.tagName.toLowerCase() == 'a')
    // group ID has changed, replace link node and identifiers
    if (li && prop.newid) {
      var newkey = 'G'+prop.source+prop.newid;
      li.id = String('rcmli'+newkey).replace(this.identifier_expr, '_');
      this.env.contactfolders[newkey] = this.env.contactfolders[key];
      this.env.contactfolders[newkey].id = prop.newid;
      this.env.group = prop.newid;
      var newprop = $.extend({}, prop);
      newprop.id = prop.newid;
      newprop.type = 'group';
      link = $('<a>').attr('href', '#')
        .attr('rel', prop.source+':'+prop.newid)
        .bind('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.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:li[0] });
    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)
      elem = $('.ff_' + col);
    elem.focus(function(){ ref.focus_textfield(this); })
      .blur(function(){ ref.blur_textfield(this); })
      .each(function(){ this._placeholder = this.title = ref.env.coltypes[col].label; ref.blur_textfield(this); });
  };
  this.insert_edit_field = function(col, section, menu)
  {
    // just make pre-defined input field visible
    var elem = $('#ff_'+col);
    if (elem.length) {
      elem.show().focus();
      $(menu).children('option[value="'+col+'"]').attr('disabled', true);
    }
    else {
      var lastelem = $('.ff_'+col),
        appendcontainer = $('#contactsection'+section+' .contactcontroller'+col);
      if (!appendcontainer.length)
        appendcontainer = $('<fieldset>').addClass('contactfieldgroup contactcontroller'+col).insertAfter($('#contactsection'+section+' .contactfieldgroup').last());
      if (appendcontainer.length && appendcontainer.get(0).nodeName == 'FIELDSET') {
        var input, colprop = this.env.coltypes[col],
          row = $('<div>').addClass('row'),
          cell = $('<div>').addClass('contactfieldcontent data'),
          label = $('<div>').addClass('contactfieldlabel label');
        if (colprop.subtypes_select)
          label.html(colprop.subtypes_select);
        else
          label.html(colprop.label);
        var name_suffix = colprop.limit != 1 ? '[]' : '';
        if (colprop.type == 'text' || colprop.type == 'date') {
          input = $('<input>')
            .addClass('ff_'+col)
            .attr('type', 'text')
            .attr('name', '_'+col+name_suffix)
            .attr('size', colprop.size)
            .appendTo(cell);
          this.init_edit_field(col, input);
        }
        else if (colprop.type == 'composite') {
          var childcol, cp, first;
          for (var childcol in colprop.childs) {
            cp = colprop.childs[childcol];
            input = $('<input>')
              .addClass('ff_'+childcol)
              .attr('type', 'text')
              .attr('name', '_'+childcol+name_suffix)
              .attr('size', cp.size)
              .appendTo(cell);
            cell.append(" ");
            this.init_edit_field(childcol, input);
            if (!first) first = input;
          }
          input = first;  // set focus to the first of this composite fields
        }
        else if (colprop.type == 'select') {
          input = $('<select>')
            .addClass('ff_'+col)
            .attr('name', '_'+col+name_suffix)
            .appendTo(cell);
          var options = input.attr('options');
          options[options.length] = new Option('---', '');
          if (colprop.options)
            $.each(colprop.options, function(i, val){ options[options.length] = new Option(val, i); });
        }
        if (input) {
          var delbutton = $('<a href="#del"></a>')
            .addClass('contactfieldbutton deletebutton')
            .attr('title', this.get_label('delete'))
            .attr('rel', col)
            .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)
            $(menu).children('option[value="'+col+'"]').attr('disabled', true);
        }
      }
    }
  };
  this.delete_edit_field = function(elem)
  {
    var col = $(elem).attr('rel'),
      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();
    else {
      $(elem).parents('div.row').remove();
      // hide entire fieldset if no more rows
      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+'"]');
      if (option.length)
        option.attr('disabled', false);
      else
        option = $('<option>').attr('value', col).html(colprop.label).appendTo(addmenu);
      addmenu.show();
    }
  };
  this.upload_contact_photo = function(form)
  {
    if (form && form.elements._photo.value) {
      this.async_upload_form(form, 'upload-photo', function(e) {
        rcmail.set_busy(false, null, rcmail.photo_upload_id);
      });
      // display upload indicator
      this.photo_upload_id = this.set_busy(true, 'uploading');
    }
  };
  this.replace_contact_photo = function(id)
  {
    var img_src = id == '-del-' ? this.env.photo_placeholder :
      this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + this.env.cid + '&_photo=' + id;
    this.set_photo_actions(id);
    $(this.gui_objects.contactphoto).children('img').attr('src', img_src);
  };
  this.photo_upload_end = function()
  {
    this.set_busy(false, null, this.photo_upload_id);
    delete this.photo_upload_id;
  };
  this.set_photo_actions = function(id)
  {
    var n, buttons = this.buttons['upload-photo'];
    for (n=0; n < buttons.length; n++)
      $('#'+buttons[n].id).html(this.get_label(id == '-del-' ? 'addphoto' : 'replacephoto'));
    $('#ff_photo').val(id);
    this.enable_command('upload-photo', this.env.coltypes.photo ? true : false);
    this.enable_command('delete-photo', this.env.coltypes.photo && id != '-del-');
  };
@@ -4011,7 +4196,7 @@
        add_url = '&_framed=1';
        target = window.frames[this.env.contentframe];
      }
      target.location.href = this.env.comm_path+'&_action=edit-prefs&_section='+id+add_url;
      this.location_href(this.env.comm_path+'&_action=edit-prefs&_section='+id+add_url, target);
    }
    return true;
@@ -4040,7 +4225,7 @@
    if (action && (id || action=='add-identity')) {
      this.set_busy(true);
      target.location.href = this.env.comm_path+'&_action='+action+'&_iid='+id+add_url;
      this.location_href(this.env.comm_path+'&_action='+action+'&_iid='+id+add_url, target);
    }
    return true;
@@ -4305,8 +4490,16 @@
      if (!this.env.frame_lock) {
        (parent.rcmail ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
      }
      target.location.href = this.env.comm_path+url;
      this.location_href(this.env.comm_path+url, target);
    }
  };
  // disables subscription checkbox (for protected folder)
  this.disable_subscription = function(folder)
  {
    var id = this.get_folder_row_id(folder);
    if (id)
      $('input[name="_subscribed[]"]', $('#'+id)).attr('disabled', true);
  };
  this.folder_size = function(folder)
@@ -4336,7 +4529,7 @@
  this.init_buttons = function()
  {
    for (var cmd in this.buttons) {
      if (typeof cmd != 'string')
      if (typeof cmd !== 'string')
        continue;
      for (var i=0; i< this.buttons[cmd].length; i++) {
@@ -4397,7 +4590,7 @@
        obj.src = button[state];
      }
      // set class name according to button state
      else if (obj && typeof(button[state])!='undefined') {
      else if (obj && button[state] !== undefined) {
        button.status = state;
        obj.className = button[state];
      }
@@ -4497,6 +4690,23 @@
    }
  };
  this.focus_textfield = function(elem)
  {
    elem._hasfocus = true;
    var $elem = $(elem);
    if ($elem.hasClass('placeholder') || $elem.val() == elem._placeholder)
      $elem.val('').removeClass('placeholder').attr('spellcheck', true);
  };
  this.blur_textfield = function(elem)
  {
    elem._hasfocus = false;
    var $elem = $(elem);
    if (elem._placeholder && (!$elem.val() || $elem.val() == elem._placeholder))
      $elem.addClass('placeholder').attr('spellcheck', false).val(elem._placeholder);
  };
  // write to the document/window title
  this.set_pagetitle = function(title)
  {
@@ -4521,35 +4731,48 @@
    type = type ? type : 'notice';
    var ref = this,
      key = msg,
      date = new Date(),
      id = type + date.getTime(),
      timeout = type == 'loading' ? this.env.request_timeout * 1000 : (this.message_time * (type == 'error' || type == 'warning' ? 2 : 1));
    if (type == 'loading' && !msg)
      msg = this.get_label('loading');
      timeout = this.message_time * (type == 'error' || type == 'warning' ? 2 : 1);
    if (type == 'loading') {
      key = 'loading';
      timeout = this.env.request_timeout * 1000;
      if (!msg)
        msg = this.get_label('loading');
    }
    // The same message is already displayed
    if (this.messages[msg]) {
      this.messages[msg].elements.push(id);
      window.setTimeout(function() { ref.hide_message(id, true); }, timeout);
    if (this.messages[key]) {
      // replace label
      if (this.messages[key].obj)
        this.messages[key].obj.html(msg);
      // store label in stack
      if (type == 'loading') {
        this.messages[key].labels.push({'id': id, 'msg': msg});
      }
      // add element and set timeout
      this.messages[key].elements.push(id);
      window.setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
      return id;
    }
    var obj = $('<div>').addClass(type).html(msg).data('msg', msg),
      cont = $(this.gui_objects.message).show();
    // create DOM object and display it
    var obj = $('<div>').addClass(type).html(msg).data('key', key),
      cont = $(this.gui_objects.message).append(obj).show();
    this.messages[key] = {'obj': obj, 'elements': [id]};
    if (type == 'loading') {
      obj.appendTo(cont);
      this.messages[msg] = {'obj': obj, 'elements': [id]};
      window.setTimeout(function() { rcmail.hide_message(id); }, timeout);
      return id;
      this.messages[key].labels = [{'id': id, 'msg': msg}];
    }
    else {
      obj.appendTo(cont).bind('mousedown', function() { return ref.hide_message(obj, true); });
      window.setTimeout(function() { ref.hide_message(id, true); }, timeout);
      this.messages[msg] = { 'obj': obj, 'elements': [id] };
      return id;
      obj.click(function() { return ref.hide_message(obj); });
    }
    window.setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
    return id;
  };
  // make a message to disapear
@@ -4559,25 +4782,37 @@
    if (this.is_framed())
      return parent.rcmail.hide_message(obj, fade);
    if (typeof(obj) == 'object') {
      // hide message object
    var k, n, i, msg, m = this.messages;
    // Hide message by object, don't use for 'loading'!
    if (typeof obj === 'object') {
      $(obj)[fade?'fadeOut':'hide']();
      var msg = $(obj).data('msg');
      msg = $(obj).data('key');
      if (this.messages[msg])
        delete this.messages[msg];
    }
    // Hide message by id
    else {
      // hide message by id
      var k, n, m = this.messages;
      for (k in m) {
        for (n in m[k].elements) {
          if (m[k] && m[k].elements[n] == obj) {
            m[k].elements.splice(n, 1);
            // hide dom element if last instance is removed
            // hide DOM element if last instance is removed
            if (!m[k].elements.length) {
              m[k].obj[fade?'fadeOut':'hide']();
              delete m[k];
            }
            // set pending action label for 'loading' message
            else if (k == 'loading') {
              for (i in m[k].labels) {
                if (m[k].labels[i].id == obj) {
                  delete m[k].labels[i];
                }
                else {
                  msg = m[k].labels[i].msg;
                }
                m[k].obj.html(msg);
              }
            }
          }
        }
@@ -4592,7 +4827,7 @@
      var current_li, target_li;
      if ((current_li = this.get_folder_li(old, prefix))) {
        $(current_li).removeClass('selected').removeClass('unfocused');
        $(current_li).removeClass('selected').addClass('unfocused');
      }
      if ((target_li = this.get_folder_li(name, prefix))) {
        $(target_li).removeClass('unfocused').addClass('selected');
@@ -4621,17 +4856,18 @@
  // and for setting some message list global variables
  this.set_message_coltypes = function(coltypes, repl)
  {
    this.env.coltypes = coltypes;
    var list = this.message_list,
      thead = list ? list.list.tHead : null,
      cell, col, n, len, th, tr;
    // set correct list titles
    var thead = this.gui_objects.messagelist ? this.gui_objects.messagelist.tHead : null,
      cell, col, n, len;
    this.env.coltypes = coltypes;
    // replace old column headers
    if (thead) {
      if (repl) {
        var th = document.createElement('thead'),
          tr = document.createElement('tr');
        th = document.createElement('thead');
        tr = document.createElement('tr');
        for (c=0, len=repl.length; c < len; c++) {
          cell = document.createElement('td');
          cell.innerHTML = repl[c].html;
@@ -4641,6 +4877,7 @@
        }
        th.appendChild(tr);
        thead.parentNode.replaceChild(th, thead);
        thead = th;
      }
      for (n=0, len=this.env.coltypes.length; n<len; n++) {
@@ -4664,15 +4901,16 @@
    if ((n = $.inArray('subject', this.env.coltypes)) >= 0) {
      this.set_env('subject_col', n);
      if (this.message_list)
        this.message_list.subject_col = n;
      if (list)
        list.subject_col = n;
    }
    if ((n = $.inArray('flag', this.env.coltypes)) >= 0)
      this.set_env('flagged_col', n);
    if ((n = $.inArray('status', this.env.coltypes)) >= 0)
      this.set_env('status_col', n);
    this.message_list.init_header();
    if (list)
      list.init_header();
  };
  // replace content of row count display
@@ -4695,7 +4933,7 @@
  this.set_quota = function(content)
  {
    if (content && this.gui_objects.quotadisplay) {
      if (typeof(content) == 'object' && content.type == 'image')
      if (typeof content === 'object' && content.type == 'image')
        this.percent_indicator(this.gui_objects.quotadisplay, content);
      else
        $(this.gui_objects.quotadisplay).html(content);
@@ -4715,11 +4953,14 @@
  // update the mailbox count display
  this.set_unread_count_display = function(mbox, set_title)
  {
    var reg, text_obj, item, mycount, childcount, div;
    var reg, link, text_obj, item, mycount, childcount, div;
    if (item = this.get_folder_li(mbox)) {
      mycount = this.env.unread_counts[mbox] ? this.env.unread_counts[mbox] : 0;
      text_obj = item.getElementsByTagName('a')[0];
      link = $(item).children('a').eq(0);
      text_obj = link.children('span.unreadcount');
      if (!text_obj.length && mycount)
        text_obj = $('<span>').addClass('unreadcount').appendTo(link);
      reg = /\s+\([0-9]+\)$/i;
      childcount = 0;
@@ -4731,12 +4972,10 @@
            childcount += this.env.unread_counts[k];
      }
      if (mycount && text_obj.innerHTML.match(reg))
        text_obj.innerHTML = text_obj.innerHTML.replace(reg, ' ('+mycount+')');
      else if (mycount)
        text_obj.innerHTML += ' ('+mycount+')';
      else
        text_obj.innerHTML = text_obj.innerHTML.replace(reg, '');
      if (mycount && text_obj.length)
        text_obj.html(' ('+mycount+')');
      else if (text_obj.length)
        text_obj.remove();
      // set parent's display
      reg = new RegExp(RegExp.escape(this.env.delimiter) + '[^' + RegExp.escape(this.env.delimiter) + ']+$');
@@ -4840,8 +5079,9 @@
      quota_width = parseInt(quota / 100 * width),
      pos = $(obj).position();
    // Opera bug?
    // workarounds for Opera and Webkit bugs
    pos.top = Math.max(0, pos.top);
    pos.left = Math.max(0, pos.left);
    this.env.indicator_width = width;
    this.env.indicator_height = height;
@@ -4879,7 +5119,7 @@
      bar1.addClass('quota_mid');
    }
    else {
      main.addClass(' quota_text_normal');
      main.addClass(' quota_text_low');
      bar1.addClass('quota_low');
    }
@@ -4918,47 +5158,74 @@
  /********************************************************/
  /*********        remote request methods        *********/
  /********************************************************/
  // compose a valid url with the given parameters
  this.url = function(action, query)
  {
    var querystring = typeof query === 'string' ? '&' + query : '';
    if (typeof action !== 'string')
      query = action;
    else if (!query || typeof query !== 'object')
      query = {};
    if (action)
      query._action = action;
    else
      query._action = this.env.action;
    var base = this.env.comm_path;
    // overwrite task name
    if (query._action.match(/([a-z]+)\/([a-z-_]+)/)) {
      query._action = RegExp.$2;
      base = base.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
    }
    // remove undefined values
    var param = {};
    for (var k in query) {
      if (query[k] !== undefined && query[k] !== null)
        param[k] = query[k];
    }
    return base + '&' + $.param(param) + querystring;
  };
  this.redirect = function(url, lock)
  {
    if (lock || lock === null)
      this.set_busy(true);
    if (this.env.framed && window.parent)
      parent.location.href = url;
    if (this.is_framed())
      parent.rcmail.redirect(url, lock);
    else
      location.href = url;
      this.location_href(url, window);
  };
  this.goto_url = function(action, query, lock)
  {
    var url = this.env.comm_path,
     querystring = query ? '&'+query : '';
    this.redirect(this.url(action, query));
  };
    // overwrite task name
    if (action.match(/([a-z]+)\/([a-z-_]+)/)) {
      action = RegExp.$2;
      url = url.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
    }
    this.redirect(url+'&_action='+action+querystring, lock);
  this.location_href = function(url, target)
  {
    // simulate real link click to force IE to send referer header
    if (bw.ie && target == window)
      $('<a>').attr('href', url).appendTo(document.body).get(0).click();
    else
      target.location.href = url;
  };
  // send a http request to the server
  this.http_request = function(action, query, lock)
  {
    var url = this.env.comm_path;
    // overwrite task name
    if (action.match(/([a-z]+)\/([a-z-_]+)/)) {
      action = RegExp.$2;
      url = url.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
    }
    var url = this.url(action, query);
    // trigger plugin hook
    var result = this.triggerEvent('request'+action, query);
    if (typeof result != 'undefined') {
    if (result !== undefined) {
      // abort if one the handlers returned false
      if (result === false)
        return false;
@@ -4966,7 +5233,7 @@
        query = result;
    }
    url += '&_remote=1&_action=' + action + (query ? '&' : '') + query;
    url += '&_remote=1';
    // send request
    console.log('HTTP GET: ' + url);
@@ -4980,17 +5247,9 @@
  // send a http POST request to the server
  this.http_post = function(action, postdata, lock)
  {
    var url = this.env.comm_path;
    var url = this.url(action);
    // overwrite task name
    if (action.match(/([a-z]+)\/([a-z-_]+)/)) {
      action = RegExp.$2;
      url = url.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
    }
    url += '&_action=' + action;
    if (postdata && typeof(postdata) == 'object') {
    if (postdata && typeof postdata === 'object') {
      postdata._remote = 1;
      postdata._unlock = (lock ? lock : 0);
    }
@@ -4999,7 +5258,7 @@
    // trigger plugin hook
    var result = this.triggerEvent('request'+action, postdata);
    if (typeof result != 'undefined') {
    if (result !== undefined) {
      // abort if one the handlers returned false
      if (result === false)
        return false;
@@ -5033,9 +5292,9 @@
      this.set_env(response.env);
    // we have labels to add
    if (typeof response.texts == 'object') {
    if (typeof response.texts === 'object') {
      for (var name in response.texts)
        if (typeof response.texts[name] == 'string')
        if (typeof response.texts[name] === 'string')
          this.add_label(name, response.texts[name]);
    }
@@ -5131,10 +5390,43 @@
    this.set_busy(false, null, lock);
    request.abort();
    if (errmsg)
    if (request.status && errmsg)
      this.display_message(this.get_label('servererror') + ' (' + errmsg + ')', 'error');
  };
  // post the given form to a hidden iframe
  this.async_upload_form = function(form, action, onload)
  {
    var ts = new Date().getTime(),
      frame_name = 'rcmupload'+ts;
    // have to do it this way for IE
    // otherwise the form will be posted to a new window
    if (document.all) {
      var html = '<iframe name="'+frame_name+'" src="program/blank.gif" style="width:0;height:0;visibility:hidden;"></iframe>';
      document.body.insertAdjacentHTML('BeforeEnd', html);
    }
    else { // for standards-compilant browsers
      var frame = document.createElement('iframe');
      frame.name = frame_name;
      frame.style.border = 'none';
      frame.style.width = 0;
      frame.style.height = 0;
      frame.style.visibility = 'hidden';
      document.body.appendChild(frame);
    }
    // handle upload errors, parsing iframe content in onload
    $(frame_name).bind('load', {ts:ts}, onload);
    form.target = frame_name;
    form.action = this.url(action, { _id:this.env.compose_id||'', _uploadid:ts });
    form.setAttribute('enctype', 'multipart/form-data');
    form.submit();
    return frame_name;
  };
  // starts interval for keep-alive/check-recent signal
  this.start_keepalive = function()
  {
@@ -5144,14 +5436,7 @@
    if (this.env.keep_alive && !this.env.framed && this.task == 'mail' && this.gui_objects.mailboxlist)
      this._int = setInterval(function(){ ref.check_for_recent(false); }, this.env.keep_alive * 1000);
    else if (this.env.keep_alive && !this.env.framed && this.task != 'login' && this.env.action != 'print')
      this._int = setInterval(function(){ ref.send_keep_alive(); }, this.env.keep_alive * 1000);
  };
  // sends keep-alive signal to the server
  this.send_keep_alive = function()
  {
    var d = new Date();
    this.http_request('keep-alive', '_t='+d.getTime());
      this._int = setInterval(function(){ ref.http_request('keep-alive'); }, this.env.keep_alive * 1000);
  };
  // sends request to check for recent messages
@@ -5160,7 +5445,7 @@
    if (this.busy)
      return;
    var lock, addurl = '_t=' + (new Date().getTime()) + '&_mbox=' + urlencode(this.env.mailbox);
    var lock, addurl = '_mbox=' + urlencode(this.env.mailbox);
    if (refresh) {
      lock = this.set_busy(true, 'checkingmail');
@@ -5200,7 +5485,7 @@
  // gets cursor position
  this.get_caret_pos = function(obj)
  {
    if (typeof(obj.selectionEnd)!='undefined')
    if (obj.selectionEnd !== undefined)
      return obj.selectionEnd;
    else if (document.selection && document.selection.createRange) {
      var range = document.selection.createRange();
@@ -5263,6 +5548,17 @@
}  // end object rcube_webmail
// some static methods
rcube_webmail.long_subject_title = function(elem, indent)
{
  if (!elem.title) {
    var $elem = $(elem);
    if ($elem.width() + indent * 15 > $elem.parent().width())
      elem.title = $elem.html();
  }
};
// copy event engine prototype
rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
rcube_webmail.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;