From 2965a981b7ec22866fbdf2d567d87e2d068d3617 Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Fri, 31 Jul 2015 16:04:08 -0400
Subject: [PATCH] Allow to search and import missing PGP pubkeys from keyservers using Publickey.js

---
 program/js/app.js |  383 +++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 346 insertions(+), 37 deletions(-)

diff --git a/program/js/app.js b/program/js/app.js
index 4cb6153..fcba219 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -3347,17 +3347,17 @@
   this.check_mailvelope = function(action)
   {
     if (typeof window.mailvelope !== 'undefined') {
-      this.mailvelope_init(action);
+      this.mailvelope_load(action);
     }
     else {
       $(window).on('mailvelope', function() {
-        ref.mailvelope_init(action);
+        ref.mailvelope_load(action);
       });
     }
   };
 
   // 
-  this.mailvelope_init = function(action)
+  this.mailvelope_load = function(action)
   {
     if (this.env.browser_capabilities)
       this.env.browser_capabilities['pgpmime'] = 1;
@@ -3366,16 +3366,21 @@
 
     mailvelope.getKeyring(keyring).then(function(kr) {
       ref.mailvelope_keyring = kr;
-    }, function(err) {
+      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;
-        keyring = keyring.identifier;
-      }, function(err) {
-        console.error(err)
+        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) {
@@ -3391,8 +3396,7 @@
           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.hide_message(msgkey);
-            ref.http_error(o, status, err, lock);
+            ref.http_error(o, status, err, msgid);
           },
           success: function(data) {
             ref.mailvelope_display_container(selector, data, keyring, msgid);
@@ -3401,19 +3405,79 @@
       }
     }
     else if (action == 'compose' && window.mailvelope) {
-      this.enable_command('compose-encrypted', true);
+      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-encrypt' command
+  // handler for the 'compose-encrypted' command
   this.compose_encrypted = function(props)
   {
     var container = $('#' + this.env.composebody).parent();
-    mailvelope.createEditorContainer('#' + container.attr('id'), keyring).then(function(editor) {
-      ref.mailvelope_editor = editor;
-      container.addClass('mailvelope');
-      $('#' + ref.env.composebody).hide();
-    });
+
+    // 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
@@ -3424,41 +3488,125 @@
     $.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
+        rcpt = RegExp.$2;
         recipients.push(rcpt);
         val = val.substr(val.indexOf(rcpt) + rcpt.length + 1).replace(/^\s*,\s*/, '');
-        console.log('*', val)
       }
     });
 
     // 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) {
-        console.log('validate', k, v)
-        if (!v) {
+        if (v === false) {
           isvalid = false;
-          alert("No key found for "+k)
+          missing_keys.push(k);
         }
       });
 
-      if (!isvalid) {
-        if (!recipients.length)
-          alert(ref.get_label('norecipientwarning'));
+      // 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;
       }
 
-      ref.mailvelope_editor.encrypt(recipients).then(function(armored) {
-        console.log('encrypted message', armored);
-        var form = this.gui_objects.messageform;
+      if (!isvalid) {
+        if (!recipients.length) {
+          alert(ref.get_label('norecipientwarning'));
+          $("[name='_to']").focus();
+        }
+        return false;
+      }
 
-        // all checks passed, send message
-        // var msgid = ref.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage')
-
-      }, function(err) {
-        console.log(err)
+      // 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;
   };
@@ -3466,15 +3614,170 @@
   // wrapper for the mailvelope.createDisplayContainer API call
   this.mailvelope_display_container = function(selector, data, keyring, msgid)
   {
-    mailvelope.createDisplayContainer(selector, data, keyring, {}).then(function() {
+    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);
-    }, function(err) {
-      console.error(err)
+    }).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);
+          });
+        });
+    });
+
   };
 
 
@@ -3749,6 +4052,7 @@
       );
     }
 
+    // delegate sending to Mailvelope routine
     if (this.mailvelope_editor) {
       return this.mailvelope_submit_messageform(draft, saveonly);
     }
@@ -4070,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();
       }
 
@@ -4140,6 +4444,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;
 

--
Gitblit v1.9.1