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