From bd0551b22076b82a6d49e9f7a2b2e0c90a1b2326 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Fri, 05 Feb 2016 07:25:27 -0500 Subject: [PATCH] Secure also downloads of addressbook exports, managesieve script exports and Enigma keys exports --- program/js/app.js | 2325 +++++++++++++++++++++++++++++++++++++++++------------------ 1 files changed, 1,615 insertions(+), 710 deletions(-) diff --git a/program/js/app.js b/program/js/app.js index 7e67a9c..45fba7e 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 @@ -58,7 +58,6 @@ request_timeout: 180, // seconds draft_autosave: 0, // seconds comm_path: './', - blankpage: 'program/resources/blank.gif', recipients_separator: ',', recipients_delimiter: ', ', popup_width: 1150, @@ -78,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) @@ -157,11 +156,14 @@ 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; } + + if (!this.env.blankpage) + this.env.blankpage = this.assets_path('program/resources/blank.gif'); // find all registered gui containers for (n in this.gui_containers) @@ -236,8 +238,6 @@ return ref.command('sort', $(this).attr('rel'), this); }); - this.gui_objects.messagelist.parentNode.onclick = function(e){ return ref.click_on_list(e || window.event); }; - this.enable_command('toggle_status', 'toggle_flag', 'sort', true); this.enable_command('set-listmode', this.env.threads && !this.is_multifolder_listing()); @@ -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,18 +342,17 @@ else if (this.env.action == 'get') this.enable_command('download', 'print', true); // show printing dialog - else if (this.env.action == 'print' && this.env.uid) { - if (bw.safari) - setTimeout('window.print()', 10); - else - window.print(); + else if (this.env.action == 'print' && this.env.uid + && !this.env.is_pgp_content && !this.env.pgp_mime_part + ) { + this.print_dialog(); } // get unread count for each mailbox if (this.gui_objects.mailboxlist) { this.env.unread_counts = {}; this.gui_objects.folderlist = this.gui_objects.mailboxlist; - this.http_request('getunread'); + this.http_request('getunread', {_page: this.env.current_page}); } // init address book widget @@ -379,6 +395,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(); @@ -405,8 +423,6 @@ .addEventListener('dragmove', function(e) { ref.drag_move(e); }) .addEventListener('dragend', function(e) { ref.drag_end(e); }) .init(); - - this.gui_objects.contactslist.parentNode.onmousedown = function(e){ return ref.click_on_list(e); }; $(this.gui_objects.qsearchbox).focusin(function() { ref.contact_list.blur(); }); @@ -441,6 +457,9 @@ this.enable_command('save', true); if (this.env.action == 'add' || this.env.action == 'edit' || this.env.action == 'search') this.init_contact_form(); + } + else if (this.env.action == 'print') { + this.print_dialog(); } break; @@ -512,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(); @@ -521,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 () { @@ -544,7 +562,7 @@ // select first input field in an edit form if (this.gui_objects.editform) $("input,select,textarea", this.gui_objects.editform) - .not(':hidden').not(':disabled').first().select(); + .not(':hidden').not(':disabled').first().select().focus(); // unset contentframe variable if preview_pane is enabled if (this.env.contentframe && !$('#' + this.env.contentframe).is(':visible')) @@ -560,13 +578,14 @@ // show message if (this.pending_message) - this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]); + this.display_message.apply(this, this.pending_message); // init treelist widget if (this.gui_objects.folderlist && window.rcube_treelist_widget) { this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, { selectable: true, id_prefix: 'rcmli', + parent_focus: true, id_encode: this.html_identifier_encode, id_decode: this.html_identifier_decode, check_droptarget: function(node) { return !node.virtual && ref.check_droptarget(node.id) } @@ -575,24 +594,25 @@ this.treelist .addEventListener('collapse', function(node) { ref.folder_collapsed(node) }) .addEventListener('expand', function(node) { ref.folder_collapsed(node) }) + .addEventListener('beforeselect', function(node) { return !ref.busy; }) .addEventListener('select', function(node) { ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }) }); } // 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 */ } }) @@ -630,11 +650,12 @@ { var ret, uid, cid, url, flag, aborted = false; - if (obj && obj.blur && !(event || rcube_event.is_keyboard(event))) + if (obj && obj.blur && !(event && rcube_event.is_keyboard(event))) obj.blur(); - // do nothing if interface is locked by other command (with exception for searching reset) - if (this.busy && !(command == 'reset-search' && this.last_command == 'search')) + // do nothing if interface is locked by another command + // with exception for searching reset and menu + if (this.busy && !(command == 'reset-search' && this.last_command == 'search') && !command.match(/^menu-/)) return false; // let the browser handle this click (shift/ctrl usually opens the link in a new window/tab) @@ -652,23 +673,26 @@ } // 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.cmp_hash != this.compose_field_hash() && !confirm(this.get_label('notsentwarning'))) + 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; // remove copy from local storage if compose screen is left intentionally this.remove_compose_data(this.env.compose_id); + this.compose_skip_unsavedcheck = true; } this.last_command = command; // process external commands if (typeof this.command_handlers[command] === 'function') { - ret = this.command_handlers[command](props, obj); + ret = this.command_handlers[command](props, obj, event); return ret !== undefined ? ret : (obj ? false : true); } else if (typeof this.command_handlers[command] === 'string') { - ret = window[this.command_handlers[command]](props, obj); + ret = window[this.command_handlers[command]](props, obj, event); return ret !== undefined ? ret : (obj ? false : true); } @@ -719,6 +743,7 @@ if (win) { this.save_compose_form_local(); + this.compose_skip_unsavedcheck = true; $("input[name='_action']", form).val('compose'); form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 }); form.target = win.name; @@ -759,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; @@ -771,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(); @@ -891,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()) @@ -994,7 +1019,7 @@ break; } - this.goto_url('get', qstring+'&_download=1', false); + this.goto_url('get', qstring+'&_download=1', false, true); break; case 'select-all': @@ -1052,12 +1077,9 @@ url = {}; if (this.task == 'mail') { - url._mbox = this.env.mailbox; + url = {_mbox: this.env.mailbox, _search: this.env.search_request}; if (props) - url._to = props; - // also send search request so we can go back to search result after message is sent - if (this.env.search_request) - url._search = this.env.search_request; + url._to = props; } // modify url if we're in addressbook else if (this.task == 'addressbook') { @@ -1082,8 +1104,12 @@ break; } } - else if (props) + else if (props && typeof props == 'string') { url._to = props; + } + else if (props && typeof props == 'object') { + $.extend(url, props); + } this.open_compose_step(url); break; @@ -1111,7 +1137,7 @@ break; case 'send': - if (!props.nocheck && !this.check_compose_input(command)) + if (!props.nocheck && !this.env.is_sent && !this.check_compose_input(command)) break; // Reset the auto-save timer @@ -1148,7 +1174,7 @@ case 'reply-list': case 'reply': if (uid = this.get_single_uid()) { - url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid)}; + url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid), _search: this.env.search_request}; if (command == 'reply-all') // do reply-list, when list is detected and popup menu wasn't used url._all = (!props && this.env.reply_all_mode == 1 && this.commands['reply-list'] ? 'list' : 'all'); @@ -1172,12 +1198,20 @@ break; case 'print': - if (this.env.action == 'get') { + if (this.task == 'addressbook') { + if (uid = this.contact_list.get_single_selection()) { + url = '&_action=print&_cid=' + uid; + if (this.env.source) + url += '&_source=' + urlencode(this.env.source); + this.open_window(this.env.comm_path + url, true, true); + } + } + else if (this.env.action == 'get') { 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); } @@ -1186,15 +1220,15 @@ 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': if (this.env.action == 'get') { - location.href = location.href.replace(/_frame=/, '_download='); + location.href = this.secure_url(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}), false, true); } break; @@ -1211,7 +1245,7 @@ case 'reset-search': 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') { @@ -1256,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')); @@ -1282,13 +1316,13 @@ case 'export': if (this.contact_list.rowcount > 0) { - this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request }); + this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request }, false, true); } break; case 'export-selected': if (this.contact_list.rowcount > 0) { - this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') }); + this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') }, false, true); } break; @@ -1402,8 +1436,10 @@ if (task == 'mail') url += '&_mbox=INBOX'; - else if (task == 'logout') + else if (task == 'logout' && !this.env.server_error) { + url = this.secure_url(url); this.clear_compose_data(); + } this.redirect(url); }; @@ -1413,7 +1449,10 @@ if (!url) url = this.env.comm_path; - return url.replace(/_task=[a-z0-9_-]+/i, '_task='+task); + if (url.match(/[?&]_task=[a-zA-Z0-9_-]+/)) + return url.replace(/_task=[a-zA-Z0-9_-]+/, '_task=' + task); + else + return url.replace(/\?.*$/, '') + '?_task=' + task; }; this.reload = function(delay) @@ -1423,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 @@ -1447,9 +1486,15 @@ return url + '?' + name + '=' + value; }; + // append CSRF protection token to the given url + this.secure_url = function(url) + { + return this.add_url(url, '_token', this.env.request_token); + }, + this.is_framed = function() { - return (this.env.framed && parent.rcmail && parent.rcmail != this && parent.rcmail.command); + return this.env.framed && parent.rcmail && parent.rcmail != this && typeof parent.rcmail.command == 'function'; }; this.save_pref = function(prop) @@ -1590,15 +1635,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)+'&'); @@ -1606,7 +1652,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); @@ -1657,7 +1704,7 @@ } skip = obj.data('parent'); } - }, 10); + }, 10, e); }; // global keypress event handler @@ -1711,19 +1758,6 @@ return true; } - - this.click_on_list = function(e) - { - if (this.gui_objects.qsearchbox) - this.gui_objects.qsearchbox.blur(); - - if (this.message_list) - this.message_list.focus(e); - else if (this.contact_list) - this.contact_list.focus(e); - - return true; - }; this.msglist_select = function(list) { @@ -1856,9 +1890,6 @@ && !this.env.mailboxes[id].virtual && (this.env.mailboxes[id].id != this.env.mailbox || this.is_multifolder_listing())) ? 1 : 0; - case 'settings': - return id != this.env.mailbox ? 1 : 0; - case 'addressbook': var target; if (id != this.env.source && (target = this.env.contactfolders[id])) { @@ -1960,7 +1991,7 @@ // attach events $.each(fn, function(i, f) { row[i].onclick = function(e) { f(e); return rcube_event.cancel(e); }; - if (bw.touch) { + if (bw.touch && row[i].addEventListener) { row[i].addEventListener('touchend', function(e) { if (e.changedTouches.length == 1) { f(e); @@ -2040,7 +2071,7 @@ } if (flags.forwarded) { status_class += ' forwarded'; - status_label += this.get_label('replied') + ' '; + status_label += this.get_label('forwarded') + ' '; } // update selection @@ -2170,10 +2201,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; @@ -2230,41 +2267,39 @@ 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) { this.preview_read_timer = setTimeout(function() { ref.set_unread_message(id, ref.env.mailbox); - ref.http_post('mark', {_uid: id, _flag: 'read', _quiet: 1}); + ref.http_post('mark', {_uid: id, _flag: 'read', _mbox: ref.env.mailbox, _quiet: 1}); }, this.env.preview_pane_mark_read * 1000); } } @@ -2374,6 +2409,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(); @@ -2407,16 +2445,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 @@ -2481,21 +2520,32 @@ // 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 && !list.in_selection(id)) + this.show_contentframe(false); + } + catch (e) {}; }; // expand all threads with unread children this.expand_unread = function() { - var r, tbody = this.gui_objects.messagelist.tBodies[0], + var r, tbody = this.message_list.tbody, new_row = tbody.firstChild; while (new_row) { @@ -2702,8 +2752,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); @@ -3238,6 +3289,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); + }, 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); + }, 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); + }); + } + }, 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(); + + }, function(err) { + console.log(err); + }); // mailvelope_editor.encrypt() + + }, function(err) { + console.error(err); + }); // mailvelope_keyring.validKeyForAddress(senders) + + }, 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); + }, 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'); + } + }).fail(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'); + } + }, function(err) { + console.log(err); + }); + }); + }); + + }; + + /*********************************************************/ /********* mailbox folders methods *********/ /*********************************************************/ @@ -3332,7 +3934,7 @@ if (!this.gui_objects.messageform) return false; - var i, 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), @@ -3366,79 +3968,36 @@ } if (!html_mode) { - this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length); + pos = this.env.top_posting ? 0 : input_message.value.length; + // 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) { + $(input_message).scrollTop(input_message.scrollHeight); } } // check for locally stored compose data - if (window.localStorage) { - var key, formdata, index = this.local_storage_get_item('compose.index', []); - - for (i = 0; i < index.length; i++) { - key = index[i]; - formdata = this.local_storage_get_item('compose.' + key, null, true); - if (!formdata) { - continue; - } - // restore saved copy of current compose_id - if (formdata.changed && key == this.env.compose_id) { - this.restore_compose_form(key, html_mode); - break; - } - // skip records from 'other' drafts - if (this.env.draft_id && formdata.draft_id && formdata.draft_id != this.env.draft_id) { - continue; - } - // skip records on reply - if (this.env.reply_msgid && formdata.reply_msgid != this.env.reply_msgid) { - continue; - } - // show dialog asking to restore the message - if (formdata.changed && formdata.session != this.env.session_id) { - this.show_popup_dialog( - this.get_label('restoresavedcomposedata') - .replace('$date', new Date(formdata.changed).toLocaleString()) - .replace('$subject', formdata._subject) - .replace(/\n/g, '<br/>'), - this.get_label('restoremessage'), - [{ - text: this.get_label('restore'), - click: function(){ - ref.restore_compose_form(key, html_mode); - ref.remove_compose_data(key); // remove old copy - ref.save_compose_form_local(); // save under current compose_id - $(this).dialog('close'); - } - }, - { - text: this.get_label('delete'), - click: function(){ - ref.remove_compose_data(key); - $(this).dialog('close'); - } - }, - { - text: this.get_label('ignore'), - click: function(){ - $(this).dialog('close'); - } - }] - ); - break; - } - } - } + if (this.env.save_localstorage) + 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; @@ -3449,6 +4008,74 @@ this.auto_save_start(); }; + this.compose_restore_dialog = function(j, html_mode) + { + var i, key, formdata, index = this.local_storage_get_item('compose.index', []); + + var show_next = function(i) { + if (++i < index.length) + ref.compose_restore_dialog(i, html_mode) + } + + for (i = j || 0; i < index.length; i++) { + key = index[i]; + formdata = this.local_storage_get_item('compose.' + key, null, true); + if (!formdata) { + continue; + } + // restore saved copy of current compose_id + if (formdata.changed && key == this.env.compose_id) { + this.restore_compose_form(key, html_mode); + break; + } + // skip records from 'other' drafts + if (this.env.draft_id && formdata.draft_id && formdata.draft_id != this.env.draft_id) { + continue; + } + // skip records on reply + if (this.env.reply_msgid && formdata.reply_msgid != this.env.reply_msgid) { + continue; + } + // show dialog asking to restore the message + if (formdata.changed && formdata.session != this.env.session_id) { + this.show_popup_dialog( + this.get_label('restoresavedcomposedata') + .replace('$date', new Date(formdata.changed).toLocaleString()) + .replace('$subject', formdata._subject) + .replace(/\n/g, '<br/>'), + this.get_label('restoremessage'), + [{ + text: this.get_label('restore'), + 'class': 'mainaction', + click: function(){ + ref.restore_compose_form(key, html_mode); + ref.remove_compose_data(key); // remove old copy + ref.save_compose_form_local(); // save under current compose_id + $(this).dialog('close'); + } + }, + { + text: this.get_label('delete'), + 'class': 'delete', + click: function(){ + ref.remove_compose_data(key); + $(this).dialog('close'); + show_next(i); + } + }, + { + text: this.get_label('ignore'), + click: function(){ + $(this).dialog('close'); + show_next(i); + } + }] + ); + break; + } + } + } + this.init_address_input_events = function(obj, props) { this.env.recipients_delimiter = this.env.recipients_separator + ' '; @@ -3457,15 +4084,40 @@ .attr({ 'autocomplete': 'off', 'aria-autocomplete': 'list', 'aria-expanded': 'false', 'role': 'combobox' }); }; - this.submit_messageform = function(draft) + this.submit_messageform = function(draft, saveonly) { var form = this.gui_objects.messageform; if (!form) return; + // the message has been sent but not saved, ask the user what to do + if (!saveonly && this.env.is_sent) { + return this.show_popup_dialog(this.get_label('messageissent'), '', + [{ + text: this.get_label('save'), + 'class': 'mainaction', + click: function() { + ref.submit_messageform(false, true); + $(this).dialog('close'); + } + }, + { + text: this.get_label('cancel'), + click: function() { + $(this).dialog('close'); + } + }] + ); + } + + // 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 ? 'savingmessage' : 'sendingmessage'), + var msgid = this.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage'), lang = this.spellcheck_lang(), files = []; @@ -3477,6 +4129,11 @@ form._draft.value = draft ? '1' : ''; form.action = this.add_url(form.action, '_unlock', msgid); form.action = this.add_url(form.action, '_lang', lang); + form.action = this.add_url(form.action, '_framed', 1); + + if (saveonly) { + form.action = this.add_url(form.action, '_saveonly', 1); + } // register timer to notify about connection timeout this.submit_timer = setTimeout(function(){ @@ -3528,7 +4185,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 }); } @@ -3618,16 +4275,22 @@ this.toggle_editor = function(props, obj, e) { // @todo: this should work also with many editors on page - var result = this.editor.toggle(props.html); + var result = this.editor.toggle(props.html, props.noconvert || false); + + // satisfy the expectations of aftertoggle-editor event subscribers + props.mode = props.html ? 'html' : 'plain'; if (!result && e) { // fix selector value if operation failed - $(e.target).filter('select').val(props.html ? 'plain' : 'html'); + props.mode = props.html ? 'plain' : 'html'; + $(e.target).filter('select').val(props.mode); } 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; @@ -3649,7 +4312,7 @@ this.save_response = function() { // show dialog to enter a name and to modify the text to be saved - var buttons = {}, text = this.editor.get_content(true, true), + var buttons = {}, text = this.editor.get_content({selection: true, format: 'text', nosig: true}), html = '<form class="propform">' + '<div class="prop block"><label>' + this.get_label('responsename') + '</label>' + '<input type="text" name="name" id="ffresponsename" size="40" /></div>' + @@ -3657,7 +4320,7 @@ '<textarea name="text" id="ffresponsetext" cols="40" rows="8"></textarea></div>' + '</form>'; - buttons[this.gettext('save')] = function(e) { + buttons[this.get_label('save')] = function(e) { var name = $('#ffresponsename').val(), text = $('#ffresponsetext').val(); @@ -3673,11 +4336,11 @@ $(this).dialog('close'); }; - buttons[this.gettext('cancel')] = function() { + buttons[this.get_label('cancel')] = function() { $(this).dialog('close'); }; - this.show_popup_dialog(html, this.gettext('newresponse'), buttons); + this.show_popup_dialog(html, this.get_label('newresponse'), buttons, {button_classes: ['mainaction']}); $('#ffresponsetext').val(text); $('#ffresponsename').select(); @@ -3697,10 +4360,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 @@ -3725,10 +4388,7 @@ // submit delete request if (key && confirm(this.get_label('deleteresponseconfirm'))) { this.http_post('settings/delete-response', { _key: key }, false); - return true; } - - return false; }; // updates spellchecker buttons on state change @@ -3762,21 +4422,20 @@ this.set_draft_id = function(id) { - var rc; - if (id && id != this.env.draft_id) { - if (rc = this.opener()) { - // refresh the drafts folder in opener window - if (rc.env.task == 'mail' && rc.env.action == '' && rc.env.mailbox == this.env.drafts_mailbox) - rc.command('checkmail'); - } + var filter = {task: 'mail', action: ''}, + rc = this.opener(false, filter) || this.opener(true, filter); + + // refresh the drafts folder in the opener window + if (rc && rc.env.mailbox == this.env.drafts_mailbox) + rc.command('checkmail'); this.env.draft_id = id; $("input[name='_draft_saveid']").val(id); // 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(); } @@ -3785,6 +4444,7 @@ // always remove local copy upon saving as draft this.remove_compose_data(this.env.compose_id); + this.compose_skip_unsavedcheck = false; }; this.auto_save_start = function() @@ -3798,10 +4458,10 @@ } // save compose form content to local storage every 5 seconds - if (!this.local_save_timer && window.localStorage) { + 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) { @@ -3809,6 +4469,21 @@ ref.compose_type_activity_last = ref.compose_type_activity; } }, 5000); + + $(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); + }); + } + + // check for unsaved changes before leaving the compose page + if (!window.onbeforeunload) { + window.onbeforeunload = function() { + if (!ref.compose_skip_unsavedcheck && ref.cmp_hash != ref.compose_field_hash()) { + return ref.get_label('notsentwarning'); + } + }; } // Unlock interface now that saving is complete @@ -3824,11 +4499,16 @@ if (val = $('[name="_' + hash_fields[i] + '"]').val()) str += val + ':'; - str += this.editor.get_content(); + str += this.editor.get_content({refresh: false}); 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; @@ -3839,6 +4519,10 @@ // store the contents of the compose form to localstorage this.save_compose_form_local = function() { + // feature is disabled + if (!this.env.save_localstorage) + return; + var formdata = { session:this.env.session_id, changed:new Date().getTime() }, ed, empty = true; @@ -3875,15 +4559,16 @@ } }); - if (window.localStorage && !empty) { + if (!empty) { var index = this.local_storage_get_item('compose.index', []), key = this.env.compose_id; - if ($.inArray(key, index) < 0) { - index.push(key); - } - this.local_storage_set_item('compose.' + key, formdata, true); - this.local_storage_set_item('compose.index', index); + if ($.inArray(key, index) < 0) { + index.push(key); + } + + this.local_storage_set_item('compose.' + key, formdata, true); + this.local_storage_set_item('compose.index', index); } }; @@ -3907,7 +4592,7 @@ // initialize HTML editor if ((formdata._is_html == '1' && !html_mode) || (formdata._is_html != '1' && html_mode)) { - this.command('toggle-editor', {id: this.env.composebody, html: !html_mode}); + this.command('toggle-editor', {id: this.env.composebody, html: !html_mode, noconvert: true}); } } }; @@ -3915,29 +4600,25 @@ // remove stored compose data from localStorage this.remove_compose_data = function(key) { - if (window.localStorage) { - var index = this.local_storage_get_item('compose.index', []); + var index = this.local_storage_get_item('compose.index', []); - if ($.inArray(key, index) >= 0) { - this.local_storage_remove_item('compose.' + key); - this.local_storage_set_item('compose.index', $.grep(index, function(val,i) { return val != key; })); - } + if ($.inArray(key, index) >= 0) { + this.local_storage_remove_item('compose.' + key); + this.local_storage_set_item('compose.index', $.grep(index, function(val,i) { return val != key; })); } }; // clear all stored compose data of this user this.clear_compose_data = function() { - if (window.localStorage) { - var i, index = this.local_storage_get_item('compose.index', []); + var i, index = this.local_storage_get_item('compose.index', []); - for (i=0; i < index.length; i++) { - this.local_storage_remove_item('compose.' + index[i]); - } - this.local_storage_remove_item('compose.index'); + for (i=0; i < index.length; i++) { + this.local_storage_remove_item('compose.' + index[i]); } - }; + this.local_storage_remove_item('compose.index'); + }; this.change_identity = function(obj, show_sig) { @@ -3946,6 +4627,19 @@ if (!show_sig) show_sig = this.env.show_sig; + + var id = obj.options[obj.selectedIndex].value, + sig = this.env.identity, + delim = this.env.recipients_separator, + rx_delim = RegExp.escape(delim); + + // enable manual signature insert + if (this.env.signatures && this.env.signatures[id]) { + this.enable_command('insert-sig', true); + this.env.compose_commands.push('insert-sig'); + } + else + this.enable_command('insert-sig', false); // first function execution if (!this.env.identities_initialized) { @@ -3956,18 +4650,11 @@ return; } - var i, rx, - id = obj.options[obj.selectedIndex].value, - sig = this.env.identity, - delim = this.env.recipients_separator, - rx_delim = RegExp.escape(delim), - headers = ['replyto', 'bcc']; - // update reply-to/bcc fields with addresses defined in identities - for (i in headers) { - var key = headers[i], - old_val = sig && this.env.identities[sig] ? this.env.identities[sig][key] : '', - new_val = id && this.env.identities[id] ? this.env.identities[id][key] : '', + $.each(['replyto', 'bcc'], function() { + var rx, key = this, + old_val = sig && ref.env.identities[sig] ? ref.env.identities[sig][key] : '', + new_val = id && ref.env.identities[id] ? ref.env.identities[id][key] : '', input = $('[name="_'+key+'"]'), input_val = input.val(); // remove old address(es) @@ -3994,15 +4681,7 @@ if (old_val || new_val) input.val(input_val).change(); - } - - // enable manual signature insert - if (this.env.signatures && this.env.signatures[id]) { - this.enable_command('insert-sig', true); - this.env.compose_commands.push('insert-sig'); - } - else - this.enable_command('insert-sig', false); + }); this.editor.change_signature(id, show_sig); this.env.identity = id; @@ -4011,7 +4690,7 @@ }; // upload (attachment) file - this.upload_file = function(form, action) + this.upload_file = function(form, action, lock) { if (!form) return; @@ -4053,6 +4732,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) @@ -4083,6 +4765,14 @@ if (upload_id) this.triggerEvent('fileuploaded', {name: name, attachment: att, id: upload_id}); + if (!this.env.attachments) + this.env.attachments = {}; + + if (upload_id && this.env.attachments[upload_id]) + delete this.env.attachments[upload_id]; + + this.env.attachments[name] = att; + if (!this.gui_objects.attachmentlist) return false; @@ -4111,11 +4801,6 @@ // set tabindex attribute var tabindex = $(this.gui_objects.attachmentlist).attr('data-tabindex') || '0'; li.find('a').attr('tabindex', tabindex); - - if (upload_id && this.env.attachments[upload_id]) - delete this.env.attachments[upload_id]; - - this.env.attachments[name] = att; return true; }; @@ -4234,6 +4919,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; @@ -4255,14 +4943,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; @@ -4281,6 +4986,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); } }; @@ -4306,28 +5025,37 @@ (this.env.search_request && (this.env.search_scope || 'base') != 'base'); }; - this.sent_successfully = function(type, msg, folders) + // action executed after mail is sent + this.sent_successfully = function(type, msg, folders, save_error) { this.display_message(msg, type); + this.compose_skip_unsavedcheck = true; if (this.env.extwin) { - this.lock_form(this.gui_objects.messageform); + if (!save_error) + this.lock_form(this.gui_objects.messageform); - var rc = this.opener(); + var filter = {task: 'mail', action: ''}, + rc = this.opener(false, filter) || this.opener(true, filter); + if (rc) { rc.display_message(msg, type); // refresh the folder where sent message was saved or replied message comes from - if (folders && rc.env.task == 'mail' && rc.env.action == '' && $.inArray(rc.env.mailbox, folders) >= 0) { + if (folders && $.inArray(rc.env.mailbox, folders) >= 0) { rc.command('checkmail'); } } - setTimeout(function() { window.close(); }, 1000); + if (!save_error) + setTimeout(function() { window.close(); }, 1000); } - else { + else if (!save_error) { // before redirect we need to wait some time for Chrome (#1486177) setTimeout(function() { ref.list_mailbox(); }, 500); } + + if (save_error) + this.env.is_sent = true; }; @@ -4429,7 +5157,7 @@ this.ksearch_destroy(); // insert all members of a group - if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].type == 'group') { + if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].type == 'group' && !this.env.contacts[id].email) { insert += this.env.contacts[id].name + this.env.recipients_delimiter; this.group2expand[this.env.contacts[id].id] = $.extend({ input: this.ksearch_input }, this.env.contacts[id]); this.http_request('mail/group-expand', {_source: this.env.contacts[id].source, _gid: this.env.contacts[id].id}, false); @@ -4546,10 +5274,6 @@ this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane').attr('role', 'listbox') .css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body); this.ksearch_pane.__ul = ul[0]; - - // register (delegate) event handlers - ul.on('mouseover', 'li', function(e){ ref.ksearch_select(e.target); }) - .on('mouseup', 'li', function(e){ ref.ksearch_click(e.target); }) } ul = this.ksearch_pane.__ul; @@ -4572,14 +5296,16 @@ // add each result line to list if (results && (len = results.length)) { for (i=0; i < len && maxlen > 0; i++) { - text = typeof results[i] === 'object' ? results[i].name : results[i]; + text = typeof results[i] === 'object' ? (results[i].display || results[i].name) : results[i]; type = typeof results[i] === 'object' ? results[i].type : ''; id = i + this.env.contacts.length; $('<li>').attr('id', 'rcmkSearchItem' + id) .attr('role', 'option') - .html(this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>')) + .html('<i class="icon"></i>' + this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>')) .addClass(type || '') .appendTo(ul) + .mouseover(function() { ref.ksearch_select(this); }) + .mouseup(function() { ref.ksearch_click(this); }) .get(0)._rcm_id = id; maxlen -= 1; } @@ -4676,15 +5402,16 @@ clearTimeout(this.preview_timer); var n, id, sid, contact, writable = false, + selected = list.selection.length, source = this.env.source ? this.env.address_sources[this.env.source] : null; // we don't have dblclick handler here, so use 200 instead of this.dblclick_time - if (id = list.get_single_selection()) + if (this.env.contentframe && (id = list.get_single_selection())) this.preview_timer = setTimeout(function(){ ref.load_contact(id, 'show'); }, 200); else if (this.env.contentframe) this.show_contentframe(false); - if (list.selection.length) { + if (selected) { list.draggable = false; // no source = search result, we'll need to detect if any of @@ -4719,11 +5446,12 @@ // if a group is currently selected, and there is at least one contact selected // thend we can enable the group-remove-selected command - this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0 && writable); - this.enable_command('compose', this.env.group || list.selection.length > 0); - this.enable_command('export-selected', 'copy', list.selection.length > 0); + this.enable_command('group-remove-selected', this.env.group && selected && writable); + this.enable_command('compose', this.env.group || selected); + this.enable_command('print', selected == 1); + this.enable_command('export-selected', 'copy', selected > 0); this.enable_command('edit', id && writable); - this.enable_command('delete', 'move', list.selection.length > 0 && writable); + this.enable_command('delete', 'move', selected && writable); return false; }; @@ -4731,10 +5459,14 @@ this.list_contacts = function(src, group, page) { var win, folder, url = {}, + refresh = src === undefined && group === undefined && page === undefined, target = window; if (!src) src = this.env.source; + + if (refresh) + group = this.env.group; if (page && this.current_page == page && src == this.env.source && group == this.env.group) return false; @@ -4743,7 +5475,7 @@ page = this.env.current_page = 1; this.reset_qsearch(); } - else if (group != this.env.group) + else if (!refresh && group != this.env.group) page = this.env.current_page = 1; if (this.env.search_id) @@ -4832,8 +5564,8 @@ this.contact_list.data = {}; this.contact_list.clear(true); this.show_contentframe(false); - this.enable_command('delete', 'move', 'copy', false); - this.enable_command('compose', this.env.group ? true : false); + this.enable_command('delete', 'move', 'copy', 'print', false); + this.enable_command('compose', this.env.group); }; this.set_group_prop = function(prop) @@ -4844,7 +5576,7 @@ // add link to pop back to parent group if (this.env.address_group_stack.length > 1) { $('<a href="#list">...</a>') - .attr('title', this.gettext('uponelevel')) + .attr('title', this.get_label('uponelevel')) .addClass('poplink') .appendTo(boxtitle) .click(function(e){ return ref.command('popgroup','',this); }); @@ -4873,7 +5605,7 @@ this.contact_list.clear_selection(); this.enable_command('compose', rec && rec.email); - this.enable_command('export-selected', rec && rec._type != 'group'); + this.enable_command('export-selected', 'print', rec && rec._type != 'group'); } else if (framed) return false; @@ -4881,6 +5613,9 @@ if (action && (cid || action == 'add') && !this.drag_active) { if (this.env.group) url._gid = this.env.group; + + if (this.env.search_request) + url._search = this.env.search_request; url._action = action; url._source = this.env.source; @@ -5121,10 +5856,10 @@ dateFormat: this.env.date_format, changeMonth: true, changeYear: true, - yearRange: '-100:+10', + yearRange: '-120:+10', showOtherMonths: true, - selectOtherMonths: true, - onSelect: function(dateText) { $(this).focus().val(dateText) } + selectOtherMonths: true +// onSelect: function(dateText) { $(this).focus().val(dateText); } }); $('input.datepicker').datepicker(); } @@ -5135,29 +5870,57 @@ .submit(function() { $('input.mainaction').click(); return false; }); }; + // group creation dialog this.group_create = function() { - this.add_input_row('contactgroup'); + var input = $('<input>').attr('type', 'text'), + content = $('<label>').text(this.get_label('namex')).append(input); + + this.show_popup_dialog(content, this.get_label('newgroup'), + [{ + text: this.get_label('save'), + 'class': 'mainaction', + click: function() { + var name; + + if (name = input.val()) { + ref.http_post('group-create', {_source: ref.env.source, _name: name}, + ref.set_busy(true, 'loading')); + } + + $(this).dialog('close'); + } + }] + ); }; + // group rename dialog this.group_rename = function() { - if (!this.env.group || !this.gui_objects.folderlist) + if (!this.env.group) return; - if (!this.name_input) { - this.enable_command('list', 'listgroup', false); - this.name_input = $('<input>').attr('type', 'text').val(this.env.contactgroups['G'+this.env.source+this.env.group].name); - this.name_input.bind('keydown', function(e) { return ref.add_input_keydown(e); }); - this.env.group_renaming = true; + var group_name = this.env.contactgroups['G' + this.env.source + this.env.group].name, + input = $('<input>').attr('type', 'text').val(group_name), + content = $('<label>').text(this.get_label('namex')).append(input); - var link, li = this.get_folder_li('G'+this.env.source+this.env.group,'',true); - if (li && (link = li.firstChild)) { - $(link).hide().before(this.name_input); - } - } + this.show_popup_dialog(content, this.get_label('grouprename'), + [{ + text: this.get_label('save'), + 'class': 'mainaction', + click: function() { + var name; - this.name_input.select().focus(); + if ((name = input.val()) && name != group_name) { + ref.http_post('group-rename', {_source: ref.env.source, _gid: ref.env.group, _name: name}, + ref.set_busy(true, 'loading')); + } + + $(this).dialog('close'); + } + }], + {open: function() { input.select(); }} + ); }; this.group_delete = function() @@ -5182,38 +5945,6 @@ this.list_contacts(prop.source, 0); }; - // @TODO: maybe it would be better to use popup instead of inserting input to the list? - this.add_input_row = function(type) - { - if (!this.gui_objects.folderlist) - return; - - if (!this.name_input) { - this.name_input = $('<input>').attr('type', 'text').data('tt', type); - this.name_input.bind('keydown', function(e) { return ref.add_input_keydown(e); }); - this.name_input_li = $('<li>').addClass(type).append(this.name_input); - - var ul, li; - - // find list (UL) element - if (type == 'contactsearch') - ul = this.gui_objects.savedsearchlist; - else - ul = $('ul.groups', this.get_folder_li(this.env.source,'',true)); - - // append to the list - li = $('li:last', ul); - if (li.length) - this.name_input_li.insertAfter(li); - else { - this.name_input_li.appendTo(ul); - ul.show(); // make sure the list is visible - } - } - - this.name_input.select().focus(); - }; - //remove selected contacts from current active group this.group_remove_selected = function() { @@ -5233,62 +5964,9 @@ } }; - // handler for keyboard events on the input field - this.add_input_keydown = function(e) - { - var key = rcube_event.get_keycode(e), - input = $(e.target), itype = input.data('tt'); - - // enter - if (key == 13) { - var newname = input.val(); - - if (newname) { - var lock = this.set_busy(true, 'loading'); - - if (itype == 'contactsearch') - this.http_post('search-create', {_search: this.env.search_request, _name: newname}, lock); - else if (this.env.group_renaming) - this.http_post('group-rename', {_source: this.env.source, _gid: this.env.group, _name: newname}, lock); - else - this.http_post('group-create', {_source: this.env.source, _name: newname}, lock); - } - return false; - } - // escape - else if (key == 27) - this.reset_add_input(); - - return true; - }; - - this.reset_add_input = function() - { - if (this.name_input) { - var li = this.name_input.parent(); - if (this.env.group_renaming) { - li.children().last().show(); - this.env.group_renaming = false; - } - else if ($('li', li.parent()).length == 1) - li.parent().hide(); - - this.name_input.remove(); - - if (this.name_input_li) - this.name_input_li.remove(); - - this.name_input = this.name_input_li = null; - } - - this.enable_command('list', 'listgroup', true); - }; - // callback for creating a new contact group this.insert_contact_group = function(prop) { - this.reset_add_input(); - prop.type = 'group'; var key = 'G'+prop.source+prop.id, @@ -5306,8 +5984,6 @@ // callback for renaming a contact group this.update_contact_group = function(prop) { - this.reset_add_input(); - var key = 'G'+prop.source+prop.id, newnode = {}; @@ -5571,8 +6247,6 @@ // callback for creating a new saved search record this.insert_saved_search = function(name, id) { - this.reset_add_input(); - var key = 'S'+id, link = $('<a>').attr('href', '#') .attr('rel', id) @@ -5588,10 +6262,28 @@ this.triggerEvent('abook_search_insert', prop); }; - // creates an input for saved search name + // creates a dialog for saved search this.search_create = function() { - this.add_input_row('contactsearch'); + var input = $('<input>').attr('type', 'text'), + content = $('<label>').text(this.get_label('namex')).append(input); + + this.show_popup_dialog(content, this.get_label('searchsave'), + [{ + text: this.get_label('save'), + 'class': 'mainaction', + click: function() { + var name; + + if (name = input.val()) { + ref.http_post('search-create', {_search: ref.env.search_request, _name: name}, + ref.set_busy(true, 'loading')); + } + + $(this).dialog('close'); + } + }] + ); }; this.search_delete = function() @@ -5702,10 +6394,8 @@ id = this.env.iid ? this.env.iid : selection[0]; // submit request with appended token - if (confirm(this.get_label('deleteidentityconfirm'))) - this.goto_url('delete-identity', { _iid: id, _token: this.env.request_token }, true); - - return true; + if (id && confirm(this.get_label('deleteidentityconfirm'))) + this.http_post('settings/delete-identity', { _iid: id }, true); }; this.update_identity_row = function(id, name, add) @@ -5749,6 +6439,23 @@ frame.location.href = this.env.blankpage; } } + + this.enable_command('delete', false); + }; + + this.remove_identity = function(id) + { + var frame, list = this.identity_list, + rid = this.html_identifier(id); + + if (list && id) { + list.remove_row(rid); + if (this.env.contentframe && (frame = this.get_frame_window(this.env.contentframe))) { + frame.location.href = this.env.blankpage; + } + } + + this.enable_command('delete', false); }; @@ -5762,64 +6469,60 @@ this.last_sub_rx = RegExp('['+delim+']?[^'+delim+']+$'); - this.subscription_list = new rcube_list_widget(this.gui_objects.subscriptionlist, - {multiselect:false, draggable:true, keyboard:true, toggleselect:true}); + this.subscription_list = new rcube_treelist_widget(this.gui_objects.subscriptionlist, { + selectable: true, + tabexit: false, + parent_focus: true, + id_prefix: 'rcmli', + id_encode: this.html_identifier_encode, + id_decode: this.html_identifier_decode, + searchbox: '#foldersearch' + }); + this.subscription_list - .addEventListener('select', function(o){ ref.subscription_select(o); }) - .addEventListener('dragstart', function(o){ ref.drag_active = true; }) - .addEventListener('dragend', function(o){ ref.subscription_move_folder(o); }) - .addEventListener('initrow', function (row) { - row.obj.onmouseover = function() { ref.focus_subscription(row.id); }; - row.obj.onmouseout = function() { ref.unfocus_subscription(row.id); }; - }) - .init() - .focus(); + .addEventListener('select', function(node) { ref.subscription_select(node.id); }) + .addEventListener('collapse', function(node) { ref.folder_collapsed(node) }) + .addEventListener('expand', function(node) { ref.folder_collapsed(node) }) + .addEventListener('search', function(p) { if (p.query) ref.subscription_select(); }) + .draggable({cancel: 'li.mailbox.root'}) + .droppable({ + // @todo: find better way, accept callback is executed for every folder + // on the list when dragging starts (and stops), this is slow, but + // I didn't find a method to check droptarget on over event + accept: function(node) { + if (!$(node).is('.mailbox')) + return false; - $('#mailboxroot') - .mouseover(function(){ ref.focus_subscription(this.id); }) - .mouseout(function(){ ref.unfocus_subscription(this.id); }) - }; + var source_folder = ref.folder_id2name($(node).attr('id')), + dest_folder = ref.folder_id2name(this.id), + source = ref.env.subscriptionrows[source_folder], + dest = ref.env.subscriptionrows[dest_folder]; - this.focus_subscription = function(id) - { - var row, folder; + return source && !source[2] + && dest_folder != source_folder.replace(ref.last_sub_rx, '') + && !dest_folder.startsWith(source_folder + ref.env.delimiter); + }, + drop: function(e, ui) { + var source = ref.folder_id2name(ui.draggable.attr('id')), + dest = ref.folder_id2name(this.id); - if (this.drag_active && this.env.mailbox && (row = document.getElementById(id))) - if (this.env.subscriptionrows[id] && - (folder = this.env.subscriptionrows[id][0]) !== null - ) { - if (this.check_droptarget(folder) && - !this.env.subscriptionrows[this.get_folder_row_id(this.env.mailbox)][2] && - folder != this.env.mailbox.replace(this.last_sub_rx, '') && - !folder.startsWith(this.env.mailbox + this.env.delimiter) - ) { - this.env.dstfolder = folder; - $(row).addClass('droptarget'); + ref.subscription_move_folder(source, dest); } - } + }); }; - this.unfocus_subscription = function(id) + this.folder_id2name = function(id) { - var row = $('#'+id); - - this.env.dstfolder = null; - - if (row.length && this.env.subscriptionrows[id]) - row.removeClass('droptarget'); - else - $(this.subscription_list.frame).removeClass('droptarget'); + return id ? ref.html_identifier_decode(id.replace(/^rcmli/, '')) : null; }; - this.subscription_select = function(list) + this.subscription_select = function(id) { - var id, folder; + var folder; - if (list && (id = list.get_single_selection()) && - (folder = this.env.subscriptionrows['rcmrow'+id]) - ) { - this.env.mailbox = folder[0]; - this.show_folder(folder[0]); + if (id && id != '*' && (folder = this.env.subscriptionrows[id])) { + this.env.mailbox = id; + this.show_folder(id); this.enable_command('delete-folder', !folder[2]); } else { @@ -5829,24 +6532,18 @@ } }; - this.subscription_move_folder = function(list) + this.subscription_move_folder = function(from, to) { - if (this.env.mailbox && this.env.dstfolder !== null && - this.env.dstfolder != this.env.mailbox && - this.env.dstfolder != this.env.mailbox.replace(this.last_sub_rx, '') - ) { - var path = this.env.mailbox.split(this.env.delimiter), + if (from && to !== null && from != to && to != from.replace(this.last_sub_rx, '')) { + var path = from.split(this.env.delimiter), basename = path.pop(), - newname = this.env.dstfolder === '' ? basename : this.env.dstfolder + this.env.delimiter + basename; + newname = to === '' || to === '*' ? basename : to + this.env.delimiter + basename; - if (newname != this.env.mailbox) { - this.http_post('rename-folder', {_folder_oldname: this.env.mailbox, _folder_newname: newname}, this.set_busy(true, 'foldermoving')); - this.subscription_list.draglayer.hide(); + if (newname != from) { + this.http_post('rename-folder', {_folder_oldname: from, _folder_newname: newname}, + this.set_busy(true, 'foldermoving')); } } - - this.drag_active = false; - this.unfocus_subscription(this.get_folder_row_id(this.env.dstfolder)); }; // tell server to create and subscribe a new mailbox @@ -5858,51 +6555,61 @@ // delete a specific mailbox with all its messages this.delete_folder = function(name) { - var id = this.get_folder_row_id(name ? name : this.env.mailbox), - folder = this.env.subscriptionrows[id][0]; + if (!name) + name = this.env.mailbox; - if (folder && confirm(this.get_label('deletefolderconfirm'))) { - var lock = this.set_busy(true, 'folderdeleting'); - this.http_post('delete-folder', {_mbox: folder}, lock); + if (name && confirm(this.get_label('deletefolderconfirm'))) { + this.http_post('delete-folder', {_mbox: name}, this.set_busy(true, 'folderdeleting')); } }; // Add folder row to the table and initialize it - this.add_folder_row = function (name, display_name, is_protected, subscribed, skip_init, class_name) + this.add_folder_row = function (id, name, display_name, is_protected, subscribed, class_name, refrow, subfolders) { if (!this.gui_objects.subscriptionlist) return false; - var row, n, tmp, tmp_name, rowid, collator, - folders = [], list = [], slist = [], - tbody = this.gui_objects.subscriptionlist.tBodies[0], - refrow = $('tr', tbody).get(1), - id = 'rcmrow'+((new Date).getTime()); + // reset searching + if (this.subscription_list.is_search()) { + this.subscription_select(); + this.subscription_list.reset_search(); + } - if (!refrow) { + // disable drag-n-drop temporarily + this.subscription_list.draggable('destroy').droppable('destroy'); + + var row, n, tmp, tmp_name, rowid, collator, pos, p, parent = '', + folders = [], list = [], slist = [], + list_element = $(this.gui_objects.subscriptionlist); + row = refrow ? refrow : $($('li', list_element).get(1)).clone(true); + + if (!row.length) { // Refresh page if we don't have a table row to clone this.goto_url('folders'); return false; } - // clone a table row if there are existing rows - row = $(refrow).clone(true); - // set ID, reset css class - row.attr({id: id, 'class': class_name}); + row.attr({id: 'rcmli' + this.html_identifier_encode(id), 'class': class_name}); + + if (!refrow || !refrow.length) { + // remove old data, subfolders and toggle + $('ul,div.treetoggle', row).remove(); + row.removeData('filtered'); + } // set folder name - row.find('td:first').html(display_name); + $('a:first', row).text(display_name); // update subscription checkbox - $('input[name="_subscribed[]"]', row).val(name) + $('input[name="_subscribed[]"]:first', row).val(id) .prop({checked: subscribed ? true : false, disabled: is_protected ? true : false}); // add to folder/row-ID map this.env.subscriptionrows[id] = [name, display_name, false]; // copy folders data to an array for sorting - $.each(this.env.subscriptionrows, function(k, v) { folders.push(v); }); + $.each(this.env.subscriptionrows, function(k, v) { v[3] = k; folders.push(v); }); try { // use collator if supported (FF29, IE11, Opera15, Chrome24) @@ -5914,65 +6621,108 @@ folders.sort(function(a, b) { var i, f1, f2, path1 = a[0].split(ref.env.delimiter), - path2 = b[0].split(ref.env.delimiter); + path2 = b[0].split(ref.env.delimiter), + len = path1.length; - for (i=0; i<path1.length; i++) { + for (i=0; i<len; i++) { f1 = path1[i]; f2 = path2[i]; if (f1 !== f2) { + if (f2 === undefined) + return 1; if (collator) return collator.compare(f1, f2); else return f1 < f2 ? -1 : 1; } + else if (i == len-1) { + return -1 + } } }); for (n in folders) { + p = folders[n][3]; // protected folder if (folders[n][2]) { - tmp_name = folders[n][0] + this.env.delimiter; + tmp_name = p + this.env.delimiter; // prefix namespace cannot have subfolders (#1488349) if (tmp_name == this.env.prefix_ns) continue; - slist.push(folders[n][0]); + slist.push(p); tmp = tmp_name; } // protected folder's child - else if (tmp && folders[n][0].startsWith(tmp)) - slist.push(folders[n][0]); + else if (tmp && p.startsWith(tmp)) + slist.push(p); // other else { - list.push(folders[n][0]); + list.push(p); tmp = null; } } // check if subfolder of a protected folder for (n=0; n<slist.length; n++) { - if (name.startsWith(slist[n] + this.env.delimiter)) - rowid = this.get_folder_row_id(slist[n]); + if (id.startsWith(slist[n] + this.env.delimiter)) + rowid = slist[n]; } // find folder position after sorting for (n=0; !rowid && n<list.length; n++) { - if (n && list[n] == name) - rowid = this.get_folder_row_id(list[n-1]); + if (n && list[n] == id) + rowid = list[n-1]; } // add row to the table - if (rowid) - $('#'+rowid).after(row); - else - row.appendTo(tbody); + if (rowid && (n = this.subscription_list.get_item(rowid, true))) { + // find parent folder + if (pos = id.lastIndexOf(this.env.delimiter)) { + parent = id.substring(0, pos); + parent = this.subscription_list.get_item(parent, true); + + // add required tree elements to the parent if not already there + if (!$('div.treetoggle', parent).length) { + $('<div> </div>').addClass('treetoggle collapsed').appendTo(parent); + } + if (!$('ul', parent).length) { + $('<ul>').css('display', 'none').appendTo(parent); + } + } + + if (parent && n == parent) { + $('ul:first', parent).append(row); + } + else { + while (p = $(n).parent().parent().get(0)) { + if (parent && p == parent) + break; + if (!$(p).is('li.mailbox')) + break; + n = p; + } + + $(n).after(row); + } + } + else { + list_element.append(row); + } + + // add subfolders + $.extend(this.env.subscriptionrows, subfolders || {}); // update list widget - this.subscription_list.clear_selection(); - if (!skip_init) - this.init_subscription_list(); + this.subscription_list.reset(true); + this.subscription_select(); - row = row.get(0); + // expand parent + if (parent) { + this.subscription_list.expand(this.folder_id2name(parent.id)); + } + + row = row.show().get(0); if (row.scrollIntoView) row.scrollIntoView(); @@ -5980,114 +6730,85 @@ }; // replace an existing table row with a new folder line (with subfolders) - this.replace_folder_row = function(oldfolder, newfolder, display_name, is_protected, class_name) + this.replace_folder_row = function(oldid, id, name, display_name, is_protected, class_name) { if (!this.gui_objects.subscriptionlist) { - if (this.is_framed) - return parent.rcmail.replace_folder_row(oldfolder, newfolder, display_name, is_protected, class_name); + if (this.is_framed()) { + // @FIXME: for some reason this 'parent' variable need to be prefixed with 'window.' + return window.parent.rcmail.replace_folder_row(oldid, id, name, display_name, is_protected, class_name); + } + return false; } - var i, n, len, name, dispname, oldrow, tmprow, row, level, - tbody = this.gui_objects.subscriptionlist.tBodies[0], - folders = this.env.subscriptionrows, - id = this.get_folder_row_id(oldfolder), - prefix_len = oldfolder.length, - subscribed = $('input[name="_subscribed[]"]', $('#'+id)).prop('checked'), - // find subfolders of renamed folder - list = this.get_subfolders(oldfolder); + // reset searching + if (this.subscription_list.is_search()) { + this.subscription_select(); + this.subscription_list.reset_search(); + } + + var subfolders = {}, + row = this.subscription_list.get_item(oldid, true), + parent = $(row).parent(), + old_folder = this.env.subscriptionrows[oldid], + prefix_len_id = oldid.length, + prefix_len_name = old_folder[0].length, + subscribed = $('input[name="_subscribed[]"]:first', row).prop('checked'); // no renaming, only update class_name - if (oldfolder == newfolder) { - $('#'+id).attr('class', class_name || ''); - this.subscription_list.focus(); + if (oldid == id) { + $(row).attr('class', class_name || ''); return; } - // replace an existing table row - this._remove_folder_row(id); - row = $(this.add_folder_row(newfolder, display_name, is_protected, subscribed, true, class_name)); + // update subfolders + $('li', row).each(function() { + var fname = ref.folder_id2name(this.id), + folder = ref.env.subscriptionrows[fname], + newid = id + fname.slice(prefix_len_id); - // detect tree depth change - if (len = list.length) { - level = (oldfolder.split(this.env.delimiter)).length - (newfolder.split(this.env.delimiter)).length; + this.id = 'rcmli' + ref.html_identifier_encode(newid); + $('input[name="_subscribed[]"]:first', this).val(newid); + folder[0] = name + folder[0].slice(prefix_len_name); + + subfolders[newid] = folder; + delete ref.env.subscriptionrows[fname]; + }); + + // get row off the list + row = $(row).detach(); + + delete this.env.subscriptionrows[oldid]; + + // remove parent list/toggle elements if not needed + if (parent.get(0) != this.gui_objects.subscriptionlist && !$('li', parent).length) { + $('ul,div.treetoggle', parent.parent()).remove(); } - // move subfolders to the new branch - for (n=0; n<len; n++) { - id = list[n]; - name = this.env.subscriptionrows[id][0]; - dispname = this.env.subscriptionrows[id][1]; - oldrow = $('#'+id); - tmprow = oldrow.clone(true); - oldrow.remove(); - row.after(tmprow); - row = tmprow; - // update folder index - name = newfolder + name.slice(prefix_len); - $('input[name="_subscribed[]"]', row).val(name); - this.env.subscriptionrows[id][0] = name; - // update the name if level is changed - if (level != 0) { - if (level > 0) { - for (i=level; i>0; i--) - dispname = dispname.replace(/^ /, ''); - } - else { - for (i=level; i<0; i++) - dispname = ' ' + dispname; - } - row.find('td:first').html(dispname); - this.env.subscriptionrows[id][1] = dispname; - } - } - - // update list widget - this.init_subscription_list(); + // move the existing table row + this.add_folder_row(id, name, display_name, is_protected, subscribed, class_name, row, subfolders); }; // remove the table row of a specific mailbox from the table - this.remove_folder_row = function(folder, subs) + this.remove_folder_row = function(folder) { - var n, len, list = [], id = this.get_folder_row_id(folder); - - // get subfolders if any - if (subs) - list = this.get_subfolders(folder); - - // remove old row - this._remove_folder_row(id); - - // remove subfolders - for (n=0, len=list.length; n<len; n++) - this._remove_folder_row(list[n]); - }; - - this._remove_folder_row = function(id) - { - this.subscription_list.remove_row(id.replace(/^rcmrow/, '')); - $('#'+id).remove(); - delete this.env.subscriptionrows[id]; - }; - - this.get_subfolders = function(folder) - { - var name, list = [], - prefix = folder + this.env.delimiter, - row = $('#'+this.get_folder_row_id(folder)).get(0); - - while (row = row.nextSibling) { - if (row.id) { - name = this.env.subscriptionrows[row.id][0]; - if (name && name.startsWith(prefix)) { - list.push(row.id); - } - else - break; - } + // reset searching + if (this.subscription_list.is_search()) { + this.subscription_select(); + this.subscription_list.reset_search(); } - return list; + var list = [], row = this.subscription_list.get_item(folder, true); + + // get subfolders if any + $('li', row).each(function() { list.push(ref.folder_id2name(this.id)); }); + + // remove folder row (and subfolders) + this.subscription_list.remove(folder); + + // update local list variable + list.push(folder); + $.each(list, function(i, v) { delete ref.env.subscriptionrows[v]; }); }; this.subscribe = function(folder) @@ -6104,15 +6825,6 @@ var lock = this.display_message(this.get_label('folderunsubscribing'), 'loading'); this.http_post('unsubscribe', {_mbox: folder}, lock); } - }; - - // helper method to find a specific mailbox row ID - this.get_folder_row_id = function(folder) - { - var id, folders = this.env.subscriptionrows; - for (id in folders) - if (folders[id] && folders[id][0] == folder) - return id; }; // when user select a folder in manager @@ -6138,9 +6850,9 @@ // disables subscription checkbox (for protected folder) this.disable_subscription = function(folder) { - var id = this.get_folder_row_id(folder); - if (id) - $('input[name="_subscribed[]"]', $('#'+id)).prop('disabled', true); + var row = this.subscription_list.get_item(folder, true); + if (row) + $('input[name="_subscribed[]"]:first', row).prop('disabled', true); }; this.folder_size = function(folder) @@ -6154,6 +6866,37 @@ $('#folder-size').replaceWith(size); }; + // filter folders by namespace + this.folder_filter = function(prefix) + { + this.subscription_list.reset_search(); + + this.subscription_list.container.children('li').each(function() { + var i, folder = ref.folder_id2name(this.id); + // show all folders + if (prefix == '---') { + } + // got namespace prefix + else if (prefix) { + if (folder !== prefix) { + $(this).data('filtered', true).hide(); + return + } + } + // no namespace prefix, filter out all other namespaces + else { + // first get all namespace roots + for (i in ref.env.ns_roots) { + if (folder === ref.env.ns_roots[i]) { + $(this).data('filtered', true).hide(); + return; + } + } + } + + $(this).removeData('filtered').show(); + }); + }; /*********************************************************/ /********* GUI functionality *********/ @@ -6316,7 +7059,7 @@ }; // display a system message, list of types in common.css (below #message definition) - this.display_message = function(msg, type, timeout) + this.display_message = function(msg, type, timeout, key) { // pass command to parent window if (this.is_framed()) @@ -6325,18 +7068,34 @@ if (!this.gui_objects.message) { // save message in order to display after page loaded if (type != 'loading') - this.pending_message = [msg, type, timeout]; + this.pending_message = [msg, type, timeout, key]; return 1; } - type = type ? type : 'notice'; + if (!type) + type = 'notice'; - var key = this.html_identifier(msg), - date = new Date(), + if (!key) + key = this.html_identifier(msg); + + var date = new Date(), id = type + date.getTime(); - if (!timeout) - timeout = this.message_time * (type == 'error' || type == 'warning' ? 2 : 1); + if (!timeout) { + switch (type) { + case 'error': + case 'warning': + timeout = this.message_time * 2; + break; + + case 'uploading': + timeout = 0; + break; + + default: + timeout = this.message_time; + } + } if (type == 'loading') { key = 'loading'; @@ -6369,7 +7128,7 @@ if (type == 'loading') { this.messages[key].labels = [{'id': id, 'msg': msg}]; } - else { + else if (type != 'uploading') { obj.click(function() { return ref.hide_message(obj); }) .attr('role', 'alert'); } @@ -6378,6 +7137,7 @@ if (timeout > 0) setTimeout(function() { ref.hide_message(id, type != 'loading'); }, timeout); + return id; }; @@ -6456,24 +7216,60 @@ this.messages = {}; }; + // display uploading message with progress indicator + // data should contain: name, total, current, percent, text + this.display_progress = function(data) + { + if (!data || !data.name) + return; + + var msg = this.messages['progress' + data.name]; + + if (!data.label) + data.label = this.get_label('uploadingmany'); + + if (!msg) { + if (!data.percent || data.percent < 100) + this.display_message(data.label, 'uploading', 0, 'progress' + data.name); + return; + } + + if (!data.total || data.percent >= 100) { + this.hide_message(msg.obj); + return; + } + + if (data.text) + data.label += ' ' + data.text; + + msg.obj.text(data.label); + }; + // open a jquery UI dialog with the given content - this.show_popup_dialog = function(html, title, buttons, options) + this.show_popup_dialog = function(content, title, buttons, options) { // forward call to parent window if (this.is_framed()) { - return parent.rcmail.show_popup_dialog(html, title, buttons, options); + return parent.rcmail.show_popup_dialog(content, title, buttons, options); } - var popup = $('<div class="popup">') - .html(html) - .dialog($.extend({ + var popup = $('<div class="popup">'); + + if (typeof content == 'object') + popup.append(content); + else + popup.html(content); + + options = $.extend({ title: title, buttons: buttons, modal: true, resizable: true, width: 500, - close: function(event, ui) { $(this).remove() } - }, options || {})); + close: function(event, ui) { $(this).remove(); } + }, options || {}); + + popup.dialog(options); // resize and center popup var win = $(window), w = win.width(), h = win.height(), @@ -6484,6 +7280,11 @@ width: Math.min(w - 20, width + 36) }); + // assign special classes to dialog buttons + $.each(options.button_classes || [], function(i, v) { + if (v) $($('.ui-dialog-buttonpane button.ui-button', popup.parent()).get(i)).addClass(v); + }); + return popup; }; @@ -6492,6 +7293,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 @@ -6548,6 +7351,9 @@ repl, cell, col, n, len, tr; this.env.listcols = listcols; + + if (!this.env.coltypes) + this.env.coltypes = {}; // replace old column headers if (thead) { @@ -6620,7 +7426,7 @@ this.set_quota = function(content) { if (this.gui_objects.quotadisplay && content && content.type == 'text') - $(this.gui_objects.quotadisplay).html(content.percent+'%').attr('title', content.title); + $(this.gui_objects.quotadisplay).text((content.percent||0) + '%').attr('title', content.title); this.triggerEvent('setquota', content); this.env.quota_content = content; @@ -6876,8 +7682,8 @@ 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) - this.hide_menu(this.menu_stack[i]); + 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) { obj.data('parent', $.last(this.menu_stack)); @@ -7040,7 +7846,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; @@ -7052,12 +7858,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 @@ -7066,7 +7872,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) @@ -7088,9 +7900,11 @@ } }; - this.goto_url = function(action, query, lock) + this.goto_url = function(action, query, lock, secure) { - this.redirect(this.url(action, query), lock); + var url = this.url(action, query) + if (secure) url = this.secure_url(url); + this.redirect(url, lock); }; this.location_href = function(url, target, frame) @@ -7119,69 +7933,54 @@ }; // send a http request to the server - this.http_request = function(action, query, lock) + this.http_request = function(action, data, lock, type) { - var url = this.url(action, query); + if (type != 'POST') + type = 'GET'; + + 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'; - - // send request - this.log('HTTP GET: ' + url); + var url = this.url(action); // reset keep-alive interval this.start_keepalive(); + // send request return $.ajax({ - type: 'GET', url: url, data: { _unlock:(lock?lock:0) }, dataType: 'json', - success: function(data){ ref.http_response(data); }, + type: type, 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); } }); }; + // send a http GET request to the server + this.http_get = this.http_request; + // 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 (postdata && typeof postdata === 'object') { - postdata._remote = 1; - postdata._unlock = (lock ? lock : 0); - } - else - postdata += (postdata ? '&' : '') + '_remote=1' + (lock ? '&_unlock='+lock : ''); - - // 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; - } - - // send request - this.log('HTTP POST: ' + url); - - // reset keep-alive interval - this.start_keepalive(); - - return $.ajax({ - type: 'POST', url: url, data: postdata, dataType: 'json', - success: function(data){ ref.http_response(data); }, - error: function(o, status, err) { ref.http_error(o, status, err, lock, action); } - }); + return this.http_request(action, data, lock, 'POST'); }; // aborts ajax request @@ -7247,7 +8046,7 @@ this.enable_command('compose', (uid && this.contact_list.rows[uid])); this.enable_command('delete', 'edit', writable); this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0)); - this.enable_command('export-selected', false); + this.enable_command('export-selected', 'print', false); } case 'move': @@ -7294,32 +8093,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') { @@ -7329,7 +8132,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 }); } @@ -7398,6 +8201,7 @@ // save message in local storage and do not redirect if (this.env.action == 'compose') { this.save_compose_form_local(); + this.compose_skip_unsavedcheck = true; } else if (redirect_url) { setTimeout(function(){ ref.redirect(redirect_url, true); }, 2000); @@ -7559,11 +8363,11 @@ } // 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, - action: this.url(action, { _id:this.env.compose_id||'', _uploadid:ts }), + action: this.url(action, {_id: this.env.compose_id || '', _uploadid: ts, _from: this.env.action}), method: 'POST'}) .attr(form.encoding ? 'encoding' : 'enctype', 'multipart/form-data') .submit(); @@ -7581,7 +8385,7 @@ // html5 file-drop API this.document_drag_hover = function(e, over) { - e.preventDefault(); + // don't e.preventDefault() here to not block text dragging on the page (#1490619) $(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('active'); }; @@ -7600,21 +8404,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.get_label('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 })) @@ -7623,10 +8445,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 }), + 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() @@ -7786,12 +8610,24 @@ }; // get window.opener.rcmail if available - this.opener = function() + this.opener = function(deep, filter) { + var i, win = window.opener; + // catch Error: Permission denied to access property rcmail try { - if (window.opener && !opener.closed && opener.rcmail) - return opener.rcmail; + if (win && !win.closed) { + // try parent of the opener window, e.g. preview frame + if (deep && (!win.rcmail || win.rcmail.env.framed) && win.parent && win.parent.rcmail) + win = win.parent; + + if (win.rcmail && filter) + for (i in filter) + if (win.rcmail.env[i] != filter[i]) + return; + + return win.rcmail; + } } catch (e) {} }; @@ -7800,20 +8636,36 @@ // and return the message uid this.get_single_uid = function() { - return this.env.uid ? this.env.uid : (this.message_list ? this.message_list.get_single_selection() : null); + var uid = this.env.uid || (this.message_list ? this.message_list.get_single_selection() : null); + var result = ref.triggerEvent('get_single_uid', { uid: uid }); + return result || uid; }; // same as above but for contacts this.get_single_cid = function() { - return this.env.cid ? this.env.cid : (this.contact_list ? this.contact_list.get_single_selection() : null); + var cid = this.env.cid || (this.contact_list ? this.contact_list.get_single_selection() : null); + var result = ref.triggerEvent('get_single_cid', { cid: cid }); + return result || cid; }; // 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 @@ -7911,14 +8763,10 @@ if (!this.env.browser_capabilities) this.env.browser_capabilities = {}; - if (this.env.browser_capabilities.pdf === undefined) - this.env.browser_capabilities.pdf = this.pdf_support_check(); - - if (this.env.browser_capabilities.flash === undefined) - this.env.browser_capabilities.flash = this.flash_support_check(); - - if (this.env.browser_capabilities.tif === undefined) - this.tif_support_check(); + $.each(['pdf', 'flash', 'tif'], function() { + if (ref.env.browser_capabilities[this] === undefined) + ref.env.browser_capabilities[this] = ref[this + '_support_check'](); + }); }; // Returns browser capabilities string @@ -7937,11 +8785,14 @@ this.tif_support_check = function() { - var img = new Image(); + window.setTimeout(function() { + var img = new Image(); + img.onload = function() { ref.env.browser_capabilities.tif = 1; }; + img.onerror = function() { ref.env.browser_capabilities.tif = 0; }; + img.src = ref.assets_path('program/resources/blank.tif'); + }, 10); - img.onload = function() { ref.env.browser_capabilities.tif = 1; }; - img.onerror = function() { ref.env.browser_capabilities.tif = 0; }; - img.src = 'program/resources/blank.tif'; + return 0; }; this.pdf_support_check = function() @@ -7954,7 +8805,7 @@ if (plugin && plugin.enabledPlugin) return 1; - if (window.ActiveXObject) { + if ('ActiveXObject' in window) { try { if (plugin = new ActiveXObject("AcroPDF.PDF")) return 1; @@ -7977,6 +8828,14 @@ return 1; } + window.setTimeout(function() { + $('<object>').css({position: 'absolute', left: '-10000px'}) + .attr({data: ref.assets_path('program/resources/dummy.pdf'), width: 1, height: 1, type: 'application/pdf'}) + .load(function() { ref.env.browser_capabilities.pdf = 1; }) + .error(function() { ref.env.browser_capabilities.pdf = 0; }) + .appendTo($('body')); + }, 10); + return 0; }; @@ -7987,7 +8846,7 @@ if (plugin && plugin.enabledPlugin) return 1; - if (window.ActiveXObject) { + if ('ActiveXObject' in window) { try { if (plugin = new ActiveXObject("ShockwaveFlash.ShockwaveFlash")) return 1; @@ -7996,6 +8855,15 @@ } return 0; + }; + + this.assets_path = function(path) + { + if (this.env.assets_path && !path.startsWith(this.env.assets_path)) { + path = this.env.assets_path + path; + } + + return path; }; // Cookie setter @@ -8015,22 +8883,52 @@ // wrapper for localStorage.getItem(key) this.local_storage_get_item = function(key, deflt, encrypted) { + var item, result; + // TODO: add encryption - var item = localStorage.getItem(this.get_local_storage_prefix() + key); - return item !== null ? JSON.parse(item) : (deflt || null); + try { + item = localStorage.getItem(this.get_local_storage_prefix() + key); + result = JSON.parse(item); + } + catch (e) { } + + return result || deflt || null; }; // wrapper for localStorage.setItem(key, data) this.local_storage_set_item = function(key, data, encrypted) { - // TODO: add encryption - return localStorage.setItem(this.get_local_storage_prefix() + key, JSON.stringify(data)); + // try/catch to handle no localStorage support, but also error + // in Safari-in-private-browsing-mode where localStorage exists + // but can't be used (#1489996) + try { + // TODO: add encryption + localStorage.setItem(this.get_local_storage_prefix() + key, JSON.stringify(data)); + return true; + } + catch (e) { + return false; + } }; // wrapper for localStorage.removeItem(key) this.local_storage_remove_item = function(key) { - return localStorage.removeItem(this.get_local_storage_prefix() + key); + try { + localStorage.removeItem(this.get_local_storage_prefix() + key); + return true; + } + catch (e) { + return false; + } + }; + + this.print_dialog = function() + { + if (bw.safari) + setTimeout('window.print()', 10); + else + window.print(); }; } // end object rcube_webmail @@ -8041,7 +8939,7 @@ if (!elem.title) { var $elem = $(elem); if ($elem.width() + (indent || 0) * 15 > $elem.parent().width()) - elem.title = $elem.text(); + elem.title = rcube_webmail.subject_text(elem); } }; @@ -8058,10 +8956,17 @@ tmp.remove(); if (w + $('span.branch', $elem).width() * 15 > $elem.width()) - elem.title = txt; + elem.title = rcube_webmail.subject_text(elem); } }; +rcube_webmail.subject_text = function(elem) +{ + var t = $(elem).clone(); + t.find('.skip-on-drag').remove(); + return t.text(); +}; + rcube_webmail.prototype.get_cookie = getCookie; // copy event engine prototype -- Gitblit v1.9.1