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