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