From 1cd3762b0d7707f4dd665c00ff4d83db6172b4a7 Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Mon, 25 May 2015 18:51:33 +0200 Subject: [PATCH 1/7] Start integrating the Mailvelope browser extension via its API. - Embed Mailvelope message container for encrypted message text and full pgp-mime messages - Prepare to embed the Mailvelope editor for composing encrypted messages (with attachments) --- program/js/app.js | 141 ++++++++++++++++++++++++ program/localization/en_US/messages.inc | 1 + program/steps/mail/func.inc | 23 +++- skins/larry/mail.css | 9 ++ skins/larry/ui.js | 8 ++ 5 files changed, 181 insertions(+), 1 deletion(-) diff --git a/program/js/app.js b/program/js/app.js index dc5c8c191..4cb61532c 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -376,6 +376,8 @@ function rcube_webmail() 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(); @@ -3341,6 +3343,141 @@ function rcube_webmail() $('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_init(action); + } + else { + $(window).on('mailvelope', function() { + ref.mailvelope_init(action); + }); + } + }; + + // + this.mailvelope_init = 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; + }, function(err) { + // attempt to create a new keyring for this app/user + mailvelope.createKeyring(keyring).then(function(kr) { + ref.mailvelope_keyring = kr; + keyring = keyring.identifier; + }, function(err) { + console.error(err) + }); + }); + + 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.hide_message(msgkey); + ref.http_error(o, status, err, lock); + }, + success: function(data) { + ref.mailvelope_display_container(selector, data, keyring, msgid); + } + }); + } + } + else if (action == 'compose' && window.mailvelope) { + this.enable_command('compose-encrypted', true); + } + }; + + // handler for the 'compose-encrypt' command + this.compose_encrypted = function(props) + { + var container = $('#' + this.env.composebody).parent(); + mailvelope.createEditorContainer('#' + container.attr('id'), keyring).then(function(editor) { + ref.mailvelope_editor = editor; + container.addClass('mailvelope'); + $('#' + ref.env.composebody).hide(); + }); + }; + + // 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*/, ''); + console.log('*', val) + } + }); + + // check if we have keys for all recipients + var isvalid = recipients.length > 0; + ref.mailvelope_keyring.validKeyForAddress(recipients).then(function(status) { + $.each(status, function(k,v) { + console.log('validate', k, v) + if (!v) { + isvalid = false; + alert("No key found for "+k) + } + }); + + if (!isvalid) { + if (!recipients.length) + alert(ref.get_label('norecipientwarning')); + return false; + } + + ref.mailvelope_editor.encrypt(recipients).then(function(armored) { + console.log('encrypted message', armored); + var form = this.gui_objects.messageform; + + // all checks passed, send message + // var msgid = ref.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage') + + }, function(err) { + console.log(err) + }); + }); + + return false; + }; + + // wrapper for the mailvelope.createDisplayContainer API call + this.mailvelope_display_container = function(selector, data, keyring, msgid) + { + mailvelope.createDisplayContainer(selector, data, keyring, {}).then(function() { + $(selector).addClass('mailvelope').find('.message-part, .part-notice').hide(); + ref.hide_message(msgid); + setTimeout(function() { $(window).resize(); }, 10); + }, function(err) { + console.error(err) + ref.hide_message(msgid); + ref.display_message('Message decryption failed: ' + err.message, 'error') + }); + }; + + /*********************************************************/ /********* mailbox folders methods *********/ /*********************************************************/ @@ -3612,6 +3749,10 @@ function rcube_webmail() ); } + if (this.mailvelope_editor) { + return this.mailvelope_submit_messageform(draft, saveonly); + } + // all checks passed, send message var msgid = this.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage'), lang = this.spellcheck_lang(), diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc index a23bfd645..15e39c3e7 100644 --- a/program/localization/en_US/messages.inc +++ b/program/localization/en_US/messages.inc @@ -56,6 +56,7 @@ $messages['contactexists'] = 'A contact with the same e-mail address already exi $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['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/func.inc b/program/steps/mail/func.inc index 684cdf933..a90541d21 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -1185,7 +1185,23 @@ function rcmail_message_body($attrib) // 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; } @@ -1219,6 +1235,11 @@ function rcmail_message_body($attrib) 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/skins/larry/mail.css b/skins/larry/mail.css index effc35f1d..9d8ddcead 100644 --- a/skins/larry/mail.css +++ b/skins/larry/mail.css @@ -881,6 +881,10 @@ div.hide-headers { margin: 8px; } +#messagebody.mailvelope > iframe { + width: 99% !important; +} + #message-objects div, #messagebody span.part-notice { margin: 8px; @@ -1298,6 +1302,11 @@ div.message-partheaders .headers-table td.header { bottom: 42px; } +#composebodycontainer.mailvelope > iframe[scrolling='no'] { + position: relative; + top: -12px; +} + #composebody { position: absolute; top: 0; diff --git a/skins/larry/ui.js b/skins/larry/ui.js index 95efccf42..984185167 100644 --- a/skins/larry/ui.js +++ b/skins/larry/ui.js @@ -468,6 +468,14 @@ function rcube_mail_ui() $('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); + } } From 7b8a0af1c86ab74346e48ade7d5354dfbf69adbe Mon Sep 17 00:00:00 2001 From: Thomas Bruederli Date: Tue, 26 May 2015 22:53:08 +0200 Subject: [PATCH 2/7] Add toolbar button to switch to Mailvelope encryption editor --- program/js/app.js | 58 ++++++++++++++++--------- program/localization/en_US/labels.inc | 2 + program/localization/en_US/messages.inc | 1 + program/steps/mail/compose.inc | 2 +- skins/larry/styles.css | 8 ++++ skins/larry/templates/compose.html | 1 + 6 files changed, 51 insertions(+), 21 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index 4cb61532c..b43ca00d6 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -279,7 +279,7 @@ function rcube_webmail() this.env.address_group_stack = []; this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin', - 'insert-response', 'save-response', 'menu-open', 'menu-close']; + 'insert-response', 'save-response', 'menu-open', 'menu-close', 'compose-encrypted']; if (this.env.drafts_mailbox) this.env.compose_commands.push('savedraft') @@ -3366,13 +3366,13 @@ function rcube_webmail() mailvelope.getKeyring(keyring).then(function(kr) { ref.mailvelope_keyring = kr; - }, function(err) { + }).catch(function(err) { // attempt to create a new keyring for this app/user mailvelope.createKeyring(keyring).then(function(kr) { ref.mailvelope_keyring = kr; keyring = keyring.identifier; - }, function(err) { - console.error(err) + }).catch(function(err) { + console.error(err); }); }); @@ -3409,11 +3409,27 @@ function rcube_webmail() this.compose_encrypted = function(props) { var container = $('#' + this.env.composebody).parent(); - mailvelope.createEditorContainer('#' + container.attr('id'), keyring).then(function(editor) { - ref.mailvelope_editor = editor; - container.addClass('mailvelope'); - $('#' + ref.env.composebody).hide(); - }); + + // remove Mailvelope editor if active + if (ref.mailvelope_editor) { + ref.mailvelope_editor = null; + ref.set_button('compose-encrypted', 'act'); + container.removeClass('mailvelope') + .find('iframe:not([aria-hidden=true])').remove(); + $('#' + ref.env.composebody).show(); + } + // embed Mailvelope editor container + else { + var options = { predefinedText: $('#' + this.env.composebody).val() }; + mailvelope.createEditorContainer('#' + container.attr('id'), ref.mailvelope_keyring.identifier, options).then(function(editor) { + ref.mailvelope_editor = editor; + ref.set_button('compose-encrypted', 'sel'); + container.addClass('mailvelope'); + $('#' + ref.env.composebody).hide(); + }).catch(function(err) { + console.error(err); + }); + } }; // callback to replace the message body with the full armored @@ -3427,7 +3443,6 @@ function rcube_webmail() rcpt = RegExp.$2 recipients.push(rcpt); val = val.substr(val.indexOf(rcpt) + rcpt.length + 1).replace(/^\s*,\s*/, ''); - console.log('*', val) } }); @@ -3435,29 +3450,32 @@ function rcube_webmail() var isvalid = recipients.length > 0; ref.mailvelope_keyring.validKeyForAddress(recipients).then(function(status) { $.each(status, function(k,v) { - console.log('validate', k, v) - if (!v) { + if (v === false) { isvalid = false; - alert("No key found for "+k) + alert(ref.get_label('nopubkeyfor').replace('$email', k)); } }); if (!isvalid) { - if (!recipients.length) + if (!recipients.length) { alert(ref.get_label('norecipientwarning')); + $("[name='_to']").focus(); + } return false; } ref.mailvelope_editor.encrypt(recipients).then(function(armored) { console.log('encrypted message', armored); - var form = this.gui_objects.messageform; + var form = ref.gui_objects.messageform; // all checks passed, send message // var msgid = ref.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage') - }, function(err) { - console.log(err) + }).catch(function(err) { + console.log(err); }); + }).catch(function(err) { + console.error(err); }); return false; @@ -3466,12 +3484,12 @@ function rcube_webmail() // wrapper for the mailvelope.createDisplayContainer API call this.mailvelope_display_container = function(selector, data, keyring, msgid) { - mailvelope.createDisplayContainer(selector, data, keyring, {}).then(function() { + mailvelope.createDisplayContainer(selector, data, keyring, { showExternalContent: this.env.safemode }).then(function() { $(selector).addClass('mailvelope').find('.message-part, .part-notice').hide(); ref.hide_message(msgid); setTimeout(function() { $(window).resize(); }, 10); - }, function(err) { - console.error(err) + }).catch(function(err) { + console.error(err); ref.hide_message(msgid); ref.display_message('Message decryption failed: ' + err.message, 'error') }); diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index 8202b140d..dd9b7b8f4 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -247,6 +247,8 @@ $labels['selectimage'] = 'Select image'; $labels['addimage'] = 'Add image'; $labels['selectmedia'] = 'Select movie'; $labels['addmedia'] = 'Add movie'; +$labels['encrypt'] = 'Encrypt'; +$labels['encryptmessage'] = 'Encrypt message'; $labels['editidents'] = 'Edit identities'; $labels['spellcheck'] = 'Spell'; diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc index 15e39c3e7..2f712f388 100644 --- a/program/localization/en_US/messages.inc +++ b/program/localization/en_US/messages.inc @@ -57,6 +57,7 @@ $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['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 5009c525a..44f4612f2 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -83,7 +83,7 @@ $OUTPUT->add_label('nosubject', 'nosenderwarning', 'norecipientwarning', 'nosubj 'messagesaved', 'converting', 'editorwarning', 'searching', 'uploading', 'uploadingmany', 'fileuploaderror', 'sendmessage', 'newresponse', 'responsename', 'responsetext', 'save', 'savingresponse', 'restoresavedcomposedata', 'restoremessage', 'delete', 'restore', 'ignore', - 'selectimportfile', 'messageissent'); + 'selectimportfile', 'messageissent', 'nopubkeyfor'); $OUTPUT->set_pagetitle($RCMAIL->gettext('compose')); diff --git a/skins/larry/styles.css b/skins/larry/styles.css index 327fd37bb..e418e0a98 100644 --- a/skins/larry/styles.css +++ b/skins/larry/styles.css @@ -2008,6 +2008,14 @@ ul.proplist li { opacity: 0.4; } +.toolbar a.button.selected { + color: #1978a1; +} + +.toolbar a.button.hidden { + display: none; +} + .dropbutton { display: inline-block; position: relative; diff --git a/skins/larry/templates/compose.html b/skins/larry/templates/compose.html index 7a5fe42ea..0ee73395b 100644 --- a/skins/larry/templates/compose.html +++ b/skins/larry/templates/compose.html @@ -33,6 +33,7 @@