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