Thomas Bruederli
2015-07-31 2965a981b7ec22866fbdf2d567d87e2d068d3617
program/js/app.js
@@ -326,10 +326,7 @@
          this.enable_command('download', 'print', true);
        // show printing dialog
        else if (this.env.action == 'print' && this.env.uid) {
          if (bw.safari)
            setTimeout('window.print()', 10);
          else
            window.print();
          this.print_dialog();
        }
        // get unread count for each mailbox
@@ -378,6 +375,8 @@
          }
          this.http_post(postact, postdata);
        }
        this.check_mailvelope(this.env.action);
        // detect browser capabilities
        if (!this.is_framed() && !this.env.extwin)
@@ -439,6 +438,9 @@
          this.enable_command('save', true);
          if (this.env.action == 'add' || this.env.action == 'edit' || this.env.action == 'search')
              this.init_contact_form();
        }
        else if (this.env.action == 'print') {
          this.print_dialog();
        }
        break;
@@ -574,6 +576,7 @@
      this.treelist
        .addEventListener('collapse', function(node) { ref.folder_collapsed(node) })
        .addEventListener('expand', function(node) { ref.folder_collapsed(node) })
        .addEventListener('beforeselect', function(node) { return !ref.busy; })
        .addEventListener('select', function(node) { ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }) });
    }
@@ -653,7 +656,7 @@
    // check input before leaving compose step
    if (this.task == 'mail' && this.env.action == 'compose' && $.inArray(command, this.env.compose_commands) < 0 && !this.env.server_error) {
      if (this.cmp_hash != this.compose_field_hash() && !confirm(this.get_label('notsentwarning')))
      if (!this.env.is_sent && this.cmp_hash != this.compose_field_hash() && !confirm(this.get_label('notsentwarning')))
        return false;
      // remove copy from local storage if compose screen is left intentionally
@@ -773,7 +776,7 @@
      case 'list':
        if (props && props != '') {
          this.reset_qsearch();
          this.reset_qsearch(true);
        }
        if (this.env.action == 'compose' && this.env.extwin) {
          window.close();
@@ -893,7 +896,7 @@
          else {
            // reload form
            if (props == 'reload') {
              form.action += '?_reload=1';
              form.action += '&_reload=1';
            }
            else if (this.task == 'settings' && (this.env.identities_level % 2) == 0  &&
              (input = $("input[name='_email']", form)) && input.length && !rcube_check_email(input.val())
@@ -1054,12 +1057,9 @@
        url = {};
        if (this.task == 'mail') {
          url._mbox = this.env.mailbox;
          url = {_mbox: this.env.mailbox, _search: this.env.search_request};
          if (props)
            url._to = props;
          // also send search request so we can go back to search result after message is sent
          if (this.env.search_request)
            url._search = this.env.search_request;
        }
        // modify url if we're in addressbook
        else if (this.task == 'addressbook') {
@@ -1117,7 +1117,7 @@
        break;
      case 'send':
        if (!props.nocheck && !this.check_compose_input(command))
        if (!props.nocheck && !this.env.is_sent && !this.check_compose_input(command))
          break;
        // Reset the auto-save timer
@@ -1154,7 +1154,7 @@
      case 'reply-list':
      case 'reply':
        if (uid = this.get_single_uid()) {
          url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid)};
          url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid), _search: this.env.search_request};
          if (command == 'reply-all')
            // do reply-list, when list is detected and popup menu wasn't used
            url._all = (!props && this.env.reply_all_mode == 1 && this.commands['reply-list'] ? 'list' : 'all');
@@ -1178,7 +1178,15 @@
        break;
      case 'print':
        if (this.env.action == 'get') {
        if (this.task == 'addressbook') {
          if (uid = this.contact_list.get_single_selection()) {
            url = '&_action=print&_cid=' + uid;
            if (this.env.source)
              url += '&_source=' + urlencode(this.env.source);
            this.open_window(this.env.comm_path + url, true, true);
          }
        }
        else if (this.env.action == 'get') {
          this.gui_objects.messagepartframe.contentWindow.print();
        }
        else if (uid = this.get_single_uid()) {
@@ -1217,7 +1225,7 @@
      case 'reset-search':
        var n, s = this.env.search_request || this.env.qsearch;
        this.reset_qsearch();
        this.reset_qsearch(true);
        this.select_all_mode = false;
        if (s && this.env.action == 'compose') {
@@ -1434,7 +1442,7 @@
    else if (delay)
      setTimeout(function() { ref.reload(); }, delay);
    else if (window.location)
      location.href = this.env.comm_path + (this.env.action ? '&_action='+this.env.action : '');
      location.href = this.url('', {_extwin: this.env.extwin});
  };
  // Add variable to GET string, replace old value if exists
@@ -1601,7 +1609,8 @@
  this.folder_collapsed = function(node)
  {
    var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders';
    var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders',
      old = this.env[prefname];
    if (node.collapsed) {
      this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&';
@@ -1617,7 +1626,8 @@
    }
    if (!this.drag_active) {
      this.command('save-pref', { name: prefname, value: this.env[prefname] });
      if (old !== this.env[prefname])
        this.command('save-pref', { name: prefname, value: this.env[prefname] });
      if (this.env.unread_counts)
        this.set_unread_count_display(node.id, false);
@@ -1955,7 +1965,7 @@
    // attach events
    $.each(fn, function(i, f) {
      row[i].onclick = function(e) { f(e); return rcube_event.cancel(e); };
      if (bw.touch) {
      if (bw.touch && row[i].addEventListener) {
        row[i].addEventListener('touchend', function(e) {
          if (e.changedTouches.length == 1) {
            f(e);
@@ -2259,7 +2269,7 @@
      if (preview && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read > 0) {
        this.preview_read_timer = setTimeout(function() {
          ref.set_unread_message(id, ref.env.mailbox);
          ref.http_post('mark', {_uid: id, _flag: 'read', _quiet: 1});
          ref.http_post('mark', {_uid: id, _flag: 'read', _mbox: ref.env.mailbox, _quiet: 1});
        }, this.env.preview_pane_mark_read * 1000);
      }
    }
@@ -2369,6 +2379,9 @@
  // list messages of a specific mailbox using filter
  this.filter_mailbox = function(filter)
  {
    if (this.filter_disabled)
      return;
    var lock = this.set_busy(true, 'searching');
    this.clear_message_list();
@@ -2402,16 +2415,17 @@
    if (sort)
      url._sort = sort;
    // also send search request to get the right messages
    if (this.env.search_request)
      url._search = this.env.search_request;
    // set page=1 if changeing to another mailbox
    // folder change, reset page, search scope, etc.
    if (this.env.mailbox != mbox) {
      page = 1;
      this.env.current_page = page;
      this.env.search_scope = 'base';
      this.select_all_mode = false;
      this.reset_search_filter();
    }
    // also send search request to get the right messages
    else if (this.env.search_request)
      url._search = this.env.search_request;
    if (!update_only) {
      // unselect selected messages and clear the list and message data
@@ -2485,6 +2499,16 @@
        selection.push(selected[i]);
    this.message_list.selection = selection;
    // reset preview frame, if currently previewed message is not selected (has been removed)
    try {
      var win = this.get_frame_window(this.env.contentframe),
        id = win.rcmail.env.uid;
      if (id && $.inArray(id, selection) < 0)
        this.show_contentframe(false);
    }
    catch (e) {};
  };
  // expand all threads with unread children
@@ -3233,6 +3257,530 @@
    this.set_alttext('delete', label);
  };
  // Initialize input element for list page jump
  this.init_pagejumper = function(element)
  {
    $(element).addClass('rcpagejumper')
      .on('focus', function(e) {
        // create and display popup with page selection
        var i, html = '';
        for (i = 1; i <= ref.env.pagecount; i++)
          html += '<li>' + i + '</li>';
        html = '<ul class="toolbarmenu">' + html + '</ul>';
        if (!ref.pagejump) {
          ref.pagejump = $('<div id="pagejump-selector" class="popupmenu"></div>')
            .appendTo(document.body)
            .on('click', 'li', function() {
              if (!ref.busy)
                $(element).val($(this).text()).change();
            });
        }
        if (ref.pagejump.data('count') != i)
          ref.pagejump.html(html);
        ref.pagejump.attr('rel', '#' + this.id).data('count', i);
        // display page selector
        ref.show_menu('pagejump-selector', true, e);
        $(this).keydown();
      })
      // keyboard navigation
      .on('keydown keyup click', function(e) {
        var current, selector = $('#pagejump-selector'),
          ul = $('ul', selector),
          list = $('li', ul),
          height = ul.height(),
          p = parseInt(this.value);
        if (e.which != 27 && e.which != 9 && e.which != 13 && !selector.is(':visible'))
          return ref.show_menu('pagejump-selector', true, e);
        if (e.type == 'keydown') {
          // arrow-down
          if (e.which == 40) {
            if (list.length > p)
              this.value = (p += 1);
          }
          // arrow-up
          else if (e.which == 38) {
            if (p > 1 && list.length > p - 1)
              this.value = (p -= 1);
          }
          // enter
          else if (e.which == 13) {
            return $(this).change();
          }
          // esc, tab
          else if (e.which == 27 || e.which == 9) {
            return $(element).val(ref.env.current_page);
          }
        }
        $('li.selected', ul).removeClass('selected');
        if ((current = $(list[p - 1])).length) {
          current.addClass('selected');
          $('#pagejump-selector').scrollTop(((ul.height() / list.length) * (p - 1)) - selector.height() / 2);
        }
      })
      .on('change', function(e) {
        // go to specified page
        var p = parseInt(this.value);
        if (p && p != ref.env.current_page && !ref.busy) {
          ref.hide_menu('pagejump-selector');
          ref.list_page(p);
        }
      });
  };
  // Update page-jumper state on list updates
  this.update_pagejumper = function()
  {
    $('input.rcpagejumper').val(this.env.current_page).prop('disabled', this.env.pagecount < 2);
  };
  // check for mailvelope API
  this.check_mailvelope = function(action)
  {
    if (typeof window.mailvelope !== 'undefined') {
      this.mailvelope_load(action);
    }
    else {
      $(window).on('mailvelope', function() {
        ref.mailvelope_load(action);
      });
    }
  };
  //
  this.mailvelope_load = function(action)
  {
    if (this.env.browser_capabilities)
      this.env.browser_capabilities['pgpmime'] = 1;
    var keyring = this.get_local_storage_prefix();
    mailvelope.getKeyring(keyring).then(function(kr) {
      ref.mailvelope_keyring = kr;
      ref.mailvelope_init(action, kr);
    }).catch(function(err) {
      // attempt to create a new keyring for this app/user
      mailvelope.createKeyring(keyring).then(function(kr) {
        ref.mailvelope_keyring = kr;
        ref.mailvelope_init(action, kr);
      }).catch(function(err) {
        console.error(err);
      });
    });
  };
  //
  this.mailvelope_init = function(action, keyring)
  {
    if (action == 'show' || action == 'preview') {
      // decrypt text body
      if (this.env.is_pgp_content && window.mailvelope) {
        var data = $(this.env.is_pgp_content).text();
        ref.mailvelope_display_container(this.env.is_pgp_content, data, keyring);
      }
      // load pgp/mime message and pass it to the mailvelope display container
      else if (this.env.pgp_mime_part && window.mailvelope) {
        var msgid = this.display_message(this.get_label('loadingdata'), 'loading'),
          selector = this.env.pgp_mime_container;
        $.ajax({
          type: 'GET',
          url: this.url('get', { '_mbox': this.env.mailbox, '_uid': this.env.uid, '_part': this.env.pgp_mime_part }),
          error: function(o, status, err) {
            ref.http_error(o, status, err, msgid);
          },
          success: function(data) {
            ref.mailvelope_display_container(selector, data, keyring, msgid);
          }
        });
      }
    }
    else if (action == 'compose' && window.mailvelope) {
      this.env.compose_commands.push('compose-encrypted');
      if (this.env.pgp_mime_message) {
        // fetch PGP/Mime part and open load into Mailvelope editor
        var lock = this.set_busy(true, this.get_label('loadingdata'));
        $.ajax({
          type: 'GET',
          url: this.url('get', this.env.pgp_mime_message),
          error: function(o, status, err) {
            ref.http_error(o, status, err, lock);
            ref.enable_command('compose-encrypted', true);
          },
          success: function(data) {
            ref.set_busy(false, null, lock);
            ref.compose_encrypted({ quotedMail: data });
            ref.enable_command('compose-encrypted', true);
          }
        });
      }
      else {
        // enable encrypted compose toggle
        this.enable_command('compose-encrypted', true);
      }
    }
  };
  // handler for the 'compose-encrypted' command
  this.compose_encrypted = function(props)
  {
    var container = $('#' + this.env.composebody).parent();
    // remove Mailvelope editor if active
    if (ref.mailvelope_editor) {
      ref.mailvelope_editor = null;
      ref.compose_skip_unsavedcheck = false;
      ref.set_button('compose-encrypted', 'act');
      container.removeClass('mailvelope')
        .find('iframe:not([aria-hidden=true])').remove();
      $('#' + ref.env.composebody).show();
      $("[name='_pgpmime']").remove();
    }
    // embed Mailvelope editor container
    else {
      var options = { predefinedText: $('#' + this.env.composebody).val() };
      if (props.quotedMail) {
        options = { quotedMail: props.quotedMail, quotedMailIndent: false };
      }
      if (this.env.compose_mode == 'reply') {
        options.quotedMailIndent = true;
        options.quotedMailHeader = this.env.compose_reply_header;
      }
      mailvelope.createEditorContainer('#' + container.attr('id'), ref.mailvelope_keyring, options).then(function(editor) {
        ref.mailvelope_editor = editor;
        ref.compose_skip_unsavedcheck = true;
        ref.set_button('compose-encrypted', 'sel');
        container.addClass('mailvelope');
        $('#' + ref.env.composebody).hide();
        // notify user about loosing attachments
        if (ref.env.attachments && !$.isEmptyObject(ref.env.attachments)) {
          alert(ref.get_label('encryptnoattachments'));
          $.each(ref.env.attachments, function(name, attach) {
            ref.remove_from_attachment_list(name);
          });
        }
      }).catch(function(err) {
        console.error(err);
      });
    }
  };
  // callback to replace the message body with the full armored
  this.mailvelope_submit_messageform = function(draft, saveonly)
  {
    // get recipients
    var recipients = [];
    $.each(['to', 'cc', 'bcc'], function(i,field) {
      var pos, rcpt, val = $.trim($('[name="_' + field + '"]').val());
      while (val.length && rcube_check_email(val, true)) {
        rcpt = RegExp.$2;
        recipients.push(rcpt);
        val = val.substr(val.indexOf(rcpt) + rcpt.length + 1).replace(/^\s*,\s*/, '');
      }
    });
    // check if we have keys for all recipients
    var isvalid = recipients.length > 0;
    ref.mailvelope_keyring.validKeyForAddress(recipients).then(function(status) {
      var missing_keys = [];
      $.each(status, function(k,v) {
        if (v === false) {
          isvalid = false;
          missing_keys.push(k);
        }
      });
      // list recipients with missing keys
      if (!isvalid && missing_keys.length) {
        // load publickey.js
        if (!$('script#publickeyjs').length) {
          $('<script>')
            .attr('id', 'publickeyjs')
            .attr('src', ref.assets_path('program/js/publickey.js'))
            .appendTo(document.body);
        }
        // display dialog with missing keys
        ref.show_popup_dialog(
          ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')) +
          '<p>' + ref.get_label('searchpubkeyservers') + '</p>',
          ref.get_label('encryptedsendialog'),
          [{
            text: ref.get_label('search'),
            'class': 'mainaction',
            click: function() {
              var $dialog = $(this);
              ref.mailvelope_search_pubkeys(missing_keys, function() {
                $dialog.dialog('close')
              });
            }
          },
          {
            text: ref.get_label('cancel'),
            click: function(){
              $(this).dialog('close');
            }
          }]
        );
        return false;
      }
      if (!isvalid) {
        if (!recipients.length) {
          alert(ref.get_label('norecipientwarning'));
          $("[name='_to']").focus();
        }
        return false;
      }
      // add sender identity to recipients to be able to decrypt our very own message
      var senders = [], selected_sender = ref.env.identities[$("[name='_from'] option:selected").val()];
      $.each(ref.env.identities, function(k, sender) {
        senders.push(sender.email);
      });
      ref.mailvelope_keyring.validKeyForAddress(senders).then(function(status) {
        valid_sender = null;
        $.each(status, function(k,v) {
          if (v !== false) {
            valid_sender = k;
            if (valid_sender == selected_sender) {
              return false;  // break
            }
          }
        });
        if (!valid_sender) {
          if (!confirm(ref.get_label('nopubkeyforsender'))) {
            return false;
          }
        }
        recipients.push(valid_sender);
        ref.mailvelope_editor.encrypt(recipients).then(function(armored) {
          // all checks passed, send message
          var form = ref.gui_objects.messageform,
            hidden = $("[name='_pgpmime']", form),
            msgid = ref.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage')
          form.target = 'savetarget';
          form._draft.value = draft ? '1' : '';
          form.action = ref.add_url(form.action, '_unlock', msgid);
          form.action = ref.add_url(form.action, '_framed', 1);
          if (saveonly) {
            form.action = ref.add_url(form.action, '_saveonly', 1);
          }
          // send pgp conent via hidden field
          if (!hidden.length) {
            hidden = $('<input type="hidden" name="_pgpmime">').appendTo(form);
          }
          hidden.val(armored);
          form.submit();
        }).catch(function(err) {
          console.log(err);
        });  // mailvelope_editor.encrypt()
      }).catch(function(err) {
        console.error(err);
      });  // mailvelope_keyring.validKeyForAddress(senders)
    }).catch(function(err) {
      console.error(err);
    });  // mailvelope_keyring.validKeyForAddress(recipients)
    return false;
  };
  // wrapper for the mailvelope.createDisplayContainer API call
  this.mailvelope_display_container = function(selector, data, keyring, msgid)
  {
    mailvelope.createDisplayContainer(selector, data, keyring, { showExternalContent: this.env.safemode }).then(function() {
      $(selector).addClass('mailvelope').find('.message-part, .part-notice').hide();
      ref.hide_message(msgid);
      setTimeout(function() { $(window).resize(); }, 10);
    }).catch(function(err) {
      console.error(err);
      ref.hide_message(msgid);
      ref.display_message('Message decryption failed: ' + err.message, 'error')
    });
  };
  // subroutine to query keyservers for public keys
  this.mailvelope_search_pubkeys = function(emails, resolve)
  {
    // query with publickey.js
    var deferreds = [],
      pk = new PublicKey(),
      lock = ref.display_message(ref.get_label('loading'), 'loading');
    $.each(emails, function(i, email) {
      var d = $.Deferred();
      pk.search(email, function(results, errorCode) {
        if (errorCode !== null) {
          // rejecting would make all fail
          // d.reject(email);
          d.resolve([email]);
        }
        else {
          d.resolve([email].concat(results));
        }
      });
      deferreds.push(d);
    });
    $.when.apply($, deferreds).then(function() {
      var missing_keys = [],
        key_selection = [];
      // alanyze results of all queries
      $.each(arguments, function(i, result) {
        var email = result.shift();
        if (!result.length) {
          missing_keys.push(email);
        }
        else {
          key_selection = key_selection.concat(result);
        }
      });
      ref.hide_message(lock);
      resolve(true);
      // show key import dialog
      if (key_selection.length) {
        ref.mailvelope_key_import_dialog(key_selection);
      }
      // some keys could not be found
      if (missing_keys.length) {
        ref.display_message(ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')), 'warning');
      }
    }, function() {
      console.error('Pubkey lookup failed with', arguments);
      ref.hide_message(lock);
      ref.display_message('pubkeysearcherror', 'error');
      resolve(false);
    });
  };
  // list the given public keys in a dialog with options to import
  // them into the local Maivelope keyring
  this.mailvelope_key_import_dialog = function(candidates)
  {
    var ul = $('<div>').addClass('listing mailvelopekeyimport');
    $.each(candidates, function(i, keyrec) {
      var li = $('<div>').addClass('key');
      if (keyrec.revoked)  li.addClass('revoked');
      if (keyrec.disabled) li.addClass('disabled');
      if (keyrec.expired)  li.addClass('expired');
      li.append($('<label>').addClass('keyid').text(ref.get_label('keyid')));
      li.append($('<a>').text(keyrec.keyid.substr(-8).toUpperCase())
        .attr('href', keyrec.info)
        .attr('target', '_blank')
        .attr('tabindex', '-1'));
      li.append($('<label>').addClass('keylen').text(ref.get_label('keylength')));
      li.append($('<span>').text(keyrec.keylen));
      if (keyrec.expirationdate) {
        li.append($('<label>').addClass('keyexpired').text(ref.get_label('keyexpired')));
        li.append($('<span>').text(new Date(keyrec.expirationdate * 1000).toDateString()));
      }
      if (keyrec.revoked) {
        li.append($('<span>').addClass('keyrevoked').text(ref.get_label('keyrevoked')));
      }
      var ul_ = $('<ul>').addClass('uids');
      $.each(keyrec.uids, function(j, uid) {
        var li_ = $('<li>').addClass('uid');
        if (uid.revoked)  li_.addClass('revoked');
        if (uid.disabled) li_.addClass('disabled');
        if (uid.expired)  li_.addClass('expired');
        ul_.append(li_.text(uid.uid));
      });
      li.append(ul_);
      li.append($('<input>')
        .attr('type', 'button')
        .attr('rel', keyrec.keyid)
        .attr('value', ref.get_label('import'))
        .addClass('button importkey')
        .prop('disabled', keyrec.revoked || keyrec.disabled || keyrec.expired));
      ul.append(li);
    });
    // display dialog with missing keys
    ref.show_popup_dialog(
      $('<div>')
        .append($('<p>').html(ref.get_label('encryptpubkeysfound')))
        .append(ul),
      ref.get_label('importpubkeys'),
      [{
        text: ref.get_label('close'),
        click: function(){
          $(this).dialog('close');
        }
      }]
    );
    // delegate handler for import button clicks
    ul.on('click', 'input.button.importkey', function() {
      var btn = $(this),
        keyid = btn.attr('rel'),
        pk = new PublicKey(),
        lock = ref.display_message(ref.get_label('loading'), 'loading');
        // fetch from keyserver and import to Mailvelope keyring
        pk.get(keyid, function(armored, errorCode) {
          ref.hide_message(lock);
          if (errorCode) {
            ref.display_message('Failed to get key from keyserver', 'error');
            return;
          }
          // import to keyring
          ref.mailvelope_keyring.importPublicKey(armored).then(function(status) {
            if (status === 'REJECTED') {
              // alert(ref.get_label('Key import was rejected'));
            }
            else {
              btn.closest('.key').fadeOut();
              ref.display_message(ref.get_label('Public key $key successfully imported into your key ring')
                .replace('$key', keyid.substr(-8).toUpperCase()), 'confirmation');
            }
          }).catch(function(err) {
            console.log(err);
          });
        });
    });
  };
  /*********************************************************/
  /*********       mailbox folders methods         *********/
  /*********************************************************/
@@ -3327,7 +3875,7 @@
    if (!this.gui_objects.messageform)
      return false;
    var i, pos, input_from = $("[name='_from']"),
    var i, elem, pos, input_from = $("[name='_from']"),
      input_to = $("[name='_to']"),
      input_subject = $("input[name='_subject']"),
      input_message = $("[name='_message']").get(0),
@@ -3362,13 +3910,15 @@
    if (!html_mode) {
      pos = this.env.top_posting ? 0 : input_message.value.length;
      this.set_caret_pos(input_message, pos);
      // add signature according to selected identity
      // if we have HTML editor, signature is added in callback
      // if we have HTML editor, signature is added in a callback
      if (input_from.prop('type') == 'select-one') {
        this.change_identity(input_from[0]);
      }
      // set initial cursor position
      this.set_caret_pos(input_message, pos);
      // scroll to the bottom of the textarea (#1490114)
      if (pos) {
@@ -3381,11 +3931,14 @@
      this.compose_restore_dialog(0, html_mode)
    if (input_to.val() == '')
      input_to.focus();
      elem = input_to;
    else if (input_subject.val() == '')
      input_subject.focus();
      elem = input_subject;
    else if (input_message)
      input_message.focus();
      elem = input_message;
    // focus first empty element (need to be visible on IE8)
    $(elem).filter(':visible').focus();
    this.env.compose_focus_elem = document.activeElement;
@@ -3472,15 +4025,40 @@
      .attr({ 'autocomplete': 'off', 'aria-autocomplete': 'list', 'aria-expanded': 'false', 'role': 'combobox' });
  };
  this.submit_messageform = function(draft)
  this.submit_messageform = function(draft, saveonly)
  {
    var form = this.gui_objects.messageform;
    if (!form)
      return;
    // the message has been sent but not saved, ask the user what to do
    if (!saveonly && this.env.is_sent) {
      return this.show_popup_dialog(this.get_label('messageissent'), '',
        [{
          text: this.get_label('save'),
          'class': 'mainaction',
          click: function() {
            ref.submit_messageform(false, true);
            $(this).dialog('close');
          }
        },
        {
          text: this.get_label('cancel'),
          click: function() {
            $(this).dialog('close');
          }
        }]
      );
    }
    // delegate sending to Mailvelope routine
    if (this.mailvelope_editor) {
      return this.mailvelope_submit_messageform(draft, saveonly);
    }
    // all checks passed, send message
    var msgid = this.set_busy(true, draft ? 'savingmessage' : 'sendingmessage'),
    var msgid = this.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage'),
      lang = this.spellcheck_lang(),
      files = [];
@@ -3493,6 +4071,10 @@
    form.action = this.add_url(form.action, '_unlock', msgid);
    form.action = this.add_url(form.action, '_lang', lang);
    form.action = this.add_url(form.action, '_framed', 1);
    if (saveonly) {
      form.action = this.add_url(form.action, '_saveonly', 1);
    }
    // register timer to notify about connection timeout
    this.submit_timer = setTimeout(function(){
@@ -3779,21 +4361,20 @@
  this.set_draft_id = function(id)
  {
    var rc;
    if (id && id != this.env.draft_id) {
      if (rc = this.opener()) {
        // refresh the drafts folder in opener window
        if (rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == this.env.drafts_mailbox)
          rc.command('checkmail');
      }
      var filter = {task: 'mail', action: ''},
        rc = this.opener(false, filter) || this.opener(true, filter);
      // refresh the drafts folder in the opener window
      if (rc && rc.env.mailbox == this.env.drafts_mailbox)
        rc.command('checkmail');
      this.env.draft_id = id;
      $("input[name='_draft_saveid']").val(id);
      // reset history of hidden iframe used for saving draft (#1489643)
      // but don't do this on timer-triggered draft-autosaving (#1489789)
      if (window.frames['savetarget'] && window.frames['savetarget'].history && !this.draft_autosave_submit) {
      if (window.frames['savetarget'] && window.frames['savetarget'].history && !this.draft_autosave_submit && !this.mailvelope_editor) {
        window.frames['savetarget'].history.back();
      }
@@ -3862,6 +4443,11 @@
    if (this.env.attachments)
      for (id in this.env.attachments)
        str += id;
    // we can't detect changes in the Mailvelope editor so assume it changed
    if (this.mailvelope_editor) {
      str += ';' + new Date().getTime();
    }
    if (save)
      this.cmp_hash = str;
@@ -3972,7 +4558,6 @@
    this.local_storage_remove_item('compose.index');
  };
  this.change_identity = function(obj, show_sig)
  {
@@ -4291,14 +4876,28 @@
    return url;
  };
  // reset search filter
  this.reset_search_filter = function()
  {
    this.filter_disabled = true;
    if (this.gui_objects.search_filter)
      $(this.gui_objects.search_filter).val('ALL').change();
    this.filter_disabled = false;
  };
  // reset quick-search form
  this.reset_qsearch = function()
  this.reset_qsearch = function(all)
  {
    if (this.gui_objects.qsearchbox)
      this.gui_objects.qsearchbox.value = '';
    if (this.env.qsearch)
      this.abort_request(this.env.qsearch);
    if (all) {
      this.env.search_scope = 'base';
      this.reset_search_filter();
    }
    this.env.qsearch = null;
    this.env.search_request = null;
@@ -4342,29 +4941,37 @@
      (this.env.search_request && (this.env.search_scope || 'base') != 'base');
  };
  this.sent_successfully = function(type, msg, folders)
  // action executed after mail is sent
  this.sent_successfully = function(type, msg, folders, save_error)
  {
    this.display_message(msg, type);
    this.compose_skip_unsavedcheck = true;
    if (this.env.extwin) {
      this.lock_form(this.gui_objects.messageform);
      if (!save_error)
        this.lock_form(this.gui_objects.messageform);
      var rc = this.opener();
      var filter = {task: 'mail', action: ''},
        rc = this.opener(false, filter) || this.opener(true, filter);
      if (rc) {
        rc.display_message(msg, type);
        // refresh the folder where sent message was saved or replied message comes from
        if (folders && rc.env.task == 'mail' && rc.env.action == '' && $.inArray(rc.env.mailbox, folders) >= 0) {
        if (folders && $.inArray(rc.env.mailbox, folders) >= 0) {
          rc.command('checkmail');
        }
      }
      setTimeout(function() { window.close(); }, 1000);
      if (!save_error)
        setTimeout(function() { window.close(); }, 1000);
    }
    else {
    else if (!save_error) {
      // before redirect we need to wait some time for Chrome (#1486177)
      setTimeout(function() { ref.list_mailbox(); }, 500);
    }
    if (save_error)
      this.env.is_sent = true;
  };
@@ -4711,6 +5318,7 @@
      clearTimeout(this.preview_timer);
    var n, id, sid, contact, writable = false,
      selected = list.selection.length,
      source = this.env.source ? this.env.address_sources[this.env.source] : null;
    // we don't have dblclick handler here, so use 200 instead of this.dblclick_time
@@ -4719,7 +5327,7 @@
    else if (this.env.contentframe)
      this.show_contentframe(false);
    if (list.selection.length) {
    if (selected) {
      list.draggable = false;
      // no source = search result, we'll need to detect if any of
@@ -4754,11 +5362,12 @@
    // if a group is currently selected, and there is at least one contact selected
    // thend we can enable the group-remove-selected command
    this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0 && writable);
    this.enable_command('compose', this.env.group || list.selection.length > 0);
    this.enable_command('export-selected', 'copy', list.selection.length > 0);
    this.enable_command('group-remove-selected', this.env.group && selected && writable);
    this.enable_command('compose', this.env.group || selected);
    this.enable_command('print', selected == 1);
    this.enable_command('export-selected', 'copy', selected > 0);
    this.enable_command('edit', id && writable);
    this.enable_command('delete', 'move', list.selection.length > 0 && writable);
    this.enable_command('delete', 'move', selected && writable);
    return false;
  };
@@ -4771,6 +5380,9 @@
    if (!src)
      src = this.env.source;
    if (refresh)
      group = this.env.group;
    if (page && this.current_page == page && src == this.env.source && group == this.env.group)
      return false;
@@ -4868,8 +5480,8 @@
    this.contact_list.data = {};
    this.contact_list.clear(true);
    this.show_contentframe(false);
    this.enable_command('delete', 'move', 'copy', false);
    this.enable_command('compose', this.env.group ? true : false);
    this.enable_command('delete', 'move', 'copy', 'print', false);
    this.enable_command('compose', this.env.group);
  };
  this.set_group_prop = function(prop)
@@ -4909,7 +5521,7 @@
        this.contact_list.clear_selection();
      this.enable_command('compose', rec && rec.email);
      this.enable_command('export-selected', rec && rec._type != 'group');
      this.enable_command('export-selected', 'print', rec && rec._type != 'group');
    }
    else if (framed)
      return false;
@@ -5160,10 +5772,10 @@
        dateFormat: this.env.date_format,
        changeMonth: true,
        changeYear: true,
        yearRange: '-100:+10',
        yearRange: '-120:+10',
        showOtherMonths: true,
        selectOtherMonths: true,
        onSelect: function(dateText) { $(this).focus().val(dateText) }
        selectOtherMonths: true
//        onSelect: function(dateText) { $(this).focus().val(dateText); }
      });
      $('input.datepicker').datepicker();
    }
@@ -5794,6 +6406,9 @@
        // on the list when dragging starts (and stops), this is slow, but
        // I didn't find a method to check droptarget on over event
        accept: function(node) {
          if (!$(node).is('.mailbox'))
            return false;
          var source_folder = ref.folder_id2name($(node).attr('id')),
            dest_folder = ref.folder_id2name(this.id),
            source = ref.env.subscriptionrows[source_folder],
@@ -5814,7 +6429,7 @@
  this.folder_id2name = function(id)
  {
    return ref.html_identifier_decode(id.replace(/^rcmli/, ''));
    return id ? ref.html_identifier_decode(id.replace(/^rcmli/, '')) : null;
  };
  this.subscription_select = function(id)
@@ -6561,14 +7176,16 @@
    else
      popup.html(content);
    popup.dialog($.extend({
    options = $.extend({
        title: title,
        buttons: buttons,
        modal: true,
        resizable: true,
        width: 500,
        close: function(event, ui) { $(this).remove(); }
      }, options || {}));
      }, options || {});
    popup.dialog(options);
    // resize and center popup
    var win = $(window), w = win.width(), h = win.height(),
@@ -6592,6 +7209,8 @@
  {
    this.enable_command('nextpage', 'lastpage', this.env.pagecount > this.env.current_page);
    this.enable_command('previouspage', 'firstpage', this.env.current_page > 1);
    this.update_pagejumper();
  };
  // mark a mailbox as selected and set environment variable
@@ -6648,6 +7267,9 @@
      repl, cell, col, n, len, tr;
    this.env.listcols = listcols;
    if (!this.env.coltypes)
      this.env.coltypes = {};
    // replace old column headers
    if (thead) {
@@ -6720,7 +7342,7 @@
  this.set_quota = function(content)
  {
    if (this.gui_objects.quotadisplay && content && content.type == 'text')
      $(this.gui_objects.quotadisplay).html(content.percent+'%').attr('title', content.title);
      $(this.gui_objects.quotadisplay).text((content.percent||0) + '%').attr('title', content.title);
    this.triggerEvent('setquota', content);
    this.env.quota_content = content;
@@ -7140,7 +7762,7 @@
  // compose a valid url with the given parameters
  this.url = function(action, query)
  {
    var querystring = typeof query === 'string' ? '&' + query : '';
    var querystring = typeof query === 'string' ? query : '';
    if (typeof action !== 'string')
      query = action;
@@ -7152,12 +7774,12 @@
    else if (this.env.action)
      query._action = this.env.action;
    var base = this.env.comm_path, k, param = {};
    var url = this.env.comm_path, k, param = {};
    // overwrite task name
    if (action && action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) {
      query._action = RegExp.$2;
      base = base.replace(/\_task=[a-z0-9_-]+/, '_task='+RegExp.$1);
      url = url.replace(/\_task=[a-z0-9_-]+/, '_task=' + RegExp.$1);
    }
    // remove undefined values
@@ -7166,7 +7788,13 @@
        param[k] = query[k];
    }
    return base + (base.indexOf('?') > -1 ? '&' : '?') + $.param(param) + querystring;
    if (param = $.param(param))
      url += (url.indexOf('?') > -1 ? '&' : '?') + param;
    if (querystring)
      url += (url.indexOf('?') > -1 ? '&' : '?') + querystring;
    return url;
  };
  this.redirect = function(url, lock)
@@ -7219,22 +7847,32 @@
  };
  // send a http request to the server
  this.http_request = function(action, query, lock)
  this.http_request = function(action, data, lock)
  {
    var url = this.url(action, query);
    if (typeof data !== 'object')
      data = rcube_parse_query(data);
    data._remote = 1;
    data._unlock = lock ? lock : 0;
    // trigger plugin hook
    var result = this.triggerEvent('request'+action, query);
    var result = this.triggerEvent('request' + action, data);
    if (result !== undefined) {
      // abort if one the handlers returned false
      if (result === false)
        return false;
      else
        url = this.url(action, result);
    // abort if one of the handlers returned false
    if (result === false) {
      if (data._unlock)
        this.set_busy(false, null, data._unlock);
      return false;
    }
    else if (result !== undefined) {
      data = result;
      if (data._action) {
        action = data._action;
        delete data._action;
      }
    }
    url += '&_remote=1';
    var url = this.url(action, data);
    // send request
    this.log('HTTP GET: ' + url);
@@ -7243,33 +7881,39 @@
    this.start_keepalive();
    return $.ajax({
      type: 'GET', url: url, data: { _unlock:(lock?lock:0) }, dataType: 'json',
      success: function(data){ ref.http_response(data); },
      type: 'GET', url: url, dataType: 'json',
      success: function(data) { ref.http_response(data); },
      error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
    });
  };
  // send a http POST request to the server
  this.http_post = function(action, postdata, lock)
  this.http_post = function(action, data, lock)
  {
    var url = this.url(action);
    if (typeof data !== 'object')
      data = rcube_parse_query(data);
    if (postdata && typeof postdata === 'object') {
      postdata._remote = 1;
      postdata._unlock = (lock ? lock : 0);
    }
    else
      postdata += (postdata ? '&' : '') + '_remote=1' + (lock ? '&_unlock='+lock : '');
    data._remote = 1;
    data._unlock = lock ? lock : 0;
    // trigger plugin hook
    var result = this.triggerEvent('request'+action, postdata);
    if (result !== undefined) {
      // abort if one of the handlers returned false
      if (result === false)
        return false;
      else
        postdata = result;
    var result = this.triggerEvent('request'+action, data);
    // abort if one of the handlers returned false
    if (result === false) {
      if (data._unlock)
        this.set_busy(false, null, data._unlock);
      return false;
    }
    else if (result !== undefined) {
      data = result;
      if (data._action) {
        action = data._action;
        delete data._action;
      }
    }
    var url = this.url(action);
    // send request
    this.log('HTTP POST: ' + url);
@@ -7278,7 +7922,7 @@
    this.start_keepalive();
    return $.ajax({
      type: 'POST', url: url, data: postdata, dataType: 'json',
      type: 'POST', url: url, data: data, dataType: 'json',
      success: function(data){ ref.http_response(data); },
      error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
    });
@@ -7347,7 +7991,7 @@
          this.enable_command('compose', (uid && this.contact_list.rows[uid]));
          this.enable_command('delete', 'edit', writable);
          this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
          this.enable_command('export-selected', false);
          this.enable_command('export-selected', 'print', false);
        }
      case 'move':
@@ -7394,32 +8038,36 @@
        this.env.qsearch = null;
      case 'list':
        if (this.task == 'mail') {
          var is_multifolder = this.is_multifolder_listing();
          var is_multifolder = this.is_multifolder_listing(),
            list = this.message_list,
            uid = this.env.list_uid;
          this.enable_command('show', 'select-all', 'select-none', this.env.messagecount > 0);
          this.enable_command('expunge', this.env.exists && !is_multifolder);
          this.enable_command('purge', this.purge_mailbox_test() && !is_multifolder);
          this.enable_command('import-messages', !is_multifolder);
          this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount && !is_multifolder);
          if ((response.action == 'list' || response.action == 'search') && this.message_list) {
            var list = this.message_list, uid = this.env.list_uid;
            // highlight message row when we're back from message page
            if (uid) {
              if (!list.rows[uid])
                uid += '-' + this.env.mailbox;
              if (list.rows[uid]) {
                list.select(uid);
          if (list) {
            if (response.action == 'list' || response.action == 'search') {
              // highlight message row when we're back from message page
              if (uid) {
                if (!list.rows[uid])
                  uid += '-' + this.env.mailbox;
                if (list.rows[uid]) {
                  list.select(uid);
                }
                delete this.env.list_uid;
              }
              delete this.env.list_uid;
              this.enable_command('set-listmode', this.env.threads && !is_multifolder);
              if (list.rowcount > 0)
                list.focus();
              this.msglist_select(list);
            }
            this.enable_command('set-listmode', this.env.threads && !is_multifolder);
            if (list.rowcount > 0)
              list.focus();
            this.msglist_select(list);
            this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:list.rowcount });
            if (response.action != 'getunread')
              this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:list.rowcount });
          }
        }
        else if (this.task == 'addressbook') {
@@ -7887,12 +8535,24 @@
  };
  // get window.opener.rcmail if available
  this.opener = function()
  this.opener = function(deep, filter)
  {
    var i, win = window.opener;
    // catch Error: Permission denied to access property rcmail
    try {
      if (window.opener && !opener.closed && opener.rcmail)
        return opener.rcmail;
      if (win && !win.closed) {
        // try parent of the opener window, e.g. preview frame
        if (deep && (!win.rcmail || win.rcmail.env.framed) && win.parent && win.parent.rcmail)
          win = win.parent;
        if (win.rcmail && filter)
          for (i in filter)
            if (win.rcmail.env[i] != filter[i])
              return;
        return win.rcmail;
      }
    }
    catch (e) {}
  };
@@ -8059,7 +8719,7 @@
    if (plugin && plugin.enabledPlugin)
        return 1;
    if (window.ActiveXObject) {
    if ('ActiveXObject' in window) {
      try {
        if (plugin = new ActiveXObject("AcroPDF.PDF"))
          return 1;
@@ -8092,7 +8752,7 @@
    if (plugin && plugin.enabledPlugin)
        return 1;
    if (window.ActiveXObject) {
    if ('ActiveXObject' in window) {
      try {
        if (plugin = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
          return 1;
@@ -8129,15 +8789,16 @@
  // wrapper for localStorage.getItem(key)
  this.local_storage_get_item = function(key, deflt, encrypted)
  {
    var item;
    var item, result;
    // TODO: add encryption
    try {
      item = localStorage.getItem(this.get_local_storage_prefix() + key);
      result = JSON.parse(item);
    }
    catch (e) { }
    return item !== null ? JSON.parse(item) : (deflt || null);
    return result || deflt || null;
  };
  // wrapper for localStorage.setItem(key, data)
@@ -8167,6 +8828,14 @@
      return false;
    }
  };
  this.print_dialog = function()
  {
    if (bw.safari)
      setTimeout('window.print()', 10);
    else
      window.print();
  };
}  // end object rcube_webmail
@@ -8176,7 +8845,7 @@
  if (!elem.title) {
    var $elem = $(elem);
    if ($elem.width() + (indent || 0) * 15 > $elem.parent().width())
      elem.title = $elem.text();
      elem.title = rcube_webmail.subject_text(elem);
  }
};
@@ -8193,10 +8862,17 @@
    tmp.remove();
    if (w + $('span.branch', $elem).width() * 15 > $elem.width())
      elem.title = txt;
      elem.title = rcube_webmail.subject_text(elem);
  }
};
rcube_webmail.subject_text = function(elem)
{
  var t = $(elem).clone();
  t.find('.skip-on-drag').remove();
  return t.text();
};
rcube_webmail.prototype.get_cookie = getCookie;
// copy event engine prototype