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