From f189d72943c490ae97323997aef8987bc58f3b5e Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Thu, 10 Sep 2015 15:27:58 -0400
Subject: [PATCH] Merged branch 'dev-mailvelope'

---
 program/js/common.js                    |    2 
 program/steps/mail/compose.inc          |   82 +++
 program/js/publickey.js                 |  456 ++++++++++++++++++++++
 program/steps/mail/sendmail.inc         |   49 ++
 program/localization/en_US/messages.inc |    6 
 program/steps/mail/func.inc             |   23 +
 program/localization/en_US/labels.inc   |    8 
 skins/larry/styles.css                  |   16 
 skins/larry/images/buttons.png          |    0 
 program/js/app.js                       |  452 ++++++++++++++++++++++
 skins/larry/mail.css                    |   67 +++
 skins/larry/templates/compose.html      |    1 
 skins/larry/ui.js                       |    8 
 13 files changed, 1,152 insertions(+), 18 deletions(-)

diff --git a/program/js/app.js b/program/js/app.js
index af48407..628fc19 100644
--- a/program/js/app.js
+++ b/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();
@@ -3346,6 +3348,444 @@
     $('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         *********/
   /*********************************************************/
@@ -3615,6 +4055,11 @@
           }
         }]
       );
+    }
+
+    // delegate sending to Mailvelope routine
+    if (this.mailvelope_editor) {
+      return this.mailvelope_submit_messageform(draft, saveonly);
     }
 
     // all checks passed, send message
@@ -3934,7 +4379,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();
       }
 
@@ -4004,6 +4449,11 @@
       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;
 
diff --git a/program/js/common.js b/program/js/common.js
index 7a0c303..329ffb5 100644
--- a/program/js/common.js
+++ b/program/js/common.js
@@ -448,7 +448,7 @@
       ],
       icann_addr = 'mailtest\\x40('+icann_domains.join('|')+')',
       word = '('+atom+'|'+quoted_string+')',
-      delim = '[,;\s\n]',
+      delim = '[,;\\s\\n]',
       local_part = word+'(\\x2e'+word+')*',
       addr_spec = '(('+local_part+'\\x40'+domain+')|('+icann_addr+'))',
       reg1 = inline ? new RegExp('(^|<|'+delim+')'+addr_spec+'($|>|'+delim+')', 'i') : new RegExp('^'+addr_spec+'$', 'i');
diff --git a/program/js/publickey.js b/program/js/publickey.js
new file mode 100644
index 0000000..41beccc
--- /dev/null
+++ b/program/js/publickey.js
@@ -0,0 +1,456 @@
+"use strict";
+
+(function(context){
+    /*
+        Default keyservers (HTTPS and CORS enabled)
+    */
+    var DEFAULT_KEYSERVERS = [
+        "https://keys.fedoraproject.org/",
+        "https://keybase.io/",
+    ];
+
+    /*
+        Initialization to create an PublicKey object.
+
+        Arguments:
+
+        * keyservers - Array of keyserver domains, default is:
+            ["https://keys.fedoraproject.org/", "https://keybase.io/"]
+
+        Examples:
+
+        //Initialize with the default keyservers
+        var hkp = new PublicKey();
+
+        //Initialize only with a specific keyserver
+        var hkp = new PublicKey(["https://key.ip6.li/"]);
+    */
+    var PublicKey = function(keyservers){
+        this.keyservers = keyservers || DEFAULT_KEYSERVERS;
+    };
+
+    /*
+        Get a public key from any keyserver based on keyId.
+
+        Arguments:
+
+        * keyId - String key id of the public key (this is usually a fingerprint)
+
+        * callback - Function that is called when finished. Two arguments are
+                passed to the callback: publicKey and errorCode. publicKey is
+                an ASCII armored OpenPGP public key. errorCode is the error code
+                (either HTTP status code or keybase error code) returned by the
+                last keyserver that was tried. If a publicKey was found,
+                errorCode is null. If no publicKey was found, publicKey is null
+                and errorCode is not null.
+
+        Examples:
+
+        //Get a valid public key
+        var hkp = new PublicKey();
+        hkp.get("F75BE4E6EF6E9DD203679E94E7F6FAD172EFEE3D", function(publicKey, errorCode){
+            errorCode !== null ? console.log(errorCode) : console.log(publicKey);
+        });
+
+        //Try to get an invalid public key
+        var hkp = new PublicKey();
+        hkp.get("bogus_id", function(publicKey, errorCode){
+            errorCode !== null ? console.log(errorCode) : console.log(publicKey);
+        });
+    */
+    PublicKey.prototype.get = function(keyId, callback, keyserverIndex, err){
+        //default starting point is at the first keyserver
+        if(keyserverIndex === undefined){
+            keyserverIndex = 0;
+        }
+
+        //no more keyservers to check, so no key found
+        if(keyserverIndex >= this.keyservers.length){
+            return callback(null, err || 404);
+        }
+
+        //set the keyserver to try next
+        var ks = this.keyservers[keyserverIndex];
+        var _this = this;
+
+        //special case for keybase
+        if(ks.indexOf("https://keybase.io/") === 0){
+
+            //don't need 0x prefix for keybase searches
+            if(keyId.indexOf("0x") === 0){
+                keyId = keyId.substr(2);
+            }
+
+            //request the public key from keybase
+            var xhr = new XMLHttpRequest();
+            xhr.open("get", "https://keybase.io/_/api/1.0/user/lookup.json" +
+                "?fields=public_keys&key_fingerprint=" + keyId);
+            xhr.onload = function(){
+                if(xhr.status === 200){
+                    var result = JSON.parse(xhr.responseText);
+
+                    //keybase error returns HTTP 200 status, which is silly
+                    if(result['status']['code'] !== 0){
+                        return _this.get(keyId, callback, keyserverIndex + 1, result['status']['code']);
+                    }
+
+                    //no public key found
+                    if(result['them'].length === 0){
+                        return _this.get(keyId, callback, keyserverIndex + 1, 404);
+                    }
+
+                    //found the public key
+                    var publicKey = result['them'][0]['public_keys']['primary']['bundle'];
+                    return callback(publicKey, null);
+                }
+                else{
+                    return _this.get(keyId, callback, keyserverIndex + 1, xhr.status);
+                }
+            };
+            xhr.send();
+        }
+
+        //normal HKP keyserver
+        else{
+            //add the 0x prefix if absent
+            if(keyId.indexOf("0x") !== 0){
+                keyId = "0x" + keyId;
+            }
+
+            //request the public key from the hkp server
+            var xhr = new XMLHttpRequest();
+            xhr.open("get", ks + "pks/lookup?op=get&options=mr&search=" + keyId);
+            xhr.onload = function(){
+                if(xhr.status === 200){
+                    return callback(xhr.responseText, null);
+                }
+                else{
+                    return _this.get(keyId, callback, keyserverIndex + 1, xhr.status);
+                }
+            };
+            xhr.send();
+        }
+    };
+
+    /*
+        Search for a public key in the keyservers.
+
+        Arguments:
+
+        * query - String to search for (usually an email, name, or username).
+
+        * callback - Function that is called when finished. Two arguments are
+                passed to the callback: results and errorCode. results is an
+                Array of users that were returned by the search. errorCode is
+                the error code (either HTTP status code or keybase error code)
+                returned by the last keyserver that was tried. If any results
+                were found, errorCode is null. If no results are found, results
+                is null and errorCode is not null.
+
+        Examples:
+
+        //Search for diafygi's key id
+        var hkp = new PublicKey();
+        hkp.search("diafygi", function(results, errorCode){
+            errorCode !== null ? console.log(errorCode) : console.log(results);
+        });
+
+        //Search for a nonexistent key id
+        var hkp = new PublicKey();
+        hkp.search("doesntexist123", function(results, errorCode){
+            errorCode !== null ? console.log(errorCode) : console.log(results);
+        });
+    */
+    PublicKey.prototype.search = function(query, callback, keyserverIndex, results, err){
+        //default starting point is at the first keyserver
+        if(keyserverIndex === undefined){
+            keyserverIndex = 0;
+        }
+
+        //initialize the results array
+        if(results === undefined){
+            results = [];
+        }
+
+        //no more keyservers to check
+        if(keyserverIndex >= this.keyservers.length){
+
+            //return error if no results
+            if(results.length === 0){
+                return callback(null, err || 404);
+            }
+
+            //return results
+            else{
+
+                //merge duplicates
+                var merged = {};
+                for(var i = 0; i < results.length; i++){
+                    var k = results[i];
+
+                    //see if there's duplicate key ids to merge
+                    if(merged[k['keyid']] !== undefined){
+
+                        for(var u = 0; u < k['uids'].length; u++){
+                            var has_this_uid = false;
+
+                            for(var m = 0; m < merged[k['keyid']]['uids'].length; m++){
+                                if(merged[k['keyid']]['uids'][m]['uid'] === k['uids'][u]){
+                                    has_this_uid = true;
+                                    break;
+                                }
+                            }
+
+                            if(!has_this_uid){
+                                merged[k['keyid']]['uids'].push(k['uids'][u])
+                            }
+                        }
+                    }
+
+                    //no duplicate found, so add it to the dict
+                    else{
+                        merged[k['keyid']] = k;
+                    }
+                }
+
+                //return a list of the merged results in the same order
+                var merged_list = [];
+                for(var i = 0; i < results.length; i++){
+                    var k = results[i];
+                    if(merged[k['keyid']] !== undefined){
+                        merged_list.push(merged[k['keyid']]);
+                        delete(merged[k['keyid']]);
+                    }
+                }
+                return callback(merged_list, null);
+            }
+        }
+
+        //set the keyserver to try next
+        var ks = this.keyservers[keyserverIndex];
+        var _this = this;
+
+        //special case for keybase
+        if(ks.indexOf("https://keybase.io/") === 0){
+
+            //request a list of users from keybase
+            var xhr = new XMLHttpRequest();
+            xhr.open("get", "https://keybase.io/_/api/1.0/user/autocomplete.json?q=" + encodeURIComponent(query));
+            xhr.onload = function(){
+                if(xhr.status === 200){
+                    var kb_json = JSON.parse(xhr.responseText);
+
+                    //keybase error returns HTTP 200 status, which is silly
+                    if(kb_json['status']['code'] !== 0){
+                        return _this.search(query, callback, keyserverIndex + 1, results, kb_json['status']['code']);
+                    }
+
+                    //no public key found
+                    if(kb_json['completions'].length === 0){
+                        return _this.search(query, callback, keyserverIndex + 1, results, 404);
+                    }
+
+                    //compose keybase user results
+                    var kb_results = [];
+                    for(var i = 0; i < kb_json['completions'].length; i++){
+                        var user = kb_json['completions'][i]['components'];
+
+                        //skip if no public key fingerprint
+                        if(user['key_fingerprint'] === undefined){
+                            continue;
+                        }
+
+                        //build keybase user result
+                        var kb_result = {
+                            "keyid": user['key_fingerprint']['val'].toUpperCase(),
+                            "href": "https://keybase.io/" + user['username']['val'] + "/key.asc",
+                            "info": "https://keybase.io/" + user['username']['val'],
+                            "algo": user['key_fingerprint']['algo'],
+                            "keylen": user['key_fingerprint']['nbits'],
+                            "creationdate": null,
+                            "expirationdate": null,
+                            "revoked": false,
+                            "disabled": false,
+                            "expired": false,
+                            "uids": [{
+                                "uid": user['username']['val'] +
+                                    " on Keybase <https://keybase.io/" +
+                                    user['username']['val'] + ">",
+                                "creationdate": null,
+                                "expirationdate": null,
+                                "revoked": false,
+                                "disabled": false,
+                                "expired": false,
+                            }]
+                        };
+
+                        //add full name
+                        if(user['full_name'] !== undefined){
+                            kb_result['uids'].push({
+                                "uid": "Full Name: " + user['full_name']['val'],
+                                "creationdate": null,
+                                "expirationdate": null,
+                                "revoked": false,
+                                "disabled": false,
+                                "expired": false,
+                            });
+                        }
+
+                        //add twitter
+                        if(user['twitter'] !== undefined){
+                            kb_result['uids'].push({
+                                "uid": user['twitter']['val'] +
+                                    " on Twitter <https://twitter.com/" +
+                                    user['twitter']['val'] + ">",
+                                "creationdate": null,
+                                "expirationdate": null,
+                                "revoked": false,
+                                "disabled": false,
+                                "expired": false,
+                            });
+                        }
+
+                        //add github
+                        if(user['github'] !== undefined){
+                            kb_result['uids'].push({
+                                "uid": user['github']['val'] +
+                                    " on Github <https://github.com/" +
+                                    user['github']['val'] + ">",
+                                "creationdate": null,
+                                "expirationdate": null,
+                                "revoked": false,
+                                "disabled": false,
+                                "expired": false,
+                            });
+                        }
+
+                        //add reddit
+                        if(user['reddit'] !== undefined){
+                            kb_result['uids'].push({
+                                "uid": user['reddit']['val'] +
+                                    " on Github <https://reddit.com/u/" +
+                                    user['reddit']['val'] + ">",
+                                "creationdate": null,
+                                "expirationdate": null,
+                                "revoked": false,
+                                "disabled": false,
+                                "expired": false,
+                            });
+                        }
+
+                        //add hackernews
+                        if(user['hackernews'] !== undefined){
+                            kb_result['uids'].push({
+                                "uid": user['hackernews']['val'] +
+                                    " on Hacker News <https://news.ycombinator.com/user?id=" +
+                                    user['hackernews']['val'] + ">",
+                                "creationdate": null,
+                                "expirationdate": null,
+                                "revoked": false,
+                                "disabled": false,
+                                "expired": false,
+                            });
+                        }
+
+                        //add coinbase
+                        if(user['coinbase'] !== undefined){
+                            kb_result['uids'].push({
+                                "uid": user['coinbase']['val'] +
+                                    " on Coinbase <https://www.coinbase.com/" +
+                                    user['coinbase']['val'] + ">",
+                                "creationdate": null,
+                                "expirationdate": null,
+                                "revoked": false,
+                                "disabled": false,
+                                "expired": false,
+                            });
+                        }
+
+                        //add websites
+                        if(user['websites'] !== undefined){
+                            for(var w = 0; w < user['websites'].length; w++){
+                                kb_result['uids'].push({
+                                    "uid": "Owns " + user['websites'][w]['val'],
+                                    "creationdate": null,
+                                    "expirationdate": null,
+                                    "revoked": false,
+                                    "disabled": false,
+                                    "expired": false,
+                                });
+                            }
+                        }
+
+                        kb_results.push(kb_result);
+                    }
+
+                    results = results.concat(kb_results);
+                    return _this.search(query, callback, keyserverIndex + 1, results, null);
+                }
+                else{
+                    return _this.search(query, callback, keyserverIndex + 1, results, xhr.status);
+                }
+            };
+            xhr.send();
+        }
+
+        //normal HKP keyserver
+        else{
+            var xhr = new XMLHttpRequest();
+            xhr.open("get", ks + "pks/lookup?op=index&options=mr&fingerprint=on&search=" + encodeURIComponent(query));
+            xhr.onload = function(){
+                if(xhr.status === 200){
+                    var ks_results = [];
+                    var raw = xhr.responseText.split("\n");
+                    var curKey = undefined;
+                    for(var i = 0; i < raw.length; i++){
+                        var line = raw[i].trim();
+
+                        //pub:<keyid>:<algo>:<keylen>:<creationdate>:<expirationdate>:<flags>
+                        if(line.indexOf("pub:") == 0){
+                            if(curKey !== undefined){
+                                ks_results.push(curKey);
+                            }
+                            var vals = line.split(":");
+                            curKey = {
+                                "keyid": vals[1],
+                                "href": ks + "pks/lookup?op=get&options=mr&search=0x" + vals[1],
+                                "info": ks + "pks/lookup?op=vindex&search=0x" + vals[1],
+                                "algo": vals[2] === "" ? null : parseInt(vals[2]),
+                                "keylen": vals[3] === "" ? null : parseInt(vals[3]),
+                                "creationdate": vals[4] === "" ? null : parseInt(vals[4]),
+                                "expirationdate": vals[5] === "" ? null : parseInt(vals[5]),
+                                "revoked": vals[6].indexOf("r") !== -1,
+                                "disabled": vals[6].indexOf("d") !== -1,
+                                "expired": vals[6].indexOf("e") !== -1,
+                                "uids": [],
+                            }
+                        }
+
+                        //uid:<escaped uid string>:<creationdate>:<expirationdate>:<flags>
+                        if(line.indexOf("uid:") == 0){
+                            var vals = line.split(":");
+                            curKey['uids'].push({
+                                "uid": decodeURIComponent(vals[1]),
+                                "creationdate": vals[2] === "" ? null : parseInt(vals[2]),
+                                "expirationdate": vals[3] === "" ? null : parseInt(vals[3]),
+                                "revoked": vals[4].indexOf("r") !== -1,
+                                "disabled": vals[4].indexOf("d") !== -1,
+                                "expired": vals[4].indexOf("e") !== -1,
+                            });
+                        }
+                    }
+                    ks_results.push(curKey);
+
+                    results = results.concat(ks_results);
+                    return _this.search(query, callback, keyserverIndex + 1, results, null);
+                }
+                else{
+                    return _this.search(query, callback, keyserverIndex + 1, results, xhr.status);
+                }
+            };
+            xhr.send();
+        }
+    };
+
+    context.PublicKey = PublicKey;
+})(typeof exports === "undefined" ? this : exports);
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index 90d2c30..b102041 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -247,6 +247,14 @@
 $labels['addimage']       = 'Add image';
 $labels['selectmedia']    = 'Select movie';
 $labels['addmedia']       = 'Add movie';
+$labels['encrypt']        = 'Encrypt';
+$labels['encryptmessage'] = 'Encrypt message';
+$labels['importpubkeys']  = 'Import public keys';
+$labels['encryptedsendialog'] = 'Sending encrypted message';
+$labels['keyid']          = 'Key ID';
+$labels['keylength']      = 'Bits';
+$labels['keyexpired']     = 'Expired';
+$labels['keyrevoked']     = 'Revoked';
 
 $labels['editidents']    = 'Edit identities';
 $labels['spellcheck']    = 'Spell';
diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc
index a8ebf88..d7bef0e 100644
--- a/program/localization/en_US/messages.inc
+++ b/program/localization/en_US/messages.inc
@@ -56,6 +56,12 @@
 $messages['contactnameexists'] = 'A contact with the same name already exists.';
 $messages['blockedimages'] = 'To protect your privacy, remote images are blocked in this message.';
 $messages['encryptedmessage'] = 'This is an encrypted message and can not be displayed. Sorry!';
+$messages['externalmessagedecryption'] = 'This is an encrypted message and can be decrypted with your browser extension.';
+$messages['nopubkeyfor'] = 'No valid public key found for $email';
+$messages['nopubkeyforsender'] = 'No valid public key found for your sender identity. Do you want to encrypt the message for the recipients only?';
+$messages['encryptnoattachments'] = 'Already uploaded attachments cannot be encrypted. Please re-add them in the encryption editor.';
+$messages['searchpubkeyservers'] = 'Do you want to search public key servers for the missing keys?';
+$messages['encryptpubkeysfound'] = 'The following public keys have been found:';
 $messages['nocontactsfound'] = 'No contacts found.';
 $messages['contactnotfound'] = 'The requested contact was not found.';
 $messages['contactsearchonly'] = 'Enter some search terms to find contacts';
diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc
index 488b82e..c5450c5 100644
--- a/program/steps/mail/compose.inc
+++ b/program/steps/mail/compose.inc
@@ -85,7 +85,10 @@
     'messagesaved', 'converting', 'editorwarning', 'searching', 'uploading', 'uploadingmany',
     'fileuploaderror', 'sendmessage', 'newresponse', 'responsename', 'responsetext', 'save',
     'savingresponse', 'restoresavedcomposedata', 'restoremessage', 'delete', 'restore', 'ignore',
-    'selectimportfile', 'messageissent');
+    'selectimportfile', 'messageissent', 'loadingdata', 'nopubkeyfor', 'nopubkeyforsender',
+    'encryptnoattachments','encryptedsendialog','searchpubkeyservers', 'importpubkeys',
+    'encryptpubkeysfound',  'search', 'close', 'import', 'keyid', 'keylength', 'keyexpired',
+    'keyrevoked');
 
 $OUTPUT->set_pagetitle($RCMAIL->gettext('compose'));
 
@@ -525,6 +528,8 @@
             if (!empty($sql_arr['bcc'])) {
                 $identities[$identity_id]['bcc'] = $sql_arr['bcc'];
             }
+
+            $identities[$identity_id]['email'] = $sql_arr['email'];
         }
 
         $out = $select_from->show($MESSAGE->compose['from']);
@@ -775,6 +780,21 @@
             }
 
             foreach ($MESSAGE->parts as $part) {
+                if ($part->realtype == 'multipart/encrypted') {
+                    // find the encrypted message payload part
+                    foreach ($MESSAGE->mime_parts as $mime_id => $mpart) {
+                        if ($mpart->mimetype == 'application/octet-stream' || !empty($mpart->filename)) {
+                            $RCMAIL->output->set_env('pgp_mime_message', array(
+                                '_mbox' => $RCMAIL->storage->get_folder(), '_uid' => $MESSAGE->uid, '_part' => $mime_id,
+                            ));
+                            $RCMAIL->output->set_env('compose_mode', $compose_mode);
+                            $MESSAGE->pgp_mime = true;
+                            break;
+                        }
+                    }
+                    continue;
+                }
+
                 // skip no-content and attachment parts (#1488557)
                 if ($part->type != 'content' || !$part->size || $MESSAGE->is_attachment($part)) {
                     continue;
@@ -797,14 +817,21 @@
         }
 
         // compose reply-body
-        if ($COMPOSE['mode'] == RCUBE_COMPOSE_REPLY)
+        if ($COMPOSE['mode'] == RCUBE_COMPOSE_REPLY) {
             $body = rcmail_create_reply_body($body, $isHtml);
+
+            if ($MESSAGE->pgp_mime) {
+                $RCMAIL->output->set_env('compose_reply_header', rcmail_get_reply_header($MESSAGE));
+            }
+        }
         // forward message body inline
-        else if ($COMPOSE['mode'] == RCUBE_COMPOSE_FORWARD)
+        else if ($COMPOSE['mode'] == RCUBE_COMPOSE_FORWARD) {
             $body = rcmail_create_forward_body($body, $isHtml);
+        }
         // load draft message body
-        else if ($COMPOSE['mode'] == RCUBE_COMPOSE_DRAFT || $COMPOSE['mode'] == RCUBE_COMPOSE_EDIT)
+        else if ($COMPOSE['mode'] == RCUBE_COMPOSE_DRAFT || $COMPOSE['mode'] == RCUBE_COMPOSE_EDIT) {
             $body = rcmail_create_draft_body($body, $isHtml);
+        }
     }
     else { // new message
         $isHtml = rcmail_compose_editor_mode();
@@ -834,7 +861,7 @@
 
 function rcmail_compose_part_body($part, $isHtml = false)
 {
-    global $RCMAIL, $MESSAGE, $LINE_LENGTH;
+    global $RCMAIL, $COMPOSE, $MESSAGE, $LINE_LENGTH;
 
     // Check if we have enough memory to handle the message in it
     // #1487424: we need up to 10x more memory than the body
@@ -848,6 +875,14 @@
     // message is cached but not exists (#1485443), or other error
     if ($body === false) {
         return '';
+    }
+
+    // register this part as pgp encrypted
+    if (strpos($body, 'BEGIN PGP MESSAGE') !== false) {
+        $MESSAGE->pgp_mime = true;
+        $RCMAIL->output->set_env('pgp_mime_message', array(
+            '_mbox' => $RCMAIL->storage->get_folder(), '_uid' => $MESSAGE->uid, '_part' => $part->mime_id,
+        ));
     }
 
     if ($isHtml) {
@@ -1015,16 +1050,7 @@
 {
     global $RCMAIL, $MESSAGE, $LINE_LENGTH;
 
-    // build reply prefix
-    $from = array_pop(rcube_mime::decode_address_list($MESSAGE->get_header('from'), 1, false, $MESSAGE->headers->charset));
-    $prefix = $RCMAIL->gettext(array(
-        'name' => 'mailreplyintro',
-        'vars' => array(
-            'date'   => $RCMAIL->format_date($MESSAGE->headers->date, $RCMAIL->config->get('date_long')),
-            'sender' => $from['name'] ? $from['name'] : rcube_utils::idn_to_utf8($from['mailto']),
-        )
-    ));
-
+    $prefix = rcmail_get_reply_header($MESSAGE);
     $reply_mode = intval($RCMAIL->config->get('reply_mode'));
 
     if (!$bodyIsHtml) {
@@ -1068,6 +1094,19 @@
     return $prefix . $body . $suffix;
 }
 
+function rcmail_get_reply_header($message)
+{
+    global $RCMAIL;
+
+    $from = array_pop(rcube_mime::decode_address_list($message->get_header('from'), 1, false, $message->headers->charset));
+    return $RCMAIL->gettext(array(
+        'name' => 'mailreplyintro',
+        'vars' => array(
+            'date'   => $RCMAIL->format_date($message->headers->date, $RCMAIL->config->get('date_long')),
+            'sender' => $from['name'] ?: rcube_utils::idn_to_utf8($from['mailto']),
+        )
+    ));
+}
 
 function rcmail_create_forward_body($body, $bodyIsHtml)
 {
@@ -1211,6 +1250,10 @@
     $cid_map  = array();
     $messages = array();
 
+    if ($message->pgp_mime) {
+        return $cid_map;
+    }
+
     foreach ((array)$message->mime_parts as $pid => $part) {
         if ($part->disposition == 'attachment' || ($part->disposition == 'inline' && $bodyIsHtml) || $part->filename) {
             // skip parts that aren't valid attachments
@@ -1277,6 +1320,11 @@
     global $RCMAIL, $COMPOSE;
 
     $cid_map = array();
+
+    if ($message->pgp_mime) {
+        return $cid_map;
+    }
+
     foreach ((array)$message->mime_parts as $pid => $part) {
         if (($part->content_id || $part->content_location) && $part->filename) {
             if ($attachment = rcmail_save_attachment($message, $pid)) {
@@ -1304,6 +1352,10 @@
     $names   = array();
     $refs    = array();
 
+    if ($MESSAGE->pgp_mime) {
+        return;
+    }
+
     $loaded_attachments = array();
     foreach ((array)$COMPOSE['attachments'] as $attachment) {
         $loaded_attachments[$attachment['name'] . $attachment['mimetype']] = $attachment;
diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
index 909b870..09cf6ed 100644
--- a/program/steps/mail/func.inc
+++ b/program/steps/mail/func.inc
@@ -1193,7 +1193,23 @@
                 // unsupported (e.g. encrypted)
                 if ($part->realtype) {
                     if ($part->realtype == 'multipart/encrypted' || $part->realtype == 'application/pkcs7-mime') {
-                        $out .= html::span('part-notice', $RCMAIL->gettext('encryptedmessage'));
+                        if (!empty($_SESSION['browser_caps']['pgpmime']) && $part->realtype == 'multipart/encrypted') {
+                            // find the encrypted message payload part
+                            foreach ($MESSAGE->mime_parts as $mime_id => $mpart) {
+                                if ($mpart->mimetype == 'application/octet-stream' || !empty($mpart->filename)) {
+                                    $out .= html::span('part-notice', $RCMAIL->gettext('externalmessagedecryption'));
+                                    $OUTPUT->set_env('pgp_mime_part', $mime_id);
+                                    $OUTPUT->set_env('pgp_mime_container', '#' . $attrib['id']);
+                                    $OUTPUT->add_label('loadingdata');
+                                    $MESSAGE->encrypted_part = $mime_id;
+                                    break;
+                                }
+                            }
+                        }
+
+                        if (!$MESSAGE->encrypted_part) {
+                            $out .= html::span('part-notice', $RCMAIL->gettext('encryptedmessage'));
+                        }
                     }
                     continue;
                 }
@@ -1227,6 +1243,11 @@
                     rcmail_message_error($MESSAGE->uid);
                 }
 
+                // check if the message body is PGP encrypted
+                if (strpos($body, 'BEGIN PGP MESSAGE') !== false) {
+                    $OUTPUT->set_env('is_pgp_content', '#' . $attrib['id']);
+                }
+
                 $plugin = $RCMAIL->plugins->exec_hook('message_body_prefix',
                     array('part' => $part, 'prefix' => ''));
 
diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc
index 6866c51..bb32f6e 100644
--- a/program/steps/mail/sendmail.inc
+++ b/program/steps/mail/sendmail.inc
@@ -246,6 +246,18 @@
 // fetch message body
 $message_body = rcube_utils::get_input_value('_message', rcube_utils::INPUT_POST, TRUE, $message_charset);
 
+if (isset($_POST['_pgpmime'])) {
+    $pgp_mime = rcube_utils::get_input_value('_pgpmime', rcube_utils::INPUT_POST);
+    $message_body = 'This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)';
+    $isHtml = false;
+
+    // clear unencrypted attachments
+    foreach ($COMPOSE['attachments'] as $attach) {
+        $RCMAIL->plugins->exec_hook('attachment_delete', $attach);
+    }
+    $COMPOSE['attachments'] = array();
+}
+
 if ($isHtml) {
     $bstyle = array();
 
@@ -475,6 +487,43 @@
     $text_charset .= ";\r\n format=flowed";
 }
 
+// compose PGP/Mime message
+if ($pgp_mime) {
+    $MAIL_MIME->addAttachment(
+        'Version: 1',
+        'application/pgp-encrypted',
+        'version.txt',  // required by Mail_mime::addAttachment()
+        false,
+        '8bit',
+        '',    // $disposition
+        '',    // $charset
+        '',    // $language
+        '',    // $location
+        null,  // $n_encoding
+        null,  // $f_encoding
+        'PGP/MIME version identification'
+    );
+
+    // patch filename out of the version part
+    foreach ($MAIL_MIME->_parts as $_i => $_part) {
+        if ($_part['c_type'] == 'application/pgp-encrypted') {
+            $MAIL_MIME->_parts[$_i]['name'] = '';
+            break;
+        }
+    }
+
+    $MAIL_MIME->addAttachment(
+        $pgp_mime,
+        'application/octet-stream',
+        'encrypted.asc',
+        false,
+        '8bit',
+        'inline'
+    );
+
+    $MAIL_MIME->setContentType('multipart/encrypted', array('protocol' => "application/pgp-encrypted"));
+}
+
 // encoding settings for mail composing
 $MAIL_MIME->setParam('text_encoding', $transfer_encoding);
 $MAIL_MIME->setParam('html_encoding', 'quoted-printable');
diff --git a/skins/larry/images/buttons.png b/skins/larry/images/buttons.png
index 713ac16..bd3e29d 100644
--- a/skins/larry/images/buttons.png
+++ b/skins/larry/images/buttons.png
Binary files differ
diff --git a/skins/larry/mail.css b/skins/larry/mail.css
index e6e2dd0..6bb8723 100644
--- a/skins/larry/mail.css
+++ b/skins/larry/mail.css
@@ -880,6 +880,10 @@
 	margin: 8px;
 }
 
+#messagebody.mailvelope > iframe {
+	width: 99% !important;
+}
+
 #message-objects div,
 #messagebody span.part-notice {
 	margin: 8px;
@@ -1297,6 +1301,16 @@
 	bottom: 42px;
 }
 
+#composebodycontainer.mailvelope {
+	right: 0;
+	z-index: 10;
+}
+
+#composebodycontainer.mailvelope > iframe[scrolling='no'] {
+	position: relative;
+	top: -12px;
+}
+
 #composebody {
 	position: absolute;
 	top: 0;
@@ -1383,3 +1397,56 @@
 #uploadform form div {
 	margin: 4px 0;
 }
+
+.mailvelopekeyimport div.key {
+	position: relative;
+	margin-bottom: 2px;
+	padding: 1em;
+	background-color: #ebebeb;
+}
+
+.mailvelopekeyimport div.key.revoked,
+.mailvelopekeyimport div.key.disabled {
+	color: #a0a0a0;
+}
+
+.mailvelopekeyimport div.key label {
+	display: inline-block;
+	margin-right: 0.5em;
+}
+
+.mailvelopekeyimport div.key label:after {
+	content: ":";
+}
+
+.mailvelopekeyimport div.key label + a,
+.mailvelopekeyimport div.key label + span {
+	display: inline-block;
+	margin-right: 2em;
+	white-space: nowrap;
+}
+
+.mailvelopekeyimport div.key label + a {
+	font-weight: bold;
+}
+
+.mailvelopekeyimport ul.uids {
+	margin: 1em 0 0 0;
+	padding: 0;
+}
+
+.mailvelopekeyimport li.uid {
+	border: 0;
+	padding: 0.3em;
+}
+
+.mailvelopekeyimport div.key input.button.importkey {
+	position: absolute;
+	top: 0.8em;
+	right: 0.8em;
+	padding: 4px 6px;
+}
+
+.mailvelopekeyimport div.key input.button[disabled] {
+	display: none;
+}
diff --git a/skins/larry/styles.css b/skins/larry/styles.css
index acb97a7..63b437b 100644
--- a/skins/larry/styles.css
+++ b/skins/larry/styles.css
@@ -2017,6 +2017,14 @@
 	opacity: 0.4;
 }
 
+.toolbar a.button.selected {
+	color: #1978a1;
+}
+
+.toolbar a.button.hidden {
+	display: none;
+}
+
 .dropbutton {
 	display: inline-block;
 	position: relative;
@@ -2161,6 +2169,14 @@
 	background-position: center -1932px;
 }
 
+.toolbar a.button.encrypt {
+	background-position: center -2025px;
+}
+
+.toolbar a.button.encrypt.selected {
+	background-position: center -2065px;
+}
+
 a.menuselector {
 	display: inline-block;
 	border: 1px solid #ababab;
diff --git a/skins/larry/templates/compose.html b/skins/larry/templates/compose.html
index 7a5fe42..0ee7339 100644
--- a/skins/larry/templates/compose.html
+++ b/skins/larry/templates/compose.html
@@ -33,6 +33,7 @@
 	<roundcube:button name="addattachment" type="link" class="button attach" label="attach" title="addattachment" onclick="UI.show_uploadform(event);return false" aria-haspopup="true" aria-expanded="false"tabindex="2" />
 	<roundcube:button command="insert-sig" type="link" class="button insertsig disabled" classAct="button insertsig" label="signature" title="insertsignature" tabindex="2" />
 	<a href="#responses" class="button responses" label="responses" title="<roundcube:label name='insertresponse' />" id="responsesmenulink" unselectable="on" onmousedown="return false" onclick="UI.toggle_popup('responsesmenu',event);return false" tabindex="2" aria-haspopup="true" aria-expanded="false" aria-owns="textresponsesmenu"><roundcube:label name="responses" /></a>
+	<roundcube:button command="compose-encrypted" type="link" class="button encrypt hidden" classAct="button encrypt" classSel="button encrypt selected" label="encrypt" title="encryptmessage" tabindex="2" />
 	<roundcube:container name="toolbar" id="compose-toolbar" />
 </div>
 
diff --git a/skins/larry/ui.js b/skins/larry/ui.js
index 0171a19..092f108 100644
--- a/skins/larry/ui.js
+++ b/skins/larry/ui.js
@@ -467,6 +467,14 @@
       $('div.rightcol').hide().attr('aria-hidden', 'true');
       $('div.leftcol').css('margin-right', '0');
     }
+
+    var mvlpe = $('#messagebody.mailvelope');
+    if (mvlpe.length) {
+      var h = $('#messagecontent').length ?
+        $('#messagecontent').height() - 16 :
+        $(window).height() - mvlpe.offset().top - 10;
+      mvlpe.height(h);
+    }
   }
 
 

--
Gitblit v1.9.1