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