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