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