From 4764079c6af092a6dfa18306601e1b33482fb756 Mon Sep 17 00:00:00 2001
From: alecpl <alec@alec.pl>
Date: Thu, 05 May 2011 03:26:50 -0400
Subject: [PATCH] - Fix bug where messages were deleted instead moved to trash folder after Shift key was used (#1487902), small code improvements

---
 program/js/app.js | 1135 +++++++++++++++++++++++++++++++++++------------------------
 1 files changed, 676 insertions(+), 459 deletions(-)

diff --git a/program/js/app.js b/program/js/app.js
index b57c447..99446d6 100644
--- a/program/js/app.js
+++ b/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()
 {
@@ -37,7 +36,7 @@
 
   // webmail client settings
   this.dblclick_time = 500;
-  this.message_time = 3000;
+  this.message_time = 2000;
 
   this.identifier_expr = new RegExp('[^0-9a-z\-_]', 'gi');
 
@@ -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
@@ -136,6 +135,14 @@
       return;
     }
 
+    // Enable debug console
+    if (!window.console || !window.console.log) {
+      window.console = new rcube_console();
+    }
+    else {
+      $('#console').hide();
+    }
+
     // find all registered gui containers
     for (var n in this.gui_containers)
       this.gui_containers[n] = $('#'+this.gui_containers[n]);
@@ -148,7 +155,7 @@
     this.init_buttons();
 
     // tell parent window that this frame is loaded
-    if (this.env.framed && parent.rcmail && parent.rcmail.set_busy) {
+    if (this.is_framed()) {
       parent.rcmail.set_busy(false, null, parent.rcmail.env.frame_lock);
       parent.rcmail.env.frame_lock = null;
     }
@@ -222,13 +229,13 @@
           }
 
           // make preview/message frame visible
-          if (this.env.action == 'preview' && this.env.framed && parent.rcmail) {
+          if (this.env.action == 'preview' && this.is_framed()) {
             this.enable_command('compose', 'add-contact', false);
             parent.rcmail.show_contentframe(true);
           }
         }
         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,15 +343,22 @@
       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')
-          this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', 'delete-folder', 'enable-threading', 'disable-threading', true);
+        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) {
+          this.enable_command('save', 'folder-size', true);
+          parent.rcmail.env.messagecount = this.env.messagecount;
+          parent.rcmail.enable_command('purge', this.env.messagecount);
+          $("input[type='text']").first().select();
+        }
 
         if (this.gui_objects.identitieslist) {
           this.identity_list = new rcube_list_widget(this.gui_objects.identitieslist, {multiselect:false, draggable:false, keyboard:false});
@@ -378,10 +392,11 @@
         // detect client timezone
         $('#rcmlogintz').val(new Date().getTimezoneOffset() / -60);
 
-        // display 'loading' message on form submit
+        // display 'loading' message on form submit, lock submit button
         $('form').submit(function () {
-          rcmail.display_message(rcmail.get_label('loading'), 'loading');
-        }); 
+          $('input[type=submit]', this).attr('disabled', true);
+          rcmail.display_message('', 'loading');
+        });
 
         this.enable_command('login', true);
         break;
@@ -407,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]();
       }
 
@@ -434,7 +449,7 @@
     // command not supported or allowed
     if (!this.commands[command]) {
       // pass command to parent window
-      if (this.env.framed && parent.rcmail && parent.rcmail.command)
+      if (this.is_framed())
         parent.rcmail.command(command, props);
 
       return false;
@@ -447,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
@@ -626,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();
@@ -639,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;
 
@@ -790,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;
@@ -802,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);
@@ -811,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;
@@ -988,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;
     }
@@ -1119,12 +1132,12 @@
 
   this.reload = function(delay)
   {
-    if (this.env.framed && parent.rcmail)
+    if (this.is_framed())
       parent.rcmail.reload(delay);
     else if (delay)
       window.setTimeout(function(){ rcmail.reload(); }, delay);
     else if (window.location)
-      location.href = this.env.comm_path;
+      location.href = this.env.comm_path + (this.env.action ? '&_action='+this.env.action : '');
   };
 
   // Add variable to GET string, replace old value if exists
@@ -1147,6 +1160,12 @@
     else
       return url + '?' + name + '=' + value;
   };
+
+  this.is_framed = function()
+  {
+    return (this.env.framed && parent.rcmail && parent.rcmail != this && parent.rcmail.command);
+  };
+
 
   /*********************************************************/
   /*********        event handling methods         *********/
@@ -1345,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) {
@@ -1381,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 = {};
     }
@@ -1480,8 +1497,6 @@
       this.command('previouspage');
     else if (list.key_pressed == 34)
       this.command('nextpage');
-    else
-      list.shiftkey = false;
   };
 
   this.msglist_get_preview = function()
@@ -1527,7 +1542,7 @@
     if (this.task == 'mail')
       allow = (this.env.mailboxes[id] && this.env.mailboxes[id].id != this.env.mailbox && !this.env.mailboxes[id].virtual);
     else if (this.task == 'settings')
-      allow = (id != this.env.folder);
+      allow = (id != this.env.mailbox);
     else if (this.task == 'addressbook') {
       if (id != this.env.source && this.env.contactfolders[id]) {
         if (this.env.contactfolders[id].type == 'group') {
@@ -1668,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;
         }
       }
@@ -1687,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
@@ -1756,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;
@@ -1822,9 +1837,9 @@
       this.show_contentframe(true);
     else {
       if (!this.env.frame_lock) {
-        (parent.rcmail ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
+        (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) {
@@ -1903,7 +1918,7 @@
     var url = '', target = window;
 
     if (!mbox)
-      mbox = this.env.mailbox;
+      mbox = this.env.mailbox ? this.env.mailbox : 'INBOX';
 
     if (add_url)
       url += add_url;
@@ -1946,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);
     }
   };
 
@@ -2352,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
@@ -2372,22 +2387,24 @@
       }
     }
 
+    add_url += '&_uid='+this.uids_to_list(a_uids);
+
     // send request to server
-    this.http_post('copy', '_uid='+a_uids.join(',')+'&_mbox='+urlencode(this.env.mailbox)+add_url, lock);
+    this.http_post('copy', '_mbox='+urlencode(this.env.mailbox)+add_url, lock);
   };
 
   // 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
     if (!mbox || mbox == this.env.mailbox || (!this.env.uid && (!this.message_list || !this.message_list.get_selection().length)))
       return;
 
-    var lock = false;
-    var add_url = '&_target_mbox='+urlencode(mbox)+'&_from='+(this.env.action ? this.env.action : '');
+    var lock = false,
+      add_url = '&_target_mbox='+urlencode(mbox)+'&_from='+(this.env.action ? this.env.action : '');
 
     // show wait message
     if (this.env.action == 'show') {
@@ -2405,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
@@ -2424,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;
@@ -2728,24 +2748,23 @@
 
   this.expunge_mailbox = function(mbox)
   {
-    var lock = false;
-    var add_url = '';
+    var lock = false,
+      url = '_mbox='+urlencode(mbox);
 
     // lock interface if it's the active mailbox
     if (mbox == this.env.mailbox) {
        lock = this.set_busy(true, 'loading');
-       add_url = '&_reload=1';
+       url += '&_reload=1';
      }
 
     // send request to server
-    var url = '_mbox='+urlencode(mbox);
-    this.http_post('expunge', url+add_url, lock);
+    this.http_post('expunge', url, lock);
   };
 
   this.purge_mailbox = function(mbox)
   {
-    var lock = false;
-    var add_url = '';
+    var lock = false,
+      url = '_mbox='+urlencode(mbox);
 
     if (!confirm(this.get_label('purgefolderconfirm')))
       return false;
@@ -2753,13 +2772,11 @@
     // lock interface if it's the active mailbox
     if (mbox == this.env.mailbox) {
        lock = this.set_busy(true, 'loading');
-       add_url = '&_reload=1';
+       url += '&_reload=1';
      }
 
     // send request to server
-    var url = '_mbox='+urlencode(mbox);
-    this.http_post('purge', url+add_url, lock);
-    return true;
+    this.http_post('purge', url, lock);
   };
 
   // test if purge command is allowed
@@ -2806,7 +2823,7 @@
       input_subject = $("input[name='_subject']"),
       input_message = $("[name='_message']").get(0),
       html_mode = $("input[name='_is_html']").val() == '1',
-      ac_fields = ['cc', 'bcc', 'replyto', 'mailreplyto', 'mailfollowupto'];
+      ac_fields = ['cc', 'bcc', 'replyto', 'followupto'];
 
     // init live search events
     this.init_address_input_events(input_to);
@@ -2873,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;
       }
@@ -2987,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+':';
@@ -3002,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();
 
@@ -3154,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) {
@@ -3193,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/))
@@ -3205,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)
@@ -3231,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))) {
@@ -3259,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]);
   };
@@ -3267,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;
   };
@@ -3436,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;
@@ -3479,21 +3467,20 @@
       min = this.env.autocomplete_min_length;
 
     // trim query string
-    q = q.replace(/(^\s+|\s+$)/g, '');
+    q = $.trim(q);
 
     // Don't (re-)search if the last results are still active
     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);
     }
 
@@ -3541,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); };
@@ -3589,7 +3576,7 @@
 
     if (this.ksearch_pane)
       this.ksearch_pane.hide();
-  };
+   };
 
 
   /*********************************************************/
@@ -3664,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
@@ -3710,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;
   };
@@ -3764,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;
@@ -3777,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
@@ -3809,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))
@@ -3826,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()
@@ -3941,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));
@@ -3956,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-');
   };
 
 
@@ -3973,16 +4171,12 @@
   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 = 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) {
-      var anchors = row.obj.getElementsByTagName('a');
-      if (anchors[0])
-        anchors[0].onclick = function() { p.command('rename-folder', row.id); return false; };
-      if (anchors[1])
-        anchors[1].onclick = function() { p.command('delete-folder', row.id); return false; };
       row.obj.onmouseover = function() { p.focus_subscription(row.id); };
       row.obj.onmouseout = function() { p.unfocus_subscription(row.id); };
     };
@@ -4002,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;
@@ -4031,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;
@@ -4056,20 +4250,21 @@
   this.focus_subscription = function(id)
   {
     var row, folder,
-      reg = RegExp('['+RegExp.escape(this.env.delimiter)+']?[^'+RegExp.escape(this.env.delimiter)+']+$');
+      delim = RegExp.escape(this.env.delimiter),
+      reg = RegExp('['+delim+']?[^'+delim+']+$');
 
-    if (this.drag_active && this.env.folder && (row = document.getElementById(id)))
+    if (this.drag_active && this.env.mailbox && (row = document.getElementById(id)))
       if (this.env.subscriptionrows[id] &&
           (folder = this.env.subscriptionrows[id][0])) {
         if (this.check_droptarget(folder) &&
-            !this.env.subscriptionrows[this.get_folder_row_id(this.env.folder)][2] &&
-            (folder != this.env.folder.replace(reg, '')) &&
-            (!folder.match(new RegExp('^'+RegExp.escape(this.env.folder+this.env.delimiter))))) {
+            !this.env.subscriptionrows[this.get_folder_row_id(this.env.mailbox)][2] &&
+            (folder != this.env.mailbox.replace(reg, '')) &&
+            (!folder.match(new RegExp('^'+RegExp.escape(this.env.mailbox+this.env.delimiter))))) {
           this.set_env('dstfolder', folder);
           $(row).addClass('droptarget');
         }
       }
-      else if (this.env.folder.match(new RegExp(RegExp.escape(this.env.delimiter)))) {
+      else if (this.env.mailbox.match(new RegExp(delim))) {
         this.set_env('dstfolder', this.env.delimiter);
         $(this.subscription_list.frame).addClass('droptarget');
       }
@@ -4089,141 +4284,55 @@
   this.subscription_select = function(list)
   {
     var id, folder;
-    if ((id = list.get_single_selection()) &&
-        this.env.subscriptionrows['rcmrow'+id] &&
-        (folder = this.env.subscriptionrows['rcmrow'+id][0]))
-      this.set_env('folder', folder);
-    else
-      this.set_env('folder', null);
 
-    if (this.gui_objects.createfolderhint)
-      $(this.gui_objects.createfolderhint).html(this.env.folder ? this.get_label('addsubfolderhint') : '');
+    if (list && (id = list.get_single_selection()) &&
+        (folder = this.env.subscriptionrows['rcmrow'+id])
+    ) {
+      this.set_env('mailbox', folder[0]);
+      this.show_folder(folder[0]);
+      this.enable_command('delete-folder', !folder[2]);
+    }
+    else {
+      this.env.mailbox = null;
+      this.show_contentframe(false);
+      this.enable_command('delete-folder', 'purge', false);
+    }
   };
 
   this.subscription_move_folder = function(list)
   {
-    var reg = RegExp('['+RegExp.escape(this.env.delimiter)+']?[^'+RegExp.escape(this.env.delimiter)+']+$');
-    if (this.env.folder && this.env.dstfolder && (this.env.dstfolder != this.env.folder) &&
-        (this.env.dstfolder != this.env.folder.replace(reg, ''))) {
-      var reg = new RegExp('[^'+RegExp.escape(this.env.delimiter)+']*['+RegExp.escape(this.env.delimiter)+']', 'g');
-      var basename = this.env.folder.replace(reg, '');
-      var newname = this.env.dstfolder==this.env.delimiter ? basename : this.env.dstfolder+this.env.delimiter+basename;
+    var delim = RegExp.escape(this.env.delimiter),
+      reg = RegExp('['+delim+']?[^'+delim+']+$');
 
-      var lock = this.set_busy(true, 'foldermoving');
-      this.http_post('rename-folder', '_folder_oldname='+urlencode(this.env.folder)+'&_folder_newname='+urlencode(newname), lock);
+    if (this.env.mailbox && this.env.dstfolder && (this.env.dstfolder != this.env.mailbox) &&
+        (this.env.dstfolder != this.env.mailbox.replace(reg, ''))
+    ) {
+      reg = new RegExp('[^'+delim+']*['+delim+']', 'g');
+      var lock = this.set_busy(true, 'foldermoving'),
+        basename = this.env.mailbox.replace(reg, ''),
+        newname = this.env.dstfolder==this.env.delimiter ? basename : this.env.dstfolder+this.env.delimiter+basename;
+
+      this.http_post('rename-folder', '_folder_oldname='+urlencode(this.env.mailbox)+'&_folder_newname='+urlencode(newname), lock);
     }
     this.drag_active = false;
     this.unfocus_subscription(this.get_folder_row_id(this.env.dstfolder));
   };
 
   // tell server to create and subscribe a new mailbox
-  this.create_folder = function(name)
+  this.create_folder = function()
   {
-    if (this.edit_folder)
-      this.reset_folder_rename();
-
-    var form;
-    if ((form = this.gui_objects.editform) && form.elements['_folder_name']) {
-      name = form.elements['_folder_name'].value;
-
-      if (name.indexOf(this.env.delimiter)>=0) {
-        alert(this.get_label('forbiddencharacter')+' ('+this.env.delimiter+')');
-        return false;
-      }
-
-      if (this.env.folder && name != '')
-        name = this.env.folder+this.env.delimiter+name;
-
-      var lock = this.set_busy(true, 'foldercreating');
-      this.http_post('create-folder', '_name='+urlencode(name), lock);
-    }
-    else if (form.elements['_folder_name'])
-      form.elements['_folder_name'].focus();
-  };
-
-  // start renaming the mailbox name.
-  // this will replace the name string with an input field
-  this.rename_folder = function(id)
-  {
-    var temp, row, form;
-
-    // reset current renaming
-    if (temp = this.edit_folder) {
-      this.reset_folder_rename();
-      if (temp == id)
-        return;
-    }
-
-    if (id && this.env.subscriptionrows[id] && (row = document.getElementById(id))) {
-      var reg = new RegExp('.*['+RegExp.escape(this.env.delimiter)+']');
-      this.name_input = document.createElement('input');
-      this.name_input.type = 'text';
-      this.name_input.value = this.env.subscriptionrows[id][0].replace(reg, '');
-
-      reg = new RegExp('['+RegExp.escape(this.env.delimiter)+']?[^'+RegExp.escape(this.env.delimiter)+']+$');
-      this.name_input.__parent = this.env.subscriptionrows[id][0].replace(reg, '');
-      this.name_input.onkeydown = function(e){ rcmail.name_input_keydown(e); };
-
-      row.cells[0].replaceChild(this.name_input, row.cells[0].firstChild);
-      this.edit_folder = id;
-      this.name_input.select();
-
-      if (form = this.gui_objects.editform)
-        form.onsubmit = function(){ return false; };
-    }
-  };
-
-  // remove the input field and write the current mailbox name to the table cell
-  this.reset_folder_rename = function()
-  {
-    var cell = this.name_input ? this.name_input.parentNode : null;
-
-    if (cell && this.edit_folder && this.env.subscriptionrows[this.edit_folder])
-      $(cell).html(this.env.subscriptionrows[this.edit_folder][1]);
-
-    this.edit_folder = null;
-  };
-
-  // handler for keyboard events on the input field
-  this.name_input_keydown = function(e)
-  {
-    var key = rcube_event.get_keycode(e);
-
-    // enter
-    if (key==13) {
-      var newname = this.name_input ? this.name_input.value : null;
-      if (this.edit_folder && newname) {
-        if (newname.indexOf(this.env.delimiter)>=0) {
-          alert(this.get_label('forbiddencharacter')+' ('+this.env.delimiter+')');
-          return false;
-        }
-
-        if (this.name_input.__parent)
-          newname = this.name_input.__parent + this.env.delimiter + newname;
-
-        var lock = this.set_busy(true, 'folderrenaming');
-        this.http_post('rename-folder', '_folder_oldname='+urlencode(this.env.subscriptionrows[this.edit_folder][0])+'&_folder_newname='+urlencode(newname), lock);
-      }
-    }
-    // escape
-    else if (key==27)
-      this.reset_folder_rename();
+    this.show_folder('', this.env.mailbox);
   };
 
   // delete a specific mailbox with all its messages
-  this.delete_folder = function(id)
+  this.delete_folder = function(name)
   {
-    var folder = this.env.subscriptionrows[id][0];
-
-    if (this.edit_folder)
-      this.reset_folder_rename();
+    var id = this.get_folder_row_id(name ? name : this.env.mailbox),
+      folder = this.env.subscriptionrows[id][0];
 
     if (folder && confirm(this.get_label('deletefolderconfirm'))) {
       var lock = this.set_busy(true, 'folderdeleting');
-      this.http_post('delete-folder', '_mboxes='+urlencode(folder), lock);
-      this.set_env('folder', null);
-
-      $(this.gui_objects.createfolderhint).html('');
+      this.http_post('delete-folder', '_mbox='+urlencode(folder), lock);
     }
   };
 
@@ -4280,18 +4389,8 @@
       // set messages count to zero
       row.cells[1].innerHTML = '*';
 
-      // update subscription/threading checkboxes
+      // update subscription checkbox
       $('input[name="_subscribed[]"]', row).val(name).attr('checked', true);
-      $('input[name="_threaded[]"]', row).val(name).attr('checked', false);
-
-      var elem;
-      // add new folder to rename-folder list and clear input field
-      if (form = this.gui_objects.editform) {
-        if (elem = form.elements['_folder_oldname'])
-          elem.options[elem.options.length] = new Option(name, name);
-        if (elem = form.elements['_folder_name'])
-          elem.value = ''; 
-      }
     }
 
     this.init_subscription_list();
@@ -4305,72 +4404,37 @@
   // replace an existing table row with a new folder line
   this.replace_folder_row = function(oldfolder, newfolder, display_name, before)
   {
-    var form, elm,
-      id = this.get_folder_row_id(oldfolder),
+    var id = this.get_folder_row_id(oldfolder),
       row = document.getElementById(id);
 
     // replace an existing table row (if found)
     this.add_folder_row(newfolder, display_name, row, before);
-
-    // rename folder in rename-folder dropdown
-    if ((form = this.gui_objects.editform) && (elm = form.elements['_folder_oldname'])) {
-      for (var i=0; i<elm.options.length; i++) {
-        if (elm.options[i].value == oldfolder) {
-          elm.options[i].text = display_name;
-          elm.options[i].value = newfolder;
-          break;
-        }
-      }
-
-      form.elements['_folder_newname'].value = '';
-    }
   };
 
   // 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)
   {
-    var form, elm, row, id = this.get_folder_row_id(folder);
+    var row, id = this.get_folder_row_id(folder);
 
     if (id && (row = document.getElementById(id)))
       row.style.display = 'none';
-
-    // remove folder from rename-folder list
-    if ((form = this.gui_objects.editform) && (elm = form.elements['_folder_oldname'])) {
-      for (var i=0; i<elm.options.length; i++) {
-        if (elm.options[i].value == folder) {
-          elm.options[i] = null;
-          break;
-        }
-      }
-    }
-
-    if (form && (elm = form.elements['_folder_newname']))
-      elm.value = '';
   };
 
   this.subscribe = function(folder)
   {
-    if (folder)
-      this.http_post('subscribe', '_mbox='+urlencode(folder));
+    if (folder) {
+      var lock = this.display_message(this.get_label('foldersubscribing'), 'loading');
+      this.http_post('subscribe', '_mbox='+urlencode(folder), lock);
+    }
   };
 
   this.unsubscribe = function(folder)
   {
-    if (folder)
-      this.http_post('unsubscribe', '_mbox='+urlencode(folder));
-  };
-
-  this.enable_threading = function(folder)
-  {
-    if (folder)
-      this.http_post('enable-threading', '_mbox='+urlencode(folder));
-  };
-
-  this.disable_threading = function(folder)
-  {
-    if (folder)
-      this.http_post('disable-threading', '_mbox='+urlencode(folder));
+    if (folder) {
+      var lock = this.display_message(this.get_label('folderunsubscribing'), 'loading');
+      this.http_post('unsubscribe', '_mbox='+urlencode(folder), lock);
+    }
   };
 
   // helper method to find a specific mailbox row ID
@@ -4389,7 +4453,7 @@
     var cell, td,
       new_row = document.createElement('tr');
 
-    for(var n=0; n<row.cells.length; n++) {
+    for (var n=0; n<row.cells.length; n++) {
       cell = row.cells[n];
       td = document.createElement('td');
 
@@ -4403,6 +4467,50 @@
     }
 
     return new_row;
+  };
+
+  // when user select a folder in manager
+  this.show_folder = function(folder, path, force)
+  {
+    var target = window,
+      url = '&_action=edit-folder&_mbox='+urlencode(folder);
+
+    if (path)
+      url += '&_path='+urlencode(path);
+
+    if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
+      target = window.frames[this.env.contentframe];
+      url += '&_framed=1';
+    }
+
+    if (String(target.location.href).indexOf(url) >= 0 && !force) {
+      this.show_contentframe(true);
+    }
+    else {
+      if (!this.env.frame_lock) {
+        (parent.rcmail ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
+      }
+      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)
+  {
+    var lock = this.set_busy(true, 'loading');
+    this.http_post('folder-size', '_mbox='+urlencode(folder), lock);
+  };
+
+  this.folder_size_update = function(size)
+  {
+    $('#folder-size').replaceWith(size);
   };
 
 
@@ -4421,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++) {
@@ -4482,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];
       }
@@ -4582,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)
   {
@@ -4593,7 +4718,7 @@
   this.display_message = function(msg, type)
   {
     // pass command to parent window
-    if (this.env.framed && parent.rcmail)
+    if (this.is_framed())
       return parent.rcmail.display_message(msg, type);
 
     if (!this.gui_objects.message) {
@@ -4605,58 +4730,89 @@
 
     type = type ? type : 'notice';
 
-    var date = new Date(),
-      id = type + date.getTime();
-
+    var ref = this,
+      key = msg,
+      date = new Date(),
+      id = type + date.getTime(),
+      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 of type 'loading' is already displayed
-      if (this.messages[msg]) {
-        this.messages[msg].elements.push(id);
-        return id;
-      }
     }
 
-    var ref = this,
-      obj = $('<div>').addClass(type).html(msg),
-      cont = $(this.gui_objects.message).show();
-
-    if (type == 'loading') {
-      obj.appendTo(cont);
-      this.messages[msg] = {'obj': obj, 'elements': [id]};
-      window.setTimeout(function() { rcmail.hide_message(id); }, this.env.request_timeout * 1000);
+    // The same message is already displayed
+    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;
     }
-    else {
-      obj.appendTo(cont).bind('mousedown', function(){ return ref.hide_message(obj); });
-      window.setTimeout(function(){ ref.hide_message(obj, true); }, this.message_time);
-      return obj;
+
+    // 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') {
+      this.messages[key].labels = [{'id': id, 'msg': msg}];
     }
+    else {
+      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
   this.hide_message = function(obj, fade)
   {
     // pass command to parent window
-    if (this.env.framed && parent.rcmail)
+    if (this.is_framed())
       return parent.rcmail.hide_message(obj, fade);
 
-    if (typeof(obj) == 'object') {
-      // custom message
+    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']();
+      msg = $(obj).data('key');
+      if (this.messages[msg])
+        delete this.messages[msg];
     }
+    // Hide message by id
     else {
-      // 'loading' message
-      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
             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);
+              }
             }
           }
         }
@@ -4671,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');
@@ -4700,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;
@@ -4720,6 +4877,7 @@
         }
         th.appendChild(tr);
         thead.parentNode.replaceChild(th, thead);
+        thead = th;
       }
 
       for (n=0, len=this.env.coltypes.length; n<len; n++) {
@@ -4743,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
@@ -4774,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);
@@ -4794,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;
@@ -4810,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) + ']+$');
@@ -4919,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;
@@ -4958,7 +5119,7 @@
       bar1.addClass('quota_mid');
     }
     else {
-      main.addClass(' quota_text_normal');
+      main.addClass(' quota_text_low');
       bar1.addClass('quota_low');
     }
 
@@ -4997,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;
@@ -5045,7 +5233,7 @@
         query = result;
     }
 
-    url += '&_remote=1&_action=' + action + (query ? '&' : '') + query;
+    url += '&_remote=1';
 
     // send request
     console.log('HTTP GET: ' + url);
@@ -5059,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);
     }
@@ -5078,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;
@@ -5112,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]);
     }
 
@@ -5210,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()
   {
@@ -5223,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
@@ -5239,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');
@@ -5279,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();
@@ -5342,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;

--
Gitblit v1.9.1