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