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