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