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