Aleksander Machniak
2016-03-26 e8b6e726f6e023a46e23c982ba7b3f76f0b657cb
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;
08da30 1372     this.triggerEvent('actionafter', { props:props, action:command, aborted:aborted });
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,
0e7b66 2041       unread_children: flags.unread_children?flags.unread_children:0,
A 2042       parent_uid: flags.parent_uid?flags.parent_uid:0,
56f41a 2043       selected: this.select_all_mode || this.message_list.in_selection(uid),
e25a35 2044       ml: flags.ml?1:0,
6b4929 2045       ctype: flags.ctype,
9684dc 2046       mbox: flags.mbox,
56f41a 2047       // flags from plugins
A 2048       flags: flags.extra_flags
f52c93 2049     });
T 2050
8fd955 2051     var c, n, col, html, css_class, label, status_class = '', status_label = '',
03fe1c 2052       tree = '', expando = '',
488074 2053       list = this.message_list,
A 2054       rows = list.rows,
8fa922 2055       message = this.env.messages[uid],
c3ce9c 2056       msg_id = this.html_identifier(uid,true),
03fe1c 2057       row_class = 'message'
609d39 2058         + (!flags.seen ? ' unread' : '')
f52c93 2059         + (flags.deleted ? ' deleted' : '')
T 2060         + (flags.flagged ? ' flagged' : '')
488074 2061         + (message.selected ? ' selected' : ''),
c3ce9c 2062       row = { cols:[], style:{}, id:'rcmrow'+msg_id, uid:uid };
519aed 2063
4438d6 2064     // message status icons
e94706 2065     css_class = 'msgicon';
98f2c9 2066     if (this.env.status_col === null) {
A 2067       css_class += ' status';
8fd955 2068       if (flags.deleted) {
TB 2069         status_class += ' deleted';
2070         status_label += this.get_label('deleted') + ' ';
2071       }
2072       else if (!flags.seen) {
2073         status_class += ' unread';
2074         status_label += this.get_label('unread') + ' ';
2075       }
2076       else if (flags.unread_children > 0) {
2077         status_class += ' unreadchildren';
2078       }
98f2c9 2079     }
8fd955 2080     if (flags.answered) {
TB 2081       status_class += ' replied';
2082       status_label += this.get_label('replied') + ' ';
2083     }
2084     if (flags.forwarded) {
2085       status_class += ' forwarded';
adc23f 2086       status_label += this.get_label('forwarded') + ' ';
8fd955 2087     }
519aed 2088
488074 2089     // update selection
A 2090     if (message.selected && !list.in_selection(uid))
2091       list.selection.push(uid);
2092
8fa922 2093     // threads
519aed 2094     if (this.env.threading) {
f52c93 2095       if (message.depth) {
c84d33 2096         // This assumes that div width is hardcoded to 15px,
c3ce9c 2097         tree += '<span id="rcmtab' + msg_id + '" class="branch" style="width:' + (message.depth * 15) + 'px;">&nbsp;&nbsp;</span>';
c84d33 2098
488074 2099         if ((rows[message.parent_uid] && rows[message.parent_uid].expanded === false)
A 2100           || ((this.env.autoexpand_threads == 0 || this.env.autoexpand_threads == 2) &&
2101             (!rows[message.parent_uid] || !rows[message.parent_uid].expanded))
2102         ) {
f52c93 2103           row.style.display = 'none';
T 2104           message.expanded = false;
2105         }
2106         else
2107           message.expanded = true;
03fe1c 2108
T 2109         row_class += ' thread expanded';
488074 2110       }
f52c93 2111       else if (message.has_children) {
d8cf6d 2112         if (message.expanded === undefined && (this.env.autoexpand_threads == 1 || (this.env.autoexpand_threads == 2 && message.unread_children))) {
f52c93 2113           message.expanded = true;
T 2114         }
2115
1bbf8c 2116         expando = '<div id="rcmexpando' + row.id + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">&nbsp;&nbsp;</div>';
03fe1c 2117         row_class += ' thread' + (message.expanded? ' expanded' : '');
c84d33 2118       }
7c494b 2119
AM 2120       if (flags.unread_children && flags.seen && !message.expanded)
2121         row_class += ' unroot';
519aed 2122     }
f52c93 2123
8fd955 2124     tree += '<span id="msgicn'+row.id+'" class="'+css_class+status_class+'" title="'+status_label+'"></span>';
03fe1c 2125     row.className = row_class;
8fa922 2126
adaddf 2127     // build subject link
0ca978 2128     if (cols.subject) {
e8bcf0 2129       var action  = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show',
TB 2130         uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid',
2131         query = { _mbox: flags.mbox };
2132       query[uid_param] = uid;
2133       cols.subject = '<a href="' + this.url(action, query) + '" onclick="return rcube_event.keyboard_only(event)"' +
2134         ' onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')" tabindex="-1"><span>'+cols.subject+'</span></a>';
f52c93 2135     }
T 2136
2137     // add each submitted col
c83535 2138     for (n in this.env.listcols) {
TB 2139       c = this.env.listcols[n];
7a5c3a 2140       col = {className: String(c).toLowerCase(), events:{}};
c83535 2141
TB 2142       if (this.env.coltypes[c] && this.env.coltypes[c].hidden) {
2143         col.className += ' hidden';
2144       }
f52c93 2145
dbd069 2146       if (c == 'flag') {
e94706 2147         css_class = (flags.flagged ? 'flagged' : 'unflagged');
8fd955 2148         label = this.get_label(css_class);
TB 2149         html = '<span id="flagicn'+row.id+'" class="'+css_class+'" title="'+label+'"></span>';
e94706 2150       }
A 2151       else if (c == 'attachment') {
8fd955 2152         label = this.get_label('withattachment');
a20496 2153         if (flags.attachmentClass)
8fd955 2154           html = '<span class="'+flags.attachmentClass+'" title="'+label+'"></span>';
a20496 2155         else if (/application\/|multipart\/(m|signed)/.test(flags.ctype))
8fd955 2156           html = '<span class="attachment" title="'+label+'"></span>';
32c657 2157         else if (/multipart\/report/.test(flags.ctype))
8fd955 2158           html = '<span class="report"></span>';
TB 2159           else
6b4929 2160           html = '&nbsp;';
4438d6 2161       }
A 2162       else if (c == 'status') {
8fd955 2163         label = '';
TB 2164         if (flags.deleted) {
4438d6 2165           css_class = 'deleted';
8fd955 2166           label = this.get_label('deleted');
TB 2167         }
2168         else if (!flags.seen) {
4438d6 2169           css_class = 'unread';
8fd955 2170           label = this.get_label('unread');
TB 2171         }
2172         else if (flags.unread_children > 0) {
98f2c9 2173           css_class = 'unreadchildren';
8fd955 2174         }
4438d6 2175         else
A 2176           css_class = 'msgicon';
8fd955 2177         html = '<span id="statusicn'+row.id+'" class="'+css_class+status_class+'" title="'+label+'"></span>';
f52c93 2178       }
6c9d49 2179       else if (c == 'threads')
A 2180         html = expando;
065d70 2181       else if (c == 'subject') {
7a5c3a 2182         if (bw.ie)
AM 2183           col.events.mouseover = function() { rcube_webmail.long_subject_title_ex(this); };
f52c93 2184         html = tree + cols[c];
065d70 2185       }
7a2bad 2186       else if (c == 'priority') {
8fd955 2187         if (flags.prio > 0 && flags.prio < 6) {
TB 2188           label = this.get_label('priority') + ' ' + flags.prio;
2189           html = '<span class="prio'+flags.prio+'" title="'+label+'"></span>';
2190         }
7a2bad 2191         else
A 2192           html = '&nbsp;';
2193       }
31aa08 2194       else if (c == 'folder') {
TB 2195         html = '<span onmouseover="rcube_webmail.long_subject_title(this)">' + cols[c] + '<span>';
2196       }
f52c93 2197       else
T 2198         html = cols[c];
2199
2200       col.innerHTML = html;
517dae 2201       row.cols.push(col);
f52c93 2202     }
T 2203
488074 2204     list.insert_row(row, attop);
f52c93 2205
T 2206     // remove 'old' row
488074 2207     if (attop && this.env.pagesize && list.rowcount > this.env.pagesize) {
A 2208       var uid = list.get_last_row();
2209       list.remove_row(uid);
2210       list.clear_selection(uid);
f52c93 2211     }
T 2212   };
2213
2214   this.set_list_sorting = function(sort_col, sort_order)
186537 2215   {
c49234 2216     var sort_old = this.env.sort_col == 'arrival' ? 'date' : this.env.sort_col,
AM 2217       sort_new = sort_col == 'arrival' ? 'date' : sort_col;
2218
f52c93 2219     // set table header class
c49234 2220     $('#rcm' + sort_old).removeClass('sorted' + this.env.sort_order.toUpperCase());
AM 2221     if (sort_new)
2222       $('#rcm' + sort_new).addClass('sorted' + sort_order);
2223
2224     // if sorting by 'arrival' is selected, click on date column should not switch to 'date'
2225     $('#rcmdate > a').prop('rel', sort_col == 'arrival' ? 'arrival' : 'date');
8fa922 2226
f52c93 2227     this.env.sort_col = sort_col;
T 2228     this.env.sort_order = sort_order;
186537 2229   };
f52c93 2230
T 2231   this.set_list_options = function(cols, sort_col, sort_order, threads)
186537 2232   {
c31360 2233     var update, post_data = {};
f52c93 2234
d8cf6d 2235     if (sort_col === undefined)
b5002a 2236       sort_col = this.env.sort_col;
A 2237     if (!sort_order)
2238       sort_order = this.env.sort_order;
b62c48 2239
f52c93 2240     if (this.env.sort_col != sort_col || this.env.sort_order != sort_order) {
T 2241       update = 1;
2242       this.set_list_sorting(sort_col, sort_order);
186537 2243     }
8fa922 2244
f52c93 2245     if (this.env.threading != threads) {
T 2246       update = 1;
c31360 2247       post_data._threads = threads;
186537 2248     }
f52c93 2249
b62c48 2250     if (cols && cols.length) {
A 2251       // make sure new columns are added at the end of the list
c83535 2252       var i, idx, name, newcols = [], oldcols = this.env.listcols;
b62c48 2253       for (i=0; i<oldcols.length; i++) {
e0efd8 2254         name = oldcols[i];
b62c48 2255         idx = $.inArray(name, cols);
A 2256         if (idx != -1) {
c3eab2 2257           newcols.push(name);
b62c48 2258           delete cols[idx];
6c9d49 2259         }
b62c48 2260       }
A 2261       for (i=0; i<cols.length; i++)
2262         if (cols[i])
c3eab2 2263           newcols.push(cols[i]);
b5002a 2264
6c9d49 2265       if (newcols.join() != oldcols.join()) {
b62c48 2266         update = 1;
c31360 2267         post_data._cols = newcols.join(',');
b62c48 2268       }
186537 2269     }
f52c93 2270
T 2271     if (update)
c31360 2272       this.list_mailbox('', '', sort_col+'_'+sort_order, post_data);
186537 2273   };
4e17e6 2274
271efe 2275   // when user double-clicks on a row
b19097 2276   this.show_message = function(id, safe, preview)
186537 2277   {
dbd069 2278     if (!id)
A 2279       return;
186537 2280
24fa5d 2281     var win, target = window,
602d74 2282       url = this.params_from_uid(id, {_caps: this.browser_capabilities()});
186537 2283
24fa5d 2284     if (preview && (win = this.get_frame_window(this.env.contentframe))) {
AM 2285       target = win;
04a688 2286       url._framed = 1;
186537 2287     }
6b47de 2288
4e17e6 2289     if (safe)
04a688 2290       url._safe = 1;
4e17e6 2291
1f020b 2292     // also send search request to get the right messages
S 2293     if (this.env.search_request)
04a688 2294       url._search = this.env.search_request;
e349a8 2295
271efe 2296     if (this.env.extwin)
04a688 2297       url._extwin = 1;
AM 2298
2299     url = this.url(preview ? 'preview': 'show', url);
271efe 2300
TB 2301     if (preview && String(target.location.href).indexOf(url) >= 0) {
bf2f39 2302       this.show_contentframe(true);
271efe 2303     }
bc4960 2304     else {
271efe 2305       if (!preview && this.env.message_extwin && !this.env.extwin)
04a688 2306         this.open_window(url, true);
271efe 2307       else
04a688 2308         this.location_href(url, target, true);
ca3c73 2309
bf2f39 2310       // mark as read and change mbox unread counter
d28dae 2311       if (preview && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read > 0) {
da5cad 2312         this.preview_read_timer = setTimeout(function() {
d28dae 2313           ref.set_unread_message(id, ref.env.mailbox);
6ca090 2314           ref.http_post('mark', {_uid: id, _flag: 'read', _mbox: ref.env.mailbox, _quiet: 1});
bc4960 2315         }, this.env.preview_pane_mark_read * 1000);
4e17e6 2316       }
bc4960 2317     }
T 2318   };
b19097 2319
d28dae 2320   // update message status and unread counter after marking a message as read
AM 2321   this.set_unread_message = function(id, folder)
2322   {
2323     var self = this;
2324
2325     // find window with messages list
2326     if (!self.message_list)
2327       self = self.opener();
2328
2329     if (!self && window.parent)
2330       self = parent.rcmail;
2331
2332     if (!self || !self.message_list)
2333       return;
2334
ae4873 2335     // this may fail in multifolder mode
AM 2336     if (self.set_message(id, 'unread', false) === false)
2337       self.set_message(id + '-' + folder, 'unread', false);
d28dae 2338
AM 2339     if (self.env.unread_counts[folder] > 0) {
2340       self.env.unread_counts[folder] -= 1;
ae4873 2341       self.set_unread_count(folder, self.env.unread_counts[folder], folder == 'INBOX' && !self.is_multifolder_listing());
d28dae 2342     }
AM 2343   };
2344
f11541 2345   this.show_contentframe = function(show)
186537 2346   {
24fa5d 2347     var frame, win, name = this.env.contentframe;
AM 2348
2349     if (name && (frame = this.get_frame_element(name))) {
2350       if (!show && (win = this.get_frame_window(name))) {
c511f5 2351         if (win.location.href.indexOf(this.env.blankpage) < 0) {
AM 2352           if (win.stop)
2353             win.stop();
2354           else // IE
2355             win.document.execCommand('Stop');
446dbe 2356
c511f5 2357           win.location.href = this.env.blankpage;
AM 2358         }
186537 2359       }
ca3c73 2360       else if (!bw.safari && !bw.konq)
24fa5d 2361         $(frame)[show ? 'show' : 'hide']();
AM 2362     }
ca3c73 2363
446dbe 2364     if (!show && this.env.frame_lock)
d808ba 2365       this.set_busy(false, null, this.env.frame_lock);
24fa5d 2366   };
AM 2367
2368   this.get_frame_element = function(id)
2369   {
2370     var frame;
2371
2372     if (id && (frame = document.getElementById(id)))
2373       return frame;
2374   };
2375
2376   this.get_frame_window = function(id)
2377   {
2378     var frame = this.get_frame_element(id);
2379
2380     if (frame && frame.name && window.frames)
2381       return window.frames[frame.name];
a16400 2382   };
A 2383
2384   this.lock_frame = function()
2385   {
2386     if (!this.env.frame_lock)
2387       (this.is_framed() ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
186537 2388   };
4e17e6 2389
T 2390   // list a specific page
2391   this.list_page = function(page)
186537 2392   {
dbd069 2393     if (page == 'next')
4e17e6 2394       page = this.env.current_page+1;
b0fd4c 2395     else if (page == 'last')
d17008 2396       page = this.env.pagecount;
b0fd4c 2397     else if (page == 'prev' && this.env.current_page > 1)
4e17e6 2398       page = this.env.current_page-1;
b0fd4c 2399     else if (page == 'first' && this.env.current_page > 1)
d17008 2400       page = 1;
186537 2401
A 2402     if (page > 0 && page <= this.env.pagecount) {
4e17e6 2403       this.env.current_page = page;
8fa922 2404
eeb73c 2405       if (this.task == 'addressbook' || this.contact_list)
053e5a 2406         this.list_contacts(this.env.source, this.env.group, page);
eeb73c 2407       else if (this.task == 'mail')
T 2408         this.list_mailbox(this.env.mailbox, page);
186537 2409     }
77de23 2410   };
AM 2411
2412   // sends request to check for recent messages
2413   this.checkmail = function()
2414   {
2415     var lock = this.set_busy(true, 'checkingmail'),
2416       params = this.check_recent_params();
2417
a59499 2418     this.http_post('check-recent', params, lock);
186537 2419   };
4e17e6 2420
e538b3 2421   // list messages of a specific mailbox using filter
A 2422   this.filter_mailbox = function(filter)
186537 2423   {
1ec105 2424     if (this.filter_disabled)
AM 2425       return;
2426
e9c47c 2427     var lock = this.set_busy(true, 'searching');
e538b3 2428
bb2699 2429     this.clear_message_list();
186537 2430
A 2431     // reset vars
2432     this.env.current_page = 1;
26b520 2433     this.env.search_filter = filter;
e9c47c 2434     this.http_request('search', this.search_params(false, filter), lock);
186537 2435   };
e538b3 2436
1e9a59 2437   // reload the current message listing
TB 2438   this.refresh_list = function()
2439   {
2440     this.list_mailbox(this.env.mailbox, this.env.current_page || 1, null, { _clear:1 }, true);
2441     if (this.message_list)
2442       this.message_list.clear_selection();
2443   };
2444
4e17e6 2445   // list messages of a specific mailbox
1e9a59 2446   this.list_mailbox = function(mbox, page, sort, url, update_only)
186537 2447   {
24fa5d 2448     var win, target = window;
c31360 2449
A 2450     if (typeof url != 'object')
2451       url = {};
4e17e6 2452
T 2453     if (!mbox)
4da0be 2454       mbox = this.env.mailbox ? this.env.mailbox : 'INBOX';
4e17e6 2455
f3b659 2456     // add sort to url if set
T 2457     if (sort)
c31360 2458       url._sort = sort;
f11541 2459
1ec105 2460     // folder change, reset page, search scope, etc.
488074 2461     if (this.env.mailbox != mbox) {
4e17e6 2462       page = 1;
T 2463       this.env.current_page = page;
1ec105 2464       this.env.search_scope = 'base';
488074 2465       this.select_all_mode = false;
da1816 2466       this.reset_search_filter();
186537 2467     }
1ec105 2468     // also send search request to get the right messages
AM 2469     else if (this.env.search_request)
2470       url._search = this.env.search_request;
be9d4d 2471
1e9a59 2472     if (!update_only) {
TB 2473       // unselect selected messages and clear the list and message data
2474       this.clear_message_list();
e737a5 2475
1e9a59 2476       if (mbox != this.env.mailbox || (mbox == this.env.mailbox && !page && !sort))
TB 2477         url._refresh = 1;
d9c83e 2478
1e9a59 2479       this.select_folder(mbox, '', true);
TB 2480       this.unmark_folder(mbox, 'recent', '', true);
2481       this.env.mailbox = mbox;
2482     }
4e17e6 2483
T 2484     // load message list remotely
186537 2485     if (this.gui_objects.messagelist) {
f52c93 2486       this.list_mailbox_remote(mbox, page, url);
4e17e6 2487       return;
186537 2488     }
8fa922 2489
24fa5d 2490     if (win = this.get_frame_window(this.env.contentframe)) {
AM 2491       target = win;
c31360 2492       url._framed = 1;
186537 2493     }
4e17e6 2494
64f7d6 2495     if (this.env.uid)
AM 2496       url._uid = this.env.uid;
2497
4e17e6 2498     // load message list to target frame/window
186537 2499     if (mbox) {
4e17e6 2500       this.set_busy(true, 'loading');
c31360 2501       url._mbox = mbox;
A 2502       if (page)
2503         url._page = page;
2504       this.location_href(url, target);
186537 2505     }
be9d4d 2506   };
A 2507
2508   this.clear_message_list = function()
2509   {
39a82a 2510     this.env.messages = {};
be9d4d 2511
39a82a 2512     this.show_contentframe(false);
AM 2513     if (this.message_list)
2514       this.message_list.clear(true);
186537 2515   };
4e17e6 2516
T 2517   // send remote request to load message list
1e9a59 2518   this.list_mailbox_remote = function(mbox, page, url)
186537 2519   {
c31360 2520     var lock = this.set_busy(true, 'loading');
A 2521
1e9a59 2522     if (typeof url != 'object')
TB 2523       url = {};
2524     url._mbox = mbox;
c31360 2525     if (page)
1e9a59 2526       url._page = page;
c31360 2527
1e9a59 2528     this.http_request('list', url, lock);
b2992d 2529     this.update_state({ _mbox: mbox, _page: (page && page > 1 ? page : null) });
186537 2530   };
488074 2531
A 2532   // removes messages that doesn't exists from list selection array
2533   this.update_selection = function()
2534   {
f1e7bb 2535     var list = this.message_list,
AM 2536       selected = list.selection,
2537       rows = list.rows,
488074 2538       i, selection = [];
A 2539
2540     for (i in selected)
2541       if (rows[selected[i]])
2542         selection.push(selected[i]);
2543
f1e7bb 2544     list.selection = selection;
5d42a9 2545
AM 2546     // reset preview frame, if currently previewed message is not selected (has been removed)
2547     try {
2548       var win = this.get_frame_window(this.env.contentframe),
2549         id = win.rcmail.env.uid;
2550
f1e7bb 2551       if (id && !list.in_selection(id))
5d42a9 2552         this.show_contentframe(false);
AM 2553     }
2554     catch (e) {};
a5fe9a 2555   };
15a9d1 2556
f52c93 2557   // expand all threads with unread children
T 2558   this.expand_unread = function()
186537 2559   {
1cb23c 2560     var r, tbody = this.message_list.tbody,
dbd069 2561       new_row = tbody.firstChild;
8fa922 2562
f52c93 2563     while (new_row) {
609d39 2564       if (new_row.nodeType == 1 && (r = this.message_list.rows[new_row.uid]) && r.unread_children) {
a945da 2565         this.message_list.expand_all(r);
A 2566         this.set_unread_children(r.uid);
f52c93 2567       }
a5fe9a 2568
186537 2569       new_row = new_row.nextSibling;
A 2570     }
a5fe9a 2571
f52c93 2572     return false;
186537 2573   };
4e17e6 2574
b5002a 2575   // thread expanding/collapsing handler
f52c93 2576   this.expand_message_row = function(e, uid)
186537 2577   {
f52c93 2578     var row = this.message_list.rows[uid];
5e3512 2579
f52c93 2580     // handle unread_children mark
T 2581     row.expanded = !row.expanded;
2582     this.set_unread_children(uid);
2583     row.expanded = !row.expanded;
2584
2585     this.message_list.expand_row(e, uid);
186537 2586   };
f11541 2587
f52c93 2588   // message list expanding
T 2589   this.expand_threads = function()
b5002a 2590   {
f52c93 2591     if (!this.env.threading || !this.env.autoexpand_threads || !this.message_list)
T 2592       return;
186537 2593
f52c93 2594     switch (this.env.autoexpand_threads) {
T 2595       case 2: this.expand_unread(); break;
2596       case 1: this.message_list.expand_all(); break;
2597     }
8fa922 2598   };
f52c93 2599
0e7b66 2600   // Initializes threads indicators/expanders after list update
bba252 2601   this.init_threads = function(roots, mbox)
0e7b66 2602   {
bba252 2603     // #1487752
A 2604     if (mbox && mbox != this.env.mailbox)
2605       return false;
2606
0e7b66 2607     for (var n=0, len=roots.length; n<len; n++)
54531f 2608       this.add_tree_icons(roots[n]);
A 2609     this.expand_threads();
0e7b66 2610   };
A 2611
2612   // adds threads tree icons to the list (or specified thread)
2613   this.add_tree_icons = function(root)
2614   {
2615     var i, l, r, n, len, pos, tmp = [], uid = [],
2616       row, rows = this.message_list.rows;
2617
2618     if (root)
2619       row = rows[root] ? rows[root].obj : null;
2620     else
517dae 2621       row = this.message_list.tbody.firstChild;
0e7b66 2622
A 2623     while (row) {
2624       if (row.nodeType == 1 && (r = rows[row.uid])) {
2625         if (r.depth) {
2626           for (i=tmp.length-1; i>=0; i--) {
2627             len = tmp[i].length;
2628             if (len > r.depth) {
2629               pos = len - r.depth;
2630               if (!(tmp[i][pos] & 2))
2631                 tmp[i][pos] = tmp[i][pos] ? tmp[i][pos]+2 : 2;
2632             }
2633             else if (len == r.depth) {
2634               if (!(tmp[i][0] & 2))
2635                 tmp[i][0] += 2;
2636             }
2637             if (r.depth > len)
2638               break;
2639           }
2640
2641           tmp.push(new Array(r.depth));
2642           tmp[tmp.length-1][0] = 1;
2643           uid.push(r.uid);
2644         }
2645         else {
2646           if (tmp.length) {
2647             for (i in tmp) {
2648               this.set_tree_icons(uid[i], tmp[i]);
2649             }
2650             tmp = [];
2651             uid = [];
2652           }
2653           if (root && row != rows[root].obj)
2654             break;
2655         }
2656       }
2657       row = row.nextSibling;
2658     }
2659
2660     if (tmp.length) {
2661       for (i in tmp) {
2662         this.set_tree_icons(uid[i], tmp[i]);
2663       }
2664     }
fb4663 2665   };
0e7b66 2666
A 2667   // adds tree icons to specified message row
2668   this.set_tree_icons = function(uid, tree)
2669   {
2670     var i, divs = [], html = '', len = tree.length;
2671
2672     for (i=0; i<len; i++) {
2673       if (tree[i] > 2)
2674         divs.push({'class': 'l3', width: 15});
2675       else if (tree[i] > 1)
2676         divs.push({'class': 'l2', width: 15});
2677       else if (tree[i] > 0)
2678         divs.push({'class': 'l1', width: 15});
2679       // separator div
2680       else if (divs.length && !divs[divs.length-1]['class'])
2681         divs[divs.length-1].width += 15;
2682       else
2683         divs.push({'class': null, width: 15});
2684     }
fb4663 2685
0e7b66 2686     for (i=divs.length-1; i>=0; i--) {
A 2687       if (divs[i]['class'])
2688         html += '<div class="tree '+divs[i]['class']+'" />';
2689       else
2690         html += '<div style="width:'+divs[i].width+'px" />';
2691     }
fb4663 2692
0e7b66 2693     if (html)
1bbf8c 2694       $('#rcmtab'+this.html_identifier(uid, true)).html(html);
0e7b66 2695   };
A 2696
f52c93 2697   // update parent in a thread
T 2698   this.update_thread_root = function(uid, flag)
0dbac3 2699   {
f52c93 2700     if (!this.env.threading)
T 2701       return;
2702
bc2acc 2703     var root = this.message_list.find_root(uid);
8fa922 2704
f52c93 2705     if (uid == root)
T 2706       return;
2707
2708     var p = this.message_list.rows[root];
2709
2710     if (flag == 'read' && p.unread_children) {
2711       p.unread_children--;
dbd069 2712     }
A 2713     else if (flag == 'unread' && p.has_children) {
f52c93 2714       // unread_children may be undefined
T 2715       p.unread_children = p.unread_children ? p.unread_children + 1 : 1;
dbd069 2716     }
A 2717     else {
f52c93 2718       return;
T 2719     }
2720
2721     this.set_message_icon(root);
2722     this.set_unread_children(root);
0dbac3 2723   };
f52c93 2724
T 2725   // update thread indicators for all messages in a thread below the specified message
2726   // return number of removed/added root level messages
2727   this.update_thread = function (uid)
2728   {
2729     if (!this.env.threading)
2730       return 0;
2731
dbd069 2732     var r, parent, count = 0,
A 2733       rows = this.message_list.rows,
2734       row = rows[uid],
2735       depth = rows[uid].depth,
2736       roots = [];
f52c93 2737
T 2738     if (!row.depth) // root message: decrease roots count
2739       count--;
2740     else if (row.unread) {
2741       // update unread_children for thread root
dbd069 2742       parent = this.message_list.find_root(uid);
f52c93 2743       rows[parent].unread_children--;
T 2744       this.set_unread_children(parent);
186537 2745     }
f52c93 2746
T 2747     parent = row.parent_uid;
2748
2749     // childrens
2750     row = row.obj.nextSibling;
2751     while (row) {
2752       if (row.nodeType == 1 && (r = rows[row.uid])) {
a945da 2753         if (!r.depth || r.depth <= depth)
A 2754           break;
f52c93 2755
a945da 2756         r.depth--; // move left
0e7b66 2757         // reset width and clear the content of a tab, icons will be added later
1bbf8c 2758         $('#rcmtab'+r.id).width(r.depth * 15).html('');
f52c93 2759         if (!r.depth) { // a new root
a945da 2760           count++; // increase roots count
A 2761           r.parent_uid = 0;
2762           if (r.has_children) {
2763             // replace 'leaf' with 'collapsed'
1bbf8c 2764             $('#'+r.id+' .leaf:first')
TB 2765               .attr('id', 'rcmexpando' + r.id)
a945da 2766               .attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed'))
d9ff47 2767               .mousedown({uid: r.uid}, function(e) {
AM 2768                 return ref.expand_message_row(e, e.data.uid);
2769               });
f52c93 2770
a945da 2771             r.unread_children = 0;
A 2772             roots.push(r);
2773           }
2774           // show if it was hidden
2775           if (r.obj.style.display == 'none')
2776             $(r.obj).show();
2777         }
2778         else {
2779           if (r.depth == depth)
2780             r.parent_uid = parent;
2781           if (r.unread && roots.length)
2782             roots[roots.length-1].unread_children++;
2783         }
2784       }
2785       row = row.nextSibling;
186537 2786     }
8fa922 2787
f52c93 2788     // update unread_children for roots
a5fe9a 2789     for (r=0; r<roots.length; r++)
AM 2790       this.set_unread_children(roots[r].uid);
f52c93 2791
T 2792     return count;
2793   };
2794
2795   this.delete_excessive_thread_rows = function()
2796   {
dbd069 2797     var rows = this.message_list.rows,
517dae 2798       tbody = this.message_list.tbody,
dbd069 2799       row = tbody.firstChild,
A 2800       cnt = this.env.pagesize + 1;
8fa922 2801
f52c93 2802     while (row) {
T 2803       if (row.nodeType == 1 && (r = rows[row.uid])) {
a945da 2804         if (!r.depth && cnt)
A 2805           cnt--;
f52c93 2806
T 2807         if (!cnt)
a945da 2808           this.message_list.remove_row(row.uid);
A 2809       }
2810       row = row.nextSibling;
186537 2811     }
A 2812   };
0dbac3 2813
25c35c 2814   // set message icon
A 2815   this.set_message_icon = function(uid)
2816   {
8fd955 2817     var css_class, label = '',
98f2c9 2818       row = this.message_list.rows[uid];
25c35c 2819
98f2c9 2820     if (!row)
25c35c 2821       return false;
e94706 2822
98f2c9 2823     if (row.icon) {
A 2824       css_class = 'msgicon';
8fd955 2825       if (row.deleted) {
98f2c9 2826         css_class += ' deleted';
8fd955 2827         label += this.get_label('deleted') + ' ';
TB 2828       }
2829       else if (row.unread) {
98f2c9 2830         css_class += ' unread';
8fd955 2831         label += this.get_label('unread') + ' ';
TB 2832       }
98f2c9 2833       else if (row.unread_children)
A 2834         css_class += ' unreadchildren';
2835       if (row.msgicon == row.icon) {
8fd955 2836         if (row.replied) {
98f2c9 2837           css_class += ' replied';
8fd955 2838           label += this.get_label('replied') + ' ';
TB 2839         }
2840         if (row.forwarded) {
98f2c9 2841           css_class += ' forwarded';
8fd955 2842           label += this.get_label('forwarded') + ' ';
TB 2843         }
98f2c9 2844         css_class += ' status';
A 2845       }
4438d6 2846
8fd955 2847       $(row.icon).attr('class', css_class).attr('title', label);
4438d6 2848     }
A 2849
98f2c9 2850     if (row.msgicon && row.msgicon != row.icon) {
8fd955 2851       label = '';
e94706 2852       css_class = 'msgicon';
8fd955 2853       if (!row.unread && row.unread_children) {
e94706 2854         css_class += ' unreadchildren';
8fd955 2855       }
TB 2856       if (row.replied) {
4438d6 2857         css_class += ' replied';
8fd955 2858         label += this.get_label('replied') + ' ';
TB 2859       }
2860       if (row.forwarded) {
4438d6 2861         css_class += ' forwarded';
8fd955 2862         label += this.get_label('forwarded') + ' ';
TB 2863       }
e94706 2864
8fd955 2865       $(row.msgicon).attr('class', css_class).attr('title', label);
f52c93 2866     }
e94706 2867
98f2c9 2868     if (row.flagicon) {
A 2869       css_class = (row.flagged ? 'flagged' : 'unflagged');
8fd955 2870       label = this.get_label(css_class);
TB 2871       $(row.flagicon).attr('class', css_class)
2872         .attr('aria-label', label)
2873         .attr('title', label);
186537 2874     }
A 2875   };
25c35c 2876
A 2877   // set message status
2878   this.set_message_status = function(uid, flag, status)
186537 2879   {
98f2c9 2880     var row = this.message_list.rows[uid];
25c35c 2881
98f2c9 2882     if (!row)
A 2883       return false;
25c35c 2884
5f3c7e 2885     if (flag == 'unread') {
AM 2886       if (row.unread != status)
2887         this.update_thread_root(uid, status ? 'unread' : 'read');
2888     }
cf22ce 2889
AM 2890     if ($.inArray(flag, ['unread', 'deleted', 'replied', 'forwarded', 'flagged']) > -1)
2891       row[flag] = status;
186537 2892   };
25c35c 2893
A 2894   // set message row status, class and icon
2895   this.set_message = function(uid, flag, status)
186537 2896   {
0746d5 2897     var row = this.message_list && this.message_list.rows[uid];
25c35c 2898
98f2c9 2899     if (!row)
A 2900       return false;
8fa922 2901
25c35c 2902     if (flag)
A 2903       this.set_message_status(uid, flag, status);
f52c93 2904
cf22ce 2905     if ($.inArray(flag, ['unread', 'deleted', 'flagged']) > -1)
AM 2906       $(row.obj)[row[flag] ? 'addClass' : 'removeClass'](flag);
163a13 2907
f52c93 2908     this.set_unread_children(uid);
25c35c 2909     this.set_message_icon(uid);
186537 2910   };
f52c93 2911
T 2912   // sets unroot (unread_children) class of parent row
2913   this.set_unread_children = function(uid)
186537 2914   {
f52c93 2915     var row = this.message_list.rows[uid];
186537 2916
0e7b66 2917     if (row.parent_uid)
f52c93 2918       return;
T 2919
2920     if (!row.unread && row.unread_children && !row.expanded)
2921       $(row.obj).addClass('unroot');
2922     else
2923       $(row.obj).removeClass('unroot');
186537 2924   };
9b3fdc 2925
A 2926   // copy selected messages to the specified mailbox
6789bf 2927   this.copy_messages = function(mbox, event)
186537 2928   {
d8cf6d 2929     if (mbox && typeof mbox === 'object')
488074 2930       mbox = mbox.id;
9a0153 2931     else if (!mbox)
6789bf 2932       return this.folder_selector(event, function(folder) { ref.command('copy', folder); });
488074 2933
463ce6 2934     // exit if current or no mailbox specified
AM 2935     if (!mbox || mbox == this.env.mailbox)
9b3fdc 2936       return;
A 2937
463ce6 2938     var post_data = this.selection_post_data({_target_mbox: mbox});
9b3fdc 2939
463ce6 2940     // exit if selection is empty
AM 2941     if (!post_data._uid)
2942       return;
c0c0c0 2943
9b3fdc 2944     // send request to server
463ce6 2945     this.http_post('copy', post_data, this.display_message(this.get_label('copyingmessage'), 'loading'));
186537 2946   };
0dbac3 2947
4e17e6 2948   // move selected messages to the specified mailbox
6789bf 2949   this.move_messages = function(mbox, event)
186537 2950   {
d8cf6d 2951     if (mbox && typeof mbox === 'object')
a61bbb 2952       mbox = mbox.id;
9a0153 2953     else if (!mbox)
6789bf 2954       return this.folder_selector(event, function(folder) { ref.command('move', folder); });
8fa922 2955
463ce6 2956     // exit if current or no mailbox specified
f50a66 2957     if (!mbox || (mbox == this.env.mailbox && !this.is_multifolder_listing()))
aa9836 2958       return;
e4bbb2 2959
463ce6 2960     var lock = false, post_data = this.selection_post_data({_target_mbox: mbox});
AM 2961
2962     // exit if selection is empty
2963     if (!post_data._uid)
2964       return;
4e17e6 2965
T 2966     // show wait message
c31360 2967     if (this.env.action == 'show')
ad334a 2968       lock = this.set_busy(true, 'movingmessage');
0b2ce9 2969     else
f11541 2970       this.show_contentframe(false);
6b47de 2971
faebf4 2972     // Hide message command buttons until a message is selected
14259c 2973     this.enable_command(this.env.message_commands, false);
faebf4 2974
a45f9b 2975     this._with_selected_messages('move', post_data, lock);
186537 2976   };
4e17e6 2977
857a38 2978   // delete selected messages from the current mailbox
c28161 2979   this.delete_messages = function(event)
84a331 2980   {
7eecf8 2981     var list = this.message_list, trash = this.env.trash_mailbox;
8fa922 2982
0b2ce9 2983     // if config is set to flag for deletion
f52c93 2984     if (this.env.flag_for_deletion) {
0b2ce9 2985       this.mark_message('delete');
f52c93 2986       return false;
84a331 2987     }
0b2ce9 2988     // if there isn't a defined trash mailbox or we are in it
476407 2989     else if (!trash || this.env.mailbox == trash)
0b2ce9 2990       this.permanently_remove_messages();
1b30a7 2991     // we're in Junk folder and delete_junk is enabled
A 2992     else if (this.env.delete_junk && this.env.junk_mailbox && this.env.mailbox == this.env.junk_mailbox)
2993       this.permanently_remove_messages();
0b2ce9 2994     // if there is a trash mailbox defined and we're not currently in it
A 2995     else {
31c171 2996       // if shift was pressed delete it immediately
c28161 2997       if ((list && list.modkey == SHIFT_KEY) || (event && rcube_event.get_modifier(event) == SHIFT_KEY)) {
31c171 2998         if (confirm(this.get_label('deletemessagesconfirm')))
S 2999           this.permanently_remove_messages();
84a331 3000       }
31c171 3001       else
476407 3002         this.move_messages(trash);
84a331 3003     }
f52c93 3004
T 3005     return true;
857a38 3006   };
cfdf04 3007
T 3008   // delete the selected messages permanently
3009   this.permanently_remove_messages = function()
186537 3010   {
463ce6 3011     var post_data = this.selection_post_data();
AM 3012
3013     // exit if selection is empty
3014     if (!post_data._uid)
cfdf04 3015       return;
8fa922 3016
f11541 3017     this.show_contentframe(false);
463ce6 3018     this._with_selected_messages('delete', post_data);
186537 3019   };
cfdf04 3020
7eecf8 3021   // Send a specific move/delete request with UIDs of all selected messages
cfdf04 3022   // @private
463ce6 3023   this._with_selected_messages = function(action, post_data, lock)
d22455 3024   {
1e9a59 3025     var count = 0, msg,
f50a66 3026       remove = (action == 'delete' || !this.is_multifolder_listing());
c31360 3027
463ce6 3028     // update the list (remove rows, clear selection)
AM 3029     if (this.message_list) {
0e7b66 3030       var n, id, root, roots = [],
A 3031         selection = this.message_list.get_selection();
3032
3033       for (n=0, len=selection.length; n<len; n++) {
cfdf04 3034         id = selection[n];
0e7b66 3035
A 3036         if (this.env.threading) {
3037           count += this.update_thread(id);
3038           root = this.message_list.find_root(id);
3039           if (root != id && $.inArray(root, roots) < 0) {
3040             roots.push(root);
3041           }
3042         }
1e9a59 3043         if (remove)
TB 3044           this.message_list.remove_row(id, (this.env.display_next && n == selection.length-1));
cfdf04 3045       }
e54bb7 3046       // make sure there are no selected rows
1e9a59 3047       if (!this.env.display_next && remove)
e54bb7 3048         this.message_list.clear_selection();
0e7b66 3049       // update thread tree icons
A 3050       for (n=0, len=roots.length; n<len; n++) {
3051         this.add_tree_icons(roots[n]);
3052       }
d22455 3053     }
132aae 3054
f52c93 3055     if (count < 0)
c31360 3056       post_data._count = (count*-1);
A 3057     // remove threads from the end of the list
1e9a59 3058     else if (count > 0 && remove)
f52c93 3059       this.delete_excessive_thread_rows();
1e9a59 3060
TB 3061     if (!remove)
3062       post_data._refresh = 1;
c50d88 3063
A 3064     if (!lock) {
a45f9b 3065       msg = action == 'move' ? 'movingmessage' : 'deletingmessage';
c50d88 3066       lock = this.display_message(this.get_label(msg), 'loading');
A 3067     }
fb7ec5 3068
cfdf04 3069     // send request to server
c31360 3070     this.http_post(action, post_data, lock);
d22455 3071   };
4e17e6 3072
3a1a36 3073   // build post data for message delete/move/copy/flag requests
463ce6 3074   this.selection_post_data = function(data)
AM 3075   {
3076     if (typeof(data) != 'object')
3077       data = {};
3078
3079     data._mbox = this.env.mailbox;
3a1a36 3080
AM 3081     if (!data._uid) {
5c421d 3082       var uids = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
3a1a36 3083       data._uid = this.uids_to_list(uids);
AM 3084     }
463ce6 3085
AM 3086     if (this.env.action)
3087       data._from = this.env.action;
3088
3089     // also send search request to get the right messages
3090     if (this.env.search_request)
3091       data._search = this.env.search_request;
3092
ccb132 3093     if (this.env.display_next && this.env.next_uid)
60e1b3 3094       data._next_uid = this.env.next_uid;
ccb132 3095
463ce6 3096     return data;
AM 3097   };
3098
4e17e6 3099   // set a specific flag to one or more messages
T 3100   this.mark_message = function(flag, uid)
186537 3101   {
463ce6 3102     var a_uids = [], r_uids = [], len, n, id,
4877db 3103       list = this.message_list;
3d3531 3104
4e17e6 3105     if (uid)
T 3106       a_uids[0] = uid;
3107     else if (this.env.uid)
3108       a_uids[0] = this.env.uid;
463ce6 3109     else if (list)
AM 3110       a_uids = list.get_selection();
5d97ac 3111
4877db 3112     if (!list)
3d3531 3113       r_uids = a_uids;
4877db 3114     else {
AM 3115       list.focus();
0e7b66 3116       for (n=0, len=a_uids.length; n<len; n++) {
5d97ac 3117         id = a_uids[n];
463ce6 3118         if ((flag == 'read' && list.rows[id].unread)
AM 3119             || (flag == 'unread' && !list.rows[id].unread)
3120             || (flag == 'delete' && !list.rows[id].deleted)
3121             || (flag == 'undelete' && list.rows[id].deleted)
3122             || (flag == 'flagged' && !list.rows[id].flagged)
3123             || (flag == 'unflagged' && list.rows[id].flagged))
d22455 3124         {
0e7b66 3125           r_uids.push(id);
d22455 3126         }
4e17e6 3127       }
4877db 3128     }
3d3531 3129
1a98a6 3130     // nothing to do
f3d37f 3131     if (!r_uids.length && !this.select_all_mode)
1a98a6 3132       return;
3d3531 3133
186537 3134     switch (flag) {
857a38 3135         case 'read':
S 3136         case 'unread':
5d97ac 3137           this.toggle_read_status(flag, r_uids);
857a38 3138           break;
S 3139         case 'delete':
3140         case 'undelete':
5d97ac 3141           this.toggle_delete_status(r_uids);
e189a6 3142           break;
A 3143         case 'flagged':
3144         case 'unflagged':
3145           this.toggle_flagged_status(flag, a_uids);
857a38 3146           break;
186537 3147     }
A 3148   };
4e17e6 3149
857a38 3150   // set class to read/unread
6b47de 3151   this.toggle_read_status = function(flag, a_uids)
T 3152   {
4fb6a2 3153     var i, len = a_uids.length,
3a1a36 3154       post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: flag}),
c50d88 3155       lock = this.display_message(this.get_label('markingmessage'), 'loading');
4fb6a2 3156
A 3157     // mark all message rows as read/unread
3158     for (i=0; i<len; i++)
3a1a36 3159       this.set_message(a_uids[i], 'unread', (flag == 'unread' ? true : false));
ab10d6 3160
c31360 3161     this.http_post('mark', post_data, lock);
6b47de 3162   };
6d2714 3163
e189a6 3164   // set image to flagged or unflagged
A 3165   this.toggle_flagged_status = function(flag, a_uids)
3166   {
4fb6a2 3167     var i, len = a_uids.length,
3a1a36 3168       post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: flag}),
c50d88 3169       lock = this.display_message(this.get_label('markingmessage'), 'loading');
4fb6a2 3170
A 3171     // mark all message rows as flagged/unflagged
3172     for (i=0; i<len; i++)
3a1a36 3173       this.set_message(a_uids[i], 'flagged', (flag == 'flagged' ? true : false));
ab10d6 3174
c31360 3175     this.http_post('mark', post_data, lock);
e189a6 3176   };
8fa922 3177
857a38 3178   // mark all message rows as deleted/undeleted
6b47de 3179   this.toggle_delete_status = function(a_uids)
T 3180   {
4fb6a2 3181     var len = a_uids.length,
A 3182       i, uid, all_deleted = true,
85fece 3183       rows = this.message_list ? this.message_list.rows : {};
8fa922 3184
4fb6a2 3185     if (len == 1) {
85fece 3186       if (!this.message_list || (rows[a_uids[0]] && !rows[a_uids[0]].deleted))
6b47de 3187         this.flag_as_deleted(a_uids);
T 3188       else
3189         this.flag_as_undeleted(a_uids);
3190
1c5853 3191       return true;
S 3192     }
8fa922 3193
4fb6a2 3194     for (i=0; i<len; i++) {
857a38 3195       uid = a_uids[i];
f3d37f 3196       if (rows[uid] && !rows[uid].deleted) {
A 3197         all_deleted = false;
3198         break;
857a38 3199       }
1c5853 3200     }
8fa922 3201
1c5853 3202     if (all_deleted)
S 3203       this.flag_as_undeleted(a_uids);
3204     else
3205       this.flag_as_deleted(a_uids);
8fa922 3206
1c5853 3207     return true;
6b47de 3208   };
4e17e6 3209
6b47de 3210   this.flag_as_undeleted = function(a_uids)
T 3211   {
3a1a36 3212     var i, len = a_uids.length,
AM 3213       post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: 'undelete'}),
c50d88 3214       lock = this.display_message(this.get_label('markingmessage'), 'loading');
4fb6a2 3215
A 3216     for (i=0; i<len; i++)
3217       this.set_message(a_uids[i], 'deleted', false);
ab10d6 3218
c31360 3219     this.http_post('mark', post_data, lock);
6b47de 3220   };
T 3221
3222   this.flag_as_deleted = function(a_uids)
3223   {
c31360 3224     var r_uids = [],
3a1a36 3225       post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: 'delete'}),
c31360 3226       lock = this.display_message(this.get_label('markingmessage'), 'loading'),
85fece 3227       rows = this.message_list ? this.message_list.rows : {},
fb7ec5 3228       count = 0;
f52c93 3229
0e7b66 3230     for (var i=0, len=a_uids.length; i<len; i++) {
1c5853 3231       uid = a_uids[i];
fb7ec5 3232       if (rows[uid]) {
d22455 3233         if (rows[uid].unread)
T 3234           r_uids[r_uids.length] = uid;
0b2ce9 3235
a945da 3236         if (this.env.skip_deleted) {
A 3237           count += this.update_thread(uid);
e54bb7 3238           this.message_list.remove_row(uid, (this.env.display_next && i == this.message_list.selection.length-1));
a945da 3239         }
A 3240         else
3241           this.set_message(uid, 'deleted', true);
3d3531 3242       }
fb7ec5 3243     }
e54bb7 3244
A 3245     // make sure there are no selected rows
f52c93 3246     if (this.env.skip_deleted && this.message_list) {
85fece 3247       if (!this.env.display_next)
fb7ec5 3248         this.message_list.clear_selection();
f52c93 3249       if (count < 0)
c31360 3250         post_data._count = (count*-1);
70da8c 3251       else if (count > 0)
f52c93 3252         // remove threads from the end of the list
T 3253         this.delete_excessive_thread_rows();
fb7ec5 3254     }
3d3531 3255
70da8c 3256     // set of messages to mark as seen
3d3531 3257     if (r_uids.length)
c31360 3258       post_data._ruid = this.uids_to_list(r_uids);
3d3531 3259
c31360 3260     if (this.env.skip_deleted && this.env.display_next && this.env.next_uid)
A 3261       post_data._next_uid = this.env.next_uid;
8fa922 3262
c31360 3263     this.http_post('mark', post_data, lock);
6b47de 3264   };
3d3531 3265
A 3266   // flag as read without mark request (called from backend)
3267   // argument should be a coma-separated list of uids
3268   this.flag_deleted_as_read = function(uids)
3269   {
70da8c 3270     var uid, i, len,
85fece 3271       rows = this.message_list ? this.message_list.rows : {};
3d3531 3272
188247 3273     if (typeof uids == 'string')
65070f 3274       uids = uids.split(',');
4fb6a2 3275
A 3276     for (i=0, len=uids.length; i<len; i++) {
3277       uid = uids[i];
3d3531 3278       if (rows[uid])
132aae 3279         this.set_message(uid, 'unread', false);
8fa922 3280     }
3d3531 3281   };
f52c93 3282
fb7ec5 3283   // Converts array of message UIDs to comma-separated list for use in URL
A 3284   // with select_all mode checking
3285   this.uids_to_list = function(uids)
3286   {
188247 3287     return this.select_all_mode ? '*' : (uids.length <= 1 ? uids.join(',') : uids);
fb7ec5 3288   };
8fa922 3289
1b30a7 3290   // Sets title of the delete button
A 3291   this.set_button_titles = function()
3292   {
3293     var label = 'deletemessage';
3294
3295     if (!this.env.flag_for_deletion
3296       && this.env.trash_mailbox && this.env.mailbox != this.env.trash_mailbox
3297       && (!this.env.delete_junk || !this.env.junk_mailbox || this.env.mailbox != this.env.junk_mailbox)
3298     )
3299       label = 'movemessagetotrash';
3300
3301     this.set_alttext('delete', label);
3302   };
f52c93 3303
9a5d9a 3304   // Initialize input element for list page jump
AM 3305   this.init_pagejumper = function(element)
3306   {
3307     $(element).addClass('rcpagejumper')
3308       .on('focus', function(e) {
3309         // create and display popup with page selection
3310         var i, html = '';
3311
3312         for (i = 1; i <= ref.env.pagecount; i++)
3313           html += '<li>' + i + '</li>';
3314
3315         html = '<ul class="toolbarmenu">' + html + '</ul>';
3316
3317         if (!ref.pagejump) {
3318           ref.pagejump = $('<div id="pagejump-selector" class="popupmenu"></div>')
3319             .appendTo(document.body)
3320             .on('click', 'li', function() {
3321               if (!ref.busy)
3322                 $(element).val($(this).text()).change();
3323             });
3324         }
3325
3326         if (ref.pagejump.data('count') != i)
3327           ref.pagejump.html(html);
3328
3329         ref.pagejump.attr('rel', '#' + this.id).data('count', i);
3330
3331         // display page selector
3332         ref.show_menu('pagejump-selector', true, e);
3333         $(this).keydown();
3334       })
3335       // keyboard navigation
9d49c8 3336       .on('keydown keyup click', function(e) {
9a5d9a 3337         var current, selector = $('#pagejump-selector'),
AM 3338           ul = $('ul', selector),
3339           list = $('li', ul),
3340           height = ul.height(),
3341           p = parseInt(this.value);
3342
9d49c8 3343         if (e.which != 27 && e.which != 9 && e.which != 13 && !selector.is(':visible'))
AM 3344           return ref.show_menu('pagejump-selector', true, e);
3345
9a5d9a 3346         if (e.type == 'keydown') {
AM 3347           // arrow-down
3348           if (e.which == 40) {
3349             if (list.length > p)
3350               this.value = (p += 1);
3351           }
3352           // arrow-up
3353           else if (e.which == 38) {
3354             if (p > 1 && list.length > p - 1)
3355               this.value = (p -= 1);
3356           }
3357           // enter
3358           else if (e.which == 13) {
3359             return $(this).change();
3360           }
820be4 3361           // esc, tab
AM 3362           else if (e.which == 27 || e.which == 9) {
3363             return $(element).val(ref.env.current_page);
3364           }
9a5d9a 3365         }
AM 3366
3367         $('li.selected', ul).removeClass('selected');
3368
3369         if ((current = $(list[p - 1])).length) {
3370           current.addClass('selected');
3371           $('#pagejump-selector').scrollTop(((ul.height() / list.length) * (p - 1)) - selector.height() / 2);
3372         }
3373       })
3374       .on('change', function(e) {
3375         // go to specified page
3376         var p = parseInt(this.value);
3377         if (p && p != ref.env.current_page && !ref.busy) {
3378           ref.hide_menu('pagejump-selector');
3379           ref.list_page(p);
3380         }
3381       });
3382   };
3383
3384   // Update page-jumper state on list updates
3385   this.update_pagejumper = function()
3386   {
3387     $('input.rcpagejumper').val(this.env.current_page).prop('disabled', this.env.pagecount < 2);
3388   };
3389
1cd376 3390   // check for mailvelope API
TB 3391   this.check_mailvelope = function(action)
3392   {
3393     if (typeof window.mailvelope !== 'undefined') {
40d152 3394       this.mailvelope_load(action);
1cd376 3395     }
TB 3396     else {
3397       $(window).on('mailvelope', function() {
40d152 3398         ref.mailvelope_load(action);
1cd376 3399       });
TB 3400     }
3401   };
3402
82dcbb 3403   // Load Mailvelope functionality (and initialize keyring if needed)
40d152 3404   this.mailvelope_load = function(action)
1cd376 3405   {
TB 3406     if (this.env.browser_capabilities)
3407       this.env.browser_capabilities['pgpmime'] = 1;
3408
82dcbb 3409     var keyring = this.env.user_id;
1cd376 3410
TB 3411     mailvelope.getKeyring(keyring).then(function(kr) {
3412       ref.mailvelope_keyring = kr;
40d152 3413       ref.mailvelope_init(action, kr);
310d49 3414     }, function(err) {
1cd376 3415       // attempt to create a new keyring for this app/user
TB 3416       mailvelope.createKeyring(keyring).then(function(kr) {
3417         ref.mailvelope_keyring = kr;
40d152 3418         ref.mailvelope_init(action, kr);
310d49 3419       }, function(err) {
7b8a0a 3420         console.error(err);
1cd376 3421       });
TB 3422     });
40d152 3423   };
1cd376 3424
82dcbb 3425   // Initializes Mailvelope editor or display container
40d152 3426   this.mailvelope_init = function(action, keyring)
TB 3427   {
82dcbb 3428     if (!window.mailvelope)
AM 3429       return;
3430
3431     if (action == 'show' || action == 'preview' || action == 'print') {
1cd376 3432       // decrypt text body
82dcbb 3433       if (this.env.is_pgp_content) {
1cd376 3434         var data = $(this.env.is_pgp_content).text();
TB 3435         ref.mailvelope_display_container(this.env.is_pgp_content, data, keyring);
3436       }
3437       // load pgp/mime message and pass it to the mailvelope display container
82dcbb 3438       else if (this.env.pgp_mime_part) {
1cd376 3439         var msgid = this.display_message(this.get_label('loadingdata'), 'loading'),
TB 3440           selector = this.env.pgp_mime_container;
3441
3442         $.ajax({
3443           type: 'GET',
3444           url: this.url('get', { '_mbox': this.env.mailbox, '_uid': this.env.uid, '_part': this.env.pgp_mime_part }),
3445           error: function(o, status, err) {
3167e5 3446             ref.http_error(o, status, err, msgid);
1cd376 3447           },
TB 3448           success: function(data) {
3449             ref.mailvelope_display_container(selector, data, keyring, msgid);
3450           }
3451         });
3452       }
3453     }
82dcbb 3454     else if (action == 'compose') {
babc30 3455       this.env.compose_commands.push('compose-encrypted');
82dcbb 3456
AM 3457       var is_html = $('input[name="_is_html"]').val() > 0;
babc30 3458
3167e5 3459       if (this.env.pgp_mime_message) {
TB 3460         // fetch PGP/Mime part and open load into Mailvelope editor
3461         var lock = this.set_busy(true, this.get_label('loadingdata'));
82dcbb 3462
3167e5 3463         $.ajax({
TB 3464           type: 'GET',
3465           url: this.url('get', this.env.pgp_mime_message),
3466           error: function(o, status, err) {
3467             ref.http_error(o, status, err, lock);
82dcbb 3468             ref.enable_command('compose-encrypted', !is_html);
3167e5 3469           },
TB 3470           success: function(data) {
3471             ref.set_busy(false, null, lock);
82dcbb 3472
AM 3473             if (is_html) {
3474               ref.command('toggle-editor', {html: false, noconvert: true});
3475               $('#' + ref.env.composebody).val('');
3476             }
3477
3167e5 3478             ref.compose_encrypted({ quotedMail: data });
TB 3479             ref.enable_command('compose-encrypted', true);
3480           }
3481         });
3482       }
3483       else {
3484         // enable encrypted compose toggle
82dcbb 3485         this.enable_command('compose-encrypted', !is_html);
3167e5 3486       }
1cd376 3487     }
TB 3488   };
3489
3167e5 3490   // handler for the 'compose-encrypted' command
1cd376 3491   this.compose_encrypted = function(props)
TB 3492   {
82dcbb 3493     var options, container = $('#' + this.env.composebody).parent();
7b8a0a 3494
TB 3495     // remove Mailvelope editor if active
3496     if (ref.mailvelope_editor) {
3497       ref.mailvelope_editor = null;
40d152 3498       ref.compose_skip_unsavedcheck = false;
7b8a0a 3499       ref.set_button('compose-encrypted', 'act');
40d152 3500
7b8a0a 3501       container.removeClass('mailvelope')
TB 3502         .find('iframe:not([aria-hidden=true])').remove();
3503       $('#' + ref.env.composebody).show();
40d152 3504       $("[name='_pgpmime']").remove();
b95a6d 3505
TB 3506       // disable commands that operate on the compose body
3507       ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', true);
3508       ref.triggerEvent('compose-encrypted', { active:false });
7b8a0a 3509     }
TB 3510     // embed Mailvelope editor container
3511     else {
82dcbb 3512       if (this.spellcheck_state())
AM 3513         this.editor.spellcheck_stop();
3514
3167e5 3515       if (props.quotedMail) {
TB 3516         options = { quotedMail: props.quotedMail, quotedMailIndent: false };
3517       }
82dcbb 3518       else {
AM 3519         options = { predefinedText: $('#' + this.env.composebody).val() };
3520       }
3521
3167e5 3522       if (this.env.compose_mode == 'reply') {
TB 3523         options.quotedMailIndent = true;
3524         options.quotedMailHeader = this.env.compose_reply_header;
3525       }
3526
40d152 3527       mailvelope.createEditorContainer('#' + container.attr('id'), ref.mailvelope_keyring, options).then(function(editor) {
7b8a0a 3528         ref.mailvelope_editor = editor;
40d152 3529         ref.compose_skip_unsavedcheck = true;
7b8a0a 3530         ref.set_button('compose-encrypted', 'sel');
40d152 3531
7b8a0a 3532         container.addClass('mailvelope');
TB 3533         $('#' + ref.env.composebody).hide();
3167e5 3534
b95a6d 3535         // disable commands that operate on the compose body
TB 3536         ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', false);
3537         ref.triggerEvent('compose-encrypted', { active:true });
3538
3167e5 3539         // notify user about loosing attachments
TB 3540         if (ref.env.attachments && !$.isEmptyObject(ref.env.attachments)) {
3541           alert(ref.get_label('encryptnoattachments'));
3542
3543           $.each(ref.env.attachments, function(name, attach) {
3544             ref.remove_from_attachment_list(name);
3545           });
3546         }
310d49 3547       }, function(err) {
7b8a0a 3548         console.error(err);
f7f75f 3549         console.log(options);
7b8a0a 3550       });
TB 3551     }
1cd376 3552   };
TB 3553
3554   // callback to replace the message body with the full armored
3555   this.mailvelope_submit_messageform = function(draft, saveonly)
3556   {
3557     // get recipients
3558     var recipients = [];
3559     $.each(['to', 'cc', 'bcc'], function(i,field) {
3560       var pos, rcpt, val = $.trim($('[name="_' + field + '"]').val());
3561       while (val.length && rcube_check_email(val, true)) {
2965a9 3562         rcpt = RegExp.$2;
1cd376 3563         recipients.push(rcpt);
TB 3564         val = val.substr(val.indexOf(rcpt) + rcpt.length + 1).replace(/^\s*,\s*/, '');
3565       }
3566     });
3567
3568     // check if we have keys for all recipients
3569     var isvalid = recipients.length > 0;
3570     ref.mailvelope_keyring.validKeyForAddress(recipients).then(function(status) {
2965a9 3571       var missing_keys = [];
1cd376 3572       $.each(status, function(k,v) {
7b8a0a 3573         if (v === false) {
1cd376 3574           isvalid = false;
2965a9 3575           missing_keys.push(k);
1cd376 3576         }
TB 3577       });
2965a9 3578
TB 3579       // list recipients with missing keys
3580       if (!isvalid && missing_keys.length) {
3581         // load publickey.js
3582         if (!$('script#publickeyjs').length) {
3583           $('<script>')
3584             .attr('id', 'publickeyjs')
3585             .attr('src', ref.assets_path('program/js/publickey.js'))
3586             .appendTo(document.body);
3587         }
3588
3589         // display dialog with missing keys
3590         ref.show_popup_dialog(
3591           ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')) +
3592           '<p>' + ref.get_label('searchpubkeyservers') + '</p>',
3593           ref.get_label('encryptedsendialog'),
3594           [{
3595             text: ref.get_label('search'),
3596             'class': 'mainaction',
3597             click: function() {
3598               var $dialog = $(this);
3599               ref.mailvelope_search_pubkeys(missing_keys, function() {
3600                 $dialog.dialog('close')
3601               });
3602             }
3603           },
3604           {
3605             text: ref.get_label('cancel'),
3606             click: function(){
3607               $(this).dialog('close');
3608             }
3609           }]
3610         );
3611         return false;
3612       }
1cd376 3613
TB 3614       if (!isvalid) {
7b8a0a 3615         if (!recipients.length) {
1cd376 3616           alert(ref.get_label('norecipientwarning'));
7b8a0a 3617           $("[name='_to']").focus();
TB 3618         }
1cd376 3619         return false;
TB 3620       }
3621
40d152 3622       // add sender identity to recipients to be able to decrypt our very own message
TB 3623       var senders = [], selected_sender = ref.env.identities[$("[name='_from'] option:selected").val()];
3624       $.each(ref.env.identities, function(k, sender) {
3625         senders.push(sender.email);
3626       });
1cd376 3627
40d152 3628       ref.mailvelope_keyring.validKeyForAddress(senders).then(function(status) {
TB 3629         valid_sender = null;
3630         $.each(status, function(k,v) {
3631           if (v !== false) {
3632             valid_sender = k;
3633             if (valid_sender == selected_sender) {
3634               return false;  // break
3635             }
3636           }
3637         });
3638
3639         if (!valid_sender) {
3640           if (!confirm(ref.get_label('nopubkeyforsender'))) {
3641             return false;
3642           }
3643         }
3644
3645         recipients.push(valid_sender);
3646
3647         ref.mailvelope_editor.encrypt(recipients).then(function(armored) {
3648           // all checks passed, send message
3649           var form = ref.gui_objects.messageform,
3650             hidden = $("[name='_pgpmime']", form),
3651             msgid = ref.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage')
3652
3653           form.target = 'savetarget';
3654           form._draft.value = draft ? '1' : '';
3655           form.action = ref.add_url(form.action, '_unlock', msgid);
3656           form.action = ref.add_url(form.action, '_framed', 1);
3657
3658           if (saveonly) {
3659             form.action = ref.add_url(form.action, '_saveonly', 1);
3660           }
3661
3662           // send pgp conent via hidden field
3663           if (!hidden.length) {
3664             hidden = $('<input type="hidden" name="_pgpmime">').appendTo(form);
3665           }
3666           hidden.val(armored);
3667
3668           form.submit();
3669
310d49 3670         }, function(err) {
40d152 3671           console.log(err);
TB 3672         });  // mailvelope_editor.encrypt()
1cd376 3673
310d49 3674       }, function(err) {
40d152 3675         console.error(err);
TB 3676       });  // mailvelope_keyring.validKeyForAddress(senders)
3677
310d49 3678     }, function(err) {
7b8a0a 3679       console.error(err);
40d152 3680     });  // mailvelope_keyring.validKeyForAddress(recipients)
1cd376 3681
TB 3682     return false;
3683   };
3684
3685   // wrapper for the mailvelope.createDisplayContainer API call
3686   this.mailvelope_display_container = function(selector, data, keyring, msgid)
3687   {
7b8a0a 3688     mailvelope.createDisplayContainer(selector, data, keyring, { showExternalContent: this.env.safemode }).then(function() {
82dcbb 3689       $(selector).addClass('mailvelope').children().not('iframe').hide();
1cd376 3690       ref.hide_message(msgid);
TB 3691       setTimeout(function() { $(window).resize(); }, 10);
310d49 3692     }, function(err) {
7b8a0a 3693       console.error(err);
1cd376 3694       ref.hide_message(msgid);
TB 3695       ref.display_message('Message decryption failed: ' + err.message, 'error')
3696     });
3697   };
3698
2965a9 3699   // subroutine to query keyservers for public keys
TB 3700   this.mailvelope_search_pubkeys = function(emails, resolve)
3701   {
3702     // query with publickey.js
3703     var deferreds = [],
3704       pk = new PublicKey(),
3705       lock = ref.display_message(ref.get_label('loading'), 'loading');
3706
3707     $.each(emails, function(i, email) {
3708       var d = $.Deferred();
3709       pk.search(email, function(results, errorCode) {
3710         if (errorCode !== null) {
3711           // rejecting would make all fail
3712           // d.reject(email);
3713           d.resolve([email]);
3714         }
3715         else {
3716           d.resolve([email].concat(results));
3717         }
3718       });
3719       deferreds.push(d);
3720     });
3721
3722     $.when.apply($, deferreds).then(function() {
3723       var missing_keys = [],
3724         key_selection = [];
3725
3726       // alanyze results of all queries
3727       $.each(arguments, function(i, result) {
3728         var email = result.shift();
3729         if (!result.length) {
3730           missing_keys.push(email);
3731         }
3732         else {
3733           key_selection = key_selection.concat(result);
3734         }
3735       });
3736
3737       ref.hide_message(lock);
3738       resolve(true);
3739
3740       // show key import dialog
3741       if (key_selection.length) {
3742         ref.mailvelope_key_import_dialog(key_selection);
3743       }
3744       // some keys could not be found
3745       if (missing_keys.length) {
3746         ref.display_message(ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')), 'warning');
3747       }
310d49 3748     }).fail(function() {
2965a9 3749       console.error('Pubkey lookup failed with', arguments);
TB 3750       ref.hide_message(lock);
3751       ref.display_message('pubkeysearcherror', 'error');
3752       resolve(false);
3753     });
3754   };
3755
3756   // list the given public keys in a dialog with options to import
3757   // them into the local Maivelope keyring
3758   this.mailvelope_key_import_dialog = function(candidates)
3759   {
3760     var ul = $('<div>').addClass('listing mailvelopekeyimport');
3761     $.each(candidates, function(i, keyrec) {
3762       var li = $('<div>').addClass('key');
3763       if (keyrec.revoked)  li.addClass('revoked');
3764       if (keyrec.disabled) li.addClass('disabled');
3765       if (keyrec.expired)  li.addClass('expired');
3766
3767       li.append($('<label>').addClass('keyid').text(ref.get_label('keyid')));
3768       li.append($('<a>').text(keyrec.keyid.substr(-8).toUpperCase())
3769         .attr('href', keyrec.info)
3770         .attr('target', '_blank')
3771         .attr('tabindex', '-1'));
3772
3773       li.append($('<label>').addClass('keylen').text(ref.get_label('keylength')));
3774       li.append($('<span>').text(keyrec.keylen));
3775
3776       if (keyrec.expirationdate) {
3777         li.append($('<label>').addClass('keyexpired').text(ref.get_label('keyexpired')));
3778         li.append($('<span>').text(new Date(keyrec.expirationdate * 1000).toDateString()));
3779       }
3780
3781       if (keyrec.revoked) {
3782         li.append($('<span>').addClass('keyrevoked').text(ref.get_label('keyrevoked')));
3783       }
3784
3785       var ul_ = $('<ul>').addClass('uids');
3786       $.each(keyrec.uids, function(j, uid) {
3787         var li_ = $('<li>').addClass('uid');
3788         if (uid.revoked)  li_.addClass('revoked');
3789         if (uid.disabled) li_.addClass('disabled');
3790         if (uid.expired)  li_.addClass('expired');
3791
3792         ul_.append(li_.text(uid.uid));
3793       });
3794
3795       li.append(ul_);
3796       li.append($('<input>')
3797         .attr('type', 'button')
3798         .attr('rel', keyrec.keyid)
3799         .attr('value', ref.get_label('import'))
3800         .addClass('button importkey')
3801         .prop('disabled', keyrec.revoked || keyrec.disabled || keyrec.expired));
3802
3803       ul.append(li);
3804     });
3805
3806     // display dialog with missing keys
3807     ref.show_popup_dialog(
3808       $('<div>')
3809         .append($('<p>').html(ref.get_label('encryptpubkeysfound')))
3810         .append(ul),
3811       ref.get_label('importpubkeys'),
3812       [{
3813         text: ref.get_label('close'),
3814         click: function(){
3815           $(this).dialog('close');
3816         }
3817       }]
3818     );
3819
3820     // delegate handler for import button clicks
3821     ul.on('click', 'input.button.importkey', function() {
3822       var btn = $(this),
3823         keyid = btn.attr('rel'),
3824         pk = new PublicKey(),
3825         lock = ref.display_message(ref.get_label('loading'), 'loading');
3826
3827         // fetch from keyserver and import to Mailvelope keyring
3828         pk.get(keyid, function(armored, errorCode) {
3829           ref.hide_message(lock);
3830
3831           if (errorCode) {
82dcbb 3832             ref.display_message(ref.get_label('keyservererror'), 'error');
2965a9 3833             return;
TB 3834           }
3835
3836           // import to keyring
3837           ref.mailvelope_keyring.importPublicKey(armored).then(function(status) {
3838             if (status === 'REJECTED') {
3839               // alert(ref.get_label('Key import was rejected'));
3840             }
3841             else {
82dcbb 3842               var $key = keyid.substr(-8).toUpperCase();
2965a9 3843               btn.closest('.key').fadeOut();
82dcbb 3844               ref.display_message(ref.get_label('keyimportsuccess').replace('$key', $key), 'confirmation');
2965a9 3845             }
310d49 3846           }, function(err) {
2965a9 3847             console.log(err);
TB 3848           });
3849         });
3850     });
3851
3852   };
3853
1cd376 3854
f52c93 3855   /*********************************************************/
T 3856   /*********       mailbox folders methods         *********/
3857   /*********************************************************/
3858
3859   this.expunge_mailbox = function(mbox)
8fa922 3860   {
c31360 3861     var lock, post_data = {_mbox: mbox};
8fa922 3862
f52c93 3863     // lock interface if it's the active mailbox
8fa922 3864     if (mbox == this.env.mailbox) {
b7fd98 3865       lock = this.set_busy(true, 'loading');
c31360 3866       post_data._reload = 1;
b7fd98 3867       if (this.env.search_request)
c31360 3868         post_data._search = this.env.search_request;
b7fd98 3869     }
f52c93 3870
T 3871     // send request to server
c31360 3872     this.http_post('expunge', post_data, lock);
8fa922 3873   };
f52c93 3874
T 3875   this.purge_mailbox = function(mbox)
8fa922 3876   {
c31360 3877     var lock, post_data = {_mbox: mbox};
8fa922 3878
f52c93 3879     if (!confirm(this.get_label('purgefolderconfirm')))
T 3880       return false;
8fa922 3881
f52c93 3882     // lock interface if it's the active mailbox
8fa922 3883     if (mbox == this.env.mailbox) {
ad334a 3884        lock = this.set_busy(true, 'loading');
c31360 3885        post_data._reload = 1;
8fa922 3886      }
f52c93 3887
T 3888     // send request to server
c31360 3889     this.http_post('purge', post_data, lock);
8fa922 3890   };
f52c93 3891
T 3892   // test if purge command is allowed
3893   this.purge_mailbox_test = function()
3894   {
8f8e26 3895     return (this.env.exists && (
AM 3896       this.env.mailbox == this.env.trash_mailbox
3897       || this.env.mailbox == this.env.junk_mailbox
6a9144 3898       || this.env.mailbox.startsWith(this.env.trash_mailbox + this.env.delimiter)
AM 3899       || this.env.mailbox.startsWith(this.env.junk_mailbox + this.env.delimiter)
8f8e26 3900     ));
f52c93 3901   };
T 3902
8fa922 3903
b566ff 3904   /*********************************************************/
S 3905   /*********           login form methods          *********/
3906   /*********************************************************/
3907
3908   // handler for keyboard events on the _user field
65444b 3909   this.login_user_keyup = function(e)
b566ff 3910   {
eb7e45 3911     var key = rcube_event.get_keycode(e),
AM 3912       passwd = $('#rcmloginpwd');
b566ff 3913
S 3914     // enter
cc97ea 3915     if (key == 13 && passwd.length && !passwd.val()) {
T 3916       passwd.focus();
3917       return rcube_event.cancel(e);
b566ff 3918     }
8fa922 3919
cc97ea 3920     return true;
b566ff 3921   };
f11541 3922
4e17e6 3923
T 3924   /*********************************************************/
3925   /*********        message compose methods        *********/
3926   /*********************************************************/
8fa922 3927
271efe 3928   this.open_compose_step = function(p)
TB 3929   {
3930     var url = this.url('mail/compose', p);
3931
3932     // open new compose window
7bf6d2 3933     if (this.env.compose_extwin && !this.env.extwin) {
ece3a5 3934       this.open_window(url);
7bf6d2 3935     }
TB 3936     else {
271efe 3937       this.redirect(url);
99e27c 3938       if (this.env.extwin)
AM 3939         window.resizeTo(Math.max(this.env.popup_width, $(window).width()), $(window).height() + 24);
7bf6d2 3940     }
271efe 3941   };
TB 3942
f52c93 3943   // init message compose form: set focus and eventhandlers
T 3944   this.init_messageform = function()
3945   {
3946     if (!this.gui_objects.messageform)
3947       return false;
8fa922 3948
65e735 3949     var i, elem, pos, input_from = $("[name='_from']"),
9be483 3950       input_to = $("[name='_to']"),
A 3951       input_subject = $("input[name='_subject']"),
3952       input_message = $("[name='_message']").get(0),
3953       html_mode = $("input[name='_is_html']").val() == '1',
0213f8 3954       ac_fields = ['cc', 'bcc', 'replyto', 'followupto'],
32da69 3955       ac_props, opener_rc = this.opener();
271efe 3956
715a39 3957     // close compose step in opener
32da69 3958     if (opener_rc && opener_rc.env.action == 'compose') {
d27a4f 3959       setTimeout(function(){
TB 3960         if (opener.history.length > 1)
3961           opener.history.back();
3962         else
3963           opener_rc.redirect(opener_rc.get_task_url('mail'));
3964       }, 100);
762565 3965       this.env.opened_extwin = true;
271efe 3966     }
0213f8 3967
A 3968     // configure parallel autocompletion
3969     if (this.env.autocomplete_threads > 0) {
3970       ac_props = {
3971         threads: this.env.autocomplete_threads,
e3acfa 3972         sources: this.env.autocomplete_sources
0213f8 3973       };
A 3974     }
f52c93 3975
T 3976     // init live search events
0213f8 3977     this.init_address_input_events(input_to, ac_props);
646b64 3978     for (i in ac_fields) {
0213f8 3979       this.init_address_input_events($("[name='_"+ac_fields[i]+"']"), ac_props);
9be483 3980     }
8fa922 3981
a4c163 3982     if (!html_mode) {
b0a8e3 3983       pos = this.env.top_posting && this.env.compose_mode ? 0 : input_message.value.length;
0b96b1 3984
a4c163 3985       // add signature according to selected identity
09225a 3986       // if we have HTML editor, signature is added in a callback
3b944e 3987       if (input_from.prop('type') == 'select-one') {
a4c163 3988         this.change_identity(input_from[0]);
A 3989       }
0b96b1 3990
09225a 3991       // set initial cursor position
AM 3992       this.set_caret_pos(input_message, pos);
3993
0b96b1 3994       // scroll to the bottom of the textarea (#1490114)
AM 3995       if (pos) {
3996         $(input_message).scrollTop(input_message.scrollHeight);
3997       }
f52c93 3998     }
T 3999
85e60a 4000     // check for locally stored compose data
44b47d 4001     if (this.env.save_localstorage)
TB 4002       this.compose_restore_dialog(0, html_mode)
85e60a 4003
f52c93 4004     if (input_to.val() == '')
65e735 4005       elem = input_to;
f52c93 4006     else if (input_subject.val() == '')
65e735 4007       elem = input_subject;
1f019c 4008     else if (input_message)
65e735 4009       elem = input_message;
AM 4010
4011     // focus first empty element (need to be visible on IE8)
4012     $(elem).filter(':visible').focus();
1f019c 4013
A 4014     this.env.compose_focus_elem = document.activeElement;
f52c93 4015
T 4016     // get summary of all field values
4017     this.compose_field_hash(true);
8fa922 4018
f52c93 4019     // start the auto-save timer
T 4020     this.auto_save_start();
4021   };
4022
b54731 4023   this.compose_restore_dialog = function(j, html_mode)
TB 4024   {
4025     var i, key, formdata, index = this.local_storage_get_item('compose.index', []);
4026
4027     var show_next = function(i) {
4028       if (++i < index.length)
4029         ref.compose_restore_dialog(i, html_mode)
4030     }
4031
4032     for (i = j || 0; i < index.length; i++) {
4033       key = index[i];
4034       formdata = this.local_storage_get_item('compose.' + key, null, true);
4035       if (!formdata) {
4036         continue;
4037       }
4038       // restore saved copy of current compose_id
4039       if (formdata.changed && key == this.env.compose_id) {
4040         this.restore_compose_form(key, html_mode);
4041         break;
4042       }
4043       // skip records from 'other' drafts
4044       if (this.env.draft_id && formdata.draft_id && formdata.draft_id != this.env.draft_id) {
4045         continue;
4046       }
4047       // skip records on reply
4048       if (this.env.reply_msgid && formdata.reply_msgid != this.env.reply_msgid) {
4049         continue;
4050       }
4051       // show dialog asking to restore the message
4052       if (formdata.changed && formdata.session != this.env.session_id) {
4053         this.show_popup_dialog(
4054           this.get_label('restoresavedcomposedata')
4055             .replace('$date', new Date(formdata.changed).toLocaleString())
4056             .replace('$subject', formdata._subject)
4057             .replace(/\n/g, '<br/>'),
4058           this.get_label('restoremessage'),
4059           [{
4060             text: this.get_label('restore'),
630d08 4061             'class': 'mainaction',
b54731 4062             click: function(){
TB 4063               ref.restore_compose_form(key, html_mode);
4064               ref.remove_compose_data(key);  // remove old copy
4065               ref.save_compose_form_local();  // save under current compose_id
4066               $(this).dialog('close');
4067             }
4068           },
4069           {
4070             text: this.get_label('delete'),
630d08 4071             'class': 'delete',
b54731 4072             click: function(){
TB 4073               ref.remove_compose_data(key);
4074               $(this).dialog('close');
4075               show_next(i);
4076             }
4077           },
4078           {
4079             text: this.get_label('ignore'),
4080             click: function(){
4081               $(this).dialog('close');
4082               show_next(i);
4083             }
4084           }]
4085         );
4086         break;
4087       }
4088     }
4089   }
4090
0213f8 4091   this.init_address_input_events = function(obj, props)
f52c93 4092   {
62c861 4093     this.env.recipients_delimiter = this.env.recipients_separator + ' ';
T 4094
184a11 4095     obj.keydown(function(e) { return ref.ksearch_keydown(e, this, props); })
6d3ab6 4096       .attr({ 'autocomplete': 'off', 'aria-autocomplete': 'list', 'aria-expanded': 'false', 'role': 'combobox' });
f52c93 4097   };
a945da 4098
c5c8e7 4099   this.submit_messageform = function(draft, saveonly)
b169de 4100   {
AM 4101     var form = this.gui_objects.messageform;
4102
4103     if (!form)
4104       return;
4105
c5c8e7 4106     // the message has been sent but not saved, ask the user what to do
AM 4107     if (!saveonly && this.env.is_sent) {
4108       return this.show_popup_dialog(this.get_label('messageissent'), '',
4109         [{
4110           text: this.get_label('save'),
4111           'class': 'mainaction',
4112           click: function() {
4113             ref.submit_messageform(false, true);
4114             $(this).dialog('close');
4115           }
4116         },
4117         {
4118           text: this.get_label('cancel'),
4119           click: function() {
4120             $(this).dialog('close');
4121           }
4122         }]
4123       );
4124     }
4125
40d152 4126     // delegate sending to Mailvelope routine
1cd376 4127     if (this.mailvelope_editor) {
TB 4128       return this.mailvelope_submit_messageform(draft, saveonly);
b169de 4129     }
AM 4130
4131     // all checks passed, send message
c5c8e7 4132     var msgid = this.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage'),
b169de 4133       lang = this.spellcheck_lang(),
AM 4134       files = [];
4135
4136     // send files list
4137     $('li', this.gui_objects.attachmentlist).each(function() { files.push(this.id.replace(/^rcmfile/, '')); });
4138     $('input[name="_attachments"]', form).val(files.join());
4139
4140     form.target = 'savetarget';
4141     form._draft.value = draft ? '1' : '';
4142     form.action = this.add_url(form.action, '_unlock', msgid);
4143     form.action = this.add_url(form.action, '_lang', lang);
7e7e45 4144     form.action = this.add_url(form.action, '_framed', 1);
c5c8e7 4145
AM 4146     if (saveonly) {
4147       form.action = this.add_url(form.action, '_saveonly', 1);
4148     }
72e24b 4149
TB 4150     // register timer to notify about connection timeout
4151     this.submit_timer = setTimeout(function(){
4152       ref.set_busy(false, null, msgid);
4153       ref.display_message(ref.get_label('requesttimedout'), 'error');
4154     }, this.env.request_timeout * 1000);
4155
b169de 4156     form.submit();
AM 4157   };
4158
635722 4159   this.compose_recipient_select = function(list)
eeb73c 4160   {
86552f 4161     var id, n, recipients = 0;
TB 4162     for (n=0; n < list.selection.length; n++) {
4163       id = list.selection[n];
4164       if (this.env.contactdata[id])
4165         recipients++;
4166     }
4167     this.enable_command('add-recipient', recipients);
eeb73c 4168   };
T 4169
4170   this.compose_add_recipient = function(field)
4171   {
b4cbed 4172     // find last focused field name
AM 4173     if (!field) {
4174       field = $(this.env.focused_field).filter(':visible');
4175       field = field.length ? field.attr('id').replace('_', '') : 'to';
4176     }
4177
1dfa85 4178     var recipients = [], input = $('#_'+field), delim = this.env.recipients_delimiter;
a945da 4179
eeb73c 4180     if (this.contact_list && this.contact_list.selection.length) {
T 4181       for (var id, n=0; n < this.contact_list.selection.length; n++) {
4182         id = this.contact_list.selection[n];
4183         if (id && this.env.contactdata[id]) {
4184           recipients.push(this.env.contactdata[id]);
4185
4186           // group is added, expand it
4187           if (id.charAt(0) == 'E' && this.env.contactdata[id].indexOf('@') < 0 && input.length) {
4188             var gid = id.substr(1);
4189             this.group2expand[gid] = { name:this.env.contactdata[id], input:input.get(0) };
c31360 4190             this.http_request('group-expand', {_source: this.env.source, _gid: gid}, false);
eeb73c 4191           }
T 4192         }
4193       }
4194     }
4195
4196     if (recipients.length && input.length) {
1dfa85 4197       var oldval = input.val(), rx = new RegExp(RegExp.escape(delim) + '\\s*$');
AM 4198       if (oldval && !rx.test(oldval))
4199         oldval += delim + ' ';
3516b0 4200       input.val(oldval + recipients.join(delim + ' ') + delim + ' ').change();
eeb73c 4201       this.triggerEvent('add-recipient', { field:field, recipients:recipients });
T 4202     }
d58c39 4203
TB 4204     return recipients.length;
eeb73c 4205   };
f52c93 4206
977a29 4207   // checks the input fields before sending a message
ac9ba4 4208   this.check_compose_input = function(cmd)
f52c93 4209   {
977a29 4210     // check input fields
646b64 4211     var input_to = $("[name='_to']"),
736790 4212       input_cc = $("[name='_cc']"),
A 4213       input_bcc = $("[name='_bcc']"),
4214       input_from = $("[name='_from']"),
646b64 4215       input_subject = $("[name='_subject']");
977a29 4216
fd51e0 4217     // check sender (if have no identities)
02e079 4218     if (input_from.prop('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
fd51e0 4219       alert(this.get_label('nosenderwarning'));
A 4220       input_from.focus();
4221       return false;
a4c163 4222     }
fd51e0 4223
977a29 4224     // check for empty recipient
cc97ea 4225     var recipients = input_to.val() ? input_to.val() : (input_cc.val() ? input_cc.val() : input_bcc.val());
a4c163 4226     if (!rcube_check_email(recipients.replace(/^\s+/, '').replace(/[\s,;]+$/, ''), true)) {
977a29 4227       alert(this.get_label('norecipientwarning'));
T 4228       input_to.focus();
4229       return false;
a4c163 4230     }
977a29 4231
ebf872 4232     // check if all files has been uploaded
01ffe0 4233     for (var key in this.env.attachments) {
d8cf6d 4234       if (typeof this.env.attachments[key] === 'object' && !this.env.attachments[key].complete) {
01ffe0 4235         alert(this.get_label('notuploadedwarning'));
T 4236         return false;
4237       }
ebf872 4238     }
8fa922 4239
977a29 4240     // display localized warning for missing subject
f52c93 4241     if (input_subject.val() == '') {
646b64 4242       var buttons = {},
AM 4243         myprompt = $('<div class="prompt">').html('<div class="message">' + this.get_label('nosubjectwarning') + '</div>')
4244           .appendTo(document.body),
4245         prompt_value = $('<input>').attr({type: 'text', size: 30}).val(this.get_label('nosubject'))
db7dcf 4246           .appendTo(myprompt),
AM 4247         save_func = function() {
4248           input_subject.val(prompt_value.val());
4249           myprompt.dialog('close');
4250           ref.command(cmd, { nocheck:true });  // repeat command which triggered this
4251         };
977a29 4252
db7dcf 4253       buttons[this.get_label('sendmessage')] = function() {
AM 4254         save_func($(this));
4255       };
4256       buttons[this.get_label('cancel')] = function() {
977a29 4257         input_subject.focus();
ac9ba4 4258         $(this).dialog('close');
T 4259       };
4260
4261       myprompt.dialog({
4262         modal: true,
4263         resizable: false,
4264         buttons: buttons,
db7dcf 4265         close: function(event, ui) { $(this).remove(); }
ac9ba4 4266       });
646b64 4267
db7dcf 4268       prompt_value.select().keydown(function(e) {
AM 4269         if (e.which == 13) save_func();
4270       });
4271
ac9ba4 4272       return false;
f52c93 4273     }
977a29 4274
736790 4275     // check for empty body
646b64 4276     if (!this.editor.get_content() && !confirm(this.get_label('nobodywarning'))) {
AM 4277       this.editor.focus();
736790 4278       return false;
A 4279     }
646b64 4280
AM 4281     // move body from html editor to textarea (just to be sure, #1485860)
4282     this.editor.save();
3940ba 4283
A 4284     return true;
4285   };
4286
646b64 4287   this.toggle_editor = function(props, obj, e)
3940ba 4288   {
646b64 4289     // @todo: this should work also with many editors on page
7d3be1 4290     var result = this.editor.toggle(props.html, props.noconvert || false);
TB 4291
4292     // satisfy the expectations of aftertoggle-editor event subscribers
4293     props.mode = props.html ? 'html' : 'plain';
4be86f 4294
646b64 4295     if (!result && e) {
AM 4296       // fix selector value if operation failed
7d3be1 4297       props.mode = props.html ? 'plain' : 'html';
TB 4298       $(e.target).filter('select').val(props.mode);
59b765 4299     }
AM 4300
4f3f3b 4301     if (result) {
AM 4302       // update internal format flag
4303       $("input[name='_is_html']").val(props.html ? 1 : 0);
82dcbb 4304       // enable encrypted compose toggle
AM 4305       this.enable_command('compose-encrypted', !props.html);
4f3f3b 4306     }
AM 4307
59b765 4308     return result;
f52c93 4309   };
41fa0b 4310
0b1de8 4311   this.insert_response = function(key)
TB 4312   {
4313     var insert = this.env.textresponses[key] ? this.env.textresponses[key].text : null;
646b64 4314
0b1de8 4315     if (!insert)
TB 4316       return false;
4317
646b64 4318     this.editor.replace(insert);
0b1de8 4319   };
TB 4320
4321   /**
4322    * Open the dialog to save a new canned response
4323    */
4324   this.save_response = function()
4325   {
4326     // show dialog to enter a name and to modify the text to be saved
45bfde 4327     var buttons = {}, text = this.editor.get_content({selection: true, format: 'text', nosig: true}),
0b1de8 4328       html = '<form class="propform">' +
TB 4329       '<div class="prop block"><label>' + this.get_label('responsename') + '</label>' +
4330       '<input type="text" name="name" id="ffresponsename" size="40" /></div>' +
4331       '<div class="prop block"><label>' + this.get_label('responsetext') + '</label>' +
4332       '<textarea name="text" id="ffresponsetext" cols="40" rows="8"></textarea></div>' +
4333       '</form>';
4334
8f8bea 4335     buttons[this.get_label('save')] = function(e) {
0b1de8 4336       var name = $('#ffresponsename').val(),
TB 4337         text = $('#ffresponsetext').val();
4338
4339       if (!text) {
4340         $('#ffresponsetext').select();
4341         return false;
4342       }
4343       if (!name)
4344         name = text.substring(0,40);
4345
4346       var lock = ref.display_message(ref.get_label('savingresponse'), 'loading');
4347       ref.http_post('settings/responses', { _insert:1, _name:name, _text:text }, lock);
4348       $(this).dialog('close');
4349     };
4350
8f8bea 4351     buttons[this.get_label('cancel')] = function() {
0b1de8 4352       $(this).dialog('close');
TB 4353     };
4354
8f8bea 4355     this.show_popup_dialog(html, this.get_label('newresponse'), buttons, {button_classes: ['mainaction']});
0b1de8 4356
TB 4357     $('#ffresponsetext').val(text);
4358     $('#ffresponsename').select();
4359   };
4360
4361   this.add_response_item = function(response)
4362   {
4363     var key = response.key;
4364     this.env.textresponses[key] = response;
4365
4366     // append to responses list
4367     if (this.gui_objects.responseslist) {
4368       var li = $('<li>').appendTo(this.gui_objects.responseslist);
4369       $('<a>').addClass('insertresponse active')
4370         .attr('href', '#')
4371         .attr('rel', key)
b2992d 4372         .attr('tabindex', '0')
2d6242 4373         .html(this.quote_html(response.name))
0b1de8 4374         .appendTo(li)
d9ff47 4375         .mousedown(function(e) {
0b1de8 4376           return rcube_event.cancel(e);
TB 4377         })
d9ff47 4378         .on('mouseup keypress', function(e) {
ea0866 4379           if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
TB 4380             ref.command('insert-response', $(this).attr('rel'));
4381             $(document.body).trigger('mouseup');  // hides the menu
4382             return rcube_event.cancel(e);
4383           }
0b1de8 4384         });
TB 4385     }
977a29 4386   };
41fa0b 4387
0ce212 4388   this.edit_responses = function()
TB 4389   {
0933d6 4390     // TODO: implement inline editing of responses
0ce212 4391   };
TB 4392
4393   this.delete_response = function(key)
4394   {
4395     if (!key && this.responses_list) {
4396       var selection = this.responses_list.get_selection();
4397       key = selection[0];
4398     }
4399
4400     // submit delete request
4401     if (key && confirm(this.get_label('deleteresponseconfirm'))) {
4402       this.http_post('settings/delete-response', { _key: key }, false);
4403     }
4404   };
4405
646b64 4406   // updates spellchecker buttons on state change
4be86f 4407   this.spellcheck_state = function()
f52c93 4408   {
646b64 4409     var active = this.editor.spellcheck_state();
41fa0b 4410
a5fe9a 4411     $.each(this.buttons.spellcheck || [], function(i, v) {
AM 4412       $('#' + v.id)[active ? 'addClass' : 'removeClass']('selected');
4413     });
4be86f 4414
A 4415     return active;
a4c163 4416   };
e170b4 4417
644e3a 4418   // get selected language
A 4419   this.spellcheck_lang = function()
4420   {
646b64 4421     return this.editor.get_language();
4be86f 4422   };
A 4423
4424   this.spellcheck_lang_set = function(lang)
4425   {
646b64 4426     this.editor.set_language(lang);
644e3a 4427   };
A 4428
340546 4429   // resume spellchecking, highlight provided mispellings without new ajax request
646b64 4430   this.spellcheck_resume = function(data)
340546 4431   {
646b64 4432     this.editor.spellcheck_resume(data);
AM 4433   };
340546 4434
f11541 4435   this.set_draft_id = function(id)
a4c163 4436   {
10936f 4437     if (id && id != this.env.draft_id) {
5a8473 4438       var filter = {task: 'mail', action: ''},
AM 4439         rc = this.opener(false, filter) || this.opener(true, filter);
4440
4441       // refresh the drafts folder in the opener window
4442       if (rc && rc.env.mailbox == this.env.drafts_mailbox)
4443         rc.command('checkmail');
10936f 4444
AM 4445       this.env.draft_id = id;
4446       $("input[name='_draft_saveid']").val(id);
4447
d27a4f 4448       // reset history of hidden iframe used for saving draft (#1489643)
467374 4449       // but don't do this on timer-triggered draft-autosaving (#1489789)
40d152 4450       if (window.frames['savetarget'] && window.frames['savetarget'].history && !this.draft_autosave_submit && !this.mailvelope_editor) {
d27a4f 4451         window.frames['savetarget'].history.back();
TB 4452       }
467374 4453
TB 4454       this.draft_autosave_submit = false;
723f4e 4455     }
90dc9b 4456
TB 4457     // always remove local copy upon saving as draft
4458     this.remove_compose_data(this.env.compose_id);
7e7e45 4459     this.compose_skip_unsavedcheck = false;
a4c163 4460   };
f11541 4461
f0f98f 4462   this.auto_save_start = function()
a4c163 4463   {
7d3d62 4464     if (this.env.draft_autosave) {
467374 4465       this.draft_autosave_submit = false;
TB 4466       this.save_timer = setTimeout(function(){
4467           ref.draft_autosave_submit = true;  // set auto-saved flag (#1489789)
4468           ref.command("savedraft");
4469       }, this.env.draft_autosave * 1000);
7d3d62 4470     }
85e60a 4471
8c7492 4472     // save compose form content to local storage every 5 seconds
44b47d 4473     if (!this.local_save_timer && window.localStorage && this.env.save_localstorage) {
8c7492 4474       // track typing activity and only save on changes
TB 4475       this.compose_type_activity = this.compose_type_activity_last = 0;
d9ff47 4476       $(document).keypress(function(e) { ref.compose_type_activity++; });
8c7492 4477
TB 4478       this.local_save_timer = setInterval(function(){
4479         if (ref.compose_type_activity > ref.compose_type_activity_last) {
4480           ref.save_compose_form_local();
4481           ref.compose_type_activity_last = ref.compose_type_activity;
4482         }
4483       }, 5000);
7e7e45 4484
718573 4485       $(window).on('unload', function() {
7e7e45 4486         // remove copy from local storage if compose screen is left after warning
TB 4487         if (!ref.env.server_error)
4488           ref.remove_compose_data(ref.env.compose_id);
4489       });
4490     }
4491
4492     // check for unsaved changes before leaving the compose page
4493     if (!window.onbeforeunload) {
4494       window.onbeforeunload = function() {
4495         if (!ref.compose_skip_unsavedcheck && ref.cmp_hash != ref.compose_field_hash()) {
4496           return ref.get_label('notsentwarning');
4497         }
4498       };
8c7492 4499     }
4b9efb 4500
S 4501     // Unlock interface now that saving is complete
4502     this.busy = false;
a4c163 4503   };
41fa0b 4504
f11541 4505   this.compose_field_hash = function(save)
a4c163 4506   {
977a29 4507     // check input fields
646b64 4508     var i, id, val, str = '', hash_fields = ['to', 'cc', 'bcc', 'subject'];
8fa922 4509
390959 4510     for (i=0; i<hash_fields.length; i++)
A 4511       if (val = $('[name="_' + hash_fields[i] + '"]').val())
4512         str += val + ':';
8fa922 4513
45bfde 4514     str += this.editor.get_content({refresh: false});
b4d940 4515
A 4516     if (this.env.attachments)
10a397 4517       for (id in this.env.attachments)
AM 4518         str += id;
b4d940 4519
40d152 4520     // we can't detect changes in the Mailvelope editor so assume it changed
TB 4521     if (this.mailvelope_editor) {
4522       str += ';' + new Date().getTime();
4523     }
4524
f11541 4525     if (save)
T 4526       this.cmp_hash = str;
b4d940 4527
977a29 4528     return str;
a4c163 4529   };
85e60a 4530
TB 4531   // store the contents of the compose form to localstorage
4532   this.save_compose_form_local = function()
4533   {
44b47d 4534     // feature is disabled
TB 4535     if (!this.env.save_localstorage)
4536       return;
4537
85e60a 4538     var formdata = { session:this.env.session_id, changed:new Date().getTime() },
TB 4539       ed, empty = true;
4540
4541     // get fresh content from editor
646b64 4542     this.editor.save();
85e60a 4543
ceb2a3 4544     if (this.env.draft_id) {
TB 4545       formdata.draft_id = this.env.draft_id;
4546     }
90dc9b 4547     if (this.env.reply_msgid) {
TB 4548       formdata.reply_msgid = this.env.reply_msgid;
4549     }
ceb2a3 4550
303e21 4551     $('input, select, textarea', this.gui_objects.messageform).each(function(i, elem) {
85e60a 4552       switch (elem.tagName.toLowerCase()) {
TB 4553         case 'input':
4554           if (elem.type == 'button' || elem.type == 'submit' || (elem.type == 'hidden' && elem.name != '_is_html')) {
4555             break;
4556           }
b7fb20 4557           formdata[elem.name] = elem.type != 'checkbox' || elem.checked ? $(elem).val() : '';
85e60a 4558
TB 4559           if (formdata[elem.name] != '' && elem.type != 'hidden')
4560             empty = false;
4561           break;
4562
4563         case 'select':
4564           formdata[elem.name] = $('option:checked', elem).val();
4565           break;
4566
4567         default:
4568           formdata[elem.name] = $(elem).val();
8c7492 4569           if (formdata[elem.name] != '')
TB 4570             empty = false;
85e60a 4571       }
TB 4572     });
4573
b0b9cf 4574     if (!empty) {
85e60a 4575       var index = this.local_storage_get_item('compose.index', []),
TB 4576         key = this.env.compose_id;
4577
b0b9cf 4578       if ($.inArray(key, index) < 0) {
AM 4579         index.push(key);
4580       }
4581
4582       this.local_storage_set_item('compose.' + key, formdata, true);
4583       this.local_storage_set_item('compose.index', index);
85e60a 4584     }
TB 4585   };
4586
4587   // write stored compose data back to form
4588   this.restore_compose_form = function(key, html_mode)
4589   {
4590     var ed, formdata = this.local_storage_get_item('compose.' + key, true);
4591
4592     if (formdata && typeof formdata == 'object') {
303e21 4593       $.each(formdata, function(k, value) {
85e60a 4594         if (k[0] == '_') {
TB 4595           var elem = $("*[name='"+k+"']");
4596           if (elem[0] && elem[0].type == 'checkbox') {
4597             elem.prop('checked', value != '');
4598           }
4599           else {
4600             elem.val(value);
4601           }
4602         }
4603       });
4604
4605       // initialize HTML editor
646b64 4606       if ((formdata._is_html == '1' && !html_mode) || (formdata._is_html != '1' && html_mode)) {
7d3be1 4607         this.command('toggle-editor', {id: this.env.composebody, html: !html_mode, noconvert: true});
85e60a 4608       }
TB 4609     }
4610   };
4611
4612   // remove stored compose data from localStorage
4613   this.remove_compose_data = function(key)
4614   {
b0b9cf 4615     var index = this.local_storage_get_item('compose.index', []);
85e60a 4616
b0b9cf 4617     if ($.inArray(key, index) >= 0) {
AM 4618       this.local_storage_remove_item('compose.' + key);
4619       this.local_storage_set_item('compose.index', $.grep(index, function(val,i) { return val != key; }));
85e60a 4620     }
TB 4621   };
4622
4623   // clear all stored compose data of this user
4624   this.clear_compose_data = function()
4625   {
b0b9cf 4626     var i, index = this.local_storage_get_item('compose.index', []);
85e60a 4627
b0b9cf 4628     for (i=0; i < index.length; i++) {
AM 4629       this.local_storage_remove_item('compose.' + index[i]);
85e60a 4630     }
b0b9cf 4631
AM 4632     this.local_storage_remove_item('compose.index');
a5fe9a 4633   };
85e60a 4634
50f56d 4635   this.change_identity = function(obj, show_sig)
655bd9 4636   {
1cded8 4637     if (!obj || !obj.options)
T 4638       return false;
4639
50f56d 4640     if (!show_sig)
A 4641       show_sig = this.env.show_sig;
4642
e0496f 4643     var id = obj.options[obj.selectedIndex].value,
TB 4644       sig = this.env.identity,
4645       delim = this.env.recipients_separator,
4646       rx_delim = RegExp.escape(delim);
4647
4648     // enable manual signature insert
4649     if (this.env.signatures && this.env.signatures[id]) {
4650       this.enable_command('insert-sig', true);
4651       this.env.compose_commands.push('insert-sig');
4652     }
4653     else
4654       this.enable_command('insert-sig', false);
4655
3b944e 4656     // first function execution
AM 4657     if (!this.env.identities_initialized) {
4658       this.env.identities_initialized = true;
4659       if (this.env.show_sig_later)
4660         this.env.show_sig = true;
4661       if (this.env.opened_extwin)
4662         return;
4663     }
15482b 4664
AM 4665     // update reply-to/bcc fields with addresses defined in identities
be6a09 4666     $.each(['replyto', 'bcc'], function() {
AM 4667       var rx, key = this,
4668         old_val = sig && ref.env.identities[sig] ? ref.env.identities[sig][key] : '',
4669         new_val = id && ref.env.identities[id] ? ref.env.identities[id][key] : '',
15482b 4670         input = $('[name="_'+key+'"]'), input_val = input.val();
AM 4671
4672       // remove old address(es)
4673       if (old_val && input_val) {
4674         rx = new RegExp('\\s*' + RegExp.escape(old_val) + '\\s*');
4675         input_val = input_val.replace(rx, '');
4676       }
4677
4678       // cleanup
8deae9 4679       rx = new RegExp(rx_delim + '\\s*' + rx_delim, 'g');
6789bf 4680       input_val = String(input_val).replace(rx, delim);
8deae9 4681       rx = new RegExp('^[\\s' + rx_delim + ']+');
AM 4682       input_val = input_val.replace(rx, '');
15482b 4683
AM 4684       // add new address(es)
8deae9 4685       if (new_val && input_val.indexOf(new_val) == -1 && input_val.indexOf(new_val.replace(/"/g, '')) == -1) {
AM 4686         if (input_val) {
4687           rx = new RegExp('[' + rx_delim + '\\s]+$')
4688           input_val = input_val.replace(rx, '') + delim + ' ';
4689         }
4690
15482b 4691         input_val += new_val + delim + ' ';
AM 4692       }
4693
4694       if (old_val || new_val)
4695         input.val(input_val).change();
be6a09 4696     });
50f56d 4697
646b64 4698     this.editor.change_signature(id, show_sig);
1cded8 4699     this.env.identity = id;
af61b9 4700     this.triggerEvent('change_identity');
1c5853 4701     return true;
655bd9 4702   };
4e17e6 4703
4f53ab 4704   // upload (attachment) file
42f8ab 4705   this.upload_file = function(form, action, lock)
a4c163 4706   {
4e17e6 4707     if (!form)
fb162e 4708       return;
8fa922 4709
271c5c 4710     // count files and size on capable browser
TB 4711     var size = 0, numfiles = 0;
4712
4713     $('input[type=file]', form).each(function(i, field) {
4714       var files = field.files ? field.files.length : (field.value ? 1 : 0);
4715
4716       // check file size
4717       if (field.files) {
4718         for (var i=0; i < files; i++)
4719           size += field.files[i].size;
4720       }
4721
4722       numfiles += files;
4723     });
8fa922 4724
4e17e6 4725     // create hidden iframe and post upload form
271c5c 4726     if (numfiles) {
TB 4727       if (this.env.max_filesize && this.env.filesizeerror && size > this.env.max_filesize) {
4728         this.display_message(this.env.filesizeerror, 'error');
08da30 4729         return false;
fe0cb6 4730       }
A 4731
4f53ab 4732       var frame_name = this.async_upload_form(form, action || 'upload', function(e) {
87a868 4733         var d, content = '';
ebf872 4734         try {
A 4735           if (this.contentDocument) {
87a868 4736             d = this.contentDocument;
01ffe0 4737           } else if (this.contentWindow) {
87a868 4738             d = this.contentWindow.document;
01ffe0 4739           }
f1aaca 4740           content = d.childNodes[1].innerHTML;
b649c4 4741         } catch (err) {}
ebf872 4742
f1aaca 4743         if (!content.match(/add2attachment/) && (!bw.opera || (ref.env.uploadframe && ref.env.uploadframe == e.data.ts))) {
87a868 4744           if (!content.match(/display_message/))
f1aaca 4745             ref.display_message(ref.get_label('fileuploaderror'), 'error');
AM 4746           ref.remove_from_attachment_list(e.data.ts);
42f8ab 4747
AM 4748           if (lock)
4749             ref.set_busy(false, null, lock);
ebf872 4750         }
01ffe0 4751         // Opera hack: handle double onload
T 4752         if (bw.opera)
f1aaca 4753           ref.env.uploadframe = e.data.ts;
ebf872 4754       });
8fa922 4755
3f9712 4756       // display upload indicator and cancel button
271c5c 4757       var content = '<span>' + this.get_label('uploading' + (numfiles > 1 ? 'many' : '')) + '</span>',
b649c4 4758         ts = frame_name.replace(/^rcmupload/, '');
A 4759
ae6d2d 4760       this.add2attachment_list(ts, { name:'', html:content, classname:'uploading', frame:frame_name, complete:false });
4171c5 4761
A 4762       // upload progress support
4763       if (this.env.upload_progress_time) {
4764         this.upload_progress_start('upload', ts);
4765       }
a36369 4766
TB 4767       // set reference to the form object
4768       this.gui_objects.attachmentform = form;
4769       return true;
a4c163 4770     }
A 4771   };
4e17e6 4772
T 4773   // add file name to attachment list
4774   // called from upload page
01ffe0 4775   this.add2attachment_list = function(name, att, upload_id)
T 4776   {
b21f8b 4777     if (upload_id)
AM 4778       this.triggerEvent('fileuploaded', {name: name, attachment: att, id: upload_id});
4779
3cc1af 4780     if (!this.env.attachments)
AM 4781       this.env.attachments = {};
4782
4783     if (upload_id && this.env.attachments[upload_id])
4784       delete this.env.attachments[upload_id];
4785
4786     this.env.attachments[name] = att;
4787
4e17e6 4788     if (!this.gui_objects.attachmentlist)
T 4789       return false;
ae6d2d 4790
10a397 4791     if (!att.complete && this.env.loadingicon)
AM 4792       att.html = '<img src="'+this.env.loadingicon+'" alt="" class="uploading" />' + att.html;
ae6d2d 4793
TB 4794     if (!att.complete && att.frame)
4795       att.html = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\', \''+att.frame+'\');" href="#cancelupload" class="cancelupload">'
9240c9 4796         + (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="'+this.get_label('cancel')+'" />' : this.get_label('cancel')) + '</a>' + att.html;
8fa922 4797
2efe33 4798     var indicator, li = $('<li>');
AM 4799
4800     li.attr('id', name)
4801       .addClass(att.classname)
4802       .html(att.html)
7a5c3a 4803       .on('mouseover', function() { rcube_webmail.long_subject_title_ex(this); });
8fa922 4804
ebf872 4805     // replace indicator's li
A 4806     if (upload_id && (indicator = document.getElementById(upload_id))) {
01ffe0 4807       li.replaceAll(indicator);
T 4808     }
4809     else { // add new li
4810       li.appendTo(this.gui_objects.attachmentlist);
4811     }
8fa922 4812
9240c9 4813     // set tabindex attribute
TB 4814     var tabindex = $(this.gui_objects.attachmentlist).attr('data-tabindex') || '0';
4815     li.find('a').attr('tabindex', tabindex);
8fa922 4816
1c5853 4817     return true;
01ffe0 4818   };
4e17e6 4819
a894ba 4820   this.remove_from_attachment_list = function(name)
01ffe0 4821   {
a36369 4822     if (this.env.attachments) {
TB 4823       delete this.env.attachments[name];
4824       $('#'+name).remove();
4825     }
01ffe0 4826   };
a894ba 4827
S 4828   this.remove_attachment = function(name)
a4c163 4829   {
01ffe0 4830     if (name && this.env.attachments[name])
4591de 4831       this.http_post('remove-attachment', { _id:this.env.compose_id, _file:name });
a894ba 4832
S 4833     return true;
a4c163 4834   };
4e17e6 4835
3f9712 4836   this.cancel_attachment_upload = function(name, frame_name)
a4c163 4837   {
3f9712 4838     if (!name || !frame_name)
V 4839       return false;
4840
4841     this.remove_from_attachment_list(name);
4842     $("iframe[name='"+frame_name+"']").remove();
4843     return false;
4171c5 4844   };
A 4845
4846   this.upload_progress_start = function(action, name)
4847   {
f1aaca 4848     setTimeout(function() { ref.http_request(action, {_progress: name}); },
4171c5 4849       this.env.upload_progress_time * 1000);
A 4850   };
4851
4852   this.upload_progress_update = function(param)
4853   {
a5fe9a 4854     var elem = $('#'+param.name + ' > span');
4171c5 4855
A 4856     if (!elem.length || !param.text)
4857       return;
4858
4859     elem.text(param.text);
4860
4861     if (!param.done)
4862       this.upload_progress_start(param.action, param.name);
a4c163 4863   };
3f9712 4864
4e17e6 4865   // send remote request to add a new contact
T 4866   this.add_contact = function(value)
a4c163 4867   {
4e17e6 4868     if (value)
c31360 4869       this.http_post('addcontact', {_address: value});
8fa922 4870
1c5853 4871     return true;
a4c163 4872   };
4e17e6 4873
f11541 4874   // send remote request to search mail or contacts
30b152 4875   this.qsearch = function(value)
a4c163 4876   {
A 4877     if (value != '') {
c31360 4878       var r, lock = this.set_busy(true, 'searching'),
10a397 4879         url = this.search_params(value),
AM 4880         action = this.env.action == 'compose' && this.contact_list ? 'search-contacts' : 'search';
3cacf9 4881
e9c47c 4882       if (this.message_list)
be9d4d 4883         this.clear_message_list();
e9c47c 4884       else if (this.contact_list)
e9a9f2 4885         this.list_contacts_clear();
e9c47c 4886
c31360 4887       if (this.env.source)
A 4888         url._source = this.env.source;
4889       if (this.env.group)
4890         url._gid = this.env.group;
4891
e9c47c 4892       // reset vars
A 4893       this.env.current_page = 1;
c31360 4894
6c27c3 4895       r = this.http_request(action, url, lock);
e9c47c 4896
A 4897       this.env.qsearch = {lock: lock, request: r};
1bbf8c 4898       this.enable_command('set-listmode', this.env.threads && (this.env.search_scope || 'base') == 'base');
26b520 4899
TB 4900       return true;
e9c47c 4901     }
26b520 4902
TB 4903     return false;
31aa08 4904   };
TB 4905
4906   this.continue_search = function(request_id)
4907   {
10a397 4908     var lock = this.set_busy(true, 'stillsearching');
31aa08 4909
10a397 4910     setTimeout(function() {
31aa08 4911       var url = ref.search_params();
TB 4912       url._continue = request_id;
4913       ref.env.qsearch = { lock: lock, request: ref.http_request('search', url, lock) };
4914     }, 100);
e9c47c 4915   };
A 4916
4917   // build URL params for search
47a783 4918   this.search_params = function(search, filter)
e9c47c 4919   {
c31360 4920     var n, url = {}, mods_arr = [],
e9c47c 4921       mods = this.env.search_mods,
4a7a86 4922       scope = this.env.search_scope || 'base',
TB 4923       mbox = scope == 'all' ? '*' : this.env.mailbox;
e9c47c 4924
A 4925     if (!filter && this.gui_objects.search_filter)
4926       filter = this.gui_objects.search_filter.value;
4927
4928     if (!search && this.gui_objects.qsearchbox)
4929       search = this.gui_objects.qsearchbox.value;
4930
4931     if (filter)
c31360 4932       url._filter = filter;
e9c47c 4933
5802e0 4934     if (this.gui_objects.search_interval)
AM 4935       url._interval = $(this.gui_objects.search_interval).val();
4936
e9c47c 4937     if (search) {
c31360 4938       url._q = search;
e9c47c 4939
47a783 4940       if (mods && this.message_list)
672621 4941         mods = mods[mbox] || mods['*'];
3cacf9 4942
672621 4943       if (mods) {
AM 4944         for (n in mods)
3cacf9 4945           mods_arr.push(n);
c31360 4946         url._headers = mods_arr.join(',');
a4c163 4947       }
A 4948     }
e9c47c 4949
1bbf8c 4950     if (scope)
TB 4951       url._scope = scope;
4952     if (mbox && scope != 'all')
c31360 4953       url._mbox = mbox;
e9c47c 4954
c31360 4955     return url;
a4c163 4956   };
4647e1 4957
da1816 4958   // reset search filter
AM 4959   this.reset_search_filter = function()
4960   {
4961     this.filter_disabled = true;
4962     if (this.gui_objects.search_filter)
4963       $(this.gui_objects.search_filter).val('ALL').change();
4964     this.filter_disabled = false;
4965   };
4966
4647e1 4967   // reset quick-search form
da1816 4968   this.reset_qsearch = function(all)
a4c163 4969   {
4647e1 4970     if (this.gui_objects.qsearchbox)
T 4971       this.gui_objects.qsearchbox.value = '';
8fa922 4972
5802e0 4973     if (this.gui_objects.search_interval)
AM 4974       $(this.gui_objects.search_interval).val('');
4975
d96151 4976     if (this.env.qsearch)
A 4977       this.abort_request(this.env.qsearch);
db0408 4978
da1816 4979     if (all) {
AM 4980       this.env.search_scope = 'base';
4981       this.reset_search_filter();
4982     }
4983
db0408 4984     this.env.qsearch = null;
4647e1 4985     this.env.search_request = null;
f8e48d 4986     this.env.search_id = null;
1bbf8c 4987
TB 4988     this.enable_command('set-listmode', this.env.threads);
a4c163 4989   };
41fa0b 4990
c83535 4991   this.set_searchscope = function(scope)
TB 4992   {
4993     var old = this.env.search_scope;
4994     this.env.search_scope = scope;
4995
4996     // re-send search query with new scope
4997     if (scope != old && this.env.search_request) {
26b520 4998       if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL')
TB 4999         this.filter_mailbox(this.env.search_filter);
5000       if (scope != 'all')
c83535 5001         this.select_folder(this.env.mailbox, '', true);
TB 5002     }
5003   };
5004
5802e0 5005   this.set_searchinterval = function(interval)
AM 5006   {
5007     var old = this.env.search_interval;
5008     this.env.search_interval = interval;
5009
5010     // re-send search query with new interval
5011     if (interval != old && this.env.search_request) {
5012       if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL')
5013         this.filter_mailbox(this.env.search_filter);
5014       if (interval)
5015         this.select_folder(this.env.mailbox, '', true);
5016     }
5017   };
5018
c83535 5019   this.set_searchmods = function(mods)
TB 5020   {
f1aaca 5021     var mbox = this.env.mailbox,
c83535 5022       scope = this.env.search_scope || 'base';
TB 5023
5024     if (scope == 'all')
5025       mbox = '*';
5026
5027     if (!this.env.search_mods)
5028       this.env.search_mods = {};
5029
672621 5030     if (mbox)
AM 5031       this.env.search_mods[mbox] = mods;
c83535 5032   };
TB 5033
f50a66 5034   this.is_multifolder_listing = function()
1e9a59 5035   {
10a397 5036     return this.env.multifolder_listing !== undefined ? this.env.multifolder_listing :
f50a66 5037       (this.env.search_request && (this.env.search_scope || 'base') != 'base');
10a397 5038   };
1e9a59 5039
5d42a9 5040   // action executed after mail is sent
c5c8e7 5041   this.sent_successfully = function(type, msg, folders, save_error)
a4c163 5042   {
ad334a 5043     this.display_message(msg, type);
7e7e45 5044     this.compose_skip_unsavedcheck = true;
271efe 5045
64afb5 5046     if (this.env.extwin) {
c5c8e7 5047       if (!save_error)
AM 5048         this.lock_form(this.gui_objects.messageform);
a4b6f5 5049
5d42a9 5050       var filter = {task: 'mail', action: ''},
AM 5051         rc = this.opener(false, filter) || this.opener(true, filter);
5052
723f4e 5053       if (rc) {
AM 5054         rc.display_message(msg, type);
66a549 5055         // refresh the folder where sent message was saved or replied message comes from
5d42a9 5056         if (folders && $.inArray(rc.env.mailbox, folders) >= 0) {
a4b6f5 5057           rc.command('checkmail');
66a549 5058         }
723f4e 5059       }
a4b6f5 5060
c5c8e7 5061       if (!save_error)
AM 5062         setTimeout(function() { window.close(); }, 1000);
271efe 5063     }
c5c8e7 5064     else if (!save_error) {
271efe 5065       // before redirect we need to wait some time for Chrome (#1486177)
a4b6f5 5066       setTimeout(function() { ref.list_mailbox(); }, 500);
271efe 5067     }
c5c8e7 5068
AM 5069     if (save_error)
5070       this.env.is_sent = true;
a4c163 5071   };
41fa0b 5072
4e17e6 5073
T 5074   /*********************************************************/
5075   /*********     keyboard live-search methods      *********/
5076   /*********************************************************/
5077
5078   // handler for keyboard events on address-fields
0213f8 5079   this.ksearch_keydown = function(e, obj, props)
2c8e84 5080   {
4e17e6 5081     if (this.ksearch_timer)
T 5082       clearTimeout(this.ksearch_timer);
5083
70da8c 5084     var key = rcube_event.get_keycode(e),
74f0a6 5085       mod = rcube_event.get_modifier(e);
4e17e6 5086
8fa922 5087     switch (key) {
6699a6 5088       case 38:  // arrow up
A 5089       case 40:  // arrow down
5090         if (!this.ksearch_visible())
8d9177 5091           return;
8fa922 5092
70da8c 5093         var dir = key == 38 ? 1 : 0,
99cdca 5094           highlight = document.getElementById('rcmkSearchItem' + this.ksearch_selected);
8fa922 5095
4e17e6 5096         if (!highlight)
cc97ea 5097           highlight = this.ksearch_pane.__ul.firstChild;
8fa922 5098
2c8e84 5099         if (highlight)
T 5100           this.ksearch_select(dir ? highlight.previousSibling : highlight.nextSibling);
4e17e6 5101
86958f 5102         return rcube_event.cancel(e);
4e17e6 5103
7f0388 5104       case 9:   // tab
A 5105         if (mod == SHIFT_KEY || !this.ksearch_visible()) {
5106           this.ksearch_hide();
5107           return;
5108         }
5109
0213f8 5110       case 13:  // enter
7f0388 5111         if (!this.ksearch_visible())
A 5112           return false;
4e17e6 5113
86958f 5114         // insert selected address and hide ksearch pane
T 5115         this.insert_recipient(this.ksearch_selected);
4e17e6 5116         this.ksearch_hide();
86958f 5117
T 5118         return rcube_event.cancel(e);
4e17e6 5119
T 5120       case 27:  // escape
5121         this.ksearch_hide();
bd3891 5122         return;
8fa922 5123
ca3c73 5124       case 37:  // left
A 5125       case 39:  // right
8d9177 5126         return;
8fa922 5127     }
4e17e6 5128
T 5129     // start timer
da5cad 5130     this.ksearch_timer = setTimeout(function(){ ref.ksearch_get_results(props); }, 200);
4e17e6 5131     this.ksearch_input = obj;
8fa922 5132
4e17e6 5133     return true;
2c8e84 5134   };
8fa922 5135
7f0388 5136   this.ksearch_visible = function()
A 5137   {
10a397 5138     return this.ksearch_selected !== null && this.ksearch_selected !== undefined && this.ksearch_value;
7f0388 5139   };
A 5140
2c8e84 5141   this.ksearch_select = function(node)
T 5142   {
d4d62a 5143     if (this.ksearch_pane && node) {
d0d7f4 5144       this.ksearch_pane.find('li.selected').removeClass('selected').removeAttr('aria-selected');
2c8e84 5145     }
T 5146
5147     if (node) {
6d3ab6 5148       $(node).addClass('selected').attr('aria-selected', 'true');
2c8e84 5149       this.ksearch_selected = node._rcm_id;
6d3ab6 5150       $(this.ksearch_input).attr('aria-activedescendant', 'rcmkSearchItem' + this.ksearch_selected);
2c8e84 5151     }
T 5152   };
86958f 5153
T 5154   this.insert_recipient = function(id)
5155   {
609d39 5156     if (id === null || !this.env.contacts[id] || !this.ksearch_input)
86958f 5157       return;
8fa922 5158
86958f 5159     // get cursor pos
c296b8 5160     var inp_value = this.ksearch_input.value,
A 5161       cpos = this.get_caret_pos(this.ksearch_input),
5162       p = inp_value.lastIndexOf(this.ksearch_value, cpos),
ec65ad 5163       trigger = false,
c296b8 5164       insert = '',
A 5165       // replace search string with full address
5166       pre = inp_value.substring(0, p),
5167       end = inp_value.substring(p+this.ksearch_value.length, inp_value.length);
0213f8 5168
A 5169     this.ksearch_destroy();
8fa922 5170
a61bbb 5171     // insert all members of a group
96f084 5172     if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].type == 'group' && !this.env.contacts[id].email) {
62c861 5173       insert += this.env.contacts[id].name + this.env.recipients_delimiter;
eeb73c 5174       this.group2expand[this.env.contacts[id].id] = $.extend({ input: this.ksearch_input }, this.env.contacts[id]);
c31360 5175       this.http_request('mail/group-expand', {_source: this.env.contacts[id].source, _gid: this.env.contacts[id].id}, false);
532c10 5176     }
TB 5177     else if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].name) {
5178       insert = this.env.contacts[id].name + this.env.recipients_delimiter;
5179       trigger = true;
a61bbb 5180     }
ec65ad 5181     else if (typeof this.env.contacts[id] === 'string') {
62c861 5182       insert = this.env.contacts[id] + this.env.recipients_delimiter;
ec65ad 5183       trigger = true;
T 5184     }
a61bbb 5185
86958f 5186     this.ksearch_input.value = pre + insert + end;
7b0eac 5187
86958f 5188     // set caret to insert pos
3dfb94 5189     this.set_caret_pos(this.ksearch_input, p + insert.length);
ec65ad 5190
8c7492 5191     if (trigger) {
532c10 5192       this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id] });
8c7492 5193       this.compose_type_activity++;
TB 5194     }
53d626 5195   };
8fa922 5196
53d626 5197   this.replace_group_recipients = function(id, recipients)
T 5198   {
eeb73c 5199     if (this.group2expand[id]) {
T 5200       this.group2expand[id].input.value = this.group2expand[id].input.value.replace(this.group2expand[id].name, recipients);
5201       this.triggerEvent('autocomplete_insert', { field:this.group2expand[id].input, insert:recipients });
5202       this.group2expand[id] = null;
8c7492 5203       this.compose_type_activity++;
53d626 5204     }
c0297f 5205   };
4e17e6 5206
T 5207   // address search processor
0213f8 5208   this.ksearch_get_results = function(props)
2c8e84 5209   {
4e17e6 5210     var inp_value = this.ksearch_input ? this.ksearch_input.value : null;
c296b8 5211
2c8e84 5212     if (inp_value === null)
4e17e6 5213       return;
8fa922 5214
cc97ea 5215     if (this.ksearch_pane && this.ksearch_pane.is(":visible"))
T 5216       this.ksearch_pane.hide();
4e17e6 5217
T 5218     // get string from current cursor pos to last comma
c296b8 5219     var cpos = this.get_caret_pos(this.ksearch_input),
62c861 5220       p = inp_value.lastIndexOf(this.env.recipients_separator, cpos-1),
c296b8 5221       q = inp_value.substring(p+1, cpos),
f8ca74 5222       min = this.env.autocomplete_min_length,
017c4f 5223       data = this.ksearch_data;
4e17e6 5224
T 5225     // trim query string
ef17c5 5226     q = $.trim(q);
4e17e6 5227
297a43 5228     // Don't (re-)search if the last results are still active
cea956 5229     if (q == this.ksearch_value)
ca3c73 5230       return;
8fa922 5231
48a065 5232     this.ksearch_destroy();
A 5233
2b3a8e 5234     if (q.length && q.length < min) {
5f7129 5235       if (!this.ksearch_info) {
A 5236         this.ksearch_info = this.display_message(
2b3a8e 5237           this.get_label('autocompletechars').replace('$min', min));
c296b8 5238       }
A 5239       return;
5240     }
5241
297a43 5242     var old_value = this.ksearch_value;
4e17e6 5243     this.ksearch_value = q;
241450 5244
297a43 5245     // ...string is empty
cea956 5246     if (!q.length)
A 5247       return;
297a43 5248
f8ca74 5249     // ...new search value contains old one and previous search was not finished or its result was empty
017c4f 5250     if (old_value && old_value.length && q.startsWith(old_value) && (!data || data.num <= 0) && this.env.contacts && !this.env.contacts.length)
297a43 5251       return;
0213f8 5252
017c4f 5253     var sources = props && props.sources ? props.sources : [''];
T 5254     var reqid = this.multi_thread_http_request({
5255       items: sources,
5256       threads: props && props.threads ? props.threads : 1,
5257       action:  props && props.action ? props.action : 'mail/autocomplete',
5258       postdata: { _search:q, _source:'%s' },
5259       lock: this.display_message(this.get_label('searching'), 'loading')
5260     });
0213f8 5261
017c4f 5262     this.ksearch_data = { id:reqid, sources:sources.slice(), num:sources.length };
2c8e84 5263   };
4e17e6 5264
0213f8 5265   this.ksearch_query_results = function(results, search, reqid)
2c8e84 5266   {
017c4f 5267     // trigger multi-thread http response callback
T 5268     this.multi_thread_http_response(results, reqid);
5269
5f5cf8 5270     // search stopped in meantime?
A 5271     if (!this.ksearch_value)
5272       return;
5273
aaffbe 5274     // ignore this outdated search response
5f5cf8 5275     if (this.ksearch_input && search != this.ksearch_value)
aaffbe 5276       return;
8fa922 5277
4e17e6 5278     // display search results
d0d7f4 5279     var i, id, len, ul, text, type, init,
48a065 5280       value = this.ksearch_value,
0213f8 5281       maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;
8fa922 5282
0213f8 5283     // create results pane if not present
A 5284     if (!this.ksearch_pane) {
5285       ul = $('<ul>');
d4d62a 5286       this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane').attr('role', 'listbox')
0213f8 5287         .css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
A 5288       this.ksearch_pane.__ul = ul[0];
5289     }
4e17e6 5290
0213f8 5291     ul = this.ksearch_pane.__ul;
A 5292
5293     // remove all search results or add to existing list if parallel search
5294     if (reqid && this.ksearch_pane.data('reqid') == reqid) {
5295       maxlen -= ul.childNodes.length;
5296     }
5297     else {
5298       this.ksearch_pane.data('reqid', reqid);
5299       init = 1;
5300       // reset content
4e17e6 5301       ul.innerHTML = '';
0213f8 5302       this.env.contacts = [];
A 5303       // move the results pane right under the input box
5304       var pos = $(this.ksearch_input).offset();
5305       this.ksearch_pane.css({ left:pos.left+'px', top:(pos.top + this.ksearch_input.offsetHeight)+'px', display: 'none'});
5306     }
812abd 5307
0213f8 5308     // add each result line to list
249815 5309     if (results && (len = results.length)) {
A 5310       for (i=0; i < len && maxlen > 0; i++) {
36d004 5311         text = typeof results[i] === 'object' ? (results[i].display || results[i].name) : results[i];
532c10 5312         type = typeof results[i] === 'object' ? results[i].type : '';
d0d7f4 5313         id = i + this.env.contacts.length;
TB 5314         $('<li>').attr('id', 'rcmkSearchItem' + id)
5315           .attr('role', 'option')
e833e8 5316           .html('<i class="icon"></i>' + this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>'))
d0d7f4 5317           .addClass(type || '')
TB 5318           .appendTo(ul)
5a897b 5319           .mouseover(function() { ref.ksearch_select(this); })
AM 5320           .mouseup(function() { ref.ksearch_click(this); })
d0d7f4 5321           .get(0)._rcm_id = id;
0213f8 5322         maxlen -= 1;
2c8e84 5323       }
T 5324     }
0213f8 5325
A 5326     if (ul.childNodes.length) {
d4d62a 5327       // set the right aria-* attributes to the input field
TB 5328       $(this.ksearch_input)
5329         .attr('aria-haspopup', 'true')
5330         .attr('aria-expanded', 'true')
6d3ab6 5331         .attr('aria-owns', 'rcmKSearchpane');
TB 5332
0213f8 5333       this.ksearch_pane.show();
6d3ab6 5334
0213f8 5335       // select the first
A 5336       if (!this.env.contacts.length) {
6d3ab6 5337         this.ksearch_select($('li:first', ul).get(0));
0213f8 5338       }
A 5339     }
5340
249815 5341     if (len)
0213f8 5342       this.env.contacts = this.env.contacts.concat(results);
A 5343
017c4f 5344     if (this.ksearch_data.id == reqid)
T 5345       this.ksearch_data.num--;
2c8e84 5346   };
8fa922 5347
2c8e84 5348   this.ksearch_click = function(node)
T 5349   {
7b0eac 5350     if (this.ksearch_input)
A 5351       this.ksearch_input.focus();
5352
2c8e84 5353     this.insert_recipient(node._rcm_id);
T 5354     this.ksearch_hide();
5355   };
4e17e6 5356
2c8e84 5357   this.ksearch_blur = function()
8fa922 5358   {
4e17e6 5359     if (this.ksearch_timer)
T 5360       clearTimeout(this.ksearch_timer);
5361
5362     this.ksearch_input = null;
5363     this.ksearch_hide();
8fa922 5364   };
4e17e6 5365
T 5366   this.ksearch_hide = function()
8fa922 5367   {
4e17e6 5368     this.ksearch_selected = null;
0213f8 5369     this.ksearch_value = '';
8fa922 5370
4e17e6 5371     if (this.ksearch_pane)
cc97ea 5372       this.ksearch_pane.hide();
31f05c 5373
d4d62a 5374     $(this.ksearch_input)
TB 5375       .attr('aria-haspopup', 'false')
5376       .attr('aria-expanded', 'false')
761ee4 5377       .removeAttr('aria-activedescendant')
d4d62a 5378       .removeAttr('aria-owns');
31f05c 5379
A 5380     this.ksearch_destroy();
5381   };
4e17e6 5382
48a065 5383   // Clears autocomplete data/requests
0213f8 5384   this.ksearch_destroy = function()
A 5385   {
017c4f 5386     if (this.ksearch_data)
T 5387       this.multi_thread_request_abort(this.ksearch_data.id);
0213f8 5388
5f7129 5389     if (this.ksearch_info)
A 5390       this.hide_message(this.ksearch_info);
5391
5392     if (this.ksearch_msg)
5393       this.hide_message(this.ksearch_msg);
5394
0213f8 5395     this.ksearch_data = null;
5f7129 5396     this.ksearch_info = null;
A 5397     this.ksearch_msg = null;
48a065 5398   };
A 5399
5400
4e17e6 5401   /*********************************************************/
T 5402   /*********         address book methods          *********/
5403   /*********************************************************/
5404
6b47de 5405   this.contactlist_keypress = function(list)
8fa922 5406   {
A 5407     if (list.key_pressed == list.DELETE_KEY)
5408       this.command('delete');
5409   };
6b47de 5410
T 5411   this.contactlist_select = function(list)
8fa922 5412   {
A 5413     if (this.preview_timer)
5414       clearTimeout(this.preview_timer);
f11541 5415
2611ac 5416     var n, id, sid, contact, writable = false,
f7af22 5417       selected = list.selection.length,
ecf295 5418       source = this.env.source ? this.env.address_sources[this.env.source] : null;
A 5419
ab845c 5420     // we don't have dblclick handler here, so use 200 instead of this.dblclick_time
0a909f 5421     if (this.env.contentframe && (id = list.get_single_selection()))
da5cad 5422       this.preview_timer = setTimeout(function(){ ref.load_contact(id, 'show'); }, 200);
8fa922 5423     else if (this.env.contentframe)
A 5424       this.show_contentframe(false);
6b47de 5425
f7af22 5426     if (selected) {
6ff6be 5427       list.draggable = false;
TB 5428
ff4a92 5429       // no source = search result, we'll need to detect if any of
AM 5430       // selected contacts are in writable addressbook to enable edit/delete
5431       // we'll also need to know sources used in selection for copy
5432       // and group-addmember operations (drag&drop)
5433       this.env.selection_sources = [];
86552f 5434
TB 5435       if (source) {
5436         this.env.selection_sources.push(this.env.source);
5437       }
5438
5439       for (n in list.selection) {
5440         contact = list.data[list.selection[n]];
5441         if (!source) {
ecf295 5442           sid = String(list.selection[n]).replace(/^[^-]+-/, '');
ff4a92 5443           if (sid && this.env.address_sources[sid]) {
86552f 5444             writable = writable || (!this.env.address_sources[sid].readonly && !contact.readonly);
ff4a92 5445             this.env.selection_sources.push(sid);
ecf295 5446           }
A 5447         }
86552f 5448         else {
TB 5449           writable = writable || (!source.readonly && !contact.readonly);
5450         }
6ff6be 5451
TB 5452         if (contact._type != 'group')
5453           list.draggable = true;
ecf295 5454       }
86552f 5455
TB 5456       this.env.selection_sources = $.unique(this.env.selection_sources);
ecf295 5457     }
A 5458
1ba07f 5459     // if a group is currently selected, and there is at least one contact selected
T 5460     // thend we can enable the group-remove-selected command
f7af22 5461     this.enable_command('group-remove-selected', this.env.group && selected && writable);
AM 5462     this.enable_command('compose', this.env.group || selected);
5463     this.enable_command('print', selected == 1);
5464     this.enable_command('export-selected', 'copy', selected > 0);
ecf295 5465     this.enable_command('edit', id && writable);
f7af22 5466     this.enable_command('delete', 'move', selected && writable);
6b47de 5467
8fa922 5468     return false;
A 5469   };
6b47de 5470
a61bbb 5471   this.list_contacts = function(src, group, page)
8fa922 5472   {
24fa5d 5473     var win, folder, url = {},
765a0b 5474       refresh = src === undefined && group === undefined && page === undefined,
053e5a 5475       target = window;
8fa922 5476
bb8012 5477     if (!src)
f11541 5478       src = this.env.source;
8fa922 5479
9e2603 5480     if (refresh)
AM 5481       group = this.env.group;
5482
a61bbb 5483     if (page && this.current_page == page && src == this.env.source && group == this.env.group)
4e17e6 5484       return false;
8fa922 5485
A 5486     if (src != this.env.source) {
053e5a 5487       page = this.env.current_page = 1;
6b603d 5488       this.reset_qsearch();
8fa922 5489     }
765a0b 5490     else if (!refresh && group != this.env.group)
a61bbb 5491       page = this.env.current_page = 1;
f11541 5492
f8e48d 5493     if (this.env.search_id)
A 5494       folder = 'S'+this.env.search_id;
6c27c3 5495     else if (!this.env.search_request)
f8e48d 5496       folder = group ? 'G'+src+group : src;
A 5497
f11541 5498     this.env.source = src;
a61bbb 5499     this.env.group = group;
86552f 5500
TB 5501     // truncate groups listing stack
5502     var index = $.inArray(this.env.group, this.env.address_group_stack);
5503     if (index < 0)
5504       this.env.address_group_stack = [];
5505     else
5506       this.env.address_group_stack = this.env.address_group_stack.slice(0,index);
5507
5508     // make sure the current group is on top of the stack
5509     if (this.env.group) {
5510       this.env.address_group_stack.push(this.env.group);
5511
5512       // mark the first group on the stack as selected in the directory list
5513       folder = 'G'+src+this.env.address_group_stack[0];
5514     }
5515     else if (this.gui_objects.addresslist_title) {
5516         $(this.gui_objects.addresslist_title).html(this.get_label('contacts'));
5517     }
5518
71a522 5519     if (!this.env.search_id)
TB 5520       this.select_folder(folder, '', true);
4e17e6 5521
T 5522     // load contacts remotely
8fa922 5523     if (this.gui_objects.contactslist) {
a61bbb 5524       this.list_contacts_remote(src, group, page);
4e17e6 5525       return;
8fa922 5526     }
4e17e6 5527
24fa5d 5528     if (win = this.get_frame_window(this.env.contentframe)) {
AM 5529       target = win;
c31360 5530       url._framed = 1;
8fa922 5531     }
A 5532
a61bbb 5533     if (group)
c31360 5534       url._gid = group;
a61bbb 5535     if (page)
c31360 5536       url._page = page;
A 5537     if (src)
5538       url._source = src;
4e17e6 5539
f11541 5540     // also send search request to get the correct listing
T 5541     if (this.env.search_request)
c31360 5542       url._search = this.env.search_request;
f11541 5543
4e17e6 5544     this.set_busy(true, 'loading');
c31360 5545     this.location_href(url, target);
8fa922 5546   };
4e17e6 5547
T 5548   // send remote request to load contacts list
a61bbb 5549   this.list_contacts_remote = function(src, group, page)
8fa922 5550   {
6b47de 5551     // clear message list first
e9a9f2 5552     this.list_contacts_clear();
4e17e6 5553
T 5554     // send request to server
c31360 5555     var url = {}, lock = this.set_busy(true, 'loading');
A 5556
5557     if (src)
5558       url._source = src;
5559     if (page)
5560       url._page = page;
5561     if (group)
5562       url._gid = group;
ad334a 5563
f11541 5564     this.env.source = src;
a61bbb 5565     this.env.group = group;
8fa922 5566
6c27c3 5567     // also send search request to get the right records
f8e48d 5568     if (this.env.search_request)
c31360 5569       url._search = this.env.search_request;
f11541 5570
eeb73c 5571     this.http_request(this.env.task == 'mail' ? 'list-contacts' : 'list', url, lock);
e9a9f2 5572   };
A 5573
5574   this.list_contacts_clear = function()
5575   {
c5a5f9 5576     this.contact_list.data = {};
e9a9f2 5577     this.contact_list.clear(true);
A 5578     this.show_contentframe(false);
f7af22 5579     this.enable_command('delete', 'move', 'copy', 'print', false);
AM 5580     this.enable_command('compose', this.env.group);
8fa922 5581   };
4e17e6 5582
86552f 5583   this.set_group_prop = function(prop)
TB 5584   {
de98a8 5585     if (this.gui_objects.addresslist_title) {
TB 5586       var boxtitle = $(this.gui_objects.addresslist_title).html('');  // clear contents
5587
5588       // add link to pop back to parent group
5589       if (this.env.address_group_stack.length > 1) {
5590         $('<a href="#list">...</a>')
8f8bea 5591           .attr('title', this.get_label('uponelevel'))
de98a8 5592           .addClass('poplink')
TB 5593           .appendTo(boxtitle)
5594           .click(function(e){ return ref.command('popgroup','',this); });
5595         boxtitle.append('&nbsp;&raquo;&nbsp;');
5596       }
5597
2e30b2 5598       boxtitle.append($('<span>').text(prop.name));
de98a8 5599     }
86552f 5600
TB 5601     this.triggerEvent('groupupdate', prop);
5602   };
5603
4e17e6 5604   // load contact record
T 5605   this.load_contact = function(cid, action, framed)
8fa922 5606   {
c5a5f9 5607     var win, url = {}, target = window,
a0e86d 5608       rec = this.contact_list ? this.contact_list.data[cid] : null;
356a79 5609
24fa5d 5610     if (win = this.get_frame_window(this.env.contentframe)) {
c31360 5611       url._framed = 1;
24fa5d 5612       target = win;
f11541 5613       this.show_contentframe(true);
1a3c91 5614
0b3b66 5615       // load dummy content, unselect selected row(s)
AM 5616       if (!cid)
1a3c91 5617         this.contact_list.clear_selection();
86552f 5618
a0e86d 5619       this.enable_command('compose', rec && rec.email);
f7af22 5620       this.enable_command('export-selected', 'print', rec && rec._type != 'group');
8fa922 5621     }
4e17e6 5622     else if (framed)
T 5623       return false;
8fa922 5624
70da8c 5625     if (action && (cid || action == 'add') && !this.drag_active) {
356a79 5626       if (this.env.group)
c31360 5627         url._gid = this.env.group;
356a79 5628
765a0b 5629       if (this.env.search_request)
AM 5630         url._search = this.env.search_request;
5631
c31360 5632       url._action = action;
A 5633       url._source = this.env.source;
5634       url._cid = cid;
5635
5636       this.location_href(url, target, true);
8fa922 5637     }
c31360 5638
1c5853 5639     return true;
8fa922 5640   };
f11541 5641
2c77f5 5642   // add/delete member to/from the group
A 5643   this.group_member_change = function(what, cid, source, gid)
5644   {
70da8c 5645     if (what != 'add')
AM 5646       what = 'del';
5647
c31360 5648     var label = this.get_label(what == 'add' ? 'addingmember' : 'removingmember'),
A 5649       lock = this.display_message(label, 'loading'),
5650       post_data = {_cid: cid, _source: source, _gid: gid};
2c77f5 5651
c31360 5652     this.http_post('group-'+what+'members', post_data, lock);
2c77f5 5653   };
A 5654
a45f9b 5655   this.contacts_drag_menu = function(e, to)
AM 5656   {
5657     var dest = to.type == 'group' ? to.source : to.id,
5658       source = this.env.source;
5659
5660     if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
5661       return true;
5662
5663     // search result may contain contacts from many sources, but if there is only one...
5664     if (source == '' && this.env.selection_sources.length == 1)
5665       source = this.env.selection_sources[0];
5666
5667     if (to.type == 'group' && dest == source) {
5668       var cid = this.contact_list.get_selection().join(',');
5669       this.group_member_change('add', cid, dest, to.id);
5670       return true;
5671     }
5672     // move action is not possible, "redirect" to copy if menu wasn't requested
5673     else if (!this.commands.move && rcube_event.get_modifier(e) != SHIFT_KEY) {
5674       this.copy_contacts(to);
5675       return true;
5676     }
5677
5678     return this.drag_menu(e, to);
5679   };
5680
5681   // copy contact(s) to the specified target (group or directory)
5682   this.copy_contacts = function(to)
8fa922 5683   {
70da8c 5684     var dest = to.type == 'group' ? to.source : to.id,
ff4a92 5685       source = this.env.source,
a45f9b 5686       group = this.env.group ? this.env.group : '',
f11541 5687       cid = this.contact_list.get_selection().join(',');
T 5688
ff4a92 5689     if (!cid || !this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
AM 5690       return;
c31360 5691
ff4a92 5692     // search result may contain contacts from many sources, but if there is only one...
AM 5693     if (source == '' && this.env.selection_sources.length == 1)
5694       source = this.env.selection_sources[0];
5695
5696     // tagret is a group
5697     if (to.type == 'group') {
5698       if (dest == source)
a45f9b 5699         return;
ff4a92 5700
a45f9b 5701       var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
AM 5702         post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group};
5703
5704       this.http_post('copy', post_data, lock);
ca38db 5705     }
ff4a92 5706     // target is an addressbook
AM 5707     else if (to.id != source) {
c31360 5708       var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
eafb68 5709         post_data = {_cid: cid, _source: this.env.source, _to: to.id, _gid: group};
c31360 5710
A 5711       this.http_post('copy', post_data, lock);
ca38db 5712     }
8fa922 5713   };
4e17e6 5714
a45f9b 5715   // move contact(s) to the specified target (group or directory)
AM 5716   this.move_contacts = function(to)
8fa922 5717   {
a45f9b 5718     var dest = to.type == 'group' ? to.source : to.id,
AM 5719       source = this.env.source,
5720       group = this.env.group ? this.env.group : '';
b17539 5721
a45f9b 5722     if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
4e17e6 5723       return;
8fa922 5724
a45f9b 5725     // search result may contain contacts from many sources, but if there is only one...
AM 5726     if (source == '' && this.env.selection_sources.length == 1)
5727       source = this.env.selection_sources[0];
4e17e6 5728
a45f9b 5729     if (to.type == 'group') {
AM 5730       if (dest == source)
5731         return;
5732
5733       this._with_selected_contacts('move', {_to: dest, _togid: to.id});
5734     }
5735     // target is an addressbook
5736     else if (to.id != source)
5737       this._with_selected_contacts('move', {_to: to.id});
5738   };
5739
5740   // delete contact(s)
5741   this.delete_contacts = function()
5742   {
5743     var undelete = this.env.source && this.env.address_sources[this.env.source].undelete;
5744
5745     if (!undelete && !confirm(this.get_label('deletecontactconfirm')))
5746       return;
5747
5748     return this._with_selected_contacts('delete');
5749   };
5750
5751   this._with_selected_contacts = function(action, post_data)
5752   {
5753     var selection = this.contact_list ? this.contact_list.get_selection() : [];
5754
a5fe9a 5755     // exit if no contact specified or if selection is empty
a45f9b 5756     if (!selection.length && !this.env.cid)
AM 5757       return;
5758
5759     var n, a_cids = [],
5760       label = action == 'delete' ? 'contactdeleting' : 'movingcontact',
5761       lock = this.display_message(this.get_label(label), 'loading');
70da8c 5762
4e17e6 5763     if (this.env.cid)
0e7b66 5764       a_cids.push(this.env.cid);
8fa922 5765     else {
ecf295 5766       for (n=0; n<selection.length; n++) {
6b47de 5767         id = selection[n];
0e7b66 5768         a_cids.push(id);
f4f8c6 5769         this.contact_list.remove_row(id, (n == selection.length-1));
8fa922 5770       }
4e17e6 5771
T 5772       // hide content frame if we delete the currently displayed contact
f11541 5773       if (selection.length == 1)
T 5774         this.show_contentframe(false);
8fa922 5775     }
4e17e6 5776
a45f9b 5777     if (!post_data)
AM 5778       post_data = {};
5779
5780     post_data._source = this.env.source;
5781     post_data._from = this.env.action;
c31360 5782     post_data._cid = a_cids.join(',');
A 5783
8458c7 5784     if (this.env.group)
c31360 5785       post_data._gid = this.env.group;
8458c7 5786
b15568 5787     // also send search request to get the right records from the next page
ecf295 5788     if (this.env.search_request)
c31360 5789       post_data._search = this.env.search_request;
b15568 5790
4e17e6 5791     // send request to server
a45f9b 5792     this.http_post(action, post_data, lock)
8fa922 5793
1c5853 5794     return true;
8fa922 5795   };
4e17e6 5796
T 5797   // update a contact record in the list
a0e86d 5798   this.update_contact_row = function(cid, cols_arr, newcid, source, data)
cc97ea 5799   {
70da8c 5800     var list = this.contact_list;
ce988a 5801
fb6d86 5802     cid = this.html_identifier(cid);
3a24a1 5803
5db6f9 5804     // when in searching mode, concat cid with the source name
A 5805     if (!list.rows[cid]) {
70da8c 5806       cid = cid + '-' + source;
5db6f9 5807       if (newcid)
70da8c 5808         newcid = newcid + '-' + source;
5db6f9 5809     }
A 5810
517dae 5811     list.update_row(cid, cols_arr, newcid, true);
dd5472 5812     list.data[cid] = data;
cc97ea 5813   };
e83f03 5814
A 5815   // add row to contacts list
c5a5f9 5816   this.add_contact_row = function(cid, cols, classes, data)
8fa922 5817   {
c84d33 5818     if (!this.gui_objects.contactslist)
e83f03 5819       return false;
8fa922 5820
56012e 5821     var c, col, list = this.contact_list,
517dae 5822       row = { cols:[] };
8fa922 5823
70da8c 5824     row.id = 'rcmrow' + this.html_identifier(cid);
4cf42f 5825     row.className = 'contact ' + (classes || '');
8fa922 5826
c84d33 5827     if (list.in_selection(cid))
e83f03 5828       row.className += ' selected';
A 5829
5830     // add each submitted col
57863c 5831     for (c in cols) {
517dae 5832       col = {};
e83f03 5833       col.className = String(c).toLowerCase();
A 5834       col.innerHTML = cols[c];
517dae 5835       row.cols.push(col);
e83f03 5836     }
8fa922 5837
c5a5f9 5838     // store data in list member
TB 5839     list.data[cid] = data;
c84d33 5840     list.insert_row(row);
8fa922 5841
c84d33 5842     this.enable_command('export', list.rowcount > 0);
e50551 5843   };
A 5844
5845   this.init_contact_form = function()
5846   {
2611ac 5847     var col;
e50551 5848
83f707 5849     if (this.env.coltypes) {
AM 5850       this.set_photo_actions($('#ff_photo').val());
5851       for (col in this.env.coltypes)
5852         this.init_edit_field(col, null);
5853     }
e50551 5854
A 5855     $('.contactfieldgroup .row a.deletebutton').click(function() {
5856       ref.delete_edit_field(this);
5857       return false;
5858     });
5859
70da8c 5860     $('select.addfieldmenu').change(function() {
e50551 5861       ref.insert_edit_field($(this).val(), $(this).attr('rel'), this);
A 5862       this.selectedIndex = 0;
5863     });
5864
537c39 5865     // enable date pickers on date fields
T 5866     if ($.datepicker && this.env.date_format) {
5867       $.datepicker.setDefaults({
5868         dateFormat: this.env.date_format,
5869         changeMonth: true,
5870         changeYear: true,
686ff4 5871         yearRange: '-120:+10',
537c39 5872         showOtherMonths: true,
b7c35d 5873         selectOtherMonths: true
686ff4 5874 //        onSelect: function(dateText) { $(this).focus().val(dateText); }
537c39 5875       });
T 5876       $('input.datepicker').datepicker();
5877     }
5878
55a2e5 5879     // Submit search form on Enter
AM 5880     if (this.env.action == 'search')
5881       $(this.gui_objects.editform).append($('<input type="submit">').hide())
5882         .submit(function() { $('input.mainaction').click(); return false; });
8fa922 5883   };
A 5884
6c5c22 5885   // group creation dialog
edfe91 5886   this.group_create = function()
a61bbb 5887   {
6c5c22 5888     var input = $('<input>').attr('type', 'text'),
AM 5889       content = $('<label>').text(this.get_label('namex')).append(input);
5890
5891     this.show_popup_dialog(content, this.get_label('newgroup'),
5892       [{
5893         text: this.get_label('save'),
630d08 5894         'class': 'mainaction',
6c5c22 5895         click: function() {
AM 5896           var name;
5897
5898           if (name = input.val()) {
5899             ref.http_post('group-create', {_source: ref.env.source, _name: name},
5900               ref.set_busy(true, 'loading'));
5901           }
5902
5903           $(this).dialog('close');
5904         }
5905       }]
5906     );
a61bbb 5907   };
8fa922 5908
6c5c22 5909   // group rename dialog
edfe91 5910   this.group_rename = function()
3baa72 5911   {
6c5c22 5912     if (!this.env.group)
3baa72 5913       return;
8fa922 5914
6c5c22 5915     var group_name = this.env.contactgroups['G' + this.env.source + this.env.group].name,
AM 5916       input = $('<input>').attr('type', 'text').val(group_name),
5917       content = $('<label>').text(this.get_label('namex')).append(input);
3baa72 5918
6c5c22 5919     this.show_popup_dialog(content, this.get_label('grouprename'),
AM 5920       [{
5921         text: this.get_label('save'),
630d08 5922         'class': 'mainaction',
6c5c22 5923         click: function() {
AM 5924           var name;
3baa72 5925
6c5c22 5926           if ((name = input.val()) && name != group_name) {
AM 5927             ref.http_post('group-rename', {_source: ref.env.source, _gid: ref.env.group, _name: name},
5928               ref.set_busy(true, 'loading'));
5929           }
5930
5931           $(this).dialog('close');
5932         }
5933       }],
5934       {open: function() { input.select(); }}
5935     );
3baa72 5936   };
8fa922 5937
edfe91 5938   this.group_delete = function()
3baa72 5939   {
5731d6 5940     if (this.env.group && confirm(this.get_label('deletegroupconfirm'))) {
A 5941       var lock = this.set_busy(true, 'groupdeleting');
c31360 5942       this.http_post('group-delete', {_source: this.env.source, _gid: this.env.group}, lock);
5731d6 5943     }
3baa72 5944   };
8fa922 5945
3baa72 5946   // callback from server upon group-delete command
bb8012 5947   this.remove_group_item = function(prop)
3baa72 5948   {
344943 5949     var key = 'G'+prop.source+prop.id;
70da8c 5950
344943 5951     if (this.treelist.remove(key)) {
1fdb55 5952       this.triggerEvent('group_delete', { source:prop.source, id:prop.id });
3baa72 5953       delete this.env.contactfolders[key];
T 5954       delete this.env.contactgroups[key];
5955     }
8fa922 5956
bb8012 5957     this.list_contacts(prop.source, 0);
3baa72 5958   };
8fa922 5959
1ba07f 5960   //remove selected contacts from current active group
T 5961   this.group_remove_selected = function()
5962   {
10a397 5963     this.http_post('group-delmembers', {_cid: this.contact_list.selection,
c31360 5964       _source: this.env.source, _gid: this.env.group});
1ba07f 5965   };
T 5966
5967   //callback after deleting contact(s) from current group
5968   this.remove_group_contacts = function(props)
5969   {
10a397 5970     if (this.env.group !== undefined && (this.env.group === props.gid)) {
c31360 5971       var n, selection = this.contact_list.get_selection();
A 5972       for (n=0; n<selection.length; n++) {
5973         id = selection[n];
5974         this.contact_list.remove_row(id, (n == selection.length-1));
1ba07f 5975       }
T 5976     }
10a397 5977   };
1ba07f 5978
a61bbb 5979   // callback for creating a new contact group
T 5980   this.insert_contact_group = function(prop)
5981   {
0dc5bc 5982     prop.type = 'group';
70da8c 5983
1564d4 5984     var key = 'G'+prop.source+prop.id,
A 5985       link = $('<a>').attr('href', '#')
5986         .attr('rel', prop.source+':'+prop.id)
f1aaca 5987         .click(function() { return ref.command('listgroup', prop, this); })
344943 5988         .html(prop.name);
0dc5bc 5989
1564d4 5990     this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
71a522 5991     this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, 'contactgroup');
8fa922 5992
344943 5993     this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key) });
3baa72 5994   };
8fa922 5995
3baa72 5996   // callback for renaming a contact group
bb8012 5997   this.update_contact_group = function(prop)
3baa72 5998   {
360bd3 5999     var key = 'G'+prop.source+prop.id,
344943 6000       newnode = {};
8fa922 6001
360bd3 6002     // group ID has changed, replace link node and identifiers
344943 6003     if (prop.newid) {
1564d4 6004       var newkey = 'G'+prop.source+prop.newid,
344943 6005         newprop = $.extend({}, prop);
1564d4 6006
360bd3 6007       this.env.contactfolders[newkey] = this.env.contactfolders[key];
ec6c39 6008       this.env.contactfolders[newkey].id = prop.newid;
360bd3 6009       this.env.group = prop.newid;
d1d9fd 6010
1564d4 6011       delete this.env.contactfolders[key];
A 6012       delete this.env.contactgroups[key];
6013
360bd3 6014       newprop.id = prop.newid;
T 6015       newprop.type = 'group';
d1d9fd 6016
344943 6017       newnode.id = newkey;
TB 6018       newnode.html = $('<a>').attr('href', '#')
360bd3 6019         .attr('rel', prop.source+':'+prop.newid)
f1aaca 6020         .click(function() { return ref.command('listgroup', newprop, this); })
360bd3 6021         .html(prop.name);
T 6022     }
6023     // update displayed group name
344943 6024     else {
TB 6025       $(this.treelist.get_item(key)).children().first().html(prop.name);
6026       this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
1564d4 6027     }
A 6028
344943 6029     // update list node and re-sort it
TB 6030     this.treelist.update(key, newnode, true);
1564d4 6031
344943 6032     this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key), newid:prop.newid });
1564d4 6033   };
A 6034
62811c 6035   this.update_group_commands = function()
A 6036   {
70da8c 6037     var source = this.env.source != '' ? this.env.address_sources[this.env.source] : null,
AM 6038       supported = source && source.groups && !source.readonly;
6039
6040     this.enable_command('group-create', supported);
6041     this.enable_command('group-rename', 'group-delete', supported && this.env.group);
3baa72 6042   };
4e17e6 6043
0501b6 6044   this.init_edit_field = function(col, elem)
T 6045   {
28391b 6046     var label = this.env.coltypes[col].label;
A 6047
0501b6 6048     if (!elem)
T 6049       elem = $('.ff_' + col);
d1d9fd 6050
28391b 6051     if (label)
A 6052       elem.placeholder(label);
0501b6 6053   };
T 6054
6055   this.insert_edit_field = function(col, section, menu)
6056   {
6057     // just make pre-defined input field visible
6058     var elem = $('#ff_'+col);
6059     if (elem.length) {
6060       elem.show().focus();
491133 6061       $(menu).children('option[value="'+col+'"]').prop('disabled', true);
0501b6 6062     }
T 6063     else {
6064       var lastelem = $('.ff_'+col),
6065         appendcontainer = $('#contactsection'+section+' .contactcontroller'+col);
e9a9f2 6066
c71e95 6067       if (!appendcontainer.length) {
A 6068         var sect = $('#contactsection'+section),
6069           lastgroup = $('.contactfieldgroup', sect).last();
6070         appendcontainer = $('<fieldset>').addClass('contactfieldgroup contactcontroller'+col);
6071         if (lastgroup.length)
6072           appendcontainer.insertAfter(lastgroup);
6073         else
6074           sect.prepend(appendcontainer);
6075       }
0501b6 6076
T 6077       if (appendcontainer.length && appendcontainer.get(0).nodeName == 'FIELDSET') {
6078         var input, colprop = this.env.coltypes[col],
24e89e 6079           input_id = 'ff_' + col + (colprop.count || 0),
0501b6 6080           row = $('<div>').addClass('row'),
T 6081           cell = $('<div>').addClass('contactfieldcontent data'),
6082           label = $('<div>').addClass('contactfieldlabel label');
e9a9f2 6083
0501b6 6084         if (colprop.subtypes_select)
T 6085           label.html(colprop.subtypes_select);
6086         else
24e89e 6087           label.html('<label for="' + input_id + '">' + colprop.label + '</label>');
0501b6 6088
T 6089         var name_suffix = colprop.limit != 1 ? '[]' : '';
70da8c 6090
0501b6 6091         if (colprop.type == 'text' || colprop.type == 'date') {
T 6092           input = $('<input>')
6093             .addClass('ff_'+col)
24e89e 6094             .attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size, id: input_id})
0501b6 6095             .appendTo(cell);
T 6096
6097           this.init_edit_field(col, input);
249815 6098
537c39 6099           if (colprop.type == 'date' && $.datepicker)
T 6100             input.datepicker();
0501b6 6101         }
5a7941 6102         else if (colprop.type == 'textarea') {
T 6103           input = $('<textarea>')
6104             .addClass('ff_'+col)
24e89e 6105             .attr({ name: '_'+col+name_suffix, cols:colprop.size, rows:colprop.rows, id: input_id })
5a7941 6106             .appendTo(cell);
T 6107
6108           this.init_edit_field(col, input);
6109         }
0501b6 6110         else if (colprop.type == 'composite') {
70da8c 6111           var i, childcol, cp, first, templ, cols = [], suffices = [];
AM 6112
b0c70b 6113           // read template for composite field order
T 6114           if ((templ = this.env[col+'_template'])) {
70da8c 6115             for (i=0; i < templ.length; i++) {
AM 6116               cols.push(templ[i][1]);
6117               suffices.push(templ[i][2]);
b0c70b 6118             }
T 6119           }
6120           else {  // list fields according to appearance in colprop
6121             for (childcol in colprop.childs)
6122               cols.push(childcol);
6123           }
ecf295 6124
70da8c 6125           for (i=0; i < cols.length; i++) {
b0c70b 6126             childcol = cols[i];
0501b6 6127             cp = colprop.childs[childcol];
T 6128             input = $('<input>')
6129               .addClass('ff_'+childcol)
b0c70b 6130               .attr({ type: 'text', name: '_'+childcol+name_suffix, size: cp.size })
0501b6 6131               .appendTo(cell);
b0c70b 6132             cell.append(suffices[i] || " ");
0501b6 6133             this.init_edit_field(childcol, input);
T 6134             if (!first) first = input;
6135           }
6136           input = first;  // set focus to the first of this composite fields
6137         }
6138         else if (colprop.type == 'select') {
6139           input = $('<select>')
6140             .addClass('ff_'+col)
24e89e 6141             .attr({ 'name': '_'+col+name_suffix, id: input_id })
0501b6 6142             .appendTo(cell);
e9a9f2 6143
0501b6 6144           var options = input.attr('options');
T 6145           options[options.length] = new Option('---', '');
6146           if (colprop.options)
6147             $.each(colprop.options, function(i, val){ options[options.length] = new Option(val, i); });
6148         }
6149
6150         if (input) {
6151           var delbutton = $('<a href="#del"></a>')
6152             .addClass('contactfieldbutton deletebutton')
491133 6153             .attr({title: this.get_label('delete'), rel: col})
0501b6 6154             .html(this.env.delbutton)
T 6155             .click(function(){ ref.delete_edit_field(this); return false })
6156             .appendTo(cell);
e9a9f2 6157
0501b6 6158           row.append(label).append(cell).appendTo(appendcontainer.show());
T 6159           input.first().focus();
e9a9f2 6160
0501b6 6161           // disable option if limit reached
T 6162           if (!colprop.count) colprop.count = 0;
6163           if (++colprop.count == colprop.limit && colprop.limit)
491133 6164             $(menu).children('option[value="'+col+'"]').prop('disabled', true);
0501b6 6165         }
T 6166       }
6167     }
6168   };
6169
6170   this.delete_edit_field = function(elem)
6171   {
6172     var col = $(elem).attr('rel'),
6173       colprop = this.env.coltypes[col],
6174       fieldset = $(elem).parents('fieldset.contactfieldgroup'),
6175       addmenu = fieldset.parent().find('select.addfieldmenu');
e9a9f2 6176
0501b6 6177     // just clear input but don't hide the last field
T 6178     if (--colprop.count <= 0 && colprop.visible)
6179       $(elem).parent().children('input').val('').blur();
6180     else {
6181       $(elem).parents('div.row').remove();
6182       // hide entire fieldset if no more rows
6183       if (!fieldset.children('div.row').length)
6184         fieldset.hide();
6185     }
e9a9f2 6186
0501b6 6187     // enable option in add-field selector or insert it if necessary
T 6188     if (addmenu.length) {
6189       var option = addmenu.children('option[value="'+col+'"]');
6190       if (option.length)
491133 6191         option.prop('disabled', false);
0501b6 6192       else
T 6193         option = $('<option>').attr('value', col).html(colprop.label).appendTo(addmenu);
6194       addmenu.show();
6195     }
6196   };
6197
6198   this.upload_contact_photo = function(form)
6199   {
6200     if (form && form.elements._photo.value) {
6201       this.async_upload_form(form, 'upload-photo', function(e) {
f1aaca 6202         ref.set_busy(false, null, ref.file_upload_id);
0501b6 6203       });
T 6204
6205       // display upload indicator
0be8bd 6206       this.file_upload_id = this.set_busy(true, 'uploading');
0501b6 6207     }
T 6208   };
e50551 6209
0501b6 6210   this.replace_contact_photo = function(id)
T 6211   {
6212     var img_src = id == '-del-' ? this.env.photo_placeholder :
8799df 6213       this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + (this.env.cid || 0) + '&_photo=' + id;
e50551 6214
A 6215     this.set_photo_actions(id);
0501b6 6216     $(this.gui_objects.contactphoto).children('img').attr('src', img_src);
T 6217   };
e50551 6218
0501b6 6219   this.photo_upload_end = function()
T 6220   {
0be8bd 6221     this.set_busy(false, null, this.file_upload_id);
TB 6222     delete this.file_upload_id;
0501b6 6223   };
T 6224
e50551 6225   this.set_photo_actions = function(id)
A 6226   {
6227     var n, buttons = this.buttons['upload-photo'];
27eb27 6228     for (n=0; buttons && n < buttons.length; n++)
589385 6229       $('a#'+buttons[n].id).html(this.get_label(id == '-del-' ? 'addphoto' : 'replacephoto'));
e50551 6230
A 6231     $('#ff_photo').val(id);
6232     this.enable_command('upload-photo', this.env.coltypes.photo ? true : false);
6233     this.enable_command('delete-photo', this.env.coltypes.photo && id != '-del-');
6234   };
6235
e9a9f2 6236   // load advanced search page
A 6237   this.advanced_search = function()
6238   {
24fa5d 6239     var win, url = {_form: 1, _action: 'search'}, target = window;
e9a9f2 6240
24fa5d 6241     if (win = this.get_frame_window(this.env.contentframe)) {
c31360 6242       url._framed = 1;
24fa5d 6243       target = win;
e9a9f2 6244       this.contact_list.clear_selection();
A 6245     }
6246
c31360 6247     this.location_href(url, target, true);
e9a9f2 6248
A 6249     return true;
ecf295 6250   };
A 6251
6252   // unselect directory/group
6253   this.unselect_directory = function()
6254   {
f8e48d 6255     this.select_folder('');
A 6256     this.enable_command('search-delete', false);
6257   };
6258
6259   // callback for creating a new saved search record
6260   this.insert_saved_search = function(name, id)
6261   {
6262     var key = 'S'+id,
6263       link = $('<a>').attr('href', '#')
6264         .attr('rel', id)
f1aaca 6265         .click(function() { return ref.command('listsearch', id, this); })
f8e48d 6266         .html(name),
344943 6267       prop = { name:name, id:id };
f8e48d 6268
71a522 6269     this.savedsearchlist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch');
3c309a 6270     this.select_folder(key,'',true);
f8e48d 6271     this.enable_command('search-delete', true);
A 6272     this.env.search_id = id;
6273
6274     this.triggerEvent('abook_search_insert', prop);
6275   };
6276
6c5c22 6277   // creates a dialog for saved search
f8e48d 6278   this.search_create = function()
A 6279   {
6c5c22 6280     var input = $('<input>').attr('type', 'text'),
AM 6281       content = $('<label>').text(this.get_label('namex')).append(input);
6282
6283     this.show_popup_dialog(content, this.get_label('searchsave'),
6284       [{
6285         text: this.get_label('save'),
630d08 6286         'class': 'mainaction',
6c5c22 6287         click: function() {
AM 6288           var name;
6289
6290           if (name = input.val()) {
6291             ref.http_post('search-create', {_search: ref.env.search_request, _name: name},
6292               ref.set_busy(true, 'loading'));
6293           }
6294
6295           $(this).dialog('close');
6296         }
6297       }]
6298     );
f8e48d 6299   };
A 6300
6301   this.search_delete = function()
6302   {
6303     if (this.env.search_request) {
6304       var lock = this.set_busy(true, 'savedsearchdeleting');
c31360 6305       this.http_post('search-delete', {_sid: this.env.search_id}, lock);
f8e48d 6306     }
A 6307   };
6308
6309   // callback from server upon search-delete command
6310   this.remove_search_item = function(id)
6311   {
6312     var li, key = 'S'+id;
71a522 6313     if (this.savedsearchlist.remove(key)) {
f8e48d 6314       this.triggerEvent('search_delete', { id:id, li:li });
A 6315     }
6316
6317     this.env.search_id = null;
6318     this.env.search_request = null;
6319     this.list_contacts_clear();
6320     this.reset_qsearch();
6321     this.enable_command('search-delete', 'search-create', false);
6322   };
6323
6324   this.listsearch = function(id)
6325   {
70da8c 6326     var lock = this.set_busy(true, 'searching');
f8e48d 6327
A 6328     if (this.contact_list) {
6329       this.list_contacts_clear();
6330     }
6331
6332     this.reset_qsearch();
71a522 6333
TB 6334     if (this.savedsearchlist) {
6335       this.treelist.select('');
6336       this.savedsearchlist.select('S'+id);
6337     }
6338     else
6339       this.select_folder('S'+id, '', true);
f8e48d 6340
A 6341     // reset vars
6342     this.env.current_page = 1;
c31360 6343     this.http_request('search', {_sid: id}, lock);
e9a9f2 6344   };
A 6345
0501b6 6346
4e17e6 6347   /*********************************************************/
T 6348   /*********        user settings methods          *********/
6349   /*********************************************************/
6350
f05834 6351   // preferences section select and load options frame
A 6352   this.section_select = function(list)
8fa922 6353   {
24fa5d 6354     var win, id = list.get_single_selection(), target = window,
c31360 6355       url = {_action: 'edit-prefs', _section: id};
8fa922 6356
f05834 6357     if (id) {
24fa5d 6358       if (win = this.get_frame_window(this.env.contentframe)) {
c31360 6359         url._framed = 1;
24fa5d 6360         target = win;
f05834 6361       }
c31360 6362       this.location_href(url, target, true);
8fa922 6363     }
f05834 6364
A 6365     return true;
8fa922 6366   };
f05834 6367
6b47de 6368   this.identity_select = function(list)
8fa922 6369   {
6b47de 6370     var id;
223ae9 6371     if (id = list.get_single_selection()) {
A 6372       this.enable_command('delete', list.rowcount > 1 && this.env.identities_level < 2);
6b47de 6373       this.load_identity(id, 'edit-identity');
223ae9 6374     }
8fa922 6375   };
4e17e6 6376
e83f03 6377   // load identity record
4e17e6 6378   this.load_identity = function(id, action)
8fa922 6379   {
223ae9 6380     if (action == 'edit-identity' && (!id || id == this.env.iid))
1c5853 6381       return false;
4e17e6 6382
24fa5d 6383     var win, target = window,
c31360 6384       url = {_action: action, _iid: id};
8fa922 6385
24fa5d 6386     if (win = this.get_frame_window(this.env.contentframe)) {
c31360 6387       url._framed = 1;
24fa5d 6388       target = win;
8fa922 6389     }
4e17e6 6390
b82fcc 6391     if (id || action == 'add-identity') {
AM 6392       this.location_href(url, target, true);
8fa922 6393     }
A 6394
1c5853 6395     return true;
8fa922 6396   };
4e17e6 6397
T 6398   this.delete_identity = function(id)
8fa922 6399   {
223ae9 6400     // exit if no identity is specified or if selection is empty
6b47de 6401     var selection = this.identity_list.get_selection();
T 6402     if (!(selection.length || this.env.iid))
4e17e6 6403       return;
8fa922 6404
4e17e6 6405     if (!id)
6b47de 6406       id = this.env.iid ? this.env.iid : selection[0];
4e17e6 6407
7c2a93 6408     // submit request with appended token
ca01e2 6409     if (id && confirm(this.get_label('deleteidentityconfirm')))
AM 6410       this.http_post('settings/delete-identity', { _iid: id }, true);
254d5e 6411   };
06c990 6412
7c2a93 6413   this.update_identity_row = function(id, name, add)
T 6414   {
517dae 6415     var list = this.identity_list,
7c2a93 6416       rid = this.html_identifier(id);
T 6417
517dae 6418     if (add) {
TB 6419       list.insert_row({ id:'rcmrow'+rid, cols:[ { className:'mail', innerHTML:name } ] });
7c2a93 6420       list.select(rid);
517dae 6421     }
TB 6422     else {
6423       list.update_row(rid, [ name ]);
7c2a93 6424     }
T 6425   };
254d5e 6426
0ce212 6427   this.update_response_row = function(response, oldkey)
TB 6428   {
6429     var list = this.responses_list;
6430
6431     if (list && oldkey) {
6432       list.update_row(oldkey, [ response.name ], response.key, true);
6433     }
6434     else if (list) {
6435       list.insert_row({ id:'rcmrow'+response.key, cols:[ { className:'name', innerHTML:response.name } ] });
6436       list.select(response.key);
6437     }
6438   };
6439
6440   this.remove_response = function(key)
6441   {
6442     var frame;
6443
6444     if (this.env.textresponses) {
6445       delete this.env.textresponses[key];
6446     }
6447
6448     if (this.responses_list) {
6449       this.responses_list.remove_row(key);
6450       if (this.env.contentframe && (frame = this.get_frame_window(this.env.contentframe))) {
6451         frame.location.href = this.env.blankpage;
6452       }
6453     }
911d4e 6454
AM 6455     this.enable_command('delete', false);
0ce212 6456   };
TB 6457
ca01e2 6458   this.remove_identity = function(id)
AM 6459   {
6460     var frame, list = this.identity_list,
6461       rid = this.html_identifier(id);
6462
6463     if (list && id) {
6464       list.remove_row(rid);
6465       if (this.env.contentframe && (frame = this.get_frame_window(this.env.contentframe))) {
6466         frame.location.href = this.env.blankpage;
6467       }
6468     }
911d4e 6469
AM 6470     this.enable_command('delete', false);
ca01e2 6471   };
AM 6472
254d5e 6473
A 6474   /*********************************************************/
6475   /*********        folder manager methods         *********/
6476   /*********************************************************/
6477
6478   this.init_subscription_list = function()
6479   {
2611ac 6480     var delim = RegExp.escape(this.env.delimiter);
04fbc5 6481
AM 6482     this.last_sub_rx = RegExp('['+delim+']?[^'+delim+']+$');
6483
c6447e 6484     this.subscription_list = new rcube_treelist_widget(this.gui_objects.subscriptionlist, {
3cb61e 6485         selectable: true,
48e340 6486         tabexit: false,
3fb36a 6487         parent_focus: true,
3cb61e 6488         id_prefix: 'rcmli',
AM 6489         id_encode: this.html_identifier_encode,
66233b 6490         id_decode: this.html_identifier_decode,
AM 6491         searchbox: '#foldersearch'
c6447e 6492     });
AM 6493
772bec 6494     this.subscription_list
c6447e 6495       .addEventListener('select', function(node) { ref.subscription_select(node.id); })
3cb61e 6496       .addEventListener('collapse', function(node) { ref.folder_collapsed(node) })
AM 6497       .addEventListener('expand', function(node) { ref.folder_collapsed(node) })
66233b 6498       .addEventListener('search', function(p) { if (p.query) ref.subscription_select(); })
3cb61e 6499       .draggable({cancel: 'li.mailbox.root'})
c6447e 6500       .droppable({
AM 6501         // @todo: find better way, accept callback is executed for every folder
6502         // on the list when dragging starts (and stops), this is slow, but
6503         // I didn't find a method to check droptarget on over event
6504         accept: function(node) {
a109d1 6505           if (!$(node).is('.mailbox'))
AM 6506             return false;
6507
3cb61e 6508           var source_folder = ref.folder_id2name($(node).attr('id')),
AM 6509             dest_folder = ref.folder_id2name(this.id),
6510             source = ref.env.subscriptionrows[source_folder],
6511             dest = ref.env.subscriptionrows[dest_folder];
04fbc5 6512
3cb61e 6513           return source && !source[2]
AM 6514             && dest_folder != source_folder.replace(ref.last_sub_rx, '')
6515             && !dest_folder.startsWith(source_folder + ref.env.delimiter);
c6447e 6516         },
AM 6517         drop: function(e, ui) {
3cb61e 6518           var source = ref.folder_id2name(ui.draggable.attr('id')),
AM 6519             dest = ref.folder_id2name(this.id);
6520
6521           ref.subscription_move_folder(source, dest);
b0dbf3 6522         }
c6447e 6523       });
3cb61e 6524   };
AM 6525
6526   this.folder_id2name = function(id)
6527   {
a109d1 6528     return id ? ref.html_identifier_decode(id.replace(/^rcmli/, '')) : null;
8fa922 6529   };
b0dbf3 6530
c6447e 6531   this.subscription_select = function(id)
8fa922 6532   {
c6447e 6533     var folder;
8fa922 6534
3cb61e 6535     if (id && id != '*' && (folder = this.env.subscriptionrows[id])) {
AM 6536       this.env.mailbox = id;
6537       this.show_folder(id);
af3c04 6538       this.enable_command('delete-folder', !folder[2]);
A 6539     }
6540     else {
6541       this.env.mailbox = null;
6542       this.show_contentframe(false);
6543       this.enable_command('delete-folder', 'purge', false);
6544     }
8fa922 6545   };
b0dbf3 6546
c6447e 6547   this.subscription_move_folder = function(from, to)
8fa922 6548   {
3cb61e 6549     if (from && to !== null && from != to && to != from.replace(this.last_sub_rx, '')) {
AM 6550       var path = from.split(this.env.delimiter),
c6447e 6551         basename = path.pop(),
3cb61e 6552         newname = to === '' || to === '*' ? basename : to + this.env.delimiter + basename;
c6447e 6553
3cb61e 6554       if (newname != from) {
AM 6555         this.http_post('rename-folder', {_folder_oldname: from, _folder_newname: newname},
c6447e 6556           this.set_busy(true, 'foldermoving'));
71cc6b 6557       }
8fa922 6558     }
A 6559   };
4e17e6 6560
24053e 6561   // tell server to create and subscribe a new mailbox
af3c04 6562   this.create_folder = function()
8fa922 6563   {
af3c04 6564     this.show_folder('', this.env.mailbox);
8fa922 6565   };
24053e 6566
T 6567   // delete a specific mailbox with all its messages
af3c04 6568   this.delete_folder = function(name)
8fa922 6569   {
3cb61e 6570     if (!name)
AM 6571       name = this.env.mailbox;
fdbb19 6572
3cb61e 6573     if (name && confirm(this.get_label('deletefolderconfirm'))) {
AM 6574       this.http_post('delete-folder', {_mbox: name}, this.set_busy(true, 'folderdeleting'));
8fa922 6575     }
A 6576   };
24053e 6577
254d5e 6578   // Add folder row to the table and initialize it
3cb61e 6579   this.add_folder_row = function (id, name, display_name, is_protected, subscribed, class_name, refrow, subfolders)
8fa922 6580   {
24053e 6581     if (!this.gui_objects.subscriptionlist)
T 6582       return false;
6583
ef4c47 6584     // reset searching
AM 6585     if (this.subscription_list.is_search()) {
6586       this.subscription_select();
6587       this.subscription_list.reset_search();
6588     }
6589
2c0d3e 6590     // disable drag-n-drop temporarily
AM 6591     this.subscription_list.draggable('destroy').droppable('destroy');
6592
3cb61e 6593     var row, n, tmp, tmp_name, rowid, collator, pos, p, parent = '',
244126 6594       folders = [], list = [], slist = [],
3cb61e 6595       list_element = $(this.gui_objects.subscriptionlist);
AM 6596       row = refrow ? refrow : $($('li', list_element).get(1)).clone(true);
8fa922 6597
3cb61e 6598     if (!row.length) {
24053e 6599       // Refresh page if we don't have a table row to clone
6b47de 6600       this.goto_url('folders');
c5c3ae 6601       return false;
8fa922 6602     }
681a59 6603
1a0343 6604     // set ID, reset css class
3cb61e 6605     row.attr({id: 'rcmli' + this.html_identifier_encode(id), 'class': class_name});
AM 6606
6607     if (!refrow || !refrow.length) {
e9ecd4 6608       // remove old data, subfolders and toggle
3cb61e 6609       $('ul,div.treetoggle', row).remove();
e9ecd4 6610       row.removeData('filtered');
3cb61e 6611     }
24053e 6612
T 6613     // set folder name
3cb61e 6614     $('a:first', row).text(display_name);
8fa922 6615
254d5e 6616     // update subscription checkbox
3cb61e 6617     $('input[name="_subscribed[]"]:first', row).val(id)
8fc0f9 6618       .prop({checked: subscribed ? true : false, disabled: is_protected ? true : false});
c5c3ae 6619
254d5e 6620     // add to folder/row-ID map
302eb2 6621     this.env.subscriptionrows[id] = [name, display_name, false];
254d5e 6622
244126 6623     // copy folders data to an array for sorting
3cb61e 6624     $.each(this.env.subscriptionrows, function(k, v) { v[3] = k; folders.push(v); });
302eb2 6625
244126 6626     try {
AM 6627       // use collator if supported (FF29, IE11, Opera15, Chrome24)
6628       collator = new Intl.Collator(this.env.locale.replace('_', '-'));
6629     }
6630     catch (e) {};
302eb2 6631
244126 6632     // sort folders
5bd871 6633     folders.sort(function(a, b) {
244126 6634       var i, f1, f2,
AM 6635         path1 = a[0].split(ref.env.delimiter),
3cb61e 6636         path2 = b[0].split(ref.env.delimiter),
AM 6637         len = path1.length;
244126 6638
3cb61e 6639       for (i=0; i<len; i++) {
244126 6640         f1 = path1[i];
AM 6641         f2 = path2[i];
6642
6643         if (f1 !== f2) {
3cb61e 6644           if (f2 === undefined)
AM 6645             return 1;
244126 6646           if (collator)
AM 6647             return collator.compare(f1, f2);
6648           else
6649             return f1 < f2 ? -1 : 1;
6650         }
3cb61e 6651         else if (i == len-1) {
AM 6652           return -1
6653         }
244126 6654       }
5bd871 6655     });
71cc6b 6656
254d5e 6657     for (n in folders) {
3cb61e 6658       p = folders[n][3];
254d5e 6659       // protected folder
A 6660       if (folders[n][2]) {
3cb61e 6661         tmp_name = p + this.env.delimiter;
18a3dc 6662         // prefix namespace cannot have subfolders (#1488349)
A 6663         if (tmp_name == this.env.prefix_ns)
6664           continue;
3cb61e 6665         slist.push(p);
18a3dc 6666         tmp = tmp_name;
254d5e 6667       }
A 6668       // protected folder's child
3cb61e 6669       else if (tmp && p.startsWith(tmp))
AM 6670         slist.push(p);
254d5e 6671       // other
A 6672       else {
3cb61e 6673         list.push(p);
254d5e 6674         tmp = null;
A 6675       }
6676     }
71cc6b 6677
T 6678     // check if subfolder of a protected folder
6679     for (n=0; n<slist.length; n++) {
3cb61e 6680       if (id.startsWith(slist[n] + this.env.delimiter))
AM 6681         rowid = slist[n];
71cc6b 6682     }
254d5e 6683
A 6684     // find folder position after sorting
71cc6b 6685     for (n=0; !rowid && n<list.length; n++) {
3cb61e 6686       if (n && list[n] == id)
AM 6687         rowid = list[n-1];
8fa922 6688     }
24053e 6689
254d5e 6690     // add row to the table
3cb61e 6691     if (rowid && (n = this.subscription_list.get_item(rowid, true))) {
AM 6692       // find parent folder
6693       if (pos = id.lastIndexOf(this.env.delimiter)) {
6694         parent = id.substring(0, pos);
6695         parent = this.subscription_list.get_item(parent, true);
6696
6697         // add required tree elements to the parent if not already there
6698         if (!$('div.treetoggle', parent).length) {
6699           $('<div>&nbsp;</div>').addClass('treetoggle collapsed').appendTo(parent);
6700         }
6701         if (!$('ul', parent).length) {
6702           $('<ul>').css('display', 'none').appendTo(parent);
6703         }
6704       }
6705
6706       if (parent && n == parent) {
6707         $('ul:first', parent).append(row);
6708       }
6709       else {
6710         while (p = $(n).parent().parent().get(0)) {
6711           if (parent && p == parent)
6712             break;
6713           if (!$(p).is('li.mailbox'))
6714             break;
6715           n = p;
6716         }
6717
6718         $(n).after(row);
6719       }
6720     }
6721     else {
c6447e 6722       list_element.append(row);
3cb61e 6723     }
AM 6724
6725     // add subfolders
6726     $.extend(this.env.subscriptionrows, subfolders || {});
b0dbf3 6727
254d5e 6728     // update list widget
3cb61e 6729     this.subscription_list.reset(true);
AM 6730     this.subscription_select();
c6447e 6731
3cb61e 6732     // expand parent
AM 6733     if (parent) {
6734       this.subscription_list.expand(this.folder_id2name(parent.id));
6735     }
254d5e 6736
e9ecd4 6737     row = row.show().get(0);
254d5e 6738     if (row.scrollIntoView)
A 6739       row.scrollIntoView();
6740
6741     return row;
8fa922 6742   };
24053e 6743
254d5e 6744   // replace an existing table row with a new folder line (with subfolders)
3cb61e 6745   this.replace_folder_row = function(oldid, id, name, display_name, is_protected, class_name)
8fa922 6746   {
7c28d4 6747     if (!this.gui_objects.subscriptionlist) {
9e9dcc 6748       if (this.is_framed()) {
AM 6749         // @FIXME: for some reason this 'parent' variable need to be prefixed with 'window.'
6750         return window.parent.rcmail.replace_folder_row(oldid, id, name, display_name, is_protected, class_name);
6751       }
c6447e 6752
254d5e 6753       return false;
7c28d4 6754     }
8fa922 6755
ef4c47 6756     // reset searching
AM 6757     if (this.subscription_list.is_search()) {
6758       this.subscription_select();
6759       this.subscription_list.reset_search();
6760     }
6761
3cb61e 6762     var subfolders = {},
AM 6763       row = this.subscription_list.get_item(oldid, true),
6764       parent = $(row).parent(),
6765       old_folder = this.env.subscriptionrows[oldid],
6766       prefix_len_id = oldid.length,
6767       prefix_len_name = old_folder[0].length,
6768       subscribed = $('input[name="_subscribed[]"]:first', row).prop('checked');
254d5e 6769
7c28d4 6770     // no renaming, only update class_name
3cb61e 6771     if (oldid == id) {
AM 6772       $(row).attr('class', class_name || '');
7c28d4 6773       return;
TB 6774     }
6775
3cb61e 6776     // update subfolders
AM 6777     $('li', row).each(function() {
6778       var fname = ref.folder_id2name(this.id),
6779         folder = ref.env.subscriptionrows[fname],
6780         newid = id + fname.slice(prefix_len_id);
254d5e 6781
3cb61e 6782       this.id = 'rcmli' + ref.html_identifier_encode(newid);
AM 6783       $('input[name="_subscribed[]"]:first', this).val(newid);
6784       folder[0] = name + folder[0].slice(prefix_len_name);
6785
6786       subfolders[newid] = folder;
6787       delete ref.env.subscriptionrows[fname];
6788     });
6789
6790     // get row off the list
6791     row = $(row).detach();
6792
6793     delete this.env.subscriptionrows[oldid];
6794
6795     // remove parent list/toggle elements if not needed
6796     if (parent.get(0) != this.gui_objects.subscriptionlist && !$('li', parent).length) {
6797       $('ul,div.treetoggle', parent.parent()).remove();
254d5e 6798     }
A 6799
3cb61e 6800     // move the existing table row
AM 6801     this.add_folder_row(id, name, display_name, is_protected, subscribed, class_name, row, subfolders);
8fa922 6802   };
24053e 6803
T 6804   // remove the table row of a specific mailbox from the table
3cb61e 6805   this.remove_folder_row = function(folder)
8fa922 6806   {
ef4c47 6807     // reset searching
AM 6808     if (this.subscription_list.is_search()) {
6809       this.subscription_select();
6810       this.subscription_list.reset_search();
6811     }
6812
3cb61e 6813     var list = [], row = this.subscription_list.get_item(folder, true);
8fa922 6814
254d5e 6815     // get subfolders if any
3cb61e 6816     $('li', row).each(function() { list.push(ref.folder_id2name(this.id)); });
254d5e 6817
3cb61e 6818     // remove folder row (and subfolders)
AM 6819     this.subscription_list.remove(folder);
254d5e 6820
3cb61e 6821     // update local list variable
AM 6822     list.push(folder);
6823     $.each(list, function(i, v) { delete ref.env.subscriptionrows[v]; });
70da8c 6824   };
4e17e6 6825
edfe91 6826   this.subscribe = function(folder)
8fa922 6827   {
af3c04 6828     if (folder) {
5be0d0 6829       var lock = this.display_message(this.get_label('foldersubscribing'), 'loading');
c31360 6830       this.http_post('subscribe', {_mbox: folder}, lock);
af3c04 6831     }
8fa922 6832   };
4e17e6 6833
edfe91 6834   this.unsubscribe = function(folder)
8fa922 6835   {
af3c04 6836     if (folder) {
5be0d0 6837       var lock = this.display_message(this.get_label('folderunsubscribing'), 'loading');
c31360 6838       this.http_post('unsubscribe', {_mbox: folder}, lock);
af3c04 6839     }
8fa922 6840   };
9bebdf 6841
af3c04 6842   // when user select a folder in manager
A 6843   this.show_folder = function(folder, path, force)
6844   {
24fa5d 6845     var win, target = window,
af3c04 6846       url = '&_action=edit-folder&_mbox='+urlencode(folder);
A 6847
6848     if (path)
6849       url += '&_path='+urlencode(path);
6850
24fa5d 6851     if (win = this.get_frame_window(this.env.contentframe)) {
AM 6852       target = win;
af3c04 6853       url += '&_framed=1';
A 6854     }
6855
c31360 6856     if (String(target.location.href).indexOf(url) >= 0 && !force)
af3c04 6857       this.show_contentframe(true);
c31360 6858     else
dc0be3 6859       this.location_href(this.env.comm_path+url, target, true);
af3c04 6860   };
A 6861
e81a30 6862   // disables subscription checkbox (for protected folder)
A 6863   this.disable_subscription = function(folder)
6864   {
3cb61e 6865     var row = this.subscription_list.get_item(folder, true);
AM 6866     if (row)
6867       $('input[name="_subscribed[]"]:first', row).prop('disabled', true);
e81a30 6868   };
A 6869
af3c04 6870   this.folder_size = function(folder)
A 6871   {
6872     var lock = this.set_busy(true, 'loading');
c31360 6873     this.http_post('folder-size', {_mbox: folder}, lock);
af3c04 6874   };
A 6875
6876   this.folder_size_update = function(size)
6877   {
6878     $('#folder-size').replaceWith(size);
6879   };
6880
e9ecd4 6881   // filter folders by namespace
AM 6882   this.folder_filter = function(prefix)
6883   {
6884     this.subscription_list.reset_search();
6885
6886     this.subscription_list.container.children('li').each(function() {
6887       var i, folder = ref.folder_id2name(this.id);
6888       // show all folders
6889       if (prefix == '---') {
6890       }
6891       // got namespace prefix
6892       else if (prefix) {
6893         if (folder !== prefix) {
6894           $(this).data('filtered', true).hide();
6895           return
6896         }
6897       }
6898       // no namespace prefix, filter out all other namespaces
6899       else {
6900         // first get all namespace roots
6901         for (i in ref.env.ns_roots) {
6902           if (folder === ref.env.ns_roots[i]) {
6903             $(this).data('filtered', true).hide();
6904             return;
6905           }
6906         }
6907       }
6908
6909       $(this).removeData('filtered').show();
6910     });
6911   };
4e17e6 6912
T 6913   /*********************************************************/
6914   /*********           GUI functionality           *********/
6915   /*********************************************************/
6916
e639c5 6917   var init_button = function(cmd, prop)
T 6918   {
6919     var elm = document.getElementById(prop.id);
6920     if (!elm)
6921       return;
6922
6923     var preload = false;
6924     if (prop.type == 'image') {
6925       elm = elm.parentNode;
6926       preload = true;
6927     }
6928
6929     elm._command = cmd;
6930     elm._id = prop.id;
6931     if (prop.sel) {
f1aaca 6932       elm.onmousedown = function(e) { return ref.button_sel(this._command, this._id); };
AM 6933       elm.onmouseup = function(e) { return ref.button_out(this._command, this._id); };
e639c5 6934       if (preload)
T 6935         new Image().src = prop.sel;
6936     }
6937     if (prop.over) {
f1aaca 6938       elm.onmouseover = function(e) { return ref.button_over(this._command, this._id); };
AM 6939       elm.onmouseout = function(e) { return ref.button_out(this._command, this._id); };
e639c5 6940       if (preload)
T 6941         new Image().src = prop.over;
6942     }
6943   };
6944
29f977 6945   // set event handlers on registered buttons
T 6946   this.init_buttons = function()
6947   {
6948     for (var cmd in this.buttons) {
d8cf6d 6949       if (typeof cmd !== 'string')
29f977 6950         continue;
8fa922 6951
ab8fda 6952       for (var i=0; i<this.buttons[cmd].length; i++) {
e639c5 6953         init_button(cmd, this.buttons[cmd][i]);
29f977 6954       }
4e17e6 6955     }
29f977 6956   };
4e17e6 6957
T 6958   // set button to a specific state
6959   this.set_button = function(command, state)
8fa922 6960   {
ea0866 6961     var n, button, obj, $obj, a_buttons = this.buttons[command],
249815 6962       len = a_buttons ? a_buttons.length : 0;
4e17e6 6963
249815 6964     for (n=0; n<len; n++) {
4e17e6 6965       button = a_buttons[n];
T 6966       obj = document.getElementById(button.id);
6967
a7dad4 6968       if (!obj || button.status === state)
ab8fda 6969         continue;
AM 6970
4e17e6 6971       // get default/passive setting of the button
ab8fda 6972       if (button.type == 'image' && !button.status) {
4e17e6 6973         button.pas = obj._original_src ? obj._original_src : obj.src;
104ee3 6974         // respect PNG fix on IE browsers
T 6975         if (obj.runtimeStyle && obj.runtimeStyle.filter && obj.runtimeStyle.filter.match(/src=['"]([^'"]+)['"]/))
6976           button.pas = RegExp.$1;
6977       }
ab8fda 6978       else if (!button.status)
4e17e6 6979         button.pas = String(obj.className);
T 6980
a7dad4 6981       button.status = state;
AM 6982
4e17e6 6983       // set image according to button state
ab8fda 6984       if (button.type == 'image' && button[state]) {
4e17e6 6985         obj.src = button[state];
8fa922 6986       }
4e17e6 6987       // set class name according to button state
ab8fda 6988       else if (button[state] !== undefined) {
c833ed 6989         obj.className = button[state];
8fa922 6990       }
4e17e6 6991       // disable/enable input buttons
ab8fda 6992       if (button.type == 'input') {
34ddfc 6993         obj.disabled = state == 'pas';
TB 6994       }
6995       else if (button.type == 'uibutton') {
e8bcf0 6996         button.status = state;
34ddfc 6997         $(obj).button('option', 'disabled', state == 'pas');
e8bcf0 6998       }
TB 6999       else {
ea0866 7000         $obj = $(obj);
TB 7001         $obj
7002           .attr('tabindex', state == 'pas' || state == 'sel' ? '-1' : ($obj.attr('data-tabindex') || '0'))
e8bcf0 7003           .attr('aria-disabled', state == 'pas' || state == 'sel' ? 'true' : 'false');
4e17e6 7004       }
8fa922 7005     }
A 7006   };
4e17e6 7007
eb6842 7008   // display a specific alttext
T 7009   this.set_alttext = function(command, label)
8fa922 7010   {
249815 7011     var n, button, obj, link, a_buttons = this.buttons[command],
A 7012       len = a_buttons ? a_buttons.length : 0;
8fa922 7013
249815 7014     for (n=0; n<len; n++) {
A 7015       button = a_buttons[n];
8fa922 7016       obj = document.getElementById(button.id);
A 7017
249815 7018       if (button.type == 'image' && obj) {
8fa922 7019         obj.setAttribute('alt', this.get_label(label));
A 7020         if ((link = obj.parentNode) && link.tagName.toLowerCase() == 'a')
7021           link.setAttribute('title', this.get_label(label));
eb6842 7022       }
8fa922 7023       else if (obj)
A 7024         obj.setAttribute('title', this.get_label(label));
7025     }
7026   };
4e17e6 7027
T 7028   // mouse over button
7029   this.button_over = function(command, id)
356a67 7030   {
04fbc5 7031     this.button_event(command, id, 'over');
356a67 7032   };
4e17e6 7033
c8c1e0 7034   // mouse down on button
S 7035   this.button_sel = function(command, id)
356a67 7036   {
04fbc5 7037     this.button_event(command, id, 'sel');
356a67 7038   };
4e17e6 7039
T 7040   // mouse out of button
7041   this.button_out = function(command, id)
04fbc5 7042   {
AM 7043     this.button_event(command, id, 'act');
7044   };
7045
7046   // event of button
7047   this.button_event = function(command, id, event)
356a67 7048   {
249815 7049     var n, button, obj, a_buttons = this.buttons[command],
A 7050       len = a_buttons ? a_buttons.length : 0;
4e17e6 7051
249815 7052     for (n=0; n<len; n++) {
4e17e6 7053       button = a_buttons[n];
8fa922 7054       if (button.id == id && button.status == 'act') {
04fbc5 7055         if (button[event] && (obj = document.getElementById(button.id))) {
AM 7056           obj[button.type == 'image' ? 'src' : 'className'] = button[event];
7057         }
7058
7059         if (event == 'sel') {
7060           this.buttons_sel[id] = command;
4e17e6 7061         }
T 7062       }
356a67 7063     }
0501b6 7064   };
7f5a84 7065
5eee00 7066   // write to the document/window title
T 7067   this.set_pagetitle = function(title)
7068   {
7069     if (title && document.title)
7070       document.title = title;
8fa922 7071   };
5eee00 7072
ad334a 7073   // display a system message, list of types in common.css (below #message definition)
0b36d1 7074   this.display_message = function(msg, type, timeout, key)
8fa922 7075   {
b716bd 7076     // pass command to parent window
27acfd 7077     if (this.is_framed())
7f5a84 7078       return parent.rcmail.display_message(msg, type, timeout);
b716bd 7079
ad334a 7080     if (!this.gui_objects.message) {
A 7081       // save message in order to display after page loaded
7082       if (type != 'loading')
0b36d1 7083         this.pending_message = [msg, type, timeout, key];
a8f496 7084       return 1;
ad334a 7085     }
f9c107 7086
0b36d1 7087     if (!type)
AM 7088       type = 'notice';
8fa922 7089
0b36d1 7090     if (!key)
AM 7091       key = this.html_identifier(msg);
7092
7093     var date = new Date(),
7f5a84 7094       id = type + date.getTime();
A 7095
0b36d1 7096     if (!timeout) {
AM 7097       switch (type) {
7098         case 'error':
7099         case 'warning':
7100           timeout = this.message_time * 2;
7101           break;
7102
7103         case 'uploading':
7104           timeout = 0;
7105           break;
7106
7107         default:
7108           timeout = this.message_time;
7109       }
7110     }
7f5a84 7111
ef292e 7112     if (type == 'loading') {
T 7113       key = 'loading';
7114       timeout = this.env.request_timeout * 1000;
7115       if (!msg)
7116         msg = this.get_label('loading');
7117     }
29b397 7118
b37e69 7119     // The same message is already displayed
ef292e 7120     if (this.messages[key]) {
57e38f 7121       // replace label
ef292e 7122       if (this.messages[key].obj)
T 7123         this.messages[key].obj.html(msg);
57e38f 7124       // store label in stack
A 7125       if (type == 'loading') {
7126         this.messages[key].labels.push({'id': id, 'msg': msg});
7127       }
7128       // add element and set timeout
ef292e 7129       this.messages[key].elements.push(id);
da5cad 7130       setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
b37e69 7131       return id;
ad334a 7132     }
8fa922 7133
57e38f 7134     // create DOM object and display it
A 7135     var obj = $('<div>').addClass(type).html(msg).data('key', key),
7136       cont = $(this.gui_objects.message).append(obj).show();
7137
7138     this.messages[key] = {'obj': obj, 'elements': [id]};
8fa922 7139
ad334a 7140     if (type == 'loading') {
57e38f 7141       this.messages[key].labels = [{'id': id, 'msg': msg}];
ad334a 7142     }
0b36d1 7143     else if (type != 'uploading') {
a539ce 7144       obj.click(function() { return ref.hide_message(obj); })
TB 7145         .attr('role', 'alert');
70cfb4 7146     }
57e38f 7147
0e530b 7148     this.triggerEvent('message', { message:msg, type:type, timeout:timeout, object:obj });
T 7149
fcc7f8 7150     if (timeout > 0)
34003c 7151       setTimeout(function() { ref.hide_message(id, type != 'loading'); }, timeout);
0b36d1 7152
57e38f 7153     return id;
8fa922 7154   };
4e17e6 7155
ad334a 7156   // make a message to disapear
A 7157   this.hide_message = function(obj, fade)
554d79 7158   {
ad334a 7159     // pass command to parent window
27acfd 7160     if (this.is_framed())
ad334a 7161       return parent.rcmail.hide_message(obj, fade);
A 7162
a8f496 7163     if (!this.gui_objects.message)
TB 7164       return;
7165
ffc2d0 7166     var k, n, i, o, m = this.messages;
57e38f 7167
A 7168     // Hide message by object, don't use for 'loading'!
d8cf6d 7169     if (typeof obj === 'object') {
ffc2d0 7170       o = $(obj);
AM 7171       k = o.data('key');
7172       this.hide_message_object(o, fade);
7173       if (m[k])
7174         delete m[k];
ad334a 7175     }
57e38f 7176     // Hide message by id
ad334a 7177     else {
ee72e4 7178       for (k in m) {
A 7179         for (n in m[k].elements) {
7180           if (m[k] && m[k].elements[n] == obj) {
7181             m[k].elements.splice(n, 1);
57e38f 7182             // hide DOM element if last instance is removed
ee72e4 7183             if (!m[k].elements.length) {
ffc2d0 7184               this.hide_message_object(m[k].obj, fade);
ee72e4 7185               delete m[k];
ad334a 7186             }
57e38f 7187             // set pending action label for 'loading' message
A 7188             else if (k == 'loading') {
7189               for (i in m[k].labels) {
7190                 if (m[k].labels[i].id == obj) {
7191                   delete m[k].labels[i];
7192                 }
7193                 else {
77f9a4 7194                   o = m[k].labels[i].msg;
AM 7195                   m[k].obj.html(o);
57e38f 7196                 }
A 7197               }
7198             }
ad334a 7199           }
A 7200         }
7201       }
7202     }
554d79 7203   };
A 7204
ffc2d0 7205   // hide message object and remove from the DOM
AM 7206   this.hide_message_object = function(o, fade)
7207   {
7208     if (fade)
7209       o.fadeOut(600, function() {$(this).remove(); });
7210     else
7211       o.hide().remove();
7212   };
7213
54dfd1 7214   // remove all messages immediately
A 7215   this.clear_messages = function()
7216   {
7217     // pass command to parent window
7218     if (this.is_framed())
7219       return parent.rcmail.clear_messages();
7220
7221     var k, n, m = this.messages;
7222
7223     for (k in m)
7224       for (n in m[k].elements)
7225         if (m[k].obj)
ffc2d0 7226           this.hide_message_object(m[k].obj);
54dfd1 7227
A 7228     this.messages = {};
7229   };
7230
0b36d1 7231   // display uploading message with progress indicator
AM 7232   // data should contain: name, total, current, percent, text
7233   this.display_progress = function(data)
7234   {
7235     if (!data || !data.name)
7236       return;
7237
7238     var msg = this.messages['progress' + data.name];
7239
7240     if (!data.label)
7241       data.label = this.get_label('uploadingmany');
7242
7243     if (!msg) {
7244       if (!data.percent || data.percent < 100)
7245         this.display_message(data.label, 'uploading', 0, 'progress' + data.name);
7246       return;
7247     }
7248
7249     if (!data.total || data.percent >= 100) {
7250       this.hide_message(msg.obj);
7251       return;
7252     }
7253
7254     if (data.text)
7255       data.label += ' ' + data.text;
7256
7257     msg.obj.text(data.label);
7258   };
7259
765ecb 7260   // open a jquery UI dialog with the given content
6c5c22 7261   this.show_popup_dialog = function(content, title, buttons, options)
765ecb 7262   {
TB 7263     // forward call to parent window
7264     if (this.is_framed()) {
6c5c22 7265       return parent.rcmail.show_popup_dialog(content, title, buttons, options);
765ecb 7266     }
TB 7267
6c5c22 7268     var popup = $('<div class="popup">');
AM 7269
7270     if (typeof content == 'object')
7271       popup.append(content);
7272     else
31b023 7273       popup.html(content);
6c5c22 7274
79e92d 7275     options = $.extend({
765ecb 7276         title: title,
c8bc8c 7277         buttons: buttons,
765ecb 7278         modal: true,
TB 7279         resizable: true,
c8bc8c 7280         width: 500,
6c5c22 7281         close: function(event, ui) { $(this).remove(); }
79e92d 7282       }, options || {});
AM 7283
7284     popup.dialog(options);
765ecb 7285
c8bc8c 7286     // resize and center popup
AM 7287     var win = $(window), w = win.width(), h = win.height(),
7288       width = popup.width(), height = popup.height();
7289
7290     popup.dialog('option', {
7291       height: Math.min(h - 40, height + 75 + (buttons ? 50 : 0)),
f14784 7292       width: Math.min(w - 20, width + 36)
c8bc8c 7293     });
6abdff 7294
630d08 7295     // assign special classes to dialog buttons
AM 7296     $.each(options.button_classes || [], function(i, v) {
7297       if (v) $($('.ui-dialog-buttonpane button.ui-button', popup.parent()).get(i)).addClass(v);
7298     });
7299
6abdff 7300     return popup;
765ecb 7301   };
TB 7302
ab8fda 7303   // enable/disable buttons for page shifting
AM 7304   this.set_page_buttons = function()
7305   {
04fbc5 7306     this.enable_command('nextpage', 'lastpage', this.env.pagecount > this.env.current_page);
AM 7307     this.enable_command('previouspage', 'firstpage', this.env.current_page > 1);
9a5d9a 7308
AM 7309     this.update_pagejumper();
ab8fda 7310   };
AM 7311
4e17e6 7312   // mark a mailbox as selected and set environment variable
fb6d86 7313   this.select_folder = function(name, prefix, encode)
f11541 7314   {
71a522 7315     if (this.savedsearchlist) {
TB 7316       this.savedsearchlist.select('');
7317     }
7318
344943 7319     if (this.treelist) {
TB 7320       this.treelist.select(name);
7321     }
7322     else if (this.gui_objects.folderlist) {
f5de03 7323       $('li.selected', this.gui_objects.folderlist).removeClass('selected');
TB 7324       $(this.get_folder_li(name, prefix, encode)).addClass('selected');
8fa922 7325
99d866 7326       // trigger event hook
f8e48d 7327       this.triggerEvent('selectfolder', { folder:name, prefix:prefix });
f11541 7328     }
T 7329   };
7330
636bd7 7331   // adds a class to selected folder
A 7332   this.mark_folder = function(name, class_name, prefix, encode)
7333   {
7334     $(this.get_folder_li(name, prefix, encode)).addClass(class_name);
a62c73 7335     this.triggerEvent('markfolder', {folder: name, mark: class_name, status: true});
636bd7 7336   };
A 7337
7338   // adds a class to selected folder
7339   this.unmark_folder = function(name, class_name, prefix, encode)
7340   {
7341     $(this.get_folder_li(name, prefix, encode)).removeClass(class_name);
a62c73 7342     this.triggerEvent('markfolder', {folder: name, mark: class_name, status: false});
636bd7 7343   };
A 7344
f11541 7345   // helper method to find a folder list item
fb6d86 7346   this.get_folder_li = function(name, prefix, encode)
f11541 7347   {
a61bbb 7348     if (!prefix)
T 7349       prefix = 'rcmli';
8fa922 7350
A 7351     if (this.gui_objects.folderlist) {
fb6d86 7352       name = this.html_identifier(name, encode);
a61bbb 7353       return document.getElementById(prefix+name);
f11541 7354     }
T 7355   };
24053e 7356
f52c93 7357   // for reordering column array (Konqueror workaround)
T 7358   // and for setting some message list global variables
c83535 7359   this.set_message_coltypes = function(listcols, repl, smart_col)
c3eab2 7360   {
5b67d3 7361     var list = this.message_list,
517dae 7362       thead = list ? list.thead : null,
c83535 7363       repl, cell, col, n, len, tr;
8fa922 7364
c83535 7365     this.env.listcols = listcols;
f52c93 7366
465ba8 7367     if (!this.env.coltypes)
TB 7368       this.env.coltypes = {};
7369
f52c93 7370     // replace old column headers
c3eab2 7371     if (thead) {
A 7372       if (repl) {
c83535 7373         thead.innerHTML = '';
5b67d3 7374         tr = document.createElement('tr');
A 7375
c3eab2 7376         for (c=0, len=repl.length; c < len; c++) {
72afe3 7377           cell = document.createElement('th');
9749da 7378           cell.innerHTML = repl[c].html || '';
c3eab2 7379           if (repl[c].id) cell.id = repl[c].id;
A 7380           if (repl[c].className) cell.className = repl[c].className;
7381           tr.appendChild(cell);
f52c93 7382         }
c83535 7383         thead.appendChild(tr);
c3eab2 7384       }
A 7385
c83535 7386       for (n=0, len=this.env.listcols.length; n<len; n++) {
TB 7387         col = this.env.listcols[n];
e0efd8 7388         if ((cell = thead.rows[0].cells[n]) && (col == 'from' || col == 'to' || col == 'fromto')) {
c83535 7389           $(cell).attr('rel', col).find('span,a').text(this.get_label(col == 'fromto' ? smart_col : col));
c3eab2 7390         }
f52c93 7391       }
T 7392     }
095d05 7393
f52c93 7394     this.env.subject_col = null;
T 7395     this.env.flagged_col = null;
98f2c9 7396     this.env.status_col = null;
c4b819 7397
c83535 7398     if (this.env.coltypes.folder)
TB 7399       this.env.coltypes.folder.hidden = !(this.env.search_request || this.env.search_id) || this.env.search_scope == 'base';
7400
7401     if ((n = $.inArray('subject', this.env.listcols)) >= 0) {
9f07d1 7402       this.env.subject_col = n;
5b67d3 7403       if (list)
A 7404         list.subject_col = n;
8fa922 7405     }
c83535 7406     if ((n = $.inArray('flag', this.env.listcols)) >= 0)
9f07d1 7407       this.env.flagged_col = n;
c83535 7408     if ((n = $.inArray('status', this.env.listcols)) >= 0)
9f07d1 7409       this.env.status_col = n;
b62c48 7410
628706 7411     if (list) {
f5799d 7412       list.hide_column('folder', (this.env.coltypes.folder && this.env.coltypes.folder.hidden) || $.inArray('folder', this.env.listcols) < 0);
5b67d3 7413       list.init_header();
628706 7414     }
f52c93 7415   };
4e17e6 7416
T 7417   // replace content of row count display
bba252 7418   this.set_rowcount = function(text, mbox)
8fa922 7419   {
bba252 7420     // #1487752
A 7421     if (mbox && mbox != this.env.mailbox)
7422       return false;
7423
cc97ea 7424     $(this.gui_objects.countdisplay).html(text);
4e17e6 7425
T 7426     // update page navigation buttons
7427     this.set_page_buttons();
8fa922 7428   };
6d2714 7429
ac5d15 7430   // replace content of mailboxname display
T 7431   this.set_mailboxname = function(content)
8fa922 7432   {
ac5d15 7433     if (this.gui_objects.mailboxname && content)
T 7434       this.gui_objects.mailboxname.innerHTML = content;
8fa922 7435   };
ac5d15 7436
58e360 7437   // replace content of quota display
6d2714 7438   this.set_quota = function(content)
8fa922 7439   {
2c1937 7440     if (this.gui_objects.quotadisplay && content && content.type == 'text')
1187f6 7441       $(this.gui_objects.quotadisplay).text((content.percent||0) + '%').attr('title', content.title);
2c1937 7442
fe1bd5 7443     this.triggerEvent('setquota', content);
2c1937 7444     this.env.quota_content = content;
8fa922 7445   };
6b47de 7446
da5fa2 7447   // update trash folder state
AM 7448   this.set_trash_count = function(count)
7449   {
7450     this[(count ? 'un' : '') + 'mark_folder'](this.env.trash_mailbox, 'empty', '', true);
7451   };
7452
4e17e6 7453   // update the mailboxlist
636bd7 7454   this.set_unread_count = function(mbox, count, set_title, mark)
8fa922 7455   {
4e17e6 7456     if (!this.gui_objects.mailboxlist)
T 7457       return false;
25d8ba 7458
85360d 7459     this.env.unread_counts[mbox] = count;
T 7460     this.set_unread_count_display(mbox, set_title);
636bd7 7461
A 7462     if (mark)
7463       this.mark_folder(mbox, mark, '', true);
d0924d 7464     else if (!count)
A 7465       this.unmark_folder(mbox, 'recent', '', true);
8fa922 7466   };
7f9d71 7467
S 7468   // update the mailbox count display
7469   this.set_unread_count_display = function(mbox, set_title)
8fa922 7470   {
de06fc 7471     var reg, link, text_obj, item, mycount, childcount, div;
dbd069 7472
fb6d86 7473     if (item = this.get_folder_li(mbox, '', true)) {
07d367 7474       mycount = this.env.unread_counts[mbox] ? this.env.unread_counts[mbox] : 0;
de06fc 7475       link = $(item).children('a').eq(0);
T 7476       text_obj = link.children('span.unreadcount');
7477       if (!text_obj.length && mycount)
7478         text_obj = $('<span>').addClass('unreadcount').appendTo(link);
15a9d1 7479       reg = /\s+\([0-9]+\)$/i;
7f9d71 7480
835a0c 7481       childcount = 0;
S 7482       if ((div = item.getElementsByTagName('div')[0]) &&
8fa922 7483           div.className.match(/collapsed/)) {
7f9d71 7484         // add children's counters
fb6d86 7485         for (var k in this.env.unread_counts)
6a9144 7486           if (k.startsWith(mbox + this.env.delimiter))
85360d 7487             childcount += this.env.unread_counts[k];
8fa922 7488       }
4e17e6 7489
de06fc 7490       if (mycount && text_obj.length)
ce86f0 7491         text_obj.html(this.env.unreadwrap.replace(/%[sd]/, mycount));
de06fc 7492       else if (text_obj.length)
T 7493         text_obj.remove();
25d8ba 7494
7f9d71 7495       // set parent's display
07d367 7496       reg = new RegExp(RegExp.escape(this.env.delimiter) + '[^' + RegExp.escape(this.env.delimiter) + ']+$');
7f9d71 7497       if (mbox.match(reg))
S 7498         this.set_unread_count_display(mbox.replace(reg, ''), false);
7499
15a9d1 7500       // set the right classes
cc97ea 7501       if ((mycount+childcount)>0)
T 7502         $(item).addClass('unread');
7503       else
7504         $(item).removeClass('unread');
8fa922 7505     }
15a9d1 7506
T 7507     // set unread count to window title
01c86f 7508     reg = /^\([0-9]+\)\s+/i;
8fa922 7509     if (set_title && document.title) {
dbd069 7510       var new_title = '',
A 7511         doc_title = String(document.title);
15a9d1 7512
85360d 7513       if (mycount && doc_title.match(reg))
T 7514         new_title = doc_title.replace(reg, '('+mycount+') ');
7515       else if (mycount)
7516         new_title = '('+mycount+') '+doc_title;
15a9d1 7517       else
5eee00 7518         new_title = doc_title.replace(reg, '');
8fa922 7519
5eee00 7520       this.set_pagetitle(new_title);
8fa922 7521     }
A 7522   };
4e17e6 7523
e5686f 7524   // display fetched raw headers
A 7525   this.set_headers = function(content)
cc97ea 7526   {
ad334a 7527     if (this.gui_objects.all_headers_row && this.gui_objects.all_headers_box && content)
cc97ea 7528       $(this.gui_objects.all_headers_box).html(content).show();
T 7529   };
a980cb 7530
e5686f 7531   // display all-headers row and fetch raw message headers
76248c 7532   this.show_headers = function(props, elem)
8fa922 7533   {
e5686f 7534     if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box || !this.env.uid)
A 7535       return;
8fa922 7536
cc97ea 7537     $(elem).removeClass('show-headers').addClass('hide-headers');
T 7538     $(this.gui_objects.all_headers_row).show();
f1aaca 7539     elem.onclick = function() { ref.command('hide-headers', '', elem); };
e5686f 7540
A 7541     // fetch headers only once
8fa922 7542     if (!this.gui_objects.all_headers_box.innerHTML) {
701905 7543       this.http_post('headers', {_uid: this.env.uid, _mbox: this.env.mailbox},
AM 7544         this.display_message(this.get_label('loading'), 'loading')
7545       );
e5686f 7546     }
8fa922 7547   };
e5686f 7548
A 7549   // hide all-headers row
76248c 7550   this.hide_headers = function(props, elem)
8fa922 7551   {
e5686f 7552     if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box)
A 7553       return;
7554
cc97ea 7555     $(elem).removeClass('hide-headers').addClass('show-headers');
T 7556     $(this.gui_objects.all_headers_row).hide();
f1aaca 7557     elem.onclick = function() { ref.command('show-headers', '', elem); };
8fa922 7558   };
e5686f 7559
9a0153 7560   // create folder selector popup, position and display it
6789bf 7561   this.folder_selector = function(event, callback)
9a0153 7562   {
AM 7563     var container = this.folder_selector_element;
7564
7565     if (!container) {
7566       var rows = [],
7567         delim = this.env.delimiter,
6789bf 7568         ul = $('<ul class="toolbarmenu">'),
TB 7569         link = document.createElement('a');
9a0153 7570
AM 7571       container = $('<div id="folder-selector" class="popupmenu"></div>');
7572       link.href = '#';
7573       link.className = 'icon';
7574
7575       // loop over sorted folders list
7576       $.each(this.env.mailboxes_list, function() {
6789bf 7577         var n = 0, s = 0,
9a0153 7578           folder = ref.env.mailboxes[this],
AM 7579           id = folder.id,
6789bf 7580           a = $(link.cloneNode(false)),
TB 7581           row = $('<li>');
9a0153 7582
AM 7583         if (folder.virtual)
6789bf 7584           a.addClass('virtual').attr('aria-disabled', 'true').attr('tabindex', '-1');
TB 7585         else
7586           a.addClass('active').data('id', folder.id);
9a0153 7587
AM 7588         if (folder['class'])
6789bf 7589           a.addClass(folder['class']);
9a0153 7590
AM 7591         // calculate/set indentation level
7592         while ((s = id.indexOf(delim, s)) >= 0) {
7593           n++; s++;
7594         }
6789bf 7595         a.css('padding-left', n ? (n * 16) + 'px' : 0);
9a0153 7596
AM 7597         // add folder name element
6789bf 7598         a.append($('<span>').text(folder.name));
9a0153 7599
6789bf 7600         row.append(a);
9a0153 7601         rows.push(row);
AM 7602       });
7603
7604       ul.append(rows).appendTo(container);
7605
7606       // temporarily show element to calculate its size
7607       container.css({left: '-1000px', top: '-1000px'})
7608         .appendTo($('body')).show();
7609
7610       // set max-height if the list is long
7611       if (rows.length > 10)
6789bf 7612         container.css('max-height', $('li', container)[0].offsetHeight * 10 + 9);
9a0153 7613
6789bf 7614       // register delegate event handler for folder item clicks
TB 7615       container.on('click', 'a.active', function(e){
7616         container.data('callback')($(this).data('id'));
7617         return false;
7618       });
9a0153 7619
AM 7620       this.folder_selector_element = container;
7621     }
7622
6789bf 7623     container.data('callback', callback);
9a0153 7624
6789bf 7625     // position menu on the screen
TB 7626     this.show_menu('folder-selector', true, event);
9a0153 7627   };
AM 7628
6789bf 7629
TB 7630   /***********************************************/
7631   /*********    popup menu functions     *********/
7632   /***********************************************/
7633
7634   // Show/hide a specific popup menu
7635   this.show_menu = function(prop, show, event)
7636   {
7637     var name = typeof prop == 'object' ? prop.menu : prop,
7638       obj = $('#'+name),
7639       ref = event && event.target ? $(event.target) : $(obj.attr('rel') || '#'+name+'link'),
7640       keyboard = rcube_event.is_keyboard(event),
7641       align = obj.attr('data-align') || '',
7642       stack = false;
7643
f0928e 7644     // find "real" button element
TB 7645     if (ref.get(0).tagName != 'A' && ref.closest('a').length)
7646       ref = ref.closest('a');
7647
6789bf 7648     if (typeof prop == 'string')
TB 7649       prop = { menu:name };
7650
7651     // let plugins or skins provide the menu element
7652     if (!obj.length) {
7653       obj = this.triggerEvent('menu-get', { name:name, props:prop, originalEvent:event });
7654     }
7655
7656     if (!obj || !obj.length) {
7657       // just delegate the action to subscribers
7658       return this.triggerEvent(show === false ? 'menu-close' : 'menu-open', { name:name, props:prop, originalEvent:event });
7659     }
7660
7661     // move element to top for proper absolute positioning
7662     obj.appendTo(document.body);
7663
7664     if (typeof show == 'undefined')
7665       show = obj.is(':visible') ? false : true;
7666
7667     if (show && ref.length) {
7668       var win = $(window),
7669         pos = ref.offset(),
7670         above = align.indexOf('bottom') >= 0;
7671
7672       stack = ref.attr('role') == 'menuitem' || ref.closest('[role=menuitem]').length > 0;
7673
7674       ref.offsetWidth = ref.outerWidth();
7675       ref.offsetHeight = ref.outerHeight();
7676       if (!above && pos.top + ref.offsetHeight + obj.height() > win.height()) {
7677         above = true;
7678       }
7679       if (align.indexOf('right') >= 0) {
7680         pos.left = pos.left + ref.outerWidth() - obj.width();
7681       }
7682       else if (stack) {
7683         pos.left = pos.left + ref.offsetWidth - 5;
7684         pos.top -= ref.offsetHeight;
7685       }
7686       if (pos.left + obj.width() > win.width()) {
7687         pos.left = win.width() - obj.width() - 12;
7688       }
7689       pos.top = Math.max(0, pos.top + (above ? -obj.height() : ref.offsetHeight));
7690       obj.css({ left:pos.left+'px', top:pos.top+'px' });
7691     }
7692
7693     // add menu to stack
7694     if (show) {
7695       // truncate stack down to the one containing the ref link
7696       for (var i = this.menu_stack.length - 1; stack && i >= 0; i--) {
741789 7697         if (!$(ref).parents('#'+this.menu_stack[i]).length && $(event.target).parent().attr('role') != 'menuitem')
3ef97f 7698           this.hide_menu(this.menu_stack[i], event);
6789bf 7699       }
TB 7700       if (stack && this.menu_stack.length) {
f5de03 7701         obj.data('parent', $.last(this.menu_stack));
TB 7702         obj.css('z-index', ($('#'+$.last(this.menu_stack)).css('z-index') || 0) + 1);
6789bf 7703       }
TB 7704       else if (!stack && this.menu_stack.length) {
7705         this.hide_menu(this.menu_stack[0], event);
7706       }
7707
a2f8fa 7708       obj.show().attr('aria-hidden', 'false').data('opener', ref.attr('aria-expanded', 'true').get(0));
6789bf 7709       this.triggerEvent('menu-open', { name:name, obj:obj, props:prop, originalEvent:event });
TB 7710       this.menu_stack.push(name);
7711
7712       this.menu_keyboard_active = show && keyboard;
7713       if (this.menu_keyboard_active) {
7714         this.focused_menu = name;
7715         obj.find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus();
7716       }
7717     }
7718     else {  // close menu
7719       this.hide_menu(name, event);
7720     }
7721
7722     return show;
7723   };
7724
7725   // hide the given popup menu (and it's childs)
7726   this.hide_menu = function(name, event)
7727   {
7728     if (!this.menu_stack.length) {
7729       // delegate to subscribers
7730       this.triggerEvent('menu-close', { name:name, props:{ menu:name }, originalEvent:event });
7731       return;
7732     }
7733
7734     var obj, keyboard = rcube_event.is_keyboard(event);
7735     for (var j=this.menu_stack.length-1; j >= 0; j--) {
7736       obj = $('#' + this.menu_stack[j]).hide().attr('aria-hidden', 'true').data('parent', false);
7737       this.triggerEvent('menu-close', { name:this.menu_stack[j], obj:obj, props:{ menu:this.menu_stack[j] }, originalEvent:event });
7738       if (this.menu_stack[j] == name) {
7739         j = -1;  // stop loop
a2f8fa 7740         if (obj.data('opener')) {
TB 7741           $(obj.data('opener')).attr('aria-expanded', 'false');
7742           if (keyboard)
7743             obj.data('opener').focus();
6789bf 7744         }
TB 7745       }
7746       this.menu_stack.pop();
7747     }
7748
7749     // focus previous menu in stack
7750     if (this.menu_stack.length && keyboard) {
7751       this.menu_keyboard_active = true;
f5de03 7752       this.focused_menu = $.last(this.menu_stack);
6789bf 7753       if (!obj || !obj.data('opener'))
TB 7754         $('#'+this.focused_menu).find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus();
7755     }
7756     else {
7757       this.focused_menu = null;
7758       this.menu_keyboard_active = false;
7759     }
7760   }
7761
9a0153 7762
AM 7763   // position a menu element on the screen in relation to other object
7764   this.element_position = function(element, obj)
7765   {
7766     var obj = $(obj), win = $(window),
5e8da2 7767       width = obj.outerWidth(),
AM 7768       height = obj.outerHeight(),
7769       menu_pos = obj.data('menu-pos'),
9a0153 7770       win_height = win.height(),
AM 7771       elem_height = $(element).height(),
7772       elem_width = $(element).width(),
7773       pos = obj.offset(),
7774       top = pos.top,
7775       left = pos.left + width;
7776
5e8da2 7777     if (menu_pos == 'bottom') {
AM 7778       top += height;
7779       left -= width;
7780     }
7781     else
7782       left -= 5;
7783
9a0153 7784     if (top + elem_height > win_height) {
AM 7785       top -= elem_height - height;
7786       if (top < 0)
7787         top = Math.max(0, (win_height - elem_height) / 2);
7788     }
7789
7790     if (left + elem_width > win.width())
7791       left -= elem_width + width;
7792
7793     element.css({left: left + 'px', top: top + 'px'});
7794   };
7795
646b64 7796   // initialize HTML editor
AM 7797   this.editor_init = function(config, id)
7798   {
7799     this.editor = new rcube_text_editor(config, id);
7800   };
7801
4e17e6 7802
T 7803   /********************************************************/
3bd94b 7804   /*********  html to text conversion functions   *********/
A 7805   /********************************************************/
7806
eda92e 7807   this.html2plain = function(html, func)
AM 7808   {
7809     return this.format_converter(html, 'html', func);
7810   };
7811
7812   this.plain2html = function(plain, func)
7813   {
7814     return this.format_converter(plain, 'plain', func);
7815   };
7816
7817   this.format_converter = function(text, format, func)
8fa922 7818   {
fb1203 7819     // warn the user (if converted content is not empty)
eda92e 7820     if (!text
AM 7821       || (format == 'html' && !(text.replace(/<[^>]+>|&nbsp;|\xC2\xA0|\s/g, '')).length)
7822       || (format != 'html' && !(text.replace(/\xC2\xA0|\s/g, '')).length)
7823     ) {
fb1203 7824       // without setTimeout() here, textarea is filled with initial (onload) content
59b765 7825       if (func)
AM 7826         setTimeout(function() { func(''); }, 50);
fb1203 7827       return true;
AM 7828     }
7829
eda92e 7830     var confirmed = this.env.editor_warned || confirm(this.get_label('editorwarning'));
AM 7831
7832     this.env.editor_warned = true;
7833
7834     if (!confirmed)
fb1203 7835       return false;
AM 7836
eda92e 7837     var url = '?_task=utils&_action=' + (format == 'html' ? 'html2text' : 'text2html'),
ad334a 7838       lock = this.set_busy(true, 'converting');
3bd94b 7839
b0eb95 7840     this.log('HTTP POST: ' + url);
3bd94b 7841
eda92e 7842     $.ajax({ type: 'POST', url: url, data: text, contentType: 'application/octet-stream',
2611ac 7843       error: function(o, status, err) { ref.http_error(o, status, err, lock); },
eda92e 7844       success: function(data) {
AM 7845         ref.set_busy(false, null, lock);
7846         if (func) func(data);
7847       }
8fa922 7848     });
fb1203 7849
AM 7850     return true;
8fa922 7851   };
962085 7852
3bd94b 7853
A 7854   /********************************************************/
4e17e6 7855   /*********        remote request methods        *********/
T 7856   /********************************************************/
0213f8 7857
0501b6 7858   // compose a valid url with the given parameters
T 7859   this.url = function(action, query)
7860   {
e8e88d 7861     var querystring = typeof query === 'string' ? query : '';
d8cf6d 7862
A 7863     if (typeof action !== 'string')
0501b6 7864       query = action;
d8cf6d 7865     else if (!query || typeof query !== 'object')
0501b6 7866       query = {};
d8cf6d 7867
0501b6 7868     if (action)
T 7869       query._action = action;
14423c 7870     else if (this.env.action)
0501b6 7871       query._action = this.env.action;
d8cf6d 7872
e8e88d 7873     var url = this.env.comm_path, k, param = {};
0501b6 7874
T 7875     // overwrite task name
14423c 7876     if (action && action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) {
0501b6 7877       query._action = RegExp.$2;
e8e88d 7878       url = url.replace(/\_task=[a-z0-9_-]+/, '_task=' + RegExp.$1);
0501b6 7879     }
d8cf6d 7880
0501b6 7881     // remove undefined values
c31360 7882     for (k in query) {
d8cf6d 7883       if (query[k] !== undefined && query[k] !== null)
0501b6 7884         param[k] = query[k];
T 7885     }
d8cf6d 7886
e8e88d 7887     if (param = $.param(param))
AM 7888       url += (url.indexOf('?') > -1 ? '&' : '?') + param;
7889
7890     if (querystring)
7891       url += (url.indexOf('?') > -1 ? '&' : '?') + querystring;
7892
7893     return url;
0501b6 7894   };
6b47de 7895
4b9efb 7896   this.redirect = function(url, lock)
8fa922 7897   {
719a25 7898     if (lock || lock === null)
4b9efb 7899       this.set_busy(true);
S 7900
7bf6d2 7901     if (this.is_framed()) {
a41dcf 7902       parent.rcmail.redirect(url, lock);
7bf6d2 7903     }
TB 7904     else {
7905       if (this.env.extwin) {
7906         if (typeof url == 'string')
7907           url += (url.indexOf('?') < 0 ? '?' : '&') + '_extwin=1';
7908         else
7909           url._extwin = 1;
7910       }
d7167e 7911       this.location_href(url, window);
7bf6d2 7912     }
8fa922 7913   };
6b47de 7914
4a4088 7915   this.goto_url = function(action, query, lock, secure)
8fa922 7916   {
4a4088 7917     var url = this.url(action, query)
TB 7918     if (secure) url = this.secure_url(url);
7919     this.redirect(url, lock);
8fa922 7920   };
4e17e6 7921
dc0be3 7922   this.location_href = function(url, target, frame)
d7167e 7923   {
dc0be3 7924     if (frame)
A 7925       this.lock_frame();
c31360 7926
A 7927     if (typeof url == 'object')
7928       url = this.env.comm_path + '&' + $.param(url);
dc0be3 7929
d7167e 7930     // simulate real link click to force IE to send referer header
T 7931     if (bw.ie && target == window)
7932       $('<a>').attr('href', url).appendTo(document.body).get(0).click();
7933     else
7934       target.location.href = url;
c442f8 7935
AM 7936     // reset keep-alive interval
7937     this.start_keepalive();
d7167e 7938   };
T 7939
b2992d 7940   // update browser location to remember current view
TB 7941   this.update_state = function(query)
7942   {
7943     if (window.history.replaceState)
7944       window.history.replaceState({}, document.title, rcmail.url('', query));
614c64 7945   };
d8cf6d 7946
7ceabc 7947   // send a http request to the server
5d84dd 7948   this.http_request = function(action, data, lock, type)
4e17e6 7949   {
5d84dd 7950     if (type != 'POST')
AM 7951       type = 'GET';
7952
c2df5d 7953     if (typeof data !== 'object')
AM 7954       data = rcube_parse_query(data);
7955
7956     data._remote = 1;
7957     data._unlock = lock ? lock : 0;
ecf759 7958
cc97ea 7959     // trigger plugin hook
c2df5d 7960     var result = this.triggerEvent('request' + action, data);
7ceabc 7961
c2df5d 7962     // abort if one of the handlers returned false
AM 7963     if (result === false) {
7964       if (data._unlock)
7965         this.set_busy(false, null, data._unlock);
7966       return false;
7967     }
7968     else if (result !== undefined) {
7969       data = result;
7970       if (data._action) {
7971         action = data._action;
7972         delete data._action;
7973       }
7ceabc 7974     }
8fa922 7975
5d84dd 7976     var url = this.url(action);
0213f8 7977
a2b638 7978     // reset keep-alive interval
AM 7979     this.start_keepalive();
7980
5d84dd 7981     // send request
0213f8 7982     return $.ajax({
5d84dd 7983       type: type, url: url, data: data, dataType: 'json',
c2df5d 7984       success: function(data) { ref.http_response(data); },
110360 7985       error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
ad334a 7986     });
cc97ea 7987   };
T 7988
5d84dd 7989   // send a http GET request to the server
AM 7990   this.http_get = this.http_request;
7991
cc97ea 7992   // send a http POST request to the server
c2df5d 7993   this.http_post = function(action, data, lock)
cc97ea 7994   {
5d84dd 7995     return this.http_request(action, data, lock, 'POST');
cc97ea 7996   };
4e17e6 7997
d96151 7998   // aborts ajax request
A 7999   this.abort_request = function(r)
8000   {
8001     if (r.request)
8002       r.request.abort();
8003     if (r.lock)
241450 8004       this.set_busy(false, null, r.lock);
d96151 8005   };
A 8006
ecf759 8007   // handle HTTP response
cc97ea 8008   this.http_response = function(response)
T 8009   {
ad334a 8010     if (!response)
A 8011       return;
8012
cc97ea 8013     if (response.unlock)
e5686f 8014       this.set_busy(false);
4e17e6 8015
2bb1f6 8016     this.triggerEvent('responsebefore', {response: response});
A 8017     this.triggerEvent('responsebefore'+response.action, {response: response});
8018
cc97ea 8019     // set env vars
T 8020     if (response.env)
8021       this.set_env(response.env);
8022
8023     // we have labels to add
d8cf6d 8024     if (typeof response.texts === 'object') {
cc97ea 8025       for (var name in response.texts)
d8cf6d 8026         if (typeof response.texts[name] === 'string')
cc97ea 8027           this.add_label(name, response.texts[name]);
T 8028     }
4e17e6 8029
ecf759 8030     // if we get javascript code from server -> execute it
cc97ea 8031     if (response.exec) {
b0eb95 8032       this.log(response.exec);
cc97ea 8033       eval(response.exec);
0e99d3 8034     }
f52c93 8035
50067d 8036     // execute callback functions of plugins
A 8037     if (response.callbacks && response.callbacks.length) {
8038       for (var i=0; i < response.callbacks.length; i++)
8039         this.triggerEvent(response.callbacks[i][0], response.callbacks[i][1]);
14259c 8040     }
50067d 8041
ecf759 8042     // process the response data according to the sent action
cc97ea 8043     switch (response.action) {
ecf759 8044       case 'delete':
0dbac3 8045         if (this.task == 'addressbook') {
ecf295 8046           var sid, uid = this.contact_list.get_selection(), writable = false;
A 8047
8048           if (uid && this.contact_list.rows[uid]) {
8049             // search results, get source ID from record ID
8050             if (this.env.source == '') {
8051               sid = String(uid).replace(/^[^-]+-/, '');
8052               writable = sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly;
8053             }
8054             else {
8055               writable = !this.env.address_sources[this.env.source].readonly;
8056             }
8057           }
0dbac3 8058           this.enable_command('compose', (uid && this.contact_list.rows[uid]));
ecf295 8059           this.enable_command('delete', 'edit', writable);
0dbac3 8060           this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
f7af22 8061           this.enable_command('export-selected', 'print', false);
0dbac3 8062         }
8fa922 8063
a45f9b 8064       case 'move':
dc2fc0 8065         if (this.env.action == 'show') {
5e9a56 8066           // re-enable commands on move/delete error
14259c 8067           this.enable_command(this.env.message_commands, true);
e25a35 8068           if (!this.env.list_post)
A 8069             this.enable_command('reply-list', false);
dbd069 8070         }
13e155 8071         else if (this.task == 'addressbook') {
A 8072           this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
8073         }
8fa922 8074
2eb032 8075       case 'purge':
cc97ea 8076       case 'expunge':
13e155 8077         if (this.task == 'mail') {
04689f 8078           if (!this.env.exists) {
13e155 8079             // clear preview pane content
A 8080             if (this.env.contentframe)
8081               this.show_contentframe(false);
8082             // disable commands useless when mailbox is empty
8083             this.enable_command(this.env.message_commands, 'purge', 'expunge',
27032f 8084               'select-all', 'select-none', 'expand-all', 'expand-unread', 'collapse-all', false);
13e155 8085           }
172e33 8086           if (this.message_list)
A 8087             this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });
0dbac3 8088         }
T 8089         break;
fdccdb 8090
77de23 8091       case 'refresh':
d41d67 8092       case 'check-recent':
ac0fc3 8093         // update message flags
AM 8094         $.each(this.env.recent_flags || {}, function(uid, flags) {
8095           ref.set_message(uid, 'deleted', flags.deleted);
8096           ref.set_message(uid, 'replied', flags.answered);
8097           ref.set_message(uid, 'unread', !flags.seen);
8098           ref.set_message(uid, 'forwarded', flags.forwarded);
8099           ref.set_message(uid, 'flagged', flags.flagged);
8100         });
8101         delete this.env.recent_flags;
8102
fdccdb 8103       case 'getunread':
f52c93 8104       case 'search':
db0408 8105         this.env.qsearch = null;
0dbac3 8106       case 'list':
T 8107         if (this.task == 'mail') {
c095e6 8108           var is_multifolder = this.is_multifolder_listing(),
AM 8109             list = this.message_list,
8110             uid = this.env.list_uid;
8111
04689f 8112           this.enable_command('show', 'select-all', 'select-none', this.env.messagecount > 0);
26b520 8113           this.enable_command('expunge', this.env.exists && !is_multifolder);
TB 8114           this.enable_command('purge', this.purge_mailbox_test() && !is_multifolder);
8115           this.enable_command('import-messages', !is_multifolder);
8116           this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount && !is_multifolder);
f52c93 8117
c095e6 8118           if (list) {
28331d 8119             if (response.action == 'list' || response.action == 'search') {
c095e6 8120               // highlight message row when we're back from message page
AM 8121               if (uid) {
8122                 if (!list.rows[uid])
8123                   uid += '-' + this.env.mailbox;
8124                 if (list.rows[uid]) {
8125                   list.select(uid);
8126                 }
8127                 delete this.env.list_uid;
8128               }
8129
28331d 8130               this.enable_command('set-listmode', this.env.threads && !is_multifolder);
c360e1 8131               if (list.rowcount > 0 && !$(document.activeElement).is('input,textarea'))
28331d 8132                 list.focus();
c10eae 8133
AM 8134               // trigger 'select' so all dependent actions update its state
8135               // e.g. plugins use this event to activate buttons (#1490647)
8136               list.triggerEvent('select');
28331d 8137             }
64f7d6 8138
c095e6 8139             if (response.action != 'getunread')
AM 8140               this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:list.rowcount });
c833ed 8141           }
0dbac3 8142         }
99d866 8143         else if (this.task == 'addressbook') {
0dbac3 8144           this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
8fa922 8145
c833ed 8146           if (response.action == 'list' || response.action == 'search') {
f8e48d 8147             this.enable_command('search-create', this.env.source == '');
A 8148             this.enable_command('search-delete', this.env.search_id);
62811c 8149             this.update_group_commands();
c360e1 8150             if (this.contact_list.rowcount > 0 && !$(document.activeElement).is('input,textarea'))
d58c39 8151               this.contact_list.focus();
99d866 8152             this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
a61bbb 8153           }
99d866 8154         }
0dbac3 8155         break;
d58c39 8156
TB 8157       case 'list-contacts':
8158       case 'search-contacts':
8159         if (this.contact_list && this.contact_list.rowcount > 0)
8160           this.contact_list.focus();
8161         break;
cc97ea 8162     }
2bb1f6 8163
ad334a 8164     if (response.unlock)
A 8165       this.hide_message(response.unlock);
8166
2bb1f6 8167     this.triggerEvent('responseafter', {response: response});
A 8168     this.triggerEvent('responseafter'+response.action, {response: response});
c442f8 8169
AM 8170     // reset keep-alive interval
8171     this.start_keepalive();
cc97ea 8172   };
ecf759 8173
T 8174   // handle HTTP request errors
110360 8175   this.http_error = function(request, status, err, lock, action)
8fa922 8176   {
9ff9f5 8177     var errmsg = request.statusText;
ecf759 8178
ad334a 8179     this.set_busy(false, null, lock);
9ff9f5 8180     request.abort();
8fa922 8181
7794ae 8182     // don't display error message on page unload (#1488547)
TB 8183     if (this.unload)
8184       return;
8185
7fbd94 8186     if (request.status && errmsg)
74d421 8187       this.display_message(this.get_label('servererror') + ' (' + errmsg + ')', 'error');
110360 8188     else if (status == 'timeout')
T 8189       this.display_message(this.get_label('requesttimedout'), 'error');
8190     else if (request.status == 0 && status != 'abort')
adaddf 8191       this.display_message(this.get_label('connerror'), 'error');
110360 8192
7fac4d 8193     // redirect to url specified in location header if not empty
J 8194     var location_url = request.getResponseHeader("Location");
72e24b 8195     if (location_url && this.env.action != 'compose')  // don't redirect on compose screen, contents might get lost (#1488926)
7fac4d 8196       this.redirect(location_url);
J 8197
daddbf 8198     // 403 Forbidden response (CSRF prevention) - reload the page.
AM 8199     // In case there's a new valid session it will be used, otherwise
8200     // login form will be presented (#1488960).
8201     if (request.status == 403) {
8202       (this.is_framed() ? parent : window).location.reload();
8203       return;
8204     }
8205
110360 8206     // re-send keep-alive requests after 30 seconds
T 8207     if (action == 'keep-alive')
92cb7f 8208       setTimeout(function(){ ref.keep_alive(); ref.start_keepalive(); }, 30000);
8fa922 8209   };
ecf759 8210
85e60a 8211   // handler for session errors detected on the server
TB 8212   this.session_error = function(redirect_url)
8213   {
8214     this.env.server_error = 401;
8215
8216     // save message in local storage and do not redirect
8217     if (this.env.action == 'compose') {
8218       this.save_compose_form_local();
7e7e45 8219       this.compose_skip_unsavedcheck = true;
85e60a 8220     }
TB 8221     else if (redirect_url) {
10a397 8222       setTimeout(function(){ ref.redirect(redirect_url, true); }, 2000);
85e60a 8223     }
TB 8224   };
8225
72e24b 8226   // callback when an iframe finished loading
TB 8227   this.iframe_loaded = function(unlock)
8228   {
8229     this.set_busy(false, null, unlock);
8230
8231     if (this.submit_timer)
8232       clearTimeout(this.submit_timer);
8233   };
8234
017c4f 8235   /**
T 8236    Send multi-threaded parallel HTTP requests to the server for a list if items.
8237    The string '%' in either a GET query or POST parameters will be replaced with the respective item value.
8238    This is the argument object expected: {
8239        items: ['foo','bar','gna'],      // list of items to send requests for
8240        action: 'task/some-action',      // Roudncube action to call
8241        query: { q:'%s' },               // GET query parameters
8242        postdata: { source:'%s' },       // POST data (sends a POST request if present)
8243        threads: 3,                      // max. number of concurrent requests
8244        onresponse: function(data){ },   // Callback function called for every response received from server
8245        whendone: function(alldata){ }   // Callback function called when all requests have been sent
8246    }
8247   */
8248   this.multi_thread_http_request = function(prop)
8249   {
eb7e45 8250     var i, item, reqid = new Date().getTime(),
AM 8251       threads = prop.threads || 1;
017c4f 8252
T 8253     prop.reqid = reqid;
8254     prop.running = 0;
8255     prop.requests = [];
8256     prop.result = [];
8257     prop._items = $.extend([], prop.items);  // copy items
8258
8259     if (!prop.lock)
8260       prop.lock = this.display_message(this.get_label('loading'), 'loading');
8261
8262     // add the request arguments to the jobs pool
8263     this.http_request_jobs[reqid] = prop;
8264
8265     // start n threads
eb7e45 8266     for (i=0; i < threads; i++) {
017c4f 8267       item = prop._items.shift();
T 8268       if (item === undefined)
8269         break;
8270
8271       prop.running++;
8272       prop.requests.push(this.multi_thread_send_request(prop, item));
8273     }
8274
8275     return reqid;
8276   };
8277
8278   // helper method to send an HTTP request with the given iterator value
8279   this.multi_thread_send_request = function(prop, item)
8280   {
65070f 8281     var k, postdata, query;
017c4f 8282
T 8283     // replace %s in post data
8284     if (prop.postdata) {
8285       postdata = {};
65070f 8286       for (k in prop.postdata) {
017c4f 8287         postdata[k] = String(prop.postdata[k]).replace('%s', item);
T 8288       }
8289       postdata._reqid = prop.reqid;
8290     }
8291     // replace %s in query
8292     else if (typeof prop.query == 'string') {
8293       query = prop.query.replace('%s', item);
8294       query += '&_reqid=' + prop.reqid;
8295     }
8296     else if (typeof prop.query == 'object' && prop.query) {
8297       query = {};
65070f 8298       for (k in prop.query) {
017c4f 8299         query[k] = String(prop.query[k]).replace('%s', item);
T 8300       }
8301       query._reqid = prop.reqid;
8302     }
8303
8304     // send HTTP GET or POST request
8305     return postdata ? this.http_post(prop.action, postdata) : this.http_request(prop.action, query);
8306   };
8307
8308   // callback function for multi-threaded http responses
8309   this.multi_thread_http_response = function(data, reqid)
8310   {
8311     var prop = this.http_request_jobs[reqid];
8312     if (!prop || prop.running <= 0 || prop.cancelled)
8313       return;
8314
8315     prop.running--;
8316
8317     // trigger response callback
8318     if (prop.onresponse && typeof prop.onresponse == 'function') {
8319       prop.onresponse(data);
8320     }
8321
8322     prop.result = $.extend(prop.result, data);
8323
8324     // send next request if prop.items is not yet empty
8325     var item = prop._items.shift();
8326     if (item !== undefined) {
8327       prop.running++;
8328       prop.requests.push(this.multi_thread_send_request(prop, item));
8329     }
8330     // trigger whendone callback and mark this request as done
8331     else if (prop.running == 0) {
8332       if (prop.whendone && typeof prop.whendone == 'function') {
8333         prop.whendone(prop.result);
8334       }
8335
8336       this.set_busy(false, '', prop.lock);
8337
8338       // remove from this.http_request_jobs pool
8339       delete this.http_request_jobs[reqid];
8340     }
8341   };
8342
8343   // abort a running multi-thread request with the given identifier
8344   this.multi_thread_request_abort = function(reqid)
8345   {
8346     var prop = this.http_request_jobs[reqid];
8347     if (prop) {
8348       for (var i=0; prop.running > 0 && i < prop.requests.length; i++) {
8349         if (prop.requests[i].abort)
8350           prop.requests[i].abort();
8351       }
8352
8353       prop.running = 0;
8354       prop.cancelled = true;
8355       this.set_busy(false, '', prop.lock);
8356     }
8357   };
8358
0501b6 8359   // post the given form to a hidden iframe
T 8360   this.async_upload_form = function(form, action, onload)
8361   {
a41aaf 8362     // create hidden iframe
AM 8363     var ts = new Date().getTime(),
8364       frame_name = 'rcmupload' + ts,
8365       frame = this.async_upload_form_frame(frame_name);
0501b6 8366
4171c5 8367     // upload progress support
A 8368     if (this.env.upload_progress_name) {
8369       var fname = this.env.upload_progress_name,
8370         field = $('input[name='+fname+']', form);
8371
8372       if (!field.length) {
8373         field = $('<input>').attr({type: 'hidden', name: fname});
65b61c 8374         field.prependTo(form);
4171c5 8375       }
A 8376
8377       field.val(ts);
8378     }
8379
a41aaf 8380     // handle upload errors by parsing iframe content in onload
d9ff47 8381     frame.on('load', {ts:ts}, onload);
0501b6 8382
c269b4 8383     $(form).attr({
A 8384         target: frame_name,
3cc1af 8385         action: this.url(action, {_id: this.env.compose_id || '', _uploadid: ts, _from: this.env.action}),
c269b4 8386         method: 'POST'})
A 8387       .attr(form.encoding ? 'encoding' : 'enctype', 'multipart/form-data')
8388       .submit();
b649c4 8389
A 8390     return frame_name;
0501b6 8391   };
ecf295 8392
a41aaf 8393   // create iframe element for files upload
AM 8394   this.async_upload_form_frame = function(name)
8395   {
8396     return $('<iframe>').attr({name: name, style: 'border: none; width: 0; height: 0; visibility: hidden'})
8397       .appendTo(document.body);
8398   };
8399
ae6d2d 8400   // html5 file-drop API
TB 8401   this.document_drag_hover = function(e, over)
8402   {
d08dc5 8403     // don't e.preventDefault() here to not block text dragging on the page (#1490619)
10a397 8404     $(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('active');
ae6d2d 8405   };
TB 8406
8407   this.file_drag_hover = function(e, over)
8408   {
8409     e.preventDefault();
8410     e.stopPropagation();
10a397 8411     $(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('hover');
ae6d2d 8412   };
TB 8413
8414   // handler when files are dropped to a designated area.
8415   // compose a multipart form data and submit it to the server
8416   this.file_dropped = function(e)
8417   {
8418     // abort event and reset UI
8419     this.file_drag_hover(e, false);
8420
8421     // prepare multipart form data composition
d56091 8422     var uri, files = e.target.files || e.dataTransfer.files,
ae6d2d 8423       formdata = window.FormData ? new FormData() : null,
0be8bd 8424       fieldname = (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'),
ae6d2d 8425       boundary = '------multipartformboundary' + (new Date).getTime(),
TB 8426       dashdash = '--', crlf = '\r\n',
d56091 8427       multipart = dashdash + boundary + crlf,
AM 8428       args = {_id: this.env.compose_id || this.env.cid || '', _remote: 1, _from: this.env.action};
ae6d2d 8429
d56091 8430     if (!files || !files.length) {
AM 8431       // Roundcube attachment, pass its uri to the backend and attach
8432       if (uri = e.dataTransfer.getData('roundcube-uri')) {
8433         var ts = new Date().getTime(),
8434           // jQuery way to escape filename (#1490530)
8f8bea 8435           content = $('<span>').text(e.dataTransfer.getData('roundcube-name') || this.get_label('attaching')).html();
d56091 8436
AM 8437         args._uri = uri;
8438         args._uploadid = ts;
8439
8440         // add to attachments list
8441         if (!this.add2attachment_list(ts, {name: '', html: content, classname: 'uploading', complete: false}))
8442           this.file_upload_id = this.set_busy(true, 'attaching');
8443
8444         this.http_post(this.env.filedrop.action || 'upload', args);
8445       }
ae6d2d 8446       return;
d56091 8447     }
ae6d2d 8448
TB 8449     // inline function to submit the files to the server
8450     var submit_data = function() {
8451       var multiple = files.length > 1,
8452         ts = new Date().getTime(),
dd7db2 8453         // jQuery way to escape filename (#1490530)
AM 8454         content = $('<span>').text(multiple ? ref.get_label('uploadingmany') : files[0].name).html();
ae6d2d 8455
TB 8456       // add to attachments list
0be8bd 8457       if (!ref.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false }))
TB 8458         ref.file_upload_id = ref.set_busy(true, 'uploading');
ae6d2d 8459
TB 8460       // complete multipart content and post request
8461       multipart += dashdash + boundary + dashdash + crlf;
8462
d56091 8463       args._uploadid = ts;
AM 8464
ae6d2d 8465       $.ajax({
TB 8466         type: 'POST',
8467         dataType: 'json',
d56091 8468         url: ref.url(ref.env.filedrop.action || 'upload', args),
ae6d2d 8469         contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary,
TB 8470         processData: false,
99e17f 8471         timeout: 0, // disable default timeout set in ajaxSetup()
ae6d2d 8472         data: formdata || multipart,
962054 8473         headers: {'X-Roundcube-Request': ref.env.request_token},
988840 8474         xhr: function() { var xhr = jQuery.ajaxSettings.xhr(); if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; return xhr; },
ae6d2d 8475         success: function(data){ ref.http_response(data); },
TB 8476         error: function(o, status, err) { ref.http_error(o, status, err, null, 'attachment'); }
8477       });
8478     };
8479
8480     // get contents of all dropped files
8481     var last = this.env.filedrop.single ? 0 : files.length - 1;
0be8bd 8482     for (var j=0, i=0, f; j <= last && (f = files[i]); i++) {
ae6d2d 8483       if (!f.name) f.name = f.fileName;
TB 8484       if (!f.size) f.size = f.fileSize;
8485       if (!f.type) f.type = 'application/octet-stream';
8486
9df79d 8487       // file name contains non-ASCII characters, do UTF8-binary string conversion.
ae6d2d 8488       if (!formdata && /[^\x20-\x7E]/.test(f.name))
TB 8489         f.name_bin = unescape(encodeURIComponent(f.name));
8490
9df79d 8491       // filter by file type if requested
ae6d2d 8492       if (this.env.filedrop.filter && !f.type.match(new RegExp(this.env.filedrop.filter))) {
TB 8493         // TODO: show message to user
8494         continue;
8495       }
8496
9df79d 8497       // do it the easy way with FormData (FF 4+, Chrome 5+, Safari 5+)
ae6d2d 8498       if (formdata) {
0be8bd 8499         formdata.append(fieldname, f);
TB 8500         if (j == last)
ae6d2d 8501           return submit_data();
TB 8502       }
8503       // use FileReader supporetd by Firefox 3.6
8504       else if (window.FileReader) {
8505         var reader = new FileReader();
8506
8507         // closure to pass file properties to async callback function
0be8bd 8508         reader.onload = (function(file, j) {
ae6d2d 8509           return function(e) {
0be8bd 8510             multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
ae6d2d 8511             multipart += '; filename="' + (f.name_bin || file.name) + '"' + crlf;
TB 8512             multipart += 'Content-Length: ' + file.size + crlf;
8513             multipart += 'Content-Type: ' + file.type + crlf + crlf;
988840 8514             multipart += reader.result + crlf;
ae6d2d 8515             multipart += dashdash + boundary + crlf;
TB 8516
0be8bd 8517             if (j == last)  // we're done, submit the data
ae6d2d 8518               return submit_data();
TB 8519           }
0be8bd 8520         })(f,j);
ae6d2d 8521         reader.readAsBinaryString(f);
TB 8522       }
8523       // Firefox 3
8524       else if (f.getAsBinary) {
0be8bd 8525         multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
ae6d2d 8526         multipart += '; filename="' + (f.name_bin || f.name) + '"' + crlf;
TB 8527         multipart += 'Content-Length: ' + f.size + crlf;
8528         multipart += 'Content-Type: ' + f.type + crlf + crlf;
8529         multipart += f.getAsBinary() + crlf;
8530         multipart += dashdash + boundary +crlf;
8531
0be8bd 8532         if (j == last)
ae6d2d 8533           return submit_data();
TB 8534       }
0be8bd 8535
TB 8536       j++;
ae6d2d 8537     }
TB 8538   };
8539
c442f8 8540   // starts interval for keep-alive signal
f52c93 8541   this.start_keepalive = function()
8fa922 8542   {
77de23 8543     if (!this.env.session_lifetime || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
390959 8544       return;
A 8545
77de23 8546     if (this._keepalive)
AM 8547       clearInterval(this._keepalive);
488074 8548
77de23 8549     this._keepalive = setInterval(function(){ ref.keep_alive(); }, this.env.session_lifetime * 0.5 * 1000);
AM 8550   };
8551
8552   // starts interval for refresh signal
8553   this.start_refresh = function()
8554   {
f22654 8555     if (!this.env.refresh_interval || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
77de23 8556       return;
AM 8557
8558     if (this._refresh)
8559       clearInterval(this._refresh);
8560
f22654 8561     this._refresh = setInterval(function(){ ref.refresh(); }, this.env.refresh_interval * 1000);
93a35c 8562   };
A 8563
8564   // sends keep-alive signal
8565   this.keep_alive = function()
8566   {
8567     if (!this.busy)
8568       this.http_request('keep-alive');
488074 8569   };
A 8570
77de23 8571   // sends refresh signal
AM 8572   this.refresh = function()
8fa922 8573   {
77de23 8574     if (this.busy) {
AM 8575       // try again after 10 seconds
8576       setTimeout(function(){ ref.refresh(); ref.start_refresh(); }, 10000);
aade7b 8577       return;
5e9a56 8578     }
T 8579
77de23 8580     var params = {}, lock = this.set_busy(true, 'refreshing');
2e1809 8581
77de23 8582     if (this.task == 'mail' && this.gui_objects.mailboxlist)
AM 8583       params = this.check_recent_params();
8584
b461a2 8585     params._last = Math.floor(this.env.lastrefresh.getTime() / 1000);
TB 8586     this.env.lastrefresh = new Date();
8587
77de23 8588     // plugins should bind to 'requestrefresh' event to add own params
a59499 8589     this.http_post('refresh', params, lock);
77de23 8590   };
AM 8591
8592   // returns check-recent request parameters
8593   this.check_recent_params = function()
8594   {
8595     var params = {_mbox: this.env.mailbox};
8596
8597     if (this.gui_objects.mailboxlist)
8598       params._folderlist = 1;
8599     if (this.gui_objects.quotadisplay)
8600       params._quota = 1;
8601     if (this.env.search_request)
8602       params._search = this.env.search_request;
8603
ac0fc3 8604     if (this.gui_objects.messagelist) {
AM 8605       params._list = 1;
8606
8607       // message uids for flag updates check
8608       params._uids = $.map(this.message_list.rows, function(row, uid) { return uid; }).join(',');
8609     }
8610
77de23 8611     return params;
8fa922 8612   };
4e17e6 8613
T 8614
8615   /********************************************************/
8616   /*********            helper methods            *********/
8617   /********************************************************/
8fa922 8618
2d6242 8619   /**
TB 8620    * Quote html entities
8621    */
8622   this.quote_html = function(str)
8623   {
8624     return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
8625   };
8626
32da69 8627   // get window.opener.rcmail if available
5a8473 8628   this.opener = function(deep, filter)
32da69 8629   {
5a8473 8630     var i, win = window.opener;
AM 8631
32da69 8632     // catch Error: Permission denied to access property rcmail
AM 8633     try {
5a8473 8634       if (win && !win.closed) {
AM 8635         // try parent of the opener window, e.g. preview frame
8636         if (deep && (!win.rcmail || win.rcmail.env.framed) && win.parent && win.parent.rcmail)
8637           win = win.parent;
8638
8639         if (win.rcmail && filter)
8640           for (i in filter)
8641             if (win.rcmail.env[i] != filter[i])
8642               return;
8643
8644         return win.rcmail;
8645       }
32da69 8646     }
AM 8647     catch (e) {}
8648   };
8649
4e17e6 8650   // check if we're in show mode or if we have a unique selection
T 8651   // and return the message uid
8652   this.get_single_uid = function()
8fa922 8653   {
9d693a 8654     var uid = this.env.uid || (this.message_list ? this.message_list.get_single_selection() : null);
J 8655     var result = ref.triggerEvent('get_single_uid', { uid: uid });
8656     return result || uid;
8fa922 8657   };
4e17e6 8658
T 8659   // same as above but for contacts
8660   this.get_single_cid = function()
8fa922 8661   {
9d693a 8662     var cid = this.env.cid || (this.contact_list ? this.contact_list.get_single_selection() : null);
J 8663     var result = ref.triggerEvent('get_single_cid', { cid: cid });
8664     return result || cid;
8fa922 8665   };
4e17e6 8666
9684dc 8667   // get the IMP mailbox of the message with the given UID
T 8668   this.get_message_mailbox = function(uid)
8669   {
3d0747 8670     var msg = (this.env.messages && uid ? this.env.messages[uid] : null) || {};
9684dc 8671     return msg.mbox || this.env.mailbox;
378efd 8672   };
9684dc 8673
602d74 8674   // build request parameters from single message id (maybe with mailbox name)
AM 8675   this.params_from_uid = function(uid, params)
8676   {
8677     if (!params)
8678       params = {};
8679
8680     params._uid = String(uid).split('-')[0];
8681     params._mbox = this.get_message_mailbox(uid);
8682
8683     return params;
8684   };
8685
8fa922 8686   // gets cursor position
4e17e6 8687   this.get_caret_pos = function(obj)
8fa922 8688   {
d8cf6d 8689     if (obj.selectionEnd !== undefined)
4e17e6 8690       return obj.selectionEnd;
3c047d 8691
AM 8692     return obj.value.length;
8fa922 8693   };
4e17e6 8694
8fa922 8695   // moves cursor to specified position
40418d 8696   this.set_caret_pos = function(obj, pos)
8fa922 8697   {
378efd 8698     try {
AM 8699       if (obj.setSelectionRange)
8700         obj.setSelectionRange(pos, pos);
40418d 8701     }
10a397 8702     catch(e) {} // catch Firefox exception if obj is hidden
8fa922 8703   };
4e17e6 8704
0b1de8 8705   // get selected text from an input field
TB 8706   this.get_input_selection = function(obj)
8707   {
378efd 8708     var start = 0, end = 0, normalizedValue = '';
0b1de8 8709
TB 8710     if (typeof obj.selectionStart == "number" && typeof obj.selectionEnd == "number") {
2d6242 8711       normalizedValue = obj.value;
TB 8712       start = obj.selectionStart;
8713       end = obj.selectionEnd;
8714     }
0b1de8 8715
378efd 8716     return {start: start, end: end, text: normalizedValue.substr(start, end-start)};
0b1de8 8717   };
TB 8718
b0d46b 8719   // disable/enable all fields of a form
4e17e6 8720   this.lock_form = function(form, lock)
8fa922 8721   {
4e17e6 8722     if (!form || !form.elements)
T 8723       return;
8fa922 8724
b0d46b 8725     var n, len, elm;
A 8726
8727     if (lock)
8728       this.disabled_form_elements = [];
8729
8730     for (n=0, len=form.elements.length; n<len; n++) {
8731       elm = form.elements[n];
8732
8733       if (elm.type == 'hidden')
4e17e6 8734         continue;
b0d46b 8735       // remember which elem was disabled before lock
A 8736       if (lock && elm.disabled)
8737         this.disabled_form_elements.push(elm);
378efd 8738       else if (lock || $.inArray(elm, this.disabled_form_elements) < 0)
b0d46b 8739         elm.disabled = lock;
8fa922 8740     }
A 8741   };
8742
06c990 8743   this.mailto_handler_uri = function()
A 8744   {
8745     return location.href.split('?')[0] + '?_task=mail&_action=compose&_to=%s';
8746   };
8747
8748   this.register_protocol_handler = function(name)
8749   {
8750     try {
8751       window.navigator.registerProtocolHandler('mailto', this.mailto_handler_uri(), name);
8752     }
d22157 8753     catch(e) {
TB 8754       this.display_message(String(e), 'error');
10a397 8755     }
06c990 8756   };
A 8757
8758   this.check_protocol_handler = function(name, elem)
8759   {
8760     var nav = window.navigator;
10a397 8761
d22157 8762     if (!nav || (typeof nav.registerProtocolHandler != 'function')) {
TB 8763       $(elem).addClass('disabled').click(function(){ return false; });
8764     }
10a397 8765     else if (typeof nav.isProtocolHandlerRegistered == 'function') {
AM 8766       var status = nav.isProtocolHandlerRegistered('mailto', this.mailto_handler_uri());
8767       if (status)
8768         $(elem).parent().find('.mailtoprotohandler-status').html(status);
8769     }
d22157 8770     else {
10a397 8771       $(elem).click(function() { ref.register_protocol_handler(name); return false; });
d22157 8772     }
06c990 8773   };
A 8774
e349a8 8775   // Checks browser capabilities eg. PDF support, TIF support
AM 8776   this.browser_capabilities_check = function()
8777   {
8778     if (!this.env.browser_capabilities)
8779       this.env.browser_capabilities = {};
8780
c3be17 8781     $.each(['pdf', 'flash', 'tif'], function() {
AM 8782       if (ref.env.browser_capabilities[this] === undefined)
8783         ref.env.browser_capabilities[this] = ref[this + '_support_check']();
8784     });
e349a8 8785   };
AM 8786
8787   // Returns browser capabilities string
8788   this.browser_capabilities = function()
8789   {
8790     if (!this.env.browser_capabilities)
8791       return '';
8792
8793     var n, ret = [];
8794
8795     for (n in this.env.browser_capabilities)
8796       ret.push(n + '=' + this.env.browser_capabilities[n]);
8797
8798     return ret.join();
8799   };
8800
8801   this.tif_support_check = function()
8802   {
c3be17 8803     window.setTimeout(function() {
AM 8804       var img = new Image();
8805       img.onload = function() { ref.env.browser_capabilities.tif = 1; };
8806       img.onerror = function() { ref.env.browser_capabilities.tif = 0; };
8807       img.src = ref.assets_path('program/resources/blank.tif');
8808     }, 10);
e349a8 8809
c3be17 8810     return 0;
e349a8 8811   };
AM 8812
8813   this.pdf_support_check = function()
8814   {
8815     var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/pdf"] : {},
8816       plugins = navigator.plugins,
8817       len = plugins.length,
8818       regex = /Adobe Reader|PDF|Acrobat/i;
8819
8820     if (plugin && plugin.enabledPlugin)
8821         return 1;
8822
b6b285 8823     if ('ActiveXObject' in window) {
e349a8 8824       try {
10a397 8825         if (plugin = new ActiveXObject("AcroPDF.PDF"))
e349a8 8826           return 1;
AM 8827       }
8828       catch (e) {}
8829       try {
10a397 8830         if (plugin = new ActiveXObject("PDF.PdfCtrl"))
e349a8 8831           return 1;
AM 8832       }
8833       catch (e) {}
8834     }
8835
8836     for (i=0; i<len; i++) {
8837       plugin = plugins[i];
8838       if (typeof plugin === 'String') {
8839         if (regex.test(plugin))
8840           return 1;
8841       }
8842       else if (plugin.name && regex.test(plugin.name))
8843         return 1;
8844     }
8845
c3be17 8846     window.setTimeout(function() {
AM 8847       $('<object>').css({position: 'absolute', left: '-10000px'})
8848         .attr({data: ref.assets_path('program/resources/dummy.pdf'), width: 1, height: 1, type: 'application/pdf'})
8849         .load(function() { ref.env.browser_capabilities.pdf = 1; })
8850         .error(function() { ref.env.browser_capabilities.pdf = 0; })
8851         .appendTo($('body'));
8852       }, 10);
8853
e349a8 8854     return 0;
AM 8855   };
8856
b9854b 8857   this.flash_support_check = function()
AM 8858   {
8859     var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/x-shockwave-flash"] : {};
8860
8861     if (plugin && plugin.enabledPlugin)
8862         return 1;
8863
b6b285 8864     if ('ActiveXObject' in window) {
b9854b 8865       try {
10a397 8866         if (plugin = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
b9854b 8867           return 1;
AM 8868       }
8869       catch (e) {}
8870     }
8871
8872     return 0;
8873   };
8874
681ba6 8875   this.assets_path = function(path)
AM 8876   {
8877     if (this.env.assets_path && !path.startsWith(this.env.assets_path)) {
8878       path = this.env.assets_path + path;
8879     }
8880
8881     return path;
8882   };
8883
ae7027 8884   // Cookie setter
AM 8885   this.set_cookie = function(name, value, expires)
8886   {
8887     setCookie(name, value, expires, this.env.cookie_path, this.env.cookie_domain, this.env.cookie_secure);
85e60a 8888   };
TB 8889
078679 8890   this.get_local_storage_prefix = function()
TB 8891   {
8892     if (!this.local_storage_prefix)
8893       this.local_storage_prefix = 'roundcube.' + (this.env.user_id || 'anonymous') + '.';
8894
8895     return this.local_storage_prefix;
8896   };
8897
85e60a 8898   // wrapper for localStorage.getItem(key)
TB 8899   this.local_storage_get_item = function(key, deflt, encrypted)
8900   {
56040b 8901     var item, result;
b0b9cf 8902
85e60a 8903     // TODO: add encryption
b0b9cf 8904     try {
AM 8905       item = localStorage.getItem(this.get_local_storage_prefix() + key);
56040b 8906       result = JSON.parse(item);
b0b9cf 8907     }
AM 8908     catch (e) { }
8909
56040b 8910     return result || deflt || null;
85e60a 8911   };
TB 8912
8913   // wrapper for localStorage.setItem(key, data)
8914   this.local_storage_set_item = function(key, data, encrypted)
8915   {
b0b9cf 8916     // try/catch to handle no localStorage support, but also error
AM 8917     // in Safari-in-private-browsing-mode where localStorage exists
8918     // but can't be used (#1489996)
8919     try {
8920       // TODO: add encryption
8921       localStorage.setItem(this.get_local_storage_prefix() + key, JSON.stringify(data));
8922       return true;
8923     }
8924     catch (e) {
8925       return false;
8926     }
85e60a 8927   };
TB 8928
8929   // wrapper for localStorage.removeItem(key)
8930   this.local_storage_remove_item = function(key)
8931   {
b0b9cf 8932     try {
AM 8933       localStorage.removeItem(this.get_local_storage_prefix() + key);
8934       return true;
8935     }
8936     catch (e) {
8937       return false;
8938     }
85e60a 8939   };
f7af22 8940
AM 8941   this.print_dialog = function()
8942   {
8943     if (bw.safari)
8944       setTimeout('window.print()', 10);
8945     else
8946       window.print();
8947   };
cc97ea 8948 }  // end object rcube_webmail
4e17e6 8949
bc3745 8950
T 8951 // some static methods
8952 rcube_webmail.long_subject_title = function(elem, indent)
8953 {
8954   if (!elem.title) {
8955     var $elem = $(elem);
31aa08 8956     if ($elem.width() + (indent || 0) * 15 > $elem.parent().width())
83b583 8957       elem.title = rcube_webmail.subject_text(elem);
bc3745 8958   }
T 8959 };
8960
7a5c3a 8961 rcube_webmail.long_subject_title_ex = function(elem)
065d70 8962 {
A 8963   if (!elem.title) {
8964     var $elem = $(elem),
eb616c 8965       txt = $.trim($elem.text()),
065d70 8966       tmp = $('<span>').text(txt)
A 8967         .css({'position': 'absolute', 'float': 'left', 'visibility': 'hidden',
8968           'font-size': $elem.css('font-size'), 'font-weight': $elem.css('font-weight')})
8969         .appendTo($('body')),
8970       w = tmp.width();
8971
8972     tmp.remove();
7a5c3a 8973     if (w + $('span.branch', $elem).width() * 15 > $elem.width())
83b583 8974       elem.title = rcube_webmail.subject_text(elem);
065d70 8975   }
A 8976 };
8977
83b583 8978 rcube_webmail.subject_text = function(elem)
AM 8979 {
8980   var t = $(elem).clone();
8981   t.find('.skip-on-drag').remove();
8982   return t.text();
8983 };
8984
ae7027 8985 rcube_webmail.prototype.get_cookie = getCookie;
AM 8986
cc97ea 8987 // copy event engine prototype
T 8988 rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
8989 rcube_webmail.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
8990 rcube_webmail.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;