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