From fe8ff85d7eabb370d85fc31ca55b9a6c6f86a356 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Thu, 15 Oct 2015 02:58:06 -0400 Subject: [PATCH] Move skin-specific code for compose encryption button to the skin --- program/js/app.js | 979 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 files changed, 840 insertions(+), 139 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index 56d07f3..f05677a 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -6,8 +6,8 @@ * @licstart The following is the entire license notice for the * JavaScript code in this file. * - * Copyright (C) 2005-2014, The Roundcube Dev Team - * Copyright (C) 2011-2014, Kolab Systems AG + * Copyright (C) 2005-2015, The Roundcube Dev Team + * Copyright (C) 2011-2015, Kolab Systems AG * * The JavaScript code in this page is free software: you can * redistribute it and/or modify it under the terms of the GNU @@ -77,7 +77,7 @@ }); // unload fix - $(window).bind('beforeunload', function() { ref.unload = true; }); + $(window).on('beforeunload', function() { ref.unload = true; }); // set environment variable(s) this.set_env = function(p, value) @@ -156,8 +156,8 @@ var n; this.task = this.env.task; - // check browser - if (this.env.server_error != 409 && (!bw.dom || !bw.xmlhttp_test() || (bw.mz && bw.vendver < 1.9) || (bw.ie && bw.vendver < 7))) { + // check browser capabilities (never use version checks here) + if (this.env.server_error != 409 && (!bw.dom || !bw.xmlhttp_test())) { this.goto_url('error', '_code=0x199'); return; } @@ -274,6 +274,23 @@ this.enable_command('compose', 'add-contact', false); parent.rcmail.show_contentframe(true); } + + // initialize drag-n-drop on attachments, so they can e.g. + // be dropped into mail compose attachments in another window + if (this.gui_objects.attachments) + $('li > a', this.gui_objects.attachments).not('.drop').on('dragstart', function(e) { + var n, href = this.href, dt = e.originalEvent.dataTransfer; + if (dt) { + // inject username to the uri + href = href.replace(/^https?:\/\//, function(m) { return m + urlencode(ref.env.username) + '@'}); + // cleanup the node to get filename without the size test + n = $(this).clone(); + n.children().remove(); + + dt.setData('roundcube-uri', href); + dt.setData('roundcube-name', $.trim(n.text())); + } + }); } else if (this.env.action == 'compose') { this.env.address_group_stack = []; @@ -304,8 +321,8 @@ if (this.gui_objects.responseslist) { $('a.insertresponse', this.gui_objects.responseslist) .attr('unselectable', 'on') - .mousedown(function(e){ return rcube_event.cancel(e); }) - .bind('mouseup keypress', function(e){ + .mousedown(function(e) { return rcube_event.cancel(e); }) + .on('mouseup keypress', function(e) { if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) { ref.command('insert-response', $(this).attr('rel')); $(document.body).trigger('mouseup'); // hides the menu @@ -325,7 +342,9 @@ else if (this.env.action == 'get') this.enable_command('download', 'print', true); // show printing dialog - else if (this.env.action == 'print' && this.env.uid) { + else if (this.env.action == 'print' && this.env.uid + && !this.env.is_pgp_content && !this.env.pgp_mime_part + ) { this.print_dialog(); } @@ -375,6 +394,8 @@ } this.http_post(postact, postdata); } + + this.check_mailvelope(this.env.action); // detect browser capabilities if (!this.is_framed() && !this.env.extwin) @@ -510,8 +531,11 @@ break; case 'login': - var input_user = $('#rcmloginuser'); - input_user.bind('keyup', function(e){ return ref.login_user_keyup(e); }); + var tz, tz_name, jstz = window.jstz, + input_user = $('#rcmloginuser'), + input_tz = $('#rcmlogintz'); + + input_user.keyup(function(e) { return ref.login_user_keyup(e); }); if (input_user.val() == '') input_user.focus(); @@ -519,14 +543,10 @@ $('#rcmloginpwd').focus(); // detect client timezone - if (window.jstz) { - var timezone = jstz.determine(); - if (timezone.name()) - $('#rcmlogintz').val(timezone.name()); - } - else { - $('#rcmlogintz').val(new Date().getStdTimezoneOffset() / -60); - } + if (jstz && (tz = jstz.determine())) + tz_name = tz.name(); + + input_tz.val(tz_name ? tz_name : (new Date().getStdTimezoneOffset() / -60)); // display 'loading' message on form submit, lock submit button $('form').submit(function () { @@ -580,19 +600,19 @@ // activate html5 file drop feature (if browser supports it and if configured) if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) { - $(document.body).bind('dragover dragleave drop', function(e){ return ref.document_drag_hover(e, e.type == 'dragover'); }); + $(document.body).on('dragover dragleave drop', function(e) { return ref.document_drag_hover(e, e.type == 'dragover'); }); $(this.gui_objects.filedrop).addClass('droptarget') - .bind('dragover dragleave', function(e){ return ref.file_drag_hover(e, e.type == 'dragover'); }) - .get(0).addEventListener('drop', function(e){ return ref.file_dropped(e); }, false); + .on('dragover dragleave', function(e) { return ref.file_drag_hover(e, e.type == 'dragover'); }) + .get(0).addEventListener('drop', function(e) { return ref.file_dropped(e); }, false); } // catch document (and iframe) mouse clicks var body_mouseup = function(e){ return ref.doc_mouse_up(e); }; $(document.body) - .bind('mouseup', body_mouseup) - .bind('keydown', function(e){ return ref.doc_keypress(e); }); + .mouseup(body_mouseup) + .keydown(function(e){ return ref.doc_keypress(e); }); - $('iframe').load(function(e) { + $('iframe').on('load', function(e) { try { $(this.contentDocument || this.contentWindow).on('mouseup', body_mouseup); } catch (e) {/* catch possible "Permission denied" error in IE */ } }) @@ -653,7 +673,9 @@ } // check input before leaving compose step - if (this.task == 'mail' && this.env.action == 'compose' && $.inArray(command, this.env.compose_commands) < 0 && !this.env.server_error) { + if (this.task == 'mail' && this.env.action == 'compose' && !this.env.server_error && command != 'save-pref' + && $.inArray(command, this.env.compose_commands) < 0 + ) { if (!this.env.is_sent && this.cmp_hash != this.compose_field_hash() && !confirm(this.get_label('notsentwarning'))) return false; @@ -762,7 +784,7 @@ case 'open': if (uid = this.get_single_uid()) { - obj.href = this.url('show', {_mbox: this.get_message_mailbox(uid), _uid: uid}); + obj.href = this.url('show', this.params_from_uid(uid)); return true; } break; @@ -774,7 +796,7 @@ case 'list': if (props && props != '') { - this.reset_qsearch(); + this.reset_qsearch(true); } if (this.env.action == 'compose' && this.env.extwin) { window.close(); @@ -894,7 +916,7 @@ else { // reload form if (props == 'reload') { - form.action += '?_reload=1'; + form.action += '&_reload=1'; } else if (this.task == 'settings' && (this.env.identities_level % 2) == 0 && (input = $("input[name='_email']", form)) && input.length && !rcube_check_email(input.val()) @@ -1188,8 +1210,8 @@ this.gui_objects.messagepartframe.contentWindow.print(); } else if (uid = this.get_single_uid()) { - url = '&_action=print&_uid='+uid+'&_mbox='+urlencode(this.get_message_mailbox(uid))+(this.env.safemode ? '&_safe=1' : ''); - if (this.open_window(this.env.comm_path + url, true, true)) { + url = this.url('print', this.params_from_uid(uid, {_safe: this.env.safemode ? 1 : 0})); + if (this.open_window(url, true, true)) { if (this.env.action != 'show') this.mark_message('read', uid); } @@ -1198,7 +1220,7 @@ case 'viewsource': if (uid = this.get_single_uid()) - this.open_window(this.env.comm_path+'&_action=viewsource&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox), true, true); + this.open_window(this.url('viewsource', this.params_from_uid(uid)), true, true); break; case 'download': @@ -1206,7 +1228,7 @@ location.href = location.href.replace(/_frame=/, '_download='); } else if (uid = this.get_single_uid()) { - this.goto_url('viewsource', { _uid: uid, _mbox: this.get_message_mailbox(uid), _save: 1 }); + this.goto_url('viewsource', this.params_from_uid(uid, {_save: 1})); } break; @@ -1221,17 +1243,16 @@ // reset quicksearch case 'reset-search': - var n, s = this.env.search_request || this.env.qsearch, - ss = this.gui_objects.qsearchbox && this.gui_objects.qsearchbox.value != ''; + var n, s = this.env.search_request || this.env.qsearch; - this.reset_qsearch(); + this.reset_qsearch(true); this.select_all_mode = false; if (s && this.env.action == 'compose') { if (this.contact_list) this.list_contacts_clear(); } - else if (s && ss && this.env.mailbox) { + else if (s && this.env.mailbox) { this.list_mailbox(this.env.mailbox, 1); } else if (s && this.task == 'addressbook') { @@ -1269,7 +1290,7 @@ $('input[name="_unlock"]', form).val(importlock); - if (!(flag = this.upload_file(form, 'import'))) { + if (!(flag = this.upload_file(form, 'import', importlock))) { this.set_busy(false, null, importlock); if (flag !== false) alert(this.get_label('selectimportfile')); @@ -1441,7 +1462,7 @@ else if (delay) setTimeout(function() { ref.reload(); }, delay); else if (window.location) - location.href = this.env.comm_path + (this.env.action ? '&_action='+this.env.action : ''); + location.href = this.url('', {_extwin: this.env.extwin}); }; // Add variable to GET string, replace old value if exists @@ -1608,15 +1629,16 @@ this.folder_collapsed = function(node) { - var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders'; + var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders', + old = this.env[prefname]; if (node.collapsed) { this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&'; // select the folder if one of its childs is currently selected // don't select if it's virtual (#1488346) - if (!node.virtual && this.env.mailbox && this.env.mailbox.startsWith(name + this.env.delimiter)) - this.command('list', name); + if (!node.virtual && this.env.mailbox && this.env.mailbox.startsWith(node.id + this.env.delimiter)) + this.command('list', node.id); } else { var reg = new RegExp('&'+urlencode(node.id)+'&'); @@ -1624,7 +1646,8 @@ } if (!this.drag_active) { - this.command('save-pref', { name: prefname, value: this.env[prefname] }); + if (old !== this.env[prefname]) + this.command('save-pref', { name: prefname, value: this.env[prefname] }); if (this.env.unread_counts) this.set_unread_count_display(node.id, false); @@ -2172,10 +2195,16 @@ this.set_list_sorting = function(sort_col, sort_order) { + var sort_old = this.env.sort_col == 'arrival' ? 'date' : this.env.sort_col, + sort_new = sort_col == 'arrival' ? 'date' : sort_col; + // set table header class - $('#rcm'+this.env.sort_col).removeClass('sorted'+(this.env.sort_order.toUpperCase())); - if (sort_col) - $('#rcm'+sort_col).addClass('sorted'+sort_order); + $('#rcm' + sort_old).removeClass('sorted' + this.env.sort_order.toUpperCase()); + if (sort_new) + $('#rcm' + sort_new).addClass('sorted' + sort_order); + + // if sorting by 'arrival' is selected, click on date column should not switch to 'date' + $('#rcmdate > a').prop('rel', sort_col == 'arrival' ? 'arrival' : 'date'); this.env.sort_col = sort_col; this.env.sort_order = sort_order; @@ -2232,35 +2261,33 @@ return; var win, target = window, - action = preview ? 'preview': 'show', - url = '&_action='+action+'&_uid='+id+'&_mbox='+urlencode(this.get_message_mailbox(id)); + url = this.params_from_uid(id, {_caps: this.browser_capabilities()}); if (preview && (win = this.get_frame_window(this.env.contentframe))) { target = win; - url += '&_framed=1'; + url._framed = 1; } if (safe) - url += '&_safe=1'; + url._safe = 1; // also send search request to get the right messages if (this.env.search_request) - url += '&_search='+this.env.search_request; - - // add browser capabilities, so we can properly handle attachments - url += '&_caps='+urlencode(this.browser_capabilities()); + url._search = this.env.search_request; if (this.env.extwin) - url += '&_extwin=1'; + url._extwin = 1; + + url = this.url(preview ? 'preview': 'show', url); if (preview && String(target.location.href).indexOf(url) >= 0) { this.show_contentframe(true); } else { if (!preview && this.env.message_extwin && !this.env.extwin) - this.open_window(this.env.comm_path+url, true); + this.open_window(url, true); else - this.location_href(this.env.comm_path+url, target, true); + this.location_href(url, target, true); // mark as read and change mbox unread counter if (preview && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read > 0) { @@ -2376,6 +2403,9 @@ // list messages of a specific mailbox using filter this.filter_mailbox = function(filter) { + if (this.filter_disabled) + return; + var lock = this.set_busy(true, 'searching'); this.clear_message_list(); @@ -2409,16 +2439,17 @@ if (sort) url._sort = sort; - // also send search request to get the right messages - if (this.env.search_request) - url._search = this.env.search_request; - - // set page=1 if changeing to another mailbox + // folder change, reset page, search scope, etc. if (this.env.mailbox != mbox) { page = 1; this.env.current_page = page; + this.env.search_scope = 'base'; this.select_all_mode = false; + this.reset_search_filter(); } + // also send search request to get the right messages + else if (this.env.search_request) + url._search = this.env.search_request; if (!update_only) { // unselect selected messages and clear the list and message data @@ -2483,22 +2514,23 @@ // removes messages that doesn't exists from list selection array this.update_selection = function() { - var selected = this.message_list.selection, - rows = this.message_list.rows, + var list = this.message_list, + selected = list.selection, + rows = list.rows, i, selection = []; for (i in selected) if (rows[selected[i]]) selection.push(selected[i]); - this.message_list.selection = selection; + list.selection = selection; // reset preview frame, if currently previewed message is not selected (has been removed) try { var win = this.get_frame_window(this.env.contentframe), id = win.rcmail.env.uid; - if (id && $.inArray(id, selection) < 0) + if (id && !list.in_selection(id)) this.show_contentframe(false); } catch (e) {}; @@ -2714,8 +2746,9 @@ $('#'+r.id+' .leaf:first') .attr('id', 'rcmexpando' + r.id) .attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed')) - .bind('mousedown', {uid: r.uid}, - function(e) { return ref.expand_message_row(e, e.data.uid); }); + .mousedown({uid: r.uid}, function(e) { + return ref.expand_message_row(e, e.data.uid); + }); r.unread_children = 0; roots.push(r); @@ -3250,6 +3283,557 @@ this.set_alttext('delete', label); }; + // Initialize input element for list page jump + this.init_pagejumper = function(element) + { + $(element).addClass('rcpagejumper') + .on('focus', function(e) { + // create and display popup with page selection + var i, html = ''; + + for (i = 1; i <= ref.env.pagecount; i++) + html += '<li>' + i + '</li>'; + + html = '<ul class="toolbarmenu">' + html + '</ul>'; + + if (!ref.pagejump) { + ref.pagejump = $('<div id="pagejump-selector" class="popupmenu"></div>') + .appendTo(document.body) + .on('click', 'li', function() { + if (!ref.busy) + $(element).val($(this).text()).change(); + }); + } + + if (ref.pagejump.data('count') != i) + ref.pagejump.html(html); + + ref.pagejump.attr('rel', '#' + this.id).data('count', i); + + // display page selector + ref.show_menu('pagejump-selector', true, e); + $(this).keydown(); + }) + // keyboard navigation + .on('keydown keyup click', function(e) { + var current, selector = $('#pagejump-selector'), + ul = $('ul', selector), + list = $('li', ul), + height = ul.height(), + p = parseInt(this.value); + + if (e.which != 27 && e.which != 9 && e.which != 13 && !selector.is(':visible')) + return ref.show_menu('pagejump-selector', true, e); + + if (e.type == 'keydown') { + // arrow-down + if (e.which == 40) { + if (list.length > p) + this.value = (p += 1); + } + // arrow-up + else if (e.which == 38) { + if (p > 1 && list.length > p - 1) + this.value = (p -= 1); + } + // enter + else if (e.which == 13) { + return $(this).change(); + } + // esc, tab + else if (e.which == 27 || e.which == 9) { + return $(element).val(ref.env.current_page); + } + } + + $('li.selected', ul).removeClass('selected'); + + if ((current = $(list[p - 1])).length) { + current.addClass('selected'); + $('#pagejump-selector').scrollTop(((ul.height() / list.length) * (p - 1)) - selector.height() / 2); + } + }) + .on('change', function(e) { + // go to specified page + var p = parseInt(this.value); + if (p && p != ref.env.current_page && !ref.busy) { + ref.hide_menu('pagejump-selector'); + ref.list_page(p); + } + }); + }; + + // Update page-jumper state on list updates + this.update_pagejumper = function() + { + $('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); + }); + } + }; + + // Load Mailvelope functionality (and initialize keyring if needed) + this.mailvelope_load = function(action) + { + if (this.env.browser_capabilities) + this.env.browser_capabilities['pgpmime'] = 1; + + var keyring = this.env.user_id; + + 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); + }); + }); + }; + + // Initializes Mailvelope editor or display container + this.mailvelope_init = function(action, keyring) + { + if (!window.mailvelope) + return; + + if (action == 'show' || action == 'preview' || action == 'print') { + // decrypt text body + if (this.env.is_pgp_content) { + 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) { + 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') { + this.env.compose_commands.push('compose-encrypted'); + + var is_html = $('input[name="_is_html"]').val() > 0; + + 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', !is_html); + }, + success: function(data) { + ref.set_busy(false, null, lock); + + if (is_html) { + ref.command('toggle-editor', {html: false, noconvert: true}); + $('#' + ref.env.composebody).val(''); + } + + ref.compose_encrypted({ quotedMail: data }); + ref.enable_command('compose-encrypted', true); + } + }); + } + else { + // enable encrypted compose toggle + this.enable_command('compose-encrypted', !is_html); + } + } + }; + + // handler for the 'compose-encrypted' command + this.compose_encrypted = function(props) + { + var options, 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(); + + // disable commands that operate on the compose body + ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', true); + ref.triggerEvent('compose-encrypted', { active:false }); + } + // embed Mailvelope editor container + else { + if (this.spellcheck_state()) + this.editor.spellcheck_stop(); + + if (props.quotedMail) { + options = { quotedMail: props.quotedMail, quotedMailIndent: false }; + } + else { + options = { predefinedText: $('#' + this.env.composebody).val() }; + } + + 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(); + + // disable commands that operate on the compose body + ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', false); + ref.triggerEvent('compose-encrypted', { active:true }); + + // 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); + console.log(options); + }); + } + }; + + // 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').children().not('iframe').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(ref.get_label('keyservererror'), '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 { + var $key = keyid.substr(-8).toUpperCase(); + btn.closest('.key').fadeOut(); + ref.display_message(ref.get_label('keyimportsuccess').replace('$key', $key), 'confirmation'); + } + }).catch(function(err) { + console.log(err); + }); + }); + }); + + }; + + /*********************************************************/ /********* mailbox folders methods *********/ /*********************************************************/ @@ -3344,7 +3928,7 @@ if (!this.gui_objects.messageform) return false; - var i, pos, input_from = $("[name='_from']"), + var i, elem, pos, input_from = $("[name='_from']"), input_to = $("[name='_to']"), input_subject = $("input[name='_subject']"), input_message = $("[name='_message']").get(0), @@ -3379,13 +3963,15 @@ if (!html_mode) { pos = this.env.top_posting ? 0 : input_message.value.length; - this.set_caret_pos(input_message, pos); // add signature according to selected identity - // if we have HTML editor, signature is added in callback + // if we have HTML editor, signature is added in a callback if (input_from.prop('type') == 'select-one') { this.change_identity(input_from[0]); } + + // set initial cursor position + this.set_caret_pos(input_message, pos); // scroll to the bottom of the textarea (#1490114) if (pos) { @@ -3398,11 +3984,14 @@ this.compose_restore_dialog(0, html_mode) if (input_to.val() == '') - input_to.focus(); + elem = input_to; else if (input_subject.val() == '') - input_subject.focus(); + elem = input_subject; else if (input_message) - input_message.focus(); + elem = input_message; + + // focus first empty element (need to be visible on IE8) + $(elem).filter(':visible').focus(); this.env.compose_focus_elem = document.activeElement; @@ -3516,6 +4105,11 @@ ); } + // delegate sending to Mailvelope routine + 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(), @@ -3585,7 +4179,7 @@ var oldval = input.val(), rx = new RegExp(RegExp.escape(delim) + '\\s*$'); if (oldval && !rx.test(oldval)) oldval += delim + ' '; - input.val(oldval + recipients.join(delim + ' ') + delim + ' '); + input.val(oldval + recipients.join(delim + ' ') + delim + ' ').change(); this.triggerEvent('add-recipient', { field:field, recipients:recipients }); } @@ -3689,6 +4283,8 @@ if (result) { // update internal format flag $("input[name='_is_html']").val(props.html ? 1 : 0); + // enable encrypted compose toggle + this.enable_command('compose-encrypted', !props.html); } return result; @@ -3758,10 +4354,10 @@ .attr('tabindex', '0') .html(this.quote_html(response.name)) .appendTo(li) - .mousedown(function(e){ + .mousedown(function(e) { return rcube_event.cancel(e); }) - .bind('mouseup keypress', function(e){ + .on('mouseup keypress', function(e) { if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) { ref.command('insert-response', $(this).attr('rel')); $(document.body).trigger('mouseup'); // hides the menu @@ -3833,7 +4429,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(); } @@ -3859,7 +4455,7 @@ if (!this.local_save_timer && window.localStorage && this.env.save_localstorage) { // track typing activity and only save on changes this.compose_type_activity = this.compose_type_activity_last = 0; - $(document).bind('keypress', function(e){ ref.compose_type_activity++; }); + $(document).keypress(function(e) { ref.compose_type_activity++; }); this.local_save_timer = setInterval(function(){ if (ref.compose_type_activity > ref.compose_type_activity_last) { @@ -3868,7 +4464,7 @@ } }, 5000); - $(window).unload(function() { + $(window).on('unload', function() { // remove copy from local storage if compose screen is left after warning if (!ref.env.server_error) ref.remove_compose_data(ref.env.compose_id); @@ -3902,6 +4498,11 @@ if (this.env.attachments) 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; @@ -4083,7 +4684,7 @@ }; // upload (attachment) file - this.upload_file = function(form, action) + this.upload_file = function(form, action, lock) { if (!form) return; @@ -4125,6 +4726,9 @@ if (!content.match(/display_message/)) ref.display_message(ref.get_label('fileuploaderror'), 'error'); ref.remove_from_attachment_list(e.data.ts); + + if (lock) + ref.set_busy(false, null, lock); } // Opera hack: handle double onload if (bw.opera) @@ -4309,6 +4913,9 @@ if (filter) url._filter = filter; + if (this.gui_objects.search_interval) + url._interval = $(this.gui_objects.search_interval).val(); + if (search) { url._q = search; @@ -4330,14 +4937,31 @@ return url; }; + // reset search filter + this.reset_search_filter = function() + { + this.filter_disabled = true; + if (this.gui_objects.search_filter) + $(this.gui_objects.search_filter).val('ALL').change(); + this.filter_disabled = false; + }; + // reset quick-search form - this.reset_qsearch = function() + this.reset_qsearch = function(all) { if (this.gui_objects.qsearchbox) this.gui_objects.qsearchbox.value = ''; + if (this.gui_objects.search_interval) + $(this.gui_objects.search_interval).val(''); + if (this.env.qsearch) this.abort_request(this.env.qsearch); + + if (all) { + this.env.search_scope = 'base'; + this.reset_search_filter(); + } this.env.qsearch = null; this.env.search_request = null; @@ -4356,6 +4980,20 @@ if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL') this.filter_mailbox(this.env.search_filter); if (scope != 'all') + this.select_folder(this.env.mailbox, '', true); + } + }; + + this.set_searchinterval = function(interval) + { + var old = this.env.search_interval; + this.env.search_interval = interval; + + // re-send search query with new interval + if (interval != old && this.env.search_request) { + if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL') + this.filter_mailbox(this.env.search_filter); + if (interval) this.select_folder(this.env.mailbox, '', true); } }; @@ -6649,6 +7287,8 @@ { this.enable_command('nextpage', 'lastpage', this.env.pagecount > this.env.current_page); this.enable_command('previouspage', 'firstpage', this.env.current_page > 1); + + this.update_pagejumper(); }; // mark a mailbox as selected and set environment variable @@ -6705,6 +7345,9 @@ repl, cell, col, n, len, tr; this.env.listcols = listcols; + + if (!this.env.coltypes) + this.env.coltypes = {}; // replace old column headers if (thead) { @@ -7033,7 +7676,7 @@ if (show) { // truncate stack down to the one containing the ref link for (var i = this.menu_stack.length - 1; stack && i >= 0; i--) { - if (!$(ref).parents('#'+this.menu_stack[i]).length) + if (!$(ref).parents('#'+this.menu_stack[i]).length && $(event.target).parent().attr('role') != 'menuitem') this.hide_menu(this.menu_stack[i], event); } if (stack && this.menu_stack.length) { @@ -7197,7 +7840,7 @@ // compose a valid url with the given parameters this.url = function(action, query) { - var querystring = typeof query === 'string' ? '&' + query : ''; + var querystring = typeof query === 'string' ? query : ''; if (typeof action !== 'string') query = action; @@ -7209,12 +7852,12 @@ else if (this.env.action) query._action = this.env.action; - var base = this.env.comm_path, k, param = {}; + var url = this.env.comm_path, k, param = {}; // overwrite task name if (action && action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) { query._action = RegExp.$2; - base = base.replace(/\_task=[a-z0-9_-]+/, '_task='+RegExp.$1); + url = url.replace(/\_task=[a-z0-9_-]+/, '_task=' + RegExp.$1); } // remove undefined values @@ -7223,7 +7866,13 @@ param[k] = query[k]; } - return base + (base.indexOf('?') > -1 ? '&' : '?') + $.param(param) + querystring; + if (param = $.param(param)) + url += (url.indexOf('?') > -1 ? '&' : '?') + param; + + if (querystring) + url += (url.indexOf('?') > -1 ? '&' : '?') + querystring; + + return url; }; this.redirect = function(url, lock) @@ -7276,22 +7925,32 @@ }; // send a http request to the server - this.http_request = function(action, query, lock) + this.http_request = function(action, data, lock) { - var url = this.url(action, query); + if (typeof data !== 'object') + data = rcube_parse_query(data); + + data._remote = 1; + data._unlock = lock ? lock : 0; // trigger plugin hook - var result = this.triggerEvent('request'+action, query); + var result = this.triggerEvent('request' + action, data); - if (result !== undefined) { - // abort if one the handlers returned false - if (result === false) - return false; - else - url = this.url(action, result); + // abort if one of the handlers returned false + if (result === false) { + if (data._unlock) + this.set_busy(false, null, data._unlock); + return false; + } + else if (result !== undefined) { + data = result; + if (data._action) { + action = data._action; + delete data._action; + } } - url += '&_remote=1'; + var url = this.url(action, data); // send request this.log('HTTP GET: ' + url); @@ -7300,33 +7959,39 @@ this.start_keepalive(); return $.ajax({ - type: 'GET', url: url, data: { _unlock:(lock?lock:0) }, dataType: 'json', - success: function(data){ ref.http_response(data); }, + type: 'GET', url: url, dataType: 'json', + success: function(data) { ref.http_response(data); }, error: function(o, status, err) { ref.http_error(o, status, err, lock, action); } }); }; // send a http POST request to the server - this.http_post = function(action, postdata, lock) + this.http_post = function(action, data, lock) { - var url = this.url(action); + if (typeof data !== 'object') + data = rcube_parse_query(data); - if (postdata && typeof postdata === 'object') { - postdata._remote = 1; - postdata._unlock = (lock ? lock : 0); - } - else - postdata += (postdata ? '&' : '') + '_remote=1' + (lock ? '&_unlock='+lock : ''); + data._remote = 1; + data._unlock = lock ? lock : 0; // trigger plugin hook - var result = this.triggerEvent('request'+action, postdata); - if (result !== undefined) { - // abort if one of the handlers returned false - if (result === false) - return false; - else - postdata = result; + var result = this.triggerEvent('request'+action, data); + + // abort if one of the handlers returned false + if (result === false) { + if (data._unlock) + this.set_busy(false, null, data._unlock); + return false; } + else if (result !== undefined) { + data = result; + if (data._action) { + action = data._action; + delete data._action; + } + } + + var url = this.url(action); // send request this.log('HTTP POST: ' + url); @@ -7335,7 +8000,7 @@ this.start_keepalive(); return $.ajax({ - type: 'POST', url: url, data: postdata, dataType: 'json', + type: 'POST', url: url, data: data, dataType: 'json', success: function(data){ ref.http_response(data); }, error: function(o, status, err) { ref.http_error(o, status, err, lock, action); } }); @@ -7451,32 +8116,36 @@ this.env.qsearch = null; case 'list': if (this.task == 'mail') { - var is_multifolder = this.is_multifolder_listing(); + var is_multifolder = this.is_multifolder_listing(), + list = this.message_list, + uid = this.env.list_uid; + this.enable_command('show', 'select-all', 'select-none', this.env.messagecount > 0); this.enable_command('expunge', this.env.exists && !is_multifolder); this.enable_command('purge', this.purge_mailbox_test() && !is_multifolder); this.enable_command('import-messages', !is_multifolder); this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount && !is_multifolder); - if ((response.action == 'list' || response.action == 'search') && this.message_list) { - var list = this.message_list, uid = this.env.list_uid; - - // highlight message row when we're back from message page - if (uid) { - if (!list.rows[uid]) - uid += '-' + this.env.mailbox; - if (list.rows[uid]) { - list.select(uid); + if (list) { + if (response.action == 'list' || response.action == 'search') { + // highlight message row when we're back from message page + if (uid) { + if (!list.rows[uid]) + uid += '-' + this.env.mailbox; + if (list.rows[uid]) { + list.select(uid); + } + delete this.env.list_uid; } - delete this.env.list_uid; + + this.enable_command('set-listmode', this.env.threads && !is_multifolder); + if (list.rowcount > 0 && !$(document.activeElement).is('input,textarea')) + list.focus(); + this.msglist_select(list); } - this.enable_command('set-listmode', this.env.threads && !is_multifolder); - if (list.rowcount > 0) - list.focus(); - this.msglist_select(list); - this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:list.rowcount }); - + if (response.action != 'getunread') + this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:list.rowcount }); } } else if (this.task == 'addressbook') { @@ -7486,7 +8155,7 @@ this.enable_command('search-create', this.env.source == ''); this.enable_command('search-delete', this.env.search_id); this.update_group_commands(); - if (this.contact_list.rowcount > 0) + if (this.contact_list.rowcount > 0 && !$(document.activeElement).is('input,textarea')) this.contact_list.focus(); this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount }); } @@ -7717,7 +8386,7 @@ } // handle upload errors by parsing iframe content in onload - frame.bind('load', {ts:ts}, onload); + frame.on('load', {ts:ts}, onload); $(form).attr({ target: frame_name, @@ -7758,21 +8427,39 @@ this.file_drag_hover(e, false); // prepare multipart form data composition - var files = e.target.files || e.dataTransfer.files, + var uri, files = e.target.files || e.dataTransfer.files, formdata = window.FormData ? new FormData() : null, fieldname = (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'), boundary = '------multipartformboundary' + (new Date).getTime(), dashdash = '--', crlf = '\r\n', - multipart = dashdash + boundary + crlf; + multipart = dashdash + boundary + crlf, + args = {_id: this.env.compose_id || this.env.cid || '', _remote: 1, _from: this.env.action}; - if (!files || !files.length) + if (!files || !files.length) { + // Roundcube attachment, pass its uri to the backend and attach + if (uri = e.dataTransfer.getData('roundcube-uri')) { + var ts = new Date().getTime(), + // jQuery way to escape filename (#1490530) + content = $('<span>').text(e.dataTransfer.getData('roundcube-name') || this.gettext('attaching')).html(); + + args._uri = uri; + args._uploadid = ts; + + // add to attachments list + if (!this.add2attachment_list(ts, {name: '', html: content, classname: 'uploading', complete: false})) + this.file_upload_id = this.set_busy(true, 'attaching'); + + this.http_post(this.env.filedrop.action || 'upload', args); + } return; + } // inline function to submit the files to the server var submit_data = function() { var multiple = files.length > 1, ts = new Date().getTime(), - content = '<span>' + (multiple ? ref.get_label('uploadingmany') : files[0].name) + '</span>'; + // jQuery way to escape filename (#1490530) + content = $('<span>').text(multiple ? ref.get_label('uploadingmany') : files[0].name).html(); // add to attachments list if (!ref.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false })) @@ -7781,10 +8468,12 @@ // complete multipart content and post request multipart += dashdash + boundary + dashdash + crlf; + args._uploadid = ts; + $.ajax({ type: 'POST', dataType: 'json', - url: ref.url(ref.env.filedrop.action || 'upload', {_id: ref.env.compose_id||ref.env.cid||'', _uploadid: ts, _remote: 1, _from: ref.env.action}), + url: ref.url(ref.env.filedrop.action || 'upload', args), contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary, processData: false, timeout: 0, // disable default timeout set in ajaxSetup() @@ -7986,10 +8675,22 @@ // get the IMP mailbox of the message with the given UID this.get_message_mailbox = function(uid) { - var msg = this.env.messages ? this.env.messages[uid] : {}; + var msg = (this.env.messages && uid ? this.env.messages[uid] : null) || {}; return msg.mbox || this.env.mailbox; }; + // build request parameters from single message id (maybe with mailbox name) + this.params_from_uid = function(uid, params) + { + if (!params) + params = {}; + + params._uid = String(uid).split('-')[0]; + params._mbox = this.get_message_mailbox(uid); + + return params; + }; + // gets cursor position this.get_caret_pos = function(obj) { -- Gitblit v1.9.1