comparison program/js/app.js @ 0:4681f974d28b

vanilla 1.3.3 distro, I hope
author Charlie Root
date Thu, 04 Jan 2018 15:52:31 -0500
parents
children 0cac1d1e799f
comparison
equal deleted inserted replaced
-1:000000000000 0:4681f974d28b
1 /**
2 * Roundcube Webmail Client Script
3 *
4 * This file is part of the Roundcube Webmail client
5 *
6 * @licstart The following is the entire license notice for the
7 * JavaScript code in this file.
8 *
9 * Copyright (C) 2005-2015, The Roundcube Dev Team
10 * Copyright (C) 2011-2015, Kolab Systems AG
11 *
12 * The JavaScript code in this page is free software: you can
13 * redistribute it and/or modify it under the terms of the GNU
14 * General Public License (GNU GPL) as published by the Free Software
15 * Foundation, either version 3 of the License, or (at your option)
16 * any later version. The code is distributed WITHOUT ANY WARRANTY;
17 * without even the implied warranty of MERCHANTABILITY or FITNESS
18 * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
19 *
20 * As additional permission under GNU GPL version 3 section 7, you
21 * may distribute non-source (e.g., minimized or compacted) forms of
22 * that code without the copy of the GNU GPL normally required by
23 * section 4, provided you include this license notice and a URL
24 * through which recipients can access the Corresponding Source.
25 *
26 * @licend The above is the entire license notice
27 * for the JavaScript code in this file.
28 *
29 * @author Thomas Bruederli <roundcube@gmail.com>
30 * @author Aleksander 'A.L.E.C' Machniak <alec@alec.pl>
31 * @author Charles McNulty <charles@charlesmcnulty.com>
32 *
33 * @requires jquery.js, common.js, list.js
34 */
35
36 function rcube_webmail()
37 {
38 this.labels = {};
39 this.buttons = {};
40 this.buttons_sel = {};
41 this.gui_objects = {};
42 this.gui_containers = {};
43 this.commands = {};
44 this.command_handlers = {};
45 this.onloads = [];
46 this.messages = {};
47 this.group2expand = {};
48 this.http_request_jobs = {};
49 this.menu_stack = [];
50
51 // webmail client settings
52 this.dblclick_time = 500;
53 this.message_time = 5000;
54 this.preview_delay_select = 400;
55 this.preview_delay_click = 60;
56 this.identifier_expr = /[^0-9a-z_-]/gi;
57
58 // environment defaults
59 this.env = {
60 request_timeout: 180, // seconds
61 draft_autosave: 0, // seconds
62 comm_path: './',
63 recipients_separator: ',',
64 recipients_delimiter: ', ',
65 popup_width: 1150,
66 popup_width_small: 900
67 };
68
69 // create protected reference to myself
70 this.ref = 'rcmail';
71 var ref = this;
72
73 // set jQuery ajax options
74 $.ajaxSetup({
75 cache: false,
76 timeout: this.env.request_timeout * 1000,
77 error: function(request, status, err){ ref.http_error(request, status, err); },
78 beforeSend: function(xmlhttp){ xmlhttp.setRequestHeader('X-Roundcube-Request', ref.env.request_token); }
79 });
80
81 // unload fix
82 $(window).on('beforeunload', function() { ref.unload = true; });
83
84 // set environment variable(s)
85 this.set_env = function(p, value)
86 {
87 if (p != null && typeof p === 'object' && !value)
88 for (var n in p)
89 this.env[n] = p[n];
90 else
91 this.env[p] = value;
92 };
93
94 // add a localized label to the client environment
95 this.add_label = function(p, value)
96 {
97 if (typeof p == 'string')
98 this.labels[p] = value;
99 else if (typeof p == 'object')
100 $.extend(this.labels, p);
101 };
102
103 // add a button to the button list
104 this.register_button = function(command, id, type, act, sel, over)
105 {
106 var button_prop = {id:id, type:type};
107
108 if (act) button_prop.act = act;
109 if (sel) button_prop.sel = sel;
110 if (over) button_prop.over = over;
111
112 if (!this.buttons[command])
113 this.buttons[command] = [];
114
115 this.buttons[command].push(button_prop);
116
117 if (this.loaded)
118 init_button(command, button_prop);
119 };
120
121 // register a specific gui object
122 this.gui_object = function(name, id)
123 {
124 this.gui_objects[name] = this.loaded ? rcube_find_object(id) : id;
125 };
126
127 // register a container object
128 this.gui_container = function(name, id)
129 {
130 this.gui_containers[name] = id;
131 };
132
133 // add a GUI element (html node) to a specified container
134 this.add_element = function(elm, container)
135 {
136 if (this.gui_containers[container] && this.gui_containers[container].jquery)
137 this.gui_containers[container].append(elm);
138 };
139
140 // register an external handler for a certain command
141 this.register_command = function(command, callback, enable)
142 {
143 this.command_handlers[command] = callback;
144
145 if (enable)
146 this.enable_command(command, true);
147 };
148
149 // execute the given script on load
150 this.add_onload = function(f)
151 {
152 this.onloads.push(f);
153 };
154
155 // initialize webmail client
156 this.init = function()
157 {
158 var n;
159 this.task = this.env.task;
160
161 // check browser capabilities (never use version checks here)
162 if (this.env.server_error != 409 && (!bw.dom || !bw.xmlhttp_test())) {
163 this.goto_url('error', '_code=0x199');
164 return;
165 }
166
167 if (!this.env.blankpage)
168 this.env.blankpage = this.assets_path('program/resources/blank.gif');
169
170 // find all registered gui containers
171 for (n in this.gui_containers)
172 this.gui_containers[n] = $('#'+this.gui_containers[n]);
173
174 // find all registered gui objects
175 for (n in this.gui_objects)
176 this.gui_objects[n] = rcube_find_object(this.gui_objects[n]);
177
178 // clickjacking protection
179 if (n = this.env.x_frame_options) {
180 try {
181 // bust frame if not allowed
182 if (n.toLowerCase() == 'deny' && top.location.href != self.location.href)
183 top.location.href = self.location.href;
184 else if (/^allow-from[\s\t]+(.+)$/i.test(n) && RegExp.$1.indexOf(top.location.origin) != 0)
185 throw 1;
186 else if (top.location.hostname != self.location.hostname)
187 throw 1;
188 } catch (e) {
189 // possible clickjacking attack: disable all form elements
190 $('form').each(function(){ ref.lock_form(this, true); });
191 this.display_message("Blocked: possible clickjacking attack!", 'error');
192 return;
193 }
194 }
195
196 // init registered buttons
197 this.init_buttons();
198
199 // tell parent window that this frame is loaded
200 if (this.is_framed()) {
201 parent.rcmail.set_busy(false, null, parent.rcmail.env.frame_lock);
202 parent.rcmail.env.frame_lock = null;
203 }
204
205 // enable general commands
206 this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref',
207 'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-close', 'menu-save', true);
208
209 // set active task button
210 this.set_button(this.task, 'sel');
211
212 if (this.env.permaurl)
213 this.enable_command('permaurl', 'extwin', true);
214
215 switch (this.task) {
216
217 case 'mail':
218 // enable mail commands
219 this.enable_command('list', 'checkmail', 'add-contact', 'search', 'reset-search', 'collapse-folder', 'import-messages', true);
220
221 if (this.gui_objects.messagelist) {
222 this.env.widescreen_list_template = [
223 {className: 'threads', cells: ['threads']},
224 {className: 'subject', cells: ['fromto', 'date', 'status', 'subject']},
225 {className: 'flags', cells: ['flag', 'attachment']}
226 ];
227
228 this.message_list = new rcube_list_widget(this.gui_objects.messagelist, {
229 multiselect:true, multiexpand:true, draggable:true, keyboard:true,
230 column_movable:this.env.col_movable, dblclick_time:this.dblclick_time
231 });
232 this.message_list
233 .addEventListener('initrow', function(o) { ref.init_message_row(o); })
234 .addEventListener('dblclick', function(o) { ref.msglist_dbl_click(o); })
235 .addEventListener('keypress', function(o) { ref.msglist_keypress(o); })
236 .addEventListener('select', function(o) { ref.msglist_select(o); })
237 .addEventListener('dragstart', function(o) { ref.drag_start(o); })
238 .addEventListener('dragmove', function(e) { ref.drag_move(e); })
239 .addEventListener('dragend', function(e) { ref.drag_end(e); })
240 .addEventListener('expandcollapse', function(o) { ref.msglist_expand(o); })
241 .addEventListener('column_replace', function(o) { ref.msglist_set_coltypes(o); })
242 .addEventListener('listupdate', function(o) { ref.triggerEvent('listupdate', o); })
243 .init();
244
245 // TODO: this should go into the list-widget code
246 $(this.message_list.thead).on('click', 'a.sortcol', function(e){
247 return ref.command('sort', $(this).attr('rel'), this);
248 });
249
250 this.enable_command('toggle_status', 'toggle_flag', 'sort', true);
251 this.enable_command('set-listmode', this.env.threads && !this.is_multifolder_listing());
252
253 // load messages
254 this.command('list');
255
256 $(this.gui_objects.qsearchbox).val(this.env.search_text).focusin(function() { ref.message_list.blur(); });
257 }
258
259 this.set_button_titles();
260
261 this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list',
262 'move', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource',
263 'print', 'load-attachment', 'download-attachment', 'show-headers', 'hide-headers', 'download',
264 'forward', 'forward-inline', 'forward-attachment', 'change-format'];
265
266 if (this.env.action == 'show' || this.env.action == 'preview') {
267 this.enable_command(this.env.message_commands, this.env.uid);
268 this.enable_command('reply-list', this.env.list_post);
269
270 if (this.env.action == 'show') {
271 this.http_request('pagenav', {_uid: this.env.uid, _mbox: this.env.mailbox, _search: this.env.search_request},
272 this.display_message('', 'loading'));
273 }
274
275 if (this.env.mail_read_time > 0)
276 setTimeout(function() {
277 ref.http_post('mark', {_uid: ref.env.uid, _flag: 'read', _mbox: ref.env.mailbox, _quiet: 1});
278 }, this.env.mail_read_time * 1000);
279
280 if (this.env.blockedobjects) {
281 if (this.gui_objects.remoteobjectsmsg)
282 this.gui_objects.remoteobjectsmsg.style.display = 'block';
283 this.enable_command('load-images', 'always-load', true);
284 }
285
286 // make preview/message frame visible
287 if (this.env.action == 'preview' && this.is_framed()) {
288 this.enable_command('compose', 'add-contact', false);
289 parent.rcmail.show_contentframe(true);
290 }
291
292 // initialize drag-n-drop on attachments, so they can e.g.
293 // be dropped into mail compose attachments in another window
294 if (this.gui_objects.attachments)
295 $('li > a', this.gui_objects.attachments).not('.drop').on('dragstart', function(e) {
296 var n, href = this.href, dt = e.originalEvent.dataTransfer;
297 if (dt) {
298 // inject username to the uri
299 href = href.replace(/^https?:\/\//, function(m) { return m + urlencode(ref.env.username) + '@'});
300 // cleanup the node to get filename without the size test
301 n = $(this).clone();
302 n.children().remove();
303
304 dt.setData('roundcube-uri', href);
305 dt.setData('roundcube-name', $.trim(n.text()));
306 }
307 });
308 }
309 else if (this.env.action == 'compose') {
310 this.env.address_group_stack = [];
311 this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel',
312 'toggle-editor', 'list-addresses', 'pushgroup', 'search', 'reset-search', 'extwin',
313 'insert-response', 'save-response', 'menu-open', 'menu-close', 'load-attachment',
314 'download-attachment', 'open-attachment', 'rename-attachment'];
315
316 if (this.env.drafts_mailbox)
317 this.env.compose_commands.push('savedraft')
318
319 this.enable_command(this.env.compose_commands, 'identities', 'responses', true);
320
321 // add more commands (not enabled)
322 $.merge(this.env.compose_commands, ['add-recipient', 'firstpage', 'previouspage', 'nextpage', 'lastpage']);
323
324 if (window.googie) {
325 this.env.editor_config.spellchecker = googie;
326 this.env.editor_config.spellcheck_observer = function(s) { ref.spellcheck_state(); };
327
328 this.env.compose_commands.push('spellcheck')
329 this.enable_command('spellcheck', true);
330 }
331
332 // initialize HTML editor
333 this.editor_init(this.env.editor_config, this.env.composebody);
334
335 // init canned response functions
336 if (this.gui_objects.responseslist) {
337 $('a.insertresponse', this.gui_objects.responseslist)
338 .attr('unselectable', 'on')
339 .mousedown(function(e) { return rcube_event.cancel(e); })
340 .on('mouseup keypress', function(e) {
341 if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
342 ref.command('insert-response', $(this).attr('rel'));
343 $(document.body).trigger('mouseup'); // hides the menu
344 return rcube_event.cancel(e);
345 }
346 });
347
348 // avoid textarea loosing focus when hitting the save-response button/link
349 $.each(this.buttons['save-response'] || [], function (i, v) {
350 $('#' + v.id).mousedown(function(e){ return rcube_event.cancel(e); })
351 });
352 }
353
354 // init message compose form
355 this.init_messageform();
356 }
357 else if (this.env.action == 'get') {
358 this.enable_command('download', true);
359
360 // Mozilla's PDF.js viewer does not allow printing from host page (#5125)
361 // to minimize user confusion we disable the Print button
362 if (bw.mz && this.env.mimetype == 'application/pdf') {
363 n = 0; // there will be two onload events, first for the preload page
364 $(this.gui_objects.messagepartframe).on('load', function() {
365 if (n++) try { if (this.contentWindow.document) ref.enable_command('print', true); }
366 catch (e) {/* ignore */}
367 });
368 }
369 else
370 this.enable_command('print', true);
371
372 if (this.env.is_message) {
373 this.enable_command('reply', 'reply-all', 'edit', 'viewsource',
374 'forward', 'forward-inline', 'forward-attachment', true);
375 if (this.env.list_post)
376 this.enable_command('reply-list', true);
377 }
378
379 // center and scale the image in preview frame
380 if (this.env.mimetype.startsWith('image/'))
381 $(this.gui_objects.messagepartframe).on('load', function() {
382 var css = 'img { max-width:100%; max-height:100%; } ' // scale
383 + 'body { display:flex; align-items:center; justify-content:center; height:100%; margin:0; }'; // align
384
385 $(this).contents().find('head').append('<style type="text/css">'+ css + '</style>');
386 });
387 }
388 // show printing dialog
389 else if (this.env.action == 'print' && this.env.uid
390 && !this.env.is_pgp_content && !this.env.pgp_mime_part
391 ) {
392 this.print_dialog();
393 }
394
395 // get unread count for each mailbox
396 if (this.gui_objects.mailboxlist) {
397 this.env.unread_counts = {};
398 this.gui_objects.folderlist = this.gui_objects.mailboxlist;
399 this.http_request('getunread', {_page: this.env.current_page});
400 }
401
402 // init address book widget
403 if (this.gui_objects.contactslist) {
404 this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
405 { multiselect:true, draggable:false, keyboard:true });
406 this.contact_list
407 .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); })
408 .addEventListener('select', function(o) { ref.compose_recipient_select(o); })
409 .addEventListener('dblclick', function(o) { ref.compose_add_recipient(); })
410 .addEventListener('keypress', function(o) {
411 if (o.key_pressed == o.ENTER_KEY) {
412 if (!ref.compose_add_recipient()) {
413 // execute link action on <enter> if not a recipient entry
414 if (o.last_selected && String(o.last_selected).charAt(0) == 'G') {
415 $(o.rows[o.last_selected].obj).find('a').first().click();
416 }
417 }
418 }
419 })
420 .init();
421
422 // remember last focused address field
423 $('#_to,#_cc,#_bcc').focus(function() { ref.env.focused_field = this; });
424 }
425
426 if (this.gui_objects.addressbookslist) {
427 this.gui_objects.folderlist = this.gui_objects.addressbookslist;
428 this.enable_command('list-addresses', true);
429 }
430
431 // ask user to send MDN
432 if (this.env.mdn_request && this.env.uid) {
433 var postact = 'sendmdn',
434 postdata = {_uid: this.env.uid, _mbox: this.env.mailbox};
435 if (!confirm(this.get_label('mdnrequest'))) {
436 postdata._flag = 'mdnsent';
437 postact = 'mark';
438 }
439 this.http_post(postact, postdata);
440 }
441
442 this.check_mailvelope(this.env.action);
443
444 // detect browser capabilities
445 if (!this.is_framed() && !this.env.extwin)
446 this.browser_capabilities_check();
447
448 break;
449
450 case 'addressbook':
451 this.env.address_group_stack = [];
452
453 if (this.gui_objects.folderlist)
454 this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups);
455
456 this.enable_command('add', 'import', this.env.writable_source);
457 this.enable_command('list', 'listgroup', 'pushgroup', 'popgroup', 'listsearch', 'search', 'reset-search', 'advanced-search', true);
458
459 if (this.gui_objects.contactslist) {
460 this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
461 {multiselect:true, draggable:this.gui_objects.folderlist?true:false, keyboard:true});
462 this.contact_list
463 .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); })
464 .addEventListener('keypress', function(o) { ref.contactlist_keypress(o); })
465 .addEventListener('select', function(o) { ref.contactlist_select(o); })
466 .addEventListener('dragstart', function(o) { ref.drag_start(o); })
467 .addEventListener('dragmove', function(e) { ref.drag_move(e); })
468 .addEventListener('dragend', function(e) { ref.drag_end(e); })
469 .init();
470
471 $(this.gui_objects.qsearchbox).focusin(function() { ref.contact_list.blur(); });
472
473 this.update_group_commands();
474 this.command('list');
475 }
476
477 if (this.gui_objects.savedsearchlist) {
478 this.savedsearchlist = new rcube_treelist_widget(this.gui_objects.savedsearchlist, {
479 id_prefix: 'rcmli',
480 id_encode: this.html_identifier_encode,
481 id_decode: this.html_identifier_decode
482 });
483
484 this.savedsearchlist.addEventListener('select', function(node) {
485 ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }); });
486 }
487
488 this.set_page_buttons();
489
490 if (this.env.cid) {
491 this.enable_command('show', 'edit', 'qrcode', true);
492 // register handlers for group assignment via checkboxes
493 if (this.gui_objects.editform) {
494 $('input.groupmember').change(function() {
495 ref.group_member_change(this.checked ? 'add' : 'del', ref.env.cid, ref.env.source, this.value);
496 });
497 }
498 }
499
500 if (this.gui_objects.editform) {
501 this.enable_command('save', true);
502 if (this.env.action == 'add' || this.env.action == 'edit' || this.env.action == 'search')
503 this.init_contact_form();
504 }
505 else if (this.env.action == 'print') {
506 this.print_dialog();
507 }
508
509 break;
510
511 case 'settings':
512 this.enable_command('preferences', 'identities', 'responses', 'save', 'folders', true);
513
514 if (this.env.action == 'identities') {
515 this.enable_command('add', this.env.identities_level < 2);
516 }
517 else if (this.env.action == 'edit-identity' || this.env.action == 'add-identity') {
518 this.enable_command('save', 'edit', 'toggle-editor', true);
519 this.enable_command('delete', this.env.identities_level < 2);
520
521 // initialize HTML editor
522 this.editor_init(this.env.editor_config, 'rcmfd_signature');
523 }
524 else if (this.env.action == 'folders') {
525 this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', true);
526 }
527 else if (this.env.action == 'edit-folder' && this.gui_objects.editform) {
528 this.enable_command('save', 'folder-size', true);
529 parent.rcmail.env.exists = this.env.messagecount;
530 parent.rcmail.enable_command('purge', this.env.messagecount);
531 }
532 else if (this.env.action == 'responses') {
533 this.enable_command('add', true);
534 }
535
536 if (this.gui_objects.identitieslist) {
537 this.identity_list = new rcube_list_widget(this.gui_objects.identitieslist,
538 {multiselect:false, draggable:false, keyboard:true});
539 this.identity_list
540 .addEventListener('select', function(o) { ref.identity_select(o); })
541 .addEventListener('keypress', function(o) {
542 if (o.key_pressed == o.ENTER_KEY) {
543 ref.identity_select(o);
544 }
545 })
546 .init()
547 .focus();
548 }
549 else if (this.gui_objects.sectionslist) {
550 this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:true});
551 this.sections_list
552 .addEventListener('select', function(o) { ref.section_select(o); })
553 .addEventListener('keypress', function(o) { if (o.key_pressed == o.ENTER_KEY) ref.section_select(o); })
554 .init()
555 .focus();
556 }
557 else if (this.gui_objects.subscriptionlist) {
558 this.init_subscription_list();
559 }
560 else if (this.gui_objects.responseslist) {
561 this.responses_list = new rcube_list_widget(this.gui_objects.responseslist, {multiselect:false, draggable:false, keyboard:true});
562 this.responses_list
563 .addEventListener('select', function(list) {
564 var win, id = list.get_single_selection();
565 ref.enable_command('delete', !!id && $.inArray(id, ref.env.readonly_responses) < 0);
566 if (id && (win = ref.get_frame_window(ref.env.contentframe))) {
567 ref.set_busy(true);
568 ref.location_href({ _action:'edit-response', _key:id, _framed:1 }, win);
569 }
570 })
571 .init()
572 .focus();
573 }
574
575 break;
576
577 case 'login':
578 var tz, tz_name, jstz = window.jstz,
579 input_user = $('#rcmloginuser'),
580 input_tz = $('#rcmlogintz');
581
582 input_user.keyup(function(e) { return ref.login_user_keyup(e); });
583
584 if (input_user.val() == '')
585 input_user.focus();
586 else
587 $('#rcmloginpwd').focus();
588
589 // detect client timezone
590 if (jstz && (tz = jstz.determine()))
591 tz_name = tz.name();
592
593 input_tz.val(tz_name ? tz_name : (new Date().getStdTimezoneOffset() / -60));
594
595 // display 'loading' message on form submit, lock submit button
596 $('form').submit(function () {
597 $('input[type=submit]', this).prop('disabled', true);
598 ref.clear_messages();
599 ref.display_message('', 'loading');
600 });
601
602 this.enable_command('login', true);
603 break;
604 }
605
606 // select first input field in an edit form
607 if (this.gui_objects.editform)
608 $("input,select,textarea", this.gui_objects.editform)
609 .not(':hidden').not(':disabled').first().select().focus();
610
611 // prevent from form submit with Enter key in file input fields
612 if (bw.ie)
613 $('input[type=file]').keydown(function(e) { if (e.keyCode == '13') e.preventDefault(); });
614
615 // flag object as complete
616 this.loaded = true;
617 this.env.lastrefresh = new Date();
618
619 // show message
620 if (this.pending_message)
621 this.display_message.apply(this, this.pending_message);
622
623 // init treelist widget
624 if (this.gui_objects.folderlist && window.rcube_treelist_widget
625 // some plugins may load rcube_treelist_widget and there's one case
626 // when this will cause problems - addressbook widget in compose,
627 // which already has been initialized using rcube_list_widget
628 && this.gui_objects.folderlist != this.gui_objects.addressbookslist
629 ) {
630 this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, {
631 selectable: true,
632 id_prefix: 'rcmli',
633 parent_focus: true,
634 id_encode: this.html_identifier_encode,
635 id_decode: this.html_identifier_decode,
636 check_droptarget: function(node) { return !node.virtual && ref.check_droptarget(node.id) }
637 });
638
639 this.treelist
640 .addEventListener('collapse', function(node) { ref.folder_collapsed(node) })
641 .addEventListener('expand', function(node) { ref.folder_collapsed(node) })
642 .addEventListener('beforeselect', function(node) { return !ref.busy; })
643 .addEventListener('select', function(node) {
644 ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' });
645 ref.mark_all_read_state();
646 });
647 }
648
649 // activate html5 file drop feature (if browser supports it and if configured)
650 if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) {
651 $(document.body).on('dragover dragleave drop', function(e) { return ref.document_drag_hover(e, e.type == 'dragover'); });
652 $(this.gui_objects.filedrop).addClass('droptarget')
653 .on('dragover dragleave', function(e) { return ref.file_drag_hover(e, e.type == 'dragover'); })
654 .get(0).addEventListener('drop', function(e) { return ref.file_dropped(e); }, false);
655 }
656
657 // catch document (and iframe) mouse clicks
658 var body_mouseup = function(e) { return ref.doc_mouse_up(e); };
659 $(document.body)
660 .mouseup(body_mouseup)
661 .keydown(function(e) { return ref.doc_keypress(e); });
662
663 rcube_webmail.set_iframe_events({mouseup: body_mouseup});
664
665 // trigger init event hook
666 this.triggerEvent('init', { task:this.task, action:this.env.action });
667
668 // execute all foreign onload scripts
669 // @deprecated
670 for (n in this.onloads) {
671 if (typeof this.onloads[n] === 'string')
672 eval(this.onloads[n]);
673 else if (typeof this.onloads[n] === 'function')
674 this.onloads[n]();
675 }
676
677 // start keep-alive and refresh intervals
678 this.start_refresh();
679 this.start_keepalive();
680 };
681
682 this.log = function(msg)
683 {
684 if (this.env.devel_mode && window.console && console.log)
685 console.log(msg);
686 };
687
688 /*********************************************************/
689 /********* client command interface *********/
690 /*********************************************************/
691
692 // execute a specific command on the web client
693 this.command = function(command, props, obj, event)
694 {
695 var ret, uid, cid, url, flag, aborted = false;
696
697 if (obj && obj.blur && !(event && rcube_event.is_keyboard(event)))
698 obj.blur();
699
700 // do nothing if interface is locked by another command
701 // with exception for searching reset and menu
702 if (this.busy && !(command == 'reset-search' && this.last_command == 'search') && !command.match(/^menu-/))
703 return false;
704
705 // let the browser handle this click (shift/ctrl usually opens the link in a new window/tab)
706 if ((obj && obj.href && String(obj.href).indexOf('#') < 0) && rcube_event.get_modifier(event)) {
707 return true;
708 }
709
710 // command not supported or allowed
711 if (!this.commands[command]) {
712 // pass command to parent window
713 if (this.is_framed())
714 parent.rcmail.command(command, props);
715
716 return false;
717 }
718
719 // check input before leaving compose step
720 if (this.task == 'mail' && this.env.action == 'compose' && !this.env.server_error && command != 'save-pref'
721 && $.inArray(command, this.env.compose_commands) < 0
722 ) {
723 if (!this.env.is_sent && this.cmp_hash != this.compose_field_hash() && !confirm(this.get_label('notsentwarning')))
724 return false;
725
726 // remove copy from local storage if compose screen is left intentionally
727 this.remove_compose_data(this.env.compose_id);
728 this.compose_skip_unsavedcheck = true;
729 }
730
731 this.last_command = command;
732
733 // process external commands
734 if (typeof this.command_handlers[command] === 'function') {
735 ret = this.command_handlers[command](props, obj, event);
736 return ret !== undefined ? ret : (obj ? false : true);
737 }
738 else if (typeof this.command_handlers[command] === 'string') {
739 ret = window[this.command_handlers[command]](props, obj, event);
740 return ret !== undefined ? ret : (obj ? false : true);
741 }
742
743 // trigger plugin hooks
744 this.triggerEvent('actionbefore', {props:props, action:command, originalEvent:event});
745 ret = this.triggerEvent('before'+command, props || event);
746 if (ret !== undefined) {
747 // abort if one of the handlers returned false
748 if (ret === false)
749 return false;
750 else
751 props = ret;
752 }
753
754 ret = undefined;
755
756 // process internal command
757 switch (command) {
758
759 case 'login':
760 if (this.gui_objects.loginform)
761 this.gui_objects.loginform.submit();
762 break;
763
764 // commands to switch task
765 case 'logout':
766 case 'mail':
767 case 'addressbook':
768 case 'settings':
769 this.switch_task(command);
770 break;
771
772 case 'about':
773 this.redirect('?_task=settings&_action=about', false);
774 break;
775
776 case 'permaurl':
777 if (obj && obj.href && obj.target)
778 return true;
779 else if (this.env.permaurl)
780 parent.location.href = this.env.permaurl;
781 break;
782
783 case 'extwin':
784 if (this.env.action == 'compose') {
785 var form = this.gui_objects.messageform,
786 win = this.open_window('');
787
788 if (win) {
789 this.save_compose_form_local();
790 this.compose_skip_unsavedcheck = true;
791 $("input[name='_action']", form).val('compose');
792 form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 });
793 form.target = win.name;
794 form.submit();
795 }
796 }
797 else {
798 this.open_window(this.env.permaurl, true);
799 }
800 break;
801
802 case 'change-format':
803 url = this.env.permaurl + '&_format=' + props;
804
805 if (this.env.action == 'preview')
806 url = url.replace(/_action=show/, '_action=preview') + '&_framed=1';
807 if (this.env.extwin)
808 url += '&_extwin=1';
809
810 location.href = url;
811 break;
812
813 case 'menu-open':
814 if (props && props.menu == 'attachmentmenu') {
815 var mimetype = this.env.attachments[props.id];
816 if (mimetype && mimetype.mimetype) // in compose format is different
817 mimetype = mimetype.mimetype;
818 this.enable_command('open-attachment', mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0);
819 }
820 this.show_menu(props, props.show || undefined, event);
821 break;
822
823 case 'menu-close':
824 this.hide_menu(props, event);
825 break;
826
827 case 'menu-save':
828 this.triggerEvent(command, {props:props, originalEvent:event});
829 return false;
830
831 case 'open':
832 if (uid = this.get_single_uid()) {
833 obj.href = this.url('show', this.params_from_uid(uid));
834 return true;
835 }
836 break;
837
838 case 'close':
839 if (this.env.extwin)
840 window.close();
841 break;
842
843 case 'list':
844 if (props && props != '') {
845 this.reset_qsearch(true);
846 }
847 if (this.env.action == 'compose' && this.env.extwin) {
848 window.close();
849 }
850 else if (this.task == 'mail') {
851 this.list_mailbox(props);
852 this.set_button_titles();
853 }
854 else if (this.task == 'addressbook')
855 this.list_contacts(props);
856 break;
857
858 case 'set-listmode':
859 this.set_list_options(null, undefined, undefined, props == 'threads' ? 1 : 0);
860 break;
861
862 case 'sort':
863 var sort_order = this.env.sort_order,
864 sort_col = !this.env.disabled_sort_col ? props : this.env.sort_col;
865
866 if (!this.env.disabled_sort_order)
867 sort_order = this.env.sort_col == sort_col && sort_order == 'ASC' ? 'DESC' : 'ASC';
868
869 // set table header and update env
870 this.set_list_sorting(sort_col, sort_order);
871
872 // reload message list
873 this.list_mailbox('', '', sort_col+'_'+sort_order);
874 break;
875
876 case 'nextpage':
877 this.list_page('next');
878 break;
879
880 case 'lastpage':
881 this.list_page('last');
882 break;
883
884 case 'previouspage':
885 this.list_page('prev');
886 break;
887
888 case 'firstpage':
889 this.list_page('first');
890 break;
891
892 case 'expunge':
893 if (this.env.exists)
894 this.expunge_mailbox(this.env.mailbox);
895 break;
896
897 case 'purge':
898 case 'empty-mailbox':
899 if (this.env.exists)
900 this.purge_mailbox(this.env.mailbox);
901 break;
902
903 // common commands used in multiple tasks
904 case 'show':
905 if (this.task == 'mail') {
906 uid = this.get_single_uid();
907 if (uid && (!this.env.uid || uid != this.env.uid)) {
908 if (this.env.mailbox == this.env.drafts_mailbox)
909 this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox });
910 else
911 this.show_message(uid);
912 }
913 }
914 else if (this.task == 'addressbook') {
915 cid = props ? props : this.get_single_cid();
916 if (cid && !(this.env.action == 'show' && cid == this.env.cid))
917 this.load_contact(cid, 'show');
918 }
919 break;
920
921 case 'add':
922 if (this.task == 'addressbook')
923 this.load_contact(0, 'add');
924 else if (this.task == 'settings' && this.env.action == 'responses') {
925 var frame;
926 if ((frame = this.get_frame_window(this.env.contentframe))) {
927 this.set_busy(true);
928 this.location_href({ _action:'add-response', _framed:1 }, frame);
929 }
930 }
931 else if (this.task == 'settings') {
932 this.identity_list.clear_selection();
933 this.load_identity(0, 'add-identity');
934 }
935 break;
936
937 case 'edit':
938 if (this.task == 'addressbook' && (cid = this.get_single_cid()))
939 this.load_contact(cid, 'edit');
940 else if (this.task == 'settings' && props)
941 this.load_identity(props, 'edit-identity');
942 else if (this.task == 'mail' && (uid = this.get_single_uid())) {
943 url = { _mbox: this.get_message_mailbox(uid) };
944 url[this.env.mailbox == this.env.drafts_mailbox && props != 'new' ? '_draft_uid' : '_uid'] = uid;
945 this.open_compose_step(url);
946 }
947 break;
948
949 case 'save':
950 var input, form = this.gui_objects.editform;
951 if (form) {
952 // adv. search
953 if (this.env.action == 'search') {
954 }
955 // user prefs
956 else if ((input = $("input[name='_pagesize']", form)) && input.length && isNaN(parseInt(input.val()))) {
957 alert(this.get_label('nopagesizewarning'));
958 input.focus();
959 break;
960 }
961 // contacts/identities
962 else {
963 // reload form
964 if (props == 'reload') {
965 form.action += '&_reload=1';
966 }
967 else if (this.task == 'settings' && (this.env.identities_level % 2) == 0 &&
968 (input = $("input[name='_email']", form)) && input.length && !rcube_check_email(input.val())
969 ) {
970 alert(this.get_label('noemailwarning'));
971 input.focus();
972 break;
973 }
974 }
975
976 // add selected source (on the list)
977 if (parent.rcmail && parent.rcmail.env.source)
978 form.action = this.add_url(form.action, '_orig_source', parent.rcmail.env.source);
979
980 form.submit();
981 }
982 break;
983
984 case 'delete':
985 // mail task
986 if (this.task == 'mail')
987 this.delete_messages(event);
988 // addressbook task
989 else if (this.task == 'addressbook')
990 this.delete_contacts();
991 // settings: canned response
992 else if (this.task == 'settings' && this.env.action == 'responses')
993 this.delete_response();
994 // settings: user identities
995 else if (this.task == 'settings')
996 this.delete_identity();
997 break;
998
999 // mail task commands
1000 case 'move':
1001 case 'moveto': // deprecated
1002 if (this.task == 'mail')
1003 this.move_messages(props, event);
1004 else if (this.task == 'addressbook')
1005 this.move_contacts(props);
1006 break;
1007
1008 case 'copy':
1009 if (this.task == 'mail')
1010 this.copy_messages(props, event);
1011 else if (this.task == 'addressbook')
1012 this.copy_contacts(props);
1013 break;
1014
1015 case 'mark':
1016 if (props)
1017 this.mark_message(props);
1018 break;
1019
1020 case 'toggle_status':
1021 case 'toggle_flag':
1022 flag = command == 'toggle_flag' ? 'flagged' : 'read';
1023
1024 if (uid = props) {
1025 // toggle flagged/unflagged
1026 if (flag == 'flagged') {
1027 if (this.message_list.rows[uid].flagged)
1028 flag = 'unflagged';
1029 }
1030 // toggle read/unread
1031 else if (this.message_list.rows[uid].deleted)
1032 flag = 'undelete';
1033 else if (!this.message_list.rows[uid].unread)
1034 flag = 'unread';
1035
1036 this.mark_message(flag, uid);
1037 }
1038
1039 break;
1040
1041 case 'always-load':
1042 if (this.env.uid && this.env.sender) {
1043 this.add_contact(this.env.sender);
1044 setTimeout(function(){ ref.command('load-images'); }, 300);
1045 break;
1046 }
1047
1048 case 'load-images':
1049 if (this.env.uid)
1050 this.show_message(this.env.uid, true, this.env.action=='preview');
1051 break;
1052
1053 case 'load-attachment':
1054 case 'open-attachment':
1055 case 'download-attachment':
1056 var params, mimetype = this.env.attachments[props];
1057
1058 if (this.env.action == 'compose') {
1059 params = {_file: props, _id: this.env.compose_id};
1060 mimetype = mimetype ? mimetype.mimetype : '';
1061 }
1062 else {
1063 params = {_mbox: this.env.mailbox, _uid: this.env.uid, _part: props};
1064 }
1065
1066 // open attachment in frame if it's of a supported mimetype
1067 if (command != 'download-attachment' && mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0) {
1068 if (this.open_window(this.url('get', $.extend({_frame: 1}, params))))
1069 break;
1070 }
1071
1072 params._download = 1;
1073
1074 // prevent from page unload warning in compose
1075 this.compose_skip_unsavedcheck = 1;
1076 this.goto_url('get', params, false, true);
1077 this.compose_skip_unsavedcheck = 0;
1078
1079 break;
1080
1081 case 'select-all':
1082 this.select_all_mode = props ? false : true;
1083 this.dummy_select = true; // prevent msg opening if there's only one msg on the list
1084 if (props == 'invert')
1085 this.message_list.invert_selection();
1086 else
1087 this.message_list.select_all(props == 'page' ? '' : props);
1088 this.dummy_select = null;
1089 break;
1090
1091 case 'select-none':
1092 this.select_all_mode = false;
1093 this.message_list.clear_selection();
1094 break;
1095
1096 case 'expand-all':
1097 this.env.autoexpand_threads = 1;
1098 this.message_list.expand_all();
1099 break;
1100
1101 case 'expand-unread':
1102 this.env.autoexpand_threads = 2;
1103 this.message_list.collapse_all();
1104 this.expand_unread();
1105 break;
1106
1107 case 'collapse-all':
1108 this.env.autoexpand_threads = 0;
1109 this.message_list.collapse_all();
1110 break;
1111
1112 case 'nextmessage':
1113 if (this.env.next_uid)
1114 this.show_message(this.env.next_uid, false, this.env.action == 'preview');
1115 break;
1116
1117 case 'lastmessage':
1118 if (this.env.last_uid)
1119 this.show_message(this.env.last_uid);
1120 break;
1121
1122 case 'previousmessage':
1123 if (this.env.prev_uid)
1124 this.show_message(this.env.prev_uid, false, this.env.action == 'preview');
1125 break;
1126
1127 case 'firstmessage':
1128 if (this.env.first_uid)
1129 this.show_message(this.env.first_uid);
1130 break;
1131
1132 case 'compose':
1133 url = {};
1134
1135 if (this.task == 'mail') {
1136 url = {_mbox: this.env.mailbox, _search: this.env.search_request};
1137 if (props)
1138 url._to = props;
1139 }
1140 // modify url if we're in addressbook
1141 else if (this.task == 'addressbook') {
1142 // switch to mail compose step directly
1143 if (props && props.indexOf('@') > 0) {
1144 url._to = props;
1145 }
1146 else {
1147 var a_cids = [];
1148 // use contact id passed as command parameter
1149 if (props)
1150 a_cids.push(props);
1151 // get selected contacts
1152 else if (this.contact_list)
1153 a_cids = this.contact_list.get_selection();
1154
1155 if (a_cids.length)
1156 this.http_post('mailto', { _cid: a_cids.join(','), _source: this.env.source }, true);
1157 else if (this.env.group)
1158 this.http_post('mailto', { _gid: this.env.group, _source: this.env.source }, true);
1159
1160 break;
1161 }
1162 }
1163 else if (props && typeof props == 'string') {
1164 url._to = props;
1165 }
1166 else if (props && typeof props == 'object') {
1167 $.extend(url, props);
1168 }
1169
1170 this.open_compose_step(url);
1171 break;
1172
1173 case 'spellcheck':
1174 if (this.spellcheck_state()) {
1175 this.editor.spellcheck_stop();
1176 }
1177 else {
1178 this.editor.spellcheck_start();
1179 }
1180 break;
1181
1182 case 'savedraft':
1183 // Reset the auto-save timer
1184 clearTimeout(this.save_timer);
1185
1186 // compose form did not change (and draft wasn't saved already)
1187 if (this.env.draft_id && this.cmp_hash == this.compose_field_hash()) {
1188 this.auto_save_start();
1189 break;
1190 }
1191
1192 this.submit_messageform(true);
1193 break;
1194
1195 case 'send':
1196 if (!props.nocheck && !this.env.is_sent && !this.check_compose_input(command))
1197 break;
1198
1199 // Reset the auto-save timer
1200 clearTimeout(this.save_timer);
1201
1202 this.submit_messageform();
1203 break;
1204
1205 case 'send-attachment':
1206 // Reset the auto-save timer
1207 clearTimeout(this.save_timer);
1208
1209 if (!(flag = this.upload_file(props || this.gui_objects.uploadform, 'upload'))) {
1210 if (flag !== false)
1211 alert(this.get_label('selectimportfile'));
1212 aborted = true;
1213 }
1214 break;
1215
1216 case 'insert-sig':
1217 this.change_identity($("[name='_from']")[0], true);
1218 break;
1219
1220 case 'list-addresses':
1221 this.list_contacts(props);
1222 this.enable_command('add-recipient', false);
1223 break;
1224
1225 case 'add-recipient':
1226 this.compose_add_recipient(props);
1227 break;
1228
1229 case 'reply-all':
1230 case 'reply-list':
1231 case 'reply':
1232 if (uid = this.get_single_uid()) {
1233 url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid), _search: this.env.search_request};
1234 if (command == 'reply-all')
1235 // do reply-list, when list is detected and popup menu wasn't used
1236 url._all = (!props && this.env.reply_all_mode == 1 && this.commands['reply-list'] ? 'list' : 'all');
1237 else if (command == 'reply-list')
1238 url._all = 'list';
1239
1240 this.open_compose_step(url);
1241 }
1242 break;
1243
1244 case 'forward-attachment':
1245 case 'forward-inline':
1246 case 'forward':
1247 var uids = this.env.uid ? [this.env.uid] : (this.message_list ? this.message_list.get_selection() : []);
1248 if (uids.length) {
1249 url = { _forward_uid: this.uids_to_list(uids), _mbox: this.env.mailbox, _search: this.env.search_request };
1250 if (command == 'forward-attachment' || (!props && this.env.forward_attachment) || uids.length > 1)
1251 url._attachment = 1;
1252 this.open_compose_step(url);
1253 }
1254 break;
1255
1256 case 'print':
1257 if (this.task == 'addressbook') {
1258 if (uid = this.contact_list.get_single_selection()) {
1259 url = '&_action=print&_cid=' + uid;
1260 if (this.env.source)
1261 url += '&_source=' + urlencode(this.env.source);
1262 this.open_window(this.env.comm_path + url, true, true);
1263 }
1264 }
1265 else if (this.env.action == 'get' && !this.env.is_message) {
1266 this.gui_objects.messagepartframe.contentWindow.print();
1267 }
1268 else if (uid = this.get_single_uid()) {
1269 url = this.url('print', this.params_from_uid(uid, {_safe: this.env.safemode ? 1 : 0}));
1270 if (this.open_window(url, true, true)) {
1271 if (this.env.action != 'show' && this.env.action != 'get')
1272 this.mark_message('read', uid);
1273 }
1274 }
1275 break;
1276
1277 case 'viewsource':
1278 if (uid = this.get_single_uid())
1279 this.open_window(this.url('viewsource', this.params_from_uid(uid)), true, true);
1280 break;
1281
1282 case 'download':
1283 if (this.env.action == 'get') {
1284 location.href = this.secure_url(location.href.replace(/_frame=/, '_download='));
1285 }
1286 else if (uid = this.get_single_uid()) {
1287 this.goto_url('viewsource', this.params_from_uid(uid, {_save: 1}), false, true);
1288 }
1289 break;
1290
1291 // quicksearch
1292 case 'search':
1293 ret = this.qsearch(props);
1294 break;
1295
1296 // reset quicksearch
1297 case 'reset-search':
1298 var n, s = this.env.search_request || this.env.qsearch;
1299
1300 this.reset_qsearch(true);
1301
1302 if (s && this.env.action == 'compose') {
1303 if (this.contact_list)
1304 this.list_contacts_clear();
1305 }
1306 else if (s && this.env.mailbox) {
1307 this.list_mailbox(this.env.mailbox, 1);
1308 }
1309 else if (s && this.task == 'addressbook') {
1310 if (this.env.source == '') {
1311 for (n in this.env.address_sources) break;
1312 this.env.source = n;
1313 this.env.group = '';
1314 }
1315 this.list_contacts(this.env.source, this.env.group, 1);
1316 }
1317 break;
1318
1319 case 'pushgroup':
1320 // add group ID and current search to stack
1321 var group = {
1322 id: props.id,
1323 search_request: this.env.search_request,
1324 page: this.env.current_page,
1325 search: this.env.search_request && this.gui_objects.qsearchbox ? this.gui_objects.qsearchbox.value : null
1326 };
1327
1328 this.env.address_group_stack.push(group);
1329 if (obj && event)
1330 rcube_event.cancel(event);
1331
1332 case 'listgroup':
1333 this.reset_qsearch();
1334 this.list_contacts(props.source, props.id, 1, group);
1335 break;
1336
1337 case 'popgroup':
1338 if (this.env.address_group_stack.length) {
1339 var old = this.env.address_group_stack.pop();
1340 this.reset_qsearch();
1341
1342 if (old.search_request) {
1343 // this code is executed when going back to the search result
1344 if (old.search && this.gui_objects.qsearchbox)
1345 $(this.gui_objects.qsearchbox).val(old.search);
1346 this.env.search_request = old.search_request;
1347 this.list_contacts_remote(null, null, this.env.current_page = old.page);
1348 }
1349 else
1350 this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1].id);
1351 }
1352 break;
1353
1354 case 'import-messages':
1355 var form = props || this.gui_objects.importform,
1356 importlock = this.set_busy(true, 'importwait');
1357
1358 $('input[name="_unlock"]', form).val(importlock);
1359
1360 if (!(flag = this.upload_file(form, 'import', importlock))) {
1361 this.set_busy(false, null, importlock);
1362 if (flag !== false)
1363 alert(this.get_label('selectimportfile'));
1364 aborted = true;
1365 }
1366 break;
1367
1368 case 'import':
1369 if (this.env.action == 'import' && this.gui_objects.importform) {
1370 var file = document.getElementById('rcmimportfile');
1371 if (file && !file.value) {
1372 alert(this.get_label('selectimportfile'));
1373 aborted = true;
1374 break;
1375 }
1376 this.gui_objects.importform.submit();
1377 this.set_busy(true, 'importwait');
1378 this.lock_form(this.gui_objects.importform, true);
1379 }
1380 else
1381 this.goto_url('import', (this.env.source ? '_target='+urlencode(this.env.source)+'&' : ''));
1382 break;
1383
1384 case 'export':
1385 if (this.contact_list.rowcount > 0) {
1386 this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request }, false, true);
1387 }
1388 break;
1389
1390 case 'export-selected':
1391 if (this.contact_list.rowcount > 0) {
1392 this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') }, false, true);
1393 }
1394 break;
1395
1396 case 'upload-photo':
1397 this.upload_contact_photo(props || this.gui_objects.uploadform);
1398 break;
1399
1400 case 'delete-photo':
1401 this.replace_contact_photo('-del-');
1402 break;
1403
1404 // user settings commands
1405 case 'preferences':
1406 case 'identities':
1407 case 'responses':
1408 case 'folders':
1409 this.goto_url('settings/' + command);
1410 break;
1411
1412 case 'undo':
1413 this.http_request('undo', '', this.display_message('', 'loading'));
1414 break;
1415
1416 // unified command call (command name == function name)
1417 default:
1418 var func = command.replace(/-/g, '_');
1419 if (this[func] && typeof this[func] === 'function') {
1420 ret = this[func](props, obj, event);
1421 }
1422 break;
1423 }
1424
1425 if (!aborted && this.triggerEvent('after'+command, props) === false)
1426 ret = false;
1427 this.triggerEvent('actionafter', { props:props, action:command, aborted:aborted, ret:ret });
1428
1429 return ret === false ? false : obj ? false : true;
1430 };
1431
1432 // set command(s) enabled or disabled
1433 this.enable_command = function()
1434 {
1435 var i, n, args = Array.prototype.slice.call(arguments),
1436 enable = args.pop(), cmd;
1437
1438 for (n=0; n<args.length; n++) {
1439 cmd = args[n];
1440 // argument of type array
1441 if (typeof cmd === 'string') {
1442 this.commands[cmd] = enable;
1443 this.set_button(cmd, (enable ? 'act' : 'pas'));
1444 this.triggerEvent('enable-command', {command: cmd, status: enable});
1445 }
1446 // push array elements into commands array
1447 else {
1448 for (i in cmd)
1449 args.push(cmd[i]);
1450 }
1451 }
1452 };
1453
1454 this.command_enabled = function(cmd)
1455 {
1456 return this.commands[cmd];
1457 };
1458
1459 // lock/unlock interface
1460 this.set_busy = function(a, message, id)
1461 {
1462 if (a && message) {
1463 var msg = this.get_label(message);
1464 if (msg == message)
1465 msg = 'Loading...';
1466
1467 id = this.display_message(msg, 'loading');
1468 }
1469 else if (!a && id) {
1470 this.hide_message(id);
1471 }
1472
1473 this.busy = a;
1474 //document.body.style.cursor = a ? 'wait' : 'default';
1475
1476 if (this.gui_objects.editform)
1477 this.lock_form(this.gui_objects.editform, a);
1478
1479 return id;
1480 };
1481
1482 // return a localized string
1483 this.get_label = function(name, domain)
1484 {
1485 if (domain && this.labels[domain+'.'+name])
1486 return this.labels[domain+'.'+name];
1487 else if (this.labels[name])
1488 return this.labels[name];
1489 else
1490 return name;
1491 };
1492
1493 // alias for convenience reasons
1494 this.gettext = this.get_label;
1495
1496 // switch to another application task
1497 this.switch_task = function(task)
1498 {
1499 if (this.task === task && task != 'mail')
1500 return;
1501
1502 var url = this.get_task_url(task);
1503
1504 if (task == 'mail')
1505 url += '&_mbox=INBOX';
1506 else if (task == 'logout' && !this.env.server_error) {
1507 url = this.secure_url(url);
1508 this.clear_compose_data();
1509 }
1510
1511 this.redirect(url);
1512 };
1513
1514 this.get_task_url = function(task, url)
1515 {
1516 if (!url)
1517 url = this.env.comm_path;
1518
1519 if (url.match(/[?&]_task=[a-zA-Z0-9_-]+/))
1520 return url.replace(/_task=[a-zA-Z0-9_-]+/, '_task=' + task);
1521 else
1522 return url.replace(/\?.*$/, '') + '?_task=' + task;
1523 };
1524
1525 this.reload = function(delay)
1526 {
1527 if (this.is_framed())
1528 parent.rcmail.reload(delay);
1529 else if (delay)
1530 setTimeout(function() { ref.reload(); }, delay);
1531 else if (window.location)
1532 location.href = this.url('', {_extwin: this.env.extwin});
1533 };
1534
1535 // Add variable to GET string, replace old value if exists
1536 this.add_url = function(url, name, value)
1537 {
1538 value = urlencode(value);
1539
1540 if (/(\?.*)$/.test(url)) {
1541 var urldata = RegExp.$1,
1542 datax = RegExp('((\\?|&)'+RegExp.escape(name)+'=[^&]*)');
1543
1544 if (datax.test(urldata)) {
1545 urldata = urldata.replace(datax, RegExp.$2 + name + '=' + value);
1546 }
1547 else
1548 urldata += '&' + name + '=' + value
1549
1550 return url.replace(/(\?.*)$/, urldata);
1551 }
1552
1553 return url + '?' + name + '=' + value;
1554 };
1555
1556 // append CSRF protection token to the given url
1557 this.secure_url = function(url)
1558 {
1559 return this.add_url(url, '_token', this.env.request_token);
1560 },
1561
1562 this.is_framed = function()
1563 {
1564 return this.env.framed && parent.rcmail && parent.rcmail != this && typeof parent.rcmail.command == 'function';
1565 };
1566
1567 this.save_pref = function(prop)
1568 {
1569 var request = {_name: prop.name, _value: prop.value};
1570
1571 if (prop.session)
1572 request._session = prop.session;
1573 if (prop.env)
1574 this.env[prop.env] = prop.value;
1575
1576 this.http_post('save-pref', request);
1577 };
1578
1579 this.html_identifier = function(str, encode)
1580 {
1581 return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_');
1582 };
1583
1584 this.html_identifier_encode = function(str)
1585 {
1586 return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
1587 };
1588
1589 this.html_identifier_decode = function(str)
1590 {
1591 str = String(str).replace(/-/g, '+').replace(/_/g, '/');
1592
1593 while (str.length % 4) str += '=';
1594
1595 return Base64.decode(str);
1596 };
1597
1598
1599 /*********************************************************/
1600 /********* event handling methods *********/
1601 /*********************************************************/
1602
1603 this.drag_menu = function(e, target)
1604 {
1605 var modkey = rcube_event.get_modifier(e),
1606 menu = this.gui_objects.dragmenu;
1607
1608 if (menu && modkey == SHIFT_KEY && this.commands['copy']) {
1609 var pos = rcube_event.get_mouse_pos(e);
1610 this.env.drag_target = target;
1611 this.show_menu(this.gui_objects.dragmenu.id, true, e);
1612 $(menu).css({top: (pos.y-10)+'px', left: (pos.x-10)+'px'});
1613 return true;
1614 }
1615
1616 return false;
1617 };
1618
1619 this.drag_menu_action = function(action)
1620 {
1621 var menu = this.gui_objects.dragmenu;
1622 if (menu) {
1623 $(menu).hide();
1624 }
1625 this.command(action, this.env.drag_target);
1626 this.env.drag_target = null;
1627 };
1628
1629 this.drag_start = function(list)
1630 {
1631 this.drag_active = true;
1632
1633 if (this.preview_timer)
1634 clearTimeout(this.preview_timer);
1635
1636 // prepare treelist widget for dragging interactions
1637 if (this.treelist)
1638 this.treelist.drag_start();
1639 };
1640
1641 this.drag_end = function(e)
1642 {
1643 var list, model;
1644
1645 if (this.treelist)
1646 this.treelist.drag_end();
1647
1648 // execute drag & drop action when mouse was released
1649 if (list = this.message_list)
1650 model = this.env.mailboxes;
1651 else if (list = this.contact_list)
1652 model = this.env.contactfolders;
1653
1654 if (this.drag_active && model && this.env.last_folder_target) {
1655 var target = model[this.env.last_folder_target];
1656 list.draglayer.hide();
1657
1658 if (this.contact_list) {
1659 if (!this.contacts_drag_menu(e, target))
1660 this.command('move', target);
1661 }
1662 else if (!this.drag_menu(e, target))
1663 this.command('move', target);
1664 }
1665
1666 this.drag_active = false;
1667 this.env.last_folder_target = null;
1668 };
1669
1670 this.drag_move = function(e)
1671 {
1672 if (this.gui_objects.folderlist) {
1673 var drag_target, oldclass,
1674 layerclass = 'draglayernormal',
1675 mouse = rcube_event.get_mouse_pos(e);
1676
1677 if (this.contact_list && this.contact_list.draglayer)
1678 oldclass = this.contact_list.draglayer.attr('class');
1679
1680 // mouse intersects a valid drop target on the treelist
1681 if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) {
1682 this.env.last_folder_target = drag_target;
1683 layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal');
1684 }
1685 else {
1686 // Clear target, otherwise drag end will trigger move into last valid droptarget
1687 this.env.last_folder_target = null;
1688 }
1689
1690 if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
1691 this.contact_list.draglayer.attr('class', layerclass);
1692 }
1693 };
1694
1695 this.collapse_folder = function(name)
1696 {
1697 if (this.treelist)
1698 this.treelist.toggle(name);
1699 };
1700
1701 this.folder_collapsed = function(node)
1702 {
1703 var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders',
1704 old = this.env[prefname];
1705
1706 if (node.collapsed) {
1707 this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&';
1708
1709 // select the folder if one of its childs is currently selected
1710 // don't select if it's virtual (#1488346)
1711 if (!node.virtual && this.env.mailbox && this.env.mailbox.startsWith(node.id + this.env.delimiter))
1712 this.command('list', node.id);
1713 }
1714 else {
1715 var reg = new RegExp('&'+urlencode(node.id)+'&');
1716 this.env[prefname] = this.env[prefname].replace(reg, '');
1717 }
1718
1719 if (!this.drag_active) {
1720 if (old !== this.env[prefname])
1721 this.command('save-pref', { name: prefname, value: this.env[prefname] });
1722
1723 if (this.env.unread_counts)
1724 this.set_unread_count_display(node.id, false);
1725 }
1726 };
1727
1728 // global mouse-click handler to cleanup some UI elements
1729 this.doc_mouse_up = function(e)
1730 {
1731 var list, id, target = rcube_event.get_target(e);
1732
1733 // ignore event if jquery UI dialog is open
1734 if ($(target).closest('.ui-dialog, .ui-widget-overlay').length)
1735 return;
1736
1737 // remove focus from list widgets
1738 if (window.rcube_list_widget && rcube_list_widget._instances.length) {
1739 $.each(rcube_list_widget._instances, function(i,list){
1740 if (list && !rcube_mouse_is_over(e, list.list.parentNode))
1741 list.blur();
1742 });
1743 }
1744
1745 // reset 'pressed' buttons
1746 if (this.buttons_sel) {
1747 for (id in this.buttons_sel)
1748 if (typeof id !== 'function')
1749 this.button_out(this.buttons_sel[id], id);
1750 this.buttons_sel = {};
1751 }
1752
1753 // reset popup menus; delayed to have updated menu_stack data
1754 setTimeout(function(e){
1755 var obj, skip, config, id, i, parents = $(target).parents();
1756 for (i = ref.menu_stack.length - 1; i >= 0; i--) {
1757 id = ref.menu_stack[i];
1758 obj = $('#' + id);
1759
1760 if (obj.is(':visible')
1761 && target != obj.data('opener')
1762 && target != obj.get(0) // check if scroll bar was clicked (#1489832)
1763 && !parents.is(obj.data('opener'))
1764 && id != skip
1765 && (obj.attr('data-editable') != 'true' || !$(target).parents('#' + id).length)
1766 && (obj.attr('data-sticky') != 'true' || !rcube_mouse_is_over(e, obj.get(0)))
1767 ) {
1768 ref.hide_menu(id, e);
1769 }
1770 skip = obj.data('parent');
1771 }
1772 }, 10, e);
1773 };
1774
1775 // global keypress event handler
1776 this.doc_keypress = function(e)
1777 {
1778 // Helper method to move focus to the next/prev active menu item
1779 var focus_menu_item = function(dir) {
1780 var obj, item, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first';
1781 if (ref.focused_menu && (obj = $('#'+ref.focused_menu))) {
1782 item = obj.find(':focus').closest('li')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]();
1783 if (!item.length)
1784 item = obj.find(':focus').closest('ul')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]();
1785 return item.focus().length;
1786 }
1787
1788 return 0;
1789 };
1790
1791 var target = e.target || {},
1792 keyCode = rcube_event.get_keycode(e);
1793
1794 if (e.keyCode != 27 && (!this.menu_keyboard_active || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')) {
1795 return true;
1796 }
1797
1798 switch (keyCode) {
1799 case 38:
1800 case 40:
1801 case 63232: // "up", in safari keypress
1802 case 63233: // "down", in safari keypress
1803 focus_menu_item(keyCode == 38 || keyCode == 63232 ? -1 : 1);
1804 return rcube_event.cancel(e);
1805
1806 case 9: // tab
1807 if (this.focused_menu) {
1808 var mod = rcube_event.get_modifier(e);
1809 if (!focus_menu_item(mod == SHIFT_KEY ? -1 : 1)) {
1810 this.hide_menu(this.focused_menu, e);
1811 }
1812 }
1813 return rcube_event.cancel(e);
1814
1815 case 27: // esc
1816 if (this.menu_stack.length)
1817 this.hide_menu(this.menu_stack[this.menu_stack.length-1], e);
1818 break;
1819 }
1820
1821 return true;
1822 }
1823
1824 this.msglist_select = function(list)
1825 {
1826 if (this.preview_timer)
1827 clearTimeout(this.preview_timer);
1828
1829 var selected = list.get_single_selection();
1830
1831 this.enable_command(this.env.message_commands, selected != null);
1832 if (selected) {
1833 // Hide certain command buttons when Drafts folder is selected
1834 if (this.env.mailbox == this.env.drafts_mailbox)
1835 this.enable_command('reply', 'reply-all', 'reply-list', 'forward', 'forward-attachment', 'forward-inline', false);
1836 // Disable reply-list when List-Post header is not set
1837 else {
1838 var msg = this.env.messages[selected];
1839 if (!msg.ml)
1840 this.enable_command('reply-list', false);
1841 }
1842 }
1843 // Multi-message commands
1844 this.enable_command('delete', 'move', 'copy', 'mark', 'forward', 'forward-attachment', list.selection.length > 0);
1845
1846 // reset all-pages-selection
1847 if (selected || (list.selection.length && list.selection.length != list.rowcount))
1848 this.select_all_mode = false;
1849
1850 // start timer for message preview (wait for double click)
1851 if (selected && this.env.contentframe && !list.multi_selecting && !this.dummy_select) {
1852 // try to be responsive and try not to overload the server when user is pressing up/down key repeatedly
1853 var now = new Date().getTime();
1854 var time_diff = now - (this._last_msglist_select_time || 0);
1855 var preview_pane_delay = this.preview_delay_click;
1856
1857 // user is selecting messages repeatedly, wait until this ends (use larger delay)
1858 if (time_diff < this.preview_delay_select) {
1859 preview_pane_delay = this.preview_delay_select;
1860 if (this.preview_timer) {
1861 clearTimeout(this.preview_timer);
1862 }
1863 if (this.env.contentframe) {
1864 this.show_contentframe(false);
1865 }
1866 }
1867
1868 this._last_msglist_select_time = now;
1869 this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, preview_pane_delay);
1870 }
1871 else if (this.env.contentframe) {
1872 this.show_contentframe(false);
1873 }
1874 };
1875
1876 this.msglist_dbl_click = function(list)
1877 {
1878 if (this.preview_timer)
1879 clearTimeout(this.preview_timer);
1880
1881 var uid = list.get_single_selection();
1882
1883 if (uid && (this.env.messages[uid].mbox || this.env.mailbox) == this.env.drafts_mailbox)
1884 this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox });
1885 else if (uid)
1886 this.show_message(uid, false, false);
1887 };
1888
1889 this.msglist_keypress = function(list)
1890 {
1891 if (list.modkey == CONTROL_KEY)
1892 return;
1893
1894 if (list.key_pressed == list.ENTER_KEY)
1895 this.command('show');
1896 else if (list.key_pressed == list.DELETE_KEY || list.key_pressed == list.BACKSPACE_KEY)
1897 this.command('delete');
1898 else if (list.key_pressed == 33)
1899 this.command('previouspage');
1900 else if (list.key_pressed == 34)
1901 this.command('nextpage');
1902 };
1903
1904 this.msglist_get_preview = function()
1905 {
1906 var uid = this.get_single_uid();
1907 if (uid && this.env.contentframe && !this.drag_active)
1908 this.show_message(uid, false, true);
1909 else if (this.env.contentframe)
1910 this.show_contentframe(false);
1911 };
1912
1913 this.msglist_expand = function(row)
1914 {
1915 if (this.env.messages[row.uid])
1916 this.env.messages[row.uid].expanded = row.expanded;
1917 $(row.obj)[row.expanded?'addClass':'removeClass']('expanded');
1918 };
1919
1920 this.msglist_set_coltypes = function(list)
1921 {
1922 var i, found, name, cols = list.thead.rows[0].cells;
1923
1924 this.env.listcols = [];
1925
1926 for (i=0; i<cols.length; i++)
1927 if (cols[i].id && cols[i].id.startsWith('rcm')) {
1928 name = cols[i].id.slice(3);
1929 this.env.listcols.push(name);
1930 }
1931
1932 if ((found = $.inArray('flag', this.env.listcols)) >= 0)
1933 this.env.flagged_col = found;
1934
1935 if ((found = $.inArray('subject', this.env.listcols)) >= 0)
1936 this.env.subject_col = found;
1937
1938 this.command('save-pref', { name: 'list_cols', value: this.env.listcols, session: 'list_attrib/columns' });
1939 };
1940
1941 this.check_droptarget = function(id)
1942 {
1943 switch (this.task) {
1944 case 'mail':
1945 return (this.env.mailboxes[id]
1946 && !this.env.mailboxes[id].virtual
1947 && (this.env.mailboxes[id].id != this.env.mailbox || this.is_multifolder_listing())) ? 1 : 0;
1948
1949 case 'addressbook':
1950 var target;
1951 if (id != this.env.source && (target = this.env.contactfolders[id])) {
1952 // droptarget is a group
1953 if (target.type == 'group') {
1954 if (target.id != this.env.group && !this.env.contactfolders[target.source].readonly) {
1955 var is_other = this.env.selection_sources.length > 1 || $.inArray(target.source, this.env.selection_sources) == -1;
1956 return !is_other || this.commands.move ? 1 : 2;
1957 }
1958 }
1959 // droptarget is a (writable) addressbook and it's not the source
1960 else if (!target.readonly && (this.env.selection_sources.length > 1 || $.inArray(id, this.env.selection_sources) == -1)) {
1961 return this.commands.move ? 1 : 2;
1962 }
1963 }
1964 }
1965
1966 return 0;
1967 };
1968
1969 // open popup window
1970 this.open_window = function(url, small, toolbar)
1971 {
1972 var wname = 'rcmextwin' + new Date().getTime();
1973
1974 url += (url.match(/\?/) ? '&' : '?') + '_extwin=1';
1975
1976 if (this.env.standard_windows)
1977 var extwin = window.open(url, wname);
1978 else {
1979 var win = this.is_framed() ? parent.window : window,
1980 page = $(win),
1981 page_width = page.width(),
1982 page_height = bw.mz ? $('body', win).height() : page.height(),
1983 w = Math.min(small ? this.env.popup_width_small : this.env.popup_width, page_width),
1984 h = page_height, // always use same height
1985 l = (win.screenLeft || win.screenX) + 20,
1986 t = (win.screenTop || win.screenY) + 20,
1987 extwin = window.open(url, wname,
1988 'width='+w+',height='+h+',top='+t+',left='+l+',resizable=yes,location=no,scrollbars=yes'
1989 +(toolbar ? ',toolbar=yes,menubar=yes,status=yes' : ',toolbar=no,menubar=no,status=no'));
1990 }
1991
1992 // detect popup blocker (#1489618)
1993 // don't care this might not work with all browsers
1994 if (!extwin || extwin.closed) {
1995 this.display_message(this.get_label('windowopenerror'), 'warning');
1996 return;
1997 }
1998
1999 // write loading... message to empty windows
2000 if (!url && extwin.document) {
2001 extwin.document.write('<html><body>' + this.get_label('loading') + '</body></html>');
2002 }
2003
2004 // allow plugins to grab the window reference (#1489413)
2005 this.triggerEvent('openwindow', { url:url, handle:extwin });
2006
2007 // focus window, delayed to bring to front
2008 setTimeout(function() { extwin && extwin.focus(); }, 10);
2009
2010 return extwin;
2011 };
2012
2013
2014 /*********************************************************/
2015 /********* (message) list functionality *********/
2016 /*********************************************************/
2017
2018 this.init_message_row = function(row)
2019 {
2020 var i, fn = {}, uid = row.uid,
2021 status_icon = (this.env.status_col != null ? 'status' : 'msg') + 'icn' + row.id;
2022
2023 if (uid && this.env.messages[uid])
2024 $.extend(row, this.env.messages[uid]);
2025
2026 // set eventhandler to status icon
2027 if (row.icon = document.getElementById(status_icon)) {
2028 fn.icon = function(e) { ref.command('toggle_status', uid); };
2029 }
2030
2031 // save message icon position too
2032 if (this.env.status_col != null)
2033 row.msgicon = document.getElementById('msgicn'+row.id);
2034 else
2035 row.msgicon = row.icon;
2036
2037 // set eventhandler to flag icon
2038 if (this.env.flagged_col != null && (row.flagicon = document.getElementById('flagicn'+row.id))) {
2039 fn.flagicon = function(e) { ref.command('toggle_flag', uid); };
2040 }
2041
2042 // set event handler to thread expand/collapse icon
2043 if (!row.depth && row.has_children && (row.expando = document.getElementById('rcmexpando'+row.id))) {
2044 fn.expando = function(e) { ref.expand_message_row(e, uid); };
2045 }
2046
2047 // attach events
2048 $.each(fn, function(i, f) {
2049 row[i].onclick = function(e) { f(e); return rcube_event.cancel(e); };
2050 if (bw.touch && row[i].addEventListener) {
2051 row[i].addEventListener('touchend', function(e) {
2052 if (e.changedTouches.length == 1) {
2053 f(e);
2054 return rcube_event.cancel(e);
2055 }
2056 }, false);
2057 }
2058 });
2059
2060 this.triggerEvent('insertrow', { uid:uid, row:row });
2061 };
2062
2063 // create a table row in the message list
2064 this.add_message_row = function(uid, cols, flags, attop)
2065 {
2066 if (!this.gui_objects.messagelist || !this.message_list)
2067 return false;
2068
2069 // Prevent from adding messages from different folder (#1487752)
2070 if (flags.mbox != this.env.mailbox && !flags.skip_mbox_check)
2071 return false;
2072
2073 // When deleting messages fast it may happen that the same message
2074 // from the next page could be added many times, we prevent this here
2075 if (this.message_list.rows[uid])
2076 return false;
2077
2078 if (!this.env.messages[uid])
2079 this.env.messages[uid] = {};
2080
2081 // merge flags over local message object
2082 $.extend(this.env.messages[uid], {
2083 deleted: flags.deleted?1:0,
2084 replied: flags.answered?1:0,
2085 unread: !flags.seen?1:0,
2086 forwarded: flags.forwarded?1:0,
2087 flagged: flags.flagged?1:0,
2088 has_children: flags.has_children?1:0,
2089 depth: flags.depth?flags.depth:0,
2090 unread_children: flags.unread_children || 0,
2091 flagged_children: flags.flagged_children || 0,
2092 parent_uid: flags.parent_uid || 0,
2093 selected: this.select_all_mode || this.message_list.in_selection(uid),
2094 ml: flags.ml?1:0,
2095 ctype: flags.ctype,
2096 mbox: flags.mbox,
2097 // flags from plugins
2098 flags: flags.extra_flags
2099 });
2100
2101 var c, n, col, html, css_class, label, status_class = '', status_label = '',
2102 tree = '', expando = '',
2103 list = this.message_list,
2104 rows = list.rows,
2105 message = this.env.messages[uid],
2106 msg_id = this.html_identifier(uid,true),
2107 row_class = 'message'
2108 + (!flags.seen ? ' unread' : '')
2109 + (flags.deleted ? ' deleted' : '')
2110 + (flags.flagged ? ' flagged' : '')
2111 + (message.selected ? ' selected' : ''),
2112 row = { cols:[], style:{}, id:'rcmrow'+msg_id, uid:uid };
2113
2114 // message status icons
2115 css_class = 'msgicon';
2116 if (this.env.status_col === null) {
2117 css_class += ' status';
2118 if (flags.deleted) {
2119 status_class += ' deleted';
2120 status_label += this.get_label('deleted') + ' ';
2121 }
2122 else if (!flags.seen) {
2123 status_class += ' unread';
2124 status_label += this.get_label('unread') + ' ';
2125 }
2126 else if (flags.unread_children > 0) {
2127 status_class += ' unreadchildren';
2128 }
2129 }
2130 if (flags.answered) {
2131 status_class += ' replied';
2132 status_label += this.get_label('replied') + ' ';
2133 }
2134 if (flags.forwarded) {
2135 status_class += ' forwarded';
2136 status_label += this.get_label('forwarded') + ' ';
2137 }
2138
2139 // update selection
2140 if (message.selected && !list.in_selection(uid))
2141 list.selection.push(uid);
2142
2143 // threads
2144 if (this.env.threading) {
2145 if (message.depth) {
2146 // This assumes that div width is hardcoded to 15px,
2147 tree += '<span id="rcmtab' + msg_id + '" class="branch" style="width:' + (message.depth * 15) + 'px;">&nbsp;&nbsp;</span>';
2148
2149 if ((rows[message.parent_uid] && rows[message.parent_uid].expanded === false)
2150 || ((this.env.autoexpand_threads == 0 || this.env.autoexpand_threads == 2) &&
2151 (!rows[message.parent_uid] || !rows[message.parent_uid].expanded))
2152 ) {
2153 row.style.display = 'none';
2154 message.expanded = false;
2155 }
2156 else
2157 message.expanded = true;
2158
2159 row_class += ' thread expanded';
2160 }
2161 else if (message.has_children) {
2162 if (message.expanded === undefined && (this.env.autoexpand_threads == 1 || (this.env.autoexpand_threads == 2 && message.unread_children))) {
2163 message.expanded = true;
2164 }
2165
2166 expando = '<div id="rcmexpando' + row.id + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">&nbsp;&nbsp;</div>';
2167 row_class += ' thread' + (message.expanded ? ' expanded' : '');
2168 }
2169
2170 if (flags.unread_children && flags.seen && !message.expanded)
2171 row_class += ' unroot';
2172
2173 if (flags.flagged_children && !message.expanded)
2174 row_class += ' flaggedroot';
2175 }
2176
2177 tree += '<span id="msgicn'+row.id+'" class="'+css_class+status_class+'" title="'+status_label+'"></span>';
2178 row.className = row_class;
2179
2180 // build subject link
2181 if (cols.subject) {
2182 var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show',
2183 uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid',
2184 query = { _mbox: flags.mbox };
2185 query[uid_param] = uid;
2186 cols.subject = '<a href="' + this.url(action, query) + '" onclick="return rcube_event.keyboard_only(event)"' +
2187 ' onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')" tabindex="-1"><span>'+cols.subject+'</span></a>';
2188 }
2189
2190 // add each submitted col
2191 for (n in this.env.listcols) {
2192 c = this.env.listcols[n];
2193 col = {className: String(c).toLowerCase(), events:{}};
2194
2195 if (this.env.coltypes[c] && this.env.coltypes[c].hidden) {
2196 col.className += ' hidden';
2197 }
2198
2199 if (c == 'flag') {
2200 css_class = (flags.flagged ? 'flagged' : 'unflagged');
2201 label = this.get_label(css_class);
2202 html = '<span id="flagicn'+row.id+'" class="'+css_class+'" title="'+label+'"></span>';
2203 }
2204 else if (c == 'attachment') {
2205 label = this.get_label('withattachment');
2206 if (flags.attachmentClass)
2207 html = '<span class="'+flags.attachmentClass+'" title="'+label+'"></span>';
2208 else if (/application\/|multipart\/(m|signed)/.test(flags.ctype))
2209 html = '<span class="attachment" title="'+label+'"></span>';
2210 else if (/multipart\/report/.test(flags.ctype))
2211 html = '<span class="report"></span>';
2212 else if (flags.ctype == 'multipart/encrypted' || flags.ctype == 'application/pkcs7-mime')
2213 html = '<span class="encrypted"></span>';
2214 else
2215 html = '&nbsp;';
2216 }
2217 else if (c == 'status') {
2218 label = '';
2219 if (flags.deleted) {
2220 css_class = 'deleted';
2221 label = this.get_label('deleted');
2222 }
2223 else if (!flags.seen) {
2224 css_class = 'unread';
2225 label = this.get_label('unread');
2226 }
2227 else if (flags.unread_children > 0) {
2228 css_class = 'unreadchildren';
2229 }
2230 else
2231 css_class = 'msgicon';
2232 html = '<span id="statusicn'+row.id+'" class="'+css_class+status_class+'" title="'+label+'"></span>';
2233 }
2234 else if (c == 'threads')
2235 html = expando;
2236 else if (c == 'subject') {
2237 html = tree + cols[c];
2238 }
2239 else if (c == 'priority') {
2240 if (flags.prio > 0 && flags.prio < 6) {
2241 label = this.get_label('priority') + ' ' + flags.prio;
2242 html = '<span class="prio'+flags.prio+'" title="'+label+'"></span>';
2243 }
2244 else
2245 html = '&nbsp;';
2246 }
2247 else if (c == 'folder') {
2248 html = '<span onmouseover="rcube_webmail.long_subject_title(this)">' + cols[c] + '<span>';
2249 }
2250 else
2251 html = cols[c];
2252
2253 col.innerHTML = html;
2254 row.cols.push(col);
2255 }
2256
2257 if (this.env.layout == 'widescreen')
2258 row = this.widescreen_message_row(row, uid, message);
2259
2260 list.insert_row(row, attop);
2261
2262 // remove 'old' row
2263 if (attop && this.env.pagesize && list.rowcount > this.env.pagesize) {
2264 var uid = list.get_last_row();
2265 list.remove_row(uid);
2266 list.clear_selection(uid);
2267 }
2268 };
2269
2270 // Converts standard message list record into "widescreen" (3-column) layout
2271 this.widescreen_message_row = function(row, uid, message)
2272 {
2273 var domrow = document.createElement('tr');
2274
2275 domrow.id = row.id;
2276 domrow.uid = row.uid;
2277 domrow.className = row.className;
2278 if (row.style) $.extend(domrow.style, row.style);
2279
2280 $.each(this.env.widescreen_list_template, function() {
2281 if (!ref.env.threading && this.className == 'threads')
2282 return;
2283
2284 var i, n, e, col, domcol,
2285 domcell = document.createElement('td');
2286
2287 if (this.className) domcell.className = this.className;
2288
2289 for (i=0; this.cells && i < this.cells.length; i++) {
2290 for (n=0; row.cols && n < row.cols.length; n++) {
2291 if (this.cells[i] == row.cols[n].className) {
2292 col = row.cols[n];
2293 domcol = document.createElement('span');
2294 domcol.className = this.cells[i];
2295 if (this.className == 'subject' && domcol.className != 'subject')
2296 domcol.className += ' skip-on-drag';
2297 if (col.innerHTML)
2298 domcol.innerHTML = col.innerHTML;
2299 domcell.appendChild(domcol);
2300 break;
2301 }
2302 }
2303 }
2304
2305 domrow.appendChild(domcell);
2306 });
2307
2308 if (this.env.threading && message.depth) {
2309 $('td.subject', domrow).attr('style', 'padding-left:' + Math.min(90, message.depth * 15) + 'px !important');
2310 $('span.branch', domrow).remove();
2311 }
2312
2313 return domrow;
2314 };
2315
2316 this.set_list_sorting = function(sort_col, sort_order)
2317 {
2318 var sort_old = this.env.sort_col == 'arrival' ? 'date' : this.env.sort_col,
2319 sort_new = sort_col == 'arrival' ? 'date' : sort_col;
2320
2321 // set table header class
2322 $('#rcm' + sort_old).removeClass('sorted' + this.env.sort_order.toUpperCase());
2323 if (sort_new)
2324 $('#rcm' + sort_new).addClass('sorted' + sort_order);
2325
2326 // if sorting by 'arrival' is selected, click on date column should not switch to 'date'
2327 $('#rcmdate > a').prop('rel', sort_col == 'arrival' ? 'arrival' : 'date');
2328
2329 this.env.sort_col = sort_col;
2330 this.env.sort_order = sort_order;
2331 };
2332
2333 this.set_list_options = function(cols, sort_col, sort_order, threads, layout)
2334 {
2335 var update, post_data = {};
2336
2337 if (sort_col === undefined)
2338 sort_col = this.env.sort_col;
2339 if (!sort_order)
2340 sort_order = this.env.sort_order;
2341
2342 if (this.env.sort_col != sort_col || this.env.sort_order != sort_order) {
2343 update = 1;
2344 this.set_list_sorting(sort_col, sort_order);
2345 }
2346
2347 if (this.env.threading != threads) {
2348 update = 1;
2349 post_data._threads = threads;
2350 }
2351
2352 if (layout && this.env.layout != layout) {
2353 this.triggerEvent('layout-change', {old_layout: this.env.layout, new_layout: layout});
2354 update = 1;
2355 this.env.layout = post_data._layout = layout;
2356 }
2357
2358 if (cols && cols.length) {
2359 // make sure new columns are added at the end of the list
2360 var i, idx, name, newcols = [], oldcols = this.env.listcols;
2361 for (i=0; i<oldcols.length; i++) {
2362 name = oldcols[i];
2363 idx = $.inArray(name, cols);
2364 if (idx != -1) {
2365 newcols.push(name);
2366 delete cols[idx];
2367 }
2368 }
2369 for (i=0; i<cols.length; i++)
2370 if (cols[i])
2371 newcols.push(cols[i]);
2372
2373 if (newcols.join() != oldcols.join()) {
2374 update = 1;
2375 post_data._cols = newcols.join(',');
2376 }
2377 }
2378
2379 if (update)
2380 this.list_mailbox('', '', sort_col+'_'+sort_order, post_data);
2381 };
2382
2383 // when user double-clicks on a row
2384 this.show_message = function(id, safe, preview)
2385 {
2386 if (!id)
2387 return;
2388
2389 var win, target = window,
2390 url = this.params_from_uid(id, {_caps: this.browser_capabilities()});
2391
2392 if (preview && (win = this.get_frame_window(this.env.contentframe))) {
2393 target = win;
2394 url._framed = 1;
2395 }
2396
2397 if (safe)
2398 url._safe = 1;
2399
2400 // also send search request to get the right messages
2401 if (this.env.search_request)
2402 url._search = this.env.search_request;
2403
2404 if (this.env.extwin)
2405 url._extwin = 1;
2406
2407 url = this.url(preview ? 'preview': 'show', url);
2408
2409 if (preview && String(target.location.href).indexOf(url) >= 0) {
2410 this.show_contentframe(true);
2411 }
2412 else {
2413 if (!preview && this.env.message_extwin && !this.env.extwin)
2414 this.open_window(url, true);
2415 else
2416 this.location_href(url, target, true);
2417 }
2418 };
2419
2420 // update message status and unread counter after marking a message as read
2421 this.set_unread_message = function(id, folder)
2422 {
2423 var self = this;
2424
2425 // find window with messages list
2426 if (!self.message_list)
2427 self = self.opener();
2428
2429 if (!self && window.parent)
2430 self = parent.rcmail;
2431
2432 if (!self || !self.message_list)
2433 return;
2434
2435 // this may fail in multifolder mode
2436 if (self.set_message(id, 'unread', false) === false)
2437 self.set_message(id + '-' + folder, 'unread', false);
2438
2439 if (self.env.unread_counts[folder] > 0) {
2440 self.env.unread_counts[folder] -= 1;
2441 self.set_unread_count(folder, self.env.unread_counts[folder], folder == 'INBOX' && !self.is_multifolder_listing());
2442 }
2443 };
2444
2445 this.show_contentframe = function(show)
2446 {
2447 var frame, win, name = this.env.contentframe;
2448
2449 if (frame = this.get_frame_element(name)) {
2450 if (!show && (win = this.get_frame_window(name))) {
2451 if (win.location.href.indexOf(this.env.blankpage) < 0) {
2452 if (win.stop)
2453 win.stop();
2454 else // IE
2455 win.document.execCommand('Stop');
2456
2457 win.location.href = this.env.blankpage;
2458 }
2459 }
2460 else if (!bw.safari && !bw.konq)
2461 $(frame)[show ? 'show' : 'hide']();
2462 }
2463
2464 if (!show && this.env.frame_lock)
2465 this.set_busy(false, null, this.env.frame_lock);
2466 };
2467
2468 this.get_frame_element = function(id)
2469 {
2470 var frame;
2471
2472 if (id && (frame = document.getElementById(id)))
2473 return frame;
2474 };
2475
2476 this.get_frame_window = function(id)
2477 {
2478 var frame = this.get_frame_element(id);
2479
2480 if (frame && frame.name && window.frames)
2481 return window.frames[frame.name];
2482 };
2483
2484 this.lock_frame = function()
2485 {
2486 if (!this.env.frame_lock)
2487 (this.is_framed() ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
2488 };
2489
2490 // list a specific page
2491 this.list_page = function(page)
2492 {
2493 if (page == 'next')
2494 page = this.env.current_page+1;
2495 else if (page == 'last')
2496 page = this.env.pagecount;
2497 else if (page == 'prev' && this.env.current_page > 1)
2498 page = this.env.current_page-1;
2499 else if (page == 'first' && this.env.current_page > 1)
2500 page = 1;
2501
2502 if (page > 0 && page <= this.env.pagecount) {
2503 this.env.current_page = page;
2504
2505 if (this.task == 'addressbook' || this.contact_list)
2506 this.list_contacts(this.env.source, this.env.group, page);
2507 else if (this.task == 'mail')
2508 this.list_mailbox(this.env.mailbox, page);
2509 }
2510 };
2511
2512 // sends request to check for recent messages
2513 this.checkmail = function()
2514 {
2515 var lock = this.set_busy(true, 'checkingmail'),
2516 params = this.check_recent_params();
2517
2518 this.http_post('check-recent', params, lock);
2519 };
2520
2521 // list messages of a specific mailbox using filter
2522 this.filter_mailbox = function(filter)
2523 {
2524 if (this.filter_disabled)
2525 return;
2526
2527 var lock = this.set_busy(true, 'searching');
2528
2529 this.clear_message_list();
2530
2531 // reset vars
2532 this.env.current_page = 1;
2533 this.env.search_filter = filter;
2534 this.http_request('search', this.search_params(false, filter), lock);
2535 };
2536
2537 // reload the current message listing
2538 this.refresh_list = function()
2539 {
2540 this.list_mailbox(this.env.mailbox, this.env.current_page || 1, null, { _clear:1 }, true);
2541 if (this.message_list)
2542 this.message_list.clear_selection();
2543 };
2544
2545 // list messages of a specific mailbox
2546 this.list_mailbox = function(mbox, page, sort, url, update_only)
2547 {
2548 var win, target = window;
2549
2550 if (typeof url != 'object')
2551 url = {};
2552
2553 if (!mbox)
2554 mbox = this.env.mailbox ? this.env.mailbox : 'INBOX';
2555
2556 // add sort to url if set
2557 if (sort)
2558 url._sort = sort;
2559
2560 // folder change, reset page, search scope, etc.
2561 if (this.env.mailbox != mbox) {
2562 page = 1;
2563 this.env.current_page = page;
2564 this.env.search_scope = 'base';
2565 this.select_all_mode = false;
2566 this.reset_search_filter();
2567 }
2568 // also send search request to get the right messages
2569 else if (this.env.search_request)
2570 url._search = this.env.search_request;
2571
2572 if (!update_only) {
2573 // unselect selected messages and clear the list and message data
2574 this.clear_message_list();
2575
2576 if (mbox != this.env.mailbox || (mbox == this.env.mailbox && !page && !sort))
2577 url._refresh = 1;
2578
2579 this.select_folder(mbox, '', true);
2580 this.unmark_folder(mbox, 'recent', '', true);
2581 this.env.mailbox = mbox;
2582 }
2583
2584 // load message list remotely
2585 if (this.gui_objects.messagelist) {
2586 this.list_mailbox_remote(mbox, page, url);
2587 return;
2588 }
2589
2590 if (win = this.get_frame_window(this.env.contentframe)) {
2591 target = win;
2592 url._framed = 1;
2593 }
2594
2595 if (this.env.uid)
2596 url._uid = this.env.uid;
2597
2598 // load message list to target frame/window
2599 if (mbox) {
2600 this.set_busy(true, 'loading');
2601 url._mbox = mbox;
2602 if (page)
2603 url._page = page;
2604 this.location_href(url, target);
2605 }
2606 };
2607
2608 this.clear_message_list = function()
2609 {
2610 this.env.messages = {};
2611
2612 this.show_contentframe(false);
2613 if (this.message_list)
2614 this.message_list.clear(true);
2615 };
2616
2617 // send remote request to load message list
2618 this.list_mailbox_remote = function(mbox, page, url)
2619 {
2620 var lock = this.set_busy(true, 'loading');
2621
2622 if (typeof url != 'object')
2623 url = {};
2624 url._mbox = mbox;
2625 if (page)
2626 url._page = page;
2627
2628 this.http_request('list', url, lock);
2629 this.update_state({ _mbox: mbox, _page: (page && page > 1 ? page : null) });
2630 };
2631
2632 // removes messages that doesn't exists from list selection array
2633 this.update_selection = function()
2634 {
2635 var list = this.message_list,
2636 selected = list.selection,
2637 rows = list.rows,
2638 i, selection = [];
2639
2640 for (i in selected)
2641 if (rows[selected[i]])
2642 selection.push(selected[i]);
2643
2644 list.selection = selection;
2645
2646 // reset preview frame, if currently previewed message is not selected (has been removed)
2647 try {
2648 var win = this.get_frame_window(this.env.contentframe),
2649 id = win.rcmail.env.uid;
2650
2651 if (id && !list.in_selection(id))
2652 this.show_contentframe(false);
2653 }
2654 catch (e) {};
2655 };
2656
2657 // expand all threads with unread children
2658 this.expand_unread = function()
2659 {
2660 var r, tbody = this.message_list.tbody,
2661 new_row = tbody.firstChild;
2662
2663 while (new_row) {
2664 if (new_row.nodeType == 1 && (r = this.message_list.rows[new_row.uid]) && r.unread_children) {
2665 this.message_list.expand_all(r);
2666 this.set_unread_children(r.uid);
2667 }
2668
2669 new_row = new_row.nextSibling;
2670 }
2671
2672 return false;
2673 };
2674
2675 // thread expanding/collapsing handler
2676 this.expand_message_row = function(e, uid)
2677 {
2678 var row = this.message_list.rows[uid];
2679
2680 // handle unread_children/flagged_children mark
2681 row.expanded = !row.expanded;
2682 this.set_unread_children(uid);
2683 this.set_flagged_children(uid);
2684 row.expanded = !row.expanded;
2685
2686 this.message_list.expand_row(e, uid);
2687 };
2688
2689 // message list expanding
2690 this.expand_threads = function()
2691 {
2692 if (!this.env.threading || !this.env.autoexpand_threads || !this.message_list)
2693 return;
2694
2695 switch (this.env.autoexpand_threads) {
2696 case 2: this.expand_unread(); break;
2697 case 1: this.message_list.expand_all(); break;
2698 }
2699 };
2700
2701 // Initializes threads indicators/expanders after list update
2702 this.init_threads = function(roots, mbox)
2703 {
2704 // #1487752
2705 if (mbox && mbox != this.env.mailbox)
2706 return false;
2707
2708 for (var n=0, len=roots.length; n<len; n++)
2709 this.add_tree_icons(roots[n]);
2710 this.expand_threads();
2711 };
2712
2713 // adds threads tree icons to the list (or specified thread)
2714 this.add_tree_icons = function(root)
2715 {
2716 var i, l, r, n, len, pos, tmp = [], uid = [],
2717 row, rows = this.message_list.rows;
2718
2719 if (root)
2720 row = rows[root] ? rows[root].obj : null;
2721 else
2722 row = this.message_list.tbody.firstChild;
2723
2724 while (row) {
2725 if (row.nodeType == 1 && (r = rows[row.uid])) {
2726 if (r.depth) {
2727 for (i=tmp.length-1; i>=0; i--) {
2728 len = tmp[i].length;
2729 if (len > r.depth) {
2730 pos = len - r.depth;
2731 if (!(tmp[i][pos] & 2))
2732 tmp[i][pos] = tmp[i][pos] ? tmp[i][pos]+2 : 2;
2733 }
2734 else if (len == r.depth) {
2735 if (!(tmp[i][0] & 2))
2736 tmp[i][0] += 2;
2737 }
2738 if (r.depth > len)
2739 break;
2740 }
2741
2742 tmp.push(new Array(r.depth));
2743 tmp[tmp.length-1][0] = 1;
2744 uid.push(r.uid);
2745 }
2746 else {
2747 if (tmp.length) {
2748 for (i in tmp) {
2749 this.set_tree_icons(uid[i], tmp[i]);
2750 }
2751 tmp = [];
2752 uid = [];
2753 }
2754 if (root && row != rows[root].obj)
2755 break;
2756 }
2757 }
2758 row = row.nextSibling;
2759 }
2760
2761 if (tmp.length) {
2762 for (i in tmp) {
2763 this.set_tree_icons(uid[i], tmp[i]);
2764 }
2765 }
2766 };
2767
2768 // adds tree icons to specified message row
2769 this.set_tree_icons = function(uid, tree)
2770 {
2771 var i, divs = [], html = '', len = tree.length;
2772
2773 for (i=0; i<len; i++) {
2774 if (tree[i] > 2)
2775 divs.push({'class': 'l3', width: 15});
2776 else if (tree[i] > 1)
2777 divs.push({'class': 'l2', width: 15});
2778 else if (tree[i] > 0)
2779 divs.push({'class': 'l1', width: 15});
2780 // separator div
2781 else if (divs.length && !divs[divs.length-1]['class'])
2782 divs[divs.length-1].width += 15;
2783 else
2784 divs.push({'class': null, width: 15});
2785 }
2786
2787 for (i=divs.length-1; i>=0; i--) {
2788 if (divs[i]['class'])
2789 html += '<div class="tree '+divs[i]['class']+'" />';
2790 else
2791 html += '<div style="width:'+divs[i].width+'px" />';
2792 }
2793
2794 if (html)
2795 $('#rcmtab'+this.html_identifier(uid, true)).html(html);
2796 };
2797
2798 // update parent in a thread
2799 this.update_thread_root = function(uid, flag)
2800 {
2801 if (!this.env.threading)
2802 return;
2803
2804 var root = this.message_list.find_root(uid);
2805
2806 if (uid == root)
2807 return;
2808
2809 var p = this.message_list.rows[root];
2810
2811 if (flag == 'read' && p.unread_children) {
2812 p.unread_children--;
2813 }
2814 else if (flag == 'unread' && p.has_children) {
2815 // unread_children may be undefined
2816 p.unread_children = (p.unread_children || 0) + 1;
2817 }
2818 else if (flag == 'unflagged' && p.flagged_children) {
2819 p.flagged_children--;
2820 }
2821 else if (flag == 'flagged' && p.has_children) {
2822 p.flagged_children = (p.flagged_children || 0) + 1;
2823 }
2824 else {
2825 return;
2826 }
2827
2828 this.set_message_icon(root);
2829 this.set_unread_children(root);
2830 this.set_flagged_children(root);
2831 };
2832
2833 // update thread indicators for all messages in a thread below the specified message
2834 // return number of removed/added root level messages
2835 this.update_thread = function(uid)
2836 {
2837 if (!this.env.threading || !this.message_list.rows[uid])
2838 return 0;
2839
2840 var r, parent, count = 0,
2841 list = this.message_list,
2842 rows = list.rows,
2843 row = rows[uid],
2844 depth = rows[uid].depth,
2845 roots = [];
2846
2847 if (!row.depth) // root message: decrease roots count
2848 count--;
2849
2850 // update unread_children for thread root
2851 if (row.depth && row.unread) {
2852 parent = list.find_root(uid);
2853 rows[parent].unread_children--;
2854 this.set_unread_children(parent);
2855 }
2856
2857 // update unread_children for thread root
2858 if (row.depth && row.flagged) {
2859 parent = list.find_root(uid);
2860 rows[parent].flagged_children--;
2861 this.set_flagged_children(parent);
2862 }
2863
2864 parent = row.parent_uid;
2865
2866 // childrens
2867 row = row.obj.nextSibling;
2868 while (row) {
2869 if (row.nodeType == 1 && (r = rows[row.uid])) {
2870 if (!r.depth || r.depth <= depth)
2871 break;
2872
2873 r.depth--; // move left
2874 // reset width and clear the content of a tab, icons will be added later
2875 $('#rcmtab'+r.id).width(r.depth * 15).html('');
2876 if (!r.depth) { // a new root
2877 count++; // increase roots count
2878 r.parent_uid = 0;
2879 if (r.has_children) {
2880 // replace 'leaf' with 'collapsed'
2881 $('#'+r.id+' .leaf:first')
2882 .attr('id', 'rcmexpando' + r.id)
2883 .attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed'))
2884 .mousedown({uid: r.uid}, function(e) {
2885 return ref.expand_message_row(e, e.data.uid);
2886 });
2887
2888 r.unread_children = 0;
2889 roots.push(r);
2890 }
2891 // show if it was hidden
2892 if (r.obj.style.display == 'none')
2893 $(r.obj).show();
2894 }
2895 else {
2896 if (r.depth == depth)
2897 r.parent_uid = parent;
2898 if (r.unread && roots.length)
2899 roots[roots.length-1].unread_children++;
2900 }
2901 }
2902 row = row.nextSibling;
2903 }
2904
2905 // update unread_children/flagged_children for roots
2906 for (r=0; r<roots.length; r++) {
2907 this.set_unread_children(roots[r].uid);
2908 this.set_flagged_children(roots[r].uid);
2909 }
2910
2911 return count;
2912 };
2913
2914 this.delete_excessive_thread_rows = function()
2915 {
2916 var rows = this.message_list.rows,
2917 tbody = this.message_list.tbody,
2918 row = tbody.firstChild,
2919 cnt = this.env.pagesize + 1;
2920
2921 while (row) {
2922 if (row.nodeType == 1 && (r = rows[row.uid])) {
2923 if (!r.depth && cnt)
2924 cnt--;
2925
2926 if (!cnt)
2927 this.message_list.remove_row(row.uid);
2928 }
2929 row = row.nextSibling;
2930 }
2931 };
2932
2933 // set message icon
2934 this.set_message_icon = function(uid)
2935 {
2936 var css_class, label = '',
2937 row = this.message_list.rows[uid];
2938
2939 if (!row)
2940 return false;
2941
2942 if (row.icon) {
2943 css_class = 'msgicon';
2944 if (row.deleted) {
2945 css_class += ' deleted';
2946 label += this.get_label('deleted') + ' ';
2947 }
2948 else if (row.unread) {
2949 css_class += ' unread';
2950 label += this.get_label('unread') + ' ';
2951 }
2952 else if (row.unread_children)
2953 css_class += ' unreadchildren';
2954 if (row.msgicon == row.icon) {
2955 if (row.replied) {
2956 css_class += ' replied';
2957 label += this.get_label('replied') + ' ';
2958 }
2959 if (row.forwarded) {
2960 css_class += ' forwarded';
2961 label += this.get_label('forwarded') + ' ';
2962 }
2963 css_class += ' status';
2964 }
2965
2966 $(row.icon).attr('class', css_class).attr('title', label);
2967 }
2968
2969 if (row.msgicon && row.msgicon != row.icon) {
2970 label = '';
2971 css_class = 'msgicon';
2972 if (!row.unread && row.unread_children) {
2973 css_class += ' unreadchildren';
2974 }
2975 if (row.replied) {
2976 css_class += ' replied';
2977 label += this.get_label('replied') + ' ';
2978 }
2979 if (row.forwarded) {
2980 css_class += ' forwarded';
2981 label += this.get_label('forwarded') + ' ';
2982 }
2983
2984 $(row.msgicon).attr('class', css_class).attr('title', label);
2985 }
2986
2987 if (row.flagicon) {
2988 css_class = (row.flagged ? 'flagged' : 'unflagged');
2989 label = this.get_label(css_class);
2990 $(row.flagicon).attr('class', css_class)
2991 .attr('aria-label', label)
2992 .attr('title', label);
2993 }
2994 };
2995
2996 // set message status
2997 this.set_message_status = function(uid, flag, status)
2998 {
2999 var row = this.message_list.rows[uid];
3000
3001 if (!row)
3002 return false;
3003
3004 if (flag == 'unread') {
3005 if (row.unread != status)
3006 this.update_thread_root(uid, status ? 'unread' : 'read');
3007 }
3008 else if (flag == 'flagged') {
3009 this.update_thread_root(uid, status ? 'flagged' : 'unflagged');
3010 }
3011
3012 if ($.inArray(flag, ['unread', 'deleted', 'replied', 'forwarded', 'flagged']) > -1)
3013 row[flag] = status;
3014 };
3015
3016 // set message row status, class and icon
3017 this.set_message = function(uid, flag, status)
3018 {
3019 var row = this.message_list && this.message_list.rows[uid];
3020
3021 if (!row)
3022 return false;
3023
3024 if (flag)
3025 this.set_message_status(uid, flag, status);
3026
3027 if ($.inArray(flag, ['unread', 'deleted', 'flagged']) > -1)
3028 $(row.obj)[row[flag] ? 'addClass' : 'removeClass'](flag);
3029
3030 this.set_unread_children(uid);
3031 this.set_message_icon(uid);
3032 };
3033
3034 // sets unroot (unread_children) class of parent row
3035 this.set_unread_children = function(uid)
3036 {
3037 var row = this.message_list.rows[uid];
3038
3039 if (row.parent_uid)
3040 return;
3041
3042 var enable = !row.unread && row.unread_children && !row.expanded;
3043 $(row.obj)[enable ? 'addClass' : 'removeClass']('unroot');
3044 };
3045
3046 // sets flaggedroot (flagged_children) class of parent row
3047 this.set_flagged_children = function(uid)
3048 {
3049 var row = this.message_list.rows[uid];
3050
3051 if (row.parent_uid)
3052 return;
3053
3054 var enable = row.flagged_children && !row.expanded;
3055 $(row.obj)[enable ? 'addClass' : 'removeClass']('flaggedroot');
3056 };
3057
3058 // copy selected messages to the specified mailbox
3059 this.copy_messages = function(mbox, event)
3060 {
3061 if (mbox && typeof mbox === 'object')
3062 mbox = mbox.id;
3063 else if (!mbox)
3064 return this.folder_selector(event, function(folder) { ref.command('copy', folder); });
3065
3066 // exit if current or no mailbox specified
3067 if (!mbox || mbox == this.env.mailbox)
3068 return;
3069
3070 var post_data = this.selection_post_data({_target_mbox: mbox});
3071
3072 // exit if selection is empty
3073 if (!post_data._uid)
3074 return;
3075
3076 // send request to server
3077 this.http_post('copy', post_data, this.display_message(this.get_label('copyingmessage'), 'loading'));
3078 };
3079
3080 // move selected messages to the specified mailbox
3081 this.move_messages = function(mbox, event)
3082 {
3083 if (mbox && typeof mbox === 'object')
3084 mbox = mbox.id;
3085 else if (!mbox)
3086 return this.folder_selector(event, function(folder) { ref.command('move', folder); });
3087
3088 // exit if current or no mailbox specified
3089 if (!mbox || (mbox == this.env.mailbox && !this.is_multifolder_listing()))
3090 return;
3091
3092 var lock = false, post_data = this.selection_post_data({_target_mbox: mbox});
3093
3094 // exit if selection is empty
3095 if (!post_data._uid)
3096 return;
3097
3098 // show wait message
3099 if (this.env.action == 'show')
3100 lock = this.set_busy(true, 'movingmessage');
3101 else
3102 this.show_contentframe(false);
3103
3104 // Hide message command buttons until a message is selected
3105 this.enable_command(this.env.message_commands, false);
3106
3107 this.with_selected_messages('move', post_data, lock);
3108 };
3109
3110 // delete selected messages from the current mailbox
3111 this.delete_messages = function(event)
3112 {
3113 var list = this.message_list, trash = this.env.trash_mailbox;
3114
3115 // if config is set to flag for deletion
3116 if (this.env.flag_for_deletion) {
3117 this.mark_message('delete');
3118 return false;
3119 }
3120 // if there isn't a defined trash mailbox or we are in it
3121 else if (!trash || this.env.mailbox == trash)
3122 this.permanently_remove_messages();
3123 // we're in Junk folder and delete_junk is enabled
3124 else if (this.env.delete_junk && this.env.junk_mailbox && this.env.mailbox == this.env.junk_mailbox)
3125 this.permanently_remove_messages();
3126 // if there is a trash mailbox defined and we're not currently in it
3127 else {
3128 // if shift was pressed delete it immediately
3129 if ((list && list.modkey == SHIFT_KEY) || (event && rcube_event.get_modifier(event) == SHIFT_KEY)) {
3130 if (confirm(this.get_label('deletemessagesconfirm')))
3131 this.permanently_remove_messages();
3132 }
3133 else
3134 this.move_messages(trash);
3135 }
3136
3137 return true;
3138 };
3139
3140 // delete the selected messages permanently
3141 this.permanently_remove_messages = function()
3142 {
3143 var post_data = this.selection_post_data();
3144
3145 // exit if selection is empty
3146 if (!post_data._uid)
3147 return;
3148
3149 this.show_contentframe(false);
3150 this.with_selected_messages('delete', post_data);
3151 };
3152
3153 // Send a specific move/delete request with UIDs of all selected messages
3154 this.with_selected_messages = function(action, post_data, lock, http_action)
3155 {
3156 var count = 0, msg,
3157 remove = (action == 'delete' || !this.is_multifolder_listing());
3158
3159 // update the list (remove rows, clear selection)
3160 if (this.message_list) {
3161 var n, id, root, roots = [],
3162 selection = this.message_list.get_selection();
3163
3164 for (n=0, len=selection.length; n<len; n++) {
3165 id = selection[n];
3166
3167 if (this.env.threading) {
3168 count += this.update_thread(id);
3169 root = this.message_list.find_root(id);
3170 if (root != id && $.inArray(root, roots) < 0) {
3171 roots.push(root);
3172 }
3173 }
3174 if (remove)
3175 this.message_list.remove_row(id, (this.env.display_next && n == selection.length-1));
3176 }
3177 // make sure there are no selected rows
3178 if (!this.env.display_next && remove)
3179 this.message_list.clear_selection();
3180 // update thread tree icons
3181 for (n=0, len=roots.length; n<len; n++) {
3182 this.add_tree_icons(roots[n]);
3183 }
3184 }
3185
3186 if (count < 0)
3187 post_data._count = (count*-1);
3188 // remove threads from the end of the list
3189 else if (count > 0 && remove)
3190 this.delete_excessive_thread_rows();
3191
3192 if (!remove)
3193 post_data._refresh = 1;
3194
3195 if (!lock) {
3196 msg = action == 'move' ? 'movingmessage' : 'deletingmessage';
3197 lock = this.display_message(this.get_label(msg), 'loading');
3198 }
3199
3200 // send request to server
3201 this.http_post(http_action || action, post_data, lock);
3202 };
3203
3204 // build post data for message delete/move/copy/flag requests
3205 this.selection_post_data = function(data)
3206 {
3207 if (typeof(data) != 'object')
3208 data = {};
3209
3210 data._mbox = this.env.mailbox;
3211
3212 if (!data._uid) {
3213 var uids = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
3214 data._uid = this.uids_to_list(uids);
3215 }
3216
3217 if (this.env.action)
3218 data._from = this.env.action;
3219
3220 // also send search request to get the right messages
3221 if (this.env.search_request)
3222 data._search = this.env.search_request;
3223
3224 if (this.env.display_next && this.env.next_uid)
3225 data._next_uid = this.env.next_uid;
3226
3227 return data;
3228 };
3229
3230 // set a specific flag to one or more messages
3231 this.mark_message = function(flag, uid)
3232 {
3233 var a_uids = [], r_uids = [], len, n, id,
3234 list = this.message_list;
3235
3236 if (uid)
3237 a_uids[0] = uid;
3238 else if (this.env.uid)
3239 a_uids[0] = this.env.uid;
3240 else if (list)
3241 a_uids = list.get_selection();
3242
3243 if (!list)
3244 r_uids = a_uids;
3245 else {
3246 list.focus();
3247 for (n=0, len=a_uids.length; n<len; n++) {
3248 id = a_uids[n];
3249 if ((flag == 'read' && list.rows[id].unread)
3250 || (flag == 'unread' && !list.rows[id].unread)
3251 || (flag == 'delete' && !list.rows[id].deleted)
3252 || (flag == 'undelete' && list.rows[id].deleted)
3253 || (flag == 'flagged' && !list.rows[id].flagged)
3254 || (flag == 'unflagged' && list.rows[id].flagged))
3255 {
3256 r_uids.push(id);
3257 }
3258 }
3259 }
3260
3261 // nothing to do
3262 if (!r_uids.length && !this.select_all_mode)
3263 return;
3264
3265 switch (flag) {
3266 case 'read':
3267 case 'unread':
3268 this.toggle_read_status(flag, r_uids);
3269 break;
3270 case 'delete':
3271 case 'undelete':
3272 this.toggle_delete_status(r_uids);
3273 break;
3274 case 'flagged':
3275 case 'unflagged':
3276 this.toggle_flagged_status(flag, a_uids);
3277 break;
3278 }
3279 };
3280
3281 // set class to read/unread
3282 this.toggle_read_status = function(flag, a_uids)
3283 {
3284 var i, len = a_uids.length,
3285 post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: flag}),
3286 lock = this.display_message(this.get_label('markingmessage'), 'loading');
3287
3288 // mark all message rows as read/unread
3289 for (i=0; i<len; i++)
3290 this.set_message(a_uids[i], 'unread', (flag == 'unread' ? true : false));
3291
3292 this.http_post('mark', post_data, lock);
3293 };
3294
3295 // set image to flagged or unflagged
3296 this.toggle_flagged_status = function(flag, a_uids)
3297 {
3298 var i, len = a_uids.length,
3299 post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: flag}),
3300 lock = this.display_message(this.get_label('markingmessage'), 'loading');
3301
3302 // mark all message rows as flagged/unflagged
3303 for (i=0; i<len; i++)
3304 this.set_message(a_uids[i], 'flagged', (flag == 'flagged' ? true : false));
3305
3306 this.http_post('mark', post_data, lock);
3307 };
3308
3309 // mark all message rows as deleted/undeleted
3310 this.toggle_delete_status = function(a_uids)
3311 {
3312 var len = a_uids.length,
3313 i, uid, all_deleted = true,
3314 rows = this.message_list ? this.message_list.rows : {};
3315
3316 if (len == 1) {
3317 if (!this.message_list || (rows[a_uids[0]] && !rows[a_uids[0]].deleted))
3318 this.flag_as_deleted(a_uids);
3319 else
3320 this.flag_as_undeleted(a_uids);
3321
3322 return true;
3323 }
3324
3325 for (i=0; i<len; i++) {
3326 uid = a_uids[i];
3327 if (rows[uid] && !rows[uid].deleted) {
3328 all_deleted = false;
3329 break;
3330 }
3331 }
3332
3333 if (all_deleted)
3334 this.flag_as_undeleted(a_uids);
3335 else
3336 this.flag_as_deleted(a_uids);
3337
3338 return true;
3339 };
3340
3341 this.flag_as_undeleted = function(a_uids)
3342 {
3343 var i, len = a_uids.length,
3344 post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: 'undelete'}),
3345 lock = this.display_message(this.get_label('markingmessage'), 'loading');
3346
3347 for (i=0; i<len; i++)
3348 this.set_message(a_uids[i], 'deleted', false);
3349
3350 this.http_post('mark', post_data, lock);
3351 };
3352
3353 this.flag_as_deleted = function(a_uids)
3354 {
3355 var r_uids = [],
3356 post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: 'delete'}),
3357 lock = this.display_message(this.get_label('markingmessage'), 'loading'),
3358 list = this.message_list,
3359 rows = list ? list.rows : {},
3360 count = 0;
3361
3362 for (var i=0, len=a_uids.length; i<len; i++) {
3363 uid = a_uids[i];
3364 if (rows[uid]) {
3365 if (rows[uid].unread)
3366 r_uids[r_uids.length] = uid;
3367
3368 if (this.env.skip_deleted) {
3369 count += this.update_thread(uid);
3370 list.remove_row(uid, (this.env.display_next && i == list.selection.length-1));
3371 }
3372 else
3373 this.set_message(uid, 'deleted', true);
3374 }
3375 }
3376
3377 // make sure there are no selected rows
3378 if (this.env.skip_deleted && list) {
3379 if (!this.env.display_next || !list.rowcount)
3380 list.clear_selection();
3381 if (count < 0)
3382 post_data._count = (count*-1);
3383 else if (count > 0)
3384 // remove threads from the end of the list
3385 this.delete_excessive_thread_rows();
3386 }
3387
3388 // set of messages to mark as seen
3389 if (r_uids.length)
3390 post_data._ruid = this.uids_to_list(r_uids);
3391
3392 if (this.env.skip_deleted && this.env.display_next && this.env.next_uid)
3393 post_data._next_uid = this.env.next_uid;
3394
3395 this.http_post('mark', post_data, lock);
3396 };
3397
3398 // flag as read without mark request (called from backend)
3399 // argument should be a coma-separated list of uids
3400 this.flag_deleted_as_read = function(uids)
3401 {
3402 var uid, i, len,
3403 rows = this.message_list ? this.message_list.rows : {};
3404
3405 if (typeof uids == 'string')
3406 uids = uids.split(',');
3407
3408 for (i=0, len=uids.length; i<len; i++) {
3409 uid = uids[i];
3410 if (rows[uid])
3411 this.set_message(uid, 'unread', false);
3412 }
3413 };
3414
3415 // Converts array of message UIDs to comma-separated list for use in URL
3416 // with select_all mode checking
3417 this.uids_to_list = function(uids)
3418 {
3419 return this.select_all_mode ? '*' : (uids.length <= 1 ? uids.join(',') : uids);
3420 };
3421
3422 // Sets title of the delete button
3423 this.set_button_titles = function()
3424 {
3425 var label = 'deletemessage';
3426
3427 if (!this.env.flag_for_deletion
3428 && this.env.trash_mailbox && this.env.mailbox != this.env.trash_mailbox
3429 && (!this.env.delete_junk || !this.env.junk_mailbox || this.env.mailbox != this.env.junk_mailbox)
3430 )
3431 label = 'movemessagetotrash';
3432
3433 this.set_alttext('delete', label);
3434 };
3435
3436 // Initialize input element for list page jump
3437 this.init_pagejumper = function(element)
3438 {
3439 $(element).addClass('rcpagejumper')
3440 .on('focus', function(e) {
3441 // create and display popup with page selection
3442 var i, html = '';
3443
3444 for (i = 1; i <= ref.env.pagecount; i++)
3445 html += '<li>' + i + '</li>';
3446
3447 html = '<ul class="toolbarmenu">' + html + '</ul>';
3448
3449 if (!ref.pagejump) {
3450 ref.pagejump = $('<div id="pagejump-selector" class="popupmenu"></div>')
3451 .appendTo(document.body)
3452 .on('click', 'li', function() {
3453 if (!ref.busy)
3454 $(element).val($(this).text()).change();
3455 });
3456 }
3457
3458 if (ref.pagejump.data('count') != i)
3459 ref.pagejump.html(html);
3460
3461 ref.pagejump.attr('rel', '#' + this.id).data('count', i);
3462
3463 // display page selector
3464 ref.show_menu('pagejump-selector', true, e);
3465 $(this).keydown();
3466 })
3467 // keyboard navigation
3468 .on('keydown keyup click', function(e) {
3469 var current, selector = $('#pagejump-selector'),
3470 ul = $('ul', selector),
3471 list = $('li', ul),
3472 height = ul.height(),
3473 p = parseInt(this.value);
3474
3475 if (e.which != 27 && e.which != 9 && e.which != 13 && !selector.is(':visible'))
3476 return ref.show_menu('pagejump-selector', true, e);
3477
3478 if (e.type == 'keydown') {
3479 // arrow-down
3480 if (e.which == 40) {
3481 if (list.length > p)
3482 this.value = (p += 1);
3483 }
3484 // arrow-up
3485 else if (e.which == 38) {
3486 if (p > 1 && list.length > p - 1)
3487 this.value = (p -= 1);
3488 }
3489 // enter
3490 else if (e.which == 13) {
3491 return $(this).change();
3492 }
3493 // esc, tab
3494 else if (e.which == 27 || e.which == 9) {
3495 return $(element).val(ref.env.current_page);
3496 }
3497 }
3498
3499 $('li.selected', ul).removeClass('selected');
3500
3501 if ((current = $(list[p - 1])).length) {
3502 current.addClass('selected');
3503 $('#pagejump-selector').scrollTop(((ul.height() / list.length) * (p - 1)) - selector.height() / 2);
3504 }
3505 })
3506 .on('change', function(e) {
3507 // go to specified page
3508 var p = parseInt(this.value);
3509 if (p && p != ref.env.current_page && !ref.busy) {
3510 ref.hide_menu('pagejump-selector');
3511 ref.list_page(p);
3512 }
3513 });
3514 };
3515
3516 // Update page-jumper state on list updates
3517 this.update_pagejumper = function()
3518 {
3519 $('input.rcpagejumper').val(this.env.current_page).prop('disabled', this.env.pagecount < 2);
3520 };
3521
3522 // check for mailvelope API
3523 this.check_mailvelope = function(action)
3524 {
3525 if (typeof window.mailvelope !== 'undefined') {
3526 this.mailvelope_load(action);
3527 }
3528 else {
3529 $(window).on('mailvelope', function() {
3530 ref.mailvelope_load(action);
3531 });
3532 }
3533 };
3534
3535 // Load Mailvelope functionality (and initialize keyring if needed)
3536 this.mailvelope_load = function(action)
3537 {
3538 if (this.env.browser_capabilities)
3539 this.env.browser_capabilities['pgpmime'] = 1;
3540
3541 var keyring = this.env.user_id;
3542
3543 mailvelope.getKeyring(keyring).then(function(kr) {
3544 ref.mailvelope_keyring = kr;
3545 ref.mailvelope_init(action, kr);
3546 }, function(err) {
3547 // attempt to create a new keyring for this app/user
3548 mailvelope.createKeyring(keyring).then(function(kr) {
3549 ref.mailvelope_keyring = kr;
3550 ref.mailvelope_init(action, kr);
3551 }, function(err) {
3552 console.error(err);
3553 });
3554 });
3555 };
3556
3557 // Initializes Mailvelope editor or display container
3558 this.mailvelope_init = function(action, keyring)
3559 {
3560 if (!window.mailvelope)
3561 return;
3562
3563 if (action == 'show' || action == 'preview' || action == 'print') {
3564 // decrypt text body
3565 if (this.env.is_pgp_content) {
3566 var data = $(this.env.is_pgp_content).text();
3567 ref.mailvelope_display_container(this.env.is_pgp_content, data, keyring);
3568 }
3569 // load pgp/mime message and pass it to the mailvelope display container
3570 else if (this.env.pgp_mime_part) {
3571 var msgid = this.display_message(this.get_label('loadingdata'), 'loading'),
3572 selector = this.env.pgp_mime_container;
3573
3574 $.ajax({
3575 type: 'GET',
3576 url: this.url('get', { '_mbox': this.env.mailbox, '_uid': this.env.uid, '_part': this.env.pgp_mime_part }),
3577 error: function(o, status, err) {
3578 ref.http_error(o, status, err, msgid);
3579 },
3580 success: function(data) {
3581 ref.mailvelope_display_container(selector, data, keyring, msgid);
3582 }
3583 });
3584 }
3585 }
3586 else if (action == 'compose') {
3587 this.env.compose_commands.push('compose-encrypted');
3588
3589 var is_html = $('input[name="_is_html"]').val() > 0;
3590
3591 if (this.env.pgp_mime_message) {
3592 // fetch PGP/Mime part and open load into Mailvelope editor
3593 var lock = this.set_busy(true, this.get_label('loadingdata'));
3594
3595 $.ajax({
3596 type: 'GET',
3597 url: this.url('get', this.env.pgp_mime_message),
3598 error: function(o, status, err) {
3599 ref.http_error(o, status, err, lock);
3600 ref.enable_command('compose-encrypted', !is_html);
3601 },
3602 success: function(data) {
3603 ref.set_busy(false, null, lock);
3604
3605 if (is_html) {
3606 ref.command('toggle-editor', {html: false, noconvert: true});
3607 $('#' + ref.env.composebody).val('');
3608 }
3609
3610 ref.compose_encrypted({ quotedMail: data });
3611 ref.enable_command('compose-encrypted', true);
3612 }
3613 });
3614 }
3615 else {
3616 // enable encrypted compose toggle
3617 this.enable_command('compose-encrypted', !is_html);
3618 }
3619
3620 // make sure to disable encryption button after toggling editor into HTML mode
3621 this.addEventListener('actionafter', function(args) {
3622 if (args.ret && args.action == 'toggle-editor')
3623 ref.enable_command('compose-encrypted', !args.props.html);
3624 });
3625 }
3626 };
3627
3628 // handler for the 'compose-encrypted' command
3629 this.compose_encrypted = function(props)
3630 {
3631 var options, container = $('#' + this.env.composebody).parent();
3632
3633 // remove Mailvelope editor if active
3634 if (ref.mailvelope_editor) {
3635 ref.mailvelope_editor = null;
3636 ref.compose_skip_unsavedcheck = false;
3637 ref.set_button('compose-encrypted', 'act');
3638
3639 container.removeClass('mailvelope')
3640 .find('iframe:not([aria-hidden=true])').remove();
3641 $('#' + ref.env.composebody).show();
3642 $("[name='_pgpmime']").remove();
3643
3644 // disable commands that operate on the compose body
3645 ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', true);
3646 ref.triggerEvent('compose-encrypted', { active:false });
3647 }
3648 // embed Mailvelope editor container
3649 else {
3650 if (this.spellcheck_state())
3651 this.editor.spellcheck_stop();
3652
3653 if (props.quotedMail) {
3654 options = { quotedMail: props.quotedMail, quotedMailIndent: false };
3655 }
3656 else {
3657 options = { predefinedText: $('#' + this.env.composebody).val() };
3658 }
3659
3660 if (this.env.compose_mode == 'reply') {
3661 options.quotedMailIndent = true;
3662 options.quotedMailHeader = this.env.compose_reply_header;
3663 }
3664
3665 mailvelope.createEditorContainer('#' + container.attr('id'), ref.mailvelope_keyring, options).then(function(editor) {
3666 ref.mailvelope_editor = editor;
3667 ref.compose_skip_unsavedcheck = true;
3668 ref.set_button('compose-encrypted', 'sel');
3669
3670 container.addClass('mailvelope');
3671 $('#' + ref.env.composebody).hide();
3672
3673 // disable commands that operate on the compose body
3674 ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', false);
3675 ref.triggerEvent('compose-encrypted', { active:true });
3676
3677 // notify user about loosing attachments
3678 if (ref.env.attachments && !$.isEmptyObject(ref.env.attachments)) {
3679 alert(ref.get_label('encryptnoattachments'));
3680
3681 $.each(ref.env.attachments, function(name, attach) {
3682 ref.remove_from_attachment_list(name);
3683 });
3684 }
3685 }, function(err) {
3686 console.error(err);
3687 console.log(options);
3688 });
3689 }
3690 };
3691
3692 // callback to replace the message body with the full armored
3693 this.mailvelope_submit_messageform = function(draft, saveonly)
3694 {
3695 // get recipients
3696 var recipients = [];
3697 $.each(['to', 'cc', 'bcc'], function(i,field) {
3698 var pos, rcpt, val = $.trim($('[name="_' + field + '"]').val());
3699 while (val.length && rcube_check_email(val, true)) {
3700 rcpt = RegExp.$2;
3701 recipients.push(rcpt);
3702 val = val.substr(val.indexOf(rcpt) + rcpt.length + 1).replace(/^\s*,\s*/, '');
3703 }
3704 });
3705
3706 // check if we have keys for all recipients
3707 var isvalid = recipients.length > 0;
3708 ref.mailvelope_keyring.validKeyForAddress(recipients).then(function(status) {
3709 var missing_keys = [];
3710 $.each(status, function(k,v) {
3711 if (v === false) {
3712 isvalid = false;
3713 missing_keys.push(k);
3714 }
3715 });
3716
3717 // list recipients with missing keys
3718 if (!isvalid && missing_keys.length) {
3719 // display dialog with missing keys
3720 ref.simple_dialog(
3721 ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')) +
3722 '<p>' + ref.get_label('searchpubkeyservers') + '</p>',
3723 'encryptedsendialog',
3724 function() {
3725 ref.mailvelope_search_pubkeys(missing_keys, function() {
3726 return true; // close dialog
3727 });
3728 },
3729 {button: 'search'}
3730 );
3731 return false;
3732 }
3733
3734 if (!isvalid) {
3735 if (!recipients.length) {
3736 alert(ref.get_label('norecipientwarning'));
3737 $("[name='_to']").focus();
3738 }
3739 return false;
3740 }
3741
3742 // add sender identity to recipients to be able to decrypt our very own message
3743 var senders = [], selected_sender = ref.env.identities[$("[name='_from'] option:selected").val()];
3744 $.each(ref.env.identities, function(k, sender) {
3745 senders.push(sender.email);
3746 });
3747
3748 ref.mailvelope_keyring.validKeyForAddress(senders).then(function(status) {
3749 valid_sender = null;
3750 $.each(status, function(k,v) {
3751 if (v !== false) {
3752 valid_sender = k;
3753 if (valid_sender == selected_sender) {
3754 return false; // break
3755 }
3756 }
3757 });
3758
3759 if (!valid_sender) {
3760 if (!confirm(ref.get_label('nopubkeyforsender'))) {
3761 return false;
3762 }
3763 }
3764
3765 recipients.push(valid_sender);
3766
3767 ref.mailvelope_editor.encrypt(recipients).then(function(armored) {
3768 // all checks passed, send message
3769 var form = ref.gui_objects.messageform,
3770 hidden = $("[name='_pgpmime']", form),
3771 msgid = ref.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage')
3772
3773 form.target = 'savetarget';
3774 form._draft.value = draft ? '1' : '';
3775 form.action = ref.add_url(form.action, '_unlock', msgid);
3776 form.action = ref.add_url(form.action, '_framed', 1);
3777
3778 if (saveonly) {
3779 form.action = ref.add_url(form.action, '_saveonly', 1);
3780 }
3781
3782 // send pgp conent via hidden field
3783 if (!hidden.length) {
3784 hidden = $('<input type="hidden" name="_pgpmime">').appendTo(form);
3785 }
3786 hidden.val(armored);
3787
3788 form.submit();
3789
3790 }, function(err) {
3791 console.log(err);
3792 }); // mailvelope_editor.encrypt()
3793
3794 }, function(err) {
3795 console.error(err);
3796 }); // mailvelope_keyring.validKeyForAddress(senders)
3797
3798 }, function(err) {
3799 console.error(err);
3800 }); // mailvelope_keyring.validKeyForAddress(recipients)
3801
3802 return false;
3803 };
3804
3805 // wrapper for the mailvelope.createDisplayContainer API call
3806 this.mailvelope_display_container = function(selector, data, keyring, msgid)
3807 {
3808 var error_handler = function(error) {
3809 // remove mailvelope frame with the error message
3810 $(selector + ' > iframe').remove();
3811 ref.hide_message(msgid);
3812 ref.display_message(error.message, 'error');
3813 };
3814
3815 mailvelope.createDisplayContainer(selector, data, keyring, { showExternalContent: this.env.safemode }).then(function(status) {
3816 if (status.error && status.error.message) {
3817 return error_handler(status.error);
3818 }
3819
3820 ref.hide_message(msgid);
3821 $(selector).addClass('mailvelope').children().not('iframe').hide();
3822
3823 // on success we can remove encrypted part from the attachments list
3824 if (ref.env.pgp_mime_part)
3825 $('#attach' + ref.env.pgp_mime_part).remove();
3826
3827 setTimeout(function() { $(window).resize(); }, 10);
3828 }, error_handler);
3829 };
3830
3831 // subroutine to query keyservers for public keys
3832 this.mailvelope_search_pubkeys = function(emails, resolve, import_handler)
3833 {
3834 // query with publickey.js
3835 var deferreds = [],
3836 pk = new PublicKey(),
3837 lock = ref.display_message(ref.get_label('loading'), 'loading');
3838
3839 $.each(emails, function(i, email) {
3840 var d = $.Deferred();
3841 pk.search(email, function(results, errorCode) {
3842 if (errorCode !== null) {
3843 // rejecting would make all fail
3844 // d.reject(email);
3845 d.resolve([email]);
3846 }
3847 else {
3848 d.resolve([email].concat(results));
3849 }
3850 });
3851 deferreds.push(d);
3852 });
3853
3854 $.when.apply($, deferreds).then(function() {
3855 var missing_keys = [],
3856 key_selection = [];
3857
3858 // alanyze results of all queries
3859 $.each(arguments, function(i, result) {
3860 var email = result.shift();
3861 if (!result.length) {
3862 missing_keys.push(email);
3863 }
3864 else {
3865 key_selection = key_selection.concat(result);
3866 }
3867 });
3868
3869 ref.hide_message(lock);
3870 resolve(true);
3871
3872 // show key import dialog
3873 if (key_selection.length) {
3874 ref.mailvelope_key_import_dialog(key_selection, import_handler);
3875 }
3876 // some keys could not be found
3877 if (missing_keys.length) {
3878 ref.display_message(ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')), 'warning');
3879 }
3880 }).fail(function() {
3881 console.error('Pubkey lookup failed with', arguments);
3882 ref.hide_message(lock);
3883 ref.display_message('pubkeysearcherror', 'error');
3884 resolve(false);
3885 });
3886 };
3887
3888 // list the given public keys in a dialog with options to import
3889 // them into the local Maivelope keyring
3890 this.mailvelope_key_import_dialog = function(candidates, import_handler)
3891 {
3892 var ul = $('<div>').addClass('listing pgpkeyimport');
3893 $.each(candidates, function(i, keyrec) {
3894 var li = $('<div>').addClass('key');
3895 if (keyrec.revoked) li.addClass('revoked');
3896 if (keyrec.disabled) li.addClass('disabled');
3897 if (keyrec.expired) li.addClass('expired');
3898
3899 li.append($('<label>').addClass('keyid').text(ref.get_label('keyid')));
3900 li.append($('<a>').text(keyrec.keyid.substr(-8).toUpperCase())
3901 .attr('href', keyrec.info)
3902 .attr('target', '_blank')
3903 .attr('tabindex', '-1'));
3904
3905 li.append($('<label>').addClass('keylen').text(ref.get_label('keylength')));
3906 li.append($('<span>').text(keyrec.keylen));
3907
3908 if (keyrec.expirationdate) {
3909 li.append($('<label>').addClass('keyexpired').text(ref.get_label('keyexpired')));
3910 li.append($('<span>').text(new Date(keyrec.expirationdate * 1000).toDateString()));
3911 }
3912
3913 if (keyrec.revoked) {
3914 li.append($('<span>').addClass('keyrevoked').text(ref.get_label('keyrevoked')));
3915 }
3916
3917 var ul_ = $('<ul>').addClass('uids');
3918 $.each(keyrec.uids, function(j, uid) {
3919 var li_ = $('<li>').addClass('uid');
3920 if (uid.revoked) li_.addClass('revoked');
3921 if (uid.disabled) li_.addClass('disabled');
3922 if (uid.expired) li_.addClass('expired');
3923
3924 ul_.append(li_.text(uid.uid));
3925 });
3926
3927 li.append(ul_);
3928 li.append($('<input>')
3929 .attr('type', 'button')
3930 .attr('rel', keyrec.keyid)
3931 .attr('value', ref.get_label('import'))
3932 .addClass('button importkey')
3933 .prop('disabled', keyrec.revoked || keyrec.disabled || keyrec.expired));
3934
3935 ul.append(li);
3936 });
3937
3938 // display dialog with missing keys
3939 ref.show_popup_dialog(
3940 $('<div>')
3941 .append($('<p>').html(ref.get_label('encryptpubkeysfound')))
3942 .append(ul),
3943 ref.get_label('importpubkeys'),
3944 [{
3945 text: ref.get_label('close'),
3946 click: function() {
3947 (ref.is_framed() ? parent.$ : $)(this).dialog('close');
3948 }
3949 }]
3950 );
3951
3952 // delegate handler for import button clicks
3953 ul.on('click', 'input.button.importkey', function() {
3954 var btn = $(this),
3955 keyid = btn.attr('rel'),
3956 pk = new PublicKey(),
3957 lock = ref.display_message(ref.get_label('loading'), 'loading');
3958
3959 // fetch from keyserver and import to Mailvelope keyring
3960 pk.get(keyid, function(armored, errorCode) {
3961 ref.hide_message(lock);
3962
3963 if (errorCode) {
3964 ref.display_message(ref.get_label('keyservererror'), 'error');
3965 return;
3966 }
3967
3968 if (import_handler) {
3969 import_handler(armored);
3970 return;
3971 }
3972
3973 // import to keyring
3974 ref.mailvelope_keyring.importPublicKey(armored).then(function(status) {
3975 if (status === 'REJECTED') {
3976 // alert(ref.get_label('Key import was rejected'));
3977 }
3978 else {
3979 var $key = keyid.substr(-8).toUpperCase();
3980 btn.closest('.key').fadeOut();
3981 ref.display_message(ref.get_label('keyimportsuccess').replace('$key', $key), 'confirmation');
3982 }
3983 }, function(err) {
3984 console.log(err);
3985 });
3986 });
3987 });
3988
3989 };
3990
3991
3992 /*********************************************************/
3993 /********* mailbox folders methods *********/
3994 /*********************************************************/
3995
3996 this.expunge_mailbox = function(mbox)
3997 {
3998 var lock, post_data = {_mbox: mbox};
3999
4000 // lock interface if it's the active mailbox
4001 if (mbox == this.env.mailbox) {
4002 lock = this.set_busy(true, 'loading');
4003 post_data._reload = 1;
4004 if (this.env.search_request)
4005 post_data._search = this.env.search_request;
4006 }
4007
4008 // send request to server
4009 this.http_post('expunge', post_data, lock);
4010 };
4011
4012 this.purge_mailbox = function(mbox)
4013 {
4014 var lock, post_data = {_mbox: mbox};
4015
4016 if (!confirm(this.get_label('purgefolderconfirm')))
4017 return false;
4018
4019 // lock interface if it's the active mailbox
4020 if (mbox == this.env.mailbox) {
4021 lock = this.set_busy(true, 'loading');
4022 post_data._reload = 1;
4023 }
4024
4025 // send request to server
4026 this.http_post('purge', post_data, lock);
4027 };
4028
4029 // test if purge command is allowed
4030 this.purge_mailbox_test = function()
4031 {
4032 return (this.env.exists && (
4033 this.env.mailbox == this.env.trash_mailbox
4034 || this.env.mailbox == this.env.junk_mailbox
4035 || this.env.mailbox.startsWith(this.env.trash_mailbox + this.env.delimiter)
4036 || this.env.mailbox.startsWith(this.env.junk_mailbox + this.env.delimiter)
4037 ));
4038 };
4039
4040 // Mark all messages as read in:
4041 // - selected folder (mode=cur)
4042 // - selected folder and its subfolders (mode=sub)
4043 // - all folders (mode=all)
4044 this.mark_all_read = function(mbox, mode)
4045 {
4046 var state, content, nodes = [],
4047 list = this.message_list,
4048 folder = mbox || this.env.mailbox,
4049 post_data = {_uid: '*', _flag: 'read', _mbox: folder, _folders: mode};
4050
4051 if (typeof mode != 'string') {
4052 state = this.mark_all_read_state(folder);
4053 if (!state)
4054 return;
4055
4056 if (state > 1) {
4057 // build content of the dialog
4058 $.each({cur: 1, sub: 2, all: 4}, function(i, v) {
4059 var label = $('<label>').attr('style', 'display:block; line-height:22px'),
4060 text = $('<span>').text(ref.get_label('folders-' + i)),
4061 input = $('<input>').attr({type: 'radio', value: i, name: 'mode'});
4062
4063 if (!(state & v)) {
4064 label.attr('class', 'disabled');
4065 input.attr('disabled', true);
4066 }
4067
4068 nodes.push(label.append(input).append(text));
4069 });
4070
4071 content = $('<div>').append(nodes);
4072 $('input:not([disabled]):first', content).attr('checked', true);
4073
4074 this.show_popup_dialog(content, this.get_label('markallread'),
4075 [{
4076 'class': 'mainaction',
4077 text: this.get_label('mark'),
4078 click: function() {
4079 ref.mark_all_read(folder, $('input:checked', this).val());
4080 $(this).dialog('close');
4081 }
4082 },
4083 {
4084 text: this.get_label('cancel'),
4085 click: function() {
4086 $(this).dialog('close');
4087 }
4088 }]
4089 );
4090
4091 return;
4092 }
4093
4094 post_data._folders = 'cur'; // only current folder has unread messages
4095 }
4096
4097 // mark messages on the list
4098 $.each(list ? list.rows : [], function(uid, row) {
4099 if (!row.unread)
4100 return;
4101
4102 var mbox = ref.env.messages[uid].mbox;
4103 if (mode == 'all' || mbox == ref.env.mailbox
4104 || (mode == 'sub' && mbox.startsWith(ref.env.mailbox + ref.env.delimiter))
4105 ) {
4106 ref.set_message(uid, 'unread', false);
4107 }
4108 });
4109
4110 // send the request
4111 this.http_post('mark', post_data, this.display_message(this.get_label('markingmessage'), 'loading'));
4112 };
4113
4114 // Enable/disable mark-all-read action depending on folders state
4115 this.mark_all_read_state = function(mbox)
4116 {
4117 var state = 0,
4118 li = this.treelist.get_item(mbox || this.env.mailbox),
4119 folder_item = $(li).is('.unread') ? 1 : 0,
4120 subfolder_items = $('li.unread', li).length,
4121 all_items = $('li.unread', ref.gui_objects.folderlist).length;
4122
4123 state += folder_item;
4124 state += subfolder_items ? 2 : 0;
4125 state += all_items > folder_item + subfolder_items ? 4 : 0;
4126
4127 this.enable_command('mark-all-read', state > 0);
4128
4129 return state;
4130 };
4131
4132
4133 /*********************************************************/
4134 /********* login form methods *********/
4135 /*********************************************************/
4136
4137 // handler for keyboard events on the _user field
4138 this.login_user_keyup = function(e)
4139 {
4140 var key = rcube_event.get_keycode(e),
4141 passwd = $('#rcmloginpwd');
4142
4143 // enter
4144 if (key == 13 && passwd.length && !passwd.val()) {
4145 passwd.focus();
4146 return rcube_event.cancel(e);
4147 }
4148
4149 return true;
4150 };
4151
4152
4153 /*********************************************************/
4154 /********* message compose methods *********/
4155 /*********************************************************/
4156
4157 this.open_compose_step = function(p)
4158 {
4159 var url = this.url('mail/compose', p);
4160
4161 // open new compose window
4162 if (this.env.compose_extwin && !this.env.extwin) {
4163 this.open_window(url);
4164 }
4165 else {
4166 this.redirect(url);
4167 if (this.env.extwin)
4168 window.resizeTo(Math.max(this.env.popup_width, $(window).width()), $(window).height() + 24);
4169 }
4170 };
4171
4172 // init message compose form: set focus and eventhandlers
4173 this.init_messageform = function()
4174 {
4175 if (!this.gui_objects.messageform)
4176 return false;
4177
4178 var i, elem, pos, input_from = $("[name='_from']"),
4179 input_to = $("[name='_to']"),
4180 input_subject = $("input[name='_subject']"),
4181 input_message = $("[name='_message']").get(0),
4182 html_mode = $("input[name='_is_html']").val() == '1',
4183 ac_fields = ['cc', 'bcc', 'replyto', 'followupto'],
4184 ac_props, opener_rc = this.opener();
4185
4186 // close compose step in opener
4187 if (opener_rc && opener_rc.env.action == 'compose') {
4188 setTimeout(function(){
4189 if (opener.history.length > 1)
4190 opener.history.back();
4191 else
4192 opener_rc.redirect(opener_rc.get_task_url('mail'));
4193 }, 100);
4194 this.env.opened_extwin = true;
4195 }
4196
4197 // configure parallel autocompletion
4198 if (this.env.autocomplete_threads > 0) {
4199 ac_props = {
4200 threads: this.env.autocomplete_threads,
4201 sources: this.env.autocomplete_sources
4202 };
4203 }
4204
4205 // init live search events
4206 this.init_address_input_events(input_to, ac_props);
4207 for (i in ac_fields) {
4208 this.init_address_input_events($("[name='_"+ac_fields[i]+"']"), ac_props);
4209 }
4210
4211 if (!html_mode) {
4212 pos = this.env.top_posting && this.env.compose_mode ? 0 : input_message.value.length;
4213
4214 // add signature according to selected identity
4215 // if we have HTML editor, signature is added in a callback
4216 if (input_from.prop('type') == 'select-one') {
4217 // for some reason the caret initially is not at pos=0 in Firefox 51 (#5628)
4218 this.set_caret_pos(input_message, 0);
4219 this.change_identity(input_from[0]);
4220 }
4221
4222 // set initial cursor position
4223 this.set_caret_pos(input_message, pos);
4224
4225 // scroll to the bottom of the textarea (#1490114)
4226 if (pos) {
4227 $(input_message).scrollTop(input_message.scrollHeight);
4228 }
4229 }
4230
4231 // check for locally stored compose data
4232 if (this.env.save_localstorage)
4233 this.compose_restore_dialog(0, html_mode)
4234
4235 if (input_to.val() == '')
4236 elem = input_to;
4237 else if (input_subject.val() == '')
4238 elem = input_subject;
4239 else if (input_message)
4240 elem = input_message;
4241
4242 // focus first empty element (need to be visible on IE8)
4243 this.env.compose_focus_elem = $(elem).filter(':visible').focus().get(0);
4244
4245 // get summary of all field values
4246 this.compose_field_hash(true);
4247
4248 // start the auto-save timer
4249 this.auto_save_start();
4250 };
4251
4252 this.compose_restore_dialog = function(j, html_mode)
4253 {
4254 var i, key, formdata, index = this.local_storage_get_item('compose.index', []);
4255
4256 var show_next = function(i) {
4257 if (++i < index.length)
4258 ref.compose_restore_dialog(i, html_mode)
4259 }
4260
4261 for (i = j || 0; i < index.length; i++) {
4262 key = index[i];
4263 formdata = this.local_storage_get_item('compose.' + key, null, true);
4264 if (!formdata) {
4265 continue;
4266 }
4267 // restore saved copy of current compose_id
4268 if (formdata.changed && key == this.env.compose_id) {
4269 this.restore_compose_form(key, html_mode);
4270 break;
4271 }
4272 // skip records from 'other' drafts
4273 if (this.env.draft_id && formdata.draft_id && formdata.draft_id != this.env.draft_id) {
4274 continue;
4275 }
4276 // skip records on reply
4277 if (this.env.reply_msgid && formdata.reply_msgid != this.env.reply_msgid) {
4278 continue;
4279 }
4280 // show dialog asking to restore the message
4281 if (formdata.changed && formdata.session != this.env.session_id) {
4282 this.show_popup_dialog(
4283 this.get_label('restoresavedcomposedata')
4284 .replace('$date', new Date(formdata.changed).toLocaleString())
4285 .replace('$subject', formdata._subject)
4286 .replace(/\n/g, '<br/>'),
4287 this.get_label('restoremessage'),
4288 [{
4289 text: this.get_label('restore'),
4290 'class': 'mainaction',
4291 click: function(){
4292 ref.restore_compose_form(key, html_mode);
4293 ref.remove_compose_data(key); // remove old copy
4294 ref.save_compose_form_local(); // save under current compose_id
4295 $(this).dialog('close');
4296 }
4297 },
4298 {
4299 text: this.get_label('delete'),
4300 'class': 'delete',
4301 click: function(){
4302 ref.remove_compose_data(key);
4303 $(this).dialog('close');
4304 show_next(i);
4305 }
4306 },
4307 {
4308 text: this.get_label('ignore'),
4309 click: function(){
4310 $(this).dialog('close');
4311 show_next(i);
4312 }
4313 }]
4314 );
4315 break;
4316 }
4317 }
4318 }
4319
4320 this.init_address_input_events = function(obj, props)
4321 {
4322 this.env.recipients_delimiter = this.env.recipients_separator + ' ';
4323
4324 obj.keydown(function(e) { return ref.ksearch_keydown(e, this, props); })
4325 .attr({ 'autocomplete': 'off', 'aria-autocomplete': 'list', 'aria-expanded': 'false', 'role': 'combobox' });
4326
4327 // hide the popup on any click
4328 $(document).on('click', function() { ref.ksearch_hide(); });
4329 };
4330
4331 this.submit_messageform = function(draft, saveonly)
4332 {
4333 var form = this.gui_objects.messageform;
4334
4335 if (!form)
4336 return;
4337
4338 // the message has been sent but not saved, ask the user what to do
4339 if (!saveonly && this.env.is_sent) {
4340 return this.simple_dialog(this.get_label('messageissent'), '',
4341 function() {
4342 ref.submit_messageform(false, true);
4343 return true;
4344 }
4345 );
4346 }
4347
4348 // delegate sending to Mailvelope routine
4349 if (this.mailvelope_editor) {
4350 return this.mailvelope_submit_messageform(draft, saveonly);
4351 }
4352
4353 // all checks passed, send message
4354 var msgid = this.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage'),
4355 lang = this.spellcheck_lang(),
4356 files = [];
4357
4358 // send files list
4359 $('li', this.gui_objects.attachmentlist).each(function() { files.push(this.id.replace(/^rcmfile/, '')); });
4360 $('input[name="_attachments"]', form).val(files.join());
4361
4362 form.target = 'savetarget';
4363 form._draft.value = draft ? '1' : '';
4364 form.action = this.add_url(form.action, '_unlock', msgid);
4365 form.action = this.add_url(form.action, '_lang', lang);
4366 form.action = this.add_url(form.action, '_framed', 1);
4367
4368 if (saveonly) {
4369 form.action = this.add_url(form.action, '_saveonly', 1);
4370 }
4371
4372 // register timer to notify about connection timeout
4373 this.submit_timer = setTimeout(function(){
4374 ref.set_busy(false, null, msgid);
4375 ref.display_message(ref.get_label('requesttimedout'), 'error');
4376 }, this.env.request_timeout * 1000);
4377
4378 form.submit();
4379 };
4380
4381 this.compose_recipient_select = function(list)
4382 {
4383 var id, n, recipients = 0;
4384 for (n=0; n < list.selection.length; n++) {
4385 id = list.selection[n];
4386 if (this.env.contactdata[id])
4387 recipients++;
4388 }
4389 this.enable_command('add-recipient', recipients);
4390 };
4391
4392 this.compose_add_recipient = function(field)
4393 {
4394 // find last focused field name
4395 if (!field) {
4396 field = $(this.env.focused_field).filter(':visible');
4397 field = field.length ? field.attr('id').replace('_', '') : 'to';
4398 }
4399
4400 var recipients = [], input = $('#_'+field), delim = this.env.recipients_delimiter;
4401
4402 if (this.contact_list && this.contact_list.selection.length) {
4403 for (var id, n=0; n < this.contact_list.selection.length; n++) {
4404 id = this.contact_list.selection[n];
4405 if (id && this.env.contactdata[id]) {
4406 recipients.push(this.env.contactdata[id]);
4407
4408 // group is added, expand it
4409 if (id.charAt(0) == 'E' && this.env.contactdata[id].indexOf('@') < 0 && input.length) {
4410 var gid = id.substr(1);
4411 this.group2expand[gid] = { name:this.env.contactdata[id], input:input.get(0) };
4412 this.http_request('group-expand', {_source: this.env.source, _gid: gid}, false);
4413 }
4414 }
4415 }
4416 }
4417
4418 if (recipients.length && input.length) {
4419 var oldval = input.val(), rx = new RegExp(RegExp.escape(delim) + '\\s*$');
4420 if (oldval && !rx.test(oldval))
4421 oldval += delim + ' ';
4422 input.val(oldval + recipients.join(delim + ' ') + delim + ' ').change();
4423 this.triggerEvent('add-recipient', { field:field, recipients:recipients });
4424 }
4425
4426 return recipients.length;
4427 };
4428
4429 // checks the input fields before sending a message
4430 this.check_compose_input = function(cmd)
4431 {
4432 // check input fields
4433 var key, recipients, dialog,
4434 limit = this.env.max_disclosed_recipients,
4435 input_to = $("[name='_to']"),
4436 input_cc = $("[name='_cc']"),
4437 input_bcc = $("[name='_bcc']"),
4438 input_from = $("[name='_from']"),
4439 input_subject = $("[name='_subject']"),
4440 get_recipients = function(fields) {
4441 fields = $.map(fields, function(v) {
4442 v = $.trim(v.val());
4443 return v.length ? v : null;
4444 });
4445 return fields.join(',').replace(/^[\s,;]+/, '').replace(/[\s,;]+$/, '');
4446 };
4447
4448 // check sender (if have no identities)
4449 if (input_from.prop('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
4450 alert(this.get_label('nosenderwarning'));
4451 input_from.focus();
4452 return false;
4453 }
4454
4455 // check for empty recipient
4456 if (!rcube_check_email(get_recipients([input_to, input_cc, input_bcc]), true)) {
4457 alert(this.get_label('norecipientwarning'));
4458 input_to.focus();
4459 return false;
4460 }
4461
4462 // check if all files has been uploaded
4463 for (key in this.env.attachments) {
4464 if (typeof this.env.attachments[key] === 'object' && !this.env.attachments[key].complete) {
4465 alert(this.get_label('notuploadedwarning'));
4466 return false;
4467 }
4468 }
4469
4470 // check disclosed recipients limit
4471 if (limit && !this.env.disclosed_recipients_warned
4472 && rcube_check_email(recipients = get_recipients([input_to, input_cc]), true, true) > limit
4473 ) {
4474 var save_func = function(move_to_bcc) {
4475 if (move_to_bcc) {
4476 var bcc = input_bcc.val();
4477 input_bcc.val((bcc ? (bcc + ', ') : '') + recipients).change();
4478 input_to.val('').change();
4479 input_cc.val('').change();
4480 }
4481
4482 dialog.dialog('close');
4483 if (ref.check_compose_input(cmd))
4484 ref.command(cmd, { nocheck:true }); // repeat command which triggered this
4485 };
4486
4487 dialog = this.show_popup_dialog(
4488 this.get_label('disclosedrecipwarning'),
4489 this.get_label('disclosedreciptitle'),
4490 [{
4491 text: this.get_label('sendmessage'),
4492 click: function() { save_func(false); },
4493 'class': 'mainaction'
4494 }, {
4495 text: this.get_label('bccinstead'),
4496 click: function() { save_func(true); }
4497 }, {
4498 text: this.get_label('cancel'),
4499 click: function() { dialog.dialog('close'); }
4500 }],
4501 {dialogClass: 'warning'}
4502 );
4503
4504 this.env.disclosed_recipients_warned = true;
4505 return false;
4506 }
4507
4508 // display localized warning for missing subject
4509 if (!this.env.nosubject_warned && input_subject.val() == '') {
4510 var prompt_value = $('<input>').attr({type: 'text', size: 40}),
4511 myprompt = $('<div class="prompt">')
4512 .append($('<div class="message">').text(this.get_label('nosubjectwarning')))
4513 .append(prompt_value),
4514 save_func = function() {
4515 input_subject.val(prompt_value.val());
4516 dialog.dialog('close');
4517 if (ref.check_compose_input(cmd))
4518 ref.command(cmd, { nocheck:true }); // repeat command which triggered this
4519 };
4520
4521 dialog = this.show_popup_dialog(
4522 myprompt,
4523 this.get_label('nosubjecttitle'),
4524 [{
4525 text: this.get_label('sendmessage'),
4526 click: function() { save_func(); },
4527 'class': 'mainaction'
4528 }, {
4529 text: this.get_label('cancel'),
4530 click: function() {
4531 input_subject.focus();
4532 dialog.dialog('close');
4533 }
4534 }],
4535 {dialogClass: 'warning'}
4536 );
4537
4538 prompt_value.select().keydown(function(e) {
4539 if (e.which == 13) save_func();
4540 });
4541
4542 this.env.nosubject_warned = true;
4543 return false;
4544 }
4545
4546 // check for empty body (only possible if not mailvelope encrypted)
4547 if (!this.mailvelope_editor && !this.editor.get_content() && !confirm(this.get_label('nobodywarning'))) {
4548 this.editor.focus();
4549 return false;
4550 }
4551
4552 // move body from html editor to textarea (just to be sure, #1485860)
4553 this.editor.save();
4554
4555 return true;
4556 };
4557
4558 this.toggle_editor = function(props, obj, e)
4559 {
4560 // @todo: this should work also with many editors on page
4561 var result = this.editor.toggle(props.html, props.noconvert || false);
4562
4563 // satisfy the expectations of aftertoggle-editor event subscribers
4564 props.mode = props.html ? 'html' : 'plain';
4565
4566 if (!result && e) {
4567 // fix selector value if operation failed
4568 props.mode = props.html ? 'plain' : 'html';
4569 $(e.target).filter('select').val(props.mode);
4570 }
4571
4572 if (result) {
4573 // update internal format flag
4574 $("input[name='_is_html']").val(props.html ? 1 : 0);
4575 }
4576
4577 return result;
4578 };
4579
4580 // Inserts a predefined response to the compose editor
4581 this.insert_response = function(key)
4582 {
4583 return this.editor.replace(this.env.textresponses[key]);
4584 };
4585
4586 /**
4587 * Open the dialog to save a new canned response
4588 */
4589 this.save_response = function()
4590 {
4591 // show dialog to enter a name and to modify the text to be saved
4592 var buttons = {}, text = this.editor.get_content({selection: true, format: 'text', nosig: true}),
4593 html = '<form class="propform">' +
4594 '<div class="prop block"><label>' + this.get_label('responsename') + '</label>' +
4595 '<input type="text" name="name" id="ffresponsename" size="40" /></div>' +
4596 '<div class="prop block"><label>' + this.get_label('responsetext') + '</label>' +
4597 '<textarea name="text" id="ffresponsetext" cols="40" rows="8"></textarea></div>' +
4598 '</form>';
4599
4600 buttons[this.get_label('save')] = function(e) {
4601 var name = $('#ffresponsename').val(),
4602 text = $('#ffresponsetext').val();
4603
4604 if (!text) {
4605 $('#ffresponsetext').select();
4606 return false;
4607 }
4608 if (!name)
4609 name = text.replace(/[\r\n]+/g, ' ').substring(0,40);
4610
4611 var lock = ref.display_message(ref.get_label('savingresponse'), 'loading');
4612 ref.http_post('settings/responses', { _insert:1, _name:name, _text:text }, lock);
4613 $(this).dialog('close');
4614 };
4615
4616 buttons[this.get_label('cancel')] = function() {
4617 $(this).dialog('close');
4618 };
4619
4620 this.show_popup_dialog(html, this.get_label('newresponse'), buttons, {button_classes: ['mainaction']});
4621
4622 $('#ffresponsetext').val(text);
4623 $('#ffresponsename').select();
4624 };
4625
4626 this.add_response_item = function(response)
4627 {
4628 var key = response.key;
4629 this.env.textresponses[key] = response;
4630
4631 // append to responses list
4632 if (this.gui_objects.responseslist) {
4633 var li = $('<li>').appendTo(this.gui_objects.responseslist);
4634 $('<a>').addClass('insertresponse active')
4635 .attr('href', '#')
4636 .attr('rel', key)
4637 .attr('tabindex', '0')
4638 .html(this.quote_html(response.name))
4639 .appendTo(li)
4640 .mousedown(function(e) {
4641 return rcube_event.cancel(e);
4642 })
4643 .on('mouseup keypress', function(e) {
4644 if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
4645 ref.command('insert-response', $(this).attr('rel'));
4646 $(document.body).trigger('mouseup'); // hides the menu
4647 return rcube_event.cancel(e);
4648 }
4649 });
4650 }
4651 };
4652
4653 this.edit_responses = function()
4654 {
4655 // TODO: implement inline editing of responses
4656 };
4657
4658 this.delete_response = function(key)
4659 {
4660 if (!key && this.responses_list) {
4661 var selection = this.responses_list.get_selection();
4662 key = selection[0];
4663 }
4664
4665 // submit delete request
4666 if (key && confirm(this.get_label('deleteresponseconfirm'))) {
4667 this.http_post('settings/delete-response', { _key: key }, false);
4668 }
4669 };
4670
4671 // updates spellchecker buttons on state change
4672 this.spellcheck_state = function()
4673 {
4674 var active = this.editor.spellcheck_state();
4675
4676 $.each(this.buttons.spellcheck || [], function(i, v) {
4677 $('#' + v.id)[active ? 'addClass' : 'removeClass']('selected');
4678 });
4679
4680 return active;
4681 };
4682
4683 // get selected language
4684 this.spellcheck_lang = function()
4685 {
4686 return this.editor.get_language();
4687 };
4688
4689 this.spellcheck_lang_set = function(lang)
4690 {
4691 this.editor.set_language(lang);
4692 };
4693
4694 // resume spellchecking, highlight provided mispellings without new ajax request
4695 this.spellcheck_resume = function(data)
4696 {
4697 this.editor.spellcheck_resume(data);
4698 };
4699
4700 this.set_draft_id = function(id)
4701 {
4702 if (id && id != this.env.draft_id) {
4703 var filter = {task: 'mail', action: ''},
4704 rc = this.opener(false, filter) || this.opener(true, filter);
4705
4706 // refresh the drafts folder in the opener window
4707 if (rc && rc.env.mailbox == this.env.drafts_mailbox)
4708 rc.command('checkmail');
4709
4710 this.env.draft_id = id;
4711 $("input[name='_draft_saveid']").val(id);
4712
4713 // reset history of hidden iframe used for saving draft (#1489643)
4714 // but don't do this on timer-triggered draft-autosaving (#1489789)
4715 if (window.frames['savetarget'] && window.frames['savetarget'].history && !this.draft_autosave_submit && !this.mailvelope_editor) {
4716 window.frames['savetarget'].history.back();
4717 }
4718
4719 this.draft_autosave_submit = false;
4720 }
4721
4722 // always remove local copy upon saving as draft
4723 this.remove_compose_data(this.env.compose_id);
4724 this.compose_skip_unsavedcheck = false;
4725 };
4726
4727 this.auto_save_start = function()
4728 {
4729 if (this.env.draft_autosave) {
4730 this.draft_autosave_submit = false;
4731 this.save_timer = setTimeout(function(){
4732 ref.draft_autosave_submit = true; // set auto-saved flag (#1489789)
4733 ref.command("savedraft");
4734 }, this.env.draft_autosave * 1000);
4735 }
4736
4737 // save compose form content to local storage every 5 seconds
4738 if (!this.local_save_timer && window.localStorage && this.env.save_localstorage) {
4739 // track typing activity and only save on changes
4740 this.compose_type_activity = this.compose_type_activity_last = 0;
4741 $(document).keypress(function(e) { ref.compose_type_activity++; });
4742
4743 this.local_save_timer = setInterval(function(){
4744 if (ref.compose_type_activity > ref.compose_type_activity_last) {
4745 ref.save_compose_form_local();
4746 ref.compose_type_activity_last = ref.compose_type_activity;
4747 }
4748 }, 5000);
4749
4750 $(window).on('unload', function() {
4751 // remove copy from local storage if compose screen is left after warning
4752 if (!ref.env.server_error)
4753 ref.remove_compose_data(ref.env.compose_id);
4754 });
4755 }
4756
4757 // check for unsaved changes before leaving the compose page
4758 if (!window.onbeforeunload) {
4759 window.onbeforeunload = function() {
4760 if (!ref.compose_skip_unsavedcheck && ref.cmp_hash != ref.compose_field_hash()) {
4761 return ref.get_label('notsentwarning');
4762 }
4763 };
4764 }
4765
4766 // Unlock interface now that saving is complete
4767 this.busy = false;
4768 };
4769
4770 this.compose_field_hash = function(save)
4771 {
4772 // check input fields
4773 var i, id, val, str = '', hash_fields = ['to', 'cc', 'bcc', 'subject'];
4774
4775 for (i=0; i<hash_fields.length; i++)
4776 if (val = $('[name="_' + hash_fields[i] + '"]').val())
4777 str += val + ':';
4778
4779 str += this.editor.get_content({refresh: false});
4780
4781 if (this.env.attachments)
4782 for (id in this.env.attachments)
4783 str += id;
4784
4785 // we can't detect changes in the Mailvelope editor so assume it changed
4786 if (this.mailvelope_editor) {
4787 str += ';' + new Date().getTime();
4788 }
4789
4790 if (save)
4791 this.cmp_hash = str;
4792
4793 return str;
4794 };
4795
4796 // store the contents of the compose form to localstorage
4797 this.save_compose_form_local = function()
4798 {
4799 // feature is disabled
4800 if (!this.env.save_localstorage)
4801 return;
4802
4803 var formdata = { session:this.env.session_id, changed:new Date().getTime() },
4804 ed, empty = true;
4805
4806 // get fresh content from editor
4807 this.editor.save();
4808
4809 if (this.env.draft_id) {
4810 formdata.draft_id = this.env.draft_id;
4811 }
4812 if (this.env.reply_msgid) {
4813 formdata.reply_msgid = this.env.reply_msgid;
4814 }
4815
4816 $('input, select, textarea', this.gui_objects.messageform).each(function(i, elem) {
4817 switch (elem.tagName.toLowerCase()) {
4818 case 'input':
4819 if (elem.type == 'button' || elem.type == 'submit' || (elem.type == 'hidden' && elem.name != '_is_html')) {
4820 break;
4821 }
4822 formdata[elem.name] = elem.type != 'checkbox' || elem.checked ? $(elem).val() : '';
4823
4824 if (formdata[elem.name] != '' && elem.type != 'hidden')
4825 empty = false;
4826 break;
4827
4828 case 'select':
4829 formdata[elem.name] = $('option:checked', elem).val();
4830 break;
4831
4832 default:
4833 formdata[elem.name] = $(elem).val();
4834 if (formdata[elem.name] != '')
4835 empty = false;
4836 }
4837 });
4838
4839 if (!empty) {
4840 var index = this.local_storage_get_item('compose.index', []),
4841 key = this.env.compose_id;
4842
4843 if ($.inArray(key, index) < 0) {
4844 index.push(key);
4845 }
4846
4847 this.local_storage_set_item('compose.' + key, formdata, true);
4848 this.local_storage_set_item('compose.index', index);
4849 }
4850 };
4851
4852 // write stored compose data back to form
4853 this.restore_compose_form = function(key, html_mode)
4854 {
4855 var ed, formdata = this.local_storage_get_item('compose.' + key, true);
4856
4857 if (formdata && typeof formdata == 'object') {
4858 $.each(formdata, function(k, value) {
4859 if (k[0] == '_') {
4860 var elem = $("*[name='"+k+"']");
4861 if (elem[0] && elem[0].type == 'checkbox') {
4862 elem.prop('checked', value != '');
4863 }
4864 else {
4865 elem.val(value);
4866 }
4867 }
4868 });
4869
4870 // initialize HTML editor
4871 if ((formdata._is_html == '1' && !html_mode) || (formdata._is_html != '1' && html_mode)) {
4872 this.command('toggle-editor', {id: this.env.composebody, html: !html_mode, noconvert: true});
4873 }
4874 }
4875 };
4876
4877 // remove stored compose data from localStorage
4878 this.remove_compose_data = function(key)
4879 {
4880 var index = this.local_storage_get_item('compose.index', []);
4881
4882 if ($.inArray(key, index) >= 0) {
4883 this.local_storage_remove_item('compose.' + key);
4884 this.local_storage_set_item('compose.index', $.grep(index, function(val,i) { return val != key; }));
4885 }
4886 };
4887
4888 // clear all stored compose data of this user
4889 this.clear_compose_data = function()
4890 {
4891 var i, index = this.local_storage_get_item('compose.index', []);
4892
4893 for (i=0; i < index.length; i++) {
4894 this.local_storage_remove_item('compose.' + index[i]);
4895 }
4896
4897 this.local_storage_remove_item('compose.index');
4898 };
4899
4900 this.change_identity = function(obj, show_sig)
4901 {
4902 if (!obj || !obj.options)
4903 return false;
4904
4905 if (!show_sig)
4906 show_sig = this.env.show_sig;
4907
4908 var id = obj.options[obj.selectedIndex].value,
4909 sig = this.env.identity,
4910 delim = this.env.recipients_separator,
4911 rx_delim = RegExp.escape(delim);
4912
4913 // enable manual signature insert
4914 if (this.env.signatures && this.env.signatures[id]) {
4915 this.enable_command('insert-sig', true);
4916 this.env.compose_commands.push('insert-sig');
4917 }
4918 else
4919 this.enable_command('insert-sig', false);
4920
4921 // first function execution
4922 if (!this.env.identities_initialized) {
4923 this.env.identities_initialized = true;
4924 if (this.env.show_sig_later)
4925 this.env.show_sig = true;
4926 if (this.env.opened_extwin)
4927 return;
4928 }
4929
4930 // update reply-to/bcc fields with addresses defined in identities
4931 $.each(['replyto', 'bcc'], function() {
4932 var rx, key = this,
4933 old_val = sig && ref.env.identities[sig] ? ref.env.identities[sig][key] : '',
4934 new_val = id && ref.env.identities[id] ? ref.env.identities[id][key] : '',
4935 input = $('[name="_'+key+'"]'), input_val = input.val();
4936
4937 // remove old address(es)
4938 if (old_val && input_val) {
4939 rx = new RegExp('\\s*' + RegExp.escape(old_val) + '\\s*');
4940 input_val = input_val.replace(rx, '');
4941 }
4942
4943 // cleanup
4944 rx = new RegExp(rx_delim + '\\s*' + rx_delim, 'g');
4945 input_val = String(input_val).replace(rx, delim);
4946 rx = new RegExp('^[\\s' + rx_delim + ']+');
4947 input_val = input_val.replace(rx, '');
4948
4949 // add new address(es)
4950 if (new_val && input_val.indexOf(new_val) == -1 && input_val.indexOf(new_val.replace(/"/g, '')) == -1) {
4951 if (input_val) {
4952 rx = new RegExp('[' + rx_delim + '\\s]+$')
4953 input_val = input_val.replace(rx, '') + delim + ' ';
4954 }
4955
4956 input_val += new_val + delim + ' ';
4957 }
4958
4959 if (old_val || new_val)
4960 input.val(input_val).change();
4961 });
4962
4963 this.editor.change_signature(id, show_sig);
4964 this.env.identity = id;
4965 this.triggerEvent('change_identity');
4966 return true;
4967 };
4968
4969 // Open file selection dialog for defined upload form
4970 // Works only on click and only with smart-upload forms
4971 this.upload_input = function(name)
4972 {
4973 $('#' + name + ' input[type="file"]').click();
4974 };
4975
4976 // upload (attachment) file
4977 this.upload_file = function(form, action, lock)
4978 {
4979 if (!form)
4980 return;
4981
4982 // count files and size on capable browser
4983 var size = 0, numfiles = 0;
4984
4985 $.each($(form).get(0).elements || [], function() {
4986 if (this.type != 'file')
4987 return;
4988
4989 var i, files = this.files ? this.files.length : (this.value ? 1 : 0);
4990
4991 // check file size
4992 if (this.files) {
4993 for (i=0; i < files; i++)
4994 size += this.files[i].size;
4995 }
4996
4997 numfiles += files;
4998 });
4999
5000 // create hidden iframe and post upload form
5001 if (numfiles) {
5002 if (this.env.max_filesize && this.env.filesizeerror && size > this.env.max_filesize) {
5003 this.display_message(this.env.filesizeerror, 'error');
5004 return false;
5005 }
5006
5007 if (this.env.max_filecount && this.env.filecounterror && numfiles > this.env.max_filecount) {
5008 this.display_message(this.env.filecounterror, 'error');
5009 return false;
5010 }
5011
5012 var frame_name = this.async_upload_form(form, action || 'upload', function(e) {
5013 var d, content = '';
5014 try {
5015 if (this.contentDocument) {
5016 d = this.contentDocument;
5017 } else if (this.contentWindow) {
5018 d = this.contentWindow.document;
5019 }
5020 content = d.childNodes[1].innerHTML;
5021 } catch (err) {}
5022
5023 if (!content.match(/add2attachment/) && (!bw.opera || (ref.env.uploadframe && ref.env.uploadframe == e.data.ts))) {
5024 if (!content.match(/display_message/))
5025 ref.display_message(ref.get_label('fileuploaderror'), 'error');
5026 ref.remove_from_attachment_list(e.data.ts);
5027
5028 if (lock)
5029 ref.set_busy(false, null, lock);
5030 }
5031 // Opera hack: handle double onload
5032 if (bw.opera)
5033 ref.env.uploadframe = e.data.ts;
5034 });
5035
5036 // display upload indicator and cancel button
5037 var content = '<span>' + this.get_label('uploading' + (numfiles > 1 ? 'many' : '')) + '</span>',
5038 ts = frame_name.replace(/^rcmupload/, '');
5039
5040 this.add2attachment_list(ts, { name:'', html:content, classname:'uploading', frame:frame_name, complete:false });
5041
5042 // upload progress support
5043 if (this.env.upload_progress_time) {
5044 this.upload_progress_start('upload', ts);
5045 }
5046
5047 // set reference to the form object
5048 this.gui_objects.attachmentform = form;
5049 return true;
5050 }
5051 };
5052
5053 // add file name to attachment list
5054 // called from upload page
5055 this.add2attachment_list = function(name, att, upload_id)
5056 {
5057 if (upload_id)
5058 this.triggerEvent('fileuploaded', {name: name, attachment: att, id: upload_id});
5059
5060 if (!this.env.attachments)
5061 this.env.attachments = {};
5062
5063 if (upload_id && this.env.attachments[upload_id])
5064 delete this.env.attachments[upload_id];
5065
5066 this.env.attachments[name] = att;
5067
5068 if (!this.gui_objects.attachmentlist)
5069 return false;
5070
5071 if (!att.complete && this.env.loadingicon)
5072 att.html = '<img src="'+this.env.loadingicon+'" alt="" class="uploading" />' + att.html;
5073
5074 if (!att.complete && att.frame)
5075 att.html = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\', \''+att.frame+'\');" href="#cancelupload" class="cancelupload">'
5076 + (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="'+this.get_label('cancel')+'" />' : this.get_label('cancel')) + '</a>' + att.html;
5077
5078 var indicator, li = $('<li>');
5079
5080 li.attr('id', name)
5081 .addClass(att.classname)
5082 .html(att.html)
5083 .on('mouseover', function() { rcube_webmail.long_subject_title_ex(this); });
5084
5085 // replace indicator's li
5086 if (upload_id && (indicator = document.getElementById(upload_id))) {
5087 li.replaceAll(indicator);
5088 }
5089 else { // add new li
5090 li.appendTo(this.gui_objects.attachmentlist);
5091 }
5092
5093 // set tabindex attribute
5094 var tabindex = $(this.gui_objects.attachmentlist).attr('data-tabindex') || '0';
5095 li.find('a').attr('tabindex', tabindex);
5096
5097 this.triggerEvent('fileappended', {name: name, attachment: att, id: upload_id, item: li});
5098
5099 return true;
5100 };
5101
5102 this.remove_from_attachment_list = function(name)
5103 {
5104 if (this.env.attachments) {
5105 delete this.env.attachments[name];
5106 $('#'+name).remove();
5107 }
5108 };
5109
5110 this.remove_attachment = function(name)
5111 {
5112 if (name && this.env.attachments[name])
5113 this.http_post('remove-attachment', { _id:this.env.compose_id, _file:name });
5114
5115 return false;
5116 };
5117
5118 this.cancel_attachment_upload = function(name, frame_name)
5119 {
5120 if (!name || !frame_name)
5121 return false;
5122
5123 this.remove_from_attachment_list(name);
5124 $("iframe[name='"+frame_name+"']").remove();
5125 return false;
5126 };
5127
5128 this.upload_progress_start = function(action, name)
5129 {
5130 setTimeout(function() { ref.http_request(action, {_progress: name}); },
5131 this.env.upload_progress_time * 1000);
5132 };
5133
5134 this.upload_progress_update = function(param)
5135 {
5136 var elem = $('#'+param.name + ' > span');
5137
5138 if (!elem.length || !param.text)
5139 return;
5140
5141 elem.text(param.text);
5142
5143 if (!param.done)
5144 this.upload_progress_start(param.action, param.name);
5145 };
5146
5147 // rename uploaded attachment (in compose)
5148 this.rename_attachment = function(id)
5149 {
5150 var attachment = this.env.attachments ? this.env.attachments[id] : null;
5151
5152 if (!attachment)
5153 return;
5154
5155 var input = $('<input>').attr({type: 'text', size: 50}).val(attachment.name),
5156 content = $('<label>').text(this.get_label('namex')).append(input);
5157
5158 this.simple_dialog(content, 'attachmentrename', function() {
5159 var name;
5160 if ((name = input.val()) && name != attachment.name) {
5161 ref.http_post('rename-attachment', {_id: ref.env.compose_id, _file: id, _name: name},
5162 ref.set_busy(true, 'loading'));
5163 return true;
5164 }
5165 },
5166 {open: function() { input.select(); }}
5167 );
5168 };
5169
5170 // update attachments list with the new name
5171 this.rename_attachment_handler = function(id, name)
5172 {
5173 var attachment = this.env.attachments ? this.env.attachments[id] : null,
5174 item = $('#' + id + ' > a.filename'),
5175 link = $('<a>');
5176
5177 if (!attachment || !name)
5178 return;
5179
5180 attachment.name = name;
5181
5182 // update attachments list
5183 if (item.length == 1) {
5184 // create a new element with new attachment name and cloned size
5185 link.text(name).append($('span', item).clone());
5186 // update attachment name element
5187 item.html(link.html());
5188 // reset parent's title which may contain the old name
5189 item.parent().attr('title', '');
5190 }
5191 };
5192
5193 // send remote request to add a new contact
5194 this.add_contact = function(value)
5195 {
5196 if (value)
5197 this.http_post('addcontact', {_address: value});
5198
5199 return true;
5200 };
5201
5202 // send remote request to search mail or contacts
5203 this.qsearch = function(value)
5204 {
5205 // Note: Some plugins would like to do search without value,
5206 // so we keep value != '' check to allow that use-case. Which means
5207 // e.g. that qsearch() with no argument will execute the search.
5208 if (value != '' || $(this.gui_objects.qsearchbox).val() || $(this.gui_objects.search_interval).val()) {
5209 var r, lock = this.set_busy(true, 'searching'),
5210 url = this.search_params(value),
5211 action = this.env.action == 'compose' && this.contact_list ? 'search-contacts' : 'search';
5212
5213 if (this.message_list)
5214 this.clear_message_list();
5215 else if (this.contact_list)
5216 this.list_contacts_clear();
5217
5218 if (this.env.source)
5219 url._source = this.env.source;
5220 if (this.env.group)
5221 url._gid = this.env.group;
5222
5223 // reset vars
5224 this.env.current_page = 1;
5225
5226 r = this.http_request(action, url, lock);
5227
5228 this.env.qsearch = {lock: lock, request: r};
5229 this.enable_command('set-listmode', this.env.threads && (this.env.search_scope || 'base') == 'base');
5230
5231 return true;
5232 }
5233
5234 return false;
5235 };
5236
5237 this.continue_search = function(request_id)
5238 {
5239 var lock = this.set_busy(true, 'stillsearching');
5240
5241 setTimeout(function() {
5242 var url = ref.search_params();
5243 url._continue = request_id;
5244 ref.env.qsearch = { lock: lock, request: ref.http_request('search', url, lock) };
5245 }, 100);
5246 };
5247
5248 // build URL params for search
5249 this.search_params = function(search, filter)
5250 {
5251 var n, url = {}, mods_arr = [],
5252 mods = this.env.search_mods,
5253 scope = this.env.search_scope || 'base',
5254 mbox = scope == 'all' ? '*' : this.env.mailbox;
5255
5256 if (!filter && this.gui_objects.search_filter)
5257 filter = this.gui_objects.search_filter.value;
5258
5259 if (!search && this.gui_objects.qsearchbox)
5260 search = this.gui_objects.qsearchbox.value;
5261
5262 if (filter)
5263 url._filter = filter;
5264
5265 if (this.gui_objects.search_interval)
5266 url._interval = $(this.gui_objects.search_interval).val();
5267
5268 if (search) {
5269 url._q = search;
5270
5271 if (mods && this.message_list)
5272 mods = mods[mbox] || mods['*'];
5273
5274 if (mods) {
5275 for (n in mods)
5276 mods_arr.push(n);
5277 url._headers = mods_arr.join(',');
5278 }
5279 }
5280
5281 if (scope)
5282 url._scope = scope;
5283 if (mbox && scope != 'all')
5284 url._mbox = mbox;
5285
5286 return url;
5287 };
5288
5289 // reset search filter
5290 this.reset_search_filter = function()
5291 {
5292 this.filter_disabled = true;
5293 if (this.gui_objects.search_filter)
5294 $(this.gui_objects.search_filter).val('ALL').change();
5295 this.filter_disabled = false;
5296 };
5297
5298 // reset quick-search form
5299 this.reset_qsearch = function(all)
5300 {
5301 if (this.gui_objects.qsearchbox)
5302 this.gui_objects.qsearchbox.value = '';
5303
5304 if (this.gui_objects.search_interval)
5305 $(this.gui_objects.search_interval).val('');
5306
5307 if (this.env.qsearch)
5308 this.abort_request(this.env.qsearch);
5309
5310 if (all) {
5311 this.env.search_scope = 'base';
5312 this.reset_search_filter();
5313 }
5314
5315 this.env.qsearch = null;
5316 this.env.search_request = null;
5317 this.env.search_id = null;
5318 this.select_all_mode = false;
5319
5320 this.enable_command('set-listmode', this.env.threads);
5321 };
5322
5323 this.set_searchscope = function(scope)
5324 {
5325 var old = this.env.search_scope;
5326 this.env.search_scope = scope;
5327
5328 // re-send search query with new scope
5329 if (scope != old && this.env.search_request) {
5330 if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL')
5331 this.filter_mailbox(this.env.search_filter);
5332 if (scope != 'all')
5333 this.select_folder(this.env.mailbox, '', true);
5334 }
5335 };
5336
5337 this.set_searchinterval = function(interval)
5338 {
5339 var old = this.env.search_interval;
5340 this.env.search_interval = interval;
5341
5342 // re-send search query with new interval
5343 if (interval != old && this.env.search_request) {
5344 if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL')
5345 this.filter_mailbox(this.env.search_filter);
5346 if (interval)
5347 this.select_folder(this.env.mailbox, '', true);
5348 }
5349 };
5350
5351 this.set_searchmods = function(mods)
5352 {
5353 var mbox = this.env.mailbox,
5354 scope = this.env.search_scope || 'base';
5355
5356 if (scope == 'all')
5357 mbox = '*';
5358
5359 if (!this.env.search_mods)
5360 this.env.search_mods = {};
5361
5362 if (mbox)
5363 this.env.search_mods[mbox] = mods;
5364 };
5365
5366 this.is_multifolder_listing = function()
5367 {
5368 return this.env.multifolder_listing !== undefined ? this.env.multifolder_listing :
5369 (this.env.search_request && (this.env.search_scope || 'base') != 'base');
5370 };
5371
5372 // action executed after mail is sent
5373 this.sent_successfully = function(type, msg, folders, save_error)
5374 {
5375 this.display_message(msg, type);
5376 this.compose_skip_unsavedcheck = true;
5377
5378 if (this.env.extwin) {
5379 if (!save_error)
5380 this.lock_form(this.gui_objects.messageform);
5381
5382 var filter = {task: 'mail', action: ''},
5383 rc = this.opener(false, filter) || this.opener(true, filter);
5384
5385 if (rc) {
5386 rc.display_message(msg, type);
5387 // refresh the folder where sent message was saved or replied message comes from
5388 if (folders && $.inArray(rc.env.mailbox, folders) >= 0) {
5389 rc.command('checkmail');
5390 }
5391 }
5392
5393 if (!save_error)
5394 setTimeout(function() { window.close(); }, 1000);
5395 }
5396 else if (!save_error) {
5397 // before redirect we need to wait some time for Chrome (#1486177)
5398 setTimeout(function() { ref.list_mailbox(); }, 500);
5399 }
5400
5401 if (save_error)
5402 this.env.is_sent = true;
5403 };
5404
5405
5406 /*********************************************************/
5407 /********* keyboard live-search methods *********/
5408 /*********************************************************/
5409
5410 // handler for keyboard events on address-fields
5411 this.ksearch_keydown = function(e, obj, props)
5412 {
5413 if (this.ksearch_timer)
5414 clearTimeout(this.ksearch_timer);
5415
5416 var key = rcube_event.get_keycode(e);
5417
5418 switch (key) {
5419 case 38: // arrow up
5420 case 40: // arrow down
5421 if (!this.ksearch_visible())
5422 return;
5423
5424 var dir = key == 38 ? 1 : 0,
5425 highlight = document.getElementById('rcmkSearchItem' + this.ksearch_selected);
5426
5427 if (!highlight)
5428 highlight = this.ksearch_pane.__ul.firstChild;
5429
5430 if (highlight)
5431 this.ksearch_select(dir ? highlight.previousSibling : highlight.nextSibling);
5432
5433 return rcube_event.cancel(e);
5434
5435 case 9: // tab
5436 if (rcube_event.get_modifier(e) == SHIFT_KEY || !this.ksearch_visible()) {
5437 this.ksearch_hide();
5438 return;
5439 }
5440
5441 case 13: // enter
5442 if (!this.ksearch_visible())
5443 return false;
5444
5445 // insert selected address and hide ksearch pane
5446 this.insert_recipient(this.ksearch_selected);
5447 this.ksearch_hide();
5448
5449 // Don't cancel on Tab, we want to jump to the next field (#5659)
5450 return key == 9 ? null : rcube_event.cancel(e);
5451
5452 case 27: // escape
5453 this.ksearch_hide();
5454 return;
5455
5456 case 37: // left
5457 case 39: // right
5458 return;
5459 }
5460
5461 // start timer
5462 this.ksearch_timer = setTimeout(function(){ ref.ksearch_get_results(props); }, 200);
5463 this.ksearch_input = obj;
5464
5465 return true;
5466 };
5467
5468 this.ksearch_visible = function()
5469 {
5470 return this.ksearch_selected !== null && this.ksearch_selected !== undefined && this.ksearch_value;
5471 };
5472
5473 this.ksearch_select = function(node)
5474 {
5475 if (this.ksearch_pane && node) {
5476 this.ksearch_pane.find('li.selected').removeClass('selected').removeAttr('aria-selected');
5477 }
5478
5479 if (node) {
5480 $(node).addClass('selected').attr('aria-selected', 'true');
5481 this.ksearch_selected = node._rcm_id;
5482 $(this.ksearch_input).attr('aria-activedescendant', 'rcmkSearchItem' + this.ksearch_selected);
5483 }
5484 };
5485
5486 this.insert_recipient = function(id)
5487 {
5488 if (id === null || !this.env.contacts[id] || !this.ksearch_input)
5489 return;
5490
5491 // get cursor pos
5492 var inp_value = this.ksearch_input.value,
5493 cpos = this.get_caret_pos(this.ksearch_input),
5494 p = inp_value.lastIndexOf(this.ksearch_value, cpos),
5495 trigger = false,
5496 insert = '',
5497 // replace search string with full address
5498 pre = inp_value.substring(0, p),
5499 end = inp_value.substring(p+this.ksearch_value.length, inp_value.length);
5500
5501 this.ksearch_destroy();
5502
5503 // insert all members of a group
5504 if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].type == 'group' && !this.env.contacts[id].email) {
5505 insert += this.env.contacts[id].name + this.env.recipients_delimiter;
5506 this.group2expand[this.env.contacts[id].id] = $.extend({ input: this.ksearch_input }, this.env.contacts[id]);
5507 this.http_request('mail/group-expand', {_source: this.env.contacts[id].source, _gid: this.env.contacts[id].id}, false);
5508 }
5509 else if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].name) {
5510 insert = this.env.contacts[id].name + this.env.recipients_delimiter;
5511 trigger = true;
5512 }
5513 else if (typeof this.env.contacts[id] === 'string') {
5514 insert = this.env.contacts[id] + this.env.recipients_delimiter;
5515 trigger = true;
5516 }
5517
5518 this.ksearch_input.value = pre + insert + end;
5519
5520 // set caret to insert pos
5521 this.set_caret_pos(this.ksearch_input, p + insert.length);
5522
5523 if (trigger) {
5524 this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id], search:this.ksearch_value_last, result_type:'person' });
5525 this.ksearch_value_last = null;
5526 this.compose_type_activity++;
5527 }
5528 };
5529
5530 this.replace_group_recipients = function(id, recipients)
5531 {
5532 if (this.group2expand[id]) {
5533 this.group2expand[id].input.value = this.group2expand[id].input.value.replace(this.group2expand[id].name, recipients);
5534 this.triggerEvent('autocomplete_insert', { field:this.group2expand[id].input, insert:recipients, data:this.group2expand[id], search:this.ksearch_value_last, result_type:'group' });
5535 this.ksearch_value_last = null;
5536 this.group2expand[id] = null;
5537 this.compose_type_activity++;
5538 }
5539 };
5540
5541 // address search processor
5542 this.ksearch_get_results = function(props)
5543 {
5544 var inp_value = this.ksearch_input ? this.ksearch_input.value : null;
5545
5546 if (inp_value === null)
5547 return;
5548
5549 if (this.ksearch_pane && this.ksearch_pane.is(":visible"))
5550 this.ksearch_pane.hide();
5551
5552 // get string from current cursor pos to last comma
5553 var cpos = this.get_caret_pos(this.ksearch_input),
5554 p = inp_value.lastIndexOf(this.env.recipients_separator, cpos-1),
5555 q = inp_value.substring(p+1, cpos),
5556 min = this.env.autocomplete_min_length,
5557 data = this.ksearch_data;
5558
5559 // trim query string
5560 q = $.trim(q);
5561
5562 // Don't (re-)search if the last results are still active
5563 if (q == this.ksearch_value)
5564 return;
5565
5566 this.ksearch_destroy();
5567
5568 if (q.length && q.length < min) {
5569 if (!this.ksearch_info) {
5570 this.ksearch_info = this.display_message(
5571 this.get_label('autocompletechars').replace('$min', min));
5572 }
5573 return;
5574 }
5575
5576 var old_value = this.ksearch_value;
5577 this.ksearch_value = q;
5578 this.ksearch_value_last = q; // Group expansion clears ksearch_value before calling autocomplete_insert trigger, therefore store it in separate variable for later consumption.
5579
5580 // ...string is empty
5581 if (!q.length)
5582 return;
5583
5584 // ...new search value contains old one and previous search was not finished or its result was empty
5585 if (old_value && old_value.length && q.startsWith(old_value) && (!data || data.num <= 0) && this.env.contacts && !this.env.contacts.length)
5586 return;
5587
5588 var sources = props && props.sources ? props.sources : [''];
5589 var reqid = this.multi_thread_http_request({
5590 items: sources,
5591 threads: props && props.threads ? props.threads : 1,
5592 action: props && props.action ? props.action : 'mail/autocomplete',
5593 postdata: { _search:q, _source:'%s' },
5594 lock: this.display_message(this.get_label('searching'), 'loading')
5595 });
5596
5597 this.ksearch_data = { id:reqid, sources:sources.slice(), num:sources.length };
5598 };
5599
5600 this.ksearch_query_results = function(results, search, reqid)
5601 {
5602 // trigger multi-thread http response callback
5603 this.multi_thread_http_response(results, reqid);
5604
5605 // search stopped in meantime?
5606 if (!this.ksearch_value)
5607 return;
5608
5609 // ignore this outdated search response
5610 if (this.ksearch_input && search != this.ksearch_value)
5611 return;
5612
5613 // display search results
5614 var i, id, len, ul, text, type, init,
5615 value = this.ksearch_value,
5616 maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;
5617
5618 // create results pane if not present
5619 if (!this.ksearch_pane) {
5620 ul = $('<ul>');
5621 this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane').attr('role', 'listbox')
5622 .css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
5623 this.ksearch_pane.__ul = ul[0];
5624 }
5625
5626 ul = this.ksearch_pane.__ul;
5627
5628 // remove all search results or add to existing list if parallel search
5629 if (reqid && this.ksearch_pane.data('reqid') == reqid) {
5630 maxlen -= ul.childNodes.length;
5631 }
5632 else {
5633 this.ksearch_pane.data('reqid', reqid);
5634 init = 1;
5635 // reset content
5636 ul.innerHTML = '';
5637 this.env.contacts = [];
5638 // move the results pane right under the input box
5639 var pos = $(this.ksearch_input).offset();
5640 this.ksearch_pane.css({ left:pos.left+'px', top:(pos.top + this.ksearch_input.offsetHeight)+'px', display: 'none'});
5641 }
5642
5643 // add each result line to list
5644 if (results && (len = results.length)) {
5645 for (i=0; i < len && maxlen > 0; i++) {
5646 text = typeof results[i] === 'object' ? (results[i].display || results[i].name) : results[i];
5647 type = typeof results[i] === 'object' ? results[i].type : '';
5648 id = i + this.env.contacts.length;
5649 $('<li>').attr('id', 'rcmkSearchItem' + id)
5650 .attr('role', 'option')
5651 .html('<i class="icon"></i>' + this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>'))
5652 .addClass(type || '')
5653 .appendTo(ul)
5654 .mouseover(function() { ref.ksearch_select(this); })
5655 .mouseup(function() { ref.ksearch_click(this); })
5656 .get(0)._rcm_id = id;
5657 maxlen -= 1;
5658 }
5659 }
5660
5661 if (ul.childNodes.length) {
5662 // set the right aria-* attributes to the input field
5663 $(this.ksearch_input)
5664 .attr('aria-haspopup', 'true')
5665 .attr('aria-expanded', 'true')
5666 .attr('aria-owns', 'rcmKSearchpane');
5667
5668 this.ksearch_pane.show();
5669
5670 // select the first
5671 if (!this.env.contacts.length) {
5672 this.ksearch_select($('li:first', ul).get(0));
5673 }
5674 }
5675
5676 if (len)
5677 this.env.contacts = this.env.contacts.concat(results);
5678
5679 if (this.ksearch_data.id == reqid)
5680 this.ksearch_data.num--;
5681 };
5682
5683 this.ksearch_click = function(node)
5684 {
5685 if (this.ksearch_input)
5686 this.ksearch_input.focus();
5687
5688 this.insert_recipient(node._rcm_id);
5689 this.ksearch_hide();
5690 };
5691
5692 this.ksearch_blur = function()
5693 {
5694 if (this.ksearch_timer)
5695 clearTimeout(this.ksearch_timer);
5696
5697 this.ksearch_input = null;
5698 this.ksearch_hide();
5699 };
5700
5701 this.ksearch_hide = function()
5702 {
5703 this.ksearch_selected = null;
5704 this.ksearch_value = '';
5705
5706 if (this.ksearch_pane)
5707 this.ksearch_pane.hide();
5708
5709 $(this.ksearch_input)
5710 .attr('aria-haspopup', 'false')
5711 .attr('aria-expanded', 'false')
5712 .removeAttr('aria-activedescendant')
5713 .removeAttr('aria-owns');
5714
5715 this.ksearch_destroy();
5716 };
5717
5718 // Clears autocomplete data/requests
5719 this.ksearch_destroy = function()
5720 {
5721 if (this.ksearch_data)
5722 this.multi_thread_request_abort(this.ksearch_data.id);
5723
5724 if (this.ksearch_info)
5725 this.hide_message(this.ksearch_info);
5726
5727 if (this.ksearch_msg)
5728 this.hide_message(this.ksearch_msg);
5729
5730 this.ksearch_data = null;
5731 this.ksearch_info = null;
5732 this.ksearch_msg = null;
5733 };
5734
5735
5736 /*********************************************************/
5737 /********* address book methods *********/
5738 /*********************************************************/
5739
5740 this.contactlist_keypress = function(list)
5741 {
5742 if (list.key_pressed == list.DELETE_KEY)
5743 this.command('delete');
5744 };
5745
5746 this.contactlist_select = function(list)
5747 {
5748 if (this.preview_timer)
5749 clearTimeout(this.preview_timer);
5750
5751 var n, id, sid, contact, writable = false,
5752 selected = list.selection.length,
5753 source = this.env.source ? this.env.address_sources[this.env.source] : null;
5754
5755 // we don't have dblclick handler here, so use 50 instead of this.dblclick_time
5756 if (this.env.contentframe && !list.multi_selecting && (id = list.get_single_selection()))
5757 this.preview_timer = setTimeout(function() { ref.load_contact(id, 'show'); }, this.preview_delay_click);
5758 else if (this.env.contentframe)
5759 this.show_contentframe(false);
5760
5761 if (selected) {
5762 list.draggable = false;
5763
5764 // no source = search result, we'll need to detect if any of
5765 // selected contacts are in writable addressbook to enable edit/delete
5766 // we'll also need to know sources used in selection for copy
5767 // and group-addmember operations (drag&drop)
5768 this.env.selection_sources = [];
5769
5770 if (source) {
5771 this.env.selection_sources.push(this.env.source);
5772 }
5773
5774 for (n in list.selection) {
5775 contact = list.data[list.selection[n]];
5776 if (!source) {
5777 sid = String(list.selection[n]).replace(/^[^-]+-/, '');
5778 if (sid && this.env.address_sources[sid]) {
5779 writable = writable || (!this.env.address_sources[sid].readonly && !contact.readonly);
5780 this.env.selection_sources.push(sid);
5781 }
5782 }
5783 else {
5784 writable = writable || (!source.readonly && !contact.readonly);
5785 }
5786
5787 if (contact._type != 'group')
5788 list.draggable = true;
5789 }
5790
5791 this.env.selection_sources = $.unique(this.env.selection_sources);
5792 }
5793
5794 // if a group is currently selected, and there is at least one contact selected
5795 // thend we can enable the group-remove-selected command
5796 this.enable_command('group-remove-selected', this.env.group && selected && writable);
5797 this.enable_command('compose', this.env.group || selected);
5798 this.enable_command('print', selected == 1);
5799 this.enable_command('export-selected', 'copy', selected > 0);
5800 this.enable_command('edit', id && writable);
5801 this.enable_command('delete', 'move', selected && writable);
5802
5803 return false;
5804 };
5805
5806 this.list_contacts = function(src, group, page, search)
5807 {
5808 var win, folder, index = -1, url = {},
5809 refresh = src === undefined && group === undefined && page === undefined,
5810 target = window;
5811
5812 if (!src)
5813 src = this.env.source;
5814
5815 if (refresh)
5816 group = this.env.group;
5817
5818 if (src != this.env.source) {
5819 page = this.env.current_page = 1;
5820 this.reset_qsearch();
5821 }
5822 else if (!refresh && group != this.env.group)
5823 page = this.env.current_page = 1;
5824
5825 if (this.env.search_id)
5826 folder = 'S'+this.env.search_id;
5827 else if (!this.env.search_request)
5828 folder = group ? 'G'+src+group : src;
5829
5830 this.env.source = src;
5831 this.env.group = group;
5832
5833 // truncate groups listing stack
5834 $.each(this.env.address_group_stack, function(i, v) {
5835 if (ref.env.group == v.id) {
5836 index = i;
5837 return false;
5838 }
5839 });
5840
5841 this.env.address_group_stack = index < 0 ? [] : this.env.address_group_stack.slice(0, index);
5842
5843 // make sure the current group is on top of the stack
5844 if (this.env.group) {
5845 if (!search) search = {};
5846 search.id = this.env.group;
5847 this.env.address_group_stack.push(search);
5848
5849 // mark the first group on the stack as selected in the directory list
5850 folder = 'G'+src+this.env.address_group_stack[0].id;
5851 }
5852 else if (this.gui_objects.addresslist_title) {
5853 $(this.gui_objects.addresslist_title).text(this.get_label('contacts'));
5854 }
5855
5856 if (!this.env.search_id)
5857 this.select_folder(folder, '', true);
5858
5859 // load contacts remotely
5860 if (this.gui_objects.contactslist) {
5861 this.list_contacts_remote(src, group, page);
5862 return;
5863 }
5864
5865 if (win = this.get_frame_window(this.env.contentframe)) {
5866 target = win;
5867 url._framed = 1;
5868 }
5869
5870 if (group)
5871 url._gid = group;
5872 if (page)
5873 url._page = page;
5874 if (src)
5875 url._source = src;
5876
5877 // also send search request to get the correct listing
5878 if (this.env.search_request)
5879 url._search = this.env.search_request;
5880
5881 this.set_busy(true, 'loading');
5882 this.location_href(url, target);
5883 };
5884
5885 // send remote request to load contacts list
5886 this.list_contacts_remote = function(src, group, page)
5887 {
5888 // clear message list first
5889 this.list_contacts_clear();
5890
5891 // send request to server
5892 var url = {}, lock = this.set_busy(true, 'loading');
5893
5894 if (src)
5895 url._source = src;
5896 if (page)
5897 url._page = page;
5898 if (group)
5899 url._gid = group;
5900
5901 this.env.source = src;
5902 this.env.group = group;
5903
5904 // also send search request to get the right records
5905 if (this.env.search_request)
5906 url._search = this.env.search_request;
5907
5908 this.http_request(this.env.task == 'mail' ? 'list-contacts' : 'list', url, lock);
5909
5910 if (this.env.task != 'mail')
5911 this.update_state({_source: src, _page: page && page > 1 ? page : null, _gid: group});
5912 };
5913
5914 this.list_contacts_clear = function()
5915 {
5916 this.contact_list.data = {};
5917 this.contact_list.clear(true);
5918 this.show_contentframe(false);
5919 this.enable_command('delete', 'move', 'copy', 'print', false);
5920 this.enable_command('compose', this.env.group);
5921 };
5922
5923 this.set_group_prop = function(prop)
5924 {
5925 if (this.gui_objects.addresslist_title) {
5926 var boxtitle = $(this.gui_objects.addresslist_title).html(''); // clear contents
5927
5928 // add link to pop back to parent group
5929 if (this.env.address_group_stack.length > 1
5930 || (this.env.address_group_stack.length == 1 && this.env.address_group_stack[0].search_request)
5931 ) {
5932 $('<a href="#list">...</a>')
5933 .attr('title', this.get_label('uponelevel'))
5934 .addClass('poplink')
5935 .appendTo(boxtitle)
5936 .click(function(e){ return ref.command('popgroup','',this); });
5937 boxtitle.append('&nbsp;&raquo;&nbsp;');
5938 }
5939
5940 boxtitle.append($('<span>').text(prop ? prop.name : this.get_label('contacts')));
5941 }
5942 };
5943
5944 // load contact record
5945 this.load_contact = function(cid, action, framed)
5946 {
5947 var win, url = {}, target = window,
5948 rec = this.contact_list ? this.contact_list.data[cid] : null;
5949
5950 if (win = this.get_frame_window(this.env.contentframe)) {
5951 url._framed = 1;
5952 target = win;
5953 this.show_contentframe(true);
5954
5955 // load dummy content, unselect selected row(s)
5956 if (!cid)
5957 this.contact_list.clear_selection();
5958
5959 this.enable_command('compose', rec && rec.email);
5960 this.enable_command('export-selected', 'print', rec && rec._type != 'group');
5961 }
5962 else if (framed)
5963 return false;
5964
5965 if (action && (cid || action == 'add') && !this.drag_active) {
5966 if (this.env.group)
5967 url._gid = this.env.group;
5968
5969 if (this.env.search_request)
5970 url._search = this.env.search_request;
5971
5972 url._action = action;
5973 url._source = this.env.source;
5974 url._cid = cid;
5975
5976 this.location_href(url, target, true);
5977 }
5978
5979 return true;
5980 };
5981
5982 // add/delete member to/from the group
5983 this.group_member_change = function(what, cid, source, gid)
5984 {
5985 if (what != 'add')
5986 what = 'del';
5987
5988 var label = this.get_label(what == 'add' ? 'addingmember' : 'removingmember'),
5989 lock = this.display_message(label, 'loading'),
5990 post_data = {_cid: cid, _source: source, _gid: gid};
5991
5992 this.http_post('group-'+what+'members', post_data, lock);
5993 };
5994
5995 this.contacts_drag_menu = function(e, to)
5996 {
5997 var dest = to.type == 'group' ? to.source : to.id,
5998 source = this.env.source;
5999
6000 if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
6001 return true;
6002
6003 // search result may contain contacts from many sources, but if there is only one...
6004 if (source == '' && this.env.selection_sources.length == 1)
6005 source = this.env.selection_sources[0];
6006
6007 if (to.type == 'group' && dest == source) {
6008 var cid = this.contact_list.get_selection().join(',');
6009 this.group_member_change('add', cid, dest, to.id);
6010 return true;
6011 }
6012 // move action is not possible, "redirect" to copy if menu wasn't requested
6013 else if (!this.commands.move && rcube_event.get_modifier(e) != SHIFT_KEY) {
6014 this.copy_contacts(to);
6015 return true;
6016 }
6017
6018 return this.drag_menu(e, to);
6019 };
6020
6021 // copy contact(s) to the specified target (group or directory)
6022 this.copy_contacts = function(to)
6023 {
6024 var dest = to.type == 'group' ? to.source : to.id,
6025 source = this.env.source,
6026 group = this.env.group ? this.env.group : '',
6027 cid = this.contact_list.get_selection().join(',');
6028
6029 if (!cid || !this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
6030 return;
6031
6032 // search result may contain contacts from many sources, but if there is only one...
6033 if (source == '' && this.env.selection_sources.length == 1)
6034 source = this.env.selection_sources[0];
6035
6036 // tagret is a group
6037 if (to.type == 'group') {
6038 if (dest == source)
6039 return;
6040
6041 var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
6042 post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group};
6043
6044 this.http_post('copy', post_data, lock);
6045 }
6046 // target is an addressbook
6047 else if (to.id != source) {
6048 var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
6049 post_data = {_cid: cid, _source: this.env.source, _to: to.id, _gid: group};
6050
6051 this.http_post('copy', post_data, lock);
6052 }
6053 };
6054
6055 // move contact(s) to the specified target (group or directory)
6056 this.move_contacts = function(to)
6057 {
6058 var dest = to.type == 'group' ? to.source : to.id,
6059 source = this.env.source,
6060 group = this.env.group ? this.env.group : '';
6061
6062 if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
6063 return;
6064
6065 // search result may contain contacts from many sources, but if there is only one...
6066 if (source == '' && this.env.selection_sources.length == 1)
6067 source = this.env.selection_sources[0];
6068
6069 if (to.type == 'group') {
6070 if (dest == source)
6071 return;
6072
6073 this._with_selected_contacts('move', {_to: dest, _togid: to.id});
6074 }
6075 // target is an addressbook
6076 else if (to.id != source)
6077 this._with_selected_contacts('move', {_to: to.id});
6078 };
6079
6080 // delete contact(s)
6081 this.delete_contacts = function()
6082 {
6083 var undelete = this.env.source && this.env.address_sources[this.env.source].undelete;
6084
6085 if (!undelete && !confirm(this.get_label('deletecontactconfirm')))
6086 return;
6087
6088 return this._with_selected_contacts('delete');
6089 };
6090
6091 this._with_selected_contacts = function(action, post_data)
6092 {
6093 var selection = this.contact_list ? this.contact_list.get_selection() : [];
6094
6095 // exit if no contact specified or if selection is empty
6096 if (!selection.length && !this.env.cid)
6097 return;
6098
6099 var n, a_cids = [],
6100 label = action == 'delete' ? 'contactdeleting' : 'movingcontact',
6101 lock = this.display_message(this.get_label(label), 'loading');
6102
6103 if (this.env.cid)
6104 a_cids.push(this.env.cid);
6105 else {
6106 for (n=0; n<selection.length; n++) {
6107 id = selection[n];
6108 a_cids.push(id);
6109 this.contact_list.remove_row(id, (n == selection.length-1));
6110 }
6111
6112 // hide content frame if we delete the currently displayed contact
6113 if (selection.length == 1)
6114 this.show_contentframe(false);
6115 }
6116
6117 if (!post_data)
6118 post_data = {};
6119
6120 post_data._source = this.env.source;
6121 post_data._from = this.env.action;
6122 post_data._cid = a_cids.join(',');
6123
6124 if (this.env.group)
6125 post_data._gid = this.env.group;
6126
6127 // also send search request to get the right records from the next page
6128 if (this.env.search_request)
6129 post_data._search = this.env.search_request;
6130
6131 // send request to server
6132 this.http_post(action, post_data, lock)
6133
6134 return true;
6135 };
6136
6137 // update a contact record in the list
6138 this.update_contact_row = function(cid, cols_arr, newcid, source, data)
6139 {
6140 var list = this.contact_list;
6141
6142 cid = this.html_identifier(cid);
6143
6144 // when in searching mode, concat cid with the source name
6145 if (!list.rows[cid]) {
6146 cid = cid + '-' + source;
6147 if (newcid)
6148 newcid = newcid + '-' + source;
6149 }
6150
6151 list.update_row(cid, cols_arr, newcid, true);
6152 list.data[cid] = data;
6153 };
6154
6155 // add row to contacts list
6156 this.add_contact_row = function(cid, cols, classes, data)
6157 {
6158 if (!this.gui_objects.contactslist)
6159 return false;
6160
6161 var c, col, list = this.contact_list,
6162 row = { cols:[] };
6163
6164 row.id = 'rcmrow' + this.html_identifier(cid);
6165 row.className = 'contact ' + (classes || '');
6166
6167 if (list.in_selection(cid))
6168 row.className += ' selected';
6169
6170 // add each submitted col
6171 for (c in cols) {
6172 col = {};
6173 col.className = String(c).toLowerCase();
6174 col.innerHTML = cols[c];
6175 row.cols.push(col);
6176 }
6177
6178 // store data in list member
6179 list.data[cid] = data;
6180 list.insert_row(row);
6181
6182 this.enable_command('export', list.rowcount > 0);
6183 };
6184
6185 this.init_contact_form = function()
6186 {
6187 var col;
6188
6189 if (this.env.coltypes) {
6190 this.set_photo_actions($('#ff_photo').val());
6191 for (col in this.env.coltypes)
6192 this.init_edit_field(col, null);
6193 }
6194
6195 $('.contactfieldgroup .row a.deletebutton').click(function() {
6196 ref.delete_edit_field(this);
6197 return false;
6198 });
6199
6200 $('select.addfieldmenu').change(function() {
6201 ref.insert_edit_field($(this).val(), $(this).attr('rel'), this);
6202 this.selectedIndex = 0;
6203 });
6204
6205 // enable date pickers on date fields
6206 if ($.datepicker && this.env.date_format) {
6207 $.datepicker.setDefaults({
6208 dateFormat: this.env.date_format,
6209 changeMonth: true,
6210 changeYear: true,
6211 yearRange: '-120:+10',
6212 showOtherMonths: true,
6213 selectOtherMonths: true
6214 // onSelect: function(dateText) { $(this).focus().val(dateText); }
6215 });
6216 $('input.datepicker').datepicker();
6217 }
6218
6219 // Submit search form on Enter
6220 if (this.env.action == 'search')
6221 $(this.gui_objects.editform).append($('<input type="submit">').hide())
6222 .submit(function() { $('input.mainaction').click(); return false; });
6223 };
6224
6225 // group creation dialog
6226 this.group_create = function()
6227 {
6228 var input = $('<input>').attr('type', 'text'),
6229 content = $('<label>').text(this.get_label('namex')).append(input),
6230 source = this.env.source;
6231
6232 this.simple_dialog(content, 'newgroup',
6233 function() {
6234 var name;
6235 if (name = input.val()) {
6236 ref.http_post('group-create', {_source: source, _name: name},
6237 ref.set_busy(true, 'loading'));
6238 return true;
6239 }
6240 });
6241 };
6242
6243 // group rename dialog
6244 this.group_rename = function()
6245 {
6246 if (!this.env.group)
6247 return;
6248
6249 var group_name = this.env.contactgroups['G' + this.env.source + this.env.group].name,
6250 input = $('<input>').attr('type', 'text').val(group_name),
6251 content = $('<label>').text(this.get_label('namex')).append(input),
6252 source = this.env.source,
6253 group = this.env.group;
6254
6255 this.simple_dialog(content, 'grouprename',
6256 function() {
6257 var name;
6258 if ((name = input.val()) && name != group_name) {
6259 ref.http_post('group-rename', {_source: source, _gid: group, _name: name},
6260 ref.set_busy(true, 'loading'));
6261 return true;
6262 }
6263 },
6264 {open: function() { input.select(); }}
6265 );
6266 };
6267
6268 this.group_delete = function()
6269 {
6270 if (this.env.group && confirm(this.get_label('deletegroupconfirm'))) {
6271 var lock = this.set_busy(true, 'groupdeleting');
6272 this.http_post('group-delete', {_source: this.env.source, _gid: this.env.group}, lock);
6273 }
6274 };
6275
6276 // callback from server upon group-delete command
6277 this.remove_group_item = function(prop)
6278 {
6279 var key = 'G'+prop.source+prop.id;
6280
6281 if (this.treelist.remove(key)) {
6282 this.triggerEvent('group_delete', { source:prop.source, id:prop.id });
6283 delete this.env.contactfolders[key];
6284 delete this.env.contactgroups[key];
6285 }
6286
6287 if (prop.source == this.env.source && prop.id == this.env.group)
6288 this.list_contacts(prop.source, 0);
6289 };
6290
6291 //remove selected contacts from current active group
6292 this.group_remove_selected = function()
6293 {
6294 this.http_post('group-delmembers', {_cid: this.contact_list.selection,
6295 _source: this.env.source, _gid: this.env.group});
6296 };
6297
6298 //callback after deleting contact(s) from current group
6299 this.remove_group_contacts = function(props)
6300 {
6301 if (this.env.group !== undefined && (this.env.group === props.gid)) {
6302 var n, selection = this.contact_list.get_selection();
6303 for (n=0; n<selection.length; n++) {
6304 id = selection[n];
6305 this.contact_list.remove_row(id, (n == selection.length-1));
6306 }
6307 }
6308 };
6309
6310 // callback for creating a new contact group
6311 this.insert_contact_group = function(prop)
6312 {
6313 prop.type = 'group';
6314
6315 var key = 'G'+prop.source+prop.id,
6316 link = $('<a>').attr('href', '#')
6317 .attr('rel', prop.source+':'+prop.id)
6318 .click(function() { return ref.command('listgroup', prop, this); })
6319 .html(prop.name);
6320
6321 this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
6322 this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, 'contactgroup');
6323
6324 this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key) });
6325 };
6326
6327 // callback for renaming a contact group
6328 this.update_contact_group = function(prop)
6329 {
6330 var key = 'G'+prop.source+prop.id,
6331 newnode = {};
6332
6333 // group ID has changed, replace link node and identifiers
6334 if (prop.newid) {
6335 var newkey = 'G'+prop.source+prop.newid,
6336 newprop = $.extend({}, prop);
6337
6338 this.env.contactfolders[newkey] = this.env.contactfolders[key];
6339 this.env.contactfolders[newkey].id = prop.newid;
6340 this.env.group = prop.newid;
6341
6342 delete this.env.contactfolders[key];
6343 delete this.env.contactgroups[key];
6344
6345 newprop.id = prop.newid;
6346 newprop.type = 'group';
6347
6348 newnode.id = newkey;
6349 newnode.html = $('<a>').attr('href', '#')
6350 .attr('rel', prop.source+':'+prop.newid)
6351 .click(function() { return ref.command('listgroup', newprop, this); })
6352 .html(prop.name);
6353 }
6354 // update displayed group name
6355 else {
6356 $(this.treelist.get_item(key)).children().first().html(prop.name);
6357 this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
6358
6359 if (prop.source == this.env.source && prop.id == this.env.group)
6360 this.set_group_prop(prop);
6361 }
6362
6363 // update list node and re-sort it
6364 this.treelist.update(key, newnode, true);
6365
6366 this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key), newid:prop.newid });
6367 };
6368
6369 this.update_group_commands = function()
6370 {
6371 var source = this.env.source != '' ? this.env.address_sources[this.env.source] : null,
6372 supported = source && source.groups && !source.readonly;
6373
6374 this.enable_command('group-create', supported);
6375 this.enable_command('group-rename', 'group-delete', supported && this.env.group);
6376 };
6377
6378 this.init_edit_field = function(col, elem)
6379 {
6380 var label = this.env.coltypes[col].label;
6381
6382 if (!elem)
6383 elem = $('.ff_' + col);
6384
6385 if (label)
6386 elem.placeholder(label);
6387 };
6388
6389 this.insert_edit_field = function(col, section, menu)
6390 {
6391 // just make pre-defined input field visible
6392 var elem = $('#ff_'+col);
6393 if (elem.length) {
6394 elem.show().focus();
6395 $(menu).children('option[value="'+col+'"]').prop('disabled', true);
6396 }
6397 else {
6398 var lastelem = $('.ff_'+col),
6399 appendcontainer = $('#contactsection'+section+' .contactcontroller'+col);
6400
6401 if (!appendcontainer.length) {
6402 var sect = $('#contactsection'+section),
6403 lastgroup = $('.contactfieldgroup', sect).last();
6404 appendcontainer = $('<fieldset>').addClass('contactfieldgroup contactcontroller'+col);
6405 if (lastgroup.length)
6406 appendcontainer.insertAfter(lastgroup);
6407 else
6408 sect.prepend(appendcontainer);
6409 }
6410
6411 if (appendcontainer.length && appendcontainer.get(0).nodeName == 'FIELDSET') {
6412 var input, colprop = this.env.coltypes[col],
6413 input_id = 'ff_' + col + (colprop.count || 0),
6414 row = $('<div>').addClass('row'),
6415 cell = $('<div>').addClass('contactfieldcontent data'),
6416 label = $('<div>').addClass('contactfieldlabel label');
6417
6418 if (colprop.subtypes_select)
6419 label.html(colprop.subtypes_select);
6420 else
6421 label.html('<label for="' + input_id + '">' + colprop.label + '</label>');
6422
6423 var name_suffix = colprop.limit != 1 ? '[]' : '';
6424
6425 if (colprop.type == 'text' || colprop.type == 'date') {
6426 input = $('<input>')
6427 .addClass('ff_'+col)
6428 .attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size, id: input_id})
6429 .appendTo(cell);
6430
6431 this.init_edit_field(col, input);
6432
6433 if (colprop.type == 'date' && $.datepicker)
6434 input.datepicker();
6435 }
6436 else if (colprop.type == 'textarea') {
6437 input = $('<textarea>')
6438 .addClass('ff_'+col)
6439 .attr({ name: '_'+col+name_suffix, cols:colprop.size, rows:colprop.rows, id: input_id })
6440 .appendTo(cell);
6441
6442 this.init_edit_field(col, input);
6443 }
6444 else if (colprop.type == 'composite') {
6445 var i, childcol, cp, first, templ, cols = [], suffices = [];
6446
6447 // read template for composite field order
6448 if ((templ = this.env[col+'_template'])) {
6449 for (i=0; i < templ.length; i++) {
6450 cols.push(templ[i][1]);
6451 suffices.push(templ[i][2]);
6452 }
6453 }
6454 else { // list fields according to appearance in colprop
6455 for (childcol in colprop.childs)
6456 cols.push(childcol);
6457 }
6458
6459 for (i=0; i < cols.length; i++) {
6460 childcol = cols[i];
6461 cp = colprop.childs[childcol];
6462 input = $('<input>')
6463 .addClass('ff_'+childcol)
6464 .attr({ type: 'text', name: '_'+childcol+name_suffix, size: cp.size })
6465 .appendTo(cell);
6466 cell.append(suffices[i] || " ");
6467 this.init_edit_field(childcol, input);
6468 if (!first) first = input;
6469 }
6470 input = first; // set focus to the first of this composite fields
6471 }
6472 else if (colprop.type == 'select') {
6473 input = $('<select>')
6474 .addClass('ff_'+col)
6475 .attr({ 'name': '_'+col+name_suffix, id: input_id })
6476 .appendTo(cell);
6477
6478 var options = input.attr('options');
6479 options[options.length] = new Option('---', '');
6480 if (colprop.options)
6481 $.each(colprop.options, function(i, val){ options[options.length] = new Option(val, i); });
6482 }
6483
6484 if (input) {
6485 var delbutton = $('<a href="#del"></a>')
6486 .addClass('contactfieldbutton deletebutton')
6487 .attr({title: this.get_label('delete'), rel: col})
6488 .html(this.env.delbutton)
6489 .click(function(){ ref.delete_edit_field(this); return false })
6490 .appendTo(cell);
6491
6492 row.append(label).append(cell).appendTo(appendcontainer.show());
6493 input.first().focus();
6494
6495 // disable option if limit reached
6496 if (!colprop.count) colprop.count = 0;
6497 if (++colprop.count == colprop.limit && colprop.limit)
6498 $(menu).children('option[value="'+col+'"]').prop('disabled', true);
6499 }
6500 }
6501 }
6502 };
6503
6504 this.delete_edit_field = function(elem)
6505 {
6506 var col = $(elem).attr('rel'),
6507 colprop = this.env.coltypes[col],
6508 fieldset = $(elem).parents('fieldset.contactfieldgroup'),
6509 addmenu = fieldset.parent().find('select.addfieldmenu');
6510
6511 // just clear input but don't hide the last field
6512 if (--colprop.count <= 0 && colprop.visible)
6513 $(elem).parent().children('input').val('').blur();
6514 else {
6515 $(elem).parents('div.row').remove();
6516 // hide entire fieldset if no more rows
6517 if (!fieldset.children('div.row').length)
6518 fieldset.hide();
6519 }
6520
6521 // enable option in add-field selector or insert it if necessary
6522 if (addmenu.length) {
6523 var option = addmenu.children('option[value="'+col+'"]');
6524 if (option.length)
6525 option.prop('disabled', false);
6526 else
6527 option = $('<option>').attr('value', col).html(colprop.label).appendTo(addmenu);
6528 addmenu.show();
6529 }
6530 };
6531
6532 this.upload_contact_photo = function(form)
6533 {
6534 if (form && form.elements._photo.value) {
6535 this.async_upload_form(form, 'upload-photo', function(e) {
6536 ref.set_busy(false, null, ref.file_upload_id);
6537 });
6538
6539 // display upload indicator
6540 this.file_upload_id = this.set_busy(true, 'uploading');
6541 }
6542 };
6543
6544 this.replace_contact_photo = function(id)
6545 {
6546 var img_src = id == '-del-' ? this.env.photo_placeholder :
6547 this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + (this.env.cid || 0) + '&_photo=' + id;
6548
6549 this.set_photo_actions(id);
6550 $(this.gui_objects.contactphoto).children('img').attr('src', img_src);
6551 };
6552
6553 this.photo_upload_end = function()
6554 {
6555 this.set_busy(false, null, this.file_upload_id);
6556 delete this.file_upload_id;
6557 };
6558
6559 this.set_photo_actions = function(id)
6560 {
6561 var n, buttons = this.buttons['upload-photo'];
6562 for (n=0; buttons && n < buttons.length; n++)
6563 $('a#'+buttons[n].id).html(this.get_label(id == '-del-' ? 'addphoto' : 'replacephoto'));
6564
6565 $('#ff_photo').val(id);
6566 this.enable_command('upload-photo', this.env.coltypes.photo ? true : false);
6567 this.enable_command('delete-photo', this.env.coltypes.photo && id != '-del-');
6568 };
6569
6570 // load advanced search page
6571 this.advanced_search = function()
6572 {
6573 var win, url = {_form: 1, _action: 'search'}, target = window;
6574
6575 if (win = this.get_frame_window(this.env.contentframe)) {
6576 url._framed = 1;
6577 target = win;
6578 this.contact_list.clear_selection();
6579 }
6580
6581 this.location_href(url, target, true);
6582
6583 return true;
6584 };
6585
6586 // unselect directory/group
6587 this.unselect_directory = function()
6588 {
6589 this.select_folder('');
6590 this.enable_command('search-delete', false);
6591 };
6592
6593 // callback for creating a new saved search record
6594 this.insert_saved_search = function(name, id)
6595 {
6596 var key = 'S'+id,
6597 link = $('<a>').attr('href', '#')
6598 .attr('rel', id)
6599 .click(function() { return ref.command('listsearch', id, this); })
6600 .html(name),
6601 prop = { name:name, id:id };
6602
6603 this.savedsearchlist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch');
6604 this.select_folder(key,'',true);
6605 this.enable_command('search-delete', true);
6606 this.env.search_id = id;
6607
6608 this.triggerEvent('abook_search_insert', prop);
6609 };
6610
6611 // creates a dialog for saved search
6612 this.search_create = function()
6613 {
6614 var input = $('<input>').attr('type', 'text'),
6615 content = $('<label>').text(this.get_label('namex')).append(input);
6616
6617 this.simple_dialog(content, 'searchsave',
6618 function() {
6619 var name;
6620 if (name = input.val()) {
6621 ref.http_post('search-create', {_search: ref.env.search_request, _name: name},
6622 ref.set_busy(true, 'loading'));
6623 return true;
6624 }
6625 }
6626 );
6627 };
6628
6629 this.search_delete = function()
6630 {
6631 if (this.env.search_request) {
6632 var lock = this.set_busy(true, 'savedsearchdeleting');
6633 this.http_post('search-delete', {_sid: this.env.search_id}, lock);
6634 }
6635 };
6636
6637 // callback from server upon search-delete command
6638 this.remove_search_item = function(id)
6639 {
6640 var li, key = 'S'+id;
6641 if (this.savedsearchlist.remove(key)) {
6642 this.triggerEvent('search_delete', { id:id, li:li });
6643 }
6644
6645 this.env.search_id = null;
6646 this.env.search_request = null;
6647 this.list_contacts_clear();
6648 this.reset_qsearch();
6649 this.enable_command('search-delete', 'search-create', false);
6650 };
6651
6652 this.listsearch = function(id)
6653 {
6654 var lock = this.set_busy(true, 'searching');
6655
6656 if (this.contact_list) {
6657 this.list_contacts_clear();
6658 }
6659
6660 this.reset_qsearch();
6661
6662 if (this.savedsearchlist) {
6663 this.treelist.select('');
6664 this.savedsearchlist.select('S'+id);
6665 }
6666 else
6667 this.select_folder('S'+id, '', true);
6668
6669 // reset vars
6670 this.env.current_page = 1;
6671 this.http_request('search', {_sid: id}, lock);
6672 };
6673
6674 // display a dialog with QR code image
6675 this.qrcode = function()
6676 {
6677 var title = this.get_label('qrcode'),
6678 buttons = [{
6679 text: this.get_label('close'),
6680 'class': 'mainaction',
6681 click: function() {
6682 (ref.is_framed() ? parent.$ : $)(this).dialog('destroy');
6683 }
6684 }],
6685 img = new Image(300, 300);
6686
6687 img.src = this.url('addressbook/qrcode', {_source: this.env.source, _cid: this.env.cid});
6688
6689 return this.show_popup_dialog(img, title, buttons, {width: 310, height: 410});
6690 };
6691
6692
6693 /*********************************************************/
6694 /********* user settings methods *********/
6695 /*********************************************************/
6696
6697 // preferences section select and load options frame
6698 this.section_select = function(list)
6699 {
6700 var win, id = list.get_single_selection(), target = window,
6701 url = {_action: 'edit-prefs', _section: id};
6702
6703 if (id) {
6704 if (win = this.get_frame_window(this.env.contentframe)) {
6705 url._framed = 1;
6706 target = win;
6707 }
6708 this.location_href(url, target, true);
6709 }
6710
6711 return true;
6712 };
6713
6714 this.identity_select = function(list)
6715 {
6716 var id;
6717 if (id = list.get_single_selection()) {
6718 this.enable_command('delete', list.rowcount > 1 && this.env.identities_level < 2);
6719 this.load_identity(id, 'edit-identity');
6720 }
6721 };
6722
6723 // load identity record
6724 this.load_identity = function(id, action)
6725 {
6726 if (action == 'edit-identity' && (!id || id == this.env.iid))
6727 return false;
6728
6729 var win, target = window,
6730 url = {_action: action, _iid: id};
6731
6732 if (win = this.get_frame_window(this.env.contentframe)) {
6733 url._framed = 1;
6734 target = win;
6735 }
6736
6737 if (id || action == 'add-identity') {
6738 this.location_href(url, target, true);
6739 }
6740
6741 return true;
6742 };
6743
6744 this.delete_identity = function(id)
6745 {
6746 // exit if no identity is specified or if selection is empty
6747 var selection = this.identity_list.get_selection();
6748 if (!(selection.length || this.env.iid))
6749 return;
6750
6751 if (!id)
6752 id = this.env.iid ? this.env.iid : selection[0];
6753
6754 // submit request with appended token
6755 if (id && confirm(this.get_label('deleteidentityconfirm')))
6756 this.http_post('settings/delete-identity', { _iid: id }, true);
6757 };
6758
6759 this.update_identity_row = function(id, name, add)
6760 {
6761 var list = this.identity_list,
6762 rid = this.html_identifier(id);
6763
6764 if (add) {
6765 list.insert_row({ id:'rcmrow'+rid, cols:[ { className:'mail', innerHTML:name } ] });
6766 list.select(rid);
6767 }
6768 else {
6769 list.update_row(rid, [ name ]);
6770 }
6771 };
6772
6773 this.update_response_row = function(response, oldkey)
6774 {
6775 var list = this.responses_list;
6776
6777 if (list && oldkey) {
6778 list.update_row(oldkey, [ response.name ], response.key, true);
6779 }
6780 else if (list) {
6781 list.insert_row({ id:'rcmrow'+response.key, cols:[ { className:'name', innerHTML:response.name } ] });
6782 list.select(response.key);
6783 }
6784 };
6785
6786 this.remove_response = function(key)
6787 {
6788 var frame;
6789
6790 if (this.env.textresponses) {
6791 delete this.env.textresponses[key];
6792 }
6793
6794 if (this.responses_list) {
6795 this.responses_list.remove_row(key);
6796 if (frame = this.get_frame_window(this.env.contentframe)) {
6797 frame.location.href = this.env.blankpage;
6798 }
6799 }
6800
6801 this.enable_command('delete', false);
6802 };
6803
6804 this.remove_identity = function(id)
6805 {
6806 var frame, list = this.identity_list,
6807 rid = this.html_identifier(id);
6808
6809 if (list && id) {
6810 list.remove_row(rid);
6811 if (frame = this.get_frame_window(this.env.contentframe)) {
6812 frame.location.href = this.env.blankpage;
6813 }
6814 }
6815
6816 this.enable_command('delete', false);
6817 };
6818
6819
6820 /*********************************************************/
6821 /********* folder manager methods *********/
6822 /*********************************************************/
6823
6824 this.init_subscription_list = function()
6825 {
6826 var delim = RegExp.escape(this.env.delimiter);
6827
6828 this.last_sub_rx = RegExp('['+delim+']?[^'+delim+']+$');
6829
6830 this.subscription_list = new rcube_treelist_widget(this.gui_objects.subscriptionlist, {
6831 selectable: true,
6832 tabexit: false,
6833 parent_focus: true,
6834 id_prefix: 'rcmli',
6835 id_encode: this.html_identifier_encode,
6836 id_decode: this.html_identifier_decode,
6837 searchbox: '#foldersearch'
6838 });
6839
6840 this.subscription_list
6841 .addEventListener('select', function(node) { ref.subscription_select(node.id); })
6842 .addEventListener('collapse', function(node) { ref.folder_collapsed(node) })
6843 .addEventListener('expand', function(node) { ref.folder_collapsed(node) })
6844 .addEventListener('search', function(p) { if (p.query) ref.subscription_select(); })
6845 .draggable({cancel: 'li.mailbox.root,input,div.treetoggle'})
6846 .droppable({
6847 // @todo: find better way, accept callback is executed for every folder
6848 // on the list when dragging starts (and stops), this is slow, but
6849 // I didn't find a method to check droptarget on over event
6850 accept: function(node) {
6851 if (!$(node).is('.mailbox'))
6852 return false;
6853
6854 var source_folder = ref.folder_id2name($(node).attr('id')),
6855 dest_folder = ref.folder_id2name(this.id),
6856 source = ref.env.subscriptionrows[source_folder],
6857 dest = ref.env.subscriptionrows[dest_folder];
6858
6859 return source && !source[2]
6860 && dest_folder != source_folder.replace(ref.last_sub_rx, '')
6861 && !dest_folder.startsWith(source_folder + ref.env.delimiter);
6862 },
6863 drop: function(e, ui) {
6864 var source = ref.folder_id2name(ui.draggable.attr('id')),
6865 dest = ref.folder_id2name(this.id);
6866
6867 ref.subscription_move_folder(source, dest);
6868 }
6869 });
6870 };
6871
6872 this.folder_id2name = function(id)
6873 {
6874 return id ? ref.html_identifier_decode(id.replace(/^rcmli/, '')) : null;
6875 };
6876
6877 this.subscription_select = function(id)
6878 {
6879 var folder;
6880
6881 if (id && id != '*' && (folder = this.env.subscriptionrows[id])) {
6882 this.env.mailbox = id;
6883 this.show_folder(id);
6884 this.enable_command('delete-folder', !folder[2]);
6885 }
6886 else {
6887 this.env.mailbox = null;
6888 this.show_contentframe(false);
6889 this.enable_command('delete-folder', 'purge', false);
6890 }
6891 };
6892
6893 this.subscription_move_folder = function(from, to)
6894 {
6895 if (from && to !== null && from != to && to != from.replace(this.last_sub_rx, '')) {
6896 var path = from.split(this.env.delimiter),
6897 basename = path.pop(),
6898 newname = to === '' || to === '*' ? basename : to + this.env.delimiter + basename;
6899
6900 if (newname != from) {
6901 this.http_post('rename-folder', {_folder_oldname: from, _folder_newname: newname},
6902 this.set_busy(true, 'foldermoving'));
6903 }
6904 }
6905 };
6906
6907 // tell server to create and subscribe a new mailbox
6908 this.create_folder = function()
6909 {
6910 this.show_folder('', this.env.mailbox);
6911 };
6912
6913 // delete a specific mailbox with all its messages
6914 this.delete_folder = function(name)
6915 {
6916 if (!name)
6917 name = this.env.mailbox;
6918
6919 if (name && confirm(this.get_label('deletefolderconfirm'))) {
6920 this.http_post('delete-folder', {_mbox: name}, this.set_busy(true, 'folderdeleting'));
6921 }
6922 };
6923
6924 // Add folder row to the table and initialize it
6925 this.add_folder_row = function (id, name, display_name, is_protected, subscribed, class_name, refrow, subfolders)
6926 {
6927 if (!this.gui_objects.subscriptionlist)
6928 return false;
6929
6930 // reset searching
6931 if (this.subscription_list.is_search()) {
6932 this.subscription_select();
6933 this.subscription_list.reset_search();
6934 }
6935
6936 // disable drag-n-drop temporarily
6937 this.subscription_list.draggable('destroy').droppable('destroy');
6938
6939 var row, n, tmp, tmp_name, rowid, collator, pos, p, parent = '',
6940 folders = [], list = [], slist = [],
6941 list_element = $(this.gui_objects.subscriptionlist);
6942 row = refrow ? refrow : $($('li', list_element).get(1)).clone(true);
6943
6944 if (!row.length) {
6945 // Refresh page if we don't have a table row to clone
6946 this.goto_url('folders');
6947 return false;
6948 }
6949
6950 // set ID, reset css class
6951 row.attr({id: 'rcmli' + this.html_identifier_encode(id), 'class': class_name});
6952
6953 if (!refrow || !refrow.length) {
6954 // remove old data, subfolders and toggle
6955 $('ul,div.treetoggle', row).remove();
6956 row.removeData('filtered');
6957 }
6958
6959 // set folder name
6960 $('a:first', row).text(display_name);
6961
6962 // update subscription checkbox
6963 $('input[name="_subscribed[]"]:first', row).val(id)
6964 .prop({checked: subscribed ? true : false, disabled: is_protected ? true : false});
6965
6966 // add to folder/row-ID map
6967 this.env.subscriptionrows[id] = [name, display_name, false];
6968
6969 // copy folders data to an array for sorting
6970 $.each(this.env.subscriptionrows, function(k, v) { v[3] = k; folders.push(v); });
6971
6972 try {
6973 // use collator if supported (FF29, IE11, Opera15, Chrome24)
6974 collator = new Intl.Collator(this.env.locale.replace('_', '-'));
6975 }
6976 catch (e) {};
6977
6978 // sort folders
6979 folders.sort(function(a, b) {
6980 var i, f1, f2,
6981 path1 = a[0].split(ref.env.delimiter),
6982 path2 = b[0].split(ref.env.delimiter),
6983 len = path1.length;
6984
6985 for (i=0; i<len; i++) {
6986 f1 = path1[i];
6987 f2 = path2[i];
6988
6989 if (f1 !== f2) {
6990 if (f2 === undefined)
6991 return 1;
6992 if (collator)
6993 return collator.compare(f1, f2);
6994 else
6995 return f1 < f2 ? -1 : 1;
6996 }
6997 else if (i == len-1) {
6998 return -1
6999 }
7000 }
7001 });
7002
7003 for (n in folders) {
7004 p = folders[n][3];
7005 // protected folder
7006 if (folders[n][2]) {
7007 tmp_name = p + this.env.delimiter;
7008 // prefix namespace cannot have subfolders (#1488349)
7009 if (tmp_name == this.env.prefix_ns)
7010 continue;
7011 slist.push(p);
7012 tmp = tmp_name;
7013 }
7014 // protected folder's child
7015 else if (tmp && p.startsWith(tmp))
7016 slist.push(p);
7017 // other
7018 else {
7019 list.push(p);
7020 tmp = null;
7021 }
7022 }
7023
7024 // check if subfolder of a protected folder
7025 for (n=0; n<slist.length; n++) {
7026 if (id.startsWith(slist[n] + this.env.delimiter))
7027 rowid = slist[n];
7028 }
7029
7030 // find folder position after sorting
7031 for (n=0; !rowid && n<list.length; n++) {
7032 if (n && list[n] == id)
7033 rowid = list[n-1];
7034 }
7035
7036 // add row to the table
7037 if (rowid && (n = this.subscription_list.get_item(rowid, true))) {
7038 // find parent folder
7039 if (pos = id.lastIndexOf(this.env.delimiter)) {
7040 parent = id.substring(0, pos);
7041 parent = this.subscription_list.get_item(parent, true);
7042
7043 // add required tree elements to the parent if not already there
7044 if (!$('div.treetoggle', parent).length) {
7045 $('<div>&nbsp;</div>').addClass('treetoggle collapsed').appendTo(parent);
7046 }
7047 if (!$('ul', parent).length) {
7048 $('<ul>').css('display', 'none').appendTo(parent);
7049 }
7050 }
7051
7052 if (parent && n == parent) {
7053 $('ul:first', parent).append(row);
7054 }
7055 else {
7056 while (p = $(n).parent().parent().get(0)) {
7057 if (parent && p == parent)
7058 break;
7059 if (!$(p).is('li.mailbox'))
7060 break;
7061 n = p;
7062 }
7063
7064 $(n).after(row);
7065 }
7066 }
7067 else {
7068 list_element.append(row);
7069 }
7070
7071 // add subfolders
7072 $.extend(this.env.subscriptionrows, subfolders || {});
7073
7074 // update list widget
7075 this.subscription_list.reset(true);
7076 this.subscription_select();
7077
7078 // expand parent
7079 if (parent) {
7080 this.subscription_list.expand(this.folder_id2name(parent.id));
7081 }
7082
7083 row = row.show().get(0);
7084 if (row.scrollIntoView)
7085 row.scrollIntoView();
7086
7087 return row;
7088 };
7089
7090 // replace an existing table row with a new folder line (with subfolders)
7091 this.replace_folder_row = function(oldid, id, name, display_name, is_protected, class_name)
7092 {
7093 if (!this.gui_objects.subscriptionlist) {
7094 if (this.is_framed()) {
7095 // @FIXME: for some reason this 'parent' variable need to be prefixed with 'window.'
7096 return window.parent.rcmail.replace_folder_row(oldid, id, name, display_name, is_protected, class_name);
7097 }
7098
7099 return false;
7100 }
7101
7102 // reset searching
7103 if (this.subscription_list.is_search()) {
7104 this.subscription_select();
7105 this.subscription_list.reset_search();
7106 }
7107
7108 var subfolders = {},
7109 row = this.subscription_list.get_item(oldid, true),
7110 parent = $(row).parent(),
7111 old_folder = this.env.subscriptionrows[oldid],
7112 prefix_len_id = oldid.length,
7113 prefix_len_name = old_folder[0].length,
7114 subscribed = $('input[name="_subscribed[]"]:first', row).prop('checked');
7115
7116 // no renaming, only update class_name
7117 if (oldid == id) {
7118 $(row).attr('class', class_name || '');
7119 return;
7120 }
7121
7122 // update subfolders
7123 $('li', row).each(function() {
7124 var fname = ref.folder_id2name(this.id),
7125 folder = ref.env.subscriptionrows[fname],
7126 newid = id + fname.slice(prefix_len_id);
7127
7128 this.id = 'rcmli' + ref.html_identifier_encode(newid);
7129 $('input[name="_subscribed[]"]:first', this).val(newid);
7130 folder[0] = name + folder[0].slice(prefix_len_name);
7131
7132 subfolders[newid] = folder;
7133 delete ref.env.subscriptionrows[fname];
7134 });
7135
7136 // get row off the list
7137 row = $(row).detach();
7138
7139 delete this.env.subscriptionrows[oldid];
7140
7141 // remove parent list/toggle elements if not needed
7142 if (parent.get(0) != this.gui_objects.subscriptionlist && !$('li', parent).length) {
7143 $('ul,div.treetoggle', parent.parent()).remove();
7144 }
7145
7146 // move the existing table row
7147 this.add_folder_row(id, name, display_name, is_protected, subscribed, class_name, row, subfolders);
7148 };
7149
7150 // remove the table row of a specific mailbox from the table
7151 this.remove_folder_row = function(folder)
7152 {
7153 // reset searching
7154 if (this.subscription_list.is_search()) {
7155 this.subscription_select();
7156 this.subscription_list.reset_search();
7157 }
7158
7159 var list = [], row = this.subscription_list.get_item(folder, true);
7160
7161 // get subfolders if any
7162 $('li', row).each(function() { list.push(ref.folder_id2name(this.id)); });
7163
7164 // remove folder row (and subfolders)
7165 this.subscription_list.remove(folder);
7166
7167 // update local list variable
7168 list.push(folder);
7169 $.each(list, function(i, v) { delete ref.env.subscriptionrows[v]; });
7170 };
7171
7172 this.subscribe = function(folder)
7173 {
7174 if (folder) {
7175 var lock = this.display_message(this.get_label('foldersubscribing'), 'loading');
7176 this.http_post('subscribe', {_mbox: folder}, lock);
7177 }
7178 };
7179
7180 this.unsubscribe = function(folder)
7181 {
7182 if (folder) {
7183 var lock = this.display_message(this.get_label('folderunsubscribing'), 'loading');
7184 this.http_post('unsubscribe', {_mbox: folder}, lock);
7185 }
7186 };
7187
7188 // when user select a folder in manager
7189 this.show_folder = function(folder, path, force)
7190 {
7191 var win, target = window,
7192 url = '&_action=edit-folder&_mbox='+urlencode(folder);
7193
7194 if (path)
7195 url += '&_path='+urlencode(path);
7196
7197 if (win = this.get_frame_window(this.env.contentframe)) {
7198 target = win;
7199 url += '&_framed=1';
7200 }
7201
7202 if (String(target.location.href).indexOf(url) >= 0 && !force)
7203 this.show_contentframe(true);
7204 else
7205 this.location_href(this.env.comm_path+url, target, true);
7206 };
7207
7208 // disables subscription checkbox (for protected folder)
7209 this.disable_subscription = function(folder)
7210 {
7211 var row = this.subscription_list.get_item(folder, true);
7212 if (row)
7213 $('input[name="_subscribed[]"]:first', row).prop('disabled', true);
7214 };
7215
7216 // resets state of subscription checkbox (e.g. on error)
7217 this.reset_subscription = function(folder, state)
7218 {
7219 var row = this.subscription_list.get_item(folder, true);
7220 if (row)
7221 $('input[name="_subscribed[]"]:first', row).prop('checked', state);
7222 };
7223
7224 this.folder_size = function(folder)
7225 {
7226 var lock = this.set_busy(true, 'loading');
7227 this.http_post('folder-size', {_mbox: folder}, lock);
7228 };
7229
7230 this.folder_size_update = function(size)
7231 {
7232 $('#folder-size').replaceWith(size);
7233 };
7234
7235 // filter folders by namespace
7236 this.folder_filter = function(prefix)
7237 {
7238 this.subscription_list.reset_search();
7239
7240 this.subscription_list.container.children('li').each(function() {
7241 var i, folder = ref.folder_id2name(this.id);
7242 // show all folders
7243 if (prefix == '---') {
7244 }
7245 // got namespace prefix
7246 else if (prefix) {
7247 if (folder !== prefix) {
7248 $(this).data('filtered', true).hide();
7249 return
7250 }
7251 }
7252 // no namespace prefix, filter out all other namespaces
7253 else {
7254 // first get all namespace roots
7255 for (i in ref.env.ns_roots) {
7256 if (folder === ref.env.ns_roots[i]) {
7257 $(this).data('filtered', true).hide();
7258 return;
7259 }
7260 }
7261 }
7262
7263 $(this).removeData('filtered').show();
7264 });
7265 };
7266
7267 /*********************************************************/
7268 /********* GUI functionality *********/
7269 /*********************************************************/
7270
7271 var init_button = function(cmd, prop)
7272 {
7273 var elm = document.getElementById(prop.id);
7274 if (!elm)
7275 return;
7276
7277 var preload = false;
7278 if (prop.type == 'image') {
7279 elm = elm.parentNode;
7280 preload = true;
7281 }
7282
7283 elm._command = cmd;
7284 elm._id = prop.id;
7285 if (prop.sel) {
7286 elm.onmousedown = function(e) { return ref.button_sel(this._command, this._id); };
7287 elm.onmouseup = function(e) { return ref.button_out(this._command, this._id); };
7288 if (preload)
7289 new Image().src = prop.sel;
7290 }
7291 if (prop.over) {
7292 elm.onmouseover = function(e) { return ref.button_over(this._command, this._id); };
7293 elm.onmouseout = function(e) { return ref.button_out(this._command, this._id); };
7294 if (preload)
7295 new Image().src = prop.over;
7296 }
7297 };
7298
7299 // set event handlers on registered buttons
7300 this.init_buttons = function()
7301 {
7302 for (var cmd in this.buttons) {
7303 if (typeof cmd !== 'string')
7304 continue;
7305
7306 for (var i=0; i<this.buttons[cmd].length; i++) {
7307 init_button(cmd, this.buttons[cmd][i]);
7308 }
7309 }
7310 };
7311
7312 // set button to a specific state
7313 this.set_button = function(command, state)
7314 {
7315 var n, button, obj, $obj, a_buttons = this.buttons[command],
7316 len = a_buttons ? a_buttons.length : 0;
7317
7318 for (n=0; n<len; n++) {
7319 button = a_buttons[n];
7320 obj = document.getElementById(button.id);
7321
7322 if (!obj || button.status === state)
7323 continue;
7324
7325 // get default/passive setting of the button
7326 if (button.type == 'image' && !button.status) {
7327 button.pas = obj._original_src ? obj._original_src : obj.src;
7328 // respect PNG fix on IE browsers
7329 if (obj.runtimeStyle && obj.runtimeStyle.filter && obj.runtimeStyle.filter.match(/src=['"]([^'"]+)['"]/))
7330 button.pas = RegExp.$1;
7331 }
7332 else if (!button.status)
7333 button.pas = String(obj.className);
7334
7335 button.status = state;
7336
7337 // set image according to button state
7338 if (button.type == 'image' && button[state]) {
7339 obj.src = button[state];
7340 }
7341 // set class name according to button state
7342 else if (button[state] !== undefined) {
7343 obj.className = button[state];
7344 }
7345 // disable/enable input buttons
7346 if (button.type == 'input') {
7347 obj.disabled = state == 'pas';
7348 }
7349 else if (button.type == 'uibutton') {
7350 button.status = state;
7351 $(obj).button('option', 'disabled', state == 'pas');
7352 }
7353 else {
7354 $obj = $(obj);
7355 $obj
7356 .attr('tabindex', state == 'pas' || state == 'sel' ? '-1' : ($obj.attr('data-tabindex') || '0'))
7357 .attr('aria-disabled', state == 'pas' || state == 'sel' ? 'true' : 'false');
7358 }
7359 }
7360 };
7361
7362 // display a specific alttext
7363 this.set_alttext = function(command, label)
7364 {
7365 var n, button, obj, link, a_buttons = this.buttons[command],
7366 len = a_buttons ? a_buttons.length : 0;
7367
7368 for (n=0; n<len; n++) {
7369 button = a_buttons[n];
7370 obj = document.getElementById(button.id);
7371
7372 if (button.type == 'image' && obj) {
7373 obj.setAttribute('alt', this.get_label(label));
7374 if ((link = obj.parentNode) && link.tagName.toLowerCase() == 'a')
7375 link.setAttribute('title', this.get_label(label));
7376 }
7377 else if (obj)
7378 obj.setAttribute('title', this.get_label(label));
7379 }
7380 };
7381
7382 // mouse over button
7383 this.button_over = function(command, id)
7384 {
7385 this.button_event(command, id, 'over');
7386 };
7387
7388 // mouse down on button
7389 this.button_sel = function(command, id)
7390 {
7391 this.button_event(command, id, 'sel');
7392 };
7393
7394 // mouse out of button
7395 this.button_out = function(command, id)
7396 {
7397 this.button_event(command, id, 'act');
7398 };
7399
7400 // event of button
7401 this.button_event = function(command, id, event)
7402 {
7403 var n, button, obj, a_buttons = this.buttons[command],
7404 len = a_buttons ? a_buttons.length : 0;
7405
7406 for (n=0; n<len; n++) {
7407 button = a_buttons[n];
7408 if (button.id == id && button.status == 'act') {
7409 if (button[event] && (obj = document.getElementById(button.id))) {
7410 obj[button.type == 'image' ? 'src' : 'className'] = button[event];
7411 }
7412
7413 if (event == 'sel') {
7414 this.buttons_sel[id] = command;
7415 }
7416 }
7417 }
7418 };
7419
7420 // write to the document/window title
7421 this.set_pagetitle = function(title)
7422 {
7423 if (title && document.title)
7424 document.title = title;
7425 };
7426
7427 // display a system message, list of types in common.css (below #message definition)
7428 this.display_message = function(msg, type, timeout, key)
7429 {
7430 // pass command to parent window
7431 if (this.is_framed())
7432 return parent.rcmail.display_message(msg, type, timeout);
7433
7434 if (!this.gui_objects.message) {
7435 // save message in order to display after page loaded
7436 if (type != 'loading')
7437 this.pending_message = [msg, type, timeout, key];
7438 return 1;
7439 }
7440
7441 if (!type)
7442 type = 'notice';
7443
7444 if (!key)
7445 key = this.html_identifier(msg);
7446
7447 var date = new Date(),
7448 id = type + date.getTime();
7449
7450 if (!timeout) {
7451 switch (type) {
7452 case 'error':
7453 case 'warning':
7454 timeout = this.message_time * 2;
7455 break;
7456
7457 case 'uploading':
7458 timeout = 0;
7459 break;
7460
7461 default:
7462 timeout = this.message_time;
7463 }
7464 }
7465
7466 if (type == 'loading') {
7467 key = 'loading';
7468 timeout = this.env.request_timeout * 1000;
7469 if (!msg)
7470 msg = this.get_label('loading');
7471 }
7472
7473 // The same message is already displayed
7474 if (this.messages[key]) {
7475 // replace label
7476 if (this.messages[key].obj)
7477 this.messages[key].obj.html(msg);
7478 // store label in stack
7479 if (type == 'loading') {
7480 this.messages[key].labels.push({'id': id, 'msg': msg});
7481 }
7482 // add element and set timeout
7483 this.messages[key].elements.push(id);
7484 setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
7485 return id;
7486 }
7487
7488 // create DOM object and display it
7489 var obj = $('<div>').addClass(type).html(msg).data('key', key),
7490 cont = $(this.gui_objects.message).append(obj).show();
7491
7492 this.messages[key] = {'obj': obj, 'elements': [id]};
7493
7494 if (type == 'loading') {
7495 this.messages[key].labels = [{'id': id, 'msg': msg}];
7496 }
7497 else if (type != 'uploading') {
7498 obj.click(function() { return ref.hide_message(obj); })
7499 .attr('role', 'alert');
7500 }
7501
7502 this.triggerEvent('message', { message:msg, type:type, timeout:timeout, object:obj });
7503
7504 if (timeout > 0)
7505 setTimeout(function() { ref.hide_message(id, type != 'loading'); }, timeout);
7506
7507 return id;
7508 };
7509
7510 // make a message to disapear
7511 this.hide_message = function(obj, fade)
7512 {
7513 // pass command to parent window
7514 if (this.is_framed())
7515 return parent.rcmail.hide_message(obj, fade);
7516
7517 if (!this.gui_objects.message)
7518 return;
7519
7520 var k, n, i, o, m = this.messages;
7521
7522 // Hide message by object, don't use for 'loading'!
7523 if (typeof obj === 'object') {
7524 o = $(obj);
7525 k = o.data('key');
7526 this.hide_message_object(o, fade);
7527 if (m[k])
7528 delete m[k];
7529 }
7530 // Hide message by id
7531 else {
7532 for (k in m) {
7533 for (n in m[k].elements) {
7534 if (m[k] && m[k].elements[n] == obj) {
7535 m[k].elements.splice(n, 1);
7536 // hide DOM element if last instance is removed
7537 if (!m[k].elements.length) {
7538 this.hide_message_object(m[k].obj, fade);
7539 delete m[k];
7540 }
7541 // set pending action label for 'loading' message
7542 else if (k == 'loading') {
7543 for (i in m[k].labels) {
7544 if (m[k].labels[i].id == obj) {
7545 delete m[k].labels[i];
7546 }
7547 else {
7548 o = m[k].labels[i].msg;
7549 m[k].obj.html(o);
7550 }
7551 }
7552 }
7553 }
7554 }
7555 }
7556 }
7557 };
7558
7559 // hide message object and remove from the DOM
7560 this.hide_message_object = function(o, fade)
7561 {
7562 if (fade)
7563 o.fadeOut(600, function() {$(this).remove(); });
7564 else
7565 o.hide().remove();
7566 };
7567
7568 // remove all messages immediately
7569 this.clear_messages = function()
7570 {
7571 // pass command to parent window
7572 if (this.is_framed())
7573 return parent.rcmail.clear_messages();
7574
7575 var k, n, m = this.messages;
7576
7577 for (k in m)
7578 for (n in m[k].elements)
7579 if (m[k].obj)
7580 this.hide_message_object(m[k].obj);
7581
7582 this.messages = {};
7583 };
7584
7585 // display uploading message with progress indicator
7586 // data should contain: name, total, current, percent, text
7587 this.display_progress = function(data)
7588 {
7589 if (!data || !data.name)
7590 return;
7591
7592 var msg = this.messages['progress' + data.name];
7593
7594 if (!data.label)
7595 data.label = this.get_label('uploadingmany');
7596
7597 if (!msg) {
7598 if (!data.percent || data.percent < 100)
7599 this.display_message(data.label, 'uploading', 0, 'progress' + data.name);
7600 return;
7601 }
7602
7603 if (!data.total || data.percent >= 100) {
7604 this.hide_message(msg.obj);
7605 return;
7606 }
7607
7608 if (data.text)
7609 data.label += ' ' + data.text;
7610
7611 msg.obj.text(data.label);
7612 };
7613
7614 // open a jquery UI dialog with the given content
7615 this.show_popup_dialog = function(content, title, buttons, options)
7616 {
7617 // forward call to parent window
7618 if (this.is_framed()) {
7619 return parent.rcmail.show_popup_dialog(content, title, buttons, options);
7620 }
7621
7622 var popup = $('<div class="popup">');
7623
7624 if (typeof content == 'object')
7625 popup.append(content);
7626 else
7627 popup.html(content);
7628
7629 options = $.extend({
7630 title: title,
7631 buttons: buttons,
7632 modal: true,
7633 resizable: true,
7634 width: 500,
7635 close: function(event, ui) { $(this).remove(); }
7636 }, options || {});
7637
7638 popup.dialog(options);
7639
7640 // resize and center popup
7641 var win = $(window), w = win.width(), h = win.height(),
7642 width = popup.width(), height = popup.height();
7643
7644 popup.dialog('option', {
7645 height: Math.min(h - 40, height + 75 + (buttons ? 50 : 0)),
7646 width: Math.min(w - 20, width + 36)
7647 });
7648
7649 // assign special classes to dialog buttons
7650 $.each(options.button_classes || [], function(i, v) {
7651 if (v) $($('.ui-dialog-buttonpane button.ui-button', popup.parent()).get(i)).addClass(v);
7652 });
7653
7654 return popup;
7655 };
7656
7657 // show_popup_dialog() wrapper for simple dialogs with Save and Cancel buttons
7658 this.simple_dialog = function(content, title, button_func, options)
7659 {
7660 var title = this.get_label(title),
7661 buttons = [{
7662 text: this.get_label((options || {}).button || 'save'),
7663 'class': 'mainaction',
7664 click: function() {
7665 if (button_func())
7666 $(this).dialog('close');
7667 }
7668 },
7669 {
7670 text: ref.get_label('cancel'),
7671 click: function() {
7672 $(this).dialog('close');
7673 }
7674 }];
7675
7676 return this.show_popup_dialog(content, title, buttons, options);
7677 };
7678
7679 // enable/disable buttons for page shifting
7680 this.set_page_buttons = function()
7681 {
7682 this.enable_command('nextpage', 'lastpage', this.env.pagecount > this.env.current_page);
7683 this.enable_command('previouspage', 'firstpage', this.env.current_page > 1);
7684
7685 this.update_pagejumper();
7686 };
7687
7688 // mark a mailbox as selected and set environment variable
7689 this.select_folder = function(name, prefix, encode)
7690 {
7691 if (this.savedsearchlist) {
7692 this.savedsearchlist.select('');
7693 }
7694
7695 if (this.treelist) {
7696 this.treelist.select(name);
7697 }
7698 else if (this.gui_objects.folderlist) {
7699 $('li.selected', this.gui_objects.folderlist).removeClass('selected');
7700 $(this.get_folder_li(name, prefix, encode)).addClass('selected');
7701
7702 // trigger event hook
7703 this.triggerEvent('selectfolder', { folder:name, prefix:prefix });
7704 }
7705 };
7706
7707 // adds a class to selected folder
7708 this.mark_folder = function(name, class_name, prefix, encode)
7709 {
7710 $(this.get_folder_li(name, prefix, encode)).addClass(class_name);
7711 this.triggerEvent('markfolder', {folder: name, mark: class_name, status: true});
7712 };
7713
7714 // adds a class to selected folder
7715 this.unmark_folder = function(name, class_name, prefix, encode)
7716 {
7717 $(this.get_folder_li(name, prefix, encode)).removeClass(class_name);
7718 this.triggerEvent('markfolder', {folder: name, mark: class_name, status: false});
7719 };
7720
7721 // helper method to find a folder list item
7722 this.get_folder_li = function(name, prefix, encode)
7723 {
7724 if (!prefix)
7725 prefix = 'rcmli';
7726
7727 if (this.gui_objects.folderlist) {
7728 name = this.html_identifier(name, encode);
7729 return document.getElementById(prefix+name);
7730 }
7731 };
7732
7733 // for reordering column array (Konqueror workaround)
7734 // and for setting some message list global variables
7735 this.set_message_coltypes = function(listcols, repl, smart_col)
7736 {
7737 var list = this.message_list,
7738 thead = list ? list.thead : null,
7739 repl, cell, col, n, len, tr;
7740
7741 this.env.listcols = listcols;
7742
7743 if (!this.env.coltypes)
7744 this.env.coltypes = {};
7745
7746 // replace old column headers
7747 if (thead) {
7748 if (repl) {
7749 thead.innerHTML = '';
7750 tr = document.createElement('tr');
7751
7752 for (c=0, len=repl.length; c < len; c++) {
7753 cell = document.createElement('th');
7754 cell.innerHTML = repl[c].html || '';
7755 if (repl[c].id) cell.id = repl[c].id;
7756 if (repl[c].className) cell.className = repl[c].className;
7757 tr.appendChild(cell);
7758 }
7759 thead.appendChild(tr);
7760 }
7761
7762 for (n=0, len=this.env.listcols.length; n<len; n++) {
7763 col = this.env.listcols[n];
7764 if ((cell = thead.rows[0].cells[n]) && (col == 'from' || col == 'to' || col == 'fromto')) {
7765 $(cell).attr('rel', col).find('span,a').text(this.get_label(col == 'fromto' ? smart_col : col));
7766 }
7767 }
7768 }
7769
7770 this.env.subject_col = null;
7771 this.env.flagged_col = null;
7772 this.env.status_col = null;
7773
7774 if (this.env.coltypes.folder)
7775 this.env.coltypes.folder.hidden = !(this.env.search_request || this.env.search_id) || this.env.search_scope == 'base';
7776
7777 if ((n = $.inArray('subject', this.env.listcols)) >= 0) {
7778 this.env.subject_col = n;
7779 if (list)
7780 list.subject_col = n;
7781 }
7782 if ((n = $.inArray('flag', this.env.listcols)) >= 0)
7783 this.env.flagged_col = n;
7784 if ((n = $.inArray('status', this.env.listcols)) >= 0)
7785 this.env.status_col = n;
7786
7787 if (list) {
7788 list.hide_column('folder', (this.env.coltypes.folder && this.env.coltypes.folder.hidden) || $.inArray('folder', this.env.listcols) < 0);
7789 list.init_header();
7790 }
7791 };
7792
7793 // replace content of row count display
7794 this.set_rowcount = function(text, mbox)
7795 {
7796 // #1487752
7797 if (mbox && mbox != this.env.mailbox)
7798 return false;
7799
7800 $(this.gui_objects.countdisplay).html(text);
7801
7802 // update page navigation buttons
7803 this.set_page_buttons();
7804 };
7805
7806 // replace content of mailboxname display
7807 this.set_mailboxname = function(content)
7808 {
7809 if (this.gui_objects.mailboxname && content)
7810 this.gui_objects.mailboxname.innerHTML = content;
7811 };
7812
7813 // replace content of quota display
7814 this.set_quota = function(content)
7815 {
7816 if (this.gui_objects.quotadisplay && content && content.type == 'text')
7817 $(this.gui_objects.quotadisplay).text((content.percent||0) + '%').attr('title', content.title);
7818
7819 this.triggerEvent('setquota', content);
7820 this.env.quota_content = content;
7821 };
7822
7823 // update trash folder state
7824 this.set_trash_count = function(count)
7825 {
7826 this[(count ? 'un' : '') + 'mark_folder'](this.env.trash_mailbox, 'empty', '', true);
7827 };
7828
7829 // update the mailboxlist
7830 this.set_unread_count = function(mbox, count, set_title, mark)
7831 {
7832 if (!this.gui_objects.mailboxlist)
7833 return false;
7834
7835 this.env.unread_counts[mbox] = count;
7836 this.set_unread_count_display(mbox, set_title);
7837
7838 if (mark)
7839 this.mark_folder(mbox, mark, '', true);
7840 else if (!count)
7841 this.unmark_folder(mbox, 'recent', '', true);
7842
7843 this.mark_all_read_state();
7844 };
7845
7846 // update the mailbox count display
7847 this.set_unread_count_display = function(mbox, set_title)
7848 {
7849 var reg, link, text_obj, item, mycount, childcount, div;
7850
7851 if (item = this.get_folder_li(mbox, '', true)) {
7852 mycount = this.env.unread_counts[mbox] ? this.env.unread_counts[mbox] : 0;
7853 link = $(item).children('a').eq(0);
7854 text_obj = link.children('span.unreadcount');
7855 if (!text_obj.length && mycount)
7856 text_obj = $('<span>').addClass('unreadcount').appendTo(link);
7857 reg = /\s+\([0-9]+\)$/i;
7858
7859 childcount = 0;
7860 if ((div = item.getElementsByTagName('div')[0]) &&
7861 div.className.match(/collapsed/)) {
7862 // add children's counters
7863 for (var k in this.env.unread_counts)
7864 if (k.startsWith(mbox + this.env.delimiter))
7865 childcount += this.env.unread_counts[k];
7866 }
7867
7868 if (mycount && text_obj.length)
7869 text_obj.html(this.env.unreadwrap.replace(/%[sd]/, mycount));
7870 else if (text_obj.length)
7871 text_obj.remove();
7872
7873 // set parent's display
7874 reg = new RegExp(RegExp.escape(this.env.delimiter) + '[^' + RegExp.escape(this.env.delimiter) + ']+$');
7875 if (mbox.match(reg))
7876 this.set_unread_count_display(mbox.replace(reg, ''), false);
7877
7878 // set the right classes
7879 if ((mycount+childcount)>0)
7880 $(item).addClass('unread');
7881 else
7882 $(item).removeClass('unread');
7883 }
7884
7885 // set unread count to window title
7886 reg = /^\([0-9]+\)\s+/i;
7887 if (set_title && document.title) {
7888 var new_title = '',
7889 doc_title = String(document.title);
7890
7891 if (mycount && doc_title.match(reg))
7892 new_title = doc_title.replace(reg, '('+mycount+') ');
7893 else if (mycount)
7894 new_title = '('+mycount+') '+doc_title;
7895 else
7896 new_title = doc_title.replace(reg, '');
7897
7898 this.set_pagetitle(new_title);
7899 }
7900 };
7901
7902 // display fetched raw headers
7903 this.set_headers = function(content)
7904 {
7905 if (this.gui_objects.all_headers_row && this.gui_objects.all_headers_box && content)
7906 $(this.gui_objects.all_headers_box).html(content).show();
7907 };
7908
7909 // display all-headers row and fetch raw message headers
7910 this.show_headers = function(props, elem)
7911 {
7912 if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box || !this.env.uid)
7913 return;
7914
7915 $(elem).removeClass('show-headers').addClass('hide-headers');
7916 $(this.gui_objects.all_headers_row).show();
7917 elem.onclick = function() { ref.command('hide-headers', '', elem); };
7918
7919 // fetch headers only once
7920 if (!this.gui_objects.all_headers_box.innerHTML) {
7921 this.http_post('headers', {_uid: this.env.uid, _mbox: this.env.mailbox},
7922 this.display_message(this.get_label('loading'), 'loading')
7923 );
7924 }
7925 };
7926
7927 // hide all-headers row
7928 this.hide_headers = function(props, elem)
7929 {
7930 if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box)
7931 return;
7932
7933 $(elem).removeClass('hide-headers').addClass('show-headers');
7934 $(this.gui_objects.all_headers_row).hide();
7935 elem.onclick = function() { ref.command('show-headers', '', elem); };
7936 };
7937
7938 // create folder selector popup, position and display it
7939 this.folder_selector = function(event, callback)
7940 {
7941 var container = this.folder_selector_element;
7942
7943 if (!container) {
7944 var rows = [],
7945 delim = this.env.delimiter,
7946 ul = $('<ul class="toolbarmenu">'),
7947 link = document.createElement('a');
7948
7949 container = $('<div id="folder-selector" class="popupmenu"></div>');
7950 link.href = '#';
7951 link.className = 'icon';
7952
7953 // loop over sorted folders list
7954 $.each(this.env.mailboxes_list, function() {
7955 var n = 0, s = 0,
7956 folder = ref.env.mailboxes[this],
7957 id = folder.id,
7958 a = $(link.cloneNode(false)),
7959 row = $('<li>');
7960
7961 if (folder.virtual)
7962 a.addClass('virtual').attr('aria-disabled', 'true').attr('tabindex', '-1');
7963 else
7964 a.addClass('active').data('id', folder.id);
7965
7966 if (folder['class'])
7967 a.addClass(folder['class']);
7968
7969 // calculate/set indentation level
7970 while ((s = id.indexOf(delim, s)) >= 0) {
7971 n++; s++;
7972 }
7973 a.css('padding-left', n ? (n * 16) + 'px' : 0);
7974
7975 // add folder name element
7976 a.append($('<span>').text(folder.name));
7977
7978 row.append(a);
7979 rows.push(row);
7980 });
7981
7982 ul.append(rows).appendTo(container);
7983
7984 // temporarily show element to calculate its size
7985 container.css({left: '-1000px', top: '-1000px'})
7986 .appendTo($('body')).show();
7987
7988 // set max-height if the list is long
7989 if (rows.length > 10)
7990 container.css('max-height', $('li', container)[0].offsetHeight * 10 + 9);
7991
7992 // register delegate event handler for folder item clicks
7993 container.on('click', 'a.active', function(e){
7994 container.data('callback')($(this).data('id'));
7995 return false;
7996 });
7997
7998 this.folder_selector_element = container;
7999 }
8000
8001 container.data('callback', callback);
8002
8003 // position menu on the screen
8004 this.show_menu('folder-selector', true, event);
8005 };
8006
8007
8008 /***********************************************/
8009 /********* popup menu functions *********/
8010 /***********************************************/
8011
8012 // Show/hide a specific popup menu
8013 this.show_menu = function(prop, show, event)
8014 {
8015 var name = typeof prop == 'object' ? prop.menu : prop,
8016 obj = $('#'+name),
8017 ref = event && event.target ? $(event.target) : $(obj.attr('rel') || '#'+name+'link'),
8018 keyboard = rcube_event.is_keyboard(event),
8019 align = obj.attr('data-align') || '',
8020 stack = false;
8021
8022 // find "real" button element
8023 if (ref.get(0).tagName != 'A' && ref.closest('a').length)
8024 ref = ref.closest('a');
8025
8026 if (typeof prop == 'string')
8027 prop = { menu:name };
8028
8029 // let plugins or skins provide the menu element
8030 if (!obj.length) {
8031 obj = this.triggerEvent('menu-get', { name:name, props:prop, originalEvent:event });
8032 }
8033
8034 if (!obj || !obj.length) {
8035 // just delegate the action to subscribers
8036 return this.triggerEvent(show === false ? 'menu-close' : 'menu-open', { name:name, props:prop, originalEvent:event });
8037 }
8038
8039 // move element to top for proper absolute positioning
8040 obj.appendTo(document.body);
8041
8042 if (typeof show == 'undefined')
8043 show = obj.is(':visible') ? false : true;
8044
8045 if (show && ref.length) {
8046 var win = $(window),
8047 pos = ref.offset(),
8048 above = align.indexOf('bottom') >= 0;
8049
8050 stack = ref.attr('role') == 'menuitem' || ref.closest('[role=menuitem]').length > 0;
8051
8052 ref.offsetWidth = ref.outerWidth();
8053 ref.offsetHeight = ref.outerHeight();
8054 if (!above && pos.top + ref.offsetHeight + obj.height() > win.height()) {
8055 above = true;
8056 }
8057 if (align.indexOf('right') >= 0) {
8058 pos.left = pos.left + ref.outerWidth() - obj.width();
8059 }
8060 else if (stack) {
8061 pos.left = pos.left + ref.offsetWidth - 5;
8062 pos.top -= ref.offsetHeight;
8063 }
8064 if (pos.left + obj.width() > win.width()) {
8065 pos.left = win.width() - obj.width() - 12;
8066 }
8067 pos.top = Math.max(0, pos.top + (above ? -obj.height() : ref.offsetHeight));
8068 obj.css({ left:pos.left+'px', top:pos.top+'px' });
8069 }
8070
8071 // add menu to stack
8072 if (show) {
8073 // truncate stack down to the one containing the ref link
8074 for (var i = this.menu_stack.length - 1; stack && i >= 0; i--) {
8075 if (!$(ref).parents('#'+this.menu_stack[i]).length && $(event.target).parent().attr('role') != 'menuitem')
8076 this.hide_menu(this.menu_stack[i], event);
8077 }
8078 if (stack && this.menu_stack.length) {
8079 obj.data('parent', $.last(this.menu_stack));
8080 obj.css('z-index', ($('#'+$.last(this.menu_stack)).css('z-index') || 0) + 1);
8081 }
8082 else if (!stack && this.menu_stack.length) {
8083 this.hide_menu(this.menu_stack[0], event);
8084 }
8085
8086 obj.show().attr('aria-hidden', 'false').data('opener', ref.attr('aria-expanded', 'true').get(0));
8087 this.triggerEvent('menu-open', { name:name, obj:obj, props:prop, originalEvent:event });
8088 this.menu_stack.push(name);
8089
8090 this.menu_keyboard_active = show && keyboard;
8091 if (this.menu_keyboard_active) {
8092 this.focused_menu = name;
8093 obj.find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus();
8094 }
8095 }
8096 else { // close menu
8097 this.hide_menu(name, event);
8098 }
8099
8100 return show;
8101 };
8102
8103 // hide the given popup menu (and it's childs)
8104 this.hide_menu = function(name, event)
8105 {
8106 if (!this.menu_stack.length) {
8107 // delegate to subscribers
8108 this.triggerEvent('menu-close', { name:name, props:{ menu:name }, originalEvent:event });
8109 return;
8110 }
8111
8112 var obj, keyboard = rcube_event.is_keyboard(event);
8113 for (var j=this.menu_stack.length-1; j >= 0; j--) {
8114 obj = $('#' + this.menu_stack[j]).hide().attr('aria-hidden', 'true').data('parent', false);
8115 this.triggerEvent('menu-close', { name:this.menu_stack[j], obj:obj, props:{ menu:this.menu_stack[j] }, originalEvent:event });
8116 if (this.menu_stack[j] == name) {
8117 j = -1; // stop loop
8118 if (obj.data('opener')) {
8119 $(obj.data('opener')).attr('aria-expanded', 'false');
8120 if (keyboard)
8121 obj.data('opener').focus();
8122 }
8123 }
8124 this.menu_stack.pop();
8125 }
8126
8127 // focus previous menu in stack
8128 if (this.menu_stack.length && keyboard) {
8129 this.menu_keyboard_active = true;
8130 this.focused_menu = $.last(this.menu_stack);
8131 if (!obj || !obj.data('opener'))
8132 $('#'+this.focused_menu).find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus();
8133 }
8134 else {
8135 this.focused_menu = null;
8136 this.menu_keyboard_active = false;
8137 }
8138 }
8139
8140
8141 // position a menu element on the screen in relation to other object
8142 this.element_position = function(element, obj)
8143 {
8144 var obj = $(obj), win = $(window),
8145 width = obj.outerWidth(),
8146 height = obj.outerHeight(),
8147 menu_pos = obj.data('menu-pos'),
8148 win_height = win.height(),
8149 elem_height = $(element).height(),
8150 elem_width = $(element).width(),
8151 pos = obj.offset(),
8152 top = pos.top,
8153 left = pos.left + width;
8154
8155 if (menu_pos == 'bottom') {
8156 top += height;
8157 left -= width;
8158 }
8159 else
8160 left -= 5;
8161
8162 if (top + elem_height > win_height) {
8163 top -= elem_height - height;
8164 if (top < 0)
8165 top = Math.max(0, (win_height - elem_height) / 2);
8166 }
8167
8168 if (left + elem_width > win.width())
8169 left -= elem_width + width;
8170
8171 element.css({left: left + 'px', top: top + 'px'});
8172 };
8173
8174 // initialize HTML editor
8175 this.editor_init = function(config, id)
8176 {
8177 this.editor = new rcube_text_editor(config, id);
8178 };
8179
8180
8181 /********************************************************/
8182 /********* html to text conversion functions *********/
8183 /********************************************************/
8184
8185 this.html2plain = function(html, func)
8186 {
8187 return this.format_converter(html, 'html', func);
8188 };
8189
8190 this.plain2html = function(plain, func)
8191 {
8192 return this.format_converter(plain, 'plain', func);
8193 };
8194
8195 this.format_converter = function(text, format, func)
8196 {
8197 // warn the user (if converted content is not empty)
8198 if (!text
8199 || (format == 'html' && !(text.replace(/<[^>]+>|&nbsp;|\xC2\xA0|\s/g, '')).length)
8200 || (format != 'html' && !(text.replace(/\xC2\xA0|\s/g, '')).length)
8201 ) {
8202 // without setTimeout() here, textarea is filled with initial (onload) content
8203 if (func)
8204 setTimeout(function() { func(''); }, 50);
8205 return true;
8206 }
8207
8208 var confirmed = this.env.editor_warned || confirm(this.get_label('editorwarning'));
8209
8210 this.env.editor_warned = true;
8211
8212 if (!confirmed)
8213 return false;
8214
8215 var url = '?_task=utils&_action=' + (format == 'html' ? 'html2text' : 'text2html'),
8216 lock = this.set_busy(true, 'converting');
8217
8218 $.ajax({ type: 'POST', url: url, data: text, contentType: 'application/octet-stream',
8219 error: function(o, status, err) { ref.http_error(o, status, err, lock); },
8220 success: function(data) {
8221 ref.set_busy(false, null, lock);
8222 if (func) func(data);
8223 }
8224 });
8225
8226 return true;
8227 };
8228
8229
8230 /********************************************************/
8231 /********* remote request methods *********/
8232 /********************************************************/
8233
8234 // compose a valid url with the given parameters
8235 this.url = function(action, query)
8236 {
8237 var querystring = typeof query === 'string' ? query : '';
8238
8239 if (typeof action !== 'string')
8240 query = action;
8241 else if (!query || typeof query !== 'object')
8242 query = {};
8243
8244 if (action)
8245 query._action = action;
8246 else if (this.env.action)
8247 query._action = this.env.action;
8248
8249 var url = this.env.comm_path, k, param = {};
8250
8251 // overwrite task name
8252 if (action && action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) {
8253 query._action = RegExp.$2;
8254 url = url.replace(/\_task=[a-z0-9_-]+/, '_task=' + RegExp.$1);
8255 }
8256
8257 // remove undefined values
8258 for (k in query) {
8259 if (query[k] !== undefined && query[k] !== null)
8260 param[k] = query[k];
8261 }
8262
8263 if (param = $.param(param))
8264 url += (url.indexOf('?') > -1 ? '&' : '?') + param;
8265
8266 if (querystring)
8267 url += (url.indexOf('?') > -1 ? '&' : '?') + querystring;
8268
8269 return url;
8270 };
8271
8272 this.redirect = function(url, lock)
8273 {
8274 if (lock || lock === null)
8275 this.set_busy(true);
8276
8277 if (this.is_framed()) {
8278 parent.rcmail.redirect(url, lock);
8279 }
8280 else {
8281 if (this.env.extwin) {
8282 if (typeof url == 'string')
8283 url += (url.indexOf('?') < 0 ? '?' : '&') + '_extwin=1';
8284 else
8285 url._extwin = 1;
8286 }
8287 this.location_href(url, window);
8288 }
8289 };
8290
8291 this.goto_url = function(action, query, lock, secure)
8292 {
8293 var url = this.url(action, query)
8294 if (secure) url = this.secure_url(url);
8295 this.redirect(url, lock);
8296 };
8297
8298 this.location_href = function(url, target, frame)
8299 {
8300 if (frame)
8301 this.lock_frame();
8302
8303 if (typeof url == 'object')
8304 url = this.env.comm_path + '&' + $.param(url);
8305
8306 // simulate real link click to force IE to send referer header
8307 if (bw.ie && target == window)
8308 $('<a>').attr('href', url).appendTo(document.body).get(0).click();
8309 else
8310 target.location.href = url;
8311
8312 // reset keep-alive interval
8313 this.start_keepalive();
8314 };
8315
8316 // update browser location to remember current view
8317 this.update_state = function(query)
8318 {
8319 if (window.history.replaceState)
8320 try {
8321 // This may throw security exception in Firefox (#5400)
8322 window.history.replaceState({}, document.title, rcmail.url('', query));
8323 }
8324 catch(e) { /* ignore */ };
8325 };
8326
8327 // send a http request to the server
8328 this.http_request = function(action, data, lock, type)
8329 {
8330 if (type != 'POST')
8331 type = 'GET';
8332
8333 if (typeof data !== 'object')
8334 data = rcube_parse_query(data);
8335
8336 data._remote = 1;
8337 data._unlock = lock ? lock : 0;
8338
8339 // trigger plugin hook
8340 var result = this.triggerEvent('request' + action, data);
8341
8342 // abort if one of the handlers returned false
8343 if (result === false) {
8344 if (data._unlock)
8345 this.set_busy(false, null, data._unlock);
8346 return false;
8347 }
8348 else if (result !== undefined) {
8349 data = result;
8350 if (data._action) {
8351 action = data._action;
8352 delete data._action;
8353 }
8354 }
8355
8356 var url = this.url(action);
8357
8358 // reset keep-alive interval
8359 this.start_keepalive();
8360
8361 // send request
8362 return $.ajax({
8363 type: type, url: url, data: data, dataType: 'json',
8364 success: function(data) { ref.http_response(data); },
8365 error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
8366 });
8367 };
8368
8369 // send a http GET request to the server
8370 this.http_get = this.http_request;
8371
8372 // send a http POST request to the server
8373 this.http_post = function(action, data, lock)
8374 {
8375 return this.http_request(action, data, lock, 'POST');
8376 };
8377
8378 // aborts ajax request
8379 this.abort_request = function(r)
8380 {
8381 if (r.request)
8382 r.request.abort();
8383 if (r.lock)
8384 this.set_busy(false, null, r.lock);
8385 };
8386
8387 // handle HTTP response
8388 this.http_response = function(response)
8389 {
8390 if (!response)
8391 return;
8392
8393 if (response.unlock)
8394 this.set_busy(false);
8395
8396 this.triggerEvent('responsebefore', {response: response});
8397 this.triggerEvent('responsebefore'+response.action, {response: response});
8398
8399 // set env vars
8400 if (response.env)
8401 this.set_env(response.env);
8402
8403 var i;
8404
8405 // we have labels to add
8406 if (typeof response.texts === 'object') {
8407 for (i in response.texts)
8408 if (typeof response.texts[i] === 'string')
8409 this.add_label(i, response.texts[i]);
8410 }
8411
8412 // if we get javascript code from server -> execute it
8413 if (response.exec) {
8414 eval(response.exec);
8415 }
8416
8417 // execute callback functions of plugins
8418 if (response.callbacks && response.callbacks.length) {
8419 for (i=0; i < response.callbacks.length; i++)
8420 this.triggerEvent(response.callbacks[i][0], response.callbacks[i][1]);
8421 }
8422
8423 // process the response data according to the sent action
8424 switch (response.action) {
8425 case 'mark':
8426 // Mark the message as Seen also in the opener/parent
8427 if ((this.env.action == 'show' || this.env.action == 'preview') && this.env.last_flag == 'SEEN')
8428 this.set_unread_message(this.env.uid, this.env.mailbox);
8429 break;
8430
8431 case 'delete':
8432 if (this.task == 'addressbook') {
8433 var sid, uid = this.contact_list.get_selection(), writable = false;
8434
8435 if (uid && this.contact_list.rows[uid]) {
8436 // search results, get source ID from record ID
8437 if (this.env.source == '') {
8438 sid = String(uid).replace(/^[^-]+-/, '');
8439 writable = sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly;
8440 }
8441 else {
8442 writable = !this.env.address_sources[this.env.source].readonly;
8443 }
8444 }
8445 this.enable_command('compose', (uid && this.contact_list.rows[uid]));
8446 this.enable_command('delete', 'edit', writable);
8447 this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
8448 this.enable_command('export-selected', 'print', false);
8449 }
8450
8451 case 'move':
8452 if (this.env.action == 'show') {
8453 // re-enable commands on move/delete error
8454 this.enable_command(this.env.message_commands, true);
8455 if (!this.env.list_post)
8456 this.enable_command('reply-list', false);
8457 }
8458 else if (this.task == 'addressbook') {
8459 this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
8460 }
8461
8462 case 'purge':
8463 case 'expunge':
8464 if (this.task == 'mail') {
8465 if (!this.env.exists) {
8466 // clear preview pane content
8467 if (this.env.contentframe)
8468 this.show_contentframe(false);
8469 // disable commands useless when mailbox is empty
8470 this.enable_command(this.env.message_commands, 'purge', 'expunge',
8471 'select-all', 'select-none', 'expand-all', 'expand-unread', 'collapse-all', false);
8472 }
8473 if (this.message_list)
8474 this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });
8475 }
8476 break;
8477
8478 case 'refresh':
8479 case 'check-recent':
8480 // update message flags
8481 $.each(this.env.recent_flags || {}, function(uid, flags) {
8482 ref.set_message(uid, 'deleted', flags.deleted);
8483 ref.set_message(uid, 'replied', flags.answered);
8484 ref.set_message(uid, 'unread', !flags.seen);
8485 ref.set_message(uid, 'forwarded', flags.forwarded);
8486 ref.set_message(uid, 'flagged', flags.flagged);
8487 });
8488 delete this.env.recent_flags;
8489
8490 case 'getunread':
8491 case 'search':
8492 this.env.qsearch = null;
8493 case 'list':
8494 if (this.task == 'mail') {
8495 var is_multifolder = this.is_multifolder_listing(),
8496 list = this.message_list,
8497 uid = this.env.list_uid;
8498
8499 this.enable_command('show', 'select-all', 'select-none', this.env.messagecount > 0);
8500 this.enable_command('expunge', this.env.exists && !is_multifolder);
8501 this.enable_command('purge', this.purge_mailbox_test() && !is_multifolder);
8502 this.enable_command('import-messages', !is_multifolder);
8503 this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount && !is_multifolder);
8504
8505 if (list) {
8506 if (response.action == 'list' || response.action == 'search') {
8507 // highlight message row when we're back from message page
8508 if (uid) {
8509 if (!list.rows[uid])
8510 uid += '-' + this.env.mailbox;
8511 if (list.rows[uid]) {
8512 list.select(uid);
8513 }
8514 delete this.env.list_uid;
8515 }
8516
8517 this.enable_command('set-listmode', this.env.threads && !is_multifolder);
8518 if (list.rowcount > 0 && !$(document.activeElement).is('input,textarea'))
8519 list.focus();
8520
8521 // trigger 'select' so all dependent actions update its state
8522 // e.g. plugins use this event to activate buttons (#1490647)
8523 list.triggerEvent('select');
8524 }
8525
8526 if (response.action != 'getunread')
8527 this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:list.rowcount });
8528 }
8529 }
8530 else if (this.task == 'addressbook') {
8531 this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
8532
8533 if (response.action == 'list' || response.action == 'search') {
8534 this.enable_command('search-create', this.env.source == '');
8535 this.enable_command('search-delete', this.env.search_id);
8536 this.update_group_commands();
8537 if (this.contact_list.rowcount > 0 && !$(document.activeElement).is('input,textarea'))
8538 this.contact_list.focus();
8539 this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
8540 }
8541 }
8542 break;
8543
8544 case 'list-contacts':
8545 case 'search-contacts':
8546 if (this.contact_list && this.contact_list.rowcount > 0)
8547 this.contact_list.focus();
8548 break;
8549 }
8550
8551 if (response.unlock)
8552 this.hide_message(response.unlock);
8553
8554 this.triggerEvent('responseafter', {response: response});
8555 this.triggerEvent('responseafter'+response.action, {response: response});
8556
8557 // reset keep-alive interval
8558 this.start_keepalive();
8559 };
8560
8561 // handle HTTP request errors
8562 this.http_error = function(request, status, err, lock, action)
8563 {
8564 var errmsg = request.statusText;
8565
8566 this.set_busy(false, null, lock);
8567 request.abort();
8568
8569 // don't display error message on page unload (#1488547)
8570 if (this.unload)
8571 return;
8572
8573 if (request.status && errmsg)
8574 this.display_message(this.get_label('servererror') + ' (' + errmsg + ')', 'error');
8575 else if (status == 'timeout')
8576 this.display_message(this.get_label('requesttimedout'), 'error');
8577 else if (request.status == 0 && status != 'abort')
8578 this.display_message(this.get_label('connerror'), 'error');
8579
8580 // redirect to url specified in location header if not empty
8581 var location_url = request.getResponseHeader("Location");
8582 if (location_url && this.env.action != 'compose') // don't redirect on compose screen, contents might get lost (#1488926)
8583 this.redirect(location_url);
8584
8585 // 403 Forbidden response (CSRF prevention) - reload the page.
8586 // In case there's a new valid session it will be used, otherwise
8587 // login form will be presented (#1488960).
8588 if (request.status == 403) {
8589 (this.is_framed() ? parent : window).location.reload();
8590 return;
8591 }
8592
8593 // re-send keep-alive requests after 30 seconds
8594 if (action == 'keep-alive')
8595 setTimeout(function(){ ref.keep_alive(); ref.start_keepalive(); }, 30000);
8596 };
8597
8598 // handler for session errors detected on the server
8599 this.session_error = function(redirect_url)
8600 {
8601 this.env.server_error = 401;
8602
8603 // save message in local storage and do not redirect
8604 if (this.env.action == 'compose') {
8605 this.save_compose_form_local();
8606 this.compose_skip_unsavedcheck = true;
8607 // stop keep-alive and refresh processes
8608 this.env.session_lifetime = 0;
8609 if (this._keepalive)
8610 clearInterval(this._keepalive);
8611 if (this._refresh)
8612 clearInterval(this._refresh);
8613 }
8614 else if (redirect_url) {
8615 setTimeout(function(){ ref.redirect(redirect_url, true); }, 2000);
8616 }
8617 };
8618
8619 // callback when an iframe finished loading
8620 this.iframe_loaded = function(unlock)
8621 {
8622 this.set_busy(false, null, unlock);
8623
8624 if (this.submit_timer)
8625 clearTimeout(this.submit_timer);
8626 };
8627
8628 /**
8629 Send multi-threaded parallel HTTP requests to the server for a list if items.
8630 The string '%' in either a GET query or POST parameters will be replaced with the respective item value.
8631 This is the argument object expected: {
8632 items: ['foo','bar','gna'], // list of items to send requests for
8633 action: 'task/some-action', // Roudncube action to call
8634 query: { q:'%s' }, // GET query parameters
8635 postdata: { source:'%s' }, // POST data (sends a POST request if present)
8636 threads: 3, // max. number of concurrent requests
8637 onresponse: function(data){ }, // Callback function called for every response received from server
8638 whendone: function(alldata){ } // Callback function called when all requests have been sent
8639 }
8640 */
8641 this.multi_thread_http_request = function(prop)
8642 {
8643 var i, item, reqid = new Date().getTime(),
8644 threads = prop.threads || 1;
8645
8646 prop.reqid = reqid;
8647 prop.running = 0;
8648 prop.requests = [];
8649 prop.result = [];
8650 prop._items = $.extend([], prop.items); // copy items
8651
8652 if (!prop.lock)
8653 prop.lock = this.display_message(this.get_label('loading'), 'loading');
8654
8655 // add the request arguments to the jobs pool
8656 this.http_request_jobs[reqid] = prop;
8657
8658 // start n threads
8659 for (i=0; i < threads; i++) {
8660 item = prop._items.shift();
8661 if (item === undefined)
8662 break;
8663
8664 prop.running++;
8665 prop.requests.push(this.multi_thread_send_request(prop, item));
8666 }
8667
8668 return reqid;
8669 };
8670
8671 // helper method to send an HTTP request with the given iterator value
8672 this.multi_thread_send_request = function(prop, item)
8673 {
8674 var k, postdata, query;
8675
8676 // replace %s in post data
8677 if (prop.postdata) {
8678 postdata = {};
8679 for (k in prop.postdata) {
8680 postdata[k] = String(prop.postdata[k]).replace('%s', item);
8681 }
8682 postdata._reqid = prop.reqid;
8683 }
8684 // replace %s in query
8685 else if (typeof prop.query == 'string') {
8686 query = prop.query.replace('%s', item);
8687 query += '&_reqid=' + prop.reqid;
8688 }
8689 else if (typeof prop.query == 'object' && prop.query) {
8690 query = {};
8691 for (k in prop.query) {
8692 query[k] = String(prop.query[k]).replace('%s', item);
8693 }
8694 query._reqid = prop.reqid;
8695 }
8696
8697 // send HTTP GET or POST request
8698 return postdata ? this.http_post(prop.action, postdata) : this.http_request(prop.action, query);
8699 };
8700
8701 // callback function for multi-threaded http responses
8702 this.multi_thread_http_response = function(data, reqid)
8703 {
8704 var prop = this.http_request_jobs[reqid];
8705 if (!prop || prop.running <= 0 || prop.cancelled)
8706 return;
8707
8708 prop.running--;
8709
8710 // trigger response callback
8711 if (prop.onresponse && typeof prop.onresponse == 'function') {
8712 prop.onresponse(data);
8713 }
8714
8715 prop.result = $.extend(prop.result, data);
8716
8717 // send next request if prop.items is not yet empty
8718 var item = prop._items.shift();
8719 if (item !== undefined) {
8720 prop.running++;
8721 prop.requests.push(this.multi_thread_send_request(prop, item));
8722 }
8723 // trigger whendone callback and mark this request as done
8724 else if (prop.running == 0) {
8725 if (prop.whendone && typeof prop.whendone == 'function') {
8726 prop.whendone(prop.result);
8727 }
8728
8729 this.set_busy(false, '', prop.lock);
8730
8731 // remove from this.http_request_jobs pool
8732 delete this.http_request_jobs[reqid];
8733 }
8734 };
8735
8736 // abort a running multi-thread request with the given identifier
8737 this.multi_thread_request_abort = function(reqid)
8738 {
8739 var prop = this.http_request_jobs[reqid];
8740 if (prop) {
8741 for (var i=0; prop.running > 0 && i < prop.requests.length; i++) {
8742 if (prop.requests[i].abort)
8743 prop.requests[i].abort();
8744 }
8745
8746 prop.running = 0;
8747 prop.cancelled = true;
8748 this.set_busy(false, '', prop.lock);
8749 }
8750 };
8751
8752 // post the given form to a hidden iframe
8753 this.async_upload_form = function(form, action, onload)
8754 {
8755 // create hidden iframe
8756 var ts = new Date().getTime(),
8757 frame_name = 'rcmupload' + ts,
8758 frame = this.async_upload_form_frame(frame_name);
8759
8760 // upload progress support
8761 if (this.env.upload_progress_name) {
8762 var fname = this.env.upload_progress_name,
8763 field = $('input[name='+fname+']', form);
8764
8765 if (!field.length) {
8766 field = $('<input>').attr({type: 'hidden', name: fname});
8767 field.prependTo(form);
8768 }
8769
8770 field.val(ts);
8771 }
8772
8773 // handle upload errors by parsing iframe content in onload
8774 frame.on('load', {ts:ts}, onload);
8775
8776 $(form).attr({
8777 target: frame_name,
8778 action: this.url(action, {_id: this.env.compose_id || '', _uploadid: ts, _from: this.env.action}),
8779 method: 'POST'})
8780 .attr(form.encoding ? 'encoding' : 'enctype', 'multipart/form-data')
8781 .submit();
8782
8783 return frame_name;
8784 };
8785
8786 // create iframe element for files upload
8787 this.async_upload_form_frame = function(name)
8788 {
8789 return $('<iframe>').attr({name: name, style: 'border: none; width: 0; height: 0; visibility: hidden'})
8790 .appendTo(document.body);
8791 };
8792
8793 // html5 file-drop API
8794 this.document_drag_hover = function(e, over)
8795 {
8796 // don't e.preventDefault() here to not block text dragging on the page (#1490619)
8797 $(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('active');
8798 };
8799
8800 this.file_drag_hover = function(e, over)
8801 {
8802 e.preventDefault();
8803 e.stopPropagation();
8804 $(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('hover');
8805 };
8806
8807 // handler when files are dropped to a designated area.
8808 // compose a multipart form data and submit it to the server
8809 this.file_dropped = function(e)
8810 {
8811 // abort event and reset UI
8812 this.file_drag_hover(e, false);
8813
8814 // prepare multipart form data composition
8815 var uri, size = 0, numfiles = 0,
8816 files = e.target.files || e.dataTransfer.files,
8817 formdata = window.FormData ? new FormData() : null,
8818 fieldname = (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'),
8819 boundary = '------multipartformboundary' + (new Date).getTime(),
8820 dashdash = '--', crlf = '\r\n',
8821 multipart = dashdash + boundary + crlf,
8822 args = {_id: this.env.compose_id || this.env.cid || '', _remote: 1, _from: this.env.action};
8823
8824 if (!files || !files.length) {
8825 // Roundcube attachment, pass its uri to the backend and attach
8826 if (uri = e.dataTransfer.getData('roundcube-uri')) {
8827 var ts = new Date().getTime(),
8828 // jQuery way to escape filename (#1490530)
8829 content = $('<span>').text(e.dataTransfer.getData('roundcube-name') || this.get_label('attaching')).html();
8830
8831 args._uri = uri;
8832 args._uploadid = ts;
8833
8834 // add to attachments list
8835 if (!this.add2attachment_list(ts, {name: '', html: content, classname: 'uploading', complete: false}))
8836 this.file_upload_id = this.set_busy(true, 'attaching');
8837
8838 this.http_post(this.env.filedrop.action || 'upload', args);
8839 }
8840 return;
8841 }
8842
8843 // inline function to submit the files to the server
8844 var submit_data = function() {
8845 if (ref.env.max_filesize && ref.env.filesizeerror && size > ref.env.max_filesize) {
8846 ref.display_message(ref.env.filesizeerror, 'error');
8847 return;
8848 }
8849
8850 if (ref.env.max_filecount && ref.env.filecounterror && numfiles > ref.env.max_filecount) {
8851 ref.display_message(ref.env.filecounterror, 'error');
8852 return;
8853 }
8854
8855 var multiple = files.length > 1,
8856 ts = new Date().getTime(),
8857 // jQuery way to escape filename (#1490530)
8858 content = $('<span>').text(multiple ? ref.get_label('uploadingmany') : files[0].name).html();
8859
8860 // add to attachments list
8861 if (!ref.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false }))
8862 ref.file_upload_id = ref.set_busy(true, 'uploading');
8863
8864 // complete multipart content and post request
8865 multipart += dashdash + boundary + dashdash + crlf;
8866
8867 args._uploadid = ts;
8868
8869 $.ajax({
8870 type: 'POST',
8871 dataType: 'json',
8872 url: ref.url(ref.env.filedrop.action || 'upload', args),
8873 contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary,
8874 processData: false,
8875 timeout: 0, // disable default timeout set in ajaxSetup()
8876 data: formdata || multipart,
8877 headers: {'X-Roundcube-Request': ref.env.request_token},
8878 xhr: function() { var xhr = jQuery.ajaxSettings.xhr(); if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; return xhr; },
8879 success: function(data){ ref.http_response(data); },
8880 error: function(o, status, err) { ref.http_error(o, status, err, null, 'attachment'); }
8881 });
8882 };
8883
8884 // get contents of all dropped files
8885 var last = this.env.filedrop.single ? 0 : files.length - 1;
8886 for (var j=0, i=0, f; j <= last && (f = files[i]); i++) {
8887 if (!f.name) f.name = f.fileName;
8888 if (!f.size) f.size = f.fileSize;
8889 if (!f.type) f.type = 'application/octet-stream';
8890
8891 // file name contains non-ASCII characters, do UTF8-binary string conversion.
8892 if (!formdata && /[^\x20-\x7E]/.test(f.name))
8893 f.name_bin = unescape(encodeURIComponent(f.name));
8894
8895 // filter by file type if requested
8896 if (this.env.filedrop.filter && !f.type.match(new RegExp(this.env.filedrop.filter))) {
8897 // TODO: show message to user
8898 continue;
8899 }
8900
8901 size += f.size;
8902 numfiles++;
8903
8904 // do it the easy way with FormData (FF 4+, Chrome 5+, Safari 5+)
8905 if (formdata) {
8906 formdata.append(fieldname, f);
8907 if (j == last)
8908 return submit_data();
8909 }
8910 // use FileReader supporetd by Firefox 3.6
8911 else if (window.FileReader) {
8912 var reader = new FileReader();
8913
8914 // closure to pass file properties to async callback function
8915 reader.onload = (function(file, j) {
8916 return function(e) {
8917 multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
8918 multipart += '; filename="' + (f.name_bin || file.name) + '"' + crlf;
8919 multipart += 'Content-Length: ' + file.size + crlf;
8920 multipart += 'Content-Type: ' + file.type + crlf + crlf;
8921 multipart += reader.result + crlf;
8922 multipart += dashdash + boundary + crlf;
8923
8924 if (j == last) // we're done, submit the data
8925 return submit_data();
8926 }
8927 })(f,j);
8928 reader.readAsBinaryString(f);
8929 }
8930 // Firefox 3
8931 else if (f.getAsBinary) {
8932 multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
8933 multipart += '; filename="' + (f.name_bin || f.name) + '"' + crlf;
8934 multipart += 'Content-Length: ' + f.size + crlf;
8935 multipart += 'Content-Type: ' + f.type + crlf + crlf;
8936 multipart += f.getAsBinary() + crlf;
8937 multipart += dashdash + boundary +crlf;
8938
8939 if (j == last)
8940 return submit_data();
8941 }
8942
8943 j++;
8944 }
8945 };
8946
8947 // starts interval for keep-alive signal
8948 this.start_keepalive = function()
8949 {
8950 if (!this.env.session_lifetime || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
8951 return;
8952
8953 if (this._keepalive)
8954 clearInterval(this._keepalive);
8955
8956 // use Math to prevent from an integer overflow (#5273)
8957 // maximum interval is 15 minutes, minimum is 30 seconds
8958 var interval = Math.min(1800, this.env.session_lifetime) * 0.5 * 1000;
8959 this._keepalive = setInterval(function() { ref.keep_alive(); }, interval < 30000 ? 30000 : interval);
8960 };
8961
8962 // starts interval for refresh signal
8963 this.start_refresh = function()
8964 {
8965 if (!this.env.refresh_interval || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
8966 return;
8967
8968 if (this._refresh)
8969 clearInterval(this._refresh);
8970
8971 this._refresh = setInterval(function(){ ref.refresh(); }, this.env.refresh_interval * 1000);
8972 };
8973
8974 // sends keep-alive signal
8975 this.keep_alive = function()
8976 {
8977 if (!this.busy)
8978 this.http_request('keep-alive');
8979 };
8980
8981 // sends refresh signal
8982 this.refresh = function()
8983 {
8984 if (this.busy) {
8985 // try again after 10 seconds
8986 setTimeout(function(){ ref.refresh(); ref.start_refresh(); }, 10000);
8987 return;
8988 }
8989
8990 var params = {}, lock = this.set_busy(true, 'refreshing');
8991
8992 if (this.task == 'mail' && this.gui_objects.mailboxlist)
8993 params = this.check_recent_params();
8994
8995 params._last = Math.floor(this.env.lastrefresh.getTime() / 1000);
8996 this.env.lastrefresh = new Date();
8997
8998 // plugins should bind to 'requestrefresh' event to add own params
8999 this.http_post('refresh', params, lock);
9000 };
9001
9002 // returns check-recent request parameters
9003 this.check_recent_params = function()
9004 {
9005 var params = {_mbox: this.env.mailbox};
9006
9007 if (this.gui_objects.mailboxlist)
9008 params._folderlist = 1;
9009 if (this.gui_objects.quotadisplay)
9010 params._quota = 1;
9011 if (this.env.search_request)
9012 params._search = this.env.search_request;
9013
9014 if (this.gui_objects.messagelist) {
9015 params._list = 1;
9016
9017 // message uids for flag updates check
9018 params._uids = $.map(this.message_list.rows, function(row, uid) { return uid; }).join(',');
9019 }
9020
9021 return params;
9022 };
9023
9024
9025 /********************************************************/
9026 /********* helper methods *********/
9027 /********************************************************/
9028
9029 /**
9030 * Quote html entities
9031 */
9032 this.quote_html = function(str)
9033 {
9034 return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
9035 };
9036
9037 // get window.opener.rcmail if available
9038 this.opener = function(deep, filter)
9039 {
9040 var i, win = window.opener;
9041
9042 // catch Error: Permission denied to access property rcmail
9043 try {
9044 if (win && !win.closed) {
9045 // try parent of the opener window, e.g. preview frame
9046 if (deep && (!win.rcmail || win.rcmail.env.framed) && win.parent && win.parent.rcmail)
9047 win = win.parent;
9048
9049 if (win.rcmail && filter)
9050 for (i in filter)
9051 if (win.rcmail.env[i] != filter[i])
9052 return;
9053
9054 return win.rcmail;
9055 }
9056 }
9057 catch (e) {}
9058 };
9059
9060 // check if we're in show mode or if we have a unique selection
9061 // and return the message uid
9062 this.get_single_uid = function()
9063 {
9064 var uid = this.env.uid || (this.message_list ? this.message_list.get_single_selection() : null);
9065 var result = ref.triggerEvent('get_single_uid', { uid: uid });
9066 return result || uid;
9067 };
9068
9069 // same as above but for contacts
9070 this.get_single_cid = function()
9071 {
9072 var cid = this.env.cid || (this.contact_list ? this.contact_list.get_single_selection() : null);
9073 var result = ref.triggerEvent('get_single_cid', { cid: cid });
9074 return result || cid;
9075 };
9076
9077 // get the IMP mailbox of the message with the given UID
9078 this.get_message_mailbox = function(uid)
9079 {
9080 var msg = (this.env.messages && uid ? this.env.messages[uid] : null) || {};
9081 return msg.mbox || this.env.mailbox;
9082 };
9083
9084 // build request parameters from single message id (maybe with mailbox name)
9085 this.params_from_uid = function(uid, params)
9086 {
9087 if (!params)
9088 params = {};
9089
9090 params._uid = String(uid).split('-')[0];
9091 params._mbox = this.get_message_mailbox(uid);
9092
9093 return params;
9094 };
9095
9096 // gets cursor position
9097 this.get_caret_pos = function(obj)
9098 {
9099 if (obj.selectionEnd !== undefined)
9100 return obj.selectionEnd;
9101
9102 return obj.value.length;
9103 };
9104
9105 // moves cursor to specified position
9106 this.set_caret_pos = function(obj, pos)
9107 {
9108 try {
9109 if (obj.setSelectionRange)
9110 obj.setSelectionRange(pos, pos);
9111 }
9112 catch(e) {} // catch Firefox exception if obj is hidden
9113 };
9114
9115 // get selected text from an input field
9116 this.get_input_selection = function(obj)
9117 {
9118 var start = 0, end = 0, normalizedValue = '';
9119
9120 if (typeof obj.selectionStart == "number" && typeof obj.selectionEnd == "number") {
9121 normalizedValue = obj.value;
9122 start = obj.selectionStart;
9123 end = obj.selectionEnd;
9124 }
9125
9126 return {start: start, end: end, text: normalizedValue.substr(start, end-start)};
9127 };
9128
9129 // disable/enable all fields of a form
9130 this.lock_form = function(form, lock)
9131 {
9132 if (!form || !form.elements)
9133 return;
9134
9135 var n, len, elm;
9136
9137 if (lock)
9138 this.disabled_form_elements = [];
9139
9140 for (n=0, len=form.elements.length; n<len; n++) {
9141 elm = form.elements[n];
9142
9143 if (elm.type == 'hidden')
9144 continue;
9145 // remember which elem was disabled before lock
9146 if (lock && elm.disabled)
9147 this.disabled_form_elements.push(elm);
9148 else if (lock || $.inArray(elm, this.disabled_form_elements) < 0)
9149 elm.disabled = lock;
9150 }
9151 };
9152
9153 this.mailto_handler_uri = function()
9154 {
9155 return location.href.split('?')[0] + '?_task=mail&_action=compose&_to=%s';
9156 };
9157
9158 this.register_protocol_handler = function(name)
9159 {
9160 try {
9161 window.navigator.registerProtocolHandler('mailto', this.mailto_handler_uri(), name);
9162 }
9163 catch(e) {
9164 this.display_message(String(e), 'error');
9165 }
9166 };
9167
9168 this.check_protocol_handler = function(name, elem)
9169 {
9170 var nav = window.navigator;
9171
9172 if (!nav || (typeof nav.registerProtocolHandler != 'function')) {
9173 $(elem).addClass('disabled').click(function(){ return false; });
9174 }
9175 else if (typeof nav.isProtocolHandlerRegistered == 'function') {
9176 var status = nav.isProtocolHandlerRegistered('mailto', this.mailto_handler_uri());
9177 if (status)
9178 $(elem).parent().find('.mailtoprotohandler-status').html(status);
9179 }
9180 else {
9181 $(elem).click(function() { ref.register_protocol_handler(name); return false; });
9182 }
9183 };
9184
9185 // Checks browser capabilities eg. PDF support, TIF support
9186 this.browser_capabilities_check = function()
9187 {
9188 if (!this.env.browser_capabilities)
9189 this.env.browser_capabilities = {};
9190
9191 $.each(['pdf', 'flash', 'tiff', 'webp'], function() {
9192 if (ref.env.browser_capabilities[this] === undefined)
9193 ref.env.browser_capabilities[this] = ref[this + '_support_check']();
9194 });
9195 };
9196
9197 // Returns browser capabilities string
9198 this.browser_capabilities = function()
9199 {
9200 if (!this.env.browser_capabilities)
9201 return '';
9202
9203 var n, ret = [];
9204
9205 for (n in this.env.browser_capabilities)
9206 ret.push(n + '=' + this.env.browser_capabilities[n]);
9207
9208 return ret.join();
9209 };
9210
9211 this.tiff_support_check = function()
9212 {
9213 this.image_support_check('tiff');
9214 return 0;
9215 };
9216
9217 this.webp_support_check = function()
9218 {
9219 this.image_support_check('webp');
9220 return 0;
9221 };
9222
9223 this.image_support_check = function(type)
9224 {
9225 window.setTimeout(function() {
9226 var img = new Image();
9227 img.onload = function() { ref.env.browser_capabilities[type] = 1; };
9228 img.onerror = function() { ref.env.browser_capabilities[type] = 0; };
9229 img.src = ref.assets_path('program/resources/blank.' + type);
9230 }, 10);
9231 };
9232
9233 this.pdf_support_check = function()
9234 {
9235 var i, plugin = navigator.mimeTypes ? navigator.mimeTypes["application/pdf"] : {},
9236 plugins = navigator.plugins,
9237 len = plugins.length,
9238 regex = /Adobe Reader|PDF|Acrobat/i;
9239
9240 if (plugin && plugin.enabledPlugin)
9241 return 1;
9242
9243 if ('ActiveXObject' in window) {
9244 try {
9245 if (plugin = new ActiveXObject("AcroPDF.PDF"))
9246 return 1;
9247 }
9248 catch (e) {}
9249 try {
9250 if (plugin = new ActiveXObject("PDF.PdfCtrl"))
9251 return 1;
9252 }
9253 catch (e) {}
9254 }
9255
9256 for (i=0; i<len; i++) {
9257 plugin = plugins[i];
9258 if (typeof plugin === 'String') {
9259 if (regex.test(plugin))
9260 return 1;
9261 }
9262 else if (plugin.name && regex.test(plugin.name))
9263 return 1;
9264 }
9265
9266 window.setTimeout(function() {
9267 $('<object>').attr({
9268 data: ref.assets_path('program/resources/dummy.pdf'),
9269 type: 'application/pdf',
9270 style: 'position: "absolute"; top: -1000px; height: 1px; width: 1px'
9271 })
9272 .on('load error', function(e) {
9273 ref.env.browser_capabilities.pdf = e.type == 'load' ? 1 : 0;
9274 $(this).remove();
9275 })
9276 .appendTo($('body'));
9277 }, 10);
9278
9279 return 0;
9280 };
9281
9282 this.flash_support_check = function()
9283 {
9284 var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/x-shockwave-flash"] : {};
9285
9286 if (plugin && plugin.enabledPlugin)
9287 return 1;
9288
9289 if ('ActiveXObject' in window) {
9290 try {
9291 if (plugin = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
9292 return 1;
9293 }
9294 catch (e) {}
9295 }
9296
9297 return 0;
9298 };
9299
9300 this.assets_path = function(path)
9301 {
9302 if (this.env.assets_path && !path.startsWith(this.env.assets_path)) {
9303 path = this.env.assets_path + path;
9304 }
9305
9306 return path;
9307 };
9308
9309 // Cookie setter
9310 this.set_cookie = function(name, value, expires)
9311 {
9312 setCookie(name, value, expires, this.env.cookie_path, this.env.cookie_domain, this.env.cookie_secure);
9313 };
9314
9315 this.get_local_storage_prefix = function()
9316 {
9317 if (!this.local_storage_prefix)
9318 this.local_storage_prefix = 'roundcube.' + (this.env.user_id || 'anonymous') + '.';
9319
9320 return this.local_storage_prefix;
9321 };
9322
9323 // wrapper for localStorage.getItem(key)
9324 this.local_storage_get_item = function(key, deflt, encrypted)
9325 {
9326 var item, result;
9327
9328 // TODO: add encryption
9329 try {
9330 item = localStorage.getItem(this.get_local_storage_prefix() + key);
9331 result = JSON.parse(item);
9332 }
9333 catch (e) { }
9334
9335 return result || deflt || null;
9336 };
9337
9338 // wrapper for localStorage.setItem(key, data)
9339 this.local_storage_set_item = function(key, data, encrypted)
9340 {
9341 // try/catch to handle no localStorage support, but also error
9342 // in Safari-in-private-browsing-mode where localStorage exists
9343 // but can't be used (#1489996)
9344 try {
9345 // TODO: add encryption
9346 localStorage.setItem(this.get_local_storage_prefix() + key, JSON.stringify(data));
9347 return true;
9348 }
9349 catch (e) {
9350 return false;
9351 }
9352 };
9353
9354 // wrapper for localStorage.removeItem(key)
9355 this.local_storage_remove_item = function(key)
9356 {
9357 try {
9358 localStorage.removeItem(this.get_local_storage_prefix() + key);
9359 return true;
9360 }
9361 catch (e) {
9362 return false;
9363 }
9364 };
9365
9366 this.print_dialog = function()
9367 {
9368 if (bw.safari)
9369 setTimeout('window.print()', 10);
9370 else
9371 window.print();
9372 };
9373 } // end object rcube_webmail
9374
9375
9376 // some static methods
9377 rcube_webmail.long_subject_title = function(elem, indent)
9378 {
9379 if (!elem.title) {
9380 var $elem = $(elem);
9381 if ($elem.width() + (indent || 0) * 15 > $elem.parent().width())
9382 elem.title = rcube_webmail.subject_text(elem);
9383 }
9384 };
9385
9386 rcube_webmail.long_subject_title_ex = function(elem)
9387 {
9388 if (!elem.title) {
9389 var $elem = $(elem),
9390 txt = $.trim($elem.text()),
9391 tmp = $('<span>').text(txt)
9392 .css({'position': 'absolute', 'float': 'left', 'visibility': 'hidden',
9393 'font-size': $elem.css('font-size'), 'font-weight': $elem.css('font-weight')})
9394 .appendTo($('body')),
9395 w = tmp.width();
9396
9397 tmp.remove();
9398 if (w + $('span.branch', $elem).width() * 15 > $elem.width())
9399 elem.title = rcube_webmail.subject_text(elem);
9400 }
9401 };
9402
9403 rcube_webmail.subject_text = function(elem)
9404 {
9405 var t = $(elem).clone();
9406 t.find('.skip-on-drag,.skip-content,.voice').remove();
9407 return t.text();
9408 };
9409
9410 // set event handlers on all iframe elements (and their contents)
9411 rcube_webmail.set_iframe_events = function(events)
9412 {
9413 $('iframe').each(function() {
9414 var frame = $(this);
9415 $.each(events, function(event_name, event_handler) {
9416 frame.on('load', function(e) {
9417 try { $(this).contents().on(event_name, event_handler); }
9418 catch (e) {/* catch possible permission error in IE */ }
9419 });
9420
9421 try { frame.contents().on(event_name, event_handler); }
9422 catch (e) {/* catch possible permission error in IE */ }
9423 });
9424 });
9425 };
9426
9427 rcube_webmail.prototype.get_cookie = getCookie;
9428
9429 // copy event engine prototype
9430 rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
9431 rcube_webmail.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
9432 rcube_webmail.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;