Thomas Bruederli
2015-07-31 2965a981b7ec22866fbdf2d567d87e2d068d3617
program/js/app.js
@@ -376,6 +376,8 @@
          this.http_post(postact, postdata);
        }
        this.check_mailvelope(this.env.action);
        // detect browser capabilities
        if (!this.is_framed() && !this.env.extwin)
          this.browser_capabilities_check();
@@ -1440,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
@@ -3255,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         *********/
  /*********************************************************/
@@ -3384,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) {
@@ -3522,6 +4050,11 @@
          }
        }]
      );
    }
    // delegate sending to Mailvelope routine
    if (this.mailvelope_editor) {
      return this.mailvelope_submit_messageform(draft, saveonly);
    }
    // all checks passed, send message
@@ -3841,7 +4374,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();
      }
@@ -3910,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;
@@ -6671,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
@@ -7222,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;
@@ -7234,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
@@ -7248,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)
@@ -7492,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') {