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/steps/mail/compose.inc | 4
program/localization/en_US/messages.inc | 4
program/localization/en_US/labels.inc | 6
program/js/publickey.js | 456 ++++++++++++++++++++++++++++++++++++++
program/js/app.js | 195 ++++++++++++++++
skins/larry/mail.css | 53 ++++
6 files changed, 714 insertions(+), 4 deletions(-)
diff --git a/program/js/app.js b/program/js/app.js
index 598e74e..fcba219 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -3488,7 +3488,7 @@
$.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*/, '');
}
@@ -3497,12 +3497,48 @@
// 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;
- alert(ref.get_label('nopubkeyfor').replace('$email', k));
+ 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) {
@@ -3589,6 +3625,161 @@
});
};
+ // 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 *********/
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 dd9b7b8..079a184 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -249,6 +249,12 @@
$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 acb4282..2bcd3a6 100644
--- a/program/localization/en_US/messages.inc
+++ b/program/localization/en_US/messages.inc
@@ -59,7 +59,9 @@
$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'] = 'Alraady uploaded attachments cannot be encrypted. Please re-add them in the encryption editor.';
+$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 6989c3a..9e1b988 100644
--- a/program/steps/mail/compose.inc
+++ b/program/steps/mail/compose.inc
@@ -84,7 +84,9 @@
'fileuploaderror', 'sendmessage', 'newresponse', 'responsename', 'responsetext', 'save',
'savingresponse', 'restoresavedcomposedata', 'restoremessage', 'delete', 'restore', 'ignore',
'selectimportfile', 'messageissent', 'loadingdata', 'nopubkeyfor', 'nopubkeyforsender',
- 'encryptnoattachments');
+ 'encryptnoattachments','encryptedsendialog','searchpubkeyservers', 'importpubkeys',
+ 'encryptpubkeysfound', 'search', 'close', 'import', 'keyid', 'keylength', 'keyexpired',
+ 'keyrevoked');
$OUTPUT->set_pagetitle($RCMAIL->gettext('compose'));
diff --git a/skins/larry/mail.css b/skins/larry/mail.css
index 47d2f48..454428a 100644
--- a/skins/larry/mail.css
+++ b/skins/larry/mail.css
@@ -1398,3 +1398,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;
+}
--
Gitblit v1.9.1