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