Aleksander Machniak
2016-05-22 87cf0a3fb158b5ffaa54a79997d7b01492d39b74
program/js/app.js
@@ -6,8 +6,8 @@
 * @licstart  The following is the entire license notice for the
 * JavaScript code in this file.
 *
 * Copyright (C) 2005-2014, The Roundcube Dev Team
 * Copyright (C) 2011-2014, Kolab Systems AG
 * Copyright (C) 2005-2015, The Roundcube Dev Team
 * Copyright (C) 2011-2015, Kolab Systems AG
 *
 * The JavaScript code in this page is free software: you can
 * redistribute it and/or modify it under the terms of the GNU
@@ -77,7 +77,7 @@
  });
  // unload fix
  $(window).bind('beforeunload', function() { ref.unload = true; });
  $(window).on('beforeunload', function() { ref.unload = true; });
  // set environment variable(s)
  this.set_env = function(p, value)
@@ -156,8 +156,8 @@
    var n;
    this.task = this.env.task;
    // check browser
    if (this.env.server_error != 409 && (!bw.dom || !bw.xmlhttp_test() || (bw.mz && bw.vendver < 1.9) || (bw.ie && bw.vendver < 7))) {
    // check browser capabilities (never use version checks here)
    if (this.env.server_error != 409 && (!bw.dom || !bw.xmlhttp_test())) {
      this.goto_url('error', '_code=0x199');
      return;
    }
@@ -274,11 +274,28 @@
            this.enable_command('compose', 'add-contact', false);
            parent.rcmail.show_contentframe(true);
          }
          // initialize drag-n-drop on attachments, so they can e.g.
          // be dropped into mail compose attachments in another window
          if (this.gui_objects.attachments)
            $('li > a', this.gui_objects.attachments).not('.drop').on('dragstart', function(e) {
              var n, href = this.href, dt = e.originalEvent.dataTransfer;
              if (dt) {
                // inject username to the uri
                href = href.replace(/^https?:\/\//, function(m) { return m + urlencode(ref.env.username) + '@'});
                // cleanup the node to get filename without the size test
                n = $(this).clone();
                n.children().remove();
                dt.setData('roundcube-uri', href);
                dt.setData('roundcube-name', $.trim(n.text()));
              }
            });
        }
        else if (this.env.action == 'compose') {
          this.env.address_group_stack = [];
          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel',
            'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin',
            'toggle-editor', 'list-addresses', 'pushgroup', 'search', 'reset-search', 'extwin',
            'insert-response', 'save-response', 'menu-open', 'menu-close'];
          if (this.env.drafts_mailbox)
@@ -304,8 +321,8 @@
          if (this.gui_objects.responseslist) {
            $('a.insertresponse', this.gui_objects.responseslist)
              .attr('unselectable', 'on')
              .mousedown(function(e){ return rcube_event.cancel(e); })
              .bind('mouseup keypress', function(e){
              .mousedown(function(e) { return rcube_event.cancel(e); })
              .on('mouseup keypress', function(e) {
                if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
                  ref.command('insert-response', $(this).attr('rel'));
                  $(document.body).trigger('mouseup');  // hides the menu
@@ -322,10 +339,19 @@
          // init message compose form
          this.init_messageform();
        }
        else if (this.env.action == 'get')
        else if (this.env.action == 'get') {
          this.enable_command('download', 'print', true);
          if (this.env.is_message) {
            this.enable_command('reply', 'reply-all', 'edit', 'viewsource',
              'forward', 'forward-inline', 'forward-attachment', true);
            if (this.env.list_post)
              this.enable_command('reply-list', true);
          }
        }
        // show printing dialog
        else if (this.env.action == 'print' && this.env.uid) {
        else if (this.env.action == 'print' && this.env.uid
          && !this.env.is_pgp_content && !this.env.pgp_mime_part
        ) {
          this.print_dialog();
        }
@@ -333,7 +359,7 @@
        if (this.gui_objects.mailboxlist) {
          this.env.unread_counts = {};
          this.gui_objects.folderlist = this.gui_objects.mailboxlist;
          this.http_request('getunread');
          this.http_request('getunread', {_page: this.env.current_page});
        }
        // init address book widget
@@ -362,7 +388,7 @@
        if (this.gui_objects.addressbookslist) {
          this.gui_objects.folderlist = this.gui_objects.addressbookslist;
          this.enable_command('list-adresses', true);
          this.enable_command('list-addresses', true);
        }
        // ask user to send MDN
@@ -375,6 +401,8 @@
          }
          this.http_post(postact, postdata);
        }
        this.check_mailvelope(this.env.action);
        // detect browser capabilities
        if (!this.is_framed() && !this.env.extwin)
@@ -510,8 +538,11 @@
        break;
      case 'login':
        var input_user = $('#rcmloginuser');
        input_user.bind('keyup', function(e){ return ref.login_user_keyup(e); });
        var tz, tz_name, jstz = window.jstz,
            input_user = $('#rcmloginuser'),
            input_tz = $('#rcmlogintz');
        input_user.keyup(function(e) { return ref.login_user_keyup(e); });
        if (input_user.val() == '')
          input_user.focus();
@@ -519,14 +550,10 @@
          $('#rcmloginpwd').focus();
        // detect client timezone
        if (window.jstz) {
          var timezone = jstz.determine();
          if (timezone.name())
            $('#rcmlogintz').val(timezone.name());
        }
        else {
          $('#rcmlogintz').val(new Date().getStdTimezoneOffset() / -60);
        }
        if (jstz && (tz = jstz.determine()))
          tz_name = tz.name();
        input_tz.val(tz_name ? tz_name : (new Date().getStdTimezoneOffset() / -60));
        // display 'loading' message on form submit, lock submit button
        $('form').submit(function () {
@@ -561,7 +588,12 @@
      this.display_message.apply(this, this.pending_message);
    // init treelist widget
    if (this.gui_objects.folderlist && window.rcube_treelist_widget) {
    if (this.gui_objects.folderlist && window.rcube_treelist_widget
      // some plugins may load rcube_treelist_widget and there's one case
      // when this will cause problems - addressbook widget in compose,
      // which already has been initialized using rcube_list_widget
      && this.gui_objects.folderlist != this.gui_objects.addressbookslist
    ) {
      this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, {
          selectable: true,
          id_prefix: 'rcmli',
@@ -580,17 +612,17 @@
    // activate html5 file drop feature (if browser supports it and if configured)
    if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) {
      $(document.body).bind('dragover dragleave drop', function(e){ return ref.document_drag_hover(e, e.type == 'dragover'); });
      $(document.body).on('dragover dragleave drop', function(e) { return ref.document_drag_hover(e, e.type == 'dragover'); });
      $(this.gui_objects.filedrop).addClass('droptarget')
        .bind('dragover dragleave', function(e){ return ref.file_drag_hover(e, e.type == 'dragover'); })
        .get(0).addEventListener('drop', function(e){ return ref.file_dropped(e); }, false);
        .on('dragover dragleave', function(e) { return ref.file_drag_hover(e, e.type == 'dragover'); })
        .get(0).addEventListener('drop', function(e) { return ref.file_dropped(e); }, false);
    }
    // catch document (and iframe) mouse clicks
    var body_mouseup = function(e){ return ref.doc_mouse_up(e); };
    $(document.body)
      .bind('mouseup', body_mouseup)
      .bind('keydown', function(e){ return ref.doc_keypress(e); });
      .mouseup(body_mouseup)
      .keydown(function(e){ return ref.doc_keypress(e); });
    $('iframe').on('load', function(e) {
        try { $(this.contentDocument || this.contentWindow).on('mouseup', body_mouseup);  }
@@ -999,7 +1031,7 @@
            break;
        }
        this.goto_url('get', qstring+'&_download=1', false);
        this.goto_url('get', qstring+'&_download=1', false, true);
        break;
      case 'select-all':
@@ -1141,7 +1173,7 @@
        this.change_identity($("[name='_from']")[0], true);
        break;
      case 'list-adresses':
      case 'list-addresses':
        this.list_contacts(props);
        this.enable_command('add-recipient', false);
        break;
@@ -1186,13 +1218,13 @@
            this.open_window(this.env.comm_path + url, true, true);
          }
        }
        else if (this.env.action == 'get') {
        else if (this.env.action == 'get' && !this.env.is_message) {
          this.gui_objects.messagepartframe.contentWindow.print();
        }
        else if (uid = this.get_single_uid()) {
          url = this.url('print', this.params_from_uid(uid, {_safe: this.env.safemode ? 1 : 0}));
          if (this.open_window(url, true, true)) {
            if (this.env.action != 'show')
            if (this.env.action != 'show' && this.env.action != 'get')
              this.mark_message('read', uid);
          }
        }
@@ -1205,10 +1237,10 @@
      case 'download':
        if (this.env.action == 'get') {
          location.href = location.href.replace(/_frame=/, '_download=');
          location.href = this.secure_url(location.href.replace(/_frame=/, '_download='));
        }
        else if (uid = this.get_single_uid()) {
          this.goto_url('viewsource', this.params_from_uid(uid, {_save: 1}));
          this.goto_url('viewsource', this.params_from_uid(uid, {_save: 1}), false, true);
        }
        break;
@@ -1296,13 +1328,13 @@
      case 'export':
        if (this.contact_list.rowcount > 0) {
          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request });
          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request }, false, true);
        }
        break;
      case 'export-selected':
        if (this.contact_list.rowcount > 0) {
          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') });
          this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') }, false, true);
        }
        break;
@@ -1337,7 +1369,7 @@
    if (!aborted && this.triggerEvent('after'+command, props) === false)
      ret = false;
    this.triggerEvent('actionafter', { props:props, action:command, aborted:aborted });
    this.triggerEvent('actionafter', { props:props, action:command, aborted:aborted, ret:ret });
    return ret === false ? false : obj ? false : true;
  };
@@ -1417,7 +1449,7 @@
    if (task == 'mail')
      url += '&_mbox=INBOX';
    else if (task == 'logout' && !this.env.server_error) {
      url += '&_token=' + this.env.request_token;
      url = this.secure_url(url);
      this.clear_compose_data();
    }
@@ -1465,6 +1497,12 @@
    return url + '?' + name + '=' + value;
  };
  // append CSRF protection token to the given url
  this.secure_url = function(url)
  {
    return this.add_url(url, '_token', this.env.request_token);
  },
  this.is_framed = function()
  {
@@ -2000,8 +2038,9 @@
      flagged: flags.flagged?1:0,
      has_children: flags.has_children?1:0,
      depth: flags.depth?flags.depth:0,
      unread_children: flags.unread_children?flags.unread_children:0,
      parent_uid: flags.parent_uid?flags.parent_uid:0,
      unread_children: flags.unread_children || 0,
      flagged_children: flags.flagged_children || 0,
      parent_uid: flags.parent_uid || 0,
      selected: this.select_all_mode || this.message_list.in_selection(uid),
      ml: flags.ml?1:0,
      ctype: flags.ctype,
@@ -2081,6 +2120,9 @@
      if (flags.unread_children && flags.seen && !message.expanded)
        row_class += ' unroot';
      if (flags.flagged_children && !message.expanded)
        row_class += ' flaggedroot';
    }
    tree += '<span id="msgicn'+row.id+'" class="'+css_class+status_class+'" title="'+status_label+'"></span>';
@@ -2118,7 +2160,7 @@
          html = '<span class="attachment" title="'+label+'"></span>';
        else if (/multipart\/report/.test(flags.ctype))
          html = '<span class="report"></span>';
          else
        else
          html = '&nbsp;';
      }
      else if (c == 'status') {
@@ -2175,10 +2217,16 @@
  this.set_list_sorting = function(sort_col, sort_order)
  {
    var sort_old = this.env.sort_col == 'arrival' ? 'date' : this.env.sort_col,
      sort_new = sort_col == 'arrival' ? 'date' : sort_col;
    // set table header class
    $('#rcm'+this.env.sort_col).removeClass('sorted'+(this.env.sort_order.toUpperCase()));
    if (sort_col)
      $('#rcm'+sort_col).addClass('sorted'+sort_order);
    $('#rcm' + sort_old).removeClass('sorted' + this.env.sort_order.toUpperCase());
    if (sort_new)
      $('#rcm' + sort_new).addClass('sorted' + sort_order);
    // if sorting by 'arrival' is selected, click on date column should not switch to 'date'
    $('#rcmdate > a').prop('rel', sort_col == 'arrival' ? 'arrival' : 'date');
    this.env.sort_col = sort_col;
    this.env.sort_order = sort_order;
@@ -2488,22 +2536,23 @@
  // removes messages that doesn't exists from list selection array
  this.update_selection = function()
  {
    var selected = this.message_list.selection,
      rows = this.message_list.rows,
    var list = this.message_list,
      selected = list.selection,
      rows = list.rows,
      i, selection = [];
    for (i in selected)
      if (rows[selected[i]])
        selection.push(selected[i]);
    this.message_list.selection = selection;
    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)
      if (id && !list.in_selection(id))
        this.show_contentframe(false);
    }
    catch (e) {};
@@ -2532,9 +2581,10 @@
  {
    var row = this.message_list.rows[uid];
    // handle unread_children mark
    // handle unread_children/flagged_children mark
    row.expanded = !row.expanded;
    this.set_unread_children(uid);
    this.set_flagged_children(uid);
    row.expanded = !row.expanded;
    this.message_list.expand_row(e, uid);
@@ -2667,7 +2717,13 @@
    }
    else if (flag == 'unread' && p.has_children) {
      // unread_children may be undefined
      p.unread_children = p.unread_children ? p.unread_children + 1 : 1;
      p.unread_children = (p.unread_children || 0) + 1;
    }
    else if (flag == 'unflagged' && p.flagged_children) {
      p.flagged_children--;
    }
    else if (flag == 'flagged' && p.has_children) {
      p.flagged_children = (p.flagged_children || 0) + 1;
    }
    else {
      return;
@@ -2675,6 +2731,7 @@
    this.set_message_icon(root);
    this.set_unread_children(root);
    this.set_flagged_children(root);
  };
  // update thread indicators for all messages in a thread below the specified message
@@ -2692,11 +2749,19 @@
    if (!row.depth) // root message: decrease roots count
      count--;
    else if (row.unread) {
      // update unread_children for thread root
    // update unread_children for thread root
    if (row.depth && row.unread) {
      parent = this.message_list.find_root(uid);
      rows[parent].unread_children--;
      this.set_unread_children(parent);
    }
    // update unread_children for thread root
    if (row.depth && row.flagged) {
      parent = this.message_list.find_root(uid);
      rows[parent].flagged_children--;
      this.set_flagged_children(parent);
    }
    parent = row.parent_uid;
@@ -2719,8 +2784,9 @@
            $('#'+r.id+' .leaf:first')
              .attr('id', 'rcmexpando' + r.id)
              .attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed'))
              .bind('mousedown', {uid: r.uid},
                function(e) { return ref.expand_message_row(e, e.data.uid); });
              .mousedown({uid: r.uid}, function(e) {
                return ref.expand_message_row(e, e.data.uid);
              });
            r.unread_children = 0;
            roots.push(r);
@@ -2739,9 +2805,11 @@
      row = row.nextSibling;
    }
    // update unread_children for roots
    for (r=0; r<roots.length; r++)
    // update unread_children/flagged_children for roots
    for (r=0; r<roots.length; r++) {
      this.set_unread_children(roots[r].uid);
      this.set_flagged_children(roots[r].uid);
    }
    return count;
  };
@@ -2840,6 +2908,9 @@
      if (row.unread != status)
        this.update_thread_root(uid, status ? 'unread' : 'read');
    }
    else if (flag == 'flagged') {
      this.update_thread_root(uid, status ? 'flagged' : 'unflagged');
    }
    if ($.inArray(flag, ['unread', 'deleted', 'replied', 'forwarded', 'flagged']) > -1)
      row[flag] = status;
@@ -2871,10 +2942,20 @@
    if (row.parent_uid)
      return;
    if (!row.unread && row.unread_children && !row.expanded)
      $(row.obj).addClass('unroot');
    else
      $(row.obj).removeClass('unroot');
    var enable = !row.unread && row.unread_children && !row.expanded;
    $(row.obj)[enable ? 'addClass' : 'removeClass']('unroot');
  };
  // sets flaggedroot (flagged_children) class of parent row
  this.set_flagged_children = function(uid)
  {
    var row = this.message_list.rows[uid];
    if (row.parent_uid)
      return;
    var enable = row.flagged_children && !row.expanded;
    $(row.obj)[enable ? 'addClass' : 'removeClass']('flaggedroot');
  };
  // copy selected messages to the specified mailbox
@@ -3341,6 +3422,469 @@
    $('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);
      });
    }
  };
  // Load Mailvelope functionality (and initialize keyring if needed)
  this.mailvelope_load = function(action)
  {
    if (this.env.browser_capabilities)
      this.env.browser_capabilities['pgpmime'] = 1;
    var keyring = this.env.user_id;
    mailvelope.getKeyring(keyring).then(function(kr) {
      ref.mailvelope_keyring = kr;
      ref.mailvelope_init(action, kr);
    }, 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);
      }, function(err) {
        console.error(err);
      });
    });
  };
  // Initializes Mailvelope editor or display container
  this.mailvelope_init = function(action, keyring)
  {
    if (!window.mailvelope)
      return;
    if (action == 'show' || action == 'preview' || action == 'print') {
      // decrypt text body
      if (this.env.is_pgp_content) {
        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) {
        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') {
      this.env.compose_commands.push('compose-encrypted');
      var is_html = $('input[name="_is_html"]').val() > 0;
      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', !is_html);
          },
          success: function(data) {
            ref.set_busy(false, null, lock);
            if (is_html) {
              ref.command('toggle-editor', {html: false, noconvert: true});
              $('#' + ref.env.composebody).val('');
            }
            ref.compose_encrypted({ quotedMail: data });
            ref.enable_command('compose-encrypted', true);
          }
        });
      }
      else {
        // enable encrypted compose toggle
        this.enable_command('compose-encrypted', !is_html);
      }
      // make sure to disable encryption button after toggling editor into HTML mode
      this.addEventListener('actionafter', function(args) {
        if (args.ret && args.action == 'toggle-editor')
          ref.enable_command('compose-encrypted', !args.props.html);
      });
    }
  };
  // handler for the 'compose-encrypted' command
  this.compose_encrypted = function(props)
  {
    var options, 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();
      // disable commands that operate on the compose body
      ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', true);
      ref.triggerEvent('compose-encrypted', { active:false });
    }
    // embed Mailvelope editor container
    else {
      if (this.spellcheck_state())
        this.editor.spellcheck_stop();
      if (props.quotedMail) {
        options = { quotedMail: props.quotedMail, quotedMailIndent: false };
      }
      else {
        options = { predefinedText: $('#' + this.env.composebody).val() };
      }
      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();
        // disable commands that operate on the compose body
        ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', false);
        ref.triggerEvent('compose-encrypted', { active:true });
        // 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);
          });
        }
      }, function(err) {
        console.error(err);
        console.log(options);
      });
    }
  };
  // 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) {
        // 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();
        }, function(err) {
          console.log(err);
        });  // mailvelope_editor.encrypt()
      }, function(err) {
        console.error(err);
      });  // mailvelope_keyring.validKeyForAddress(senders)
    }, 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').children().not('iframe').hide();
      ref.hide_message(msgid);
      setTimeout(function() { $(window).resize(); }, 10);
    }, 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');
      }
    }).fail(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(ref.get_label('keyservererror'), '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 {
              var $key = keyid.substr(-8).toUpperCase();
              btn.closest('.key').fadeOut();
              ref.display_message(ref.get_label('keyimportsuccess').replace('$key', $key), 'confirmation');
            }
          }, function(err) {
            console.log(err);
          });
        });
    });
  };
  /*********************************************************/
  /*********       mailbox folders methods         *********/
  /*********************************************************/
@@ -3469,7 +4013,7 @@
    }
    if (!html_mode) {
      pos = this.env.top_posting ? 0 : input_message.value.length;
      pos = this.env.top_posting && this.env.compose_mode ? 0 : input_message.value.length;
      // add signature according to selected identity
      // if we have HTML editor, signature is added in a callback
@@ -3610,6 +4154,11 @@
          }
        }]
      );
    }
    // delegate sending to Mailvelope routine
    if (this.mailvelope_editor) {
      return this.mailvelope_submit_messageform(draft, saveonly);
    }
    // all checks passed, send message
@@ -3814,7 +4363,7 @@
      '<textarea name="text" id="ffresponsetext" cols="40" rows="8"></textarea></div>' +
      '</form>';
    buttons[this.gettext('save')] = function(e) {
    buttons[this.get_label('save')] = function(e) {
      var name = $('#ffresponsename').val(),
        text = $('#ffresponsetext').val();
@@ -3830,11 +4379,11 @@
      $(this).dialog('close');
    };
    buttons[this.gettext('cancel')] = function() {
    buttons[this.get_label('cancel')] = function() {
      $(this).dialog('close');
    };
    this.show_popup_dialog(html, this.gettext('newresponse'), buttons, {button_classes: ['mainaction']});
    this.show_popup_dialog(html, this.get_label('newresponse'), buttons, {button_classes: ['mainaction']});
    $('#ffresponsetext').val(text);
    $('#ffresponsename').select();
@@ -3854,10 +4403,10 @@
        .attr('tabindex', '0')
        .html(this.quote_html(response.name))
        .appendTo(li)
        .mousedown(function(e){
        .mousedown(function(e) {
          return rcube_event.cancel(e);
        })
        .bind('mouseup keypress', function(e){
        .on('mouseup keypress', function(e) {
          if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
            ref.command('insert-response', $(this).attr('rel'));
            $(document.body).trigger('mouseup');  // hides the menu
@@ -3929,7 +4478,7 @@
      // 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();
      }
@@ -3955,7 +4504,7 @@
    if (!this.local_save_timer && window.localStorage && this.env.save_localstorage) {
      // track typing activity and only save on changes
      this.compose_type_activity = this.compose_type_activity_last = 0;
      $(document).bind('keypress', function(e){ ref.compose_type_activity++; });
      $(document).keypress(function(e) { ref.compose_type_activity++; });
      this.local_save_timer = setInterval(function(){
        if (ref.compose_type_activity > ref.compose_type_activity_last) {
@@ -3998,6 +4547,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;
@@ -4408,6 +4962,9 @@
    if (filter)
      url._filter = filter;
    if (this.gui_objects.search_interval)
      url._interval = $(this.gui_objects.search_interval).val();
    if (search) {
      url._q = search;
@@ -4444,6 +5001,9 @@
    if (this.gui_objects.qsearchbox)
      this.gui_objects.qsearchbox.value = '';
    if (this.gui_objects.search_interval)
      $(this.gui_objects.search_interval).val('');
    if (this.env.qsearch)
      this.abort_request(this.env.qsearch);
@@ -4469,6 +5029,20 @@
      if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL')
        this.filter_mailbox(this.env.search_filter);
      if (scope != 'all')
        this.select_folder(this.env.mailbox, '', true);
    }
  };
  this.set_searchinterval = function(interval)
  {
    var old = this.env.search_interval;
    this.env.search_interval = interval;
    // re-send search query with new interval
    if (interval != old && this.env.search_request) {
      if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL')
        this.filter_mailbox(this.env.search_filter);
      if (interval)
        this.select_folder(this.env.mailbox, '', true);
    }
  };
@@ -5045,7 +5619,7 @@
      // add link to pop back to parent group
      if (this.env.address_group_stack.length > 1) {
        $('<a href="#list">...</a>')
          .attr('title', this.gettext('uponelevel'))
          .attr('title', this.get_label('uponelevel'))
          .addClass('poplink')
          .appendTo(boxtitle)
          .click(function(e){ return ref.command('popgroup','',this); });
@@ -7294,8 +7868,6 @@
    var url = '?_task=utils&_action=' + (format == 'html' ? 'html2text' : 'text2html'),
      lock = this.set_busy(true, 'converting');
    this.log('HTTP POST: ' + url);
    $.ajax({ type: 'POST', url: url, data: text, contentType: 'application/octet-stream',
      error: function(o, status, err) { ref.http_error(o, status, err, lock); },
      success: function(data) {
@@ -7369,9 +7941,11 @@
    }
  };
  this.goto_url = function(action, query, lock)
  this.goto_url = function(action, query, lock, secure)
  {
    this.redirect(this.url(action, query), lock);
    var url = this.url(action, query)
    if (secure) url = this.secure_url(url);
    this.redirect(url, lock);
  };
  this.location_href = function(url, target, frame)
@@ -7400,8 +7974,11 @@
  };
  // send a http request to the server
  this.http_request = function(action, data, lock)
  this.http_request = function(action, data, lock, type)
  {
    if (type != 'POST')
      type = 'GET';
    if (typeof data !== 'object')
      data = rcube_parse_query(data);
@@ -7425,60 +8002,26 @@
      }
    }
    var url = this.url(action, data);
    // send request
    this.log('HTTP GET: ' + url);
    var url = this.url(action);
    // reset keep-alive interval
    this.start_keepalive();
    // send request
    return $.ajax({
      type: 'GET', url: url, dataType: 'json',
      type: type, 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); }
    });
  };
  // send a http GET request to the server
  this.http_get = this.http_request;
  // send a http POST request to the server
  this.http_post = function(action, data, lock)
  {
    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, 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);
    // reset keep-alive interval
    this.start_keepalive();
    return $.ajax({
      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); }
    });
    return this.http_request(action, data, lock, 'POST');
  };
  // aborts ajax request
@@ -7506,22 +8049,23 @@
    if (response.env)
      this.set_env(response.env);
    var i;
    // we have labels to add
    if (typeof response.texts === 'object') {
      for (var name in response.texts)
        if (typeof response.texts[name] === 'string')
          this.add_label(name, response.texts[name]);
      for (i in response.texts)
        if (typeof response.texts[i] === 'string')
          this.add_label(i, response.texts[i]);
    }
    // if we get javascript code from server -> execute it
    if (response.exec) {
      this.log(response.exec);
      eval(response.exec);
    }
    // execute callback functions of plugins
    if (response.callbacks && response.callbacks.length) {
      for (var i=0; i < response.callbacks.length; i++)
      for (i=0; i < response.callbacks.length; i++)
        this.triggerEvent(response.callbacks[i][0], response.callbacks[i][1]);
    }
@@ -7616,7 +8160,10 @@
              this.enable_command('set-listmode', this.env.threads && !is_multifolder);
              if (list.rowcount > 0 && !$(document.activeElement).is('input,textarea'))
                list.focus();
              this.msglist_select(list);
              // trigger 'select' so all dependent actions update its state
              // e.g. plugins use this event to activate buttons (#1490647)
              list.triggerEvent('select');
            }
            if (response.action != 'getunread')
@@ -7861,7 +8408,7 @@
    }
    // handle upload errors by parsing iframe content in onload
    frame.bind('load', {ts:ts}, onload);
    frame.on('load', {ts:ts}, onload);
    $(form).attr({
        target: frame_name,
@@ -7883,7 +8430,7 @@
  // html5 file-drop API
  this.document_drag_hover = function(e, over)
  {
    e.preventDefault();
    // don't e.preventDefault() here to not block text dragging on the page (#1490619)
    $(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('active');
  };
@@ -7902,21 +8449,39 @@
    this.file_drag_hover(e, false);
    // prepare multipart form data composition
    var files = e.target.files || e.dataTransfer.files,
    var uri, files = e.target.files || e.dataTransfer.files,
      formdata = window.FormData ? new FormData() : null,
      fieldname = (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'),
      boundary = '------multipartformboundary' + (new Date).getTime(),
      dashdash = '--', crlf = '\r\n',
      multipart = dashdash + boundary + crlf;
      multipart = dashdash + boundary + crlf,
      args = {_id: this.env.compose_id || this.env.cid || '', _remote: 1, _from: this.env.action};
    if (!files || !files.length)
    if (!files || !files.length) {
      // Roundcube attachment, pass its uri to the backend and attach
      if (uri = e.dataTransfer.getData('roundcube-uri')) {
        var ts = new Date().getTime(),
          // jQuery way to escape filename (#1490530)
          content = $('<span>').text(e.dataTransfer.getData('roundcube-name') || this.get_label('attaching')).html();
        args._uri = uri;
        args._uploadid = ts;
        // add to attachments list
        if (!this.add2attachment_list(ts, {name: '', html: content, classname: 'uploading', complete: false}))
          this.file_upload_id = this.set_busy(true, 'attaching');
        this.http_post(this.env.filedrop.action || 'upload', args);
      }
      return;
    }
    // inline function to submit the files to the server
    var submit_data = function() {
      var multiple = files.length > 1,
        ts = new Date().getTime(),
        content = '<span>' + (multiple ? ref.get_label('uploadingmany') : files[0].name) + '</span>';
        // jQuery way to escape filename (#1490530)
        content = $('<span>').text(multiple ? ref.get_label('uploadingmany') : files[0].name).html();
      // add to attachments list
      if (!ref.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false }))
@@ -7925,10 +8490,12 @@
      // complete multipart content and post request
      multipart += dashdash + boundary + dashdash + crlf;
      args._uploadid = ts;
      $.ajax({
        type: 'POST',
        dataType: 'json',
        url: ref.url(ref.env.filedrop.action || 'upload', {_id: ref.env.compose_id||ref.env.cid||'', _uploadid: ts, _remote: 1, _from: ref.env.action}),
        url: ref.url(ref.env.filedrop.action || 'upload', args),
        contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary,
        processData: false,
        timeout: 0, // disable default timeout set in ajaxSetup()
@@ -8241,14 +8808,10 @@
    if (!this.env.browser_capabilities)
      this.env.browser_capabilities = {};
    if (this.env.browser_capabilities.pdf === undefined)
      this.env.browser_capabilities.pdf = this.pdf_support_check();
    if (this.env.browser_capabilities.flash === undefined)
      this.env.browser_capabilities.flash = this.flash_support_check();
    if (this.env.browser_capabilities.tif === undefined)
      this.tif_support_check();
    $.each(['pdf', 'flash', 'tif'], function() {
      if (ref.env.browser_capabilities[this] === undefined)
        ref.env.browser_capabilities[this] = ref[this + '_support_check']();
    });
  };
  // Returns browser capabilities string
@@ -8267,11 +8830,14 @@
  this.tif_support_check = function()
  {
    var img = new Image();
    window.setTimeout(function() {
      var img = new Image();
      img.onload = function() { ref.env.browser_capabilities.tif = 1; };
      img.onerror = function() { ref.env.browser_capabilities.tif = 0; };
      img.src = ref.assets_path('program/resources/blank.tif');
    }, 10);
    img.onload = function() { ref.env.browser_capabilities.tif = 1; };
    img.onerror = function() { ref.env.browser_capabilities.tif = 0; };
    img.src = this.assets_path('program/resources/blank.tif');
    return 0;
  };
  this.pdf_support_check = function()
@@ -8307,6 +8873,14 @@
        return 1;
    }
    window.setTimeout(function() {
      $('<object>').css({position: 'absolute', left: '-10000px'})
        .attr({data: ref.assets_path('program/resources/dummy.pdf'), width: 1, height: 1, type: 'application/pdf'})
        .load(function() { ref.env.browser_capabilities.pdf = 1; })
        .error(function() { ref.env.browser_capabilities.pdf = 0; })
        .appendTo($('body'));
      }, 10);
    return 0;
  };