]> git.donarmstrong.com Git - roundcube.git/blob - program/js/app.js.src
a127b29331cc56a5f55817b54c3f91588492d940
[roundcube.git] / program / js / app.js.src
1 /*
2  +-----------------------------------------------------------------------+
3  | Roundcube Webmail Client Script                                       |
4  |                                                                       |
5  | This file is part of the Roundcube Webmail client                     |
6  | Copyright (C) 2005-2010, The Roundcube Dev Team                       |
7  | Licensed under the GNU GPL                                            |
8  |                                                                       |
9  +-----------------------------------------------------------------------+
10  | Authors: Thomas Bruederli <roundcube@gmail.com>                       |
11  |          Aleksander 'A.L.E.C' Machniak <alec@alec.pl>                 |
12  |          Charles McNulty <charles@charlesmcnulty.com>                 |
13  +-----------------------------------------------------------------------+
14  | Requires: jquery.js, common.js, list.js                               |
15  +-----------------------------------------------------------------------+
16
17   $Id: app.js 5281 2011-09-27 07:29:49Z alec $
18 */
19
20 function rcube_webmail()
21 {
22   this.env = {};
23   this.labels = {};
24   this.buttons = {};
25   this.buttons_sel = {};
26   this.gui_objects = {};
27   this.gui_containers = {};
28   this.commands = {};
29   this.command_handlers = {};
30   this.onloads = [];
31   this.messages = {};
32
33   // create protected reference to myself
34   this.ref = 'rcmail';
35   var ref = this;
36
37   // webmail client settings
38   this.dblclick_time = 500;
39   this.message_time = 2000;
40
41   this.identifier_expr = new RegExp('[^0-9a-z\-_]', 'gi');
42
43   // default environment vars
44   this.env.keep_alive = 60;        // seconds
45   this.env.request_timeout = 180;  // seconds
46   this.env.draft_autosave = 0;     // seconds
47   this.env.comm_path = './';
48   this.env.blankpage = 'program/blank.gif';
49
50   // set jQuery ajax options
51   $.ajaxSetup({
52     cache:false,
53     error:function(request, status, err){ ref.http_error(request, status, err); },
54     beforeSend:function(xmlhttp){ xmlhttp.setRequestHeader('X-Roundcube-Request', ref.env.request_token); }
55   });
56
57   // set environment variable(s)
58   this.set_env = function(p, value)
59   {
60     if (p != null && typeof p === 'object' && !value)
61       for (var n in p)
62         this.env[n] = p[n];
63     else
64       this.env[p] = value;
65   };
66
67   // add a localized label to the client environment
68   this.add_label = function(p, value)
69   {
70     if (typeof p == 'string')
71       this.labels[p] = value;
72     else if (typeof p == 'object')
73       $.extend(this.labels, p);
74   };
75
76   // add a button to the button list
77   this.register_button = function(command, id, type, act, sel, over)
78   {
79     if (!this.buttons[command])
80       this.buttons[command] = [];
81
82     var button_prop = {id:id, type:type};
83     if (act) button_prop.act = act;
84     if (sel) button_prop.sel = sel;
85     if (over) button_prop.over = over;
86
87     this.buttons[command].push(button_prop);
88     
89     if (this.loaded)
90       init_button(command, button_prop);
91   };
92
93   // register a specific gui object
94   this.gui_object = function(name, id)
95   {
96     this.gui_objects[name] = this.loaded ? rcube_find_object(id) : id;
97   };
98
99   // register a container object
100   this.gui_container = function(name, id)
101   {
102     this.gui_containers[name] = id;
103   };
104
105   // add a GUI element (html node) to a specified container
106   this.add_element = function(elm, container)
107   {
108     if (this.gui_containers[container] && this.gui_containers[container].jquery)
109       this.gui_containers[container].append(elm);
110   };
111
112   // register an external handler for a certain command
113   this.register_command = function(command, callback, enable)
114   {
115     this.command_handlers[command] = callback;
116
117     if (enable)
118       this.enable_command(command, true);
119   };
120
121   // execute the given script on load
122   this.add_onload = function(f)
123   {
124     this.onloads.push(f);
125   };
126
127   // initialize webmail client
128   this.init = function()
129   {
130     var p = this;
131     this.task = this.env.task;
132
133     // check browser
134     if (!bw.dom || !bw.xmlhttp_test()) {
135       this.goto_url('error', '_code=0x199');
136       return;
137     }
138
139     // find all registered gui containers
140     for (var n in this.gui_containers)
141       this.gui_containers[n] = $('#'+this.gui_containers[n]);
142
143     // find all registered gui objects
144     for (var n in this.gui_objects)
145       this.gui_objects[n] = rcube_find_object(this.gui_objects[n]);
146
147     // init registered buttons
148     this.init_buttons();
149
150     // tell parent window that this frame is loaded
151     if (this.is_framed()) {
152       parent.rcmail.set_busy(false, null, parent.rcmail.env.frame_lock);
153       parent.rcmail.env.frame_lock = null;
154     }
155
156     // enable general commands
157     this.enable_command('logout', 'mail', 'addressbook', 'settings', 'save-pref', 'undo', true);
158
159     if (this.env.permaurl)
160       this.enable_command('permaurl', true);
161
162     switch (this.task) {
163
164       case 'mail':
165         // enable mail commands
166         this.enable_command('list', 'checkmail', 'compose', 'add-contact', 'search', 'reset-search', 'collapse-folder', true);
167
168         if (this.gui_objects.messagelist) {
169
170           this.message_list = new rcube_list_widget(this.gui_objects.messagelist, {
171             multiselect:true, multiexpand:true, draggable:true, keyboard:true,
172             column_movable:this.env.col_movable, dblclick_time:this.dblclick_time
173             });
174           this.message_list.row_init = function(o){ p.init_message_row(o); };
175           this.message_list.addEventListener('dblclick', function(o){ p.msglist_dbl_click(o); });
176           this.message_list.addEventListener('click', function(o){ p.msglist_click(o); });
177           this.message_list.addEventListener('keypress', function(o){ p.msglist_keypress(o); });
178           this.message_list.addEventListener('select', function(o){ p.msglist_select(o); });
179           this.message_list.addEventListener('dragstart', function(o){ p.drag_start(o); });
180           this.message_list.addEventListener('dragmove', function(e){ p.drag_move(e); });
181           this.message_list.addEventListener('dragend', function(e){ p.drag_end(e); });
182           this.message_list.addEventListener('expandcollapse', function(e){ p.msglist_expand(e); });
183           this.message_list.addEventListener('column_replace', function(e){ p.msglist_set_coltypes(e); });
184
185           document.onmouseup = function(e){ return p.doc_mouse_up(e); };
186           this.gui_objects.messagelist.parentNode.onmousedown = function(e){ return p.click_on_list(e); };
187
188           this.message_list.init();
189           this.enable_command('toggle_status', 'toggle_flag', 'menu-open', 'menu-save', true);
190
191           // load messages
192           this.command('list');
193         }
194
195         if (this.gui_objects.qsearchbox) {
196           if (this.env.search_text != null) {
197             this.gui_objects.qsearchbox.value = this.env.search_text;
198           }
199           $(this.gui_objects.qsearchbox).focusin(function() { rcmail.message_list.blur(); });
200         }
201
202         if (!this.env.flag_for_deletion && this.env.trash_mailbox && this.env.mailbox != this.env.trash_mailbox)
203           this.set_alttext('delete', 'movemessagetotrash');
204
205         this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list', 'forward',
206           'moveto', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource', 'download',
207           'print', 'load-attachment', 'load-headers', 'forward-attachment'];
208
209         if (this.env.action=='show' || this.env.action=='preview') {
210           this.enable_command(this.env.message_commands, this.env.uid);
211           this.enable_command('reply-list', this.env.list_post);
212
213           if (this.env.action == 'show') {
214             this.http_request('pagenav', '_uid='+this.env.uid+'&_mbox='+urlencode(this.env.mailbox),
215               this.display_message('', 'loading'));
216           }
217
218           if (this.env.blockedobjects) {
219             if (this.gui_objects.remoteobjectsmsg)
220               this.gui_objects.remoteobjectsmsg.style.display = 'block';
221             this.enable_command('load-images', 'always-load', true);
222           }
223
224           // make preview/message frame visible
225           if (this.env.action == 'preview' && this.is_framed()) {
226             this.enable_command('compose', 'add-contact', false);
227             parent.rcmail.show_contentframe(true);
228           }
229         }
230         else if (this.env.action == 'compose') {
231           this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor'];
232
233           if (this.env.drafts_mailbox)
234             this.env.compose_commands.push('savedraft')
235
236           this.enable_command(this.env.compose_commands, 'identities', true);
237
238           if (this.env.spellcheck) {
239             this.env.spellcheck.spelling_state_observer = function(s){ ref.set_spellcheck_state(s); };
240             this.env.compose_commands.push('spellcheck')
241             this.set_spellcheck_state('ready');
242             if ($("input[name='_is_html']").val() == '1')
243               this.display_spellcheck_controls(false);
244           }
245
246           document.onmouseup = function(e){ return p.doc_mouse_up(e); };
247
248           // init message compose form
249           this.init_messageform();
250         }
251         // show printing dialog
252         else if (this.env.action == 'print' && this.env.uid)
253           if (bw.safari)
254             window.setTimeout('window.print()', 10);
255           else
256             window.print();
257
258         // get unread count for each mailbox
259         if (this.gui_objects.mailboxlist) {
260           this.env.unread_counts = {};
261           this.gui_objects.folderlist = this.gui_objects.mailboxlist;
262           this.http_request('getunread', '');
263         }
264
265         // ask user to send MDN
266         if (this.env.mdn_request && this.env.uid) {
267           var mdnurl = '_uid='+this.env.uid+'&_mbox='+urlencode(this.env.mailbox);
268           if (confirm(this.get_label('mdnrequest')))
269             this.http_post('sendmdn', mdnurl);
270           else
271             this.http_post('mark', mdnurl+'&_flag=mdnsent');
272         }
273
274         break;
275
276       case 'addressbook':
277         if (this.gui_objects.folderlist)
278           this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups);
279
280         if (this.gui_objects.contactslist) {
281
282           this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
283             {multiselect:true, draggable:this.gui_objects.folderlist?true:false, keyboard:true});
284           this.contact_list.row_init = function(row){ p.triggerEvent('insertrow', { cid:row.uid, row:row }); };
285           this.contact_list.addEventListener('keypress', function(o){ p.contactlist_keypress(o); });
286           this.contact_list.addEventListener('select', function(o){ p.contactlist_select(o); });
287           this.contact_list.addEventListener('dragstart', function(o){ p.drag_start(o); });
288           this.contact_list.addEventListener('dragmove', function(e){ p.drag_move(e); });
289           this.contact_list.addEventListener('dragend', function(e){ p.drag_end(e); });
290           this.contact_list.init();
291
292           if (this.env.cid)
293             this.contact_list.highlight_row(this.env.cid);
294
295           this.gui_objects.contactslist.parentNode.onmousedown = function(e){ return p.click_on_list(e); };
296           document.onmouseup = function(e){ return p.doc_mouse_up(e); };
297           if (this.gui_objects.qsearchbox) {
298             $(this.gui_objects.qsearchbox).focusin(function() { rcmail.contact_list.blur(); });
299           }
300
301           this.update_group_commands();
302         }
303
304         this.set_page_buttons();
305
306         if (this.env.cid) {
307           this.enable_command('show', 'edit', true);
308           // register handlers for group assignment via checkboxes
309           if (this.gui_objects.editform) {
310             $('input.groupmember').change(function() {
311               ref.group_member_change(this.checked ? 'add' : 'del', ref.env.cid, ref.env.source, this.value);
312             });
313           }
314         }
315
316         if (this.gui_objects.editform) {
317           this.enable_command('save', true);
318           if (this.env.action == 'add' || this.env.action == 'edit')
319               this.init_contact_form();
320         }
321         if (this.gui_objects.qsearchbox) {
322           this.enable_command('search', 'reset-search', 'moveto', true);
323         }
324
325         if (this.contact_list && this.contact_list.rowcount > 0)
326           this.enable_command('export', true);
327
328         this.enable_command('add', 'import', this.env.writable_source);
329         this.enable_command('list', 'listgroup', 'advanced-search', true);
330
331         // load contacts of selected source
332         if (!this.env.action)
333           this.command('list', this.env.source);
334         break;
335
336
337       case 'settings':
338         this.enable_command('preferences', 'identities', 'save', 'folders', true);
339
340         if (this.env.action == 'identities') {
341           this.enable_command('add', this.env.identities_level < 2);
342         }
343         else if (this.env.action == 'edit-identity' || this.env.action == 'add-identity') {
344           this.enable_command('add', this.env.identities_level < 2);
345           this.enable_command('save', 'delete', 'edit', 'toggle-editor', true);
346         }
347         else if (this.env.action == 'folders') {
348           this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', true);
349         }
350         else if (this.env.action == 'edit-folder' && this.gui_objects.editform) {
351           this.enable_command('save', 'folder-size', true);
352           parent.rcmail.env.messagecount = this.env.messagecount;
353           parent.rcmail.enable_command('purge', this.env.messagecount);
354           $("input[type='text']").first().select();
355         }
356
357         if (this.gui_objects.identitieslist) {
358           this.identity_list = new rcube_list_widget(this.gui_objects.identitieslist, {multiselect:false, draggable:false, keyboard:false});
359           this.identity_list.addEventListener('select', function(o){ p.identity_select(o); });
360           this.identity_list.init();
361           this.identity_list.focus();
362
363           if (this.env.iid)
364             this.identity_list.highlight_row(this.env.iid);
365         }
366         else if (this.gui_objects.sectionslist) {
367           this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:false});
368           this.sections_list.addEventListener('select', function(o){ p.section_select(o); });
369           this.sections_list.init();
370           this.sections_list.focus();
371         }
372         else if (this.gui_objects.subscriptionlist)
373           this.init_subscription_list();
374
375         break;
376
377       case 'login':
378         var input_user = $('#rcmloginuser');
379         input_user.bind('keyup', function(e){ return rcmail.login_user_keyup(e); });
380
381         if (input_user.val() == '')
382           input_user.focus();
383         else
384           $('#rcmloginpwd').focus();
385
386         // detect client timezone
387         $('#rcmlogintz').val(new Date().getTimezoneOffset() / -60);
388
389         // display 'loading' message on form submit, lock submit button
390         $('form').submit(function () {
391           $('input[type=submit]', this).prop('disabled', true);
392           rcmail.display_message('', 'loading');
393         });
394
395         this.enable_command('login', true);
396         break;
397
398       default:
399         break;
400       }
401
402     // prevent from form submit with Enter key in file input fields
403     if (bw.ie)
404       $('input[type=file]').keydown(function(e) { if (e.keyCode == '13') e.preventDefault(); });
405
406     // flag object as complete
407     this.loaded = true;
408
409     // show message
410     if (this.pending_message)
411       this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]);
412
413     // map implicit containers
414     if (this.gui_objects.folderlist)
415       this.gui_containers.foldertray = $(this.gui_objects.folderlist);
416
417     // trigger init event hook
418     this.triggerEvent('init', { task:this.task, action:this.env.action });
419
420     // execute all foreign onload scripts
421     // @deprecated
422     for (var i in this.onloads) {
423       if (typeof this.onloads[i] === 'string')
424         eval(this.onloads[i]);
425       else if (typeof this.onloads[i] === 'function')
426         this.onloads[i]();
427       }
428
429     // start keep-alive interval
430     this.start_keepalive();
431   };
432
433   this.log = function(msg)
434   {
435     if (window.console && console.log)
436       console.log(msg);
437   };
438
439   /*********************************************************/
440   /*********       client command interface        *********/
441   /*********************************************************/
442
443   // execute a specific command on the web client
444   this.command = function(command, props, obj)
445   {
446     if (obj && obj.blur)
447       obj.blur();
448
449     if (this.busy)
450       return false;
451
452     // command not supported or allowed
453     if (!this.commands[command]) {
454       // pass command to parent window
455       if (this.is_framed())
456         parent.rcmail.command(command, props);
457
458       return false;
459     }
460
461     // check input before leaving compose step
462     if (this.task=='mail' && this.env.action=='compose' && $.inArray(command, this.env.compose_commands)<0) {
463       if (this.cmp_hash != this.compose_field_hash() && !confirm(this.get_label('notsentwarning')))
464         return false;
465     }
466
467     // process external commands
468     if (typeof this.command_handlers[command] === 'function') {
469       var ret = this.command_handlers[command](props, obj);
470       return ret !== undefined ? ret : (obj ? false : true);
471     }
472     else if (typeof this.command_handlers[command] === 'string') {
473       var ret = window[this.command_handlers[command]](props, obj);
474       return ret !== undefined ? ret : (obj ? false : true);
475     }
476
477     // trigger plugin hooks
478     this.triggerEvent('actionbefore', {props:props, action:command});
479     var ret = this.triggerEvent('before'+command, props);
480     if (ret !== undefined) {
481       // abort if one the handlers returned false
482       if (ret === false)
483         return false;
484       else
485         props = ret;
486     }
487
488     // process internal command
489     switch (command) {
490
491       case 'login':
492         if (this.gui_objects.loginform)
493           this.gui_objects.loginform.submit();
494         break;
495
496       // commands to switch task
497       case 'mail':
498       case 'addressbook':
499       case 'settings':
500       case 'logout':
501         this.switch_task(command);
502         break;
503
504       case 'permaurl':
505         if (obj && obj.href && obj.target)
506           return true;
507         else if (this.env.permaurl)
508           parent.location.href = this.env.permaurl;
509         break;
510
511       case 'menu-open':
512       case 'menu-save':
513         this.triggerEvent(command, {props:props});
514         return false;
515
516       case 'open':
517         var uid;
518         if (uid = this.get_single_uid()) {
519           obj.href = '?_task='+this.env.task+'&_action=show&_mbox='+urlencode(this.env.mailbox)+'&_uid='+uid;
520           return true;
521         }
522         break;
523
524       case 'list':
525         if (this.task=='mail') {
526           if (!this.env.search_request || (props && props != this.env.mailbox))
527             this.reset_qsearch();
528
529           this.list_mailbox(props);
530
531           if (this.env.trash_mailbox && !this.env.flag_for_deletion)
532             this.set_alttext('delete', this.env.mailbox != this.env.trash_mailbox ? 'movemessagetotrash' : 'deletemessage');
533         }
534         else if (this.task == 'addressbook') {
535           if (!this.env.search_request || (props != this.env.source))
536             this.reset_qsearch();
537
538           this.list_contacts(props);
539           this.enable_command('add', 'import', this.env.writable_source);
540         }
541         break;
542
543       case 'load-headers':
544         this.load_headers(obj);
545         break;
546
547       case 'sort':
548         var sort_order, sort_col = props;
549
550         if (this.env.sort_col==sort_col)
551           sort_order = this.env.sort_order=='ASC' ? 'DESC' : 'ASC';
552         else
553           sort_order = 'ASC';
554
555         // set table header and update env
556         this.set_list_sorting(sort_col, sort_order);
557
558         // reload message list
559         this.list_mailbox('', '', sort_col+'_'+sort_order);
560         break;
561
562       case 'nextpage':
563         this.list_page('next');
564         break;
565
566       case 'lastpage':
567         this.list_page('last');
568         break;
569
570       case 'previouspage':
571         this.list_page('prev');
572         break;
573
574       case 'firstpage':
575         this.list_page('first');
576         break;
577
578       case 'expunge':
579         if (this.env.messagecount)
580           this.expunge_mailbox(this.env.mailbox);
581         break;
582
583       case 'purge':
584       case 'empty-mailbox':
585         if (this.env.messagecount)
586           this.purge_mailbox(this.env.mailbox);
587         break;
588
589       // common commands used in multiple tasks
590       case 'show':
591         if (this.task == 'mail') {
592           var uid = this.get_single_uid();
593           if (uid && (!this.env.uid || uid != this.env.uid)) {
594             if (this.env.mailbox == this.env.drafts_mailbox)
595               this.goto_url('compose', '_draft_uid='+uid+'&_mbox='+urlencode(this.env.mailbox), true);
596             else
597               this.show_message(uid);
598           }
599         }
600         else if (this.task == 'addressbook') {
601           var cid = props ? props : this.get_single_cid();
602           if (cid && !(this.env.action == 'show' && cid == this.env.cid))
603             this.load_contact(cid, 'show');
604         }
605         break;
606
607       case 'add':
608         if (this.task == 'addressbook')
609           this.load_contact(0, 'add');
610         else if (this.task == 'settings') {
611           this.identity_list.clear_selection();
612           this.load_identity(0, 'add-identity');
613         }
614         break;
615
616       case 'edit':
617         var cid;
618         if (this.task=='addressbook' && (cid = this.get_single_cid()))
619           this.load_contact(cid, 'edit');
620         else if (this.task=='settings' && props)
621           this.load_identity(props, 'edit-identity');
622         else if (this.task=='mail' && (cid = this.get_single_uid())) {
623           var url = (this.env.mailbox == this.env.drafts_mailbox) ? '_draft_uid=' : '_uid=';
624           this.goto_url('compose', url+cid+'&_mbox='+urlencode(this.env.mailbox), true);
625         }
626         break;
627
628       case 'save':
629         var input, form = this.gui_objects.editform;
630         if (form) {
631           // adv. search
632           if (this.env.action == 'search') {
633           }
634           // user prefs
635           else if ((input = $("input[name='_pagesize']", form)) && input.length && isNaN(parseInt(input.val()))) {
636             alert(this.get_label('nopagesizewarning'));
637             input.focus();
638             break;
639           }
640           // contacts/identities
641           else {
642             // reload form
643             if (props == 'reload') {
644               form.action += '?_reload=1';
645             }
646             else if (this.task == 'settings' && (this.env.identities_level % 2) == 0  &&
647               (input = $("input[name='_email']", form)) && input.length && !rcube_check_email(input.val())
648             ) {
649               alert(this.get_label('noemailwarning'));
650               input.focus();
651               break;
652             }
653
654             // clear empty input fields
655             $('input.placeholder').each(function(){ if (this.value == this._placeholder) this.value = ''; });
656           }
657
658           // add selected source (on the list)
659           if (parent.rcmail && parent.rcmail.env.source)
660             form.action = this.add_url(form.action, '_orig_source', parent.rcmail.env.source);
661
662           form.submit();
663         }
664         break;
665
666       case 'delete':
667         // mail task
668         if (this.task == 'mail')
669           this.delete_messages();
670         // addressbook task
671         else if (this.task == 'addressbook')
672           this.delete_contacts();
673         // user settings task
674         else if (this.task == 'settings')
675           this.delete_identity();
676         break;
677
678       // mail task commands
679       case 'move':
680       case 'moveto':
681         if (this.task == 'mail')
682           this.move_messages(props);
683         else if (this.task == 'addressbook' && this.drag_active)
684           this.copy_contact(null, props);
685         break;
686
687       case 'copy':
688         if (this.task == 'mail')
689           this.copy_messages(props);
690         break;
691
692       case 'mark':
693         if (props)
694           this.mark_message(props);
695         break;
696
697       case 'toggle_status':
698         if (props && !props._row)
699           break;
700
701         var uid, flag = 'read';
702
703         if (props._row.uid) {
704           uid = props._row.uid;
705
706           // toggle read/unread
707           if (this.message_list.rows[uid].deleted) {
708             flag = 'undelete';
709           }
710           else if (!this.message_list.rows[uid].unread)
711             flag = 'unread';
712         }
713
714         this.mark_message(flag, uid);
715         break;
716
717       case 'toggle_flag':
718         if (props && !props._row)
719           break;
720
721         var uid, flag = 'flagged';
722
723         if (props._row.uid) {
724           uid = props._row.uid;
725           // toggle flagged/unflagged
726           if (this.message_list.rows[uid].flagged)
727             flag = 'unflagged';
728           }
729         this.mark_message(flag, uid);
730         break;
731
732       case 'always-load':
733         if (this.env.uid && this.env.sender) {
734           this.add_contact(urlencode(this.env.sender));
735           window.setTimeout(function(){ ref.command('load-images'); }, 300);
736           break;
737         }
738
739       case 'load-images':
740         if (this.env.uid)
741           this.show_message(this.env.uid, true, this.env.action=='preview');
742         break;
743
744       case 'load-attachment':
745         var qstring = '_mbox='+urlencode(this.env.mailbox)+'&_uid='+this.env.uid+'&_part='+props.part;
746
747         // open attachment in frame if it's of a supported mimetype
748         if (this.env.uid && props.mimetype && this.env.mimetypes && $.inArray(props.mimetype, this.env.mimetypes)>=0) {
749           if (props.mimetype == 'text/html')
750             qstring += '&_safe=1';
751           this.attachment_win = window.open(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1', 'rcubemailattachment');
752           if (this.attachment_win) {
753             window.setTimeout(function(){ ref.attachment_win.focus(); }, 10);
754             break;
755           }
756         }
757
758         this.goto_url('get', qstring+'&_download=1', false);
759         break;
760
761       case 'select-all':
762         this.select_all_mode = props ? false : true;
763         this.dummy_select = true; // prevent msg opening if there's only one msg on the list
764         if (props == 'invert')
765           this.message_list.invert_selection();
766         else
767           this.message_list.select_all(props == 'page' ? '' : props);
768         this.dummy_select = null;
769         break;
770
771       case 'select-none':
772         this.select_all_mode = false;
773         this.message_list.clear_selection();
774         break;
775
776       case 'expand-all':
777         this.env.autoexpand_threads = 1;
778         this.message_list.expand_all();
779         break;
780
781       case 'expand-unread':
782         this.env.autoexpand_threads = 2;
783         this.message_list.collapse_all();
784         this.expand_unread();
785         break;
786
787       case 'collapse-all':
788         this.env.autoexpand_threads = 0;
789         this.message_list.collapse_all();
790         break;
791
792       case 'nextmessage':
793         if (this.env.next_uid)
794           this.show_message(this.env.next_uid, false, this.env.action=='preview');
795         break;
796
797       case 'lastmessage':
798         if (this.env.last_uid)
799           this.show_message(this.env.last_uid);
800         break;
801
802       case 'previousmessage':
803         if (this.env.prev_uid)
804           this.show_message(this.env.prev_uid, false, this.env.action=='preview');
805         break;
806
807       case 'firstmessage':
808         if (this.env.first_uid)
809           this.show_message(this.env.first_uid);
810         break;
811
812       case 'checkmail':
813         this.check_for_recent(true);
814         break;
815
816       case 'compose':
817         var url = this.env.comm_path+'&_action=compose';
818
819         if (this.task == 'mail') {
820           url += '&_mbox='+urlencode(this.env.mailbox);
821
822           if (this.env.mailbox == this.env.drafts_mailbox) {
823             var uid;
824             if (uid = this.get_single_uid())
825               url += '&_draft_uid='+uid;
826           }
827           else if (props)
828              url += '&_to='+urlencode(props);
829         }
830         // modify url if we're in addressbook
831         else if (this.task == 'addressbook') {
832           // switch to mail compose step directly
833           if (props && props.indexOf('@') > 0) {
834             url = this.get_task_url('mail', url);
835             this.redirect(url + '&_to='+urlencode(props));
836             break;
837           }
838
839           // use contact_id passed as command parameter
840           var n, len, a_cids = [];
841           if (props)
842             a_cids.push(props);
843           // get selected contacts
844           else if (this.contact_list) {
845             var selection = this.contact_list.get_selection();
846             for (n=0, len=selection.length; n<len; n++)
847               a_cids.push(selection[n]);
848           }
849
850           if (a_cids.length)
851             this.http_post('mailto', {_cid: a_cids.join(','), _source: this.env.source}, true);
852
853           break;
854         }
855
856         this.redirect(url);
857         break;
858
859       case 'spellcheck':
860         if (window.tinyMCE && tinyMCE.get(this.env.composebody)) {
861           tinyMCE.execCommand('mceSpellCheck', true);
862         }
863         else if (this.env.spellcheck && this.env.spellcheck.spellCheck && this.spellcheck_ready) {
864           this.env.spellcheck.spellCheck();
865           this.set_spellcheck_state('checking');
866         }
867         break;
868
869       case 'savedraft':
870         // Reset the auto-save timer
871         self.clearTimeout(this.save_timer);
872
873         if (!this.gui_objects.messageform)
874           break;
875
876         // if saving Drafts is disabled in main.inc.php
877         // or if compose form did not change
878         if (!this.env.drafts_mailbox || this.cmp_hash == this.compose_field_hash())
879           break;
880
881         var form = this.gui_objects.messageform,
882           msgid = this.set_busy(true, 'savingmessage');
883
884         form.target = "savetarget";
885         form._draft.value = '1';
886         form.action = this.add_url(form.action, '_unlock', msgid);
887         form.submit();
888         break;
889
890       case 'send':
891         if (!this.gui_objects.messageform)
892           break;
893
894         if (!this.check_compose_input())
895           break;
896
897         // Reset the auto-save timer
898         self.clearTimeout(this.save_timer);
899
900         // all checks passed, send message
901         var lang = this.spellcheck_lang(),
902           form = this.gui_objects.messageform,
903           msgid = this.set_busy(true, 'sendingmessage');
904
905         form.target = 'savetarget';
906         form._draft.value = '';
907         form.action = this.add_url(form.action, '_unlock', msgid);
908         form.action = this.add_url(form.action, '_lang', lang);
909         form.submit();
910
911         // clear timeout (sending could take longer)
912         clearTimeout(this.request_timer);
913         break;
914
915       case 'send-attachment':
916         // Reset the auto-save timer
917         self.clearTimeout(this.save_timer);
918
919         this.upload_file(props)
920         break;
921
922       case 'insert-sig':
923         this.change_identity($("[name='_from']")[0], true);
924         break;
925
926       case 'reply-all':
927       case 'reply-list':
928       case 'reply':
929         var uid;
930         if (uid = this.get_single_uid()) {
931           var url = '_reply_uid='+uid+'&_mbox='+urlencode(this.env.mailbox);
932           if (command == 'reply-all')
933             // do reply-list, when list is detected and popup menu wasn't used 
934             url += '&_all=' + (!props && this.commands['reply-list'] ? 'list' : 'all');
935           else if (command == 'reply-list')
936             url += '&_all=list';
937
938           this.goto_url('compose', url, true);
939         }
940         break;
941
942       case 'forward-attachment':
943       case 'forward':
944         var uid, url;
945         if (uid = this.get_single_uid()) {
946           url = '_forward_uid='+uid+'&_mbox='+urlencode(this.env.mailbox);
947           if (command == 'forward-attachment' || (!props && this.env.forward_attachment))
948             url += '&_attachment=1';
949           this.goto_url('compose', url, true);
950         }
951         break;
952
953       case 'print':
954         var uid;
955         if (uid = this.get_single_uid()) {
956           ref.printwin = window.open(this.env.comm_path+'&_action=print&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox)+(this.env.safemode ? '&_safe=1' : ''));
957           if (this.printwin) {
958             window.setTimeout(function(){ ref.printwin.focus(); }, 20);
959             if (this.env.action != 'show')
960               this.mark_message('read', uid);
961           }
962         }
963         break;
964
965       case 'viewsource':
966         var uid;
967         if (uid = this.get_single_uid()) {
968           ref.sourcewin = window.open(this.env.comm_path+'&_action=viewsource&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox));
969           if (this.sourcewin)
970             window.setTimeout(function(){ ref.sourcewin.focus(); }, 20);
971           }
972         break;
973
974       case 'download':
975         var uid;
976         if (uid = this.get_single_uid())
977           this.goto_url('viewsource', '&_uid='+uid+'&_mbox='+urlencode(this.env.mailbox)+'&_save=1');
978         break;
979
980       // quicksearch
981       case 'search':
982         if (!props && this.gui_objects.qsearchbox)
983           props = this.gui_objects.qsearchbox.value;
984         if (props) {
985           this.qsearch(props);
986           break;
987         }
988
989       // reset quicksearch
990       case 'reset-search':
991         var n, s = this.env.search_request || this.env.qsearch;
992
993         this.reset_qsearch();
994         this.select_all_mode = false;
995
996         if (s && this.env.mailbox)
997           this.list_mailbox(this.env.mailbox, 1);
998         else if (s && this.task == 'addressbook') {
999           if (this.env.source == '') {
1000             for (n in this.env.address_sources) break;
1001             this.env.source = n;
1002             this.env.group = '';
1003           }
1004           this.list_contacts(this.env.source, this.env.group, 1);
1005         }
1006         break;
1007
1008       case 'listgroup':
1009         this.list_contacts(props.source, props.id);
1010         break;
1011
1012       case 'import':
1013         if (this.env.action == 'import' && this.gui_objects.importform) {
1014           var file = document.getElementById('rcmimportfile');
1015           if (file && !file.value) {
1016             alert(this.get_label('selectimportfile'));
1017             break;
1018           }
1019           this.gui_objects.importform.submit();
1020           this.set_busy(true, 'importwait');
1021           this.lock_form(this.gui_objects.importform, true);
1022         }
1023         else
1024           this.goto_url('import', (this.env.source ? '_target='+urlencode(this.env.source)+'&' : ''));
1025         break;
1026
1027       case 'export':
1028         if (this.contact_list.rowcount > 0) {
1029           this.goto_url('export', { _source:this.env.source, _gid:this.env.group, _search:this.env.search_request });
1030         }
1031         break;
1032
1033       case 'upload-photo':
1034         this.upload_contact_photo(props);
1035         break;
1036
1037       case 'delete-photo':
1038         this.replace_contact_photo('-del-');
1039         break;
1040
1041       // user settings commands
1042       case 'preferences':
1043       case 'identities':
1044       case 'folders':
1045         this.goto_url('settings/' + command);
1046         break;
1047
1048       case 'undo':
1049         this.http_request('undo', '', this.display_message('', 'loading'));
1050         break;
1051
1052       // unified command call (command name == function name)
1053       default:
1054         var func = command.replace(/-/g, '_');
1055         if (this[func] && typeof this[func] === 'function')
1056           this[func](props);
1057         break;
1058     }
1059
1060     this.triggerEvent('after'+command, props);
1061     this.triggerEvent('actionafter', {props:props, action:command});
1062
1063     return obj ? false : true;
1064   };
1065
1066   // set command(s) enabled or disabled
1067   this.enable_command = function()
1068   {
1069     var args = Array.prototype.slice.call(arguments),
1070       enable = args.pop(), cmd;
1071
1072     for (var n=0; n<args.length; n++) {
1073       cmd = args[n];
1074       // argument of type array
1075       if (typeof cmd === 'string') {
1076         this.commands[cmd] = enable;
1077         this.set_button(cmd, (enable ? 'act' : 'pas'));
1078       }
1079       // push array elements into commands array
1080       else {
1081         for (var i in cmd)
1082           args.push(cmd[i]);
1083       }
1084     }
1085   };
1086
1087   // lock/unlock interface
1088   this.set_busy = function(a, message, id)
1089   {
1090     if (a && message) {
1091       var msg = this.get_label(message);
1092       if (msg == message)
1093         msg = 'Loading...';
1094
1095       id = this.display_message(msg, 'loading');
1096     }
1097     else if (!a && id) {
1098       this.hide_message(id);
1099     }
1100
1101     this.busy = a;
1102     //document.body.style.cursor = a ? 'wait' : 'default';
1103
1104     if (this.gui_objects.editform)
1105       this.lock_form(this.gui_objects.editform, a);
1106
1107     // clear pending timer
1108     if (this.request_timer)
1109       clearTimeout(this.request_timer);
1110
1111     // set timer for requests
1112     if (a && this.env.request_timeout)
1113       this.request_timer = window.setTimeout(function(){ ref.request_timed_out(); }, this.env.request_timeout * 1000);
1114
1115     return id;
1116   };
1117
1118   // return a localized string
1119   this.get_label = function(name, domain)
1120   {
1121     if (domain && this.labels[domain+'.'+name])
1122       return this.labels[domain+'.'+name];
1123     else if (this.labels[name])
1124       return this.labels[name];
1125     else
1126       return name;
1127   };
1128
1129   // alias for convenience reasons
1130   this.gettext = this.get_label;
1131
1132   // switch to another application task
1133   this.switch_task = function(task)
1134   {
1135     if (this.task===task && task!='mail')
1136       return;
1137
1138     var url = this.get_task_url(task);
1139     if (task=='mail')
1140       url += '&_mbox=INBOX';
1141
1142     this.redirect(url);
1143   };
1144
1145   this.get_task_url = function(task, url)
1146   {
1147     if (!url)
1148       url = this.env.comm_path;
1149
1150     return url.replace(/_task=[a-z]+/, '_task='+task);
1151   };
1152
1153   // called when a request timed out
1154   this.request_timed_out = function()
1155   {
1156     this.set_busy(false);
1157     this.display_message('Request timed out!', 'error');
1158   };
1159
1160   this.reload = function(delay)
1161   {
1162     if (this.is_framed())
1163       parent.rcmail.reload(delay);
1164     else if (delay)
1165       window.setTimeout(function(){ rcmail.reload(); }, delay);
1166     else if (window.location)
1167       location.href = this.env.comm_path + (this.env.action ? '&_action='+this.env.action : '');
1168   };
1169
1170   // Add variable to GET string, replace old value if exists
1171   this.add_url = function(url, name, value)
1172   {
1173     value = urlencode(value);
1174
1175     if (/(\?.*)$/.test(url)) {
1176       var urldata = RegExp.$1,
1177         datax = RegExp('((\\?|&)'+RegExp.escape(name)+'=[^&]*)');
1178
1179       if (datax.test(urldata)) {
1180         urldata = urldata.replace(datax, RegExp.$2 + name + '=' + value);
1181       }
1182       else
1183         urldata += '&' + name + '=' + value
1184
1185       return url.replace(/(\?.*)$/, urldata);
1186     }
1187     else
1188       return url + '?' + name + '=' + value;
1189   };
1190
1191   this.is_framed = function()
1192   {
1193     return (this.env.framed && parent.rcmail && parent.rcmail != this && parent.rcmail.command);
1194   };
1195
1196   this.save_pref = function(prop)
1197   {
1198     var request = {'_name': prop.name, '_value': prop.value};
1199
1200     if (prop.session)
1201       request['_session'] = prop.session;
1202     if (prop.env)
1203       this.env[prop.env] = prop.value;
1204
1205     this.http_post('save-pref', request);
1206   };
1207
1208
1209   /*********************************************************/
1210   /*********        event handling methods         *********/
1211   /*********************************************************/
1212
1213   this.drag_menu = function(e, target)
1214   {
1215     var modkey = rcube_event.get_modifier(e),
1216       menu = this.gui_objects.message_dragmenu;
1217
1218     if (menu && modkey == SHIFT_KEY && this.commands['copy']) {
1219       var pos = rcube_event.get_mouse_pos(e);
1220       this.env.drag_target = target;
1221       $(menu).css({top: (pos.y-10)+'px', left: (pos.x-10)+'px'}).show();
1222       return true;
1223     }
1224
1225     return false;
1226   };
1227
1228   this.drag_menu_action = function(action)
1229   {
1230     var menu = this.gui_objects.message_dragmenu;
1231     if (menu) {
1232       $(menu).hide();
1233     }
1234     this.command(action, this.env.drag_target);
1235     this.env.drag_target = null;
1236   };
1237
1238   this.drag_start = function(list)
1239   {
1240     var model = this.task == 'mail' ? this.env.mailboxes : this.env.contactfolders;
1241
1242     this.drag_active = true;
1243
1244     if (this.preview_timer)
1245       clearTimeout(this.preview_timer);
1246     if (this.preview_read_timer)
1247       clearTimeout(this.preview_read_timer);
1248
1249     // save folderlist and folders location/sizes for droptarget calculation in drag_move()
1250     if (this.gui_objects.folderlist && model) {
1251       this.initialBodyScrollTop = bw.ie ? 0 : window.pageYOffset;
1252       this.initialListScrollTop = this.gui_objects.folderlist.parentNode.scrollTop;
1253
1254       var li, pos, list, height;
1255       list = $(this.gui_objects.folderlist);
1256       pos = list.offset();
1257       this.env.folderlist_coords = { x1:pos.left, y1:pos.top, x2:pos.left + list.width(), y2:pos.top + list.height() };
1258
1259       this.env.folder_coords = [];
1260       for (var k in model) {
1261         if (li = this.get_folder_li(k)) {
1262           // only visible folders
1263           if (height = li.firstChild.offsetHeight) {
1264             pos = $(li.firstChild).offset();
1265             this.env.folder_coords[k] = { x1:pos.left, y1:pos.top,
1266               x2:pos.left + li.firstChild.offsetWidth, y2:pos.top + height, on:0 };
1267           }
1268         }
1269       }
1270     }
1271   };
1272
1273   this.drag_end = function(e)
1274   {
1275     this.drag_active = false;
1276     this.env.last_folder_target = null;
1277
1278     if (this.folder_auto_timer) {
1279       window.clearTimeout(this.folder_auto_timer);
1280       this.folder_auto_timer = null;
1281       this.folder_auto_expand = null;
1282     }
1283
1284     // over the folders
1285     if (this.gui_objects.folderlist && this.env.folder_coords) {
1286       for (var k in this.env.folder_coords) {
1287         if (this.env.folder_coords[k].on)
1288           $(this.get_folder_li(k)).removeClass('droptarget');
1289       }
1290     }
1291   };
1292
1293   this.drag_move = function(e)
1294   {
1295     if (this.gui_objects.folderlist && this.env.folder_coords) {
1296       // offsets to compensate for scrolling while dragging a message
1297       var boffset = bw.ie ? -document.documentElement.scrollTop : this.initialBodyScrollTop;
1298       var moffset = this.initialListScrollTop-this.gui_objects.folderlist.parentNode.scrollTop;
1299       var toffset = -moffset-boffset;
1300       var li, div, pos, mouse, check, oldclass,
1301         layerclass = 'draglayernormal';
1302
1303       if (this.contact_list && this.contact_list.draglayer)
1304         oldclass = this.contact_list.draglayer.attr('class');
1305
1306       mouse = rcube_event.get_mouse_pos(e);
1307       pos = this.env.folderlist_coords;
1308       mouse.y += toffset;
1309
1310       // if mouse pointer is outside of folderlist
1311       if (mouse.x < pos.x1 || mouse.x >= pos.x2 || mouse.y < pos.y1 || mouse.y >= pos.y2) {
1312         if (this.env.last_folder_target) {
1313           $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget');
1314           this.env.folder_coords[this.env.last_folder_target].on = 0;
1315           this.env.last_folder_target = null;
1316         }
1317         if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
1318           this.contact_list.draglayer.attr('class', layerclass);
1319         return;
1320       }
1321
1322       // over the folders
1323       for (var k in this.env.folder_coords) {
1324         pos = this.env.folder_coords[k];
1325         if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.y >= pos.y1 && mouse.y < pos.y2){
1326          if ((check = this.check_droptarget(k))) {
1327             li = this.get_folder_li(k);
1328             div = $(li.getElementsByTagName('div')[0]);
1329
1330             // if the folder is collapsed, expand it after 1sec and restart the drag & drop process.
1331             if (div.hasClass('collapsed')) {
1332               if (this.folder_auto_timer)
1333                 window.clearTimeout(this.folder_auto_timer);
1334
1335               this.folder_auto_expand = k;
1336               this.folder_auto_timer = window.setTimeout(function() {
1337                   rcmail.command('collapse-folder', rcmail.folder_auto_expand);
1338                   rcmail.drag_start(null);
1339                 }, 1000);
1340             } else if (this.folder_auto_timer) {
1341               window.clearTimeout(this.folder_auto_timer);
1342               this.folder_auto_timer = null;
1343               this.folder_auto_expand = null;
1344             }
1345
1346             $(li).addClass('droptarget');
1347             this.env.folder_coords[k].on = 1;
1348             this.env.last_folder_target = k;
1349             layerclass = 'draglayer' + (check > 1 ? 'copy' : 'normal');
1350           } else { // Clear target, otherwise drag end will trigger move into last valid droptarget
1351             this.env.last_folder_target = null;
1352           }
1353         }
1354         else if (pos.on) {
1355           $(this.get_folder_li(k)).removeClass('droptarget');
1356           this.env.folder_coords[k].on = 0;
1357         }
1358       }
1359
1360       if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
1361         this.contact_list.draglayer.attr('class', layerclass);
1362     }
1363   };
1364
1365   this.collapse_folder = function(id)
1366   {
1367     var li = this.get_folder_li(id),
1368       div = $(li.getElementsByTagName('div')[0]);
1369
1370     if (!div || (!div.hasClass('collapsed') && !div.hasClass('expanded')))
1371       return;
1372
1373     var ul = $(li.getElementsByTagName('ul')[0]);
1374
1375     if (div.hasClass('collapsed')) {
1376       ul.show();
1377       div.removeClass('collapsed').addClass('expanded');
1378       var reg = new RegExp('&'+urlencode(id)+'&');
1379       this.env.collapsed_folders = this.env.collapsed_folders.replace(reg, '');
1380     }
1381     else {
1382       ul.hide();
1383       div.removeClass('expanded').addClass('collapsed');
1384       this.env.collapsed_folders = this.env.collapsed_folders+'&'+urlencode(id)+'&';
1385
1386       // select parent folder if one of its childs is currently selected
1387       if (this.env.mailbox.indexOf(id + this.env.delimiter) == 0)
1388         this.command('list', id);
1389     }
1390
1391     // Work around a bug in IE6 and IE7, see #1485309
1392     if (bw.ie6 || bw.ie7) {
1393       var siblings = li.nextSibling ? li.nextSibling.getElementsByTagName('ul') : null;
1394       if (siblings && siblings.length && (li = siblings[0]) && li.style && li.style.display != 'none') {
1395         li.style.display = 'none';
1396         li.style.display = '';
1397       }
1398     }
1399
1400     this.command('save-pref', { name: 'collapsed_folders', value: this.env.collapsed_folders });
1401     this.set_unread_count_display(id, false);
1402   };
1403
1404   this.doc_mouse_up = function(e)
1405   {
1406     var model, list, li, id;
1407
1408     if (list = this.message_list) {
1409       if (!rcube_mouse_is_over(e, list.list.parentNode))
1410         list.blur();
1411       else
1412         list.focus();
1413       model = this.env.mailboxes;
1414     }
1415     else if (list = this.contact_list) {
1416       if (!rcube_mouse_is_over(e, list.list.parentNode))
1417         list.blur();
1418       else
1419         list.focus();
1420       model = this.env.contactfolders;
1421     }
1422     else if (this.ksearch_value) {
1423       this.ksearch_blur();
1424     }
1425
1426     // handle mouse release when dragging
1427     if (this.drag_active && model && this.env.last_folder_target) {
1428       var target = model[this.env.last_folder_target];
1429
1430       $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget');
1431       this.env.last_folder_target = null;
1432       list.draglayer.hide();
1433
1434       if (!this.drag_menu(e, target))
1435         this.command('moveto', target);
1436     }
1437
1438     // reset 'pressed' buttons
1439     if (this.buttons_sel) {
1440       for (id in this.buttons_sel)
1441         if (typeof id !== 'function')
1442           this.button_out(this.buttons_sel[id], id);
1443       this.buttons_sel = {};
1444     }
1445   };
1446
1447   this.click_on_list = function(e)
1448   {
1449     if (this.gui_objects.qsearchbox)
1450       this.gui_objects.qsearchbox.blur();
1451
1452     if (this.message_list)
1453       this.message_list.focus();
1454     else if (this.contact_list)
1455       this.contact_list.focus();
1456
1457     return true;
1458   };
1459
1460   this.msglist_select = function(list)
1461   {
1462     if (this.preview_timer)
1463       clearTimeout(this.preview_timer);
1464     if (this.preview_read_timer)
1465       clearTimeout(this.preview_read_timer);
1466
1467     var selected = list.get_single_selection() != null;
1468
1469     this.enable_command(this.env.message_commands, selected);
1470     if (selected) {
1471       // Hide certain command buttons when Drafts folder is selected
1472       if (this.env.mailbox == this.env.drafts_mailbox)
1473         this.enable_command('reply', 'reply-all', 'reply-list', 'forward', 'forward-attachment', false);
1474       // Disable reply-list when List-Post header is not set
1475       else {
1476         var msg = this.env.messages[list.get_single_selection()];
1477         if (!msg.ml)
1478           this.enable_command('reply-list', false);
1479       }
1480     }
1481     // Multi-message commands
1482     this.enable_command('delete', 'moveto', 'copy', 'mark', (list.selection.length > 0 ? true : false));
1483
1484     // reset all-pages-selection
1485     if (selected || (list.selection.length && list.selection.length != list.rowcount))
1486       this.select_all_mode = false;
1487
1488     // start timer for message preview (wait for double click)
1489     if (selected && this.env.contentframe && !list.multi_selecting && !this.dummy_select)
1490       this.preview_timer = window.setTimeout(function(){ ref.msglist_get_preview(); }, 200);
1491     else if (this.env.contentframe)
1492       this.show_contentframe(false);
1493   };
1494
1495   // This allow as to re-select selected message and display it in preview frame
1496   this.msglist_click = function(list)
1497   {
1498     if (list.multi_selecting || !this.env.contentframe)
1499       return;
1500
1501     if (list.get_single_selection() && window.frames && window.frames[this.env.contentframe]) {
1502       if (window.frames[this.env.contentframe].location.href.indexOf(this.env.blankpage)>=0) {
1503         if (this.preview_timer)
1504           clearTimeout(this.preview_timer);
1505         if (this.preview_read_timer)
1506           clearTimeout(this.preview_read_timer);
1507         this.preview_timer = window.setTimeout(function(){ ref.msglist_get_preview(); }, 200);
1508       }
1509     }
1510   };
1511
1512   this.msglist_dbl_click = function(list)
1513   {
1514     if (this.preview_timer)
1515       clearTimeout(this.preview_timer);
1516
1517     if (this.preview_read_timer)
1518       clearTimeout(this.preview_read_timer);
1519
1520     var uid = list.get_single_selection();
1521     if (uid && this.env.mailbox == this.env.drafts_mailbox)
1522       this.goto_url('compose', '_draft_uid='+uid+'&_mbox='+urlencode(this.env.mailbox), true);
1523     else if (uid)
1524       this.show_message(uid, false, false);
1525   };
1526
1527   this.msglist_keypress = function(list)
1528   {
1529     if (list.key_pressed == list.ENTER_KEY)
1530       this.command('show');
1531     else if (list.key_pressed == list.DELETE_KEY)
1532       this.command('delete');
1533     else if (list.key_pressed == list.BACKSPACE_KEY)
1534       this.command('delete');
1535     else if (list.key_pressed == 33)
1536       this.command('previouspage');
1537     else if (list.key_pressed == 34)
1538       this.command('nextpage');
1539   };
1540
1541   this.msglist_get_preview = function()
1542   {
1543     var uid = this.get_single_uid();
1544     if (uid && this.env.contentframe && !this.drag_active)
1545       this.show_message(uid, false, true);
1546     else if (this.env.contentframe)
1547       this.show_contentframe(false);
1548   };
1549
1550   this.msglist_expand = function(row)
1551   {
1552     if (this.env.messages[row.uid])
1553       this.env.messages[row.uid].expanded = row.expanded;
1554   };
1555
1556   this.msglist_set_coltypes = function(list)
1557   {
1558     var i, found, name, cols = list.list.tHead.rows[0].cells;
1559
1560     this.env.coltypes = [];
1561
1562     for (i=0; i<cols.length; i++)
1563       if (cols[i].id && cols[i].id.match(/^rcm/)) {
1564         name = cols[i].id.replace(/^rcm/, '');
1565         this.env.coltypes.push(name == 'to' ? 'from' : name);
1566       }
1567
1568     if ((found = $.inArray('flag', this.env.coltypes)) >= 0)
1569       this.env.flagged_col = found;
1570
1571     if ((found = $.inArray('subject', this.env.coltypes)) >= 0)
1572       this.env.subject_col = found;
1573
1574     this.command('save-pref', { name: 'list_cols', value: this.env.coltypes, session: 'list_attrib/columns' });
1575   };
1576
1577   this.check_droptarget = function(id)
1578   {
1579     var allow = false, copy = false;
1580
1581     if (this.task == 'mail')
1582       allow = (this.env.mailboxes[id] && this.env.mailboxes[id].id != this.env.mailbox && !this.env.mailboxes[id].virtual);
1583     else if (this.task == 'settings')
1584       allow = (id != this.env.mailbox);
1585     else if (this.task == 'addressbook') {
1586       if (id != this.env.source && this.env.contactfolders[id]) {
1587         if (this.env.contactfolders[id].type == 'group') {
1588           var target_abook = this.env.contactfolders[id].source;
1589           allow = this.env.contactfolders[id].id != this.env.group && !this.env.contactfolders[target_abook].readonly;
1590           copy = target_abook != this.env.source;
1591         }
1592         else {
1593           allow = !this.env.contactfolders[id].readonly;
1594           copy = true;
1595         }
1596       }
1597     }
1598
1599     return allow ? (copy ? 2 : 1) : 0;
1600   };
1601
1602
1603   /*********************************************************/
1604   /*********     (message) list functionality      *********/
1605   /*********************************************************/
1606
1607   this.init_message_row = function(row)
1608   {
1609     var expando, self = this, uid = row.uid,
1610       status_icon = (this.env.status_col != null ? 'status' : 'msg') + 'icn' + row.uid;
1611
1612     if (uid && this.env.messages[uid])
1613       $.extend(row, this.env.messages[uid]);
1614
1615     // set eventhandler to status icon
1616     if (row.icon = document.getElementById(status_icon)) {
1617       row.icon._row = row.obj;
1618       row.icon.onmousedown = function(e) { self.command('toggle_status', this); rcube_event.cancel(e); };
1619     }
1620
1621     // save message icon position too
1622     if (this.env.status_col != null)
1623       row.msgicon = document.getElementById('msgicn'+row.uid);
1624     else
1625       row.msgicon = row.icon;
1626
1627     // set eventhandler to flag icon, if icon found
1628     if (this.env.flagged_col != null && (row.flagicon = document.getElementById('flagicn'+row.uid))) {
1629       row.flagicon._row = row.obj;
1630       row.flagicon.onmousedown = function(e) { self.command('toggle_flag', this); rcube_event.cancel(e); };
1631     }
1632
1633     if (!row.depth && row.has_children && (expando = document.getElementById('rcmexpando'+row.uid))) {
1634       row.expando = expando;
1635       expando.onmousedown = function(e) { return self.expand_message_row(e, uid); };
1636     }
1637
1638     this.triggerEvent('insertrow', { uid:uid, row:row });
1639   };
1640
1641   // create a table row in the message list
1642   this.add_message_row = function(uid, cols, flags, attop)
1643   {
1644     if (!this.gui_objects.messagelist || !this.message_list)
1645       return false;
1646
1647     if (!this.env.messages[uid])
1648       this.env.messages[uid] = {};
1649
1650     // merge flags over local message object
1651     $.extend(this.env.messages[uid], {
1652       deleted: flags.deleted?1:0,
1653       replied: flags.replied?1:0,
1654       unread: flags.unread?1:0,
1655       forwarded: flags.forwarded?1:0,
1656       flagged: flags.flagged?1:0,
1657       has_children: flags.has_children?1:0,
1658       depth: flags.depth?flags.depth:0,
1659       unread_children: flags.unread_children?flags.unread_children:0,
1660       parent_uid: flags.parent_uid?flags.parent_uid:0,
1661       selected: this.select_all_mode || this.message_list.in_selection(uid),
1662       ml: flags.ml?1:0,
1663       ctype: flags.ctype,
1664       // flags from plugins
1665       flags: flags.extra_flags
1666     });
1667
1668     var c, html, tree = expando = '',
1669       list = this.message_list,
1670       rows = list.rows,
1671       tbody = this.gui_objects.messagelist.tBodies[0],
1672       rowcount = tbody.rows.length,
1673       even = rowcount%2,
1674       message = this.env.messages[uid],
1675       css_class = 'message'
1676         + (even ? ' even' : ' odd')
1677         + (flags.unread ? ' unread' : '')
1678         + (flags.deleted ? ' deleted' : '')
1679         + (flags.flagged ? ' flagged' : '')
1680         + (flags.unread_children && !flags.unread && !this.env.autoexpand_threads ? ' unroot' : '')
1681         + (message.selected ? ' selected' : ''),
1682       // for performance use DOM instead of jQuery here
1683       row = document.createElement('tr'),
1684       col = document.createElement('td');
1685
1686     row.id = 'rcmrow'+uid;
1687     row.className = css_class;
1688
1689     // message status icons
1690     css_class = 'msgicon';
1691     if (this.env.status_col === null) {
1692       css_class += ' status';
1693       if (flags.deleted)
1694         css_class += ' deleted';
1695       else if (flags.unread)
1696         css_class += ' unread';
1697       else if (flags.unread_children > 0)
1698         css_class += ' unreadchildren';
1699     }
1700     if (flags.replied)
1701       css_class += ' replied';
1702     if (flags.forwarded)
1703       css_class += ' forwarded';
1704
1705     // update selection
1706     if (message.selected && !list.in_selection(uid))
1707       list.selection.push(uid);
1708
1709     // threads
1710     if (this.env.threading) {
1711       // This assumes that div width is hardcoded to 15px,
1712       var width = message.depth * 15;
1713       if (message.depth) {
1714         if ((rows[message.parent_uid] && rows[message.parent_uid].expanded === false)
1715           || ((this.env.autoexpand_threads == 0 || this.env.autoexpand_threads == 2) &&
1716             (!rows[message.parent_uid] || !rows[message.parent_uid].expanded))
1717         ) {
1718           row.style.display = 'none';
1719           message.expanded = false;
1720         }
1721         else
1722           message.expanded = true;
1723       }
1724       else if (message.has_children) {
1725         if (message.expanded === undefined && (this.env.autoexpand_threads == 1 || (this.env.autoexpand_threads == 2 && message.unread_children))) {
1726           message.expanded = true;
1727         }
1728       }
1729
1730       if (width)
1731         tree += '<span id="rcmtab' + uid + '" class="branch" style="width:' + width + 'px;">&nbsp;&nbsp;</span>';
1732
1733       if (message.has_children && !message.depth)
1734         expando = '<div id="rcmexpando' + uid + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">&nbsp;&nbsp;</div>';
1735     }
1736
1737     tree += '<span id="msgicn'+uid+'" class="'+css_class+'">&nbsp;</span>';
1738
1739     // build subject link 
1740     if (!bw.ie && cols.subject) {
1741       var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show';
1742       var uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid';
1743       cols.subject = '<a href="./?_task=mail&_action='+action+'&_mbox='+urlencode(flags.mbox)+'&'+uid_param+'='+uid+'"'+
1744         ' onclick="return rcube_event.cancel(event)" onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')">'+cols.subject+'</a>';
1745     }
1746
1747     // add each submitted col
1748     for (var n in this.env.coltypes) {
1749       c = this.env.coltypes[n];
1750       col = document.createElement('td');
1751       col.className = String(c).toLowerCase();
1752
1753       if (c == 'flag') {
1754         css_class = (flags.flagged ? 'flagged' : 'unflagged');
1755         html = '<span id="flagicn'+uid+'" class="'+css_class+'">&nbsp;</span>';
1756       }
1757       else if (c == 'attachment') {
1758         if (/application\/|multipart\/m/.test(flags.ctype))
1759           html = '<span class="attachment">&nbsp;</span>';
1760         else if (/multipart\/report/.test(flags.ctype))
1761           html = '<span class="report">&nbsp;</span>';
1762         else
1763           html = '&nbsp;';
1764       }
1765       else if (c == 'status') {
1766         if (flags.deleted)
1767           css_class = 'deleted';
1768         else if (flags.unread)
1769           css_class = 'unread';
1770         else if (flags.unread_children > 0)
1771           css_class = 'unreadchildren';
1772         else
1773           css_class = 'msgicon';
1774         html = '<span id="statusicn'+uid+'" class="'+css_class+'">&nbsp;</span>';
1775       }
1776       else if (c == 'threads')
1777         html = expando;
1778       else if (c == 'subject')
1779         html = tree + cols[c];
1780       else
1781         html = cols[c];
1782
1783       col.innerHTML = html;
1784
1785       row.appendChild(col);
1786     }
1787
1788     list.insert_row(row, attop);
1789
1790     // remove 'old' row
1791     if (attop && this.env.pagesize && list.rowcount > this.env.pagesize) {
1792       var uid = list.get_last_row();
1793       list.remove_row(uid);
1794       list.clear_selection(uid);
1795     }
1796   };
1797
1798   this.set_list_sorting = function(sort_col, sort_order)
1799   {
1800     // set table header class
1801     $('#rcm'+this.env.sort_col).removeClass('sorted'+(this.env.sort_order.toUpperCase()));
1802     if (sort_col)
1803       $('#rcm'+sort_col).addClass('sorted'+sort_order);
1804
1805     this.env.sort_col = sort_col;
1806     this.env.sort_order = sort_order;
1807   };
1808
1809   this.set_list_options = function(cols, sort_col, sort_order, threads)
1810   {
1811     var update, add_url = '';
1812
1813     if (sort_col === undefined)
1814       sort_col = this.env.sort_col;
1815     if (!sort_order)
1816       sort_order = this.env.sort_order;
1817
1818     if (this.env.sort_col != sort_col || this.env.sort_order != sort_order) {
1819       update = 1;
1820       this.set_list_sorting(sort_col, sort_order);
1821     }
1822
1823     if (this.env.threading != threads) {
1824       update = 1;
1825       add_url += '&_threads=' + threads;
1826     }
1827
1828     if (cols && cols.length) {
1829       // make sure new columns are added at the end of the list
1830       var i, idx, name, newcols = [], oldcols = this.env.coltypes;
1831       for (i=0; i<oldcols.length; i++) {
1832         name = oldcols[i] == 'to' ? 'from' : oldcols[i];
1833         idx = $.inArray(name, cols);
1834         if (idx != -1) {
1835           newcols.push(name);
1836           delete cols[idx];
1837         }
1838       }
1839       for (i=0; i<cols.length; i++)
1840         if (cols[i])
1841           newcols.push(cols[i]);
1842
1843       if (newcols.join() != oldcols.join()) {
1844         update = 1;
1845         add_url += '&_cols=' + newcols.join(',');
1846       }
1847     }
1848
1849     if (update)
1850       this.list_mailbox('', '', sort_col+'_'+sort_order, add_url);
1851   };
1852
1853   // when user doble-clicks on a row
1854   this.show_message = function(id, safe, preview)
1855   {
1856     if (!id)
1857       return;
1858
1859     var target = window,
1860       action = preview ? 'preview': 'show',
1861       url = '&_action='+action+'&_uid='+id+'&_mbox='+urlencode(this.env.mailbox);
1862
1863     if (preview && this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
1864       target = window.frames[this.env.contentframe];
1865       url += '&_framed=1';
1866     }
1867
1868     if (safe)
1869       url += '&_safe=1';
1870
1871     // also send search request to get the right messages
1872     if (this.env.search_request)
1873       url += '&_search='+this.env.search_request;
1874
1875     if (action == 'preview' && String(target.location.href).indexOf(url) >= 0)
1876       this.show_contentframe(true);
1877     else {
1878       this.location_href(this.env.comm_path+url, target, true);
1879
1880       // mark as read and change mbox unread counter
1881       if (action == 'preview' && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read >= 0) {
1882         this.preview_read_timer = window.setTimeout(function() {
1883           ref.set_message(id, 'unread', false);
1884           ref.update_thread_root(id, 'read');
1885           if (ref.env.unread_counts[ref.env.mailbox]) {
1886             ref.env.unread_counts[ref.env.mailbox] -= 1;
1887             ref.set_unread_count(ref.env.mailbox, ref.env.unread_counts[ref.env.mailbox], ref.env.mailbox == 'INBOX');
1888           }
1889           if (ref.env.preview_pane_mark_read > 0)
1890             ref.http_post('mark', '_uid='+id+'&_flag=read&_quiet=1');
1891         }, this.env.preview_pane_mark_read * 1000);
1892       }
1893     }
1894   };
1895
1896   this.show_contentframe = function(show)
1897   {
1898     var frm, win;
1899     if (this.env.contentframe && (frm = $('#'+this.env.contentframe)) && frm.length) {
1900       if (!show && (win = window.frames[this.env.contentframe])) {
1901         if (win.location && win.location.href.indexOf(this.env.blankpage)<0)
1902           win.location.href = this.env.blankpage;
1903       }
1904       else if (!bw.safari && !bw.konq)
1905         frm[show ? 'show' : 'hide']();
1906       }
1907
1908     if (!show && this.busy)
1909       this.set_busy(false, null, this.env.frame_lock);
1910   };
1911
1912   this.lock_frame = function()
1913   {
1914     if (!this.env.frame_lock)
1915       (this.is_framed() ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
1916   };
1917
1918   // list a specific page
1919   this.list_page = function(page)
1920   {
1921     if (page == 'next')
1922       page = this.env.current_page+1;
1923     else if (page == 'last')
1924       page = this.env.pagecount;
1925     else if (page == 'prev' && this.env.current_page > 1)
1926       page = this.env.current_page-1;
1927     else if (page == 'first' && this.env.current_page > 1)
1928       page = 1;
1929
1930     if (page > 0 && page <= this.env.pagecount) {
1931       this.env.current_page = page;
1932
1933       if (this.task == 'mail')
1934         this.list_mailbox(this.env.mailbox, page);
1935       else if (this.task == 'addressbook')
1936         this.list_contacts(this.env.source, this.env.group, page);
1937     }
1938   };
1939
1940   // list messages of a specific mailbox using filter
1941   this.filter_mailbox = function(filter)
1942   {
1943     var search, lock = this.set_busy(true, 'searching');
1944
1945     if (this.gui_objects.qsearchbox)
1946       search = this.gui_objects.qsearchbox.value;
1947
1948     this.clear_message_list();
1949
1950     // reset vars
1951     this.env.current_page = 1;
1952     this.http_request('search', '_filter='+filter
1953         + (search ? '&_q='+urlencode(search) : '')
1954         + (this.env.mailbox ? '&_mbox='+urlencode(this.env.mailbox) : ''), lock);
1955   };
1956
1957   // list messages of a specific mailbox
1958   this.list_mailbox = function(mbox, page, sort, add_url)
1959   {
1960     var url = '', target = window;
1961
1962     if (!mbox)
1963       mbox = this.env.mailbox ? this.env.mailbox : 'INBOX';
1964
1965     if (add_url)
1966       url += add_url;
1967
1968     // add sort to url if set
1969     if (sort)
1970       url += '&_sort=' + sort;
1971
1972     // also send search request to get the right messages
1973     if (this.env.search_request)
1974       url += '&_search='+this.env.search_request;
1975
1976     // set page=1 if changeing to another mailbox
1977     if (this.env.mailbox != mbox) {
1978       page = 1;
1979       this.env.current_page = page;
1980       this.select_all_mode = false;
1981     }
1982
1983     // unselect selected messages and clear the list and message data
1984     this.clear_message_list();
1985
1986     if (mbox != this.env.mailbox || (mbox == this.env.mailbox && !page && !sort))
1987       url += '&_refresh=1';
1988
1989     this.select_folder(mbox, this.env.mailbox);
1990     this.env.mailbox = mbox;
1991
1992     // load message list remotely
1993     if (this.gui_objects.messagelist) {
1994       this.list_mailbox_remote(mbox, page, url);
1995       return;
1996     }
1997
1998     if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
1999       target = window.frames[this.env.contentframe];
2000       url += '&_framed=1';
2001     }
2002
2003     // load message list to target frame/window
2004     if (mbox) {
2005       this.set_busy(true, 'loading');
2006       this.location_href(this.env.comm_path+'&_mbox='+urlencode(mbox)+(page ? '&_page='+page : '')+url, target);
2007     }
2008   };
2009
2010   this.clear_message_list = function()
2011   {
2012       this.env.messages = {};
2013       this.last_selected = 0;
2014
2015       this.show_contentframe(false);
2016       if (this.message_list)
2017         this.message_list.clear(true);
2018   };
2019
2020   // send remote request to load message list
2021   this.list_mailbox_remote = function(mbox, page, add_url)
2022   {
2023     // clear message list first
2024     this.message_list.clear();
2025
2026     // send request to server
2027     var url = '_mbox='+urlencode(mbox)+(page ? '&_page='+page : ''),
2028       lock = this.set_busy(true, 'loading');
2029     this.http_request('list', url+add_url, lock);
2030   };
2031
2032   // removes messages that doesn't exists from list selection array
2033   this.update_selection = function()
2034   {
2035     var selected = this.message_list.selection,
2036       rows = this.message_list.rows,
2037       i, selection = [];
2038
2039     for (i in selected)
2040       if (rows[selected[i]])
2041         selection.push(selected[i]);
2042
2043     this.message_list.selection = selection;
2044   }
2045
2046   // expand all threads with unread children
2047   this.expand_unread = function()
2048   {
2049     var r, tbody = this.gui_objects.messagelist.tBodies[0],
2050       new_row = tbody.firstChild;
2051
2052     while (new_row) {
2053       if (new_row.nodeType == 1 && (r = this.message_list.rows[new_row.uid])
2054             && r.unread_children) {
2055             this.message_list.expand_all(r);
2056             this.set_unread_children(r.uid);
2057       }
2058       new_row = new_row.nextSibling;
2059     }
2060     return false;
2061   };
2062
2063   // thread expanding/collapsing handler
2064   this.expand_message_row = function(e, uid)
2065   {
2066     var row = this.message_list.rows[uid];
2067
2068     // handle unread_children mark
2069     row.expanded = !row.expanded;
2070     this.set_unread_children(uid);
2071     row.expanded = !row.expanded;
2072
2073     this.message_list.expand_row(e, uid);
2074   };
2075
2076   // message list expanding
2077   this.expand_threads = function()
2078   {
2079     if (!this.env.threading || !this.env.autoexpand_threads || !this.message_list)
2080       return;
2081
2082     switch (this.env.autoexpand_threads) {
2083       case 2: this.expand_unread(); break;
2084       case 1: this.message_list.expand_all(); break;
2085     }
2086   };
2087
2088   // Initializes threads indicators/expanders after list update
2089   this.init_threads = function(roots)
2090   {
2091     for (var n=0, len=roots.length; n<len; n++)
2092       this.add_tree_icons(roots[n]);
2093     this.expand_threads();
2094   };
2095
2096   // adds threads tree icons to the list (or specified thread)
2097   this.add_tree_icons = function(root)
2098   {
2099     var i, l, r, n, len, pos, tmp = [], uid = [],
2100       row, rows = this.message_list.rows;
2101
2102     if (root)
2103       row = rows[root] ? rows[root].obj : null;
2104     else
2105       row = this.message_list.list.tBodies[0].firstChild;
2106
2107     while (row) {
2108       if (row.nodeType == 1 && (r = rows[row.uid])) {
2109         if (r.depth) {
2110           for (i=tmp.length-1; i>=0; i--) {
2111             len = tmp[i].length;
2112             if (len > r.depth) {
2113               pos = len - r.depth;
2114               if (!(tmp[i][pos] & 2))
2115                 tmp[i][pos] = tmp[i][pos] ? tmp[i][pos]+2 : 2;
2116             }
2117             else if (len == r.depth) {
2118               if (!(tmp[i][0] & 2))
2119                 tmp[i][0] += 2;
2120             }
2121             if (r.depth > len)
2122               break;
2123           }
2124
2125           tmp.push(new Array(r.depth));
2126           tmp[tmp.length-1][0] = 1;
2127           uid.push(r.uid);
2128         }
2129         else {
2130           if (tmp.length) {
2131             for (i in tmp) {
2132               this.set_tree_icons(uid[i], tmp[i]);
2133             }
2134             tmp = [];
2135             uid = [];
2136           }
2137           if (root && row != rows[root].obj)
2138             break;
2139         }
2140       }
2141       row = row.nextSibling;
2142     }
2143
2144     if (tmp.length) {
2145       for (i in tmp) {
2146         this.set_tree_icons(uid[i], tmp[i]);
2147       }
2148     }
2149   };
2150
2151   // adds tree icons to specified message row
2152   this.set_tree_icons = function(uid, tree)
2153   {
2154     var i, divs = [], html = '', len = tree.length;
2155
2156     for (i=0; i<len; i++) {
2157       if (tree[i] > 2)
2158         divs.push({'class': 'l3', width: 15});
2159       else if (tree[i] > 1)
2160         divs.push({'class': 'l2', width: 15});
2161       else if (tree[i] > 0)
2162         divs.push({'class': 'l1', width: 15});
2163       // separator div
2164       else if (divs.length && !divs[divs.length-1]['class'])
2165         divs[divs.length-1].width += 15;
2166       else
2167         divs.push({'class': null, width: 15});
2168     }
2169
2170     for (i=divs.length-1; i>=0; i--) {
2171       if (divs[i]['class'])
2172         html += '<div class="tree '+divs[i]['class']+'" />';
2173       else
2174         html += '<div style="width:'+divs[i].width+'px" />';
2175     }
2176
2177     if (html)
2178       $('#rcmtab'+uid).html(html);
2179   };
2180
2181   // update parent in a thread
2182   this.update_thread_root = function(uid, flag)
2183   {
2184     if (!this.env.threading)
2185       return;
2186
2187     var root = this.message_list.find_root(uid);
2188
2189     if (uid == root)
2190       return;
2191
2192     var p = this.message_list.rows[root];
2193
2194     if (flag == 'read' && p.unread_children) {
2195       p.unread_children--;
2196     }
2197     else if (flag == 'unread' && p.has_children) {
2198       // unread_children may be undefined
2199       p.unread_children = p.unread_children ? p.unread_children + 1 : 1;
2200     }
2201     else {
2202       return;
2203     }
2204
2205     this.set_message_icon(root);
2206     this.set_unread_children(root);
2207   };
2208
2209   // update thread indicators for all messages in a thread below the specified message
2210   // return number of removed/added root level messages
2211   this.update_thread = function (uid)
2212   {
2213     if (!this.env.threading)
2214       return 0;
2215
2216     var r, parent, count = 0,
2217       rows = this.message_list.rows,
2218       row = rows[uid],
2219       depth = rows[uid].depth,
2220       roots = [];
2221
2222     if (!row.depth) // root message: decrease roots count
2223       count--;
2224     else if (row.unread) {
2225       // update unread_children for thread root
2226       parent = this.message_list.find_root(uid);
2227       rows[parent].unread_children--;
2228       this.set_unread_children(parent);
2229     }
2230
2231     parent = row.parent_uid;
2232
2233     // childrens
2234     row = row.obj.nextSibling;
2235     while (row) {
2236       if (row.nodeType == 1 && (r = rows[row.uid])) {
2237             if (!r.depth || r.depth <= depth)
2238               break;
2239
2240             r.depth--; // move left
2241         // reset width and clear the content of a tab, icons will be added later
2242             $('#rcmtab'+r.uid).width(r.depth * 15).html('');
2243         if (!r.depth) { // a new root
2244               count++; // increase roots count
2245               r.parent_uid = 0;
2246               if (r.has_children) {
2247                 // replace 'leaf' with 'collapsed'
2248                 $('#rcmrow'+r.uid+' '+'.leaf:first')
2249               .attr('id', 'rcmexpando' + r.uid)
2250                   .attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed'))
2251               .bind('mousedown', {uid:r.uid, p:this},
2252                     function(e) { return e.data.p.expand_message_row(e, e.data.uid); });
2253
2254                 r.unread_children = 0;
2255                 roots.push(r);
2256               }
2257               // show if it was hidden
2258               if (r.obj.style.display == 'none')
2259                 $(r.obj).show();
2260             }
2261             else {
2262               if (r.depth == depth)
2263                 r.parent_uid = parent;
2264               if (r.unread && roots.length)
2265                 roots[roots.length-1].unread_children++;
2266             }
2267           }
2268           row = row.nextSibling;
2269     }
2270
2271     // update unread_children for roots
2272     for (var i=0; i<roots.length; i++)
2273       this.set_unread_children(roots[i].uid);
2274
2275     return count;
2276   };
2277
2278   this.delete_excessive_thread_rows = function()
2279   {
2280     var rows = this.message_list.rows,
2281       tbody = this.message_list.list.tBodies[0],
2282       row = tbody.firstChild,
2283       cnt = this.env.pagesize + 1;
2284
2285     while (row) {
2286       if (row.nodeType == 1 && (r = rows[row.uid])) {
2287             if (!r.depth && cnt)
2288               cnt--;
2289
2290         if (!cnt)
2291               this.message_list.remove_row(row.uid);
2292           }
2293           row = row.nextSibling;
2294     }
2295   };
2296
2297   // set message icon
2298   this.set_message_icon = function(uid)
2299   {
2300     var css_class,
2301       row = this.message_list.rows[uid];
2302
2303     if (!row)
2304       return false;
2305
2306     if (row.icon) {
2307       css_class = 'msgicon';
2308       if (row.deleted)
2309         css_class += ' deleted';
2310       else if (row.unread)
2311         css_class += ' unread';
2312       else if (row.unread_children)
2313         css_class += ' unreadchildren';
2314       if (row.msgicon == row.icon) {
2315         if (row.replied)
2316           css_class += ' replied';
2317         if (row.forwarded)
2318           css_class += ' forwarded';
2319         css_class += ' status';
2320       }
2321
2322       row.icon.className = css_class;
2323     }
2324
2325     if (row.msgicon && row.msgicon != row.icon) {
2326       css_class = 'msgicon';
2327       if (!row.unread && row.unread_children)
2328         css_class += ' unreadchildren';
2329       if (row.replied)
2330         css_class += ' replied';
2331       if (row.forwarded)
2332         css_class += ' forwarded';
2333
2334       row.msgicon.className = css_class;
2335     }
2336
2337     if (row.flagicon) {
2338       css_class = (row.flagged ? 'flagged' : 'unflagged');
2339       row.flagicon.className = css_class;
2340     }
2341   };
2342
2343   // set message status
2344   this.set_message_status = function(uid, flag, status)
2345   {
2346     var row = this.message_list.rows[uid];
2347
2348     if (!row)
2349       return false;
2350
2351     if (flag == 'unread')
2352       row.unread = status;
2353     else if(flag == 'deleted')
2354       row.deleted = status;
2355     else if (flag == 'replied')
2356       row.replied = status;
2357     else if (flag == 'forwarded')
2358       row.forwarded = status;
2359     else if (flag == 'flagged')
2360       row.flagged = status;
2361   };
2362
2363   // set message row status, class and icon
2364   this.set_message = function(uid, flag, status)
2365   {
2366     var row = this.message_list.rows[uid];
2367
2368     if (!row)
2369       return false;
2370
2371     if (flag)
2372       this.set_message_status(uid, flag, status);
2373
2374     var rowobj = $(row.obj);
2375
2376     if (row.unread && !rowobj.hasClass('unread'))
2377       rowobj.addClass('unread');
2378     else if (!row.unread && rowobj.hasClass('unread'))
2379       rowobj.removeClass('unread');
2380
2381     if (row.deleted && !rowobj.hasClass('deleted'))
2382       rowobj.addClass('deleted');
2383     else if (!row.deleted && rowobj.hasClass('deleted'))
2384       rowobj.removeClass('deleted');
2385
2386     if (row.flagged && !rowobj.hasClass('flagged'))
2387       rowobj.addClass('flagged');
2388     else if (!row.flagged && rowobj.hasClass('flagged'))
2389       rowobj.removeClass('flagged');
2390
2391     this.set_unread_children(uid);
2392     this.set_message_icon(uid);
2393   };
2394
2395   // sets unroot (unread_children) class of parent row
2396   this.set_unread_children = function(uid)
2397   {
2398     var row = this.message_list.rows[uid];
2399
2400     if (row.parent_uid)
2401       return;
2402
2403     if (!row.unread && row.unread_children && !row.expanded)
2404       $(row.obj).addClass('unroot');
2405     else
2406       $(row.obj).removeClass('unroot');
2407   };
2408
2409   // copy selected messages to the specified mailbox
2410   this.copy_messages = function(mbox)
2411   {
2412     if (mbox && typeof mbox === 'object')
2413       mbox = mbox.id;
2414
2415     // exit if current or no mailbox specified or if selection is empty
2416     if (!mbox || mbox == this.env.mailbox || (!this.env.uid && (!this.message_list || !this.message_list.get_selection().length)))
2417       return;
2418
2419     var a_uids = [],
2420       lock = this.display_message(this.get_label('copyingmessage'), 'loading'),
2421       add_url = '&_target_mbox='+urlencode(mbox)+'&_from='+(this.env.action ? this.env.action : '');
2422
2423     if (this.env.uid)
2424       a_uids[0] = this.env.uid;
2425     else {
2426       var selection = this.message_list.get_selection();
2427       for (var n in selection) {
2428         a_uids.push(selection[n]);
2429       }
2430     }
2431
2432     add_url += '&_uid='+this.uids_to_list(a_uids);
2433
2434     // send request to server
2435     this.http_post('copy', '_mbox='+urlencode(this.env.mailbox)+add_url, lock);
2436   };
2437
2438   // move selected messages to the specified mailbox
2439   this.move_messages = function(mbox)
2440   {
2441     if (mbox && typeof mbox === 'object')
2442       mbox = mbox.id;
2443
2444     // exit if current or no mailbox specified or if selection is empty
2445     if (!mbox || mbox == this.env.mailbox || (!this.env.uid && (!this.message_list || !this.message_list.get_selection().length)))
2446       return;
2447
2448     var lock = false,
2449       add_url = '&_target_mbox='+urlencode(mbox)+'&_from='+(this.env.action ? this.env.action : '');
2450
2451     // show wait message
2452     if (this.env.action == 'show') {
2453       lock = this.set_busy(true, 'movingmessage');
2454     }
2455     else
2456       this.show_contentframe(false);
2457
2458     // Hide message command buttons until a message is selected
2459     this.enable_command(this.env.message_commands, false);
2460
2461     this._with_selected_messages('moveto', lock, add_url);
2462   };
2463
2464   // delete selected messages from the current mailbox
2465   this.delete_messages = function()
2466   {
2467     var uid, i, len, trash = this.env.trash_mailbox,
2468       list = this.message_list,
2469       selection = list ? $.merge([], list.get_selection()) : [];
2470
2471     // exit if no mailbox specified or if selection is empty
2472     if (!this.env.uid && !selection.length)
2473       return;
2474
2475     // also select childs of collapsed rows
2476     for (i=0, len=selection.length; i<len; i++) {
2477       uid = selection[i];
2478       if (list.rows[uid].has_children && !list.rows[uid].expanded)
2479         list.select_childs(uid);
2480     }
2481
2482     // if config is set to flag for deletion
2483     if (this.env.flag_for_deletion) {
2484       this.mark_message('delete');
2485       return false;
2486     }
2487     // if there isn't a defined trash mailbox or we are in it
2488     // @TODO: we should check if defined trash mailbox exists
2489     else if (!trash || this.env.mailbox == trash)
2490       this.permanently_remove_messages();
2491     // if there is a trash mailbox defined and we're not currently in it
2492     else {
2493       // if shift was pressed delete it immediately
2494       if (list && list.shiftkey) {
2495         if (confirm(this.get_label('deletemessagesconfirm')))
2496           this.permanently_remove_messages();
2497       }
2498       else
2499         this.move_messages(trash);
2500     }
2501
2502     return true;
2503   };
2504
2505   // delete the selected messages permanently
2506   this.permanently_remove_messages = function()
2507   {
2508     // exit if no mailbox specified or if selection is empty
2509     if (!this.env.uid && (!this.message_list || !this.message_list.get_selection().length))
2510       return;
2511
2512     this.show_contentframe(false);
2513     this._with_selected_messages('delete', false, '&_from='+(this.env.action ? this.env.action : ''));
2514   };
2515
2516   // Send a specifc moveto/delete request with UIDs of all selected messages
2517   // @private
2518   this._with_selected_messages = function(action, lock, add_url)
2519   {
2520     var a_uids = [], count = 0, msg;
2521
2522     if (this.env.uid)
2523       a_uids[0] = this.env.uid;
2524     else {
2525       var n, id, root, roots = [],
2526         selection = this.message_list.get_selection();
2527
2528       for (n=0, len=selection.length; n<len; n++) {
2529         id = selection[n];
2530         a_uids.push(id);
2531
2532         if (this.env.threading) {
2533           count += this.update_thread(id);
2534           root = this.message_list.find_root(id);
2535           if (root != id && $.inArray(root, roots) < 0) {
2536             roots.push(root);
2537           }
2538         }
2539         this.message_list.remove_row(id, (this.env.display_next && n == selection.length-1));
2540       }
2541       // make sure there are no selected rows
2542       if (!this.env.display_next)
2543         this.message_list.clear_selection();
2544       // update thread tree icons
2545       for (n=0, len=roots.length; n<len; n++) {
2546         this.add_tree_icons(roots[n]);
2547       }
2548     }
2549
2550     // also send search request to get the right messages
2551     if (this.env.search_request)
2552       add_url += '&_search='+this.env.search_request;
2553
2554     if (this.env.display_next && this.env.next_uid)
2555       add_url += '&_next_uid='+this.env.next_uid;
2556
2557     if (count < 0)
2558       add_url += '&_count='+(count*-1);
2559     else if (count > 0) 
2560       // remove threads from the end of the list
2561       this.delete_excessive_thread_rows();
2562
2563     add_url += '&_uid='+this.uids_to_list(a_uids);
2564
2565     if (!lock) {
2566       msg = action == 'moveto' ? 'movingmessage' : 'deletingmessage';
2567       lock = this.display_message(this.get_label(msg), 'loading');
2568     }
2569
2570     // send request to server
2571     this.http_post(action, '_mbox='+urlencode(this.env.mailbox)+add_url, lock);
2572   };
2573
2574   // set a specific flag to one or more messages
2575   this.mark_message = function(flag, uid)
2576   {
2577     var a_uids = [], r_uids = [], len, n, id,
2578       selection = this.message_list ? this.message_list.get_selection() : [];
2579
2580     if (uid)
2581       a_uids[0] = uid;
2582     else if (this.env.uid)
2583       a_uids[0] = this.env.uid;
2584     else if (this.message_list) {
2585       for (n=0, len=selection.length; n<len; n++) {
2586           a_uids.push(selection[n]);
2587       }
2588     }
2589
2590     if (!this.message_list)
2591       r_uids = a_uids;
2592     else
2593       for (n=0, len=a_uids.length; n<len; n++) {
2594         id = a_uids[n];
2595         if ((flag=='read' && this.message_list.rows[id].unread) 
2596             || (flag=='unread' && !this.message_list.rows[id].unread)
2597             || (flag=='delete' && !this.message_list.rows[id].deleted)
2598             || (flag=='undelete' && this.message_list.rows[id].deleted)
2599             || (flag=='flagged' && !this.message_list.rows[id].flagged)
2600             || (flag=='unflagged' && this.message_list.rows[id].flagged))
2601         {
2602           r_uids.push(id);
2603         }
2604       }
2605
2606     // nothing to do
2607     if (!r_uids.length && !this.select_all_mode)
2608       return;
2609
2610     switch (flag) {
2611         case 'read':
2612         case 'unread':
2613           this.toggle_read_status(flag, r_uids);
2614           break;
2615         case 'delete':
2616         case 'undelete':
2617           this.toggle_delete_status(r_uids);
2618           break;
2619         case 'flagged':
2620         case 'unflagged':
2621           this.toggle_flagged_status(flag, a_uids);
2622           break;
2623     }
2624   };
2625
2626   // set class to read/unread
2627   this.toggle_read_status = function(flag, a_uids)
2628   {
2629     var i, len = a_uids.length,
2630       url = '_uid='+this.uids_to_list(a_uids)+'&_flag='+flag,
2631       lock = this.display_message(this.get_label('markingmessage'), 'loading');
2632
2633     // mark all message rows as read/unread
2634     for (i=0; i<len; i++)
2635       this.set_message(a_uids[i], 'unread', (flag=='unread' ? true : false));
2636
2637     // also send search request to get the right messages
2638     if (this.env.search_request)
2639       url += '&_search='+this.env.search_request;
2640
2641     this.http_post('mark', url, lock);
2642
2643     for (i=0; i<len; i++)
2644       this.update_thread_root(a_uids[i], flag);
2645   };
2646
2647   // set image to flagged or unflagged
2648   this.toggle_flagged_status = function(flag, a_uids)
2649   {
2650     var i, len = a_uids.length,
2651       url = '_uid='+this.uids_to_list(a_uids)+'&_flag='+flag,
2652       lock = this.display_message(this.get_label('markingmessage'), 'loading');
2653
2654     // mark all message rows as flagged/unflagged
2655     for (i=0; i<len; i++)
2656       this.set_message(a_uids[i], 'flagged', (flag=='flagged' ? true : false));
2657
2658     // also send search request to get the right messages
2659     if (this.env.search_request)
2660       url += '&_search='+this.env.search_request;
2661
2662     this.http_post('mark', url, lock);
2663   };
2664
2665   // mark all message rows as deleted/undeleted
2666   this.toggle_delete_status = function(a_uids)
2667   {
2668     var len = a_uids.length,
2669       i, uid, all_deleted = true,
2670       rows = this.message_list ? this.message_list.rows : [];
2671
2672     if (len == 1) {
2673       if (!rows.length || (rows[a_uids[0]] && !rows[a_uids[0]].deleted))
2674         this.flag_as_deleted(a_uids);
2675       else
2676         this.flag_as_undeleted(a_uids);
2677
2678       return true;
2679     }
2680
2681     for (i=0; i<len; i++) {
2682       uid = a_uids[i];
2683       if (rows[uid] && !rows[uid].deleted) {
2684         all_deleted = false;
2685         break;
2686       }
2687     }
2688
2689     if (all_deleted)
2690       this.flag_as_undeleted(a_uids);
2691     else
2692       this.flag_as_deleted(a_uids);
2693
2694     return true;
2695   };
2696
2697   this.flag_as_undeleted = function(a_uids)
2698   {
2699     var i, len=a_uids.length,
2700       url = '_uid='+this.uids_to_list(a_uids)+'&_flag=undelete',
2701       lock = this.display_message(this.get_label('markingmessage'), 'loading');
2702
2703     for (i=0; i<len; i++)
2704       this.set_message(a_uids[i], 'deleted', false);
2705
2706     // also send search request to get the right messages
2707     if (this.env.search_request)
2708       url += '&_search='+this.env.search_request;
2709
2710     this.http_post('mark', url, lock);
2711     return true;
2712   };
2713
2714   this.flag_as_deleted = function(a_uids)
2715   {
2716     var add_url = '',
2717       r_uids = [],
2718       rows = this.message_list ? this.message_list.rows : [],
2719       count = 0;
2720
2721     for (var i=0, len=a_uids.length; i<len; i++) {
2722       uid = a_uids[i];
2723       if (rows[uid]) {
2724         if (rows[uid].unread)
2725           r_uids[r_uids.length] = uid;
2726
2727             if (this.env.skip_deleted) {
2728               count += this.update_thread(uid);
2729           this.message_list.remove_row(uid, (this.env.display_next && i == this.message_list.selection.length-1));
2730             }
2731             else
2732               this.set_message(uid, 'deleted', true);
2733       }
2734     }
2735
2736     // make sure there are no selected rows
2737     if (this.env.skip_deleted && this.message_list) {
2738       if(!this.env.display_next)
2739         this.message_list.clear_selection();
2740       if (count < 0)
2741         add_url += '&_count='+(count*-1);
2742       else if (count > 0) 
2743         // remove threads from the end of the list
2744         this.delete_excessive_thread_rows();
2745     }
2746
2747     add_url = '&_from='+(this.env.action ? this.env.action : ''),
2748       lock = this.display_message(this.get_label('markingmessage'), 'loading');
2749
2750     // ??
2751     if (r_uids.length)
2752       add_url += '&_ruid='+this.uids_to_list(r_uids);
2753
2754     if (this.env.skip_deleted) {
2755       if (this.env.display_next && this.env.next_uid)
2756         add_url += '&_next_uid='+this.env.next_uid;
2757     }
2758
2759     // also send search request to get the right messages
2760     if (this.env.search_request)
2761       add_url += '&_search='+this.env.search_request;
2762
2763     this.http_post('mark', '_uid='+this.uids_to_list(a_uids)+'&_flag=delete'+add_url, lock);
2764     return true;
2765   };
2766
2767   // flag as read without mark request (called from backend)
2768   // argument should be a coma-separated list of uids
2769   this.flag_deleted_as_read = function(uids)
2770   {
2771     var icn_src, uid, i, len,
2772       rows = this.message_list ? this.message_list.rows : [];
2773
2774     uids = String(uids).split(',');
2775
2776     for (i=0, len=uids.length; i<len; i++) {
2777       uid = uids[i];
2778       if (rows[uid])
2779         this.set_message(uid, 'unread', false);
2780     }
2781   };
2782
2783   // Converts array of message UIDs to comma-separated list for use in URL
2784   // with select_all mode checking
2785   this.uids_to_list = function(uids)
2786   {
2787     return this.select_all_mode ? '*' : uids.join(',');
2788   };
2789
2790
2791   /*********************************************************/
2792   /*********       mailbox folders methods         *********/
2793   /*********************************************************/
2794
2795   this.expunge_mailbox = function(mbox)
2796   {
2797     var lock, url = '_mbox='+urlencode(mbox);
2798
2799     // lock interface if it's the active mailbox
2800     if (mbox == this.env.mailbox) {
2801       lock = this.set_busy(true, 'loading');
2802       url += '&_reload=1';
2803       if (this.env.search_request)
2804         url += '&_search='+this.env.search_request;
2805     }
2806
2807     // send request to server
2808     this.http_post('expunge', url, lock);
2809   };
2810
2811   this.purge_mailbox = function(mbox)
2812   {
2813     var lock = false,
2814       url = '_mbox='+urlencode(mbox);
2815
2816     if (!confirm(this.get_label('purgefolderconfirm')))
2817       return false;
2818
2819     // lock interface if it's the active mailbox
2820     if (mbox == this.env.mailbox) {
2821        lock = this.set_busy(true, 'loading');
2822        url += '&_reload=1';
2823      }
2824
2825     // send request to server
2826     this.http_post('purge', url, lock);
2827   };
2828
2829   // test if purge command is allowed
2830   this.purge_mailbox_test = function()
2831   {
2832     return (this.env.messagecount && (this.env.mailbox == this.env.trash_mailbox || this.env.mailbox == this.env.junk_mailbox
2833       || this.env.mailbox.match('^' + RegExp.escape(this.env.trash_mailbox) + RegExp.escape(this.env.delimiter))
2834       || this.env.mailbox.match('^' + RegExp.escape(this.env.junk_mailbox) + RegExp.escape(this.env.delimiter))));
2835   };
2836
2837
2838   /*********************************************************/
2839   /*********           login form methods          *********/
2840   /*********************************************************/
2841
2842   // handler for keyboard events on the _user field
2843   this.login_user_keyup = function(e)
2844   {
2845     var key = rcube_event.get_keycode(e);
2846     var passwd = $('#rcmloginpwd');
2847
2848     // enter
2849     if (key == 13 && passwd.length && !passwd.val()) {
2850       passwd.focus();
2851       return rcube_event.cancel(e);
2852     }
2853
2854     return true;
2855   };
2856
2857
2858   /*********************************************************/
2859   /*********        message compose methods        *********/
2860   /*********************************************************/
2861
2862   // init message compose form: set focus and eventhandlers
2863   this.init_messageform = function()
2864   {
2865     if (!this.gui_objects.messageform)
2866       return false;
2867
2868     var input_from = $("[name='_from']"),
2869       input_to = $("[name='_to']"),
2870       input_subject = $("input[name='_subject']"),
2871       input_message = $("[name='_message']").get(0),
2872       html_mode = $("input[name='_is_html']").val() == '1',
2873       ac_fields = ['cc', 'bcc', 'replyto', 'followupto'],
2874       ac_props;
2875
2876     // configure parallel autocompletion
2877     if (this.env.autocomplete_threads > 0) {
2878       ac_props = {
2879         threads: this.env.autocomplete_threads,
2880         sources: this.env.autocomplete_sources
2881       };
2882     }
2883
2884     // init live search events
2885     this.init_address_input_events(input_to, ac_props);
2886     for (var i in ac_fields) {
2887       this.init_address_input_events($("[name='_"+ac_fields[i]+"']"), ac_props);
2888     }
2889
2890     if (!html_mode) {
2891       this.set_caret_pos(input_message, this.env.top_posting ? 0 : $(input_message).val().length);
2892       // add signature according to selected identity
2893       // if we have HTML editor, signature is added in callback
2894       if (input_from.prop('type') == 'select-one' && $("input[name='_draft_saveid']").val() == '') {
2895         this.change_identity(input_from[0]);
2896       }
2897     }
2898
2899     if (input_to.val() == '')
2900       input_to.focus();
2901     else if (input_subject.val() == '')
2902       input_subject.focus();
2903     else if (input_message)
2904       input_message.focus();
2905
2906     this.env.compose_focus_elem = document.activeElement;
2907
2908     // get summary of all field values
2909     this.compose_field_hash(true);
2910
2911     // start the auto-save timer
2912     this.auto_save_start();
2913   };
2914
2915   this.init_address_input_events = function(obj, props)
2916   {
2917     obj[bw.ie || bw.safari || bw.chrome ? 'keydown' : 'keypress'](function(e) { return ref.ksearch_keydown(e, this, props); })
2918       .attr('autocomplete', 'off');
2919   };
2920
2921   // checks the input fields before sending a message
2922   this.check_compose_input = function()
2923   {
2924     // check input fields
2925     var ed, input_to = $("[name='_to']"),
2926       input_cc = $("[name='_cc']"),
2927       input_bcc = $("[name='_bcc']"),
2928       input_from = $("[name='_from']"),
2929       input_subject = $("[name='_subject']"),
2930       input_message = $("[name='_message']");
2931
2932     // check sender (if have no identities)
2933     if (input_from.prop('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
2934       alert(this.get_label('nosenderwarning'));
2935       input_from.focus();
2936       return false;
2937     }
2938
2939     // check for empty recipient
2940     var recipients = input_to.val() ? input_to.val() : (input_cc.val() ? input_cc.val() : input_bcc.val());
2941     if (!rcube_check_email(recipients.replace(/^\s+/, '').replace(/[\s,;]+$/, ''), true)) {
2942       alert(this.get_label('norecipientwarning'));
2943       input_to.focus();
2944       return false;
2945     }
2946
2947     // check if all files has been uploaded
2948     for (var key in this.env.attachments) {
2949       if (typeof this.env.attachments[key] === 'object' && !this.env.attachments[key].complete) {
2950         alert(this.get_label('notuploadedwarning'));
2951         return false;
2952       }
2953     }
2954
2955     // display localized warning for missing subject
2956     if (input_subject.val() == '') {
2957       var subject = prompt(this.get_label('nosubjectwarning'), this.get_label('nosubject'));
2958
2959       // user hit cancel, so don't send
2960       if (!subject && subject !== '') {
2961         input_subject.focus();
2962         return false;
2963       }
2964       else
2965         input_subject.val((subject ? subject : this.get_label('nosubject')));
2966     }
2967
2968     // Apply spellcheck changes if spell checker is active
2969     this.stop_spellchecking();
2970
2971     if (window.tinyMCE)
2972       ed = tinyMCE.get(this.env.composebody);
2973
2974     // check for empty body
2975     if (!ed && input_message.val() == '' && !confirm(this.get_label('nobodywarning'))) {
2976       input_message.focus();
2977       return false;
2978     }
2979     else if (ed) {
2980       if (!ed.getContent() && !confirm(this.get_label('nobodywarning'))) {
2981         ed.focus();
2982         return false;
2983       }
2984       // move body from html editor to textarea (just to be sure, #1485860)
2985       tinyMCE.triggerSave();
2986     }
2987
2988     return true;
2989   };
2990
2991   this.toggle_editor = function(props)
2992   {
2993     if (props.mode == 'html') {
2994       this.display_spellcheck_controls(false);
2995       this.plain2html($('#'+props.id).val(), props.id);
2996       tinyMCE.execCommand('mceAddControl', false, props.id);
2997     }
2998     else {
2999       var thisMCE = tinyMCE.get(props.id), existingHtml;
3000       if (thisMCE.plugins.spellchecker && thisMCE.plugins.spellchecker.active)
3001         thisMCE.execCommand('mceSpellCheck', false);
3002
3003       if (existingHtml = thisMCE.getContent()) {
3004         if (!confirm(this.get_label('editorwarning'))) {
3005           return false;
3006         }
3007         this.html2plain(existingHtml, props.id);
3008       }
3009       tinyMCE.execCommand('mceRemoveControl', false, props.id);
3010       this.display_spellcheck_controls(true);
3011     }
3012
3013     return true;
3014   };
3015
3016   this.stop_spellchecking = function()
3017   {
3018     var ed;
3019     if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody))) {
3020       if (ed.plugins.spellchecker && ed.plugins.spellchecker.active)
3021         ed.execCommand('mceSpellCheck');
3022     }
3023     else if ((ed = this.env.spellcheck) && !this.spellcheck_ready) {
3024       $(ed.spell_span).trigger('click');
3025       this.set_spellcheck_state('ready');
3026     }
3027   };
3028
3029   this.display_spellcheck_controls = function(vis)
3030   {
3031     if (this.env.spellcheck) {
3032       // stop spellchecking process
3033       if (!vis)
3034         this.stop_spellchecking();
3035
3036       $(this.env.spellcheck.spell_container).css('visibility', vis ? 'visible' : 'hidden');
3037     }
3038   };
3039
3040   this.set_spellcheck_state = function(s)
3041   {
3042     this.spellcheck_ready = (s == 'ready' || s == 'no_error_found');
3043     this.enable_command('spellcheck', this.spellcheck_ready);
3044   };
3045
3046   // get selected language
3047   this.spellcheck_lang = function()
3048   {
3049     var ed;
3050     if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)) && ed.plugins.spellchecker) {
3051       return ed.plugins.spellchecker.selectedLang;
3052     }
3053     else if (this.env.spellcheck) {
3054       return GOOGIE_CUR_LANG;
3055     }
3056   };
3057
3058   // resume spellchecking, highlight provided mispellings without new ajax request
3059   this.spellcheck_resume = function(ishtml, data)
3060   {
3061     if (ishtml) {
3062       var ed = tinyMCE.get(this.env.composebody);
3063         sp = ed.plugins.spellchecker;
3064
3065       sp.active = 1;
3066       sp._markWords(data);
3067       ed.nodeChanged();
3068     }
3069     else {
3070       var sp = this.env.spellcheck;
3071       sp.prepare(false, true);
3072       sp.processData(data);
3073     }
3074   }
3075
3076   this.set_draft_id = function(id)
3077   {
3078     $("input[name='_draft_saveid']").val(id);
3079   };
3080
3081   this.auto_save_start = function()
3082   {
3083     if (this.env.draft_autosave)
3084       this.save_timer = self.setTimeout(function(){ ref.command("savedraft"); }, this.env.draft_autosave * 1000);
3085
3086     // Unlock interface now that saving is complete
3087     this.busy = false;
3088   };
3089
3090   this.compose_field_hash = function(save)
3091   {
3092     // check input fields
3093     var ed, str = '',
3094       value_to = $("[name='_to']").val(),
3095       value_cc = $("[name='_cc']").val(),
3096       value_bcc = $("[name='_bcc']").val(),
3097       value_subject = $("[name='_subject']").val();
3098
3099     if (value_to)
3100       str += value_to+':';
3101     if (value_cc)
3102       str += value_cc+':';
3103     if (value_bcc)
3104       str += value_bcc+':';
3105     if (value_subject)
3106       str += value_subject+':';
3107
3108     if (window.tinyMCE && (ed = tinyMCE.get(this.env.composebody)))
3109       str += ed.getContent();
3110     else
3111       str += $("[name='_message']").val();
3112
3113     if (this.env.attachments)
3114       for (var upload_id in this.env.attachments)
3115         str += upload_id;
3116
3117     if (save)
3118       this.cmp_hash = str;
3119
3120     return str;
3121   };
3122
3123   this.change_identity = function(obj, show_sig)
3124   {
3125     if (!obj || !obj.options)
3126       return false;
3127
3128     if (!show_sig)
3129       show_sig = this.env.show_sig;
3130
3131     var cursor_pos, p = -1,
3132       id = obj.options[obj.selectedIndex].value,
3133       input_message = $("[name='_message']"),
3134       message = input_message.val(),
3135       is_html = ($("input[name='_is_html']").val() == '1'),
3136       sig = this.env.identity,
3137       sig_separator = this.env.sig_above && (this.env.compose_mode == 'reply' || this.env.compose_mode == 'forward') ? '---' : '-- ';
3138
3139     // enable manual signature insert
3140     if (this.env.signatures && this.env.signatures[id]) {
3141       this.enable_command('insert-sig', true);
3142       this.env.compose_commands.push('insert-sig');
3143     }
3144     else
3145       this.enable_command('insert-sig', false);
3146
3147     if (!is_html) {
3148       // remove the 'old' signature
3149       if (show_sig && sig && this.env.signatures && this.env.signatures[sig]) {
3150
3151         sig = this.env.signatures[sig].is_html ? this.env.signatures[sig].plain_text : this.env.signatures[sig].text;
3152         sig = sig.replace(/\r\n/g, '\n');
3153
3154         if (!sig.match(/^--[ -]\n/))
3155           sig = sig_separator + '\n' + sig;
3156
3157         p = this.env.sig_above ? message.indexOf(sig) : message.lastIndexOf(sig);
3158         if (p >= 0)
3159           message = message.substring(0, p) + message.substring(p+sig.length, message.length);
3160       }
3161       // add the new signature string
3162       if (show_sig && this.env.signatures && this.env.signatures[id]) {
3163         sig = this.env.signatures[id]['is_html'] ? this.env.signatures[id]['plain_text'] : this.env.signatures[id]['text'];
3164         sig = sig.replace(/\r\n/g, '\n');
3165
3166         if (!sig.match(/^--[ -]\n/))
3167           sig = sig_separator + '\n' + sig;
3168
3169         if (this.env.sig_above) {
3170           if (p >= 0) { // in place of removed signature
3171             message = message.substring(0, p) + sig + message.substring(p, message.length);
3172             cursor_pos = p - 1;
3173           }
3174           else if (pos = this.get_caret_pos(input_message.get(0))) { // at cursor position
3175             message = message.substring(0, pos) + '\n' + sig + '\n\n' + message.substring(pos, message.length);
3176             cursor_pos = pos;
3177           }
3178           else { // on top
3179             cursor_pos = 0;
3180             message = '\n\n' + sig + '\n\n' + message.replace(/^[\r\n]+/, '');
3181           }
3182         }
3183         else {
3184           message = message.replace(/[\r\n]+$/, '');
3185           cursor_pos = !this.env.top_posting && message.length ? message.length+1 : 0;
3186           message += '\n\n' + sig;
3187         }
3188       }
3189       else
3190         cursor_pos = this.env.top_posting ? 0 : message.length;
3191
3192       input_message.val(message);
3193
3194       // move cursor before the signature
3195       this.set_caret_pos(input_message.get(0), cursor_pos);
3196     }
3197     else if (show_sig && this.env.signatures) {  // html
3198       var editor = tinyMCE.get(this.env.composebody),
3199         sigElem = editor.dom.get('_rc_sig');
3200
3201       // Append the signature as a div within the body
3202       if (!sigElem) {
3203         var body = editor.getBody(),
3204           doc = editor.getDoc();
3205
3206         sigElem = doc.createElement('div');
3207         sigElem.setAttribute('id', '_rc_sig');
3208
3209         if (this.env.sig_above) {
3210           // if no existing sig and top posting then insert at caret pos
3211           editor.getWin().focus(); // correct focus in IE & Chrome
3212
3213           var node = editor.selection.getNode();
3214           if (node.nodeName == 'BODY') {
3215             // no real focus, insert at start
3216             body.insertBefore(sigElem, body.firstChild);
3217             body.insertBefore(doc.createElement('br'), body.firstChild);
3218           }
3219           else {
3220             body.insertBefore(sigElem, node.nextSibling);
3221             body.insertBefore(doc.createElement('br'), node.nextSibling);
3222           }
3223         }
3224         else {
3225           if (bw.ie)  // add empty line before signature on IE
3226             body.appendChild(doc.createElement('br'));
3227
3228           body.appendChild(sigElem);
3229         }
3230       }
3231
3232       if (this.env.signatures[id]) {
3233         if (this.env.signatures[id].is_html) {
3234           sig = this.env.signatures[id].text;
3235           if (!this.env.signatures[id].plain_text.match(/^--[ -]\r?\n/))
3236             sig = sig_separator + '<br />' + sig;
3237         }
3238         else {
3239           sig = this.env.signatures[id].text;
3240           if (!sig.match(/^--[ -]\r?\n/))
3241             sig = sig_separator + '\n' + sig;
3242           sig = '<pre>' + sig + '</pre>';
3243         }
3244
3245         sigElem.innerHTML = sig;
3246       }
3247     }
3248
3249     this.env.identity = id;
3250     return true;
3251   };
3252
3253   // upload attachment file
3254   this.upload_file = function(form)
3255   {
3256     if (!form)
3257       return false;
3258
3259     // get file input field, count files on capable browser
3260     var i, size = 0, field = $('input[type=file]', form).get(0),
3261       files = field.files ? field.files.length : field.value ? 1 : 0;
3262
3263     // create hidden iframe and post upload form
3264     if (files) {
3265       // check file size
3266       if (field.files && this.env.max_filesize && this.env.filesizeerror) {
3267         for (i=0; i<files; i++)
3268           size += field.files[i].size;
3269         if (size && size > this.env.max_filesize) {
3270           this.display_message(this.env.filesizeerror, 'error');
3271           return;
3272         }
3273       }
3274
3275       var frame_name = this.async_upload_form(form, 'upload', function(e) {
3276         var d, content = '';
3277         try {
3278           if (this.contentDocument) {
3279             d = this.contentDocument;
3280           } else if (this.contentWindow) {
3281             d = this.contentWindow.document;
3282           }
3283           content = d.childNodes[0].innerHTML;
3284         } catch (err) {}
3285
3286         if (!content.match(/add2attachment/) && (!bw.opera || (rcmail.env.uploadframe && rcmail.env.uploadframe == e.data.ts))) {
3287           if (!content.match(/display_message/))
3288             rcmail.display_message(rcmail.get_label('fileuploaderror'), 'error');
3289           rcmail.remove_from_attachment_list(e.data.ts);
3290         }
3291         // Opera hack: handle double onload
3292         if (bw.opera)
3293           rcmail.env.uploadframe = e.data.ts;
3294       });
3295
3296       // display upload indicator and cancel button
3297       var content = '<span>' + this.get_label('uploading' + (files > 1 ? 'many' : '')) + '</span>',
3298         ts = frame_name.replace(/^rcmupload/, '');
3299
3300       if (this.env.loadingicon)
3301         content = '<img src="'+this.env.loadingicon+'" alt="" />'+content;
3302       if (this.env.cancelicon)
3303         content = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+ts+'\', \''+frame_name+'\');" href="#cancelupload"><img src="'+this.env.cancelicon+'" alt="" /></a>'+content;
3304       this.add2attachment_list(ts, { name:'', html:content, complete:false });
3305
3306       // upload progress support
3307       if (this.env.upload_progress_time) {
3308         this.upload_progress_start('upload', ts);
3309       }
3310     }
3311
3312     // set reference to the form object
3313     this.gui_objects.attachmentform = form;
3314     return true;
3315   };
3316
3317   // add file name to attachment list
3318   // called from upload page
3319   this.add2attachment_list = function(name, att, upload_id)
3320   {
3321     if (!this.gui_objects.attachmentlist)
3322       return false;
3323
3324     var indicator, li = $('<li>').attr('id', name).html(att.html);
3325
3326     // replace indicator's li
3327     if (upload_id && (indicator = document.getElementById(upload_id))) {
3328       li.replaceAll(indicator);
3329     }
3330     else { // add new li
3331       li.appendTo(this.gui_objects.attachmentlist);
3332     }
3333
3334     if (upload_id && this.env.attachments[upload_id])
3335       delete this.env.attachments[upload_id];
3336
3337     this.env.attachments[name] = att;
3338
3339     return true;
3340   };
3341
3342   this.remove_from_attachment_list = function(name)
3343   {
3344     if (this.env.attachments[name])
3345       delete this.env.attachments[name];
3346
3347     if (!this.gui_objects.attachmentlist)
3348       return false;
3349
3350     var list = this.gui_objects.attachmentlist.getElementsByTagName("li");
3351     for (i=0; i<list.length; i++)
3352       if (list[i].id == name)
3353         this.gui_objects.attachmentlist.removeChild(list[i]);
3354   };
3355
3356   this.remove_attachment = function(name)
3357   {
3358     if (name && this.env.attachments[name])
3359       this.http_post('remove-attachment', { _id:this.env.compose_id, _file:name });
3360
3361     return true;
3362   };
3363
3364   this.cancel_attachment_upload = function(name, frame_name)
3365   {
3366     if (!name || !frame_name)
3367       return false;
3368
3369     this.remove_from_attachment_list(name);
3370     $("iframe[name='"+frame_name+"']").remove();
3371     return false;
3372   };
3373
3374   this.upload_progress_start = function(action, name)
3375   {
3376     window.setTimeout(function() { rcmail.http_request(action, {_progress: name}); },
3377       this.env.upload_progress_time * 1000);
3378   };
3379
3380   this.upload_progress_update = function(param)
3381   {
3382     var elem = $('#'+param.name + '> span');
3383
3384     if (!elem.length || !param.text)
3385       return;
3386
3387     elem.text(param.text);
3388
3389     if (!param.done)
3390       this.upload_progress_start(param.action, param.name);
3391   };
3392
3393   // send remote request to add a new contact
3394   this.add_contact = function(value)
3395   {
3396     if (value)
3397       this.http_post('addcontact', '_address='+value);
3398
3399     return true;
3400   };
3401
3402   // send remote request to search mail or contacts
3403   this.qsearch = function(value)
3404   {
3405     if (value != '') {
3406       var n, r, addurl = '', mods_arr = [],
3407         mods = this.env.search_mods,
3408         mbox = this.env.mailbox,
3409         lock = this.set_busy(true, 'searching');
3410
3411       if (this.message_list) {
3412         this.clear_message_list();
3413         if (mods)
3414           mods = mods[mbox] ? mods[mbox] : mods['*'];
3415       } else if (this.contact_list) {
3416         this.list_contacts_clear();
3417       }
3418
3419       if (mods) {
3420         for (n in mods)
3421           mods_arr.push(n);
3422         addurl += '&_headers='+mods_arr.join(',');
3423       }
3424
3425       if (this.gui_objects.search_filter)
3426         addurl += '&_filter=' + this.gui_objects.search_filter.value;
3427
3428       // reset vars
3429       this.env.current_page = 1;
3430       r = this.http_request('search', '_q='+urlencode(value)
3431         + (mbox ? '&_mbox='+urlencode(mbox) : '')
3432         + (this.env.source ? '&_source='+urlencode(this.env.source) : '')
3433         + (this.env.group ? '&_gid='+urlencode(this.env.group) : '')
3434         + (addurl ? addurl : ''), lock);
3435
3436       this.env.qsearch = {lock: lock, request: r};
3437     }
3438   };
3439
3440   // reset quick-search form
3441   this.reset_qsearch = function()
3442   {
3443     if (this.gui_objects.qsearchbox)
3444       this.gui_objects.qsearchbox.value = '';
3445
3446     if (this.env.qsearch)
3447       this.abort_request(this.env.qsearch);
3448
3449     this.env.qsearch = null;
3450     this.env.search_request = null;
3451   };
3452
3453   this.sent_successfully = function(type, msg)
3454   {
3455     this.display_message(msg, type);
3456     // before redirect we need to wait some time for Chrome (#1486177)
3457     window.setTimeout(function(){ ref.list_mailbox(); }, 500);
3458   };
3459
3460
3461   /*********************************************************/
3462   /*********     keyboard live-search methods      *********/
3463   /*********************************************************/
3464
3465   // handler for keyboard events on address-fields
3466   this.ksearch_keydown = function(e, obj, props)
3467   {
3468     if (this.ksearch_timer)
3469       clearTimeout(this.ksearch_timer);
3470
3471     var highlight,
3472       key = rcube_event.get_keycode(e),
3473       mod = rcube_event.get_modifier(e);
3474
3475     switch (key) {
3476       case 38:  // key up
3477       case 40:  // key down
3478         if (!this.ksearch_pane)
3479           break;
3480
3481         var dir = key==38 ? 1 : 0;
3482
3483         highlight = document.getElementById('rcmksearchSelected');
3484         if (!highlight)
3485           highlight = this.ksearch_pane.__ul.firstChild;
3486
3487         if (highlight)
3488           this.ksearch_select(dir ? highlight.previousSibling : highlight.nextSibling);
3489
3490         return rcube_event.cancel(e);
3491
3492       case 9:   // tab
3493         if (mod == SHIFT_KEY || !this.ksearch_visible()) {
3494           this.ksearch_hide();
3495           return;
3496         }
3497
3498       case 13:  // enter
3499         if (!this.ksearch_visible())
3500           return false;
3501
3502         // insert selected address and hide ksearch pane
3503         this.insert_recipient(this.ksearch_selected);
3504         this.ksearch_hide();
3505
3506         return rcube_event.cancel(e);
3507
3508       case 27:  // escape
3509         this.ksearch_hide();
3510         return;
3511
3512       case 37:  // left
3513       case 39:  // right
3514         if (mod != SHIFT_KEY)
3515               return;
3516     }
3517
3518     // start timer
3519     this.ksearch_timer = window.setTimeout(function(){ ref.ksearch_get_results(props); }, 200);
3520     this.ksearch_input = obj;
3521
3522     return true;
3523   };
3524
3525   this.ksearch_visible = function()
3526   {
3527     return (this.ksearch_selected !== null && this.ksearch_selected !== undefined && this.ksearch_value);
3528   };
3529
3530   this.ksearch_select = function(node)
3531   {
3532     var current = $('#rcmksearchSelected');
3533     if (current[0] && node) {
3534       current.removeAttr('id').removeClass('selected');
3535     }
3536
3537     if (node) {
3538       $(node).attr('id', 'rcmksearchSelected').addClass('selected');
3539       this.ksearch_selected = node._rcm_id;
3540     }
3541   };
3542
3543   this.insert_recipient = function(id)
3544   {
3545     if (!this.env.contacts[id] || !this.ksearch_input)
3546       return;
3547
3548     // get cursor pos
3549     var inp_value = this.ksearch_input.value,
3550       cpos = this.get_caret_pos(this.ksearch_input),
3551       p = inp_value.lastIndexOf(this.ksearch_value, cpos),
3552       trigger = false,
3553       insert = '',
3554       // replace search string with full address
3555       pre = inp_value.substring(0, p),
3556       end = inp_value.substring(p+this.ksearch_value.length, inp_value.length);
3557
3558     this.ksearch_destroy();
3559
3560     // insert all members of a group
3561     if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].id) {
3562       insert += this.env.contacts[id].name + ', ';
3563       this.group2expand = $.extend({}, this.env.contacts[id]);
3564       this.group2expand.input = this.ksearch_input;
3565       this.http_request('mail/group-expand', '_source='+urlencode(this.env.contacts[id].source)+'&_gid='+urlencode(this.env.contacts[id].id), false);
3566     }
3567     else if (typeof this.env.contacts[id] === 'string') {
3568       insert = this.env.contacts[id] + ', ';
3569       trigger = true;
3570     }
3571
3572     this.ksearch_input.value = pre + insert + end;
3573
3574     // set caret to insert pos
3575     cpos = p+insert.length;
3576     if (this.ksearch_input.setSelectionRange)
3577       this.ksearch_input.setSelectionRange(cpos, cpos);
3578
3579     if (trigger)
3580       this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert });
3581   };
3582
3583   this.replace_group_recipients = function(id, recipients)
3584   {
3585     if (this.group2expand && this.group2expand.id == id) {
3586       this.group2expand.input.value = this.group2expand.input.value.replace(this.group2expand.name, recipients);
3587       this.triggerEvent('autocomplete_insert', { field:this.group2expand.input, insert:recipients });
3588       this.group2expand = null;
3589     }
3590   };
3591
3592   // address search processor
3593   this.ksearch_get_results = function(props)
3594   {
3595     var inp_value = this.ksearch_input ? this.ksearch_input.value : null;
3596
3597     if (inp_value === null)
3598       return;
3599
3600     if (this.ksearch_pane && this.ksearch_pane.is(":visible"))
3601       this.ksearch_pane.hide();
3602
3603     // get string from current cursor pos to last comma
3604     var cpos = this.get_caret_pos(this.ksearch_input),
3605       p = inp_value.lastIndexOf(',', cpos-1),
3606       q = inp_value.substring(p+1, cpos),
3607       min = this.env.autocomplete_min_length,
3608       ac = this.ksearch_data;
3609
3610     // trim query string
3611     q = $.trim(q);
3612
3613     // Don't (re-)search if the last results are still active
3614     if (q == this.ksearch_value)
3615       return;
3616
3617     if (q.length && q.length < min) {
3618       if (!this.env.acinfo) {
3619         this.env.acinfo = this.display_message(
3620           this.get_label('autocompletechars').replace('$min', min));
3621       }
3622       return;
3623     }
3624     else if (this.env.acinfo) {
3625       this.hide_message(this.env.acinfo);
3626     }
3627
3628     var old_value = this.ksearch_value;
3629     this.ksearch_value = q;
3630
3631     this.ksearch_destroy();
3632
3633     // ...string is empty
3634     if (!q.length)
3635       return;
3636
3637     // ...new search value contains old one and previous search was not finished or its result was empty
3638     if (old_value && old_value.length && q.indexOf(old_value) == 0 && (!ac || !ac.num) && this.env.contacts && !this.env.contacts.length)
3639       return;
3640
3641     var i, lock, source, xhr, reqid = new Date().getTime(),
3642       threads = props && props.threads ? props.threads : 1,
3643       sources = props && props.sources ? props.sources : [],
3644       action = props && props.action ? props.action : 'mail/autocomplete';
3645
3646     this.ksearch_data = {id: reqid, sources: sources.slice(), action: action,
3647       locks: [], requests: [], num: sources.length};
3648
3649     for (i=0; i<threads; i++) {
3650       source = this.ksearch_data.sources.shift();
3651       if (threads > 1 && source === null)
3652         break;
3653
3654       lock = this.display_message(this.get_label('searching'), 'loading');
3655       xhr = this.http_post(action, '_search='+urlencode(q)+'&_id='+reqid
3656         + (source ? '&_source='+urlencode(source) : ''), lock);
3657
3658       this.ksearch_data.locks.push(lock);
3659       this.ksearch_data.requests.push(xhr);
3660     }
3661   };
3662
3663   this.ksearch_query_results = function(results, search, reqid)
3664   {
3665     // search stopped in meantime?
3666     if (!this.ksearch_value)
3667       return;
3668
3669     // ignore this outdated search response
3670     if (this.ksearch_input && search != this.ksearch_value)
3671       return;
3672
3673     // display search results
3674     var p, ul, li, text, init, s_val = this.ksearch_value,
3675       maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;
3676
3677     // create results pane if not present
3678     if (!this.ksearch_pane) {
3679       ul = $('<ul>');
3680       this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane')
3681         .css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
3682       this.ksearch_pane.__ul = ul[0];
3683     }
3684
3685     ul = this.ksearch_pane.__ul;
3686
3687     // remove all search results or add to existing list if parallel search
3688     if (reqid && this.ksearch_pane.data('reqid') == reqid) {
3689       maxlen -= ul.childNodes.length;
3690     }
3691     else {
3692       this.ksearch_pane.data('reqid', reqid);
3693       init = 1;
3694       // reset content
3695       ul.innerHTML = '';
3696       this.env.contacts = [];
3697       // move the results pane right under the input box
3698       var pos = $(this.ksearch_input).offset();
3699       this.ksearch_pane.css({ left:pos.left+'px', top:(pos.top + this.ksearch_input.offsetHeight)+'px', display: 'none'});
3700     }
3701
3702     // add each result line to list
3703     if (results && results.length) {
3704       for (i=0; i < results.length && maxlen > 0; i++) {
3705         text = typeof results[i] === 'object' ? results[i].name : results[i];
3706         li = document.createElement('LI');
3707         li.innerHTML = text.replace(new RegExp('('+RegExp.escape(s_val)+')', 'ig'), '##$1%%').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/##([^%]+)%%/g, '<b>$1</b>');
3708         li.onmouseover = function(){ ref.ksearch_select(this); };
3709         li.onmouseup = function(){ ref.ksearch_click(this) };
3710         li._rcm_id = this.env.contacts.length + i;
3711         ul.appendChild(li);
3712         maxlen -= 1;
3713       }
3714     }
3715
3716     if (ul.childNodes.length) {
3717       this.ksearch_pane.show();
3718       // select the first
3719       if (!this.env.contacts.length) {
3720         $('li:first', ul).attr('id', 'rcmksearchSelected').addClass('selected');
3721         this.ksearch_selected = 0;
3722       }
3723     }
3724
3725     if (results && results.length)
3726       this.env.contacts = this.env.contacts.concat(results);
3727
3728     // run next parallel search
3729     if (maxlen > 0 && this.ksearch_data.id == reqid && this.ksearch_data.sources.length) {
3730       var lock, xhr, props = this.ksearch_data, source = props.sources.shift();
3731       if (source) {
3732       data.num--;
3733         lock = this.display_message(this.get_label('searching'), 'loading');
3734         xhr = this.http_post(props.action, '_search='+urlencode(s_val)+'&_id='+reqid
3735           +'&_source='+urlencode(source), lock);
3736
3737         this.ksearch_data.locks.push(lock);
3738         this.ksearch_data.requests.push(xhr);
3739       }
3740     }
3741   };
3742
3743   this.ksearch_click = function(node)
3744   {
3745     if (this.ksearch_input)
3746       this.ksearch_input.focus();
3747
3748     this.insert_recipient(node._rcm_id);
3749     this.ksearch_hide();
3750   };
3751
3752   this.ksearch_blur = function()
3753   {
3754     if (this.ksearch_timer)
3755       clearTimeout(this.ksearch_timer);
3756
3757     this.ksearch_input = null;
3758     this.ksearch_hide();
3759   };
3760
3761   this.ksearch_hide = function()
3762   {
3763     this.ksearch_selected = null;
3764     this.ksearch_value = '';
3765
3766     if (this.ksearch_pane)
3767       this.ksearch_pane.hide();
3768
3769     this.ksearch_destroy();
3770   };
3771
3772   // Aborts pending autocomplete requests
3773   this.ksearch_destroy = function()
3774   {
3775     var i, len, ac = this.ksearch_data;
3776
3777     if (!ac)
3778       return;
3779
3780     for (i=0, len=ac.locks.length; i<len; i++)
3781       this.abort_request({request: ac.requests[i], lock: ac.locks[i]});
3782
3783     this.ksearch_data = null;
3784   }
3785
3786   /*********************************************************/
3787   /*********         address book methods          *********/
3788   /*********************************************************/
3789
3790   this.contactlist_keypress = function(list)
3791   {
3792     if (list.key_pressed == list.DELETE_KEY)
3793       this.command('delete');
3794   };
3795
3796   this.contactlist_select = function(list)
3797   {
3798     if (this.preview_timer)
3799       clearTimeout(this.preview_timer);
3800
3801     var n, id, sid, ref = this, writable = false,
3802       source = this.env.source ? this.env.address_sources[this.env.source] : null;
3803
3804     if (id = list.get_single_selection())
3805       this.preview_timer = window.setTimeout(function(){ ref.load_contact(id, 'show'); }, 200);
3806     else if (this.env.contentframe)
3807       this.show_contentframe(false);
3808
3809     // no source = search result, we'll need to detect if any of
3810     // selected contacts are in writable addressbook to enable edit/delete
3811     if (list.selection.length) {
3812       if (!source) {
3813         for (n in list.selection) {
3814           sid = String(list.selection[n]).replace(/^[^-]+-/, '');
3815           if (sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly) {
3816             writable = true;
3817             break;
3818           }
3819         }
3820       }
3821       else {
3822         writable = !source.readonly;
3823       }
3824     }
3825
3826     this.enable_command('compose', list.selection.length > 0);
3827     this.enable_command('edit', id && writable);
3828     this.enable_command('delete', list.selection.length && writable);
3829
3830     return false;
3831   };
3832
3833   this.list_contacts = function(src, group, page)
3834   {
3835     var add_url = '',
3836       target = window;
3837
3838     if (!src)
3839       src = this.env.source;
3840
3841     if (page && this.current_page == page && src == this.env.source && group == this.env.group)
3842       return false;
3843
3844     if (src != this.env.source) {
3845       page = this.env.current_page = 1;
3846       this.reset_qsearch();
3847     }
3848     else if (group != this.env.group)
3849       page = this.env.current_page = 1;
3850
3851     this.select_folder((group ? 'G'+src+group : src), (this.env.group ? 'G'+this.env.source+this.env.group : this.env.source));
3852
3853     this.env.source = src;
3854     this.env.group = group;
3855
3856     // load contacts remotely
3857     if (this.gui_objects.contactslist) {
3858       this.list_contacts_remote(src, group, page);
3859       return;
3860     }
3861
3862     if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
3863       target = window.frames[this.env.contentframe];
3864       add_url = '&_framed=1';
3865     }
3866
3867     if (group)
3868       add_url += '&_gid='+group;
3869     if (page)
3870       add_url += '&_page='+page;
3871
3872     // also send search request to get the correct listing
3873     if (this.env.search_request)
3874       add_url += '&_search='+this.env.search_request;
3875
3876     this.set_busy(true, 'loading');
3877     this.location_href(this.env.comm_path + (src ? '&_source='+urlencode(src) : '') + add_url, target);
3878   };
3879
3880   // send remote request to load contacts list
3881   this.list_contacts_remote = function(src, group, page)
3882   {
3883     // clear message list first
3884     this.list_contacts_clear();
3885
3886     // send request to server
3887     var url = (src ? '_source='+urlencode(src) : '') + (page ? (src?'&':'') + '_page='+page : ''),
3888       lock = this.set_busy(true, 'loading');
3889
3890     this.env.source = src;
3891     this.env.group = group;
3892
3893     if (group)
3894       url += '&_gid='+group;
3895
3896     // also send search request to get the right messages 
3897     if (this.env.search_request) 
3898       url += '&_search='+this.env.search_request;
3899
3900     this.http_request('list', url, lock);
3901   };
3902
3903   this.list_contacts_clear = function()
3904   {
3905     this.contact_list.clear(true);
3906     this.show_contentframe(false);
3907     this.enable_command('delete', 'compose', false);
3908   };
3909
3910   // load contact record
3911   this.load_contact = function(cid, action, framed)
3912   {
3913     var add_url = '', target = window;
3914
3915     if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
3916       add_url = '&_framed=1';
3917       target = window.frames[this.env.contentframe];
3918       this.show_contentframe(true);
3919
3920       // load dummy content
3921       if (!cid) {
3922         // unselect selected row(s)
3923         this.contact_list.clear_selection();
3924         this.enable_command('delete', 'compose', false);
3925       }
3926     }
3927     else if (framed)
3928       return false;
3929
3930     if (action && (cid || action=='add') && !this.drag_active) {
3931       if (this.env.group)
3932         add_url += '&_gid='+urlencode(this.env.group);
3933
3934       this.location_href(this.env.comm_path+'&_action='+action
3935         +'&_source='+urlencode(this.env.source)
3936         +'&_cid='+urlencode(cid) + add_url, target, true);
3937     }
3938     return true;
3939   };
3940
3941   // add/delete member to/from the group
3942   this.group_member_change = function(what, cid, source, gid)
3943   {
3944     what = what == 'add' ? 'add' : 'del';
3945     var lock = this.display_message(this.get_label(what == 'add' ? 'addingmember' : 'removingmember'), 'loading');
3946
3947     this.http_post('group-'+what+'members', '_cid='+urlencode(cid)
3948       + '&_source='+urlencode(source)
3949       + '&_gid='+urlencode(gid), lock);
3950   };
3951
3952   // copy a contact to the specified target (group or directory)
3953   this.copy_contact = function(cid, to)
3954   {
3955     if (!cid)
3956       cid = this.contact_list.get_selection().join(',');
3957
3958     if (to.type == 'group' && to.source == this.env.source)
3959       this.group_member_change('add', cid, to.source, to.id);
3960     else if (to.type == 'group' && !this.env.address_sources[to.source].readonly) {
3961       var lock = this.display_message(this.get_label('copyingcontact'), 'loading');
3962       this.http_post('copy', '_cid='+urlencode(cid)
3963         + '&_source='+urlencode(this.env.source)
3964         + '&_to='+urlencode(to.source)
3965         + '&_togid='+urlencode(to.id)
3966         + (this.env.group ? '&_gid='+urlencode(this.env.group) : ''), lock);
3967     }
3968     else if (to.id != this.env.source && cid && this.env.address_sources[to.id] && !this.env.address_sources[to.id].readonly) {
3969       var lock = this.display_message(this.get_label('copyingcontact'), 'loading');
3970       this.http_post('copy', '_cid='+urlencode(cid)
3971         + '&_source='+urlencode(this.env.source)
3972         + '&_to='+urlencode(to.id)
3973         + (this.env.group ? '&_gid='+urlencode(this.env.group) : ''), lock);
3974     }
3975   };
3976
3977   this.delete_contacts = function()
3978   {
3979     // exit if no mailbox specified or if selection is empty
3980     var selection = this.contact_list.get_selection();
3981     if (!(selection.length || this.env.cid) || !confirm(this.get_label('deletecontactconfirm')))
3982       return;
3983
3984     var id, n, a_cids = [], qs = '';
3985
3986     if (this.env.cid)
3987       a_cids.push(this.env.cid);
3988     else {
3989       for (n=0; n<selection.length; n++) {
3990         id = selection[n];
3991         a_cids.push(id);
3992         this.contact_list.remove_row(id, (n == selection.length-1));
3993       }
3994
3995       // hide content frame if we delete the currently displayed contact
3996       if (selection.length == 1)
3997         this.show_contentframe(false);
3998     }
3999
4000     if (this.env.group)
4001       qs += '&_gid='+urlencode(this.env.group);
4002
4003     // also send search request to get the right records from the next page
4004     if (this.env.search_request)
4005       qs += '&_search='+this.env.search_request;
4006
4007     // send request to server
4008     this.http_post('delete', '_cid='+urlencode(a_cids.join(','))+'&_source='+urlencode(this.env.source)+'&_from='+(this.env.action ? this.env.action : '')+qs);
4009
4010     return true;
4011   };
4012
4013   // update a contact record in the list
4014   this.update_contact_row = function(cid, cols_arr, newcid, source)
4015   {
4016     var c, row, list = this.contact_list;
4017
4018     cid = String(cid).replace(this.identifier_expr, '_');
4019
4020     // when in searching mode, concat cid with the source name
4021     if (!list.rows[cid]) {
4022       cid = cid+'-'+source;
4023       if (newcid)
4024         newcid = newcid+'-'+source;
4025     }
4026
4027     if (list.rows[cid] && (row = list.rows[cid].obj)) {
4028       for (c=0; c<cols_arr.length; c++)
4029         if (row.cells[c])
4030           $(row.cells[c]).html(cols_arr[c]);
4031
4032       // cid change
4033       if (newcid) {
4034         newcid = String(newcid).replace(this.identifier_expr, '_');
4035         row.id = 'rcmrow' + newcid;
4036         list.remove_row(cid);
4037         list.init_row(row);
4038         list.selection[0] = newcid;
4039         row.style.display = '';
4040       }
4041     }
4042   };
4043
4044   // add row to contacts list
4045   this.add_contact_row = function(cid, cols, select)
4046   {
4047     if (!this.gui_objects.contactslist || !this.gui_objects.contactslist.tBodies[0])
4048       return false;
4049
4050     var tbody = this.gui_objects.contactslist.tBodies[0],
4051       rowcount = tbody.rows.length,
4052       even = rowcount%2,
4053       row = document.createElement('tr');
4054
4055     row.id = 'rcmrow'+String(cid).replace(this.identifier_expr, '_');
4056     row.className = 'contact '+(even ? 'even' : 'odd');
4057
4058     if (this.contact_list.in_selection(cid))
4059       row.className += ' selected';
4060
4061     // add each submitted col
4062     for (var c in cols) {
4063       col = document.createElement('td');
4064       col.className = String(c).toLowerCase();
4065       col.innerHTML = cols[c];
4066       row.appendChild(col);
4067     }
4068
4069     this.contact_list.insert_row(row);
4070
4071     this.enable_command('export', (this.contact_list.rowcount > 0));
4072   };
4073
4074   this.init_contact_form = function()
4075   {
4076     var ref = this, col;
4077
4078     this.set_photo_actions($('#ff_photo').val());
4079
4080     for (col in this.env.coltypes)
4081       this.init_edit_field(col, null);
4082
4083     $('.contactfieldgroup .row a.deletebutton').click(function() {
4084       ref.delete_edit_field(this);
4085       return false;
4086     });
4087
4088     $('select.addfieldmenu').change(function(e) {
4089       ref.insert_edit_field($(this).val(), $(this).attr('rel'), this);
4090       this.selectedIndex = 0;
4091     });
4092
4093     $("input[type='text']:visible").first().focus();
4094   };
4095
4096   this.group_create = function()
4097   {
4098     if (!this.gui_objects.folderlist)
4099       return;
4100
4101     if (!this.name_input) {
4102       this.name_input = $('<input>').attr('type', 'text');
4103       this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
4104       this.name_input_li = $('<li>').addClass('contactgroup').append(this.name_input);
4105
4106       var li = this.get_folder_li(this.env.source)
4107       this.name_input_li.insertAfter(li);
4108     }
4109
4110     this.name_input.select().focus();
4111   };
4112
4113   this.group_rename = function()
4114   {
4115     if (!this.env.group || !this.gui_objects.folderlist)
4116       return;
4117
4118     if (!this.name_input) {
4119       this.enable_command('list', 'listgroup', false);
4120       this.name_input = $('<input>').attr('type', 'text').val(this.env.contactgroups['G'+this.env.source+this.env.group].name);
4121       this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); });
4122       this.env.group_renaming = true;
4123
4124       var link, li = this.get_folder_li(this.env.source+this.env.group, 'rcmliG');
4125       if (li && (link = li.firstChild)) {
4126         $(link).hide().before(this.name_input);
4127       }
4128     }
4129
4130     this.name_input.select().focus();
4131   };
4132
4133   this.group_delete = function()
4134   {
4135     if (this.env.group && confirm(this.get_label('deletegroupconfirm'))) {
4136       var lock = this.set_busy(true, 'groupdeleting');
4137       this.http_post('group-delete', '_source='+urlencode(this.env.source)+'&_gid='+urlencode(this.env.group), lock);
4138     }
4139   };
4140
4141   // callback from server upon group-delete command
4142   this.remove_group_item = function(prop)
4143   {
4144     var li, key = 'G'+prop.source+prop.id;
4145     if ((li = this.get_folder_li(key))) {
4146       this.triggerEvent('group_delete', { source:prop.source, id:prop.id, li:li });
4147
4148       li.parentNode.removeChild(li);
4149       delete this.env.contactfolders[key];
4150       delete this.env.contactgroups[key];
4151     }
4152
4153     this.list_contacts(prop.source, 0);
4154   };
4155
4156   // handler for keyboard events on the input field
4157   this.add_input_keydown = function(e)
4158   {
4159     var key = rcube_event.get_keycode(e);
4160
4161     // enter
4162     if (key == 13) {
4163       var newname = this.name_input.val();
4164
4165       if (newname) {
4166         var lock = this.set_busy(true, 'loading');
4167         if (this.env.group_renaming)
4168           this.http_post('group-rename', '_source='+urlencode(this.env.source)+'&_gid='+urlencode(this.env.group)+'&_name='+urlencode(newname), lock);
4169         else
4170           this.http_post('group-create', '_source='+urlencode(this.env.source)+'&_name='+urlencode(newname), lock);
4171       }
4172       return false;
4173     }
4174     // escape
4175     else if (key == 27)
4176       this.reset_add_input();
4177
4178     return true;
4179   };
4180
4181   this.reset_add_input = function()
4182   {
4183     if (this.name_input) {
4184       if (this.env.group_renaming) {
4185         var li = this.name_input.parent();
4186         li.children().last().show();
4187         this.env.group_renaming = false;
4188       }
4189
4190       this.name_input.remove();
4191
4192       if (this.name_input_li)
4193         this.name_input_li.remove();
4194
4195       this.name_input = this.name_input_li = null;
4196     }
4197
4198     this.enable_command('list', 'listgroup', true);
4199   };
4200
4201   // callback for creating a new contact group
4202   this.insert_contact_group = function(prop)
4203   {
4204     this.reset_add_input();
4205
4206     prop.type = 'group';
4207     var key = 'G'+prop.source+prop.id,
4208       link = $('<a>').attr('href', '#')
4209         .attr('rel', prop.source+':'+prop.id)
4210         .click(function() { return rcmail.command('listgroup', prop, this); })
4211         .html(prop.name),
4212       li = $('<li>').attr({id: 'rcmli'+key.replace(this.identifier_expr, '_'), 'class': 'contactgroup'})
4213         .append(link);
4214
4215     this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
4216     this.add_contact_group_row(prop, li);
4217
4218     this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:li[0] });
4219   };
4220
4221   // callback for renaming a contact group
4222   this.update_contact_group = function(prop)
4223   {
4224     this.reset_add_input();
4225
4226     var key = 'G'+prop.source+prop.id,
4227       li = this.get_folder_li(key),
4228       link;
4229
4230     // group ID has changed, replace link node and identifiers
4231     if (li && prop.newid) {
4232       var newkey = 'G'+prop.source+prop.newid,
4233         newprop = $.extend({}, prop);;
4234
4235       li.id = String('rcmli'+newkey).replace(this.identifier_expr, '_');
4236       this.env.contactfolders[newkey] = this.env.contactfolders[key];
4237       this.env.contactfolders[newkey].id = prop.newid;
4238       this.env.group = prop.newid;
4239
4240       delete this.env.contactfolders[key];
4241       delete this.env.contactgroups[key];
4242
4243       newprop.id = prop.newid;
4244       newprop.type = 'group';
4245
4246       link = $('<a>').attr('href', '#')
4247         .attr('rel', prop.source+':'+prop.newid)
4248         .click(function() { return rcmail.command('listgroup', newprop, this); })
4249         .html(prop.name);
4250       $(li).children().replaceWith(link);
4251     }
4252     // update displayed group name
4253     else if (li && (link = li.firstChild) && link.tagName.toLowerCase() == 'a')
4254       link.innerHTML = prop.name;
4255
4256     this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
4257     this.add_contact_group_row(prop, $(li), true);
4258
4259     this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:li[0], newid:prop.newid });
4260   };
4261
4262   // add contact group row to the list, with sorting
4263   this.add_contact_group_row = function(prop, li, reloc)
4264   {
4265     var row, name = prop.name.toUpperCase(),
4266       sibling = this.get_folder_li(prop.source),
4267       prefix = 'rcmliG'+(prop.source).replace(this.identifier_expr, '_');
4268
4269     // When renaming groups, we need to remove it from DOM and insert it in the proper place
4270     if (reloc) {
4271       row = li.clone(true);
4272       li.remove();
4273     }
4274     else
4275       row = li;
4276
4277     $('li[id^="'+prefix+'"]', this.gui_objects.folderlist).each(function(i, elem) {
4278       if (name >= $(this).text().toUpperCase())
4279         sibling = elem;
4280       else
4281         return false;
4282     });
4283
4284     row.insertAfter(sibling);
4285   };
4286
4287   this.update_group_commands = function()
4288   {
4289     var source = this.env.source != '' ? this.env.address_sources[this.env.source] : null;
4290     this.enable_command('group-create', (source && source.groups && !source.readonly));
4291     this.enable_command('group-rename', 'group-delete', (source && source.groups && this.env.group && !source.readonly));
4292   };
4293
4294   this.init_edit_field = function(col, elem)
4295   {
4296     if (!elem)
4297       elem = $('.ff_' + col);
4298
4299     elem.focus(function(){ ref.focus_textfield(this); })
4300       .blur(function(){ ref.blur_textfield(this); })
4301       .each(function(){ this._placeholder = this.title = ref.env.coltypes[col].label; ref.blur_textfield(this); });
4302   };
4303
4304   this.insert_edit_field = function(col, section, menu)
4305   {
4306     // just make pre-defined input field visible
4307     var elem = $('#ff_'+col);
4308     if (elem.length) {
4309       elem.show().focus();
4310       $(menu).children('option[value="'+col+'"]').prop('disabled', true);
4311     }
4312     else {
4313       var lastelem = $('.ff_'+col),
4314         appendcontainer = $('#contactsection'+section+' .contactcontroller'+col);
4315
4316       if (!appendcontainer.length)
4317         appendcontainer = $('<fieldset>').addClass('contactfieldgroup contactcontroller'+col).insertAfter($('#contactsection'+section+' .contactfieldgroup').last());
4318
4319       if (appendcontainer.length && appendcontainer.get(0).nodeName == 'FIELDSET') {
4320         var input, colprop = this.env.coltypes[col],
4321           row = $('<div>').addClass('row'),
4322           cell = $('<div>').addClass('contactfieldcontent data'),
4323           label = $('<div>').addClass('contactfieldlabel label');
4324
4325         if (colprop.subtypes_select)
4326           label.html(colprop.subtypes_select);
4327         else
4328           label.html(colprop.label);
4329
4330         var name_suffix = colprop.limit != 1 ? '[]' : '';
4331         if (colprop.type == 'text' || colprop.type == 'date') {
4332           input = $('<input>')
4333             .addClass('ff_'+col)
4334             .attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size})
4335             .appendTo(cell);
4336
4337           this.init_edit_field(col, input);
4338         }
4339         else if (colprop.type == 'composite') {
4340           var childcol, cp, first, templ, cols = [], suffices = [];
4341           // read template for composite field order
4342           if ((templ = this.env[col+'_template'])) {
4343             for (var j=0; j < templ.length; j++) {
4344               cols.push(templ[j][1]);
4345               suffices.push(templ[j][2]);
4346             }
4347           }
4348           else {  // list fields according to appearance in colprop
4349             for (childcol in colprop.childs)
4350               cols.push(childcol);
4351           }
4352
4353           for (var i=0; i < cols.length; i++) {
4354             childcol = cols[i];
4355             cp = colprop.childs[childcol];
4356             input = $('<input>')
4357               .addClass('ff_'+childcol)
4358               .attr({ type: 'text', name: '_'+childcol+name_suffix, size: cp.size })
4359               .appendTo(cell);
4360             cell.append(suffices[i] || " ");
4361             this.init_edit_field(childcol, input);
4362             if (!first) first = input;
4363           }
4364           input = first;  // set focus to the first of this composite fields
4365         }
4366         else if (colprop.type == 'select') {
4367           input = $('<select>')
4368             .addClass('ff_'+col)
4369             .attr('name', '_'+col+name_suffix)
4370             .appendTo(cell);
4371
4372           var options = input.attr('options');
4373           options[options.length] = new Option('---', '');
4374           if (colprop.options)
4375             $.each(colprop.options, function(i, val){ options[options.length] = new Option(val, i); });
4376         }
4377
4378         if (input) {
4379           var delbutton = $('<a href="#del"></a>')
4380             .addClass('contactfieldbutton deletebutton')
4381             .attr({title: this.get_label('delete'), rel: col})
4382             .html(this.env.delbutton)
4383             .click(function(){ ref.delete_edit_field(this); return false })
4384             .appendTo(cell);
4385
4386           row.append(label).append(cell).appendTo(appendcontainer.show());
4387           input.first().focus();
4388
4389           // disable option if limit reached
4390           if (!colprop.count) colprop.count = 0;
4391           if (++colprop.count == colprop.limit && colprop.limit)
4392             $(menu).children('option[value="'+col+'"]').prop('disabled', true);
4393         }
4394       }
4395     }
4396   };
4397
4398   this.delete_edit_field = function(elem)
4399   {
4400     var col = $(elem).attr('rel'),
4401       colprop = this.env.coltypes[col],
4402       fieldset = $(elem).parents('fieldset.contactfieldgroup'),
4403       addmenu = fieldset.parent().find('select.addfieldmenu');
4404
4405     // just clear input but don't hide the last field
4406     if (--colprop.count <= 0 && colprop.visible)
4407       $(elem).parent().children('input').val('').blur();
4408     else {
4409       $(elem).parents('div.row').remove();
4410       // hide entire fieldset if no more rows
4411       if (!fieldset.children('div.row').length)
4412         fieldset.hide();
4413     }
4414
4415     // enable option in add-field selector or insert it if necessary
4416     if (addmenu.length) {
4417       var option = addmenu.children('option[value="'+col+'"]');
4418       if (option.length)
4419         option.prop('disabled', false);
4420       else
4421         option = $('<option>').attr('value', col).html(colprop.label).appendTo(addmenu);
4422       addmenu.show();
4423     }
4424   };
4425
4426   this.upload_contact_photo = function(form)
4427   {
4428     if (form && form.elements._photo.value) {
4429       this.async_upload_form(form, 'upload-photo', function(e) {
4430         rcmail.set_busy(false, null, rcmail.photo_upload_id);
4431       });
4432
4433       // display upload indicator
4434       this.photo_upload_id = this.set_busy(true, 'uploading');
4435     }
4436   };
4437
4438   this.replace_contact_photo = function(id)
4439   {
4440     var img_src = id == '-del-' ? this.env.photo_placeholder :
4441       this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + this.env.cid + '&_photo=' + id;
4442
4443     this.set_photo_actions(id);
4444     $(this.gui_objects.contactphoto).children('img').attr('src', img_src);
4445   };
4446
4447   this.photo_upload_end = function()
4448   {
4449     this.set_busy(false, null, this.photo_upload_id);
4450     delete this.photo_upload_id;
4451   };
4452
4453   this.set_photo_actions = function(id)
4454   {
4455     var n, buttons = this.buttons['upload-photo'];
4456     for (n=0; buttons && n < buttons.length; n++)
4457       $('#'+buttons[n].id).html(this.get_label(id == '-del-' ? 'addphoto' : 'replacephoto'));
4458
4459     $('#ff_photo').val(id);
4460     this.enable_command('upload-photo', this.env.coltypes.photo ? true : false);
4461     this.enable_command('delete-photo', this.env.coltypes.photo && id != '-del-');
4462   };
4463
4464   // load advanced search page
4465   this.advanced_search = function()
4466   {
4467     var add_url = '&_form=1', target = window;
4468
4469     if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
4470       add_url += '&_framed=1';
4471       target = window.frames[this.env.contentframe];
4472       this.contact_list.clear_selection();
4473     }
4474
4475     this.location_href(this.env.comm_path+'&_action=search'+add_url, target, true);
4476
4477     return true;
4478   };
4479
4480   // unselect directory/group
4481   this.unselect_directory = function()
4482   {
4483     if (this.env.address_sources.length > 1 || this.env.group != '') {
4484       this.select_folder('', (this.env.group ? 'G'+this.env.source+this.env.group : this.env.source));
4485       this.env.group = '';
4486       this.env.source = '';
4487     }
4488   };
4489
4490
4491   /*********************************************************/
4492   /*********        user settings methods          *********/
4493   /*********************************************************/
4494
4495   // preferences section select and load options frame
4496   this.section_select = function(list)
4497   {
4498     var id = list.get_single_selection(), add_url = '', target = window;
4499
4500     if (id) {
4501       if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
4502         add_url = '&_framed=1';
4503         target = window.frames[this.env.contentframe];
4504       }
4505       this.location_href(this.env.comm_path+'&_action=edit-prefs&_section='+id+add_url, target, true);
4506     }
4507
4508     return true;
4509   };
4510
4511   this.identity_select = function(list)
4512   {
4513     var id;
4514     if (id = list.get_single_selection())
4515       this.load_identity(id, 'edit-identity');
4516   };
4517
4518   // load identity record
4519   this.load_identity = function(id, action)
4520   {
4521     if (action=='edit-identity' && (!id || id==this.env.iid))
4522       return false;
4523
4524     var add_url = '', target = window;
4525
4526     if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
4527       add_url = '&_framed=1';
4528       target = window.frames[this.env.contentframe];
4529       document.getElementById(this.env.contentframe).style.visibility = 'inherit';
4530     }
4531
4532     if (action && (id || action=='add-identity')) {
4533       this.set_busy(true);
4534       this.location_href(this.env.comm_path+'&_action='+action+'&_iid='+id+add_url, target);
4535     }
4536
4537     return true;
4538   };
4539
4540   this.delete_identity = function(id)
4541   {
4542     // exit if no mailbox specified or if selection is empty
4543     var selection = this.identity_list.get_selection();
4544     if (!(selection.length || this.env.iid))
4545       return;
4546
4547     if (!id)
4548       id = this.env.iid ? this.env.iid : selection[0];
4549
4550     // append token to request
4551     this.goto_url('delete-identity', '_iid='+id+'&_token='+this.env.request_token, true);
4552
4553     return true;
4554   };
4555
4556
4557   /*********************************************************/
4558   /*********        folder manager methods         *********/
4559   /*********************************************************/
4560
4561   this.init_subscription_list = function()
4562   {
4563     var p = this;
4564     this.subscription_list = new rcube_list_widget(this.gui_objects.subscriptionlist,
4565       {multiselect:false, draggable:true, keyboard:false, toggleselect:true});
4566     this.subscription_list.addEventListener('select', function(o){ p.subscription_select(o); });
4567     this.subscription_list.addEventListener('dragstart', function(o){ p.drag_active = true; });
4568     this.subscription_list.addEventListener('dragend', function(o){ p.subscription_move_folder(o); });
4569     this.subscription_list.row_init = function (row) {
4570       row.obj.onmouseover = function() { p.focus_subscription(row.id); };
4571       row.obj.onmouseout = function() { p.unfocus_subscription(row.id); };
4572     };
4573     this.subscription_list.init();
4574     $('#mailboxroot')
4575       .mouseover(function(){ p.focus_subscription(this.id); })
4576       .mouseout(function(){ p.unfocus_subscription(this.id); })
4577   };
4578
4579   this.focus_subscription = function(id)
4580   {
4581     var row, folder,
4582       delim = RegExp.escape(this.env.delimiter),
4583       reg = RegExp('['+delim+']?[^'+delim+']+$');
4584
4585     if (this.drag_active && this.env.mailbox && (row = document.getElementById(id)))
4586       if (this.env.subscriptionrows[id] &&
4587           (folder = this.env.subscriptionrows[id][0]) !== null
4588       ) {
4589         if (this.check_droptarget(folder) &&
4590             !this.env.subscriptionrows[this.get_folder_row_id(this.env.mailbox)][2] &&
4591             (folder != this.env.mailbox.replace(reg, '')) &&
4592             (!folder.match(new RegExp('^'+RegExp.escape(this.env.mailbox+this.env.delimiter))))
4593         ) {
4594           this.env.dstfolder = folder;
4595           $(row).addClass('droptarget');
4596         }
4597       }
4598   };
4599
4600   this.unfocus_subscription = function(id)
4601   {
4602     var row = $('#'+id);
4603
4604     this.env.dstfolder = null;
4605     if (this.env.subscriptionrows[id] && row[0])
4606       row.removeClass('droptarget');
4607     else
4608       $(this.subscription_list.frame).removeClass('droptarget');
4609   };
4610
4611   this.subscription_select = function(list)
4612   {
4613     var id, folder;
4614
4615     if (list && (id = list.get_single_selection()) &&
4616         (folder = this.env.subscriptionrows['rcmrow'+id])
4617     ) {
4618       this.env.mailbox = folder[0];
4619       this.show_folder(folder[0]);
4620       this.enable_command('delete-folder', !folder[2]);
4621     }
4622     else {
4623       this.env.mailbox = null;
4624       this.show_contentframe(false);
4625       this.enable_command('delete-folder', 'purge', false);
4626     }
4627   };
4628
4629   this.subscription_move_folder = function(list)
4630   {
4631     var delim = RegExp.escape(this.env.delimiter),
4632       reg = RegExp('['+delim+']?[^'+delim+']+$');
4633
4634     if (this.env.mailbox && this.env.dstfolder !== null && (this.env.dstfolder != this.env.mailbox) &&
4635         (this.env.dstfolder != this.env.mailbox.replace(reg, ''))
4636     ) {
4637       reg = new RegExp('[^'+delim+']*['+delim+']', 'g');
4638       var basename = this.env.mailbox.replace(reg, ''),
4639         newname = this.env.dstfolder === '' ? basename : this.env.dstfolder+this.env.delimiter+basename;
4640
4641       if (newname != this.env.mailbox) {
4642         this.http_post('rename-folder', '_folder_oldname='+urlencode(this.env.mailbox)+'&_folder_newname='+urlencode(newname), this.set_busy(true, 'foldermoving'));
4643         this.subscription_list.draglayer.hide();
4644       }
4645     }
4646     this.drag_active = false;
4647     this.unfocus_subscription(this.get_folder_row_id(this.env.dstfolder));
4648   };
4649
4650   // tell server to create and subscribe a new mailbox
4651   this.create_folder = function()
4652   {
4653     this.show_folder('', this.env.mailbox);
4654   };
4655
4656   // delete a specific mailbox with all its messages
4657   this.delete_folder = function(name)
4658   {
4659     var id = this.get_folder_row_id(name ? name : this.env.mailbox),
4660       folder = this.env.subscriptionrows[id][0];
4661
4662     if (folder && confirm(this.get_label('deletefolderconfirm'))) {
4663       var lock = this.set_busy(true, 'folderdeleting');
4664       this.http_post('delete-folder', '_mbox='+urlencode(folder), lock);
4665     }
4666   };
4667
4668   // Add folder row to the table and initialize it
4669   this.add_folder_row = function (name, display_name, is_protected, subscribed, skip_init, class_name)
4670   {
4671     if (!this.gui_objects.subscriptionlist)
4672       return false;
4673
4674     var row, n, i, tmp, folders, rowid, list = [], slist = [],
4675       tbody = this.gui_objects.subscriptionlist.tBodies[0],
4676       refrow = $('tr', tbody).get(1),
4677       id = 'rcmrow'+((new Date).getTime());
4678
4679     if (!refrow) {
4680       // Refresh page if we don't have a table row to clone
4681       this.goto_url('folders');
4682       return false;
4683     }
4684
4685     // clone a table row if there are existing rows
4686     row = $(refrow).clone(true);
4687
4688     // set ID, reset css class
4689     row.attr('id', id);
4690     row.attr('class', class_name);
4691
4692     // set folder name
4693     row.find('td:first').html(display_name);
4694
4695     // update subscription checkbox
4696     $('input[name="_subscribed[]"]', row).val(name)
4697       .prop({checked: subscribed ? true : false, disabled: is_protected ? true : false});
4698
4699     // add to folder/row-ID map
4700     this.env.subscriptionrows[id] = [name, display_name, 0];
4701
4702     // sort folders, to find a place where to insert the row
4703     folders = [];
4704     $.each(this.env.subscriptionrows, function(k,v){ folders.push(v) });
4705     folders.sort(function(a,b){ return a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0) });
4706
4707     for (n in folders) {
4708       // protected folder
4709       if (folders[n][2]) {
4710         slist.push(folders[n][0]);
4711         tmp = folders[n][0]+this.env.delimiter;
4712       }
4713       // protected folder's child
4714       else if (tmp && folders[n][0].indexOf(tmp) == 0)
4715         slist.push(folders[n][0]);
4716       // other
4717       else {
4718         list.push(folders[n][0]);
4719         tmp = null;
4720       }
4721     }
4722
4723     // check if subfolder of a protected folder
4724     for (n=0; n<slist.length; n++) {
4725       if (name.indexOf(slist[n]+this.env.delimiter) == 0)
4726         rowid = this.get_folder_row_id(slist[n]);
4727     }
4728
4729     // find folder position after sorting
4730     for (n=0; !rowid && n<list.length; n++) {
4731       if (n && list[n] == name)
4732         rowid = this.get_folder_row_id(list[n-1]);
4733     }
4734
4735     // add row to the table
4736     if (rowid)
4737       $('#'+rowid).after(row);
4738     else
4739       row.appendTo(tbody);
4740
4741     // update list widget
4742     this.subscription_list.clear_selection();
4743     if (!skip_init)
4744       this.init_subscription_list();
4745
4746     row = row.get(0);
4747     if (row.scrollIntoView)
4748       row.scrollIntoView();
4749
4750     return row;
4751   };
4752
4753   // replace an existing table row with a new folder line (with subfolders)
4754   this.replace_folder_row = function(oldfolder, newfolder, display_name, is_protected, class_name)
4755   {
4756     if (!this.gui_objects.subscriptionlist)
4757       return false;
4758
4759     var i, n, len, name, dispname, oldrow, tmprow, row, level,
4760       tbody = this.gui_objects.subscriptionlist.tBodies[0],
4761       folders = this.env.subscriptionrows,
4762       id = this.get_folder_row_id(oldfolder),
4763       regex = new RegExp('^'+RegExp.escape(oldfolder)),
4764       subscribed = $('input[name="_subscribed[]"]', $('#'+id)).prop('checked'),
4765       // find subfolders of renamed folder
4766       list = this.get_subfolders(oldfolder);
4767
4768     // replace an existing table row
4769     this._remove_folder_row(id);
4770     row = $(this.add_folder_row(newfolder, display_name, is_protected, subscribed, true, class_name));
4771
4772     // detect tree depth change
4773     if (len = list.length) {
4774       level = (oldfolder.split(this.env.delimiter)).length - (newfolder.split(this.env.delimiter)).length;
4775     }
4776
4777     // move subfolders to the new branch
4778     for (n=0; n<len; n++) {
4779       id = list[n];
4780       name = this.env.subscriptionrows[id][0];
4781       dispname = this.env.subscriptionrows[id][1];
4782       oldrow = $('#'+id);
4783       tmprow = oldrow.clone(true);
4784       oldrow.remove();
4785       row.after(tmprow);
4786       row = tmprow;
4787       // update folder index
4788       name = name.replace(regex, newfolder);
4789       $('input[name="_subscribed[]"]', row).val(name);
4790       this.env.subscriptionrows[id][0] = name;
4791       // update the name if level is changed
4792       if (level != 0) {
4793         if (level > 0) {
4794           for (i=level; i>0; i--)
4795             dispname = dispname.replace(/^&nbsp;&nbsp;&nbsp;&nbsp;/, '');
4796         }
4797         else {
4798           for (i=level; i<0; i++)
4799             dispname = '&nbsp;&nbsp;&nbsp;&nbsp;' + dispname;
4800         }
4801         row.find('td:first').html(dispname);
4802         this.env.subscriptionrows[id][1] = dispname;
4803       }
4804     }
4805
4806     // update list widget
4807     this.init_subscription_list();
4808   };
4809
4810   // remove the table row of a specific mailbox from the table
4811   this.remove_folder_row = function(folder, subs)
4812   {
4813     var n, len, list = [], id = this.get_folder_row_id(folder);
4814
4815     // get subfolders if any
4816     if (subs)
4817       list = this.get_subfolders(folder);
4818
4819     // remove old row
4820     this._remove_folder_row(id);
4821
4822     // remove subfolders
4823     for (n=0, len=list.length; n<len; n++)
4824       this._remove_folder_row(list[n]);
4825   };
4826
4827   this._remove_folder_row = function(id)
4828   {
4829     this.subscription_list.remove_row(id.replace(/^rcmrow/, ''));
4830     $('#'+id).remove();
4831     delete this.env.subscriptionrows[id];
4832   }
4833
4834   this.get_subfolders = function(folder)
4835   {
4836     var name, list = [],
4837       regex = new RegExp('^'+RegExp.escape(folder)+RegExp.escape(this.env.delimiter)),
4838       row = $('#'+this.get_folder_row_id(folder)).get(0);
4839
4840     while (row = row.nextSibling) {
4841       if (row.id) {
4842         name = this.env.subscriptionrows[row.id][0];
4843         if (regex.test(name)) {
4844           list.push(row.id);
4845         }
4846         else
4847           break;
4848       }
4849     }
4850
4851     return list;
4852   }
4853
4854   this.subscribe = function(folder)
4855   {
4856     if (folder) {
4857       var lock = this.display_message(this.get_label('foldersubscribing'), 'loading');
4858       this.http_post('subscribe', '_mbox='+urlencode(folder), lock);
4859     }
4860   };
4861
4862   this.unsubscribe = function(folder)
4863   {
4864     if (folder) {
4865       var lock = this.display_message(this.get_label('folderunsubscribing'), 'loading');
4866       this.http_post('unsubscribe', '_mbox='+urlencode(folder), lock);
4867     }
4868   };
4869
4870   // helper method to find a specific mailbox row ID
4871   this.get_folder_row_id = function(folder)
4872   {
4873     var id, folders = this.env.subscriptionrows;
4874     for (id in folders)
4875       if (folders[id] && folders[id][0] == folder)
4876         break;
4877
4878     return id;
4879   };
4880
4881   // when user select a folder in manager
4882   this.show_folder = function(folder, path, force)
4883   {
4884     var target = window,
4885       url = '&_action=edit-folder&_mbox='+urlencode(folder);
4886
4887     if (path)
4888       url += '&_path='+urlencode(path);
4889
4890     if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
4891       target = window.frames[this.env.contentframe];
4892       url += '&_framed=1';
4893     }
4894
4895     if (String(target.location.href).indexOf(url) >= 0 && !force) {
4896       this.show_contentframe(true);
4897     }
4898     else {
4899       this.location_href(this.env.comm_path+url, target, true);
4900     }
4901   };
4902
4903   // disables subscription checkbox (for protected folder)
4904   this.disable_subscription = function(folder)
4905   {
4906     var id = this.get_folder_row_id(folder);
4907     if (id)
4908       $('input[name="_subscribed[]"]', $('#'+id)).prop('disabled', true);
4909   };
4910
4911   this.folder_size = function(folder)
4912   {
4913     var lock = this.set_busy(true, 'loading');
4914     this.http_post('folder-size', '_mbox='+urlencode(folder), lock);
4915   };
4916
4917   this.folder_size_update = function(size)
4918   {
4919     $('#folder-size').replaceWith(size);
4920   };
4921
4922
4923   /*********************************************************/
4924   /*********           GUI functionality           *********/
4925   /*********************************************************/
4926
4927   var init_button = function(cmd, prop)
4928   {
4929     var elm = document.getElementById(prop.id);
4930     if (!elm)
4931       return;
4932
4933     var preload = false;
4934     if (prop.type == 'image') {
4935       elm = elm.parentNode;
4936       preload = true;
4937     }
4938
4939     elm._command = cmd;
4940     elm._id = prop.id;
4941     if (prop.sel) {
4942       elm.onmousedown = function(e){ return rcmail.button_sel(this._command, this._id); };
4943       elm.onmouseup = function(e){ return rcmail.button_out(this._command, this._id); };
4944       if (preload)
4945         new Image().src = prop.sel;
4946     }
4947     if (prop.over) {
4948       elm.onmouseover = function(e){ return rcmail.button_over(this._command, this._id); };
4949       elm.onmouseout = function(e){ return rcmail.button_out(this._command, this._id); };
4950       if (preload)
4951         new Image().src = prop.over;
4952     }
4953   };
4954
4955   // enable/disable buttons for page shifting
4956   this.set_page_buttons = function()
4957   {
4958     this.enable_command('nextpage', 'lastpage', (this.env.pagecount > this.env.current_page));
4959     this.enable_command('previouspage', 'firstpage', (this.env.current_page > 1));
4960   };
4961
4962   // set event handlers on registered buttons
4963   this.init_buttons = function()
4964   {
4965     for (var cmd in this.buttons) {
4966       if (typeof cmd !== 'string')
4967         continue;
4968
4969       for (var i=0; i< this.buttons[cmd].length; i++) {
4970         init_button(cmd, this.buttons[cmd][i]);
4971       }
4972     }
4973   };
4974
4975   // set button to a specific state
4976   this.set_button = function(command, state)
4977   {
4978     var button, obj, a_buttons = this.buttons[command];
4979
4980     if (!a_buttons || !a_buttons.length)
4981       return false;
4982
4983     for (var n=0; n<a_buttons.length; n++) {
4984       button = a_buttons[n];
4985       obj = document.getElementById(button.id);
4986
4987       // get default/passive setting of the button
4988       if (obj && button.type=='image' && !button.status) {
4989         button.pas = obj._original_src ? obj._original_src : obj.src;
4990         // respect PNG fix on IE browsers
4991         if (obj.runtimeStyle && obj.runtimeStyle.filter && obj.runtimeStyle.filter.match(/src=['"]([^'"]+)['"]/))
4992           button.pas = RegExp.$1;
4993       }
4994       else if (obj && !button.status)
4995         button.pas = String(obj.className);
4996
4997       // set image according to button state
4998       if (obj && button.type=='image' && button[state]) {
4999         button.status = state;
5000         obj.src = button[state];
5001       }
5002       // set class name according to button state
5003       else if (obj && button[state] !== undefined) {
5004         button.status = state;
5005         obj.className = button[state];
5006       }
5007       // disable/enable input buttons
5008       if (obj && button.type=='input') {
5009         button.status = state;
5010         obj.disabled = !state;
5011       }
5012     }
5013   };
5014
5015   // display a specific alttext
5016   this.set_alttext = function(command, label)
5017   {
5018     if (!this.buttons[command] || !this.buttons[command].length)
5019       return;
5020
5021     var button, obj, link;
5022     for (var n=0; n<this.buttons[command].length; n++) {
5023       button = this.buttons[command][n];
5024       obj = document.getElementById(button.id);
5025
5026       if (button.type=='image' && obj) {
5027         obj.setAttribute('alt', this.get_label(label));
5028         if ((link = obj.parentNode) && link.tagName.toLowerCase() == 'a')
5029           link.setAttribute('title', this.get_label(label));
5030       }
5031       else if (obj)
5032         obj.setAttribute('title', this.get_label(label));
5033     }
5034   };
5035
5036   // mouse over button
5037   this.button_over = function(command, id)
5038   {
5039     var button, elm, a_buttons = this.buttons[command];
5040
5041     if (!a_buttons || !a_buttons.length)
5042       return false;
5043
5044     for (var n=0; n<a_buttons.length; n++) {
5045       button = a_buttons[n];
5046       if (button.id == id && button.status == 'act') {
5047         elm = document.getElementById(button.id);
5048         if (elm && button.over) {
5049           if (button.type == 'image')
5050             elm.src = button.over;
5051           else
5052             elm.className = button.over;
5053         }
5054       }
5055     }
5056   };
5057
5058   // mouse down on button
5059   this.button_sel = function(command, id)
5060   {
5061     var button, elm, a_buttons = this.buttons[command];
5062
5063     if (!a_buttons || !a_buttons.length)
5064       return;
5065
5066     for (var n=0; n<a_buttons.length; n++) {
5067       button = a_buttons[n];
5068       if (button.id == id && button.status == 'act') {
5069         elm = document.getElementById(button.id);
5070         if (elm && button.sel) {
5071           if (button.type == 'image')
5072             elm.src = button.sel;
5073           else
5074             elm.className = button.sel;
5075         }
5076         this.buttons_sel[id] = command;
5077       }
5078     }
5079   };
5080
5081   // mouse out of button
5082   this.button_out = function(command, id)
5083   {
5084     var button, elm, a_buttons = this.buttons[command];
5085
5086     if (!a_buttons || !a_buttons.length)
5087       return;
5088
5089     for (var n=0; n<a_buttons.length; n++) {
5090       button = a_buttons[n];
5091       if (button.id == id && button.status == 'act') {
5092         elm = document.getElementById(button.id);
5093         if (elm && button.act) {
5094           if (button.type == 'image')
5095             elm.src = button.act;
5096           else
5097             elm.className = button.act;
5098         }
5099       }
5100     }
5101   };
5102
5103
5104   this.focus_textfield = function(elem)
5105   {
5106     elem._hasfocus = true;
5107     var $elem = $(elem);
5108     if ($elem.hasClass('placeholder') || $elem.val() == elem._placeholder)
5109       $elem.val('').removeClass('placeholder').attr('spellcheck', true);
5110   };
5111
5112   this.blur_textfield = function(elem)
5113   {
5114     elem._hasfocus = false;
5115     var $elem = $(elem);
5116     if (elem._placeholder && (!$elem.val() || $elem.val() == elem._placeholder))
5117       $elem.addClass('placeholder').attr('spellcheck', false).val(elem._placeholder);
5118   };
5119
5120   // write to the document/window title
5121   this.set_pagetitle = function(title)
5122   {
5123     if (title && document.title)
5124       document.title = title;
5125   };
5126
5127   // display a system message, list of types in common.css (below #message definition)
5128   this.display_message = function(msg, type, timeout)
5129   {
5130     // pass command to parent window
5131     if (this.is_framed())
5132       return parent.rcmail.display_message(msg, type, timeout);
5133
5134     if (!this.gui_objects.message) {
5135       // save message in order to display after page loaded
5136       if (type != 'loading')
5137         this.pending_message = new Array(msg, type, timeout);
5138       return false;
5139     }
5140
5141     type = type ? type : 'notice';
5142
5143     var ref = this,
5144       key = String(msg).replace(this.identifier_expr, '_'),
5145       date = new Date(),
5146       id = type + date.getTime();
5147
5148     if (!timeout)
5149       timeout = this.message_time * (type == 'error' || type == 'warning' ? 2 : 1);
5150
5151     if (type == 'loading') {
5152       key = 'loading';
5153       timeout = this.env.request_timeout * 1000;
5154       if (!msg)
5155         msg = this.get_label('loading');
5156     }
5157
5158     // The same message is already displayed
5159     if (this.messages[key]) {
5160       // replace label
5161       if (this.messages[key].obj)
5162         this.messages[key].obj.html(msg);
5163       // store label in stack
5164       if (type == 'loading') {
5165         this.messages[key].labels.push({'id': id, 'msg': msg});
5166       }
5167       // add element and set timeout
5168       this.messages[key].elements.push(id);
5169       window.setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
5170       return id;
5171     }
5172
5173     // create DOM object and display it
5174     var obj = $('<div>').addClass(type).html(msg).data('key', key),
5175       cont = $(this.gui_objects.message).append(obj).show();
5176
5177     this.messages[key] = {'obj': obj, 'elements': [id]};
5178
5179     if (type == 'loading') {
5180       this.messages[key].labels = [{'id': id, 'msg': msg}];
5181     }
5182     else {
5183       obj.click(function() { return ref.hide_message(obj); });
5184     }
5185
5186     if (timeout > 0)
5187       window.setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
5188     return id;
5189   };
5190
5191   // make a message to disapear
5192   this.hide_message = function(obj, fade)
5193   {
5194     // pass command to parent window
5195     if (this.is_framed())
5196       return parent.rcmail.hide_message(obj, fade);
5197
5198     var k, n, i, msg, m = this.messages;
5199
5200     // Hide message by object, don't use for 'loading'!
5201     if (typeof obj === 'object') {
5202       $(obj)[fade?'fadeOut':'hide']();
5203       msg = $(obj).data('key');
5204       if (this.messages[msg])
5205         delete this.messages[msg];
5206     }
5207     // Hide message by id
5208     else {
5209       for (k in m) {
5210         for (n in m[k].elements) {
5211           if (m[k] && m[k].elements[n] == obj) {
5212             m[k].elements.splice(n, 1);
5213             // hide DOM element if last instance is removed
5214             if (!m[k].elements.length) {
5215               m[k].obj[fade?'fadeOut':'hide']();
5216               delete m[k];
5217             }
5218             // set pending action label for 'loading' message
5219             else if (k == 'loading') {
5220               for (i in m[k].labels) {
5221                 if (m[k].labels[i].id == obj) {
5222                   delete m[k].labels[i];
5223                 }
5224                 else {
5225                   msg = m[k].labels[i].msg;
5226                 }
5227                 m[k].obj.html(msg);
5228               }
5229             }
5230           }
5231         }
5232       }
5233     }
5234   };
5235
5236   // mark a mailbox as selected and set environment variable
5237   this.select_folder = function(name, old, prefix)
5238   {
5239     if (this.gui_objects.folderlist) {
5240       var current_li, target_li;
5241
5242       if ((current_li = this.get_folder_li(old, prefix))) {
5243         $(current_li).removeClass('selected').addClass('unfocused');
5244       }
5245       if ((target_li = this.get_folder_li(name, prefix))) {
5246         $(target_li).removeClass('unfocused').addClass('selected');
5247       }
5248
5249       // trigger event hook
5250       this.triggerEvent('selectfolder', { folder:name, old:old, prefix:prefix });
5251     }
5252   };
5253
5254   // helper method to find a folder list item
5255   this.get_folder_li = function(name, prefix)
5256   {
5257     if (!prefix)
5258       prefix = 'rcmli';
5259
5260     if (this.gui_objects.folderlist) {
5261       name = String(name).replace(this.identifier_expr, '_');
5262       return document.getElementById(prefix+name);
5263     }
5264
5265     return null;
5266   };
5267
5268   // for reordering column array (Konqueror workaround)
5269   // and for setting some message list global variables
5270   this.set_message_coltypes = function(coltypes, repl)
5271   {
5272     var list = this.message_list,
5273       thead = list ? list.list.tHead : null,
5274       cell, col, n, len, th, tr;
5275
5276     this.env.coltypes = coltypes;
5277
5278     // replace old column headers
5279     if (thead) {
5280       if (repl) {
5281         th = document.createElement('thead');
5282         tr = document.createElement('tr');
5283
5284         for (c=0, len=repl.length; c < len; c++) {
5285           cell = document.createElement('td');
5286           cell.innerHTML = repl[c].html;
5287           if (repl[c].id) cell.id = repl[c].id;
5288           if (repl[c].className) cell.className = repl[c].className;
5289           tr.appendChild(cell);
5290         }
5291         th.appendChild(tr);
5292         thead.parentNode.replaceChild(th, thead);
5293         thead = th;
5294       }
5295
5296       for (n=0, len=this.env.coltypes.length; n<len; n++) {
5297         col = this.env.coltypes[n];
5298         if ((cell = thead.rows[0].cells[n]) && (col=='from' || col=='to')) {
5299           cell.id = 'rcm'+col;
5300           // if we have links for sorting, it's a bit more complicated...
5301           if (cell.firstChild && cell.firstChild.tagName.toLowerCase()=='a') {
5302             cell = cell.firstChild;
5303             cell.onclick = function(){ return rcmail.command('sort', this.__col, this); };
5304             cell.__col = col;
5305           }
5306           cell.innerHTML = this.get_label(col);
5307         }
5308       }
5309     }
5310
5311     this.env.subject_col = null;
5312     this.env.flagged_col = null;
5313     this.env.status_col = null;
5314
5315     if ((n = $.inArray('subject', this.env.coltypes)) >= 0) {
5316       this.env.subject_col = n;
5317       if (list)
5318         list.subject_col = n;
5319     }
5320     if ((n = $.inArray('flag', this.env.coltypes)) >= 0)
5321       this.env.flagged_col = n;
5322     if ((n = $.inArray('status', this.env.coltypes)) >= 0)
5323       this.env.status_col = n;
5324
5325     if (list)
5326       list.init_header();
5327   };
5328
5329   // replace content of row count display
5330   this.set_rowcount = function(text)
5331   {
5332     $(this.gui_objects.countdisplay).html(text);
5333
5334     // update page navigation buttons
5335     this.set_page_buttons();
5336   };
5337
5338   // replace content of mailboxname display
5339   this.set_mailboxname = function(content)
5340   {
5341     if (this.gui_objects.mailboxname && content)
5342       this.gui_objects.mailboxname.innerHTML = content;
5343   };
5344
5345   // replace content of quota display
5346   this.set_quota = function(content)
5347   {
5348     if (content && this.gui_objects.quotadisplay) {
5349       if (typeof content === 'object' && content.type == 'image')
5350         this.percent_indicator(this.gui_objects.quotadisplay, content);
5351       else
5352         $(this.gui_objects.quotadisplay).html(content);
5353     }
5354   };
5355
5356   // update the mailboxlist
5357   this.set_unread_count = function(mbox, count, set_title)
5358   {
5359     if (!this.gui_objects.mailboxlist)
5360       return false;
5361
5362     this.env.unread_counts[mbox] = count;
5363     this.set_unread_count_display(mbox, set_title);
5364   };
5365
5366   // update the mailbox count display
5367   this.set_unread_count_display = function(mbox, set_title)
5368   {
5369     var reg, link, text_obj, item, mycount, childcount, div;
5370
5371     if (item = this.get_folder_li(mbox)) {
5372       mycount = this.env.unread_counts[mbox] ? this.env.unread_counts[mbox] : 0;
5373       link = $(item).children('a').eq(0);
5374       text_obj = link.children('span.unreadcount');
5375       if (!text_obj.length && mycount)
5376         text_obj = $('<span>').addClass('unreadcount').appendTo(link);
5377       reg = /\s+\([0-9]+\)$/i;
5378
5379       childcount = 0;
5380       if ((div = item.getElementsByTagName('div')[0]) &&
5381           div.className.match(/collapsed/)) {
5382         // add children's counters
5383         for (var k in this.env.unread_counts) 
5384           if (k.indexOf(mbox + this.env.delimiter) == 0)
5385             childcount += this.env.unread_counts[k];
5386       }
5387
5388       if (mycount && text_obj.length)
5389         text_obj.html(' ('+mycount+')');
5390       else if (text_obj.length)
5391         text_obj.remove();
5392
5393       // set parent's display
5394       reg = new RegExp(RegExp.escape(this.env.delimiter) + '[^' + RegExp.escape(this.env.delimiter) + ']+$');
5395       if (mbox.match(reg))
5396         this.set_unread_count_display(mbox.replace(reg, ''), false);
5397
5398       // set the right classes
5399       if ((mycount+childcount)>0)
5400         $(item).addClass('unread');
5401       else
5402         $(item).removeClass('unread');
5403     }
5404
5405     // set unread count to window title
5406     reg = /^\([0-9]+\)\s+/i;
5407     if (set_title && document.title) {
5408       var new_title = '',
5409         doc_title = String(document.title);
5410
5411       if (mycount && doc_title.match(reg))
5412         new_title = doc_title.replace(reg, '('+mycount+') ');
5413       else if (mycount)
5414         new_title = '('+mycount+') '+doc_title;
5415       else
5416         new_title = doc_title.replace(reg, '');
5417
5418       this.set_pagetitle(new_title);
5419     }
5420   };
5421
5422   this.toggle_prefer_html = function(checkbox)
5423   {
5424     var elem;
5425     if (elem = document.getElementById('rcmfd_addrbook_show_images'))
5426       elem.disabled = !checkbox.checked;
5427   };
5428
5429   this.toggle_preview_pane = function(checkbox)
5430   {
5431     var elem;
5432     if (elem = document.getElementById('rcmfd_preview_pane_mark_read'))
5433       elem.disabled = !checkbox.checked;
5434   };
5435
5436   // display fetched raw headers
5437   this.set_headers = function(content)
5438   {
5439     if (this.gui_objects.all_headers_row && this.gui_objects.all_headers_box && content)
5440       $(this.gui_objects.all_headers_box).html(content).show();
5441   };
5442
5443   // display all-headers row and fetch raw message headers
5444   this.load_headers = function(elem)
5445   {
5446     if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box || !this.env.uid)
5447       return;
5448
5449     $(elem).removeClass('show-headers').addClass('hide-headers');
5450     $(this.gui_objects.all_headers_row).show();
5451     elem.onclick = function() { rcmail.hide_headers(elem); };
5452
5453     // fetch headers only once
5454     if (!this.gui_objects.all_headers_box.innerHTML) {
5455       var lock = this.display_message(this.get_label('loading'), 'loading');
5456       this.http_post('headers', '_uid='+this.env.uid, lock);
5457     }
5458   };
5459
5460   // hide all-headers row
5461   this.hide_headers = function(elem)
5462   {
5463     if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box)
5464       return;
5465
5466     $(elem).removeClass('hide-headers').addClass('show-headers');
5467     $(this.gui_objects.all_headers_row).hide();
5468     elem.onclick = function() { rcmail.load_headers(elem); };
5469   };
5470
5471   // percent (quota) indicator
5472   this.percent_indicator = function(obj, data)
5473   {
5474     if (!data || !obj)
5475       return false;
5476
5477     var limit_high = 80,
5478       limit_mid  = 55,
5479       width = data.width ? data.width : this.env.indicator_width ? this.env.indicator_width : 100,
5480       height = data.height ? data.height : this.env.indicator_height ? this.env.indicator_height : 14,
5481       quota = data.percent ? Math.abs(parseInt(data.percent)) : 0,
5482       quota_width = parseInt(quota / 100 * width),
5483       pos = $(obj).position();
5484
5485     // workarounds for Opera and Webkit bugs
5486     pos.top = Math.max(0, pos.top);
5487     pos.left = Math.max(0, pos.left);
5488
5489     this.env.indicator_width = width;
5490     this.env.indicator_height = height;
5491
5492     // overlimit
5493     if (quota_width > width) {
5494       quota_width = width;
5495       quota = 100; 
5496     }
5497
5498     if (data.title)
5499       data.title = this.get_label('quota') + ': ' +  data.title;
5500
5501     // main div
5502     var main = $('<div>');
5503     main.css({position: 'absolute', top: pos.top, left: pos.left,
5504             width: width + 'px', height: height + 'px', zIndex: 100, lineHeight: height + 'px'})
5505           .attr('title', data.title).addClass('quota_text').html(quota + '%');
5506     // used bar
5507     var bar1 = $('<div>');
5508     bar1.css({position: 'absolute', top: pos.top + 1, left: pos.left + 1,
5509             width: quota_width + 'px', height: height + 'px', zIndex: 99});
5510     // background
5511     var bar2 = $('<div>');
5512     bar2.css({position: 'absolute', top: pos.top + 1, left: pos.left + 1,
5513             width: width + 'px', height: height + 'px', zIndex: 98})
5514           .addClass('quota_bg');
5515
5516     if (quota >= limit_high) {
5517       main.addClass(' quota_text_high');
5518       bar1.addClass('quota_high');
5519     }
5520     else if(quota >= limit_mid) {
5521       main.addClass(' quota_text_mid');
5522       bar1.addClass('quota_mid');
5523     }
5524     else {
5525       main.addClass(' quota_text_low');
5526       bar1.addClass('quota_low');
5527     }
5528
5529     // replace quota image
5530     $(obj).html('').append(bar1).append(bar2).append(main);
5531     // update #quotaimg title
5532     $('#quotaimg').attr('title', data.title);
5533   };
5534
5535   /********************************************************/
5536   /*********  html to text conversion functions   *********/
5537   /********************************************************/
5538
5539   this.html2plain = function(htmlText, id)
5540   {
5541     var rcmail = this,
5542       url = '?_task=utils&_action=html2text',
5543       lock = this.set_busy(true, 'converting');
5544
5545     this.log('HTTP POST: ' + url);
5546
5547     $.ajax({ type: 'POST', url: url, data: htmlText, contentType: 'application/octet-stream',
5548       error: function(o, status, err) { rcmail.http_error(o, status, err, lock); },
5549       success: function(data) { rcmail.set_busy(false, null, lock); $(document.getElementById(id)).val(data); rcmail.log(data); }
5550     });
5551   };
5552
5553   this.plain2html = function(plainText, id)
5554   {
5555     var lock = this.set_busy(true, 'converting');
5556     $(document.getElementById(id)).val('<pre>'+plainText+'</pre>');
5557     this.set_busy(false, null, lock);
5558   };
5559
5560
5561   /********************************************************/
5562   /*********        remote request methods        *********/
5563   /********************************************************/
5564
5565   // compose a valid url with the given parameters
5566   this.url = function(action, query)
5567   {
5568     var querystring = typeof query === 'string' ? '&' + query : '';
5569
5570     if (typeof action !== 'string')
5571       query = action;
5572     else if (!query || typeof query !== 'object')
5573       query = {};
5574
5575     if (action)
5576       query._action = action;
5577     else
5578       query._action = this.env.action;
5579
5580     var base = this.env.comm_path;
5581
5582     // overwrite task name
5583     if (query._action.match(/([a-z]+)\/([a-z-_.]+)/)) {
5584       query._action = RegExp.$2;
5585       base = base.replace(/\_task=[a-z]+/, '_task='+RegExp.$1);
5586     }
5587
5588     // remove undefined values
5589     var param = {};
5590     for (var k in query) {
5591       if (query[k] !== undefined && query[k] !== null)
5592         param[k] = query[k];
5593     }
5594
5595     return base + '&' + $.param(param) + querystring;
5596   };
5597
5598   this.redirect = function(url, lock)
5599   {
5600     if (lock || lock === null)
5601       this.set_busy(true);
5602
5603     if (this.is_framed())
5604       parent.rcmail.redirect(url, lock);
5605     else
5606       this.location_href(url, window);
5607   };
5608
5609   this.goto_url = function(action, query, lock)
5610   {
5611     this.redirect(this.url(action, query));
5612   };
5613
5614   this.location_href = function(url, target, frame)
5615   {
5616     if (frame)
5617       this.lock_frame();
5618
5619     // simulate real link click to force IE to send referer header
5620     if (bw.ie && target == window)
5621       $('<a>').attr('href', url).appendTo(document.body).get(0).click();
5622     else
5623       target.location.href = url;
5624   };
5625
5626   // send a http request to the server
5627   this.http_request = function(action, query, lock)
5628   {
5629     var url = this.url(action, query);
5630
5631     // trigger plugin hook
5632     var result = this.triggerEvent('request'+action, query);
5633
5634     if (result !== undefined) {
5635       // abort if one the handlers returned false
5636       if (result === false)
5637         return false;
5638       else
5639         query = result;
5640     }
5641
5642     url += '&_remote=1';
5643
5644     // send request
5645     this.log('HTTP GET: ' + url);
5646
5647     return $.ajax({
5648       type: 'GET', url: url, data: { _unlock:(lock?lock:0) }, dataType: 'json',
5649       success: function(data){ ref.http_response(data); },
5650       error: function(o, status, err) { rcmail.http_error(o, status, err, lock); }
5651     });
5652   };
5653
5654   // send a http POST request to the server
5655   this.http_post = function(action, postdata, lock)
5656   {
5657     var url = this.url(action);
5658
5659     if (postdata && typeof postdata === 'object') {
5660       postdata._remote = 1;
5661       postdata._unlock = (lock ? lock : 0);
5662     }
5663     else
5664       postdata += (postdata ? '&' : '') + '_remote=1' + (lock ? '&_unlock='+lock : '');
5665
5666     // trigger plugin hook
5667     var result = this.triggerEvent('request'+action, postdata);
5668     if (result !== undefined) {
5669       // abort if one the handlers returned false
5670       if (result === false)
5671         return false;
5672       else
5673         postdata = result;
5674     }
5675
5676     // send request
5677     this.log('HTTP POST: ' + url);
5678
5679     return $.ajax({
5680       type: 'POST', url: url, data: postdata, dataType: 'json',
5681       success: function(data){ ref.http_response(data); },
5682       error: function(o, status, err) { rcmail.http_error(o, status, err, lock); }
5683     });
5684   };
5685
5686   // aborts ajax request
5687   this.abort_request = function(r)
5688   {
5689     if (r.request)
5690       r.request.abort();
5691     if (r.lock)
5692       this.set_busy(false, null, r.lock);
5693   };
5694
5695   // handle HTTP response
5696   this.http_response = function(response)
5697   {
5698     if (!response)
5699       return;
5700
5701     if (response.unlock)
5702       this.set_busy(false);
5703
5704     this.triggerEvent('responsebefore', {response: response});
5705     this.triggerEvent('responsebefore'+response.action, {response: response});
5706
5707     // set env vars
5708     if (response.env)
5709       this.set_env(response.env);
5710
5711     // we have labels to add
5712     if (typeof response.texts === 'object') {
5713       for (var name in response.texts)
5714         if (typeof response.texts[name] === 'string')
5715           this.add_label(name, response.texts[name]);
5716     }
5717
5718     // if we get javascript code from server -> execute it
5719     if (response.exec) {
5720       this.log(response.exec);
5721       eval(response.exec);
5722     }
5723
5724     // execute callback functions of plugins
5725     if (response.callbacks && response.callbacks.length) {
5726       for (var i=0; i < response.callbacks.length; i++)
5727         this.triggerEvent(response.callbacks[i][0], response.callbacks[i][1]);
5728     }
5729
5730     // process the response data according to the sent action
5731     switch (response.action) {
5732       case 'delete':
5733         if (this.task == 'addressbook') {
5734           var sid, uid = this.contact_list.get_selection(), writable = false;
5735
5736           if (uid && this.contact_list.rows[uid]) {
5737             // search results, get source ID from record ID
5738             if (this.env.source == '') {
5739               sid = String(uid).replace(/^[^-]+-/, '');
5740               writable = sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly;
5741             }
5742             else {
5743               writable = !this.env.address_sources[this.env.source].readonly;
5744             }
5745           }
5746           this.enable_command('compose', (uid && this.contact_list.rows[uid]));
5747           this.enable_command('delete', 'edit', writable);
5748           this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
5749         }
5750
5751       case 'moveto':
5752         if (this.env.action == 'show') {
5753           // re-enable commands on move/delete error
5754           this.enable_command(this.env.message_commands, true);
5755           if (!this.env.list_post)
5756             this.enable_command('reply-list', false);
5757         }
5758         else if (this.task == 'addressbook') {
5759           this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
5760         }
5761
5762       case 'purge':
5763       case 'expunge':
5764         if (this.task == 'mail') {
5765           if (!this.env.messagecount) {
5766             // clear preview pane content
5767             if (this.env.contentframe)
5768               this.show_contentframe(false);
5769             // disable commands useless when mailbox is empty
5770             this.enable_command(this.env.message_commands, 'purge', 'expunge',
5771               'select-all', 'select-none', 'sort', 'expand-all', 'expand-unread', 'collapse-all', false);
5772           }
5773           if (this.message_list)
5774             this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });
5775         }
5776         break;
5777
5778       case 'check-recent':
5779       case 'getunread':
5780       case 'search':
5781         this.env.qsearch = null;
5782       case 'list':
5783         if (this.task == 'mail') {
5784           this.enable_command('show', 'expunge', 'select-all', 'select-none', 'sort', (this.env.messagecount > 0));
5785           this.enable_command('purge', this.purge_mailbox_test());
5786           this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount);
5787
5788           if (response.action == 'list' || response.action == 'search') {
5789             this.msglist_select(this.message_list);
5790             this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });
5791           }
5792         }
5793         else if (this.task == 'addressbook') {
5794           this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
5795
5796           if (response.action == 'list' || response.action == 'search') {
5797             this.update_group_commands();
5798             this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
5799           }
5800         }
5801         break;
5802     }
5803
5804     if (response.unlock)
5805       this.hide_message(response.unlock);
5806
5807     this.triggerEvent('responseafter', {response: response});
5808     this.triggerEvent('responseafter'+response.action, {response: response});
5809   };
5810
5811   // handle HTTP request errors
5812   this.http_error = function(request, status, err, lock)
5813   {
5814     var errmsg = request.statusText;
5815
5816     this.set_busy(false, null, lock);
5817     request.abort();
5818
5819     if (request.status && errmsg)
5820       this.display_message(this.get_label('servererror') + ' (' + errmsg + ')', 'error');
5821   };
5822
5823   // post the given form to a hidden iframe
5824   this.async_upload_form = function(form, action, onload)
5825   {
5826     var ts = new Date().getTime(),
5827       frame_name = 'rcmupload'+ts;
5828
5829     // upload progress support
5830     if (this.env.upload_progress_name) {
5831       var fname = this.env.upload_progress_name,
5832         field = $('input[name='+fname+']', form);
5833
5834       if (!field.length) {
5835         field = $('<input>').attr({type: 'hidden', name: fname});
5836         field.prependTo(form);
5837       }
5838
5839       field.val(ts);
5840     }
5841
5842     // have to do it this way for IE
5843     // otherwise the form will be posted to a new window
5844     if (document.all) {
5845       var html = '<iframe name="'+frame_name+'" src="program/blank.gif" style="width:0;height:0;visibility:hidden;"></iframe>';
5846       document.body.insertAdjacentHTML('BeforeEnd', html);
5847     }
5848     else { // for standards-compilant browsers
5849       var frame = document.createElement('iframe');
5850       frame.name = frame_name;
5851       frame.style.border = 'none';
5852       frame.style.width = 0;
5853       frame.style.height = 0;
5854       frame.style.visibility = 'hidden';
5855       document.body.appendChild(frame);
5856     }
5857
5858     // handle upload errors, parsing iframe content in onload
5859     $(frame_name).bind('load', {ts:ts}, onload);
5860
5861     $(form).attr({
5862         target: frame_name,
5863         action: this.url(action, { _id:this.env.compose_id||'', _uploadid:ts }),
5864         method: 'POST'})
5865       .attr(form.encoding ? 'encoding' : 'enctype', 'multipart/form-data')
5866       .submit();
5867
5868     return frame_name;
5869   };
5870
5871   // starts interval for keep-alive/check-recent signal
5872   this.start_keepalive = function()
5873   {
5874     if (this._int)
5875       clearInterval(this._int);
5876
5877     if (this.env.keep_alive && !this.env.framed && this.task == 'mail' && this.gui_objects.mailboxlist)
5878       this._int = setInterval(function(){ ref.check_for_recent(false); }, this.env.keep_alive * 1000);
5879     else if (this.env.keep_alive && !this.env.framed && this.task != 'login' && this.env.action != 'print')
5880       this._int = setInterval(function(){ ref.keep_alive(); }, this.env.keep_alive * 1000);
5881   };
5882
5883   // sends keep-alive signal
5884   this.keep_alive = function()
5885   {
5886     if (!this.busy)
5887       this.http_request('keep-alive');
5888   };
5889
5890   // sends request to check for recent messages
5891   this.check_for_recent = function(refresh)
5892   {
5893     if (this.busy)
5894       return;
5895
5896     var lock, addurl = '_mbox=' + urlencode(this.env.mailbox);
5897
5898     if (refresh) {
5899       lock = this.set_busy(true, 'checkingmail');
5900       addurl += '&_refresh=1';
5901       // reset check-recent interval
5902       this.start_keepalive();
5903     }
5904
5905     if (this.gui_objects.messagelist)
5906       addurl += '&_list=1';
5907     if (this.gui_objects.quotadisplay)
5908       addurl += '&_quota=1';
5909     if (this.env.search_request)
5910       addurl += '&_search=' + this.env.search_request;
5911
5912     this.http_request('check-recent', addurl, lock);
5913   };
5914
5915
5916   /********************************************************/
5917   /*********            helper methods            *********/
5918   /********************************************************/
5919
5920   // check if we're in show mode or if we have a unique selection
5921   // and return the message uid
5922   this.get_single_uid = function()
5923   {
5924     return this.env.uid ? this.env.uid : (this.message_list ? this.message_list.get_single_selection() : null);
5925   };
5926
5927   // same as above but for contacts
5928   this.get_single_cid = function()
5929   {
5930     return this.env.cid ? this.env.cid : (this.contact_list ? this.contact_list.get_single_selection() : null);
5931   };
5932
5933   // gets cursor position
5934   this.get_caret_pos = function(obj)
5935   {
5936     if (obj.selectionEnd !== undefined)
5937       return obj.selectionEnd;
5938     else if (document.selection && document.selection.createRange) {
5939       var range = document.selection.createRange();
5940       if (range.parentElement()!=obj)
5941         return 0;
5942
5943       var gm = range.duplicate();
5944       if (obj.tagName == 'TEXTAREA')
5945         gm.moveToElementText(obj);
5946       else
5947         gm.expand('textedit');
5948
5949       gm.setEndPoint('EndToStart', range);
5950       var p = gm.text.length;
5951
5952       return p<=obj.value.length ? p : -1;
5953     }
5954     else
5955       return obj.value.length;
5956   };
5957
5958   // moves cursor to specified position
5959   this.set_caret_pos = function(obj, pos)
5960   {
5961     if (obj.setSelectionRange)
5962       obj.setSelectionRange(pos, pos);
5963     else if (obj.createTextRange) {
5964       var range = obj.createTextRange();
5965       range.collapse(true);
5966       range.moveEnd('character', pos);
5967       range.moveStart('character', pos);
5968       range.select();
5969     }
5970   };
5971
5972   // disable/enable all fields of a form
5973   this.lock_form = function(form, lock)
5974   {
5975     if (!form || !form.elements)
5976       return;
5977
5978     var n, len, elm;
5979
5980     if (lock)
5981       this.disabled_form_elements = [];
5982
5983     for (n=0, len=form.elements.length; n<len; n++) {
5984       elm = form.elements[n];
5985
5986       if (elm.type == 'hidden')
5987         continue;
5988       // remember which elem was disabled before lock
5989       if (lock && elm.disabled)
5990         this.disabled_form_elements.push(elm);
5991       // check this.disabled_form_elements before inArray() as a workaround for FF5 bug
5992       // http://bugs.jquery.com/ticket/9873
5993       else if (lock || (this.disabled_form_elements && $.inArray(elm, this.disabled_form_elements)<0))
5994         elm.disabled = lock;
5995     }
5996   };
5997
5998 }  // end object rcube_webmail
5999
6000
6001 // some static methods
6002 rcube_webmail.long_subject_title = function(elem, indent)
6003 {
6004   if (!elem.title) {
6005     var $elem = $(elem);
6006     if ($elem.width() + indent * 15 > $elem.parent().width())
6007       elem.title = $elem.html();
6008   }
6009 };
6010
6011 // copy event engine prototype
6012 rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
6013 rcube_webmail.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
6014 rcube_webmail.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;
6015